@parity/product-sdk-host 0.16.0 → 0.17.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.
@@ -1,12 +1,14 @@
1
1
  import { TrUApiClient, ChainIdentifier, HexString } from '@parity/truapi';
2
+ import { ConnectionStatus } from '@parity/truapi/sandbox';
2
3
 
3
4
  /**
4
5
  * Access to the in-house TruAPI client (`@parity/truapi`) for the host package.
5
6
  *
6
- * Environment detection and the lazily-built, cached client come from
7
- * `@parity/truapi/sandbox`; this module layers the product-sdk-specific glue on
8
- * top — an async {@link getClient} accessor and {@link subscribeWithInterrupt},
9
- * which adapts a truapi stream into the host's {@link HostSubscription} shape.
7
+ * Environment detection, the lazily-built cached client, and the connection-status
8
+ * signal come from `@parity/truapi/sandbox`; this module layers the
9
+ * product-sdk-specific glue on top — an async {@link getClient} accessor,
10
+ * {@link subscribeConnectionStatus}, and {@link subscribeWithInterrupt}, which
11
+ * adapts a truapi stream into the host's {@link HostSubscription} shape.
10
12
  *
11
13
  * @module
12
14
  */
@@ -20,6 +22,9 @@ import { TrUApiClient, ChainIdentifier, HexString } from '@parity/truapi';
20
22
  * Calling this in a production build silently reroutes every host accessor to
21
23
  * the injected client, so we warn — it almost always means a `/testing` import
22
24
  * leaked into a production path.
25
+ *
26
+ * Injecting or clearing notifies {@link subscribeConnectionStatus} subscribers,
27
+ * so a product's "host lost" path can be exercised by disposing the fake host.
23
28
  */
24
29
  declare function setTruApiClient(client: TrUApiClient | null): void;
25
30
  /**
@@ -27,6 +32,38 @@ declare function setTruApiClient(client: TrUApiClient | null): void;
27
32
  * sandbox heuristic (iframe / webview marker / injected message port).
28
33
  */
29
34
  declare function isCorrectEnvironment(): boolean;
35
+ /**
36
+ * Connection lifecycle of the host channel: `"connecting"` while the client waits
37
+ * for the host, `"connected"` once the channel is established, `"disconnected"`
38
+ * outside a host container or after the channel closes.
39
+ *
40
+ * Not the same concept as `@parity/product-sdk-signer`'s identically-shaped
41
+ * `ConnectionStatus`, which tracks a signer provider rather than the transport.
42
+ */
43
+ type HostConnectionStatus = ConnectionStatus;
44
+ /**
45
+ * Test-only: push `status` to every {@link subscribeConnectionStatus} subscriber,
46
+ * so a product can exercise its reconnecting / offline UI. The host-side
47
+ * counterpart of `@parity/product-sdk-signer`'s `FakeSignerProvider.emitStatus`.
48
+ * Exposed through `@parity/product-sdk-host/testing`, not the main entry.
49
+ */
50
+ declare function emitConnectionStatus(status: HostConnectionStatus): void;
51
+ /**
52
+ * Subscribe to host-channel connection status. The callback fires synchronously
53
+ * with the current status and again on every change; the returned function
54
+ * unsubscribes. Repeats of the status you already have are suppressed.
55
+ *
56
+ * This is the **transport** channel. For the host's account-level connection —
57
+ * what drives `@parity/product-sdk-signer`'s `ConnectionStatus` — use
58
+ * `AccountsProvider.subscribeAccountConnectionStatus` instead.
59
+ *
60
+ * Subscribing is not passive: outside an established channel the first subscribe
61
+ * builds the client and provider, so this can be what constructs the transport.
62
+ *
63
+ * Honours the `setTruApiClient` seam — an injected client is connected by
64
+ * definition, and injecting or clearing one notifies live subscribers.
65
+ */
66
+ declare function subscribeConnectionStatus(callback: (status: HostConnectionStatus) => void): () => void;
30
67
 
31
68
  /**
32
69
  * Host chain discovery. Resolves chain roles to genesis hashes against the
@@ -67,4 +104,4 @@ interface HostChainDiscovery {
67
104
  */
68
105
  declare function getHostChainInfo(identifiers: readonly HostChainIdentifier[]): Promise<HostChainDiscovery | null>;
69
106
 
