@novasamatech/host-api 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hostApi.js CHANGED
@@ -293,6 +293,9 @@ export function createHostApi(transport) {
293
293
  paymentStatusSubscribe(args, callback) {
294
294
  return transport.subscribe('host_payment_status_subscribe', args, callback);
295
295
  },
296
+ paymentTopUpStatusSubscribe(args, callback) {
297
+ return transport.subscribe('host_payment_top_up_status_subscribe', args, callback);
298
+ },
296
299
  requestResourceAllocation(payload) {
297
300
  return makeRequest(transport.request('host_request_resource_allocation', payload), reason => ({
298
301
  tag: payload.tag,
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export { NotificationId, PushNotification, PushNotificationError } from './proto
31
31
  export { NavigateToErr } from './protocol/v1/navigation.js';
32
32
  export { PreimageKey, PreimageSubmitErr, PreimageValue } from './protocol/v1/preimage.js';
33
33
  export { AllocatableResource, AllocationOutcome, ResourceAllocationErr } from './protocol/v1/resourceAllocation.js';
34
- export { PaymentBalance, PaymentBalanceErr, PaymentId, PaymentReceipt, PaymentRequestErr, PaymentStatus, PaymentStatusErr, PaymentTopUpErr, PaymentTopUpSource, } from './protocol/v1/payments.js';
34
+ export { PaymentBalance, PaymentBalanceErr, PaymentId, PaymentReceipt, PaymentRequestErr, PaymentStatus, PaymentStatusErr, PaymentTopUpErr, PaymentTopUpId, PaymentTopUpSource, PaymentTopUpStatus, PaymentTopUpStatusErr, } from './protocol/v1/payments.js';
35
35
  export { Arrangement, BorderStyle, ButtonVariant, ColorToken, ContentAlignment, CustomRendererNode, Dimensions, HorizontalAlignment, Modifier, Shape, Size, TypographyStyle, VerticalAlignment, } from './protocol/v1/customRenderer.js';
36
36
  export { ChainHeadEvent, ChainHeadFollowV1_start, ChainIdentifier, ChainInfoErr, OperationStartedResult, RuntimeType, StorageQueryItem, StorageQueryType, StorageResultItem, TransactionBroadcastV1_request, TransactionBroadcastV1_response, TransactionStopV1_request, TransactionStopV1_response, } from './protocol/v1/chainInteraction.js';
37
37
  export { CoinPaymentBalance, CoinPaymentCheque, CoinPaymentErr, CoinPaymentListenForItem, CoinPaymentPurseId, CoinPaymentPurseInfo, CoinPaymentReceivable, CoinPaymentStatus, CoinPaymentTransmissionChannel, } from './protocol/v1/coinPayment.js';
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ export { NotificationId, PushNotification, PushNotificationError } from './proto
23
23
  export { NavigateToErr } from './protocol/v1/navigation.js';
24
24
  export { PreimageKey, PreimageSubmitErr, PreimageValue } from './protocol/v1/preimage.js';
25
25
  export { AllocatableResource, AllocationOutcome, ResourceAllocationErr } from './protocol/v1/resourceAllocation.js';
26
- export { PaymentBalance, PaymentBalanceErr, PaymentId, PaymentReceipt, PaymentRequestErr, PaymentStatus, PaymentStatusErr, PaymentTopUpErr, PaymentTopUpSource, } from './protocol/v1/payments.js';
26
+ export { PaymentBalance, PaymentBalanceErr, PaymentId, PaymentReceipt, PaymentRequestErr, PaymentStatus, PaymentStatusErr, PaymentTopUpErr, PaymentTopUpId, PaymentTopUpSource, PaymentTopUpStatus, PaymentTopUpStatusErr, } from './protocol/v1/payments.js';
27
27
  export { Arrangement, BorderStyle, ButtonVariant, ColorToken, ContentAlignment, CustomRendererNode, Dimensions, HorizontalAlignment, Modifier, Shape, Size, TypographyStyle, VerticalAlignment, } from './protocol/v1/customRenderer.js';
28
28
  export { ChainHeadEvent, ChainHeadFollowV1_start, ChainIdentifier, ChainInfoErr, OperationStartedResult, RuntimeType, StorageQueryItem, StorageQueryType, StorageResultItem, TransactionBroadcastV1_request, TransactionBroadcastV1_response, TransactionStopV1_request, TransactionStopV1_response, } from './protocol/v1/chainInteraction.js';
29
29
  export { CoinPaymentBalance, CoinPaymentCheque, CoinPaymentErr, CoinPaymentListenForItem, CoinPaymentPurseId, CoinPaymentPurseInfo, CoinPaymentReceivable, CoinPaymentStatus, CoinPaymentTransmissionChannel, } from './protocol/v1/coinPayment.js';
@@ -942,11 +942,10 @@ export declare const hostApiProtocol: {
942
942
  tag: "Coins";
943
943
  value: Uint8Array<ArrayBufferLike>[];
944
944
  };
945
+ id: Uint8Array<ArrayBufferLike>;
945
946
  }>, Codec<import("./callError.js").CallResultValue<undefined, import("@novasamatech/scale").CodecError<{
946
947
  reason: string;
947
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
948
- credited: bigint;
949
- }, "PaymentTopUpErr::PartialPayment">>>];
948
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>>];
950
949
  }>;
951
950
  readonly host_payment_request: VersionedProtocolRequest<{
952
951
  readonly v1: [Codec<{
@@ -1482,5 +1481,47 @@ export declare const hostApiProtocol: {
1482
1481
  reason: string;
1483
1482
  }, "WorkerErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "WorkerErr::TooManyOpen">>>];
1484
1483
  }>;
1484
+ readonly host_payment_top_up_status_subscribe: VersionedProtocolSubscription<{
1485
+ readonly v1: [import("@novasamatech/scale").BytesCodec<number>, Codec<{
1486
+ tag: "Detecting";
1487
+ value: undefined;
1488
+ } | {
1489
+ tag: "Claiming";
1490
+ value: undefined;
1491
+ } | {
1492
+ tag: "Claimed";
1493
+ value: {
1494
+ finalized: boolean;
1495
+ };
1496
+ } | {
1497
+ tag: "ClaimedPartially";
1498
+ value: {
1499
+ actualClaimed: bigint;
1500
+ };
1501
+ } | {
1502
+ tag: "NotClaimed";
1503
+ value: undefined;
1504
+ }>, [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
1505
+ reason: string;
1506
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
1507
+ reason: string;
1508
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>] & {
1509
+ enc: import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
1510
+ reason: string;
1511
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
1512
+ dec: import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
1513
+ reason: string;
1514
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
1515
+ } & {
1516
+ readonly NotFound: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpStatusErr::NotFound">;
1517
+ readonly Unknown: import("@novasamatech/scale").ErrCodec<{
1518
+ reason: string;
1519
+ }, "PaymentTopUpStatusErr::Unknown">;
1520
+ } & {
1521
+ [Symbol.hasInstance](v: unknown): v is import("@novasamatech/scale").CodecError<{
1522
+ reason: string;
1523
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">;
1524
+ }];
1525
+ }>;
1485
1526
  };
1486
1527
  export {};
@@ -14,7 +14,7 @@ import { StorageClearV1_request, StorageClearV1_response, StorageReadV1_request,
14
14
  import { LocaleSubscribeV1_interrupt, LocaleSubscribeV1_receive, LocaleSubscribeV1_start } from './v1/locale.js';
15
15
  import { NavigateToV1_request, NavigateToV1_response } from './v1/navigation.js';
16
16
  import { PushNotificationCancelV1_request, PushNotificationCancelV1_response, PushNotificationV1_request, PushNotificationV1_response, } from './v1/notification.js';
17
- 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';
17
+ import { PaymentBalanceSubscribeV1_interrupt, PaymentBalanceSubscribeV1_receive, PaymentBalanceSubscribeV1_start, PaymentRequestV1_request, PaymentRequestV1_response, PaymentStatusSubscribeV1_interrupt, PaymentStatusSubscribeV1_receive, PaymentStatusSubscribeV1_start, PaymentTopUpStatusSubscribeV1_interrupt, PaymentTopUpStatusSubscribeV1_receive, PaymentTopUpStatusSubscribeV1_start, PaymentTopUpV1_request, PaymentTopUpV1_response, } from './v1/payments.js';
18
18
  import { PreimageLookupSubscribeV1_interrupt, PreimageLookupSubscribeV1_receive, PreimageLookupSubscribeV1_start, PreimageSubmitV1_request, PreimageSubmitV1_response, } from './v1/preimage.js';
19
19
  import { RemotePermissionV1_request, RemotePermissionV1_response } from './v1/remotePermission.js';
20
20
  import { RequestResourceAllocationV1_request, RequestResourceAllocationV1_response } from './v1/resourceAllocation.js';
@@ -317,4 +317,14 @@ export const hostApiProtocol = {
317
317
  host_worker_end_operation: versionedRequest(indexer.request(), {
318
318
  v1: [WorkerEndOperationV1_request, WorkerEndOperationV1_response],
319
319
  }),
320
+ // Appended rather than placed next to `host_payment_top_up`: the index is a
321
+ // single running counter, so slotting it into the payment block would shift
322
+ // every id after it.
323
+ host_payment_top_up_status_subscribe: versionedSubscription(indexer.subscription(), {
324
+ v1: [
325
+ PaymentTopUpStatusSubscribeV1_start,
326
+ PaymentTopUpStatusSubscribeV1_receive,
327
+ PaymentTopUpStatusSubscribeV1_interrupt,
328
+ ],
329
+ }),
320
330
  };
@@ -995,13 +995,12 @@ export declare const MessagePayload: EnumCodec<Record<"host_handshake_request",
995
995
  tag: "Coins";
996
996
  value: Uint8Array<ArrayBufferLike>[];
997
997
  };
998
+ id: Uint8Array<ArrayBufferLike>;
998
999
  }>;
999
1000
  }>> & Record<"host_payment_top_up_response", EnumCodec<{
1000
1001
  readonly v1: Codec<import("./callError.js").CallResultValue<undefined, import("@novasamatech/scale").CodecError<{
1001
1002
  reason: string;
1002
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
1003
- credited: bigint;
1004
- }, "PaymentTopUpErr::PartialPayment">>>;
1003
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>>;
1005
1004
  }>> & Record<"host_payment_request_request", EnumCodec<{
1006
1005
  readonly v1: Codec<{
1007
1006
  from: number | undefined;
@@ -1577,7 +1576,52 @@ export declare const MessagePayload: EnumCodec<Record<"host_handshake_request",
1577
1576
  readonly v1: Codec<import("./callError.js").CallResultValue<undefined, import("@novasamatech/scale").CodecError<{
1578
1577
  reason: string;
1579
1578
  }, "WorkerErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "WorkerErr::TooManyOpen">>>;
1580
- }>>>;
1579
+ }>> & Record<"host_payment_top_up_status_subscribe_start", EnumCodec<{
1580
+ readonly v1: import("@novasamatech/scale").BytesCodec<number>;
1581
+ }>> & Record<"host_payment_top_up_status_subscribe_receive", EnumCodec<{
1582
+ readonly v1: Codec<{
1583
+ tag: "Detecting";
1584
+ value: undefined;
1585
+ } | {
1586
+ tag: "Claiming";
1587
+ value: undefined;
1588
+ } | {
1589
+ tag: "Claimed";
1590
+ value: {
1591
+ finalized: boolean;
1592
+ };
1593
+ } | {
1594
+ tag: "ClaimedPartially";
1595
+ value: {
1596
+ actualClaimed: bigint;
1597
+ };
1598
+ } | {
1599
+ tag: "NotClaimed";
1600
+ value: undefined;
1601
+ }>;
1602
+ }>> & Record<"host_payment_top_up_status_subscribe_interrupt", EnumCodec<{
1603
+ readonly v1: [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
1604
+ reason: string;
1605
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
1606
+ reason: string;
1607
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>] & {
1608
+ enc: import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
1609
+ reason: string;
1610
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
1611
+ dec: import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
1612
+ reason: string;
1613
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
1614
+ } & {
1615
+ readonly NotFound: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpStatusErr::NotFound">;
1616
+ readonly Unknown: import("@novasamatech/scale").ErrCodec<{
1617
+ reason: string;
1618
+ }, "PaymentTopUpStatusErr::Unknown">;
1619
+ } & {
1620
+ [Symbol.hasInstance](v: unknown): v is import("@novasamatech/scale").CodecError<{
1621
+ reason: string;
1622
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">;
1623
+ };
1624
+ }>> & Record<"host_payment_top_up_status_subscribe_stop", Codec<undefined>>>;
1581
1625
  export declare const Message: Codec<{
1582
1626
  requestId: string;
1583
1627
  payload: {
@@ -3006,6 +3050,7 @@ export declare const Message: Codec<{
3006
3050
  tag: "Coins";
3007
3051
  value: Uint8Array<ArrayBufferLike>[];
3008
3052
  };
3053
+ id: Uint8Array<ArrayBufferLike>;
3009
3054
  };
3010
3055
  };
3011
3056
  } | {
@@ -3014,9 +3059,7 @@ export declare const Message: Codec<{
3014
3059
  tag: "v1";
3015
3060
  value: import("./callError.js").CallResultValue<undefined, import("@novasamatech/scale").CodecError<{
3016
3061
  reason: string;
3017
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
3018
- credited: bigint;
3019
- }, "PaymentTopUpErr::PartialPayment">>;
3062
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>;
3020
3063
  };
3021
3064
  } | {
3022
3065
  tag: "host_payment_request_request";
@@ -3756,6 +3799,48 @@ export declare const Message: Codec<{
3756
3799
  reason: string;
3757
3800
  }, "WorkerErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "WorkerErr::TooManyOpen">>;
3758
3801
  };
3802
+ } | {
3803
+ tag: "host_payment_top_up_status_subscribe_start";
3804
+ value: {
3805
+ tag: "v1";
3806
+ value: Uint8Array<ArrayBufferLike>;
3807
+ };
3808
+ } | {
3809
+ tag: "host_payment_top_up_status_subscribe_receive";
3810
+ value: {
3811
+ tag: "v1";
3812
+ value: {
3813
+ tag: "Detecting";
3814
+ value: undefined;
3815
+ } | {
3816
+ tag: "Claiming";
3817
+ value: undefined;
3818
+ } | {
3819
+ tag: "Claimed";
3820
+ value: {
3821
+ finalized: boolean;
3822
+ };
3823
+ } | {
3824
+ tag: "ClaimedPartially";
3825
+ value: {
3826
+ actualClaimed: bigint;
3827
+ };
3828
+ } | {
3829
+ tag: "NotClaimed";
3830
+ value: undefined;
3831
+ };
3832
+ };
3833
+ } | {
3834
+ tag: "host_payment_top_up_status_subscribe_interrupt";
3835
+ value: {
3836
+ tag: "v1";
3837
+ value: import("@novasamatech/scale").CodecError<{
3838
+ reason: string;
3839
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">;
3840
+ };
3841
+ } | {
3842
+ tag: "host_payment_top_up_status_subscribe_stop";
3843
+ value: undefined;
3759
3844
  };
3760
3845
  }>;
