@peers-app/peers-sdk 0.23.4 → 0.23.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/context/data-context.d.ts +36 -5
  2. package/dist/context/data-context.js +44 -9
  3. package/dist/context/user-context.d.ts +44 -0
  4. package/dist/context/user-context.js +91 -12
  5. package/dist/contracts/__tests__/contract-provider-router.test.js +26 -0
  6. package/dist/contracts/__tests__/contract-resolvers.test.js +44 -0
  7. package/dist/contracts/__tests__/registry.test.js +104 -0
  8. package/dist/contracts/contract-provider-router.d.ts +21 -0
  9. package/dist/contracts/contract-provider-router.js +31 -5
  10. package/dist/contracts/contract-proxy.d.ts +3 -0
  11. package/dist/contracts/contract-proxy.js +3 -0
  12. package/dist/contracts/contract-resolvers.d.ts +21 -3
  13. package/dist/contracts/contract-resolvers.js +105 -32
  14. package/dist/contracts/index.d.ts +3 -3
  15. package/dist/contracts/registry.d.ts +59 -0
  16. package/dist/contracts/registry.js +155 -25
  17. package/dist/contracts/system-contracts.js +3 -1
  18. package/dist/data/index.d.ts +1 -0
  19. package/dist/data/index.js +1 -0
  20. package/dist/data/orm/types.d.ts +19 -0
  21. package/dist/data/orm/types.js +11 -0
  22. package/dist/data/package-table-identity.d.ts +46 -0
  23. package/dist/data/package-table-identity.js +71 -0
  24. package/dist/data/package-table-identity.test.d.ts +1 -0
  25. package/dist/data/package-table-identity.test.js +46 -0
  26. package/dist/data/package-version-resolver.d.ts +13 -0
  27. package/dist/data/package-version-resolver.js +142 -6
  28. package/dist/data/package-version-resolver.test.js +42 -0
  29. package/dist/data/persistent-var-identity.d.ts +37 -0
  30. package/dist/data/persistent-var-identity.js +69 -0
  31. package/dist/data/persistent-vars.d.ts +3 -5
  32. package/dist/data/persistent-vars.js +25 -13
  33. package/dist/data/persistent-vars.package-scope.test.d.ts +1 -0
  34. package/dist/data/persistent-vars.package-scope.test.js +27 -0
  35. package/dist/data/tools.d.ts +3 -3
  36. package/dist/package-loader/contract-package-loader.js +20 -8
  37. package/dist/package-loader/contract-package-loader.test.js +16 -0
  38. package/dist/package-loader/index.d.ts +4 -0
  39. package/dist/package-loader/index.js +4 -0
  40. package/dist/package-loader/isolated-artifact.d.ts +233 -0
  41. package/dist/package-loader/isolated-artifact.js +643 -0
  42. package/dist/package-loader/isolated-artifact.test.d.ts +1 -0
  43. package/dist/package-loader/isolated-artifact.test.js +270 -0
  44. package/dist/package-loader/isolated-persistent-var-gateway.d.ts +20 -0
  45. package/dist/package-loader/isolated-persistent-var-gateway.js +106 -0
  46. package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +1 -0
  47. package/dist/package-loader/isolated-persistent-var-gateway.test.js +56 -0
  48. package/dist/package-loader/isolated-runtime.d.ts +229 -0
  49. package/dist/package-loader/isolated-runtime.js +37 -0
  50. package/dist/package-loader/isolated-table-gateway.d.ts +16 -0
  51. package/dist/package-loader/isolated-table-gateway.js +375 -0
  52. package/dist/package-loader/isolated-table-gateway.test.d.ts +1 -0
  53. package/dist/package-loader/isolated-table-gateway.test.js +206 -0
  54. package/dist/package-loader/isolated-value.d.ts +14 -0
  55. package/dist/package-loader/isolated-value.js +63 -0
  56. package/dist/package-loader/package-loader.d.ts +123 -4
  57. package/dist/package-loader/package-loader.js +417 -46
  58. package/dist/package-loader/package-loader.test.js +478 -12
  59. package/dist/package-loader/resolve-consumed-contracts.d.ts +3 -3
  60. package/dist/package-loader/resolve-consumed-contracts.js +18 -10
  61. package/dist/utils.d.ts +21 -2
  62. package/dist/utils.js +46 -7
  63. package/package.json +1 -1
@@ -13,6 +13,7 @@ exports.updatePackagePrefs = updatePackagePrefs;
13
13
  exports.getEffectivePackagePrefs = getEffectivePackagePrefs;
14
14
  exports.resolveDevicePackageVersion = resolveDevicePackageVersion;
15
15
  exports.hasDeviceFollowOverride = hasDeviceFollowOverride;
16
+ exports.readStoredPackageLocalPath = readStoredPackageLocalPath;
16
17
  const user_context_singleton_1 = require("../context/user-context-singleton");
17
18
  const assistants_1 = require("./assistants");
18
19
  const group_member_roles_1 = require("./group-member-roles");
