@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.
Files changed (37) hide show
  1. package/dist/contracts/__tests__/builder.test.js +25 -0
  2. package/dist/contracts/__tests__/contract-provider-router.test.js +45 -0
  3. package/dist/contracts/__tests__/contract-proxy.test.js +67 -0
  4. package/dist/contracts/__tests__/contract-resolvers.test.js +133 -0
  5. package/dist/contracts/__tests__/remote-contract-transport.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/remote-contract-transport.test.js +89 -0
  7. package/dist/contracts/builder.d.ts +47 -1
  8. package/dist/contracts/builder.js +80 -22
  9. package/dist/contracts/contract-provider-router.d.ts +6 -0
  10. package/dist/contracts/contract-provider-router.js +25 -3
  11. package/dist/contracts/contract-proxy.d.ts +41 -1
  12. package/dist/contracts/contract-proxy.js +103 -0
  13. package/dist/contracts/contract-resolvers.d.ts +47 -13
  14. package/dist/contracts/contract-resolvers.js +125 -35
  15. package/dist/contracts/extract.d.ts +4 -1
  16. package/dist/contracts/extract.js +3 -1
  17. package/dist/contracts/index.d.ts +4 -3
  18. package/dist/contracts/index.js +10 -2
  19. package/dist/contracts/remote-contract-transport.d.ts +69 -0
  20. package/dist/contracts/remote-contract-transport.js +95 -0
  21. package/dist/contracts/types.d.ts +57 -0
  22. package/dist/data/tools.d.ts +4 -0
  23. package/dist/data/tools.js +7 -0
  24. package/dist/package-loader/isolated-artifact.d.ts +44 -5
  25. package/dist/package-loader/isolated-artifact.js +82 -4
  26. package/dist/package-loader/isolated-artifact.test.js +51 -3
  27. package/dist/package-loader/isolated-runtime.d.ts +17 -2
  28. package/dist/package-loader/isolated-runtime.js +3 -1
  29. package/dist/package-loader/isolated-table-gateway.d.ts +1 -1
  30. package/dist/package-loader/isolated-table-gateway.js +54 -0
  31. package/dist/package-loader/isolated-table-gateway.test.js +61 -0
  32. package/dist/package-loader/package-loader.d.ts +15 -1
  33. package/dist/package-loader/package-loader.js +48 -0
  34. package/dist/package-loader/resolve-consumed-contracts.d.ts +6 -2
  35. package/dist/package-loader/resolve-consumed-contracts.js +30 -2
  36. package/dist/package-loader/resolve-consumed-contracts.test.js +43 -0
  37. 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("rejects unsupported contract features instead of dropping them", () => {
238
+ test("maps declared events to the public contract definition", () => {
239
239
  const { artifact } = makeArtifact();
240
240
  artifact.manifest.provides[0].events = [
241
- { name: "changed", payloadFields: [] },
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
- expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/events is not supported/);
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
- /** Registry-selected provider package. */
172
- providerPackageId: string;
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 { IIsolatedPackageTableGateway } from "./isolated-runtime";
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 after its
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 after its
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.23.10",
3
+ "version": "0.23.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"