@peers-app/peers-sdk 0.26.4 → 0.26.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 (47) hide show
  1. package/dist/contracts/__tests__/contract-resolvers.test.js +24 -0
  2. package/dist/contracts/__tests__/legacy-removal.test.js +38 -0
  3. package/dist/contracts/__tests__/remote-contract-transport.test.js +23 -32
  4. package/dist/contracts/__tests__/system-contracts.test.js +80 -0
  5. package/dist/contracts/contract-resolvers.d.ts +1 -1
  6. package/dist/contracts/contract-resolvers.js +3 -2
  7. package/dist/contracts/create-local-consumer.d.ts +2 -1
  8. package/dist/contracts/create-local-consumer.js +1 -1
  9. package/dist/contracts/index.d.ts +2 -0
  10. package/dist/contracts/index.js +2 -0
  11. package/dist/contracts/remote-contract-transport.d.ts +3 -3
  12. package/dist/contracts/remote-contract-transport.js +19 -17
  13. package/dist/contracts/system/host-shell-contract.d.ts +191 -0
  14. package/dist/contracts/system/host-shell-contract.js +190 -0
  15. package/dist/contracts/system/index.d.ts +4 -0
  16. package/dist/contracts/system/index.js +4 -0
  17. package/dist/contracts/system/packages-contract.d.ts +257 -0
  18. package/dist/contracts/system/packages-contract.js +213 -0
  19. package/dist/data/create-group.d.ts +1 -1
  20. package/dist/data/create-group.js +1 -1
  21. package/dist/data/package-version-resolver.d.ts +8 -2
  22. package/dist/data/package-version-resolver.js +28 -2
  23. package/dist/data/package-version-resolver.test.js +42 -0
  24. package/dist/package-installer/package-author-signer.d.ts +19 -0
  25. package/dist/package-installer/package-author-signer.js +24 -0
  26. package/dist/package-installer/package-creator.d.ts +1 -7
  27. package/dist/package-installer/package-creator.js +0 -12
  28. package/dist/package-installer/package-creator.test.js +0 -16
  29. package/dist/package-installer/package-installer.d.ts +5 -0
  30. package/dist/package-installer/package-installer.js +60 -37
  31. package/dist/package-installer/package-installer.test.js +129 -0
  32. package/dist/package-installer/types.d.ts +5 -11
  33. package/dist/package-loader/index.d.ts +1 -0
  34. package/dist/package-loader/index.js +1 -0
  35. package/dist/package-loader/isolated-runtime.d.ts +18 -0
  36. package/dist/package-loader/package-loader.d.ts +67 -4
  37. package/dist/package-loader/package-loader.js +300 -52
  38. package/dist/package-loader/package-loader.test.js +413 -1
  39. package/dist/package-loader/package-runtime-state.d.ts +112 -0
  40. package/dist/package-loader/package-runtime-state.js +18 -0
  41. package/dist/package-loader/resolve-consumed-contracts.js +5 -4
  42. package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
  43. package/dist/rpc-types.d.ts +0 -121
  44. package/dist/rpc-types.js +0 -39
  45. package/dist/system-ids.d.ts +13 -0
  46. package/dist/system-ids.js +14 -1
  47. package/package.json +1 -1
@@ -12,8 +12,8 @@ const package_version_resolver_1 = require("../data/package-version-resolver");
12
12
  const package_versions_1 = require("../data/package-versions");
13
13
  const packages_1 = require("../data/packages");
14
14
  const persistent_vars_1 = require("../data/persistent-vars");
15
- const rpc_types_1 = require("../rpc-types");
16
15
  const utils_1 = require("../utils");
