@camstack/system 1.2.474 → 1.2.476

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 (91) hide show
  1. package/dist/{agent-https-DRwEbLL7.js → agent-https-DkXBXEvz.js} +8 -8
  2. package/dist/{agent-https-CbczsyM0.mjs → agent-https-vpf_D97z.mjs} +8 -8
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/calendars/calendars.addon.js +1 -1
  14. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  15. package/dist/builtins/camera-grid/index.js +1 -1
  16. package/dist/builtins/camera-grid/index.mjs +1 -1
  17. package/dist/builtins/composer/composer-live-set.d.ts +9 -1
  18. package/dist/builtins/composer/composer.addon.js +130 -11
  19. package/dist/builtins/composer/composer.addon.mjs +130 -11
  20. package/dist/builtins/composer/composer.d.ts +8 -1
  21. package/dist/builtins/composer/composition-runtime.d.ts +2 -0
  22. package/dist/builtins/composer/composition-target.d.ts +6 -0
  23. package/dist/builtins/composer/owned-fields-target.d.ts +9 -0
  24. package/dist/builtins/console-logging/index.js +1 -1
  25. package/dist/builtins/console-logging/index.mjs +1 -1
  26. package/dist/builtins/core-blocks/block-supervisor.d.ts +19 -1
  27. package/dist/builtins/core-blocks/composition-api.d.ts +7 -1
  28. package/dist/builtins/core-blocks/core-blocks.addon.js +108 -10
  29. package/dist/builtins/core-blocks/core-blocks.addon.mjs +108 -10
  30. package/dist/builtins/device-history/device-history.addon.js +1 -1
  31. package/dist/builtins/device-history/device-history.addon.mjs +1 -1
  32. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  33. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  34. package/dist/builtins/hub-forwarder/index.js +1 -1
  35. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  36. package/dist/builtins/keychain/index.js +3 -3
  37. package/dist/builtins/keychain/index.mjs +3 -3
  38. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  39. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  40. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  41. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  42. package/dist/builtins/local-network/local-network.addon.js +2 -2
  43. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  44. package/dist/builtins/loki-logging/index.js +1 -1
  45. package/dist/builtins/loki-logging/index.mjs +1 -1
  46. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  47. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  48. package/dist/builtins/platform-probe/index.js +1 -1
  49. package/dist/builtins/platform-probe/index.mjs +1 -1
  50. package/dist/builtins/record-entries/index.js +1 -1
  51. package/dist/builtins/record-entries/index.mjs +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +1 -1
  55. package/dist/builtins/snapshot/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/index.js +1 -1
  59. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  60. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  61. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  62. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  63. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  64. package/dist/builtins/system-config/system-config.addon.js +8 -8
  65. package/dist/builtins/system-config/system-config.addon.mjs +8 -8
  66. package/dist/builtins/winston-logging/index.js +1 -1
  67. package/dist/builtins/winston-logging/index.mjs +1 -1
  68. package/dist/{composition-sources-CsBIIit3.mjs → composition-sources-B5nK7pHt.mjs} +1 -1
  69. package/dist/{composition-sources-CJv3W669.js → composition-sources-BNOP9mFp.js} +1 -1
  70. package/dist/{constants-C0hrXcEr.js → constants-D4VigrMM.js} +1 -1
  71. package/dist/{constants-Dy8CaDeT.mjs → constants-jbhoFV7w.mjs} +1 -1
  72. package/dist/{device-ca-CHdsePe5.mjs → device-ca-CvBZeyVX.mjs} +14 -13
  73. package/dist/{device-ca-Dg3rSU48.js → device-ca-DhyQeXsa.js} +14 -13
  74. package/dist/{dist-BugavoVR.mjs → dist-B_V9r61Z.mjs} +433 -22
  75. package/dist/{dist-Sn-HiZ4e.js → dist-CNU7cdFf.js} +433 -22
  76. package/dist/file-host-worker.js +1 -1
  77. package/dist/file-host-worker.mjs +1 -1
  78. package/dist/{hub-mdns-ciao-BDvqGIw4.mjs → hub-mdns-ciao-BuUg0gpI.mjs} +4 -4
  79. package/dist/{hub-mdns-ciao-DkncLbg9.js → hub-mdns-ciao-DFidf52M.js} +4 -4
  80. package/dist/index.js +7 -7
  81. package/dist/index.mjs +7 -7
  82. package/dist/{lib-8oZrELLm.js → lib-C3U1yluo.js} +1 -1
  83. package/dist/{lib-DK38ompL.mjs → lib-DFZblCIu.mjs} +1 -1
  84. package/dist/{restore-archive-gYKzaJ4Y.js → restore-archive-BtCheeWu.js} +1 -1
  85. package/dist/{restore-archive-CFBm30Gk.mjs → restore-archive-CDo4Zlmx.mjs} +1 -1
  86. package/dist/tls/cert-utc-time.d.ts +1 -0
  87. package/dist/{wal-maintenance-DyMUz_Hy.js → wal-maintenance-B7xBeIFi.js} +0 -0
  88. package/dist/{wal-maintenance-BnhkcEkI.mjs → wal-maintenance-DItlr0Yu.mjs} +0 -0
  89. package/dist/{x509.es-DK__ETIe.mjs → x509.es-BRiJgOvu.mjs} +24 -1
  90. package/dist/{x509.es-C4cOo48M.js → x509.es-WtnRNyYS.js} +29 -0
  91. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { $n as planComposition, $r as DeviceType, Gr as compositionItemFieldPath, Hr as composedDeviceRef, It as composerCapability, Jr as createStatefulExpressionMemory, Kr as compositionSliceKey, Lt as compositionFieldKey, N as DeclaredDevices, Oa as EventCategory, Ot as buildCapabilityLookup, Ur as composedDeviceStableId, Vr as cloneStatefulExpressionMemory, Wi as createEvent, Wr as compositionFeatureEnabled, Xr as toExpressionValue, Yr as disarmRoseMemory, a as BaseDevice, cn as evaluateCompositionField, d as COMPOSED_ITEM_FIXED_FIELDS, dn as expressionContainsBuiltinCall, ei as errMsg, qr as compositionSourceKey, r as ALL_CAPABILITY_DEFINITIONS, ti as BaseAddon, zr as COMPOSER_ADDON_ID } from "../../dist-BugavoVR.mjs";