3761
3846
  export type MessageAction = MessagePayloadSchema['tag'];
@@ -1,5 +1,7 @@
1
1
  export declare const Sr25519SecretKey: import("@novasamatech/scale").BytesCodec<number>;
2
2
  export declare const PaymentId: import("scale-ts").Codec<string>;
3
+ /** Product-supplied, opaque 32-byte idempotency key for a top up. */
4
+ export declare const PaymentTopUpId: import("@novasamatech/scale").BytesCodec<number>;
3
5
  export declare const CoinPaymentPurseId: import("scale-ts").Codec<number>;
4
6
  export declare const PaymentTopUpSource: import("scale-ts").Codec<{
5
7
  tag: "ProductAccount";
@@ -33,6 +35,26 @@ export declare const PaymentStatus: import("scale-ts").Codec<{
33
35
  tag: "Completed";
34
36
  value: undefined;
35
37
  }>;
38
+ export declare const PaymentTopUpStatus: import("scale-ts").Codec<{
39
+ tag: "Detecting";
40
+ value: undefined;
41
+ } | {
42
+ tag: "Claiming";
43
+ value: undefined;
44
+ } | {
45
+ tag: "Claimed";
46
+ value: {
47
+ finalized: boolean;
48
+ };
49
+ } | {
50
+ tag: "ClaimedPartially";
51
+ value: {
52
+ actualClaimed: bigint;
53
+ };
54
+ } | {
55
+ tag: "NotClaimed";
56
+ value: undefined;
57
+ }>;
36
58
  export declare const PaymentBalanceErr: [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
37
59
  reason: string;
38
60
  }, "PaymentBalanceErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentBalanceErr::PermissionDenied">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
@@ -54,43 +76,49 @@ export declare const PaymentBalanceErr: [import("scale-ts").Encoder<import("@nov
54
76
  reason: string;
55
77
  }, "PaymentBalanceErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentBalanceErr::PermissionDenied">;
56
78
  };
57
- export declare const PartialPaymentErr: import("scale-ts").Codec<{
58
- credited: bigint;
59
- }>;
60
79
  export declare const PaymentTopUpErr: [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
61
80
  reason: string;
62
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
63
- credited: bigint;
64
- }, "PaymentTopUpErr::PartialPayment">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
81
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
65
82
  reason: string;
66
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
67
- credited: bigint;
68
- }, "PaymentTopUpErr::PartialPayment">>] & {
83
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>] & {
69
84
  enc: import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
70
85
  reason: string;
71
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
72
- credited: bigint;
73
- }, "PaymentTopUpErr::PartialPayment">>;
86
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>;
74
87
  dec: import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
