@peers-app/peers-sdk 0.26.6 → 0.27.1
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/README.md +3 -1
- package/bin/peers-package.js +13 -0
- package/define/package.json +4 -0
- package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
- package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
- package/dist/contracts/__tests__/also-implements.test.js +110 -0
- package/dist/contracts/__tests__/builder.test.js +45 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
- package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
- package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
- package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
- package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
- package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
- package/dist/contracts/__tests__/registry.test.js +68 -0
- package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
- package/dist/contracts/__tests__/target-dispatch.js +24 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
- package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
- package/dist/contracts/__tests__/validate.test.js +177 -7
- package/dist/contracts/__tests__/workflow-context.test.js +0 -11
- package/dist/contracts/builder.d.ts +16 -2
- package/dist/contracts/builder.js +44 -3
- package/dist/contracts/contract-definition-schema.d.ts +251 -0
- package/dist/contracts/contract-definition-schema.js +48 -0
- package/dist/contracts/contract-provider-router.d.ts +2 -0
- package/dist/contracts/contract-provider-router.js +5 -2
- package/dist/contracts/contract-proxy.d.ts +52 -18
- package/dist/contracts/contract-proxy.js +144 -203
- package/dist/contracts/contract-resolvers.d.ts +6 -4
- package/dist/contracts/contract-resolvers.js +32 -9
- package/dist/contracts/frozen-contracts.d.ts +59 -0
- package/dist/contracts/frozen-contracts.js +86 -0
- package/dist/contracts/host-contract-client.d.ts +134 -0
- package/dist/contracts/host-contract-client.js +396 -0
- package/dist/contracts/host-contract-transport.d.ts +2 -0
- package/dist/contracts/host-contract-transport.js +8 -0
- package/dist/contracts/index.d.ts +6 -4
- package/dist/contracts/index.js +11 -5
- package/dist/contracts/registry.d.ts +31 -1
- package/dist/contracts/registry.js +100 -6
- package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
- package/dist/contracts/system/contract-catalog-contract.js +17 -5
- package/dist/contracts/tool-catalog-service.js +26 -6
- package/dist/contracts/tool-catalog.d.ts +1 -1
- package/dist/contracts/tool-catalog.js +4 -1
- package/dist/contracts/types.d.ts +32 -2
- package/dist/contracts/validate.d.ts +16 -1
- package/dist/contracts/validate.js +139 -25
- package/dist/data/change-tracking.d.ts +17 -5
- package/dist/data/change-tracking.js +22 -6
- package/dist/data/change-tracking.test.d.ts +1 -0
- package/dist/data/change-tracking.test.js +62 -0
- package/dist/data/create-group.js +1 -1
- package/dist/data/group-members.d.ts +14 -0
- package/dist/data/group-members.js +36 -0
- package/dist/data/group-members.test.js +49 -0
- package/dist/data/orm/sql.data-source.d.ts +9 -0
- package/dist/data/orm/sql.data-source.js +47 -2
- package/dist/data/orm/sql.data-source.test.js +142 -0
- package/dist/data/orm/table-container.js +9 -9
- package/dist/data/orm/table-container.test.js +44 -0
- package/dist/data/orm/types.d.ts +41 -1
- package/dist/data/orm/types.js +159 -28
- package/dist/data/orm/types.test.js +129 -0
- package/dist/data/peer-types.d.ts +8 -62
- package/dist/data/persistent-vars.d.ts +4 -1
- package/dist/data/persistent-vars.js +7 -2
- package/dist/data/tools.d.ts +29 -221
- package/dist/define/also-implements.test.d.ts +1 -0
- package/dist/define/also-implements.test.js +123 -0
- package/dist/define/context.d.ts +77 -0
- package/dist/define/context.js +2 -0
- package/dist/define/contract-ref.d.ts +177 -0
- package/dist/define/contract-ref.js +67 -0
- package/dist/define/declare-contract.d.ts +167 -0
- package/dist/define/declare-contract.js +220 -0
- package/dist/define/declare-contract.test.d.ts +1 -0
- package/dist/define/declare-contract.test.js +573 -0
- package/dist/define/define-package.d.ts +203 -0
- package/dist/define/define-package.js +289 -0
- package/dist/define/define.test.d.ts +1 -0
- package/dist/define/define.test.js +1010 -0
- package/dist/define/empty-fields.d.ts +15 -0
- package/dist/define/empty-fields.js +134 -0
- package/dist/define/empty-fields.test.d.ts +1 -0
- package/dist/define/empty-fields.test.js +55 -0
- package/dist/define/events-observables.test.d.ts +1 -0
- package/dist/define/events-observables.test.js +1013 -0
- package/dist/define/events.d.ts +81 -0
- package/dist/define/events.js +81 -0
- package/dist/define/generated-field.d.ts +20 -0
- package/dist/define/generated-field.js +25 -0
- package/dist/define/index.d.ts +28 -0
- package/dist/define/index.js +62 -0
- package/dist/define/lazy-members.d.ts +8 -0
- package/dist/define/lazy-members.js +38 -0
- package/dist/define/manifest.d.ts +63 -0
- package/dist/define/manifest.js +653 -0
- package/dist/define/observables.d.ts +146 -0
- package/dist/define/observables.js +187 -0
- package/dist/define/serve-members.d.ts +21 -0
- package/dist/define/serve-members.js +152 -0
- package/dist/define/serve-members.test.d.ts +1 -0
- package/dist/define/serve-members.test.js +630 -0
- package/dist/define/serve.d.ts +22 -0
- package/dist/define/serve.js +58 -0
- package/dist/define/system-contracts.d.ts +41 -0
- package/dist/define/system-contracts.js +21 -0
- package/dist/define/table-handle.d.ts +13 -0
- package/dist/define/table-handle.js +36 -0
- package/dist/define/table.d.ts +128 -0
- package/dist/define/table.js +122 -0
- package/dist/define/tool.d.ts +129 -0
- package/dist/define/tool.js +91 -0
- package/dist/define/vars.d.ts +82 -0
- package/dist/define/vars.js +256 -0
- package/dist/define/vars.test.d.ts +1 -0
- package/dist/define/vars.test.js +39 -0
- package/dist/define/wire-value.d.ts +12 -0
- package/dist/define/wire-value.js +76 -0
- package/dist/define/worker-state.d.ts +15 -0
- package/dist/define/worker-state.js +45 -0
- package/dist/device/tx-encoding.test.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/invites/group-approval.js +4 -2
- package/dist/invites/group-approval.test.js +2 -0
- package/dist/observable.d.ts +42 -2
- package/dist/observable.js +25 -7
- package/dist/package-build/build-package.d.ts +60 -0
- package/dist/package-build/build-package.js +312 -0
- package/dist/package-build/build-package.test.d.ts +1 -0
- package/dist/package-build/build-package.test.js +204 -0
- package/dist/package-build/cli.d.ts +7 -0
- package/dist/package-build/cli.js +218 -0
- package/dist/package-build/contract-add.d.ts +35 -0
- package/dist/package-build/contract-add.js +97 -0
- package/dist/package-build/contract-add.test.d.ts +1 -0
- package/dist/package-build/contract-add.test.js +467 -0
- package/dist/package-build/contract-source.d.ts +59 -0
- package/dist/package-build/contract-source.js +471 -0
- package/dist/package-build/freeze-contracts.d.ts +56 -0
- package/dist/package-build/freeze-contracts.js +198 -0
- package/dist/package-build/freeze-contracts.test.d.ts +1 -0
- package/dist/package-build/freeze-contracts.test.js +355 -0
- package/dist/package-installer/package-creator.d.ts +4 -0
- package/dist/package-installer/package-creator.js +57 -16
- package/dist/package-installer/package-creator.test.js +86 -0
- package/dist/package-loader/contract-members.d.ts +53 -0
- package/dist/package-loader/contract-members.js +31 -0
- package/dist/package-loader/contract-package-loader.js +5 -1
- package/dist/package-loader/contract-package-loader.test.js +11 -0
- package/dist/package-loader/host-authority-loading.test.js +9 -5
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-artifact.d.ts +80 -17
- package/dist/package-loader/isolated-artifact.js +257 -51
- package/dist/package-loader/isolated-artifact.test.js +294 -21
- package/dist/package-loader/isolated-runtime.d.ts +22 -7
- package/dist/package-loader/isolated-table-gateway.js +30 -11
- package/dist/package-loader/isolated-table-gateway.test.js +243 -0
- package/dist/package-loader/package-loader.d.ts +8 -1
- package/dist/package-loader/package-loader.js +39 -20
- package/dist/package-loader/package-loader.test.js +47 -0
- package/dist/peer-id.d.ts +25 -0
- package/dist/peer-id.js +99 -0
- package/dist/serial-json.compat.test.d.ts +1 -0
- package/dist/serial-json.compat.test.js +369 -0
- package/dist/serial-json.d.ts +41 -5
- package/dist/serial-json.js +216 -157
- package/dist/serial-json.oracle.d.ts +21 -0
- package/dist/serial-json.oracle.js +222 -0
- package/dist/serial-json.test.js +315 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/mock-host-contract.d.ts +123 -0
- package/dist/testing/mock-host-contract.js +434 -0
- package/dist/testing/mock-host-contract.test.d.ts +1 -0
- package/dist/testing/mock-host-contract.test.js +782 -0
- package/dist/types/field-type.d.ts +35 -18
- package/dist/types/field-type.js +4 -1
- package/dist/types/zod-types.d.ts +9 -0
- package/dist/types/zod-types.js +8 -2
- package/dist/utils.d.ts +22 -25
- package/dist/utils.js +45 -92
- package/dist/utils.test.js +36 -0
- package/package.json +9 -2
- 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`)));
|