2
- import { n as normalizedNativeOwner } from "../../composition-sources-CsBIIit3.mjs";
1
+ import { $n as planComposition, $r as DeviceType, Gr as compositionItemFieldPath, Hr as composedDeviceRef, It as composerCapability, Jr as createStatefulExpressionMemory, Kr as compositionSliceKey, Lt as compositionFieldKey, N as DeclaredDevices, Oa as EventCategory, Ot as buildCapabilityLookup, Ur as composedDeviceStableId, Vr as cloneStatefulExpressionMemory, Wi as createEvent, Wr as compositionFeatureEnabled, Xr as toExpressionValue, Yr as disarmRoseMemory, a as BaseDevice, cn as evaluateCompositionField, d as COMPOSED_ITEM_FIXED_FIELDS, dn as expressionContainsBuiltinCall, ei as errMsg, qr as compositionSourceKey, r as ALL_CAPABILITY_DEFINITIONS, ti as BaseAddon, zr as COMPOSER_ADDON_ID } from "../../dist-B_V9r61Z.mjs";
2
+ import { n as normalizedNativeOwner } from "../../composition-sources-B5nK7pHt.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/builtins/shared/device-state-changed.ts
5
5
  /** Structural narrowing helper for untrusted bus payloads. */
@@ -788,6 +788,41 @@ function wantedClaims(live, claimGate) {
788
788
  for (const owner of held) wanted.push(...claimGate.claimsOf(owner));
789
789
  return wanted;
790
790
  }
791
+ /**
792
+ * Claims an operator has already confirmed are no longer wanted: a deleted
793
+ * block (`retiredBlockIds`), or a cap an edit dropped. Released on this apply,
794
+ * not after a second one — a missing block in a single snapshot is still the
795
+ * two-apply gate (`wantedClaims`). A disabled block is not here: its runtime
796
+ * hands the fields back itself.
797
+ */
798
+ function confirmedReleaseKeys(retiredBlockIds, planned, previous, claimGate) {
799
+ const retired = new Set(retiredBlockIds);
800
+ const seen = /* @__PURE__ */ new Set();
801
+ const keys = [];
802
+ const push = (key) => {
803
+ const id = claimKey(key);
804
+ if (seen.has(id)) return;
805
+ seen.add(id);
806
+ keys.push(key);
807
+ };
808
+ for (const owner of retired) for (const key of claimGate.claimsOf(owner)) push(key);
809
+ const plannedById = new Map(planned.map((block) => [block.block.id, block]));
810
+ for (const [blockId, entry] of previous) {
811
+ if (retired.has(blockId)) continue;
812
+ const next = plannedById.get(blockId);
813
+ if (next === void 0 || !next.block.enabled) continue;
814
+ if (entry.planned.block.updatedAt === next.block.updatedAt) continue;
815
+ for (const key of claimGate.claimsOf(blockId)) if (!planStillWantsClaim(next, key)) push(key);
816
+ }
817
+ return keys;
818
+ }
819
+ /** True when this plan still holds `key` — including while the plan cannot say, so a bad read releases nothing. */
820
+ function planStillWantsClaim(planned, key) {
821
+ const target = planned.target;
822
+ if (target.kind !== "existing") return false;
823
+ if (target.waiting !== null || target.gone !== null || target.deviceId === null || planned.problems.length > 0) return true;
824
+ return target.deviceId === key.deviceId && target.features.some((feature) => feature.cap === key.capName);
825
+ }
791
826
  //#endregion
792
827
  //#region src/builtins/composer/composition-declarations.ts
