Monorepo for Tangled tangled.org

appview: refactor ingester, ingest spindle records

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 460e1816 f1a8e659

verified
Changed files
+213 -92
appview
+201 -91
appview/ingester.go
··· 3 import ( 4 "context" 5 "encoding/json" 6 - "errors" 7 "fmt" 8 - "io" 9 - "log" 10 - "net/http" 11 - "strings" 12 "time" 13 14 "github.com/bluesky-social/indigo/atproto/syntax" ··· 16 "github.com/go-git/go-git/v5/plumbing" 17 "github.com/ipfs/go-cid" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/rbac" 21 ) 22 23 - type Ingester func(ctx context.Context, e *models.Event) error 24 25 - func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester { 26 return func(ctx context.Context, e *models.Event) error { 27 var err error 28 defer func() { 29 eventTime := e.TimeUS 30 lastTimeUs := eventTime + 1 31 - if err := d.SaveLastTimeUs(lastTimeUs); err != nil { 32 err = fmt.Errorf("(deferred) failed to save last time us: %w", err) 33 } 34 }() ··· 39 40 switch e.Commit.Collection { 41 case tangled.GraphFollowNSID: 42 - ingestFollow(&d, e) 43 case tangled.FeedStarNSID: 44 - ingestStar(&d, e) 45 case tangled.PublicKeyNSID: 46 - ingestPublicKey(&d, e) 47 case tangled.RepoArtifactNSID: 48 - ingestArtifact(&d, e, enforcer) 49 case tangled.ActorProfileNSID: 50 - ingestProfile(&d, e) 51 case tangled.SpindleMemberNSID: 52 - ingestSpindleMember(&d, e, enforcer) 53 case tangled.SpindleNSID: 54 - ingestSpindle(&d, e, true) // TODO: change this to dynamic 55 } 56 57 return err 58 } 59 } 60 61 - func ingestStar(d *db.DbWrapper, e *models.Event) error { 62 var err error 63 did := e.Did 64 65 switch e.Commit.Operation { 66 case models.CommitOperationCreate, models.CommitOperationUpdate: 67 var subjectUri syntax.ATURI ··· 70 record := tangled.FeedStar{} 71 err := json.Unmarshal(raw, &record) 72 if err != nil { 73 - log.Println("invalid record") 74 return err 75 } 76 77 subjectUri, err = syntax.ParseATURI(record.Subject) 78 if err != nil { 79 - log.Println("invalid record") 80 return err 81 } 82 - err = db.AddStar(d, did, subjectUri, e.Commit.RKey) 83 case models.CommitOperationDelete: 84 - err = db.DeleteStarByRkey(d, did, e.Commit.RKey) 85 } 86 87 if err != nil { ··· 91 return nil 92 } 93 94 - func ingestFollow(d *db.DbWrapper, e *models.Event) error { 95 var err error 96 did := e.Did 97 98 switch e.Commit.Operation { 99 case models.CommitOperationCreate, models.CommitOperationUpdate: ··· 101 record := tangled.GraphFollow{} 102 err = json.Unmarshal(raw, &record) 103 if err != nil { 104 - log.Println("invalid record") 105 return err 106 } 107 108 subjectDid := record.Subject 109 - err = db.AddFollow(d, did, subjectDid, e.Commit.RKey) 110 case models.CommitOperationDelete: 111 - err = db.DeleteFollowByRkey(d, did, e.Commit.RKey) 112 } 113 114 if err != nil { ··· 118 return nil 119 } 120 121 - func ingestPublicKey(d *db.DbWrapper, e *models.Event) error { 122 did := e.Did 123 var err error 124 125 switch e.Commit.Operation { 126 case models.CommitOperationCreate, models.CommitOperationUpdate: 127 - log.Println("processing add of pubkey") 128 raw := json.RawMessage(e.Commit.Record) 129 record := tangled.PublicKey{} 130 err = json.Unmarshal(raw, &record) 131 if err != nil { 132 - log.Printf("invalid record: %s", err) 133 return err 134 } 135 136 name := record.Name 137 key := record.Key 138 - err = db.AddPublicKey(d, did, name, key, e.Commit.RKey) 139 case models.CommitOperationDelete: 140 - log.Println("processing delete of pubkey") 141 - err = db.DeletePublicKeyByRkey(d, did, e.Commit.RKey) 142 } 143 144 if err != nil { ··· 148 return nil 149 } 150 151 - func ingestArtifact(d *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error { 152 did := e.Did 153 var err error 154 155 switch e.Commit.Operation { 156 case models.CommitOperationCreate, models.CommitOperationUpdate: 157 raw := json.RawMessage(e.Commit.Record) 158 record := tangled.RepoArtifact{} 159 err = json.Unmarshal(raw, &record) 160 if err != nil { 161 - log.Printf("invalid record: %s", err) 162 return err 163 } 164 ··· 167 return err 168 } 169 170 - repo, err := db.GetRepoByAtUri(d, repoAt.String()) 171 if err != nil { 172 return err 173 } 174 175 - ok, err := enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 176 if err != nil || !ok { 177 return err 178 } ··· 194 MimeType: record.Artifact.MimeType, 195 } 196 197 - err = db.AddArtifact(d, artifact) 198 case models.CommitOperationDelete: 199 - err = db.DeleteArtifact(d, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 200 } 201 202 if err != nil { ··· 206 return nil 207 } 208 209 - func ingestProfile(d *db.DbWrapper, e *models.Event) error { 210 did := e.Did 211 var err error 212 213 if e.Commit.RKey != "self" { 214 return fmt.Errorf("ingestProfile only ingests `self` record") ··· 220 record := tangled.ActorProfile{} 221 err = json.Unmarshal(raw, &record) 222 if err != nil { 223 - log.Printf("invalid record: %s", err) 224 return err 225 } 226 ··· 267 PinnedRepos: pinned, 268 } 269 270 - ddb, ok := d.Execer.(*db.DB) 271 if !ok { 272 return fmt.Errorf("failed to index profile record, invalid db cast") 273 } ··· 284 285 err = db.UpsertProfile(tx, &profile) 286 case models.CommitOperationDelete: 287 - err = db.DeleteArtifact(d, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 288 } 289 290 if err != nil { ··· 294 return nil 295 } 296 297 - func ingestSpindleMember(_ *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error { 298 did := e.Did 299 var err error 300 301 switch e.Commit.Operation { 302 case models.CommitOperationCreate: 303 raw := json.RawMessage(e.Commit.Record) 304 record := tangled.SpindleMember{} 305 err = json.Unmarshal(raw, &record) 306 if err != nil { 307 - log.Printf("invalid record: %s", err) 308 return err 309 } 310 311 // only spindle owner can invite to spindles 312 - ok, err := enforcer.IsSpindleInviteAllowed(did, record.Instance) 313 if err != nil || !ok { 314 return fmt.Errorf("failed to enforce permissions: %w", err) 315 } 316 317 - err = enforcer.AddSpindleMember(record.Instance, record.Subject) 318 if err != nil { 319 - return fmt.Errorf("failed to add member: %w", err) 320 } 321 } 322 323 return nil 324 } 325 326 - func ingestSpindle(d *db.DbWrapper, e *models.Event, dev bool) error { 327 did := e.Did 328 var err error 329 330 switch e.Commit.Operation { 331 case models.CommitOperationCreate: 332 raw := json.RawMessage(e.Commit.Record) 333 record := tangled.Spindle{} 334 err = json.Unmarshal(raw, &record) 335 if err != nil { 336 - log.Printf("invalid record: %s", err) 337 return err 338 } 339 340 - // this is a special record whose rkey is the instance of the spindle itself 341 instance := e.Commit.RKey 342 343 - owner, err := fetchOwner(context.TODO(), instance, dev) 344 if err != nil { 345 - log.Printf("failed to verify owner of %s: %s", instance, err) 346 return err 347 } 348 349 - // verify that the spindle owner points back to this did 350 - if owner != did { 351 - log.Printf("incorrect owner for domain: %s, %s != %s", instance, owner, did) 352 return err 353 } 354 355 - // mark this spindle as registered 356 - ddb, ok := d.Execer.(*db.DB) 357 if !ok { 358 return fmt.Errorf("failed to index profile record, invalid db cast") 359 } 360 361 - _, err = db.VerifySpindle( 362 - ddb, 363 db.FilterEq("owner", did), 364 db.FilterEq("instance", instance), 365 ) 366 367 - return err 368 - } 369 370 - return nil 371 - } 372 373 - func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 374 - scheme := "https" 375 - if dev { 376 - scheme = "http" 377 - } 378 - 379 - url := fmt.Sprintf("%s://%s/owner", scheme, domain) 380 - req, err := http.NewRequest("GET", url, nil) 381 - if err != nil { 382 - return "", err 383 - } 384 - 385 - client := &http.Client{ 386 - Timeout: 1 * time.Second, 387 - } 388 - 389 - resp, err := client.Do(req.WithContext(ctx)) 390 - if err != nil || resp.StatusCode != 200 { 391 - return "", errors.New("failed to fetch /owner") 392 - } 393 - 394 - body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 395 - if err != nil { 396 - return "", fmt.Errorf("failed to read /owner response: %w", err) 397 - } 398 - 399 - did := strings.TrimSpace(string(body)) 400 - if did == "" { 401 - return "", errors.New("empty DID in /owner response") 402 } 403 404 - return did, nil 405 }
··· 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 + "log/slog" 8 "time" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" ··· 12 "github.com/go-git/go-git/v5/plumbing" 13 "github.com/ipfs/go-cid" 14 "tangled.sh/tangled.sh/core/api/tangled" 15 + "tangled.sh/tangled.sh/core/appview/config" 16 "tangled.sh/tangled.sh/core/appview/db" 17 + "tangled.sh/tangled.sh/core/appview/idresolver" 18 + "tangled.sh/tangled.sh/core/appview/spindleverify" 19 "tangled.sh/tangled.sh/core/rbac" 20 ) 21 22 + type Ingester struct { 23 + Db db.DbWrapper 24 + Enforcer *rbac.Enforcer 25 + IdResolver *idresolver.Resolver 26 + Config *config.Config 27 + Logger *slog.Logger 28 + } 29 + 30 + type processFunc func(ctx context.Context, e *models.Event) error 31 32 + func (i *Ingester) Ingest() processFunc { 33 return func(ctx context.Context, e *models.Event) error { 34 var err error 35 defer func() { 36 eventTime := e.TimeUS 37 lastTimeUs := eventTime + 1 38 + if err := i.Db.SaveLastTimeUs(lastTimeUs); err != nil { 39 err = fmt.Errorf("(deferred) failed to save last time us: %w", err) 40 } 41 }() ··· 46 47 switch e.Commit.Collection { 48 case tangled.GraphFollowNSID: 49 + err = i.ingestFollow(e) 50 case tangled.FeedStarNSID: 51 + err = i.ingestStar(e) 52 case tangled.PublicKeyNSID: 53 + err = i.ingestPublicKey(e) 54 case tangled.RepoArtifactNSID: 55 + err = i.ingestArtifact(e) 56 case tangled.ActorProfileNSID: 57 + err = i.ingestProfile(e) 58 case tangled.SpindleMemberNSID: 59 + err = i.ingestSpindleMember(e) 60 case tangled.SpindleNSID: 61 + err = i.ingestSpindle(e) 62 + } 63 + 64 + if err != nil { 65 + l := i.Logger.With("nsid", e.Commit.Collection) 66 + l.Error("error ingesting record", "err", err) 67 } 68 69 return err 70 } 71 } 72 73 + func (i *Ingester) ingestStar(e *models.Event) error { 74 var err error 75 did := e.Did 76 77 + l := i.Logger.With("handler", "ingestStar") 78 + l = l.With("nsid", e.Commit.Collection) 79 + 80 switch e.Commit.Operation { 81 case models.CommitOperationCreate, models.CommitOperationUpdate: 82 var subjectUri syntax.ATURI ··· 85 record := tangled.FeedStar{} 86 err := json.Unmarshal(raw, &record) 87 if err != nil { 88 + l.Error("invalid record", "err", err) 89 return err 90 } 91 92 subjectUri, err = syntax.ParseATURI(record.Subject) 93 if err != nil { 94 + l.Error("invalid record", "err", err) 95 return err 96 } 97 + err = db.AddStar(i.Db, did, subjectUri, e.Commit.RKey) 98 case models.CommitOperationDelete: 99 + err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) 100 } 101 102 if err != nil { ··· 106 return nil 107 } 108 109 + func (i *Ingester) ingestFollow(e *models.Event) error { 110 var err error 111 did := e.Did 112 + 113 + l := i.Logger.With("handler", "ingestFollow") 114 + l = l.With("nsid", e.Commit.Collection) 115 116 switch e.Commit.Operation { 117 case models.CommitOperationCreate, models.CommitOperationUpdate: ··· 119 record := tangled.GraphFollow{} 120 err = json.Unmarshal(raw, &record) 121 if err != nil { 122 + l.Error("invalid record", "err", err) 123 return err 124 } 125 126 subjectDid := record.Subject 127 + err = db.AddFollow(i.Db, did, subjectDid, e.Commit.RKey) 128 case models.CommitOperationDelete: 129 + err = db.DeleteFollowByRkey(i.Db, did, e.Commit.RKey) 130 } 131 132 if err != nil { ··· 136 return nil 137 } 138 139 + func (i *Ingester) ingestPublicKey(e *models.Event) error { 140 did := e.Did 141 var err error 142 143 + l := i.Logger.With("handler", "ingestPublicKey") 144 + l = l.With("nsid", e.Commit.Collection) 145 + 146 switch e.Commit.Operation { 147 case models.CommitOperationCreate, models.CommitOperationUpdate: 148 + l.Debug("processing add of pubkey") 149 raw := json.RawMessage(e.Commit.Record) 150 record := tangled.PublicKey{} 151 err = json.Unmarshal(raw, &record) 152 if err != nil { 153 + l.Error("invalid record", "err", err) 154 return err 155 } 156 157 name := record.Name 158 key := record.Key 159 + err = db.AddPublicKey(i.Db, did, name, key, e.Commit.RKey) 160 case models.CommitOperationDelete: 161 + l.Debug("processing delete of pubkey") 162 + err = db.DeletePublicKeyByRkey(i.Db, did, e.Commit.RKey) 163 } 164 165 if err != nil { ··· 169 return nil 170 } 171 172 + func (i *Ingester) ingestArtifact(e *models.Event) error { 173 did := e.Did 174 var err error 175 176 + l := i.Logger.With("handler", "ingestArtifact") 177 + l = l.With("nsid", e.Commit.Collection) 178 + 179 switch e.Commit.Operation { 180 case models.CommitOperationCreate, models.CommitOperationUpdate: 181 raw := json.RawMessage(e.Commit.Record) 182 record := tangled.RepoArtifact{} 183 err = json.Unmarshal(raw, &record) 184 if err != nil { 185 + l.Error("invalid record", "err", err) 186 return err 187 } 188 ··· 191 return err 192 } 193 194 + repo, err := db.GetRepoByAtUri(i.Db, repoAt.String()) 195 if err != nil { 196 return err 197 } 198 199 + ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 200 if err != nil || !ok { 201 return err 202 } ··· 218 MimeType: record.Artifact.MimeType, 219 } 220 221 + err = db.AddArtifact(i.Db, artifact) 222 case models.CommitOperationDelete: 223 + err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 224 } 225 226 if err != nil { ··· 230 return nil 231 } 232 233 + func (i *Ingester) ingestProfile(e *models.Event) error { 234 did := e.Did 235 var err error 236 + 237 + l := i.Logger.With("handler", "ingestProfile") 238 + l = l.With("nsid", e.Commit.Collection) 239 240 if e.Commit.RKey != "self" { 241 return fmt.Errorf("ingestProfile only ingests `self` record") ··· 247 record := tangled.ActorProfile{} 248 err = json.Unmarshal(raw, &record) 249 if err != nil { 250 + l.Error("invalid record", "err", err) 251 return err 252 } 253 ··· 294 PinnedRepos: pinned, 295 } 296 297 + ddb, ok := i.Db.Execer.(*db.DB) 298 if !ok { 299 return fmt.Errorf("failed to index profile record, invalid db cast") 300 } ··· 311 312 err = db.UpsertProfile(tx, &profile) 313 case models.CommitOperationDelete: 314 + err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 315 } 316 317 if err != nil { ··· 321 return nil 322 } 323 324 + func (i *Ingester) ingestSpindleMember(e *models.Event) error { 325 did := e.Did 326 var err error 327 328 + l := i.Logger.With("handler", "ingestSpindleMember") 329 + l = l.With("nsid", e.Commit.Collection) 330 + 331 switch e.Commit.Operation { 332 case models.CommitOperationCreate: 333 raw := json.RawMessage(e.Commit.Record) 334 record := tangled.SpindleMember{} 335 err = json.Unmarshal(raw, &record) 336 if err != nil { 337 + l.Error("invalid record", "err", err) 338 return err 339 } 340 341 // only spindle owner can invite to spindles 342 + ok, err := i.Enforcer.IsSpindleInviteAllowed(did, record.Instance) 343 if err != nil || !ok { 344 return fmt.Errorf("failed to enforce permissions: %w", err) 345 } 346 347 + memberId, err := i.IdResolver.ResolveIdent(context.Background(), record.Subject) 348 if err != nil { 349 + return err 350 + } 351 + 352 + if memberId.Handle.IsInvalidHandle() { 353 + return err 354 + } 355 + 356 + ddb, ok := i.Db.Execer.(*db.DB) 357 + if !ok { 358 + return fmt.Errorf("failed to index profile record, invalid db cast") 359 + } 360 + 361 + err = db.AddSpindleMember(ddb, db.SpindleMember{ 362 + Did: syntax.DID(did), 363 + Rkey: e.Commit.RKey, 364 + Instance: record.Instance, 365 + Subject: memberId.DID, 366 + }) 367 + if !ok { 368 + return fmt.Errorf("failed to add to db: %w", err) 369 + } 370 + 371 + err = i.Enforcer.AddSpindleMember(record.Instance, memberId.DID.String()) 372 + if err != nil { 373 + return fmt.Errorf("failed to update ACLs: %w", err) 374 + } 375 + case models.CommitOperationDelete: 376 + rkey := e.Commit.RKey 377 + 378 + ddb, ok := i.Db.Execer.(*db.DB) 379 + if !ok { 380 + return fmt.Errorf("failed to index profile record, invalid db cast") 381 + } 382 + 383 + // get record from db first 384 + members, err := db.GetSpindleMembers( 385 + ddb, 386 + db.FilterEq("did", did), 387 + db.FilterEq("rkey", rkey), 388 + ) 389 + if err != nil || len(members) != 1 { 390 + return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members)) 391 + } 392 + member := members[0] 393 + 394 + tx, err := ddb.Begin() 395 + if err != nil { 396 + return fmt.Errorf("failed to start txn: %w", err) 397 + } 398 + 399 + // remove record by rkey && update enforcer 400 + if err = db.RemoveSpindleMember( 401 + tx, 402 + db.FilterEq("did", did), 403 + db.FilterEq("rkey", rkey), 404 + ); err != nil { 405 + return fmt.Errorf("failed to remove from db: %w", err) 406 + } 407 + 408 + // update enforcer 409 + err = i.Enforcer.RemoveSpindleMember(member.Instance, member.Subject.String()) 410 + if err != nil { 411 + return fmt.Errorf("failed to update ACLs: %w", err) 412 + } 413 + 414 + if err = tx.Commit(); err != nil { 415 + return fmt.Errorf("failed to commit txn: %w", err) 416 + } 417 + 418 + if err = i.Enforcer.E.SavePolicy(); err != nil { 419 + return fmt.Errorf("failed to save ACLs: %w", err) 420 } 421 } 422 423 return nil 424 } 425 426 + func (i *Ingester) ingestSpindle(e *models.Event) error { 427 did := e.Did 428 var err error 429 430 + l := i.Logger.With("handler", "ingestSpindle") 431 + l = l.With("nsid", e.Commit.Collection) 432 + 433 switch e.Commit.Operation { 434 case models.CommitOperationCreate: 435 raw := json.RawMessage(e.Commit.Record) 436 record := tangled.Spindle{} 437 err = json.Unmarshal(raw, &record) 438 if err != nil { 439 + l.Error("invalid record", "err", err) 440 return err 441 } 442 443 instance := e.Commit.RKey 444 445 + ddb, ok := i.Db.Execer.(*db.DB) 446 + if !ok { 447 + return fmt.Errorf("failed to index profile record, invalid db cast") 448 + } 449 + 450 + err := db.AddSpindle(ddb, db.Spindle{ 451 + Owner: syntax.DID(did), 452 + Instance: instance, 453 + }) 454 if err != nil { 455 + l.Error("failed to add spindle to db", "err", err, "instance", instance) 456 return err 457 } 458 459 + err = spindleverify.RunVerification(context.Background(), instance, did, i.Config.Core.Dev) 460 + if err != nil { 461 + l.Error("failed to add spindle to db", "err", err, "instance", instance) 462 return err 463 } 464 465 + _, err = spindleverify.MarkVerified(ddb, i.Enforcer, instance, did) 466 + if err != nil { 467 + return fmt.Errorf("failed to mark verified: %w", err) 468 + } 469 + 470 + return nil 471 + 472 + case models.CommitOperationDelete: 473 + instance := e.Commit.RKey 474 + 475 + ddb, ok := i.Db.Execer.(*db.DB) 476 if !ok { 477 return fmt.Errorf("failed to index profile record, invalid db cast") 478 } 479 480 + tx, err := ddb.Begin() 481 + if err != nil { 482 + return err 483 + } 484 + defer func() { 485 + tx.Rollback() 486 + i.Enforcer.E.LoadPolicy() 487 + }() 488 + 489 + err = db.DeleteSpindle( 490 + tx, 491 db.FilterEq("owner", did), 492 db.FilterEq("instance", instance), 493 ) 494 + if err != nil { 495 + return err 496 + } 497 498 + err = i.Enforcer.RemoveSpindle(instance) 499 + if err != nil { 500 + return err 501 + } 502 503 + err = tx.Commit() 504 + if err != nil { 505 + return err 506 + } 507 508 + err = i.Enforcer.E.SavePolicy() 509 + if err != nil { 510 + return err 511 + } 512 } 513 514 + return nil 515 }
+12 -1
appview/state/state.go
··· 31 "tangled.sh/tangled.sh/core/eventconsumer" 32 "tangled.sh/tangled.sh/core/jetstream" 33 "tangled.sh/tangled.sh/core/knotclient" 34 "tangled.sh/tangled.sh/core/rbac" 35 ) 36 ··· 93 tangled.PublicKeyNSID, 94 tangled.RepoArtifactNSID, 95 tangled.ActorProfileNSID, 96 }, 97 nil, 98 slog.Default(), ··· 106 if err != nil { 107 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 108 } 109 - err = jc.StartJetstream(ctx, appview.Ingest(wrapper, enforcer)) 110 if err != nil { 111 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 112 }
··· 31 "tangled.sh/tangled.sh/core/eventconsumer" 32 "tangled.sh/tangled.sh/core/jetstream" 33 "tangled.sh/tangled.sh/core/knotclient" 34 + tlog "tangled.sh/tangled.sh/core/log" 35 "tangled.sh/tangled.sh/core/rbac" 36 ) 37 ··· 94 tangled.PublicKeyNSID, 95 tangled.RepoArtifactNSID, 96 tangled.ActorProfileNSID, 97 + tangled.SpindleMemberNSID, 98 + tangled.SpindleNSID, 99 }, 100 nil, 101 slog.Default(), ··· 109 if err != nil { 110 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 111 } 112 + 113 + ingester := appview.Ingester{ 114 + Db: wrapper, 115 + Enforcer: enforcer, 116 + IdResolver: res, 117 + Config: config, 118 + Logger: tlog.New("ingester"), 119 + } 120 + err = jc.StartJetstream(ctx, ingester.Ingest()) 121 if err != nil { 122 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 123 }