@camstack/system 1.2.63 → 1.2.65
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-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- 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 +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +3 -3
- package/dist/builtins/backup-orchestrator/backup-orchestrator.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 +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +10 -4
- package/dist/builtins/device-manager/device-manager.addon.mjs +10 -4
- package/dist/builtins/device-manager/device-state-mirror.d.ts +7 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.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/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 +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- 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 +2 -2
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
- package/dist/builtins/platform-probe/index.js +2 -2
- package/dist/builtins/platform-probe/index.mjs +2 -2
- 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/sqlite-settings.addon.js +29 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +29 -3
- package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +19 -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 +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-LDrOFrd-.js → dist-CIkGOAcz.js} +96 -2
- package/dist/{dist-BJ2w3uoN.mjs → dist-DqPyKwVB.mjs} +96 -2
- package/dist/http/asset-compression.d.ts +93 -0
- package/dist/index.js +63 -9
- package/dist/index.mjs +63 -9
- package/dist/kernel/heap-watch.d.ts +55 -0
- package/dist/{model-download-service-D8B-4ktF.mjs → model-download-service-CTBHzORJ.mjs} +220 -2
- package/dist/{model-download-service-Cpc6SWzV.js → model-download-service-Caj1kaSi.js} +220 -2
- package/package.json +1 -1
- package/dist/{manifest-python-deps-DNfmBu20.js → manifest-python-deps-BE6l8Y4W.js} +1 -1
- package/dist/{manifest-python-deps-CopL1_5C.mjs → manifest-python-deps-Dz1ryHIN.mjs} +1 -1
|
@@ -12,6 +12,25 @@ export interface RawSqliteDatabase {
|
|
|
12
12
|
};
|
|
13
13
|
loadExtension?(path: string): void;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The largest `k` sqlite-vec will accept in a KNN query.
|
|
17
|
+
*
|
|
18
|
+
* This is the EXTENSION's own compile-time limit, not a number chosen here —
|
|
19
|
+
* measured against the real extension, which answers a larger `k` with
|
|
20
|
+
* `SqliteError: k value in knn query too large, provided 20000 and the limit
|
|
21
|
+
* is 4096` rather than with rows.
|
|
22
|
+
*
|
|
23
|
+
* It matters that this is the extension's number. The vector door looked like
|
|
24
|
+
* a bypass of {@link SETTINGS_QUERY_HARD_ROW_CAP}: `vector-store.cap.ts`
|
|
25
|
+
* declares `topK: z.number().int().positive()` with no `.max()`, and it is
|
|
26
|
+
* bound straight into `k = ?` on the same connection, in the same process, as
|
|
27
|
+
* the capped `data-store-provider.query`. It is not a bypass — sqlite-vec was
|
|
28
|
+
* holding the line the whole time, which caps a KNN's heap cost at 4 096 rows
|
|
29
|
+
* of metadata. What was actually broken is that it held the line by THROWING,
|
|
30
|
+
* so an over-large `topK` reached the caller as a raw SQLite error instead of
|
|
31
|
+
* as the bounded answer it could always have been served.
|
|
32
|
+
*/
|
|
33
|
+
export declare const VEC_KNN_MAX_K = 4096;
|
|
15
34
|
export interface SqliteVecIndexDeps {
|
|
16
35
|
readonly db: RawSqliteDatabase;
|
|
17
36
|
readonly logger: IScopedLogger;
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CIkGOAcz.js");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_chunk.__toESM(node_path);
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dt as BaseAddon, it as storageCapability, jt as parseJsonObject, nt as settingsStoreCapability, p as StorageLocationTypeSchema } from "../../dist-
|
|
1
|
+
import { dt as BaseAddon, it as storageCapability, jt as parseJsonObject, nt as settingsStoreCapability, p as StorageLocationTypeSchema } from "../../dist-DqPyKwVB.mjs";
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import { buildStorageLocationRegistry } from "@camstack/system";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CIkGOAcz.js");
|
|
7
7
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
8
8
|
/**
|
|
9
9
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ot as hydrateSchema, dt as BaseAddon, ut as errMsg } from "../../dist-
|
|
1
|
+
import { Ot as hydrateSchema, dt as BaseAddon, ut as errMsg } from "../../dist-DqPyKwVB.mjs";
|
|
2
2
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
3
3
|
/**
|
|
4
4
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CIkGOAcz.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as logDestinationCapability, dt as BaseAddon } from "../../dist-
|
|
1
|
+
import { G as logDestinationCapability, dt as BaseAddon } from "../../dist-DqPyKwVB.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import path from "node:path";
|
|
@@ -15278,6 +15278,43 @@ var CameraRecordingStatusSchema = zod.z.object({
|
|
|
15278
15278
|
storageBytes: zod.z.number()
|
|
15279
15279
|
});
|
|
15280
15280
|
/**
|
|
15281
|
+
* The stages of the status fan-out that issue a BOUNDED remote read, and can
|
|
15282
|
+
* therefore be cut short. Every other block (`assignment`, `decoder`, `motion`,
|
|
15283
|
+
* `audio`) is composed from orchestrator-local state and cannot time out.
|
|
15284
|
+
*
|
|
15285
|
+
* `switches` is the operator's per-camera function switches — the read behind
|
|
15286
|
+
* `switchedOff`, not a block of its own.
|
|
15287
|
+
*/
|
|
15288
|
+
var CameraStatusStageSchema = zod.z.enum([
|
|
15289
|
+
"source",
|
|
15290
|
+
"broker",
|
|
15291
|
+
"detection",
|
|
15292
|
+
"recording",
|
|
15293
|
+
"switches"
|
|
15294
|
+
]);
|
|
15295
|
+
/**
|
|
15296
|
+
* WHY a stage could not be trusted.
|
|
15297
|
+
*
|
|
15298
|
+
* - `timeout` — the stage never answered and the whole status call paid the
|
|
15299
|
+
* full per-stage bound for it. This is the one that used to be invisible.
|
|
15300
|
+
* - `error` — the source answered with a rejection.
|
|
15301
|
+
* - `partial` — the stage RETURNED, but from an incomplete set of sources: its
|
|
15302
|
+
* value is a floor, not a total. `switchedOff` derived from a partial switch
|
|
15303
|
+
* read can be empty while the operator has switches off.
|
|
15304
|
+
*/
|
|
15305
|
+
var CameraStatusDegradationReasonSchema = zod.z.enum([
|
|
15306
|
+
"timeout",
|
|
15307
|
+
"error",
|
|
15308
|
+
"partial"
|
|
15309
|
+
]);
|
|
15310
|
+
/** One stage of the fan-out that could NOT be read, and how long it cost. */
|
|
15311
|
+
var CameraStatusDegradationSchema = zod.z.object({
|
|
15312
|
+
stage: CameraStatusStageSchema,
|
|
15313
|
+
reason: CameraStatusDegradationReasonSchema,
|
|
15314
|
+
/** Wall-clock ms spent on the stage before it was abandoned. */
|
|
15315
|
+
elapsedMs: zod.z.number()
|
|
15316
|
+
});
|
|
15317
|
+
/**
|
|
15281
15318
|
* Aggregated per-camera pipeline status — server-composed, single call.
|
|
15282
15319
|
*
|
|
15283
15320
|
* The `assignment` and `source` blocks are always present.
|
|
@@ -15307,9 +15344,28 @@ var CameraStatusSchema = zod.z.object({
|
|
|
15307
15344
|
* differently — a quiet camera that looks identical to a dead one is the
|
|
15308
15345
|
* silence-reads-as-never-happened trap this repo keeps paying for.
|
|
15309
15346
|
*
|
|
15310
|
-
* Empty when nothing is off
|
|
15347
|
+
* Empty when nothing is off, and never contains a switch no provider offers
|
|
15348
|
+
* — but an empty list is only a POSITIVE claim when `degraded` does not name
|
|
15349
|
+
* `'switches'`. When it does, the switch set could not be read and nothing
|
|
15350
|
+
* here may be rendered as "the operator turned nothing off": that is the
|
|
15351
|
+
* D62 failure (a camera we could not read painted as broken) in the very
|
|
15352
|
+
* field that exists to prevent it.
|
|
15311
15353
|
*/
|
|
15312
15354
|
switchedOff: zod.z.array(CameraSwitchIdSchema).readonly(),
|
|
15355
|
+
/**
|
|
15356
|
+
* Stages of the bounded fan-out that were CUT SHORT — a timeout or a
|
|
15357
|
+
* rejection — and whose block is therefore `null` because we could not
|
|
15358
|
+
* READ it, not because there is nothing there.
|
|
15359
|
+
*
|
|
15360
|
+
* Without this, three different facts arrive as the same `null`: "the stage
|
|
15361
|
+
* timed out", "the stage failed", and "this camera legitimately has no
|
|
15362
|
+
* decoder / no recording". Every surface that draws a conclusion from a null
|
|
15363
|
+
* block (or from an empty `switchedOff`) must consult this first; a stage
|
|
15364
|
+
* named here supports no conclusion at all, only "unknown".
|
|
15365
|
+
*
|
|
15366
|
+
* Empty on a clean read — the overwhelmingly common case.
|
|
15367
|
+
*/
|
|
15368
|
+
degraded: zod.z.array(CameraStatusDegradationSchema).readonly(),
|
|
15313
15369
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
15314
15370
|
fetchedAt: zod.z.number()
|
|
15315
15371
|
});
|
|
@@ -19525,7 +19581,29 @@ var FaceInfoSchema = zod.z.object({
|
|
|
19525
19581
|
recognizedIdentityId: zod.z.string().optional(),
|
|
19526
19582
|
identityName: zod.z.string().optional(),
|
|
19527
19583
|
assigned: zod.z.boolean(),
|
|
19584
|
+
/**
|
|
19585
|
+
* The crop, inline, base64.
|
|
19586
|
+
*
|
|
19587
|
+
* **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
|
|
19588
|
+
* field alone is ~2.87 MiB, re-sent in full on every operator assign and
|
|
19589
|
+
* every 30 s poll, base64-inflated over the msgpack socket and held in the
|
|
19590
|
+
* query heap. It stays for callers that have not migrated; `includeCrops:
|
|
19591
|
+
* false` turns it off once they have.
|
|
19592
|
+
*/
|
|
19528
19593
|
base64: zod.z.string().optional(),
|
|
19594
|
+
/**
|
|
19595
|
+
* Same crop, as a data-plane URL for `<img src>` — the move the admin
|
|
19596
|
+
* snapshot surfaces made on 2026-08-08.
|
|
19597
|
+
*
|
|
19598
|
+
* Served by the `event-media` plane, which resolves a raw MediaStore key and
|
|
19599
|
+
* is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
|
|
19600
|
+
* cookie, so no header plumbing is needed. The bytes then ride the browser's
|
|
19601
|
+
* HTTP cache with an ETag and `immutable`, instead of the WebSocket.
|
|
19602
|
+
*
|
|
19603
|
+
* Absent when the face has no stored crop, or when the addon has no data
|
|
19604
|
+
* plane — callers fall back to {@link base64}.
|
|
19605
|
+
*/
|
|
19606
|
+
cropUrl: zod.z.string().optional(),
|
|
19529
19607
|
/** Design B: the face bbox (pixel space) on the key frame — lets a detail
|
|
19530
19608
|
* view draw the box over the native `keyFrameMediaKey` frame. Absent on
|
|
19531
19609
|
* legacy rows written before design B. */
|
|
@@ -19601,7 +19679,23 @@ var faceGalleryCapability = {
|
|
|
19601
19679
|
}),
|
|
19602
19680
|
listRecentFaces: method(zod.z.object({
|
|
19603
19681
|
limit: zod.z.number().int().positive().optional(),
|
|
19604
|
-
filter: FaceFilterEnum.optional()
|
|
19682
|
+
filter: FaceFilterEnum.optional(),
|
|
19683
|
+
/**
|
|
19684
|
+
* Inline the base64 crop on every row. Default `true` — the existing
|
|
19685
|
+
* behaviour, kept so no caller breaks.
|
|
19686
|
+
*
|
|
19687
|
+
* Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
|
|
19688
|
+
* drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
|
|
19689
|
+
* the browser cache the images.
|
|
19690
|
+
*
|
|
19691
|
+
* **This is an INPUT field, so it does not reach the addon until the
|
|
19692
|
+
* next train.** The hub router validates cap inputs against its own
|
|
19693
|
+
* compiled Zod, which strips a key it does not know — verified today
|
|
19694
|
+
* on the OUTPUT side, where an additive field DOES arrive immediately
|
|
19695
|
+
* (`Track.hasFace`). Until the train ships, sending `false` is
|
|
19696
|
+
* harmless and simply keeps the crops inline.
|
|
19697
|
+
*/
|
|
19698
|
+
includeCrops: zod.z.boolean().optional()
|
|
19605
19699
|
}).optional(), zod.z.array(FaceInfoSchema).readonly()),
|
|
19606
19700
|
getFaceByTrack: method(zod.z.object({
|
|
19607
19701
|
deviceId: zod.z.number().int(),
|
|
@@ -15278,6 +15278,43 @@ var CameraRecordingStatusSchema = z.object({
|
|
|
15278
15278
|
storageBytes: z.number()
|
|
15279
15279
|
});
|
|
15280
15280
|
/**
|
|
15281
|
+
* The stages of the status fan-out that issue a BOUNDED remote read, and can
|
|
15282
|
+
* therefore be cut short. Every other block (`assignment`, `decoder`, `motion`,
|
|
15283
|
+
* `audio`) is composed from orchestrator-local state and cannot time out.
|
|
15284
|
+
*
|
|
15285
|
+
* `switches` is the operator's per-camera function switches — the read behind
|
|
15286
|
+
* `switchedOff`, not a block of its own.
|
|
15287
|
+
*/
|
|
15288
|
+
var CameraStatusStageSchema = z.enum([
|
|
15289
|
+
"source",
|
|
15290
|
+
"broker",
|
|
15291
|
+
"detection",
|
|
15292
|
+
"recording",
|
|
15293
|
+
"switches"
|
|
15294
|
+
]);
|
|
15295
|
+
/**
|
|
15296
|
+
* WHY a stage could not be trusted.
|
|
15297
|
+
*
|
|
15298
|
+
* - `timeout` — the stage never answered and the whole status call paid the
|
|
15299
|
+
* full per-stage bound for it. This is the one that used to be invisible.
|
|
15300
|
+
* - `error` — the source answered with a rejection.
|
|
15301
|
+
* - `partial` — the stage RETURNED, but from an incomplete set of sources: its
|
|
15302
|
+
* value is a floor, not a total. `switchedOff` derived from a partial switch
|
|
15303
|
+
* read can be empty while the operator has switches off.
|
|
15304
|
+
*/
|
|
15305
|
+
var CameraStatusDegradationReasonSchema = z.enum([
|
|
15306
|
+
"timeout",
|
|
15307
|
+
"error",
|
|
15308
|
+
"partial"
|
|
15309
|
+
]);
|
|
15310
|
+
/** One stage of the fan-out that could NOT be read, and how long it cost. */
|
|
15311
|
+
var CameraStatusDegradationSchema = z.object({
|
|
15312
|
+
stage: CameraStatusStageSchema,
|
|
15313
|
+
reason: CameraStatusDegradationReasonSchema,
|
|
15314
|
+
/** Wall-clock ms spent on the stage before it was abandoned. */
|
|
15315
|
+
elapsedMs: z.number()
|
|
15316
|
+
});
|
|
15317
|
+
/**
|
|
15281
15318
|
* Aggregated per-camera pipeline status — server-composed, single call.
|
|
15282
15319
|
*
|
|
15283
15320
|
* The `assignment` and `source` blocks are always present.
|
|
@@ -15307,9 +15344,28 @@ var CameraStatusSchema = z.object({
|
|
|
15307
15344
|
* differently — a quiet camera that looks identical to a dead one is the
|
|
15308
15345
|
* silence-reads-as-never-happened trap this repo keeps paying for.
|
|
15309
15346
|
*
|
|
15310
|
-
* Empty when nothing is off
|
|
15347
|
+
* Empty when nothing is off, and never contains a switch no provider offers
|
|
15348
|
+
* — but an empty list is only a POSITIVE claim when `degraded` does not name
|
|
15349
|
+
* `'switches'`. When it does, the switch set could not be read and nothing
|
|
15350
|
+
* here may be rendered as "the operator turned nothing off": that is the
|
|
15351
|
+
* D62 failure (a camera we could not read painted as broken) in the very
|
|
15352
|
+
* field that exists to prevent it.
|
|
15311
15353
|
*/
|
|
15312
15354
|
switchedOff: z.array(CameraSwitchIdSchema).readonly(),
|
|
15355
|
+
/**
|
|
15356
|
+
* Stages of the bounded fan-out that were CUT SHORT — a timeout or a
|
|
15357
|
+
* rejection — and whose block is therefore `null` because we could not
|
|
15358
|
+
* READ it, not because there is nothing there.
|
|
15359
|
+
*
|
|
15360
|
+
* Without this, three different facts arrive as the same `null`: "the stage
|
|
15361
|
+
* timed out", "the stage failed", and "this camera legitimately has no
|
|
15362
|
+
* decoder / no recording". Every surface that draws a conclusion from a null
|
|
15363
|
+
* block (or from an empty `switchedOff`) must consult this first; a stage
|
|
15364
|
+
* named here supports no conclusion at all, only "unknown".
|
|
15365
|
+
*
|
|
15366
|
+
* Empty on a clean read — the overwhelmingly common case.
|
|
15367
|
+
*/
|
|
15368
|
+
degraded: z.array(CameraStatusDegradationSchema).readonly(),
|
|
15313
15369
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
15314
15370
|
fetchedAt: z.number()
|
|
15315
15371
|
});
|
|
@@ -19525,7 +19581,29 @@ var FaceInfoSchema = z.object({
|
|
|
19525
19581
|
recognizedIdentityId: z.string().optional(),
|
|
19526
19582
|
identityName: z.string().optional(),
|
|
19527
19583
|
assigned: z.boolean(),
|
|
19584
|
+
/**
|
|
19585
|
+
* The crop, inline, base64.
|
|
19586
|
+
*
|
|
19587
|
+
* **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
|
|
19588
|
+
* field alone is ~2.87 MiB, re-sent in full on every operator assign and
|
|
19589
|
+
* every 30 s poll, base64-inflated over the msgpack socket and held in the
|
|
19590
|
+
* query heap. It stays for callers that have not migrated; `includeCrops:
|
|
19591
|
+
* false` turns it off once they have.
|
|
19592
|
+
*/
|
|
19528
19593
|
base64: z.string().optional(),
|
|
19594
|
+
/**
|
|
19595
|
+
* Same crop, as a data-plane URL for `<img src>` — the move the admin
|
|
19596
|
+
* snapshot surfaces made on 2026-08-08.
|
|
19597
|
+
*
|
|
19598
|
+
* Served by the `event-media` plane, which resolves a raw MediaStore key and
|
|
19599
|
+
* is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
|
|
19600
|
+
* cookie, so no header plumbing is needed. The bytes then ride the browser's
|
|
19601
|
+
* HTTP cache with an ETag and `immutable`, instead of the WebSocket.
|
|
19602
|
+
*
|
|
19603
|
+
* Absent when the face has no stored crop, or when the addon has no data
|
|
19604
|
+
* plane — callers fall back to {@link base64}.
|
|
19605
|
+
*/
|
|
19606
|
+
cropUrl: z.string().optional(),
|
|
19529
19607
|
/** Design B: the face bbox (pixel space) on the key frame — lets a detail
|
|
19530
19608
|
* view draw the box over the native `keyFrameMediaKey` frame. Absent on
|
|
19531
19609
|
* legacy rows written before design B. */
|
|
@@ -19601,7 +19679,23 @@ var faceGalleryCapability = {
|
|
|
19601
19679
|
}),
|
|
19602
19680
|
listRecentFaces: method(z.object({
|
|
19603
19681
|
limit: z.number().int().positive().optional(),
|
|
19604
|
-
filter: FaceFilterEnum.optional()
|
|
19682
|
+
filter: FaceFilterEnum.optional(),
|
|
19683
|
+
/**
|
|
19684
|
+
* Inline the base64 crop on every row. Default `true` — the existing
|
|
19685
|
+
* behaviour, kept so no caller breaks.
|
|
19686
|
+
*
|
|
19687
|
+
* Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
|
|
19688
|
+
* drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
|
|
19689
|
+
* the browser cache the images.
|
|
19690
|
+
*
|
|
19691
|
+
* **This is an INPUT field, so it does not reach the addon until the
|
|
19692
|
+
* next train.** The hub router validates cap inputs against its own
|
|
19693
|
+
* compiled Zod, which strips a key it does not know — verified today
|
|
19694
|
+
* on the OUTPUT side, where an additive field DOES arrive immediately
|
|
19695
|
+
* (`Track.hasFace`). Until the train ships, sending `false` is
|
|
19696
|
+
* harmless and simply keeps the crops inline.
|
|
19697
|
+
*/
|
|
19698
|
+
includeCrops: z.boolean().optional()
|
|
19605
19699
|
}).optional(), z.array(FaceInfoSchema).readonly()),
|
|
19606
19700
|
getFaceByTrack: method(z.object({
|
|
19607
19701
|
deviceId: z.number().int(),
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-coding for addon-served static assets: `Accept-Encoding` negotiation,
|
|
3
|
+
* an eligibility rule, and a BOUNDED cache of the compressed bytes.
|
|
4
|
+
*
|
|
5
|
+
* ## Why the ADDON owns the encoding
|
|
6
|
+
*
|
|
7
|
+
* The hub's `/addon/:addonId/*` bridge is registered `{ compress: false }`. That
|
|
8
|
+
* is not an oversight — on 2026-07-16 the global `@fastify/compress` was found
|
|
9
|
+
* emitting an EMPTY brotli stream (`content-encoding: br`, `content-length: 0`)
|
|
10
|
+
* for any compressible body over its 1 KiB threshold once the body had crossed
|
|
11
|
+
* the UDS route bridge of a FORKED addon. It silently broke the notifier SVG
|
|
12
|
+
* icons; the deploy-bundle pull route had already dodged the same class with
|
|
13
|
+
* `compress: false`. The resolution then, and the contract now, is that an addon
|
|
14
|
+
* route owns its content-coding END TO END: it negotiates, it compresses, it
|
|
15
|
+
* stamps `content-encoding` + `Vary`, and the hub pipes those bytes through
|
|
16
|
+
* untouched (`proxyToUpstream` replays the upstream headers verbatim).
|
|
17
|
+
*
|
|
18
|
+
* So compression belongs HERE, in the shared file data-plane, and nowhere above
|
|
19
|
+
* it. Anything that re-compresses on top of this reintroduces the empty-brotli
|
|
20
|
+
* bug rather than doubling the ratio.
|
|
21
|
+
*
|
|
22
|
+
* ## Why the bytes are cached, and why the cache is bounded
|
|
23
|
+
*
|
|
24
|
+
* Measured on the stream-broker embed bundle (`index-<hash>.js`, 1,443,142 B):
|
|
25
|
+
* gzip-6 → 360,868 B, brotli-5 → 327,419 B in 27 ms, brotli-11 → 293,677 B in
|
|
26
|
+
* 2,233 ms. Quality 5 is the request-path choice: 77% off the wire for 27 ms
|
|
27
|
+
* paid ONCE. Quality 11 buys another 2.3% for eighty times the CPU, on a path
|
|
28
|
+
* where a real client is waiting.
|
|
29
|
+
*
|
|
30
|
+
* The cache is an LRU bounded by BOTH entry count and total bytes, and it
|
|
31
|
+
* refuses assets over {@link COMPRESSION_MAX_ASSET_BYTES} outright. An unbounded
|
|
32
|
+
* `Map` keyed by URL is how a long-lived process walks into an OOM, and this
|
|
33
|
+
* repo has already spent a session on one.
|
|
34
|
+
*
|
|
35
|
+
* The key carries `mtimeMs` and `size`, so a rebuild that rewrites an asset in
|
|
36
|
+
* place under an UNCHANGED name (an SPA shell, a service worker) can never be
|
|
37
|
+
* served from the previous build's bytes.
|
|
38
|
+
*/
|
|
39
|
+
/** The codings this layer will produce. Identity is the absence of one. */
|
|
40
|
+
export type ContentEncoding = 'br' | 'gzip';
|
|
41
|
+
/**
|
|
42
|
+
* Below this the framing and the `Vary` cache split cost more than the
|
|
43
|
+
* compression saves. Same threshold the hub's global compressor is registered
|
|
44
|
+
* with, deliberately.
|
|
45
|
+
*/
|
|
46
|
+
export declare const COMPRESSION_MIN_BYTES = 1024;
|
|
47
|
+
/**
|
|
48
|
+
* Above this an asset is streamed identity and never buffered. Compression
|
|
49
|
+
* requires the whole body in RAM twice; a multi-hundred-MB recording segment
|
|
50
|
+
* must not be able to do that, whatever its content type says.
|
|
51
|
+
*/
|
|
52
|
+
export declare const COMPRESSION_MAX_ASSET_BYTES: number;
|
|
53
|
+
/** Is a body of this content type worth compressing at all? */
|
|
54
|
+
export declare function isCompressibleContentType(contentType: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Pick a coding for this request: brotli when offered, else gzip, else identity.
|
|
57
|
+
*
|
|
58
|
+
* Returns `null` for identity — including when the header is ABSENT. A client
|
|
59
|
+
* that offers nothing gets the raw bytes; guessing on its behalf is how a
|
|
60
|
+
* non-negotiating consumer (a native fetch, a probe, an OTA puller) ends up with
|
|
61
|
+
* a body it cannot read.
|
|
62
|
+
*/
|
|
63
|
+
export declare function negotiateContentEncoding(acceptEncoding: string | undefined): ContentEncoding | null;
|
|
64
|
+
/** Identity of one compressed body. `mtimeMs`+`size` make a rebuild-in-place a MISS. */
|
|
65
|
+
export interface CompressedAssetKey {
|
|
66
|
+
readonly path: string;
|
|
67
|
+
readonly mtimeMs: number;
|
|
68
|
+
readonly size: number;
|
|
69
|
+
readonly encoding: ContentEncoding;
|
|
70
|
+
}
|
|
71
|
+
/** Counters for the cache. Exported so a test can prove the compressor ran ONCE. */
|
|
72
|
+
export interface AssetCompressionStats {
|
|
73
|
+
/** Bodies actually run through zlib. */
|
|
74
|
+
readonly compressions: number;
|
|
75
|
+
/** Requests served from already-compressed bytes. */
|
|
76
|
+
readonly hits: number;
|
|
77
|
+
readonly entries: number;
|
|
78
|
+
readonly bytes: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Compressed bytes for one asset, compressing at most once per key.
|
|
82
|
+
*
|
|
83
|
+
* `null` means "send the original" — the asset is too large to buffer, the
|
|
84
|
+
* source could not be read, or compression did not actually shrink it. Every
|
|
85
|
+
* failure degrades to identity, so this can only ever be an optimisation.
|
|
86
|
+
*
|
|
87
|
+
* Concurrent callers for the same key share ONE compression: a cold cache hit by
|
|
88
|
+
* a dozen parallel asset requests must not run zlib a dozen times.
|
|
89
|
+
*/
|
|
90
|
+
export declare function getCompressedAsset(key: CompressedAssetKey, readSource: () => Promise<Buffer>): Promise<Buffer | null>;
|
|
91
|
+
export declare function getAssetCompressionStats(): AssetCompressionStats;
|
|
92
|
+
/** Test seam — the cache and its counters are process-wide. */
|
|
93
|
+
export declare function resetAssetCompressionCache(): void;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("./dist-
|
|
4
|
-
const require_model_download_service = require("./model-download-service-
|
|
5
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
3
|
+
const require_dist = require("./dist-CIkGOAcz.js");
|
|
4
|
+
const require_model_download_service = require("./model-download-service-Caj1kaSi.js");
|
|
5
|
+
const require_manifest_python_deps = require("./manifest-python-deps-BE6l8Y4W.js");
|
|
6
6
|
const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
|
|
7
7
|
const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
|
|
8
8
|
const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
|
|
@@ -41,10 +41,10 @@ let node_fs$1 = require_chunk.__toESM(node_fs, 1);
|
|
|
41
41
|
node_fs = require_chunk.__toESM(node_fs);
|
|
42
42
|
let node_path = require("node:path");
|
|
43
43
|
node_path = require_chunk.__toESM(node_path, 1);
|
|
44
|
-
let _camstack_types_addon = require("@camstack/types/addon");
|
|
45
|
-
let node_child_process = require("node:child_process");
|
|
46
44
|
let node_util = require("node:util");
|
|
47
45
|
node_util = require_chunk.__toESM(node_util);
|
|
46
|
+
let _camstack_types_addon = require("@camstack/types/addon");
|
|
47
|
+
let node_child_process = require("node:child_process");
|
|
48
48
|
let node_os = require("node:os");
|
|
49
49
|
node_os = require_chunk.__toESM(node_os);
|
|
50
50
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -139,6 +139,30 @@ var HEAP_WATCH_INTERVAL_MS = 6e4;
|
|
|
139
139
|
* the fatal by more than one interval under realistic growth. */
|
|
140
140
|
var HEAP_WATCH_WARN_RATIO = .8;
|
|
141
141
|
/**
|
|
142
|
+
* Escalate above this share of the ceiling.
|
|
143
|
+
*
|
|
144
|
+
* 0.70 is 3002MB: clear of the measured routine max (2847MB) so steady state
|
|
145
|
+
* never escalates, and below {@link HEAP_WATCH_WARN_RATIO} so the fast cadence
|
|
146
|
+
* is already running by the time the warn is reachable.
|
|
147
|
+
*/
|
|
148
|
+
var HEAP_WATCH_ESCALATE_RATIO = .7;
|
|
149
|
+
/**
|
|
150
|
+
* Release escalation below this share.
|
|
151
|
+
*
|
|
152
|
+
* Hysteresis, not a second threshold: hub-main's heap sawtooths continuously,
|
|
153
|
+
* so a single ratio would flap the mode on every GC. 0.60 is 2573MB — a real
|
|
154
|
+
* retreat rather than the trough of one collection.
|
|
155
|
+
*/
|
|
156
|
+
var HEAP_WATCH_DEESCALATE_RATIO = .6;
|
|
157
|
+
/**
|
|
158
|
+
* The cadence state machine, pure so the thresholds are testable without
|
|
159
|
+
* allocating three gigabytes to reach them.
|
|
160
|
+
*/
|
|
161
|
+
function nextMode(current, usedRatio, escalateRatio = HEAP_WATCH_ESCALATE_RATIO, deescalateRatio = HEAP_WATCH_DEESCALATE_RATIO) {
|
|
162
|
+
if (current === "escalated") return usedRatio < deescalateRatio ? "steady" : "escalated";
|
|
163
|
+
return usedRatio >= escalateRatio ? "escalated" : "steady";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
142
166
|
* Reclaim once the process holds this many MB beyond what it is USING.
|
|
143
167
|
*
|
|
144
168
|
* Live calibration, both readings from the same hub minutes apart: 2491MB
|
|
@@ -229,6 +253,9 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
229
253
|
const now = reclaimOptions?.now ?? (() => Date.now());
|
|
230
254
|
const triggerMb = reclaimOptions?.triggerMb ?? 1536;
|
|
231
255
|
const minIntervalMs = reclaimOptions?.minIntervalMs ?? 6e5;
|
|
256
|
+
const fastIntervalMs = reclaimOptions?.fastIntervalMs ?? 2e3;
|
|
257
|
+
const escalateRatio = reclaimOptions?.escalateRatio ?? .7;
|
|
258
|
+
const deescalateRatio = reclaimOptions?.deescalateRatio ?? .6;
|
|
232
259
|
let lastReclaimAt = Number.NEGATIVE_INFINITY;
|
|
233
260
|
const read = () => {
|
|
234
261
|
const limit = reclaimOptions?.heapLimitBytes ?? node_v8.getHeapStatistics().heap_size_limit;
|
|
@@ -249,16 +276,43 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
249
276
|
const after = read();
|
|
250
277
|
sink.info(`[mem] reclaim ${label} stranded=${strandedMb(sample)}MB rss=${sample.rssMb}MB→${after.rssMb}MB freed=${sample.rssMb - after.rssMb}MB arrayBuffers=${sample.arrayBuffersMb}MB→${after.arrayBuffersMb}MB took=${now() - startedAt}ms`);
|
|
251
278
|
};
|
|
279
|
+
let mode = "steady";
|
|
280
|
+
let lastLoggedAt = Number.NEGATIVE_INFINITY;
|
|
281
|
+
/**
|
|
282
|
+
* PROBING and LOGGING are separate cadences, and that separation is the whole
|
|
283
|
+
* point.
|
|
284
|
+
*
|
|
285
|
+
* The 2026-08-08 fatal escaped a 60s heartbeat because the burst began and
|
|
286
|
+
* ended between two samples. Deciding "is the heap elevated?" only when a
|
|
287
|
+
* line is due would reproduce that exactly: the process would still be read
|
|
288
|
+
* once a minute, and a 62-second climb would still be invisible. So the timer
|
|
289
|
+
* runs at the FAST period always — `process.memoryUsage()` is tens of
|
|
290
|
+
* microseconds, i.e. free at this rate — and the slow period governs only how
|
|
291
|
+
* often a steady process is allowed to say so.
|
|
292
|
+
*
|
|
293
|
+
* Never probe slower than the log cadence, or a caller passing a short
|
|
294
|
+
* `intervalMs` would silently get the long one.
|
|
295
|
+
*/
|
|
296
|
+
const probeIntervalMs = Math.min(fastIntervalMs, intervalMs);
|
|
252
297
|
const tick = () => {
|
|
253
298
|
try {
|
|
254
299
|
const sample = read();
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
300
|
+
const previous = mode;
|
|
301
|
+
mode = nextMode(previous, sample.usedRatio, escalateRatio, deescalateRatio);
|
|
302
|
+
const at = now();
|
|
303
|
+
const due = at - lastLoggedAt >= intervalMs;
|
|
304
|
+
if (mode === "escalated" || due) {
|
|
305
|
+
lastLoggedAt = at;
|
|
306
|
+
const line = format$1(label, sample);
|
|
307
|
+
if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
|
|
308
|
+
else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
|
|
309
|
+
else sink.info(line);
|
|
310
|
+
}
|
|
311
|
+
if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine — logging every ${intervalMs}ms`);
|
|
258
312
|
maybeReclaim(sample);
|
|
259
313
|
} catch {}
|
|
260
314
|
};
|
|
261
|
-
const timer = setInterval(tick,
|
|
315
|
+
const timer = setInterval(tick, probeIntervalMs);
|
|
262
316
|
timer.unref?.();
|
|
263
317
|
tick();
|
|
264
318
|
let stopped = false;
|