793
828
  /**
@@ -1713,6 +1748,13 @@ var CompositionRuntime = class {
1713
1748
  this.stopped = true;
1714
1749
  this.deps.timer.disarm();
1715
1750
  }
1751
+ /** `stop`, then wait until the target has sent nothing further. */
1752
+ quiesce() {
1753
+ this.stop();
1754
+ const quiesce = this.deps.target.quiesce;
1755
+ if (quiesce === void 0) return Promise.resolve();
1756
+ return quiesce.call(this.deps.target);
1757
+ }
1716
1758
  state() {
1717
1759
  return {
1718
1760
  failure: this.failure,
@@ -2216,6 +2258,10 @@ var OwnedFieldsTarget = class {
2216
2258
  listeners = [];
2217
2259
  /** Per cap, bumped by every `withhold`: a claim that settles under another epoch claimed a stale field set and does not land (I-2). */
2218
2260
  epoch = /* @__PURE__ */ new Map();
2261
+ /** Set by `quiesce`: nothing further is sent. */
2262
+ closed = false;
2263
+ idle = Promise.resolve();
2264
+ markIdle = null;
2219
2265
  constructor(deps) {
2220
2266
  this.deps = deps;
2221
2267
  this.id = deps.deviceId;
@@ -2240,6 +2286,27 @@ var OwnedFieldsTarget = class {
2240
2286
  }
2241
2287
  /** `device-status` belongs to the native device; availability lives in the block report only. */
2242
2288
  setAvailable(_available) {}
2289
+ /**
2290
+ * The block has stopped. Writes still queued are dropped; the call in flight
2291
+ * is allowed to finish, and then nothing else is sent.
2292
+ */
2293
+ quiesce() {
2294
+ this.closed = true;
2295
+ const dropped = [...this.pending.keys()];
2296
+ this.pending = /* @__PURE__ */ new Map();
2297
+ for (const cap of dropped) this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
2298
+ tags: { deviceId: this.deps.deviceId },
2299
+ meta: {
2300
+ cap,
2301
+ owner: this.deps.owner
2302
+ }
2303
+ });
2304
+ if (this.inFlight.size === 0) return Promise.resolve();
2305
+ if (this.markIdle === null) this.idle = new Promise((resolve) => {
2306
+ this.markIdle = resolve;
2307
+ });
2308
+ return this.idle;
2309
+ }
2243
2310
  onAsyncFailure(listener) {
2244
2311
  this.listeners = [...this.listeners, listener];
2245
2312
  }
@@ -2255,6 +2322,17 @@ var OwnedFieldsTarget = class {
2255
2322
  return this.deps.fieldsOf(cap).filter((f) => held === void 0 || !held.has(f));
2256
2323
  }
2257
2324
  enqueue(cap, op) {
2325
+ if (this.closed) {
2326
+ this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
2327
+ tags: { deviceId: this.deps.deviceId },
2328
+ meta: {
2329
+ cap,
2330
+ owner: this.deps.owner,
2331
+ op: op.kind
2332
+ }
2333
+ });
2334
+ return;
2335
+ }
2258
2336
  if (!this.inFlight.has(cap)) {
2259
2337
  this.send(cap, op);
2260
2338
  return;
@@ -2277,6 +2355,11 @@ var OwnedFieldsTarget = class {
2277
2355
  }
2278
2356
  settled(cap) {
2279
2357
  this.inFlight = new Set([...this.inFlight].filter((c) => c !== cap));
2358
+ if (this.closed) {
2359
+ this.pending = /* @__PURE__ */ new Map();
2360
+ if (this.inFlight.size === 0) this.markIdle?.();
2361
+ return;
2362
+ }
2280
2363
  const next = this.pending.get(cap);
2281
2364
  if (next === void 0) return;
2282
2365
  this.pending = new Map([...this.pending].filter(([c]) => c !== cap));
@@ -2693,7 +2776,11 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
2693
2776
  * filter in `composer.addon.ts` stays load-bearing. Its fields are CLAIMED on
2694
2777
  * the hub under the block id (`OwnedFieldsTarget`), and released through the
2695
2778
  * `ClaimGate` — two applies must agree, seeded at boot from the claims index —
2696
- * when the block is gone, disabled, or lost the feature. Its own replaced
2779
+ * when the block merely vanished from one snapshot. An operator delete
2780
+ * (`retiredBlockIds`) or an edit that drops a cap releases those claims on
2781
+ * THIS apply, after the old runtime has quiesced and before any replacement
2782
+ * claims; a release that does not land is logged with `tags: { deviceId }` and
2783
+ * returned on the apply. Its own replaced
2697
2784
  * fields are read in the NATIVE view (rule 6). Its target row gone on two
2698
2785
  * agreeing index reads fails the block by name and ungrafts what it added —
2699
2786
  * whether the loss was seen on `device.unregistered` or on a later plan; its
@@ -2841,12 +2928,16 @@ var Composer = class {
2841
2928
  };
2842
2929
  });
2843
2930
  await this.composed.reconcile(gate.integrationId, declarations, planned);
2931
+ const releasing = confirmedReleaseKeys(input.retiredBlockIds ?? [], planned, this.live, this.claimGate);
2844
2932
  const rebuilt = await this.rebuildRuntimes(planned, declarations, restart, rebuildErrors);
2933
+ await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
2934
+ const unreleasedClaims = await this.releaseConfirmed(releasing);
2845
2935
  await this.syncSources(rebuilt);
2846
2936
  await this.reconcileClaims();
2847
2937
  return {
2848
2938
  blocks: planned.map((p) => this.verdict(p.block.id)),
2849
- acted: true
2939
+ acted: true,
2940
+ unreleasedClaims
2850
2941
  };
2851
2942
  }
