@camstack/addon-pipeline-orchestrator 1.2.325 → 1.2.327

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/index.mjs CHANGED
@@ -5390,7 +5390,7 @@ var ZodIssueCode = {
5390
5390
  var ZodFirstPartyTypeKind;
5391
5391
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5392
5392
  //#endregion
5393
- //#region ../types/dist/sleep-DRqxtc0C.mjs
5393
+ //#region ../types/dist/sleep-bwsTj221.mjs
5394
5394
  /**
5395
5395
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5396
5396
  * window to float samples (D455).
@@ -6657,7 +6657,6 @@ function createHandle(raw, opts) {
6657
6657
  const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
6658
6658
  const readAddonStoreResult = raw.readAddonStoreResult;
6659
6659
  const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
6660
- const readDeviceStoreBatch = raw.readDeviceStoreBatch;
6661
6660
  const overrides = {
6662
6661
  readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
6663
6662
  ...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
@@ -6681,12 +6680,12 @@ function createHandle(raw, opts) {
6681
6680
  };
6682
6681
  } } : {},
6683
6682
  readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
6684
- ...readDeviceStoreBatch !== void 0 ? { readDeviceStoreBatch: async (deviceIds) => {
6685
- const rows = await readDeviceStoreBatch.call(raw, deviceIds);
6683
+ readDeviceStoreBatch: async (deviceIds) => {
6684
+ const rows = await raw.readDeviceStoreBatch(deviceIds);
6686
6685
  const out = /* @__PURE__ */ new Map();
6687
6686
  for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
6688
6687
  return out;
6689
- } } : {},
6688
+ },
6690
6689
  writeAddonStore: async (patch) => {
6691
6690
  await writeAddon(patch);
6692
6691
  },
@@ -7513,7 +7512,7 @@ var BaseAddon = class {
7513
7512
  return this._addonSecretTree;
7514
7513
  }
7515
7514
  deviceSecretTree() {
7516
- this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
7515
+ this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
7517
7516
  return this._deviceSecretTree;
7518
7517
  }
7519
7518
  /**
@@ -7589,42 +7588,135 @@ var BaseAddon = class {
7589
7588
  this[timerOf] = timer;
7590
7589
  }
7591
7590
  /**
7592
- * Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
7593
- * transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
7594
- * swap, or any race where the SqliteSettingsBackend is between shutdown and
7595
- * re-initialize) doesn't propagate up into `initialize()` and leave the
7591
+ * Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
7592
+ * settings-store outage doesn't propagate up into `initialize()` and leave the
7596
7593
  * addon permanently broken.
7597
7594
  *
7598
- * Retry only the two known infra fingerprints. Anything else propagates so
7599
- * real bugs surface immediately. After the budget expires we fall back to
7600
- * `{}` (defaults) — the addon's first successful patch will rehydrate.
7595
+ * Two kinds of outage, two budgets:
7596
+ * - the store's own restart fingerprints (`SqliteSettingsBackend not
7597
+ * initialized`, `provider not available`): a short budget, then the addon
7598
+ * falls back to `{}` (defaults) - its first successful patch rehydrates;
7599
+ * - a TRANSPORT failure (the hub is restarting while this runner boots:
7600
+ * `discovery deadline exceeded`, `parent could not route`, a refused or
7601
+ * timed-out connection). Since D870 a runner's settings read THROWS instead
7602
+ * of answering `{}`, so without a budget a hub restart ends in a respawn
7603
+ * loop. A long, bounded budget; only after it the error propagates - the
7604
+ * addon never runs on defaults for a store it could not ask.
7605
+ * Anything else propagates immediately so real bugs surface.
7601
7606
  */
