@peers-app/peers-sdk 0.25.21 → 0.26.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/dist/contracts/__tests__/builder.test.js +0 -2
- package/dist/contracts/__tests__/contract-provider-router.test.js +51 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +21 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +84 -0
- package/dist/contracts/__tests__/legacy-removal.test.d.ts +1 -0
- package/dist/contracts/__tests__/legacy-removal.test.js +90 -0
- package/dist/contracts/__tests__/system-contracts.test.js +88 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.js +44 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +446 -0
- package/dist/contracts/__tests__/tool-catalog.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog.test.js +649 -0
- package/dist/contracts/__tests__/workflow-context.test.d.ts +1 -0
- package/dist/contracts/__tests__/workflow-context.test.js +61 -0
- package/dist/contracts/builder.d.ts +9 -8
- package/dist/contracts/builder.js +34 -16
- package/dist/contracts/contract-provider-router.d.ts +12 -1
- package/dist/contracts/contract-provider-router.js +40 -2
- package/dist/contracts/contract-proxy.d.ts +20 -6
- package/dist/contracts/contract-proxy.js +23 -12
- package/dist/contracts/contract-resolvers.d.ts +22 -3
- package/dist/contracts/contract-resolvers.js +86 -7
- package/dist/contracts/extract.d.ts +7 -0
- package/dist/contracts/extract.js +3 -0
- package/dist/contracts/index.d.ts +11 -4
- package/dist/contracts/index.js +14 -2
- package/dist/contracts/registry.d.ts +3 -0
- package/dist/contracts/registry.js +2 -0
- package/dist/contracts/system/desktop-tools-contract.d.ts +5 -0
- package/dist/contracts/system/desktop-tools-contract.js +88 -0
- package/dist/contracts/system/host-tools-contract.d.ts +5 -0
- package/dist/contracts/system/host-tools-contract.js +76 -0
- package/dist/contracts/system/index.d.ts +3 -0
- package/dist/contracts/system/index.js +3 -0
- package/dist/contracts/system/tool-catalog-contract.d.ts +73 -0
- package/dist/contracts/system/tool-catalog-contract.js +70 -0
- package/dist/contracts/system-contracts.d.ts +3 -1
- package/dist/contracts/system-contracts.js +20 -6
- package/dist/contracts/tool-catalog-identity.d.ts +35 -0
- package/dist/contracts/tool-catalog-identity.js +88 -0
- package/dist/contracts/tool-catalog-migrations.d.ts +12 -0
- package/dist/contracts/tool-catalog-migrations.js +179 -0
- package/dist/contracts/tool-catalog-service.d.ts +30 -0
- package/dist/contracts/tool-catalog-service.js +287 -0
- package/dist/contracts/tool-catalog.d.ts +91 -0
- package/dist/contracts/tool-catalog.js +490 -0
- package/dist/contracts/types.d.ts +7 -5
- package/dist/data/messages.d.ts +3 -3
- package/dist/data/orm/sql.data-source.d.ts +5 -0
- package/dist/data/orm/sql.data-source.js +28 -10
- package/dist/data/orm/sql.data-source.test.d.ts +1 -1
- package/dist/data/orm/sql.data-source.test.js +160 -0
- package/dist/data/orm/table-container.d.ts +14 -0
- package/dist/data/orm/table-container.js +20 -0
- package/dist/data/tool-access-approvals.d.ts +6 -6
- package/dist/data/tool-catalog-fields.d.ts +31 -0
- package/dist/data/tool-catalog-fields.js +67 -0
- package/dist/data/tools.d.ts +38 -10
- package/dist/data/tools.js +31 -8
- package/dist/data/workflow-logs.d.ts +6 -2
- package/dist/data/workflow-logs.js +7 -5
- package/dist/data/workflow-runs.d.ts +6 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -3
- package/dist/package-loader/contract-package-loader.d.ts +12 -3
- package/dist/package-loader/contract-package-loader.js +54 -50
- package/dist/package-loader/contract-package-loader.test.d.ts +1 -1
- package/dist/package-loader/contract-package-loader.test.js +172 -13
- package/dist/package-loader/host-authority-loading.test.js +16 -3
- package/dist/package-loader/isolated-runtime.d.ts +14 -2
- package/dist/package-loader/isolated-table-gateway.d.ts +3 -1
- package/dist/package-loader/isolated-table-gateway.js +54 -5
- package/dist/package-loader/isolated-table-gateway.test.js +123 -0
- package/dist/package-loader/package-loader.d.ts +14 -2
- package/dist/package-loader/package-loader.js +184 -62
- package/dist/package-loader/package-loader.test.js +674 -1
- package/dist/package-loader/resolve-consumed-contracts.js +1 -1
- package/dist/system-ids.d.ts +6 -0
- package/dist/system-ids.js +7 -1
- package/dist/types/peers-package.d.ts +3 -2
- package/dist/types/workflow-run-context.d.ts +12 -2
- package/package.json +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -17
- package/dist/tools/tools-factory.d.ts +0 -5
- package/dist/tools/tools-factory.js +0 -34
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
require("../index");
|
|
4
|
+
const mockBeforeToolSave = jest.fn();
|
|
5
|
+
jest.mock("../data/tools", () => {
|
|
6
|
+
const actual = jest.requireActual("../data/tools");
|
|
7
|
+
const contexts = new WeakMap();
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
Tools: (context) => {
|
|
11
|
+
let rows = contexts.get(context);
|
|
12
|
+
if (!rows) {
|
|
13
|
+
rows = new Map();
|
|
14
|
+
contexts.set(context, rows);
|
|
15
|
+
}
|
|
16
|
+
const records = rows;
|
|
17
|
+
return {
|
|
18
|
+
list: async () => [...records.values()],
|
|
19
|
+
get: async (id) => records.get(id),
|
|
20
|
+
save: async (row, opts) => {
|
|
21
|
+
await mockBeforeToolSave(row);
|
|
22
|
+
if (!opts?.insertOnly || !records.has(row.toolId))
|
|
23
|
+
records.set(row.toolId, row);
|
|
24
|
+
return records.get(row.toolId);
|
|
25
|
+
},
|
|
26
|
+
update: async (patch) => {
|
|
27
|
+
const row = { ...records.get(patch.toolId), ...patch };
|
|
28
|
+
records.set(patch.toolId, row);
|
|
29
|
+
return row;
|
|
30
|
+
},
|
|
31
|
+
dataChanged: { subscribe: () => ({ unsubscribe: () => true }) },
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
11
36
|
jest.mock("../data/assistants", () => ({
|
|
12
37
|
Assistants: () => ({
|
|
13
38
|
save: jest.fn().mockResolvedValue(undefined),
|
|
14
39
|
}),
|
|
15
40
|
}));
|
|
16
41
|
const registry_1 = require("../contracts/registry");
|
|
42
|
+
const tool_catalog_1 = require("../contracts/tool-catalog");
|
|
43
|
+
const tool_catalog_identity_1 = require("../contracts/tool-catalog-identity");
|
|
44
|
+
const table_container_1 = require("../data/orm/table-container");
|
|
45
|
+
const tools_1 = require("../data/tools");
|
|
46
|
+
const field_type_1 = require("../types/field-type");
|
|
17
47
|
const utils_1 = require("../utils");
|
|
18
48
|
const contract_package_loader_1 = require("./contract-package-loader");
|
|
19
49
|
describe("installContractPackage registry wiring", () => {
|
|
@@ -40,7 +70,6 @@ describe("installContractPackage registry wiring", () => {
|
|
|
40
70
|
alsoImplements: new Map(),
|
|
41
71
|
assistants: [],
|
|
42
72
|
appNavs: [],
|
|
43
|
-
toolInstances: [],
|
|
44
73
|
tableDefinitions: [],
|
|
45
74
|
...overrides,
|
|
46
75
|
};
|
|
@@ -49,7 +78,7 @@ describe("installContractPackage registry wiring", () => {
|
|
|
49
78
|
return {
|
|
50
79
|
dataContextId: "test-ctx",
|
|
51
80
|
tableContainer: {
|
|
52
|
-
|
|
81
|
+
registerTableDefinitions: jest.fn(),
|
|
53
82
|
},
|
|
54
83
|
};
|
|
55
84
|
}
|
|
@@ -62,9 +91,59 @@ describe("installContractPackage registry wiring", () => {
|
|
|
62
91
|
expect(registry.getProviderPackageId(contractId, 1)).toBe(packageId);
|
|
63
92
|
expect(registry.getContractKeysForPackage(packageId)).toEqual([`${contractId}@1`]);
|
|
64
93
|
});
|
|
94
|
+
it("persists CLI runner metadata without executable registration", async () => {
|
|
95
|
+
const registry = new registry_1.ContractRegistry();
|
|
96
|
+
const context = mockDataContext();
|
|
97
|
+
const tool = {
|
|
98
|
+
toolId: "00mlwl2km0iymo1hwhtvqij2n",
|
|
99
|
+
name: "cli_assistant_runner",
|
|
100
|
+
usageDescription: "CLI",
|
|
101
|
+
code: "",
|
|
102
|
+
inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
103
|
+
outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
104
|
+
isAssistantRunner: true,
|
|
105
|
+
configSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
|
|
106
|
+
};
|
|
107
|
+
const definition = makeDefinition({
|
|
108
|
+
toolMetadata: [
|
|
109
|
+
{
|
|
110
|
+
contractId: "00mp287gdomtx12ald8oelyp0",
|
|
111
|
+
version: 1,
|
|
112
|
+
toolName: tool.name,
|
|
113
|
+
isAssistantRunner: true,
|
|
114
|
+
configSchema: tool.configSchema,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
definition.contracts[0] = {
|
|
119
|
+
...definition.contracts[0],
|
|
120
|
+
contractId: "00mp287gdomtx12ald8oelyp0",
|
|
121
|
+
tools: [{ name: tool.name, usageDescription: "CLI", inputFields: [], outputFields: [] }],
|
|
122
|
+
};
|
|
123
|
+
await (0, contract_package_loader_1.installContractPackage)(context, definition, { registry });
|
|
124
|
+
expect(await (0, tools_1.Tools)(context).get(tool.toolId)).toMatchObject({
|
|
125
|
+
isAssistantRunner: true,
|
|
126
|
+
configSchema: tool.configSchema,
|
|
127
|
+
catalogState: "resolved",
|
|
128
|
+
});
|
|
129
|
+
await registry.toolCatalog?.dispose();
|
|
130
|
+
jest.clearAllMocks();
|
|
131
|
+
});
|
|
65
132
|
it("does not register providers when no registry is provided", async () => {
|
|
66
133
|
const dataContext = mockDataContext();
|
|
67
|
-
|
|
134
|
+
const dispose = jest.spyOn(tool_catalog_1.ToolCatalog.prototype, "dispose");
|
|
135
|
+
try {
|
|
136
|
+
await expect((0, contract_package_loader_1.installContractPackage)(dataContext, makeDefinition())).resolves.toBeUndefined();
|
|
137
|
+
// The throwaway registry's catalog must not outlive the call.
|
|
138
|
+
expect(dispose).toHaveBeenCalledTimes(1);
|
|
139
|
+
const registry = new registry_1.ContractRegistry();
|
|
140
|
+
await (0, contract_package_loader_1.installContractPackage)(dataContext, makeDefinition(), { registry });
|
|
141
|
+
expect(dispose).toHaveBeenCalledTimes(1);
|
|
142
|
+
await registry.toolCatalog?.dispose();
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
dispose.mockRestore();
|
|
146
|
+
}
|
|
68
147
|
});
|
|
69
148
|
it("keeps the previous provider active when package persistence fails", async () => {
|
|
70
149
|
const registry = new registry_1.ContractRegistry();
|
|
@@ -72,7 +151,7 @@ describe("installContractPackage registry wiring", () => {
|
|
|
72
151
|
previous.contracts[0] = { ...previous.contracts[0], name: "Previous Contract" };
|
|
73
152
|
await (0, contract_package_loader_1.installContractPackage)(mockDataContext(), previous, { registry });
|
|
74
153
|
const dataContext = mockDataContext();
|
|
75
|
-
dataContext.tableContainer.
|
|
154
|
+
dataContext.tableContainer.registerTableDefinitions.mockImplementation(() => {
|
|
76
155
|
throw new Error("table registration failed");
|
|
77
156
|
});
|
|
78
157
|
const replacement = makeDefinition({
|
|
@@ -82,6 +161,87 @@ describe("installContractPackage registry wiring", () => {
|
|
|
82
161
|
expect(registry.resolve(contractId, 1)?.name).toBe("Previous Contract");
|
|
83
162
|
expect(registry.getProviderPackageId(contractId, 1)).toBe(packageId);
|
|
84
163
|
});
|
|
164
|
+
it.each([
|
|
165
|
+
"catalog-write",
|
|
166
|
+
"invalid-table",
|
|
167
|
+
])("keeps active host tables through %s failure and permits retry", async (failure) => {
|
|
168
|
+
const registry = new registry_1.ContractRegistry();
|
|
169
|
+
const container = new table_container_1.TableContainer((metaData) => ({ metaData }));
|
|
170
|
+
const context = { dataContextId: (0, utils_1.newid)(), tableContainer: container };
|
|
171
|
+
const definition = makeDefinition();
|
|
172
|
+
definition.contracts[0].tools = [
|
|
173
|
+
{ name: "read", usageDescription: "Read", inputFields: [], outputFields: [] },
|
|
174
|
+
];
|
|
175
|
+
const tableId = (0, utils_1.newid)();
|
|
176
|
+
const tableDefinition = (primaryKeyName) => ({
|
|
177
|
+
metaData: {
|
|
178
|
+
tableId,
|
|
179
|
+
name: "State",
|
|
180
|
+
description: "State",
|
|
181
|
+
primaryKeyName,
|
|
182
|
+
fields: [{ name: primaryKeyName, type: field_type_1.FieldType.id }],
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
definition.tableDefinitions = [tableDefinition("oldId")];
|
|
186
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
187
|
+
try {
|
|
188
|
+
await (0, contract_package_loader_1.installContractPackage)(context, definition, { registry });
|
|
189
|
+
const previous = registry.resolve(contractId, 1);
|
|
190
|
+
const oldTable = container.getTableByName(`State_${tableId}`);
|
|
191
|
+
let saves = 0;
|
|
192
|
+
mockBeforeToolSave.mockImplementation(async (row) => {
|
|
193
|
+
if (failure === "catalog-write" && row.contractId === contractId && ++saves === 2) {
|
|
194
|
+
expect(container.getTableByName(`State_${tableId}`)).toBe(oldTable);
|
|
195
|
+
throw new Error("final catalog disk failure");
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
const replacement = { ...definition, tableDefinitions: [tableDefinition("newId")] };
|
|
199
|
+
if (failure === "invalid-table") {
|
|
200
|
+
const invalid = tableDefinition("invalidId");
|
|
201
|
+
invalid.metaData.tableId = "invalid";
|
|
202
|
+
replacement.tableDefinitions.push(invalid);
|
|
203
|
+
}
|
|
204
|
+
await expect((0, contract_package_loader_1.installContractPackage)(context, replacement, { registry })).rejects.toThrow(failure === "catalog-write" ? "final catalog disk failure" : "Invalid tableId");
|
|
205
|
+
expect(registry.resolve(contractId, 1)).toBe(previous);
|
|
206
|
+
expect(container.getTableByName(`State_${tableId}`)).toBe(oldTable);
|
|
207
|
+
expect(oldTable.metaData.primaryKeyName).toBe("oldId");
|
|
208
|
+
mockBeforeToolSave.mockReset();
|
|
209
|
+
replacement.tableDefinitions = [tableDefinition("newId")];
|
|
210
|
+
await (0, contract_package_loader_1.installContractPackage)(context, replacement, { registry });
|
|
211
|
+
expect(container.getTableByName(`State_${tableId}`).metaData.primaryKeyName).toBe("newId");
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
mockBeforeToolSave.mockReset();
|
|
215
|
+
warn.mockRestore();
|
|
216
|
+
await registry.toolCatalog?.dispose();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
it("indexes public host definitions without instances and activates only after loader preparation", async () => {
|
|
220
|
+
const registry = new registry_1.ContractRegistry();
|
|
221
|
+
const dataContext = mockDataContext();
|
|
222
|
+
const definition = makeDefinition();
|
|
223
|
+
definition.contracts[0].tools = [
|
|
224
|
+
{ name: "read", usageDescription: "Read", inputFields: [], outputFields: [] },
|
|
225
|
+
];
|
|
226
|
+
const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
|
|
227
|
+
const activated = jest.fn();
|
|
228
|
+
const prepare = jest.fn(async () => {
|
|
229
|
+
expect(await (0, tools_1.Tools)(dataContext).get(toolId)).toMatchObject({ catalogState: "resolved" });
|
|
230
|
+
expect(registry.resolve(contractId, 1)).toBeUndefined();
|
|
231
|
+
throw new Error("loader preparation failed");
|
|
232
|
+
});
|
|
233
|
+
await expect((0, contract_package_loader_1.installContractPackage)(dataContext, definition, {
|
|
234
|
+
registry,
|
|
235
|
+
prepare,
|
|
236
|
+
onActivated: activated,
|
|
237
|
+
})).rejects.toThrow(/loader preparation failed/);
|
|
238
|
+
expect(activated).not.toHaveBeenCalled();
|
|
239
|
+
expect(registry.resolve(contractId, 1)).toBeUndefined();
|
|
240
|
+
await (0, contract_package_loader_1.installContractPackage)(dataContext, definition, { registry, onActivated: activated });
|
|
241
|
+
expect(activated).toHaveBeenCalledTimes(1);
|
|
242
|
+
expect((await (0, tools_1.Tools)(dataContext).list()).filter((row) => row.contractId === contractId)).toHaveLength(1);
|
|
243
|
+
await registry.toolCatalog?.dispose();
|
|
244
|
+
});
|
|
85
245
|
});
|
|
86
246
|
describe("extractPackageDefinition", () => {
|
|
87
247
|
it("returns a package definition when packageId and contracts are present", () => {
|
|
@@ -93,7 +253,6 @@ describe("extractPackageDefinition", () => {
|
|
|
93
253
|
alsoImplements: new Map(),
|
|
94
254
|
assistants: [],
|
|
95
255
|
appNavs: [],
|
|
96
|
-
toolInstances: [],
|
|
97
256
|
tableDefinitions: [],
|
|
98
257
|
};
|
|
99
258
|
expect((0, contract_package_loader_1.extractPackageDefinition)({ packageDefinition: def })).toBe(def);
|
|
@@ -6,13 +6,26 @@ test("host-backed package endpoints honor declared propagation and reread live m
|
|
|
6
6
|
const contractId = (0, index_1.newid)();
|
|
7
7
|
const userId = (0, index_1.newid)();
|
|
8
8
|
const groupId = (0, index_1.newid)();
|
|
9
|
+
const rows = new Map();
|
|
10
|
+
const tools = {
|
|
11
|
+
list: async () => [...rows.values()],
|
|
12
|
+
get: async (id) => rows.get(id),
|
|
13
|
+
save: async (row, opts) => {
|
|
14
|
+
if (!opts?.insertOnly || !rows.has(row.toolId))
|
|
15
|
+
rows.set(row.toolId, row);
|
|
16
|
+
return rows.get(row.toolId);
|
|
17
|
+
},
|
|
18
|
+
dataChanged: { subscribe: () => ({ unsubscribe: () => true }) },
|
|
19
|
+
};
|
|
9
20
|
const dataContext = {
|
|
10
21
|
dataContextId: groupId,
|
|
11
22
|
groupId,
|
|
12
23
|
tableContainer: {
|
|
13
|
-
getTable: (meta) => meta.name === "
|
|
14
|
-
?
|
|
15
|
-
:
|
|
24
|
+
getTable: (meta) => meta.name === "Tools"
|
|
25
|
+
? tools
|
|
26
|
+
: meta.name === "Groups"
|
|
27
|
+
? { get: async () => ({ founderUserId: (0, index_1.newid)() }) }
|
|
28
|
+
: { findOne: async () => ({ userId, role: index_1.GroupMemberRole.Admin }) },
|
|
16
29
|
},
|
|
17
30
|
};
|
|
18
31
|
Object.assign(dataContext, {
|
|
@@ -28,6 +28,8 @@ export interface IIsolatedCallPrincipal {
|
|
|
28
28
|
}
|
|
29
29
|
/** Trusted lifecycle metadata supplied when an isolated artifact is installed. */
|
|
30
30
|
export interface IIsolatedPackageInstallOptions {
|
|
31
|
+
/** Host load-generation guard, rechecked before publication after asynchronous preparation. */
|
|
32
|
+
assertCurrent?: () => void;
|
|
31
33
|
/**
|
|
32
34
|
* Contract maturity derived from the trusted active PackageVersion.
|
|
33
35
|
* Guest artifacts cannot select or change this value.
|
|
@@ -82,6 +84,12 @@ export interface IIsolatedPackageRuntime {
|
|
|
82
84
|
provides(contractId: string, version: number): boolean;
|
|
83
85
|
/** True when this runtime has installed `packageId`. */
|
|
84
86
|
isInstalled(packageId: string): boolean;
|
|
87
|
+
/** Read effective policy without starting a worker. */
|
|
88
|
+
getEffectiveToolLevel?(ref: {
|
|
89
|
+
contractId: string;
|
|
90
|
+
version: number;
|
|
91
|
+
toolName: string;
|
|
92
|
+
}, providerPackageId?: string): number;
|
|
85
93
|
/**
|
|
86
94
|
* Run a tool the package provides, starting its worker if needed.
|
|
87
95
|
*
|
|
@@ -142,9 +150,13 @@ export interface IIsolatedPackageTableGateway {
|
|
|
142
150
|
register(declaration: IIsolatedPackageTableDeclaration): Promise<void>;
|
|
143
151
|
/**
|
|
144
152
|
* Validate and bind a package's complete table set as one registration.
|
|
145
|
-
*
|
|
153
|
+
* Run `activate` synchronously after all awaited schema persistence, immediately
|
|
154
|
+
* before publishing the prepared bindings. The callback must check cancellation
|
|
155
|
+
* before changing provider state and must not return a promise. No awaited work
|
|
156
|
+
* may separate it from binding publication. If preparation or the callback fails,
|
|
157
|
+
* restore persisted definitions and retain all previous bindings before rejecting.
|
|
146
158
|
*/
|
|
147
|
-
registerBatch(declarations: readonly IIsolatedPackageTableDeclaration[]): Promise<void>;
|
|
159
|
+
registerBatch(declarations: readonly IIsolatedPackageTableDeclaration[], activate?: () => void): Promise<void>;
|
|
148
160
|
/** Return a copied record, or `undefined` when it does not exist. */
|
|
149
161
|
get(ref: IIsolatedPackageTableRef, recordId: string): Promise<Record<string, unknown> | undefined>;
|
|
150
162
|
/** Insert or replace a copied record and return the stored copy. */
|
|
@@ -9,7 +9,9 @@ import { type IIsolatedPackageTableGateway } from "./isolated-runtime";
|
|
|
9
9
|
export declare function createMemoryIsolatedPackageTableGateway(): IIsolatedPackageTableGateway;
|
|
10
10
|
/**
|
|
11
11
|
* Create a per-data-context gateway that registers isolated table schemas in
|
|
12
|
-
* `TableContainer` and performs host-owned `get` / `save`.
|
|
12
|
+
* `TableContainer` and performs host-owned `get` / `save`. Registration serializes
|
|
13
|
+
* schema persistence with synchronous provider activation; a rejected activation
|
|
14
|
+
* restores the stored definitions before subsequent gateway calls can proceed.
|
|
13
15
|
*
|
|
14
16
|
* @param dataContext - Owning personal or group data context.
|
|
15
17
|
*/
|
|
@@ -190,12 +190,13 @@ function createMemoryIsolatedPackageTableGateway() {
|
|
|
190
190
|
const emitChange = (key, op, dataObject) => {
|
|
191
191
|
void emitterFor(key).emit({ dataObject, op, source: "local" });
|
|
192
192
|
};
|
|
193
|
-
const registerBatch = async (batch) => {
|
|
193
|
+
const registerBatch = async (batch, activate) => {
|
|
194
194
|
const candidateDeclarations = new Map(declarations);
|
|
195
195
|
const candidateByTableId = new Map(byTableId);
|
|
196
196
|
for (const declaration of batch) {
|
|
197
197
|
registerDeclaration(candidateDeclarations, candidateByTableId, declaration);
|
|
198
198
|
}
|
|
199
|
+
activate?.();
|
|
199
200
|
declarations.clear();
|
|
200
201
|
byTableId.clear();
|
|
201
202
|
for (const [key, declaration] of candidateDeclarations) {
|
|
@@ -317,7 +318,9 @@ function createMemoryIsolatedPackageTableGateway() {
|
|
|
317
318
|
}
|
|
318
319
|
/**
|
|
319
320
|
* Create a per-data-context gateway that registers isolated table schemas in
|
|
320
|
-
* `TableContainer` and performs host-owned `get` / `save`.
|
|
321
|
+
* `TableContainer` and performs host-owned `get` / `save`. Registration serializes
|
|
322
|
+
* schema persistence with synchronous provider activation; a rejected activation
|
|
323
|
+
* restores the stored definitions before subsequent gateway calls can proceed.
|
|
321
324
|
*
|
|
322
325
|
* @param dataContext - Owning personal or group data context.
|
|
323
326
|
*/
|
|
@@ -357,7 +360,7 @@ function createIsolatedPackageTableGateway(dataContext) {
|
|
|
357
360
|
return dataContext.tableContainer.getTable(activeMetaData);
|
|
358
361
|
};
|
|
359
362
|
const ensureTable = async (declaration) => activateTable(await prepareTable(declaration));
|
|
360
|
-
const registerBatch = (batch) => {
|
|
363
|
+
const registerBatch = (batch, activate) => {
|
|
361
364
|
const registration = registrationQueue.then(async () => {
|
|
362
365
|
const candidateDeclarations = new Map(declarations);
|
|
363
366
|
const candidateByTableId = new Map(byTableId);
|
|
@@ -372,8 +375,54 @@ function createIsolatedPackageTableGateway(dataContext) {
|
|
|
372
375
|
plans.push(await prepareTable(declaration));
|
|
373
376
|
}
|
|
374
377
|
}
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
// Prepare schemas before touching durable or live definitions. Do not create
|
|
379
|
+
// Table instances here: that also schedules unguarded definition persistence.
|
|
380
|
+
const definitions = plans.map(({ activeMetaData }) => ({
|
|
381
|
+
metaData: activeMetaData,
|
|
382
|
+
schema: (0, types_1.fieldsToSchema)(activeMetaData.fields),
|
|
383
|
+
}));
|
|
384
|
+
const definitionsTable = (0, table_definitions_table_1.TableDefinitions)(dataContext);
|
|
385
|
+
const attempted = [];
|
|
386
|
+
try {
|
|
387
|
+
for (const plan of plans) {
|
|
388
|
+
if (!plan.existingRecord || plan.activeMetaData === plan.incomingMetaData) {
|
|
389
|
+
// Include the current write in rollback even if storage writes then rejects.
|
|
390
|
+
attempted.push(plan);
|
|
391
|
+
await definitionsTable.save({
|
|
392
|
+
tableId: requiredTableId(plan.activeMetaData),
|
|
393
|
+
name: (0, package_table_identity_1.packageScopedTableName)(requiredPackageId(plan.activeMetaData), plan.activeMetaData.name),
|
|
394
|
+
metaData: plan.activeMetaData,
|
|
395
|
+
versionNumber: plan.activeMetaData.versionNumber ?? 0,
|
|
396
|
+
}, { restoreIfDeleted: true });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Cancellation/registry validation and provider activation happen in the
|
|
400
|
+
// same synchronous turn as binding publication. An unload cannot slip
|
|
401
|
+
// between a completed registration promise and the runtime's guard.
|
|
402
|
+
activate?.();
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
const failures = [];
|
|
406
|
+
for (const plan of attempted.reverse()) {
|
|
407
|
+
try {
|
|
408
|
+
if (plan.existingRecord) {
|
|
409
|
+
await definitionsTable.save(plan.existingRecord, { restoreIfDeleted: true });
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
await definitionsTable.delete(requiredTableId(plan.activeMetaData));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (rollbackError) {
|
|
416
|
+
failures.push(rollbackError);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (failures.length) {
|
|
420
|
+
throw new AggregateError([error, ...failures], "Isolated table registration rollback failed");
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
for (const definition of definitions) {
|
|
425
|
+
dataContext.tableContainer.registerTableDefinition(definition, { overwrite: true });
|
|
377
426
|
}
|
|
378
427
|
declarations.clear();
|
|
379
428
|
byTableId.clear();
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const SQLiteDB = require("better-sqlite3");
|
|
4
|
+
const sql_data_source_1 = require("../data/orm/sql.data-source");
|
|
5
|
+
const table_container_1 = require("../data/orm/table-container");
|
|
3
6
|
const package_table_identity_1 = require("../data/package-table-identity");
|
|
7
|
+
const table_definitions_table_1 = require("../data/table-definitions-table");
|
|
4
8
|
const field_type_1 = require("../types/field-type");
|
|
5
9
|
const utils_1 = require("../utils");
|
|
6
10
|
const isolated_table_gateway_1 = require("./isolated-table-gateway");
|
|
@@ -265,3 +269,122 @@ describe("isolated package table gateway", () => {
|
|
|
265
269
|
expect(handlers.size).toBe(0);
|
|
266
270
|
});
|
|
267
271
|
});
|
|
272
|
+
describe("guarded isolated schema activation", () => {
|
|
273
|
+
test("memory bindings remain unchanged when activation rejects", async () => {
|
|
274
|
+
const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
|
|
275
|
+
const old = makeDeclaration({ versionNumber: 1 });
|
|
276
|
+
const next = {
|
|
277
|
+
...old,
|
|
278
|
+
versionNumber: 2,
|
|
279
|
+
primaryKeyName: "newId",
|
|
280
|
+
fields: [{ name: "newId", type: field_type_1.FieldType.id }],
|
|
281
|
+
};
|
|
282
|
+
await gateway.register(old);
|
|
283
|
+
await expect(gateway.registerBatch([next], () => {
|
|
284
|
+
throw new Error("load invalidated");
|
|
285
|
+
})).rejects.toThrow("load invalidated");
|
|
286
|
+
const record = { stateId: (0, utils_1.newid)(), count: 1 };
|
|
287
|
+
await expect(gateway.save(old, record)).resolves.toEqual(record);
|
|
288
|
+
await expect(gateway.register(old)).resolves.toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
test.each([
|
|
291
|
+
"invalidation",
|
|
292
|
+
"storage",
|
|
293
|
+
])("restores SQLite definitions after %s during registration, including fresh tables and restart", async (failure) => {
|
|
294
|
+
const db = new SQLiteDB(":memory:");
|
|
295
|
+
const sql = {
|
|
296
|
+
async close() {
|
|
297
|
+
db.close();
|
|
298
|
+
},
|
|
299
|
+
async get(query, params = []) {
|
|
300
|
+
return db.prepare(query).get(params);
|
|
301
|
+
},
|
|
302
|
+
async all(query, params = []) {
|
|
303
|
+
return db.prepare(query).all(params);
|
|
304
|
+
},
|
|
305
|
+
async exec(query, params = []) {
|
|
306
|
+
return { changes: db.prepare(query).run(params).changes };
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const context = () => ({
|
|
310
|
+
dataContextId: (0, utils_1.newid)(),
|
|
311
|
+
tableContainer: new table_container_1.TableContainer((metaData, schema) => new sql_data_source_1.SQLDataSource(sql, metaData, schema)),
|
|
312
|
+
});
|
|
313
|
+
const dataContext = context();
|
|
314
|
+
const gateway = (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext);
|
|
315
|
+
const old = makeDeclaration({ versionNumber: 1 });
|
|
316
|
+
const next = {
|
|
317
|
+
...old,
|
|
318
|
+
versionNumber: 2,
|
|
319
|
+
primaryKeyName: "newId",
|
|
320
|
+
fields: [{ name: "newId", type: field_type_1.FieldType.id }],
|
|
321
|
+
};
|
|
322
|
+
const added = makeDeclaration({
|
|
323
|
+
packageId: old.packageId,
|
|
324
|
+
tableName: "Added",
|
|
325
|
+
versionNumber: 1,
|
|
326
|
+
});
|
|
327
|
+
const definitions = (0, table_definitions_table_1.TableDefinitions)(dataContext);
|
|
328
|
+
const oldId = (0, package_table_identity_1.packageScopedTableId)(old.packageId, old.tableName);
|
|
329
|
+
const addedId = (0, package_table_identity_1.packageScopedTableId)(added.packageId, added.tableName);
|
|
330
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
331
|
+
let release;
|
|
332
|
+
let arrived;
|
|
333
|
+
const gate = new Promise((resolve) => {
|
|
334
|
+
release = resolve;
|
|
335
|
+
});
|
|
336
|
+
const saving = new Promise((resolve) => {
|
|
337
|
+
arrived = resolve;
|
|
338
|
+
});
|
|
339
|
+
let saveSpy;
|
|
340
|
+
try {
|
|
341
|
+
await gateway.register(old);
|
|
342
|
+
const record = { stateId: (0, utils_1.newid)(), count: 1 };
|
|
343
|
+
await gateway.save(old, record);
|
|
344
|
+
const original = await definitions.get(oldId);
|
|
345
|
+
const oldTable = dataContext.tableContainer.getTableByName(`${old.tableName}_${old.packageId}`);
|
|
346
|
+
const save = definitions.save.bind(definitions);
|
|
347
|
+
let current = true;
|
|
348
|
+
saveSpy = jest.spyOn(definitions, "save").mockImplementation(async (row, opts) => {
|
|
349
|
+
const result = await save(row, opts);
|
|
350
|
+
if (row.tableId === addedId) {
|
|
351
|
+
arrived();
|
|
352
|
+
await gate;
|
|
353
|
+
if (failure === "storage")
|
|
354
|
+
throw new Error("storage failed after write");
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
});
|
|
358
|
+
const activated = jest.fn(() => {
|
|
359
|
+
if (!current)
|
|
360
|
+
throw new Error("load invalidated");
|
|
361
|
+
});
|
|
362
|
+
const registering = gateway.registerBatch([next, added], activated);
|
|
363
|
+
const rejected = expect(registering).rejects.toThrow(failure === "storage" ? "storage failed after write" : "load invalidated");
|
|
364
|
+
await saving;
|
|
365
|
+
expect(dataContext.tableContainer.getTableByName(`${old.tableName}_${old.packageId}`)).toBe(oldTable);
|
|
366
|
+
current = false;
|
|
367
|
+
release();
|
|
368
|
+
await rejected;
|
|
369
|
+
expect(activated).toHaveBeenCalledTimes(failure === "storage" ? 0 : 1);
|
|
370
|
+
expect(await definitions.get(oldId)).toEqual(original);
|
|
371
|
+
expect(await definitions.get(addedId)).toBeUndefined();
|
|
372
|
+
expect(dataContext.tableContainer.getTableByName(`${old.tableName}_${old.packageId}`)).toBe(oldTable);
|
|
373
|
+
await expect(gateway.save(old, record)).resolves.toEqual(record);
|
|
374
|
+
await expect(gateway.get(added, (0, utils_1.newid)())).rejects.toThrow(/Undeclared/);
|
|
375
|
+
// A cold gateway reads the restored durable version, so v1 can reinstall.
|
|
376
|
+
const restarted = (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(context());
|
|
377
|
+
await expect(restarted.register(old)).resolves.toBeUndefined();
|
|
378
|
+
await expect(restarted.get(old, record.stateId)).resolves.toEqual(record);
|
|
379
|
+
saveSpy.mockRestore();
|
|
380
|
+
await expect(gateway.registerBatch([next, added])).resolves.toBeUndefined();
|
|
381
|
+
expect((await definitions.get(oldId))?.versionNumber).toBe(2);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
release();
|
|
385
|
+
saveSpy?.mockRestore();
|
|
386
|
+
warn.mockRestore();
|
|
387
|
+
db.close();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -2,6 +2,7 @@ import type { DataContext } from "../context/data-context";
|
|
|
2
2
|
import { type IContractProviderEndpointRequest } from "../contracts/contract-provider-router";
|
|
3
3
|
import { type IContractConsumer, type IContractProviderEndpoint } from "../contracts/contract-proxy";
|
|
4
4
|
import { ContractRegistry } from "../contracts/registry";
|
|
5
|
+
import { ToolCatalog } from "../contracts/tool-catalog";
|
|
5
6
|
import type { IConsumedContract, IRemoteContract, IRemoteContractConsumer } from "../contracts/types";
|
|
6
7
|
import { type IPackage } from "../data/packages";
|
|
7
8
|
import type { IPeersPackage } from "../types/peers-package";
|
|
@@ -29,17 +30,25 @@ export declare class PackageLoader {
|
|
|
29
30
|
/** Host-injected Zod module used as the `zod` require. */
|
|
30
31
|
static Zod: any;
|
|
31
32
|
private hostConsumes;
|
|
33
|
+
private loadGenerations;
|
|
34
|
+
private hostActivationGenerations;
|
|
35
|
+
private consumedContractRouters;
|
|
36
|
+
private disposed;
|
|
32
37
|
private packageInstances;
|
|
33
|
-
/** Packages
|
|
38
|
+
/** Packages installed through definePackage or isolation; providers own their executable members. */
|
|
34
39
|
private contractInstalledPackages;
|
|
35
40
|
/** Packages installed through the isolated runtime (no host eval). */
|
|
36
41
|
private isolatedPackageIds;
|
|
37
42
|
/**
|
|
38
43
|
* In-memory contract providers for this data context. Seeded with system
|
|
39
|
-
* contracts
|
|
44
|
+
* contracts after catalog readiness; package providers are added via
|
|
40
45
|
* {@link installContractPackage} or the isolated runtime.
|
|
41
46
|
*/
|
|
42
47
|
readonly contractRegistry: ContractRegistry;
|
|
48
|
+
/** Permanent tool metadata owner for this context. */
|
|
49
|
+
readonly toolCatalog: ToolCatalog;
|
|
50
|
+
/** Lazy, retryable readiness boundary; performs no constructor-time database work. */
|
|
51
|
+
get catalogReady(): Promise<void>;
|
|
43
52
|
/** Optional isolated-provider supervisor created from the host factory. */
|
|
44
53
|
readonly isolatedRuntime?: IIsolatedPackageRuntime;
|
|
45
54
|
/** Optional host `require` used when reading local bundles or evaluating legacy packages. */
|
|
@@ -87,6 +96,7 @@ export declare class PackageLoader {
|
|
|
87
96
|
* @throws If no provider or matching endpoint implementation is available.
|
|
88
97
|
*/
|
|
89
98
|
resolveContractEndpoint(request: IContractProviderEndpointRequest, opts?: IPackageContractEndpointOptions): Promise<IContractProviderEndpoint>;
|
|
99
|
+
private guardResolutionTools;
|
|
90
100
|
/**
|
|
91
101
|
* Bind a legacy package's declared consume to its selected provider.
|
|
92
102
|
*
|
|
@@ -129,6 +139,7 @@ export declare class PackageLoader {
|
|
|
129
139
|
* Dispose the isolated runtime for this data context.
|
|
130
140
|
*/
|
|
131
141
|
dispose(): Promise<void>;
|
|
142
|
+
private retireHostEndpoints;
|
|
132
143
|
/**
|
|
133
144
|
* Resolve and load every non-disabled package in this data context.
|
|
134
145
|
* Disabled packages that were previously loaded are unloaded.
|
|
@@ -156,6 +167,7 @@ export declare class PackageLoader {
|
|
|
156
167
|
packageVersionId?: string;
|
|
157
168
|
}): Promise<IPeersPackage | undefined>;
|
|
158
169
|
private _readLocalBundle;
|
|
170
|
+
private guardLoad;
|
|
159
171
|
/**
|
|
160
172
|
* Post-correction: if the package definition carries a version string,
|
|
161
173
|
* update the selected IPackageVersion record to match — but only when that
|