@flighthq/ipc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export * from './ipc';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './ipc';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAC"}
package/dist/ipc.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { IpcBackend, IpcChannel, IpcMessageEvent, IpcSignals, IpcTarget } from '@flighthq/types';
2
+ export declare function createIpcChannel(name: string): IpcChannel;
3
+ export declare function createWebIpcBackend(): IpcBackend;
4
+ export declare function enableIpcSignals(): IpcSignals;
5
+ export declare function getIpcBackend(): IpcBackend;
6
+ export declare function getIpcListenerCount(channel: string | Readonly<IpcChannel>): number;
7
+ export declare function getIpcSignals(): Readonly<IpcSignals> | null;
8
+ export declare function hasIpcBackend(): boolean;
9
+ export declare function invokeIpc(channel: string | Readonly<IpcChannel>, ...args: readonly unknown[]): Promise<unknown>;
10
+ export declare function invokeIpcWithTimeout(channel: string | Readonly<IpcChannel>, timeoutMs: number, ...args: readonly unknown[]): Promise<unknown>;
11
+ export declare function onceIpcMessage(channel: string | Readonly<IpcChannel>, listener: (...args: readonly unknown[]) => void): () => void;
12
+ export declare function onIpcInvoke(channel: string | Readonly<IpcChannel>, handler: (...args: readonly unknown[]) => unknown | Promise<unknown>): () => void;
13
+ export declare function onIpcMessage(channel: string | Readonly<IpcChannel>, listener: (...args: readonly unknown[]) => void): () => void;
14
+ export declare function onIpcMessageEvent(channel: string | Readonly<IpcChannel>, listener: (event: Readonly<IpcMessageEvent>) => void): () => void;
15
+ export declare function removeAllIpcListeners(channel?: string | Readonly<IpcChannel>): void;
16
+ export declare function sendIpcMessage(channel: string | Readonly<IpcChannel>, ...args: readonly unknown[]): void;
17
+ export declare function sendIpcMessageTo(target: Readonly<IpcTarget>, channel: string | Readonly<IpcChannel>, ...args: readonly unknown[]): void;
18
+ export declare function setIpcBackend(backend: IpcBackend | null): void;
19
+ //# sourceMappingURL=ipc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipc.d.ts","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EACV,eAAe,EACf,UAAU,EACV,SAAS,EACV,MAAM,iBAAiB,CAAC;AAwCzB,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAEzD;AAID,wBAAgB,mBAAmB,IAAI,UAAU,CAahD;AAKD,wBAAgB,gBAAgB,IAAI,UAAU,CAQ7C;AAGD,wBAAgB,aAAa,IAAI,UAAU,CAG1C;AAGD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,CAGlF;AAGD,wBAAgB,aAAa,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAE3D;AAID,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAID,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAE/G;AAID,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EACtC,SAAS,EAAE,MAAM,EACjB,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAC1B,OAAO,CAAC,OAAO,CAAC,CAYlB;AAID,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EACtC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,IAAI,GAC9C,MAAM,IAAI,CAOZ;AAKD,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EACtC,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACnE,MAAM,IAAI,CAIZ;AAID,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EACtC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,IAAI,GAC9C,MAAM,IAAI,CAgBZ;AAID,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EACtC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,KAAK,IAAI,GACnD,MAAM,IAAI,CAyBZ;AAGD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAYnF;AAGD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAExG;AAID,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,EAC3B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EACtC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAC1B,IAAI,CAEN;AAGD,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI,CAG9D"}
package/dist/ipc.js ADDED
@@ -0,0 +1,199 @@
1
+ import { createSignal, emitSignal } from '@flighthq/signals';
2
+ import { IpcTimeoutError } from '@flighthq/types';
3
+ // Resolves a channel argument that may be either a string or an IpcChannel descriptor.
4
+ function resolveChannel(channel) {
5
+ return typeof channel === 'string' ? channel : channel.name;
6
+ }
7
+ // ---- Bronze: listener registry for once/removeAll/count ----
8
+ // Package-local listener tracking, keyed by channel name. Used by onceIpcMessage,
9
+ // removeAllIpcListeners, and getIpcListenerCount. Each entry is the unsubscribe thunk
10
+ // returned by the backend, associated with a channel string.
11
+ const _listeners = new Map();
12
+ function _trackListener(channel, unsubscribe) {
13
+ let set = _listeners.get(channel);
14
+ if (set === undefined) {
15
+ set = new Set();
16
+ _listeners.set(channel, set);
17
+ }
18
+ set.add(unsubscribe);
19
+ }
20
+ function _untrackListener(channel, unsubscribe) {
21
+ const set = _listeners.get(channel);
22
+ if (set !== undefined) {
23
+ set.delete(unsubscribe);
24
+ if (set.size === 0)
25
+ _listeners.delete(channel);
26
+ }
27
+ }
28
+ // ---- IpcSignals (Silver opt-in) ----
29
+ let _ipcSignals = null;
30
+ // ---- Core backend ----
31
+ // Creates a typed channel descriptor. Functions that accept a channel string also accept an
32
+ // IpcChannel, so a feature can publish its channel constants once and get a single grep target.
33
+ export function createIpcChannel(name) {
34
+ return { name };
35
+ }
36
+ // Builds the default web backend. There is no main process on web, so send no-ops, invoke resolves
37
+ // to undefined, and subscribe returns an inert unsubscribe — a native host is required for real IPC.
38
+ export function createWebIpcBackend() {
39
+ return {
40
+ send() { },
41
+ invoke() {
42
+ return Promise.resolve(undefined);
43
+ },
44
+ subscribe() {
45
+ return () => { };
46
+ },
47
+ getCapabilities() {
48
+ return { canHandle: false, canInvoke: false, canSend: false, canTarget: false };
49
+ },
50
+ };
51
+ }
52
+ // Activates the optional IpcSignals group and returns it. Calling this is when the cost is assumed.
53
+ // The returned object is shared for the lifetime of the package; calling enableIpcSignals multiple
54
+ // times returns the same instance.
55
+ export function enableIpcSignals() {
56
+ if (_ipcSignals === null) {
57
+ _ipcSignals = {
58
+ onBackendChanged: createSignal(),
59
+ onChannelMessage: createSignal(),
60
+ };
61
+ }
62
+ return _ipcSignals;
63
+ }
64
+ // Returns the active IpcBackend, or a lazily-created web default. There is always a backend.
65
+ export function getIpcBackend() {
66
+ if (_backend === null)
67
+ _backend = createWebIpcBackend();
68
+ return _backend;
69
+ }
70
+ // Returns the count of active in-package listeners on a channel. Returns 0 for an unknown channel.
71
+ export function getIpcListenerCount(channel) {
72
+ const name = resolveChannel(channel);
73
+ return _listeners.get(name)?.size ?? 0;
74
+ }
75
+ // Returns the active IpcSignals group, or null if enableIpcSignals has not been called.
76
+ export function getIpcSignals() {
77
+ return _ipcSignals;
78
+ }
79
+ // Returns true when a real native backend is installed, false when the lazy web default is active.
80
+ // Use this to distinguish "no main process" (web) from a connected host without performing an invoke.
81
+ export function hasIpcBackend() {
82
+ return _backend !== null;
83
+ }
84
+ // Sends a request on `channel` and resolves with the host's response, or undefined on web.
85
+ // Rejects with a plain Error if the host handler throws.
86
+ export function invokeIpc(channel, ...args) {
87
+ return getIpcBackend().invoke(resolveChannel(channel), args);
88
+ }
89
+ // Sends a request on `channel` and resolves with the host's response, or rejects with
90
+ // IpcTimeoutError if the invoke does not complete within timeoutMs milliseconds.
91
+ export function invokeIpcWithTimeout(channel, timeoutMs, ...args) {
92
+ const name = resolveChannel(channel);
93
+ const invoke = getIpcBackend().invoke(name, args);
94
+ const timeout = new Promise((_, reject) => {
95
+ const id = setTimeout(() => reject(new IpcTimeoutError(name, timeoutMs)), timeoutMs);
96
+ // Attach a no-op catch so the invoke rejection does not surface if timeout fires first.
97
+ invoke.then(() => clearTimeout(id), () => clearTimeout(id));
98
+ });
99
+ return Promise.race([invoke, timeout]);
100
+ }
101
+ // Subscribes `listener` to a single message on `channel`, then auto-unsubscribes.
102
+ // Returns an unsubscribe thunk for the not-yet-fired case.
103
+ export function onceIpcMessage(channel, listener) {
104
+ let unsubscribe = null;
105
+ unsubscribe = onIpcMessage(channel, (...args) => {
106
+ unsubscribe?.();
107
+ listener(...args);
108
+ });
109
+ return unsubscribe;
110
+ }
111
+ // Registers a responder for invokeIpc calls on `channel`. The handler may return a value or a
112
+ // Promise. Returns an unregister thunk. Requires a backend that supports `handle`; if the backend
113
+ // does not support it, returns an inert no-op unsubscribe.
114
+ export function onIpcInvoke(channel, handler) {
115
+ const backend = getIpcBackend();
116
+ if (typeof backend.handle !== 'function')
117
+ return () => { };
118
+ return backend.handle(resolveChannel(channel), handler);
119
+ }
120
+ // Subscribes `listener` to messages on `channel`; returns an unsubscribe function.
121
+ // Incoming args are spread to the listener, mirroring sendIpcMessage's variadic shape.
122
+ export function onIpcMessage(channel, listener) {
123
+ const name = resolveChannel(channel);
124
+ const backend = getIpcBackend();
125
+ const signals = _ipcSignals;
126
+ const unsubscribe = backend.subscribe(name, (args) => {
127
+ if (signals !== null)
128
+ emitSignal(signals.onChannelMessage, name);
129
+ listener(...args);
130
+ });
131
+ const tracked = () => {
132
+ unsubscribe();
133
+ _untrackListener(name, tracked);
134
+ };
135
+ _trackListener(name, tracked);
136
+ return tracked;
137
+ }
138
+ // Subscribes `listener` to messages on `channel`, delivering an IpcMessageEvent with channel,
139
+ // senderId, args, and a reply thunk. The existing args-spread onIpcMessage path stays untouched.
140
+ export function onIpcMessageEvent(channel, listener) {
141
+ const name = resolveChannel(channel);
142
+ const backend = getIpcBackend();
143
+ const signals = _ipcSignals;
144
+ const unsubscribe = backend.subscribe(name, (args) => {
145
+ if (signals !== null)
146
+ emitSignal(signals.onChannelMessage, name);
147
+ const event = {
148
+ channel: name,
149
+ senderId: -1,
150
+ args,
151
+ reply(...replyArgs) {
152
+ if (this.senderId === -1)
153
+ return;
154
+ backend.sendTo?.({ windowId: this.senderId }, name, replyArgs);
155
+ },
156
+ };
157
+ listener(event);
158
+ });
159
+ const tracked = () => {
160
+ unsubscribe();
161
+ _untrackListener(name, tracked);
162
+ };
163
+ _trackListener(name, tracked);
164
+ return tracked;
165
+ }
166
+ // Drops every in-package listener for `channel`, or all channels when omitted.
167
+ export function removeAllIpcListeners(channel) {
168
+ if (channel !== undefined) {
169
+ const name = resolveChannel(channel);
170
+ const set = _listeners.get(name);
171
+ if (set !== undefined) {
172
+ for (const unsubscribe of [...set])
173
+ unsubscribe();
174
+ }
175
+ }
176
+ else {
177
+ for (const [, set] of [..._listeners]) {
178
+ for (const unsubscribe of [...set])
179
+ unsubscribe();
180
+ }
181
+ }
182
+ }
183
+ // Sends a fire-and-forget message on `channel`. No-ops on web (no main process).
184
+ export function sendIpcMessage(channel, ...args) {
185
+ getIpcBackend().send(resolveChannel(channel), args);
186
+ }
187
+ // Sends a fire-and-forget message to a specific target window/process. No-ops when the backend
188
+ // does not support targeted send (i.e. `getCapabilities().canTarget` is false or `sendTo` is absent).
189
+ export function sendIpcMessageTo(target, channel, ...args) {
190
+ getIpcBackend().sendTo?.(target, resolveChannel(channel), args);
191
+ }
192
+ // Installs a native host IPC backend; pass null to fall back to the web default.
193
+ export function setIpcBackend(backend) {
194
+ _backend = backend;
195
+ if (_ipcSignals !== null)
196
+ emitSignal(_ipcSignals.onBackendChanged);
197
+ }
198
+ let _backend = null;
199
+ //# sourceMappingURL=ipc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAS7D,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,uFAAuF;AACvF,SAAS,cAAc,CAAC,OAAsC;IAC5D,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,+DAA+D;AAE/D,kFAAkF;AAClF,sFAAsF;AACtF,6DAA6D;AAC7D,MAAM,UAAU,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEtD,SAAS,cAAc,CAAC,OAAe,EAAE,WAAuB;IAC9D,IAAI,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;QAChB,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;IACD,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAe,EAAE,WAAuB;IAChE,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACxB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;YAAE,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;AACH,CAAC;AAED,uCAAuC;AAEvC,IAAI,WAAW,GAAsB,IAAI,CAAC;AAE1C,yBAAyB;AAEzB,4FAA4F;AAC5F,gGAAgG;AAChG,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,EAAE,IAAI,EAAE,CAAC;AAClB,CAAC;AAED,mGAAmG;AACnG,qGAAqG;AACrG,MAAM,UAAU,mBAAmB;IACjC,OAAO;QACL,IAAI,KAAI,CAAC;QACT,MAAM;YACJ,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;QACD,SAAS;YACP,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;QAClB,CAAC;QACD,eAAe;YACb,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QAClF,CAAC;KACF,CAAC;AACJ,CAAC;AAED,oGAAoG;AACpG,mGAAmG;AACnG,mCAAmC;AACnC,MAAM,UAAU,gBAAgB;IAC9B,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,WAAW,GAAG;YACZ,gBAAgB,EAAE,YAAY,EAAE;YAChC,gBAAgB,EAAE,YAAY,EAAE;SACjC,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,aAAa;IAC3B,IAAI,QAAQ,KAAK,IAAI;QAAE,QAAQ,GAAG,mBAAmB,EAAE,CAAC;IACxD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,mBAAmB,CAAC,OAAsC;IACxE,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,mGAAmG;AACnG,sGAAsG;AACtG,MAAM,UAAU,aAAa;IAC3B,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC3B,CAAC;AAED,2FAA2F;AAC3F,yDAAyD;AACzD,MAAM,UAAU,SAAS,CAAC,OAAsC,EAAE,GAAG,IAAwB;IAC3F,OAAO,aAAa,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED,sFAAsF;AACtF,iFAAiF;AACjF,MAAM,UAAU,oBAAoB,CAClC,OAAsC,EACtC,SAAiB,EACjB,GAAG,IAAwB;IAE3B,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAU,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QACjD,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,eAAe,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QACrF,wFAAwF;QACxF,MAAM,CAAC,IAAI,CACT,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,EACtB,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CACvB,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,kFAAkF;AAClF,2DAA2D;AAC3D,MAAM,UAAU,cAAc,CAC5B,OAAsC,EACtC,QAA+C;IAE/C,IAAI,WAAW,GAAwB,IAAI,CAAC;IAC5C,WAAW,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE;QAC9C,WAAW,EAAE,EAAE,CAAC;QAChB,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IACH,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,8FAA8F;AAC9F,kGAAkG;AAClG,2DAA2D;AAC3D,MAAM,UAAU,WAAW,CACzB,OAAsC,EACtC,OAAoE;IAEpE,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAC1D,OAAO,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC;AAED,mFAAmF;AACnF,uFAAuF;AACvF,MAAM,UAAU,YAAY,CAC1B,OAAsC,EACtC,QAA+C;IAE/C,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,WAAW,CAAC;IAE5B,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;QACnD,IAAI,OAAO,KAAK,IAAI;YAAE,UAAU,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;QACjE,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,WAAW,EAAE,CAAC;QACd,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC,CAAC;IACF,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,8FAA8F;AAC9F,iGAAiG;AACjG,MAAM,UAAU,iBAAiB,CAC/B,OAAsC,EACtC,QAAoD;IAEpD,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,WAAW,CAAC;IAE5B,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;QACnD,IAAI,OAAO,KAAK,IAAI;YAAE,UAAU,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;QACjE,MAAM,KAAK,GAAoB;YAC7B,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,CAAC,CAAC;YACZ,IAAI;YACJ,KAAK,CAAC,GAAG,SAA6B;gBACpC,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;oBAAE,OAAO;gBACjC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;YACjE,CAAC;SACF,CAAC;QACF,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,WAAW,EAAE,CAAC;QACd,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC,CAAC;IACF,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,qBAAqB,CAAC,OAAuC;IAC3E,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,KAAK,MAAM,WAAW,IAAI,CAAC,GAAG,GAAG,CAAC;gBAAE,WAAW,EAAE,CAAC;QACpD,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,EAAE,CAAC;YACtC,KAAK,MAAM,WAAW,IAAI,CAAC,GAAG,GAAG,CAAC;gBAAE,WAAW,EAAE,CAAC;QACpD,CAAC;IACH,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAAC,OAAsC,EAAE,GAAG,IAAwB;IAChG,aAAa,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC;AAED,+FAA+F;AAC/F,sGAAsG;AACtG,MAAM,UAAU,gBAAgB,CAC9B,MAA2B,EAC3B,OAAsC,EACtC,GAAG,IAAwB;IAE3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;AAClE,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,QAAQ,GAAG,OAAO,CAAC;IACnB,IAAI,WAAW,KAAK,IAAI;QAAE,UAAU,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAC;AACrE,CAAC;AAED,IAAI,QAAQ,GAAsB,IAAI,CAAC"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/ipc",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/signals": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Inter-process messaging (send, invoke, per-channel subscribe) over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,495 @@
1
+ import type { IpcBackend, IpcBackendCapabilities, IpcChannel, IpcSignals } from '@flighthq/types';
2
+ import { IpcTimeoutError } from '@flighthq/types';
3
+
4
+ import {
5
+ createIpcChannel,
6
+ createWebIpcBackend,
7
+ enableIpcSignals,
8
+ getIpcBackend,
9
+ getIpcListenerCount,
10
+ getIpcSignals,
11
+ hasIpcBackend,
12
+ invokeIpc,
13
+ invokeIpcWithTimeout,
14
+ onceIpcMessage,
15
+ onIpcInvoke,
16
+ onIpcMessage,
17
+ onIpcMessageEvent,
18
+ removeAllIpcListeners,
19
+ sendIpcMessage,
20
+ sendIpcMessageTo,
21
+ setIpcBackend,
22
+ } from './ipc';
23
+
24
+ interface FakeIpcBackend extends IpcBackend {
25
+ sent: { channel: string; args: readonly unknown[] }[];
26
+ sentTo: { target: { windowId: number }; channel: string; args: readonly unknown[] }[];
27
+ result: unknown;
28
+ handlers: Map<string, (...args: readonly unknown[]) => unknown | Promise<unknown>>;
29
+ fire: (channel: string, args: readonly unknown[]) => void;
30
+ }
31
+
32
+ function fakeBackend(
33
+ opts: { canSend?: boolean; canInvoke?: boolean; canHandle?: boolean; canTarget?: boolean } = {},
34
+ ): FakeIpcBackend {
35
+ const { canSend = true, canInvoke = true, canHandle = true, canTarget = true } = opts;
36
+
37
+ const listeners = new Map<string, (args: readonly unknown[]) => void>();
38
+ const backend: FakeIpcBackend = {
39
+ sent: [],
40
+ sentTo: [],
41
+ result: undefined,
42
+ handlers: new Map(),
43
+ send(channel, args) {
44
+ this.sent.push({ channel, args });
45
+ },
46
+ invoke(channel, _args) {
47
+ return Promise.resolve(this.result);
48
+ },
49
+ subscribe(channel, listener) {
50
+ listeners.set(channel, listener);
51
+ return () => {
52
+ listeners.delete(channel);
53
+ };
54
+ },
55
+ handle(channel, handler) {
56
+ this.handlers.set(channel, handler);
57
+ return () => {
58
+ this.handlers.delete(channel);
59
+ };
60
+ },
61
+ sendTo(target, channel, args) {
62
+ this.sentTo.push({ target, channel, args });
63
+ },
64
+ getCapabilities(): Readonly<IpcBackendCapabilities> {
65
+ return { canHandle, canInvoke, canSend, canTarget };
66
+ },
67
+ fire(channel, args) {
68
+ listeners.get(channel)?.(args);
69
+ },
70
+ };
71
+ return backend;
72
+ }
73
+
74
+ afterEach(() => {
75
+ setIpcBackend(null);
76
+ removeAllIpcListeners();
77
+ });
78
+
79
+ describe('createIpcChannel', () => {
80
+ it('returns a descriptor with the given name', () => {
81
+ const ch: IpcChannel = createIpcChannel('events.ready');
82
+ expect(ch.name).toBe('events.ready');
83
+ });
84
+
85
+ it('accepts IpcChannel descriptors in sendIpcMessage', () => {
86
+ const backend = fakeBackend();
87
+ setIpcBackend(backend);
88
+ const ch = createIpcChannel('log');
89
+ sendIpcMessage(ch, 'hello');
90
+ expect(backend.sent).toEqual([{ channel: 'log', args: ['hello'] }]);
91
+ });
92
+
93
+ it('accepts IpcChannel descriptors in onIpcMessage', () => {
94
+ const backend = fakeBackend();
95
+ setIpcBackend(backend);
96
+ const ch = createIpcChannel('event');
97
+ const received: unknown[] = [];
98
+ onIpcMessage(ch, (...args) => received.push(...args));
99
+ backend.fire('event', ['x']);
100
+ expect(received).toEqual(['x']);
101
+ });
102
+ });
103
+
104
+ describe('createWebIpcBackend', () => {
105
+ it('no-ops send, resolves invoke to undefined, returns an inert unsubscribe', async () => {
106
+ const backend = createWebIpcBackend();
107
+ expect(() => backend.send('channel', [1])).not.toThrow();
108
+ expect(await backend.invoke('channel', [])).toBeUndefined();
109
+ expect(typeof backend.subscribe('channel', () => {})).toBe('function');
110
+ });
111
+
112
+ it('reports canSend/canInvoke/canHandle/canTarget as false', () => {
113
+ const backend = createWebIpcBackend();
114
+ const caps = backend.getCapabilities!();
115
+ expect(caps.canSend).toBe(false);
116
+ expect(caps.canInvoke).toBe(false);
117
+ expect(caps.canHandle).toBe(false);
118
+ expect(caps.canTarget).toBe(false);
119
+ });
120
+ });
121
+
122
+ describe('enableIpcSignals', () => {
123
+ it('returns an IpcSignals group', () => {
124
+ const signals: IpcSignals = enableIpcSignals();
125
+ expect(signals.onBackendChanged).toBeDefined();
126
+ expect(signals.onChannelMessage).toBeDefined();
127
+ });
128
+
129
+ it('returns the same instance on repeated calls', () => {
130
+ const a = enableIpcSignals();
131
+ const b = enableIpcSignals();
132
+ expect(a).toBe(b);
133
+ });
134
+
135
+ it('fires onBackendChanged when setIpcBackend is called', () => {
136
+ const signals = enableIpcSignals();
137
+ let fired = 0;
138
+ signals.onBackendChanged.emit = () => {
139
+ fired++;
140
+ };
141
+ setIpcBackend(fakeBackend());
142
+ expect(fired).toBe(1);
143
+ setIpcBackend(null);
144
+ expect(fired).toBe(2);
145
+ });
146
+
147
+ it('fires onChannelMessage when a message is received', () => {
148
+ const signals = enableIpcSignals();
149
+ const backend = fakeBackend();
150
+ setIpcBackend(backend);
151
+ const channels: string[] = [];
152
+ signals.onChannelMessage.emit = (ch) => {
153
+ channels.push(ch);
154
+ };
155
+ onIpcMessage('events', () => {});
156
+ backend.fire('events', []);
157
+ expect(channels).toEqual(['events']);
158
+ });
159
+
160
+ it('does not emit onChannelMessage at subscribe time, only on delivery', () => {
161
+ const signals = enableIpcSignals();
162
+ const backend = fakeBackend();
163
+ setIpcBackend(backend);
164
+ const channels: string[] = [];
165
+ signals.onChannelMessage.emit = (ch) => {
166
+ channels.push(ch);
167
+ };
168
+ onIpcMessage('events', () => {});
169
+ onIpcMessageEvent('events2', () => {});
170
+ // Registering listeners must not emit; the signal is a per-delivery notification.
171
+ expect(channels).toEqual([]);
172
+ backend.fire('events', []);
173
+ backend.fire('events2', []);
174
+ expect(channels).toEqual(['events', 'events2']);
175
+ });
176
+ });
177
+
178
+ describe('getIpcBackend', () => {
179
+ it('falls back to a web backend', () => {
180
+ expect(getIpcBackend()).not.toBeNull();
181
+ });
182
+
183
+ it('returns the registered backend', () => {
184
+ const backend = fakeBackend();
185
+ setIpcBackend(backend);
186
+ expect(getIpcBackend()).toBe(backend);
187
+ });
188
+ });
189
+
190
+ describe('getIpcListenerCount', () => {
191
+ it('returns 0 for an unknown channel', () => {
192
+ expect(getIpcListenerCount('never')).toBe(0);
193
+ });
194
+
195
+ it('increments when listeners are added', () => {
196
+ const backend = fakeBackend();
197
+ setIpcBackend(backend);
198
+ onIpcMessage('ch', () => {});
199
+ onIpcMessage('ch', () => {});
200
+ expect(getIpcListenerCount('ch')).toBe(2);
201
+ });
202
+
203
+ it('decrements when a listener unsubscribes', () => {
204
+ const backend = fakeBackend();
205
+ setIpcBackend(backend);
206
+ const off = onIpcMessage('ch', () => {});
207
+ expect(getIpcListenerCount('ch')).toBe(1);
208
+ off();
209
+ expect(getIpcListenerCount('ch')).toBe(0);
210
+ });
211
+
212
+ it('accepts an IpcChannel descriptor', () => {
213
+ const backend = fakeBackend();
214
+ setIpcBackend(backend);
215
+ const ch = createIpcChannel('typed');
216
+ onIpcMessage(ch, () => {});
217
+ expect(getIpcListenerCount(ch)).toBe(1);
218
+ });
219
+ });
220
+
221
+ describe('getIpcSignals', () => {
222
+ it('returns null before enableIpcSignals is called', async () => {
223
+ // The IpcSignals group is a module-level singleton, so once any test in this suite calls
224
+ // enableIpcSignals the value persists. Re-import the module in isolation to assert the
225
+ // lazy-null contract deterministically: a fresh module has no group until enable is called.
226
+ vi.resetModules();
227
+ const fresh = await import('./ipc');
228
+ expect(fresh.getIpcSignals()).toBeNull();
229
+ // Delivery still works with the group disabled — the emit is guarded, never required.
230
+ const backend = fakeBackend();
231
+ fresh.setIpcBackend(backend);
232
+ const received: unknown[] = [];
233
+ fresh.onIpcMessage('disabled', (...args) => received.push(...args));
234
+ backend.fire('disabled', ['x']);
235
+ expect(received).toEqual(['x']);
236
+ expect(fresh.getIpcSignals()).toBeNull();
237
+ fresh.setIpcBackend(null);
238
+ });
239
+
240
+ it('returns the signals object after enableIpcSignals', () => {
241
+ const signals = enableIpcSignals();
242
+ expect(getIpcSignals()).toBe(signals);
243
+ });
244
+ });
245
+
246
+ describe('hasIpcBackend', () => {
247
+ it('returns false before any backend is set', () => {
248
+ expect(hasIpcBackend()).toBe(false);
249
+ });
250
+
251
+ it('returns true after setting a native backend', () => {
252
+ setIpcBackend(fakeBackend());
253
+ expect(hasIpcBackend()).toBe(true);
254
+ });
255
+
256
+ it('returns false after resetting to null', () => {
257
+ setIpcBackend(fakeBackend());
258
+ setIpcBackend(null);
259
+ expect(hasIpcBackend()).toBe(false);
260
+ });
261
+ });
262
+
263
+ describe('invokeIpc', () => {
264
+ it('passes variadic args as an array and resolves the backend result', async () => {
265
+ const backend = fakeBackend();
266
+ backend.result = 42;
267
+ setIpcBackend(backend);
268
+ expect(await invokeIpc('compute', 1, 2)).toBe(42);
269
+ });
270
+
271
+ it('accepts an IpcChannel descriptor', async () => {
272
+ const backend = fakeBackend();
273
+ backend.result = 'ok';
274
+ setIpcBackend(backend);
275
+ expect(await invokeIpc(createIpcChannel('compute'), 1)).toBe('ok');
276
+ });
277
+ });
278
+
279
+ describe('invokeIpcWithTimeout', () => {
280
+ it('resolves when the backend responds before timeout', async () => {
281
+ const backend = fakeBackend();
282
+ backend.result = 99;
283
+ setIpcBackend(backend);
284
+ expect(await invokeIpcWithTimeout('cmd', 1000)).toBe(99);
285
+ });
286
+
287
+ it('rejects with IpcTimeoutError when the backend does not respond in time', async () => {
288
+ // Replace invoke with one that never resolves.
289
+ const backend = fakeBackend();
290
+ backend.invoke = (_channel, _args) => new Promise(() => {});
291
+ setIpcBackend(backend);
292
+ await expect(invokeIpcWithTimeout('cmd', 10)).rejects.toBeInstanceOf(IpcTimeoutError);
293
+ });
294
+
295
+ it('IpcTimeoutError carries channel and timeoutMs', async () => {
296
+ const backend = fakeBackend();
297
+ backend.invoke = () => new Promise(() => {});
298
+ setIpcBackend(backend);
299
+ let err: IpcTimeoutError | undefined;
300
+ try {
301
+ await invokeIpcWithTimeout('slow-channel', 5);
302
+ } catch (e) {
303
+ err = e as IpcTimeoutError;
304
+ }
305
+ expect(err?.channel).toBe('slow-channel');
306
+ expect(err?.timeoutMs).toBe(5);
307
+ });
308
+ });
309
+
310
+ describe('onceIpcMessage', () => {
311
+ it('auto-unsubscribes after the first message', () => {
312
+ const backend = fakeBackend();
313
+ setIpcBackend(backend);
314
+ const received: unknown[] = [];
315
+ onceIpcMessage('ping', (...args) => received.push(...args));
316
+ backend.fire('ping', ['first']);
317
+ backend.fire('ping', ['second']);
318
+ expect(received).toEqual(['first']);
319
+ });
320
+
321
+ it('can be manually unsubscribed before the message fires', () => {
322
+ const backend = fakeBackend();
323
+ setIpcBackend(backend);
324
+ const received: unknown[] = [];
325
+ const off = onceIpcMessage('ping', () => received.push('fired'));
326
+ off();
327
+ backend.fire('ping', []);
328
+ expect(received).toHaveLength(0);
329
+ });
330
+ });
331
+
332
+ describe('onIpcInvoke', () => {
333
+ it('registers a handler via backend.handle', async () => {
334
+ const backend = fakeBackend();
335
+ setIpcBackend(backend);
336
+ onIpcInvoke('compute', (...args) => (args[0] as number) * 2);
337
+ expect(backend.handlers.has('compute')).toBe(true);
338
+ });
339
+
340
+ it('returns an inert unsubscribe when backend has no handle method', () => {
341
+ const backend = fakeBackend();
342
+ // Remove optional handle method.
343
+ delete (backend as Partial<FakeIpcBackend>).handle;
344
+ setIpcBackend(backend);
345
+ const off = onIpcInvoke('compute', () => {});
346
+ expect(typeof off).toBe('function');
347
+ expect(() => off()).not.toThrow();
348
+ });
349
+
350
+ it('unregisters the handler when unsubscribe is called', () => {
351
+ const backend = fakeBackend();
352
+ setIpcBackend(backend);
353
+ const off = onIpcInvoke('compute', () => {});
354
+ off();
355
+ expect(backend.handlers.has('compute')).toBe(false);
356
+ });
357
+ });
358
+
359
+ describe('onIpcMessage', () => {
360
+ it('spreads incoming args to the listener and unsubscribes', () => {
361
+ const backend = fakeBackend();
362
+ setIpcBackend(backend);
363
+ const received: unknown[] = [];
364
+ const unsubscribe = onIpcMessage('event', (...args) => received.push(args));
365
+ backend.fire('event', ['a', 'b']);
366
+ expect(received).toEqual([['a', 'b']]);
367
+ unsubscribe();
368
+ backend.fire('event', ['c']);
369
+ expect(received).toEqual([['a', 'b']]);
370
+ });
371
+
372
+ it('tracks listener count correctly', () => {
373
+ const backend = fakeBackend();
374
+ setIpcBackend(backend);
375
+ expect(getIpcListenerCount('evt')).toBe(0);
376
+ const off = onIpcMessage('evt', () => {});
377
+ expect(getIpcListenerCount('evt')).toBe(1);
378
+ off();
379
+ expect(getIpcListenerCount('evt')).toBe(0);
380
+ });
381
+ });
382
+
383
+ describe('onIpcMessageEvent', () => {
384
+ it('delivers an IpcMessageEvent with channel, senderId -1, and args', () => {
385
+ const backend = fakeBackend();
386
+ setIpcBackend(backend);
387
+ const events: Array<{ channel: string; senderId: number; args: readonly unknown[] }> = [];
388
+ const off = onIpcMessageEvent('ch', (ev) =>
389
+ events.push({ channel: ev.channel, senderId: ev.senderId, args: ev.args }),
390
+ );
391
+ backend.fire('ch', ['hello', 42]);
392
+ expect(events).toEqual([{ channel: 'ch', senderId: -1, args: ['hello', 42] }]);
393
+ off();
394
+ });
395
+
396
+ it('reply is a no-op when senderId is -1, even with a sendTo-capable backend', () => {
397
+ // No backend surfaces sender identity yet, so senderId is permanently -1 and reply must
398
+ // early-return before touching backend.sendTo. Use a sendTo-capable fake to prove the gate
399
+ // is on senderId, not method presence: a reply still produces zero outbound sends. This pins
400
+ // the forward-compatible contract until a backend supplies a real senderId.
401
+ const backend = fakeBackend();
402
+ setIpcBackend(backend);
403
+ let observedSenderId: number | undefined;
404
+ const off = onIpcMessageEvent('ch', (ev) => {
405
+ observedSenderId = ev.senderId;
406
+ ev.reply('pong');
407
+ });
408
+ backend.fire('ch', []);
409
+ expect(observedSenderId).toBe(-1);
410
+ expect(typeof backend.sendTo).toBe('function');
411
+ expect(backend.sentTo).toHaveLength(0);
412
+ off();
413
+ });
414
+
415
+ it('tracks listener count and unsubscribes correctly', () => {
416
+ const backend = fakeBackend();
417
+ setIpcBackend(backend);
418
+ const off = onIpcMessageEvent('ch2', () => {});
419
+ expect(getIpcListenerCount('ch2')).toBe(1);
420
+ off();
421
+ expect(getIpcListenerCount('ch2')).toBe(0);
422
+ });
423
+ });
424
+
425
+ describe('removeAllIpcListeners', () => {
426
+ it('removes all listeners for a specific channel', () => {
427
+ const backend = fakeBackend();
428
+ setIpcBackend(backend);
429
+ const received: string[] = [];
430
+ onIpcMessage('a', () => received.push('a1'));
431
+ onIpcMessage('a', () => received.push('a2'));
432
+ onIpcMessage('b', () => received.push('b1'));
433
+ removeAllIpcListeners('a');
434
+ backend.fire('a', []);
435
+ backend.fire('b', []);
436
+ // 'a' listeners removed, 'b' still active.
437
+ expect(received).toEqual(['b1']);
438
+ });
439
+
440
+ it('removes all listeners for all channels when omitted', () => {
441
+ const backend = fakeBackend();
442
+ setIpcBackend(backend);
443
+ const received: string[] = [];
444
+ onIpcMessage('a', () => received.push('a'));
445
+ onIpcMessage('b', () => received.push('b'));
446
+ removeAllIpcListeners();
447
+ backend.fire('a', []);
448
+ backend.fire('b', []);
449
+ expect(received).toHaveLength(0);
450
+ });
451
+
452
+ it('accepts an IpcChannel descriptor', () => {
453
+ const backend = fakeBackend();
454
+ setIpcBackend(backend);
455
+ const ch = createIpcChannel('typed');
456
+ const received: number[] = [];
457
+ onIpcMessage(ch, () => received.push(1));
458
+ removeAllIpcListeners(ch);
459
+ backend.fire('typed', []);
460
+ expect(received).toHaveLength(0);
461
+ });
462
+ });
463
+
464
+ describe('sendIpcMessage', () => {
465
+ it('forwards channel and args array to the backend', () => {
466
+ const backend = fakeBackend();
467
+ setIpcBackend(backend);
468
+ sendIpcMessage('log', 'hi', 7);
469
+ expect(backend.sent).toEqual([{ channel: 'log', args: ['hi', 7] }]);
470
+ });
471
+ });
472
+
473
+ describe('sendIpcMessageTo', () => {
474
+ it('forwards target, channel, and args to backend.sendTo', () => {
475
+ const backend = fakeBackend();
476
+ setIpcBackend(backend);
477
+ sendIpcMessageTo({ windowId: 3 }, 'log', 'hello');
478
+ expect(backend.sentTo).toEqual([{ target: { windowId: 3 }, channel: 'log', args: ['hello'] }]);
479
+ });
480
+
481
+ it('no-ops when backend has no sendTo method', () => {
482
+ const backend = fakeBackend();
483
+ delete (backend as Partial<FakeIpcBackend>).sendTo;
484
+ setIpcBackend(backend);
485
+ expect(() => sendIpcMessageTo({ windowId: 1 }, 'log', 'hi')).not.toThrow();
486
+ });
487
+ });
488
+
489
+ describe('setIpcBackend', () => {
490
+ it('clears back to the web fallback when passed null', () => {
491
+ setIpcBackend(fakeBackend());
492
+ setIpcBackend(null);
493
+ expect(getIpcBackend()).not.toBeNull();
494
+ });
495
+ });