@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.
- 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/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/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,1013 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const isolated_artifact_1 = require("../package-loader/isolated-artifact");
|
|
4
|
+
const peer_id_1 = require("../peer-id");
|
|
5
|
+
const index_1 = require("./index");
|
|
6
|
+
const manifest_1 = require("./manifest");
|
|
7
|
+
const packageId = (0, peer_id_1.newid)();
|
|
8
|
+
const contractId = (0, peer_id_1.newid)();
|
|
9
|
+
const otherContractId = (0, peer_id_1.newid)();
|
|
10
|
+
/** A worker ctx that records emits and publishes, and keeps the observable write handlers. */
|
|
11
|
+
function workerHost(publish = async () => null) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const saves = [];
|
|
14
|
+
const setters = new Map();
|
|
15
|
+
const ctx = {
|
|
16
|
+
emitContractEvent: async (...args) => {
|
|
17
|
+
calls.push(["emit", ...args]);
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
publishOwnedObservable: async (...args) => {
|
|
21
|
+
calls.push(["publish", ...args]);
|
|
22
|
+
return publish(args[3]);
|
|
23
|
+
},
|
|
24
|
+
onOwnedObservableSet: (id, version, name, handler) => {
|
|
25
|
+
setters.set(`${id}@${version}:${name}`, handler);
|
|
26
|
+
},
|
|
27
|
+
getOwnedPersistentVar: async () => null,
|
|
28
|
+
setOwnedPersistentVar: async (...args) => {
|
|
29
|
+
saves.push(args);
|
|
30
|
+
return args[2];
|
|
31
|
+
},
|
|
32
|
+
watchOwnedPersistentVar: async () => () => undefined,
|
|
33
|
+
};
|
|
34
|
+
return { ctx, calls, saves, setters };
|
|
35
|
+
}
|
|
36
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
37
|
+
const ping = (0, index_1.defineTool)({
|
|
38
|
+
name: "ping",
|
|
39
|
+
usageDescription: "Answers.",
|
|
40
|
+
accessLevel: index_1.AccessLevel.Reader,
|
|
41
|
+
inputSchema: index_1.z.object({}),
|
|
42
|
+
outputSchema: index_1.z.object({}),
|
|
43
|
+
handler: () => ({}),
|
|
44
|
+
});
|
|
45
|
+
const alertEvent = (0, index_1.defineEvent)({
|
|
46
|
+
name: "alert",
|
|
47
|
+
description: "Something finished.",
|
|
48
|
+
payloadSchema: index_1.z.object({
|
|
49
|
+
title: index_1.z.string().describe("Short title."),
|
|
50
|
+
kind: index_1.z.string().default("info").describe("Alert kind."),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
/** Declare a package with `fill` adding members to two provided contracts. */
|
|
54
|
+
function define(fill) {
|
|
55
|
+
const main = { events: [], observables: [] };
|
|
56
|
+
const other = { events: [], observables: [] };
|
|
57
|
+
fill(main, other);
|
|
58
|
+
return (0, index_1.definePackage)((pkg) => {
|
|
59
|
+
pkg.packageId = packageId;
|
|
60
|
+
pkg.version = "1.0.0";
|
|
61
|
+
pkg.provides((0, index_1.defineContract)({
|
|
62
|
+
contractId,
|
|
63
|
+
version: 1,
|
|
64
|
+
name: "Main",
|
|
65
|
+
description: "The main contract.",
|
|
66
|
+
tools: [ping],
|
|
67
|
+
events: main.events,
|
|
68
|
+
observables: main.observables,
|
|
69
|
+
}));
|
|
70
|
+
pkg.provides((0, index_1.defineContract)({
|
|
71
|
+
contractId: otherContractId,
|
|
72
|
+
version: 1,
|
|
73
|
+
name: "Other",
|
|
74
|
+
description: "Another contract.",
|
|
75
|
+
tools: [ping],
|
|
76
|
+
events: other.events,
|
|
77
|
+
observables: other.observables,
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
describe("outside a worker", () => {
|
|
82
|
+
test("emit checks the payload and sends nothing", async () => {
|
|
83
|
+
await expect(alertEvent.emit({ title: "Done" })).resolves.toBeUndefined();
|
|
84
|
+
await expect(alertEvent.emit({ title: 5 })).rejects.toThrow(/Expected string/);
|
|
85
|
+
const heartbeat = (0, index_1.defineEvent)({ name: "heartbeat", description: "No payload." });
|
|
86
|
+
await expect(heartbeat.emit()).resolves.toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
test("emit checks what the bridge can carry, without structuredClone (jsdom lacks it)", async () => {
|
|
89
|
+
const event = (0, index_1.defineEvent)({
|
|
90
|
+
name: "anything",
|
|
91
|
+
description: "An untyped payload.",
|
|
92
|
+
payloadSchema: index_1.z.object({ value: index_1.z.any() }),
|
|
93
|
+
});
|
|
94
|
+
const global = globalThis;
|
|
95
|
+
const original = global.structuredClone;
|
|
96
|
+
delete global.structuredClone;
|
|
97
|
+
try {
|
|
98
|
+
const shared = { label: "same" };
|
|
99
|
+
await expect(event.emit({ value: { a: shared, b: [shared], when: new Date(0) } })).resolves.toBeUndefined();
|
|
100
|
+
await expect(event.emit({ value: new Map() })).rejects.toThrow("value.value is a Map, which can't be sent");
|
|
101
|
+
await expect(event.emit({ value: [() => 1] })).rejects.toThrow("value.value[0] is a function, which can't be sent");
|
|
102
|
+
await expect(event.emit({ value: Number.NaN })).rejects.toThrow(/only finite numbers/);
|
|
103
|
+
const cycle = {};
|
|
104
|
+
cycle.self = cycle;
|
|
105
|
+
await expect(event.emit({ value: cycle })).rejects.toThrow(/cycles can't be sent/);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
global.structuredClone = original;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
test("an object payload schema rejects null instead of treating it as an empty object", async () => {
|
|
112
|
+
const event = (0, index_1.defineEvent)({
|
|
113
|
+
name: "defaults",
|
|
114
|
+
description: "An object payload.",
|
|
115
|
+
payloadSchema: index_1.z.object({ kind: index_1.z.string().default("info") }),
|
|
116
|
+
});
|
|
117
|
+
await expect(event.emit(null)).rejects.toThrow(/Expected object/);
|
|
118
|
+
});
|
|
119
|
+
test("an event or observable is checked when it's declared", () => {
|
|
120
|
+
expect(() => (0, index_1.defineEvent)({ name: "", description: "" })).toThrow(/requires a name/);
|
|
121
|
+
expect(() => (0, index_1.defineEvent)({
|
|
122
|
+
name: "x",
|
|
123
|
+
description: "",
|
|
124
|
+
payloadSchema: index_1.z.object({}),
|
|
125
|
+
payloadFields: [],
|
|
126
|
+
})).toThrow(/either payloadSchema or payloadFields/);
|
|
127
|
+
const source = (0, index_1.observable)(0);
|
|
128
|
+
expect(() => (0, index_1.defineObservable)({ name: "count", description: "Count.", source })).toThrow(/either a schema or a valueType/);
|
|
129
|
+
expect(() => (0, index_1.defineObservable)({
|
|
130
|
+
name: "count",
|
|
131
|
+
description: "Count.",
|
|
132
|
+
schema: index_1.z.number(),
|
|
133
|
+
source: 0,
|
|
134
|
+
})).toThrow(/needs a source/);
|
|
135
|
+
expect(() => (0, index_1.defineObservable)({
|
|
136
|
+
name: "count",
|
|
137
|
+
description: "Count.",
|
|
138
|
+
schema: index_1.z.number(),
|
|
139
|
+
source: (0, index_1.deviceVar)(`no-default-${(0, peer_id_1.newid)()}`),
|
|
140
|
+
})).toThrow(/needs a defaultValue/);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
test("an emit made before the worker starts waits for the start", async () => {
|
|
144
|
+
const definition = define((main) => {
|
|
145
|
+
main.events = [alertEvent];
|
|
146
|
+
});
|
|
147
|
+
const global = globalThis;
|
|
148
|
+
global.onWorkerStart = () => undefined;
|
|
149
|
+
try {
|
|
150
|
+
const host = workerHost();
|
|
151
|
+
const pending = alertEvent.emit({ title: "Early" });
|
|
152
|
+
await settle();
|
|
153
|
+
expect(host.calls).toEqual([]);
|
|
154
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
155
|
+
await pending;
|
|
156
|
+
expect(host.calls).toEqual([
|
|
157
|
+
["emit", contractId, 1, "alert", { title: "Early", kind: "info" }],
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
delete global.onWorkerStart;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
describe("the manifest", () => {
|
|
165
|
+
test.each([
|
|
166
|
+
index_1.z.object({ note: index_1.z.string().optional() }).nullable().readonly(),
|
|
167
|
+
index_1.z.object({ note: index_1.z.string().optional() }).nullable().brand(),
|
|
168
|
+
index_1.z
|
|
169
|
+
.object({ note: index_1.z.string().optional() })
|
|
170
|
+
.nullable()
|
|
171
|
+
.refine(() => true),
|
|
172
|
+
index_1.z.object({ note: index_1.z.string().optional() }).nullable().default(null),
|
|
173
|
+
])("a wrapped nullable whole observable value is allowed (%#)", (schema) => {
|
|
174
|
+
const definition = define((main) => {
|
|
175
|
+
main.observables = [
|
|
176
|
+
(0, index_1.defineObservable)({
|
|
177
|
+
name: "maybe",
|
|
178
|
+
description: "May have no value.",
|
|
179
|
+
schema,
|
|
180
|
+
source: (0, index_1.observable)(null),
|
|
181
|
+
}),
|
|
182
|
+
];
|
|
183
|
+
});
|
|
184
|
+
const manifest = (0, manifest_1.buildPackageManifest)(definition);
|
|
185
|
+
expect(manifest.provides[0].observables?.[0].value).toMatchObject({
|
|
186
|
+
type: "object",
|
|
187
|
+
optional: true,
|
|
188
|
+
fields: [{ name: "note", type: "string", optional: true }],
|
|
189
|
+
});
|
|
190
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)({ manifest, providerSource: "void 0;" }, { strict: true })).not.toThrow();
|
|
191
|
+
});
|
|
192
|
+
test("nullable fields inside a nullable observable still fail the build", () => {
|
|
193
|
+
const definition = define((main) => {
|
|
194
|
+
main.observables = [
|
|
195
|
+
(0, index_1.defineObservable)({
|
|
196
|
+
name: "maybe",
|
|
197
|
+
description: "May have no value.",
|
|
198
|
+
schema: index_1.z.object({ note: index_1.z.string().nullable() }).nullable().readonly(),
|
|
199
|
+
source: (0, index_1.observable)(null),
|
|
200
|
+
}),
|
|
201
|
+
];
|
|
202
|
+
});
|
|
203
|
+
expect(() => (0, manifest_1.buildPackageManifest)(definition)).toThrow('Observable maybe value: Field "value.note" uses .nullable()');
|
|
204
|
+
});
|
|
205
|
+
test("an IR event configuration may leave payloadSchema undefined", () => {
|
|
206
|
+
const options = {
|
|
207
|
+
name: "configured",
|
|
208
|
+
description: "Configured from IR.",
|
|
209
|
+
payloadSchema: undefined,
|
|
210
|
+
payloadFields: [{ name: "title", type: index_1.FieldType.string }],
|
|
211
|
+
};
|
|
212
|
+
const event = (0, index_1.defineEvent)(options);
|
|
213
|
+
const definition = define((main) => {
|
|
214
|
+
main.events = [event];
|
|
215
|
+
});
|
|
216
|
+
expect((0, manifest_1.buildPackageManifest)(definition).provides[0].events).toEqual([
|
|
217
|
+
{
|
|
218
|
+
name: options.name,
|
|
219
|
+
description: options.description,
|
|
220
|
+
payloadFields: options.payloadFields,
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
test("event fields describe parsed output through wrappers, transforms, and arrays", () => {
|
|
225
|
+
const event = (0, index_1.defineEvent)({
|
|
226
|
+
name: "shapes",
|
|
227
|
+
description: "Parsed event values.",
|
|
228
|
+
payloadSchema: index_1.z.object({
|
|
229
|
+
maybe: index_1.z.string().optional().brand().readonly(),
|
|
230
|
+
detail: index_1.z.string().nullish(),
|
|
231
|
+
size: index_1.z.string().transform((value) => value.length),
|
|
232
|
+
matrix: index_1.z.array(index_1.z.array(index_1.z.number())),
|
|
233
|
+
nullableItems: index_1.z.array(index_1.z.string().nullable()),
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
const definition = define((main) => {
|
|
237
|
+
main.events = [event];
|
|
238
|
+
});
|
|
239
|
+
expect((0, manifest_1.buildPackageManifest)(definition).provides[0].events?.[0].payloadFields).toEqual([
|
|
240
|
+
{ name: "maybe", type: "string", optional: true },
|
|
241
|
+
{ name: "detail", type: "string", optional: true },
|
|
242
|
+
{ name: "size", type: "any", optional: true },
|
|
243
|
+
{ name: "matrix", type: "any", isArray: true },
|
|
244
|
+
{ name: "nullableItems", type: "any", isArray: true },
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
test("nested event fields use parsed output rules at every level", () => {
|
|
248
|
+
const event = (0, index_1.defineEvent)({
|
|
249
|
+
name: "nested",
|
|
250
|
+
description: "Nested parsed values.",
|
|
251
|
+
payloadSchema: index_1.z.object({
|
|
252
|
+
detail: index_1.z.object({
|
|
253
|
+
changed: index_1.z.string().transform((value) => value.length),
|
|
254
|
+
piped: index_1.z.string().transform(Number).pipe(index_1.z.number()),
|
|
255
|
+
items: index_1.z.array(index_1.z.object({ matrix: index_1.z.array(index_1.z.array(index_1.z.number())) })),
|
|
256
|
+
modes: index_1.z.array(index_1.z.enum(["on", "off"]).nullable()),
|
|
257
|
+
}),
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
const definition = define((main) => {
|
|
261
|
+
main.events = [event];
|
|
262
|
+
});
|
|
263
|
+
const manifest = (0, manifest_1.buildPackageManifest)(definition);
|
|
264
|
+
expect(manifest.provides[0].events?.[0].payloadFields).toEqual([
|
|
265
|
+
{
|
|
266
|
+
name: "detail",
|
|
267
|
+
type: "object",
|
|
268
|
+
fields: [
|
|
269
|
+
{ name: "changed", type: "any", optional: true },
|
|
270
|
+
{ name: "piped", type: "number" },
|
|
271
|
+
{
|
|
272
|
+
name: "items",
|
|
273
|
+
type: "object",
|
|
274
|
+
isArray: true,
|
|
275
|
+
fields: [{ name: "matrix", type: "any", isArray: true }],
|
|
276
|
+
},
|
|
277
|
+
{ name: "modes", type: "any", isArray: true },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
]);
|
|
281
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)({ manifest, providerSource: "void 0;" }, { strict: true })).not.toThrow();
|
|
282
|
+
});
|
|
283
|
+
test("carries events and observables as IR", () => {
|
|
284
|
+
const location = (0, index_1.groupVar)(`location-${(0, peer_id_1.newid)()}`, {
|
|
285
|
+
defaultValue: { query: "", unit: "F" },
|
|
286
|
+
});
|
|
287
|
+
const conditions = (0, index_1.observable)(null);
|
|
288
|
+
const definition = define((main, other) => {
|
|
289
|
+
main.events = [
|
|
290
|
+
alertEvent,
|
|
291
|
+
(0, index_1.defineEvent)({ name: "ping", description: "No payload." }),
|
|
292
|
+
{ name: "raw", description: "IR as is.", payloadFields: [] },
|
|
293
|
+
];
|
|
294
|
+
main.observables = [
|
|
295
|
+
(0, index_1.defineObservable)({
|
|
296
|
+
name: "location",
|
|
297
|
+
description: "Saved city.",
|
|
298
|
+
schema: index_1.z.object({ query: index_1.z.string(), unit: index_1.z.enum(["F", "C"]) }),
|
|
299
|
+
writable: true,
|
|
300
|
+
source: location,
|
|
301
|
+
}),
|
|
302
|
+
(0, index_1.defineObservable)({
|
|
303
|
+
name: "conditions",
|
|
304
|
+
description: "Latest reading.",
|
|
305
|
+
schema: index_1.z.object({ temp: index_1.z.number() }).nullable(),
|
|
306
|
+
source: conditions,
|
|
307
|
+
}),
|
|
308
|
+
(0, index_1.defineObservable)({
|
|
309
|
+
name: "recent",
|
|
310
|
+
description: "Recent cities.",
|
|
311
|
+
schema: index_1.z.array(index_1.z.string()),
|
|
312
|
+
source: (0, index_1.observable)([]),
|
|
313
|
+
}),
|
|
314
|
+
(0, index_1.defineObservable)({
|
|
315
|
+
name: "status",
|
|
316
|
+
description: "IR value type.",
|
|
317
|
+
valueType: index_1.FieldType.string,
|
|
318
|
+
source: (0, index_1.observable)("idle"),
|
|
319
|
+
}),
|
|
320
|
+
(0, index_1.defineObservable)({
|
|
321
|
+
name: "mode",
|
|
322
|
+
description: "An enum.",
|
|
323
|
+
schema: index_1.z.enum(["a", "b"]).default("a"),
|
|
324
|
+
source: (0, index_1.observable)("a"),
|
|
325
|
+
}),
|
|
326
|
+
(0, index_1.defineObservable)({
|
|
327
|
+
name: "answer",
|
|
328
|
+
description: "A literal.",
|
|
329
|
+
schema: index_1.z.literal(42),
|
|
330
|
+
source: (0, index_1.observable)(42),
|
|
331
|
+
}),
|
|
332
|
+
(0, index_1.defineObservable)({
|
|
333
|
+
name: "owner",
|
|
334
|
+
description: "A peer id.",
|
|
335
|
+
schema: index_1.zodPeerId,
|
|
336
|
+
source: (0, index_1.observable)((0, peer_id_1.newid)()),
|
|
337
|
+
}),
|
|
338
|
+
];
|
|
339
|
+
other.observables = [
|
|
340
|
+
{
|
|
341
|
+
name: "skill",
|
|
342
|
+
description: "IR with its variable.",
|
|
343
|
+
valueType: index_1.FieldType.object,
|
|
344
|
+
writable: false,
|
|
345
|
+
persistentVar: { scope: "device", name: "skill", defaultValue: {} },
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
});
|
|
349
|
+
const manifest = (0, manifest_1.buildPackageManifest)(definition);
|
|
350
|
+
const [main, other] = manifest.provides;
|
|
351
|
+
expect(main.events).toEqual([
|
|
352
|
+
{
|
|
353
|
+
name: "alert",
|
|
354
|
+
description: "Something finished.",
|
|
355
|
+
payloadFields: [
|
|
356
|
+
{ name: "title", type: "string", description: "Short title." },
|
|
357
|
+
{ name: "kind", type: "string", description: "Alert kind." },
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
{ name: "ping", description: "No payload.", payloadFields: [] },
|
|
361
|
+
{ name: "raw", description: "IR as is.", payloadFields: [] },
|
|
362
|
+
]);
|
|
363
|
+
expect(main.observables).toEqual([
|
|
364
|
+
{
|
|
365
|
+
name: "location",
|
|
366
|
+
description: "Saved city.",
|
|
367
|
+
valueType: "object",
|
|
368
|
+
value: {
|
|
369
|
+
type: "object",
|
|
370
|
+
fields: [
|
|
371
|
+
{ name: "query", type: "string" },
|
|
372
|
+
{ name: "unit", type: "string", enumValues: ["F", "C"] },
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
writable: true,
|
|
376
|
+
persistentVar: {
|
|
377
|
+
scope: "group",
|
|
378
|
+
name: expect.stringMatching(/^location-/),
|
|
379
|
+
defaultValue: { query: "", unit: "F" },
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "conditions",
|
|
384
|
+
description: "Latest reading.",
|
|
385
|
+
valueType: "any",
|
|
386
|
+
// The full shape says what `valueType` can't: an object, or empty (null).
|
|
387
|
+
value: { type: "object", optional: true, fields: [{ name: "temp", type: "number" }] },
|
|
388
|
+
writable: false,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "recent",
|
|
392
|
+
description: "Recent cities.",
|
|
393
|
+
valueType: "object",
|
|
394
|
+
value: { type: "string", isArray: true },
|
|
395
|
+
writable: false,
|
|
396
|
+
},
|
|
397
|
+
{ name: "status", description: "IR value type.", valueType: "string", writable: false },
|
|
398
|
+
{
|
|
399
|
+
name: "mode",
|
|
400
|
+
description: "An enum.",
|
|
401
|
+
valueType: "string",
|
|
402
|
+
value: { type: "string", enumValues: ["a", "b"] },
|
|
403
|
+
writable: false,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "answer",
|
|
407
|
+
description: "A literal.",
|
|
408
|
+
valueType: "number",
|
|
409
|
+
value: { type: "number", enumValues: [42] },
|
|
410
|
+
writable: false,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "owner",
|
|
414
|
+
description: "A peer id.",
|
|
415
|
+
valueType: "id",
|
|
416
|
+
value: { type: "id" },
|
|
417
|
+
writable: false,
|
|
418
|
+
},
|
|
419
|
+
]);
|
|
420
|
+
expect(other.observables).toEqual([
|
|
421
|
+
{
|
|
422
|
+
name: "skill",
|
|
423
|
+
description: "IR with its variable.",
|
|
424
|
+
valueType: "object",
|
|
425
|
+
writable: false,
|
|
426
|
+
persistentVar: { scope: "device", name: "skill", defaultValue: {} },
|
|
427
|
+
},
|
|
428
|
+
]);
|
|
429
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)({ manifest, providerSource: "x" })).not.toThrow();
|
|
430
|
+
});
|
|
431
|
+
test("transforms use any without executing schema code to infer the wire type", () => {
|
|
432
|
+
const transform = jest.fn((value) => value.length);
|
|
433
|
+
const refine = jest.fn(() => true);
|
|
434
|
+
const definition = define((main) => {
|
|
435
|
+
main.observables = [
|
|
436
|
+
(0, index_1.defineObservable)({
|
|
437
|
+
name: "length",
|
|
438
|
+
description: "A transformed value.",
|
|
439
|
+
schema: index_1.z.string().transform(transform),
|
|
440
|
+
writable: true,
|
|
441
|
+
source: (0, index_1.observable)(0),
|
|
442
|
+
}),
|
|
443
|
+
(0, index_1.defineObservable)({
|
|
444
|
+
name: "optional",
|
|
445
|
+
description: "An optional refined value.",
|
|
446
|
+
schema: index_1.z.string().optional().refine(refine),
|
|
447
|
+
source: (0, index_1.observable)(),
|
|
448
|
+
}),
|
|
449
|
+
];
|
|
450
|
+
});
|
|
451
|
+
expect((0, manifest_1.buildPackageManifest)(definition).provides[0].observables?.map((o) => o.valueType)).toEqual([index_1.FieldType.any, index_1.FieldType.any]);
|
|
452
|
+
expect(transform).not.toHaveBeenCalled();
|
|
453
|
+
expect(refine).not.toHaveBeenCalled();
|
|
454
|
+
});
|
|
455
|
+
test.each([
|
|
456
|
+
index_1.z.date().nullable(),
|
|
457
|
+
index_1.z.bigint().optional(),
|
|
458
|
+
index_1.z.array(index_1.z.date()),
|
|
459
|
+
index_1.z.object({ nested: index_1.z.map(index_1.z.string(), index_1.z.number()) }),
|
|
460
|
+
index_1.z.record(index_1.z.set(index_1.z.string())),
|
|
461
|
+
index_1.z.tuple([index_1.z.string(), index_1.z.symbol()]),
|
|
462
|
+
index_1.z.union([index_1.z.string(), index_1.z.date()]),
|
|
463
|
+
index_1.z.lazy(() => index_1.z.object({ nested: index_1.z.date() })),
|
|
464
|
+
index_1.z.literal(1n),
|
|
465
|
+
index_1.z.nan(),
|
|
466
|
+
])("rejects non-JSON schemas inside wrappers and containers (%#)", (schema) => {
|
|
467
|
+
const definition = define((main) => {
|
|
468
|
+
main.observables = [
|
|
469
|
+
(0, index_1.defineObservable)({
|
|
470
|
+
name: "invalid",
|
|
471
|
+
description: "Non-JSON.",
|
|
472
|
+
schema,
|
|
473
|
+
source: (0, index_1.observable)(null),
|
|
474
|
+
}),
|
|
475
|
+
];
|
|
476
|
+
});
|
|
477
|
+
expect(() => (0, manifest_1.buildPackageManifest)(definition)).toThrow(/its value must be JSON/);
|
|
478
|
+
});
|
|
479
|
+
test("rejects what an observable can't hold or a contract can't list", () => {
|
|
480
|
+
const withObservable = (item) => define((main) => {
|
|
481
|
+
main.observables = [item];
|
|
482
|
+
});
|
|
483
|
+
expect(() => (0, manifest_1.buildPackageManifest)(withObservable((0, index_1.defineObservable)({
|
|
484
|
+
name: "when",
|
|
485
|
+
description: "A date.",
|
|
486
|
+
schema: index_1.z.date(),
|
|
487
|
+
source: (0, index_1.observable)(new Date()),
|
|
488
|
+
})))).toThrow(/ZodDate schema; its value must be JSON/);
|
|
489
|
+
expect(() => (0, manifest_1.buildPackageManifest)(withObservable((0, index_1.defineObservable)({
|
|
490
|
+
name: "unit",
|
|
491
|
+
description: "Unit.",
|
|
492
|
+
schema: index_1.z.enum(["F", "C"]),
|
|
493
|
+
source: (0, index_1.deviceVar)(`unit-${(0, peer_id_1.newid)()}`, { defaultValue: "K" }),
|
|
494
|
+
})))).toThrow(/defaultValue doesn't match the schema/);
|
|
495
|
+
// The worker publishes an in-memory source's value at start; one the host would reject
|
|
496
|
+
// fails the build rather than every worker start.
|
|
497
|
+
expect(() => (0, manifest_1.buildPackageManifest)(withObservable((0, index_1.defineObservable)({
|
|
498
|
+
name: "label",
|
|
499
|
+
description: "Unset label.",
|
|
500
|
+
schema: index_1.z.string(),
|
|
501
|
+
source: (0, index_1.observable)(),
|
|
502
|
+
})))).toThrow(/label's source starts as undefined, which doesn't match its valueType string/);
|
|
503
|
+
expect(() => (0, manifest_1.buildPackageManifest)(withObservable((0, index_1.defineObservable)({
|
|
504
|
+
name: "reading",
|
|
505
|
+
description: "No reading yet.",
|
|
506
|
+
schema: index_1.z.object({ temp: index_1.z.number() }),
|
|
507
|
+
source: (0, index_1.observable)(null),
|
|
508
|
+
})))).toThrow(/reading's source starts as null, which doesn't match its valueType object/);
|
|
509
|
+
expect(() => (0, manifest_1.buildPackageManifest)(withObservable((0, index_1.defineObservable)({
|
|
510
|
+
name: "count",
|
|
511
|
+
description: "IR value type.",
|
|
512
|
+
valueType: index_1.FieldType.number,
|
|
513
|
+
source: (0, index_1.observable)("zero"),
|
|
514
|
+
})))).toThrow(/count's source starts as a string, which doesn't match its valueType number/);
|
|
515
|
+
expect(() => withObservable({ name: "status", description: "No binding.", valueType: "string" })).toThrow(/IR without a persistentVar/);
|
|
516
|
+
expect(() => withObservable({ description: "No name." })).toThrow(/isn't from defineObservable/);
|
|
517
|
+
const status = (0, index_1.defineObservable)({
|
|
518
|
+
name: "status",
|
|
519
|
+
description: "Status.",
|
|
520
|
+
valueType: index_1.FieldType.string,
|
|
521
|
+
source: (0, index_1.observable)("idle"),
|
|
522
|
+
});
|
|
523
|
+
expect(() => define((main) => {
|
|
524
|
+
main.observables = [status, status];
|
|
525
|
+
})).toThrow(/observable "status" twice/);
|
|
526
|
+
expect(() => define((main) => {
|
|
527
|
+
main.events = [alertEvent, { name: "alert", description: "Again." }];
|
|
528
|
+
})).toThrow(/event "alert" twice/);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
describe("in a worker", () => {
|
|
532
|
+
test("emits omit undefined fields and snapshot optional array entries without losing wire types", async () => {
|
|
533
|
+
const event = (0, index_1.defineEvent)({
|
|
534
|
+
name: "optional",
|
|
535
|
+
description: "Optional payload values.",
|
|
536
|
+
payloadSchema: index_1.z.object({
|
|
537
|
+
extra: index_1.z.string().optional(),
|
|
538
|
+
nested: index_1.z.object({ extra: index_1.z.string().optional() }),
|
|
539
|
+
items: index_1.z.array(index_1.z.string().optional()),
|
|
540
|
+
when: index_1.z.date(),
|
|
541
|
+
bytes: index_1.z.instanceof(Uint8Array),
|
|
542
|
+
}),
|
|
543
|
+
});
|
|
544
|
+
const definition = define((main) => {
|
|
545
|
+
main.events = [event];
|
|
546
|
+
});
|
|
547
|
+
const host = workerHost();
|
|
548
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
549
|
+
const when = new Date("2026-10-21T00:00:00Z");
|
|
550
|
+
const bytes = new Uint8Array([1, 2]);
|
|
551
|
+
const payload = {
|
|
552
|
+
extra: undefined,
|
|
553
|
+
nested: { extra: undefined },
|
|
554
|
+
items: [undefined],
|
|
555
|
+
when,
|
|
556
|
+
bytes,
|
|
557
|
+
};
|
|
558
|
+
const sent = event.emit(payload);
|
|
559
|
+
bytes[0] = 3;
|
|
560
|
+
await sent;
|
|
561
|
+
expect(host.calls).toEqual([
|
|
562
|
+
[
|
|
563
|
+
"emit",
|
|
564
|
+
contractId,
|
|
565
|
+
1,
|
|
566
|
+
"optional",
|
|
567
|
+
{ nested: {}, items: [null], when, bytes: new Uint8Array([1, 2]) },
|
|
568
|
+
],
|
|
569
|
+
]);
|
|
570
|
+
expect(Object.hasOwn(payload, "extra")).toBe(true);
|
|
571
|
+
expect(payload.nested).toEqual({ extra: undefined });
|
|
572
|
+
expect(payload.items).toEqual([undefined]);
|
|
573
|
+
});
|
|
574
|
+
test("emits, writes, and publications leave out empty optional fields", async () => {
|
|
575
|
+
const event = (0, index_1.defineEvent)({
|
|
576
|
+
name: "rated",
|
|
577
|
+
description: "A rating changed.",
|
|
578
|
+
payloadSchema: index_1.z.object({
|
|
579
|
+
note: index_1.z.string().optional(),
|
|
580
|
+
where: index_1.z.object({ zip: index_1.z.string().optional() }),
|
|
581
|
+
count: index_1.z.number().default(1),
|
|
582
|
+
}),
|
|
583
|
+
});
|
|
584
|
+
const place = (0, index_1.observable)({
|
|
585
|
+
query: "a",
|
|
586
|
+
unit: null,
|
|
587
|
+
});
|
|
588
|
+
const definition = define((main) => {
|
|
589
|
+
main.events = [event];
|
|
590
|
+
main.observables = [
|
|
591
|
+
(0, index_1.defineObservable)({
|
|
592
|
+
name: "place",
|
|
593
|
+
description: "Saved place.",
|
|
594
|
+
schema: index_1.z.object({ query: index_1.z.string(), unit: index_1.z.enum(["F", "C"]).optional() }),
|
|
595
|
+
writable: true,
|
|
596
|
+
source: place,
|
|
597
|
+
}),
|
|
598
|
+
];
|
|
599
|
+
});
|
|
600
|
+
const host = workerHost();
|
|
601
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
602
|
+
expect(host.calls).toEqual([["publish", contractId, 1, "place", { query: "a" }]]);
|
|
603
|
+
await event.emit({ note: null, where: { zip: null }, count: null });
|
|
604
|
+
expect(host.calls.at(-1)).toEqual(["emit", contractId, 1, "rated", { where: {}, count: 1 }]);
|
|
605
|
+
const write = host.setters.get(`${contractId}@1:place`);
|
|
606
|
+
await expect(Promise.resolve(write?.({ query: "b", unit: null }))).resolves.toEqual({
|
|
607
|
+
query: "b",
|
|
608
|
+
});
|
|
609
|
+
expect(place()).toEqual({ query: "b" });
|
|
610
|
+
});
|
|
611
|
+
test("array observables clean their object fields on publication and write", async () => {
|
|
612
|
+
const source = (0, index_1.observable)([{ note: null }]);
|
|
613
|
+
const definition = define((main) => {
|
|
614
|
+
main.observables = [
|
|
615
|
+
(0, index_1.defineObservable)({
|
|
616
|
+
name: "items",
|
|
617
|
+
description: "Items.",
|
|
618
|
+
schema: index_1.z.array(index_1.z.object({ note: index_1.z.string().optional() })),
|
|
619
|
+
writable: true,
|
|
620
|
+
source,
|
|
621
|
+
}),
|
|
622
|
+
];
|
|
623
|
+
});
|
|
624
|
+
const host = workerHost();
|
|
625
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
626
|
+
expect(host.calls).toEqual([["publish", contractId, 1, "items", [{}]]]);
|
|
627
|
+
const write = host.setters.get(`${contractId}@1:items`);
|
|
628
|
+
await expect(Promise.resolve(write?.([{ note: null }]))).resolves.toStrictEqual([{}]);
|
|
629
|
+
expect(source()).toStrictEqual([{}]);
|
|
630
|
+
});
|
|
631
|
+
test("variables load before the initial publication of sources derived from them", async () => {
|
|
632
|
+
const saved = (0, index_1.deviceVar)(`derived-${(0, peer_id_1.newid)()}`, { defaultValue: 0 });
|
|
633
|
+
const source = (0, index_1.observable)(0);
|
|
634
|
+
const subscription = saved.subscribe(source);
|
|
635
|
+
const definition = define((main) => {
|
|
636
|
+
main.observables = [
|
|
637
|
+
(0, index_1.defineObservable)({
|
|
638
|
+
name: "derived",
|
|
639
|
+
description: "Derived from stored state.",
|
|
640
|
+
schema: index_1.z.number(),
|
|
641
|
+
source,
|
|
642
|
+
}),
|
|
643
|
+
];
|
|
644
|
+
});
|
|
645
|
+
let release = () => { };
|
|
646
|
+
const gate = new Promise((resolve) => {
|
|
647
|
+
release = resolve;
|
|
648
|
+
});
|
|
649
|
+
const host = workerHost(async (value) => {
|
|
650
|
+
if (value === 7)
|
|
651
|
+
await gate;
|
|
652
|
+
});
|
|
653
|
+
host.ctx.getOwnedPersistentVar = async () => 7;
|
|
654
|
+
let started = false;
|
|
655
|
+
const starting = (0, index_1.startPackage)(definition, host.ctx).then(() => {
|
|
656
|
+
started = true;
|
|
657
|
+
});
|
|
658
|
+
try {
|
|
659
|
+
await settle();
|
|
660
|
+
expect(host.calls).toEqual([["publish", contractId, 1, "derived", 7]]);
|
|
661
|
+
expect(started).toBe(false);
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
release();
|
|
665
|
+
await starting;
|
|
666
|
+
subscription.dispose();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
test("onStart hooks run in order after variables load, before the first publication", async () => {
|
|
670
|
+
const saved = (0, index_1.deviceVar)(`hooked-${(0, peer_id_1.newid)()}`, { defaultValue: 0 });
|
|
671
|
+
const source = (0, index_1.observable)(0);
|
|
672
|
+
const seen = [];
|
|
673
|
+
const host = workerHost();
|
|
674
|
+
host.ctx.getOwnedPersistentVar = async () => 7;
|
|
675
|
+
const definition = (0, index_1.definePackage)((pkg) => {
|
|
676
|
+
pkg.packageId = packageId;
|
|
677
|
+
pkg.version = "1.0.0";
|
|
678
|
+
pkg.provides((0, index_1.defineContract)({
|
|
679
|
+
contractId,
|
|
680
|
+
version: 1,
|
|
681
|
+
name: "Main",
|
|
682
|
+
description: "The main contract.",
|
|
683
|
+
tools: [ping],
|
|
684
|
+
observables: [
|
|
685
|
+
(0, index_1.defineObservable)({
|
|
686
|
+
name: "loaded",
|
|
687
|
+
description: "Set at the start.",
|
|
688
|
+
schema: index_1.z.number(),
|
|
689
|
+
source,
|
|
690
|
+
}),
|
|
691
|
+
],
|
|
692
|
+
}));
|
|
693
|
+
pkg.onStart(async (ctx) => {
|
|
694
|
+
seen.push(["first", saved(), ctx === host.ctx]);
|
|
695
|
+
await settle();
|
|
696
|
+
source(saved() + 1);
|
|
697
|
+
});
|
|
698
|
+
pkg.onStart(() => {
|
|
699
|
+
seen.push(["second", source()]);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
703
|
+
expect(seen).toEqual([
|
|
704
|
+
["first", 7, true],
|
|
705
|
+
["second", 8],
|
|
706
|
+
]);
|
|
707
|
+
// Readers' first value is the one the hook set.
|
|
708
|
+
expect(host.calls).toEqual([["publish", contractId, 1, "loaded", 8]]);
|
|
709
|
+
});
|
|
710
|
+
test("a failing onStart hook fails the start, and onStart takes only functions", async () => {
|
|
711
|
+
const definition = (0, index_1.definePackage)((pkg) => {
|
|
712
|
+
pkg.packageId = packageId;
|
|
713
|
+
pkg.version = "1.0.0";
|
|
714
|
+
pkg.onStart(() => {
|
|
715
|
+
throw new Error("no cache");
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
await expect((0, index_1.startPackage)(definition, workerHost().ctx)).rejects.toThrow("no cache");
|
|
719
|
+
expect(() => (0, index_1.definePackage)((pkg) => {
|
|
720
|
+
pkg.packageId = packageId;
|
|
721
|
+
pkg.version = "1.0.0";
|
|
722
|
+
pkg.onStart("later");
|
|
723
|
+
})).toThrow("pkg.onStart takes a function");
|
|
724
|
+
});
|
|
725
|
+
test("emit parses the payload and sends it on every contract that lists the event, in order", async () => {
|
|
726
|
+
const unlisted = (0, index_1.defineEvent)({ name: "unlisted", description: "On no contract." });
|
|
727
|
+
const definition = define((main, other) => {
|
|
728
|
+
main.events = [alertEvent];
|
|
729
|
+
other.events = [alertEvent];
|
|
730
|
+
});
|
|
731
|
+
const host = workerHost();
|
|
732
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
733
|
+
const first = alertEvent.emit({ title: "One" });
|
|
734
|
+
const second = alertEvent.emit({ title: "Two", kind: "timer" });
|
|
735
|
+
await Promise.all([first, second]);
|
|
736
|
+
expect(host.calls).toEqual([
|
|
737
|
+
["emit", contractId, 1, "alert", { title: "One", kind: "info" }],
|
|
738
|
+
["emit", otherContractId, 1, "alert", { title: "One", kind: "info" }],
|
|
739
|
+
["emit", contractId, 1, "alert", { title: "Two", kind: "timer" }],
|
|
740
|
+
["emit", otherContractId, 1, "alert", { title: "Two", kind: "timer" }],
|
|
741
|
+
]);
|
|
742
|
+
await expect(alertEvent.emit({})).rejects.toThrow(/Required/);
|
|
743
|
+
await expect(unlisted.emit(undefined)).rejects.toThrow(/isn't listed on a contract/);
|
|
744
|
+
});
|
|
745
|
+
test("an in-memory observable publishes at the start and after each change, one at a time", async () => {
|
|
746
|
+
const count = (0, index_1.observable)(0);
|
|
747
|
+
const stored = (0, index_1.deviceVar)(`stored-${(0, peer_id_1.newid)()}`, { defaultValue: 1 });
|
|
748
|
+
const definition = define((main, other) => {
|
|
749
|
+
const counter = (0, index_1.defineObservable)({
|
|
750
|
+
name: "count",
|
|
751
|
+
description: "A count.",
|
|
752
|
+
schema: index_1.z.number(),
|
|
753
|
+
source: count,
|
|
754
|
+
});
|
|
755
|
+
main.observables = [
|
|
756
|
+
counter,
|
|
757
|
+
(0, index_1.defineObservable)({
|
|
758
|
+
name: "stored",
|
|
759
|
+
description: "Served by the host.",
|
|
760
|
+
schema: index_1.z.number(),
|
|
761
|
+
source: stored,
|
|
762
|
+
}),
|
|
763
|
+
];
|
|
764
|
+
other.observables = [counter];
|
|
765
|
+
});
|
|
766
|
+
let active = 0;
|
|
767
|
+
let overlapped = false;
|
|
768
|
+
const host = workerHost(async () => {
|
|
769
|
+
active += 1;
|
|
770
|
+
overlapped ||= active > 2;
|
|
771
|
+
await settle();
|
|
772
|
+
active -= 1;
|
|
773
|
+
return null;
|
|
774
|
+
});
|
|
775
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
776
|
+
expect(host.calls).toEqual([
|
|
777
|
+
["publish", contractId, 1, "count", 0],
|
|
778
|
+
["publish", otherContractId, 1, "count", 0],
|
|
779
|
+
]);
|
|
780
|
+
count(1);
|
|
781
|
+
count(2);
|
|
782
|
+
stored(5);
|
|
783
|
+
await settle();
|
|
784
|
+
await settle();
|
|
785
|
+
await settle();
|
|
786
|
+
expect(host.calls.slice(2)).toEqual([
|
|
787
|
+
["publish", contractId, 1, "count", 1],
|
|
788
|
+
["publish", otherContractId, 1, "count", 1],
|
|
789
|
+
["publish", contractId, 1, "count", 2],
|
|
790
|
+
["publish", otherContractId, 1, "count", 2],
|
|
791
|
+
]);
|
|
792
|
+
// Each value's publishes run together; the next value waits for them.
|
|
793
|
+
expect(overlapped).toBe(false);
|
|
794
|
+
// The variable-backed observable is saved like any variable; the host serves it.
|
|
795
|
+
expect(host.saves).toEqual([["device", expect.stringMatching(/^stored-/), 5]]);
|
|
796
|
+
// Read-only: no write handler.
|
|
797
|
+
expect(host.setters.size).toBe(0);
|
|
798
|
+
});
|
|
799
|
+
test("queued publications capture each object before later mutation", async () => {
|
|
800
|
+
const value = (0, index_1.observable)({ count: 0 });
|
|
801
|
+
const definition = define((main) => {
|
|
802
|
+
main.observables = [
|
|
803
|
+
(0, index_1.defineObservable)({
|
|
804
|
+
name: "snapshot",
|
|
805
|
+
description: "A snapshot.",
|
|
806
|
+
schema: index_1.z.object({ count: index_1.z.number() }),
|
|
807
|
+
source: value,
|
|
808
|
+
}),
|
|
809
|
+
];
|
|
810
|
+
});
|
|
811
|
+
const host = workerHost();
|
|
812
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
813
|
+
host.calls.length = 0;
|
|
814
|
+
const next = { count: 1 };
|
|
815
|
+
value(next);
|
|
816
|
+
next.count = 2;
|
|
817
|
+
value.notifySubscribers();
|
|
818
|
+
await settle();
|
|
819
|
+
expect(host.calls).toEqual([
|
|
820
|
+
["publish", contractId, 1, "snapshot", { count: 1 }],
|
|
821
|
+
["publish", contractId, 1, "snapshot", { count: 2 }],
|
|
822
|
+
]);
|
|
823
|
+
});
|
|
824
|
+
test("publications and parsed write replies normalize optional values without changing the source", async () => {
|
|
825
|
+
const source = (0, index_1.observable)({
|
|
826
|
+
extra: undefined,
|
|
827
|
+
nested: { extra: undefined },
|
|
828
|
+
items: [undefined],
|
|
829
|
+
});
|
|
830
|
+
const definition = define((main) => {
|
|
831
|
+
main.observables = [
|
|
832
|
+
(0, index_1.defineObservable)({
|
|
833
|
+
name: "optional",
|
|
834
|
+
description: "Optional values.",
|
|
835
|
+
schema: index_1.z.string().transform(() => ({
|
|
836
|
+
extra: undefined,
|
|
837
|
+
nested: { extra: undefined },
|
|
838
|
+
items: [undefined],
|
|
839
|
+
})),
|
|
840
|
+
writable: true,
|
|
841
|
+
source,
|
|
842
|
+
}),
|
|
843
|
+
];
|
|
844
|
+
});
|
|
845
|
+
const host = workerHost();
|
|
846
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
847
|
+
expect(host.calls).toEqual([
|
|
848
|
+
["publish", contractId, 1, "optional", { nested: {}, items: [null] }],
|
|
849
|
+
]);
|
|
850
|
+
const write = host.setters.get(`${contractId}@1:optional`);
|
|
851
|
+
await expect(Promise.resolve(write?.("next"))).resolves.toEqual({ nested: {}, items: [null] });
|
|
852
|
+
expect(host.calls.at(-1)).toEqual([
|
|
853
|
+
"publish",
|
|
854
|
+
contractId,
|
|
855
|
+
1,
|
|
856
|
+
"optional",
|
|
857
|
+
{ nested: {}, items: [null] },
|
|
858
|
+
]);
|
|
859
|
+
expect(Object.hasOwn(source(), "extra")).toBe(true);
|
|
860
|
+
expect(source().nested).toEqual({ extra: undefined });
|
|
861
|
+
expect(source().items).toEqual([undefined]);
|
|
862
|
+
});
|
|
863
|
+
test("a write is parsed, set on the source, and resolves once the stored value is published", async () => {
|
|
864
|
+
const level = (0, index_1.observable)(1);
|
|
865
|
+
level.subscribe((value) => {
|
|
866
|
+
if (value > 10)
|
|
867
|
+
level(10);
|
|
868
|
+
});
|
|
869
|
+
const definition = define((main) => {
|
|
870
|
+
main.observables = [
|
|
871
|
+
(0, index_1.defineObservable)({
|
|
872
|
+
name: "level",
|
|
873
|
+
description: "Level, at most 10.",
|
|
874
|
+
schema: index_1.z.number().int(),
|
|
875
|
+
writable: true,
|
|
876
|
+
source: level,
|
|
877
|
+
}),
|
|
878
|
+
];
|
|
879
|
+
});
|
|
880
|
+
const host = workerHost();
|
|
881
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
882
|
+
const write = host.setters.get(`${contractId}@1:level`);
|
|
883
|
+
expect(write).toBeDefined();
|
|
884
|
+
host.calls.length = 0;
|
|
885
|
+
await expect(Promise.resolve(write?.(1.5))).rejects.toThrow(/integer/);
|
|
886
|
+
expect(level()).toBe(1);
|
|
887
|
+
expect(host.calls).toEqual([]);
|
|
888
|
+
await expect(Promise.resolve(write?.(4))).resolves.toBe(4);
|
|
889
|
+
expect(level()).toBe(4);
|
|
890
|
+
expect(host.calls).toEqual([["publish", contractId, 1, "level", 4]]);
|
|
891
|
+
// The source's own subscriber clamps the value; what's published and returned is the result.
|
|
892
|
+
host.calls.length = 0;
|
|
893
|
+
await expect(Promise.resolve(write?.(50))).resolves.toBe(10);
|
|
894
|
+
expect(host.calls).toEqual([["publish", contractId, 1, "level", 10]]);
|
|
895
|
+
});
|
|
896
|
+
test("publications need no structuredClone and keep a __proto__ key as a field", async () => {
|
|
897
|
+
const source = (0, index_1.observable)(JSON.parse('{"__proto__": 1}'));
|
|
898
|
+
const definition = define((main) => {
|
|
899
|
+
main.observables = [
|
|
900
|
+
(0, index_1.defineObservable)({
|
|
901
|
+
name: "counts",
|
|
902
|
+
description: "Counts by name.",
|
|
903
|
+
valueType: index_1.FieldType.object,
|
|
904
|
+
writable: true,
|
|
905
|
+
source,
|
|
906
|
+
}),
|
|
907
|
+
];
|
|
908
|
+
});
|
|
909
|
+
const global = globalThis;
|
|
910
|
+
const original = global.structuredClone;
|
|
911
|
+
delete global.structuredClone;
|
|
912
|
+
try {
|
|
913
|
+
const host = workerHost();
|
|
914
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
915
|
+
const published = host.calls[0]?.[4];
|
|
916
|
+
expect(Object.getPrototypeOf(published)).toBe(Object.prototype);
|
|
917
|
+
expect(Object.getOwnPropertyDescriptor(published, "__proto__")?.value).toBe(1);
|
|
918
|
+
const write = host.setters.get(`${contractId}@1:counts`);
|
|
919
|
+
await expect(Promise.resolve(write?.({ total: 2 }))).resolves.toEqual({ total: 2 });
|
|
920
|
+
}
|
|
921
|
+
finally {
|
|
922
|
+
global.structuredClone = original;
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
test("an object write returns the published snapshot even if its source mutates meanwhile", async () => {
|
|
926
|
+
const source = (0, index_1.observable)({ count: 0 });
|
|
927
|
+
const definition = define((main) => {
|
|
928
|
+
main.observables = [
|
|
929
|
+
(0, index_1.defineObservable)({
|
|
930
|
+
name: "snapshot",
|
|
931
|
+
description: "A snapshot.",
|
|
932
|
+
writable: true,
|
|
933
|
+
schema: index_1.z.object({ count: index_1.z.number() }),
|
|
934
|
+
source,
|
|
935
|
+
}),
|
|
936
|
+
];
|
|
937
|
+
});
|
|
938
|
+
let release = () => { };
|
|
939
|
+
const gate = new Promise((resolve) => {
|
|
940
|
+
release = resolve;
|
|
941
|
+
});
|
|
942
|
+
const host = workerHost(async (value) => {
|
|
943
|
+
if (value.count === 1)
|
|
944
|
+
await gate;
|
|
945
|
+
});
|
|
946
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
947
|
+
const write = host.setters.get(`${contractId}@1:snapshot`);
|
|
948
|
+
const pending = Promise.resolve(write?.({ count: 1 }));
|
|
949
|
+
try {
|
|
950
|
+
await settle();
|
|
951
|
+
source().count = 2;
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
release();
|
|
955
|
+
}
|
|
956
|
+
await expect(pending).resolves.toEqual({ count: 1 });
|
|
957
|
+
});
|
|
958
|
+
test("an initial publish failure rejects startup; later background failures are logged", async () => {
|
|
959
|
+
const status = (0, index_1.observable)("idle");
|
|
960
|
+
const definition = define((main) => {
|
|
961
|
+
main.observables = [
|
|
962
|
+
(0, index_1.defineObservable)({
|
|
963
|
+
name: "status",
|
|
964
|
+
description: "Status.",
|
|
965
|
+
schema: index_1.z.string(),
|
|
966
|
+
source: status,
|
|
967
|
+
}),
|
|
968
|
+
];
|
|
969
|
+
});
|
|
970
|
+
const errors = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
971
|
+
let fail = true;
|
|
972
|
+
const host = workerHost(async () => {
|
|
973
|
+
if (fail)
|
|
974
|
+
throw new Error("host said no");
|
|
975
|
+
return null;
|
|
976
|
+
});
|
|
977
|
+
try {
|
|
978
|
+
await expect((0, index_1.startPackage)(definition, host.ctx)).rejects.toThrow("host said no");
|
|
979
|
+
fail = false;
|
|
980
|
+
await (0, index_1.startPackage)(definition, host.ctx);
|
|
981
|
+
fail = true;
|
|
982
|
+
status("broken");
|
|
983
|
+
await settle();
|
|
984
|
+
expect(errors).toHaveBeenCalledWith('Publishing observable "status" failed', expect.any(Error));
|
|
985
|
+
fail = false;
|
|
986
|
+
status("busy");
|
|
987
|
+
await settle();
|
|
988
|
+
expect(host.calls.at(-1)).toEqual(["publish", contractId, 1, "status", "busy"]);
|
|
989
|
+
}
|
|
990
|
+
finally {
|
|
991
|
+
errors.mockRestore();
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
test("starting again serves the new ctx only", async () => {
|
|
995
|
+
const mood = (0, index_1.observable)("calm");
|
|
996
|
+
const definition = define((main) => {
|
|
997
|
+
main.observables = [
|
|
998
|
+
(0, index_1.defineObservable)({ name: "mood", description: "Mood.", schema: index_1.z.string(), source: mood }),
|
|
999
|
+
];
|
|
1000
|
+
});
|
|
1001
|
+
const first = workerHost();
|
|
1002
|
+
const second = workerHost();
|
|
1003
|
+
await (0, index_1.startPackage)(definition, first.ctx);
|
|
1004
|
+
await (0, index_1.startPackage)(definition, second.ctx);
|
|
1005
|
+
mood("busy");
|
|
1006
|
+
await settle();
|
|
1007
|
+
expect(first.calls).toEqual([["publish", contractId, 1, "mood", "calm"]]);
|
|
1008
|
+
expect(second.calls).toEqual([
|
|
1009
|
+
["publish", contractId, 1, "mood", "calm"],
|
|
1010
|
+
["publish", contractId, 1, "mood", "busy"],
|
|
1011
|
+
]);
|
|
1012
|
+
});
|
|
1013
|
+
});
|