@novasamatech/host-api 0.7.9-4 → 0.7.9-6

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/hostApi.js CHANGED
@@ -8,6 +8,7 @@ import { DeriveEntropyErr } from './protocol/v1/deriveEntropy.js';
8
8
  import { HandshakeErr } from './protocol/v1/handshake.js';
9
9
  import { StorageErr } from './protocol/v1/localStorage.js';
10
10
  import { NavigateToErr } from './protocol/v1/navigation.js';
11
+ import { PushNotificationError } from './protocol/v1/notification.js';
11
12
  import { PaymentRequestErr, PaymentTopUpErr } from './protocol/v1/payments.js';
12
13
  import { PreimageSubmitErr } from './protocol/v1/preimage.js';
13
14
  import { ResourceAllocationErr } from './protocol/v1/resourceAllocation.js';
@@ -44,6 +45,12 @@ export function createHostApi(transport) {
44
45
  },
45
46
  pushNotification(payload) {
46
47
  return makeRequest(transport.request('host_push_notification', payload), reason => ({
48
+ tag: payload.tag,
49
+ value: new PushNotificationError.Unknown({ reason }),
50
+ }));
51
+ },
52
+ pushNotificationCancel(payload) {
53
+ return makeRequest(transport.request('host_push_notification_cancel', payload), reason => ({
47
54
  tag: payload.tag,
48
55
  value: new GenericError({ reason }),
49
56
  }));
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';
@@ -21,7 +22,7 @@ export { SignedStatement, SignedStatementsPage, Statement, StatementProofErr, To
21
22
  export { StorageErr } from './protocol/v1/localStorage.js';
22
23
  export { DevicePermission } from './protocol/v1/devicePermission.js';
23
24
  export { RemotePermission } from './protocol/v1/remotePermission.js';
24
- export { PushNotification } from './protocol/v1/notification.js';
25
+ export { NotificationId, PushNotification, PushNotificationError } from './protocol/v1/notification.js';
25
26
  export { NavigateToErr } from './protocol/v1/navigation.js';
26
27
  export { PreimageKey, PreimageSubmitErr, PreimageValue } from './protocol/v1/preimage.js';
27
28
  export { AllocatableResource, AllocationOutcome, ResourceAllocationErr } from './protocol/v1/resourceAllocation.js';
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ export { SignedStatement, SignedStatementsPage, Statement, StatementProofErr, To
16
16
  export { StorageErr } from './protocol/v1/localStorage.js';
17
17
  export { DevicePermission } from './protocol/v1/devicePermission.js';
18
18
  export { RemotePermission } from './protocol/v1/remotePermission.js';
19
- export { PushNotification } from './protocol/v1/notification.js';
19
+ export { NotificationId, PushNotification, PushNotificationError } from './protocol/v1/notification.js';
20
20
  export { NavigateToErr } from './protocol/v1/navigation.js';
21
21
  export { PreimageKey, PreimageSubmitErr, PreimageValue } from './protocol/v1/preimage.js';
22
22
  export { AllocatableResource, AllocationOutcome, ResourceAllocationErr } from './protocol/v1/resourceAllocation.js';
@@ -36,9 +36,10 @@ export declare const hostApiProtocol: {
36
36
  readonly v1: [Codec<{
37
37
  text: string;
38
38
  deeplink: string | undefined;
39
- }>, Codec<import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
39
+ scheduledAt: bigint | undefined;
40
+ }>, Codec<import("scale-ts").ResultPayload<number, import("@novasamatech/scale").CodecError<{
40
41
  reason: string;
41
- }, "GenericError">>>];
42
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>>];
42
43
  }>;
43
44
  readonly host_navigate_to: VersionedProtocolRequest<{
44
45
  readonly v1: [Codec<string>, Codec<import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
@@ -982,5 +983,10 @@ export declare const hostApiProtocol: {
982
983
  reason: string;
983
984
  }, "StatementProofErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "StatementProofErr::UnableToSign"> | import("@novasamatech/scale").CodecError<undefined, "StatementProofErr::UnknownAccount">>>];
984
985
  }>;
986
+ readonly host_push_notification_cancel: VersionedProtocolRequest<{
987
+ readonly v1: [Codec<number>, Codec<import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
988
+ reason: string;
989
+ }, "GenericError">>>];
990
+ }>;
985
991
  };
986
992
  export {};
@@ -10,7 +10,7 @@ import { FeatureV1_request, FeatureV1_response } from './v1/feature.js';
10
10
  import { HandshakeV1_request, HandshakeV1_response } from './v1/handshake.js';
