@camstack/system 1.2.137 → 1.2.138

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.
@@ -3949,6 +3949,112 @@ var DeviceMetaStore = class {
3949
3949
  };
3950
3950
  };
3951
3951
  //#endregion
3952
+ //#region src/builtins/device-manager/fleet-read-census.ts
3953
+ /** How many stack frames identify a call site. One frame is a method; three are
3954
+ * a cause — `listFleet < listAll < onDeviceRegistered` is the answer, `listAll`
3955
+ * alone is not. Same choice, for the same reason, as `read-census.ts`. */
3956
+ var CENSUS_STACK_FRAMES = 3;
3957
+ function siteFromStack(stack) {
3958
+ if (stack === void 0) return "unknown";
3959
+ const frames = stack.split("\n").slice(1).map((line) => line.trim().replace(/^at\s+/, "")).filter((line) => line.length > 0).slice(0, CENSUS_STACK_FRAMES).map((line) => {
3960
+ const open = line.indexOf(" (");
3961
+ return (open > 0 ? line.slice(0, open) : line).replace(/^async\s+/, "");
3962
+ });
3963
+ return frames.length === 0 ? "unknown" : frames.join(" < ");
3964
+ }
3965
+ var FleetReadCensus = class FleetReadCensus {
3966
+ /**
3967
+ * Longest window the census may be armed for.
3968
+ *
3969
+ * Ten minutes covers a boot several times over and bounds what an operator
3970
+ * pays if they set the variable and forget it. A diagnostic that can be left
3971
+ * on becomes a tax nobody remembers enabling.
3972
+ */
3973
+ static MAX_WINDOW_MS = 10 * 6e4;
3974
+ windowMs;
3975
+ closesAt;
3976
+ counts = /* @__PURE__ */ new Map();
3977
+ reported = false;
3978
+ constructor(rawEnv, nowMs) {
3979
+ const parsed = rawEnv === void 0 ? NaN : Number(rawEnv);
3980
+ const valid = Number.isFinite(parsed) && parsed > 0;
3981
+ this.windowMs = valid ? Math.min(parsed, FleetReadCensus.MAX_WINDOW_MS) : 0;
3982
+ this.closesAt = valid ? nowMs + this.windowMs : 0;
3983
+ }
3984
+ get armed() {
3985
+ return this.windowMs > 0;
3986
+ }
3987
+ /** Test seam: pretend the window closed. Production reads the clock. */
3988
+ closeAt(nowMs) {
3989
+ if (nowMs >= this.closesAt) this.forceClosed = true;
3990
+ }
3991
+ forceClosed = false;
3992
+ isOpen(nowMs) {
3993
+ if (!this.armed || this.forceClosed) return false;
3994
+ return nowMs < this.closesAt;
3995
+ }
3996
+ /**
3997
+ * Capture the caller, or `null` when the window is shut.
3998
+ *
3999
+ * Called at the START of a read. A read that began inside the window belongs
4000
+ * to it even if it lands after the close — otherwise the slow reads at the
4001
+ * edge would be attributed to nobody, and the slow ones are the interesting
4002
+ * ones.
4003
+ */
4004
+ siteOf(nowMs = Date.now()) {
4005
+ if (!this.isOpen(nowMs)) return null;
4006
+ const holder = {};
4007
+ Error.captureStackTrace(holder, this.siteOf);
4008
+ return siteFromStack(holder.stack);
4009
+ }
4010
+ /** Count one read at an already-captured site. A `null` site is not counted. */
4011
+ record(site, rowCount) {
4012
+ if (site === null) return;
4013
+ const existing = this.counts.get(site);
4014
+ if (existing === void 0) {
4015
+ this.counts.set(site, {
4016
+ site,
4017
+ calls: 1,
4018
+ rows: rowCount
4019
+ });
4020
+ return;
4021
+ }
4022
+ existing.calls += 1;
4023
+ existing.rows += rowCount;
4024
+ }
4025
+ /** Attributed sites, busiest first. */
4026
+ rows() {
4027
+ return [...this.counts.values()].map((c) => ({
4028
+ site: c.site,
4029
+ calls: c.calls,
4030
+ rows: c.rows
4031
+ })).sort((a, b) => b.calls - a.calls);
4032
+ }
4033
+ /**
4034
+ * Emit the report ONCE.
4035
+ *
4036
+ * `perMin` is included because the raw count is unreadable without the window
4037
+ * it was taken over — "4 149" means nothing until you know it was 66 seconds.
4038
+ */
4039
+ report(logger, elapsedMs) {
4040
+ if (this.reported || !this.armed) return;
4041
+ this.reported = true;
4042
+ const rows = this.rows();
4043
+ const totalCalls = rows.reduce((acc, r) => acc + r.calls, 0);
4044
+ logger.info("fleet read census", { meta: {
4045
+ elapsedMs,
4046
+ sites: rows.length,
4047
+ totalCalls,
4048
+ rows: rows.map((r) => ({
4049
+ site: r.site,
4050
+ calls: r.calls,
4051
+ rows: r.rows,
4052
+ perMin: elapsedMs === 0 ? 0 : Math.round(r.calls * 6e4 / elapsedMs)
4053
+ }))
4054
+ } });
4055
+ }
4056
+ };
4057
+ //#endregion
3952
4058
  //#region src/builtins/device-manager/device-row-store.ts