@@ -60,7 +61,13 @@ async function updatePackagePrefs(packageId, prefs, dataContext) {
60
61
  const { activePackageVersionId: _, ...rest } = prefs;
61
62
  effectivePrefs = rest;
62
63
  }
63
- pvar({ ...existing, ...effectivePrefs });
64
+ const next = { ...existing, ...effectivePrefs };
65
+ for (const key of Object.keys(next)) {
66
+ if (next[key] === undefined) {
67
+ delete next[key];
68
+ }
69
+ }
70
+ pvar(next);
64
71
  }
65
72
  /**
66
73
  * Pure function: merge raw device prefs with the group-level `IPackage`
@@ -111,6 +118,37 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
111
118
  if (!effectiveActivePvId) {
112
119
  return { loaded: false, upgraded: false };
113
120
  }
121
+ const localPath = opts?.localPath ??
122
+ (await readStoredPackageLocalPath(pkg.packageId, dataContext).catch(() => undefined));
123
+ const compatibilityTargetId = rawPrefs.compatibilityFallbackForPackageVersionId;
124
+ if (compatibilityTargetId &&
125
+ compatibilityTargetId !== effectiveActivePvId &&
126
+ dataContext.packageLoader.supportsIsolatedPackages()) {
127
+ const compatibilityTarget = await (0, package_versions_1.PackageVersions)(dataContext)
128
+ .get(compatibilityTargetId)
129
+ .catch(() => undefined);
130
+ if (compatibilityTarget) {
131
+ try {
132
+ const restored = await dataContext.packageLoader.loadPackage(pkg, {
133
+ force: true,
134
+ localPath,
135
+ packageVersionId: compatibilityTargetId,
136
+ });
137
+ if (restored) {
138
+ await updatePackagePrefs(pkg.packageId, {
139
+ activePackageVersionId: compatibilityTargetId === pkg.activePackageVersionId
140
+ ? undefined
141
+ : compatibilityTargetId,
142
+ compatibilityFallbackForPackageVersionId: undefined,
143
+ }, dataContext);
144
+ return { loaded: true, pv: compatibilityTarget, upgraded: true };
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.warn(`[PackageVersionResolver] Compatibility target ${compatibilityTarget.version} remains unavailable for ${pkg.name}:`, error instanceof Error ? error.message : String(error));
149
+ }
150
+ }
151
+ }
114
152
  // If the device (or the group-level default) has pinned this package, just load
115
153
  // the current version — never auto-upgrade.
116
154
  if (isPinned) {
@@ -121,9 +159,15 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
121
159
  return { loaded: false, pv: undefined, upgraded: false };
122
160
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
123
161
  force: opts?.force,
124
- localPath: opts?.localPath,
162
+ localPath,
125
163
  packageVersionId: effectiveActivePvId,
126
164
  });
165
+ if (!instance) {
166
+ const fallback = await loadOlderCompatiblePackageVersion(pkg, dataContext, pv, followRange, followTags, localPath, rawPrefs.pinned === true);
167
+ if (fallback) {
168
+ return { loaded: true, pv: fallback.pv, upgraded: false };
169
+ }
170
+ }
127
171
  return { loaded: !!instance, pv, upgraded: false };
128
172
  }
129
173
  // Load the currently active PV for comparison
@@ -139,9 +183,15 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
139
183
  if (activePv?.versionTag === "dev") {
140
184
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
141
185
  force: opts?.force,
142
- localPath: opts?.localPath,
186
+ localPath,
143
187
  packageVersionId: effectiveActivePvId,
144
188
  });
189
+ if (!instance) {
190
+ const fallback = await loadOlderCompatiblePackageVersion(pkg, dataContext, activePv, followRange, followTags, localPath, rawPrefs.pinned === true);
191
+ if (fallback) {
192
+ return { loaded: true, pv: fallback.pv, upgraded: false };
193
+ }
194
+ }
145
195
  return { loaded: !!instance, pv: activePv, upgraded: false };
146
196
  }
147
197
  // Evaluate all available PVs to find the best one
@@ -167,9 +217,15 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
167
217
  if (activePv) {
168
218
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
169
219
  force: opts?.force,
170
- localPath: opts?.localPath,
220
+ localPath,
171
221
  packageVersionId: effectiveActivePvId,
172
222
  });
223
+ if (!instance) {
224
+ const fallback = await loadOlderCompatiblePackageVersion(pkg, dataContext, activePv, followRange, followTags, localPath, rawPrefs.pinned === true);
225
+ if (fallback) {
226
+ return { loaded: true, pv: fallback.pv, upgraded: false };
227
+ }
228
+ }
173
229
  return { loaded: !!instance, pv: activePv, upgraded: false };
174
230
  }
175
231
  return { loaded: false, upgraded: false };
@@ -178,7 +234,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
178
234
  // auto-download missing chunks from peers)
179
235
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
180
236
  force: true,
181
- localPath: opts?.localPath,
237
+ localPath,
182
238
  packageVersionId: bestPv.packageVersionId,
183
239
  });
184
240
  if (!instance) {
@@ -186,9 +242,15 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
186
242
  if (activePv) {
187
243
  const fallbackInstance = await dataContext.packageLoader.loadPackage(pkg, {
188
244
  force: opts?.force,
189
- localPath: opts?.localPath,
245
+ localPath,
190
246
  packageVersionId: effectiveActivePvId,
191
247
  });
248
+ if (!fallbackInstance) {
249
+ const olderFallback = await loadOlderCompatiblePackageVersion(pkg, dataContext, activePv, followRange, followTags, localPath, rawPrefs.pinned === true);
250
+ if (olderFallback) {
251
+ return { loaded: true, pv: olderFallback.pv, upgraded: false };
252
+ }
253
+ }
192
254
  return { loaded: !!fallbackInstance, pv: activePv, upgraded: false };
193
255
  }
194
256
  return { loaded: false, upgraded: false };
@@ -227,9 +289,60 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
227
289
  }
228
290
  await updatePackagePrefs(pkg.packageId, {
229
291
  activePackageVersionId: advancedGroupActiveVersion ? undefined : bestPv.packageVersionId,
292
+ compatibilityFallbackForPackageVersionId: undefined,
230
293
  }, dataContext);
231
294
  return { loaded: true, pv: bestPv, upgraded: true };
232
295
  }
296
+ async function loadOlderCompatiblePackageVersion(pkg, dataContext, unavailableVersion, followRange, followTags, localPath, devicePinned) {
297
+ if (dataContext.packageLoader.supportsIsolatedPackages()) {
298
+ return undefined;
299
+ }
300
+ const versions = await (0, package_versions_1.PackageVersions)(dataContext).list({ packageId: pkg.packageId });
301
+ const candidates = versions
302
+ .filter((candidate) => {
303
+ if (candidate.packageVersionId === unavailableVersion.packageVersionId) {
304
+ return false;
305
+ }
306
+ if ((0, package_versions_1.isNewerVersion)(unavailableVersion.version, candidate.version)) {
307
+ return false;
308
+ }
309
+ if (!(0, package_versions_1.doesTagMatch)(unavailableVersion.versionTag, candidate.versionTag, followTags, undefined)) {
310
+ return false;
311
+ }
312
+ return (0, package_versions_1.isVersionInRange)(unavailableVersion.version, candidate.version, followRange);
313
+ })
314
+ .sort((left, right) => {
315
+ if ((0, package_versions_1.isNewerVersion)(right.version, left.version)) {
316
+ return -1;
317
+ }
318
+ if ((0, package_versions_1.isNewerVersion)(left.version, right.version)) {
319
+ return 1;
320
+ }
321
+ return 0;
322
+ });
323
+ for (const candidate of candidates) {
324
+ try {
325
+ const instance = await dataContext.packageLoader.loadPackage(pkg, {
326
+ force: true,
327
+ localPath,
328
+ packageVersionId: candidate.packageVersionId,
329
+ });
330
+ if (!instance) {
331
+ continue;
332
+ }
333
+ await updatePackagePrefs(pkg.packageId, {
334
+ activePackageVersionId: candidate.packageVersionId,
335
+ compatibilityFallbackForPackageVersionId: unavailableVersion.packageVersionId,
336
+ ...(devicePinned ? { pinned: true } : {}),
337
+ }, dataContext);
338
+ return { instance, pv: candidate };
339
+ }
340
+ catch (error) {
341
+ console.warn(`[PackageVersionResolver] Compatibility fallback ${candidate.version} failed for ${pkg.name}:`, error instanceof Error ? error.message : String(error));
342
+ }
343
+ }
344
+ return undefined;
345
+ }
233
346
  /**
234
347
  * Save a loaded package's assistants and workflows into the data context.
235
348
  * Contract-installed packages are skipped because their content is already
@@ -255,6 +368,29 @@ function hasDeviceFollowOverride(prefs) {
255
368
  return false;
256
369
  return prefs.followRange != null || prefs.followTags != null || prefs.pinned != null;
257
370
  }
371
+ /**
372
+ * Read the stored local checkout path for a package, if one is set.
373
+ *
374
+ * Used so isolated local-dev packages can load `dist/package.bundle.js` from
375
+ * disk even when startup resolution does not receive an explicit `localPath`.
376
+ *
377
+ * @param packageId - Package whose `packageLocalPath_*` device var to read.
378
+ * @param dataContext - Data context that owns the device var.
379
+ * @returns Trimmed path, or `undefined` when unset or empty.
380
+ */
381
+ async function readStoredPackageLocalPath(packageId, dataContext) {
382
+ const pvar = (0, persistent_vars_1.groupDeviceVar)(`packageLocalPath_${packageId}`, {
383
+ defaultValue: "",
384
+ dataContext,
385
+ });
386
+ await pvar.loadingPromise;
387
+ const value = pvar();
388
+ if (typeof value !== "string") {
389
+ return undefined;
390
+ }
391
+ const trimmed = value.trim();
392
+ return trimmed || undefined;
393
+ }
258
394
  /** Convert `IPackage.versionFollowRange` to the resolver's range type. */
