@peers-app/peers-sdk 0.26.4 → 0.26.6
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/contracts/__tests__/contract-resolvers.test.js +24 -0
- package/dist/contracts/__tests__/legacy-removal.test.js +38 -0
- package/dist/contracts/__tests__/remote-contract-transport.test.js +23 -32
- package/dist/contracts/__tests__/system-contracts.test.js +80 -0
- package/dist/contracts/contract-resolvers.d.ts +1 -1
- package/dist/contracts/contract-resolvers.js +3 -2
- package/dist/contracts/create-local-consumer.d.ts +2 -1
- package/dist/contracts/create-local-consumer.js +1 -1
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/remote-contract-transport.d.ts +3 -3
- package/dist/contracts/remote-contract-transport.js +19 -17
- package/dist/contracts/system/host-shell-contract.d.ts +191 -0
- package/dist/contracts/system/host-shell-contract.js +190 -0
- package/dist/contracts/system/index.d.ts +4 -0
- package/dist/contracts/system/index.js +4 -0
- package/dist/contracts/system/packages-contract.d.ts +257 -0
- package/dist/contracts/system/packages-contract.js +213 -0
- package/dist/data/create-group.d.ts +1 -1
- package/dist/data/create-group.js +1 -1
- package/dist/data/group-members.d.ts +6 -1
- package/dist/data/group-members.js +12 -4
- package/dist/data/groups.d.ts +6 -1
- package/dist/data/groups.js +12 -4
- package/dist/data/package-version-resolver.d.ts +8 -2
- package/dist/data/package-version-resolver.js +28 -2
- package/dist/data/package-version-resolver.test.js +42 -0
- package/dist/data/package-versions.d.ts +5 -1
- package/dist/data/package-versions.js +11 -4
- package/dist/data/packages.d.ts +7 -1
- package/dist/data/packages.js +13 -4
- package/dist/data/table-signing.test.d.ts +1 -0
- package/dist/data/table-signing.test.js +118 -0
- package/dist/data/users.d.ts +1 -2
- package/dist/data/users.js +8 -5
- package/dist/package-installer/package-author-signer.d.ts +19 -0
- package/dist/package-installer/package-author-signer.js +24 -0
- package/dist/package-installer/package-creator.d.ts +1 -7
- package/dist/package-installer/package-creator.js +0 -12
- package/dist/package-installer/package-creator.test.js +0 -16
- package/dist/package-installer/package-installer.d.ts +5 -0
- package/dist/package-installer/package-installer.js +60 -37
- package/dist/package-installer/package-installer.test.js +129 -0
- package/dist/package-installer/types.d.ts +5 -11
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-runtime.d.ts +18 -0
- package/dist/package-loader/package-loader.d.ts +67 -4
- package/dist/package-loader/package-loader.js +300 -52
- package/dist/package-loader/package-loader.test.js +413 -1
- package/dist/package-loader/package-runtime-state.d.ts +112 -0
- package/dist/package-loader/package-runtime-state.js +18 -0
- package/dist/package-loader/resolve-consumed-contracts.js +5 -4
- package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
- package/dist/rpc-types.d.ts +0 -121
- package/dist/rpc-types.js +0 -39
- package/dist/system-ids.d.ts +13 -0
- package/dist/system-ids.js +14 -1
- package/package.json +1 -1
|
@@ -48,6 +48,8 @@ const contract_proxy_1 = require("../contracts/contract-proxy");
|
|
|
48
48
|
const contract_resolvers_1 = require("../contracts/contract-resolvers");
|
|
49
49
|
const contract_transport_1 = require("../contracts/contract-transport");
|
|
50
50
|
const registry_1 = require("../contracts/registry");
|
|
51
|
+
const remote_contract_transport_1 = require("../contracts/remote-contract-transport");
|
|
52
|
+
const device_operations_contract_1 = require("../contracts/system/device-operations-contract");
|
|
51
53
|
const tool_catalog_identity_1 = require("../contracts/tool-catalog-identity");
|
|
52
54
|
const access_level_1 = require("../data/access-level");
|
|
53
55
|
const groupPermissions = require("../data/group-permissions");
|
|
@@ -59,6 +61,33 @@ const isolated_runtime_1 = require("./isolated-runtime");
|
|
|
59
61
|
const package_loader_1 = require("./package-loader");
|
|
60
62
|
const consumedContracts = require("./resolve-consumed-contracts");
|
|
61
63
|
describe("host load invalidation", () => {
|
|
64
|
+
it("retains legacy package identity and refuses system calls before forwarding", async () => {
|
|
65
|
+
const packageId = (0, utils_1.newid)();
|
|
66
|
+
const context = { dataContextId: (0, utils_1.newid)() };
|
|
67
|
+
const loader = new package_loader_1.PackageLoader(context);
|
|
68
|
+
Object.assign(context, { packageLoader: loader });
|
|
69
|
+
const emit = jest.fn(async () => "must not execute");
|
|
70
|
+
(0, remote_contract_transport_1.setRemoteContractTransportFactory)(() => ({ emit, on: () => () => { } }));
|
|
71
|
+
const definition = (0, builder_1.definePackage)((pkg) => {
|
|
72
|
+
pkg.packageId = packageId;
|
|
73
|
+
pkg.version = "1.0.0";
|
|
74
|
+
pkg.remote(system_ids_1.deviceOperationsContractId, 1);
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
await loader.catalogReady;
|
|
78
|
+
loader.contractRegistry.register(system_ids_1.peersSystemPackageId, (0, device_operations_contract_1.getDeviceOperationsContractDefinition)());
|
|
79
|
+
await consumedContracts.resolveConsumedContracts(context, loader.contractRegistry, definition);
|
|
80
|
+
const remote = (await definition.remotes?.[0].handle
|
|
81
|
+
?.consumer);
|
|
82
|
+
await expect(remote.device((0, utils_1.newid)()).tools.getDeviceStatus()).rejects.toThrow("packages cannot call system contract");
|
|
83
|
+
expect(emit).not.toHaveBeenCalled();
|
|
84
|
+
remote.dispose();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
(0, remote_contract_transport_1.setRemoteContractTransportFactory)(undefined);
|
|
88
|
+
await loader.dispose();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
62
91
|
it.each([
|
|
63
92
|
["contract", "quarantine"],
|
|
64
93
|
["contract", "unload"],
|
|
@@ -294,7 +323,7 @@ describe("host load invalidation", () => {
|
|
|
294
323
|
const externalConsumer = (0, contract_proxy_1.createContractConsumer)(definition.contracts[0], clientTransport);
|
|
295
324
|
try {
|
|
296
325
|
await load();
|
|
297
|
-
consumer = await loader.resolveConsumedContract({ contractId, version: 1 });
|
|
326
|
+
consumer = await loader.resolveConsumedContract({ contractId, version: 1 }, (0, utils_1.newid)());
|
|
298
327
|
expect(await consumer?.tools.read()).toBe("OLD");
|
|
299
328
|
expect(await externalConsumer.tools.read()).toBe("OLD");
|
|
300
329
|
endpoint = await loader.resolveContractEndpoint({
|
|
@@ -834,6 +863,7 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
834
863
|
installed.add(next.manifest.packageId);
|
|
835
864
|
return { packageId: next.manifest.packageId, provided: [], consumes: [] };
|
|
836
865
|
}),
|
|
866
|
+
updateContractLifecycle: jest.fn(async () => { }),
|
|
837
867
|
uninstall: jest.fn(async (id) => {
|
|
838
868
|
installed.delete(id);
|
|
839
869
|
}),
|
|
@@ -1289,6 +1319,12 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
1289
1319
|
});
|
|
1290
1320
|
it("retires isolated state only after a legacy bundle evaluates successfully", async () => {
|
|
1291
1321
|
const runtime = makeRuntime();
|
|
1322
|
+
Object.assign(runtime, {
|
|
1323
|
+
getPackageHealth: () => ({
|
|
1324
|
+
workerRunning: false,
|
|
1325
|
+
lastWorkerError: { message: "old worker", at: 1 },
|
|
1326
|
+
}),
|
|
1327
|
+
});
|
|
1292
1328
|
mockGet.mockResolvedValue(selectedVersion());
|
|
1293
1329
|
mockGetFileContents
|
|
1294
1330
|
.mockResolvedValueOnce(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)))
|
|
@@ -1301,5 +1337,381 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
1301
1337
|
expect(runtime.uninstall).toHaveBeenCalledWith(packageId, { preserveRegistry: false });
|
|
1302
1338
|
expect(runtime.isInstalled(packageId)).toBe(false);
|
|
1303
1339
|
expect(loader.isIsolatedInstalled(packageId)).toBe(false);
|
|
1340
|
+
expect(loader.runtimeStates()[packageId].worker).toBeUndefined();
|
|
1341
|
+
expect(loader.getPackageRuntimeState(packageId)?.worker).toBeUndefined();
|
|
1342
|
+
});
|
|
1343
|
+
describe("runtime state", () => {
|
|
1344
|
+
beforeEach(() => {
|
|
1345
|
+
// An earlier executionMode case leaves the shared artifact privileged.
|
|
1346
|
+
delete artifact.manifest.executionMode;
|
|
1347
|
+
});
|
|
1348
|
+
it("records running state with the runtime's provides and consumes", async () => {
|
|
1349
|
+
const runtime = makeRuntime();
|
|
1350
|
+
const provided = [{ contractId, version: 1 }];
|
|
1351
|
+
const consumes = [{ contractId: (0, utils_1.newid)(), version: 2, optional: true, satisfied: false }];
|
|
1352
|
+
runtime.install.mockImplementationOnce((async (next) => ({
|
|
1353
|
+
packageId: next.manifest.packageId,
|
|
1354
|
+
provided,
|
|
1355
|
+
consumes,
|
|
1356
|
+
})));
|
|
1357
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1358
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1359
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-running" });
|
|
1360
|
+
const seen = [];
|
|
1361
|
+
loader.runtimeStates.subscribe((states) => seen.push(states[packageId]?.status));
|
|
1362
|
+
await loader.loadPackage(pkg);
|
|
1363
|
+
expect(loader.getPackageRuntimeState(packageId)).toEqual({
|
|
1364
|
+
packageId,
|
|
1365
|
+
status: "running",
|
|
1366
|
+
running: {
|
|
1367
|
+
packageVersionId: pkg.activePackageVersionId,
|
|
1368
|
+
version: "1.0.0",
|
|
1369
|
+
versionTag: "stable",
|
|
1370
|
+
kind: "isolated",
|
|
1371
|
+
executionMode: "sandbox",
|
|
1372
|
+
provides: provided,
|
|
1373
|
+
consumes,
|
|
1374
|
+
},
|
|
1375
|
+
updatedAt: expect.any(Number),
|
|
1376
|
+
});
|
|
1377
|
+
expect(seen).toContain("running");
|
|
1378
|
+
await loader.dispose();
|
|
1379
|
+
expect(loader.getPackageRuntimeStates()).toEqual({});
|
|
1380
|
+
});
|
|
1381
|
+
it("keeps the running build on an onlyIfChanged load and reloads a changed one", async () => {
|
|
1382
|
+
const runtime = makeRuntime();
|
|
1383
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "dev" }));
|
|
1384
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1385
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
1386
|
+
try {
|
|
1387
|
+
const first = await loader.loadPackage(pkg);
|
|
1388
|
+
expect(runtime.install).toHaveBeenCalledTimes(1);
|
|
1389
|
+
await expect(loader.loadPackage(pkg, { force: true, onlyIfChanged: true })).resolves.toBe(first);
|
|
1390
|
+
expect(runtime.replace).not.toHaveBeenCalled();
|
|
1391
|
+
// A promotion changes only the tag, which is not part of the bundle.
|
|
1392
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "beta" }));
|
|
1393
|
+
await loader.loadPackage(pkg, { force: true, onlyIfChanged: true });
|
|
1394
|
+
expect(runtime.replace).not.toHaveBeenCalled();
|
|
1395
|
+
expect(loader.getPackageRuntimeState(packageId)?.running?.versionTag).toBe("beta");
|
|
1396
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "stable" }));
|
|
1397
|
+
await loader.loadPackage(pkg, { force: true, onlyIfChanged: true });
|
|
1398
|
+
expect(runtime.updateContractLifecycle).toHaveBeenLastCalledWith(packageId, expect.objectContaining({ contractLifecycle: "stable", versionTag: "stable" }));
|
|
1399
|
+
expect(runtime.replace).not.toHaveBeenCalled();
|
|
1400
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)({
|
|
1401
|
+
...artifact,
|
|
1402
|
+
providerSource: `${artifact.providerSource}\n// rebuilt`,
|
|
1403
|
+
})));
|
|
1404
|
+
await loader.loadPackage(pkg, { force: true, onlyIfChanged: true });
|
|
1405
|
+
expect(runtime.replace).toHaveBeenCalledTimes(1);
|
|
1406
|
+
await loader.loadPackage(pkg, { force: true });
|
|
1407
|
+
expect(runtime.replace).toHaveBeenCalledTimes(2);
|
|
1408
|
+
}
|
|
1409
|
+
finally {
|
|
1410
|
+
await loader.dispose();
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
it.each([
|
|
1414
|
+
"unload",
|
|
1415
|
+
"dispose",
|
|
1416
|
+
"replace",
|
|
1417
|
+
])("does not overwrite %s while an unchanged-build comparison is pending", async (action) => {
|
|
1418
|
+
const runtime = makeRuntime();
|
|
1419
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "dev" }));
|
|
1420
|
+
const bundle = Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact));
|
|
1421
|
+
mockGetFileContents.mockResolvedValue(bundle);
|
|
1422
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
1423
|
+
let release;
|
|
1424
|
+
let entered;
|
|
1425
|
+
const waiting = new Promise((resolve) => {
|
|
1426
|
+
entered = resolve;
|
|
1427
|
+
});
|
|
1428
|
+
const pendingRead = new Promise((resolve) => {
|
|
1429
|
+
release = resolve;
|
|
1430
|
+
});
|
|
1431
|
+
try {
|
|
1432
|
+
await loader.loadPackage(pkg);
|
|
1433
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "beta" }));
|
|
1434
|
+
mockGetFileContents.mockImplementationOnce(() => {
|
|
1435
|
+
entered();
|
|
1436
|
+
return pendingRead;
|
|
1437
|
+
});
|
|
1438
|
+
const pending = loader.loadPackage(pkg, { force: true, onlyIfChanged: true });
|
|
1439
|
+
await waiting;
|
|
1440
|
+
if (action === "replace") {
|
|
1441
|
+
mockGet.mockResolvedValue(selectedVersion({ versionTag: "stable" }));
|
|
1442
|
+
await loader.loadPackage(pkg, { force: true });
|
|
1443
|
+
}
|
|
1444
|
+
else if (action === "unload") {
|
|
1445
|
+
await loader.unloadPackage(packageId);
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
await loader.dispose();
|
|
1449
|
+
}
|
|
1450
|
+
const latest = loader.getPackageRuntimeState(packageId);
|
|
1451
|
+
release(bundle);
|
|
1452
|
+
await expect(pending).resolves.toBeUndefined();
|
|
1453
|
+
expect(loader.getPackageRuntimeState(packageId)).toEqual(latest);
|
|
1454
|
+
expect(runtime.updateContractLifecycle).not.toHaveBeenCalled();
|
|
1455
|
+
expect(runtime.install).toHaveBeenCalledTimes(1);
|
|
1456
|
+
expect(runtime.replace).toHaveBeenCalledTimes(action === "replace" ? 1 : 0);
|
|
1457
|
+
}
|
|
1458
|
+
finally {
|
|
1459
|
+
release(bundle);
|
|
1460
|
+
await loader.dispose();
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
it("validates a changed version label even when its bundle is unchanged", async () => {
|
|
1464
|
+
makeRuntime();
|
|
1465
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1466
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1467
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
1468
|
+
try {
|
|
1469
|
+
await loader.loadPackage(pkg);
|
|
1470
|
+
mockGet.mockResolvedValue(selectedVersion({ version: "2.0.0" }));
|
|
1471
|
+
await expect(loader.loadPackage(pkg, { force: true, onlyIfChanged: true })).rejects.toThrow(/version mismatch/);
|
|
1472
|
+
expect(loader.getPackageRuntimeState(packageId)?.running?.version).toBe("1.0.0");
|
|
1473
|
+
}
|
|
1474
|
+
finally {
|
|
1475
|
+
await loader.dispose();
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
it("keeps the previous running version when a replacement fails", async () => {
|
|
1479
|
+
const runtime = makeRuntime();
|
|
1480
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1481
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1482
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-replace" });
|
|
1483
|
+
await loader.loadPackage(pkg);
|
|
1484
|
+
runtime.replace.mockRejectedValueOnce(new Error("replacement rejected"));
|
|
1485
|
+
await expect(loader.loadPackage(pkg, { force: true })).rejects.toThrow(/replacement rejected/);
|
|
1486
|
+
const state = loader.getPackageRuntimeState(packageId);
|
|
1487
|
+
expect(state?.status).toBe("failed");
|
|
1488
|
+
expect(state?.running?.version).toBe("1.0.0");
|
|
1489
|
+
expect(state?.lastError).toEqual({
|
|
1490
|
+
phase: "load",
|
|
1491
|
+
message: "replacement rejected",
|
|
1492
|
+
packageVersionId: pkg.activePackageVersionId,
|
|
1493
|
+
version: "1.0.0",
|
|
1494
|
+
at: expect.any(Number),
|
|
1495
|
+
});
|
|
1496
|
+
await loader.loadPackage(pkg, { force: true });
|
|
1497
|
+
expect(loader.getPackageRuntimeState(packageId)?.status).toBe("running");
|
|
1498
|
+
expect(loader.getPackageRuntimeState(packageId)?.lastError).toBeUndefined();
|
|
1499
|
+
});
|
|
1500
|
+
it("records a malformed isolated envelope as a failed load", async () => {
|
|
1501
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1502
|
+
mockGetFileContents.mockResolvedValue(Buffer.from("PEERS_ISOLATED_PACKAGE_V1\n{nope"));
|
|
1503
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-malformed" });
|
|
1504
|
+
await expect(loader.loadPackage(pkg)).rejects.toThrow(/JSON is invalid/);
|
|
1505
|
+
const state = loader.getPackageRuntimeState(packageId);
|
|
1506
|
+
expect(state?.status).toBe("failed");
|
|
1507
|
+
expect(state?.running).toBeUndefined();
|
|
1508
|
+
expect(state?.lastError?.message).toMatch(/JSON is invalid/);
|
|
1509
|
+
});
|
|
1510
|
+
it("records a failed legacy evaluation", async () => {
|
|
1511
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1512
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1513
|
+
mockGetFileContents.mockResolvedValue(Buffer.from("throw new Error('legacy failed');"));
|
|
1514
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-legacy" });
|
|
1515
|
+
try {
|
|
1516
|
+
await expect(loader.loadPackage(pkg)).resolves.toBeUndefined();
|
|
1517
|
+
expect(loader.getPackageRuntimeState(packageId)).toMatchObject({
|
|
1518
|
+
status: "failed",
|
|
1519
|
+
lastError: { phase: "load", message: "legacy failed" },
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
finally {
|
|
1523
|
+
warn.mockRestore();
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
it.each([
|
|
1527
|
+
"module.exports = 42;",
|
|
1528
|
+
"// No exports",
|
|
1529
|
+
"module.exports = {};",
|
|
1530
|
+
"module.exports = { default: {} };",
|
|
1531
|
+
])("records unusable legacy exports as a failed load: %s", async (bundle) => {
|
|
1532
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1533
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1534
|
+
mockGetFileContents.mockResolvedValue(Buffer.from(bundle));
|
|
1535
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-empty" });
|
|
1536
|
+
try {
|
|
1537
|
+
await expect(loader.loadPackage(pkg)).resolves.toBeUndefined();
|
|
1538
|
+
expect(loader.getPackageRuntimeState(packageId)).toMatchObject({
|
|
1539
|
+
status: "failed",
|
|
1540
|
+
lastError: { phase: "load", message: "Package bundle exports nothing usable" },
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
finally {
|
|
1544
|
+
warn.mockRestore();
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
it("keeps the previous package when a replacement exports nothing", async () => {
|
|
1548
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1549
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1550
|
+
mockGetFileContents.mockResolvedValue(Buffer.from(`module.exports = { packageId: "${packageId}" };`));
|
|
1551
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
1552
|
+
try {
|
|
1553
|
+
const previous = await loader.loadPackage(pkg);
|
|
1554
|
+
const running = loader.getPackageRuntimeState(packageId)?.running;
|
|
1555
|
+
mockGetFileContents.mockResolvedValue(Buffer.from("// No exports"));
|
|
1556
|
+
await expect(loader.loadPackage(pkg, { force: true })).resolves.toBeUndefined();
|
|
1557
|
+
expect(loader.getPackageRuntimeState(packageId)).toMatchObject({
|
|
1558
|
+
status: "failed",
|
|
1559
|
+
running,
|
|
1560
|
+
lastError: { message: "Package bundle exports nothing usable" },
|
|
1561
|
+
});
|
|
1562
|
+
await expect(loader.loadPackage(pkg)).resolves.toBe(previous);
|
|
1563
|
+
}
|
|
1564
|
+
finally {
|
|
1565
|
+
warn.mockRestore();
|
|
1566
|
+
await loader.dispose();
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
it("records unsupported when the host has no isolated runtime", async () => {
|
|
1570
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1571
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1572
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1573
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-unsupported" });
|
|
1574
|
+
try {
|
|
1575
|
+
await loader.loadPackage(pkg);
|
|
1576
|
+
expect(loader.getPackageRuntimeState(packageId)).toMatchObject({
|
|
1577
|
+
status: "unsupported",
|
|
1578
|
+
reason: expect.stringMatching(/no isolated package runtime/),
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
finally {
|
|
1582
|
+
warn.mockRestore();
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
it("records unavailable when the selected bundle is not on this device", async () => {
|
|
1586
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1587
|
+
makeRuntime();
|
|
1588
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1589
|
+
mockGetFileContents.mockResolvedValue(undefined);
|
|
1590
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-unavailable" });
|
|
1591
|
+
try {
|
|
1592
|
+
await expect(loader.loadPackage(pkg)).resolves.toBeUndefined();
|
|
1593
|
+
expect(loader.getPackageRuntimeState(packageId)).toMatchObject({
|
|
1594
|
+
status: "unavailable",
|
|
1595
|
+
reason: expect.stringMatching(/not on this device/),
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
finally {
|
|
1599
|
+
warn.mockRestore();
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
it("records disabled and unloaded packages without a running version", async () => {
|
|
1603
|
+
makeRuntime();
|
|
1604
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1605
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1606
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-disabled" });
|
|
1607
|
+
await loader.loadPackage(pkg);
|
|
1608
|
+
await loader.loadPackage({ ...pkg, disabled: true });
|
|
1609
|
+
expect(loader.getPackageRuntimeState(packageId)).toEqual({
|
|
1610
|
+
packageId,
|
|
1611
|
+
status: "disabled",
|
|
1612
|
+
reason: expect.any(String),
|
|
1613
|
+
updatedAt: expect.any(Number),
|
|
1614
|
+
});
|
|
1615
|
+
await loader.unloadPackage(packageId, { reason: "uninstalled" });
|
|
1616
|
+
expect(loader.getPackageRuntimeState(packageId)).toEqual({
|
|
1617
|
+
packageId,
|
|
1618
|
+
status: "unloaded",
|
|
1619
|
+
reason: "uninstalled",
|
|
1620
|
+
updatedAt: expect.any(Number),
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
it("does not let a superseded load overwrite newer state", async () => {
|
|
1624
|
+
const runtime = makeRuntime();
|
|
1625
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-superseded" });
|
|
1626
|
+
let release;
|
|
1627
|
+
let entered;
|
|
1628
|
+
const gate = new Promise((resolve) => {
|
|
1629
|
+
release = resolve;
|
|
1630
|
+
});
|
|
1631
|
+
const waiting = new Promise((resolve) => {
|
|
1632
|
+
entered = resolve;
|
|
1633
|
+
});
|
|
1634
|
+
mockSignAndSave.mockImplementationOnce(async () => {
|
|
1635
|
+
entered();
|
|
1636
|
+
await gate;
|
|
1637
|
+
});
|
|
1638
|
+
mockGet.mockResolvedValue(selectedVersion({ version: "0.0.1", versionTag: "dev" }));
|
|
1639
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1640
|
+
const reading = jest.fn(() => (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact));
|
|
1641
|
+
loader._readLocalBundle = reading;
|
|
1642
|
+
try {
|
|
1643
|
+
const installing = loader.loadPackage(pkg, { force: true, localPath: "/pkg" });
|
|
1644
|
+
await waiting;
|
|
1645
|
+
await loader.unloadPackage(packageId);
|
|
1646
|
+
release();
|
|
1647
|
+
await expect(installing).rejects.toThrow(/invalidated/);
|
|
1648
|
+
expect(runtime.install).not.toHaveBeenCalled();
|
|
1649
|
+
expect(loader.getPackageRuntimeState(packageId)?.status).toBe("unloaded");
|
|
1650
|
+
expect(loader.getPackageRuntimeState(packageId)?.lastError).toBeUndefined();
|
|
1651
|
+
}
|
|
1652
|
+
finally {
|
|
1653
|
+
release();
|
|
1654
|
+
await loader.dispose();
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
it("publishes isolated worker health and detaches on disposal", async () => {
|
|
1658
|
+
const runtime = makeRuntime();
|
|
1659
|
+
let health = {
|
|
1660
|
+
workerRunning: false,
|
|
1661
|
+
};
|
|
1662
|
+
let notify;
|
|
1663
|
+
const unsubscribe = jest.fn();
|
|
1664
|
+
Object.assign(runtime, {
|
|
1665
|
+
getPackageHealth: jest.fn(() => health),
|
|
1666
|
+
subscribePackageHealth: (listener) => {
|
|
1667
|
+
notify = listener;
|
|
1668
|
+
return unsubscribe;
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1672
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1673
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-state-health" });
|
|
1674
|
+
await loader.loadPackage(pkg);
|
|
1675
|
+
const seen = [];
|
|
1676
|
+
loader.runtimeStates.subscribe((states) => seen.push(states[packageId]?.worker));
|
|
1677
|
+
health = { workerRunning: false, lastWorkerError: { message: "boom", at: 1 } };
|
|
1678
|
+
notify(packageId);
|
|
1679
|
+
expect(loader.getPackageRuntimeState(packageId)?.worker).toEqual(health);
|
|
1680
|
+
expect(loader.getPackageRuntimeStates()[packageId]?.worker).toEqual(health);
|
|
1681
|
+
expect(loader.runtimeStates()[packageId]?.worker).toEqual(health);
|
|
1682
|
+
expect(seen).toContainEqual(health);
|
|
1683
|
+
await loader.dispose();
|
|
1684
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
1685
|
+
notify(packageId);
|
|
1686
|
+
expect(loader.runtimeStates()).toEqual({});
|
|
1687
|
+
});
|
|
1688
|
+
it.each([
|
|
1689
|
+
"failure",
|
|
1690
|
+
"unavailable",
|
|
1691
|
+
])("ignores an older load's late %s after a newer load", async (outcome) => {
|
|
1692
|
+
makeRuntime();
|
|
1693
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
1694
|
+
let finish;
|
|
1695
|
+
let fail;
|
|
1696
|
+
const pending = new Promise((resolve, reject) => {
|
|
1697
|
+
finish = resolve;
|
|
1698
|
+
fail = reject;
|
|
1699
|
+
});
|
|
1700
|
+
mockGetFileContents.mockReturnValueOnce(pending);
|
|
1701
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
1702
|
+
const older = loader.loadPackage(pkg, { force: true });
|
|
1703
|
+
// Let the older load start its file read before the newer attempt.
|
|
1704
|
+
await Promise.resolve();
|
|
1705
|
+
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
1706
|
+
await loader.loadPackage(pkg, { force: true });
|
|
1707
|
+
if (outcome === "failure")
|
|
1708
|
+
fail(new Error("old failure"));
|
|
1709
|
+
else
|
|
1710
|
+
finish(undefined);
|
|
1711
|
+
await older;
|
|
1712
|
+
expect(loader.getPackageRuntimeState(packageId)?.status).toBe("running");
|
|
1713
|
+
expect(loader.getPackageRuntimeState(packageId)?.lastError).toBeUndefined();
|
|
1714
|
+
await loader.dispose();
|
|
1715
|
+
});
|
|
1304
1716
|
});
|
|
1305
1717
|
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observed, per-device runtime state of the packages in one data context.
|
|
3
|
+
*
|
|
4
|
+
* The {@link import("./package-loader").PackageLoader} records what it actually
|
|
5
|
+
* loaded, what failed, and why. State is in memory only: it describes this
|
|
6
|
+
* process and resets when the process restarts (and reloads everything).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Outcome of the most recent load, unload, or replacement attempt for a package.
|
|
10
|
+
*
|
|
11
|
+
* - `running` — the selected version loaded and is serving.
|
|
12
|
+
* - `failed` — the last attempt failed. {@link IPackageRuntimeState.running} still names
|
|
13
|
+
* the previous version when it kept serving.
|
|
14
|
+
* - `unavailable` — the selected version's bundle is not on this device yet (for example
|
|
15
|
+
* still syncing).
|
|
16
|
+
* - `unsupported` — this host cannot run the package (an isolated package on a host
|
|
17
|
+
* without an isolated runtime).
|
|
18
|
+
* - `disabled` — the package row is disabled for the group.
|
|
19
|
+
* - `unloaded` — the package was unloaded or uninstalled.
|
|
20
|
+
*/
|
|
21
|
+
export type PackageRuntimeStatus = "running" | "failed" | "unavailable" | "unsupported" | "disabled" | "unloaded";
|
|
22
|
+
/**
|
|
23
|
+
* How a running package executes.
|
|
24
|
+
*
|
|
25
|
+
* - `isolated` — an isolated artifact served by the host's isolated runtime.
|
|
26
|
+
* - `contract` — an in-process `definePackage()` bundle.
|
|
27
|
+
* - `legacy` — an in-process legacy bundle without a package definition.
|
|
28
|
+
*/
|
|
29
|
+
export type PackageRuntimeKind = "isolated" | "contract" | "legacy";
|
|
30
|
+
/** A contract a running package provides. */
|
|
31
|
+
export interface IPackageRuntimeProvidedContract {
|
|
32
|
+
/** Provided contract identity. */
|
|
33
|
+
contractId: string;
|
|
34
|
+
/** Provided contract version. */
|
|
35
|
+
version: number;
|
|
36
|
+
}
|
|
37
|
+
/** A contract a running isolated package consumes, with whether a provider matched. */
|
|
38
|
+
export interface IPackageRuntimeConsumedContract {
|
|
39
|
+
/** Consumed contract identity. */
|
|
40
|
+
contractId: string;
|
|
41
|
+
/** Consumed contract version. */
|
|
42
|
+
version: number;
|
|
43
|
+
/** True when the package declared the dependency optional. */
|
|
44
|
+
optional?: boolean;
|
|
45
|
+
/** True when a provider for the dependency was available at install time. */
|
|
46
|
+
satisfied: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** The package version currently serving on this device. */
|
|
49
|
+
export interface IPackageRunningVersion {
|
|
50
|
+
/** PackageVersion record that was loaded, when known. */
|
|
51
|
+
packageVersionId?: string;
|
|
52
|
+
/** Semantic version of the loaded PackageVersion. */
|
|
53
|
+
version?: string;
|
|
54
|
+
/** Release channel of the loaded PackageVersion (`dev`, `beta`, `stable`, ...). */
|
|
55
|
+
versionTag?: string;
|
|
56
|
+
/** How the package executes. */
|
|
57
|
+
kind: PackageRuntimeKind;
|
|
58
|
+
/** Isolated execution mode; absent for in-process packages. */
|
|
59
|
+
executionMode?: "sandbox" | "privileged";
|
|
60
|
+
/** Contracts the isolated runtime reported as provided. */
|
|
61
|
+
provides?: IPackageRuntimeProvidedContract[];
|
|
62
|
+
/** Contracts the isolated runtime reported as consumed. */
|
|
63
|
+
consumes?: IPackageRuntimeConsumedContract[];
|
|
64
|
+
}
|
|
65
|
+
/** The most recent failure observed for a package. */
|
|
66
|
+
export interface IPackageRuntimeError {
|
|
67
|
+
/** `load` — installing or evaluating the bundle; `worker` — starting the isolated worker. */
|
|
68
|
+
phase: "load" | "worker";
|
|
69
|
+
/** Error message. */
|
|
70
|
+
message: string;
|
|
71
|
+
/** PackageVersion whose load failed, when known. */
|
|
72
|
+
packageVersionId?: string;
|
|
73
|
+
/** Semantic version whose load failed, when known. */
|
|
74
|
+
version?: string;
|
|
75
|
+
/** Epoch milliseconds when the failure was recorded. */
|
|
76
|
+
at: number;
|
|
77
|
+
}
|
|
78
|
+
/** Health of a package's isolated worker, reported by the isolated runtime. */
|
|
79
|
+
export interface IPackageWorkerHealth {
|
|
80
|
+
/** True while a worker process/thread is running for the package. */
|
|
81
|
+
workerRunning: boolean;
|
|
82
|
+
/** Last worker start or crash failure since the package was installed or replaced. */
|
|
83
|
+
lastWorkerError?: {
|
|
84
|
+
message: string;
|
|
85
|
+
at: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Observed runtime state of one package on this device, in one data context. */
|
|
89
|
+
export interface IPackageRuntimeState {
|
|
90
|
+
/** Package the state describes. */
|
|
91
|
+
packageId: string;
|
|
92
|
+
/** Outcome of the most recent attempt. */
|
|
93
|
+
status: PackageRuntimeStatus;
|
|
94
|
+
/** What is serving now; kept when a replacement fails. */
|
|
95
|
+
running?: IPackageRunningVersion;
|
|
96
|
+
/** Most recent failure; cleared when a later load succeeds. */
|
|
97
|
+
lastError?: IPackageRuntimeError;
|
|
98
|
+
/** Human-readable explanation for `unavailable`, `unsupported`, `disabled`, or `unloaded`. */
|
|
99
|
+
reason?: string;
|
|
100
|
+
/** Isolated worker health, merged on read when the runtime reports it. */
|
|
101
|
+
worker?: IPackageWorkerHealth;
|
|
102
|
+
/** Epoch milliseconds of the last change. */
|
|
103
|
+
updatedAt: number;
|
|
104
|
+
}
|
|
105
|
+
/** Map of package id to observed runtime state for one data context. */
|
|
106
|
+
export type PackageRuntimeStates = Record<string, IPackageRuntimeState>;
|
|
107
|
+
/**
|
|
108
|
+
* Return the message of an unknown thrown value.
|
|
109
|
+
*
|
|
110
|
+
* @param error - Thrown value.
|
|
111
|
+
*/
|
|
112
|
+
export declare function runtimeErrorMessage(error: unknown): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Observed, per-device runtime state of the packages in one data context.
|
|
4
|
+
*
|
|
5
|
+
* The {@link import("./package-loader").PackageLoader} records what it actually
|
|
6
|
+
* loaded, what failed, and why. State is in memory only: it describes this
|
|
7
|
+
* process and resets when the process restarts (and reloads everything).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.runtimeErrorMessage = runtimeErrorMessage;
|
|
11
|
+
/**
|
|
12
|
+
* Return the message of an unknown thrown value.
|
|
13
|
+
*
|
|
14
|
+
* @param error - Thrown value.
|
|
15
|
+
*/
|
|
16
|
+
function runtimeErrorMessage(error) {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
@@ -31,8 +31,8 @@ async function resolveConsumedContracts(dataContext, registry, packageDefinition
|
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
33
|
const consumer = dataContext.packageLoader?.resolveConsumedContract
|
|
34
|
-
? await dataContext.packageLoader.resolveConsumedContract(dep)
|
|
35
|
-
: await resolveLegacyLocalConsumer(dataContext, selected, dep.contractId, dep.version);
|
|
34
|
+
? await dataContext.packageLoader.resolveConsumedContract(dep, packageDefinition.packageId)
|
|
35
|
+
: await resolveLegacyLocalConsumer(dataContext, selected, dep.contractId, dep.version, packageDefinition.packageId);
|
|
36
36
|
if (!consumer) {
|
|
37
37
|
settleMissing(dep.optional === true, handle, dep.contractId, dep.version, `provider ${selected.providerPackageId} has no live endpoint`);
|
|
38
38
|
continue;
|
|
@@ -61,7 +61,7 @@ async function resolveRemoteContracts(dataContext, packageDefinition) {
|
|
|
61
61
|
if (!loader?.resolveRemoteContract) {
|
|
62
62
|
throw new Error(`Remote contract ${key} requires a package loader on the data context`);
|
|
63
63
|
}
|
|
64
|
-
const remote = await loader.resolveRemoteContract(declaration);
|
|
64
|
+
const remote = await loader.resolveRemoteContract(declaration, packageDefinition.packageId);
|
|
65
65
|
if (!remote) {
|
|
66
66
|
throw new Error(`Remote contract ${key} could not be resolved: its definition is not registered on this device`);
|
|
67
67
|
}
|
|
@@ -72,7 +72,7 @@ async function resolveRemoteContracts(dataContext, packageDefinition) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
async function resolveLegacyLocalConsumer(dataContext, selected, contractId, version) {
|
|
75
|
+
async function resolveLegacyLocalConsumer(dataContext, selected, contractId, version, callerPackageId) {
|
|
76
76
|
const resolver = (0, contract_resolvers_1.getContractResolver)(contractId, version, selected.providerPackageId, dataContext);
|
|
77
77
|
if (!resolver) {
|
|
78
78
|
return undefined;
|
|
@@ -80,6 +80,7 @@ async function resolveLegacyLocalConsumer(dataContext, selected, contractId, ver
|
|
|
80
80
|
const resolution = await resolver({ dataContext });
|
|
81
81
|
return (0, create_local_consumer_1.createLocalContractConsumer)(selected.definition, resolution, {
|
|
82
82
|
dataContextId: dataContext.dataContextId,
|
|
83
|
+
context: { dataContext, callerPackageId },
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
86
|
function settleMissing(optional, handle, contractId, version, reason) {
|
|
@@ -135,7 +135,7 @@ describe("resolveConsumedContracts", () => {
|
|
|
135
135
|
remoteHandle = pkg.remote(contractId, 1);
|
|
136
136
|
});
|
|
137
137
|
await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(dataContext, new registry_1.ContractRegistry(), packageDefinition);
|
|
138
|
-
expect(resolveRemoteContract).toHaveBeenCalledWith(expect.objectContaining({ contractId, version: 1 }));
|
|
138
|
+
expect(resolveRemoteContract).toHaveBeenCalledWith(expect.objectContaining({ contractId, version: 1 }), packageDefinition.packageId);
|
|
139
139
|
await expect(remoteHandle?.consumer).resolves.toBe(remote);
|
|
140
140
|
});
|
|
141
141
|
it("rejects a remote handle when the contract definition is unknown locally", async () => {
|