3953
4059
  /**
3954
4060
  * @durable class=ledger owner=device-manager
@@ -4278,9 +4384,17 @@ var DeviceRowStore = class {
4278
4384
  backend;
4279
4385
  logger;
4280
4386
  declared = null;
4281
- constructor(backend, logger) {
4387
+ /** Call-site attribution for fleet reads. Armed only by
4388
+ * `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
4389
+ census;
4390
+ constructor(backend, logger, census) {
4282
4391
  this.backend = backend;
4283
4392
  this.logger = logger;
4393
+ this.census = census ?? new FleetReadCensus(process.env["CAMSTACK_FLEET_READ_CENSUS_MS"], Date.now());
4394
+ if (this.census.armed) {
4395
+ const startedAt = Date.now();
4396
+ setTimeout(() => this.census.report(this.logger, Date.now() - startedAt), this.census.windowMs).unref?.();
4397
+ }
4284
4398
  }
4285
4399
  /**
4286
4400
  * Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
@@ -4449,11 +4563,13 @@ var DeviceRowStore = class {
4449
4563
  }
4450
4564
  async list(filter, columns) {
4451
4565
  await this.declare();
4566
+ const site = this.census.siteOf();
4452
4567
  const records = await this.backend.query({
4453
4568
  collection: DEVICE_ROWS_COLLECTION,
4454
4569
  filter,
4455
4570
  ...columns === void 0 ? {} : { columns }
4456
4571
  });
4572
+ this.census.record(site, records.length);
4457
4573
  const out = [];
4458
4574
  for (const record of records) {
4459
4575
  const decoded = decodeDeviceRow(record.data);
@@ -3944,6 +3944,112 @@ var DeviceMetaStore = class {
3944
3944
  };
3945
3945
  };
3946
3946
  //#endregion
3947
+ //#region src/builtins/device-manager/fleet-read-census.ts
3948
+ /** How many stack frames identify a call site. One frame is a method; three are
3949
+ * a cause — `listFleet < listAll < onDeviceRegistered` is the answer, `listAll`
3950
+ * alone is not. Same choice, for the same reason, as `read-census.ts`. */
3951
+ var CENSUS_STACK_FRAMES = 3;
3952
+ function siteFromStack(stack) {
3953
+ if (stack === void 0) return "unknown";
3954
+ const frames = stack.split("\n").slice(1).map((line) => line.trim().replace(/^at\s+/, "")).filter((line) => line.length > 0).slice(0, CENSUS_STACK_FRAMES).map((line) => {
3955
+ const open = line.indexOf(" (");
3956
+ return (open > 0 ? line.slice(0, open) : line).replace(/^async\s+/, "");
3957
+ });
3958
+ return frames.length === 0 ? "unknown" : frames.join(" < ");
3959
+ }
3960
+ var FleetReadCensus = class FleetReadCensus {
3961
+ /**
3962
+ * Longest window the census may be armed for.
3963
+ *
3964
+ * Ten minutes covers a boot several times over and bounds what an operator
3965
+ * pays if they set the variable and forget it. A diagnostic that can be left
3966
+ * on becomes a tax nobody remembers enabling.
3967
+ */
3968
+ static MAX_WINDOW_MS = 10 * 6e4;
3969
+ windowMs;
3970
+ closesAt;
3971
+ counts = /* @__PURE__ */ new Map();
3972
+ reported = false;
3973
+ constructor(rawEnv, nowMs) {
3974
+ const parsed = rawEnv === void 0 ? NaN : Number(rawEnv);
3975
+ const valid = Number.isFinite(parsed) && parsed > 0;
3976
+ this.windowMs = valid ? Math.min(parsed, FleetReadCensus.MAX_WINDOW_MS) : 0;
3977
+ this.closesAt = valid ? nowMs + this.windowMs : 0;
3978
+ }
3979
+ get armed() {
3980
+ return this.windowMs > 0;
3981
+ }
3982
+ /** Test seam: pretend the window closed. Production reads the clock. */
3983
+ closeAt(nowMs) {
3984
+ if (nowMs >= this.closesAt) this.forceClosed = true;
3985
+ }
3986
+ forceClosed = false;
3987
+ isOpen(nowMs) {
3988
+ if (!this.armed || this.forceClosed) return false;
3989
+ return nowMs < this.closesAt;
3990
+ }
3991
+ /**
3992
+ * Capture the caller, or `null` when the window is shut.
3993
+ *
3994
+ * Called at the START of a read. A read that began inside the window belongs
3995
+ * to it even if it lands after the close — otherwise the slow reads at the
3996
+ * edge would be attributed to nobody, and the slow ones are the interesting
3997
+ * ones.
3998
+ */
3999
+ siteOf(nowMs = Date.now()) {
4000
+ if (!this.isOpen(nowMs)) return null;
4001
+ const holder = {};
4002
+ Error.captureStackTrace(holder, this.siteOf);
4003
+ return siteFromStack(holder.stack);
4004
+ }
4005
+ /** Count one read at an already-captured site. A `null` site is not counted. */
4006
+ record(site, rowCount) {
4007
+ if (site === null) return;
4008
+ const existing = this.counts.get(site);
4009
+ if (existing === void 0) {
4010
+ this.counts.set(site, {
4011
+ site,
4012
+ calls: 1,
4013
+ rows: rowCount
4014
+ });
4015
+ return;
4016
+ }
4017
+ existing.calls += 1;
4018
+ existing.rows += rowCount;
4019
+ }
4020
+ /** Attributed sites, busiest first. */
4021
+ rows() {
4022
+ return [...this.counts.values()].map((c) => ({
4023
+ site: c.site,
4024
+ calls: c.calls,
4025
+ rows: c.rows
4026
+ })).sort((a, b) => b.calls - a.calls);
4027
+ }
4028
+ /**
4029
+ * Emit the report ONCE.
4030
+ *
4031
+ * `perMin` is included because the raw count is unreadable without the window
4032
+ * it was taken over — "4 149" means nothing until you know it was 66 seconds.
4033
+ */
4034
+ report(logger, elapsedMs) {
4035
+ if (this.reported || !this.armed) return;
4036
+ this.reported = true;
4037
+ const rows = this.rows();
4038
+ const totalCalls = rows.reduce((acc, r) => acc + r.calls, 0);
4039
+ logger.info("fleet read census", { meta: {
4040
+ elapsedMs,
4041
+ sites: rows.length,
4042
+ totalCalls,
4043
+ rows: rows.map((r) => ({
4044
+ site: r.site,
4045
+ calls: r.calls,
4046
+ rows: r.rows,
4047
+ perMin: elapsedMs === 0 ? 0 : Math.round(r.calls * 6e4 / elapsedMs)
4048
+ }))
4049
+ } });
4050
+ }
4051
+ };
4052
+ //#endregion
3947
4053
  //#region src/builtins/device-manager/device-row-store.ts
