@peers-app/peers-sdk 0.26.6 → 0.27.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.
Files changed (182) hide show
  1. package/README.md +3 -1
  2. package/bin/peers-package.js +13 -0
  3. package/define/package.json +4 -0
  4. package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
  5. package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/also-implements.test.js +110 -0
  7. package/dist/contracts/__tests__/builder.test.js +45 -0
  8. package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
  9. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
  10. package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
  11. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
  12. package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
  14. package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
  15. package/dist/contracts/__tests__/registry.test.js +68 -0
  16. package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
  17. package/dist/contracts/__tests__/target-dispatch.js +24 -0
  18. package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
  19. package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
  20. package/dist/contracts/__tests__/validate.test.js +177 -7
  21. package/dist/contracts/__tests__/workflow-context.test.js +0 -11
  22. package/dist/contracts/builder.d.ts +16 -2
  23. package/dist/contracts/builder.js +44 -3
  24. package/dist/contracts/contract-definition-schema.d.ts +251 -0
  25. package/dist/contracts/contract-definition-schema.js +48 -0
  26. package/dist/contracts/contract-provider-router.d.ts +2 -0
  27. package/dist/contracts/contract-provider-router.js +5 -2
  28. package/dist/contracts/contract-proxy.d.ts +52 -18
  29. package/dist/contracts/contract-proxy.js +144 -203
  30. package/dist/contracts/contract-resolvers.d.ts +6 -4
  31. package/dist/contracts/contract-resolvers.js +32 -9
  32. package/dist/contracts/frozen-contracts.d.ts +59 -0
  33. package/dist/contracts/frozen-contracts.js +86 -0
  34. package/dist/contracts/host-contract-client.d.ts +134 -0
  35. package/dist/contracts/host-contract-client.js +396 -0
  36. package/dist/contracts/host-contract-transport.d.ts +2 -0
  37. package/dist/contracts/host-contract-transport.js +8 -0
  38. package/dist/contracts/index.d.ts +6 -4
  39. package/dist/contracts/index.js +11 -5
  40. package/dist/contracts/registry.d.ts +31 -1
  41. package/dist/contracts/registry.js +100 -6
  42. package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
  43. package/dist/contracts/system/contract-catalog-contract.js +17 -5
  44. package/dist/contracts/tool-catalog-service.js +26 -6
  45. package/dist/contracts/tool-catalog.d.ts +1 -1
  46. package/dist/contracts/tool-catalog.js +4 -1
  47. package/dist/contracts/types.d.ts +32 -2
  48. package/dist/contracts/validate.d.ts +16 -1
  49. package/dist/contracts/validate.js +139 -25
  50. package/dist/data/change-tracking.d.ts +17 -5
  51. package/dist/data/change-tracking.js +22 -6
  52. package/dist/data/change-tracking.test.d.ts +1 -0
  53. package/dist/data/change-tracking.test.js +62 -0
  54. package/dist/data/orm/sql.data-source.d.ts +9 -0
  55. package/dist/data/orm/sql.data-source.js +47 -2
  56. package/dist/data/orm/sql.data-source.test.js +142 -0
  57. package/dist/data/orm/table-container.js +9 -9
  58. package/dist/data/orm/table-container.test.js +44 -0
  59. package/dist/data/orm/types.d.ts +41 -1
  60. package/dist/data/orm/types.js +159 -28
  61. package/dist/data/orm/types.test.js +129 -0
  62. package/dist/data/peer-types.d.ts +8 -62
  63. package/dist/data/persistent-vars.d.ts +4 -1
  64. package/dist/data/persistent-vars.js +7 -2
  65. package/dist/data/tools.d.ts +29 -221
  66. package/dist/define/also-implements.test.d.ts +1 -0
  67. package/dist/define/also-implements.test.js +123 -0
  68. package/dist/define/context.d.ts +77 -0
  69. package/dist/define/context.js +2 -0
  70. package/dist/define/contract-ref.d.ts +177 -0
  71. package/dist/define/contract-ref.js +67 -0
  72. package/dist/define/declare-contract.d.ts +167 -0
  73. package/dist/define/declare-contract.js +220 -0
  74. package/dist/define/declare-contract.test.d.ts +1 -0
  75. package/dist/define/declare-contract.test.js +573 -0
  76. package/dist/define/define-package.d.ts +203 -0
  77. package/dist/define/define-package.js +289 -0
  78. package/dist/define/define.test.d.ts +1 -0
  79. package/dist/define/define.test.js +1010 -0
  80. package/dist/define/empty-fields.d.ts +15 -0
  81. package/dist/define/empty-fields.js +134 -0
  82. package/dist/define/empty-fields.test.d.ts +1 -0
  83. package/dist/define/empty-fields.test.js +55 -0
  84. package/dist/define/events-observables.test.d.ts +1 -0
  85. package/dist/define/events-observables.test.js +1013 -0
  86. package/dist/define/events.d.ts +81 -0
  87. package/dist/define/events.js +81 -0
  88. package/dist/define/generated-field.d.ts +20 -0
  89. package/dist/define/generated-field.js +25 -0
  90. package/dist/define/index.d.ts +28 -0
  91. package/dist/define/index.js +62 -0
  92. package/dist/define/lazy-members.d.ts +8 -0
  93. package/dist/define/lazy-members.js +38 -0
  94. package/dist/define/manifest.d.ts +63 -0
  95. package/dist/define/manifest.js +653 -0
  96. package/dist/define/observables.d.ts +146 -0
  97. package/dist/define/observables.js +187 -0
  98. package/dist/define/serve-members.d.ts +21 -0
  99. package/dist/define/serve-members.js +152 -0
  100. package/dist/define/serve-members.test.d.ts +1 -0
  101. package/dist/define/serve-members.test.js +630 -0
  102. package/dist/define/serve.d.ts +22 -0
  103. package/dist/define/serve.js +58 -0
  104. package/dist/define/system-contracts.d.ts +41 -0
  105. package/dist/define/system-contracts.js +21 -0
  106. package/dist/define/table-handle.d.ts +13 -0
  107. package/dist/define/table-handle.js +36 -0
  108. package/dist/define/table.d.ts +128 -0
  109. package/dist/define/table.js +122 -0
  110. package/dist/define/tool.d.ts +129 -0
  111. package/dist/define/tool.js +91 -0
  112. package/dist/define/vars.d.ts +82 -0
  113. package/dist/define/vars.js +256 -0
  114. package/dist/define/vars.test.d.ts +1 -0
  115. package/dist/define/vars.test.js +39 -0
  116. package/dist/define/wire-value.d.ts +12 -0
  117. package/dist/define/wire-value.js +76 -0
  118. package/dist/define/worker-state.d.ts +15 -0
  119. package/dist/define/worker-state.js +45 -0
  120. package/dist/device/tx-encoding.test.js +10 -0
  121. package/dist/index.d.ts +3 -0
  122. package/dist/index.js +5 -1
  123. package/dist/observable.d.ts +42 -2
  124. package/dist/observable.js +25 -7
  125. package/dist/package-build/build-package.d.ts +60 -0
  126. package/dist/package-build/build-package.js +312 -0
  127. package/dist/package-build/build-package.test.d.ts +1 -0
  128. package/dist/package-build/build-package.test.js +204 -0
  129. package/dist/package-build/cli.d.ts +7 -0
  130. package/dist/package-build/cli.js +218 -0
  131. package/dist/package-build/contract-add.d.ts +35 -0
  132. package/dist/package-build/contract-add.js +97 -0
  133. package/dist/package-build/contract-add.test.d.ts +1 -0
  134. package/dist/package-build/contract-add.test.js +467 -0
  135. package/dist/package-build/contract-source.d.ts +59 -0
  136. package/dist/package-build/contract-source.js +471 -0
  137. package/dist/package-build/freeze-contracts.d.ts +56 -0
  138. package/dist/package-build/freeze-contracts.js +198 -0
  139. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  140. package/dist/package-build/freeze-contracts.test.js +355 -0
  141. package/dist/package-installer/package-creator.d.ts +4 -0
  142. package/dist/package-installer/package-creator.js +57 -16
  143. package/dist/package-installer/package-creator.test.js +86 -0
  144. package/dist/package-loader/contract-members.d.ts +53 -0
  145. package/dist/package-loader/contract-members.js +31 -0
  146. package/dist/package-loader/contract-package-loader.js +5 -1
  147. package/dist/package-loader/contract-package-loader.test.js +11 -0
  148. package/dist/package-loader/host-authority-loading.test.js +9 -5
  149. package/dist/package-loader/index.d.ts +1 -0
  150. package/dist/package-loader/index.js +1 -0
  151. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  152. package/dist/package-loader/isolated-artifact.js +257 -51
  153. package/dist/package-loader/isolated-artifact.test.js +294 -21
  154. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  155. package/dist/package-loader/isolated-table-gateway.js +30 -11
  156. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  157. package/dist/package-loader/package-loader.d.ts +8 -1
  158. package/dist/package-loader/package-loader.js +39 -20
  159. package/dist/package-loader/package-loader.test.js +47 -0
  160. package/dist/peer-id.d.ts +25 -0
  161. package/dist/peer-id.js +99 -0
  162. package/dist/serial-json.compat.test.d.ts +1 -0
  163. package/dist/serial-json.compat.test.js +369 -0
  164. package/dist/serial-json.d.ts +41 -5
  165. package/dist/serial-json.js +216 -157
  166. package/dist/serial-json.oracle.d.ts +21 -0
  167. package/dist/serial-json.oracle.js +222 -0
  168. package/dist/serial-json.test.js +315 -0
  169. package/dist/testing/index.d.ts +4 -0
  170. package/dist/testing/index.js +8 -0
  171. package/dist/testing/mock-host-contract.d.ts +123 -0
  172. package/dist/testing/mock-host-contract.js +434 -0
  173. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  174. package/dist/testing/mock-host-contract.test.js +782 -0
  175. package/dist/types/field-type.d.ts +35 -18
  176. package/dist/types/field-type.js +4 -1
  177. package/dist/types/zod-types.d.ts +9 -0
  178. package/dist/types/zod-types.js +8 -2
  179. package/dist/utils.d.ts +1 -25
  180. package/dist/utils.js +10 -90
  181. package/package.json +9 -2
  182. package/testing/package.json +4 -0
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FROZEN_CONTRACTS_FILE = void 0;
4
+ exports.checkFrozenContracts = checkFrozenContracts;
5
+ exports.freezeContracts = freezeContracts;
6
+ exports.describeFrozenContractChanges = describeFrozenContractChanges;
7
+ const validate_1 = require("./validate");
8
+ // A stable package release freezes every contract version it provides: its shape can't change
9
+ // again, only its descriptions and tool policy. A package records what its stable releases froze
10
+ // in `frozen-contracts.json`, checked in at its root, so its build and tests can refuse a change
11
+ // before a release ships one. `peers-package contract freeze` writes it.
12
+ /** The file, at a package's root, that records the contract versions its stable releases froze. */
13
+ exports.FROZEN_CONTRACTS_FILE = "frozen-contracts.json";
14
+ const ABOUT = "Contract versions this package's stable releases froze. A frozen version's shape can't change (descriptions and tool policy can); declare a new version instead. Written by `peers-package contract freeze`.";
15
+ const key = (contract) => `${contract.contractId}@${contract.version}`;
16
+ /**
17
+ * Check the contracts a package provides against the versions its stable releases froze.
18
+ *
19
+ * @param frozen - The package's `frozen-contracts.json`.
20
+ * @param provided - The contracts the package provides now. A `devTag` is ignored.
21
+ * @returns The frozen versions it changed, and those it no longer provides.
22
+ * @throws When the package provides the same contract identity more than once.
23
+ */
24
+ function checkFrozenContracts(frozen, provided) {
25
+ const current = new Map();
26
+ for (const contract of provided) {
27
+ const identity = key(contract);
28
+ if (current.has(identity))
29
+ throw new Error(`Duplicate provided contract ${identity}`);
30
+ current.set(identity, contract);
31
+ }
32
+ const check = { changed: [], notProvided: [] };
33
+ for (const contract of frozen.contracts) {
34
+ const now = current.get(key(contract));
35
+ if (!now) {
36
+ check.notProvided.push(contract);
37
+ continue;
38
+ }
39
+ const changes = (0, validate_1.contractShapeChanges)(contract, toJson(now));
40
+ if (changes.length > 0)
41
+ check.changed.push({ contract, changes });
42
+ }
43
+ return check;
44
+ }
45
+ /**
46
+ * Record every contract version a release provides as frozen by that release, keeping the
47
+ * versions already frozen. Refuses (records nothing) when the release changed a frozen version.
48
+ *
49
+ * @param frozen - The package's `frozen-contracts.json`, or undefined when it has none yet.
50
+ * @param provided - The contracts the release provides.
51
+ * @param packageVersion - The release's package version.
52
+ * @returns The file's new content, the versions it added, and the check that decided.
53
+ * @throws When the package version is blank or provided contract identities repeat.
54
+ */
55
+ function freezeContracts(frozen, provided, packageVersion) {
56
+ if (typeof packageVersion !== "string" || !packageVersion.trim()) {
57
+ throw new Error("The package has no version to freeze its contracts as");
58
+ }
59
+ const existing = frozen ?? { contracts: [] };
60
+ const check = checkFrozenContracts(existing, provided);
61
+ if (check.changed.length > 0)
62
+ return { frozen: existing, added: [], check };
63
+ const known = new Set(existing.contracts.map(key));
64
+ const added = provided
65
+ .filter((contract) => !known.has(key(contract)))
66
+ .map((contract) => frozenContract(contract, packageVersion));
67
+ const contracts = [...existing.contracts, ...added].sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
68
+ return { frozen: { about: ABOUT, contracts }, added, check };
69
+ }
70
+ /**
71
+ * Describe a check's failures, one line per changed contract version, for a build or test error.
72
+ *
73
+ * @param check - What {@link checkFrozenContracts} found.
74
+ */
75
+ function describeFrozenContractChanges(check) {
76
+ return check.changed.map(({ contract, changes }) => `${contract.name} (${key(contract)}), frozen by ${contract.frozenBy}: ${changes.join("; ")}. Declare a new version instead.`);
77
+ }
78
+ /** A provided contract as a frozen entry: JSON data, without its dev tag. */
79
+ function frozenContract(contract, packageVersion) {
80
+ const { devTag: _dev, ...shape } = toJson(contract);
81
+ return { ...shape, frozenBy: packageVersion };
82
+ }
83
+ /** A definition as JSON data; legacy default factories are omitted without executing them. */
84
+ function toJson(contract) {
85
+ return JSON.parse(JSON.stringify(contract));
86
+ }
@@ -0,0 +1,134 @@
1
+ import type { IDataChangedEvent } from "../data/orm/subscribable.data-source";
2
+ import { type AnyContractTools, type ContractEventPayloadsOf, type ContractIdOf, type ContractObservableValuesOf, type ContractObservableWritabilityOf, type ContractTableInputsOf, type ContractTableRowsOf, type ContractToolsOf, type ContractVersionOf, type IContractRef } from "../define/contract-ref";
3
+ import type { IPackageTableHandle } from "../define/table";
4
+ import { type Observable, type ReadonlyObservable } from "../observable";
5
+ import { type IContractSubscription } from "./contract-proxy";
6
+ /** Options for a subscription made through a {@link hostContract} client. */
7
+ export interface IHostSubscribeOptions {
8
+ /**
9
+ * Called each time the subscription is live again after the host connection was replaced
10
+ * (a reconnect), including when its first attachment failed (`ready` resolved `false`).
11
+ * Anything sent while it was down was missed, so read again.
12
+ */
13
+ onReconnect?: () => void;
14
+ }
15
+ /**
16
+ * A table on a {@link hostContract} client: the same methods as a package's own table handle,
17
+ * plus `dataChanged`. Every call goes through the providing package's table (its class, if it
18
+ * has one), so `save` takes the schema's input and the provider's defaults and transforms apply.
19
+ */
20
+ export interface IHostContractTable<T extends Record<string, any>, TInput = T> extends IPackageTableHandle<T, TInput> {
21
+ /** Changes to the table's rows, pushed by the provider. */
22
+ readonly dataChanged: {
23
+ subscribe(handler: (event: IDataChangedEvent<T>) => void, options?: IHostSubscribeOptions): IContractSubscription;
24
+ };
25
+ }
26
+ /** An event on a {@link hostContract} client. */
27
+ export interface IHostContractEvent<P> {
28
+ /** Receive each payload the provider emits from now on. */
29
+ subscribe(handler: (payload: P) => void, options?: IHostSubscribeOptions): IContractSubscription;
30
+ }
31
+ type HostTable<R, I> = R extends Record<string, any> ? IHostContractTable<R, I> : never;
32
+ /**
33
+ * A UI's client for one contract on the local host, typed from its reference. Every member is
34
+ * made on first use. Calls wait for the host connection, and fail if it is lost or replaced
35
+ * before they reply; subscriptions and observables follow it across reconnects.
36
+ */
37
+ export interface IHostContractClient<T> {
38
+ /** The contract's id. */
39
+ readonly contractId: string;
40
+ /** The contract's version. */
41
+ readonly version: number;
42
+ /** Tools: `await news.tools.refresh()`. */
43
+ readonly tools: ContractToolsOf<T>;
44
+ /** Tables the contract exposes: `await news.tables.Feeds.list()`. */
45
+ readonly tables: {
46
+ readonly [K in keyof ContractTableRowsOf<T>]: HostTable<ContractTableRowsOf<T>[K], K extends keyof ContractTableInputsOf<T> ? ContractTableInputsOf<T>[K] : never>;
47
+ };
48
+ /** Events: `weather.events.alert.subscribe((payload) => …)`. */
49
+ readonly events: {
50
+ readonly [K in keyof ContractEventPayloadsOf<T>]: IHostContractEvent<ContractEventPayloadsOf<T>[K]>;
51
+ };
52
+ /**
53
+ * Observables, as live local copies: read with `weather.observables.location()` (or
54
+ * `useObservable`), write with `weather.observables.location(value)`. Each subscribes on first
55
+ * access and reads `undefined` until the provider's value arrives. A write the provider refuses
56
+ * (the observable is read-only, or the value doesn't fit) is logged, and the copy reads the
57
+ * provider's value again. A write still awaiting its reply when the connection is replaced is
58
+ * logged as failed too, along with writes queued on that connection. Writes made while
59
+ * disconnected or after reconnecting can use the new connection, and the copy follows its
60
+ * value. Read-only declarations expose only the read and subscription methods. Await an
61
+ * observable's `loadingPromise` before relying on its initial value; it resolves after the
62
+ * first snapshot attempt or disposal, including if that read failed (and was logged).
63
+ */
64
+ readonly observables: {
65
+ readonly [K in keyof ContractObservableValuesOf<T>]: (true extends (K extends keyof ContractObservableWritabilityOf<T> ? ContractObservableWritabilityOf<T>[K] : boolean) ? Observable<ContractObservableValuesOf<T>[K] | undefined> : ReadonlyObservable<ContractObservableValuesOf<T>[K] | undefined>) & {
66
+ /** Settles after the first snapshot attempt, or when the client is disposed. */
67
+ readonly loadingPromise: Promise<void>;
68
+ };
69
+ };
70
+ /** Cancel connection waits, end subscriptions, and refuse new calls and writes. Idempotent. */
71
+ dispose(): void;
72
+ }
73
+ /** Options for {@link hostContract}. */
74
+ export interface IHostContractOptions {
75
+ /**
76
+ * The data context every call is scoped to, read when the client connects (and again after a
77
+ * reconnect). Defaults to the active group, or the personal context when none is active.
78
+ */
79
+ dataContextId?: () => Promise<string | undefined>;
80
+ /** How long a call waits for the host connection before it fails. Defaults to 10 seconds. */
81
+ timeoutMs?: number;
82
+ /**
83
+ * Reach this provider of the contract rather than the group's default: to show a widget for
84
+ * every installed provider, say.
85
+ */
86
+ providerPackageId?: string;
87
+ }
88
+ /**
89
+ * Replace the default data context for clients made without one, or restore it with
90
+ * `undefined`. Test hosts use it, where there is no signed-in user. Not exported from the SDK.
91
+ *
92
+ * @internal
93
+ * @param resolver - Resolves the data context each time a client connects.
94
+ */
95
+ export declare function setDefaultHostDataContextId(resolver: (() => Promise<string | undefined>) | undefined): void;
96
+ /**
97
+ * A UI's client for a contract on the local host: tools, tables (with `dataChanged`), events,
98
+ * and observables, typed from the reference.
99
+ *
100
+ * ```ts
101
+ * import type { News } from "../package"; // types only: no handler code in the UI bundle
102
+ * export const news = hostContract<typeof News>(contractId, contractVersion);
103
+ *
104
+ * const headlines = await news.tables.Headlines.list({}, { pageSize: 100 });
105
+ * await news.tools.refresh();
106
+ * ```
107
+ *
108
+ * The client needs no contract definition: members are made on first use. Calls wait for the
109
+ * host connection (up to `timeoutMs`). A tool or table call still waiting for its reply when
110
+ * the connection is lost or replaced fails, though the provider may have run it. Subscriptions
111
+ * and observables wait as long as it takes, and re-attach after a reconnect. Subscription `ready` acknowledges the first attachment;
112
+ * `onReconnect` runs once each replacement subscription is live. Dispose independent clients
113
+ * made with options when their owner ends; shared clients normally live for the window.
114
+ *
115
+ * Calls are scoped to the active group, read when the client connects; switching groups
116
+ * reloads the window. Without options, every call for the same contract id and version
117
+ * returns the same client, even from a new reference.
118
+ *
119
+ * Calls act as the signed-in user, like every UI call today. Without a contract type, the
120
+ * client is untyped, as with an untyped `contractRef`.
121
+ *
122
+ * @param contractId - The contract's id; with a contract type, it must be that contract's.
123
+ * @param version - The contract's version; with a contract type, it must be that contract's.
124
+ * @param options - Data context, provider, and connection wait.
125
+ */
126
+ export declare function hostContract<T = AnyContractTools>(contractId: ContractIdOf<T>, version: ContractVersionOf<T>, options?: IHostContractOptions): IHostContractClient<T>;
127
+ /**
128
+ * A UI's client for a contract on the local host, from `contractRef` or `declareContract`.
129
+ *
130
+ * @param ref - From `contractRef` or `declareContract`, typed with the contract.
131
+ * @param options - Data context, provider, and connection wait.
132
+ */
133
+ export declare function hostContract<T>(ref: IContractRef<T>, options?: IHostContractOptions): IHostContractClient<T>;
134
+ export {};
@@ -0,0 +1,396 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setDefaultHostDataContextId = setDefaultHostDataContextId;
4
+ exports.hostContract = hostContract;
5
+ const user_context_singleton_1 = require("../context/user-context-singleton");
6
+ const contract_ref_1 = require("../define/contract-ref");
7
+ const lazy_members_1 = require("../define/lazy-members");
8
+ const table_handle_1 = require("../define/table-handle");
9
+ const observable_1 = require("../observable");
10
+ const contract_proxy_1 = require("./contract-proxy");
11
+ const host_contract_transport_1 = require("./host-contract-transport");
12
+ /** Default wait for the host connection before a call fails. */
13
+ const DEFAULT_TIMEOUT_MS = 10_000;
14
+ /** Shared clients, one per contract id and version, for `hostContract` without options. */
15
+ const sharedClients = new Map();
16
+ async function activeDataContextId() {
17
+ if (defaultDataContextId) {
18
+ return defaultDataContextId();
19
+ }
20
+ const user = await (0, user_context_singleton_1.getUserContext)();
21
+ return user.currentlyActiveGroupId() || user.userId;
22
+ }
23
+ let defaultDataContextId;
24
+ /**
25
+ * Replace the default data context for clients made without one, or restore it with
26
+ * `undefined`. Test hosts use it, where there is no signed-in user. Not exported from the SDK.
27
+ *
28
+ * @internal
29
+ * @param resolver - Resolves the data context each time a client connects.
30
+ */
31
+ function setDefaultHostDataContextId(resolver) {
32
+ defaultDataContextId = resolver;
33
+ }
34
+ /** Implementation of both forms. */
35
+ function hostContract(...args) {
36
+ let ref;
37
+ let options;
38
+ if (typeof args[0] === "string") {
39
+ ref = (0, contract_ref_1.contractRef)(args[0], args[1]);
40
+ options = args[2];
41
+ }
42
+ else {
43
+ ref = args[0];
44
+ options = args[1];
45
+ }
46
+ if (typeof ref !== "function" || ref.kind !== "contract-ref") {
47
+ throw new Error("hostContract takes (contractId, version) or a reference from contractRef(contractId, version)");
48
+ }
49
+ if (options === undefined) {
50
+ const key = `${ref.contractId}@${ref.version}`;
51
+ const shared = sharedClients.get(key);
52
+ if (shared) {
53
+ return shared;
54
+ }
55
+ const client = createHostContractClient(ref, {}, () => sharedClients.delete(key));
56
+ sharedClients.set(key, client);
57
+ return client;
58
+ }
59
+ return createHostContractClient(ref, options, () => { });
60
+ }
61
+ /** Stop waiting for a context lookup or reply when its connection or caller ends. */
62
+ function abortable(promise, signals) {
63
+ const aborted = signals.find((signal) => signal.aborted);
64
+ if (aborted)
65
+ return Promise.reject(aborted.reason);
66
+ return new Promise((resolve, reject) => {
67
+ const finish = (action) => {
68
+ for (const signal of signals)
69
+ signal.removeEventListener("abort", abort);
70
+ action();
71
+ };
72
+ const abort = () => finish(() => reject(signals.find((signal) => signal.aborted)?.reason));
73
+ for (const signal of signals)
74
+ signal.addEventListener("abort", abort, { once: true });
75
+ promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
76
+ });
77
+ }
78
+ /**
79
+ * Stop waiting for a reply once its connection is lost or replaced: a dropped socket never
80
+ * answers. The request may still have reached the provider. A reply already on its way, or a
81
+ * transport that fails its calls as it closes, gets a turn to land first: the provider's answer
82
+ * beats the guess that it was lost, so a call that ran isn't reported as failed.
83
+ */
84
+ function untilReplaced(reply, signal) {
85
+ reply.catch(() => undefined);
86
+ return new Promise((resolve, reject) => {
87
+ let timer;
88
+ const replaced = () => {
89
+ timer = setTimeout(() => reject(signal.reason), 0);
90
+ };
91
+ const settle = (finish) => {
92
+ signal.removeEventListener("abort", replaced);
93
+ clearTimeout(timer);
94
+ finish();
95
+ };
96
+ if (signal.aborted)
97
+ replaced();
98
+ else
99
+ signal.addEventListener("abort", replaced, { once: true });
100
+ reply.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
101
+ });
102
+ }
103
+ function createHostContractClient(ref, options, onDispose) {
104
+ const { contractId, version } = ref;
105
+ if (options.providerPackageId !== undefined &&
106
+ (typeof options.providerPackageId !== "string" || options.providerPackageId.trim() === "")) {
107
+ throw new Error("hostContract providerPackageId must be a non-empty package id");
108
+ }
109
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
110
+ const resolveDataContextId = options.dataContextId ?? activeDataContextId;
111
+ let disposed = false;
112
+ let connection;
113
+ let pending;
114
+ let unwatch;
115
+ /** Re-attach callbacks for every live subscription and observable. */
116
+ const followers = new Map();
117
+ const lifetime = new AbortController();
118
+ let contextLifetime = new AbortController();
119
+ const disposedError = () => new Error(`Host contract ${contractId} v${version} is disposed`);
120
+ const watchTransport = () => {
121
+ if (unwatch) {
122
+ return;
123
+ }
124
+ unwatch = (0, host_contract_transport_1.subscribeHostContractTransport)(() => {
125
+ contextLifetime.abort(new Error(`Host connection for ${contractId} v${version} was lost or replaced`));
126
+ contextLifetime = new AbortController();
127
+ pending = undefined;
128
+ if (connection) {
129
+ connection.members.dispose();
130
+ connection = undefined;
131
+ }
132
+ for (const follower of [...followers.keys()]) {
133
+ follower();
134
+ }
135
+ }, { emitCurrent: false });
136
+ };
137
+ /** The connection for the current transport, made on first use. */
138
+ const connect = async (wait) => {
139
+ const signal = wait.signal ?? lifetime.signal;
140
+ for (;;) {
141
+ if (disposed) {
142
+ throw disposedError();
143
+ }
144
+ if (signal.aborted)
145
+ throw signal.reason;
146
+ watchTransport();
147
+ const transport = (0, host_contract_transport_1.getHostContractTransport)() ?? (await (0, host_contract_transport_1.waitForHostContractTransport)({ ...wait, signal }));
148
+ if (disposed) {
149
+ throw disposedError();
150
+ }
151
+ if ((0, host_contract_transport_1.getHostContractTransport)() !== transport) {
152
+ continue;
153
+ }
154
+ if (connection?.transport === transport) {
155
+ return connection;
156
+ }
157
+ if (pending?.transport !== transport) {
158
+ pending = { transport, dataContextId: resolveDataContextId() };
159
+ }
160
+ const request = pending;
161
+ const contextSignal = contextLifetime.signal;
162
+ let dataContextId;
163
+ try {
164
+ dataContextId = await abortable(request.dataContextId, [contextSignal, signal]);
165
+ }
166
+ catch (error) {
167
+ if (!disposed && !signal.aborted && contextSignal.aborted)
168
+ continue;
169
+ if (pending === request) {
170
+ pending = undefined;
171
+ }
172
+ throw error;
173
+ }
174
+ if (disposed) {
175
+ throw disposedError();
176
+ }
177
+ if (contextSignal.aborted || (0, host_contract_transport_1.getHostContractTransport)() !== transport) {
178
+ continue;
179
+ }
180
+ if (connection?.transport !== transport) {
181
+ connection = {
182
+ transport,
183
+ signal: contextSignal,
184
+ members: (0, contract_proxy_1.createContractConsumerMembers)(contractId, version, transport, {
185
+ ...(dataContextId ? { dataContextId } : {}),
186
+ ...(options.providerPackageId ? { providerPackageId: options.providerPackageId } : {}),
187
+ }),
188
+ };
189
+ }
190
+ return connection;
191
+ }
192
+ };
193
+ /**
194
+ * Keep a subscription attached to the current connection: now (once connected), and again
195
+ * after every reconnect, when `onReconnect` runs.
196
+ */
197
+ const follow = (attach, onReconnect) => {
198
+ if (disposed)
199
+ throw disposedError();
200
+ let active = true;
201
+ let current;
202
+ let attachedBefore = false;
203
+ let attempt;
204
+ let settleReady;
205
+ const ready = new Promise((resolve) => {
206
+ settleReady = resolve;
207
+ });
208
+ const reattach = async (signal) => {
209
+ try {
210
+ const next = await connect({ signal });
211
+ if (!active || signal.aborted || connection !== next || current?.connection === next)
212
+ return;
213
+ current?.subscription.unsubscribe();
214
+ const subscription = attach(next.members);
215
+ const attached = { connection: next, subscription };
216
+ current = attached;
217
+ const live = await (subscription.ready ?? Promise.resolve(true));
218
+ if (!active || signal.aborted || current !== attached)
219
+ return;
220
+ if (!live) {
221
+ settleReady(false);
222
+ attachedBefore = true;
223
+ return;
224
+ }
225
+ settleReady(true);
226
+ if (attachedBefore)
227
+ onReconnect?.();
228
+ attachedBefore = true;
229
+ }
230
+ catch (error) {
231
+ if (active && !disposed && !signal.aborted) {
232
+ settleReady(false);
233
+ // `ready` already said false, so report a later attachment as a reconnect.
234
+ attachedBefore = true;
235
+ console.error("Host contract subscription failed", error);
236
+ }
237
+ }
238
+ };
239
+ const run = () => {
240
+ attempt?.abort();
241
+ if (current && current.connection !== connection) {
242
+ current.subscription.unsubscribe();
243
+ current = undefined;
244
+ }
245
+ attempt = new AbortController();
246
+ void reattach(attempt.signal);
247
+ };
248
+ const unsubscribe = () => {
249
+ if (!active)
250
+ return false;
251
+ active = false;
252
+ attempt?.abort();
253
+ followers.delete(run);
254
+ current?.subscription.unsubscribe();
255
+ current = undefined;
256
+ settleReady(false);
257
+ return true;
258
+ };
259
+ followers.set(run, unsubscribe);
260
+ run();
261
+ return { ready, unsubscribe };
262
+ };
263
+ /** Make a call on the current connection; it fails if that connection is replaced first. */
264
+ const call = async (send) => {
265
+ const next = await connect({ timeoutMs });
266
+ return untilReplaced(Promise.resolve().then(() => send(next.members)), next.signal);
267
+ };
268
+ const tools = (0, lazy_members_1.lazyMembers)((toolName) => (...args) => call((members) => members.tool(toolName)(...args)));
269
+ const tables = (0, lazy_members_1.lazyMembers)((tableName) => ({
270
+ ...(0, table_handle_1.packageTableMethods)((operation, args) => call((members) => members.table(tableName)[operation](...args))),
271
+ dataChanged: {
272
+ subscribe: (handler, subscribeOptions) => follow((members) => members.table(tableName).dataChanged.subscribe(handler), subscribeOptions?.onReconnect),
273
+ },
274
+ }));
275
+ const events = (0, lazy_members_1.lazyMembers)((eventName) => ({
276
+ subscribe: (handler, subscribeOptions) => follow((members) => members.event(eventName).subscribe(handler), subscribeOptions?.onReconnect),
277
+ }));
278
+ const observables = (0, lazy_members_1.lazyMembers)((observableName) => {
279
+ const local = (0, observable_1.observable)();
280
+ let finishLoading;
281
+ const loadingPromise = new Promise((resolve) => {
282
+ finishLoading = () => {
283
+ lifetime.signal.removeEventListener("abort", finishLoading);
284
+ resolve();
285
+ };
286
+ });
287
+ if (disposed)
288
+ finishLoading();
289
+ else
290
+ lifetime.signal.addEventListener("abort", finishLoading, { once: true });
291
+ let mirror;
292
+ let applying = false;
293
+ let localRevision = 0;
294
+ let remoteRevision = 0;
295
+ // Each value has its own ordered write queue. Retire writes queued on a replaced connection;
296
+ // writes made during an outage wait for the next one rather than target a disposed mirror.
297
+ let writes = Promise.resolve();
298
+ const apply = (value) => {
299
+ applying = true;
300
+ try {
301
+ local(value);
302
+ }
303
+ finally {
304
+ applying = false;
305
+ }
306
+ };
307
+ // A reply lost with its connection would hold every later write, so writes stop waiting
308
+ // once the connection is replaced; the next mirror brings its value.
309
+ local.subscribe((value) => {
310
+ if (applying)
311
+ return;
312
+ const written = ++localRevision;
313
+ const queuedOn = (0, host_contract_transport_1.getHostContractTransport)() ? contextLifetime.signal : lifetime.signal;
314
+ writes = writes.then(async () => {
315
+ try {
316
+ const next = await connect({ timeoutMs, signal: queuedOn });
317
+ if (next.signal.aborted)
318
+ throw next.signal.reason;
319
+ const before = remoteRevision;
320
+ const result = await untilReplaced(next.members.setObservable(observableName, value), next.signal);
321
+ if (!disposed &&
322
+ connection === next &&
323
+ remoteRevision === before &&
324
+ localRevision === written) {
325
+ apply(result);
326
+ }
327
+ }
328
+ catch (error) {
329
+ if (!disposed) {
330
+ console.error("Contract observable set failed", error);
331
+ const current = mirror;
332
+ const signal = contextLifetime.signal;
333
+ await untilReplaced(current?.refresh() ?? Promise.resolve(), signal).catch(() => undefined);
334
+ if (current && mirror === current && !disposed && localRevision === written) {
335
+ apply(current.observable());
336
+ }
337
+ }
338
+ }
339
+ });
340
+ });
341
+ follow((members) => {
342
+ // The provider decides writability: no definition is needed in this client.
343
+ const made = members.observable(observableName, true, () => { });
344
+ mirror = made;
345
+ const subscription = made.observable.subscribe((value) => {
346
+ remoteRevision++;
347
+ apply(value);
348
+ });
349
+ void made.loaded.then(() => {
350
+ if (mirror === made)
351
+ finishLoading();
352
+ });
353
+ return {
354
+ unsubscribe: () => {
355
+ subscription.dispose();
356
+ if (mirror === made)
357
+ mirror = undefined;
358
+ return true;
359
+ },
360
+ };
361
+ });
362
+ const exposed = ((...args) => {
363
+ if (args.length === 0)
364
+ return local();
365
+ if (disposed)
366
+ throw disposedError();
367
+ return local(args[0]);
368
+ });
369
+ exposed.subscribe = local.subscribe;
370
+ exposed.notifySubscribers = local.notifySubscribers;
371
+ exposed.subscriberCount = local.subscriberCount;
372
+ return Object.assign(exposed, { loadingPromise });
373
+ });
374
+ return Object.freeze({
375
+ contractId,
376
+ version,
377
+ tools,
378
+ tables,
379
+ events,
380
+ observables,
381
+ dispose: () => {
382
+ if (disposed) {
383
+ return;
384
+ }
385
+ disposed = true;
386
+ lifetime.abort(disposedError());
387
+ contextLifetime.abort(disposedError());
388
+ for (const unsubscribe of [...followers.values()])
389
+ unsubscribe();
390
+ unwatch?.();
391
+ connection?.members.dispose();
392
+ connection = undefined;
393
+ onDispose();
394
+ },
395
+ });
396
+ }
@@ -31,8 +31,10 @@ export declare function subscribeHostContractTransport(listener: HostContractTra
31
31
  * Wait for the next available UI-to-host contract transport.
32
32
  *
33
33
  * @param opts.timeoutMs Optional deadline; rejects if no transport registers in time.
34
+ * @param opts.signal Cancels the wait and releases its listener.
34
35
  * @returns The current transport immediately, or the next registered transport.
35
36
  */
36
37
  export declare function waitForHostContractTransport(opts?: {
37
38
  timeoutMs?: number;
39
+ signal?: AbortSignal;
38
40
  }): Promise<IContractTransport>;
@@ -70,9 +70,14 @@ function subscribeHostContractTransport(listener, opts = {}) {
70
70
  * Wait for the next available UI-to-host contract transport.
71
71
  *
72
72
  * @param opts.timeoutMs Optional deadline; rejects if no transport registers in time.
73
+ * @param opts.signal Cancels the wait and releases its listener.
73
74
  * @returns The current transport immediately, or the next registered transport.
74
75
  */
75
76
  function waitForHostContractTransport(opts = {}) {
77
+ const { signal } = opts;
78
+ if (signal?.aborted) {
79
+ return Promise.reject(signal.reason);
80
+ }
76
81
  const current = getHostContractTransport();
77
82
  if (current) {
78
83
  return Promise.resolve(current);
@@ -90,6 +95,7 @@ function waitForHostContractTransport(opts = {}) {
90
95
  clearTimeout(timer);
91
96
  }
92
97
  unsubscribe();
98
+ signal?.removeEventListener("abort", abort);
93
99
  action();
94
100
  };
95
101
  const unsubscribe = subscribeHostContractTransport((transport) => {
@@ -98,6 +104,8 @@ function waitForHostContractTransport(opts = {}) {
98
104
  }
99
105
  finish(() => resolve(transport));
100
106
  }, { emitCurrent: false });
107
+ const abort = () => finish(() => reject(signal?.reason));
108
+ signal?.addEventListener("abort", abort, { once: true });
101
109
  if (timeoutMs !== undefined) {
102
110
  timer = setTimeout(() => {
103
111
  finish(() => reject(new Error(`Timed out waiting for host contract transport after ${timeoutMs}ms`)));