@peers-app/peers-sdk 0.23.4 → 0.23.5
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/dist/context/data-context.d.ts +36 -5
- package/dist/context/data-context.js +44 -9
- package/dist/context/user-context.d.ts +44 -0
- package/dist/context/user-context.js +91 -12
- package/dist/contracts/__tests__/contract-provider-router.test.js +26 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +44 -0
- package/dist/contracts/__tests__/registry.test.js +104 -0
- package/dist/contracts/contract-provider-router.d.ts +21 -0
- package/dist/contracts/contract-provider-router.js +31 -5
- package/dist/contracts/contract-proxy.d.ts +3 -0
- package/dist/contracts/contract-proxy.js +3 -0
- package/dist/contracts/contract-resolvers.d.ts +21 -3
- package/dist/contracts/contract-resolvers.js +105 -32
- package/dist/contracts/index.d.ts +3 -3
- package/dist/contracts/registry.d.ts +59 -0
- package/dist/contracts/registry.js +155 -25
- package/dist/contracts/system-contracts.js +3 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/orm/types.d.ts +19 -0
- package/dist/data/orm/types.js +11 -0
- package/dist/data/package-table-identity.d.ts +46 -0
- package/dist/data/package-table-identity.js +71 -0
- package/dist/data/package-table-identity.test.d.ts +1 -0
- package/dist/data/package-table-identity.test.js +46 -0
- package/dist/data/package-version-resolver.d.ts +13 -0
- package/dist/data/package-version-resolver.js +142 -6
- package/dist/data/package-version-resolver.test.js +42 -0
- package/dist/data/persistent-var-identity.d.ts +37 -0
- package/dist/data/persistent-var-identity.js +69 -0
- package/dist/data/persistent-vars.d.ts +3 -5
- package/dist/data/persistent-vars.js +25 -13
- package/dist/data/persistent-vars.package-scope.test.d.ts +1 -0
- package/dist/data/persistent-vars.package-scope.test.js +27 -0
- package/dist/data/tools.d.ts +3 -3
- package/dist/package-loader/contract-package-loader.js +20 -8
- package/dist/package-loader/contract-package-loader.test.js +16 -0
- package/dist/package-loader/index.d.ts +4 -0
- package/dist/package-loader/index.js +4 -0
- package/dist/package-loader/isolated-artifact.d.ts +233 -0
- package/dist/package-loader/isolated-artifact.js +643 -0
- package/dist/package-loader/isolated-artifact.test.d.ts +1 -0
- package/dist/package-loader/isolated-artifact.test.js +270 -0
- package/dist/package-loader/isolated-persistent-var-gateway.d.ts +20 -0
- package/dist/package-loader/isolated-persistent-var-gateway.js +106 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.js +56 -0
- package/dist/package-loader/isolated-runtime.d.ts +229 -0
- package/dist/package-loader/isolated-runtime.js +37 -0
- package/dist/package-loader/isolated-table-gateway.d.ts +16 -0
- package/dist/package-loader/isolated-table-gateway.js +375 -0
- package/dist/package-loader/isolated-table-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-table-gateway.test.js +206 -0
- package/dist/package-loader/isolated-value.d.ts +14 -0
- package/dist/package-loader/isolated-value.js +63 -0
- package/dist/package-loader/package-loader.d.ts +123 -4
- package/dist/package-loader/package-loader.js +417 -46
- package/dist/package-loader/package-loader.test.js +478 -12
- package/dist/package-loader/resolve-consumed-contracts.d.ts +3 -3
- package/dist/package-loader/resolve-consumed-contracts.js +18 -10
- package/dist/utils.d.ts +21 -2
- package/dist/utils.js +46 -7
- package/package.json +1 -1
|
@@ -10,8 +10,12 @@ jest.mock("../data/package-versions", () => ({
|
|
|
10
10
|
}),
|
|
11
11
|
computePackageVersionHash: (version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) => mockComputePackageVersionHash(version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash),
|
|
12
12
|
}));
|
|
13
|
+
const contract_resolvers_1 = require("../contracts/contract-resolvers");
|
|
13
14
|
const registry_1 = require("../contracts/registry");
|
|
14
15
|
const system_ids_1 = require("../system-ids");
|
|
16
|
+
const utils_1 = require("../utils");
|
|
17
|
+
const isolated_artifact_1 = require("./isolated-artifact");
|
|
18
|
+
const isolated_runtime_1 = require("./isolated-runtime");
|
|
15
19
|
const package_loader_1 = require("./package-loader");
|
|
16
20
|
describe("PackageLoader.correctPackageVersion", () => {
|
|
17
21
|
const activePvId = "pv-active";
|
|
@@ -27,13 +31,11 @@ describe("PackageLoader.correctPackageVersion", () => {
|
|
|
27
31
|
mockSignAndSave.mockResolvedValue(undefined);
|
|
28
32
|
loader = new package_loader_1.PackageLoader({ dataContextId: "ctx" });
|
|
29
33
|
});
|
|
30
|
-
async function correctPackageVersion() {
|
|
31
|
-
loader.correctPackageVersion(pkg, def);
|
|
32
|
-
// Flush the fire-and-forget promise chain
|
|
33
|
-
await new Promise((r) => setImmediate(r));
|
|
34
|
+
async function correctPackageVersion(selectedPackageVersion) {
|
|
35
|
+
await loader.correctPackageVersion(pkg, selectedPackageVersion, def.version);
|
|
34
36
|
}
|
|
35
37
|
it("updates the active PV hash and clears a stale author signature", async () => {
|
|
36
|
-
|
|
38
|
+
await correctPackageVersion({
|
|
37
39
|
packageVersionId: activePvId,
|
|
38
40
|
version: "0.3.1",
|
|
39
41
|
versionTag: "dev",
|
|
@@ -43,7 +45,6 @@ describe("PackageLoader.correctPackageVersion", () => {
|
|
|
43
45
|
packageAuthorSignature: "stale-sig",
|
|
44
46
|
packageVersionHash: "old-hash",
|
|
45
47
|
});
|
|
46
|
-
await correctPackageVersion();
|
|
47
48
|
expect(mockComputePackageVersionHash).toHaveBeenCalledWith("0.3.6", "dev", "bundle-hash", "routes-hash", "ui-hash");
|
|
48
49
|
expect(mockSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
49
50
|
packageVersionId: activePvId,
|
|
@@ -54,30 +55,27 @@ describe("PackageLoader.correctPackageVersion", () => {
|
|
|
54
55
|
expect(mockSignAndSave.mock.calls[0][0].packageAuthorSignature).toBeUndefined();
|
|
55
56
|
});
|
|
56
57
|
it("does not update the active PV when it is stable", async () => {
|
|
57
|
-
|
|
58
|
+
await correctPackageVersion({
|
|
58
59
|
packageVersionId: activePvId,
|
|
59
60
|
version: "0.3.5",
|
|
60
61
|
versionTag: "stable",
|
|
61
62
|
});
|
|
62
|
-
await correctPackageVersion();
|
|
63
63
|
expect(mockSignAndSave).not.toHaveBeenCalled();
|
|
64
64
|
});
|
|
65
65
|
it("does not update the active PV when it is beta", async () => {
|
|
66
|
-
|
|
66
|
+
await correctPackageVersion({
|
|
67
67
|
packageVersionId: activePvId,
|
|
68
68
|
version: "0.3.5",
|
|
69
69
|
versionTag: "beta",
|
|
70
70
|
});
|
|
71
|
-
await correctPackageVersion();
|
|
72
71
|
expect(mockSignAndSave).not.toHaveBeenCalled();
|
|
73
72
|
});
|
|
74
73
|
it("does nothing when the active PV version already matches", async () => {
|
|
75
|
-
|
|
74
|
+
await correctPackageVersion({
|
|
76
75
|
packageVersionId: activePvId,
|
|
77
76
|
version: "0.3.6",
|
|
78
77
|
versionTag: "dev",
|
|
79
78
|
});
|
|
80
|
-
await correctPackageVersion();
|
|
81
79
|
expect(mockSignAndSave).not.toHaveBeenCalled();
|
|
82
80
|
});
|
|
83
81
|
});
|
|
@@ -95,4 +93,472 @@ describe("PackageLoader.contractRegistry", () => {
|
|
|
95
93
|
expect(loader.contractRegistry.resolve(system_ids_1.logsContractId, 1)?.name).toBe("System Logs");
|
|
96
94
|
expect(loader.contractRegistry.getProviderPackageId(system_ids_1.deviceOperationsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
|
|
97
95
|
});
|
|
96
|
+
it("resolves the registry-selected provider through its scoped resolver", async () => {
|
|
97
|
+
const dataContext = { dataContextId: "ctx-selected-provider" };
|
|
98
|
+
const loader = new package_loader_1.PackageLoader(dataContext);
|
|
99
|
+
const contractId = (0, utils_1.newid)();
|
|
100
|
+
const providerA = (0, utils_1.newid)();
|
|
101
|
+
const providerB = (0, utils_1.newid)();
|
|
102
|
+
const definition = {
|
|
103
|
+
contractId,
|
|
104
|
+
version: 1,
|
|
105
|
+
devTag: "dev",
|
|
106
|
+
name: "Selected Provider",
|
|
107
|
+
description: "test",
|
|
108
|
+
tables: [],
|
|
109
|
+
tools: [],
|
|
110
|
+
observables: [],
|
|
111
|
+
events: [],
|
|
112
|
+
};
|
|
113
|
+
loader.contractRegistry.register(providerA, definition);
|
|
114
|
+
loader.contractRegistry.register(providerB, definition);
|
|
115
|
+
loader.contractRegistry.setActiveProvider(contractId, 1, providerB);
|
|
116
|
+
const resolverA = jest.fn(() => ({ contractId, version: 1 }));
|
|
117
|
+
const resolverB = jest.fn(() => ({ contractId, version: 1 }));
|
|
118
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, resolverA, { providerPackageId: providerA });
|
|
119
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, resolverB, { providerPackageId: providerB });
|
|
120
|
+
const endpoint = await loader.resolveContractEndpoint({
|
|
121
|
+
contractId,
|
|
122
|
+
version: 1,
|
|
123
|
+
context: {},
|
|
124
|
+
notify: async () => undefined,
|
|
125
|
+
});
|
|
126
|
+
expect(resolverA).not.toHaveBeenCalled();
|
|
127
|
+
expect(resolverB).toHaveBeenCalledWith({ dataContext });
|
|
128
|
+
endpoint.dispose();
|
|
129
|
+
});
|
|
130
|
+
it("preserves legacy unscoped resolvers without a registry provider", async () => {
|
|
131
|
+
const dataContext = { dataContextId: "ctx-legacy-resolver" };
|
|
132
|
+
const loader = new package_loader_1.PackageLoader(dataContext);
|
|
133
|
+
const contractId = (0, utils_1.newid)();
|
|
134
|
+
const resolver = jest.fn(() => ({ contractId, version: 1 }));
|
|
135
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, resolver);
|
|
136
|
+
const endpoint = await loader.resolveContractEndpoint({
|
|
137
|
+
contractId,
|
|
138
|
+
version: 1,
|
|
139
|
+
context: {},
|
|
140
|
+
notify: async () => undefined,
|
|
141
|
+
});
|
|
142
|
+
expect(resolver).toHaveBeenCalledWith({ dataContext });
|
|
143
|
+
endpoint.dispose();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
const mockGetFileContents = jest.fn();
|
|
147
|
+
jest.mock("../data/files", () => ({
|
|
148
|
+
Files: () => ({
|
|
149
|
+
getFileContents: (...args) => mockGetFileContents(...args),
|
|
150
|
+
}),
|
|
151
|
+
}));
|
|
152
|
+
describe("PackageLoader isolated artifacts", () => {
|
|
153
|
+
const packageId = (0, utils_1.newid)();
|
|
154
|
+
const contractId = (0, utils_1.newid)();
|
|
155
|
+
const artifact = {
|
|
156
|
+
manifest: {
|
|
157
|
+
packageId,
|
|
158
|
+
version: "1.0.0",
|
|
159
|
+
provides: [
|
|
160
|
+
{
|
|
161
|
+
contractId,
|
|
162
|
+
version: 1,
|
|
163
|
+
name: "Greeter",
|
|
164
|
+
tools: [{ name: "greet", usageDescription: "Hello", inputFields: [], outputFields: [] }],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
consumes: [],
|
|
168
|
+
},
|
|
169
|
+
providerSource: "globalThis.__shouldNotEvalOnHost = true;",
|
|
170
|
+
};
|
|
171
|
+
const pkg = {
|
|
172
|
+
packageId,
|
|
173
|
+
name: "Isolated Greeter",
|
|
174
|
+
activePackageVersionId: (0, utils_1.newid)(),
|
|
175
|
+
};
|
|
176
|
+
const selectedVersion = (overrides = {}) => ({
|
|
177
|
+
packageVersionId: pkg.activePackageVersionId,
|
|
178
|
+
packageId,
|
|
179
|
+
version: artifact.manifest.version,
|
|
180
|
+
versionTag: "stable",
|
|
181
|
+
packageBundleFileId: (0, utils_1.newid)(),
|
|
182
|
+
packageBundleFileHash: "bundle-hash",
|
|
183
|
+
...overrides,
|
|
184
|
+
});
|
|
185
|
+
afterEach(() => {
|
|
186
|
+
(0, isolated_runtime_1.setIsolatedPackageRuntimeFactory)(undefined);
|
|
187
|
+
mockGetFileContents.mockReset();
|
|
188
|
+
delete globalThis.__shouldNotEvalOnHost;
|
|
189
|
+
});
|
|
190
|
+
function makeRuntime() {
|
|
191
|
+
const installed = new Set();
|
|
192
|
+
const runtime = {
|
|
193
|
+
install: jest.fn(async (next) => {
|
|
194
|
+
installed.add(next.manifest.packageId);
|
|
195
|
+
return { packageId: next.manifest.packageId, provided: [], consumes: [] };
|
|
196
|
+
}),
|
|
197
|
+
replace: jest.fn(async (next) => {
|
|
198
|
+
installed.add(next.manifest.packageId);
|
|
199
|
+
return { packageId: next.manifest.packageId, provided: [], consumes: [] };
|
|
200
|
+
}),
|
|
201
|
+
uninstall: jest.fn(async (id) => {
|
|
202
|
+
installed.delete(id);
|
|
203
|
+
}),
|
|
204
|
+
resolveEndpoint: jest.fn(async () => undefined),
|
|
205
|
+
provides: jest.fn(() => false),
|
|
206
|
+
isInstalled: jest.fn((id) => installed.has(id)),
|
|
207
|
+
dispose: jest.fn(async () => {
|
|
208
|
+
installed.clear();
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
(0, isolated_runtime_1.setIsolatedPackageRuntimeFactory)(() => runtime);
|
|
212
|
+
return runtime;
|
|
213
|
+
}
|
|
214
|
+
it("installs a valid isolated artifact without evaluating provider source", async () => {
|
|
215
|
+
const runtime = makeRuntime();
|
|
216
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
217
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
218
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-iso" });
|
|
219
|
+
const loaded = await loader.loadPackage(pkg);
|
|
220
|
+
expect(loaded).toEqual({ packageId, appNavs: undefined });
|
|
221
|
+
expect(runtime.install).toHaveBeenCalledWith(artifact, expect.objectContaining({
|
|
222
|
+
contractLifecycle: "stable",
|
|
223
|
+
packageVersionId: pkg.activePackageVersionId,
|
|
224
|
+
packageVersion: "1.0.0",
|
|
225
|
+
versionTag: "stable",
|
|
226
|
+
}));
|
|
227
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(true);
|
|
228
|
+
expect(loader.isContractInstalled(packageId)).toBe(true);
|
|
229
|
+
expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
test.each([
|
|
232
|
+
["dev", "mutable"],
|
|
233
|
+
["beta", "mutable"],
|
|
234
|
+
["stable", "stable"],
|
|
235
|
+
])("maps trusted %s package versions to %s contracts", async (versionTag, lifecycle) => {
|
|
236
|
+
const runtime = makeRuntime();
|
|
237
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag }));
|
|
238
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
239
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: `ctx-${versionTag}` });
|
|
240
|
+
await loader.loadPackage(pkg);
|
|
241
|
+
expect(runtime.install).toHaveBeenCalledWith(artifact, expect.objectContaining({ contractLifecycle: lifecycle, versionTag }));
|
|
242
|
+
});
|
|
243
|
+
it("rejects an artifact whose package ID does not match the package record", async () => {
|
|
244
|
+
const runtime = makeRuntime();
|
|
245
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
246
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
247
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-mismatch" });
|
|
248
|
+
await expect(loader.loadPackage({ ...pkg, packageId: (0, utils_1.newid)(), activePackageVersionId: (0, utils_1.newid)() })).rejects.toThrow(/packageId mismatch/);
|
|
249
|
+
expect(runtime.install).not.toHaveBeenCalled();
|
|
250
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
it("skips a valid isolated artifact when no runtime factory is registered", async () => {
|
|
253
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
254
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
255
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
256
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-nofactory" });
|
|
257
|
+
try {
|
|
258
|
+
await expect(loader.loadPackage(pkg)).resolves.toBeUndefined();
|
|
259
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(false);
|
|
260
|
+
expect(loader.isContractInstalled(packageId)).toBe(false);
|
|
261
|
+
expect(warn).toHaveBeenCalledWith(expect.stringMatching(/does not provide an isolated runtime/));
|
|
262
|
+
expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
warn.mockRestore();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
it("clears a previously loaded legacy provider when isolation is unsupported", async () => {
|
|
269
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
270
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
271
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
272
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-no-runtime-transition" });
|
|
273
|
+
const legacyDefinition = {
|
|
274
|
+
contractId,
|
|
275
|
+
version: 1,
|
|
276
|
+
devTag: "dev",
|
|
277
|
+
name: "Legacy provider",
|
|
278
|
+
description: "",
|
|
279
|
+
tables: [],
|
|
280
|
+
tools: [],
|
|
281
|
+
observables: [],
|
|
282
|
+
events: [],
|
|
283
|
+
};
|
|
284
|
+
loader.contractRegistry.register(packageId, legacyDefinition);
|
|
285
|
+
loader.packageInstances[packageId] = { packageId, appNavs: [] };
|
|
286
|
+
loader.contractInstalledPackages.add(packageId);
|
|
287
|
+
try {
|
|
288
|
+
await expect(loader.loadPackage(pkg, { force: true })).resolves.toBeUndefined();
|
|
289
|
+
expect(loader.contractRegistry.getProviderPackageId(contractId, 1)).toBeUndefined();
|
|
290
|
+
expect(loader.packageInstances[packageId]).toBeUndefined();
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
warn.mockRestore();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
it("preserves a working legacy provider when isolated installation fails", async () => {
|
|
297
|
+
const runtime = makeRuntime();
|
|
298
|
+
runtime.install.mockRejectedValueOnce(new Error("migration failed"));
|
|
299
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
300
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
301
|
+
const loader = new package_loader_1.PackageLoader({
|
|
302
|
+
dataContextId: "ctx-failed-isolated-transition",
|
|
303
|
+
});
|
|
304
|
+
const legacyDefinition = {
|
|
305
|
+
contractId,
|
|
306
|
+
version: 1,
|
|
307
|
+
devTag: "dev",
|
|
308
|
+
name: "Legacy provider",
|
|
309
|
+
description: "",
|
|
310
|
+
tables: [],
|
|
311
|
+
tools: [],
|
|
312
|
+
observables: [],
|
|
313
|
+
events: [],
|
|
314
|
+
};
|
|
315
|
+
loader.contractRegistry.register(packageId, legacyDefinition);
|
|
316
|
+
const previous = { packageId, appNavs: [] };
|
|
317
|
+
loader.packageInstances[packageId] = previous;
|
|
318
|
+
loader.contractInstalledPackages.add(packageId);
|
|
319
|
+
await expect(loader.loadPackage(pkg, { force: true })).rejects.toThrow(/migration failed/);
|
|
320
|
+
expect(loader.contractRegistry.getProviderPackageId(contractId, 1)).toBe(packageId);
|
|
321
|
+
expect(loader.packageInstances[packageId]).toBe(previous);
|
|
322
|
+
});
|
|
323
|
+
it("rejects a package ID mismatch even when the host has no isolated runtime", async () => {
|
|
324
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
325
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
326
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-nofactory-mismatch" });
|
|
327
|
+
await expect(loader.loadPackage({ ...pkg, packageId: (0, utils_1.newid)(), activePackageVersionId: (0, utils_1.newid)() })).rejects.toThrow(/packageId mismatch/);
|
|
328
|
+
});
|
|
329
|
+
it("continues loading legacy packages after skipping an unsupported isolated package", async () => {
|
|
330
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
331
|
+
const legacyPackageId = (0, utils_1.newid)();
|
|
332
|
+
const legacyPkg = {
|
|
333
|
+
packageId: legacyPackageId,
|
|
334
|
+
name: "Legacy Package",
|
|
335
|
+
activePackageVersionId: (0, utils_1.newid)(),
|
|
336
|
+
};
|
|
337
|
+
mockGet.mockImplementation(async (packageVersionId) => ({
|
|
338
|
+
packageVersionId,
|
|
339
|
+
packageId: packageVersionId === pkg.activePackageVersionId ? packageId : legacyPackageId,
|
|
340
|
+
version: "1.0.0",
|
|
341
|
+
packageBundleFileId: (0, utils_1.newid)(),
|
|
342
|
+
packageBundleFileHash: "bundle-hash",
|
|
343
|
+
versionTag: "dev",
|
|
344
|
+
}));
|
|
345
|
+
mockGetFileContents
|
|
346
|
+
.mockResolvedValueOnce(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)))
|
|
347
|
+
.mockResolvedValueOnce(Buffer.from(`module.exports = { packageId: "${legacyPackageId}", appNavs: [] };`));
|
|
348
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-mixed-host" });
|
|
349
|
+
try {
|
|
350
|
+
await expect(loader.loadPackage(pkg)).resolves.toBeUndefined();
|
|
351
|
+
await expect(loader.loadPackage(legacyPkg)).resolves.toEqual({
|
|
352
|
+
packageId: legacyPackageId,
|
|
353
|
+
appNavs: [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
warn.mockRestore();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
it("fails closed on a malformed isolated envelope and never evaluates it", async () => {
|
|
361
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
362
|
+
mockGetFileContents.mockResolvedValue(Buffer.from(`${"PEERS_ISOLATED_PACKAGE_V1"}\n{nope`));
|
|
363
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-bad" });
|
|
364
|
+
await expect(loader.loadPackage(pkg)).rejects.toThrow(/JSON is invalid/);
|
|
365
|
+
expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
|
|
366
|
+
});
|
|
367
|
+
it("passes table and package-pvar gateways from a complete data context", () => {
|
|
368
|
+
const factory = jest.fn(() => ({
|
|
369
|
+
install: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),
|
|
370
|
+
replace: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),
|
|
371
|
+
uninstall: jest.fn(async () => undefined),
|
|
372
|
+
resolveEndpoint: jest.fn(async () => undefined),
|
|
373
|
+
provides: jest.fn(() => false),
|
|
374
|
+
isInstalled: jest.fn(() => false),
|
|
375
|
+
dispose: jest.fn(async () => undefined),
|
|
376
|
+
}));
|
|
377
|
+
(0, isolated_runtime_1.setIsolatedPackageRuntimeFactory)(factory);
|
|
378
|
+
const tableContainer = { registerTableDefinition: jest.fn(), getTable: jest.fn() };
|
|
379
|
+
const dataContext = {
|
|
380
|
+
dataContextId: "ctx-tables",
|
|
381
|
+
tableContainer,
|
|
382
|
+
};
|
|
383
|
+
dataContext.userContext = {
|
|
384
|
+
userDataContext: dataContext,
|
|
385
|
+
};
|
|
386
|
+
new package_loader_1.PackageLoader(dataContext);
|
|
387
|
+
expect(factory).toHaveBeenCalledWith(expect.objectContaining({
|
|
388
|
+
dataContextId: "ctx-tables",
|
|
389
|
+
providerGateway: expect.objectContaining({
|
|
390
|
+
resolveEndpoint: expect.any(Function),
|
|
391
|
+
}),
|
|
392
|
+
persistentVarGateway: expect.objectContaining({
|
|
393
|
+
get: expect.any(Function),
|
|
394
|
+
set: expect.any(Function),
|
|
395
|
+
}),
|
|
396
|
+
tableGateway: expect.objectContaining({
|
|
397
|
+
register: expect.any(Function),
|
|
398
|
+
get: expect.any(Function),
|
|
399
|
+
save: expect.any(Function),
|
|
400
|
+
}),
|
|
401
|
+
}));
|
|
402
|
+
});
|
|
403
|
+
it("unloads an isolated package on force reload and disable", async () => {
|
|
404
|
+
const runtime = makeRuntime();
|
|
405
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
406
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
407
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-unload" });
|
|
408
|
+
await loader.loadPackage(pkg);
|
|
409
|
+
await loader.loadPackage(pkg, { force: true });
|
|
410
|
+
expect(runtime.uninstall).not.toHaveBeenCalled();
|
|
411
|
+
expect(runtime.install).toHaveBeenCalledTimes(1);
|
|
412
|
+
expect(runtime.replace).toHaveBeenCalledTimes(1);
|
|
413
|
+
await loader.loadPackage({ ...pkg, disabled: true });
|
|
414
|
+
expect(runtime.uninstall).toHaveBeenCalledWith(packageId);
|
|
415
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(false);
|
|
416
|
+
});
|
|
417
|
+
it("prefers a local isolated bundle over a stored isolated Files bundle", async () => {
|
|
418
|
+
const runtime = makeRuntime();
|
|
419
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "dev" }));
|
|
420
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)({
|
|
421
|
+
...artifact,
|
|
422
|
+
manifest: { ...artifact.manifest, version: "1.0.0" },
|
|
423
|
+
})));
|
|
424
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-local-iso" });
|
|
425
|
+
loader.require = ((moduleId) => {
|
|
426
|
+
if (moduleId === "fs") {
|
|
427
|
+
return {
|
|
428
|
+
readFileSync: () => (0, isolated_artifact_1.serializeIsolatedPackageArtifact)({
|
|
429
|
+
...artifact,
|
|
430
|
+
manifest: { ...artifact.manifest, version: "2.0.0" },
|
|
431
|
+
}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (moduleId === "path") {
|
|
435
|
+
return { join: (...parts) => parts.join("/") };
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`unexpected require ${moduleId}`);
|
|
438
|
+
});
|
|
439
|
+
await loader.loadPackage(pkg, { localPath: "/tmp/isolation-local" });
|
|
440
|
+
expect(runtime.install).toHaveBeenCalledWith(expect.objectContaining({
|
|
441
|
+
manifest: expect.objectContaining({ version: "2.0.0" }),
|
|
442
|
+
}), expect.objectContaining({
|
|
443
|
+
contractLifecycle: "mutable",
|
|
444
|
+
packageVersion: "2.0.0",
|
|
445
|
+
versionTag: "dev",
|
|
446
|
+
}));
|
|
447
|
+
});
|
|
448
|
+
it("does not override a stable selected version from a local checkout", async () => {
|
|
449
|
+
const runtime = makeRuntime();
|
|
450
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "stable" }));
|
|
451
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)({
|
|
452
|
+
...artifact,
|
|
453
|
+
manifest: { ...artifact.manifest, version: "1.0.0" },
|
|
454
|
+
})));
|
|
455
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-stable-local" });
|
|
456
|
+
loader.require = ((moduleId) => {
|
|
457
|
+
if (moduleId === "fs") {
|
|
458
|
+
return {
|
|
459
|
+
readFileSync: () => (0, isolated_artifact_1.serializeIsolatedPackageArtifact)({
|
|
460
|
+
...artifact,
|
|
461
|
+
manifest: { ...artifact.manifest, version: "2.0.0" },
|
|
462
|
+
}),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (moduleId === "path") {
|
|
466
|
+
return { join: (...parts) => parts.join("/") };
|
|
467
|
+
}
|
|
468
|
+
throw new Error(`unexpected require ${moduleId}`);
|
|
469
|
+
});
|
|
470
|
+
await loader.loadPackage(pkg, { localPath: "/tmp/stable-checkout" });
|
|
471
|
+
expect(runtime.install).toHaveBeenCalledWith(expect.objectContaining({
|
|
472
|
+
manifest: expect.objectContaining({ version: "1.0.0" }),
|
|
473
|
+
}), expect.objectContaining({ contractLifecycle: "stable", packageVersion: "1.0.0" }));
|
|
474
|
+
});
|
|
475
|
+
it("rejects a beta or stable manifest version mismatch", async () => {
|
|
476
|
+
const runtime = makeRuntime();
|
|
477
|
+
mockGet.mockResolvedValue(selectedVersion({ version: "1.0.0", versionTag: "beta" }));
|
|
478
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)({
|
|
479
|
+
...artifact,
|
|
480
|
+
manifest: { ...artifact.manifest, version: "2.0.0" },
|
|
481
|
+
})));
|
|
482
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-version-mismatch" });
|
|
483
|
+
await expect(loader.loadPackage(pkg)).rejects.toThrow(/version mismatch/);
|
|
484
|
+
expect(runtime.install).not.toHaveBeenCalled();
|
|
485
|
+
});
|
|
486
|
+
it("uses the instance require callback for legacy package dependencies", async () => {
|
|
487
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
488
|
+
const runtime = makeRuntime();
|
|
489
|
+
const requiredValue = { source: "injected-require" };
|
|
490
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
491
|
+
mockGetFileContents.mockResolvedValue(Buffer.from(`module.exports = { packageId: "${packageId}", appNavs: [require("custom-module")] };`));
|
|
492
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-custom-require" });
|
|
493
|
+
loader.require = ((moduleId) => {
|
|
494
|
+
if (moduleId === "custom-module") {
|
|
495
|
+
return requiredValue;
|
|
496
|
+
}
|
|
497
|
+
throw new Error(`unexpected require ${moduleId}`);
|
|
498
|
+
});
|
|
499
|
+
try {
|
|
500
|
+
await expect(loader.loadPackage(pkg)).resolves.toEqual({
|
|
501
|
+
packageId,
|
|
502
|
+
appNavs: [requiredValue],
|
|
503
|
+
});
|
|
504
|
+
expect(runtime.install).not.toHaveBeenCalled();
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
warn.mockRestore();
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
it("resolves the virtual PeersSDK import through the canonical package name", async () => {
|
|
511
|
+
const runtime = makeRuntime();
|
|
512
|
+
const sdkModule = { marker: "canonical-sdk" };
|
|
513
|
+
const previousPeersSDK = package_loader_1.PackageLoader.PeersSDK;
|
|
514
|
+
package_loader_1.PackageLoader.PeersSDK = undefined;
|
|
515
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
516
|
+
mockGetFileContents.mockResolvedValue(Buffer.from(`module.exports = { packageId: "${packageId}", appNavs: [require("PeersSDK").marker] };`));
|
|
517
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-canonical-sdk" });
|
|
518
|
+
loader.require = ((moduleId) => {
|
|
519
|
+
if (moduleId === "@peers-app/peers-sdk") {
|
|
520
|
+
return sdkModule;
|
|
521
|
+
}
|
|
522
|
+
throw new Error(`unexpected require ${moduleId}`);
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
await expect(loader.loadPackage(pkg)).resolves.toEqual({
|
|
526
|
+
packageId,
|
|
527
|
+
appNavs: ["canonical-sdk"],
|
|
528
|
+
});
|
|
529
|
+
expect(runtime.install).not.toHaveBeenCalled();
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
package_loader_1.PackageLoader.PeersSDK = previousPeersSDK;
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
it("keeps an isolated provider active when legacy evaluation fails", async () => {
|
|
536
|
+
const runtime = makeRuntime();
|
|
537
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
538
|
+
mockGetFileContents.mockResolvedValueOnce(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
539
|
+
const loader = new package_loader_1.PackageLoader({
|
|
540
|
+
dataContextId: "ctx-failed-legacy-transition",
|
|
541
|
+
});
|
|
542
|
+
await loader.loadPackage(pkg);
|
|
543
|
+
mockGetFileContents.mockResolvedValueOnce(Buffer.from("throw new Error('legacy failed');"));
|
|
544
|
+
await expect(loader.loadPackage(pkg, { force: true })).resolves.toBeUndefined();
|
|
545
|
+
expect(runtime.uninstall).not.toHaveBeenCalled();
|
|
546
|
+
expect(runtime.isInstalled(packageId)).toBe(true);
|
|
547
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
it("retires isolated state only after a legacy bundle evaluates successfully", async () => {
|
|
550
|
+
const runtime = makeRuntime();
|
|
551
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
552
|
+
mockGetFileContents
|
|
553
|
+
.mockResolvedValueOnce(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)))
|
|
554
|
+
.mockResolvedValueOnce(Buffer.from(`module.exports = { packageId: "${packageId}" };`));
|
|
555
|
+
const loader = new package_loader_1.PackageLoader({
|
|
556
|
+
dataContextId: "ctx-successful-legacy-transition",
|
|
557
|
+
});
|
|
558
|
+
await loader.loadPackage(pkg);
|
|
559
|
+
await expect(loader.loadPackage(pkg, { force: true })).resolves.toEqual({ packageId });
|
|
560
|
+
expect(runtime.uninstall).toHaveBeenCalledWith(packageId, { preserveRegistry: false });
|
|
561
|
+
expect(runtime.isInstalled(packageId)).toBe(false);
|
|
562
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(false);
|
|
563
|
+
});
|
|
98
564
|
});
|
|
@@ -6,8 +6,8 @@ import type { IPackageDefinitionResult } from "../contracts/types";
|
|
|
6
6
|
* providers have been registered.
|
|
7
7
|
*
|
|
8
8
|
* A consume succeeds only when {@link ContractRegistry.selectProvider} finds a
|
|
9
|
-
* matching provider
|
|
10
|
-
*
|
|
11
|
-
* missing required deps reject the handle.
|
|
9
|
+
* matching provider and the owning PackageLoader can bind an endpoint. The
|
|
10
|
+
* endpoint may be resolver-backed or isolated. Missing optional deps settle to
|
|
11
|
+
* `undefined`; missing required deps reject the handle.
|
|
12
12
|
*/
|
|
13
13
|
export declare function resolveConsumedContracts(dataContext: DataContext, registry: ContractRegistry, packageDefinition: IPackageDefinitionResult): Promise<void>;
|
|
@@ -9,9 +9,9 @@ const create_local_consumer_1 = require("../contracts/create-local-consumer");
|
|
|
9
9
|
* providers have been registered.
|
|
10
10
|
*
|
|
11
11
|
* A consume succeeds only when {@link ContractRegistry.selectProvider} finds a
|
|
12
|
-
* matching provider
|
|
13
|
-
*
|
|
14
|
-
* missing required deps reject the handle.
|
|
12
|
+
* matching provider and the owning PackageLoader can bind an endpoint. The
|
|
13
|
+
* endpoint may be resolver-backed or isolated. Missing optional deps settle to
|
|
14
|
+
* `undefined`; missing required deps reject the handle.
|
|
15
15
|
*/
|
|
16
16
|
async function resolveConsumedContracts(dataContext, registry, packageDefinition) {
|
|
17
17
|
for (const dep of packageDefinition.consumes) {
|
|
@@ -25,15 +25,13 @@ async function resolveConsumedContracts(dataContext, registry, packageDefinition
|
|
|
25
25
|
settleMissing(dep.optional === true, handle, dep.contractId, dep.version, "no matching provider");
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const consumer = dataContext.packageLoader?.resolveConsumedContract
|
|
29
|
+
? await dataContext.packageLoader.resolveConsumedContract(dep)
|
|
30
|
+
: await resolveLegacyLocalConsumer(dataContext, selected, dep.contractId, dep.version);
|
|
31
|
+
if (!consumer) {
|
|
32
|
+
settleMissing(dep.optional === true, handle, dep.contractId, dep.version, `provider ${selected.providerPackageId} has no live endpoint`);
|
|
31
33
|
continue;
|
|
32
34
|
}
|
|
33
|
-
const resolution = await resolver({ dataContext });
|
|
34
|
-
const consumer = (0, create_local_consumer_1.createLocalContractConsumer)(selected.definition, resolution, {
|
|
35
|
-
dataContextId: dataContext.dataContextId,
|
|
36
|
-
});
|
|
37
35
|
(0, builder_1.settleConsumedContractHandle)(handle, consumer);
|
|
38
36
|
}
|
|
39
37
|
catch (err) {
|
|
@@ -46,6 +44,16 @@ async function resolveConsumedContracts(dataContext, registry, packageDefinition
|
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
}
|
|
47
|
+
async function resolveLegacyLocalConsumer(dataContext, selected, contractId, version) {
|
|
48
|
+
const resolver = (0, contract_resolvers_1.getContractResolver)(contractId, version, selected.providerPackageId);
|
|
49
|
+
if (!resolver) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const resolution = await resolver({ dataContext });
|
|
53
|
+
return (0, create_local_consumer_1.createLocalContractConsumer)(selected.definition, resolution, {
|
|
54
|
+
dataContextId: dataContext.dataContextId,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
49
57
|
function settleMissing(optional, handle, contractId, version, reason) {
|
|
50
58
|
if (optional) {
|
|
51
59
|
(0, builder_1.settleConsumedContractHandle)(handle, undefined);
|
package/dist/utils.d.ts
CHANGED
|
@@ -46,6 +46,21 @@ export declare function deterministicPvarId(name: string): string;
|
|
|
46
46
|
* (starts with the `000pvar0` prefix).
|
|
47
47
|
*/
|
|
48
48
|
export declare function isDeterministicPvarId(id: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Generate a stable host-only peer ID for a package-owned table.
|
|
51
|
+
*
|
|
52
|
+
* The physical SQLite name is derived separately; this ID keys synced
|
|
53
|
+
* `TableDefinitions` metadata and never comes from isolated guest code.
|
|
54
|
+
*
|
|
55
|
+
* @param identity Stable package-scoped table identity.
|
|
56
|
+
*/
|
|
57
|
+
export declare function deterministicTableId(identity: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Return whether a peer ID was generated by {@link deterministicTableId}.
|
|
60
|
+
*
|
|
61
|
+
* @param id Peer ID to inspect.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isDeterministicTableId(id: string): boolean;
|
|
49
64
|
/** Resolves after `ms` milliseconds (0 yields a microtask-yield on most runtimes). */
|
|
50
65
|
export declare function sleep(ms?: number): Promise<void>;
|
|
51
66
|
/** Turns `fooBar` into `Foo Bar` for labels (also replaces `_` with space). */
|
|
@@ -96,7 +111,11 @@ export declare function retryOnErrorOrTimeout<T>({ fn, connection, opts, }: {
|
|
|
96
111
|
};
|
|
97
112
|
}): Promise<T>;
|
|
98
113
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
114
|
+
* Return a table's physical storage name.
|
|
115
|
+
*
|
|
116
|
+
* Trusted package-scoped metadata uses `"name_packageId"`; every existing
|
|
117
|
+
* table continues to use `"name_tableId"` (or just `"name"` for system tables).
|
|
118
|
+
*
|
|
119
|
+
* @param metaData Table metadata from the ORM layer.
|
|
101
120
|
*/
|
|
102
121
|
export declare function getFullTableName(metaData: ITableMetaData): string;
|