@peers-app/peers-sdk 0.23.10 → 0.23.12
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 +25 -0
- package/dist/contracts/__tests__/contract-provider-router.test.js +45 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +67 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +133 -0
- package/dist/contracts/__tests__/remote-contract-transport.test.d.ts +1 -0
- package/dist/contracts/__tests__/remote-contract-transport.test.js +89 -0
- package/dist/contracts/builder.d.ts +47 -1
- package/dist/contracts/builder.js +80 -22
- package/dist/contracts/contract-provider-router.d.ts +6 -0
- package/dist/contracts/contract-provider-router.js +25 -3
- package/dist/contracts/contract-proxy.d.ts +41 -1
- package/dist/contracts/contract-proxy.js +103 -0
- package/dist/contracts/contract-resolvers.d.ts +47 -13
- package/dist/contracts/contract-resolvers.js +125 -35
- package/dist/contracts/extract.d.ts +4 -1
- package/dist/contracts/extract.js +3 -1
- package/dist/contracts/index.d.ts +4 -3
- package/dist/contracts/index.js +10 -2
- package/dist/contracts/remote-contract-transport.d.ts +69 -0
- package/dist/contracts/remote-contract-transport.js +95 -0
- package/dist/contracts/types.d.ts +57 -0
- package/dist/data/tools.d.ts +4 -0
- package/dist/data/tools.js +7 -0
- package/dist/package-loader/isolated-artifact.d.ts +44 -5
- package/dist/package-loader/isolated-artifact.js +82 -4
- package/dist/package-loader/isolated-artifact.test.js +51 -3
- package/dist/package-loader/isolated-runtime.d.ts +17 -2
- package/dist/package-loader/isolated-runtime.js +3 -1
- package/dist/package-loader/isolated-table-gateway.d.ts +1 -1
- package/dist/package-loader/isolated-table-gateway.js +54 -0
- package/dist/package-loader/isolated-table-gateway.test.js +61 -0
- package/dist/package-loader/package-loader.d.ts +15 -1
- package/dist/package-loader/package-loader.js +48 -0
- package/dist/package-loader/resolve-consumed-contracts.d.ts +6 -2
- package/dist/package-loader/resolve-consumed-contracts.js +30 -2
- package/dist/package-loader/resolve-consumed-contracts.test.js +43 -0
- package/package.json +1 -1
|
@@ -235,12 +235,60 @@ describe("isolated package artifact", () => {
|
|
|
235
235
|
{ contractId: targetContractId, version: { from: 2, to: 4 } },
|
|
236
236
|
]);
|
|
237
237
|
});
|
|
238
|
-
test("
|
|
238
|
+
test("maps declared events to the public contract definition", () => {
|
|
239
239
|
const { artifact } = makeArtifact();
|
|
240
240
|
artifact.manifest.provides[0].events = [
|
|
241
|
-
{
|
|
241
|
+
{
|
|
242
|
+
name: "changed",
|
|
243
|
+
description: "State changed",
|
|
244
|
+
payloadFields: [{ name: "count", type: field_type_1.FieldType.number }],
|
|
245
|
+
},
|
|
246
|
+
{ name: "pinged", description: "No payload" },
|
|
242
247
|
];
|
|
243
|
-
|
|
248
|
+
const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
|
|
249
|
+
expect(manifest.provides[0].events?.[1].payloadFields).toEqual([]);
|
|
250
|
+
const definition = (0, isolated_artifact_1.isolatedContractToDefinition)(manifest.provides[0], manifest.tables ?? []);
|
|
251
|
+
expect(definition.events).toEqual([
|
|
252
|
+
{
|
|
253
|
+
name: "changed",
|
|
254
|
+
description: "State changed",
|
|
255
|
+
payloadFields: [{ name: "count", type: field_type_1.FieldType.number }],
|
|
256
|
+
},
|
|
257
|
+
{ name: "pinged", description: "No payload", payloadFields: [] },
|
|
258
|
+
]);
|
|
259
|
+
expect((0, isolated_artifact_1.findProvidedContractEvent)(manifest.provides, definition.contractId, 1, "changed")).toBe(manifest.provides[0].events?.[0]);
|
|
260
|
+
expect((0, isolated_artifact_1.packageProvidesEvent)(manifest.provides, definition.contractId, 1, "missing")).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
test("allows an events-only contract", () => {
|
|
263
|
+
const { artifact } = makeArtifact();
|
|
264
|
+
artifact.manifest.provides[0].tools = [];
|
|
265
|
+
artifact.manifest.provides[0].events = [{ name: "changed", description: "State changed" }];
|
|
266
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).not.toThrow();
|
|
267
|
+
artifact.manifest.provides[0].events = [];
|
|
268
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/at least one contract member/);
|
|
269
|
+
});
|
|
270
|
+
test("rejects malformed or duplicate events", () => {
|
|
271
|
+
const { artifact } = makeArtifact();
|
|
272
|
+
artifact.manifest.provides[0].events = [
|
|
273
|
+
{ name: "changed", description: "a" },
|
|
274
|
+
{ name: "changed", description: "b" },
|
|
275
|
+
];
|
|
276
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/duplicate event "changed"/);
|
|
277
|
+
artifact.manifest.provides[0].events = [{ name: "changed" }];
|
|
278
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/description is required/);
|
|
279
|
+
artifact.manifest.provides[0].events = [
|
|
280
|
+
{ name: "changed", description: "a", accessLevel: 50 },
|
|
281
|
+
];
|
|
282
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/accessLevel is not supported/);
|
|
283
|
+
artifact.manifest.provides[0].events = [
|
|
284
|
+
{ name: "changed", description: "a", payloadFields: [{ name: "x", type: "nope" }] },
|
|
285
|
+
];
|
|
286
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/type must be a FieldType/);
|
|
287
|
+
});
|
|
288
|
+
test("rejects unsupported contract features instead of dropping them", () => {
|
|
289
|
+
const { artifact } = makeArtifact();
|
|
290
|
+
artifact.manifest.provides[0].streams = [];
|
|
291
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/streams is not supported/);
|
|
244
292
|
});
|
|
245
293
|
test("rejects missing public table references and malformed observable bindings", () => {
|
|
246
294
|
const { artifact } = makeArtifact();
|
|
@@ -126,7 +126,19 @@ export interface IIsolatedPackageTableGateway {
|
|
|
126
126
|
* The host validates declaration and provider identity before calling the gateway.
|
|
127
127
|
*/
|
|
128
128
|
call(ref: IIsolatedPackageTableRef, operation: IsolatedContractTableOperation, args: unknown[]): Promise<unknown>;
|
|
129
|
+
/**
|
|
130
|
+
* Subscribe to a host-owned table event. Only `dataChanged` is supported;
|
|
131
|
+
* payloads are copied before delivery. The host validates declaration and
|
|
132
|
+
* contract exposure before calling the gateway.
|
|
133
|
+
*
|
|
134
|
+
* @returns A callback that detaches the host-side subscription.
|
|
135
|
+
*/
|
|
136
|
+
subscribe(ref: IIsolatedPackageTableRef, eventName: IsolatedContractTableEvent, handler: (event: unknown) => void): Promise<() => void>;
|
|
129
137
|
}
|
|
138
|
+
/** Table events consumers may subscribe to on an isolated contract table. */
|
|
139
|
+
export declare const ISOLATED_CONTRACT_TABLE_EVENTS: readonly ["dataChanged"];
|
|
140
|
+
/** Consumer-subscribable event on an isolated contract table. */
|
|
141
|
+
export type IsolatedContractTableEvent = (typeof ISOLATED_CONTRACT_TABLE_EVENTS)[number];
|
|
130
142
|
/**
|
|
131
143
|
* Trusted identity of one package-owned pvar.
|
|
132
144
|
*
|
|
@@ -168,8 +180,11 @@ export interface IIsolatedPackagePersistentVarGateway {
|
|
|
168
180
|
export interface IIsolatedContractProviderEndpointRequest extends IContractProviderEndpointRequest {
|
|
169
181
|
/** Installed isolated package making the consumed-contract call. */
|
|
170
182
|
consumerPackageId: string;
|
|
171
|
-
/**
|
|
172
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Registry-selected provider package. Omitted for a device-targeted route
|
|
185
|
+
* (`targetDeviceId` set), where the executing device selects the provider.
|
|
186
|
+
*/
|
|
187
|
+
providerPackageId?: string;
|
|
173
188
|
/** Optional deadline the gateway should enforce for provider setup/calls. */
|
|
174
189
|
timeoutMs?: number;
|
|
175
190
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ISOLATED_CONTRACT_TABLE_OPERATIONS = void 0;
|
|
3
|
+
exports.ISOLATED_CONTRACT_TABLE_EVENTS = exports.ISOLATED_CONTRACT_TABLE_OPERATIONS = void 0;
|
|
4
4
|
exports.setIsolatedPackageRuntimeFactory = setIsolatedPackageRuntimeFactory;
|
|
5
5
|
exports.getIsolatedPackageRuntimeFactory = getIsolatedPackageRuntimeFactory;
|
|
6
6
|
/** Standard table methods exposed through isolated contract-table references. */
|
|
@@ -14,6 +14,8 @@ exports.ISOLATED_CONTRACT_TABLE_OPERATIONS = [
|
|
|
14
14
|
"update",
|
|
15
15
|
"delete",
|
|
16
16
|
];
|
|
17
|
+
/** Table events consumers may subscribe to on an isolated contract table. */
|
|
18
|
+
exports.ISOLATED_CONTRACT_TABLE_EVENTS = ["dataChanged"];
|
|
17
19
|
let isolatedRuntimeFactory;
|
|
18
20
|
/**
|
|
19
21
|
* Register the process-wide isolated package runtime factory.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DataContext } from "../context/data-context";
|
|
2
|
-
import type
|
|
2
|
+
import { type IIsolatedPackageTableGateway } from "./isolated-runtime";
|
|
3
3
|
/**
|
|
4
4
|
* In-memory isolated table gateway for tests. Bindings and rows survive
|
|
5
5
|
* `register` of the same owner so unload/reinstall can resume persisted data.
|
|
@@ -6,7 +6,9 @@ const table_container_1 = require("../data/orm/table-container");
|
|
|
6
6
|
const types_1 = require("../data/orm/types");
|
|
7
7
|
const package_table_identity_1 = require("../data/package-table-identity");
|
|
8
8
|
const table_definitions_table_1 = require("../data/table-definitions-table");
|
|
9
|
+
const events_1 = require("../events");
|
|
9
10
|
const utils_1 = require("../utils");
|
|
11
|
+
const isolated_runtime_1 = require("./isolated-runtime");
|
|
10
12
|
const isolated_value_1 = require("./isolated-value");
|
|
11
13
|
function tableKey(ref) {
|
|
12
14
|
return `${ref.packageId}:${ref.tableName}`;
|
|
@@ -159,6 +161,12 @@ function copyTableResult(value) {
|
|
|
159
161
|
function matchesFilter(row, filter = {}) {
|
|
160
162
|
return Object.entries(filter).every(([name, value]) => row[name] === value);
|
|
161
163
|
}
|
|
164
|
+
function assertTableEvent(eventName) {
|
|
165
|
+
if (!isolated_runtime_1.ISOLATED_CONTRACT_TABLE_EVENTS.includes(eventName)) {
|
|
166
|
+
throw new Error(`Unsupported isolated table event '${eventName}'`);
|
|
167
|
+
}
|
|
168
|
+
return eventName;
|
|
169
|
+
}
|
|
162
170
|
/**
|
|
163
171
|
* In-memory isolated table gateway for tests. Bindings and rows survive
|
|
164
172
|
* `register` of the same owner so unload/reinstall can resume persisted data.
|
|
@@ -169,6 +177,19 @@ function createMemoryIsolatedPackageTableGateway() {
|
|
|
169
177
|
const declarations = new Map();
|
|
170
178
|
const byTableId = new Map();
|
|
171
179
|
const rows = new Map();
|
|
180
|
+
const emitters = new Map();
|
|
181
|
+
const emitterFor = (key) => {
|
|
182
|
+
let emitter = emitters.get(key);
|
|
183
|
+
if (!emitter) {
|
|
184
|
+
// Unique per gateway instance so parallel tests never share a global event name.
|
|
185
|
+
emitter = new events_1.Emitter(`isolated-memory-table:${(0, utils_1.newid)()}:${key}`);
|
|
186
|
+
emitters.set(key, emitter);
|
|
187
|
+
}
|
|
188
|
+
return emitter;
|
|
189
|
+
};
|
|
190
|
+
const emitChange = (key, op, dataObject) => {
|
|
191
|
+
void emitterFor(key).emit({ dataObject, op, source: "local" });
|
|
192
|
+
};
|
|
172
193
|
const registerBatch = async (batch) => {
|
|
173
194
|
const candidateDeclarations = new Map(declarations);
|
|
174
195
|
const candidateByTableId = new Map(byTableId);
|
|
@@ -213,6 +234,7 @@ function createMemoryIsolatedPackageTableGateway() {
|
|
|
213
234
|
const tableRows = rows.get(key) ?? new Map();
|
|
214
235
|
tableRows.set(recordId, stored);
|
|
215
236
|
rows.set(key, tableRows);
|
|
237
|
+
emitChange(key, "save", stored);
|
|
216
238
|
return (0, isolated_value_1.copyIsolatedValue)(stored);
|
|
217
239
|
},
|
|
218
240
|
async call(ref, operation, args) {
|
|
@@ -259,6 +281,7 @@ function createMemoryIsolatedPackageTableGateway() {
|
|
|
259
281
|
throw new Error(`Record ${id} does not exist`);
|
|
260
282
|
}
|
|
261
283
|
tableRows.set(id, stored);
|
|
284
|
+
emitChange(key, operation, stored);
|
|
262
285
|
return (0, isolated_value_1.copyIsolatedValue)(stored);
|
|
263
286
|
}
|
|
264
287
|
case "delete": {
|
|
@@ -268,11 +291,28 @@ function createMemoryIsolatedPackageTableGateway() {
|
|
|
268
291
|
if (typeof id !== "string") {
|
|
269
292
|
throw new Error(`Isolated table delete requires "${declaration.primaryKeyName}"`);
|
|
270
293
|
}
|
|
294
|
+
const existing = tableRows.get(id);
|
|
271
295
|
tableRows.delete(id);
|
|
296
|
+
if (existing) {
|
|
297
|
+
emitChange(key, "delete", existing);
|
|
298
|
+
}
|
|
272
299
|
return undefined;
|
|
273
300
|
}
|
|
274
301
|
}
|
|
275
302
|
},
|
|
303
|
+
async subscribe(ref, eventName, handler) {
|
|
304
|
+
assertTableEvent(eventName);
|
|
305
|
+
const key = tableKey(ref);
|
|
306
|
+
if (!declarations.has(key)) {
|
|
307
|
+
throw new Error(`Undeclared isolated table ${ref.tableName}`);
|
|
308
|
+
}
|
|
309
|
+
const subscription = emitterFor(key).event.subscribe((event) => {
|
|
310
|
+
handler((0, isolated_value_1.copyIsolatedValue)(event));
|
|
311
|
+
});
|
|
312
|
+
return () => {
|
|
313
|
+
subscription.unsubscribe();
|
|
314
|
+
};
|
|
315
|
+
},
|
|
276
316
|
};
|
|
277
317
|
}
|
|
278
318
|
/**
|
|
@@ -371,5 +411,19 @@ function createIsolatedPackageTableGateway(dataContext) {
|
|
|
371
411
|
const result = await method.apply(table, (0, isolated_value_1.copyIsolatedValue)(args));
|
|
372
412
|
return copyTableResult(result);
|
|
373
413
|
},
|
|
414
|
+
async subscribe(ref, eventName, handler) {
|
|
415
|
+
assertTableEvent(eventName);
|
|
416
|
+
await registrationQueue;
|
|
417
|
+
const table = await ensureTable(resolve(ref));
|
|
418
|
+
const subscription = table.dataChanged.subscribe((event) => {
|
|
419
|
+
handler({
|
|
420
|
+
...event,
|
|
421
|
+
dataObject: copyTableResult(event.dataObject),
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
return () => {
|
|
425
|
+
subscription.unsubscribe();
|
|
426
|
+
};
|
|
427
|
+
},
|
|
374
428
|
};
|
|
375
429
|
}
|
|
@@ -203,4 +203,65 @@ describe("isolated package table gateway", () => {
|
|
|
203
203
|
count: 4,
|
|
204
204
|
});
|
|
205
205
|
});
|
|
206
|
+
test("memory gateway delivers copied dataChanged events for save/call and detaches", async () => {
|
|
207
|
+
const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
|
|
208
|
+
const declaration = makeDeclaration();
|
|
209
|
+
const other = makeDeclaration();
|
|
210
|
+
const recordId = (0, utils_1.newid)();
|
|
211
|
+
await gateway.registerBatch([declaration, other]);
|
|
212
|
+
const received = [];
|
|
213
|
+
const unsubscribe = await gateway.subscribe(declaration, "dataChanged", (event) => {
|
|
214
|
+
received.push(event);
|
|
215
|
+
});
|
|
216
|
+
await gateway.save(declaration, { stateId: recordId, count: 1 });
|
|
217
|
+
await gateway.call(declaration, "update", [{ stateId: recordId, count: 2 }]);
|
|
218
|
+
await gateway.save(other, { stateId: (0, utils_1.newid)(), count: 9 });
|
|
219
|
+
await gateway.call(declaration, "delete", [recordId]);
|
|
220
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
221
|
+
expect(received).toEqual([
|
|
222
|
+
{ dataObject: { stateId: recordId, count: 1 }, op: "save", source: "local" },
|
|
223
|
+
{ dataObject: { stateId: recordId, count: 2 }, op: "update", source: "local" },
|
|
224
|
+
{ dataObject: { stateId: recordId, count: 2 }, op: "delete", source: "local" },
|
|
225
|
+
]);
|
|
226
|
+
const first = received[0];
|
|
227
|
+
first.dataObject.count = 99;
|
|
228
|
+
await expect(gateway.get(declaration, recordId)).resolves.toBeUndefined();
|
|
229
|
+
unsubscribe();
|
|
230
|
+
await gateway.save(declaration, { stateId: recordId, count: 3 });
|
|
231
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
232
|
+
expect(received).toHaveLength(3);
|
|
233
|
+
await expect(gateway.subscribe(declaration, "somethingElse", () => undefined)).rejects.toThrow(/Unsupported isolated table event/);
|
|
234
|
+
await expect(gateway.subscribe(makeDeclaration(), "dataChanged", () => undefined)).rejects.toThrow(/Undeclared isolated table/);
|
|
235
|
+
});
|
|
236
|
+
test("data-context gateway subscribes to the host table dataChanged event", async () => {
|
|
237
|
+
const declaration = makeDeclaration();
|
|
238
|
+
const handlers = new Set();
|
|
239
|
+
const table = {
|
|
240
|
+
get: jest.fn(),
|
|
241
|
+
save: jest.fn(),
|
|
242
|
+
dataChanged: {
|
|
243
|
+
subscribe: jest.fn((handler) => {
|
|
244
|
+
handlers.add(handler);
|
|
245
|
+
return { unsubscribe: () => handlers.delete(handler) };
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const gateway = (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)({
|
|
250
|
+
tableContainer: { registerTableDefinition: jest.fn(), getTable: jest.fn(() => table) },
|
|
251
|
+
});
|
|
252
|
+
await gateway.register(declaration);
|
|
253
|
+
const received = [];
|
|
254
|
+
const unsubscribe = await gateway.subscribe(declaration, "dataChanged", (event) => {
|
|
255
|
+
received.push(event);
|
|
256
|
+
});
|
|
257
|
+
expect(table.dataChanged.subscribe).toHaveBeenCalledTimes(1);
|
|
258
|
+
const row = { stateId: (0, utils_1.newid)(), count: 1 };
|
|
259
|
+
for (const handler of handlers) {
|
|
260
|
+
handler({ dataObject: row, op: "save", source: "local" });
|
|
261
|
+
}
|
|
262
|
+
expect(received).toEqual([{ dataObject: row, op: "save", source: "local" }]);
|
|
263
|
+
expect(received[0].dataObject).not.toBe(row);
|
|
264
|
+
unsubscribe();
|
|
265
|
+
expect(handlers.size).toBe(0);
|
|
266
|
+
});
|
|
206
267
|
});
|
|
@@ -2,7 +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 type { IConsumedContract } from "../contracts/types";
|
|
5
|
+
import type { IConsumedContract, IRemoteContract, IRemoteContractConsumer } from "../contracts/types";
|
|
6
6
|
import { type IPackage } from "../data/packages";
|
|
7
7
|
import type { IPeersPackage } from "../types/peers-package";
|
|
8
8
|
import { type IIsolatedPackageRuntime } from "./isolated-runtime";
|
|
@@ -96,6 +96,20 @@ export declare class PackageLoader {
|
|
|
96
96
|
* @returns A live consumer proxy, or `undefined` when no provider matches.
|
|
97
97
|
*/
|
|
98
98
|
resolveConsumedContract(dependency: IConsumedContract): Promise<IContractConsumer | undefined>;
|
|
99
|
+
/**
|
|
100
|
+
* Bind a legacy package's `remote()` declaration to a per-device consumer factory.
|
|
101
|
+
*
|
|
102
|
+
* No local provider is selected: every device consumer stamps `targetDeviceId` and
|
|
103
|
+
* this loader's own router forwards the call through
|
|
104
|
+
* {@link resolveContractEndpoint}, which hands remote targets to the mesh transport
|
|
105
|
+
* and resolves this device's id locally. The contract definition must already be
|
|
106
|
+
* registered in this context (from the package's own `provides` or another
|
|
107
|
+
* installed package) so the proxy knows the tool names.
|
|
108
|
+
*
|
|
109
|
+
* @param declaration - Validated package remote declaration.
|
|
110
|
+
* @returns A per-device consumer factory, or `undefined` when the contract is unknown.
|
|
111
|
+
*/
|
|
112
|
+
resolveRemoteContract(declaration: IRemoteContract): Promise<IRemoteContractConsumer<IContractConsumer> | undefined>;
|
|
99
113
|
/**
|
|
100
114
|
* Resolve a sandbox-backed provider endpoint when this context owns an
|
|
101
115
|
* isolated provider for the requested contract.
|
|
@@ -7,6 +7,7 @@ const contract_proxy_1 = require("../contracts/contract-proxy");
|
|
|
7
7
|
const contract_resolvers_1 = require("../contracts/contract-resolvers");
|
|
8
8
|
const contract_transport_1 = require("../contracts/contract-transport");
|
|
9
9
|
const registry_1 = require("../contracts/registry");
|
|
10
|
+
const remote_contract_transport_1 = require("../contracts/remote-contract-transport");
|
|
10
11
|
const system_contracts_1 = require("../contracts/system-contracts");
|
|
11
12
|
const files_1 = require("../data/files");
|
|
12
13
|
const package_version_resolver_1 = require("../data/package-version-resolver");
|
|
@@ -132,6 +133,16 @@ class PackageLoader {
|
|
|
132
133
|
* @throws If no provider or matching endpoint implementation is available.
|
|
133
134
|
*/
|
|
134
135
|
async resolveContractEndpoint(request, opts = {}) {
|
|
136
|
+
// A route that names another device is forwarded over the mesh instead of being
|
|
137
|
+
// resolved against a local provider. Only routers serving local principals set
|
|
138
|
+
// targetDeviceId; remote-caller routers reject it before reaching this point.
|
|
139
|
+
const remoteEndpoint = (0, remote_contract_transport_1.resolveRemoteTargetEndpoint)(request, {
|
|
140
|
+
localDeviceId: this.dataContext.userContext?.deviceId(),
|
|
141
|
+
userContext: this.dataContext.userContext,
|
|
142
|
+
});
|
|
143
|
+
if (remoteEndpoint) {
|
|
144
|
+
return remoteEndpoint;
|
|
145
|
+
}
|
|
135
146
|
const providerPackageId = opts.providerPackageId ??
|
|
136
147
|
request.providerPackageId ??
|
|
137
148
|
this.contractRegistry.getProviderPackageId(request.contractId, request.version);
|
|
@@ -244,6 +255,43 @@ class PackageLoader {
|
|
|
244
255
|
};
|
|
245
256
|
return consumer;
|
|
246
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Bind a legacy package's `remote()` declaration to a per-device consumer factory.
|
|
260
|
+
*
|
|
261
|
+
* No local provider is selected: every device consumer stamps `targetDeviceId` and
|
|
262
|
+
* this loader's own router forwards the call through
|
|
263
|
+
* {@link resolveContractEndpoint}, which hands remote targets to the mesh transport
|
|
264
|
+
* and resolves this device's id locally. The contract definition must already be
|
|
265
|
+
* registered in this context (from the package's own `provides` or another
|
|
266
|
+
* installed package) so the proxy knows the tool names.
|
|
267
|
+
*
|
|
268
|
+
* @param declaration - Validated package remote declaration.
|
|
269
|
+
* @returns A per-device consumer factory, or `undefined` when the contract is unknown.
|
|
270
|
+
*/
|
|
271
|
+
async resolveRemoteContract(declaration) {
|
|
272
|
+
const definition = this.contractRegistry.getDefinition(declaration.contractId, declaration.version);
|
|
273
|
+
if (!definition) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const [consumerTransport, hostTransport] = (0, contract_transport_1.inProcessTransportPair)();
|
|
277
|
+
const router = (0, contract_provider_router_1.createContractProviderRouter)(hostTransport, {
|
|
278
|
+
authorizeCall: () => ({
|
|
279
|
+
dataContextId: this.dataContext.dataContextId,
|
|
280
|
+
context: { dataContext: this.dataContext },
|
|
281
|
+
}),
|
|
282
|
+
resolveProviderPackageId: (request) => this.contractRegistry.getProviderPackageId(request.contractId, request.version),
|
|
283
|
+
resolveEndpoint: (request) => this.resolveContractEndpoint(request),
|
|
284
|
+
});
|
|
285
|
+
const remote = (0, contract_proxy_1.createRemoteContractConsumer)(definition, consumerTransport, {
|
|
286
|
+
dataContextId: this.dataContext.dataContextId,
|
|
287
|
+
});
|
|
288
|
+
const dispose = remote.dispose.bind(remote);
|
|
289
|
+
remote.dispose = () => {
|
|
290
|
+
dispose();
|
|
291
|
+
router.dispose();
|
|
292
|
+
};
|
|
293
|
+
return remote;
|
|
294
|
+
}
|
|
247
295
|
/**
|
|
248
296
|
* Resolve a sandbox-backed provider endpoint when this context owns an
|
|
249
297
|
* isolated provider for the requested contract.
|
|
@@ -2,12 +2,16 @@ import type { DataContext } from "../context/data-context";
|
|
|
2
2
|
import type { ContractRegistry } from "../contracts/registry";
|
|
3
3
|
import type { IPackageDefinitionResult } from "../contracts/types";
|
|
4
4
|
/**
|
|
5
|
-
* Settle each deferred `consumes` handle on a package definition
|
|
6
|
-
* providers have been registered.
|
|
5
|
+
* Settle each deferred `consumes` and `remote` handle on a package definition
|
|
6
|
+
* after its providers have been registered.
|
|
7
7
|
*
|
|
8
8
|
* A consume succeeds only when {@link ContractRegistry.selectProvider} finds a
|
|
9
9
|
* matching provider and the owning PackageLoader can bind an endpoint. The
|
|
10
10
|
* endpoint may be resolver-backed or isolated. Missing optional deps settle to
|
|
11
11
|
* `undefined`; missing required deps reject the handle.
|
|
12
|
+
*
|
|
13
|
+
* A remote declaration needs no local provider: it settles to a per-device
|
|
14
|
+
* consumer factory as long as the contract definition is registered locally and
|
|
15
|
+
* a PackageLoader owns the context; otherwise the handle rejects.
|
|
12
16
|
*/
|
|
13
17
|
export declare function resolveConsumedContracts(dataContext: DataContext, registry: ContractRegistry, packageDefinition: IPackageDefinitionResult): Promise<void>;
|
|
@@ -5,15 +5,20 @@ const builder_1 = require("../contracts/builder");
|
|
|
5
5
|
const contract_resolvers_1 = require("../contracts/contract-resolvers");
|
|
6
6
|
const create_local_consumer_1 = require("../contracts/create-local-consumer");
|
|
7
7
|
/**
|
|
8
|
-
* Settle each deferred `consumes` handle on a package definition
|
|
9
|
-
* providers have been registered.
|
|
8
|
+
* Settle each deferred `consumes` and `remote` handle on a package definition
|
|
9
|
+
* after its providers have been registered.
|
|
10
10
|
*
|
|
11
11
|
* A consume succeeds only when {@link ContractRegistry.selectProvider} finds a
|
|
12
12
|
* matching provider and the owning PackageLoader can bind an endpoint. The
|
|
13
13
|
* endpoint may be resolver-backed or isolated. Missing optional deps settle to
|
|
14
14
|
* `undefined`; missing required deps reject the handle.
|
|
15
|
+
*
|
|
16
|
+
* A remote declaration needs no local provider: it settles to a per-device
|
|
17
|
+
* consumer factory as long as the contract definition is registered locally and
|
|
18
|
+
* a PackageLoader owns the context; otherwise the handle rejects.
|
|
15
19
|
*/
|
|
16
20
|
async function resolveConsumedContracts(dataContext, registry, packageDefinition) {
|
|
21
|
+
await resolveRemoteContracts(dataContext, packageDefinition);
|
|
17
22
|
for (const dep of packageDefinition.consumes) {
|
|
18
23
|
const handle = dep.handle;
|
|
19
24
|
if (!handle) {
|
|
@@ -44,6 +49,29 @@ async function resolveConsumedContracts(dataContext, registry, packageDefinition
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
52
|
+
async function resolveRemoteContracts(dataContext, packageDefinition) {
|
|
53
|
+
for (const declaration of packageDefinition.remotes ?? []) {
|
|
54
|
+
const handle = declaration.handle;
|
|
55
|
+
if (!handle) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const key = `${declaration.contractId}@${declaration.version}`;
|
|
59
|
+
try {
|
|
60
|
+
const loader = dataContext.packageLoader;
|
|
61
|
+
if (!loader?.resolveRemoteContract) {
|
|
62
|
+
throw new Error(`Remote contract ${key} requires a package loader on the data context`);
|
|
63
|
+
}
|
|
64
|
+
const remote = await loader.resolveRemoteContract(declaration);
|
|
65
|
+
if (!remote) {
|
|
66
|
+
throw new Error(`Remote contract ${key} could not be resolved: its definition is not registered on this device`);
|
|
67
|
+
}
|
|
68
|
+
(0, builder_1.settleContractHandle)(handle, remote);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
(0, builder_1.rejectContractHandle)(handle, err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
47
75
|
async function resolveLegacyLocalConsumer(dataContext, selected, contractId, version) {
|
|
48
76
|
const resolver = (0, contract_resolvers_1.getContractResolver)(contractId, version, selected.providerPackageId);
|
|
49
77
|
if (!resolver) {
|
|
@@ -120,4 +120,47 @@ describe("resolveConsumedContracts", () => {
|
|
|
120
120
|
}
|
|
121
121
|
await expect(consumeHandle.consumer).resolves.toBeUndefined();
|
|
122
122
|
});
|
|
123
|
+
describe("remote declarations", () => {
|
|
124
|
+
it("settles a remote handle through the package loader without a local provider", async () => {
|
|
125
|
+
const remote = { contractId, version: 1, device: jest.fn(), dispose: jest.fn() };
|
|
126
|
+
const resolveRemoteContract = jest.fn().mockResolvedValue(remote);
|
|
127
|
+
const dataContext = {
|
|
128
|
+
dataContextId: "test-ctx",
|
|
129
|
+
packageLoader: { resolveRemoteContract },
|
|
130
|
+
};
|
|
131
|
+
let remoteHandle;
|
|
132
|
+
const packageDefinition = (0, builder_1.definePackage)((pkg) => {
|
|
133
|
+
pkg.packageId = (0, utils_1.newid)();
|
|
134
|
+
pkg.version = "1.0.0";
|
|
135
|
+
remoteHandle = pkg.remote(contractId, 1);
|
|
136
|
+
});
|
|
137
|
+
await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(dataContext, new registry_1.ContractRegistry(), packageDefinition);
|
|
138
|
+
expect(resolveRemoteContract).toHaveBeenCalledWith(expect.objectContaining({ contractId, version: 1 }));
|
|
139
|
+
await expect(remoteHandle?.consumer).resolves.toBe(remote);
|
|
140
|
+
});
|
|
141
|
+
it("rejects a remote handle when the contract definition is unknown locally", async () => {
|
|
142
|
+
const dataContext = {
|
|
143
|
+
dataContextId: "test-ctx",
|
|
144
|
+
packageLoader: { resolveRemoteContract: jest.fn().mockResolvedValue(undefined) },
|
|
145
|
+
};
|
|
146
|
+
let remoteHandle;
|
|
147
|
+
const packageDefinition = (0, builder_1.definePackage)((pkg) => {
|
|
148
|
+
pkg.packageId = (0, utils_1.newid)();
|
|
149
|
+
pkg.version = "1.0.0";
|
|
150
|
+
remoteHandle = pkg.remote(contractId, 1);
|
|
151
|
+
});
|
|
152
|
+
await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(dataContext, new registry_1.ContractRegistry(), packageDefinition);
|
|
153
|
+
await expect(remoteHandle?.consumer).rejects.toThrow(/not registered on this device/);
|
|
154
|
+
});
|
|
155
|
+
it("rejects a remote handle when no package loader owns the context", async () => {
|
|
156
|
+
let remoteHandle;
|
|
157
|
+
const packageDefinition = (0, builder_1.definePackage)((pkg) => {
|
|
158
|
+
pkg.packageId = (0, utils_1.newid)();
|
|
159
|
+
pkg.version = "1.0.0";
|
|
160
|
+
remoteHandle = pkg.remote(contractId, 1);
|
|
161
|
+
});
|
|
162
|
+
await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(mockDataContext(), new registry_1.ContractRegistry(), packageDefinition);
|
|
163
|
+
await expect(remoteHandle?.consumer).rejects.toThrow(/requires a package loader/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
123
166
|
});
|