@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 +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/protocol/impl.d.ts +8 -2
- package/dist/protocol/impl.js +4 -1
- package/dist/protocol/messageCodec.d.ts +26 -4
- package/dist/protocol/v1/notification.d.ts +29 -1
- package/dist/protocol/v1/notification.js +12 -3
- 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/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';
|
package/dist/protocol/impl.d.ts
CHANGED
|
@@ -36,9 +36,10 @@ export declare const hostApiProtocol: {
|
|
|
36
36
|
readonly v1: [Codec<{
|
|
37
37
|
text: string;
|
|
38
38
|
deeplink: string | undefined;
|
|
39
|
-
|
|
39
|
+
scheduledAt: bigint | undefined;
|
|
40
|
+
}>, Codec<import("scale-ts").ResultPayload<number, import("@novasamatech/scale").CodecError<{
|
|
40
41
|
reason: string;
|
|
41
|
-
}, "
|
|
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 {};
|
package/dist/protocol/impl.js
CHANGED
|
@@ -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<
|
|
34
|
+
readonly v1: Codec<import("scale-ts").ResultPayload<number, import("@novasamatech/scale").CodecError<{
|
|
34
35
|
reason: string;
|
|
35
|
-
}, "
|
|
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<
|
|
1108
|
+
value: import("scale-ts").ResultPayload<number, import("@novasamatech/scale").CodecError<{
|
|
1101
1109
|
reason: string;
|
|
1102
|
-
}, "
|
|
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<
|
|
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 {
|
|
2
|
-
import {
|
|
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(
|
|
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) => {
|
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-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-
|
|
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",
|