16
+ const package_author_signer_1 = require("./package-author-signer");
17
17
  /**
18
18
  * Install or update a dev PackageVersion from locally-built bundle files.
19
19
  *
@@ -21,6 +21,11 @@ const utils_1 = require("../utils");
21
21
  * bundles from `dist/`, hashes them for deduplication, saves File records
22
22
  * only when content has changed, upserts the PackageVersion, and optionally
23
23
  * triggers the package loader and device version resolution.
24
+ *
25
+ * When the bundle hash matches the existing PackageVersion, no files or PV are
26
+ * rewritten. The Packages row still gets a missing `activePackageVersionId`
27
+ * (and description), and when the id was missing the version is activated on
28
+ * this device unless `skipResolve`.
24
29
  */
25
30
  async function installPackageFromBundles(dataContext, deps, packageId, opts) {
26
31
  const versionTag = opts?.versionTag ?? "dev";
@@ -53,23 +58,24 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
53
58
  const uiHash = uiBundle ? (0, file_types_1.computeFileHash)(uiBundle) : undefined;
54
59
  // 5. Compute PV hash
55
60
  const pvHash = (0, package_versions_1.computePackageVersionHash)(version, "", packageHash, routesHash, uiHash);
56
- // 6. Early exit if unchanged (still repair missing appNavs — see syncDevBundles skipLoad)
61
+ // 6. Early exit if unchanged. Still repair missing appNavs (see syncDevBundles
62
+ // skipLoad), and still fill an empty activePackageVersionId (e.g. reimport after
63
+ // the Packages row was deleted). If it was empty, activate on the device unless
64
+ // skipResolve.
57
65
  const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
58
66
  const existingPv = await pvTable.findOne({ packageId, versionTag });
59
67
  if (existingPv && existingPv.packageVersionHash === pvHash) {
60
- if (existingPv.appNavs?.length) {
61
- return {
62
- packageVersion: existingPv,
63
- loaded: undefined,
64
- unchanged: true,
65
- };
68
+ let loaded;
69
+ let unchanged = true;
70
+ if (!existingPv.appNavs?.length) {
71
+ loaded = await loadAndPersistAppNavs(dataContext, packageId, existingPv, localPath);
72
+ unchanged = !loaded?.appNavs?.length;
66
73
  }
67
- const loaded = await loadAndPersistAppNavs(dataContext, packageId, existingPv, localPath);
68
- return {
69
- packageVersion: existingPv,
70
- loaded,
71
- unchanged: !loaded?.appNavs?.length,
72
- };
74
+ await activateInstalledPackageVersion(dataContext, packageId, existingPv, info, localPath, {
75
+ skipResolve: opts?.skipResolve,
76
+ resolveOnlyWhenActivated: true,
77
+ });
78
+ return { packageVersion: existingPv, loaded, unchanged };
73
79
  }
74
80
  // 7. Save bundle files (deduped by hash)
75
81
  const packageFile = await saveBundleFile(dataContext, packageBundle, `${packageId}-package.bundle.js`, packageId);
@@ -110,31 +116,48 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
110
116
  if (!opts?.skipLoad || !savedPv.appNavs?.length) {
111
117
  loaded = await loadAndPersistAppNavs(dataContext, packageId, savedPv, localPath);
112
118
  }
113
- // 10. Update package record if needed (description, first-install activation)
119
+ // 10–11. Update package record and activate on device (unless skipResolve)
120
+ await activateInstalledPackageVersion(dataContext, packageId, savedPv, info, localPath, {
121
+ skipResolve: opts?.skipResolve,
122
+ });
123
+ return { packageVersion: savedPv, loaded, unchanged: false };
124
+ }
125
+ /**
126
+ * Sync the Packages row with an installed PackageVersion, then activate it on
127
+ * this device.
128
+ *
129
+ * Updates the description when it differs, and sets `activePackageVersionId`
130
+ * when it is empty (first install, or reimport after the row was deleted).
131
+ * Unless `skipResolve`, writes device prefs and force-resolves the version.
132
+ * With `resolveOnlyWhenActivated`, the device step runs only when this call
133
+ * filled an empty `activePackageVersionId`.
134
+ */
135
+ async function activateInstalledPackageVersion(dataContext, packageId, packageVersion, info, localPath, opts) {
136
+ // Update package record if needed (description, first-install activation)
114
137
  const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
115
- if (pkg) {
116
- let needsSave = false;
117
- if (info.description && info.description !== pkg.description) {
118
- pkg.description = info.description;
119
- needsSave = true;
120
- }
121
- if (!pkg.activePackageVersionId) {
122
- pkg.activePackageVersionId = savedPv.packageVersionId;
123
- needsSave = true;
124
- }
125
- if (needsSave) {
126
- await (0, packages_1.Packages)(dataContext).signAndSave(pkg);
127
- }
138
+ if (!pkg)
139
+ return;
140
+ let needsSave = false;
141
+ if (info.description && info.description !== pkg.description) {
142
+ pkg.description = info.description;
143
+ needsSave = true;
128
144
  }
129
- // 11. Activate on device (unless skipResolve)
130
- if (!opts?.skipResolve && pkg) {
131
- await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: savedPv.packageVersionId }, dataContext);
132
- await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dataContext, {
133
- force: true,
134
- localPath,
135
- });
145
+ const activated = !pkg.activePackageVersionId;
146
+ if (activated) {
147
+ pkg.activePackageVersionId = packageVersion.packageVersionId;
148
+ needsSave = true;
136
149
  }
137
- return { packageVersion: savedPv, loaded, unchanged: false };
150
+ if (needsSave) {
151
+ await (0, packages_1.Packages)(dataContext).signAndSave(pkg);
152
+ }
153
+ // Activate on device
154
+ if (opts.skipResolve || (opts.resolveOnlyWhenActivated && !activated))
155
+ return;
156
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: packageVersion.packageVersionId }, dataContext);
157
+ await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dataContext, {
158
+ force: true,
159
+ localPath,
160
+ });
138
161
  }