11
11
  import { StorageClearV1_request, StorageClearV1_response, StorageReadV1_request, StorageReadV1_response, StorageWriteV1_request, StorageWriteV1_response, } from './v1/localStorage.js';
12
12
  import { NavigateToV1_request, NavigateToV1_response } from './v1/navigation.js';
13
- import { PushNotificationV1_request, PushNotificationV1_response } from './v1/notification.js';
13
+ import { PushNotificationCancelV1_request, PushNotificationCancelV1_response, PushNotificationV1_request, PushNotificationV1_response, } from './v1/notification.js';
14
14
  import { PaymentBalanceSubscribeV1_interrupt, PaymentBalanceSubscribeV1_receive, PaymentBalanceSubscribeV1_start, PaymentRequestV1_request, PaymentRequestV1_response, PaymentStatusSubscribeV1_interrupt, PaymentStatusSubscribeV1_receive, PaymentStatusSubscribeV1_start, PaymentTopUpV1_request, PaymentTopUpV1_response, } from './v1/payments.js';
15
15
  import { PreimageLookupSubscribeV1_interrupt, PreimageLookupSubscribeV1_receive, PreimageLookupSubscribeV1_start, PreimageSubmitV1_request, PreimageSubmitV1_response, } from './v1/preimage.js';
16
16
  import { RemotePermissionV1_request, RemotePermissionV1_response } from './v1/remotePermission.js';
@@ -211,4 +211,7 @@ export const hostApiProtocol = {
211
211
  remote_statement_store_create_proof_authorized: versionedRequest({
212
212
  v1: [StatementStoreCreateProofAuthorizedV1_request, StatementStoreCreateProofAuthorizedV1_response],
213
213
  }),
214
+ host_push_notification_cancel: versionedRequest({
215
+ v1: [PushNotificationCancelV1_request, PushNotificationCancelV1_response],
216
+ }),
214
217
  };
@@ -28,11 +28,12 @@ export declare const MessagePayload: EnumCodec<Record<"host_handshake_request",
28
28
  readonly v1: Codec<{
29
29
  text: string;
30
30
  deeplink: string | undefined;
31
+ scheduledAt: bigint | undefined;
31
32
  }>;
32
33
  }>> & Record<"host_push_notification_response", EnumCodec<{
33
- readonly v1: Codec<import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
34
+ readonly v1: Codec<import("scale-ts").ResultPayload<number, import("@novasamatech/scale").CodecError<{
34
35
  reason: string;
35
- }, "GenericError">>>;
36
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>>;
36
37
  }>> & Record<"host_navigate_to_request", EnumCodec<{
37
38
  readonly v1: Codec<string>;
38
39
  }>> & Record<"host_navigate_to_response", EnumCodec<{
@@ -1050,6 +1051,12 @@ export declare const MessagePayload: EnumCodec<Record<"host_handshake_request",
1050
1051
  }, import("@novasamatech/scale").CodecError<{
1051
1052
  reason: string;
1052
1053
  }, "StatementProofErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "StatementProofErr::UnableToSign"> | import("@novasamatech/scale").CodecError<undefined, "StatementProofErr::UnknownAccount">>>;
1054
+ }>> & Record<"host_push_notification_cancel_request", EnumCodec<{
1055
+ readonly v1: Codec<number>;
1056
+ }>> & Record<"host_push_notification_cancel_response", EnumCodec<{
1057
+ readonly v1: Codec<import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
1058
+ reason: string;
1059
+ }, "GenericError">>>;
1053
1060
  }>>>;
1054
1061
  export declare const Message: Codec<{
1055
1062
  requestId: string;
@@ -1091,15 +1098,16 @@ export declare const Message: Codec<{
1091
1098
  value: {
1092
1099
  text: string;
1093
1100
  deeplink: string | undefined;
1101
+ scheduledAt: bigint | undefined;
1094
1102
  };
1095
1103
  };
1096
1104
  } | {
1097
1105
  tag: "host_push_notification_response";
1098
1106
  value: {
1099
1107
  tag: "v1";
1100
- value: import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
1108
+ value: import("scale-ts").ResultPayload<number, import("@novasamatech/scale").CodecError<{
1101
1109
  reason: string;
1102
- }, "GenericError">>;
1110
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>;
1103
1111
  };
1104
1112
  } | {
1105
1113
  tag: "host_navigate_to_request";
@@ -2583,6 +2591,20 @@ export declare const Message: Codec<{
2583
2591
  reason: string;
2584
2592
  }, "StatementProofErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "StatementProofErr::UnableToSign"> | import("@novasamatech/scale").CodecError<undefined, "StatementProofErr::UnknownAccount">>;
2585
2593
  };
