@camstack/addon-post-analysis 1.2.123 → 1.2.125

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.
@@ -25937,6 +25937,11 @@ var embeddingActions = require_dist.defineCustomActions({
25937
25937
  "embedding.reembedFacesStatus": require_dist.customAction(NoInputSchema$1, ReembedFacesStatusSchema)
25938
25938
  });
25939
25939
  //#endregion
25940
+ //#region src/pipeline-analytics/include-crops-default.ts
25941
+ function resolveIncludeCrops(input) {
25942
+ return input?.includeCrops === true;
25943
+ }
25944
+ //#endregion
25940
25945
  //#region src/pipeline-analytics/map-with-concurrency.ts
25941
25946
  /**
25942
25947
  * Apply `mapper` to every item with at most `limit` in flight at once.
@@ -26595,7 +26600,7 @@ var FaceGalleryProvider = class {
26595
26600
  });
26596
26601
  const identities = await this.identityStore.listIdentities();
26597
26602
  const nameMap = new Map(identities.map((i) => [i.id, i.name]));
26598
- const includeCrops = input?.includeCrops ?? true;
26603
+ const includeCrops = resolveIncludeCrops(input);
26599
26604
  return mapWithConcurrency(faces, FACE_CROP_FETCH_CONCURRENCY, async (face) => {
26600
26605
  const base64 = includeCrops ? await this.faceCropBase64(face) : void 0;
26601
26606
  const cropUrl = this.cropUrlFor(face);
@@ -30867,7 +30872,7 @@ var PlateGalleryProvider = class {
30867
30872
  return media.length > 0 ? media[0].base64 : void 0;
30868
30873
  }
30869
30874
  async toPlateInfo(plate, nameMap, opts) {
30870
- const base64 = opts?.includeCrops ?? true ? await this.plateCropBase64(plate) : void 0;
30875
+ const base64 = resolveIncludeCrops(opts) ? await this.plateCropBase64(plate) : void 0;
30871
30876
  const cropUrl = this.cropUrlFor(plate);
30872
30877
  const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
30873
30878
  return {
@@ -30905,7 +30910,7 @@ var PlateGalleryProvider = class {
30905
30910
  ...input?.limit !== void 0 ? { limit: input.limit } : {}
30906
30911
  });
30907
30912
  const nameMap = await this.vehicleNameMap();
30908
- const includeCrops = input?.includeCrops ?? true;
30913
+ const includeCrops = resolveIncludeCrops(input);
30909
30914
  return Promise.all(rows.map((p) => this.toPlateInfo(p, nameMap, { includeCrops })));
30910
30915
  }
30911
30916
  async getPlateByTrack(input) {
@@ -49922,44 +49927,65 @@ var TrackStore = class {
49922
49927
  async listDeviceIdsSince(since) {
49923
49928
  return this.collectDeviceIds(Math.max(0, Math.floor(since)));
49924
49929
  }
49930
+ /**
49931
+ * The distinct `deviceId`s with a track at or after `fromLastSeen`.
49932
+ *
49933
+ * ## Why this is a histogram and not a paged read
49934
+ *
49935
+ * It used to drain the table in 501-row pages on a `lastSeen` range with no
49936
+ * `deviceId`, ordered by `lastSeen`. The indexes on this table are
49937
+ * `(deviceId, lastSeen)` and `(deviceId, firstSeen)` — neither can serve a
49938
+ * `lastSeen` range from the left — so `EXPLAIN QUERY PLAN` on the live
49939
+ * database answered:
49940
+ *
49941
+ * ```
49942
+ * SCAN pipeline-analytics:tracks
49943
+ * USE TEMP B-TREE FOR ORDER BY
49944
+ * ```
49945
+ *
49946
+ * A full scan of a 155 MB table, plus a sorter holding every matching row —
49947
+ * and the store has no column projection, so each row carries its ~8.5 KB
49948
+ * `positions`/`snapshots` JSON through the sort. PER PAGE. Draining 17 940
49949
+ * rows took 36 such pages: measured on a copy of the live database, 36 full
49950
+ * scans, and on the hub itself single statements of 1 196–1 615 ms each,
49951
+ * every one of them blocking the one synchronous thread the whole cluster
49952
+ * reads configuration through. `listDeviceIds` has ~10 callers.
49953
+ *
49954
+ * The answer being asked for is at most a couple of dozen integers. A
49955
+ * histogram on `deviceId` with `bucketSize: 1` is `GROUP BY deviceId`, and
49956
+ * the planner serves it from an index that already exists:
49957
+ *
49958
+ * ```
49959
+ * SCAN pipeline-analytics:tracks USING COVERING INDEX idx_tracks_device_lastSeen
49960
+ * USE TEMP B-TREE FOR GROUP BY
49961
+ * ```
49962
+ *
49963
+ * 384 KB of index instead of 155 MB of table, ONE statement instead of 36:
49964
+ * **6.0 ms against 678 ms** on the same copy, warm cache both times. No new
49965
+ * index, so no write amplification and no extra bytes — the cheap question
49966
+ * came first, it just was not being asked (D56).
49967
+ *
49968
+ * `bucket` IS the deviceId: the column is INTEGER and the bucket expression
49969
+ * is `CAST((deviceId - 0) / 1 AS INTEGER)`. Best-effort, as before — a
49970
+ * failure yields [] and the caller treats it as "no devices", which is the
49971
+ * same answer it got from a failed page.
49972
+ */
49925
49973
  async collectDeviceIds(fromLastSeen) {
49926
- const seenDevices = /* @__PURE__ */ new Set();
49927
- const seenIds = /* @__PURE__ */ new Set();
49928
- const PAGE = 500;
49929
- let cursor = fromLastSeen;
49930
49974
  try {
49931
- for (;;) {
49932
- const rows = await this.store.query.query({
49933
- collection: TRACKS_COLLECTION,
49934
- filter: {
49935
- whereBetween: { lastSeen: [cursor, Number.MAX_SAFE_INTEGER] },
49936
- orderBy: {
49937
- field: "lastSeen",
49938
- direction: "asc"
49939
- },
49940
- limit: PAGE
49941
- }
49942
- });
49943
- if (rows.length === 0) break;
49944
- let newInPage = 0;
49945
- let maxLastSeen = cursor;
49946
- for (const r of rows) {
49947
- if (typeof r.id === "string" && !seenIds.has(r.id)) {
49948
- seenIds.add(r.id);
49949
- newInPage++;
49950
- }
49951
- const ls = Number(r.data["lastSeen"]);
49952
- if (Number.isFinite(ls) && ls > maxLastSeen) maxLastSeen = ls;
49953
- const deviceId = Number(r.data["deviceId"]);
49954
- if (Number.isFinite(deviceId)) seenDevices.add(deviceId);
49955
- }
49956
- if (rows.length < PAGE || newInPage === 0) break;
49957
- cursor = maxLastSeen;
49958
- }
49975
+ const buckets = await this.store.histogram.query({
49976
+ collection: TRACKS_COLLECTION,
49977
+ field: "deviceId",
49978
+ bucketSize: 1,
49979
+ origin: 0,
49980
+ ...fromLastSeen > 0 ? { filter: { whereBetween: { lastSeen: [fromLastSeen, Number.MAX_SAFE_INTEGER] } } } : {}
49981
+ });
49982
+ const ids = [];
49983
+ for (const b of buckets) if (b.count > 0 && Number.isFinite(b.bucket)) ids.push(b.bucket);
49984
+ return ids;
49959
49985
  } catch (err) {
49960
49986
  this.logger.warn("TrackStore.listDeviceIds failed", { meta: { error: String(err) } });
49987
+ return [];
49961
49988
  }
49962
- return [...seenDevices];
49963
49989
  }
