@peers-app/peers-sdk 0.26.5 → 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.
- 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/group-members.d.ts +6 -1
- package/dist/data/group-members.js +12 -4
- package/dist/data/groups.d.ts +6 -1
- package/dist/data/groups.js +12 -4
- 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/package-versions.d.ts +5 -1
- package/dist/data/package-versions.js +11 -4
- package/dist/data/packages.d.ts +7 -1
- package/dist/data/packages.js +13 -4
- 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/table-signing.test.d.ts +1 -0
- package/dist/data/table-signing.test.js +118 -0
- package/dist/data/tools.d.ts +29 -221
- package/dist/data/users.d.ts +1 -2
- package/dist/data/users.js +8 -5
- 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/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 +1 -25
- package/dist/utils.js +10 -90
- package/package.json +9 -2
- package/testing/package.json +4 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mockHostContract = mockHostContract;
|
|
4
|
+
const contract_proxy_1 = require("../contracts/contract-proxy");
|
|
5
|
+
const contract_transport_1 = require("../contracts/contract-transport");
|
|
6
|
+
const host_contract_client_1 = require("../contracts/host-contract-client");
|
|
7
|
+
const host_contract_transport_1 = require("../contracts/host-contract-transport");
|
|
8
|
+
const types_1 = require("../data/orm/types");
|
|
9
|
+
const declare_contract_1 = require("../define/declare-contract");
|
|
10
|
+
const define_package_1 = require("../define/define-package");
|
|
11
|
+
const empty_fields_1 = require("../define/empty-fields");
|
|
12
|
+
const manifest_1 = require("../define/manifest");
|
|
13
|
+
const events_1 = require("../events");
|
|
14
|
+
const observable_1 = require("../observable");
|
|
15
|
+
const isolated_value_1 = require("../package-loader/isolated-value");
|
|
16
|
+
const peer_id_1 = require("../peer-id");
|
|
17
|
+
let active;
|
|
18
|
+
/**
|
|
19
|
+
* Stand in for the local host in a UI test: `hostContract` clients for this contract (including
|
|
20
|
+
* a package's shared `news`-style client) connect to it, with no signed-in user and no
|
|
21
|
+
* provider. Tables hold rows in memory and send `dataChanged`; tools answer with fakes.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const host = mockHostContract(News, {
|
|
25
|
+
* tables: { Headlines: [{ headlineId: newid(), title: "Hello", ... }] },
|
|
26
|
+
* tools: { refresh: async () => ({ errors: [] }) },
|
|
27
|
+
* });
|
|
28
|
+
* // render the UI; it reads and writes through `news` as usual
|
|
29
|
+
* await host.tables.Headlines.save(nextHeadline); // the UI sees the change
|
|
30
|
+
* host.dispose();
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* A UI's table calls go through the table's class (`PackageTable` or the package's subclass),
|
|
34
|
+
* as on the host, with a test context: its `callOwnedTable` reaches the mock's rows, and its
|
|
35
|
+
* other calls fail. So a save runs the class's overrides and the schema, defaults included,
|
|
36
|
+
* and each change reaches subscribers through the class's `dataChanged`. Writes through
|
|
37
|
+
* `host.tables` are storage writes, with no class or schema in between; the class still
|
|
38
|
+
* filters the changes they send.
|
|
39
|
+
*
|
|
40
|
+
* Table filters are equality (`{ feedId }`) and `$in` (`{ status: { $in: ["a", "b"] } }`); a
|
|
41
|
+
* filter with other operators (`$or`, `$matchWords`, ...) or a text search fails; test those
|
|
42
|
+
* reads against a real host.
|
|
43
|
+
* `onCall` can delay or fail calls, but cannot replace their answers. Paging and `sortBy` work.
|
|
44
|
+
* Stored rows validate the manifest's fields, as host storage does. UI
|
|
45
|
+
* writes to an observable must fit its manifest value type, then any declared schema. Tool
|
|
46
|
+
* access levels aren't checked (UI calls act as the signed-in user); private tools refuse UI
|
|
47
|
+
* calls, as on the host. Disposing rejects calls still waiting for an answer. One mock host
|
|
48
|
+
* runs at a time.
|
|
49
|
+
*
|
|
50
|
+
* @param contract - The contract value from `defineContract` (tests can import it; UI code
|
|
51
|
+
* imports only its type), or a declaration from `declareContract`, whose observables start
|
|
52
|
+
* empty unless `observables` gives them values.
|
|
53
|
+
* @param options - Tool fakes, starting rows and values, data context, and a call hook.
|
|
54
|
+
*/
|
|
55
|
+
function mockHostContract(contract, options = {}) {
|
|
56
|
+
if (!(0, define_package_1.isPackageContract)(contract) && !(0, declare_contract_1.isContractDeclaration)(contract)) {
|
|
57
|
+
throw new Error("mockHostContract takes a contract from defineContract or declareContract");
|
|
58
|
+
}
|
|
59
|
+
// A declaration's members have no handlers or sources: fakes and start values stand in.
|
|
60
|
+
const members = ((0, define_package_1.isPackageContract)(contract) ? contract : contract.declaration);
|
|
61
|
+
if (active) {
|
|
62
|
+
throw new Error(`mockHostContract: a mock host for ${active.contractId} is still running; dispose it first`);
|
|
63
|
+
}
|
|
64
|
+
const { contractId, version } = members;
|
|
65
|
+
const fakes = (options.tools ?? {});
|
|
66
|
+
const startRows = (options.tables ?? {});
|
|
67
|
+
const startValues = (options.observables ?? {});
|
|
68
|
+
const tables = {};
|
|
69
|
+
for (const table of members.tables) {
|
|
70
|
+
const mock = mockTable(contractId, definitionOf(table), startRows[nameOf(table)] ?? []);
|
|
71
|
+
tables[mock.name] = mock;
|
|
72
|
+
}
|
|
73
|
+
for (const name of Object.keys(startRows)) {
|
|
74
|
+
if (!tables[name])
|
|
75
|
+
throw new Error(`mockHostContract: ${contractId} has no table '${name}'`);
|
|
76
|
+
}
|
|
77
|
+
// UI calls reach each table's class, as on the host; its storage calls reach these rows.
|
|
78
|
+
const providerContext = mockProviderContext(contractId, tables);
|
|
79
|
+
const servedTables = {};
|
|
80
|
+
for (const table of members.tables) {
|
|
81
|
+
const mock = tables[nameOf(table)];
|
|
82
|
+
servedTables[mock.name] =
|
|
83
|
+
typeof table === "function"
|
|
84
|
+
? servedThroughClass(table, mock, providerContext)
|
|
85
|
+
: mock.source;
|
|
86
|
+
}
|
|
87
|
+
const tools = {};
|
|
88
|
+
for (const tool of members.tools) {
|
|
89
|
+
const definition = definitionOf(tool);
|
|
90
|
+
const name = definition.name;
|
|
91
|
+
tools[name] = {
|
|
92
|
+
tool: {
|
|
93
|
+
name,
|
|
94
|
+
usageDescription: definition.usageDescription ?? "",
|
|
95
|
+
// No access checks: there is no signed-in user, and UI calls act as the full user.
|
|
96
|
+
accessLevel: 0,
|
|
97
|
+
},
|
|
98
|
+
toolFn: async (input) => {
|
|
99
|
+
// The host lets only the owning package's checked dispatcher reach a private tool.
|
|
100
|
+
if (definition.private) {
|
|
101
|
+
throw new Error(`mockHostContract: tool '${name}' is private; the host refuses UI calls`);
|
|
102
|
+
}
|
|
103
|
+
const fake = fakes[name];
|
|
104
|
+
if (!fake)
|
|
105
|
+
throw new Error(`mockHostContract: no fake for tool '${name}'`);
|
|
106
|
+
const schema = definition.inputSchema;
|
|
107
|
+
const result = await fake(schema ? schema.parse((0, empty_fields_1.withoutEmptySchemaFields)(schema, input ?? {})) : input);
|
|
108
|
+
return (0, empty_fields_1.withoutEmptySchemaFields)(definition.outputSchema, result);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
for (const name of Object.keys(fakes)) {
|
|
113
|
+
if (!tools[name])
|
|
114
|
+
throw new Error(`mockHostContract: ${contractId} has no tool '${name}'`);
|
|
115
|
+
}
|
|
116
|
+
const observables = {};
|
|
117
|
+
const servedObservables = {};
|
|
118
|
+
const observableWritability = {};
|
|
119
|
+
for (const member of members.observables) {
|
|
120
|
+
const definition = definitionOf(member);
|
|
121
|
+
const value = (0, observable_1.observable)(Object.hasOwn(startValues, definition.name)
|
|
122
|
+
? startValues[definition.name]
|
|
123
|
+
: servedAtStart(member, definition));
|
|
124
|
+
observables[definition.name] = value;
|
|
125
|
+
servedObservables[definition.name] = checkedWrites(value, definition.name, definition.schema
|
|
126
|
+
? (0, manifest_1.schemaValueType)(definition.name, definition.schema)
|
|
127
|
+
: definition.valueType, definition.schema);
|
|
128
|
+
observableWritability[definition.name] = definition.writable === true;
|
|
129
|
+
}
|
|
130
|
+
for (const name of Object.keys(startValues)) {
|
|
131
|
+
if (!Object.hasOwn(observables, name)) {
|
|
132
|
+
throw new Error(`mockHostContract: ${contractId} has no observable '${name}'`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const events = {};
|
|
136
|
+
for (const member of members.events) {
|
|
137
|
+
const name = nameOf(member);
|
|
138
|
+
events[name] = new events_1.Emitter(`mock-host:${contractId}:${name}:${(0, peer_id_1.newid)()}`);
|
|
139
|
+
}
|
|
140
|
+
const [consumerSide, providerSide] = (0, contract_transport_1.inProcessTransportPair)();
|
|
141
|
+
const session = (0, contract_proxy_1.createContractProviderSession)({
|
|
142
|
+
contractId,
|
|
143
|
+
version,
|
|
144
|
+
// Only the methods a contract table serves over the wire, plus `dataChanged`.
|
|
145
|
+
tables: servedTables,
|
|
146
|
+
tools,
|
|
147
|
+
observables: servedObservables,
|
|
148
|
+
observableWritability,
|
|
149
|
+
events: Object.fromEntries(Object.entries(events).map(([name, e]) => [name, e.event])),
|
|
150
|
+
}, providerSide);
|
|
151
|
+
let disposed = false;
|
|
152
|
+
const disposedError = () => new Error(`Mock host for ${contractId} is disposed`);
|
|
153
|
+
const assertActive = () => {
|
|
154
|
+
if (disposed)
|
|
155
|
+
throw disposedError();
|
|
156
|
+
};
|
|
157
|
+
/** Rejects each call still waiting (on `onCall` or a fake) when the host is disposed. */
|
|
158
|
+
const waiting = new Set();
|
|
159
|
+
const untilDisposed = (answer) => new Promise((resolve, reject) => {
|
|
160
|
+
waiting.add(reject);
|
|
161
|
+
answer()
|
|
162
|
+
.then(resolve, reject)
|
|
163
|
+
.finally(() => waiting.delete(reject));
|
|
164
|
+
});
|
|
165
|
+
// Copy supported wire values without requiring structuredClone (absent in Jest's jsdom).
|
|
166
|
+
const transport = {
|
|
167
|
+
emit: async (channel, ...args) => {
|
|
168
|
+
// Disconnect cleanup is harmless even after the provider session has ended.
|
|
169
|
+
if (channel === contract_transport_1.CONTRACT_REQUEST_CHANNEL && args[0]?.operation === "unsubscribe" && disposed)
|
|
170
|
+
return;
|
|
171
|
+
assertActive();
|
|
172
|
+
return untilDisposed(async () => {
|
|
173
|
+
const sent = (0, isolated_value_1.copyIsolatedValue)(args);
|
|
174
|
+
if (channel === contract_transport_1.CONTRACT_REQUEST_CHANNEL && options.onCall) {
|
|
175
|
+
const call = sent[0];
|
|
176
|
+
await options.onCall({
|
|
177
|
+
member: call.member,
|
|
178
|
+
name: call.name,
|
|
179
|
+
operation: call.operation,
|
|
180
|
+
args: call.args ?? [],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
assertActive();
|
|
184
|
+
const answer = await consumerSide.emit(channel, ...sent);
|
|
185
|
+
assertActive();
|
|
186
|
+
return (0, isolated_value_1.copyIsolatedValue)(answer);
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
on: (channel, handler) => consumerSide.on(channel, (...args) => handler(...(0, isolated_value_1.copyIsolatedValue)(args))),
|
|
190
|
+
};
|
|
191
|
+
const dataContextId = options.dataContextId ?? (0, peer_id_1.newid)();
|
|
192
|
+
active = { contractId };
|
|
193
|
+
(0, host_contract_client_1.setDefaultHostDataContextId)(async () => dataContextId);
|
|
194
|
+
const unregister = (0, host_contract_transport_1.registerHostContractTransport)(transport);
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
tables: Object.freeze(Object.fromEntries(Object.entries(tables).map(([name, t]) => [name, t.handle]))),
|
|
197
|
+
observables: Object.freeze({ ...observables }),
|
|
198
|
+
events: Object.freeze(Object.fromEntries(Object.entries(events).map(([name, emitter]) => [
|
|
199
|
+
name,
|
|
200
|
+
Object.freeze({
|
|
201
|
+
emit: async (payload) => {
|
|
202
|
+
await emitter.emit(payload);
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
]))),
|
|
206
|
+
dispose: () => {
|
|
207
|
+
if (disposed)
|
|
208
|
+
return;
|
|
209
|
+
disposed = true;
|
|
210
|
+
for (const reject of [...waiting])
|
|
211
|
+
reject(disposedError());
|
|
212
|
+
waiting.clear();
|
|
213
|
+
unregister();
|
|
214
|
+
session.dispose();
|
|
215
|
+
(0, host_contract_client_1.setDefaultHostDataContextId)(undefined);
|
|
216
|
+
active = undefined;
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The context a mock host's table classes run with: `callOwnedTable` reaches the mock's rows;
|
|
222
|
+
* anything else a class might call isn't available in a mock host.
|
|
223
|
+
*/
|
|
224
|
+
function mockProviderContext(contractId, tables) {
|
|
225
|
+
const callOwnedTable = async (tableName, operation, args = []) => {
|
|
226
|
+
const table = tables[tableName];
|
|
227
|
+
if (!table) {
|
|
228
|
+
throw new Error(`mockHostContract: ${contractId} has no table '${tableName}'; a mock host holds only its contract's tables`);
|
|
229
|
+
}
|
|
230
|
+
const method = table.source[operation];
|
|
231
|
+
if (typeof method !== "function") {
|
|
232
|
+
throw new Error(`mockHostContract: table '${tableName}' has no method '${operation}'`);
|
|
233
|
+
}
|
|
234
|
+
return method(...args);
|
|
235
|
+
};
|
|
236
|
+
return new Proxy({ callOwnedTable }, {
|
|
237
|
+
get(_target, property) {
|
|
238
|
+
if (property === "callOwnedTable")
|
|
239
|
+
return callOwnedTable;
|
|
240
|
+
if (typeof property !== "string" || property === "then")
|
|
241
|
+
return undefined;
|
|
242
|
+
return () => {
|
|
243
|
+
throw new Error(`mockHostContract: ctx.${property} isn't available in a mock host`);
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A table as the host serves it from a guest API 3 provider: each call goes to the table's
|
|
250
|
+
* class, and each stored change passes through the class's `dataChanged`, in order.
|
|
251
|
+
*/
|
|
252
|
+
function servedThroughClass(table, mock, ctx) {
|
|
253
|
+
const instance = () => table(ctx);
|
|
254
|
+
const served = {
|
|
255
|
+
dataChanged: {
|
|
256
|
+
subscribe(handler) {
|
|
257
|
+
let active = true;
|
|
258
|
+
let tail = Promise.resolve();
|
|
259
|
+
const subscription = mock.source.dataChanged.subscribe((event) => {
|
|
260
|
+
const snapshot = (0, isolated_value_1.copyIsolatedValue)(event);
|
|
261
|
+
tail = tail.then(async () => {
|
|
262
|
+
if (!active)
|
|
263
|
+
return;
|
|
264
|
+
try {
|
|
265
|
+
const sent = await table(ctx).dataChanged(snapshot);
|
|
266
|
+
if (active && sent !== undefined && sent !== null)
|
|
267
|
+
await handler(sent);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
console.error(`mockHostContract: '${mock.name}' dataChanged failed`, error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
unsubscribe() {
|
|
276
|
+
active = false;
|
|
277
|
+
subscription.unsubscribe();
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
for (const method of ["get", "list", "count", "findOne", "save", "delete"]) {
|
|
284
|
+
served[method] = (...args) => instance()[method](...args);
|
|
285
|
+
}
|
|
286
|
+
return served;
|
|
287
|
+
}
|
|
288
|
+
/** A member's definition: `defineTable` and the rest keep it on `definition`; IR is the member. */
|
|
289
|
+
function definitionOf(member) {
|
|
290
|
+
const value = member;
|
|
291
|
+
return value.definition ?? member;
|
|
292
|
+
}
|
|
293
|
+
function nameOf(member) {
|
|
294
|
+
return definitionOf(member).name;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* An observable's value before anything writes it, as the host serves it: a stored one's
|
|
298
|
+
* default, or what an in-memory one's source holds, which its worker publishes when it starts.
|
|
299
|
+
*/
|
|
300
|
+
function servedAtStart(member, definition) {
|
|
301
|
+
const stored = member.persistentVar;
|
|
302
|
+
if (stored)
|
|
303
|
+
return (0, isolated_value_1.copyIsolatedValue)(stored.defaultValue);
|
|
304
|
+
const source = definition.source;
|
|
305
|
+
return typeof source === "function" ? (0, isolated_value_1.copyIsolatedValue)(source()) : undefined;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* What the provider session serves for one observable: the host's value, with UI writes checked
|
|
309
|
+
* against the manifest's value type before any declared schema, so defaults and coercion
|
|
310
|
+
* cannot bypass the host's type check. The observable then keeps the parsed value, as the
|
|
311
|
+
* package does when the write reaches it (an optional field sent empty left out); IR
|
|
312
|
+
* declarations keep the value as sent. Reads and publications clean schema-described fields
|
|
313
|
+
* too, without changing the underlying source, as the package's dispatcher does.
|
|
314
|
+
*/
|
|
315
|
+
function checkedWrites(value, name, valueType, schema) {
|
|
316
|
+
const served = ((...args) => {
|
|
317
|
+
if (args.length === 0)
|
|
318
|
+
return (0, empty_fields_1.withoutEmptySchemaFields)(schema, value());
|
|
319
|
+
if (!(0, manifest_1.fitsValueType)(args[0], valueType)) {
|
|
320
|
+
throw new Error(`Observable '${name}' value must match ${valueType}`);
|
|
321
|
+
}
|
|
322
|
+
const parsed = schema ? schema.parse((0, empty_fields_1.withoutEmptySchemaFields)(schema, args[0])) : args[0];
|
|
323
|
+
return (0, empty_fields_1.withoutEmptySchemaFields)(schema, value(parsed));
|
|
324
|
+
});
|
|
325
|
+
served.subscribe = (subscriber) => value.subscribe((next) => subscriber((0, empty_fields_1.withoutEmptySchemaFields)(schema, next)));
|
|
326
|
+
served.notifySubscribers = value.notifySubscribers;
|
|
327
|
+
served.subscriberCount = value.subscriberCount;
|
|
328
|
+
return served;
|
|
329
|
+
}
|
|
330
|
+
function mockTable(contractId, definition, start) {
|
|
331
|
+
const name = definition.name;
|
|
332
|
+
const key = definition.primaryKeyName;
|
|
333
|
+
// Match isolated storage: the fields the manifest carries, never executable Zod defaults.
|
|
334
|
+
const fields = definition.schema ? (0, manifest_1.irFields)(definition.schema) : definition.fields;
|
|
335
|
+
const schema = (0, types_1.fieldsToSchema)(fields);
|
|
336
|
+
// As in host storage, an optional field that is null or undefined is left out.
|
|
337
|
+
const parse = (row) => {
|
|
338
|
+
const copied = (0, types_1.withoutEmptyFields)(fields, (0, isolated_value_1.copyIsolatedValue)(row));
|
|
339
|
+
return { ...schema.parse(copied), ...copied };
|
|
340
|
+
};
|
|
341
|
+
const rows = start.map(parse);
|
|
342
|
+
const changes = new events_1.Emitter(`mock-host:${contractId}:${name}:${(0, peer_id_1.newid)()}`);
|
|
343
|
+
const indexOf = (id) => rows.findIndex((row) => row[key] === id);
|
|
344
|
+
const query = (filter, params = {}) => {
|
|
345
|
+
for (const option of Object.keys(params)) {
|
|
346
|
+
if (!["page", "pageSize", "sortBy"].includes(option) && params[option] !== undefined) {
|
|
347
|
+
throw new Error(`mockHostContract: table '${name}' does not support query option '${option}'`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const matched = rows.filter(matches(name, filter));
|
|
351
|
+
for (const field of [...(params.sortBy ?? [])].reverse()) {
|
|
352
|
+
const desc = field.startsWith("-");
|
|
353
|
+
const by = desc ? field.slice(1) : field;
|
|
354
|
+
matched.sort((a, b) => compare(a[by], b[by]) * (desc ? -1 : 1));
|
|
355
|
+
}
|
|
356
|
+
if (params.page || params.pageSize) {
|
|
357
|
+
const page = Math.round(params.page || 1);
|
|
358
|
+
const pageSize = Math.round(params.pageSize || 100);
|
|
359
|
+
return matched.slice(pageSize * (page - 1), pageSize * page);
|
|
360
|
+
}
|
|
361
|
+
return matched;
|
|
362
|
+
};
|
|
363
|
+
const save = async (record) => {
|
|
364
|
+
const row = parse(record);
|
|
365
|
+
const at = indexOf(row[key]);
|
|
366
|
+
if (at >= 0)
|
|
367
|
+
rows[at] = row;
|
|
368
|
+
else
|
|
369
|
+
rows.push(row);
|
|
370
|
+
await changes.emit({ dataObject: row, op: at >= 0 ? "update" : "insert", source: "local" });
|
|
371
|
+
return (0, isolated_value_1.copyIsolatedValue)(row);
|
|
372
|
+
};
|
|
373
|
+
const remove = async (target) => {
|
|
374
|
+
const id = target && typeof target === "object" ? target[key] : target;
|
|
375
|
+
const at = indexOf(id);
|
|
376
|
+
if (at < 0)
|
|
377
|
+
return;
|
|
378
|
+
const [row] = rows.splice(at, 1);
|
|
379
|
+
await changes.emit({ dataObject: row, op: "delete", source: "local" });
|
|
380
|
+
};
|
|
381
|
+
const methods = {
|
|
382
|
+
list: async (filter, params) => (0, isolated_value_1.copyIsolatedValue)(query(filter, params)),
|
|
383
|
+
get: async (id) => {
|
|
384
|
+
const at = indexOf(id);
|
|
385
|
+
return at >= 0 ? (0, isolated_value_1.copyIsolatedValue)(rows[at]) : undefined;
|
|
386
|
+
},
|
|
387
|
+
count: async (filter) => query(filter).length,
|
|
388
|
+
findOne: async (filter) => (0, isolated_value_1.copyIsolatedValue)(query(filter)[0]),
|
|
389
|
+
save,
|
|
390
|
+
delete: remove,
|
|
391
|
+
};
|
|
392
|
+
const handle = Object.freeze({
|
|
393
|
+
...methods,
|
|
394
|
+
get rows() {
|
|
395
|
+
return (0, isolated_value_1.copyIsolatedValue)(rows);
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
return { name, source: { ...methods, dataChanged: changes.event }, handle };
|
|
399
|
+
}
|
|
400
|
+
/** Equality filters only: a filter with operators is refused rather than guessed at. */
|
|
401
|
+
function matches(table, filter) {
|
|
402
|
+
if (filter === undefined || filter === null)
|
|
403
|
+
return () => true;
|
|
404
|
+
const tests = Object.entries(filter).map(([field, value]) => {
|
|
405
|
+
if (!field.startsWith("$")) {
|
|
406
|
+
if (value === null || typeof value !== "object" || value instanceof Date) {
|
|
407
|
+
return (row) => same(row[field], value);
|
|
408
|
+
}
|
|
409
|
+
const candidates = value.$in;
|
|
410
|
+
if (Object.keys(value).length === 1 && Array.isArray(candidates)) {
|
|
411
|
+
return (row) => candidates.some((candidate) => same(row[field], candidate));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
throw new Error(`mockHostContract: table '${table}' supports equality and $in filters only, got ${JSON.stringify(filter)}`);
|
|
415
|
+
});
|
|
416
|
+
return (row) => tests.every((test) => test(row));
|
|
417
|
+
}
|
|
418
|
+
function same(a, b) {
|
|
419
|
+
// Optional fields are stored as SQL NULL, whether omitted or explicitly undefined.
|
|
420
|
+
if (a == null && b == null)
|
|
421
|
+
return true;
|
|
422
|
+
if (a instanceof Date && b instanceof Date)
|
|
423
|
+
return a.getTime() === b.getTime();
|
|
424
|
+
return a === b;
|
|
425
|
+
}
|
|
426
|
+
function compare(a, b) {
|
|
427
|
+
if (a === b)
|
|
428
|
+
return 0;
|
|
429
|
+
if (a === undefined || a === null)
|
|
430
|
+
return -1;
|
|
431
|
+
if (b === undefined || b === null)
|
|
432
|
+
return 1;
|
|
433
|
+
return a < b ? -1 : 1;
|
|
434
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|