75
88
  reason: string;
76
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
77
- credited: bigint;
78
- }, "PaymentTopUpErr::PartialPayment">>;
89
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>;
79
90
  } & {
80
- readonly InsufficientFunds: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpErr::InsufficientFunds">;
81
91
  readonly InvalidSource: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpErr::InvalidSource">;
82
- readonly PartialPayment: import("@novasamatech/scale").ErrCodec<{
83
- credited: bigint;
84
- }, "PaymentTopUpErr::PartialPayment">;
92
+ readonly AlreadyExists: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpErr::AlreadyExists">;
93
+ readonly SourceBusy: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpErr::SourceBusy">;
85
94
  readonly Unknown: import("@novasamatech/scale").ErrCodec<{
86
95
  reason: string;
87
96
  }, "PaymentTopUpErr::Unknown">;
88
97
  } & {
89
98
  [Symbol.hasInstance](v: unknown): v is import("@novasamatech/scale").CodecError<{
90
99
  reason: string;
91
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
92
- credited: bigint;
93
- }, "PaymentTopUpErr::PartialPayment">;
100
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">;
101
+ };
102
+ export declare const PaymentTopUpStatusErr: [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
103
+ reason: string;
104
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
105
+ reason: string;
106
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>] & {
107
+ enc: import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
108
+ reason: string;
109
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
110
+ dec: import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
111
+ reason: string;
112
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
113
+ } & {
114
+ readonly NotFound: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpStatusErr::NotFound">;
115
+ readonly Unknown: import("@novasamatech/scale").ErrCodec<{
116
+ reason: string;
117
+ }, "PaymentTopUpStatusErr::Unknown">;
118
+ } & {
119
+ [Symbol.hasInstance](v: unknown): v is import("@novasamatech/scale").CodecError<{
120
+ reason: string;
121
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">;
94
122
  };
95
123
  export declare const PaymentRequestErr: [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<undefined, "PaymentRequestErr::Rejected"> | import("@novasamatech/scale").CodecError<{
96
124
  reason: string;
@@ -181,12 +209,11 @@ export declare const PaymentTopUpV1_request: import("scale-ts").Codec<{
181
209
  tag: "Coins";
182
210
  value: Uint8Array<ArrayBufferLike>[];
183
211
  };
212
+ id: Uint8Array<ArrayBufferLike>;
184
213
  }>;
185
214
  export declare const PaymentTopUpV1_response: import("scale-ts").Codec<import("../callError.js").CallResultValue<undefined, import("@novasamatech/scale").CodecError<{
186
215
  reason: string;
187
- }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InsufficientFunds"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<{
188
- credited: bigint;
189
- }, "PaymentTopUpErr::PartialPayment">>>;
216
+ }, "PaymentTopUpErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::InvalidSource"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::AlreadyExists"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpErr::SourceBusy">>>;
190
217
  export declare const PaymentRequestV1_request: import("scale-ts").Codec<{
191
218
  from: number | undefined;
192
219
  amount: bigint;
@@ -229,3 +256,45 @@ export declare const PaymentStatusSubscribeV1_interrupt: [import("scale-ts").Enc
229
256
  reason: string;
230
257
  }, "PaymentStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentStatusErr::PaymentNotFound">;
231
258
  };
