@camstack/types 1.2.59 → 1.2.60

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.
@@ -90,24 +90,28 @@ export declare const snapshotCapability: {
90
90
  *
91
91
  * `getSnapshotOverview` is cache-only by contract: it answers from whatever
92
92
  * the wrapper happens to hold and never captures. Under D93 the client
93
- * versions its image URL on that answer, and an image REQUEST is what enrols
94
- * a camera in the keep-warm loop. Both of those are satisfiable by the
95
- * client's own image cache — `expo-image` is URL-keyed and never revalidates
96
- * — so a URL painted in a previous session comes off disk with no network,
97
- * no enrolment, and nothing warming. Measured on the live hub: reopening
98
- * after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
99
- * HTTP requests, and the fleet only recovered because a later poll happened
100
- * to observe a different identity.
93
+ * versions its image URL on that answer, and an image REQUEST was the only
94
+ * demand signal. Both of those are satisfiable by the client's own image
95
+ * cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
96
+ * in a previous session comes off disk with no network, no demand, and no
97
+ * capture. Measured on the live hub: reopening after two minutes idle
98
+ * painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
99
+ * fleet only recovered because a later poll happened to observe a different
100
+ * identity.
101
101
  *
102
102
  * ## The two properties that fix it
103
103
  *
104
104
  * **It is an RPC, so no client cache can answer it.** The demand signal
105
- * always reaches the wrapper. This method therefore MAY create keep-warm
106
- * subscriptions, where `getSnapshotOverview` must never (D93) — the
107
- * distinction is not "one is newer" but that the overview poll is app-wide
108
- * (a creating overview would warm every camera on the install) while this is
109
- * called by a rendered surface naming the tiles it is actually painting, at
110
- * the width it is painting them.
105
+ * always reaches the wrapper. This method therefore CAPTURES, where
106
+ * `getSnapshotOverview` must never (D93) — the distinction is not "one is
107
+ * newer" but that the overview poll is app-wide (a capturing overview would
108
+ * dial every camera on the install) while this is called by a rendered
109
+ * surface naming the tiles it is actually painting, at the width it is
110
+ * painting them.
111
+ *
112
+ * Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
113
+ * server-side keep-warm loop was removed (operator directive — on-demand,
114
+ * always), so a camera nobody is looking at costs nothing at all.
111
115
  *
112
116
  * **It waits, briefly and boundedly, for the capture it triggered.** The
113
117
  * returned `capturedAt` is the frame the link will serve, not the frame the