7602
7607
  async readAddonStoreWithRetry() {
7603
7608
  const settings = this.rawSettings();
7604
7609
  if (!settings) return {};
7605
- const delaysMs = [
7606
- 150,
7607
- 350,
7608
- 600,
7609
- 900
7610
- ];
7611
7610
  let lastErr;
7612
- for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7611
+ for (let attempt = 0;; attempt++) try {
7613
7612
  const stored = await settings.readAddonStore() ?? {};
7614
7613
  this.settingsStoreReady = true;
7615
7614
  return stored;
7616
7615
  } catch (err) {
7617
7616
  lastErr = err;
7618
- const msg = err instanceof Error ? err.message : String(err);
7619
- if (!(msg.includes("SqliteSettingsBackend not initialized") || msg.includes("provider not available"))) throw err;
7620
- if (attempt === delaysMs.length) break;
7621
- await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7617
+ const kind = classifyStoreError(err);
7618
+ if (kind === "permanent") throw err;
7619
+ const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
7620
+ if (delay === void 0) {
7621
+ if (kind === "transport") throw err;
7622
+ break;
7623
+ }
7624
+ await new Promise((r) => setTimeout(r, delay));
7622
7625
  }
7623
7626
  this.settingsStoreReady = false;
7624
7627
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7625
7628
  return {};
7626
7629
  }
7627
7630
  };
7631
+ /** The settings store's own restart fingerprints. */
7632
+ var RESTART_RETRY_DELAYS_MS = [
7633
+ 150,
7634
+ 350,
7635
+ 600,
7636
+ 900
7637
+ ];
7638
+ /** A hub restarting under a booting runner: ~100 s in total. */
7639
+ var TRANSPORT_RETRY_DELAYS_MS = [
7640
+ 150,
7641
+ 350,
7642
+ 600,
7643
+ 900,
7644
+ 2e3,
7645
+ 5e3,
7646
+ 1e4,
7647
+ 2e4,
7648
+ 3e4,
7649
+ 3e4
7650
+ ];
7651
+ var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
7652
+ /** Node / socket error codes of a connection that did not happen or broke. */
7653
+ var TRANSPORT_CODES = new Set([
7654
+ "ECONNREFUSED",
7655
+ "ECONNRESET",
7656
+ "ETIMEDOUT",
7657
+ "EPIPE",
7658
+ "EHOSTUNREACH",
7659
+ "ENETUNREACH"
7660
+ ]);
7661
+ /** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
7662
+ var UNAVAILABLE_TRPC_CODES = new Set([
7663
+ "TIMEOUT",
7664
+ "SERVICE_UNAVAILABLE",
7665
+ "BAD_GATEWAY",
7666
+ "GATEWAY_TIMEOUT"
7667
+ ]);
7668
+ /** The exact phrases OUR transport raises when it cannot reach the hub. */
7669
+ var TRANSPORT_PHRASES = [
7670
+ "discovery deadline exceeded",
7671
+ "parent could not route child unowned cap call",
7672
+ "UDS request timed out after",
7673
+ "channel closed"
7674
+ ];
7675
+ /** Moleculer error `type`s of a call that could not be served right now. */
7676
+ var MOLECULER_TRANSIENT_TYPES = new Set([
7677
+ "REQUEST_TIMEOUT",
7678
+ "SERVICE_NOT_FOUND",
7679
+ "SERVICE_NOT_AVAILABLE",
7680
+ "REQUEST_REJECTED",
7681
+ "BROKER_DISCONNECTED"
7682
+ ]);
7683
+ function stringProp(value, key) {
7684
+ if (typeof value !== "object" || value === null) return void 0;
7685
+ const found = Reflect.get(value, key);
7686
+ return typeof found === "string" ? found : void 0;
7687
+ }
7688
+ /** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
7689
+ function errorCode(err) {
7690
+ const out = {};
7691
+ const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
7692
+ const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
7693
+ const node = stringProp(err, "code") ?? stringProp(cause, "code");
7694
+ if (node !== void 0) out.node = node;
7695
+ const trpc = stringProp(data, "code");
7696
+ if (trpc !== void 0) out.trpc = trpc;
7697
+ return out;
7698
+ }
7699
+ /**
7700
+ * Is a settings read failure worth retrying, and with which budget? Only
7701
+ * EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
7702
+ * one of the exact phrases our own transport raises. A message that merely
7703
+ * contains "timed out" or "unreachable" is not evidence of anything, and
7704
+ * `settings store not wired` is a wiring bug that retrying cannot fix.
7705
+ */
7706
+ function classifyStoreError(err) {
7707
+ const message = err instanceof Error ? err.message : String(err);
7708
+ if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
7709
+ const code = errorCode(err);
7710
+ if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
7711
+ if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
7712
+ if (typeof err === "object" && err !== null) {
7713
+ if (Reflect.get(err, "retryable") === true) return "transport";
7714
+ const type = stringProp(err, "type");
7715
+ if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
7716
+ }
7717
+ if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
7718
+ return "permanent";
7719
+ }
7628
7720
  /**
7629
7721
  * Collect the keys of every field marked `perNode: true`, recursing into
7630
7722
  * layout containers (`group` fields and `sub-tabs` tabs) the same way
@@ -12770,11 +12862,10 @@ var ClipModelMetaSchema = object({
12770
12862
  /**
12771
12863
  * The `vector-store` index this model's vectors live in. ONE feature space
12772
12864
  * per index: two models share an index only when their vectors are ranked
12773
- * against each other, which for CLIP means never across families. MobileCLIP
12774
- * S1 and S2 share `object-clip` for history (separated by the row's
12775
- * `modelId`); any other model gets its own.
12865
+ * against each other, which for CLIP means never across families. Every
12866
+ * model gets its own.
12776
12867
  */