259
+ export declare const PaymentTopUpStatusSubscribeV1_start: import("@novasamatech/scale").BytesCodec<number>;
260
+ export declare const PaymentTopUpStatusSubscribeV1_receive: import("scale-ts").Codec<{
261
+ tag: "Detecting";
262
+ value: undefined;
263
+ } | {
264
+ tag: "Claiming";
265
+ value: undefined;
266
+ } | {
267
+ tag: "Claimed";
268
+ value: {
269
+ finalized: boolean;
270
+ };
271
+ } | {
272
+ tag: "ClaimedPartially";
273
+ value: {
274
+ actualClaimed: bigint;
275
+ };
276
+ } | {
277
+ tag: "NotClaimed";
278
+ value: undefined;
279
+ }>;
280
+ export declare const PaymentTopUpStatusSubscribeV1_interrupt: [import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
281
+ reason: string;
282
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>, import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
283
+ reason: string;
284
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>] & {
285
+ enc: import("scale-ts").Encoder<import("@novasamatech/scale").CodecError<{
286
+ reason: string;
287
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
288
+ dec: import("scale-ts").Decoder<import("@novasamatech/scale").CodecError<{
289
+ reason: string;
290
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">>;
291
+ } & {
292
+ readonly NotFound: import("@novasamatech/scale").ErrCodec<undefined, "PaymentTopUpStatusErr::NotFound">;
293
+ readonly Unknown: import("@novasamatech/scale").ErrCodec<{
294
+ reason: string;
295
+ }, "PaymentTopUpStatusErr::Unknown">;
296
+ } & {
297
+ [Symbol.hasInstance](v: unknown): v is import("@novasamatech/scale").CodecError<{
298
+ reason: string;
299
+ }, "PaymentTopUpStatusErr::Unknown"> | import("@novasamatech/scale").CodecError<undefined, "PaymentTopUpStatusErr::NotFound">;
300
+ };
@@ -1,11 +1,13 @@
1
1
  import { Bytes, Enum, ErrEnum } from '@novasamatech/scale';
2
- import { Option, Struct, Vector, _void, str, u128, u32 } from 'scale-ts';
2
+ import { Option, Struct, Vector, _void, bool, str, u128, u32 } from 'scale-ts';
3
3
  import { CallResult } from '../callError.js';
4
4
  import { GenericErr } from '../commonCodecs.js';
5
5
  import { DerivationIndex } from './accounts.js';
6
6
  // common types
7
7
  export const Sr25519SecretKey = Bytes(64);
8
8
  export const PaymentId = str;
9
+ /** Product-supplied, opaque 32-byte idempotency key for a top up. */
10
+ export const PaymentTopUpId = Bytes(32);
9
11
  // Optional purse selector (RFC 0017). `undefined` (None) targets MAIN_PURSE.
10
12
  export const CoinPaymentPurseId = u32;
11
13
  export const PaymentTopUpSource = Enum({
@@ -25,19 +27,40 @@ export const PaymentStatus = Enum({
25
27
  Completed: _void,
26
28
  Failed: str,
27
29
  });
30
+ // A status never advances once it reaches a terminal variant (`Claimed` with
31
+ // `finalized`, `ClaimedPartially`, `NotClaimed`); the host takes no further
32
+ // action and keeps the status readable indefinitely.
33
+ export const PaymentTopUpStatus = Enum({
34
+ // Waiting for the requested amount to appear on the source.
35
+ Detecting: _void,
36
+ Claiming: _void,
37
+ // At least the requested amount was claimed. Terminal once `finalized`.
38
+ Claimed: Struct({ finalized: bool }),
39
+ // Terminal: the host could only claim `actualClaimed`, less than requested.
40
+ // Usually means the source never held enough balance to cover the request —
41
+ // e.g. a re-orged pre-funding transfer. Requesting an amount below the
42
+ // smallest coinage denomination lands here too: the host claims
43
+ // `amount - amount % 2^min_coinage_exponent`.
44
+ ClaimedPartially: Struct({ actualClaimed: u128 }),
45
+ // Terminal: nothing could be claimed, most likely because the host never
46
+ // observed any balance at the source.
47
+ NotClaimed: _void,
48
+ });
28
49
  // errors
29
50
  export const PaymentBalanceErr = ErrEnum('PaymentBalanceErr', {
30
51
  PermissionDenied: [_void, 'permission denied'],
31
52
  Unknown: [GenericErr, 'unknown error'],
32
53
  });
33
- // Carries the amount that was credited when only some coins could be claimed.
34
- export const PartialPaymentErr = Struct({
35
- credited: u128,
36
- });
37
54
  export const PaymentTopUpErr = ErrEnum('PaymentTopUpErr', {
38
- InsufficientFunds: [_void, 'insufficient funds'],
39
55
  InvalidSource: [_void, 'invalid source'],
40
- PartialPayment: [PartialPaymentErr, ({ credited }) => `partial payment: credited ${credited}`],
56
+ AlreadyExists: [_void, 'a top up with this id already exists'],
57
+ // Only one top up can be live per source; the source frees up once the
58
+ // previous top up reaches a terminal status.
59
+ SourceBusy: [_void, 'source already has a top up in progress'],
60
+ Unknown: [GenericErr, 'unknown error'],
61
+ });
62
+ export const PaymentTopUpStatusErr = ErrEnum('PaymentTopUpStatusErr', {
63
+ NotFound: [_void, 'top up not found'],
41
64
  Unknown: [GenericErr, 'unknown error'],
42
65
  });
43
66
  export const PaymentRequestErr = ErrEnum('PaymentRequestErr', {
@@ -60,6 +83,7 @@ export const PaymentTopUpV1_request = Struct({
60
83
  into: Option(CoinPaymentPurseId),
61
84
  amount: u128,
62
85
  source: PaymentTopUpSource,
86
+ id: PaymentTopUpId,
63
87
  });
64
88
  export const PaymentTopUpV1_response = CallResult(_void, PaymentTopUpErr);
65
89
  // host_payment_request
@@ -73,3 +97,7 @@ export const PaymentRequestV1_response = CallResult(PaymentReceipt, PaymentReque
73
97
  export const PaymentStatusSubscribeV1_start = PaymentId;
74
98
  export const PaymentStatusSubscribeV1_receive = PaymentStatus;
75
99
  export const PaymentStatusSubscribeV1_interrupt = PaymentStatusErr;
100
+ // host_payment_top_up_status_subscribe
101
+ export const PaymentTopUpStatusSubscribeV1_start = PaymentTopUpId;
102
+ export const PaymentTopUpStatusSubscribeV1_receive = PaymentTopUpStatus;
103
+ export const PaymentTopUpStatusSubscribeV1_interrupt = PaymentTopUpStatusErr;
package/dist/transport.js CHANGED
@@ -80,6 +80,56 @@ export function createTransport(provider) {
80
80
  const messageProvider = createMessageProvider(provider);
81
81
  // subscriptions management (multiplexing)
82
82
  const activeSubscriptions = new Map();
83
+ // Wires a real subscription on the transport; later subscribers with the same
84
+ // start payload join it through `listeners` instead of opening another. The
85
+ // first listener is registered before `start` is posted: an in-process host
86
+ // may send its first value synchronously while handling it.
87
+ function openSubscription(method, subscriptionKey, startPayload, listener) {
88
+ const requestId = createRequestId();
89
+ const stopAction = composeAction(method, 'stop');
90
+ const interruptAction = composeAction(method, 'interrupt');
91
+ const receiveAction = composeAction(method, 'receive');
92
+ const unsubscribeReceive = transport.listenMessages(receiveAction, (receivedId, data) => {
93
+ if (receivedId === requestId) {
94
+ for (const listener of subscription.listeners) {
95
+ try {
96
+ listener.call(data.value);
97
+ }
98
+ catch (e) {
99
+ provider.logger.error(`subscription "${method}" listener threw`, e);
100
+ }
101
+ }
102
+ }
103
+ });
104
+ const unsubscribeInterrupt = transport.listenMessages(interruptAction, (receivedId, data) => {
105
+ if (receivedId === requestId) {
106
+ // The host has dropped this subscription. Tear it down first so a
107
+ // re-subscribe with the same payload, even from inside onInterrupt,
108
+ // opens a fresh one instead of joining this dead entry.
109
+ stopSubscription();
110
+ subscription.latchedInterrupt = { payload: data.value };
111
+ subscription.interruptEvents.emit('interrupt', data.value);
112
+ }
113
+ });
114
+ const stopSubscription = () => {
115
+ activeSubscriptions.delete(subscriptionKey);
116
+ unsubscribeReceive();
117
+ unsubscribeInterrupt();
118
+ };
119
+ const subscription = {
120
+ requestId,
121
+ listeners: [listener],
122
+ interruptEvents: createNanoEvents(),
123
+ kill: () => {
124
+ stopSubscription();
125
+ const stopPayload = enumValue(stopAction, undefined);
126
+ transport.postMessage(requestId, stopPayload);
127
+ },
128
+ };
129
+ activeSubscriptions.set(subscriptionKey, subscription);
130
+ transport.postMessage(requestId, startPayload);
131
+ return subscription;
132
+ }
83
133
  // Lazy provider subscription — zero per-message decode cost while no
84
134
  // debug listener is attached.
85
135
  let debugListenerCount = 0;
@@ -208,17 +258,14 @@ export function createTransport(provider) {
208
258
  },
209
259
  subscribe(method, payload, callback) {
210
260
  checks();
211
- const events = createNanoEvents();
212
261
  const startAction = composeAction(method, 'start');
213
262
  const startPayload = enumValue(startAction, payload);
214
263
  const subscriptionKey = getSubscriptionKey(method, startPayload);
215
- let subscription = activeSubscriptions.get(subscriptionKey);
216
264
  function unsubscribeListener() {
217
265
  const subscription = activeSubscriptions.get(subscriptionKey);
218
266
  if (subscription) {
219
267
  const newListeners = subscription.listeners.filter(listener => listener.call !== callback);
220
268
  if (newListeners.length === 0) {
221
- activeSubscriptions.delete(subscriptionKey);
222
269
  subscription.kill();
223
270
  }
224
271
  else {
@@ -230,61 +277,21 @@ export function createTransport(provider) {
230
277
  call: callback,
231
278
  unsubscribe: unsubscribeListener,
232
279
  };
233
- const publicSubscription = {
280
+ const existing = activeSubscriptions.get(subscriptionKey);
281
+ existing?.listeners.push(listener);
282
+ const subscription = existing ?? openSubscription(method, subscriptionKey, startPayload, listener);
283
+ return {
234
284
  unsubscribe: unsubscribeListener,
235
285
  onInterrupt(callback) {
236
- return events.on('interrupt', callback);
286
+ if (subscription.latchedInterrupt) {
287
+ callback(subscription.latchedInterrupt.payload);
288
+ return () => {
289
+ /* already delivered */
290
+ };
291
+ }
292
+ return subscription.interruptEvents.on('interrupt', callback);
237
293
  },
238
294
  };
239
- // wiring up a real subscription
240
- if (!subscription) {
241
- const requestId = createRequestId();
242
- const stopAction = composeAction(method, 'stop');
243
- const interruptAction = composeAction(method, 'interrupt');
244
- const receiveAction = composeAction(method, 'receive');
245
- const unsubscribeReceive = transport.listenMessages(receiveAction, (receivedId, data) => {
246
- if (receivedId === requestId) {
247
- const subscription = activeSubscriptions.get(subscriptionKey);
248
- if (subscription) {
249
- for (const listener of subscription.listeners) {
250
- try {
251
- listener.call(data.value);
252
- }
253
- catch (e) {
254
- provider.logger.error(`subscription "${method}" listener threw`, e);
255
- }
256
- }
257
- }
258
- }
259
- });
260
- const unsubscribeInterrupt = transport.listenMessages(interruptAction, (receivedId, data) => {
261
- if (receivedId === requestId) {
262
- events.emit('interrupt', data.value);
263
- stopSubscription();
264
- }
265
- });
266
- const stopSubscription = () => {
267
- unsubscribeReceive();
268
- unsubscribeInterrupt();
269
- events.events = {};
270
- };
271
- // creating subscription
272
- subscription = {
273
- requestId,
274
- kill: () => {
275
- stopSubscription();
276
- const stopPayload = enumValue(stopAction, undefined);
277
- transport.postMessage(requestId, stopPayload);
278
- },
279
- listeners: [listener],
280
- };
281
- activeSubscriptions.set(subscriptionKey, subscription);
282
- transport.postMessage(requestId, startPayload);
283
- }
284
- else {
285
- subscription.listeners.push(listener);
286
- }
287
- return publicSubscription;
288
295
  },
289
296
  handleSubscription(method, handler) {
290
297
  checks();
@@ -302,8 +309,12 @@ export function createTransport(provider) {
302
309
  transport.postMessage(requestId, receivePayload);
303
310
  }, value => {
304
311
  interrupted = true;
312
+ // Undefined while the handler is still running; then the handler's
313
+ // own cleanup, which must run since no `stop` will ever arrive.
314
+ const cleanup = subscriptions.get(requestId);
305
315
  subscriptions.delete(requestId);
306
316
  transport.postMessage(requestId, enumValue(interruptAction, value));
317
+ cleanup?.();
307
318
  });
308
319
  if (interrupted) {
309
320
  unsubscribe();
@@ -57,6 +57,134 @@ describe('transport', () => {
57
57
  expect(s1Handler).toHaveBeenCalledTimes(1);
58
58
  expect(s2Handler).toHaveBeenCalledTimes(2);
59
59
  });
60
+ it('starts a fresh host subscription when re-subscribing with the same payload after an interrupt', () => {
61
+ const providers = createProviders();
62
+ const events = createNanoEvents();
63
+ const host = createTransport(providers.host);
64
+ const sdk = createTransport(providers.sdk);
65
+ const containerHandler = vi.fn((_, send, interrupt) => {
66
+ const unsubPush = events.on('push', () => send({ tag: 'v1', value: 'connected' }));
67
+ const unsubInterrupt = events.on('interrupt', () => interrupt({ tag: 'v1', value: undefined }));
68
+ return () => {
69
+ unsubPush();
70
+ unsubInterrupt();
71
+ };
72
+ });
73
+ host.handleSubscription('host_account_connection_status_subscribe', containerHandler);
74
+ const first = vi.fn();
75
+ sdk.subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, first);
76
+ expect(containerHandler).toHaveBeenCalledTimes(1);
77
+ events.emit('interrupt');
78
+ const second = vi.fn();
79
+ sdk.subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, second);
80
+ // The interrupted subscription is gone: the second subscribe must open a
81
+ // new one on the host rather than joining the dead entry.
82
+ expect(containerHandler).toHaveBeenCalledTimes(2);
83
+ events.emit('push');
84
+ expect(first).not.toHaveBeenCalled();
85
+ expect(second).toHaveBeenCalledTimes(1);
86
+ });
87
+ it('delivers an interrupt raised synchronously from the host handler to a later onInterrupt listener', () => {
88
+ const providers = createProviders();
89
+ const host = createTransport(providers.host);
90
+ const sdk = createTransport(providers.sdk);
91
+ host.handleSubscription('host_account_connection_status_subscribe', (_, _send, interrupt) => {
92
+ interrupt({ tag: 'v1', value: undefined });
93
+ return vi.fn();
94
+ });
95
+ const subscription = sdk.subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, vi.fn());
96
+ // With an in-process host the interrupt arrives before subscribe() has
97
+ // returned, so onInterrupt is necessarily attached afterwards.
98
+ const interrupted = vi.fn();
99
+ subscription.onInterrupt(interrupted);
100
+ expect(interrupted).toHaveBeenCalledExactlyOnceWith({ tag: 'v1', value: undefined });
101
+ });
102
+ it('delivers an interrupt to every subscriber sharing a multiplexed subscription', () => {
103
+ const providers = createProviders();
104
+ const events = createNanoEvents();
105
+ const host = createTransport(providers.host);
106
+ const sdk = createTransport(providers.sdk);
107
+ host.handleSubscription('host_account_connection_status_subscribe', (_, _send, interrupt) => events.on('interrupt', () => interrupt({ tag: 'v1', value: undefined })));
108
+ const first = vi.fn();
109
+ const second = vi.fn();
110
+ sdk
111
+ .subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, vi.fn())
112
+ .onInterrupt(first);
113
+ sdk
114
+ .subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, vi.fn())
115
+ .onInterrupt(second);
116
+ events.emit('interrupt');
117
+ expect(first).toHaveBeenCalledExactlyOnceWith({ tag: 'v1', value: undefined });
118
+ expect(second).toHaveBeenCalledExactlyOnceWith({ tag: 'v1', value: undefined });
119
+ });
120
+ it('delivers a later interrupt to a listener attached before it', () => {
121
+ const providers = createProviders();
122
+ const events = createNanoEvents();
123
+ const host = createTransport(providers.host);
124
+ const sdk = createTransport(providers.sdk);
125
+ host.handleSubscription('host_account_connection_status_subscribe', (_, _send, interrupt) => events.on('interrupt', () => interrupt({ tag: 'v1', value: undefined })));
126
+ const interrupted = vi.fn();
127
+ sdk
128
+ .subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, vi.fn())
129
+ .onInterrupt(interrupted);
130
+ events.emit('interrupt');
131
+ expect(interrupted).toHaveBeenCalledExactlyOnceWith({ tag: 'v1', value: undefined });
132
+ });
133
+ it('drops receive frames and makes unsubscribe a no-op once interrupted', () => {
134
+ const providers = createProviders();
135
+ const events = createNanoEvents();
136
+ const host = createTransport(providers.host);
137
+ const sdk = createTransport(providers.sdk);
138
+ const hostCleanup = vi.fn();
139
+ host.handleSubscription('host_account_connection_status_subscribe', (_, send, interrupt) => {
140
+ events.on('interrupt', () => interrupt({ tag: 'v1', value: undefined }));
141
+ events.on('push', () => send({ tag: 'v1', value: 'connected' }));
142
+ return hostCleanup;
143
+ });
144
+ const callback = vi.fn();
145
+ const subscription = sdk.subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, callback);
146
+ events.emit('interrupt');
147
+ // The host-side handler is already torn down by the interrupt itself.
148
+ expect(hostCleanup).toHaveBeenCalledTimes(1);
149
+ events.emit('push');
150
+ expect(callback).not.toHaveBeenCalled();
151
+ // Nothing is left to stop on the host, so no second cleanup.
152
+ subscription.unsubscribe();
153
+ expect(hostCleanup).toHaveBeenCalledTimes(1);
154
+ });
155
+ it('opens a fresh host subscription when re-subscribing from inside onInterrupt', () => {
156
+ const providers = createProviders();
157
+ const events = createNanoEvents();
158
+ const host = createTransport(providers.host);
159
+ const sdk = createTransport(providers.sdk);
160
+ const containerHandler = vi.fn((_, send, interrupt) => {
161
+ events.on('interrupt', () => interrupt({ tag: 'v1', value: undefined }));
162
+ return events.on('push', () => send({ tag: 'v1', value: 'connected' }));
163
+ });
164
+ host.handleSubscription('host_account_connection_status_subscribe', containerHandler);
165
+ const retried = vi.fn();
166
+ sdk
167
+ .subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, vi.fn())
168
+ .onInterrupt(() => {
169
+ sdk.subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, retried);
170
+ });
171
+ events.emit('interrupt');
172
+ expect(containerHandler).toHaveBeenCalledTimes(2);
173
+ events.emit('push');
174
+ expect(retried).toHaveBeenCalledTimes(1);
175
+ });
176
+ it('delivers a value the host sends synchronously while handling start', () => {
177
+ const providers = createProviders();
178
+ const host = createTransport(providers.host);
179
+ const sdk = createTransport(providers.sdk);
180
+ host.handleSubscription('host_account_connection_status_subscribe', (_, send) => {
181
+ send({ tag: 'v1', value: 'connected' });
182
+ return vi.fn();
183
+ });
184
+ const callback = vi.fn();
185
+ sdk.subscribe('host_account_connection_status_subscribe', { tag: 'v1', value: undefined }, callback);
186
+ expect(callback).toHaveBeenCalledExactlyOnceWith({ tag: 'v1', value: 'connected' });
187
+ });
60
188
  it('isolates a throwing subscriber so siblings on the same subscription still receive', () => {
61
189
  const providers = createProviders();
62
190
  const events = createNanoEvents();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/host-api",
3
3
  "type": "module",
4
- "version": "0.10.1",
4
+ "version": "0.11.0",
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.10.1",
25
+ "@novasamatech/scale": "0.11.0",
26
26
  "nanoevents": "10.0.0",
27
27
  "nanoid": "6.0.1",
28
28
  "neverthrow": "^8.2.0",
@@ -1 +0,0 @@
1
- export {};
@@ -1,29 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { hostApiProtocol } from './impl.js';
3
- import { MessagePayload } from './messageCodec.js';
4
- describe('MessagePayload ABI', () => {
5
- it('declares an explicit base index per method matching contiguous serialization order', () => {
6
- let next = 0;
7
- for (const payload of Object.values(hostApiProtocol)) {
8
- expect(payload.index).toBe(next);
9
- // requests expand to 2 actions (request/response),
10
- // subscriptions to 4 (start/stop/interrupt/receive)
11
- next += payload.method === 'subscribe' ? 4 : 2;
12
- }
13
- });
14
- it('encodes the first action with leading index byte 0', () => {
15
- const bytes = MessagePayload.enc({ tag: 'host_handshake_request', value: { tag: 'v1', value: 1 } });
16
- expect(bytes[0]).toBe(0);
17
- });
18
- it('encodes the last action with its explicit index byte', () => {
19
- const bytes = MessagePayload.enc({
20
- tag: 'host_push_notification_cancel_response',
21
- value: { tag: 'v1', value: { success: true, value: undefined } },
22
- });
23
- expect(bytes[0]).toBe(129);
24
- });
25
- it('round-trips an action through its explicit index', () => {
26
- const value = { tag: 'host_handshake_request', value: { tag: 'v1', value: 7 } };
27
- expect(MessagePayload.dec(MessagePayload.enc(value))).toEqual(value);
28
- });
29
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,37 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Theme, ThemeName, ThemeVariant } from './theme.js';
3
- describe('ThemeName', () => {
4
- it('encodes/decodes the Custom variant with its string payload', () => {
5
- const value = { tag: 'Custom', value: 'midnight' };
6
- expect(ThemeName.dec(ThemeName.enc(value))).toEqual(value);
7
- });
8
- it('encodes/decodes the Default variant', () => {
9
- const value = { tag: 'Default', value: undefined };
10
- expect(ThemeName.dec(ThemeName.enc(value))).toEqual(value);
11
- });
12
- it('pins the wire indexes: Custom=0, Default=1', () => {
13
- // Custom('x'): tag byte 0, then SCALE compact-prefixed string "x"
14
- expect(ThemeName.enc({ tag: 'Custom', value: 'x' })).toEqual(new Uint8Array([0, 4, 120]));
15
- expect(ThemeName.enc({ tag: 'Default', value: undefined })).toEqual(new Uint8Array([1]));
16
- });
17
- });
18
- describe('ThemeVariant', () => {
19
- it('encodes/decodes Light and Dark', () => {
20
- expect(ThemeVariant.enc('Light')).toEqual(new Uint8Array([0]));
21
- expect(ThemeVariant.enc('Dark')).toEqual(new Uint8Array([1]));
22
- expect(ThemeVariant.dec(new Uint8Array([0]))).toEqual('Light');
23
- expect(ThemeVariant.dec(new Uint8Array([1]))).toEqual('Dark');
24
- });
25
- });
26
- describe('Theme', () => {
27
- it('round-trips a Default/Light theme', () => {
28
- const value = { name: { tag: 'Default', value: undefined }, variant: 'Light' };
29
- expect(Theme.dec(Theme.enc(value))).toEqual(value);
30
- });
31
- it('round-trips a Custom/Dark theme and pins its byte layout', () => {
32
- const value = { name: { tag: 'Custom', value: 'dark' }, variant: 'Dark' };
33
- // name: Custom(0) + compact(4)=0x10 + "dark"; variant: Dark(1)
34
- expect(Theme.enc(value)).toEqual(new Uint8Array([0, 16, 100, 97, 114, 107, 1]));
35
- expect(Theme.dec(Theme.enc(value))).toEqual(value);
36
- });
37
- });