2594
+ } | {
2595
+ tag: "host_push_notification_cancel_request";
2596
+ value: {
2597
+ tag: "v1";
2598
+ value: number;
2599
+ };
2600
+ } | {
2601
+ tag: "host_push_notification_cancel_response";
2602
+ value: {
2603
+ tag: "v1";
2604
+ value: import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
2605
+ reason: string;
2606
+ }, "GenericError">>;
2607
+ };
2586
2608
  };
2587
2609
  }>;
2588
2610
  export type MessageAction = MessagePayloadSchema['tag'];
@@ -1,11 +1,39 @@
1
+ export declare const NotificationId: import("scale-ts").Codec<number>;
1
2
  export declare const PushNotification: import("scale-ts").Codec<{
2
3
  text: string;
3
4
  deeplink: string | undefined;
5
+ scheduledAt: bigint | undefined;
4
6
  }>;
7
+ export declare const PushNotificationError: [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
8
+ reason: string;
9
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
10
+ reason: string;
11
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>] & {
12
+ enc: import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
13
+ reason: string;
14
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>;
15
+ dec: import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
16
+ reason: string;
17
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>;
18
+ } & {
19
+ readonly ScheduleLimitReached: import("@novasamatech/scale").ErrCodec<undefined, "PushNotificationError::ScheduleLimitReached">;
20
+ readonly Unknown: import("@novasamatech/scale").ErrCodec<{
21
+ reason: string;
22
+ }, "PushNotificationError::Unknown">;
23
+ } & {
24
+ [Symbol.hasInstance](v: unknown): v is import("@novasamatech/scale").CodecError<{
25
+ reason: string;
26
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">;
27
+ };
5
28
  export declare const PushNotificationV1_request: import("scale-ts").Codec<{
6
29
  text: string;
7
30
  deeplink: string | undefined;
31
+ scheduledAt: bigint | undefined;
8
32
  }>;
9
- export declare const PushNotificationV1_response: import("scale-ts").Codec<import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
33
+ export declare const PushNotificationV1_response: import("scale-ts").Codec<import("scale-ts").ResultPayload<number, import("@novasamatech/scale").CodecError<{
34
+ reason: string;
35
+ }, "PushNotificationError::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PushNotificationError::ScheduleLimitReached">>>;
36
+ export declare const PushNotificationCancelV1_request: import("scale-ts").Codec<number>;
37
+ export declare const PushNotificationCancelV1_response: import("scale-ts").Codec<import("scale-ts").ResultPayload<undefined, import("@novasamatech/scale").CodecError<{
10
38
  reason: string;
11
39
  }, "GenericError">>>;
@@ -1,8 +1,17 @@
1
- import { Option, Result, Struct, _void, str } from 'scale-ts';
2
- import { GenericError } from '../commonCodecs.js';
1
+ import { ErrEnum } from '@novasamatech/scale';
2
+ import { Option, Result, Struct, _void, str, u32, u64 } from 'scale-ts';
3
+ import { GenericErr, GenericError } from '../commonCodecs.js';
4
+ export const NotificationId = u32;
3
5
  export const PushNotification = Struct({
4
6
  text: str,
5
7
  deeplink: Option(str),
8
+ scheduledAt: Option(u64),
9
+ });
10
+ export const PushNotificationError = ErrEnum('PushNotificationError', {
11
+ ScheduleLimitReached: [_void, 'Schedule limit reached'],
12
+ Unknown: [GenericErr, 'Unknown error'],
6
13
  });
7
14
  export const PushNotificationV1_request = PushNotification;
8
- export const PushNotificationV1_response = Result(_void, GenericError);
15
+ export const PushNotificationV1_response = Result(NotificationId, PushNotificationError);
16
+ export const PushNotificationCancelV1_request = NotificationId;
17
+ export const PushNotificationCancelV1_response = Result(_void, GenericError);
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) => {
@@ -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",
4
+ "version": "0.7.9-6",
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-4",
25
+ "@novasamatech/scale": "0.7.9-6",
26
26
  "nanoevents": "9.1.0",
27
27
  "nanoid": "5.1.9",
28
28
  "neverthrow": "^8.2.0",