12777
- vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
12868
+ vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
12778
12869
  /**
12779
12870
  * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
12780
12871
  * must be unique per tokenizer: every model's siblings land in ONE flat
@@ -16946,6 +17037,28 @@ id: union([string(), number()]) }), {
16946
17037
  kind: "mutation",
16947
17038
  auth: "admin"
16948
17039
  });
17040
+ /** One table of the settings database. */
17041
+ var StorageTableSchema = object({
17042
+ /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
17043
+ name: string(),
17044
+ /** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
17045
+ owner: string().nullable(),
17046
+ rows: number().int().nonnegative(),
17047
+ /** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
17048
+ bytes: number().int().nonnegative().nullable(),
17049
+ /** Declared by an addon since this engine process started (a running addon claims it). */
17050
+ declaredThisBoot: boolean()
17051
+ });
17052
+ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
17053
+ collection: string().min(1),
17054
+ expectRows: number().int().nonnegative()
17055
+ }), object({
17056
+ dropped: literal(true),
17057
+ rows: number().int().nonnegative()
17058
+ }), {
17059
+ kind: "mutation",
17060
+ auth: "admin"
17061
+ });
16949
17062
  /**
16950
17063
  * `keychain` — the hub's encrypted store of secret values (D787).
16951
17064
  *
@@ -17487,6 +17600,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
17487
17600
  kind: "mutation",
17488
17601
  auth: "admin",
17489
17602
  caller: "required"
17603
+ }), method(object({
17604
+ ref: string().min(1),
17605
+ value: string().min(1)
17606
+ }), object({
17607
+ ref: string(),
17608
+ version: number().int()
17609
+ }), {
17610
+ kind: "mutation",
17611
+ auth: "admin",
17612
+ caller: "required"
17490
17613
  }), method(object({ refs: array(string().min(1)).max(1e3) }), object({
17491
17614
  deleted: array(string()).readonly(),
17492
17615
  kept: array(object({
@@ -56702,7 +56825,19 @@ var TlsStatusSchema = object({
56702
56825
  sans: array(string()),
56703
56826
  caCertPem: string().nullable(),
56704
56827
  reissueError: string().nullable(),
56705
- restartRequired: boolean()
56828
+ restartRequired: boolean(),
56829
+ /** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
56830
+ caMissing: boolean(),
56831
+ /** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
56832
+ localCaConstrained: boolean().nullable(),
56833
+ /** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
56834
+ caReplacedAt: string().nullable(),
56835
+ /** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
56836
+ storedUpload: object({
56837
+ leafFingerprintSha256: string(),
56838
+ validTo: string(),
56839
+ hasCa: boolean()
56840
+ }).nullable()
56706
56841
  });
56707
56842
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
56708
56843
  /**
@@ -56758,6 +56893,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
56758
56893
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
56759
56894
  kind: "mutation",
56760
56895
  auth: "admin"
56896
+ }), method(_void(), TlsStatusSchema, {
56897
+ kind: "mutation",
56898
+ auth: "admin"
56761
56899
  });
56762
56900
  object({
56763
56901
  /** Lifecycle state of the lock. `jammed` means the motor reported
@@ -65893,6 +66031,12 @@ Object.freeze({
65893
66031
  addonId: null,
65894
66032
  access: "create"
65895
66033
  },
66034
+ "keychainAdmin.importSecret": {
66035
+ capName: "keychain-admin",
66036
+ capScope: "system",
66037
+ addonId: null,
66038
+ access: "create"
66039
+ },
65896
66040
  "keychainAdmin.listAudit": {
65897
66041
  capName: "keychain-admin",
65898
66042
  capScope: "system",
@@ -66289,6 +66433,12 @@ Object.freeze({
66289
66433
  addonId: null,
66290
66434
  access: "create"
66291
66435
  },
66436
+ "localNetwork.useStoredUploadedCertificate": {
66437
+ capName: "local-network",
66438
+ capScope: "system",
66439
+ addonId: null,
66440
+ access: "create"
66441
+ },
66292
66442
  "lockControl.getStatus": {
66293
66443
  capName: "lock-control",
66294
66444
  capScope: "device",
@@ -69673,6 +69823,18 @@ Object.freeze({
69673
69823
  addonId: null,
69674
69824
  access: "create"
69675
69825
  },
69826
+ "storageAdmin.dropCollection": {
69827
+ capName: "storage-admin",
69828
+ capScope: "system",
69829
+ addonId: null,
69830
+ access: "delete"
69831
+ },
69832
+ "storageAdmin.listTables": {
69833
+ capName: "storage-admin",
69834
+ capScope: "system",
69835
+ addonId: null,
69836
+ access: "view"
69837
+ },
69676
69838
  "storageEvictable.evict": {
69677
69839
  capName: "storage-evictable",
69678
69840
  capScope: "system",
@@ -74718,7 +74880,11 @@ var REMOVED_MODEL_IDS = {
74718
74880
  "face-embedding": ["arcface-r100", "inception-resnet-v1"],
74719
74881
  "plate-detection": ["yolov8n-plate"],
74720
74882
  "plate-ocr": ["vgg-english-g2"],
74721
- "clip-embedding": ["mobileclip-s1", "mobileclip-s2"],
74883
+ "clip-embedding": [
74884
+ "mobileclip-s0",
74885
+ "mobileclip-s1",
74886
+ "mobileclip-s2"
74887
+ ],
74722
74888
  "package-detection": ["yolov8n-package"],
74723
74889
  "bird-classifier": ["bird-classifier-aiy-edgetpu", "bird-nabirds-404"],
74724
74890
  "animal-classifier": ["animals-10"],
@@ -76615,6 +76781,22 @@ var DurableLedger = class DurableLedger {
76615
76781
  }
76616
76782
  }
76617
76783
  /**
76784
+ * Drop every MIRRORED row about `deviceId` (per the spec's `deviceIdOf`),
76785
+ * leaving the durable rows to the caller's `deleteWhere` (D871). Returns how
76786
+ * many mirror entries went; a spec without `deviceIdOf` evicts nothing.
76787
+ */
76788
+ evictDevice(deviceId) {
76789
+ const of = this.spec.deviceIdOf;
76790
+ if (of === void 0) return 0;
76791
+ let evicted = 0;
76792
+ for (const [key, row] of [...this.mirror.entries()]) {
76793
+ if (of(row) !== deviceId) continue;
76794
+ this.mirror.delete(key);
76795
+ evicted += 1;
76796
+ }
76797
+ return evicted;
76798
+ }
76799
+ /**
76618
76800
  * Drop every mirrored key NOT in `activeKeys`. Returns how many rows went.
76619
76801
  *
76620
76802
  * **The caller must hold an AUTHORITATIVE active set.** A prune driven by a
@@ -83946,15 +84128,10 @@ var DeviceFeaturesMirror = class {
83946
84128
  async hydrate() {
83947
84129
  const settings = this.settings;
83948
84130
  if (settings === null) return 0;
83949
- const list = settings.listDeviceStoreIds;
83950
- if (list !== void 0) {
83951
- const ids = await list.call(settings);
83952
- const batch = settings.readDeviceStoreBatch;
83953
- const blobs = batch === void 0 ? new Map(await Promise.all(ids.map(async (id) => [id, await settings.readDeviceStore(id)]))) : await batch.call(settings, ids);
83954
- for (const [deviceId, blob] of blobs) {
83955
- const row = rowOf(deviceId, blob[DEVICE_FEATURES_KEY]);
83956
- if (row !== null) this.local.set(deviceId, row);
83957
- }
84131
+ const ids = await settings.listDeviceStoreIds();
84132
+ for (const [deviceId, blob] of await settings.readDeviceStoreBatch(ids)) {
84133
+ const row = rowOf(deviceId, blob[DEVICE_FEATURES_KEY]);
84134
+ if (row !== null) this.local.set(deviceId, row);
83958
84135
  }
83959
84136
  this.logger.info("device-features mirror restored", { meta: { devices: this.local.size } });
83960
84137
  return this.local.size;
@@ -87612,25 +87789,33 @@ var NodeStressController = class NodeStressController {
87612
87789
  //#region src/durable/lts-aggregator.ts
87613
87790
  /** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
87614
87791
  var LTS_BUCKET_MS = 5 * 6e4;
87792
+ /** Buckets written per transaction — bounds one statement, not the flush. */
87793
+ var FLUSH_BATCH = 200;
87615
87794
  /** How often the row cap is enforced. Rarely: it is a bound, not a deadline. */
