@camstack/types 1.2.114 → 1.2.115
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/addon.js +2 -1
- package/dist/addon.mjs +2 -1
- package/dist/capabilities/data-store-provider.cap.d.ts +34 -0
- package/dist/capabilities/device-manager.cap.d.ts +99 -0
- package/dist/capabilities/index.d.ts +6 -3
- package/dist/capabilities/log-channels.cap.d.ts +93 -0
- package/dist/capabilities/settings-store.cap.d.ts +81 -1
- package/dist/capabilities/system.cap.d.ts +109 -5
- package/dist/generated/addon-api.d.ts +44 -0
- package/dist/generated/capability-router-map.d.ts +5 -2
- package/dist/generated/collection-array-methods.d.ts +1 -1
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/method-device-selectors.d.ts +2 -2
- package/dist/generated/system-proxy.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +717 -6
- package/dist/index.mjs +703 -7
- package/dist/interfaces/logging.d.ts +15 -0
- package/dist/interfaces/storage.d.ts +26 -0
- package/dist/logging/log-channel-provider.d.ts +62 -0
- package/dist/logging/log-channel.d.ts +194 -0
- package/dist/logging/log-channel.singleton.d.ts +33 -0
- package/package.json +1 -1
|
@@ -56,6 +56,21 @@ export interface LogTags {
|
|
|
56
56
|
* emitting code can reuse whatever handle the addon already has.
|
|
57
57
|
*/
|
|
58
58
|
readonly sessionId?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The per-component log CHANNEL this line was emitted on (e.g.
|
|
61
|
+
* `stream-broker.webrtc`). Set by {@link LogChannelGate.log} and by nothing
|
|
62
|
+
* else, so `| json | logChannel="stream-broker.webrtc"` selects exactly the
|
|
63
|
+
* lines an armed channel produced — without matching on message text, which
|
|
64
|
+
* is what makes a channel readable a day later.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately NOT `channel`: that tag is already taken and means something
|
|
67
|
+
* else entirely — the NVR sub-channel index a Reolink hub camera sits on
|
|
68
|
+
* (`accessories/siren.ts`, `accessories/floodlight.ts`), and it is a NUMBER.
|
|
69
|
+
* One tag key meaning both `2` and `stream-broker.rtp` would make every
|
|
70
|
+
* LogQL selector over it ambiguous, which is the same "two things under one
|
|
71
|
+
* name" defect this mechanism exists to remove.
|
|
72
|
+
*/
|
|
73
|
+
readonly logChannel?: string;
|
|
59
74
|
/** Arbitrary extra tags */
|
|
60
75
|
readonly [key: string]: string | number | undefined;
|
|
61
76
|
}
|
|
@@ -114,6 +114,30 @@ export interface SettingsIsEmptyInput {
|
|
|
114
114
|
readonly namespace?: string;
|
|
115
115
|
readonly collection: SettingsCollection;
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* One scalar an {@link ISettingsBackend.aggregate} call asks for. `as` names the
|
|
119
|
+
* result slot, so `MIN(startMs)` and `MAX(startMs)` can be asked in one call.
|
|
120
|
+
*/
|
|
121
|
+
export interface SettingsAggregateField {
|
|
122
|
+
readonly as: string;
|
|
123
|
+
readonly field: string;
|
|
124
|
+
readonly op: 'sum' | 'min' | 'max';
|
|
125
|
+
}
|
|
126
|
+
export interface SettingsAggregateInput {
|
|
127
|
+
readonly namespace?: string;
|
|
128
|
+
readonly collection: SettingsCollection;
|
|
129
|
+
readonly fields: readonly SettingsAggregateField[];
|
|
130
|
+
readonly filter?: QueryFilter;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `COUNT(*)` plus one value per requested field. A value is `null` when no row
|
|
134
|
+
* matched — never 0, because "nothing is there" and "the sum is zero" are
|
|
135
|
+
* different facts about a disk.
|
|
136
|
+
*/
|
|
137
|
+
export interface SettingsAggregateResult {
|
|
138
|
+
readonly count: number;
|
|
139
|
+
readonly values: Readonly<Record<string, number | null>>;
|
|
140
|
+
}
|
|
117
141
|
export interface SettingsHistogramInput {
|
|
118
142
|
readonly namespace?: string;
|
|
119
143
|
readonly collection: SettingsCollection;
|
|
@@ -147,6 +171,8 @@ export interface ISettingsBackend {
|
|
|
147
171
|
delete(input: SettingsDeleteInput): Promise<void>;
|
|
148
172
|
/** Count entries in a collection */
|
|
149
173
|
count(input: SettingsCountInput): Promise<number>;
|
|
174
|
+
/** COUNT(*) plus one SUM/MIN/MAX per field, in one statement. */
|
|
175
|
+
aggregate(input: SettingsAggregateInput): Promise<SettingsAggregateResult>;
|
|
150
176
|
/** Grouped counts: COUNT(*) per ((field - origin) / bucketSize) bucket. */
|
|
151
177
|
histogram(input: SettingsHistogramInput): Promise<readonly HistogramBucket[]>;
|
|
152
178
|
/** Check if a collection is empty (used for first-boot detection) */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-liner an addon registers to join the channel mechanism.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* context.registerProvider('log-channels', createLogChannelsProvider(this.ctx.logger))
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* Everything it does is bookkeeping over the process-wide
|
|
9
|
+
* {@link LogChannelRegistry}: enumerate the declarations, and push the
|
|
10
|
+
* document's armed set into the mirror. It holds no value of its own, so a
|
|
11
|
+
* second provider in another addon of the same process cannot disagree with
|
|
12
|
+
* this one — they share the registry.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the ticker lives here
|
|
15
|
+
*
|
|
16
|
+
* A window is a deadline, and something has to notice the deadline passing.
|
|
17
|
+
* It must not be the log path: a diagnostic that adds a `Date.now()` to the
|
|
18
|
+
* path it is measuring measures itself. So the provider owns a coarse
|
|
19
|
+
* interval that calls {@link LogChannelRegistry.tick} and writes ONE line per
|
|
20
|
+
* closed channel — because a channel going quiet with no line reads as "that
|
|
21
|
+
* branch was never taken", which is the wrong conclusion and an expensive one.
|
|
22
|
+
*/
|
|
23
|
+
import type { IScopedLogger } from '../interfaces/logging.js';
|
|
24
|
+
import type { LogChannelWindow } from './log-channel.js';
|
|
25
|
+
/**
|
|
26
|
+
* How often expiry is noticed. Coarse on purpose: the cost of a channel
|
|
27
|
+
* running a few seconds past its deadline is a few extra lines, and the cost
|
|
28
|
+
* of a tight timer in every addon process is paid forever.
|
|
29
|
+
*/
|
|
30
|
+
export declare const LOG_CHANNEL_TICK_MS = 5000;
|
|
31
|
+
export interface LogChannelsProviderHandle {
|
|
32
|
+
readonly list: () => readonly LogChannelDescriptorOut[];
|
|
33
|
+
readonly apply: (input: LogChannelApplyInput) => LogChannelApplyOut;
|
|
34
|
+
/** Stop the expiry ticker. Call from the addon's `shutdown()`. */
|
|
35
|
+
readonly stop: () => void;
|
|
36
|
+
}
|
|
37
|
+
export interface LogChannelApplyInput {
|
|
38
|
+
readonly windows: readonly LogChannelWindow[];
|
|
39
|
+
}
|
|
40
|
+
export interface LogChannelApplyOut {
|
|
41
|
+
readonly armed: number;
|
|
42
|
+
readonly unknown: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
interface LogChannelDescriptorOut {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly description: string;
|
|
47
|
+
readonly defaultLevel: 'info' | 'warn' | 'error';
|
|
48
|
+
readonly perDevice: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface LogChannelsProviderOptions {
|
|
51
|
+
/** Injected so a spec can drive expiry without waiting. */
|
|
52
|
+
readonly now?: () => number;
|
|
53
|
+
readonly tickMs?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the `log-channels` provider for this process.
|
|
57
|
+
*
|
|
58
|
+
* `logger` is used ONLY off the hot path — for the arm/expiry lines — so a
|
|
59
|
+
* channel that is never armed costs this module nothing but a timer.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createLogChannelsProvider(logger: IScopedLogger, options?: LogChannelsProviderOptions): LogChannelsProviderHandle;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-component log CHANNELS — the gate a hot path consults, and the registry
|
|
3
|
+
* an addon declares its channels in.
|
|
4
|
+
*
|
|
5
|
+
* ## Two axes, deliberately separated
|
|
6
|
+
*
|
|
7
|
+
* - **DECLARATION** — which channels exist. Only the addon knows:
|
|
8
|
+
* `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
|
|
9
|
+
* baichuan/handshake. A hand-wired central list rots at the first addition,
|
|
10
|
+
* and rots silently. So a channel is declared where it is consulted, and the
|
|
11
|
+
* `log-channels` capability enumerates the declarations.
|
|
12
|
+
* - **VALUE** — at which level, for which scope, until when. That stays ONE
|
|
13
|
+
* thing: the logging settings document on the `system` cap. Two authorities
|
|
14
|
+
* over the values is the exact defect
|
|
15
|
+
* `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
|
|
16
|
+
* remove; re-introducing it from the cure side would be grotesque.
|
|
17
|
+
*
|
|
18
|
+
* Nothing in this file reads a clock, an env var or a store. The registry is
|
|
19
|
+
* a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
|
|
20
|
+
* the hot path with a value somebody actually read, and by
|
|
21
|
+
* {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
|
|
22
|
+
* never reaches here, so it can neither disarm an armed channel nor arm a
|
|
23
|
+
* disarmed one (D49).
|
|
24
|
+
*
|
|
25
|
+
* ## The canonical call shape
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* if (CH_RTP.on && CH_RTP.wants(deviceId)) {
|
|
29
|
+
* CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
|
|
34
|
+
* read. Disarmed, a call site costs one load and one branch, and the `extras`
|
|
35
|
+
* object literal is never constructed because it lives inside the branch. It
|
|
36
|
+
* is the same shape already proven in production at `stream-broker.ts:1650`,
|
|
37
|
+
* and the same discipline `LoggingGate.allowsDestination` uses for the
|
|
38
|
+
* destination floor (measured at 1.93 ns/call when off).
|
|
39
|
+
*
|
|
40
|
+
* ## Why a channel emits at `info`
|
|
41
|
+
*
|
|
42
|
+
* `loki-logging.addon.ts` pins the destination default at `info` and
|
|
43
|
+
* `loki-destination.ts` drops everything below it, so a line emitted at
|
|
44
|
+
* `debug` never reaches Loki and the hub's in-memory ring only holds ~35
|
|
45
|
+
* minutes. A diagnostic that cannot be read an hour later is worse than no
|
|
46
|
+
* diagnostic, because it looks done. {@link LogChannelGate.log} therefore
|
|
47
|
+
* emits at the channel's declared level, whose schema floor is `info`.
|
|
48
|
+
*/
|
|
49
|
+
import { z } from 'zod';
|
|
50
|
+
import type { IScopedLogger, LogExtras } from '../interfaces/logging.js';
|
|
51
|
+
/**
|
|
52
|
+
* The level a channel writes at once armed.
|
|
53
|
+
*
|
|
54
|
+
* `debug` is absent ON PURPOSE and not by omission: below `info` the line does
|
|
55
|
+
* not leave the process for Loki, and the whole point of arming a channel is
|
|
56
|
+
* to read it later.
|
|
57
|
+
*/
|
|
58
|
+
export declare const LogChannelLevelSchema: z.ZodEnum<{
|
|
59
|
+
error: "error";
|
|
60
|
+
info: "info";
|
|
61
|
+
warn: "warn";
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* What an addon declares about one channel. No value, no state — a
|
|
65
|
+
* declaration is inert.
|
|
66
|
+
*/
|
|
67
|
+
export declare const LogChannelDescriptorSchema: z.ZodObject<{
|
|
68
|
+
name: z.ZodString;
|
|
69
|
+
description: z.ZodString;
|
|
70
|
+
defaultLevel: z.ZodEnum<{
|
|
71
|
+
error: "error";
|
|
72
|
+
info: "info";
|
|
73
|
+
warn: "warn";
|
|
74
|
+
}>;
|
|
75
|
+
perDevice: z.ZodBoolean;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
export type LogChannelDescriptor = z.infer<typeof LogChannelDescriptorSchema>;
|
|
78
|
+
/**
|
|
79
|
+
* An armed window over one channel, as the document hands it to a mirror.
|
|
80
|
+
*
|
|
81
|
+
* A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
|
|
82
|
+
* expires by itself, which is the one failure a boolean cannot avoid.
|
|
83
|
+
*/
|
|
84
|
+
export declare const LogChannelWindowSchema: z.ZodObject<{
|
|
85
|
+
channel: z.ZodString;
|
|
86
|
+
armedUntilMs: z.ZodNumber;
|
|
87
|
+
deviceIds: z.ZodNullable<z.ZodReadonly<z.ZodArray<z.ZodNumber>>>;
|
|
88
|
+
}, z.core.$strip>;
|
|
89
|
+
export type LogChannelWindow = z.infer<typeof LogChannelWindowSchema>;
|
|
90
|
+
/**
|
|
91
|
+
* The gate a hot path holds.
|
|
92
|
+
*
|
|
93
|
+
* Obtain it ONCE — at module scope or in a constructor — and keep the
|
|
94
|
+
* reference. Looking a channel up by name per line would put a Map lookup on
|
|
95
|
+
* the path this class exists to keep free.
|
|
96
|
+
*/
|
|
97
|
+
export declare class LogChannelGate {
|
|
98
|
+
readonly descriptor: LogChannelDescriptor;
|
|
99
|
+
/**
|
|
100
|
+
* HOT PATH GUARD. A plain data FIELD, and it must stay one.
|
|
101
|
+
*
|
|
102
|
+
* `log-channel.spec.ts` asserts the property descriptor has no getter and
|
|
103
|
+
* booby-traps the device set, so turning this into an accessor — or reading
|
|
104
|
+
* anything before it — fails the spec instead of taxing every line the
|
|
105
|
+
* process emits.
|
|
106
|
+
*/
|
|
107
|
+
on: boolean;
|
|
108
|
+
/** `null` while armed for every camera. Never read while `on` is false. */
|
|
109
|
+
private devices;
|
|
110
|
+
private level;
|
|
111
|
+
private closesAtMs;
|
|
112
|
+
constructor(descriptor: LogChannelDescriptor);
|
|
113
|
+
/** Epoch ms this channel disarms itself at. 0 when disarmed. */
|
|
114
|
+
get armedUntilMs(): number;
|
|
115
|
+
/**
|
|
116
|
+
* Does this channel want a line about `deviceId`?
|
|
117
|
+
*
|
|
118
|
+
* Call it only behind `gate.on &&`. On its own it is still correct — the
|
|
119
|
+
* guard is repeated inside — but the point of the prefix is that a disarmed
|
|
120
|
+
* channel must not pay the call at all.
|
|
121
|
+
*/
|
|
122
|
+
wants(deviceId: number): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Emit one line on this channel, at the channel's declared level.
|
|
125
|
+
*
|
|
126
|
+
* The channel name is added as `tags.logChannel` so LogQL can select the
|
|
127
|
+
* channel without matching on the message text, and whatever `tags` the
|
|
128
|
+
* caller passed — `deviceId` above all — is preserved.
|
|
129
|
+
*/
|
|
130
|
+
log(logger: IScopedLogger, message: string, extras: LogExtras): void;
|
|
131
|
+
/**
|
|
132
|
+
* Arm (or RE-arm, restarting) this channel. Off the hot path only.
|
|
133
|
+
*
|
|
134
|
+
* An empty `deviceIds` list is treated as "every camera" rather than "no
|
|
135
|
+
* camera": a window that matches nothing is indistinguishable from a
|
|
136
|
+
* disarmed one, and the operator who asked for it would wait for lines that
|
|
137
|
+
* can never come.
|
|
138
|
+
*/
|
|
139
|
+
arm(window: LogChannelWindow): void;
|
|
140
|
+
/** Disarm. Off the hot path only. */
|
|
141
|
+
disarm(): void;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Every channel this PROCESS declares, and the mirror of what is armed on it.
|
|
145
|
+
*
|
|
146
|
+
* One per process. A forked runner has its own, and it is refreshed through
|
|
147
|
+
* the `log-channels` capability by the hub that owns the document — the
|
|
148
|
+
* registry never reaches for a value itself.
|
|
149
|
+
*/
|
|
150
|
+
export declare class LogChannelRegistry {
|
|
151
|
+
private readonly gates;
|
|
152
|
+
/**
|
|
153
|
+
* Declare a channel and get its gate.
|
|
154
|
+
*
|
|
155
|
+
* A duplicate name throws. Two declarations of one name is a programming
|
|
156
|
+
* error, not a merge: the operator would arm one and the other would stay
|
|
157
|
+
* dark, which is the dead-knob shape (D62) with an extra step.
|
|
158
|
+
*/
|
|
159
|
+
declare(descriptor: LogChannelDescriptor): LogChannelGate;
|
|
160
|
+
/** The declarations, sorted by name so a list is stable to read and diff. */
|
|
161
|
+
list(): readonly LogChannelDescriptor[];
|
|
162
|
+
/** The gate for a declared channel, or `undefined`. */
|
|
163
|
+
gate(name: string): LogChannelGate | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* Apply the FULL set of armed windows. Off the hot path.
|
|
166
|
+
*
|
|
167
|
+
* Full, not incremental, and that is the whole design: the document is the
|
|
168
|
+
* authority, so a channel the document does not name is disarmed here. An
|
|
169
|
+
* incremental apply would let a disarm get lost in transit and leave a
|
|
170
|
+
* channel running that nobody can see is running.
|
|
171
|
+
*
|
|
172
|
+
* A window already past its deadline is ignored rather than armed — a
|
|
173
|
+
* restore that re-armed an expired window would make a forgotten diagnostic
|
|
174
|
+
* immortal across restarts.
|
|
175
|
+
*
|
|
176
|
+
* Returns the names it could not place, so the caller can log them: a
|
|
177
|
+
* channel named in the document that this process does not declare is
|
|
178
|
+
* either a typo or an addon that has not booted yet, and both deserve a
|
|
179
|
+
* line rather than silence.
|
|
180
|
+
*/
|
|
181
|
+
apply(windows: readonly LogChannelWindow[], nowMs: number): readonly string[];
|
|
182
|
+
/**
|
|
183
|
+
* Disarm whatever has run out. Called on a timer, NEVER from a log path — a
|
|
184
|
+
* diagnostic that adds a `Date.now()` to the path it is measuring measures
|
|
185
|
+
* itself.
|
|
186
|
+
*
|
|
187
|
+
* Returns the names it closed, so the caller can write the one line that
|
|
188
|
+
* says a window ended and stops "it went quiet" from reading as "the branch
|
|
189
|
+
* was not taken".
|
|
190
|
+
*/
|
|
191
|
+
tick(nowMs: number): readonly string[];
|
|
192
|
+
/** The channels armed right now, as the document would describe them. */
|
|
193
|
+
armed(): readonly LogChannelWindow[];
|
|
194
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide holder for the {@link LogChannelRegistry}.
|
|
3
|
+
*
|
|
4
|
+
* Three call sites that never meet need the SAME instance: the hot paths that
|
|
5
|
+
* declare a gate at module scope, the `log-channels` provider that enumerates
|
|
6
|
+
* the declarations for the hub, and the same provider applying the windows the
|
|
7
|
+
* document hands down. A registry built inside any one of them would be
|
|
8
|
+
* refreshed and collected — the shape of a knob that never does anything.
|
|
9
|
+
*
|
|
10
|
+
* Same idiom as `logging-gate.singleton.ts` and
|
|
11
|
+
* `http-request-census.singleton.ts`.
|
|
12
|
+
*/
|
|
13
|
+
import { LogChannelRegistry, type LogChannelDescriptor, type LogChannelGate } from './log-channel.js';
|
|
14
|
+
/** The process-wide log channel registry. Created empty on first use. */
|
|
15
|
+
export declare function getLogChannelRegistry(): LogChannelRegistry;
|
|
16
|
+
/**
|
|
17
|
+
* Declare a channel on the process-wide registry and get its gate.
|
|
18
|
+
*
|
|
19
|
+
* The one call an addon makes. Keep the returned gate in a module-scope
|
|
20
|
+
* `const`: looking a channel up by name per line would put a Map lookup on
|
|
21
|
+
* exactly the path this mechanism exists to keep free.
|
|
22
|
+
*
|
|
23
|
+
* `scripts/check-log-channel-gated.ts` reads these call sites. It pairs the
|
|
24
|
+
* declared name with the binding it is assigned to and refuses to let a
|
|
25
|
+
* channel ship that no `<binding>.on` anywhere consults — a declared channel
|
|
26
|
+
* nobody reads is a knob the operator turns with nothing happening, forever,
|
|
27
|
+
* and without a line. That is D62, and this repo has now shipped it three
|
|
28
|
+
* times (`audioThresholdDbfs`, the HA entities with no source, the second
|
|
29
|
+
* per-camera switch that wrote a store nobody read).
|
|
30
|
+
*/
|
|
31
|
+
export declare function declareLogChannel(descriptor: LogChannelDescriptor): LogChannelGate;
|
|
32
|
+
/** Test-only: drop the instance so a spec starts from an empty registry. */
|
|
33
|
+
export declare function __resetLogChannelRegistryForTests(): void;
|