feat(tasks,server,web): bridge outbox events to the SSE bus (U3)
OutboxRelay gains a `mirror` option: listed types are claimed even without a task route and, after the claim transaction commits, published in order to the realtime bus (publish failures are logged, delivery state unaffected). The worker mirrors feed.*, decision.*, download.*, organization.* and subscription.* (feed.due excluded) with their business-id payloads, so the home page, inbox, task stream and feed tables finally receive them. Feed tables refresh on feed.fetched / feed.health_changed; the task stream also listens to feed and decision events. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
04401309fb
commit
615917bb45
6 files changed
+136
-6
No files matched your search
@@ -138,4 +138,40 @@ describe.skipIf(!hasPg)("bootstrap + worker(真实 PG)", () => {
|
||||
const rows = await sql<{ status: string }>`select status from outbox_event`.execute(db);
|
||||
expect(rows.rows).toEqual([{ status: "dispatched" }]);
|
||||
}, 40_000);
|
||||
|
||||
it("outbox → SSE 桥:feed / decision / download / organization 事件提交后镜像到总线,feed.due 不推", async () => {
|
||||
const got: { type: string; payload: unknown }[] = [];
|
||||
const off = booted.ctx.events.subscribe((e) => {
|
||||
if (e.type !== EventType.heartbeat) got.push({ type: e.type, payload: e.payload });
|
||||
});
|
||||
const decisionId = "33333333-3333-4333-8333-333333333333";
|
||||
const feedId = "44444444-4444-4444-8444-444444444444";
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await booted.ctx.outbox.emit(trx, EventType.feedDue, { feedId, reason: "manual" });
|
||||
await booted.ctx.outbox.emit(trx, EventType.feedFetched, {
|
||||
feedId,
|
||||
newItems: 2,
|
||||
updatedItems: 0,
|
||||
notModified: false,
|
||||
});
|
||||
await booted.ctx.outbox.emit(trx, EventType.decisionCreated, {
|
||||
decisionId,
|
||||
kind: "identity_unresolved",
|
||||
severity: "info",
|
||||
title: "待识别",
|
||||
});
|
||||
});
|
||||
const deadline = Date.now() + 20_000;
|
||||
while (got.length < 2 && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
|
||||
off();
|
||||
// 同一事务内的事件 at 相同,相对顺序不保证
|
||||
got.sort((a, b) => a.type.localeCompare(b.type));
|
||||
expect(got).toEqual([
|
||||
{ type: EventType.decisionCreated, payload: expect.objectContaining({ decisionId }) },
|
||||
{ type: EventType.feedFetched, payload: expect.objectContaining({ feedId, newItems: 2 }) },
|
||||
]);
|
||||
// feed.due 没有路由也不镜像,保持待认领
|
||||
const due = await sql<{ status: string }>`select status from outbox_event where type = 'feed.due'`.execute(db);
|
||||
expect(due.rows).toEqual([{ status: "pending" }]);
|
||||
}, 30_000);
|
||||
});
|
||||
Reference in new issue
Block a user