@flighthq/ipc 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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.
- package/README.md +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ipc.d.ts +3 -22
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +34 -223
- package/dist/ipc.js.map +1 -1
- package/package.json +5 -4
- package/src/ipc.test.ts +122 -561
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @flighthq/ipc
|
|
2
2
|
|
|
3
|
-
Inter-process
|
|
3
|
+
Inter-process message reception over an explicit host capability
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/ipc`. The `@flighthq
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/ipc@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/ipc@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/ipc)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { onIpcMessage, onceIpcMessage } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { onIpcMessage, onceIpcMessage } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/ipc.d.ts
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function enableIpcSignals(): IpcSignals;
|
|
5
|
-
export declare function explainIpcBackend(): BackendExplanation;
|
|
6
|
-
export declare function getIpcBackend(): IpcBackend;
|
|
7
|
-
export declare function getIpcListenerCount(channel: string | Readonly<IpcChannel>): number;
|
|
8
|
-
export declare function getIpcSignals(): Readonly<IpcSignals> | null;
|
|
9
|
-
export declare function hasIpcBackend(): boolean;
|
|
10
|
-
export declare function installIpcHostBackend(backend: IpcBackend): void;
|
|
11
|
-
export declare function invokeIpc(channel: string | Readonly<IpcChannel>, ...args: readonly unknown[]): Promise<unknown>;
|
|
12
|
-
export declare function invokeIpcWithTimeout(channel: string | Readonly<IpcChannel>, timeoutMs: number, ...args: readonly unknown[]): Promise<unknown>;
|
|
13
|
-
export declare function observeIpcHostResult(operation: string, succeeded: boolean): void;
|
|
14
|
-
export declare function onceIpcMessage(channel: string | Readonly<IpcChannel>, listener: (...args: readonly unknown[]) => void): () => void;
|
|
15
|
-
export declare function onIpcInvoke(channel: string | Readonly<IpcChannel>, handler: (...args: readonly unknown[]) => unknown | Promise<unknown>): () => void;
|
|
16
|
-
export declare function onIpcMessage(channel: string | Readonly<IpcChannel>, listener: (...args: readonly unknown[]) => void): () => void;
|
|
17
|
-
export declare function onIpcMessageEvent(channel: string | Readonly<IpcChannel>, listener: (event: Readonly<IpcMessageEvent>) => void): () => void;
|
|
18
|
-
export declare function removeAllIpcListeners(channel?: string | Readonly<IpcChannel>): void;
|
|
19
|
-
export declare function resetIpcBackendForTest(): void;
|
|
20
|
-
export declare function sendIpcMessage(channel: string | Readonly<IpcChannel>, ...args: readonly unknown[]): void;
|
|
21
|
-
export declare function sendIpcMessageTo(target: Readonly<IpcTarget>, channel: string | Readonly<IpcChannel>, ...args: readonly unknown[]): void;
|
|
22
|
-
export declare function setIpcBackend(backend: IpcBackend | null): void;
|
|
1
|
+
import type { HasIpcMessage } from '@flighthq/types/contract';
|
|
2
|
+
export declare function onceIpcMessage(host: HasIpcMessage, channel: string, listener: (...args: readonly unknown[]) => void): () => void;
|
|
3
|
+
export declare function onIpcMessage(host: HasIpcMessage, channel: string, listener: (...args: readonly unknown[]) => void): () => void;
|
|
23
4
|
//# sourceMappingURL=ipc.d.ts.map
|
package/dist/ipc.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipc.d.ts","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ipc.d.ts","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAkB9D,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,IAAI,GAC9C,MAAM,IAAI,CAiBZ;AAID,wBAAgB,YAAY,CAC1B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,IAAI,GAC9C,MAAM,IAAI,CAEZ"}
|
package/dist/ipc.js
CHANGED
|
@@ -1,231 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
set.add(unsubscribe);
|
|
18
|
-
}
|
|
19
|
-
function _untrackListener(channel, unsubscribe) {
|
|
20
|
-
const set = _listeners.get(channel);
|
|
21
|
-
if (set !== undefined) {
|
|
22
|
-
set.delete(unsubscribe);
|
|
23
|
-
if (set.size === 0)
|
|
24
|
-
_listeners.delete(channel);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
let _ipcSignals = null;
|
|
28
|
-
// Creates a typed channel descriptor. Functions that accept a channel string also accept an
|
|
29
|
-
// IpcChannel, so a feature can publish its channel constants once and get a single grep target.
|
|
30
|
-
export function createIpcChannel(name) {
|
|
31
|
-
return { name };
|
|
32
|
-
}
|
|
33
|
-
// Activates the optional IpcSignals group and returns it. Calling this is when the cost is assumed.
|
|
34
|
-
// The returned object is shared for the lifetime of the package; calling enableIpcSignals multiple
|
|
35
|
-
// times returns the same instance.
|
|
36
|
-
export function enableIpcSignals() {
|
|
37
|
-
if (_ipcSignals === null) {
|
|
38
|
-
_ipcSignals = {
|
|
39
|
-
onBackendChanged: createSignal(),
|
|
40
|
-
onChannelMessage: createSignal(),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return _ipcSignals;
|
|
44
|
-
}
|
|
45
|
-
// Returns a BackendExplanation describing the active backend layer, conflict state, and viability.
|
|
46
|
-
export function explainIpcBackend() {
|
|
47
|
-
if (_custom !== null) {
|
|
48
|
-
return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
|
|
49
|
-
}
|
|
50
|
-
if (_host !== null) {
|
|
51
|
-
return {
|
|
52
|
-
conflict: _hostConflict,
|
|
53
|
-
layer: 'host',
|
|
54
|
-
operation: _hostObservation !== null ? _hostObservation.operation : null,
|
|
55
|
-
viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
|
|
59
|
-
}
|
|
60
|
-
// Returns the active IpcBackend. Precedence: custom > host > sentinel.
|
|
61
|
-
export function getIpcBackend() {
|
|
62
|
-
return _custom ?? _host ?? _sentinel;
|
|
63
|
-
}
|
|
64
|
-
// Returns the count of active in-package listeners on a channel. Returns 0 for an unknown channel.
|
|
65
|
-
export function getIpcListenerCount(channel) {
|
|
66
|
-
const name = resolveChannel(channel);
|
|
67
|
-
return _listeners.get(name)?.size ?? 0;
|
|
68
|
-
}
|
|
69
|
-
// Returns the active IpcSignals group, or null if enableIpcSignals has not been called.
|
|
70
|
-
export function getIpcSignals() {
|
|
71
|
-
return _ipcSignals;
|
|
72
|
-
}
|
|
73
|
-
// Returns true when a custom or host backend is installed, false when only the sentinel is active.
|
|
74
|
-
export function hasIpcBackend() {
|
|
75
|
-
return _custom !== null || _host !== null;
|
|
76
|
-
}
|
|
77
|
-
// Installs the host-layer IPC backend. Idempotent: a second call with the same backend is a no-op;
|
|
78
|
-
// a second call with a different backend sets the conflict flag and keeps the first.
|
|
79
|
-
export function installIpcHostBackend(backend) {
|
|
80
|
-
if (_host !== null) {
|
|
81
|
-
if (_host !== backend)
|
|
82
|
-
_hostConflict = true;
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
_host = backend;
|
|
86
|
-
}
|
|
87
|
-
// Sends a request on `channel` and resolves with the host's response, or undefined on web.
|
|
88
|
-
// Rejects with a plain Error if the host handler throws.
|
|
89
|
-
export function invokeIpc(channel, ...args) {
|
|
90
|
-
return getIpcBackend().invoke(resolveChannel(channel), args);
|
|
91
|
-
}
|
|
92
|
-
// Sends a request on `channel` and resolves with the host's response, or rejects with
|
|
93
|
-
// IpcTimeoutError if the invoke does not complete within timeoutMs milliseconds.
|
|
94
|
-
export function invokeIpcWithTimeout(channel, timeoutMs, ...args) {
|
|
95
|
-
const name = resolveChannel(channel);
|
|
96
|
-
const invoke = getIpcBackend().invoke(name, args);
|
|
97
|
-
const timeout = new Promise((_, reject) => {
|
|
98
|
-
const id = setTimeout(() => reject(new IpcTimeoutError(name, timeoutMs)), timeoutMs);
|
|
99
|
-
// Attach a no-op catch so the invoke rejection does not surface if timeout fires first.
|
|
100
|
-
invoke.then(() => clearTimeout(id), () => clearTimeout(id));
|
|
101
|
-
});
|
|
102
|
-
return Promise.race([invoke, timeout]);
|
|
103
|
-
}
|
|
104
|
-
// Records the result of a host-layer IPC operation for diagnostic reporting via explainIpcBackend.
|
|
105
|
-
export function observeIpcHostResult(operation, succeeded) {
|
|
106
|
-
_hostObservation = {
|
|
107
|
-
operation,
|
|
108
|
-
viability: succeeded ? 'available' : 'runtime-api-unavailable',
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
// Subscribes `listener` to a single message on `channel`, then auto-unsubscribes.
|
|
112
|
-
// Returns an unsubscribe thunk for the not-yet-fired case.
|
|
113
|
-
export function onceIpcMessage(channel, listener) {
|
|
1
|
+
// Inter-process message reception, over the explicitly supplied host. Every operation takes its host and
|
|
2
|
+
// reads `host.ipc.message` directly: there is no resolver, no sentinel, no installed-backend state and
|
|
3
|
+
// no "no provider" runtime arm, because the `HasIpcMessage` witness is what guarantees the slot exists.
|
|
4
|
+
// A caller without a provider cannot call these at all — that is a compile error, not a false answer.
|
|
5
|
+
//
|
|
6
|
+
// ★ THE WHOLE AMBIENT FAMILY IS GONE — getIpcBackend / setIpcBackend / installIpcHostBackend /
|
|
7
|
+
// resetIpcBackendForTest / observeIpcHostResult / explainIpcBackend / explainIpcOperation /
|
|
8
|
+
// hasIpcBackend / hasIpcOperation, plus the module-scoped _custom/_host/_hostConflict/_hostObservation/
|
|
9
|
+
// _sentinel/_listeners registry. The sentinel in particular answered every operation, so a caller could
|
|
10
|
+
// not tell an installed backend from nothing at all.
|
|
11
|
+
// Subscribes to messages on `channel` for the NEXT message only, then releases the subscription.
|
|
12
|
+
// Returns the unsubscribe, so a caller that never receives a message can still stop listening.
|
|
13
|
+
//
|
|
14
|
+
// The unsubscribe is idempotent and ORIGIN-PINNED: it releases exactly the subscription this call
|
|
15
|
+
// opened, whether it runs after the first message or before any arrives.
|
|
16
|
+
export function onceIpcMessage(host, channel, listener) {
|
|
114
17
|
let unsubscribe = null;
|
|
115
|
-
|
|
18
|
+
let done = false;
|
|
19
|
+
const release = () => {
|
|
20
|
+
if (done)
|
|
21
|
+
return;
|
|
22
|
+
done = true;
|
|
23
|
+
// `unsubscribe` is null only if the provider delivered synchronously during subscribe; the guard
|
|
24
|
+
// below releases in that case, so neither ordering leaks a subscription.
|
|
116
25
|
unsubscribe?.();
|
|
117
|
-
listener(...args);
|
|
118
|
-
});
|
|
119
|
-
return unsubscribe;
|
|
120
|
-
}
|
|
121
|
-
// Registers a responder for invokeIpc calls on `channel`. The handler may return a value or a
|
|
122
|
-
// Promise. Returns an unregister thunk. Requires a backend that supports `handle`; if the backend
|
|
123
|
-
// does not support it, returns an inert no-op unsubscribe.
|
|
124
|
-
export function onIpcInvoke(channel, handler) {
|
|
125
|
-
const backend = getIpcBackend();
|
|
126
|
-
if (typeof backend.handle !== 'function')
|
|
127
|
-
return () => { };
|
|
128
|
-
return backend.handle(resolveChannel(channel), handler);
|
|
129
|
-
}
|
|
130
|
-
// Subscribes `listener` to messages on `channel`; returns an unsubscribe function.
|
|
131
|
-
// Incoming args are spread to the listener, mirroring sendIpcMessage's variadic shape.
|
|
132
|
-
export function onIpcMessage(channel, listener) {
|
|
133
|
-
const name = resolveChannel(channel);
|
|
134
|
-
const backend = getIpcBackend();
|
|
135
|
-
const signals = _ipcSignals;
|
|
136
|
-
const unsubscribe = backend.subscribe(name, (args) => {
|
|
137
|
-
if (signals !== null)
|
|
138
|
-
emitSignal(signals.onChannelMessage, name);
|
|
139
|
-
listener(...args);
|
|
140
|
-
});
|
|
141
|
-
const tracked = () => {
|
|
142
|
-
unsubscribe();
|
|
143
|
-
_untrackListener(name, tracked);
|
|
144
26
|
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
export function onIpcMessageEvent(channel, listener) {
|
|
151
|
-
const name = resolveChannel(channel);
|
|
152
|
-
const backend = getIpcBackend();
|
|
153
|
-
const signals = _ipcSignals;
|
|
154
|
-
const unsubscribe = backend.subscribe(name, (args) => {
|
|
155
|
-
if (signals !== null)
|
|
156
|
-
emitSignal(signals.onChannelMessage, name);
|
|
157
|
-
const event = {
|
|
158
|
-
channel: name,
|
|
159
|
-
senderId: -1,
|
|
160
|
-
args,
|
|
161
|
-
reply(...replyArgs) {
|
|
162
|
-
if (this.senderId === -1)
|
|
163
|
-
return;
|
|
164
|
-
backend.sendTo?.({ windowId: this.senderId }, name, replyArgs);
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
listener(event);
|
|
27
|
+
unsubscribe = host.ipc.message.subscribe(channel, (args) => {
|
|
28
|
+
if (done)
|
|
29
|
+
return;
|
|
30
|
+
release();
|
|
31
|
+
listener(...args);
|
|
168
32
|
});
|
|
169
|
-
|
|
33
|
+
if (done)
|
|
170
34
|
unsubscribe();
|
|
171
|
-
|
|
172
|
-
};
|
|
173
|
-
_trackListener(name, tracked);
|
|
174
|
-
return tracked;
|
|
175
|
-
}
|
|
176
|
-
// Drops every in-package listener for `channel`, or all channels when omitted.
|
|
177
|
-
export function removeAllIpcListeners(channel) {
|
|
178
|
-
if (channel !== undefined) {
|
|
179
|
-
const name = resolveChannel(channel);
|
|
180
|
-
const set = _listeners.get(name);
|
|
181
|
-
if (set !== undefined) {
|
|
182
|
-
for (const unsubscribe of [...set])
|
|
183
|
-
unsubscribe();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
for (const [, set] of [..._listeners]) {
|
|
188
|
-
for (const unsubscribe of [...set])
|
|
189
|
-
unsubscribe();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Resets all backend state to initial values. Test-only — not part of the public API.
|
|
194
|
-
export function resetIpcBackendForTest() {
|
|
195
|
-
_custom = null;
|
|
196
|
-
_host = null;
|
|
197
|
-
_hostConflict = false;
|
|
198
|
-
_hostObservation = null;
|
|
199
|
-
}
|
|
200
|
-
// Sends a fire-and-forget message on `channel`. No-ops when only the sentinel is active.
|
|
201
|
-
export function sendIpcMessage(channel, ...args) {
|
|
202
|
-
getIpcBackend().send(resolveChannel(channel), args);
|
|
203
|
-
}
|
|
204
|
-
// Sends a fire-and-forget message to a specific target window/process. No-ops when the backend
|
|
205
|
-
// does not support targeted send (i.e. `getCapabilities().canTarget` is false or `sendTo` is absent).
|
|
206
|
-
export function sendIpcMessageTo(target, channel, ...args) {
|
|
207
|
-
getIpcBackend().sendTo?.(target, resolveChannel(channel), args);
|
|
35
|
+
return release;
|
|
208
36
|
}
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
emitSignal(_ipcSignals.onBackendChanged);
|
|
37
|
+
// Subscribes to every message on `channel`. Returns the unsubscribe for THIS subscription alone —
|
|
38
|
+
// releasing one listener never disturbs another on the same channel.
|
|
39
|
+
export function onIpcMessage(host, channel, listener) {
|
|
40
|
+
return host.ipc.message.subscribe(channel, (args) => listener(...args));
|
|
214
41
|
}
|
|
215
|
-
const _sentinel = {
|
|
216
|
-
send() { },
|
|
217
|
-
invoke() {
|
|
218
|
-
return Promise.resolve(undefined);
|
|
219
|
-
},
|
|
220
|
-
subscribe() {
|
|
221
|
-
return () => { };
|
|
222
|
-
},
|
|
223
|
-
getCapabilities() {
|
|
224
|
-
return { canHandle: false, canInvoke: false, canSend: false, canTarget: false };
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
let _custom = null;
|
|
228
|
-
let _host = null;
|
|
229
|
-
let _hostConflict = false;
|
|
230
|
-
let _hostObservation = null;
|
|
231
42
|
//# sourceMappingURL=ipc.js.map
|
package/dist/ipc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"AAEA,yGAAyG;AACzG,uGAAuG;AACvG,wGAAwG;AACxG,sGAAsG;AACtG,EAAE;AACF,+FAA+F;AAC/F,4FAA4F;AAC5F,wGAAwG;AACxG,wGAAwG;AACxG,qDAAqD;AAErD,iGAAiG;AACjG,+FAA+F;AAC/F,EAAE;AACF,kGAAkG;AAClG,yEAAyE;AACzE,MAAM,UAAU,cAAc,CAC5B,IAAmB,EACnB,OAAe,EACf,QAA+C;IAE/C,IAAI,WAAW,GAAwB,IAAI,CAAC;IAC5C,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,IAAI;YAAE,OAAO;QACjB,IAAI,GAAG,IAAI,CAAC;QACZ,iGAAiG;QACjG,yEAAyE;QACzE,WAAW,EAAE,EAAE,CAAC;IAClB,CAAC,CAAC;IACF,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;QACzD,IAAI,IAAI;YAAE,OAAO;QACjB,OAAO,EAAE,CAAC;QACV,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IACH,IAAI,IAAI;QAAE,WAAW,EAAE,CAAC;IACxB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,kGAAkG;AAClG,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAC1B,IAAmB,EACnB,OAAe,EACf,QAA+C;IAE/C,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;AAC1E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/ipc",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,12 +38,13 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/signals": "0.5.
|
|
42
|
-
"@flighthq/types": "0.5.
|
|
41
|
+
"@flighthq/signals": "0.5.1-edge.638.d4dbd4e",
|
|
42
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@flighthq/entity": "*",
|
|
45
46
|
"typescript": "^5.3.0"
|
|
46
47
|
},
|
|
47
|
-
"description": "Inter-process
|
|
48
|
+
"description": "Inter-process message reception over an explicit host capability",
|
|
48
49
|
"sideEffects": false
|
|
49
50
|
}
|
package/src/ipc.test.ts
CHANGED
|
@@ -1,575 +1,136 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
onIpcMessageEvent,
|
|
17
|
-
removeAllIpcListeners,
|
|
18
|
-
sendIpcMessage,
|
|
19
|
-
sendIpcMessageTo,
|
|
20
|
-
setIpcBackend,
|
|
21
|
-
explainIpcBackend,
|
|
22
|
-
installIpcHostBackend,
|
|
23
|
-
observeIpcHostResult,
|
|
24
|
-
resetIpcBackendForTest,
|
|
25
|
-
} from './ipc';
|
|
26
|
-
|
|
27
|
-
interface FakeIpcBackend extends IpcBackend {
|
|
28
|
-
sent: { channel: string; args: readonly unknown[] }[];
|
|
29
|
-
sentTo: { target: { windowId: number }; channel: string; args: readonly unknown[] }[];
|
|
30
|
-
result: unknown;
|
|
31
|
-
handlers: Map<string, (...args: readonly unknown[]) => unknown | Promise<unknown>>;
|
|
32
|
-
fire: (channel: string, args: readonly unknown[]) => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function fakeBackend(
|
|
36
|
-
opts: { canSend?: boolean; canInvoke?: boolean; canHandle?: boolean; canTarget?: boolean } = {},
|
|
37
|
-
): FakeIpcBackend {
|
|
38
|
-
const { canSend = true, canInvoke = true, canHandle = true, canTarget = true } = opts;
|
|
39
|
-
|
|
40
|
-
const listeners = new Map<string, (args: readonly unknown[]) => void>();
|
|
41
|
-
const backend: FakeIpcBackend = {
|
|
42
|
-
sent: [],
|
|
43
|
-
sentTo: [],
|
|
44
|
-
result: undefined,
|
|
45
|
-
handlers: new Map(),
|
|
46
|
-
send(channel, args) {
|
|
47
|
-
this.sent.push({ channel, args });
|
|
48
|
-
},
|
|
49
|
-
invoke(channel, _args) {
|
|
50
|
-
return Promise.resolve(this.result);
|
|
51
|
-
},
|
|
52
|
-
subscribe(channel, listener) {
|
|
53
|
-
listeners.set(channel, listener);
|
|
54
|
-
return () => {
|
|
55
|
-
listeners.delete(channel);
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
handle(channel, handler) {
|
|
59
|
-
this.handlers.set(channel, handler);
|
|
60
|
-
return () => {
|
|
61
|
-
this.handlers.delete(channel);
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
sendTo(target, channel, args) {
|
|
65
|
-
this.sentTo.push({ target, channel, args });
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import type { EntityWithoutRuntime, HasIpcMessage, IpcMessageBackend } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import { onIpcMessage, onceIpcMessage } from './ipc';
|
|
5
|
+
|
|
6
|
+
// A host carrying a recording message provider. Nothing installs anywhere, so two hosts can be live at
|
|
7
|
+
// once — the property the ambient seam could not express.
|
|
8
|
+
function messageHost(): HasIpcMessage & {
|
|
9
|
+
deliver(channel: string, ...args: readonly unknown[]): void;
|
|
10
|
+
subscriberCount(channel: string): number;
|
|
11
|
+
} {
|
|
12
|
+
const channels = new Map<string, Set<(args: readonly unknown[]) => void>>();
|
|
13
|
+
return {
|
|
14
|
+
deliver(channel: string, ...args: readonly unknown[]): void {
|
|
15
|
+
for (const listener of [...(channels.get(channel) ?? [])]) listener(args);
|
|
66
16
|
},
|
|
67
|
-
|
|
68
|
-
|
|
17
|
+
ipc: {
|
|
18
|
+
message: createEntity<EntityWithoutRuntime<IpcMessageBackend>>({
|
|
19
|
+
subscribe(channel: string, listener: (args: readonly unknown[]) => void): () => void {
|
|
20
|
+
const set = channels.get(channel) ?? new Set();
|
|
21
|
+
channels.set(channel, set);
|
|
22
|
+
set.add(listener);
|
|
23
|
+
return () => set.delete(listener);
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
69
26
|
},
|
|
70
|
-
|
|
71
|
-
|
|
27
|
+
subscriberCount(channel: string): number {
|
|
28
|
+
return channels.get(channel)?.size ?? 0;
|
|
72
29
|
},
|
|
73
30
|
};
|
|
74
|
-
return backend;
|
|
75
31
|
}
|
|
76
32
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
expect(
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const signals: IpcSignals = enableIpcSignals();
|
|
110
|
-
expect(signals.onBackendChanged).toBeDefined();
|
|
111
|
-
expect(signals.onChannelMessage).toBeDefined();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('returns the same instance on repeated calls', () => {
|
|
115
|
-
const a = enableIpcSignals();
|
|
116
|
-
const b = enableIpcSignals();
|
|
117
|
-
expect(a).toBe(b);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('fires onBackendChanged when setIpcBackend is called', () => {
|
|
121
|
-
const signals = enableIpcSignals();
|
|
122
|
-
let fired = 0;
|
|
123
|
-
signals.onBackendChanged.emit = () => {
|
|
124
|
-
fired++;
|
|
33
|
+
describe('onceIpcMessage', () => {
|
|
34
|
+
it('delivers the first message and then releases the subscription', () => {
|
|
35
|
+
const host = messageHost();
|
|
36
|
+
const seen: (readonly unknown[])[] = [];
|
|
37
|
+
onceIpcMessage(host, 'ping', (...args) => seen.push(args));
|
|
38
|
+
host.deliver('ping', 1);
|
|
39
|
+
host.deliver('ping', 2);
|
|
40
|
+
expect(seen).toEqual([[1]]);
|
|
41
|
+
expect(host.subscriberCount('ping')).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// A caller that never receives a message must still be able to stop listening.
|
|
45
|
+
it('returns an unsubscribe that works before any message arrives', () => {
|
|
46
|
+
const host = messageHost();
|
|
47
|
+
let count = 0;
|
|
48
|
+
const stop = onceIpcMessage(host, 'ping', () => count++);
|
|
49
|
+
stop();
|
|
50
|
+
expect(host.subscriberCount('ping')).toBe(0);
|
|
51
|
+
host.deliver('ping');
|
|
52
|
+
expect(count).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('releases exactly once across repeated stop calls', () => {
|
|
56
|
+
let releases = 0;
|
|
57
|
+
const host: HasIpcMessage = {
|
|
58
|
+
ipc: {
|
|
59
|
+
message: createEntity<EntityWithoutRuntime<IpcMessageBackend>>({
|
|
60
|
+
subscribe(): () => void {
|
|
61
|
+
return () => releases++;
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
125
65
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
expect(
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
66
|
+
const stop = onceIpcMessage(host, 'ping', () => {});
|
|
67
|
+
stop();
|
|
68
|
+
stop();
|
|
69
|
+
expect(releases).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ★ ACQUISITION RACE: a provider that delivers synchronously DURING subscribe returns its unsubscribe
|
|
73
|
+
// only afterwards. Without the guard the subscription would leak, because release ran before there was
|
|
74
|
+
// anything to release.
|
|
75
|
+
it('releases even when the provider delivers during subscribe', () => {
|
|
76
|
+
let released = false;
|
|
77
|
+
const host: HasIpcMessage = {
|
|
78
|
+
ipc: {
|
|
79
|
+
message: createEntity<EntityWithoutRuntime<IpcMessageBackend>>({
|
|
80
|
+
subscribe(_channel, listener): () => void {
|
|
81
|
+
listener([42]);
|
|
82
|
+
return () => (released = true);
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
139
86
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
expect(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
it('does not emit onChannelMessage at subscribe time, only on delivery', () => {
|
|
146
|
-
const signals = enableIpcSignals();
|
|
147
|
-
const backend = fakeBackend();
|
|
148
|
-
setIpcBackend(backend);
|
|
149
|
-
const channels: string[] = [];
|
|
150
|
-
signals.onChannelMessage.emit = (ch) => {
|
|
151
|
-
channels.push(ch);
|
|
152
|
-
};
|
|
153
|
-
onIpcMessage('events', () => {});
|
|
154
|
-
onIpcMessageEvent('events2', () => {});
|
|
155
|
-
// Registering listeners must not emit; the signal is a per-delivery notification.
|
|
156
|
-
expect(channels).toEqual([]);
|
|
157
|
-
backend.fire('events', []);
|
|
158
|
-
backend.fire('events2', []);
|
|
159
|
-
expect(channels).toEqual(['events', 'events2']);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('explainIpcBackend', () => {
|
|
164
|
-
afterEach(() => resetIpcBackendForTest());
|
|
165
|
-
|
|
166
|
-
it('reports host-not-enabled when no backend is installed', () => {
|
|
167
|
-
resetIpcBackendForTest();
|
|
168
|
-
const explanation = explainIpcBackend();
|
|
169
|
-
expect(explanation.layer).toBe('host-not-enabled');
|
|
170
|
-
expect(explanation.conflict).toBe(false);
|
|
171
|
-
expect(explanation.viability).toBe('unobserved');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('reports custom layer when a custom backend is set', () => {
|
|
175
|
-
setIpcBackend(fakeBackend());
|
|
176
|
-
expect(explainIpcBackend().layer).toBe('custom');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('reports host layer when a host backend is installed', () => {
|
|
180
|
-
installIpcHostBackend(fakeBackend());
|
|
181
|
-
expect(explainIpcBackend().layer).toBe('host');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('reports conflict when two different host backends are installed', () => {
|
|
185
|
-
installIpcHostBackend(fakeBackend());
|
|
186
|
-
installIpcHostBackend(fakeBackend());
|
|
187
|
-
expect(explainIpcBackend().conflict).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe('getIpcBackend', () => {
|
|
192
|
-
it('falls back to a web backend', () => {
|
|
193
|
-
expect(getIpcBackend()).not.toBeNull();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('returns the registered backend', () => {
|
|
197
|
-
const backend = fakeBackend();
|
|
198
|
-
setIpcBackend(backend);
|
|
199
|
-
expect(getIpcBackend()).toBe(backend);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe('getIpcBackend (sentinel)', () => {
|
|
204
|
-
it('no-ops send, resolves invoke to undefined, returns an inert unsubscribe', async () => {
|
|
205
|
-
const backend = getIpcBackend();
|
|
206
|
-
expect(() => backend.send('channel', [1])).not.toThrow();
|
|
207
|
-
expect(await backend.invoke('channel', [])).toBeUndefined();
|
|
208
|
-
expect(typeof backend.subscribe('channel', () => {})).toBe('function');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('reports canSend/canInvoke/canHandle/canTarget as false', () => {
|
|
212
|
-
const backend = getIpcBackend();
|
|
213
|
-
const caps = backend.getCapabilities!();
|
|
214
|
-
expect(caps.canSend).toBe(false);
|
|
215
|
-
expect(caps.canInvoke).toBe(false);
|
|
216
|
-
expect(caps.canHandle).toBe(false);
|
|
217
|
-
expect(caps.canTarget).toBe(false);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe('getIpcListenerCount', () => {
|
|
222
|
-
it('returns 0 for an unknown channel', () => {
|
|
223
|
-
expect(getIpcListenerCount('never')).toBe(0);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('increments when listeners are added', () => {
|
|
227
|
-
const backend = fakeBackend();
|
|
228
|
-
setIpcBackend(backend);
|
|
229
|
-
onIpcMessage('ch', () => {});
|
|
230
|
-
onIpcMessage('ch', () => {});
|
|
231
|
-
expect(getIpcListenerCount('ch')).toBe(2);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('decrements when a listener unsubscribes', () => {
|
|
235
|
-
const backend = fakeBackend();
|
|
236
|
-
setIpcBackend(backend);
|
|
237
|
-
const off = onIpcMessage('ch', () => {});
|
|
238
|
-
expect(getIpcListenerCount('ch')).toBe(1);
|
|
239
|
-
off();
|
|
240
|
-
expect(getIpcListenerCount('ch')).toBe(0);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('accepts an IpcChannel descriptor', () => {
|
|
244
|
-
const backend = fakeBackend();
|
|
245
|
-
setIpcBackend(backend);
|
|
246
|
-
const ch = createIpcChannel('typed');
|
|
247
|
-
onIpcMessage(ch, () => {});
|
|
248
|
-
expect(getIpcListenerCount(ch)).toBe(1);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('getIpcSignals', () => {
|
|
253
|
-
it('returns null before enableIpcSignals is called', async () => {
|
|
254
|
-
// The IpcSignals group is a module-level singleton, so once any test in this suite calls
|
|
255
|
-
// enableIpcSignals the value persists. Re-import the module in isolation to assert the
|
|
256
|
-
// lazy-null contract deterministically: a fresh module has no group until enable is called.
|
|
257
|
-
vi.resetModules();
|
|
258
|
-
const fresh = await import('./ipc');
|
|
259
|
-
expect(fresh.getIpcSignals()).toBeNull();
|
|
260
|
-
// Delivery still works with the group disabled — the emit is guarded, never required.
|
|
261
|
-
const backend = fakeBackend();
|
|
262
|
-
fresh.setIpcBackend(backend);
|
|
263
|
-
const received: unknown[] = [];
|
|
264
|
-
fresh.onIpcMessage('disabled', (...args) => received.push(...args));
|
|
265
|
-
backend.fire('disabled', ['x']);
|
|
266
|
-
expect(received).toEqual(['x']);
|
|
267
|
-
expect(fresh.getIpcSignals()).toBeNull();
|
|
268
|
-
fresh.setIpcBackend(null);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('returns the signals object after enableIpcSignals', () => {
|
|
272
|
-
const signals = enableIpcSignals();
|
|
273
|
-
expect(getIpcSignals()).toBe(signals);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe('hasIpcBackend', () => {
|
|
278
|
-
it('returns false before any backend is set', () => {
|
|
279
|
-
expect(hasIpcBackend()).toBe(false);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('returns true after setting a native backend', () => {
|
|
283
|
-
setIpcBackend(fakeBackend());
|
|
284
|
-
expect(hasIpcBackend()).toBe(true);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('returns false after resetting to null', () => {
|
|
288
|
-
setIpcBackend(fakeBackend());
|
|
289
|
-
setIpcBackend(null);
|
|
290
|
-
expect(hasIpcBackend()).toBe(false);
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('installIpcHostBackend', () => {
|
|
295
|
-
afterEach(() => resetIpcBackendForTest());
|
|
296
|
-
|
|
297
|
-
it('installs a host backend that getIpcBackend returns', () => {
|
|
298
|
-
const backend = fakeBackend();
|
|
299
|
-
installIpcHostBackend(backend);
|
|
300
|
-
expect(getIpcBackend()).toBe(backend);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('is first-host-wins: a second different backend sets conflict', () => {
|
|
304
|
-
const first = fakeBackend();
|
|
305
|
-
const second = fakeBackend();
|
|
306
|
-
installIpcHostBackend(first);
|
|
307
|
-
installIpcHostBackend(second);
|
|
308
|
-
expect(getIpcBackend()).toBe(first);
|
|
309
|
-
expect(explainIpcBackend().conflict).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('invokeIpc', () => {
|
|
314
|
-
it('passes variadic args as an array and resolves the backend result', async () => {
|
|
315
|
-
const backend = fakeBackend();
|
|
316
|
-
backend.result = 42;
|
|
317
|
-
setIpcBackend(backend);
|
|
318
|
-
expect(await invokeIpc('compute', 1, 2)).toBe(42);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('accepts an IpcChannel descriptor', async () => {
|
|
322
|
-
const backend = fakeBackend();
|
|
323
|
-
backend.result = 'ok';
|
|
324
|
-
setIpcBackend(backend);
|
|
325
|
-
expect(await invokeIpc(createIpcChannel('compute'), 1)).toBe('ok');
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
describe('invokeIpcWithTimeout', () => {
|
|
330
|
-
it('resolves when the backend responds before timeout', async () => {
|
|
331
|
-
const backend = fakeBackend();
|
|
332
|
-
backend.result = 99;
|
|
333
|
-
setIpcBackend(backend);
|
|
334
|
-
expect(await invokeIpcWithTimeout('cmd', 1000)).toBe(99);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('rejects with IpcTimeoutError when the backend does not respond in time', async () => {
|
|
338
|
-
// Replace invoke with one that never resolves.
|
|
339
|
-
const backend = fakeBackend();
|
|
340
|
-
backend.invoke = (_channel, _args) => new Promise(() => {});
|
|
341
|
-
setIpcBackend(backend);
|
|
342
|
-
await expect(invokeIpcWithTimeout('cmd', 10)).rejects.toBeInstanceOf(IpcTimeoutError);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('IpcTimeoutError carries channel and timeoutMs', async () => {
|
|
346
|
-
const backend = fakeBackend();
|
|
347
|
-
backend.invoke = () => new Promise(() => {});
|
|
348
|
-
setIpcBackend(backend);
|
|
349
|
-
let err: IpcTimeoutError | undefined;
|
|
350
|
-
try {
|
|
351
|
-
await invokeIpcWithTimeout('slow-channel', 5);
|
|
352
|
-
} catch (e) {
|
|
353
|
-
err = e as IpcTimeoutError;
|
|
354
|
-
}
|
|
355
|
-
expect(err?.channel).toBe('slow-channel');
|
|
356
|
-
expect(err?.timeoutMs).toBe(5);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
describe('observeIpcHostResult', () => {
|
|
361
|
-
afterEach(() => resetIpcBackendForTest());
|
|
362
|
-
|
|
363
|
-
it('records a successful observation', () => {
|
|
364
|
-
installIpcHostBackend(fakeBackend());
|
|
365
|
-
observeIpcHostResult('send', true);
|
|
366
|
-
const explanation = explainIpcBackend();
|
|
367
|
-
expect(explanation.operation).toBe('send');
|
|
368
|
-
expect(explanation.viability).toBe('available');
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('records a failed observation', () => {
|
|
372
|
-
installIpcHostBackend(fakeBackend());
|
|
373
|
-
observeIpcHostResult('send', false);
|
|
374
|
-
expect(explainIpcBackend().viability).toBe('runtime-api-unavailable');
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe('onceIpcMessage', () => {
|
|
379
|
-
it('auto-unsubscribes after the first message', () => {
|
|
380
|
-
const backend = fakeBackend();
|
|
381
|
-
setIpcBackend(backend);
|
|
382
|
-
const received: unknown[] = [];
|
|
383
|
-
onceIpcMessage('ping', (...args) => received.push(...args));
|
|
384
|
-
backend.fire('ping', ['first']);
|
|
385
|
-
backend.fire('ping', ['second']);
|
|
386
|
-
expect(received).toEqual(['first']);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('can be manually unsubscribed before the message fires', () => {
|
|
390
|
-
const backend = fakeBackend();
|
|
391
|
-
setIpcBackend(backend);
|
|
392
|
-
const received: unknown[] = [];
|
|
393
|
-
const off = onceIpcMessage('ping', () => received.push('fired'));
|
|
394
|
-
off();
|
|
395
|
-
backend.fire('ping', []);
|
|
396
|
-
expect(received).toHaveLength(0);
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
describe('onIpcInvoke', () => {
|
|
401
|
-
it('registers a handler via backend.handle', async () => {
|
|
402
|
-
const backend = fakeBackend();
|
|
403
|
-
setIpcBackend(backend);
|
|
404
|
-
onIpcInvoke('compute', (...args) => (args[0] as number) * 2);
|
|
405
|
-
expect(backend.handlers.has('compute')).toBe(true);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it('returns an inert unsubscribe when backend has no handle method', () => {
|
|
409
|
-
const backend = fakeBackend();
|
|
410
|
-
// Remove optional handle method.
|
|
411
|
-
delete (backend as Partial<FakeIpcBackend>).handle;
|
|
412
|
-
setIpcBackend(backend);
|
|
413
|
-
const off = onIpcInvoke('compute', () => {});
|
|
414
|
-
expect(typeof off).toBe('function');
|
|
415
|
-
expect(() => off()).not.toThrow();
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('unregisters the handler when unsubscribe is called', () => {
|
|
419
|
-
const backend = fakeBackend();
|
|
420
|
-
setIpcBackend(backend);
|
|
421
|
-
const off = onIpcInvoke('compute', () => {});
|
|
422
|
-
off();
|
|
423
|
-
expect(backend.handlers.has('compute')).toBe(false);
|
|
87
|
+
const seen: (readonly unknown[])[] = [];
|
|
88
|
+
onceIpcMessage(host, 'ping', (...args) => seen.push(args));
|
|
89
|
+
expect(seen).toEqual([[42]]);
|
|
90
|
+
expect(released).toBe(true);
|
|
424
91
|
});
|
|
425
92
|
});
|
|
426
93
|
|
|
427
94
|
describe('onIpcMessage', () => {
|
|
428
|
-
it('
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
expect(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// the forward-compatible contract until a backend supplies a real senderId.
|
|
469
|
-
const backend = fakeBackend();
|
|
470
|
-
setIpcBackend(backend);
|
|
471
|
-
let observedSenderId: number | undefined;
|
|
472
|
-
const off = onIpcMessageEvent('ch', (ev) => {
|
|
473
|
-
observedSenderId = ev.senderId;
|
|
474
|
-
ev.reply('pong');
|
|
475
|
-
});
|
|
476
|
-
backend.fire('ch', []);
|
|
477
|
-
expect(observedSenderId).toBe(-1);
|
|
478
|
-
expect(typeof backend.sendTo).toBe('function');
|
|
479
|
-
expect(backend.sentTo).toHaveLength(0);
|
|
480
|
-
off();
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
it('tracks listener count and unsubscribes correctly', () => {
|
|
484
|
-
const backend = fakeBackend();
|
|
485
|
-
setIpcBackend(backend);
|
|
486
|
-
const off = onIpcMessageEvent('ch2', () => {});
|
|
487
|
-
expect(getIpcListenerCount('ch2')).toBe(1);
|
|
488
|
-
off();
|
|
489
|
-
expect(getIpcListenerCount('ch2')).toBe(0);
|
|
490
|
-
});
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
describe('removeAllIpcListeners', () => {
|
|
494
|
-
it('removes all listeners for a specific channel', () => {
|
|
495
|
-
const backend = fakeBackend();
|
|
496
|
-
setIpcBackend(backend);
|
|
497
|
-
const received: string[] = [];
|
|
498
|
-
onIpcMessage('a', () => received.push('a1'));
|
|
499
|
-
onIpcMessage('a', () => received.push('a2'));
|
|
500
|
-
onIpcMessage('b', () => received.push('b1'));
|
|
501
|
-
removeAllIpcListeners('a');
|
|
502
|
-
backend.fire('a', []);
|
|
503
|
-
backend.fire('b', []);
|
|
504
|
-
// 'a' listeners removed, 'b' still active.
|
|
505
|
-
expect(received).toEqual(['b1']);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it('removes all listeners for all channels when omitted', () => {
|
|
509
|
-
const backend = fakeBackend();
|
|
510
|
-
setIpcBackend(backend);
|
|
511
|
-
const received: string[] = [];
|
|
512
|
-
onIpcMessage('a', () => received.push('a'));
|
|
513
|
-
onIpcMessage('b', () => received.push('b'));
|
|
514
|
-
removeAllIpcListeners();
|
|
515
|
-
backend.fire('a', []);
|
|
516
|
-
backend.fire('b', []);
|
|
517
|
-
expect(received).toHaveLength(0);
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
it('accepts an IpcChannel descriptor', () => {
|
|
521
|
-
const backend = fakeBackend();
|
|
522
|
-
setIpcBackend(backend);
|
|
523
|
-
const ch = createIpcChannel('typed');
|
|
524
|
-
const received: number[] = [];
|
|
525
|
-
onIpcMessage(ch, () => received.push(1));
|
|
526
|
-
removeAllIpcListeners(ch);
|
|
527
|
-
backend.fire('typed', []);
|
|
528
|
-
expect(received).toHaveLength(0);
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
describe('resetIpcBackendForTest', () => {
|
|
533
|
-
it('clears all backend slots', () => {
|
|
534
|
-
setIpcBackend(fakeBackend());
|
|
535
|
-
installIpcHostBackend(fakeBackend());
|
|
536
|
-
observeIpcHostResult('send', true);
|
|
537
|
-
resetIpcBackendForTest();
|
|
538
|
-
expect(explainIpcBackend().layer).toBe('host-not-enabled');
|
|
539
|
-
expect(explainIpcBackend().conflict).toBe(false);
|
|
540
|
-
expect(explainIpcBackend().viability).toBe('unobserved');
|
|
541
|
-
});
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
describe('sendIpcMessage', () => {
|
|
545
|
-
it('forwards channel and args array to the backend', () => {
|
|
546
|
-
const backend = fakeBackend();
|
|
547
|
-
setIpcBackend(backend);
|
|
548
|
-
sendIpcMessage('log', 'hi', 7);
|
|
549
|
-
expect(backend.sent).toEqual([{ channel: 'log', args: ['hi', 7] }]);
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
describe('sendIpcMessageTo', () => {
|
|
554
|
-
it('forwards target, channel, and args to backend.sendTo', () => {
|
|
555
|
-
const backend = fakeBackend();
|
|
556
|
-
setIpcBackend(backend);
|
|
557
|
-
sendIpcMessageTo({ windowId: 3 }, 'log', 'hello');
|
|
558
|
-
expect(backend.sentTo).toEqual([{ target: { windowId: 3 }, channel: 'log', args: ['hello'] }]);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
it('no-ops when backend has no sendTo method', () => {
|
|
562
|
-
const backend = fakeBackend();
|
|
563
|
-
delete (backend as Partial<FakeIpcBackend>).sendTo;
|
|
564
|
-
setIpcBackend(backend);
|
|
565
|
-
expect(() => sendIpcMessageTo({ windowId: 1 }, 'log', 'hi')).not.toThrow();
|
|
566
|
-
});
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
describe('setIpcBackend', () => {
|
|
570
|
-
it('clears back to the web fallback when passed null', () => {
|
|
571
|
-
setIpcBackend(fakeBackend());
|
|
572
|
-
setIpcBackend(null);
|
|
573
|
-
expect(getIpcBackend()).not.toBeNull();
|
|
95
|
+
it('delivers every message on its channel with the sent arguments', () => {
|
|
96
|
+
const host = messageHost();
|
|
97
|
+
const seen: (readonly unknown[])[] = [];
|
|
98
|
+
onIpcMessage(host, 'ping', (...args) => seen.push(args));
|
|
99
|
+
host.deliver('ping', 1, 'two');
|
|
100
|
+
host.deliver('ping', 3);
|
|
101
|
+
expect(seen).toEqual([[1, 'two'], [3]]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('delivers only its own channel', () => {
|
|
105
|
+
const host = messageHost();
|
|
106
|
+
let count = 0;
|
|
107
|
+
onIpcMessage(host, 'wanted', () => count++);
|
|
108
|
+
host.deliver('other', 1);
|
|
109
|
+
expect(count).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ★ ORIGIN-PINNED: releasing one listener must not disturb another on the SAME channel.
|
|
113
|
+
it('unsubscribes exactly its own subscription', () => {
|
|
114
|
+
const host = messageHost();
|
|
115
|
+
const kept: unknown[] = [];
|
|
116
|
+
const stopDropped = onIpcMessage(host, 'shared', () => kept.push('dropped'));
|
|
117
|
+
onIpcMessage(host, 'shared', () => kept.push('kept'));
|
|
118
|
+
stopDropped();
|
|
119
|
+
host.deliver('shared');
|
|
120
|
+
expect(kept).toEqual(['kept']);
|
|
121
|
+
expect(host.subscriberCount('shared')).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ★ Two hosts live at once, each serving its own subscription. Unwritable under the ambient seam,
|
|
125
|
+
// which had exactly one answer per process.
|
|
126
|
+
it('routes each subscription to the host it was given', () => {
|
|
127
|
+
const first = messageHost();
|
|
128
|
+
const second = messageHost();
|
|
129
|
+
const seen: string[] = [];
|
|
130
|
+
onIpcMessage(first, 'c', () => seen.push('first'));
|
|
131
|
+
onIpcMessage(second, 'c', () => seen.push('second'));
|
|
132
|
+
second.deliver('c');
|
|
133
|
+
first.deliver('c');
|
|
134
|
+
expect(seen).toEqual(['second', 'first']);
|
|
574
135
|
});
|
|
575
136
|
});
|