49964
49990
  /** Historical query — hits the persisted collection. With `zone` set,
49965
49991
  * candidates are SQL-prefiltered on the envelope columns (overlap test via
@@ -25915,6 +25915,11 @@ var embeddingActions = defineCustomActions({
25915
25915
  "embedding.reembedFacesStatus": customAction(NoInputSchema$1, ReembedFacesStatusSchema)
25916
25916
  });
25917
25917
  //#endregion
25918
+ //#region src/pipeline-analytics/include-crops-default.ts
25919
+ function resolveIncludeCrops(input) {
25920
+ return input?.includeCrops === true;
25921
+ }
25922
+ //#endregion
25918
25923
  //#region src/pipeline-analytics/map-with-concurrency.ts
25919
25924
  /**
25920
25925
  * Apply `mapper` to every item with at most `limit` in flight at once.
@@ -26573,7 +26578,7 @@ var FaceGalleryProvider = class {
26573
26578
  });
26574
26579
  const identities = await this.identityStore.listIdentities();
26575
26580
  const nameMap = new Map(identities.map((i) => [i.id, i.name]));
26576
- const includeCrops = input?.includeCrops ?? true;
26581
+ const includeCrops = resolveIncludeCrops(input);
26577
26582
  return mapWithConcurrency(faces, FACE_CROP_FETCH_CONCURRENCY, async (face) => {
26578
26583
  const base64 = includeCrops ? await this.faceCropBase64(face) : void 0;
26579
26584
  const cropUrl = this.cropUrlFor(face);
@@ -30843,7 +30848,7 @@ var PlateGalleryProvider = class {
30843
30848
  return media.length > 0 ? media[0].base64 : void 0;
30844
30849
  }
30845
30850
  async toPlateInfo(plate, nameMap, opts) {
30846
- const base64 = opts?.includeCrops ?? true ? await this.plateCropBase64(plate) : void 0;
30851
+ const base64 = resolveIncludeCrops(opts) ? await this.plateCropBase64(plate) : void 0;
30847
30852
  const cropUrl = this.cropUrlFor(plate);
30848
30853
  const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
30849
30854
  return {
@@ -30881,7 +30886,7 @@ var PlateGalleryProvider = class {
30881
30886
  ...input?.limit !== void 0 ? { limit: input.limit } : {}
30882
30887
  });
30883
30888
  const nameMap = await this.vehicleNameMap();
30884
- const includeCrops = input?.includeCrops ?? true;
30889
+ const includeCrops = resolveIncludeCrops(input);
30885
30890
  return Promise.all(rows.map((p) => this.toPlateInfo(p, nameMap, { includeCrops })));
30886
30891
  }
30887
30892
  async getPlateByTrack(input) {
@@ -49859,44 +49864,65 @@ var TrackStore = class {
49859
49864
  async listDeviceIdsSince(since) {
49860
49865
  return this.collectDeviceIds(Math.max(0, Math.floor(since)));
49861
49866
  }
49867
+ /**
49868
+ * The distinct `deviceId`s with a track at or after `fromLastSeen`.
49869
+ *
49870
+ * ## Why this is a histogram and not a paged read
49871
+ *
49872
+ * It used to drain the table in 501-row pages on a `lastSeen` range with no
49873
+ * `deviceId`, ordered by `lastSeen`. The indexes on this table are
49874
+ * `(deviceId, lastSeen)` and `(deviceId, firstSeen)` — neither can serve a
49875
+ * `lastSeen` range from the left — so `EXPLAIN QUERY PLAN` on the live
49876
+ * database answered:
49877
+ *
49878
+ * ```
49879
+ * SCAN pipeline-analytics:tracks
49880
+ * USE TEMP B-TREE FOR ORDER BY
49881
+ * ```
49882
+ *
49883
+ * A full scan of a 155 MB table, plus a sorter holding every matching row —
49884
+ * and the store has no column projection, so each row carries its ~8.5 KB
49885
+ * `positions`/`snapshots` JSON through the sort. PER PAGE. Draining 17 940
49886
+ * rows took 36 such pages: measured on a copy of the live database, 36 full
49887
+ * scans, and on the hub itself single statements of 1 196–1 615 ms each,
49888
+ * every one of them blocking the one synchronous thread the whole cluster
49889
+ * reads configuration through. `listDeviceIds` has ~10 callers.
49890
+ *
49891
+ * The answer being asked for is at most a couple of dozen integers. A
49892
+ * histogram on `deviceId` with `bucketSize: 1` is `GROUP BY deviceId`, and
49893
+ * the planner serves it from an index that already exists:
49894
+ *
49895
+ * ```
49896
+ * SCAN pipeline-analytics:tracks USING COVERING INDEX idx_tracks_device_lastSeen
49897
+ * USE TEMP B-TREE FOR GROUP BY
49898
+ * ```
49899
+ *
49900
+ * 384 KB of index instead of 155 MB of table, ONE statement instead of 36:
49901
+ * **6.0 ms against 678 ms** on the same copy, warm cache both times. No new
49902
+ * index, so no write amplification and no extra bytes — the cheap question
49903
+ * came first, it just was not being asked (D56).
49904
+ *
49905
+ * `bucket` IS the deviceId: the column is INTEGER and the bucket expression
49906
+ * is `CAST((deviceId - 0) / 1 AS INTEGER)`. Best-effort, as before — a
49907
+ * failure yields [] and the caller treats it as "no devices", which is the
49908
+ * same answer it got from a failed page.
49909
+ */
49862
49910
  async collectDeviceIds(fromLastSeen) {
49863
- const seenDevices = /* @__PURE__ */ new Set();
49864
- const seenIds = /* @__PURE__ */ new Set();
49865
- const PAGE = 500;
49866
- let cursor = fromLastSeen;
49867
49911
  try {
49868
- for (;;) {
49869
- const rows = await this.store.query.query({
49870
- collection: TRACKS_COLLECTION,
49871
- filter: {
49872
- whereBetween: { lastSeen: [cursor, Number.MAX_SAFE_INTEGER] },
49873
- orderBy: {
49874
- field: "lastSeen",
49875
- direction: "asc"
49876
- },
49877
- limit: PAGE
49878
- }
49879
- });
49880
- if (rows.length === 0) break;
49881
- let newInPage = 0;
49882
- let maxLastSeen = cursor;
49883
- for (const r of rows) {
49884
- if (typeof r.id === "string" && !seenIds.has(r.id)) {
49885
- seenIds.add(r.id);
49886
- newInPage++;
49887
- }
49888
- const ls = Number(r.data["lastSeen"]);
49889
- if (Number.isFinite(ls) && ls > maxLastSeen) maxLastSeen = ls;
49890
- const deviceId = Number(r.data["deviceId"]);
49891
- if (Number.isFinite(deviceId)) seenDevices.add(deviceId);
49892
- }
49893
- if (rows.length < PAGE || newInPage === 0) break;
49894
- cursor = maxLastSeen;
49895
- }
49912
+ const buckets = await this.store.histogram.query({
49913
+ collection: TRACKS_COLLECTION,
49914
+ field: "deviceId",
49915
+ bucketSize: 1,
49916
+ origin: 0,
49917
+ ...fromLastSeen > 0 ? { filter: { whereBetween: { lastSeen: [fromLastSeen, Number.MAX_SAFE_INTEGER] } } } : {}
49918
+ });
49919
+ const ids = [];
49920
+ for (const b of buckets) if (b.count > 0 && Number.isFinite(b.bucket)) ids.push(b.bucket);
49921
+ return ids;
49896
49922
  } catch (err) {
49897
49923
  this.logger.warn("TrackStore.listDeviceIds failed", { meta: { error: String(err) } });
49924
+ return [];
49898
49925
  }
49899
- return [...seenDevices];
49900
49926
  }
49901
49927
  /** Historical query — hits the persisted collection. With `zone` set,
49902
49928
  * candidates are SQL-prefiltered on the envelope columns (overlap test via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.123",
3
+ "version": "1.2.125",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",