feat(jobs): repo-level embed triggers (pages/refs/source_docs)

create/update on embeddable repos enqueue embed.text with a singleton
key that coalesces rapid edits. No-op when the queue is not running
(server tests construct createApp without booting pg-boss).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-01 03:48:03 +10:00
parent 37b7753360
commit e558be49a9
5 changed files with 88 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
import { pool } from '../pool.js';
import { recordAudit } from './audit_stub.js';
import { triggerEmbed } from '../../jobs/triggers.js';
async function snapshot(client, page_id, body_md, edited_by) {
await client.query(
@@ -21,6 +22,7 @@ export async function create({ space_id, slug, title, body_md = '', parent_id },
await snapshot(client, r.id, body_md, actor?.kind);
await client.query('COMMIT');
await recordAudit(actor, 'create', 'page', r.id, null, r);
await triggerEmbed('page', r.id);
return r;
} catch (e) {
await client.query('ROLLBACK'); throw e;
@@ -79,6 +81,7 @@ export async function update(id, patch, actor) {
}
await client.query('COMMIT');
await recordAudit(actor, 'update', 'page', id, before, r);
if (patch.embedding === undefined) await triggerEmbed('page', id);
return r;
} catch (e) {
await client.query('ROLLBACK'); throw e;

View File

@@ -1,5 +1,6 @@
import { pool } from '../pool.js';
import { recordAudit } from './audit_stub.js';
import { triggerEmbed } from '../../jobs/triggers.js';
const FIELDS = [
'space_id','kind','source_url','title','description','summary',
@@ -21,6 +22,7 @@ export async function create(input, actor) {
vals
);
await recordAudit(actor, 'create', 'ref', r.id, null, r);
await triggerEmbed('ref', r.id);
return r;
}
@@ -72,6 +74,7 @@ export async function update(id, patch, actor) {
vals
);
await recordAudit(actor, 'update', 'ref', id, before, r);
if (patch.embedding === undefined) await triggerEmbed('ref', id);
return r;
}

View File

@@ -1,5 +1,6 @@
import { pool } from '../pool.js';
import { recordAudit } from './audit_stub.js';
import { triggerEmbed } from '../../jobs/triggers.js';
const FIELDS = ['resource_id','name','upstream_url','version','format','sync_source','local_path','body_text','embedding','last_synced','metadata'];
@@ -14,6 +15,7 @@ export async function create(input, actor) {
vals
);
await recordAudit(actor, 'create', 'source_doc', r.id, null, r);
await triggerEmbed('source_doc', r.id);
return r;
}
@@ -43,6 +45,7 @@ export async function update(id, patch, actor) {
vals
);
await recordAudit(actor, 'update', 'source_doc', id, before, r);
if (patch.embedding === undefined) await triggerEmbed('source_doc', id);
return r;
}

18
lib/jobs/triggers.js Normal file
View File

@@ -0,0 +1,18 @@
import * as queue from './queue.js';
import { log } from '../log.js';
// Fire-and-forget enqueue of an embed.text job after a repo write.
// Never blocks the write: if the queue is not running (server tests),
// or pg-boss errors transiently, we log + move on. Pending rows get
// picked up by a future re-embed cron in Plan 4+.
export async function triggerEmbed(entity_type, entity_id) {
if (!queue.instance()) return; // not started — no-op
try {
await queue.enqueue('embed.text',
{ entity_type, entity_id },
{ singletonKey: `${entity_type}:${entity_id}` }
);
} catch (e) {
log.warn({ err: e, entity_type, entity_id }, 'triggerEmbed failed');
}
}