259
395
  function rangeFromPkg(pkg) {
260
396
  const r = pkg.versionFollowRange;
@@ -180,6 +180,48 @@ describe("resolveDevicePackageVersion — regression coverage", () => {
180
180
  expect(result.upgraded).toBe(true);
181
181
  expect(loadedPvIds).toContain(betaPv.packageVersionId);
182
182
  });
183
+ it("falls back locally when isolation is unavailable and automatically recovers later", async () => {
184
+ const dc = freshGroupDc();
185
+ const packageId = (0, utils_1.newid)();
186
+ const legacyV1 = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
187
+ const legacyV1_5 = makePV(packageId, { version: "1.5.0", versionTag: "stable" });
188
+ const isolatedV2 = makePV(packageId, { version: "2.0.0", versionTag: "stable" });
189
+ await (0, package_versions_1.PackageVersions)(dc).save(legacyV1);
190
+ await (0, package_versions_1.PackageVersions)(dc).save(legacyV1_5);
191
+ await (0, package_versions_1.PackageVersions)(dc).save(isolatedV2);
192
+ const pkg = makePkg({
193
+ packageId,
194
+ activePackageVersionId: isolatedV2.packageVersionId,
195
+ versionFollowRange: "pinned",
196
+ followVersionTags: "stable",
197
+ });
198
+ await (0, packages_1.Packages)(dc).save(pkg);
199
+ let isolationAvailable = false;
200
+ jest
201
+ .spyOn(dc.packageLoader, "supportsIsolatedPackages")
202
+ .mockImplementation(() => isolationAvailable);
203
+ jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
204
+ if (opts?.packageVersionId === isolatedV2.packageVersionId && !isolationAvailable) {
205
+ return undefined;
206
+ }
207
+ return { packageId };
208
+ });
209
+ const fallback = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
210
+ expect(fallback.loaded).toBe(true);
211
+ expect(fallback.upgraded).toBe(false);
212
+ expect(fallback.pv?.packageVersionId).toBe(legacyV1_5.packageVersionId);
213
+ const prefs = (0, package_version_resolver_1.packagePrefsVar)(packageId, dc);
214
+ await prefs.loadingPromise;
215
+ expect(prefs()?.activePackageVersionId).toBe(legacyV1_5.packageVersionId);
216
+ expect(prefs()?.compatibilityFallbackForPackageVersionId).toBe(isolatedV2.packageVersionId);
217
+ isolationAvailable = true;
218
+ const recovered = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
219
+ expect(recovered.loaded).toBe(true);
220
+ expect(recovered.upgraded).toBe(true);
221
+ expect(recovered.pv?.packageVersionId).toBe(isolatedV2.packageVersionId);
222
+ expect(prefs()?.activePackageVersionId).toBeUndefined();
223
+ expect(prefs()?.compatibilityFallbackForPackageVersionId).toBeUndefined();
224
+ });
183
225
  });