70
- export { type HostChainDiscovery as H, type HostChainIdentifier as a, getHostChainInfo as g, isCorrectEnvironment as i, setTruApiClient as s };
107
+ export { type HostChainDiscovery as H, type HostChainIdentifier as a, type HostConnectionStatus as b, setTruApiClient as c, emitConnectionStatus as e, getHostChainInfo as g, isCorrectEnvironment as i, subscribeConnectionStatus as s };
@@ -0,0 +1,89 @@
1
+ import { isCorrectEnvironment as isCorrectEnvironment$1, subscribeConnectionStatus as subscribeConnectionStatus$1, getClientSync as getClientSync$1 } from '@parity/truapi/sandbox';
2
+
3
+ // src/transport.ts
4
+ var clientOverride = null;
5
+ var localStatusListeners = /* @__PURE__ */ new Set();
6
+ function isProductionBuild() {
7
+ try {
8
+ return process.env.NODE_ENV === "production";
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ function setTruApiClient(client) {
14
+ if (client !== null && isProductionBuild()) {
15
+ console.warn(
16
+ "[product-sdk] setTruApiClient() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will silently reroute all host access to the injected client."
17
+ );
18
+ }
19
+ const wasOverridden = clientOverride !== null;
20
+ clientOverride = client;
21
+ if (wasOverridden !== (client !== null)) {
22
+ notifyLocalStatusListeners(client !== null ? "connected" : "disconnected");
23
+ }
24
+ }
25
+ function getClientSync() {
26
+ return clientOverride ?? getClientSync$1();
27
+ }
28
+ function isCorrectEnvironment() {
29
+ return clientOverride !== null || isCorrectEnvironment$1();
30
+ }
31
+ async function getClient() {
32
+ return getClientSync();
33
+ }
34
+ function latchDisconnected(previous, next) {
35
+ return next === "connecting" && previous === "disconnected" ? "disconnected" : next;
36
+ }
37
+ function notifyLocalStatusListeners(status) {
38
+ for (const listener of [...localStatusListeners]) listener(status);
39
+ }
40
+ function emitConnectionStatus(status) {
41
+ if (isProductionBuild()) {
42
+ console.warn(
43
+ "[product-sdk] emitConnectionStatus() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will report a fabricated connection status to real subscribers."
44
+ );
45
+ }
46
+ notifyLocalStatusListeners(status);
47
+ }
48
+ function subscribeConnectionStatus(callback) {
49
+ let last = null;
50
+ const deliver = (status, fromSandbox) => {
51
+ const next = fromSandbox ? latchDisconnected(last, status) : status;
52
+ if (next === last) return;
53
+ last = next;
54
+ callback(next);
55
+ };
56
+ const onLocal = (status) => deliver(status, false);
57
+ localStatusListeners.add(onLocal);
58
+ if (clientOverride !== null) {
59
+ onLocal("connected");
60
+ return () => void localStatusListeners.delete(onLocal);
61
+ }
62
+ const unsubscribeSandbox = subscribeConnectionStatus$1((status) => deliver(status, true));
63
+ return () => {
64
+ localStatusListeners.delete(onLocal);
65
+ unsubscribeSandbox();
66
+ };
67
+ }
68
+ function subscribeWithInterrupt(observable, onNext) {
69
+ let interruptCallback;
70
+ const sub = observable.subscribe({
71
+ next: onNext,
72
+ error: (reason) => interruptCallback?.(reason),
73
+ complete: () => interruptCallback?.()
74
+ });
75
+ return {
76
+ subscriptionId: sub.subscriptionId,
77
+ unsubscribe: () => sub.unsubscribe(),
78
+ onInterrupt: (callback) => {
79
+ interruptCallback = callback;
80
+ return () => {
81
+ if (interruptCallback === callback) interruptCallback = void 0;
82
+ };
83
+ }
84
+ };
85
+ }
86
+
87
+ export { emitConnectionStatus, getClient, isCorrectEnvironment, setTruApiClient, subscribeConnectionStatus, subscribeWithInterrupt };
88
+ //# sourceMappingURL=chunk-PAKEUP2Q.js.map
89
+ //# sourceMappingURL=chunk-PAKEUP2Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport.ts"],"names":["sandboxGetClientSync","sandboxIsCorrectEnvironment","sandboxSubscribeConnectionStatus"],"mappings":"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","file":"chunk-PAKEUP2Q.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Access to the in-house TruAPI client (`@parity/truapi`) for the host package.\n *\n * Environment detection, the lazily-built cached client, and the connection-status\n * signal come from `@parity/truapi/sandbox`; this module layers the\n * product-sdk-specific glue on top — an async {@link getClient} accessor,\n * {@link subscribeConnectionStatus}, and {@link subscribeWithInterrupt}, which\n * adapts a truapi stream into the host's {@link HostSubscription} shape.\n *\n * @module\n */\n\nimport type { ObservableLike, TrUApiClient } from \"@parity/truapi\";\nimport {\n type ConnectionStatus,\n getClientSync as sandboxGetClientSync,\n isCorrectEnvironment as sandboxIsCorrectEnvironment,\n subscribeConnectionStatus as sandboxSubscribeConnectionStatus,\n} from \"@parity/truapi/sandbox\";\n\nimport type { HostSubscription } from \"./types.js\";\n\n/** A {@link HostSubscription} carrying the transport-assigned subscription id. */\nexport interface TransportSubscription extends HostSubscription {\n readonly subscriptionId: string;\n}\n\n// Test-only override. When set — via `setTruApiClient`, exposed through\n// `@parity/product-sdk-host/testing` — every host accessor resolves this client\n// instead of the sandbox one. `null` in production, so the branches below are\n// no-ops there.\nlet clientOverride: TrUApiClient | null = null;\n\n// Status subscribers registered here rather than only in the sandbox, so that\n// flipping the test seam is an *event*. The sandbox tracks only the client it\n// built itself, so it cannot know an injected client appeared or went away.\nconst localStatusListeners = new Set<(status: HostConnectionStatus) => void>();\n\nfunction isProductionBuild(): boolean {\n try {\n // Must stay a plain `process.env.NODE_ENV` member expression: bundlers\n // (Vite, esbuild, webpack) substitute it textually, which is how this\n // check works in browser builds where `process` doesn't exist.\n return process.env.NODE_ENV === \"production\";\n } catch {\n // No `process` and no bundler define — can't tell, stay quiet.\n return false;\n }\n}\n\n/**\n * Test-only seam: force {@link getClient} / {@link getClientSync} to return\n * `client`, and {@link isCorrectEnvironment} to report `true`. Pass `null` to\n * restore normal detection. Exposed through `@parity/product-sdk-host/testing`,\n * not the package's main entry.\n *\n * Calling this in a production build silently reroutes every host accessor to\n * the injected client, so we warn — it almost always means a `/testing` import\n * leaked into a production path.\n *\n * Injecting or clearing notifies {@link subscribeConnectionStatus} subscribers,\n * so a product's \"host lost\" path can be exercised by disposing the fake host.\n */\nexport function setTruApiClient(client: TrUApiClient | null): void {\n if (client !== null && isProductionBuild()) {\n console.warn(\n \"[product-sdk] setTruApiClient() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will silently reroute all host access to the injected client.\",\n );\n }\n const wasOverridden = clientOverride !== null;\n clientOverride = client;\n if (wasOverridden !== (client !== null)) {\n notifyLocalStatusListeners(client !== null ? \"connected\" : \"disconnected\");\n }\n}\n\n/**\n * Synchronous TruAPI client accessor. Returns the injected test client when one\n * is set, otherwise the sandbox client (`null` outside a host container).\n */\nexport function getClientSync(): TrUApiClient | null {\n return clientOverride ?? sandboxGetClientSync();\n}\n\n/**\n * Host-container detection. `true` when a test client is injected, otherwise the\n * sandbox heuristic (iframe / webview marker / injected message port).\n */\nexport function isCorrectEnvironment(): boolean {\n return clientOverride !== null || sandboxIsCorrectEnvironment();\n}\n\n/**\n * Get the TruAPI client. Returns `null` outside a host container. Async wrapper\n * over {@link getClientSync} for the host wrappers that already `await` it.\n */\nexport async function getClient(): Promise<TrUApiClient | null> {\n return getClientSync();\n}\n\n/**\n * Connection lifecycle of the host channel: `\"connecting\"` while the client waits\n * for the host, `\"connected\"` once the channel is established, `\"disconnected\"`\n * outside a host container or after the channel closes.\n *\n * Not the same concept as `@parity/product-sdk-signer`'s identically-shaped\n * `ConnectionStatus`, which tracks a signer provider rather than the transport.\n */\nexport type HostConnectionStatus = ConnectionStatus;\n\n/**\n * Correct one defect in the sandbox's status signal: `@parity/truapi` never clears\n * its cached client when the pipe closes, so a subscriber arriving after a\n * disconnect re-derives `\"connecting\"` from the dead client — and because the\n * sandbox fans every change out to all listeners, that rewrites everyone's state\n * with no way back. Hold `\"disconnected\"` until a real `\"connected\"` arrives.\n *\n * Applies to sandbox-sourced statuses only. A status pushed by the test seam is\n * deliberate and passes through, so a fake host can still drive a reconnect.\n *\n * Outstanding upstream, not tied to the version we happen to be on: `sandbox.js`\n * is byte-identical from 0.7.0 through 0.9.0 (npm latest) and still unfixed on\n * `paritytech/host-rust-core` main, the repo formerly named truapi. Remove once\n * it clears the cached client on close.\n */\nfunction latchDisconnected(\n previous: HostConnectionStatus | null,\n next: HostConnectionStatus,\n): HostConnectionStatus {\n return next === \"connecting\" && previous === \"disconnected\" ? \"disconnected\" : next;\n}\n\nfunction notifyLocalStatusListeners(status: HostConnectionStatus): void {\n // Iterate a snapshot: a listener that unsubscribes itself, or re-enters\n // `setTruApiClient`, must not mutate the set mid-loop.\n for (const listener of [...localStatusListeners]) listener(status);\n}\n\n/**\n * Test-only: push `status` to every {@link subscribeConnectionStatus} subscriber,\n * so a product can exercise its reconnecting / offline UI. The host-side\n * counterpart of `@parity/product-sdk-signer`'s `FakeSignerProvider.emitStatus`.\n * Exposed through `@parity/product-sdk-host/testing`, not the main entry.\n */\nexport function emitConnectionStatus(status: HostConnectionStatus): void {\n if (isProductionBuild()) {\n console.warn(\n \"[product-sdk] emitConnectionStatus() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will report a fabricated connection status to real subscribers.\",\n );\n }\n notifyLocalStatusListeners(status);\n}\n\n/**\n * Subscribe to host-channel connection status. The callback fires synchronously\n * with the current status and again on every change; the returned function\n * unsubscribes. Repeats of the status you already have are suppressed.\n *\n * This is the **transport** channel. For the host's account-level connection —\n * what drives `@parity/product-sdk-signer`'s `ConnectionStatus` — use\n * `AccountsProvider.subscribeAccountConnectionStatus` instead.\n *\n * Subscribing is not passive: outside an established channel the first subscribe\n * builds the client and provider, so this can be what constructs the transport.\n *\n * Honours the `setTruApiClient` seam — an injected client is connected by\n * definition, and injecting or clearing one notifies live subscribers.\n */\nexport function subscribeConnectionStatus(\n callback: (status: HostConnectionStatus) => void,\n): () => void {\n let last: HostConnectionStatus | null = null;\n\n // One wrapped callback for both sources, so `last` stays coherent: the seam\n // and the sandbox must not each keep their own idea of what was delivered.\n const deliver = (status: HostConnectionStatus, fromSandbox: boolean): void => {\n const next = fromSandbox ? latchDisconnected(last, status) : status;\n if (next === last) return;\n last = next;\n callback(next);\n };\n\n const onLocal = (status: HostConnectionStatus) => deliver(status, false);\n localStatusListeners.add(onLocal);\n\n if (clientOverride !== null) {\n onLocal(\"connected\");\n return () => void localStatusListeners.delete(onLocal);\n }\n\n const unsubscribeSandbox = sandboxSubscribeConnectionStatus((status) => deliver(status, true));\n return () => {\n localStatusListeners.delete(onLocal);\n unsubscribeSandbox();\n };\n}\n\n/**\n * Adapt a truapi `ObservableLike` stream into the host's callback-style\n * {@link HostSubscription} (`unsubscribe` + `onInterrupt`). `onNext` fires for\n * each item; the registered `onInterrupt` callback fires when the host ends the\n * subscription server-side — which the generated client surfaces as either\n * `complete` (a host interrupt frame) or `error` (transport close). Shared by\n * the statement-store and preimage adapters, which both expose this shape.\n */\nexport function subscribeWithInterrupt<Item, Reason = never>(\n observable: ObservableLike<Item, Reason>,\n onNext: (item: Item) => void,\n): TransportSubscription {\n let interruptCallback: ((reason?: unknown) => void) | undefined;\n const sub = observable.subscribe({\n next: onNext,\n error: (reason) => interruptCallback?.(reason),\n complete: () => interruptCallback?.(),\n });\n return {\n subscriptionId: sub.subscriptionId,\n unsubscribe: () => sub.unsubscribe(),\n onInterrupt: (callback) => {\n interruptCallback = callback;\n return () => {\n if (interruptCallback === callback) interruptCallback = undefined;\n };\n },\n };\n}\n\nif (import.meta.vitest) {\n const { test, expect, afterEach } = import.meta.vitest;\n\n afterEach(() => setTruApiClient(null));\n\n // Environment detection and client building are covered by `@parity/truapi`'s\n // own sandbox tests; here we only assert the local glue degrades outside a\n // host container.\n test(\"getClientSync returns null outside a container\", () => {\n expect(getClientSync()).toBeNull();\n });\n\n test(\"getClient resolves null outside a container\", async () => {\n expect(await getClient()).toBeNull();\n });\n\n test(\"setTruApiClient overrides the client and container detection\", async () => {\n const fake = {} as TrUApiClient;\n setTruApiClient(fake);\n expect(getClientSync()).toBe(fake);\n expect(await getClient()).toBe(fake);\n expect(isCorrectEnvironment()).toBe(true);\n\n setTruApiClient(null);\n expect(getClientSync()).toBeNull();\n expect(isCorrectEnvironment()).toBe(false);\n });\n\n test(\"setTruApiClient warns when injecting in a production build\", () => {\n const original = process.env.NODE_ENV;\n const warnings: string[] = [];\n const realWarn = console.warn;\n console.warn = (...args: unknown[]) => void warnings.push(String(args[0]));\n try {\n process.env.NODE_ENV = \"production\";\n setTruApiClient({} as TrUApiClient);\n expect(warnings).toHaveLength(1);\n expect(warnings[0]).toContain(\"production build\");\n\n // Clearing the override must not warn.\n setTruApiClient(null);\n expect(warnings).toHaveLength(1);\n } finally {\n console.warn = realWarn;\n process.env.NODE_ENV = original;\n }\n });\n\n test(\"subscribeConnectionStatus reports disconnected outside a container\", () => {\n const statuses: HostConnectionStatus[] = [];\n\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n expect(statuses).toEqual([\"disconnected\"]);\n unsubscribe();\n });\n\n test(\"subscribeConnectionStatus reports connected for an injected client\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n // The sandbox only tracks the client it built itself, so without the\n // override branch this would report \"disconnected\" while every other\n // accessor resolved the injected client.\n expect(statuses).toEqual([\"connected\"]);\n unsubscribe();\n });\n\n test(\"disposing the injected client notifies live subscribers\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n setTruApiClient(null);\n\n expect(statuses).toEqual([\"connected\", \"disconnected\"]);\n unsubscribe();\n });\n\n test(\"injecting a client notifies subscribers that started without one\", () => {\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n setTruApiClient({} as TrUApiClient);\n\n expect(statuses).toEqual([\"disconnected\", \"connected\"]);\n unsubscribe();\n });\n\n test(\"unsubscribe stops seam notifications\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n unsubscribe();\n setTruApiClient(null);\n\n expect(statuses).toEqual([\"connected\"]);\n expect(localStatusListeners.size).toBe(0);\n });\n\n test(\"emitConnectionStatus drives transitions, including a reconnect\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n emitConnectionStatus(\"disconnected\");\n // A seam-pushed \"connecting\" after \"disconnected\" is deliberate, so it must\n // survive the sandbox latch — otherwise no test could drive a reconnect.\n emitConnectionStatus(\"connecting\");\n emitConnectionStatus(\"connected\");\n\n expect(statuses).toEqual([\"connected\", \"disconnected\", \"connecting\", \"connected\"]);\n unsubscribe();\n });\n\n test(\"repeats of the current status are suppressed\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n emitConnectionStatus(\"connected\");\n emitConnectionStatus(\"connected\");\n\n expect(statuses).toEqual([\"connected\"]);\n unsubscribe();\n });\n\n test(\"a listener that unsubscribes itself mid-notification is safe\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const handle: { unsubscribe?: () => void } = {};\n handle.unsubscribe = subscribeConnectionStatus((status) => {\n statuses.push(status);\n handle.unsubscribe?.();\n });\n\n setTruApiClient(null);\n\n expect(statuses).toEqual([\"connected\", \"disconnected\"]);\n expect(localStatusListeners.size).toBe(0);\n });\n\n // The sandbox latch can't be driven through the public surface — it needs a\n // real provider close — so the correction is pinned as a pure function.\n test(\"latchDisconnected holds disconnected through the stale-cache connecting\", () => {\n expect(latchDisconnected(\"disconnected\", \"connecting\")).toBe(\"disconnected\");\n });\n\n test(\"latchDisconnected passes every other transition through\", () => {\n expect(latchDisconnected(null, \"disconnected\")).toBe(\"disconnected\");\n expect(latchDisconnected(null, \"connecting\")).toBe(\"connecting\");\n expect(latchDisconnected(\"connecting\", \"connected\")).toBe(\"connected\");\n expect(latchDisconnected(\"connected\", \"disconnected\")).toBe(\"disconnected\");\n // A genuine reconnect still gets through once the channel re-establishes.\n expect(latchDisconnected(\"connected\", \"connecting\")).toBe(\"connecting\");\n });\n\n test(\"subscribeWithInterrupt preserves the transport subscription id\", () => {\n const observable = {\n subscribe: () => ({\n subscriptionId: \"p:17\",\n unsubscribe: () => {},\n }),\n [Symbol.observable]() {\n return this;\n },\n } as ObservableLike<never>;\n\n const subscription = subscribeWithInterrupt(observable, () => {});\n\n expect(subscription.subscriptionId).toBe(\"p:17\");\n });\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -5,8 +5,9 @@ import { SdkError } from '@parity/product-sdk-errors';
5
5
  export { SdkError, isSdkError } from '@parity/product-sdk-errors';
6
6
  import { Result } from '@parity/result';
7
7
  export { Result, err, ok } from '@parity/result';
8
- export { H as HostChainDiscovery, a as HostChainIdentifier, g as getHostChainInfo, i as isInsideContainerSync } from './chain-discovery-nLrPzb3d.js';
8
+ export { H as HostChainDiscovery, a as HostChainIdentifier, b as HostConnectionStatus, g as getHostChainInfo, i as isInsideContainerSync, s as subscribeConnectionStatus } from './chain-discovery-BEvu7HaV.js';
9
9
  import { ResultAsync as ResultAsync$1 } from 'neverthrow';
10
+ import '@parity/truapi/sandbox';
10
11
 
11
12
  /**
12
13
  * Public types for the host wrappers.
@@ -156,12 +157,14 @@ declare function getStatementStore(): Promise<HostStatementStore | null>;
156
157
  * chain-specific endpoints used by multiple packages.
157
158
  */
158
159
  /**
159
- * Bulletin Chain RPC endpoints per network environment. `paseo` (Paseo Next v2)
160
- * and `devnet` (public Paseo testnet) are populated today; `polkadot` and
161
- * `kusama` are reserved for when those Bulletin deployments go live.
160
+ * Bulletin Chain RPC endpoints per network environment. `paseo` (Paseo Next v2),
161
+ * `previewnet` (zombienet, a step ahead of paseo), and `devnet` (public Paseo
162
+ * testnet) are populated today; `polkadot` and `kusama` are reserved for when
163
+ * those Bulletin deployments go live.
162
164
  */
163
165
  declare const BULLETIN_RPCS: {
164
166
  readonly paseo: readonly ["wss://paseo-bulletin-next-rpc.polkadot.io"];
167
+ readonly previewnet: readonly ["wss://previewnet.substrate.dev/bulletin"];
165
168
  readonly devnet: readonly ["wss://bulletin-paseo.tservices.es:8443"];
166
169
  readonly polkadot: string[];
167
170
  readonly kusama: string[];
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { getClient, isCorrectEnvironment, subscribeWithInterrupt } from './chunk-GDXSV7JV.js';
2
- export { isCorrectEnvironment as isInsideContainerSync } from './chunk-GDXSV7JV.js';
1
+ import { getClient, isCorrectEnvironment, subscribeWithInterrupt } from './chunk-PAKEUP2Q.js';
2
+ export { isCorrectEnvironment as isInsideContainerSync, subscribeConnectionStatus } from './chunk-PAKEUP2Q.js';
3
3
  import { createLogger } from '@parity/product-sdk-logger';
4
4
  import { scale } from '@parity/truapi';
5
5
  import { err, ok } from '@parity/result';
@@ -68,6 +68,23 @@ function isHostError(error) {
68
68
  var log = createLogger("host:papi");
69
69
  var JSON_RPC_INTERNAL_ERROR = -32603;
70
70
  var JSON_RPC_METHOD_NOT_FOUND = -32601;
71
+ function followOperationId(item) {
72
+ switch (item.tag) {
73
+ case "OperationBodyDone":
74
+ case "OperationCallDone":
75
+ case "OperationStorageItems":
76
+ case "OperationStorageDone":
77
+ case "OperationWaitingForContinue":
78
+ case "OperationInaccessible":
79
+ case "OperationError":
80
+ return item.value.operationId;
81
+ default:
82
+ return void 0;
83
+ }
84
+ }
85
+ function isTerminalOperationItem(item) {
86
+ return item.tag === "OperationBodyDone" || item.tag === "OperationCallDone" || item.tag === "OperationStorageDone" || item.tag === "OperationInaccessible" || item.tag === "OperationError";
87
+ }
71
88
  var STORAGE_TYPE_MAP = {
72
89
  value: "Value",
73
90
  hash: "Hash",
@@ -174,6 +191,8 @@ function createHostPapiProvider(client, genesisHash) {
174
191
  return (onMessage) => {
175
192
  const activeFollows = /* @__PURE__ */ new Map();
176
193
  const activeBroadcasts = /* @__PURE__ */ new Set();
194
+ const followOperations = /* @__PURE__ */ new Map();
195
+ const pendingOperationStarts = /* @__PURE__ */ new Map();
177
196
  function sendJsonRpcResponse(id, result) {
178
197
  onMessage({ jsonrpc: "2.0", id, result });
179
198
  }
@@ -187,6 +206,72 @@ function createHostPapiProvider(client, genesisHash) {
187
206
  params: { subscription, result: event }
188
207
  });
189
208
  }
209
+ function forwardFollowItem(followSubscriptionId, item) {
210
+ const operationId = followOperationId(item);
211
+ if (operationId === void 0) {
212
+ sendFollowEvent(followSubscriptionId, convertFollowEventToJsonRpc(item));
213
+ return;
214
+ }
215
+ const operations = followOperations.get(followSubscriptionId);
216
+ if (!operations) return;
217
+ let operation = operations.get(operationId);
218
+ if (!operation) {
219
+ if ((pendingOperationStarts.get(followSubscriptionId) ?? 0) === 0) {
220
+ sendFollowEvent(followSubscriptionId, convertFollowEventToJsonRpc(item));
221
+ return;
222
+ }
223
+ operation = { announced: false, items: [] };
224
+ operations.set(operationId, operation);
225
+ }
226
+ if (!operation.announced) {
227
+ operation.items.push(item);
228
+ return;
229
+ }
230
+ sendFollowEvent(followSubscriptionId, convertFollowEventToJsonRpc(item));
231
+ if (isTerminalOperationItem(item)) {
232
+ operations.delete(operationId);
233
+ }
234
+ }
235
+ function sendOperationStartedResponse(id, followSubscriptionId, result) {
236
+ sendJsonRpcResponse(id, convertOperationResultToJsonRpc(result));
237
+ if (result.tag !== "Started") return;
238
+ const operations = followOperations.get(followSubscriptionId);
239
+ if (!operations) return;
240
+ const operationId = result.value.operationId;
241
+ let operation = operations.get(operationId);
242
+ if (!operation) {
243
+ operation = { announced: true, items: [] };
244
+ operations.set(operationId, operation);
245
+ return;
246
+ }
247
+ operation.announced = true;
248
+ const pendingItems = operation.items;
249
+ operation.items = [];
250
+ for (const item of pendingItems) {
251
+ forwardFollowItem(followSubscriptionId, item);
252
+ }
253
+ }
254
+ function startOperationRequest(id, followSubscriptionId) {
255
+ const pending = pendingOperationStarts.get(followSubscriptionId);
256
+ if (pending !== void 0) {
257
+ pendingOperationStarts.set(followSubscriptionId, pending + 1);
258
+ }
259
+ const settle = () => {
260
+ const outstanding = pendingOperationStarts.get(followSubscriptionId);
261
+ if (outstanding === void 0) return;
262
+ pendingOperationStarts.set(followSubscriptionId, Math.max(0, outstanding - 1));
263
+ };
264
+ return {
265
+ ok: (response) => {
266
+ settle();
267
+ sendOperationStartedResponse(id, followSubscriptionId, response.operation);
268
+ },
269
+ err: (error) => {
270
+ settle();
271
+ hostError(id)(error);
272
+ }
273
+ };
274
+ }
190
275
  const hostError = (id) => (error) => sendJsonRpcError(id, JSON_RPC_INTERNAL_ERROR, formatHostError(error));
191
276
  function handleMessage(message) {
192
277
  const { id, method } = message;
@@ -199,8 +284,10 @@ function createHostPapiProvider(client, genesisHash) {
199
284
  const forwardItem = (followSubscriptionId2, item) => {
200
285
  if (item.tag === "Stop" && activeFollows.delete(followSubscriptionId2)) {
201
286
  ref.handle?.unsubscribe();
287
+ followOperations.delete(followSubscriptionId2);
288
+ pendingOperationStarts.delete(followSubscriptionId2);
202
289
  }
203
- sendFollowEvent(followSubscriptionId2, convertFollowEventToJsonRpc(item));
290
+ forwardFollowItem(followSubscriptionId2, item);
204
291
  };
205
292
  ref.handle = subscribeWithInterrupt(
206
293
  chain.followHeadSubscribe({ request: { genesisHash, withRuntime } }),
@@ -224,11 +311,15 @@ function createHostPapiProvider(client, genesisHash) {
224
311
  break;
225
312
  }
226
313
  ref.handle.onInterrupt(() => {
314
+ followOperations.delete(followSubscriptionId);
315
+ pendingOperationStarts.delete(followSubscriptionId);
227
316
  if (activeFollows.delete(followSubscriptionId)) {
228
317
  sendFollowEvent(followSubscriptionId, { event: "stop" });
229
318
  }
230
319
  });
231
320
  activeFollows.set(followSubscriptionId, ref.handle);
321
+ followOperations.set(followSubscriptionId, /* @__PURE__ */ new Map());
322
+ pendingOperationStarts.set(followSubscriptionId, 0);
232
323
  sendJsonRpcResponse(id, followSubscriptionId);
233
324
  for (const item of pendingItems) {
234
325
  forwardItem(followSubscriptionId, item);
@@ -242,6 +333,8 @@ function createHostPapiProvider(client, genesisHash) {
242
333
  follow.unsubscribe();
243
334
  activeFollows.delete(followSubId);
244
335
  }
336
+ followOperations.delete(followSubId);
337
+ pendingOperationStarts.delete(followSubId);
245
338
  sendJsonRpcResponse(id, null);
246
339
  break;
247
340
  }
@@ -255,13 +348,8 @@ function createHostPapiProvider(client, genesisHash) {
255
348
  }
256
349
  case "chainHead_v1_body": {
257
350
  const [followSubscriptionId, hash] = params;
258
- chain.getHeadBody({ genesisHash, followSubscriptionId, hash }).match(
259
- (response) => sendJsonRpcResponse(
260
- id,
261
- convertOperationResultToJsonRpc(response.operation)
262
- ),
263
- hostError(id)
264
- );
351
+ const bodyStart = startOperationRequest(id, followSubscriptionId);
352
+ chain.getHeadBody({ genesisHash, followSubscriptionId, hash }).match(bodyStart.ok, bodyStart.err);
265
353
  break;
266
354
  }
267
355
  case "chainHead_v1_storage": {
@@ -270,6 +358,7 @@ function createHostPapiProvider(client, genesisHash) {
270
358
  key: item.key,
271
359
  queryType: convertStorageType(item.type)
272
360
  }));
361
+ const storageStart = startOperationRequest(id, followSubscriptionId);
273
362
  chain.getHeadStorage({
274
363
  genesisHash,
275
364
  followSubscriptionId,
@@ -281,30 +370,19 @@ function createHostPapiProvider(client, genesisHash) {
281
370
  // the inner Hex codec on `null`, which throws
282
371
  // (`null.startsWith`). Coerce `null` → `undefined`.
283
372
  childTrie: childTrie ?? void 0
284
- }).match(
285
- (response) => sendJsonRpcResponse(
286
- id,
287
- convertOperationResultToJsonRpc(response.operation)
288
- ),
289
- hostError(id)
290
- );
373
+ }).match(storageStart.ok, storageStart.err);
291
374
  break;
292
375
  }
293
376
  case "chainHead_v1_call": {
294
377
  const [followSubscriptionId, hash, fn, callParameters] = params;
378
+ const callStart = startOperationRequest(id, followSubscriptionId);
295
379
  chain.callHead({
296
380
  genesisHash,
297
381
  followSubscriptionId,
298
382
  hash,
299
383
  function: fn,
300
384
  callParameters
301
- }).match(
302
- (response) => sendJsonRpcResponse(
303
- id,
304
- convertOperationResultToJsonRpc(response.operation)
305
- ),
306
- hostError(id)
307
- );
385
+ }).match(callStart.ok, callStart.err);
308
386
  break;
309
387
  }
310
388
  case "chainHead_v1_unpin": {
@@ -320,7 +398,10 @@ function createHostPapiProvider(client, genesisHash) {
320
398
  }
321
399
  case "chainHead_v1_stopOperation": {
322
400
  const [followSubscriptionId, operationId] = params;
323
- chain.stopHeadOperation({ genesisHash, followSubscriptionId, operationId }).match(() => sendJsonRpcResponse(id, null), hostError(id));
401
+ chain.stopHeadOperation({ genesisHash, followSubscriptionId, operationId }).match(() => {
402
+ followOperations.get(followSubscriptionId)?.delete(operationId);
403
+ sendJsonRpcResponse(id, null);
404
+ }, hostError(id));
324
405
  break;
325
406
  }
326
407
  case "chainSpec_v1_genesisHash": {
@@ -387,6 +468,8 @@ function createHostPapiProvider(client, genesisHash) {
387
468
  handle.unsubscribe();
388
469
  }
389
470
  activeFollows.clear();
471
+ followOperations.clear();
472
+ pendingOperationStarts.clear();
390
473
  for (const operationId of activeBroadcasts) {
391
474
  chain.stopTransaction({ genesisHash, operationId }).match(
392
475
  () => {
@@ -575,6 +658,7 @@ async function getStatementStore() {
575
658
  // src/chains.ts
576
659
  var BULLETIN_RPCS = {
577
660
  paseo: ["wss://paseo-bulletin-next-rpc.polkadot.io"],
661
+ previewnet: ["wss://previewnet.substrate.dev/bulletin"],
578
662
  devnet: ["wss://bulletin-paseo.tservices.es:8443"],
579
663
  polkadot: [],
580
664
  kusama: []
@@ -665,13 +749,18 @@ function sameRingLocation(a, b) {
665
749
  function findRingVrfKeyHandle(keys, ring) {
666
750
  return keys.find((key) => key.rings.some((candidate) => sameRingLocation(candidate, ring)))?.handle;
667
751
  }
668
- function deriveTxExtVersion(metadata) {
669
- const versions = unifyMetadata(decAnyMetadata(metadata)).extrinsic.version;
752
+ function selectHostTxExtVersion(versions) {
670
753
  if (versions.length === 0) {
671
754
  throw new Error("No extrinsic version found in metadata");
672
755
  }
673
- const latestVersion = versions.reduce((acc, v) => Math.max(acc, v), 0);
674
- return latestVersion === 4 ? 0 : latestVersion;
756
+ if (versions.includes(4)) {
757
+ return 0;
758
+ }
759
+ return versions.reduce((acc, version) => Math.max(acc, version), 0);
760
+ }
761
+ function deriveTxExtVersion(metadata) {
762
+ const versions = unifyMetadata(decAnyMetadata(metadata)).extrinsic.version;
763
+ return selectHostTxExtVersion(versions);
675
764
  }
676
765
  var deps = { deriveTxExtVersion };
677
766
  function toHostExtensions(signedExtensions) {