@camstack/system 1.2.447 → 1.2.449
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/assets/device-history/icon.svg +4 -0
- package/dist/{agent-https-5DYAOJMO.js → agent-https-CMz4S56_.js} +2 -2
- package/dist/{agent-https-BWUEf6ev.mjs → agent-https-D6lRxHma.mjs} +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +2 -2
- package/dist/builtins/alerts/alerts.addon.mjs +2 -2
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/capture/capture-engine.d.ts +87 -0
- package/dist/builtins/device-history/capture/capture-filter.d.ts +37 -0
- package/dist/builtins/device-history/capture/device-directory.d.ts +47 -0
- package/dist/builtins/device-history/capture/field-path.d.ts +10 -0
- package/dist/builtins/device-history/capture/point-writer.d.ts +63 -0
- package/dist/builtins/device-history/device-history.addon.d.ts +38 -0
- package/dist/builtins/device-history/device-history.addon.js +2823 -0
- package/dist/builtins/device-history/device-history.addon.mjs +2816 -0
- package/dist/builtins/device-history/index.d.ts +3 -0
- package/dist/builtins/device-history/ingest/producer-ingest.d.ts +89 -0
- package/dist/builtins/device-history/query/history-query.d.ts +69 -0
- package/dist/builtins/device-history/retention/departure-tracker.d.ts +33 -0
- package/dist/builtins/device-history/retention/retention.d.ts +67 -0
- package/dist/builtins/device-history/rollup/rollup-engine.d.ts +72 -0
- package/dist/builtins/device-history/rollup/rollup-math.d.ts +44 -0
- package/dist/builtins/device-history/runtime/history-runtime.d.ts +93 -0
- package/dist/builtins/device-history/status/counters.d.ts +55 -0
- package/dist/builtins/device-history/store/history-door.d.ts +7 -0
- package/dist/builtins/device-history/store/history-schema.d.ts +26 -0
- package/dist/builtins/device-history/store/history-store.d.ts +86 -0
- package/dist/builtins/device-history/store/row-readers.d.ts +11 -0
- package/dist/builtins/device-history/store/series-registry.d.ts +39 -0
- package/dist/builtins/device-history/time/bucket-time.d.ts +25 -0
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +4 -4
- package/dist/builtins/keychain/index.mjs +4 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +3 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +4 -6
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +12 -15
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +12 -15
- package/dist/builtins/sqlite-storage/vector-provider.d.ts +6 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BwIIltnZ.js → composition-sources-BQmhc3se.js} +1 -1
- package/dist/{composition-sources-BmC5jAUv.mjs → composition-sources-DzuruDVY.mjs} +1 -1
- package/dist/{constants-DA_tjzIk.js → constants-BPWA-r-Y.js} +1 -1
- package/dist/{constants-6XHGmEUF.mjs → constants-gzGZbnwO.mjs} +1 -1
- package/dist/{device-ca-CnbrgfmW.js → device-ca-75rt3U_U.js} +1 -1
- package/dist/{device-ca-Bh_ClxMO.mjs → device-ca-D05nqNXE.mjs} +1 -1
- package/dist/{dist-BkNfEJKa.js → dist-Ct88catN.js} +385 -1
- package/dist/{dist-CkZIrsAP.mjs → dist-DV2ll9Hi.mjs} +356 -2
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CoyIUWHC.mjs → hub-mdns-ciao-BPOg2iyc.mjs} +2 -2
- package/dist/{hub-mdns-ciao-m5LIr4oA.js → hub-mdns-ciao-DAU1WttG.js} +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -7
- package/dist/index.mjs +9 -8
- package/dist/{phase6-declare-gate-BDWZuB2n.mjs → phase6-declare-gate-BJplW6tX.mjs} +1 -1
- package/dist/{phase6-declare-gate-Bl7KocEs.js → phase6-declare-gate-C6WqagkC.js} +1 -1
- package/dist/{restore-archive-B57xI5Ue.js → restore-archive-Cprdh3P9.js} +1 -1
- package/dist/{restore-archive-CMr4V7Fi.mjs → restore-archive-Jiz63mVB.mjs} +1 -1
- package/dist/{wal-maintenance-Bqm4zrNA.mjs → wal-maintenance-BineOch3.mjs} +0 -0
- package/dist/{wal-maintenance-CuG1DUca.js → wal-maintenance-mwCBe3Vr.js} +0 -0
- package/package.json +24 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The producer door (plan §5.1): `declareSeries`, `record`, `recordBuckets`.
|
|
3
|
+
*
|
|
4
|
+
* Ownership is the transport-stamped caller (`callerAddonId`), never anything
|
|
5
|
+
* the caller claims; a key is `addon:<callerAddonId>/<name>`, so one addon can
|
|
6
|
+
* only ever declare keys inside its own prefix and a write to another addon's
|
|
7
|
+
* key is `not-owner`. Every refusal is COUNTED per (key, reason) and returned
|
|
8
|
+
* (D391): a producer that is silently dropped looks exactly like one that never
|
|
9
|
+
* produced.
|
|
10
|
+
*
|
|
11
|
+
* `record` is for low-rate points (at most one per 10 s per series, sustained);
|
|
12
|
+
* `recordBuckets` takes CLOSED five-minute buckets and is an idempotent upsert
|
|
13
|
+
* keyed (series, bucketStart), written before the call returns so a failure
|
|
14
|
+
* reaches the producer, which re-queues (D58).
|
|
15
|
+
*/
|
|
16
|
+
import { type DeviceHistoryIngestResult, type DeviceHistorySubject, type IScopedLogger } from "@camstack/types";
|
|
17
|
+
import type { DeviceDirectory } from "../capture/device-directory.js";
|
|
18
|
+
import type { PointWriter } from "../capture/point-writer.js";
|
|
19
|
+
import type { HistoryCounters } from "../status/counters.js";
|
|
20
|
+
import type { HistoryStore } from "../store/history-store.js";
|
|
21
|
+
import { type SeriesRegistry } from "../store/series-registry.js";
|
|
22
|
+
/** One point per series per this many ms, sustained. */
|
|
23
|
+
export declare const RECORD_MIN_INTERVAL_MS: number;
|
|
24
|
+
/** A point or bucket may be at most this far in the future (clock skew). */
|
|
25
|
+
export declare const FUTURE_TOLERANCE_MS: number;
|
|
26
|
+
export interface DeclareSeriesInput {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly subjectKind: "device" | "node";
|
|
29
|
+
readonly kind: "gauge" | "counter" | "boolean";
|
|
30
|
+
readonly unit?: string;
|
|
31
|
+
readonly displayName: string;
|
|
32
|
+
readonly ingest: "points" | "buckets";
|
|
33
|
+
}
|
|
34
|
+
export interface RecordPointInput {
|
|
35
|
+
readonly key: string;
|
|
36
|
+
readonly subject: DeviceHistorySubject;
|
|
37
|
+
readonly scope: string;
|
|
38
|
+
readonly ts: number;
|
|
39
|
+
readonly value: number;
|
|
40
|
+
}
|
|
41
|
+
export interface RecordBucketInput {
|
|
42
|
+
readonly key: string;
|
|
43
|
+
readonly subject: DeviceHistorySubject;
|
|
44
|
+
readonly scope: string;
|
|
45
|
+
readonly bucketStart: number;
|
|
46
|
+
readonly samples: number;
|
|
47
|
+
readonly sum: number;
|
|
48
|
+
readonly min: number;
|
|
49
|
+
readonly max: number;
|
|
50
|
+
readonly last: number | null;
|
|
51
|
+
readonly lastTs: number | null;
|
|
52
|
+
}
|
|
53
|
+
export interface IngestDeps {
|
|
54
|
+
readonly registry: SeriesRegistry;
|
|
55
|
+
readonly store: HistoryStore;
|
|
56
|
+
readonly directory: DeviceDirectory;
|
|
57
|
+
readonly writer: PointWriter;
|
|
58
|
+
readonly counters: HistoryCounters;
|
|
59
|
+
readonly logger: IScopedLogger;
|
|
60
|
+
readonly clock: () => number;
|
|
61
|
+
readonly optedOut: () => ReadonlySet<number>;
|
|
62
|
+
readonly retention: () => {
|
|
63
|
+
readonly rawMs: number;
|
|
64
|
+
readonly fiveMinuteMs: number;
|
|
65
|
+
};
|
|
66
|
+
/** A 5m row landed; the hour containing it is dirty. */
|
|
67
|
+
readonly onBucketWritten: (seriesId: number, bucketStart: number) => void;
|
|
68
|
+
}
|
|
69
|
+
export declare class ProducerIngest {
|
|
70
|
+
private readonly deps;
|
|
71
|
+
/** Last accepted point time per series, for the sustained-rate bound. */
|
|
72
|
+
private readonly lastAccepted;
|
|
73
|
+
constructor(deps: IngestDeps);
|
|
74
|
+
/** A purged series leaves no rate-limit state behind. */
|
|
75
|
+
forgetSeries(series: {
|
|
76
|
+
key: string;
|
|
77
|
+
subject: string;
|
|
78
|
+
scope: string;
|
|
79
|
+
}): void;
|
|
80
|
+
declareSeries(callerAddonId: string | null, input: DeclareSeriesInput): Promise<{
|
|
81
|
+
key: string;
|
|
82
|
+
}>;
|
|
83
|
+
/** Why a write to `metric` by `caller` for this subject must be refused, or null. */
|
|
84
|
+
private refusalFor;
|
|
85
|
+
record(callerAddonId: string | null, points: readonly RecordPointInput[]): Promise<DeviceHistoryIngestResult>;
|
|
86
|
+
recordBuckets(callerAddonId: string | null, buckets: readonly RecordBucketInput[]): Promise<DeviceHistoryIngestResult>;
|
|
87
|
+
/** Persist the widened span of every series a bucket batch touched, then remember it. */
|
|
88
|
+
private recordSpans;
|
|
89
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads: `listSeries` and `query` (plan §5.1).
|
|
3
|
+
*
|
|
4
|
+
* `auto` resolution picks the finest tier whose retention covers `from` and
|
|
5
|
+
* whose point count fits `maxPoints`: a rollup tier fits when
|
|
6
|
+
* `ceil(range / bucket) <= maxPoints`, raw fits when a read of `maxPoints + 1`
|
|
7
|
+
* rows comes back short. The coarsest covering tier is the last resort and is
|
|
8
|
+
* truncated to its newest `maxPoints` points (`truncated: true`) rather than
|
|
9
|
+
* answering nothing.
|
|
10
|
+
*
|
|
11
|
+
* `before` is the last point strictly before the window in the chosen tier, so a
|
|
12
|
+
* step chart can start at the value in force when the window opens — a quiet
|
|
13
|
+
* bucket has no row, and the held value is otherwise invisible.
|
|
14
|
+
*/
|
|
15
|
+
import type { DeviceHistoryQueryPoint, DeviceHistoryResolution, DeviceHistorySeriesInfo, DeviceHistorySubject } from "@camstack/types";
|
|
16
|
+
import { type RetentionPolicy } from "../retention/retention.js";
|
|
17
|
+
import type { HistoryStore } from "../store/history-store.js";
|
|
18
|
+
import { type SeriesRegistry } from "../store/series-registry.js";
|
|
19
|
+
import { type HistoryCatalog } from "../capture/capture-engine.js";
|
|
20
|
+
type Tier = DeviceHistoryResolution;
|
|
21
|
+
export interface QueryInput {
|
|
22
|
+
readonly key: string;
|
|
23
|
+
readonly subject: DeviceHistorySubject;
|
|
24
|
+
readonly scope?: string;
|
|
25
|
+
readonly from: number;
|
|
26
|
+
readonly to: number;
|
|
27
|
+
readonly resolution: Tier | "auto";
|
|
28
|
+
readonly maxPoints: number;
|
|
29
|
+
}
|
|
30
|
+
export interface QuerySeries {
|
|
31
|
+
readonly scope: string;
|
|
32
|
+
readonly before: {
|
|
33
|
+
readonly t: number;
|
|
34
|
+
readonly value: number | null;
|
|
35
|
+
} | null;
|
|
36
|
+
readonly points: readonly DeviceHistoryQueryPoint[];
|
|
37
|
+
}
|
|
38
|
+
export interface QueryOutput {
|
|
39
|
+
readonly resolution: Tier;
|
|
40
|
+
readonly series: readonly QuerySeries[];
|
|
41
|
+
readonly truncated: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface QueryDeps {
|
|
44
|
+
readonly registry: SeriesRegistry;
|
|
45
|
+
readonly store: HistoryStore;
|
|
46
|
+
readonly clock: () => number;
|
|
47
|
+
readonly retention: () => RetentionPolicy;
|
|
48
|
+
readonly catalog: HistoryCatalog;
|
|
49
|
+
/** The device's current slice of a cap, for a `unitFrom` series; `null` when unreadable. */
|
|
50
|
+
readonly readCapSlice: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
|
|
51
|
+
}
|
|
52
|
+
export declare class HistoryQuery {
|
|
53
|
+
private readonly deps;
|
|
54
|
+
constructor(deps: QueryDeps);
|
|
55
|
+
query(input: QueryInput): Promise<QueryOutput>;
|
|
56
|
+
/** Tiers whose retention covers `from`, finest first; the coarsest is always present. */
|
|
57
|
+
private coveringTiers;
|
|
58
|
+
/** Cheap fit test for rollup tiers; raw is decided by the read itself. */
|
|
59
|
+
private fits;
|
|
60
|
+
private readTier;
|
|
61
|
+
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
62
|
+
private shape;
|
|
63
|
+
private before;
|
|
64
|
+
listSeries(subject: DeviceHistorySubject): Promise<readonly DeviceHistorySeriesInfo[]>;
|
|
65
|
+
/** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
|
|
66
|
+
private unitOf;
|
|
67
|
+
private specOf;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Since when each device has been ABSENT from every successful device listing.
|
|
3
|
+
*
|
|
4
|
+
* A listing that answered is not a listing that was complete: after a hub restart
|
|
5
|
+
* a provider addon that is still booting leaves its devices out of an otherwise
|
|
6
|
+
* successful `listAll`, and change-only capture means a stable series has not been
|
|
7
|
+
* written for days. Deciding "gone" from one listing, or from the series' own last
|
|
8
|
+
* write, would delete live history for good (D49). So the only thing a listing may
|
|
9
|
+
* do here is move a clock: the first listing that misses a device records WHEN
|
|
10
|
+
* (persisted, so a restart neither resets it to now nor treats it as old), any
|
|
11
|
+
* listing that shows the device clears it, and a series becomes purgeable only
|
|
12
|
+
* after {@link DEPARTED_ABSENCE_MS} of unbroken absence.
|
|
13
|
+
*
|
|
14
|
+
* The `device.unregistered` event is deliberately not used: it is telemetry and
|
|
15
|
+
* may be dropped, and a provider that restarts emits it for devices that come back.
|
|
16
|
+
*/
|
|
17
|
+
import type { IScopedLogger } from "@camstack/types";
|
|
18
|
+
import type { HistoryStore } from "../store/history-store.js";
|
|
19
|
+
import type { SeriesRegistry } from "../store/series-registry.js";
|
|
20
|
+
export declare const DEPARTED_ABSENCE_MS: number;
|
|
21
|
+
export declare class DepartureTracker {
|
|
22
|
+
private readonly store;
|
|
23
|
+
private readonly registry;
|
|
24
|
+
private readonly logger;
|
|
25
|
+
private missing;
|
|
26
|
+
constructor(store: HistoryStore, registry: SeriesRegistry, logger: IScopedLogger);
|
|
27
|
+
load(): Promise<void>;
|
|
28
|
+
missingSince(deviceId: number): number | undefined;
|
|
29
|
+
/** Forget a device's clock — its series were removed because it was DELETED (D865). */
|
|
30
|
+
forget(deviceId: number): Promise<void>;
|
|
31
|
+
/** Feed one SUCCESSFUL listing. A failed read never reaches here. */
|
|
32
|
+
observe(listed: ReadonlySet<number>, now: number): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention (plan §5.5, operator answer §13): raw 7 days, 5m 14 days, 1h 1 year,
|
|
3
|
+
* 1d forever — global addon settings.
|
|
4
|
+
*
|
|
5
|
+
* Per series and per tier the delete is a PRIMARY KEY range
|
|
6
|
+
* (`series_id = ? AND ts BETWEEN 0 AND cutoff-1`), so no `ts` index exists to
|
|
7
|
+
* maintain. A row whose time equals the cutoff is KEPT. Each series is visited
|
|
8
|
+
* at most once per {@link RETENTION_STEP_MS} of cutoff advance (an hour for the
|
|
9
|
+
* short tiers, a day for the long ones): without that, every active series
|
|
10
|
+
* would pay an empty-range delete on every ten-minute run.
|
|
11
|
+
*
|
|
12
|
+
* Every delete is SLICED by time (a day for the short tiers, 30 days for the long
|
|
13
|
+
* ones) with a yield between slices, so one statement never holds the history
|
|
14
|
+
* thread for a whole backlog.
|
|
15
|
+
*
|
|
16
|
+
* A series is purged only after its device has been absent from EVERY successful
|
|
17
|
+
* device listing for a week ({@link DepartureTracker}); it is never judged from the
|
|
18
|
+
* series' own last write, which for a stable change-only series is days old while
|
|
19
|
+
* the device is perfectly alive. An OPTED-OUT device keeps its history until
|
|
20
|
+
* retention ages it out: stopping capture is the switch; deleting history is not.
|
|
21
|
+
*/
|
|
22
|
+
import type { IScopedLogger } from "@camstack/types";
|
|
23
|
+
import type { DepartureTracker } from "./departure-tracker.js";
|
|
24
|
+
import type { HistoryCounters } from "../status/counters.js";
|
|
25
|
+
import type { HistoryStore, StoredTier } from "../store/history-store.js";
|
|
26
|
+
import type { SeriesRegistry } from "../store/series-registry.js";
|
|
27
|
+
export interface RetentionPolicy {
|
|
28
|
+
readonly rawDays: number;
|
|
29
|
+
readonly fiveMinuteDays: number;
|
|
30
|
+
readonly hourDays: number;
|
|
31
|
+
/** `null` (or 0) keeps the daily tier forever. */
|
|
32
|
+
readonly dayDays: number | null;
|
|
33
|
+
}
|
|
34
|
+
export declare const DEFAULT_RETENTION: RetentionPolicy;
|
|
35
|
+
/** Cutoff per tier: rows strictly older are deleted. `null` = keep forever. */
|
|
36
|
+
export declare function retentionCutoffs(policy: RetentionPolicy, now: number): Readonly<Record<StoredTier, number | null>>;
|
|
37
|
+
export interface RetentionDeps {
|
|
38
|
+
readonly store: HistoryStore;
|
|
39
|
+
readonly registry: SeriesRegistry;
|
|
40
|
+
readonly departures: DepartureTracker;
|
|
41
|
+
readonly counters: HistoryCounters;
|
|
42
|
+
readonly logger: IScopedLogger;
|
|
43
|
+
/** Gives the thread back between slices; a spec counts it. */
|
|
44
|
+
readonly yieldNow?: () => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare class RetentionJob {
|
|
47
|
+
private readonly deps;
|
|
48
|
+
private readonly appliedUpTo;
|
|
49
|
+
private running;
|
|
50
|
+
constructor(deps: RetentionDeps);
|
|
51
|
+
run(policy: RetentionPolicy, now: number): Promise<void>;
|
|
52
|
+
private doRun;
|
|
53
|
+
/** Delete `[lo, hiInclusive]` of one tier in time slices, yielding between them. */
|
|
54
|
+
/** Drop the visit markers of a purged series. */
|
|
55
|
+
forgetSeries(seriesId: number): void;
|
|
56
|
+
private deleteSliced;
|
|
57
|
+
/** Tier by tier in slices, then the series row (and whatever the slices missed). */
|
|
58
|
+
private purgeSeries;
|
|
59
|
+
/**
|
|
60
|
+
* Delete every series of a DELETED device now (D865). Driven by a tombstone
|
|
61
|
+
* through the `device-removal-hook`, never by a listing: the week-long
|
|
62
|
+
* absence clock above remains the backstop for a device removed before
|
|
63
|
+
* tombstones existed.
|
|
64
|
+
*/
|
|
65
|
+
purgeDevice(deviceId: number, now: number): Promise<number>;
|
|
66
|
+
private purgeDeparted;
|
|
67
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollups 5m / 1h / 1d, computed by this addon and nowhere else (plan §5.5).
|
|
3
|
+
*
|
|
4
|
+
* Every rollup is a RECOMPUTE from the tier below — never an increment — so
|
|
5
|
+
* running one twice, after a restart, or after a late write gives the same row:
|
|
6
|
+
*
|
|
7
|
+
* - 5m (point series only): from the raw rows of the bucket, once it has
|
|
8
|
+
* closed (+30 s grace). Raw is the source of truth, which is why a restart
|
|
9
|
+
* loses nothing: {@link RollupEngine.bootRecompute} rebuilds the recent
|
|
10
|
+
* window, the still-open bucket included, from raw.
|
|
11
|
+
* - 1h: from the 5m rows when the hour closes (+5 min grace).
|
|
12
|
+
* - 1d: from the 1h rows at hub-local midnight (+15 min grace).
|
|
13
|
+
*
|
|
14
|
+
* A write marks the bucket above it DIRTY; a late `recordBuckets` into an hour
|
|
15
|
+
* that was already rolled just marks it again, and the next tick recomputes it.
|
|
16
|
+
* The dirty sets are memory: a crash forgets them, and the boot window
|
|
17
|
+
* ({@link BOOT_WINDOW_MS}) is what makes that harmless.
|
|
18
|
+
*/
|
|
19
|
+
import type { IScopedLogger } from "@camstack/types";
|
|
20
|
+
import type { HistoryCounters } from "../status/counters.js";
|
|
21
|
+
import type { HistoryStore } from "../store/history-store.js";
|
|
22
|
+
import type { SeriesRegistry } from "../store/series-registry.js";
|
|
23
|
+
import type { WrittenPoints } from "../capture/point-writer.js";
|
|
24
|
+
/** Series handled per tier per tick: bounds one tick on a catch-up. */
|
|
25
|
+
export declare const MAX_SERIES_PER_TICK: number;
|
|
26
|
+
/** How far back a boot rebuilds from raw and re-marks the upper tiers. */
|
|
27
|
+
export declare const BOOT_WINDOW_MS: number;
|
|
28
|
+
export interface RollupEngineDeps {
|
|
29
|
+
readonly store: HistoryStore;
|
|
30
|
+
readonly registry: SeriesRegistry;
|
|
31
|
+
readonly counters: HistoryCounters;
|
|
32
|
+
readonly logger: IScopedLogger;
|
|
33
|
+
readonly clock: () => number;
|
|
34
|
+
}
|
|
35
|
+
export declare class RollupEngine {
|
|
36
|
+
private readonly deps;
|
|
37
|
+
private readonly dirty5m;
|
|
38
|
+
private readonly dirty1h;
|
|
39
|
+
private readonly dirty1d;
|
|
40
|
+
private ticking;
|
|
41
|
+
constructor(deps: RollupEngineDeps);
|
|
42
|
+
dirtyCounts(): {
|
|
43
|
+
readonly m5: number;
|
|
44
|
+
readonly h1: number;
|
|
45
|
+
readonly d1: number;
|
|
46
|
+
};
|
|
47
|
+
/** Raw points landed: their 5-minute buckets (and a late bucket's successor) are dirty. */
|
|
48
|
+
markPoints(written: readonly WrittenPoints[]): void;
|
|
49
|
+
/** A 5m row landed (computed, or a producer bucket): its hour is dirty. */
|
|
50
|
+
markFiveMinuteRow(seriesId: number, bucketStart: number): void;
|
|
51
|
+
/** One pass over the three tiers. Single-flight; errors are counted and the work stays dirty. */
|
|
52
|
+
tick(): Promise<void>;
|
|
53
|
+
private doTick;
|
|
54
|
+
private guarded;
|
|
55
|
+
private closedBuckets;
|
|
56
|
+
/** Forget everything held for a series that was purged. */
|
|
57
|
+
forgetSeries(seriesId: number): void;
|
|
58
|
+
private tickFiveMinute;
|
|
59
|
+
private tickHour;
|
|
60
|
+
private tickDay;
|
|
61
|
+
/** Raw → 5m rows for buckets in `[from, toExclusive)`; writes them and marks their hours. */
|
|
62
|
+
recomputeFiveMinute(seriesId: number, from: number, toExclusive: number, horizon: number): Promise<number>;
|
|
63
|
+
private recomputeHour;
|
|
64
|
+
private recomputeDay;
|
|
65
|
+
private recomputeFrom;
|
|
66
|
+
/**
|
|
67
|
+
* After a restart the dirty sets are gone. Rebuild the recent window from raw
|
|
68
|
+
* (the still-open bucket included, time-weighted up to `now`), then mark the
|
|
69
|
+
* 1h / 1d buckets of that window dirty so the next ticks recompute them.
|
|
70
|
+
*/
|
|
71
|
+
bootRecompute(now: number): Promise<number>;
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface RollupRow {
|
|
2
|
+
readonly seriesId: number;
|
|
3
|
+
readonly bucketStart: number;
|
|
4
|
+
readonly samples: number;
|
|
5
|
+
readonly sum: number;
|
|
6
|
+
readonly min: number;
|
|
7
|
+
readonly max: number;
|
|
8
|
+
/** `null` is unknown, never zero (D393). */
|
|
9
|
+
readonly last: number | null;
|
|
10
|
+
readonly lastTs: number | null;
|
|
11
|
+
/** Σ value·ms over the covered time; `null` for bucket-ingested series. */
|
|
12
|
+
readonly twSum: number | null;
|
|
13
|
+
/** Milliseconds `twSum` covers. */
|
|
14
|
+
readonly twMs: number | null;
|
|
15
|
+
}
|
|
16
|
+
export interface RawPoint {
|
|
17
|
+
readonly ts: number;
|
|
18
|
+
readonly value: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RawToFiveMinuteInput {
|
|
21
|
+
readonly seriesId: number;
|
|
22
|
+
/** Ascending by `ts`. */
|
|
23
|
+
readonly points: readonly RawPoint[];
|
|
24
|
+
/** The newest point strictly before the first bucket in `points`, if any. */
|
|
25
|
+
readonly carryIn: RawPoint | null;
|
|
26
|
+
/** Time-weighting stops here for a bucket that has not closed yet. */
|
|
27
|
+
readonly horizon: number;
|
|
28
|
+
}
|
|
29
|
+
/** One row per 5-minute bucket that has at least one point. */
|
|
30
|
+
export declare function rawToFiveMinuteRows(input: RawToFiveMinuteInput): readonly RollupRow[];
|
|
31
|
+
export interface CombineInput {
|
|
32
|
+
readonly seriesId: number;
|
|
33
|
+
readonly windowStart: number;
|
|
34
|
+
readonly windowEnd: number;
|
|
35
|
+
readonly childWidthMs: number;
|
|
36
|
+
/** Child rows inside the window, ascending by `bucketStart`. */
|
|
37
|
+
readonly rows: readonly RollupRow[];
|
|
38
|
+
/** The `last` of the newest child row BEFORE the window, if known. */
|
|
39
|
+
readonly carryIn: number | null;
|
|
40
|
+
/** Time-weighting stops here for a window that has not closed yet. */
|
|
41
|
+
readonly horizon: number;
|
|
42
|
+
}
|
|
43
|
+
/** One row of the next tier from the child rows of its window; `null` when there are none. */
|
|
44
|
+
export declare function combineRows(input: CombineInput): RollupRow | null;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assembled device-history engine, with no knowledge of the addon shell:
|
|
3
|
+
* everything it needs from the hub arrives as a dependency (the door, the
|
|
4
|
+
* device directory source, the snapshot read), so a spec can run the whole
|
|
5
|
+
* thing over a real `history.db` in a temp dir with a controlled clock.
|
|
6
|
+
*
|
|
7
|
+
* Boot is two steps so the shell can subscribe to the bus in between and lose
|
|
8
|
+
* nothing:
|
|
9
|
+
* {@link HistoryRuntime.prepare} declare, load, seed — nothing is written;
|
|
10
|
+
* {@link HistoryRuntime.finishBoot} rebuild the recent rollup window, first reconcile.
|
|
11
|
+
* Then {@link HistoryRuntime.startTimers} arms flush (10 s), rollups (60 s),
|
|
12
|
+
* reconcile (5 min), retention (10 min) and the row counts (30 min).
|
|
13
|
+
*/
|
|
14
|
+
import type { DeviceHistoryStatus, IScopedLogger } from "@camstack/types";
|
|
15
|
+
import { CaptureEngine, type HistoryCatalog, type SnapshotMap } from "../capture/capture-engine.js";
|
|
16
|
+
import { DeviceDirectory, type DeviceTypeSource } from "../capture/device-directory.js";
|
|
17
|
+
import { PointWriter } from "../capture/point-writer.js";
|
|
18
|
+
import { ProducerIngest } from "../ingest/producer-ingest.js";
|
|
19
|
+
import { HistoryQuery } from "../query/history-query.js";
|
|
20
|
+
import { DepartureTracker } from "../retention/departure-tracker.js";
|
|
21
|
+
import { RetentionJob, type RetentionPolicy } from "../retention/retention.js";
|
|
22
|
+
import { RollupEngine } from "../rollup/rollup-engine.js";
|
|
23
|
+
import { HistoryCounters } from "../status/counters.js";
|
|
24
|
+
import type { HistoryDoor } from "../store/history-door.js";
|
|
25
|
+
import { HistoryStore } from "../store/history-store.js";
|
|
26
|
+
import { SeriesRegistry } from "../store/series-registry.js";
|
|
27
|
+
export declare const ROLLUP_INTERVAL_MS: number;
|
|
28
|
+
export declare const RECONCILE_INTERVAL_MS: number;
|
|
29
|
+
export declare const RETENTION_INTERVAL_MS: number;
|
|
30
|
+
/**
|
|
31
|
+
* ONE exact row count per boot (a `COUNT(*)` is a full scan on the history thread), taken late so
|
|
32
|
+
* boot is not spent scanning the raw table. After it the figures move with the writes and deletes
|
|
33
|
+
* the addon itself makes — an estimate, labelled so in the docs.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FIRST_COUNT_DELAY_MS: number;
|
|
36
|
+
export interface HistoryRuntimeDeps {
|
|
37
|
+
readonly door: HistoryDoor;
|
|
38
|
+
readonly catalog: HistoryCatalog;
|
|
39
|
+
readonly deviceSource: DeviceTypeSource;
|
|
40
|
+
readonly readSnapshots: () => Promise<SnapshotMap>;
|
|
41
|
+
readonly readCapSlice: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
|
|
42
|
+
readonly retention: () => RetentionPolicy;
|
|
43
|
+
readonly logger: IScopedLogger;
|
|
44
|
+
readonly clock: () => number;
|
|
45
|
+
}
|
|
46
|
+
export declare class HistoryRuntime {
|
|
47
|
+
private readonly deps;
|
|
48
|
+
readonly counters: HistoryCounters;
|
|
49
|
+
readonly store: HistoryStore;
|
|
50
|
+
readonly registry: SeriesRegistry;
|
|
51
|
+
readonly directory: DeviceDirectory;
|
|
52
|
+
readonly rollup: RollupEngine;
|
|
53
|
+
readonly writer: PointWriter;
|
|
54
|
+
readonly capture: CaptureEngine;
|
|
55
|
+
readonly ingest: ProducerIngest;
|
|
56
|
+
readonly query: HistoryQuery;
|
|
57
|
+
readonly retentionJob: RetentionJob;
|
|
58
|
+
readonly departures: DepartureTracker;
|
|
59
|
+
private optedOutDevices;
|
|
60
|
+
private readonly tierCounts;
|
|
61
|
+
private readonly startedAt;
|
|
62
|
+
private timers;
|
|
63
|
+
private countTimeout;
|
|
64
|
+
private ready;
|
|
65
|
+
constructor(deps: HistoryRuntimeDeps);
|
|
66
|
+
prepare(): Promise<void>;
|
|
67
|
+
finishBoot(): Promise<void>;
|
|
68
|
+
startTimers(): void;
|
|
69
|
+
stop(): Promise<void>;
|
|
70
|
+
/** Emit pending trailing points, then write the buffer. */
|
|
71
|
+
flushNow(): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Restore changes a dropped event missed. A FAILED read changes nothing (D49):
|
|
74
|
+
* it is counted and logged, and the next pass tries again.
|
|
75
|
+
*/
|
|
76
|
+
reconcile(): Promise<void>;
|
|
77
|
+
refreshCounts(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* ONE aggregated INFO naming what was skipped, refused, deleted or failed since the
|
|
80
|
+
* previous report (D391) — the counts a per-event line would drown. Silent when
|
|
81
|
+
* nothing happened.
|
|
82
|
+
*/
|
|
83
|
+
reportWindow(): void;
|
|
84
|
+
isEnabled(deviceId: number): boolean;
|
|
85
|
+
setEnabled(deviceId: number, enabled: boolean): Promise<boolean>;
|
|
86
|
+
/** Delete the history of a deleted device: its series, opt-out and absence clock. */
|
|
87
|
+
removeDevice(deviceId: number): Promise<{
|
|
88
|
+
series: number;
|
|
89
|
+
}>;
|
|
90
|
+
/** Series held per device, for the admin clean-up listing. */
|
|
91
|
+
seriesByDevice(): ReadonlyMap<number, number>;
|
|
92
|
+
status(): DeviceHistoryStatus;
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counters since boot, read by `getStatus`. Everything the addon DROPS or
|
|
3
|
+
* REFUSES is counted here (D391): a silent branch reads as "never happened".
|
|
4
|
+
* `summarySinceLast` turns the same counters into the aggregated INFO line the
|
|
5
|
+
* runtime logs once per pass.
|
|
6
|
+
*/
|
|
7
|
+
export type CountedTier = "raw" | "5m" | "1h" | "1d";
|
|
8
|
+
export interface WindowSummary {
|
|
9
|
+
readonly skipped: Record<string, number>;
|
|
10
|
+
readonly refused: Record<string, number>;
|
|
11
|
+
readonly deleted: Record<string, number>;
|
|
12
|
+
readonly flushFailures: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class HistoryCounters {
|
|
15
|
+
eventsSeen: number;
|
|
16
|
+
pointsEmitted: number;
|
|
17
|
+
pointsWritten: number;
|
|
18
|
+
pointsDropped: number;
|
|
19
|
+
trailingEmitted: number;
|
|
20
|
+
reconciledPoints: number;
|
|
21
|
+
reconcileRuns: number;
|
|
22
|
+
reconcileFailures: number;
|
|
23
|
+
lastReconcileAt: number | null;
|
|
24
|
+
lastFlushAt: number | null;
|
|
25
|
+
flushFailures: number;
|
|
26
|
+
ingestAccepted: number;
|
|
27
|
+
rollupErrors: number;
|
|
28
|
+
last5mAt: number | null;
|
|
29
|
+
last1hAt: number | null;
|
|
30
|
+
last1dAt: number | null;
|
|
31
|
+
retentionLastRunAt: number | null;
|
|
32
|
+
purgedSeries: number;
|
|
33
|
+
directoryRefreshAt: number | null;
|
|
34
|
+
private readonly skipped;
|
|
35
|
+
private readonly refused;
|
|
36
|
+
private readonly deleted;
|
|
37
|
+
private reported;
|
|
38
|
+
/** Net rows written minus deleted per tier since boot — the estimate behind `getStatus` row counts. */
|
|
39
|
+
private readonly rowDelta;
|
|
40
|
+
/** Newest bucket seen per (tier, series): a row beyond it is NEW, a rewrite of an older one is not. */
|
|
41
|
+
private readonly newestBucket;
|
|
42
|
+
skip(reason: string, count?: number): void;
|
|
43
|
+
refuse(reason: string, count?: number): void;
|
|
44
|
+
addDeleted(tier: string, count: number): void;
|
|
45
|
+
addRawRows(count: number): void;
|
|
46
|
+
/** Rollup rows written: only a bucket newer than the series' newest known is counted as a new row. */
|
|
47
|
+
noteRollupRows(tier: "5m" | "1h" | "1d", seriesId: number, bucketStarts: readonly number[]): void;
|
|
48
|
+
rowDeltaOf(tier: CountedTier): number;
|
|
49
|
+
forgetSeries(seriesId: number): void;
|
|
50
|
+
skippedRecord(): Record<string, number>;
|
|
51
|
+
refusedRecord(): Record<string, number>;
|
|
52
|
+
deletedRecord(): Record<string, number>;
|
|
53
|
+
/** What was skipped, refused, deleted or failed since the last call; `null` when nothing. */
|
|
54
|
+
summarySinceLast(): WindowSummary | null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of the settings door device-history uses. A `Pick` of the
|
|
3
|
+
* generated client, never a hand-written interface (a hand-written copy keeps
|
|
4
|
+
* compiling after a method is removed and fails at runtime).
|
|
5
|
+
*/
|
|
6
|
+
import type { SettingsStoreClient } from "@camstack/types";
|
|
7
|
+
export type HistoryDoor = Pick<SettingsStoreClient, "declareCollection" | "query" | "insert" | "upsertMany" | "transact" | "deleteWhere" | "update" | "count" | "delete">;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `history.db` data model (plan §4), declared through the settings door
|
|
3
|
+
* with `database: 'history'` (D851). The engine turns each declaration into the
|
|
4
|
+
* DDL the plan lists; a spec compares `sqlite_master` with it.
|
|
5
|
+
*
|
|
6
|
+
* The opt-out table is configuration, not history: it names no database and so
|
|
7
|
+
* lives in `camstack.db`, which the nightly backup carries.
|
|
8
|
+
*/
|
|
9
|
+
import type { DeclareCollectionInput } from "@camstack/types";
|
|
10
|
+
import type { RollupTier } from "../time/bucket-time.js";
|
|
11
|
+
export declare const HISTORY_NAMESPACE = "device-history";
|
|
12
|
+
export declare const COLLECTIONS: {
|
|
13
|
+
readonly metrics: "metrics";
|
|
14
|
+
readonly series: "series";
|
|
15
|
+
readonly raw: "raw";
|
|
16
|
+
readonly rollup5m: "rollup-5m";
|
|
17
|
+
readonly rollup1h: "rollup-1h";
|
|
18
|
+
readonly rollup1d: "rollup-1d";
|
|
19
|
+
readonly optOuts: "device-opt-outs";
|
|
20
|
+
readonly departures: "device-departures";
|
|
21
|
+
};
|
|
22
|
+
export type TierCollection = (typeof COLLECTIONS)["rollup5m" | "rollup1h" | "rollup1d"];
|
|
23
|
+
export declare function rollupCollectionOf(tier: RollupTier): TierCollection;
|
|
24
|
+
/** Table name the engine creates for a collection of this addon. */
|
|
25
|
+
export declare function tableNameOf(collection: string): string;
|
|
26
|
+
export declare const HISTORY_DECLARATIONS: readonly DeclareCollectionInput[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every read and write device-history makes against its tables, through the
|
|
3
|
+
* settings door (D44). No addon-side code builds SQL; the shapes below are what
|
|
4
|
+
* the engine prepares, and a spec asserts the plan of each on the REAL
|
|
5
|
+
* statement (D447): every range is a PRIMARY KEY search on `(series_id, ts)` or
|
|
6
|
+
* `(series_id, bucket_start)`, so no `ts` index exists or is needed.
|
|
7
|
+
*
|
|
8
|
+
* Every read states a `limit` — the door's default row bound is 2 000 and an
|
|
9
|
+
* unstated one would silently truncate (D58's cousin).
|
|
10
|
+
*/
|
|
11
|
+
import type { DeviceHistoryKind } from "@camstack/types";
|
|
12
|
+
import type { RawPoint, RollupRow } from "../rollup/rollup-math.js";
|
|
13
|
+
import type { RollupTier } from "../time/bucket-time.js";
|
|
14
|
+
import type { HistoryDoor } from "./history-door.js";
|
|
15
|
+
export interface MetricRecord {
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly source: "cap" | "addon";
|
|
18
|
+
readonly ownerAddon: string;
|
|
19
|
+
readonly subjectKind: "device" | "node";
|
|
20
|
+
readonly kind: DeviceHistoryKind;
|
|
21
|
+
readonly unit: string | null;
|
|
22
|
+
readonly displayName: string;
|
|
23
|
+
readonly ingest: "points" | "buckets";
|
|
24
|
+
readonly declaredAt: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SeriesRecord {
|
|
27
|
+
readonly id: number;
|
|
28
|
+
readonly key: string;
|
|
29
|
+
/** `d:<deviceId>` | `n:<nodeId>`. */
|
|
30
|
+
readonly subject: string;
|
|
31
|
+
readonly deviceId: number | null;
|
|
32
|
+
readonly scope: string;
|
|
33
|
+
readonly firstTs: number;
|
|
34
|
+
readonly lastTs: number;
|
|
35
|
+
}
|
|
36
|
+
export interface NewSeries {
|
|
37
|
+
readonly key: string;
|
|
38
|
+
readonly subject: string;
|
|
39
|
+
readonly deviceId: number | null;
|
|
40
|
+
readonly scope: string;
|
|
41
|
+
readonly ts: number;
|
|
42
|
+
}
|
|
43
|
+
export interface SeriesSpanUpdate {
|
|
44
|
+
readonly id: number;
|
|
45
|
+
readonly lastTs: number;
|
|
46
|
+
}
|
|
47
|
+
export type StoredTier = "raw" | RollupTier;
|
|
48
|
+
/** Rows per read page. */
|
|
49
|
+
export declare const READ_PAGE = 5e3;
|
|
50
|
+
export declare class HistoryStore {
|
|
51
|
+
private readonly door;
|
|
52
|
+
constructor(door: HistoryDoor);
|
|
53
|
+
declare(): Promise<void>;
|
|
54
|
+
loadMetrics(): Promise<readonly MetricRecord[]>;
|
|
55
|
+
upsertMetric(metric: MetricRecord): Promise<void>;
|
|
56
|
+
/** Series with `id > afterId`, ascending — the boot load pages with this. */
|
|
57
|
+
loadSeriesPage(afterId: number, limit: number): Promise<readonly SeriesRecord[]>;
|
|
58
|
+
findSeries(key: string, subject: string, scope: string): Promise<SeriesRecord | null>;
|
|
59
|
+
insertSeries(series: NewSeries): Promise<SeriesRecord>;
|
|
60
|
+
touchSeries(updates: readonly SeriesSpanUpdate[]): Promise<void>;
|
|
61
|
+
widenSeriesStart(id: number, firstTs: number): Promise<void>;
|
|
62
|
+
upsertRaw(rows: readonly {
|
|
63
|
+
seriesId: number;
|
|
64
|
+
ts: number;
|
|
65
|
+
value: number;
|
|
66
|
+
}[]): Promise<void>;
|
|
67
|
+
/** Points of a series in `[from, toInclusive]`, ascending, at most `limit`. */
|
|
68
|
+
readRaw(seriesId: number, from: number, toInclusive: number, limit: number, direction?: "asc" | "desc"): Promise<readonly RawPoint[]>;
|
|
69
|
+
/** The newest raw point strictly before `ts`, if any. */
|
|
70
|
+
rawBefore(seriesId: number, ts: number): Promise<RawPoint | null>;
|
|
71
|
+
upsertRollups(tier: RollupTier, rows: readonly RollupRow[]): Promise<void>;
|
|
72
|
+
readRollups(tier: RollupTier, seriesId: number, from: number, toInclusive: number, limit: number, direction?: "asc" | "desc"): Promise<readonly RollupRow[]>;
|
|
73
|
+
/** The newest rollup row of `tier` strictly before `bucketStart`, if any. */
|
|
74
|
+
rollupBefore(tier: RollupTier, seriesId: number, bucketStart: number): Promise<RollupRow | null>;
|
|
75
|
+
/** Delete one series' rows of a tier with the order column in `[lo, hiInclusive]`. */
|
|
76
|
+
deleteRange(tier: StoredTier, seriesId: number, lo: number, hiInclusive: number): Promise<number>;
|
|
77
|
+
/** Delete every row of one series, in every tier, then the series row. */
|
|
78
|
+
deleteSeries(seriesId: number): Promise<void>;
|
|
79
|
+
countRows(tier: StoredTier): Promise<number>;
|
|
80
|
+
loadOptOuts(): Promise<ReadonlySet<number>>;
|
|
81
|
+
setOptOut(deviceId: number, at: number): Promise<void>;
|
|
82
|
+
loadDepartures(): Promise<ReadonlyMap<number, number>>;
|
|
83
|
+
markMissing(deviceIds: readonly number[], at: number): Promise<void>;
|
|
84
|
+
clearMissing(deviceIds: readonly number[]): Promise<void>;
|
|
85
|
+
clearOptOut(deviceId: number): Promise<void>;
|
|
86
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed reads of a door row. The door hands back `Record<string, unknown>`;
|
|
3
|
+
* a column that is not what the schema promised is a corrupt or foreign row,
|
|
4
|
+
* and a store that guessed (`Number(x ?? 0)`) would turn "unknown" into zero
|
|
5
|
+
* (D393). So these throw, naming the column.
|
|
6
|
+
*/
|
|
7
|
+
export type RowData = Readonly<Record<string, unknown>>;
|
|
8
|
+
export declare function readNumber(data: RowData, column: string): number;
|
|
9
|
+
export declare function readNumberOrNull(data: RowData, column: string): number | null;
|
|
10
|
+
export declare function readText(data: RowData, column: string): string;
|
|
11
|
+
export declare function readTextOrNull(data: RowData, column: string): string | null;
|