184
226
  describe("resolveDevicePackageVersion — expected behavior (control)", () => {
185
227
  it("advances the group active version when an admin auto-upgrades without device overrides", async () => {
@@ -0,0 +1,37 @@
1
+ /** Storage/synchronization scopes supported by Peers persistent variables. */
2
+ export declare const PERSISTENT_VAR_SCOPES: readonly ["device", "user", "group", "groupDevice", "groupUser"];
3
+ /** One of the five Peers persistent-variable storage scopes. */
4
+ export type PersistentVarScope = (typeof PERSISTENT_VAR_SCOPES)[number];
5
+ /**
6
+ * Prefix a package-local pvar name with trusted package identity.
7
+ *
8
+ * This must run on the host before deterministic ID generation. Guest code
9
+ * supplies only `logicalName`; it never chooses `packageId`.
10
+ *
11
+ * @param packageId - Trusted owning package ID.
12
+ * @param logicalName - Package-local variable name.
13
+ * @returns Stable package-prefixed base name.
14
+ */
15
+ export declare function packageScopedPersistentVarName(packageId: string, logicalName: string): string;
16
+ /**
17
+ * Return the physical DB name for a pvar scope.
18
+ *
19
+ * Group-device and group-user pvars append the trusted route context so one
20
+ * logical variable can hold a different value for each group.
21
+ *
22
+ * @param name - Base variable name, already package-prefixed when package-owned.
23
+ * @param scope - Persistent-variable scope.
24
+ * @param dataContextId - Trusted route context required by group-dependent scopes.
25
+ * @returns Name stored in `PersistentVars` and hashed by `deterministicPvarId`.
26
+ */
27
+ export declare function persistentVarStorageName(name: string, scope: PersistentVarScope, dataContextId?: string): string;
28
+ /**
29
+ * Derive the full physical DB name for an isolated package-owned pvar.
30
+ *
31
+ * @param packageId - Trusted owning package ID.
32
+ * @param logicalName - Guest-visible package-local name.
33
+ * @param scope - Persistent-variable scope.
34
+ * @param dataContextId - Trusted route context for group-dependent scopes.
35
+ * @returns Package-prefixed, scope-qualified storage name.
36
+ */
37
+ export declare function packageScopedPersistentVarStorageName(packageId: string, logicalName: string, scope: PersistentVarScope, dataContextId?: string): string;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PERSISTENT_VAR_SCOPES = void 0;
4
+ exports.packageScopedPersistentVarName = packageScopedPersistentVarName;
5
+ exports.persistentVarStorageName = persistentVarStorageName;
6
+ exports.packageScopedPersistentVarStorageName = packageScopedPersistentVarStorageName;
7
+ const utils_1 = require("../utils");
8
+ /** Storage/synchronization scopes supported by Peers persistent variables. */
9
+ exports.PERSISTENT_VAR_SCOPES = [
10
+ "device",
11
+ "user",
12
+ "group",
13
+ "groupDevice",
14
+ "groupUser",
15
+ ];
16
+ /**
17
+ * Prefix a package-local pvar name with trusted package identity.
18
+ *
19
+ * This must run on the host before deterministic ID generation. Guest code
20
+ * supplies only `logicalName`; it never chooses `packageId`.
21
+ *
22
+ * @param packageId - Trusted owning package ID.
23
+ * @param logicalName - Package-local variable name.
24
+ * @returns Stable package-prefixed base name.
25
+ */
26
+ function packageScopedPersistentVarName(packageId, logicalName) {
27
+ if (!(0, utils_1.isid)(packageId)) {
28
+ throw new Error(`Invalid package ID for persistent variable: ${packageId}`);
29
+ }
30
+ if (typeof logicalName !== "string" || logicalName.trim() === "") {
31
+ throw new Error("Persistent variable name must be a non-empty string");
32
+ }
33
+ return `${packageId}_${logicalName}`;
34
+ }
35
+ /**
36
+ * Return the physical DB name for a pvar scope.
37
+ *
38
+ * Group-device and group-user pvars append the trusted route context so one
39
+ * logical variable can hold a different value for each group.
40
+ *
41
+ * @param name - Base variable name, already package-prefixed when package-owned.
42
+ * @param scope - Persistent-variable scope.
43
+ * @param dataContextId - Trusted route context required by group-dependent scopes.
44
+ * @returns Name stored in `PersistentVars` and hashed by `deterministicPvarId`.
45
+ */
46
+ function persistentVarStorageName(name, scope, dataContextId) {
47
+ if (!exports.PERSISTENT_VAR_SCOPES.includes(scope)) {
48
+ throw new Error(`Invalid persistent variable scope: ${String(scope)}`);
49
+ }
50
+ if (scope === "groupDevice" || scope === "groupUser") {
51
+ if (!dataContextId) {
52
+ throw new Error(`Persistent variable scope ${scope} requires a data context`);
53
+ }
54
+ return `${name}_${dataContextId}`;
55
+ }
56
+ return name;
57
+ }
58
+ /**
59
+ * Derive the full physical DB name for an isolated package-owned pvar.
60
+ *
61
+ * @param packageId - Trusted owning package ID.
62
+ * @param logicalName - Guest-visible package-local name.
63
+ * @param scope - Persistent-variable scope.
64
+ * @param dataContextId - Trusted route context for group-dependent scopes.
65
+ * @returns Package-prefixed, scope-qualified storage name.
66
+ */
67
+ function packageScopedPersistentVarStorageName(packageId, logicalName, scope, dataContextId) {
68
+ return persistentVarStorageName(packageScopedPersistentVarName(packageId, logicalName), scope, dataContextId);
69
+ }
@@ -11,6 +11,8 @@ import { type Observable } from "../observable";
11
11
  import type { ISaveOptions } from "./orm";
12
12
  import { Table } from "./orm/table";
13
13
  import { type ITableMetaData } from "./orm/types";
14
+ import { type PersistentVarScope } from "./persistent-var-identity";
15
+ export { PERSISTENT_VAR_SCOPES, type PersistentVarScope, packageScopedPersistentVarName, packageScopedPersistentVarStorageName, persistentVarStorageName, } from "./persistent-var-identity";
14
16
  declare const schema: z.ZodObject<{
15
17
  persistentVarId: z.ZodEffects<z.ZodString, string, string>;
16
18
  name: z.ZodString;
@@ -26,7 +28,6 @@ declare const schema: z.ZodObject<{
26
28
  userCreated: z.ZodOptional<z.ZodBoolean>;
27
29
  isSecret: z.ZodOptional<z.ZodBoolean>;
28
30
  modifiedAt: z.ZodOptional<z.ZodNumber>;
29
- packageId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
30
31
  }, "strip", z.ZodTypeAny, {
31
32
  name: string;
32
33
  value: {
@@ -38,7 +39,6 @@ declare const schema: z.ZodObject<{
38
39
  userCreated?: boolean | undefined;
39
40
  isSecret?: boolean | undefined;
40
41
  modifiedAt?: number | undefined;
41
- packageId?: string | undefined;
42
42
  }, {
43
43
  name: string;
44
44
  value: {
@@ -50,7 +50,6 @@ declare const schema: z.ZodObject<{
50
50
  userCreated?: boolean | undefined;
51
51
  isSecret?: boolean | undefined;
52
52
  modifiedAt?: number | undefined;
53
- packageId?: string | undefined;
54
53
  }>;
55
54
  /** Table metadata for persisted observable variables. */
56
55
  export declare const persistentVarsMetaData: ITableMetaData;
@@ -91,7 +90,7 @@ export type PersistentVar<T> = Observable<T> & {
91
90
  /** Resolve a persistent-variable record by name, preferring device scope. */
92
91
  export declare function getPersistentVar(name: string, dataContext?: DataContext): Promise<IPersistentVar | undefined>;
93
92
  interface IPersistentVarOptionsBase {
94
- scope?: "device" | "user" | "group" | "groupDevice" | "groupUser";
93
+ scope?: PersistentVarScope;
95
94
  userContext?: UserContext;
96
95
  dataContext?: DataContext;
97
96
  isSecret?: boolean;
@@ -133,4 +132,3 @@ export declare function groupDeviceVar<T = string>(name: string, opts?: Omit<IPe
133
132
  */
134
133
  export declare function groupUserVar<T = string>(name: string, opts: Omit<IPersistentVarOptionsWithDefault<T>, "scope">): PersistentVar<T>;
135
134
  export declare function groupUserVar<T = string>(name: string, opts?: Omit<IPersistentVarOptionsWithoutDefault<T>, "scope">): PersistentVar<T | undefined>;
136
- export {};
@@ -40,7 +40,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
40
40
  done = true;
41
41
  };
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.PersistentVarsTable = exports.persistentVarsMetaData = void 0;
43
+ exports.PersistentVarsTable = exports.persistentVarsMetaData = exports.persistentVarStorageName = exports.packageScopedPersistentVarStorageName = exports.packageScopedPersistentVarName = exports.PERSISTENT_VAR_SCOPES = void 0;
44
44
  exports.PersistentVars = PersistentVars;
45
45
  exports.getPersistentVar = getPersistentVar;
46
46
  exports.deviceVar = deviceVar;
@@ -59,10 +59,16 @@ const decorators_1 = require("./orm/decorators");
59
59
  const table_1 = require("./orm/table");
60
60
  const table_definitions_system_1 = require("./orm/table-definitions.system");
61
61
  const types_1 = require("./orm/types");
62
+ const persistent_var_identity_1 = require("./persistent-var-identity");
63
+ var persistent_var_identity_2 = require("./persistent-var-identity");
64
+ Object.defineProperty(exports, "PERSISTENT_VAR_SCOPES", { enumerable: true, get: function () { return persistent_var_identity_2.PERSISTENT_VAR_SCOPES; } });
65
+ Object.defineProperty(exports, "packageScopedPersistentVarName", { enumerable: true, get: function () { return persistent_var_identity_2.packageScopedPersistentVarName; } });
66
+ Object.defineProperty(exports, "packageScopedPersistentVarStorageName", { enumerable: true, get: function () { return persistent_var_identity_2.packageScopedPersistentVarStorageName; } });
67
+ Object.defineProperty(exports, "persistentVarStorageName", { enumerable: true, get: function () { return persistent_var_identity_2.persistentVarStorageName; } });
62
68
  const schema = zod_1.z.object({
63
69
  persistentVarId: zod_types_1.zodPeerId,
64
70
  name: zod_1.z.string(),
65
- scope: zod_1.z.enum(["device", "user", "group", "groupDevice", "groupUser"]),
71
+ scope: zod_1.z.enum(persistent_var_identity_1.PERSISTENT_VAR_SCOPES),
66
72
  value: zod_1.z
67
73
  .object({
68
74
  value: zod_1.z.any(),
@@ -75,9 +81,6 @@ const schema = zod_1.z.object({
75
81
  .number()
76
82
  .optional()
77
83
  .describe("Timestamp of when this pvar was last modified (Date.now())"),
78
- packageId: zod_types_1.zodPeerId
79
- .optional()
80
- .describe("The package that owns this persistent variable, if any"),
81
84
  });
82
85
  /** Table metadata for persisted observable variables. */
83
86
  exports.persistentVarsMetaData = {
@@ -186,8 +189,21 @@ async function getPersistentVar(name, dataContext) {
186
189
  }
187
190
  return pVars[0];
188
191
  }
189
- /** Cache for pvar instances keyed by scope:name:userContextId:dataContextId */
192
+ /** Cache for pvar instances keyed by scope, name, user-context instance, and data context. */
190
193
  const pvarCache = new Map();
194
+ const userContextCacheIds = new WeakMap();
195
+ let nextUserContextCacheId = 1;
196
+ function userContextCacheId(userContext) {
197
+ if (!userContext) {
198
+ return "";
199
+ }
200
+ let id = userContextCacheIds.get(userContext);
201
+ if (id === undefined) {
202
+ id = nextUserContextCacheId++;
203
+ userContextCacheIds.set(userContext, id);
204
+ }
205
+ return String(id);
206
+ }
191
207
  function deviceVar(name, opts) {
192
208
  const fullOpts = { ...opts, scope: "device" };
193
209
  if (opts?.defaultValue !== undefined) {
@@ -236,7 +252,7 @@ function groupUserVar(name, opts) {
236
252
  function persistentVarFactory(name, opts) {
237
253
  const scope = opts.scope || "device";
238
254
  // Check cache - key is based on the parameters passed in
239
- const cacheKey = `${scope}:${name}:${opts?.userContext?.userId ?? ""}:${opts?.dataContext?.dataContextId ?? ""}`;
255
+ const cacheKey = `${scope}:${name}:${userContextCacheId(opts?.userContext)}:${opts?.dataContext?.dataContextId ?? ""}`;
240
256
  const cached = pvarCache.get(cacheKey);
241
257
  if (cached) {
242
258
  return cached;
@@ -273,12 +289,8 @@ function persistentVarFactory(name, opts) {
273
289
  }
274
290
  }
275
291
  function getVarNameInDb() {
276
- if (scope === "groupDevice" || scope === "groupUser") {
277
- const dc = opts?.dataContext || userContext.defaultDataContext();
278
- // if it's a group var saved in the user's personal db, it's postfixed with the dataContextId to make it unique
279
- return `${name}_${dc?.dataContextId}`;
280
- }
281
- return name;
292
+ const dc = opts?.dataContext || userContext.defaultDataContext();
293
+ return (0, persistent_var_identity_1.persistentVarStorageName)(name, scope, dc?.dataContextId);
282
294
  }
283
295
  async function loadRecFromDb() {
284
296
  const dc = getDataContext();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("../utils");
4
+ const persistent_var_identity_1 = require("./persistent-var-identity");
5
+ describe("package-scoped persistent variable identity", () => {
6
+ test("prefixes package identity before deterministic ID generation", () => {
7
+ const packageId = (0, utils_1.newid)();
8
+ const physicalName = (0, persistent_var_identity_1.packageScopedPersistentVarStorageName)(packageId, "settings", "device", (0, utils_1.newid)());
9
+ expect(physicalName).toBe(`${packageId}_settings`);
10
+ expect((0, utils_1.deterministicPvarId)(physicalName)).not.toBe((0, utils_1.deterministicPvarId)("settings"));
11
+ });
12
+ test("qualifies group-dependent scopes with the trusted context", () => {
13
+ const packageId = (0, utils_1.newid)();
14
+ const dataContextId = (0, utils_1.newid)();
15
+ const baseName = (0, persistent_var_identity_1.packageScopedPersistentVarName)(packageId, "thread:dynamic");
16
+ expect((0, persistent_var_identity_1.persistentVarStorageName)(baseName, "device", dataContextId)).toBe(baseName);
17
+ expect((0, persistent_var_identity_1.persistentVarStorageName)(baseName, "user", dataContextId)).toBe(baseName);
18
+ expect((0, persistent_var_identity_1.persistentVarStorageName)(baseName, "group", dataContextId)).toBe(baseName);
19
+ expect((0, persistent_var_identity_1.persistentVarStorageName)(baseName, "groupDevice", dataContextId)).toBe(`${baseName}_${dataContextId}`);
20
+ expect((0, persistent_var_identity_1.persistentVarStorageName)(baseName, "groupUser", dataContextId)).toBe(`${baseName}_${dataContextId}`);
21
+ });
22
+ test("rejects invalid identity inputs", () => {
23
+ expect(() => (0, persistent_var_identity_1.packageScopedPersistentVarName)("not-an-id", "settings")).toThrow(/package ID/);
24
+ expect(() => (0, persistent_var_identity_1.packageScopedPersistentVarName)((0, utils_1.newid)(), " ")).toThrow(/non-empty/);
25
+ expect(() => (0, persistent_var_identity_1.persistentVarStorageName)("settings", "groupUser")).toThrow(/data context/);
26
+ });
27
+ });
@@ -250,7 +250,6 @@ export declare const toolSchema: z.ZodObject<{
250
250
  }[];
251
251
  };
252
252
  accessLevel: number;
253
- packageId?: string | undefined;
254
253
  detailedDescription?: string | undefined;
255
254
  configSchema?: {
256
255
  type: IOSchemaType;
@@ -266,6 +265,7 @@ export declare const toolSchema: z.ZodObject<{
266
265
  } | undefined;
267
266
  codeRepository?: string | undefined;
268
267
  isAssistantRunner?: boolean | undefined;
268
+ packageId?: string | undefined;
269
269
  }, {
270
270
  name: string;
271
271
  toolId: string;
@@ -295,7 +295,6 @@ export declare const toolSchema: z.ZodObject<{
295
295
  }[] | undefined;
296
296
  };
297
297
  code?: string | undefined;
298
- packageId?: string | undefined;
299
298
  detailedDescription?: string | undefined;
300
299
  configSchema?: {
301
300
  type: IOSchemaType;
@@ -311,6 +310,7 @@ export declare const toolSchema: z.ZodObject<{
311
310
  } | undefined;
312
311
  codeRepository?: string | undefined;
313
312
  isAssistantRunner?: boolean | undefined;
313
+ packageId?: string | undefined;
314
314
  accessLevel?: number | undefined;
315
315
  }>;
316
316
  /** A tool definition stored in the Tools table. */
@@ -357,7 +357,6 @@ export declare function Tools(dataContext?: DataContext): import("..").Table<{
357
357
  }[];
358
358
  };
359
359
  accessLevel: number;
360
- packageId?: string | undefined;
361
360
  detailedDescription?: string | undefined;
362
361
  configSchema?: {
363
362
  type: IOSchemaType;
@@ -373,4 +372,5 @@ export declare function Tools(dataContext?: DataContext): import("..").Table<{
373
372
  } | undefined;
374
373
  codeRepository?: string | undefined;
375
374
  isAssistantRunner?: boolean | undefined;
375
+ packageId?: string | undefined;
376
376
  }>;
@@ -19,9 +19,8 @@ const tools_2 = require("../tools");
19
19
  async function installContractPackage(dataContext, packageDefinition, opts) {
20
20
  const { packageId } = packageDefinition;
21
21
  const registry = opts?.registry;
22
- // Register contracts in the in-memory registry
23
- if (registry) {
24
- for (const contract of packageDefinition.contracts) {
22
+ const registrations = registry
23
+ ? packageDefinition.contracts.map((contract) => {
25
24
  const key = `${contract.contractId}@${contract.version}`;
26
25
  const alsoImpl = packageDefinition.alsoImplements.get(key) ?? [];
27
26
  // If the contract is already frozen in the registry, re-register as
@@ -31,11 +30,14 @@ async function installContractPackage(dataContext, packageDefinition, opts) {
31
30
  if (existing && !existing.devTag && contract.devTag === "dev") {
32
31
  effectiveContract = { ...contract, devTag: undefined };
33
32
  }
34
- const result = registry.register(packageId, effectiveContract, alsoImpl);
35
- if (!result.valid) {
36
- console.error(`[ContractPackageLoader] Contract registration failed for ${contract.name}:`, result.errors);
37
- }
38
- }
33
+ return { definition: effectiveContract, alsoImplements: alsoImpl };
34
+ })
35
+ : undefined;
36
+ const validation = registrations ? registry?.validateBatch(packageId, registrations) : undefined;
37
+ if (validation && !validation.valid) {
38
+ throw new Error(`Atomic contract registration failed for package ${packageId}: ${validation.errors
39
+ .map((error) => error.message)
40
+ .join("; ")}`);
39
41
  }
40
42
  // Install tool instances with packageId
41
43
  for (const toolInstance of packageDefinition.toolInstances) {
@@ -55,6 +57,16 @@ async function installContractPackage(dataContext, packageDefinition, opts) {
55
57
  const assistantWithPackageId = { ...assistant, packageId };
56
58
  await Assistants(dataContext).save(assistantWithPackageId);
57
59
  }
60
+ // Activate the new provider only after all fallible package-owned persistence
61
+ // has succeeded, so a failed update leaves the previous provider routable.
62
+ if (registry && registrations) {
63
+ const result = registry.replaceBatch(packageId, registrations);
64
+ if (!result.valid) {
65
+ throw new Error(`Atomic contract registration failed for package ${packageId}: ${result.errors
66
+ .map((error) => error.message)
67
+ .join("; ")}`);
68
+ }
69
+ }
58
70
  }
59
71
  /**
60
72
  * Attempts to extract an `IPackageDefinitionResult` from a bundle's module exports.
@@ -66,6 +66,22 @@ describe("installContractPackage registry wiring", () => {
66
66
  const dataContext = mockDataContext();
67
67
  await expect((0, contract_package_loader_1.installContractPackage)(dataContext, makeDefinition())).resolves.toBeUndefined();
68
68
  });
69
+ it("keeps the previous provider active when package persistence fails", async () => {
70
+ const registry = new registry_1.ContractRegistry();
71
+ const previous = makeDefinition();
72
+ previous.contracts[0] = { ...previous.contracts[0], name: "Previous Contract" };
73
+ await (0, contract_package_loader_1.installContractPackage)(mockDataContext(), previous, { registry });
74
+ const dataContext = mockDataContext();
75
+ dataContext.tableContainer.registerTableDefinition.mockImplementation(() => {
76
+ throw new Error("table registration failed");
77
+ });
78
+ const replacement = makeDefinition({
79
+ tableDefinitions: [{}],
80
+ });
81
+ await expect((0, contract_package_loader_1.installContractPackage)(dataContext, replacement, { registry })).rejects.toThrow(/table registration failed/);
82
+ expect(registry.resolve(contractId, 1)?.name).toBe("Previous Contract");
83
+ expect(registry.getProviderPackageId(contractId, 1)).toBe(packageId);
84
+ });
69
85
  });
70
86
  describe("extractPackageDefinition", () => {
71
87
  it("returns a package definition when packageId and contracts are present", () => {