@@ -193,6 +193,18 @@ export declare class DeclaredDevices {
193
193
  */
194
194
  private ensureIntegration;
195
195
  private readIndex;
196
+ /**
197
+ * Devices this kernel already has CONSTRUCTED, by stableId.
198
+ *
199
+ * Distinct from {@link readIndex}, and the distinction is the bug: the index
200
+ * is persisted rows, this is live objects. A row without an object must be
201
+ * adopted; an object must be left exactly as it is.
202
+ *
203
+ * Failure is non-fatal and deliberately so — an empty map degrades to the
204
+ * previous behaviour (attempt the adopt) rather than skipping a device that
205
+ * genuinely needs bringing up.
206
+ */
207
+ private readLiveByStableId;
196
208
  /**
197
209
  * One declaration: adopt what exists, create what does not.
198
210
  *
package/dist/index.js CHANGED
@@ -17815,24 +17815,28 @@ var snapshotCapability = {
17815
17815
  *
17816
17816
  * `getSnapshotOverview` is cache-only by contract: it answers from whatever
17817
17817
  * the wrapper happens to hold and never captures. Under D93 the client
17818
- * versions its image URL on that answer, and an image REQUEST is what enrols
17819
- * a camera in the keep-warm loop. Both of those are satisfiable by the
17820
- * client's own image cache — `expo-image` is URL-keyed and never revalidates
17821
- * — so a URL painted in a previous session comes off disk with no network,
17822
- * no enrolment, and nothing warming. Measured on the live hub: reopening
17823
- * after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
17824
- * HTTP requests, and the fleet only recovered because a later poll happened
17825
- * to observe a different identity.
17818
+ * versions its image URL on that answer, and an image REQUEST was the only
17819
+ * demand signal. Both of those are satisfiable by the client's own image
17820
+ * cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
17821
+ * in a previous session comes off disk with no network, no demand, and no
17822
+ * capture. Measured on the live hub: reopening after two minutes idle
17823
+ * painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
17824
+ * fleet only recovered because a later poll happened to observe a different
17825
+ * identity.
17826
17826
  *
17827
17827
  * ## The two properties that fix it
17828
17828
  *
17829
17829
  * **It is an RPC, so no client cache can answer it.** The demand signal
17830
- * always reaches the wrapper. This method therefore MAY create keep-warm
17831
- * subscriptions, where `getSnapshotOverview` must never (D93) — the
17832
- * distinction is not "one is newer" but that the overview poll is app-wide
17833
- * (a creating overview would warm every camera on the install) while this is
17834
- * called by a rendered surface naming the tiles it is actually painting, at
17835
- * the width it is painting them.
17830
+ * always reaches the wrapper. This method therefore CAPTURES, where
17831
+ * `getSnapshotOverview` must never (D93) — the distinction is not "one is
17832
+ * newer" but that the overview poll is app-wide (a capturing overview would
17833
+ * dial every camera on the install) while this is called by a rendered
17834
+ * surface naming the tiles it is actually painting, at the width it is
17835
+ * painting them.
17836
+ *
17837
+ * Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
17838
+ * server-side keep-warm loop was removed (operator directive — on-demand,
17839
+ * always), so a camera nobody is looking at costs nothing at all.
17836
17840
  *
17837
17841
  * **It waits, briefly and boundedly, for the capture it triggered.** The
17838
17842
  * returned `capturedAt` is the frame the link will serve, not the frame the
@@ -29148,9 +29152,10 @@ var DeclaredDevices = class {
29148
29152
  }
29149
29153
  const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
29150
29154
  const index = await this.readIndex();
29155
+ const live = await this.readLiveByStableId();
29151
29156
  const outcomes = [];
29152
29157
  for (const declaration of spec.devices) {
29153
- const outcome = await this.applyDeclaration(declaration, integrationId, index);
29158
+ const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
29154
29159
  if (outcome !== null) outcomes.push(outcome);
29155
29160
  }
29156
29161
  return {
@@ -29196,6 +29201,26 @@ var DeclaredDevices = class {
29196
29201
  return new Map(rows.map((row) => [row.stableId, row]));
29197
29202
  }
29198
29203
  /**
29204
+ * Devices this kernel already has CONSTRUCTED, by stableId.
29205
+ *
29206
+ * Distinct from {@link readIndex}, and the distinction is the bug: the index
29207
+ * is persisted rows, this is live objects. A row without an object must be
29208
+ * adopted; an object must be left exactly as it is.
29209
+ *
29210
+ * Failure is non-fatal and deliberately so — an empty map degrades to the
29211
+ * previous behaviour (attempt the adopt) rather than skipping a device that
29212
+ * genuinely needs bringing up.
29213
+ */
29214
+ async readLiveByStableId() {
29215
+ try {
29216
+ const devices = await this.ports.devices.getAll();
29217
+ return new Map(devices.map((device) => [device.stableId, device]));
29218
+ } catch (err) {
29219
+ this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
29220
+ return /* @__PURE__ */ new Map();
29221
+ }
29222
+ }
29223
+ /**
29199
29224
  * One declaration: adopt what exists, create what does not.
29200
29225
  *
29201
29226
  * The create branch is the destructive one — it seeds `initialMeta`, and
@@ -29204,8 +29229,15 @@ var DeclaredDevices = class {
29204
29229
  * the declared name over the operator's rename. D49: that branch needs a
29205
29230
  * second read to agree.
29206
29231
  */
29207
- async applyDeclaration(declaration, integrationId, index) {
29232
+ async applyDeclaration(declaration, integrationId, index, live) {
29208
29233
  try {
29234
+ const alreadyLive = live.get(declaration.stableId);
29235
+ if (alreadyLive !== void 0) return {
29236
+ stableId: declaration.stableId,
29237
+ deviceId: alreadyLive.id,
29238
+ device: alreadyLive,
29239
+ created: false
29240
+ };
29209
29241
  let existing = index.get(declaration.stableId);
29210
29242
  if (existing === void 0) {
29211
29243
  existing = (await this.readIndex()).get(declaration.stableId);
package/dist/index.mjs CHANGED
@@ -17814,24 +17814,28 @@ var snapshotCapability = {
17814
17814
  *
17815
17815
  * `getSnapshotOverview` is cache-only by contract: it answers from whatever
17816
17816
  * the wrapper happens to hold and never captures. Under D93 the client
17817
- * versions its image URL on that answer, and an image REQUEST is what enrols
17818
- * a camera in the keep-warm loop. Both of those are satisfiable by the
17819
- * client's own image cache — `expo-image` is URL-keyed and never revalidates
17820
- * — so a URL painted in a previous session comes off disk with no network,
17821
- * no enrolment, and nothing warming. Measured on the live hub: reopening
17822
- * after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
17823
- * HTTP requests, and the fleet only recovered because a later poll happened
17824
- * to observe a different identity.
17817
+ * versions its image URL on that answer, and an image REQUEST was the only
17818
+ * demand signal. Both of those are satisfiable by the client's own image
17819
+ * cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
17820
+ * in a previous session comes off disk with no network, no demand, and no
17821
+ * capture. Measured on the live hub: reopening after two minutes idle
17822
+ * painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
17823
+ * fleet only recovered because a later poll happened to observe a different
17824
+ * identity.
17825
17825
  *
17826
17826
  * ## The two properties that fix it
17827
17827
  *
17828
17828
  * **It is an RPC, so no client cache can answer it.** The demand signal
17829
- * always reaches the wrapper. This method therefore MAY create keep-warm
17830
- * subscriptions, where `getSnapshotOverview` must never (D93) — the
17831
- * distinction is not "one is newer" but that the overview poll is app-wide
17832
- * (a creating overview would warm every camera on the install) while this is
17833
- * called by a rendered surface naming the tiles it is actually painting, at
17834
- * the width it is painting them.
17829
+ * always reaches the wrapper. This method therefore CAPTURES, where
17830
+ * `getSnapshotOverview` must never (D93) — the distinction is not "one is
17831
+ * newer" but that the overview poll is app-wide (a capturing overview would
17832
+ * dial every camera on the install) while this is called by a rendered
17833
+ * surface naming the tiles it is actually painting, at the width it is
17834
+ * painting them.
17835
+ *
17836
+ * Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
17837
+ * server-side keep-warm loop was removed (operator directive — on-demand,
17838
+ * always), so a camera nobody is looking at costs nothing at all.
17835
17839
  *
17836
17840
  * **It waits, briefly and boundedly, for the capture it triggered.** The
17837
17841
  * returned `capturedAt` is the frame the link will serve, not the frame the
@@ -29147,9 +29151,10 @@ var DeclaredDevices = class {
29147
29151
  }
29148
29152
  const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
29149
29153
  const index = await this.readIndex();
29154
+ const live = await this.readLiveByStableId();
29150
29155
  const outcomes = [];
29151
29156
  for (const declaration of spec.devices) {
29152
- const outcome = await this.applyDeclaration(declaration, integrationId, index);
29157
+ const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
29153
29158
  if (outcome !== null) outcomes.push(outcome);
29154
29159
  }
29155
29160
  return {
@@ -29195,6 +29200,26 @@ var DeclaredDevices = class {
29195
29200
  return new Map(rows.map((row) => [row.stableId, row]));
29196
29201
  }
29197
29202
  /**
29203
+ * Devices this kernel already has CONSTRUCTED, by stableId.
29204
+ *
29205
+ * Distinct from {@link readIndex}, and the distinction is the bug: the index
29206
+ * is persisted rows, this is live objects. A row without an object must be
29207
+ * adopted; an object must be left exactly as it is.
29208
+ *
29209
+ * Failure is non-fatal and deliberately so — an empty map degrades to the
29210
+ * previous behaviour (attempt the adopt) rather than skipping a device that
29211
+ * genuinely needs bringing up.
29212
+ */
29213
+ async readLiveByStableId() {
29214
+ try {
29215
+ const devices = await this.ports.devices.getAll();
29216
+ return new Map(devices.map((device) => [device.stableId, device]));
29217
+ } catch (err) {
29218
+ this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
29219
+ return /* @__PURE__ */ new Map();
29220
+ }
29221
+ }
29222
+ /**
29198
29223
  * One declaration: adopt what exists, create what does not.
29199
29224
  *
29200
29225
  * The create branch is the destructive one — it seeds `initialMeta`, and
@@ -29203,8 +29228,15 @@ var DeclaredDevices = class {
29203
29228
  * the declared name over the operator's rename. D49: that branch needs a
29204
29229
  * second read to agree.
29205
29230
  */
29206
- async applyDeclaration(declaration, integrationId, index) {
29231
+ async applyDeclaration(declaration, integrationId, index, live) {
29207
29232
  try {
29233
+ const alreadyLive = live.get(declaration.stableId);
29234
+ if (alreadyLive !== void 0) return {
29235
+ stableId: declaration.stableId,
29236
+ deviceId: alreadyLive.id,
29237
+ device: alreadyLive,
29238
+ created: false
29239
+ };
29208
29240
  let existing = index.get(declaration.stableId);
29209
29241
  if (existing === void 0) {
29210
29242
  existing = (await this.readIndex()).get(declaration.stableId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/types",
3
- "version": "1.2.59",
3
+ "version": "1.2.60",
4
4
  "description": "Shared types, interfaces, and model catalogs for the CamStack detection ecosystem",
5
5
  "keywords": [
6
6
  "camstack",