2852
2943
  plan(block, restart, claimsWait) {
@@ -2867,7 +2958,13 @@ var Composer = class {
2867
2958
  async rebuildRuntimes(planned, declarations, restart, rebuildErrors) {
2868
2959
  const reset = /* @__PURE__ */ new Set();
2869
2960
  const fresh = /* @__PURE__ */ new Set();
2961
+ const stopped = [];
2870
2962
  const next = /* @__PURE__ */ new Map();
2963
+ const halt = (runtime) => {
2964
+ if (runtime === null) return;
2965
+ runtime.stop();
2966
+ stopped.push(runtime);
2967
+ };
2871
2968
  for (const p of planned) {
2872
2969
  const previous = this.live.get(p.block.id);
2873
2970
  const lost = lostTarget(p, previous?.planned);
@@ -2938,7 +3035,7 @@ var Composer = class {
2938
3035
  enabled: p.block.enabled,
2939
3036
  settled: p.problems.length === 0
2940
3037
  });
2941
- previous?.runtime?.stop();
3038
+ halt(previous?.runtime ?? null);
2942
3039
  if (runtime !== null) fresh.add(runtime);
2943
3040
  next.set(p.block.id, {
2944
3041
  planned: p,
@@ -2953,12 +3050,15 @@ var Composer = class {
2953
3050
  for (const [blockId, entry] of this.live) {
2954
3051
  if (next.has(blockId)) continue;
2955
3052
  if (held.has(composedDeviceStableId(blockId))) next.set(blockId, entry);
2956
- else entry.runtime?.stop();
3053
+ else halt(entry.runtime);
2957
3054
  }
2958
3055
  this.live = next;
2959
3056
  const liveRuntimes = new Set([...next.values()].map((e) => e.runtime));
2960
3057
  this.unsynced = new Set([...[...this.unsynced].filter((r) => liveRuntimes.has(r)), ...fresh]);
2961
- return { reset };
3058
+ return {
3059
+ reset,
3060
+ stopped
3061
+ };
2962
3062
  }
2963
3063
  ungraftGone(blockId, target, deviceId) {
2964
3064
  this.grafts.gone(deviceId);
@@ -3042,21 +3142,40 @@ var Composer = class {
3042
3142
  if (!this.claimSeed.done) return;
3043
3143
  for (const key of this.claimGate.filter(wantedClaims(this.live, this.claimGate))) await this.release(key);
3044
3144
  }
3145
+ /** Confirmed releases, awaited. A failure is the string the hub gave; the gate retries it later. */
3146
+ async releaseConfirmed(keys) {
3147
+ const unreleased = [];
3148
+ for (const key of keys) {
3149
+ const error = await this.release(key);
3150
+ if (error === null) continue;
3151
+ unreleased.push({
3152
+ deviceId: key.deviceId,
3153
+ capName: key.capName,
3154
+ owner: key.owner,
3155
+ error
3156
+ });
3157
+ }
3158
+ return unreleased;
3159
+ }
3160
+ /** Null when the hub confirmed the release (or held nothing of ours). */
3045
3161
  async release(key) {
3046
3162
  let outcome;
3047
3163
  try {
3048
3164
  outcome = await this.deps.claims.releaseClaim(key);
3049
3165
  } catch (err) {
3050
- this.claimGate.releaseFailed(key, errMsg(err));
3051
- return;
3166
+ const error = errMsg(err);
3167
+ this.claimGate.releaseFailed(key, error);
3168
+ return error;
3052
3169
  }
3053
3170
  if (!outcome.ok && !RELEASE_SETTLED_CODES.has(outcome.code)) {
3054
- this.claimGate.releaseFailed(key, `${outcome.code}: ${outcome.message}`);
3055
- return;
3171
+ const error = `${outcome.code}: ${outcome.message}`;
3172
+ this.claimGate.releaseFailed(key, error);
3173
+ return error;
3056
3174
  }
3057
3175
  this.claimGate.released(key, outcome.ok ? "released" : outcome.code);
3058
3176
  await this.grafts.released(key.deviceId, key.capName);
3059
3177
  await this.onClaimEvent(key.deviceId, key.capName, key.owner, "free");
3178
+ return null;
3060
3179
  }
3061
3180
  /**
3062
3181
  * Rule 5: a customization whose target row is gone — the tracker's two
@@ -19,7 +19,11 @@
19
19
  * filter in `composer.addon.ts` stays load-bearing. Its fields are CLAIMED on
20
20
  * the hub under the block id (`OwnedFieldsTarget`), and released through the
21
21
  * `ClaimGate` — two applies must agree, seeded at boot from the claims index —
22
- * when the block is gone, disabled, or lost the feature. Its own replaced
22
+ * when the block merely vanished from one snapshot. An operator delete
23
+ * (`retiredBlockIds`) or an edit that drops a cap releases those claims on
24
+ * THIS apply, after the old runtime has quiesced and before any replacement
25
+ * claims; a release that does not land is logged with `tags: { deviceId }` and
26
+ * returned on the apply. Its own replaced
23
27
  * fields are read in the NATIVE view (rule 6). Its target row gone on two
24
28
  * agreeing index reads fails the block by name and ungrafts what it added —
25
29
  * whether the loss was seen on `device.unregistered` or on a later plan; its
@@ -139,6 +143,9 @@ export declare class Composer {
139
143
  private syncTracker;
140
144
  /** Rule 3: release what no live customization wants, once two applies agree (`wantedClaims`). */
141
145
  private reconcileClaims;
146
+ /** Confirmed releases, awaited. A failure is the string the hub gave; the gate retries it later. */
147
+ private releaseConfirmed;
148
+ /** Null when the hub confirmed the release (or held nothing of ours). */
142
149
  private release;
143
150
  /**
144
151
  * Rule 5: a customization whose target row is gone — the tracker's two
@@ -115,6 +115,8 @@ export declare class CompositionRuntime {
115
115
  disable(reason: string): void;
116
116
  /** Terminal: no write, no evaluation and no timer after this. */
117
117
  stop(): void;
118
+ /** `stop`, then wait until the target has sent nothing further. */
119
+ quiesce(): Promise<void>;
118
120
  state(): CompositionRuntimeState;
119
121
  /**
120
122
  * The target's enqueued write of `cap` did not land: the cap is write-pending
@@ -44,6 +44,12 @@ export interface ComposedWriteTarget {
44
44
  * existing targets.
45
45
  */
46
46
  withhold?(cap: string, fields: readonly string[]): WriteOutcome;
47
+ /**
48
+ * Drop writes not yet sent and resolve once the one in flight has settled.
49
+ * A stopped block calls this before its claims are released, so that claim
50
+ * cannot land afterwards.
51
+ */
52
+ quiesce?(): Promise<void>;
47
53
  }
48
54
  export interface DeadlineTimer {
49
55
  arm(at: number, fire: () => void): void;
@@ -64,11 +64,20 @@ export declare class OwnedFieldsTarget implements ComposedWriteTarget {
64
64
  private listeners;
65
65
  /** Per cap, bumped by every `withhold`: a claim that settles under another epoch claimed a stale field set and does not land (I-2). */
66
66
  private epoch;
67
+ /** Set by `quiesce`: nothing further is sent. */
68
+ private closed;
69
+ private idle;
70
+ private markIdle;
67
71
  constructor(deps: OwnedFieldsTargetDeps);
68
72
  seed(cap: string, slice: SlicePatch): WriteOutcome;
69
73
  patch(cap: string, patch: SlicePatch): WriteOutcome;
70
74
  /** `device-status` belongs to the native device; availability lives in the block report only. */
71
75
  setAvailable(_available: boolean): void;
76
+ /**
77
+ * The block has stopped. Writes still queued are dropped; the call in flight
78
+ * is allowed to finish, and then nothing else is sent.
79
+ */
80
+ quiesce(): Promise<void>;
72
81
  onAsyncFailure(listener: AsyncFailureListener): void;
73
82
  withhold(cap: string, fields: readonly string[]): WriteOutcome;
74
83
  private claimableFields;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-Sn-HiZ4e.js");
6
+ const require_dist = require("../../dist-CNU7cdFf.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Ln as logDestinationCapability, ti as BaseAddon } from "../../dist-BugavoVR.mjs";
1
+ import { Ln as logDestinationCapability, ti as BaseAddon } from "../../dist-B_V9r61Z.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -19,7 +19,7 @@
19
19
  * status the operator can see, with the reason attached. A block that is
20
20
  * silently absent is the failure mode this whole feature has to avoid.
21
21
  */
22
- import { type ComposerApplyInput, type ComposerApplyResult, type ComposerUnreadableBlock, type CoreBlock, type IScopedLogger } from "@camstack/types";
22
+ import { type ComposerApplyInput, type ComposerApplyResult, type ComposerUnreadableBlock, type CoreBlock, type IScopedLogger, type UnreleasedClaim } from "@camstack/types";
23
23
  /** The runner lifecycle states `$process.list` reports. `stopping`/`stopped`
24
24
  * are transient states of a runner on its way out and are treated as absent. */
25
25
  export type BlockRunnerState = "starting" | "running" | "stopping" | "stopped" | "crashed" | "failed";
@@ -107,6 +107,10 @@ export declare class BlockSupervisor {
107
107
  private emptyAppliesOwed;
108
108
  /** Consecutive applies that could not reach the composer: warned once per run. */
109
109
  private failedApplies;
110
+ /** Composition ids the operator deleted. Passed until their claims are released. */
111
+ private retiredBlockIds;
112
+ /** What the last composition apply said about claim releases. */
113
+ private releaseReport;
110
114
  constructor(deps: BlockSupervisorDeps);
111
115
  /** Start the reconcile timer. The first pass runs immediately. */
112
116
  start(intervalMs: number): void;
@@ -128,6 +132,17 @@ export declare class BlockSupervisor {
128
132
  * timer tick could stop a runner the tick had just decided was healthy and
129
133
  * then never re-launch it.
130
134
  */
135
+ /**
136
+ * The block row is already gone. The next apply names it retired so its
137
+ * claims are released before this resolves. A release that does not land is
138
+ * returned; the id stays retired so the following pass tries again.
139
+ */
140
+ retireComposition(blockId: string): Promise<readonly UnreleasedClaim[]>;
141
+ /**
142
+ * One apply, awaited. Used when an edit replaced a block's caps: the
143
+ * composer releases what the new plan dropped before this resolves.
144
+ */
145
+ convergeForClaims(): Promise<readonly UnreleasedClaim[]>;
131
146
  restart(blockId: string): Promise<void>;
132
147
  private runRestart;
133
148
  private runPass;
@@ -139,6 +154,9 @@ export declare class BlockSupervisor {
139
154
  * pass of this same converge is the retry — no timer of its own (D3).
140
155
  */
141
156
  private convergeCompositions;
157
+ private claimsOf;
158
+ /** Remember which retired ids still hold a claim. An apply that did not act keeps every id. */
159
+ private noteRelease;
142
160
  /** Compile, write the package, spawn. Each failure is reported ON the block
143
161
  * — the author asked for this to run, so they are owed the reason it does
144
162
  * not. */
@@ -12,7 +12,7 @@
12
12
  * server derives it (`customizationBlockName`), refuses one the client sends,
13
13
  * and never lets it move to another device. One per device, by block id (S2).
14
14
  */
15
- import { type CapabilityDefinition, type CapabilityLookup, type CompositionBlockPatch, type CompositionBlockState, type CompositionSourceRef, type CoreBlock, type ICoreBlocksProvider, type IScopedLogger } from "@camstack/types";
15
+ import { type CapabilityDefinition, type CapabilityLookup, type CompositionBlockPatch, type CompositionBlockState, type CompositionSourceRef, type CoreBlock, type ICoreBlocksProvider, type IScopedLogger, type UnreleasedClaim } from "@camstack/types";
16
16
  import type { NamedCompositionInput } from "./core-block-store.js";
17
17
  import type { CompositionDescriptions } from "./composition-sources.js";
18
18
  export interface CompositionStoreView {
@@ -30,6 +30,12 @@ export interface CompositionApiDeps {
30
30
  readonly lookupCap: CapabilityLookup;
31
31
  readonly composerStates: (blockIds: readonly string[]) => Promise<readonly CompositionBlockState[]>;
32
32
  readonly convergeSoon: () => void;
33
+ /**
34
+ * One composer apply, awaited. An edit that replaces caps does not return
35
+ * until the caps it dropped have been released, or the release has failed
36
+ * and is in the list.
37
+ */
38
+ readonly convergeForClaims: () => Promise<readonly UnreleasedClaim[]>;
33
39
  readonly logger: IScopedLogger;
34
40
  /** Every capability definition — the listing is derived from them, never from the registry's live state. */
35
41
  readonly capabilityDefinitions: readonly CapabilityDefinition[];
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-Sn-HiZ4e.js");
3
+ const require_dist = require("../../dist-CNU7cdFf.js");
4
4
  const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
5
- const require_composition_sources = require("../../composition-sources-CJv3W669.js");
5
+ const require_composition_sources = require("../../composition-sources-BNOP9mFp.js");
6
6
  const require_prefix_range = require("../../prefix-range-D_KueXbQ.js");
7
7
  const require_prefix_walk = require("../../prefix-walk-Dv6MLZlM.js");
8
8
  let zod = require("zod");
@@ -246,6 +246,13 @@ var BlockSupervisor = class {
246
246
  emptyAppliesOwed = 2;
247
247
  /** Consecutive applies that could not reach the composer: warned once per run. */
248
248
  failedApplies = 0;
249
+ /** Composition ids the operator deleted. Passed until their claims are released. */
250
+ retiredBlockIds = /* @__PURE__ */ new Set();
251
+ /** What the last composition apply said about claim releases. */
252
+ releaseReport = {
253
+ error: null,
254
+ unreleased: []
255
+ };
249
256
  constructor(deps) {
250
257
  this.deps = deps;
251
258
  }
@@ -281,6 +288,26 @@ var BlockSupervisor = class {
281
288
  * timer tick could stop a runner the tick had just decided was healthy and
282
289
  * then never re-launch it.
283
290
  */
291
+ /**
292
+ * The block row is already gone. The next apply names it retired so its
293
+ * claims are released before this resolves. A release that does not land is
294
+ * returned; the id stays retired so the following pass tries again.
295
+ */
296
+ retireComposition(blockId) {
297
+ this.retiredBlockIds = new Set([...this.retiredBlockIds, blockId]);
298
+ return this.converge().then(() => this.claimsOf(blockId));
299
+ }
300
+ /**
301
+ * One apply, awaited. Used when an edit replaced a block's caps: the
302
+ * composer releases what the new plan dropped before this resolves.
303
+ */
304
+ convergeForClaims() {
305
+ if (this.deps.composer === null) return Promise.reject(/* @__PURE__ */ new Error("core-blocks: no composer is available — a replaced claim cannot be released"));
306
+ return this.converge().then(() => {
307
+ if (this.releaseReport.error !== null) throw new Error(`core-blocks: a replaced claim was not released — ${this.releaseReport.error}`);
308
+ return this.releaseReport.unreleased;
309
+ });
310
+ }
284
311
  restart(blockId) {
285
312
  const block = this.deps.store.get(blockId);
286
313
  if (block !== null && require_dist.coreBlockSourceKind(block) === "composition") {
@@ -369,9 +396,13 @@ var BlockSupervisor = class {
369
396
  });
370
397
  const unreadable = this.deps.store.unreadableCompositions();
371
398
  const nothingToSay = compositions.length === 0 && unreadable.length === 0;
372
- if (nothingToSay && this.emptyAppliesOwed === 0) return;
399
+ if (nothingToSay && this.emptyAppliesOwed === 0 && this.retiredBlockIds.size === 0) return;
373
400
  const composer = this.deps.composer;
374
401
  if (composer === null) {
402
+ if (this.retiredBlockIds.size > 0) this.releaseReport = {
403
+ error: "no composer is available on this node — claims cannot be released",
404
+ unreleased: []
405
+ };
375
406
  for (const { block } of compositions) await this.report(block.id, "failed", "no composer is available on this node — a composition cannot run here");
376
407
  return;
377
408
  }
@@ -394,7 +425,8 @@ var BlockSupervisor = class {
394
425
  composition
395
426
  })),
396
427
  restartBlockIds: [...restart],
397
- unreadableBlocks: [...unreadable]
428
+ unreadableBlocks: [...unreadable],
429
+ retiredBlockIds: [...this.retiredBlockIds]
398
430
  });
399
431
  if (this.failedApplies > 0) {
400
432
  this.deps.logger.info("the composer answered again", { meta: { failedApplies: this.failedApplies } });
@@ -402,6 +434,7 @@ var BlockSupervisor = class {
402
434
  }
403
435
  this.pendingCompositionRestarts = new Set([...this.pendingCompositionRestarts].filter((id) => !restart.has(id)));
404
436
  this.emptyAppliesOwed = !nothingToSay || !result.acted ? 2 : Math.max(0, this.emptyAppliesOwed - 1);
437
+ this.noteRelease(result);
405
438
  const verdicts = new Map(result.blocks.map((v) => [v.blockId, v]));
406
439
  for (const { block } of compositions) {
407
440
  const verdict = verdicts.get(block.id);
@@ -409,13 +442,39 @@ var BlockSupervisor = class {
409
442
  else await this.report(block.id, verdict.status, verdict.reason);
410
443
  }
411
444
  } catch (err) {
445
+ const error = require_dist.errMsg(err);
446
+ this.releaseReport = {
447
+ error,
448
+ unreleased: []
449
+ };
412
450
  this.failedApplies += 1;
413
451
  if (this.failedApplies === 1) this.deps.logger.warn("the composer could not be reached — compositions keep their last status", { meta: {
414
- error: require_dist.errMsg(err),
452
+ error,
415
453
  compositions: compositions.length
416
454
  } });
417
- for (const { block } of compositions) if (block.status === "stopped" && block.enabled) await this.report(block.id, "starting", `the composer is not reachable yet: ${require_dist.errMsg(err)}`);
455
+ for (const { block } of compositions) if (block.status === "stopped" && block.enabled) await this.report(block.id, "starting", `the composer is not reachable yet: ${error}`);
456
+ }
457
+ }
458
+ claimsOf(blockId) {
459
+ if (this.releaseReport.error !== null) throw new Error(`core-blocks: claims of "${blockId}" were not released — ${this.releaseReport.error}`);
460
+ return this.releaseReport.unreleased.filter((claim) => claim.owner === blockId);
461
+ }
462
+ /** Remember which retired ids still hold a claim. An apply that did not act keeps every id. */
463
+ noteRelease(result) {
464
+ const unreleased = result.unreleasedClaims ?? [];
465
+ if (!result.acted) {
466
+ this.releaseReport = {
467
+ error: "the composer did not apply — claims stay until it does",
468
+ unreleased
469
+ };
470
+ return;
418
471
  }
472
+ this.releaseReport = {
473
+ error: null,
474
+ unreleased
475
+ };
476
+ const failed = new Set(unreleased.map((claim) => claim.owner));
477
+ this.retiredBlockIds = new Set([...this.retiredBlockIds].filter((id) => failed.has(id)));
419
478
  }
420
479
  /** Compile, write the package, spawn. Each failure is reported ON the block
421
480
  * — the author asked for this to run, so they are owed the reason it does
@@ -2082,7 +2141,21 @@ function buildCompositionApi(deps) {
2082
2141
  blockId,
2083
2142
  by: caller.userId
2084
2143
  } });
2085
- deps.convergeSoon();
2144
+ if (block.composition !== void 0) {
2145
+ const unreleased = (await deps.convergeForClaims()).filter((claim) => claim.owner === blockId);
2146
+ if (unreleased.length > 0) {
2147
+ for (const claim of unreleased) deps.logger.error("composition saved but a replaced claim was not released", {
2148
+ tags: { deviceId: claim.deviceId },
2149
+ meta: {
2150
+ blockId,
2151
+ cap: claim.capName,
2152
+ error: claim.error
2153
+ }
2154
+ });
2155
+ const detail = unreleased.map((claim) => `${claim.capName} on device ${claim.deviceId}: ${claim.error}`).join("; ");
2156
+ throw new Error(`core-blocks: "${blockId}" was saved but a replaced claim was not released — ${detail}`);
2157
+ }
2158
+ } else deps.convergeSoon();
2086
2159
  return { block: updated };
2087
2160
  }),
2088
2161
  getComposition: async ({ blockId }) => {
@@ -2458,9 +2531,30 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
2458
2531
  return { block: updated };
2459
2532
  },
2460
2533
  delete: async ({ blockId }) => {
2534
+ const existing = this.requireStore().get(blockId);
2535
+ const composition = existing !== null && require_dist.coreBlockComposition(existing) !== null;
2461
2536
  await this.requireStore().delete(blockId);
2462
2537
  await this.requirePrefs().delete(blockId);
2463
- this.convergeSoon();
2538
+ if (composition && this.supervisor === null) {
2539
+ this.ctx.logger.error("composition deleted but no supervisor can release its claims", { meta: { blockId } });
2540
+ throw new Error(`core-blocks: "${blockId}" was deleted but no supervisor runs here — its claims were not released`);
2541
+ }
2542
+ if (composition && this.supervisor !== null) {
2543
+ const unreleased = await this.supervisor.retireComposition(blockId);
2544
+ if (unreleased.length > 0) {
2545
+ for (const claim of unreleased) this.ctx.logger.error("composition deleted but its claim was not released", {
2546
+ tags: { deviceId: claim.deviceId },
2547
+ meta: {
2548
+ blockId,
2549
+ cap: claim.capName,
2550
+ owner: claim.owner,
2551
+ error: claim.error
2552
+ }
2553
+ });
2554
+ const detail = unreleased.map((claim) => `${claim.capName} on device ${claim.deviceId}: ${claim.error}`).join("; ");
2555
+ throw new Error(`core-blocks: "${blockId}" was deleted but its claims were not released — ${detail}`);
2556
+ }
2557
+ } else this.convergeSoon();
2464
2558
  return { success: true };
2465
2559
  },
2466
2560
  setEnabled: async ({ blockId, enabled }) => {
@@ -2469,9 +2563,9 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
2469
2563
  return { block: updated };
2470
2564
  },
2471
2565
  /**
2472
- * Awaited, unlike every other write here.
2566
+ * Awaited, like a composition delete.
2473
2567
  *
2474
- * `create`/`update` deliberately do not wait — the write is already
2568
+ * `create`/`update` of a code block deliberately do not wait — the write is already
2475
2569
  * durable and converging forks a process. A restart has no durable half:
2476
2570
  * the whole thing IS the converge, so returning before it ran would hand
2477
2571
  * the operator the old status and read as "nothing happened".
@@ -2552,6 +2646,10 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
2552
2646
  lookupCap: require_dist.buildCapabilityLookup(require_dist.ALL_CAPABILITY_DEFINITIONS),
2553
2647
  composerStates: async (blockIds) => (await requireApi().composer.getStates.query({ blockIds: [...blockIds] })).states,
2554
2648
  convergeSoon: () => this.convergeSoon(),
2649
+ convergeForClaims: () => {
2650
+ if (this.supervisor === null) return Promise.reject(/* @__PURE__ */ new Error("core-blocks: no block supervisor runs on this node — claims cannot be released"));
2651
+ return this.supervisor.convergeForClaims();
2652
+ },
2555
2653
  logger: this.ctx.logger.child("compositions"),
2556
2654
  capabilityDefinitions: require_dist.ALL_CAPABILITY_DEFINITIONS
2557
2655
  });