3948
4054
  /**
3949
4055
  * @durable class=ledger owner=device-manager
@@ -4273,9 +4379,17 @@ var DeviceRowStore = class {
4273
4379
  backend;
4274
4380
  logger;
4275
4381
  declared = null;
4276
- constructor(backend, logger) {
4382
+ /** Call-site attribution for fleet reads. Armed only by
4383
+ * `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
4384
+ census;
4385
+ constructor(backend, logger, census) {
4277
4386
  this.backend = backend;
4278
4387
  this.logger = logger;
4388
+ this.census = census ?? new FleetReadCensus(process.env["CAMSTACK_FLEET_READ_CENSUS_MS"], Date.now());
4389
+ if (this.census.armed) {
4390
+ const startedAt = Date.now();
4391
+ setTimeout(() => this.census.report(this.logger, Date.now() - startedAt), this.census.windowMs).unref?.();
4392
+ }
4279
4393
  }
4280
4394
  /**
4281
4395
  * Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
@@ -4444,11 +4558,13 @@ var DeviceRowStore = class {
4444
4558
  }
4445
4559
  async list(filter, columns) {
4446
4560
  await this.declare();
4561
+ const site = this.census.siteOf();
4447
4562
  const records = await this.backend.query({
4448
4563
  collection: DEVICE_ROWS_COLLECTION,
4449
4564
  filter,
4450
4565
  ...columns === void 0 ? {} : { columns }
4451
4566
  });
4567
+ this.census.record(site, records.length);
4452
4568
  const out = [];
4453
4569
  for (const record of records) {
4454
4570
  const decoded = decodeDeviceRow(record.data);
@@ -1,6 +1,7 @@
1
1
  import { ChildLayout, CollectionColumn, CollectionIndex, DeviceDisplayOverride, IScopedLogger, MutationFilter, QueryFilter, SettingsRecord, SettingsStoreClient } from '@camstack/types';
2
2
  import { RetiredRowStore } from '../sqlite-storage/retired-settings-keys.js';
3
3
  import { PersistedDeviceMeta } from './device-meta-types.js';
4
+ import { FleetReadCensus } from './fleet-read-census.js';
4
5
  /**
5
6
  * @durable class=ledger owner=device-manager
6
7
  * write="one row per device, written by `allocateDeviceId` (identity placeholder),
@@ -211,7 +212,10 @@ export declare class DeviceRowStore {
211
212
  private readonly backend;
212
213
  private readonly logger;
213
214
  private declared;
214
- constructor(backend: DeviceRowBackend, logger: IScopedLogger);
215
+ /** Call-site attribution for fleet reads. Armed only by
216
+ * `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
217
+ private readonly census;
218
+ constructor(backend: DeviceRowBackend, logger: IScopedLogger, census?: FleetReadCensus);
215
219
  /**
216
220
  * Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
217
221
  * concurrent first-callers issue one declaration rather than N. A rejection
@@ -0,0 +1,47 @@
1
+ import { IScopedLogger } from '@camstack/types';
2
+ /** One attributed call site. */
3
+ export interface FleetCensusRow {
4
+ readonly site: string;
5
+ readonly calls: number;
6
+ readonly rows: number;
7
+ }
8
+ export declare class FleetReadCensus {
9
+ /**
10
+ * Longest window the census may be armed for.
11
+ *
12
+ * Ten minutes covers a boot several times over and bounds what an operator
13
+ * pays if they set the variable and forget it. A diagnostic that can be left
14
+ * on becomes a tax nobody remembers enabling.
15
+ */
16
+ static readonly MAX_WINDOW_MS: number;
17
+ readonly windowMs: number;
18
+ private readonly closesAt;
19
+ private readonly counts;
20
+ private reported;
21
+ constructor(rawEnv: string | undefined, nowMs: number);
22
+ get armed(): boolean;
23
+ /** Test seam: pretend the window closed. Production reads the clock. */
24
+ closeAt(nowMs: number): void;
25
+ private forceClosed;
26
+ private isOpen;
27
+ /**
28
+ * Capture the caller, or `null` when the window is shut.
29
+ *
30
+ * Called at the START of a read. A read that began inside the window belongs
31
+ * to it even if it lands after the close — otherwise the slow reads at the
32
+ * edge would be attributed to nobody, and the slow ones are the interesting
33
+ * ones.
34
+ */
35
+ siteOf(nowMs?: number): string | null;
36
+ /** Count one read at an already-captured site. A `null` site is not counted. */
37
+ record(site: string | null, rowCount: number): void;
38
+ /** Attributed sites, busiest first. */
39
+ rows(): readonly FleetCensusRow[];
40
+ /**
41
+ * Emit the report ONCE.
42
+ *
43
+ * `perMin` is included because the raw count is unreadable without the window
44
+ * it was taken over — "4 149" means nothing until you know it was 66 seconds.
45
+ */
46
+ report(logger: IScopedLogger, elapsedMs: number): void;
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.137",
3
+ "version": "1.2.138",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",