139
162
  /**
140
163
  * Load a package bundle and persist `appNavs` onto the PackageVersion when the
@@ -159,7 +182,7 @@ async function loadAndPersistAppNavs(dataContext, packageId, pv, localPath) {
159
182
  return loaded;
160
183
  pv.appNavs = loaded.appNavs;
161
184
  if (pv.packageAuthorSignature) {
162
- const authorSig = await rpc_types_1.rpcServerCalls.signPackageAuthorVersion({
185
+ const authorSig = await (0, package_author_signer_1.signPackageAuthorVersionOnHost)({
163
186
  packageId: pv.packageId,
164
187
  packageVersionId: pv.packageVersionId,
165
188
  version: pv.version,
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const file_types_1 = require("../data/files/file.types");
4
4
  const utils_1 = require("../utils");
5
+ const package_author_signer_1 = require("./package-author-signer");
5
6
  const package_installer_1 = require("./package-installer");
6
7
  // --- Mocks for SDK internals ---
7
8
  const mockFindOnePV = jest.fn();
@@ -258,6 +259,28 @@ describe("installPackageFromBundles", () => {
258
259
  });
259
260
  return { deps, files };
260
261
  }
262
+ /** A PV whose hash matches the `setupBasicInstall` bundles. */
263
+ function matchingPv(overrides = {}) {
264
+ const packageHash = (0, file_types_1.computeFileHash)(packageBundle);
265
+ const routesHash = (0, file_types_1.computeFileHash)(routesBundle);
266
+ const uiHash = (0, file_types_1.computeFileHash)(uiBundle);
267
+ const { computePackageVersionHash } = jest.requireActual("../data/package-versions");
268
+ return {
269
+ packageVersionId: (0, utils_1.newid)(),
270
+ packageId,
271
+ version: "1.2.3",
272
+ versionTag: "dev",
273
+ packageVersionHash: computePackageVersionHash("1.2.3", "", packageHash, routesHash, uiHash),
274
+ packageBundleFileId: (0, utils_1.newid)(),
275
+ packageBundleFileHash: packageHash,
276
+ routesBundleFileHash: routesHash,
277
+ uiBundleFileHash: uiHash,
278
+ signature: "",
279
+ createdBy: "user1",
280
+ createdAt: "2024-01-01T00:00:00.000Z",
281
+ ...overrides,
282
+ };
283
+ }
261
284
  it("installs a new package version from bundles", async () => {
262
285
  const { deps } = setupBasicInstall();
263
286
  const dc = mockDataContext();
@@ -455,6 +478,31 @@ describe("installPackageFromBundles", () => {
455
478
  expect(mockSignAndSavePV).toHaveBeenCalledTimes(2);
456
479
  expect(mockSignAndSavePV.mock.calls[1][0].appNavs).toEqual(appNavs);
457
480
  });
481
+ it.each([
482
+ ["re-signs with the host signer", "new-signature"],
483
+ ["clears the signature without a host signer", undefined],
484
+ ])("%s when appNavs change on a signed version", async (_name, expected) => {
485
+ const { deps } = setupBasicInstall();
486
+ const signer = jest.fn(async () => "new-signature");
487
+ (0, package_author_signer_1.setPackageAuthorVersionSigner)(expected ? signer : undefined);
488
+ mockFindOnePV.mockResolvedValue(matchingPv({ packageAuthorSignature: "old-signature" }));
489
+ const appNavs = [{ name: "App", iconClassName: "bi bi-app", navigationPath: "app" }];
490
+ const dc = mockDataContext({
491
+ packageLoader: { loadPackage: jest.fn().mockResolvedValue({ packageId, appNavs }) },
492
+ });
493
+ try {
494
+ await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, { skipResolve: true });
495
+ const saved = mockSignAndSavePV.mock.calls.at(-1)?.[0];
496
+ expect(saved.appNavs).toEqual(appNavs);
497
+ expect(saved.packageAuthorSignature).toBe(expected);
498
+ if (expected) {
499
+ expect(signer).toHaveBeenCalledWith(expect.objectContaining({ packageId, appNavs, versionTag: "dev" }));
500
+ }
501
+ }
502
+ finally {
503
+ (0, package_author_signer_1.setPackageAuthorVersionSigner)(undefined);
504
+ }
505
+ });
458
506
  it("repairs missing appNavs on unchanged early-return", async () => {
459
507
  const { deps } = setupBasicInstall();
460
508
  const packageHash = (0, file_types_1.computeFileHash)(packageBundle);
@@ -489,6 +537,87 @@ describe("installPackageFromBundles", () => {
489
537
  expect(result.packageVersion.appNavs).toEqual(appNavs);
490
538
  expect(mockSignAndSavePV).toHaveBeenCalledTimes(1);
491
539
  });
540
+ it("reimport after Packages row deletion fills activePackageVersionId and activates", async () => {
541
+ const { deps } = setupBasicInstall();
542
+ const dc = mockDataContext();
543
+ const localPath = `/packages/${packageId}`;
544
+ const freshPkg = () => ({
545
+ packageId,
546
+ name: "test-pkg",
547
+ description: "Test package",
548
+ signature: "",
549
+ createdBy: "user1",
550
+ });
551
+ mockGetPkg.mockResolvedValue(freshPkg());
552
+ const first = await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, {
553
+ localPath,
554
+ skipLoad: true,
555
+ skipResolve: true,
556
+ });
557
+ expect(first.unchanged).toBe(false);
558
+ const pvId = first.packageVersion.packageVersionId;
559
+ // Uninstall deleted the Packages row; the PV row remains.
560
+ mockFindOnePV.mockResolvedValue(first.packageVersion);
561
+ mockSignAndSavePkg.mockClear();
562
+ mockSignAndSavePV.mockClear();
563
+ mockSaveFile.mockClear();
564
+ mockUpdatePackagePrefs.mockClear();
565
+ mockResolveDevicePackageVersion.mockClear();
566
+ mockGetPkg.mockResolvedValue(freshPkg());
567
+ const result = await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, { localPath });
568
+ expect(result.unchanged).toBe(true);
569
+ expect(result.packageVersion.packageVersionId).toBe(pvId);
570
+ expect(mockSaveFile).not.toHaveBeenCalled();
571
+ expect(mockSignAndSavePV).not.toHaveBeenCalled();
572
+ expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({ activePackageVersionId: pvId }));
573
+ expect(mockUpdatePackagePrefs).toHaveBeenCalledWith(packageId, { activePackageVersionId: pvId }, dc);
574
+ expect(mockResolveDevicePackageVersion).toHaveBeenCalledWith(expect.objectContaining({ packageId, activePackageVersionId: pvId }), dc, { force: true, localPath });
575
+ });
576
+ it("unchanged with skipResolve fills activePackageVersionId without resolving", async () => {
577
+ const { deps } = setupBasicInstall();
578
+ const dc = mockDataContext();
579
+ const localPath = `/packages/${packageId}`;
580
+ const existingPv = matchingPv();
581
+ mockFindOnePV.mockResolvedValue(existingPv);
582
+ mockGetPkg.mockResolvedValue({
583
+ packageId,
584
+ name: "test-pkg",
585
+ description: "Test package",
586
+ signature: "",
587
+ createdBy: "user1",
588
+ });
589
+ const result = await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, {
590
+ localPath,
591
+ skipLoad: true,
592
+ skipResolve: true,
593
+ });
594
+ expect(result.unchanged).toBe(true);
595
+ expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({ activePackageVersionId: existingPv.packageVersionId }));
596
+ expect(mockUpdatePackagePrefs).not.toHaveBeenCalled();
597
+ expect(mockResolveDevicePackageVersion).not.toHaveBeenCalled();
598
+ });
599
+ it("unchanged with an active version does not re-activate", async () => {
600
+ const { deps } = setupBasicInstall();
601
+ const dc = mockDataContext();
602
+ const localPath = `/packages/${packageId}`;
603
+ const existingPv = matchingPv({
604
+ appNavs: [{ name: "App", iconClassName: "bi bi-app", navigationPath: "app" }],
605
+ });
606
+ mockFindOnePV.mockResolvedValue(existingPv);
607
+ mockGetPkg.mockResolvedValue({
608
+ packageId,
609
+ name: "test-pkg",
610
+ description: "Test package",
611
+ activePackageVersionId: existingPv.packageVersionId,
612
+ signature: "",
613
+ createdBy: "user1",
614
+ });
615
+ const result = await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, { localPath });
616
+ expect(result.unchanged).toBe(true);
617
+ expect(mockSignAndSavePkg).not.toHaveBeenCalled();
618
+ expect(mockUpdatePackagePrefs).not.toHaveBeenCalled();
619
+ expect(mockResolveDevicePackageVersion).not.toHaveBeenCalled();
620
+ });
492
621
  it("calls updatePackagePrefs and resolveDevicePackageVersion when skipResolve is false", async () => {
493
622
  const { deps } = setupBasicInstall();
494
623
  const dc = mockDataContext();
@@ -56,7 +56,11 @@ export interface IInstallOpts {
56
56
  export interface IInstallResult {
57
57
  packageVersion: IPackageVersion;
58
58
  loaded: IPeersPackage | undefined;
59
- /** True when the on-disk bundles match the existing PackageVersion hash — no writes occurred. */
59
+ /**
60
+ * True when the on-disk bundles match the existing PackageVersion hash and this
61
+ * call persisted no new appNavs. Bundle files and the PackageVersion row are not
62
+ * rewritten; the Packages row and device activation may still be updated.
63
+ */
60
64
  unchanged: boolean;
61
65
  }
62
66
  /** Metadata extracted from a local package directory's `package.json`. */
@@ -117,16 +121,6 @@ export interface ICreatePackageResult {
117
121
  */
118
122
  templateWarning?: string;
119
123
  }
120
- /** Host/RPC result for adding, creating, or updating a package. */
121
- export interface IAddOrUpdatePackageResult {
122
- /** Package that was created, imported, cloned, or updated. */
123
- packageId: string;
124
- /**
125
- * Non-fatal notice after a successful local create (for example a stale
126
- * template cache). Undefined on update, clone, and import of an existing directory.
127
- */
128
- warning?: string;
129
- }
130
124
  /** Options for {@link clonePackage}. */
131
125
  export interface IClonePackageOpts {
132
126
  /** Remote git URL to clone. */
@@ -5,3 +5,4 @@ export * from "./isolated-runtime";
5
5
  export * from "./isolated-table-gateway";
6
6
  export * from "./package-loader";
7
7
  export * from "./package-network-policy";
8
+ export * from "./package-runtime-state";
@@ -21,3 +21,4 @@ __exportStar(require("./isolated-runtime"), exports);
21
21
  __exportStar(require("./isolated-table-gateway"), exports);
22
22
  __exportStar(require("./package-loader"), exports);
23
23
  __exportStar(require("./package-network-policy"), exports);
24
+ __exportStar(require("./package-runtime-state"), exports);
@@ -5,6 +5,7 @@ import type { ContractRegistry } from "../contracts/registry";
5
5
  import type { PersistentVarScope } from "../data/persistent-vars";
6
6
  import type { IField } from "../types/field-type";
7
7
  import type { IIsolatedContractToolRef, IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
8
+ import type { IPackageWorkerHealth } from "./package-runtime-state";
8
9
  /** Standard table methods exposed through isolated contract-table references. */
9
10
  export declare const ISOLATED_CONTRACT_TABLE_OPERATIONS: readonly ["get", "list", "count", "findOne", "save", "insert", "update", "delete"];
10
11
  /** Consumer-callable operation on an isolated contract table. */
@@ -68,6 +69,12 @@ export interface IIsolatedPackageRuntime {
68
69
  * previous worker and preserve the old registration if replacement fails.
69
70
  */
70
71
  replace(artifact: IIsolatedPackageArtifact, options?: IIsolatedPackageInstallOptions): Promise<IIsolatedInstallResult>;
72
+ /**
73
+ * Refresh trusted contract maturity for an unchanged installed artifact, without
74
+ * restarting its worker or invalidating calls in flight. Stable promotion freezes
75
+ * the contract shapes in the registry through the normal catalog publication checks.
76
+ */
77
+ updateContractLifecycle?(packageId: string, options: IIsolatedPackageInstallOptions): Promise<void>;
71
78
  /** Stop the package worker (if any) and optionally unregister its contracts. */
72
79
  uninstall(packageId: string, options?: IIsolatedPackageUninstallOptions): Promise<void>;
73
80
  /**
@@ -84,6 +91,17 @@ export interface IIsolatedPackageRuntime {
84
91
  provides(contractId: string, version: number): boolean;
85
92
  /** True when this runtime has installed `packageId`. */
86
93
  isInstalled(packageId: string): boolean;
94
+ /**
95
+ * Report whether the package's worker is running and its last worker start or crash
96
+ * failure since install or replacement. Workers start lazily, so a broken provider only
97
+ * shows here after its first call.
98
+ *
99
+ * @param packageId - Installed package to query.
100
+ * @returns Health, or `undefined` when the package is not installed here.
101
+ */
102
+ getPackageHealth?(packageId: string): IPackageWorkerHealth | undefined;
103
+ /** Subscribe to worker health changes without starting workers. Returns an unsubscribe function. */
104
+ subscribePackageHealth?(listener: (packageId: string) => void): () => void;
87
105
  /** Read effective policy without starting a worker. */
88
106
  getEffectiveToolLevel?(ref: {
89
107
  contractId: string;
@@ -5,9 +5,18 @@ import { ContractRegistry } from "../contracts/registry";
5
5
  import { ToolCatalog } from "../contracts/tool-catalog";
6
6
  import type { IConsumedContract, IRemoteContract, IRemoteContractConsumer } from "../contracts/types";
7
7
  import { type IPackage } from "../data/packages";
8
+ import { type Observable } from "../observable";
8
9
  import type { IPeersPackage } from "../types/peers-package";
9
10
  import { type IIsolatedPackageRuntime } from "./isolated-runtime";
11
+ import { type IPackageRuntimeState, type PackageRuntimeStates, type PackageRuntimeStatus } from "./package-runtime-state";
10
12
  import "../contracts/system";
13
+ /** Options for {@link PackageLoader.unloadPackage}. */
14
+ export interface IPackageUnloadOptions {
15
+ /** Runtime status to record once the package is unloaded. Defaults to `unloaded`. */
16
+ status?: Extract<PackageRuntimeStatus, "unloaded" | "disabled" | "unsupported">;
17
+ /** Explanation recorded with the status. */
18
+ reason?: string;
19
+ }
11
20
  /** Host-only options for {@link PackageLoader.resolveContractEndpoint}. */
12
21
  export interface IPackageContractEndpointOptions {
13
22
  /** Provider package selected from trusted registry state. */
@@ -39,6 +48,17 @@ export declare class PackageLoader {
39
48
  private contractInstalledPackages;
40
49
  /** Packages installed through the isolated runtime (no host eval). */
41
50
  private isolatedPackageIds;
51
+ /** Per-package counter bumped on every runtime-state change; lets slow unloads yield. */
52
+ private runtimeStateSequences;
53
+ /** Hash of the bundle each loaded package was built from, for `onlyIfChanged` loads. */
54
+ private loadedBundleHashes;
55
+ private unsubscribePackageHealth?;
56
+ /**
57
+ * Observed runtime state of each package this loader has attempted, keyed by package id.
58
+ * In memory only; read merged with isolated worker health through
59
+ * {@link getPackageRuntimeState} and {@link getPackageRuntimeStates}.
60
+ */
61
+ readonly runtimeStates: Observable<PackageRuntimeStates>;
42
62
  /**
43
63
  * In-memory contract providers for this data context. Seeded with system
44
64
  * contracts after catalog readiness; package providers are added via
@@ -74,6 +94,23 @@ export declare class PackageLoader {
74
94
  isIsolatedInstalled(packageId: string): boolean;
75
95
  /** Return whether this host configured an isolated-package runtime. */
76
96
  supportsIsolatedPackages(): boolean;
97
+ /**
98
+ * Return the observed runtime state of one package, merged with isolated worker health.
99
+ *
100
+ * @param packageId - Package to query.
101
+ * @returns The state, or `undefined` when this loader has never attempted the package.
102
+ */
103
+ getPackageRuntimeState(packageId: string): IPackageRuntimeState | undefined;
104
+ /** Return every recorded package runtime state, merged with isolated worker health. */
105
+ getPackageRuntimeStates(): PackageRuntimeStates;
106
+ private setRuntimeState;
107
+ private recordRunning;
108
+ private recordStatus;
109
+ /**
110
+ * Record a failed load attempt unless a newer load or unload superseded it. The previous
111
+ * `running` version is kept: a failed replacement leaves the old version serving.
112
+ */
113
+ private recordLoadFailure;
77
114
  private dispatchToolFromProvider;
78
115
  /**
79
116
  * Return the active provider selected by this data context's trusted registry.
@@ -105,9 +142,10 @@ export declare class PackageLoader {
105
142
  * before any consumer call payload is processed.
106
143
  *
107
144
  * @param dependency - Validated package consume declaration.
145
+ * @param callerPackageId - Owning package, supplied by the loader rather than call input.
108
146
  * @returns A live consumer proxy, or `undefined` when no provider matches.
109
147
  */
110
- resolveConsumedContract(dependency: IConsumedContract): Promise<IContractConsumer | undefined>;
148
+ resolveConsumedContract(dependency: IConsumedContract, callerPackageId: string): Promise<IContractConsumer | undefined>;
111
149
  /**
112
150
  * Bind a legacy package's `remote()` declaration to a per-device consumer factory.
113
151
  *
@@ -119,9 +157,10 @@ export declare class PackageLoader {
119
157
  * installed package) so the proxy knows the tool names.
120
158
  *
121
159
  * @param declaration - Validated package remote declaration.
160
+ * @param callerPackageId - Owning package, retained for local and remote authorization.
122
161
  * @returns A per-device consumer factory, or `undefined` when the contract is unknown.
123
162
  */
124
- resolveRemoteContract(declaration: IRemoteContract): Promise<IRemoteContractConsumer<IContractConsumer> | undefined>;
163
+ resolveRemoteContract(declaration: IRemoteContract, callerPackageId: string): Promise<IRemoteContractConsumer<IContractConsumer> | undefined>;
125
164
  /**
126
165
  * Resolve a sandbox-backed provider endpoint when this context owns an
127
166
  * isolated provider for the requested contract.
@@ -132,9 +171,13 @@ export declare class PackageLoader {
132
171
  /**
133
172
  * Stop an isolated worker (if any) and drop the in-memory package instance.
134
173
  *
174
+ * Records the given runtime status unless a newer load recorded state while the unload
175
+ * was in flight.
176
+ *
135
177
  * @param packageId - Package to unload.
178
+ * @param opts - Runtime status and reason to record.
136
179
  */
137
- unloadPackage(packageId: string): Promise<void>;
180
+ unloadPackage(packageId: string, opts?: IPackageUnloadOptions): Promise<void>;
138
181
  /**
139
182
  * Dispose the isolated runtime for this data context.
140
183
  */
@@ -158,14 +201,34 @@ export declare class PackageLoader {
158
201
  * isolated `dist/package.bundle.js`, that disk artifact wins over a stale
159
202
  * stored Files bundle so local-dev packages pick up new contract versions.
160
203
  *
204
+ * With `onlyIfChanged`, a forced load is skipped when the package is running and the
205
+ * selected version and bundle are exactly the ones already loaded. A changed release
206
+ * channel updates the reported label and isolated contract maturity without restarting
207
+ * the worker. Sync reconciliation uses this so an echoed or unrelated row
208
+ * change does not restart the worker and fail calls in flight. Explicit reloads omit it.
209
+ *
161
210
  * @param pkg - Package row.
162
- * @param opts - Optional force reload, local path fallback, and PV id.
211
+ * @param opts - Optional force reload, `onlyIfChanged`, local path fallback, and PV id.
163
212
  */
164
213
  loadPackage(pkg: IPackage, opts?: {
165
214
  force?: boolean;
215
+ onlyIfChanged?: boolean;
166
216
  localPath?: string;
167
217
  packageVersionId?: string;
168
218
  }): Promise<IPeersPackage | undefined>;
219
+ /**
220
+ * Read the bundle a load would use: the selected version's stored file, replaced by the
221
+ * rebuilt local `dist/package.bundle.js` for a dev version with a local path.
222
+ */
223
+ private readSelectedBundle;
224
+ /**
225
+ * True when the package is running the exact version and bundle a load would select, so an
226
+ * `onlyIfChanged` load can keep it. Promotion refreshes the release-channel label and
227
+ * isolated contract maturity without reloading, since the tag is not part of the bundle.
228
+ */
229
+ private keepRunningBuild;
230
+ /** Record the bundle a current, successful load ran, and pass its result through. */
231
+ private rememberBundle;
169
232
  private _readLocalBundle;
170
233
  private guardLoad;
171
234
  /**