···11+import { pino } from 'pino'
22+import { Database } from '../db/connection.js'
33+import type { NormalizedEvent } from '../types/events.js'
44+import { ensureActor } from '../utils/actor-utils.js'
55+66+const logger = pino({ name: 'actor-handler' })
77+88+/**
99+ * This handler is called by all other handlers to ensure that
1010+ * any DID referenced in an event has a corresponding actor entry.
1111+ *
1212+ * @param evt The normalized event to process
1313+ * @param db Database connection
1414+ */
1515+export async function handleActorReferences(evt: NormalizedEvent, db: Database): Promise<void> {
1616+ try {
1717+ // Always ensure the author DID has an actor
1818+ if (evt.did) {
1919+ await ensureActor(evt.did, evt.handle || undefined, db)
2020+ }
2121+2222+ // Handle subject DIDs for follow, block, like events
2323+ if (['follow', 'block', 'like'].includes(evt.event) && evt.record?.subject) {
2424+ // Subject is usually a DID in format did:plc:12345
2525+ const subjectDid = evt.record.subject as string
2626+ if (subjectDid && subjectDid.startsWith('did:')) {
2727+ await ensureActor(subjectDid, undefined, db)
2828+ }
2929+ }
3030+3131+ // Handle reply references for posts
3232+ if (evt.collection === 'so.sprk.feed.post' && evt.record?.reply) {
3333+ const reply = evt.record.reply as { root?: { uri?: string }, parent?: { uri?: string } }
3434+3535+ // Extract DIDs from reply URIs (format: at://did:plc:12345/...)
3636+ if (reply.root?.uri) {
3737+ const rootDid = extractDidFromUri(reply.root.uri)
3838+ if (rootDid) {
3939+ await ensureActor(rootDid, undefined, db)
4040+ }
4141+ }
4242+4343+ if (reply.parent?.uri) {
4444+ const parentDid = extractDidFromUri(reply.parent.uri)
4545+ if (parentDid && parentDid !== extractDidFromUri(reply.root?.uri || '')) {
4646+ await ensureActor(parentDid, undefined, db)
4747+ }
4848+ }
4949+ }
5050+5151+ // Handle repost subjects
5252+ if (evt.collection === 'so.sprk.feed.repost' && evt.record?.subject?.uri) {
5353+ const subjectUri = evt.record.subject.uri as string
5454+ const subjectDid = extractDidFromUri(subjectUri)
5555+ if (subjectDid) {
5656+ await ensureActor(subjectDid, undefined, db)
5757+ }
5858+ }
5959+ } catch (error) {
6060+ logger.error({ error, uri: evt.uri }, 'Error while handling actor references')
6161+ }
6262+}
6363+6464+/**
6565+ * Extracts a DID from an AT URI (at://did:plc:12345/...)
6666+ *
6767+ * @param uri The URI to extract the DID from
6868+ * @returns The extracted DID or undefined
6969+ */
7070+function extractDidFromUri(uri: string): string | undefined {
7171+ if (!uri) return undefined
7272+7373+ // Match a DID in an AT URI format
7474+ const match = uri.match(/at:\/\/(did:[a-zA-Z0-9:]+)\//)
7575+ return match ? match[1] : undefined
7676+}
+5-1
services/ingester/src/handlers/index.ts
···1111import { handleMusicEvent } from './music-handler.js'
1212import { handleLookEvent } from './look-handler.js'
1313import { handleGeneratorEvent } from './generator-handler.js'
1414+import { handleActorReferences } from './actor-handler.js'
14151516const logger = pino({ name: 'event-handler' })
16171718export async function handleEvent(evt: NormalizedEvent, db: Database): Promise<void> {
1819 try {
1919- // Handle different events based on collection
2020+ // First, ensure all actor references are handled properly
2121+ await handleActorReferences(evt, db)
2222+2323+ // Then handle different events based on collection
2024 if (evt.collection === 'so.sprk.feed.like') {
2125 await handleLikeEvent(evt, db)
2226 return
+14-3
services/ingester/src/handlers/profile-handler.ts
···11import { pino } from 'pino'
22import { Database } from '../db/connection.js'
33import type { NormalizedEvent } from '../types/events.js'
44+import { ensureActor, linkProfileToActor } from '../utils/actor-utils.js'
55+import type { ProfileDocument } from '../db/models.js'
4657const logger = pino({ name: 'profile-handler' })
68···3739 }, 'Processing profile event')
38403941 try {
4242+ // First, ensure we have an actor for this DID
4343+ await ensureActor(evt.did, evt.handle || undefined, db)
4444+4045 const profileData = {
4146 uri: evt.uri,
4247 displayName: record.displayName,
···5358 cid: evt.commit.cid
5459 }
55605656- await db.models.Profile.findOneAndUpdate(
6161+ // Save the profile
6262+ const profile = await db.models.Profile.findOneAndUpdate(
5763 { uri: evt.uri },
5864 profileData,
5965 { upsert: true, new: true }
6060- )
6666+ ) as ProfileDocument
6767+6868+ if (profile && profile._id) {
6969+ // Link the profile to the actor
7070+ await linkProfileToActor(evt.did, profile._id.toString(), evt.commit.cid, db)
7171+ }
61726273 logger.info(
6374 { uri: evt.uri },
6464- 'Successfully saved profile to database'
7575+ 'Successfully saved profile to database and linked to actor'
6576 )
6677 } catch (error) {
6778 logger.error(
+91
services/ingester/src/utils/actor-utils.ts
···11+import { pino } from 'pino'
22+import { Database } from '../db/connection.js'
33+44+const logger = pino({ name: 'actor-utils' })
55+66+/**
77+ * Ensures that an actor exists for the given DID.
88+ * If the actor doesn't exist, it creates a new one.
99+ *
1010+ * @param did The DID to ensure has an actor
1111+ * @param handle Optional handle associated with the DID
1212+ * @param db Database connection
1313+ * @returns The actor document, either existing or newly created
1414+ */
1515+export async function ensureActor(
1616+ did: string,
1717+ handle?: string,
1818+ db?: Database
1919+): Promise<any> {
2020+ if (!db) {
2121+ logger.warn({ did }, 'No database connection provided to ensureActor')
2222+ return null
2323+ }
2424+2525+ try {
2626+ // Try to find existing actor
2727+ const existingActor = await db.models.Actor.findOne({ did })
2828+2929+ if (existingActor) {
3030+ // If handle is provided and different from existing, update it
3131+ if (handle && existingActor.handle !== handle) {
3232+ existingActor.handle = handle
3333+ await existingActor.save()
3434+ logger.info({ did, handle }, 'Updated actor handle')
3535+ }
3636+ return existingActor
3737+ }
3838+3939+ // Create new actor if none exists
4040+ const now = new Date()
4141+ const uri = `at://${did}/app.bsky.actor.profile`
4242+4343+ const newActor = await db.models.Actor.create({
4444+ uri,
4545+ did,
4646+ handle: handle || undefined,
4747+ followersCount: 0,
4848+ followingCount: 0,
4949+ postsCount: 0,
5050+ indexedAt: now.toISOString(),
5151+ isLabeler: false,
5252+ priorityNotifications: false
5353+ })
5454+5555+ logger.info({ did, handle }, 'Created new actor')
5656+ return newActor
5757+ } catch (error) {
5858+ logger.error({ error, did, handle }, 'Failed to ensure actor exists')
5959+ return null
6060+ }
6161+}
6262+6363+/**
6464+ * Links a profile to an actor
6565+ *
6666+ * @param did The DID of the actor
6767+ * @param profileId The MongoDB ID of the profile
6868+ * @param profileCid The CID of the profile
6969+ * @param db Database connection
7070+ */
7171+export async function linkProfileToActor(
7272+ did: string,
7373+ profileId: string,
7474+ profileCid: string,
7575+ db: Database
7676+): Promise<void> {
7777+ try {
7878+ await db.models.Actor.findOneAndUpdate(
7979+ { did },
8080+ {
8181+ profile: profileId,
8282+ profileCid
8383+ },
8484+ { new: true }
8585+ )
8686+8787+ logger.info({ did, profileId }, 'Linked profile to actor')
8888+ } catch (error) {
8989+ logger.error({ error, did, profileId }, 'Failed to link profile to actor')
9090+ }
9191+}