/** * 广播的跨进程桥 —— **别的后端动了同一张单,这边的浏览器也得知道**。 * * 2026-08-24 用户报的第三件事:「一直卡在 AI 推进中,刷新后才发现已经推完了」。 * 推送本身没坏 —— 坏在 `createBroadcaster()` 是进程内的一个 Set:那会儿本机与 * 局域网另一台各跑一个后端连同一个库,单子被那一台推着走,这一台的浏览器一个字没收到。 * * 这里钉两条:①自己推的顺手 NOTIFY 出去;②**别人发的才在本地转发,自己发的不收** —— * 不然一条推送在两个进程之间来回弹。 */ import { describe, expect, it } from 'vitest'; import { bridgeBroadcaster, TOUCH_CHANNEL } from '../broadcast-bridge.ts'; import { createBroadcaster } from '../broadcast.ts'; /** 假连接池:记下 NOTIFY 出去的载荷,并留一个口子把「别人的通知」塞进来 */ function fakePool() { const notified: Array<{ channel: string; payload: string }> = []; let onNotification: ((m: { channel: string; payload?: string }) => void) | undefined; const client = { on(event: string, handler: (arg: never) => void) { if (event === 'notification') onNotification = handler as never; }, query: async () => ({ rows: [] }), release: () => undefined, }; return { notified, /** 模拟别的进程(或自己)发来一条 */ deliver(wire: Record | string) { // 字符串原样送 —— 「载荷坏了」那条用例要的是真·不是 JSON 的字节 onNotification?.({ channel: TOUCH_CHANNEL, payload: typeof wire === 'string' ? wire : JSON.stringify(wire) }); }, get armed() { return onNotification !== undefined; }, pool: { query: async (_sql: string, params: unknown[]) => { notified.push({ channel: String(params[0]), payload: String(params[1]) }); return { rows: [] }; }, connect: async () => client, }, }; } const settle = (): Promise => new Promise((r) => setTimeout(r, 0)); describe('bridgeBroadcaster', () => { it('本地推一条 → 顺手 NOTIFY 到库上(带着「谁发的」)', async () => { const fake = fakePool(); const bus = createBroadcaster(); bridgeBroadcaster(fake.pool as never, bus); bus.publish({ kind: 'task', id: 't-1', reason: 'advanced' }); await settle(); expect(fake.notified).toHaveLength(1); expect(fake.notified[0]?.channel).toBe(TOUCH_CHANNEL); const wire = JSON.parse(fake.notified[0]?.payload ?? '{}') as Record; expect(wire['id']).toBe('t-1'); expect(typeof wire['from']).toBe('string'); // 认「谁发的」靠它 }); it('**别的进程发来的,在本地转发一次** —— 这正是那一台推进、这一台刷新的那条路', async () => { const fake = fakePool(); const bus = createBroadcaster(); bridgeBroadcaster(fake.pool as never, bus); await settle(); expect(fake.armed).toBe(true); const got: string[] = []; bus.subscribe((t) => got.push(`${t.kind}:${t.id}:${t.reason}`)); fake.deliver({ kind: 'task', id: 't-9', reason: 'advanced', from: '别的机器' }); expect(got).toEqual(['task:t-9:advanced']); }); it('**自己发的不再收** —— 否则一条推送在两个进程之间来回弹', async () => { const fake = fakePool(); const bus = createBroadcaster(); bridgeBroadcaster(fake.pool as never, bus); bus.publish({ kind: 'task', id: 't-1', reason: 'advanced' }); await settle(); const self = (JSON.parse(fake.notified[0]?.payload ?? '{}') as { from?: string }).from; const got: string[] = []; bus.subscribe((t) => got.push(t.id)); fake.deliver({ kind: 'task', id: 't-1', reason: 'advanced', from: self }); expect(got).toEqual([]); }); it('载荷坏了只丢这一条,连接照旧活着', async () => { const fake = fakePool(); const bus = createBroadcaster(); bridgeBroadcaster(fake.pool as never, bus); await settle(); const got: string[] = []; bus.subscribe((t) => got.push(t.id)); expect(() => fake.deliver('这不是 JSON')).not.toThrow(); // 是 JSON 但形状不对的也得丢掉 —— 频道全库公用,谁都能往里发一句话 expect(() => fake.deliver('"光秃秃一个字符串"')).not.toThrow(); expect(() => fake.deliver({ 没有: 'kind 与 id' })).not.toThrow(); fake.deliver({ kind: 'task', id: 't-3', reason: 'advanced', from: '别处' }); expect(got).toEqual(['t-3']); }); });