@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.
- package/dist/context/data-context.d.ts +36 -5
- package/dist/context/data-context.js +44 -9
- package/dist/context/user-context.d.ts +44 -0
- package/dist/context/user-context.js +91 -12
- package/dist/contracts/__tests__/contract-provider-router.test.js +26 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +44 -0
- package/dist/contracts/__tests__/registry.test.js +104 -0
- package/dist/contracts/contract-provider-router.d.ts +21 -0
- package/dist/contracts/contract-provider-router.js +31 -5
- package/dist/contracts/contract-proxy.d.ts +3 -0
- package/dist/contracts/contract-proxy.js +3 -0
- package/dist/contracts/contract-resolvers.d.ts +21 -3
- package/dist/contracts/contract-resolvers.js +105 -32
- package/dist/contracts/index.d.ts +3 -3
- package/dist/contracts/registry.d.ts +59 -0
- package/dist/contracts/registry.js +155 -25
- package/dist/contracts/system-contracts.js +3 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/orm/types.d.ts +19 -0
- package/dist/data/orm/types.js +11 -0
- package/dist/data/package-table-identity.d.ts +46 -0
- package/dist/data/package-table-identity.js +71 -0
- package/dist/data/package-table-identity.test.d.ts +1 -0
- package/dist/data/package-table-identity.test.js +46 -0
- package/dist/data/package-version-resolver.d.ts +13 -0
- package/dist/data/package-version-resolver.js +142 -6
- package/dist/data/package-version-resolver.test.js +42 -0
- package/dist/data/persistent-var-identity.d.ts +37 -0
- package/dist/data/persistent-var-identity.js +69 -0
- package/dist/data/persistent-vars.d.ts +3 -5
- package/dist/data/persistent-vars.js +25 -13
- package/dist/data/persistent-vars.package-scope.test.d.ts +1 -0
- package/dist/data/persistent-vars.package-scope.test.js +27 -0
- package/dist/data/tools.d.ts +3 -3
- package/dist/package-loader/contract-package-loader.js +20 -8
- package/dist/package-loader/contract-package-loader.test.js +16 -0
- package/dist/package-loader/index.d.ts +4 -0
- package/dist/package-loader/index.js +4 -0
- package/dist/package-loader/isolated-artifact.d.ts +233 -0
- package/dist/package-loader/isolated-artifact.js +643 -0
- package/dist/package-loader/isolated-artifact.test.d.ts +1 -0
- package/dist/package-loader/isolated-artifact.test.js +270 -0
- package/dist/package-loader/isolated-persistent-var-gateway.d.ts +20 -0
- package/dist/package-loader/isolated-persistent-var-gateway.js +106 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.js +56 -0
- package/dist/package-loader/isolated-runtime.d.ts +229 -0
- package/dist/package-loader/isolated-runtime.js +37 -0
- package/dist/package-loader/isolated-table-gateway.d.ts +16 -0
- package/dist/package-loader/isolated-table-gateway.js +375 -0
- package/dist/package-loader/isolated-table-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-table-gateway.test.js +206 -0
- package/dist/package-loader/isolated-value.d.ts +14 -0
- package/dist/package-loader/isolated-value.js +63 -0
- package/dist/package-loader/package-loader.d.ts +123 -4
- package/dist/package-loader/package-loader.js +417 -46
- package/dist/package-loader/package-loader.test.js +478 -12
- package/dist/package-loader/resolve-consumed-contracts.d.ts +3 -3
- package/dist/package-loader/resolve-consumed-contracts.js +18 -10
- package/dist/utils.d.ts +21 -2
- package/dist/utils.js +46 -7
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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?:
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
277
|
-
|
|
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
|
+
});
|
package/dist/data/tools.d.ts
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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", () => {
|