@novasamatech/host-api 0.7.9-5 → 0.7.9
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/dist/index.d.ts +2 -1
- package/dist/transport.js +56 -0
- package/dist/transport.spec.js +117 -0
- package/dist/types.d.ts +18 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type { ConnectionStatus, HostApiMethod, Logger, RequestHandler, Subscription, SubscriptionHandler, Transport, } from './types.js';
|
|
1
|
+
export type { ConnectionStatus, DebugMessageEvent, HostApiMethod, Logger, RequestHandler, Subscription, SubscriptionHandler, Transport, } from './types.js';
|
|
2
|
+
export type { MessagePayloadSchema } from './protocol/messageCodec.js';
|
|
2
3
|
export type { Provider } from './provider.js';
|
|
3
4
|
export { createRequestId } from './helpers.js';
|
|
4
5
|
export type { HostApi } from './hostApi.js';
|
package/dist/transport.js
CHANGED
|
@@ -79,6 +79,27 @@ export function createTransport(provider) {
|
|
|
79
79
|
const messageProvider = createMessageProvider(provider);
|
|
80
80
|
// subscriptions management (multiplexing)
|
|
81
81
|
const activeSubscriptions = new Map();
|
|
82
|
+
// Lazy provider subscription — zero per-message decode cost while no
|
|
83
|
+
// debug listener is attached.
|
|
84
|
+
let debugListenerCount = 0;
|
|
85
|
+
let debugProviderUnsubscribe = null;
|
|
86
|
+
function ensureDebugProviderSubscription() {
|
|
87
|
+
if (debugProviderUnsubscribe)
|
|
88
|
+
return;
|
|
89
|
+
debugProviderUnsubscribe = messageProvider.subscribe(message => {
|
|
90
|
+
events.emit('debugMessage', {
|
|
91
|
+
direction: 'incoming',
|
|
92
|
+
requestId: message.requestId,
|
|
93
|
+
payload: message.payload,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function maybeDisposeDebugProviderSubscription() {
|
|
98
|
+
if (debugListenerCount > 0)
|
|
99
|
+
return;
|
|
100
|
+
debugProviderUnsubscribe?.();
|
|
101
|
+
debugProviderUnsubscribe = null;
|
|
102
|
+
}
|
|
82
103
|
const transport = {
|
|
83
104
|
provider,
|
|
84
105
|
isCorrectEnvironment() {
|
|
@@ -288,6 +309,9 @@ export function createTransport(provider) {
|
|
|
288
309
|
},
|
|
289
310
|
postMessage(requestId, payload) {
|
|
290
311
|
checks();
|
|
312
|
+
if (debugListenerCount > 0) {
|
|
313
|
+
events.emit('debugMessage', { direction: 'outgoing', requestId, payload });
|
|
314
|
+
}
|
|
291
315
|
messageProvider.postMessage({ requestId, payload });
|
|
292
316
|
},
|
|
293
317
|
listenMessages(action, callback, onError) {
|
|
@@ -311,12 +335,44 @@ export function createTransport(provider) {
|
|
|
311
335
|
},
|
|
312
336
|
destroy() {
|
|
313
337
|
disposed = true;
|
|
338
|
+
debugProviderUnsubscribe?.();
|
|
339
|
+
debugProviderUnsubscribe = null;
|
|
340
|
+
debugListenerCount = 0;
|
|
314
341
|
provider.dispose();
|
|
315
342
|
changeConnectionStatus('disconnected');
|
|
316
343
|
events.emit('destroy');
|
|
317
344
|
events.events = {};
|
|
318
345
|
handshakeAbortController.abort('Transport disposed');
|
|
319
346
|
},
|
|
347
|
+
onDebugMessage(callback) {
|
|
348
|
+
debugListenerCount++;
|
|
349
|
+
ensureDebugProviderSubscription();
|
|
350
|
+
// Wrap each listener individually: nanoevents iterates listeners
|
|
351
|
+
// synchronously and a throw aborts the loop, so without per-listener
|
|
352
|
+
// isolation a single broken listener could starve siblings *and*
|
|
353
|
+
// (on the incoming side) starve unrelated messageProvider subscribers.
|
|
354
|
+
// Route to console.error (not provider.logger.error) so debug-callback
|
|
355
|
+
// bugs stay distinct from real protocol errors — matches the same
|
|
356
|
+
// policy used by host-papp's debugBus.
|
|
357
|
+
const safeCallback = (event) => {
|
|
358
|
+
try {
|
|
359
|
+
callback(event);
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
console.error('debug listener threw', e);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const unsubscribe = events.on('debugMessage', safeCallback);
|
|
366
|
+
let disposed = false;
|
|
367
|
+
return () => {
|
|
368
|
+
if (disposed)
|
|
369
|
+
return;
|
|
370
|
+
disposed = true;
|
|
371
|
+
unsubscribe();
|
|
372
|
+
debugListenerCount--;
|
|
373
|
+
maybeDisposeDebugProviderSubscription();
|
|
374
|
+
};
|
|
375
|
+
},
|
|
320
376
|
};
|
|
321
377
|
if (provider.isCorrectEnvironment()) {
|
|
322
378
|
transport.handleRequest('host_handshake', async (version) => {
|
package/dist/transport.spec.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { enumValue } from '@novasamatech/scale';
|
|
1
2
|
import { createNanoEvents } from 'nanoevents';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { JAM_CODEC_PROTOCOL_ID } from './constants.js';
|
|
3
5
|
import { createDefaultLogger } from './logger.js';
|
|
4
6
|
import { createTransport } from './transport.js';
|
|
5
7
|
function createProviders() {
|
|
@@ -18,6 +20,7 @@ function createProviders() {
|
|
|
18
20
|
sdk: createProvider('toSdk', 'toHost'),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
23
|
+
const samplePayload = () => enumValue('host_handshake_request', enumValue('v1', JAM_CODEC_PROTOCOL_ID));
|
|
21
24
|
describe('transport', () => {
|
|
22
25
|
describe('subscription', () => {
|
|
23
26
|
it('should multiplex subscriptions', () => {
|
|
@@ -55,4 +58,118 @@ describe('transport', () => {
|
|
|
55
58
|
expect(s2Handler).toHaveBeenCalledTimes(2);
|
|
56
59
|
});
|
|
57
60
|
});
|
|
61
|
+
describe('debug hook', () => {
|
|
62
|
+
it('emits outgoing events when postMessage is called and still delivers the message', () => {
|
|
63
|
+
const providers = createProviders();
|
|
64
|
+
const host = createTransport(providers.host);
|
|
65
|
+
const sdk = createTransport(providers.sdk);
|
|
66
|
+
const debugListener = vi.fn();
|
|
67
|
+
host.onDebugMessage(debugListener);
|
|
68
|
+
const sdkReceived = vi.fn();
|
|
69
|
+
sdk.listenMessages('host_handshake_request', sdkReceived);
|
|
70
|
+
const requestId = 'req-1';
|
|
71
|
+
const payload = samplePayload();
|
|
72
|
+
host.postMessage(requestId, payload);
|
|
73
|
+
expect(debugListener).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(debugListener).toHaveBeenCalledWith(expect.objectContaining({ direction: 'outgoing', requestId, payload }));
|
|
75
|
+
expect(sdkReceived).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(sdkReceived).toHaveBeenCalledWith(requestId, expect.objectContaining({ tag: 'host_handshake_request' }));
|
|
77
|
+
});
|
|
78
|
+
it('emits incoming events with decoded payload', () => {
|
|
79
|
+
const providers = createProviders();
|
|
80
|
+
const host = createTransport(providers.host);
|
|
81
|
+
const sdk = createTransport(providers.sdk);
|
|
82
|
+
const debugListener = vi.fn();
|
|
83
|
+
host.onDebugMessage(debugListener);
|
|
84
|
+
const requestId = 'req-2';
|
|
85
|
+
const payload = samplePayload();
|
|
86
|
+
sdk.postMessage(requestId, payload);
|
|
87
|
+
// host receives sdk's message, plus host's own outgoing handshake
|
|
88
|
+
// attempts (none yet, since isReady() wasn't called). Filter to incoming.
|
|
89
|
+
const incoming = debugListener.mock.calls.map(([event]) => event).filter(e => e.direction === 'incoming');
|
|
90
|
+
expect(incoming).toHaveLength(1);
|
|
91
|
+
expect(incoming[0]).toEqual(expect.objectContaining({
|
|
92
|
+
direction: 'incoming',
|
|
93
|
+
requestId,
|
|
94
|
+
payload: expect.objectContaining({ tag: 'host_handshake_request' }),
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
it('supports multiple listeners and stops after unsubscribe', () => {
|
|
98
|
+
const providers = createProviders();
|
|
99
|
+
const host = createTransport(providers.host);
|
|
100
|
+
const a = vi.fn();
|
|
101
|
+
const b = vi.fn();
|
|
102
|
+
const unsubscribeA = host.onDebugMessage(a);
|
|
103
|
+
host.onDebugMessage(b);
|
|
104
|
+
host.postMessage('req-a', samplePayload());
|
|
105
|
+
expect(a).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(b).toHaveBeenCalledTimes(1);
|
|
107
|
+
unsubscribeA();
|
|
108
|
+
// calling unsubscribe twice must be a no-op
|
|
109
|
+
unsubscribeA();
|
|
110
|
+
host.postMessage('req-b', samplePayload());
|
|
111
|
+
expect(a).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(b).toHaveBeenCalledTimes(2);
|
|
113
|
+
});
|
|
114
|
+
it('survives a throwing listener without breaking delivery', () => {
|
|
115
|
+
const providers = createProviders();
|
|
116
|
+
const host = createTransport(providers.host);
|
|
117
|
+
const sdk = createTransport(providers.sdk);
|
|
118
|
+
// Swap console.error directly so the expected throws don't pollute test
|
|
119
|
+
// output. Transport routes debug-callback failures to console.error (not
|
|
120
|
+
// provider.logger) so they stay distinct from real protocol errors.
|
|
121
|
+
const originalConsoleError = console.error;
|
|
122
|
+
const errorSpy = vi.fn();
|
|
123
|
+
console.error = errorSpy;
|
|
124
|
+
try {
|
|
125
|
+
host.onDebugMessage(() => {
|
|
126
|
+
throw new Error('listener boom');
|
|
127
|
+
});
|
|
128
|
+
const goodListener = vi.fn();
|
|
129
|
+
host.onDebugMessage(goodListener);
|
|
130
|
+
const sdkReceived = vi.fn();
|
|
131
|
+
sdk.listenMessages('host_handshake_request', sdkReceived);
|
|
132
|
+
// outgoing: a throwing listener must not block messageProvider.postMessage
|
|
133
|
+
host.postMessage('out-1', samplePayload());
|
|
134
|
+
expect(sdkReceived).toHaveBeenCalledTimes(1);
|
|
135
|
+
// incoming: a throwing listener must not block other host listenMessages subscribers
|
|
136
|
+
const hostReceived = vi.fn();
|
|
137
|
+
host.listenMessages('host_handshake_request', hostReceived);
|
|
138
|
+
sdk.postMessage('in-1', samplePayload());
|
|
139
|
+
expect(hostReceived).toHaveBeenCalledTimes(1);
|
|
140
|
+
// the second good listener still fired despite the first one throwing
|
|
141
|
+
expect(goodListener).toHaveBeenCalled();
|
|
142
|
+
// and the throws were observed on console.error, not propagated
|
|
143
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
console.error = originalConsoleError;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
it('cleans up the debug subscription on destroy() and blocks further sends', () => {
|
|
150
|
+
const providers = createProviders();
|
|
151
|
+
const host = createTransport(providers.host);
|
|
152
|
+
const sdk = createTransport(providers.sdk);
|
|
153
|
+
const listener = vi.fn();
|
|
154
|
+
host.onDebugMessage(listener);
|
|
155
|
+
host.destroy();
|
|
156
|
+
// postMessage on a destroyed transport throws
|
|
157
|
+
expect(() => host.postMessage('after-destroy', samplePayload())).toThrow(/Transport is disposed/);
|
|
158
|
+
// incoming traffic from the peer no longer surfaces to the listener
|
|
159
|
+
sdk.postMessage('in-after-destroy', samplePayload());
|
|
160
|
+
expect(listener).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
it('does not emit outgoing events when no listener is attached', () => {
|
|
163
|
+
const providers = createProviders();
|
|
164
|
+
const host = createTransport(providers.host);
|
|
165
|
+
// sanity: no listener attached, postMessage works fine
|
|
166
|
+
expect(() => host.postMessage('req', samplePayload())).not.toThrow();
|
|
167
|
+
// attach + detach + send: no events should fire to the (now-detached) listener
|
|
168
|
+
const listener = vi.fn();
|
|
169
|
+
const unsubscribe = host.onDebugMessage(listener);
|
|
170
|
+
unsubscribe();
|
|
171
|
+
host.postMessage('req2', samplePayload());
|
|
172
|
+
expect(listener).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
58
175
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export type MessageProvider = {
|
|
|
19
19
|
postMessage(message: CodecType<typeof Message>): void;
|
|
20
20
|
subscribe(fn: (message: CodecType<typeof Message>) => void): VoidFunction;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* EXPERIMENTAL. A single message observed on the transport, in its
|
|
24
|
+
* decoded (non-SCALE) form. Intended for host-side introspection.
|
|
25
|
+
*/
|
|
26
|
+
export type DebugMessageEvent = {
|
|
27
|
+
/** `outgoing` = sent by this side via `postMessage`; `incoming` = received from the peer. */
|
|
28
|
+
direction: 'incoming' | 'outgoing';
|
|
29
|
+
requestId: string;
|
|
30
|
+
payload: MessagePayloadSchema;
|
|
31
|
+
};
|
|
22
32
|
export type Transport = {
|
|
23
33
|
readonly provider: Provider;
|
|
24
34
|
isCorrectEnvironment(): boolean;
|
|
@@ -32,4 +42,12 @@ export type Transport = {
|
|
|
32
42
|
handleSubscription<const Method extends HostApiMethod>(method: Method, handler: SubscriptionHandler<Method>): VoidFunction;
|
|
33
43
|
postMessage(requestId: string, payload: MessagePayloadSchema): void;
|
|
34
44
|
listenMessages<const Action extends MessageAction>(action: Action, callback: (requestId: string, data: PickMessagePayload<Action>) => void, onError?: (error: unknown) => void): VoidFunction;
|
|
45
|
+
/**
|
|
46
|
+
* EXPERIMENTAL. Subscribe to every message crossing this transport
|
|
47
|
+
* in either direction, in decoded form. Returns an unsubscribe
|
|
48
|
+
* function. Multiple listeners are supported; the underlying
|
|
49
|
+
* provider is subscribed lazily — there is no per-message cost
|
|
50
|
+
* while no listener is attached.
|
|
51
|
+
*/
|
|
52
|
+
onDebugMessage(callback: (event: DebugMessageEvent) => void): VoidFunction;
|
|
35
53
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novasamatech/host-api",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.9
|
|
4
|
+
"version": "0.7.9",
|
|
5
5
|
"description": "Host API: transport implementation for host - product integration.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@novasamatech/scale": "0.7.9
|
|
25
|
+
"@novasamatech/scale": "0.7.9",
|
|
26
26
|
"nanoevents": "9.1.0",
|
|
27
27
|
"nanoid": "5.1.9",
|
|
28
28
|
"neverthrow": "^8.2.0",
|