返回 oh-my-ppt
event-bridge.ts
根目录 / src / main / ipc / runtime / event-bridge.ts
1 import type { RuntimeEventEnvelope, RuntimeEventFilter, TypedEventBus } from '../../agent-runtime'
2
3 type RuntimeEventWindow = {
4 id: number
5 isDestroyed(): boolean
6 webContents: {
7 isDestroyed(): boolean
8 send(channel: string, payload: unknown): void
9 }
10 }
11
12 export type RuntimeEventWebContents = {
13 id: number
14 isDestroyed(): boolean
15 send(channel: string, payload: unknown): void
16 once(event: 'destroyed', listener: () => void): unknown
17 removeListener(event: 'destroyed', listener: () => void): unknown
18 }
19
20 export type RuntimeEventSubscriber = {
21 subscriberId: string
22 filter?: Omit<RuntimeEventFilter, 'subscriberId'>
23 send: (event: RuntimeEventEnvelope) => void
24 }
25
26 export type RuntimeEventChannelMessage = {
27 channel: string
28 payload: unknown
29 }
30
31 /**
32 * Electron-facing lifecycle adapter. Domain-specific channel translation is deliberately added
33 * only when that domain migrates to TypedEventBus.
34 */
35 export class RuntimeEventBridge {
36 private readonly unsubscribeBySubscriberId = new Map<string, () => void>()
37
38 constructor(private readonly eventBus: TypedEventBus) {}
39
40 register(subscriber: RuntimeEventSubscriber): () => void {
41 this.unregister(subscriber.subscriberId)
42 const unsubscribe = this.eventBus.subscribe(
43 { ...subscriber.filter, subscriberId: subscriber.subscriberId },
44 subscriber.send
45 )
46 this.unsubscribeBySubscriberId.set(subscriber.subscriberId, unsubscribe)
47 return (): void => {
48 if (this.unsubscribeBySubscriberId.get(subscriber.subscriberId) !== unsubscribe) return
49 this.unsubscribeBySubscriberId.delete(subscriber.subscriberId)
50 unsubscribe()
51 }
52 }
53
54 unregister(subscriberId: string): void {
55 const unsubscribe = this.unsubscribeBySubscriberId.get(subscriberId)
56 if (!unsubscribe) return
57 this.unsubscribeBySubscriberId.delete(subscriberId)
58 unsubscribe()
59 }
60
61 /**
62 * Registers a targeted Electron subscriber and couples its lifetime to the
63 * underlying webContents. Broadcast adapters intentionally discover live
64 * windows for each send instead of creating one subscriber per window.
65 */
66 registerWebContents(args: {
67 subscriberId: string
68 webContents: RuntimeEventWebContents
69 filter?: Omit<RuntimeEventFilter, 'subscriberId'>
70 translate: (event: RuntimeEventEnvelope) => RuntimeEventChannelMessage | null
71 onSendError?: (args: { event: RuntimeEventEnvelope; error: unknown }) => void
72 }): () => void {
73 if (args.webContents.isDestroyed()) return () => undefined
74
75 let disposed = false
76 let unregister: (() => void) | undefined
77 const dispose = (): void => {
78 if (disposed) return
79 disposed = true
80 args.webContents.removeListener('destroyed', dispose)
81 // Use the disposer returned by register() rather than unregister(id): a
82 // subsequent registration may have reused this subscriberId for a newer
83 // webContents. The registered disposer deliberately becomes a no-op in
84 // that case, so an old window's destroyed callback cannot remove it.
85 unregister?.()
86 }
87 unregister = this.register({
88 subscriberId: args.subscriberId,
89 filter: args.filter,
90 send: (event) => {
91 if (args.webContents.isDestroyed()) return
92 const message = args.translate(event)
93 if (!message) return
94 try {
95 args.webContents.send(message.channel, message.payload)
96 } catch (error) {
97 try {
98 args.onSendError?.({ event, error })
99 } catch {
100 // Event delivery diagnostics must not affect other subscribers or jobs.
101 }
102 }
103 }
104 })
105 args.webContents.once('destroyed', dispose)
106
107 return dispose
108 }
109
110 /**
111 * Compatibility adapter for legacy broadcast IPC channels. The runtime only
112 * publishes typed events; Electron window discovery and send failures stay
113 * on this IPC-side boundary.
114 */
115 registerWindowBroadcast(args: {
116 subscriberId: string
117 filter?: Omit<RuntimeEventFilter, 'subscriberId'>
118 windows: () => RuntimeEventWindow[]
119 translate: (event: RuntimeEventEnvelope) => RuntimeEventChannelMessage | null
120 onSendError?: (args: { event: RuntimeEventEnvelope; windowId: number; error: unknown }) => void
121 }): () => void {
122 return this.register({
123 subscriberId: args.subscriberId,
124 filter: args.filter,
125 send: (event) => {
126 const message = args.translate(event)
127 if (!message) return
128 for (const win of args.windows()) {
129 if (win.isDestroyed() || win.webContents.isDestroyed()) continue
130 try {
131 win.webContents.send(message.channel, message.payload)
132 } catch (error) {
133 try {
134 args.onSendError?.({ event, windowId: win.id, error })
135 } catch {
136 // Event delivery diagnostics must not affect other windows or jobs.
137 }
138 }
139 }
140 }
141 })
142 }
143 }
144
144 lines TYPESCRIPT