87616
87795
  var CAP_SWEEP_INTERVAL_MS = 6 * 36e5;
87796
+ /**
87797
+ * The row's identity IS its four key columns (D871): a composite
87798
+ * `primaryKey` + `WITHOUT ROWID`, so the table is its own index and there is no
87799
+ * TEXT `id` repeating what the row already says. The old layout kept
87800
+ * `<subject>|<series>|<scope>|<bucketStart>` as a TEXT primary key beside the
87801
+ * four columns — 85 MB of rows plus a 44 MB autoindex for 1.07 M rows. A
87802
+ * composite-key collection is written with `upsertMany` (a bucket flushed twice
87803
+ * replaces itself) and read with `query` / `count` / `deleteWhere`; the calls
87804
+ * that address one row by id are refused by the engine.
87805
+ */
87806
+ var LTS_KEY_COLUMNS = [
87807
+ "subject",
87808
+ "series",
87809
+ "scope",
87810
+ "bucketStart"
87811
+ ];
87617
87812
  var LTS_COLUMNS = [
87618
- (
87619
- /** `<subject>|<series>|<scope>|<bucketStart>` — deterministic, so a bucket
87620
- * flushed twice replaces itself rather than doubling. */
87621
- {
87622
- name: "id",
87623
- type: "TEXT",
87624
- primaryKey: true,
87625
- notNull: true
87626
- }),
87627
87813
  (
87628
87814
  /** The camera id, or the node id. One column, because every query is
87629
87815
  * "this thing over time" and the thing is one or the other. */
87630
87816
  {
87631
87817
  name: "subject",
87632
- type: "TEXT",
87633
- notNull: true
87818
+ type: "TEXT"
87634
87819
  }),
87635
87820
  (
87636
87821
  /** Numeric mirror of `subject` for camera rows, so a per-camera question is
@@ -87642,22 +87827,19 @@ var LTS_COLUMNS = [
87642
87827
  }),
87643
87828
  {
87644
87829
  name: "series",
87645
- type: "TEXT",
87646
- notNull: true
87830
+ type: "TEXT"
87647
87831
  },
87648
87832
  (
87649
87833
  /** Sub-scope within a series: a zoneId for occupancy, `''` otherwise. */
87650
87834
  {
87651
87835
  name: "scope",
87652
- type: "TEXT",
87653
- notNull: true
87836
+ type: "TEXT"
87654
87837
  }),
87655
87838
  (
87656
87839
  /** Bucket start, wall-clock aligned to {@link LTS_BUCKET_MS}. */
87657
87840
  {
87658
87841
  name: "bucketStart",
87659
- type: "INTEGER",
87660
- notNull: true
87842
+ type: "INTEGER"
87661
87843
  }),
87662
87844
  {
87663
87845
  name: "samples",
@@ -87680,12 +87862,15 @@ var LTS_COLUMNS = [
87680
87862
  notNull: true
87681
87863
  }
87682
87864
  ];
87865
+ /**
87866
+ * The primary key serves every per-subject read (`subject, series, scope` then a
87867
+ * `bucketStart` range, already in bucket order); this index serves the read that
87868
+ * names a series without a subject. The old `deviceId` index went with the TEXT
87869
+ * id: nothing queries a series by the numeric mirror.
87870
+ */
87683
87871
  var LTS_INDEXES = [{
87684
87872
  name: "idx_lts_series_bucket",
87685
87873
  columns: ["series", "bucketStart"]
87686
- }, {
87687
- name: "idx_lts_device",
87688
- columns: ["deviceId"]
87689
87874
  }];
87690
87875
  function ltsBucketStart(at, bucketMs = LTS_BUCKET_MS) {
87691
87876
  return Math.floor(at / bucketMs) * bucketMs;
@@ -87714,6 +87899,8 @@ var LtsAggregator = class {
87714
87899
  return store.declareCollection.mutate({
87715
87900
  collection,
87716
87901
  columns: [...LTS_COLUMNS],
87902
+ primaryKey: [...LTS_KEY_COLUMNS],
87903
+ withoutRowid: true,
87717
87904
  indexes: [...LTS_INDEXES]
87718
87905
  });
87719
87906
  }
@@ -87773,43 +87960,60 @@ var LtsAggregator = class {
87773
87960
  return 0;
87774
87961
  }
87775
87962
  let written = 0;
87776
- for (const bucket of due) {
87777
- const key = rowId(bucket.subject, bucket.series, bucket.scope, bucket.bucketStart);
87778
- try {
87779
- await this.store.set.mutate({
87780
- collection: this.collection,
87781
- key,
87782
- value: {
87783
- subject: bucket.subject,
87784
- ...bucket.deviceId !== void 0 ? { deviceId: bucket.deviceId } : {},
87785
- series: bucket.series,
87786
- scope: bucket.scope,
87787
- bucketStart: bucket.bucketStart,
87788
- samples: bucket.samples,
87789
- sum: bucket.sum,
87790
- min: bucket.min,
87791
- max: bucket.max
87792
- }
87793
- });
87794
- written += 1;
87795
- this.groups.add(groupKey(bucket.subject, bucket.series, bucket.scope));
87796
- } catch (err) {
87797
- this.logger.warn("lts bucket write failed — this interval will be missing", {
87798
- ...bucket.deviceId !== void 0 ? { tags: { deviceId: bucket.deviceId } } : {},
87799
- meta: {
87800
- collection: this.collection,
87801
- series: bucket.series,
87802
- subject: bucket.subject,
87803
- bucketStart: bucket.bucketStart,
87804
- error: String(err)
87805
- }
87806
- });
87807
- }
87808
- this.open.delete(key);
87963
+ for (const batch of chunk(due, FLUSH_BATCH)) {
87964
+ written += await this.writeBatch(batch);
87965
+ for (const bucket of batch) this.open.delete(rowId(bucket.subject, bucket.series, bucket.scope, bucket.bucketStart));
87809
87966
  }
87810
87967
  await this.maybeSweepCap(now);
87811
87968
  return written;
87812
87969
  }
87970
+ /**
87971
+ * Upsert one batch in a single transaction. If the batch is refused as a
87972
+ * whole (all-or-nothing), each bucket is retried alone so one bad row costs
87973
+ * one interval, not forty. Returns the number of rows written.
87974
+ */
87975
+ async writeBatch(batch) {
87976
+ try {
87977
+ await this.upsert(batch);
87978
+ for (const bucket of batch) this.groups.add(groupKey(bucket.subject, bucket.series, bucket.scope));
87979
+ return batch.length;
87980
+ } catch (err) {
87981
+ if (batch.length > 1) {
87982
+ let written = 0;
87983
+ for (const bucket of batch) written += await this.writeBatch([bucket]);
87984
+ return written;
87985
+ }
87986
+ const bucket = batch[0];
87987
+ this.logger.warn("lts bucket write failed — this interval will be missing", {
87988
+ ...bucket?.deviceId !== void 0 ? { tags: { deviceId: bucket.deviceId } } : {},
87989
+ meta: {
87990
+ collection: this.collection,
87991
+ series: bucket?.series,
87992
+ subject: bucket?.subject,
87993
+ bucketStart: bucket?.bucketStart,
87994
+ error: String(err)
87995
+ }
87996
+ });
87997
+ return 0;
87998
+ }
87999
+ }
88000
+ async upsert(batch) {
88001
+ await this.store.upsertMany.mutate({
88002
+ collection: this.collection,
88003
+ conflict: [...LTS_KEY_COLUMNS],
88004
+ records: batch.map((bucket) => ({ data: {
88005
+ subject: bucket.subject,
88006
+ deviceId: bucket.deviceId ?? null,
88007
+ series: bucket.series,
88008
+ scope: bucket.scope,
88009
+ bucketStart: bucket.bucketStart,
88010
+ samples: bucket.samples,
88011
+ sum: bucket.sum,
88012
+ min: bucket.min,
88013
+ max: bucket.max
88014
+ } }))
88015
+ });
88016
+ }
87813
88017
  /** Read closed buckets back. The read surface every chart will use. */
87814
88018
  async read(query = {}) {
87815
88019
  const where = {};
@@ -87894,6 +88098,11 @@ var LtsAggregator = class {
87894
88098
  }
87895
88099
  }
87896
88100
  };
88101
+ function chunk(items, size) {
88102
+ const out = [];
88103
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
88104
+ return out;
88105
+ }
87897
88106
  function groupKey(subject, series, scope) {
87898
88107
  return `${subject}${series}${scope}`;
87899
88108
  }
@@ -87952,7 +88161,7 @@ function recordToRow(data) {
87952
88161
  * write="one row per (node, series, 5-min bucket), written ONCE when the bucket closes; a node reporting no samples writes nothing"
87953
88162
  * retention="row CAP per (subject, series, scope) — 105,120 rows ≈ one year of 5-minute buckets. Not an age sweep: an LTS row summarises an interval and has no owner to be orphaned from."
87954
88163
  */
87955
- var NODE_STRESS_LTS_COLLECTION = "pipeline-orchestrator:stats-5m";
88164
+ var NODE_STRESS_LTS_COLLECTION = "pipeline-orchestrator:stats-5m-v2";
87956
88165
  var NodeStressLts = class {
87957
88166
  lts;
87958
88167
  constructor(deps) {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DUstnfGf.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CxcjyRWF.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.2.325",
3
+ "version": "1.2.327",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",