// The knowledge jobs and the subscribers that enqueue them. // // A web request writes the document row and publishes the outbox event in one transaction; the // module's own subscriber (running in the worker's outbox relay) turns the event into the job, // with the (collection, file) pair as the singleton key — a redelivered event adds no second job. // The tenant comes from the container, never from the payload (decision AT-F6). import { type EventContext, internal, type JobDef, subscribe } from "@conusai-kit/core" import { JOB_QUEUE } from "@conusai-kit/data" import { z } from "zod" import { documentIndexRequested, documentRemoveRequested, INDEX_JOB, REMOVE_JOB } from "../contract/index.js" import { indexDocument, removeDocument } from "../core/index-job.js" import { KNOWLEDGE_SCOPE, type KnowledgeScope } from "../core/ports.js" import { knowledgeSearchDeps } from "./procedures.js" export const documentPayload = z.object({ documentId: z.string(), collectionName: z.string(), fileId: z.string(), }) export type DocumentJobPayload = z.output export const indexJobKey = (collectionName: string, fileId: string): string => `knowledge.index.${collectionName}.${fileId}` export const removeJobKey = (collectionName: string, fileId: string): string => `knowledge.remove.${collectionName}.${fileId}` function projectScopeOf(ctx: EventContext, payload: { workspaceId: string; projectId: string }): KnowledgeScope { const ambient = ctx.container.resolve(KNOWLEDGE_SCOPE) if (ambient.level !== "project") throw internal("a knowledge event resolves to a project scope") return { ...ambient, workspaceId: payload.workspaceId, projectId: payload.projectId } } export const indexJob: JobDef = { payload: documentPayload, async handler(payload, ctx) { const scope = ctx.container.resolve(KNOWLEDGE_SCOPE) if (scope.level !== "project") throw internal(`a knowledge job runs in a project scope, not a ${scope.level} one`) await indexDocument(knowledgeSearchDeps(ctx.container), scope, payload.documentId, { ...(ctx.signal === undefined ? {} : { signal: ctx.signal }), }) }, } export const removeJob: JobDef = { payload: documentPayload, async handler(payload, ctx) { const scope = ctx.container.resolve(KNOWLEDGE_SCOPE) if (scope.level !== "project") throw internal(`a knowledge job runs in a project scope, not a ${scope.level} one`) await removeDocument(knowledgeSearchDeps(ctx.container), scope, payload.documentId) }, } async function onDocumentIndexRequested( event: { name: string payload: { workspaceId: string; projectId: string; documentId: string; collectionName: string; fileId: string } }, ctx: EventContext, ): Promise { const scope = projectScopeOf(ctx, event.payload) await ctx.container.resolve(JOB_QUEUE).enqueue( scope, INDEX_JOB, { documentId: event.payload.documentId, collectionName: event.payload.collectionName, fileId: event.payload.fileId, }, { key: indexJobKey(event.payload.collectionName, event.payload.fileId), }, ) } async function onDocumentRemoveRequested( event: { name: string payload: { workspaceId: string; projectId: string; documentId: string; collectionName: string; fileId: string } }, ctx: EventContext, ): Promise { const scope = projectScopeOf(ctx, event.payload) await ctx.container.resolve(JOB_QUEUE).enqueue( scope, REMOVE_JOB, { documentId: event.payload.documentId, collectionName: event.payload.collectionName, fileId: event.payload.fileId, }, { key: removeJobKey(event.payload.collectionName, event.payload.fileId), }, ) } export const KNOWLEDGE_JOBS = { [INDEX_JOB]: indexJob, [REMOVE_JOB]: removeJob } export const KNOWLEDGE_SUBSCRIPTIONS = [ subscribe(documentIndexRequested, onDocumentIndexRequested), subscribe(documentRemoveRequested, onDocumentRemoveRequested), ]