@kymoscope/sdk 0.3.0 → 0.4.0
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/assets/{importer.worker-Drm9u2Rc.js → importer.worker-CLBGjz4d.js} +1 -1
- package/dist/assets/{offscreenLineWorker-mShhnyXX.js → offscreenLineWorker-BLxay-Po.js} +2 -2
- package/dist/importers-B6rkbTwQ.js +5 -0
- package/dist/keys-KE0GTKAF.js +10 -0
- package/dist/kymoscope-embed.js +91 -90
- package/dist/kymoscope-sdk.js +86 -84
- package/dist/rules/kyr1.schema.json +135 -0
- package/dist/rules.js +2 -0
- package/dist/styles/charts.css +191 -0
- package/dist/threshold-C9it0Y7P.js +2 -0
- package/dist/types/charts/AnnotationLayer.d.ts +7 -2
- package/dist/types/charts/CameraPanel.d.ts +43 -0
- package/dist/types/charts/ChartBody.d.ts +12 -0
- package/dist/types/charts/beacon.d.ts +4 -1
- package/dist/types/charts/cameraLayout.d.ts +10 -0
- package/dist/types/charts/liveOverlay.d.ts +14 -0
- package/dist/types/charts/runtime.d.ts +12 -1
- package/dist/types/charts/useWorksheetData.d.ts +1 -5
- package/dist/types/components/GaugeChart.d.ts +2 -0
- package/dist/types/components/MultiBandCanvasChart.d.ts +4 -2
- package/dist/types/components/dashboardGridLayout.d.ts +1 -1
- package/dist/types/components/multiBandHover.d.ts +7 -0
- package/dist/types/engine/alerts.d.ts +56 -0
- package/dist/types/engine/columns.d.ts +16 -0
- package/dist/types/engine/createEngine.d.ts +60 -2
- package/dist/types/engine/derive.d.ts +1 -1
- package/dist/types/engine/errors.d.ts +1 -1
- package/dist/types/engine/liveController.d.ts +11 -1
- package/dist/types/engine/types.d.ts +41 -2
- package/dist/types/index.d.ts +26 -3
- package/dist/types/lib/annotations.d.ts +20 -0
- package/dist/types/lib/beacon.d.ts +2 -1
- package/dist/types/lib/data/columnarDataSource.d.ts +1 -0
- package/dist/types/lib/data/dataSource.d.ts +3 -0
- package/dist/types/lib/data/duckDbDataSource.d.ts +1 -0
- package/dist/types/lib/data/historyDataSource.d.ts +51 -0
- package/dist/types/lib/data/inMemoryDataSource.d.ts +1 -0
- package/dist/types/lib/data/liveDataSource.d.ts +20 -1
- package/dist/types/lib/data/pointColumns.d.ts +11 -0
- package/dist/types/lib/duckdb/duckdbInstance.d.ts +5 -0
- package/dist/types/lib/historian/blockCache.d.ts +32 -0
- package/dist/types/lib/historian/export.d.ts +31 -0
- package/dist/types/lib/historian/khd1.d.ts +41 -0
- package/dist/types/lib/historian/parquet.d.ts +21 -0
- package/dist/types/lib/historian/reader.d.ts +73 -0
- package/dist/types/lib/live/blockWire.d.ts +16 -0
- package/dist/types/lib/live/buffer.d.ts +34 -0
- package/dist/types/lib/live/cameraSender.d.ts +70 -0
- package/dist/types/lib/live/clockMap.d.ts +20 -0
- package/dist/types/lib/live/crypto.d.ts +7 -0
- package/dist/types/lib/live/publisher.d.ts +163 -0
- package/dist/types/lib/live/types.d.ts +19 -1
- package/dist/types/lib/live/videoTrack.d.ts +96 -0
- package/dist/types/lib/live/videoWire.d.ts +35 -0
- package/dist/types/lib/live/wire.d.ts +4 -0
- package/dist/types/lib/panelChartSettings.d.ts +40 -1
- package/dist/types/lib/recordings/export.d.ts +1 -0
- package/dist/types/lib/recordings/videoSource.d.ts +56 -0
- package/dist/types/lib/render/alertGlyph.d.ts +7 -0
- package/dist/types/lib/render/glSeries.d.ts +1 -1
- package/dist/types/lib/segments/align.d.ts +85 -0
- package/dist/types/lib/segments/liveAxis.d.ts +58 -0
- package/dist/types/lib/segments/rules.d.ts +77 -0
- package/dist/types/lib/workspaces/io.d.ts +6 -0
- package/dist/types/lib/yAxisTicks.d.ts +11 -0
- package/dist/types/rules/compile.d.ts +47 -0
- package/dist/types/rules/evaluator.d.ts +51 -0
- package/dist/types/rules/index.d.ts +4 -0
- package/dist/types/rules/threshold.d.ts +3 -0
- package/dist/types/rules/types.d.ts +89 -0
- package/dist/types/rules/validate.d.ts +6 -0
- package/dist/types/types/loadedDataset.d.ts +5 -0
- package/dist/types/types/plotSeries.d.ts +5 -0
- package/package.json +7 -2
- package/dist/importers-DHeUdZcP.js +0 -5
- package/dist/krc-BIfw0rX1.js +0 -9
|
@@ -5,3 +5,8 @@ import * as duckdb from '@duckdb/duckdb-wasm';
|
|
|
5
5
|
export declare function wasmModuleUrl(url: string): Promise<string>;
|
|
6
6
|
/** Lazily instantiate a single DuckDB-WASM database for this tab. */
|
|
7
7
|
export declare function getDuckDb(): Promise<duckdb.AsyncDuckDB>;
|
|
8
|
+
/** The DuckDB of getDuckDb() with the Parquet extension loaded (the browser build doesn't include it). It is loaded
|
|
9
|
+
* from this site (`/duckdb-ext/v<version>/wasm_eh/parquet.duckdb_extension.wasm`, DuckDB's own signed file copied
|
|
10
|
+
* into public/), never from extensions.duckdb.org. A failed load is retried on the next call.
|
|
11
|
+
* ponytail: same origin as the page; an SDK host that serves the app elsewhere copies that folder too. */
|
|
12
|
+
export declare function getParquetDuckDb(): Promise<duckdb.AsyncDuckDB>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History bytes cached in the origin-private file system (spec D2; plan P4): "<segmentId>/footer" and
|
|
3
|
+
* "<segmentId>/<block>". It keeps the bytes as stored (sealed): the device holds ciphertext only, and a segment id
|
|
4
|
+
* never changes content (a new version is a new segment), so nothing is invalidated. Past the budget (min(2 GiB,
|
|
5
|
+
* 20 % of the origin's quota)) the oldest-written files go, down to 90 %. Without OPFS (old browsers, some private
|
|
6
|
+
* windows) every call is a no-op. ponytail: eviction by write time (FIFO); track reads (LRU) if old ranges miss too often.
|
|
7
|
+
* ponytail: each tab keeps its own index and total, so several tabs writing at once can overshoot the budget until one
|
|
8
|
+
* of them rescans (next page load); share the index through a BroadcastChannel or a Web Lock if that ever matters.
|
|
9
|
+
*/
|
|
10
|
+
export interface BlockCache {
|
|
11
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
12
|
+
put(key: string, bytes: Uint8Array): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** The part of FileSystemDirectoryHandle the cache uses (a fake in tests). */
|
|
15
|
+
export interface CacheDir {
|
|
16
|
+
getFileHandle(name: string, o?: {
|
|
17
|
+
create?: boolean;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
getFile(): Promise<File>;
|
|
20
|
+
createWritable(): Promise<{
|
|
21
|
+
write(b: BufferSource): Promise<void>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
abort?(): Promise<void>;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
removeEntry(name: string): Promise<void>;
|
|
27
|
+
entries(): AsyncIterable<[string, {
|
|
28
|
+
kind: string;
|
|
29
|
+
getFile?: () => Promise<File>;
|
|
30
|
+
}]>;
|
|
31
|
+
}
|
|
32
|
+
export declare function blockCache(root?: () => Promise<CacheDir | null>, budget?: () => Promise<number>): BlockCache;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExportPart } from '../recordings/export';
|
|
2
|
+
import { type HistoryPiece } from '../data/historyDataSource';
|
|
3
|
+
import { type HistoryReadOptions, type HistorySegmentRef } from './reader';
|
|
4
|
+
export type HistoryExportFormat = 'csv' | 'mf4' | 'parquet';
|
|
5
|
+
/** ponytail: wide tables over unaligned tag groups grow as times x channels (float32 cells); Parquet (long) has no such limit. */
|
|
6
|
+
export declare const MAX_WIDE_CELLS = 25000000;
|
|
7
|
+
/** ponytail: the Parquet file is built in DuckDB-WASM's memory (~4 GB address space); stream row groups out if larger
|
|
8
|
+
* exports are needed. */
|
|
9
|
+
export declare const MAX_PARQUET_VALUES = 30000000;
|
|
10
|
+
/** The shortest decimal that reads back as the same float32 (0.1, not 0.10000000149011612). */
|
|
11
|
+
export declare function f32text(v: number): string;
|
|
12
|
+
/** ISO 8601 UTC with microseconds. */
|
|
13
|
+
export declare function isoMicros(t: number): string;
|
|
14
|
+
/** One row per distinct time across `channels`, NaN where a channel has none. */
|
|
15
|
+
export declare function wideDay(pieces: HistoryPiece[], channels: string[]): {
|
|
16
|
+
t: Float64Array;
|
|
17
|
+
cols: Float32Array[];
|
|
18
|
+
};
|
|
19
|
+
export declare function exportHistoryTo(o: Omit<HistoryReadOptions, 'level'>, format: HistoryExportFormat, write: (part: ExportPart) => void | Promise<void>, opts?: {
|
|
20
|
+
onProgress?: (fraction: number) => void;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
skippedDays: string[];
|
|
23
|
+
missingDays: string[];
|
|
24
|
+
}>;
|
|
25
|
+
/** Days of raw values into one Parquet file: appended to DuckDB `chunk` values at a time, at most `max` values. */
|
|
26
|
+
export declare function parquetTo(raw: HistorySegmentRef[], day: (s: HistorySegmentRef) => Promise<{
|
|
27
|
+
pieces: HistoryPiece[];
|
|
28
|
+
}>, channels: string[], write: (part: ExportPart) => void | Promise<void>, step: (i: number) => void, limits?: {
|
|
29
|
+
max: number;
|
|
30
|
+
chunk: number;
|
|
31
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const KHD1_TRAILER_BYTES = 9;
|
|
2
|
+
export type HistoryKind = 'raw' | 'rollup';
|
|
3
|
+
export type HistoryLevel = 0 | 60 | 3600;
|
|
4
|
+
export interface KhdChannel {
|
|
5
|
+
n: string;
|
|
6
|
+
u: string;
|
|
7
|
+
}
|
|
8
|
+
export interface KhdBlock {
|
|
9
|
+
ch: number[];
|
|
10
|
+
t0: number;
|
|
11
|
+
t1: number;
|
|
12
|
+
off: number;
|
|
13
|
+
len: number;
|
|
14
|
+
rows: number;
|
|
15
|
+
values: number;
|
|
16
|
+
}
|
|
17
|
+
export interface KhdFooter {
|
|
18
|
+
format: 1;
|
|
19
|
+
stream: string;
|
|
20
|
+
day: string;
|
|
21
|
+
tz: string;
|
|
22
|
+
kind: HistoryKind;
|
|
23
|
+
level: HistoryLevel;
|
|
24
|
+
t0: number;
|
|
25
|
+
t1: number;
|
|
26
|
+
channels: KhdChannel[];
|
|
27
|
+
blocks: KhdBlock[];
|
|
28
|
+
}
|
|
29
|
+
/** Which file of a history: what the AAD of its blocks and footer binds. */
|
|
30
|
+
export interface KhdFileId {
|
|
31
|
+
stream: string;
|
|
32
|
+
day: string;
|
|
33
|
+
kind: HistoryKind;
|
|
34
|
+
level: HistoryLevel;
|
|
35
|
+
}
|
|
36
|
+
export declare const blockAad: (f: KhdFileId, index: number | "footer") => string;
|
|
37
|
+
/** The sealed footer's length, from bytes that end where the file ends (at least the 9-byte trailer). */
|
|
38
|
+
export declare function footerLength(tail: Uint8Array): number;
|
|
39
|
+
/** One sealed block (or the footer) opened; WebCrypto throws on a wrong key, file or index. */
|
|
40
|
+
export declare function openSealed(key: CryptoKey, sealed: Uint8Array, f: KhdFileId, index: number | 'footer'): Promise<Uint8Array>;
|
|
41
|
+
export declare function openFooter(key: CryptoKey, sealed: Uint8Array, f: KhdFileId): Promise<KhdFooter>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RawBlock {
|
|
2
|
+
t: Float64Array;
|
|
3
|
+
cols: Float32Array[];
|
|
4
|
+
}
|
|
5
|
+
export interface RollupBlock {
|
|
6
|
+
t: Float64Array;
|
|
7
|
+
ch: Int32Array;
|
|
8
|
+
min: Float32Array;
|
|
9
|
+
max: Float32Array;
|
|
10
|
+
}
|
|
11
|
+
/** Loads DuckDB-WASM ahead of the first read (the History dialog calls it while the user picks). */
|
|
12
|
+
/** Loads DuckDB and runs one tiny Parquet round trip, so the first real decode doesn't pay the first-query cost
|
|
13
|
+
* (0.3-0.7 s measured, historian-notes/phase4.md). Call it while the user picks channels. */
|
|
14
|
+
export declare function warmHistoryDecoder(): void;
|
|
15
|
+
/** A raw block's times (Unix seconds) and the columns of `channels` (footer indexes; NaN = no value). */
|
|
16
|
+
export declare function decodeRawBlock(bytes: Uint8Array, channels: number[]): Promise<RawBlock>;
|
|
17
|
+
/** Rollup blocks' rows of their `channels`, one result per block, sorted by (ch, t): one query for the whole batch. */
|
|
18
|
+
export declare function decodeRollupBlocks(blocks: Array<{
|
|
19
|
+
bytes: Uint8Array;
|
|
20
|
+
channels: number[];
|
|
21
|
+
}>): Promise<RollupBlock[]>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a time range of a history (spec D1; plan Formats "Reading a range"): one file per local day at the wanted
|
|
3
|
+
* level (else the nearest that day has), its footer (memo, cache, one suffix GET, or the whole file when small), then
|
|
4
|
+
* only the blocks that overlap the range and hold a wanted channel (neighbours in one GET), RANGE_PARALLEL requests at
|
|
5
|
+
* a time; opened with the history key and decoded into pieces. Days need no joining: each block carries its own times.
|
|
6
|
+
*/
|
|
7
|
+
import { type HistoryPiece } from '../data/historyDataSource';
|
|
8
|
+
import { type HistoryKind, type HistoryLevel, type KhdFooter } from './khd1';
|
|
9
|
+
import type { BlockCache } from './blockCache';
|
|
10
|
+
/** One verified file of a history (the latest version of its day, kind and level). */
|
|
11
|
+
export interface HistorySegmentRef {
|
|
12
|
+
id: string;
|
|
13
|
+
day: string;
|
|
14
|
+
kind: HistoryKind;
|
|
15
|
+
level: HistoryLevel;
|
|
16
|
+
bytes: number;
|
|
17
|
+
keyId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface HistoryReadOptions {
|
|
20
|
+
historyStreamId: string;
|
|
21
|
+
/** Segments of the days in range (any levels; the latest version of each). */
|
|
22
|
+
segments: HistorySegmentRef[];
|
|
23
|
+
t0: number;
|
|
24
|
+
t1: number;
|
|
25
|
+
channels: string[];
|
|
26
|
+
level: HistoryLevel;
|
|
27
|
+
key: (keyId: string) => Promise<CryptoKey>;
|
|
28
|
+
/** Bytes [start, end) of a segment's file. */
|
|
29
|
+
fetchRange: (segmentId: string, start: number, end: number, signal?: AbortSignal) => Promise<Uint8Array>;
|
|
30
|
+
cache?: BlockCache;
|
|
31
|
+
/** Opened footers by segment id, kept by the caller between reads of one history. */
|
|
32
|
+
footers?: Map<string, KhdFooter>;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** The segments of the same days listed again. A read that fails (a 404 or 403, or no URL for a segment) asks for
|
|
35
|
+
* it once: a newer version may have replaced a segment meanwhile (today's summaries every 15 min, a late-data
|
|
36
|
+
* rebuild), and the older one is deleted. When the listing picks other files the read runs again on them; when it
|
|
37
|
+
* picks the same, the error stands. */
|
|
38
|
+
relist?: () => Promise<HistorySegmentRef[]>;
|
|
39
|
+
/** false: blocks are read from `cache` but not added to it (an export would push out what charts reuse). Default true. */
|
|
40
|
+
cacheWrite?: boolean;
|
|
41
|
+
/** 'gap': a segment whose `fetchRange` throws an error with `code: 'missing'` (its file is gone from storage) is read
|
|
42
|
+
* as no data for its day, listed in `missingDays`, and the rest of the range is read. Default 'error': the read fails. */
|
|
43
|
+
onMissing?: 'error' | 'gap';
|
|
44
|
+
}
|
|
45
|
+
export interface HistoryRange {
|
|
46
|
+
pieces: HistoryPiece[];
|
|
47
|
+
units: Record<string, string>;
|
|
48
|
+
/** The level each day was read at (a day without the wanted level falls back). */
|
|
49
|
+
levels: Record<string, HistoryLevel>;
|
|
50
|
+
/** Days whose file is missing from storage (onMissing 'gap'): read as no data. */
|
|
51
|
+
missingDays: string[];
|
|
52
|
+
}
|
|
53
|
+
export declare const RANGE_PARALLEL = 16;
|
|
54
|
+
export declare const HISTORY_PIXELS = 2000;
|
|
55
|
+
/** The coarsest level giving at least half a bucket per pixel over `spanS` (plan P2). */
|
|
56
|
+
export declare function chooseLevel(spanS: number, pixels?: number): HistoryLevel;
|
|
57
|
+
/** One file per day, in day order: the wanted level, else the nearest the day has (raw expired, today, a failed upload). */
|
|
58
|
+
export declare function pickSegments(segments: HistorySegmentRef[], level: HistoryLevel): HistorySegmentRef[];
|
|
59
|
+
/** f over items, n at a time; results in order. */
|
|
60
|
+
export declare function pool<T, R>(items: T[], n: number, f: (x: T) => Promise<R>): Promise<R[]>;
|
|
61
|
+
/** Byte ranges covering `blocks`: neighbours closer than 64 KiB join, up to 8 MiB per range. */
|
|
62
|
+
export declare function coalesce(blocks: Array<{
|
|
63
|
+
off: number;
|
|
64
|
+
len: number;
|
|
65
|
+
}>): Array<{
|
|
66
|
+
start: number;
|
|
67
|
+
end: number;
|
|
68
|
+
}>;
|
|
69
|
+
type Reader = Pick<HistoryReadOptions, 'historyStreamId' | 'key' | 'fetchRange' | 'cache' | 'footers' | 'signal'>;
|
|
70
|
+
/** A segment's opened footer: the memo, else the cache, else the file (whole when ≤ 512 KiB, kept in `whole`). */
|
|
71
|
+
export declare function readFooter(o: Reader, s: HistorySegmentRef, whole?: Map<string, Uint8Array<ArrayBufferLike>>): Promise<KhdFooter>;
|
|
72
|
+
export declare function readHistoryRange(o: HistoryReadOptions): Promise<HistoryRange>;
|
|
73
|
+
export {};
|
|
@@ -18,6 +18,22 @@ export interface BlockSchema {
|
|
|
18
18
|
export declare function decodeBlockSchema(p: Uint8Array): BlockSchema;
|
|
19
19
|
/** KIND_BLOCK_SCHEMA plaintext with a given id (tests and local publishers; senders use the CRC-32 of the table). */
|
|
20
20
|
export declare function encodeBlockSchema(id: number, channels: BlockChannel[]): Uint8Array;
|
|
21
|
+
/** One channel of a KIND_LAST_VALUES: its last finite value and that value's time (stream seconds). */
|
|
22
|
+
export interface LastValue {
|
|
23
|
+
name: string;
|
|
24
|
+
unit: string;
|
|
25
|
+
t: number;
|
|
26
|
+
v: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The channels with a value of a decrypted KIND_LAST_VALUES plaintext (u8 version 1, u8 flags, u16 reserved,
|
|
30
|
+
* u32 schema id, u32 count, count f64 times then count f64 values, NaN time = none yet; little-endian), named by
|
|
31
|
+
* the schema it refers to; null until that schema has arrived. Throws RangeError if malformed.
|
|
32
|
+
* Normative format: docs/superpowers/specs/2026-10-26-last-values.md; pinned by tests/vectors/last-values-v1.json.
|
|
33
|
+
*/
|
|
34
|
+
export declare function decodeLastValues(p: Uint8Array, schemas: ReadonlyMap<number, BlockSchema>): LastValue[] | null;
|
|
35
|
+
/** KIND_LAST_VALUES plaintext of `t` / `v` in `schema`'s channel order, NaN for none (tests and local publishers). */
|
|
36
|
+
export declare function encodeLastValues(schema: BlockSchema, t: ArrayLike<number>, v: ArrayLike<number>): Uint8Array;
|
|
21
37
|
/** A decrypted KIND_BLOCK plaintext's rows; null until its schema has arrived. Throws RangeError if malformed. */
|
|
22
38
|
export declare function decodeBlock(p: Uint8Array, schemas: ReadonlyMap<number, BlockSchema>): Promise<{
|
|
23
39
|
block: LiveColumnsBlock;
|
|
@@ -79,6 +79,10 @@ export declare class LiveBuffer {
|
|
|
79
79
|
private readonly mirrors;
|
|
80
80
|
/** The last decimation of a window in the history (its samples don't change while it stays put). */
|
|
81
81
|
private historyDec;
|
|
82
|
+
/** The buckets of the row snapshot (see toDataset), kept across calls like timeDec. */
|
|
83
|
+
private snapDec;
|
|
84
|
+
/** Each column's newest finite value and its time, kept past the ring (see lastKnown). */
|
|
85
|
+
private latest;
|
|
82
86
|
/**
|
|
83
87
|
* `capacity`: samples kept (the oldest are dropped past it). Memory grows with
|
|
84
88
|
* the samples actually received; `maxBytes` caps it — once the columns would
|
|
@@ -273,4 +277,34 @@ export declare class LiveBuffer {
|
|
|
273
277
|
* keeps a sample selected until it ages out of the buffer.
|
|
274
278
|
*/
|
|
275
279
|
toDataset(maxRows?: number): Dataset;
|
|
280
|
+
/**
|
|
281
|
+
* At most ~`maxRows` rows of the ring (time ascending): per bucket of time, at its centre (the oldest and
|
|
282
|
+
* newest buckets at the oldest and newest sample's time), a row of each column's min and, where any differs,
|
|
283
|
+
* one of its max. The buckets are kept across calls and fold only the samples pushed since (see timeBuckets),
|
|
284
|
+
* on the absolute grid floor(t / bucket), so a snapshot refreshed while the ring rolls doesn't re-quantise.
|
|
285
|
+
* Null when every sample has the same time.
|
|
286
|
+
*/
|
|
287
|
+
private snapshotRows;
|
|
288
|
+
/** Min/max of `name`'s samples in the ring, or null when it has none (columnBounds reads that as 0..0). */
|
|
289
|
+
valueBounds(name: string): {
|
|
290
|
+
x0: number;
|
|
291
|
+
x1: number;
|
|
292
|
+
} | null;
|
|
293
|
+
/** Whether `name` has had a finite value (a sample or a last known value) since the stream started (or was
|
|
294
|
+
* cleared), in the ring or not. */
|
|
295
|
+
hasValues(name: string): boolean;
|
|
296
|
+
/** How many columns have had a finite value (moves when one gets its first). */
|
|
297
|
+
get valuedCount(): number;
|
|
298
|
+
/** The newest finite value of `name` and its time (a sample, or a last known value when newer), past the ring. */
|
|
299
|
+
lastKnown(name: string): {
|
|
300
|
+
t: number;
|
|
301
|
+
v: number;
|
|
302
|
+
} | undefined;
|
|
303
|
+
/**
|
|
304
|
+
* A channel's last value as the sender knows it (KIND_LAST_VALUES): not a sample (the ring, decimation, cycles
|
|
305
|
+
* and readers of samples never see it), only the value a quiet channel holds. Creates the column (no samples)
|
|
306
|
+
* so the channel is listed; a newer sample always wins.
|
|
307
|
+
*/
|
|
308
|
+
setLastKnown(name: string, unit: string, t: number, v: number): void;
|
|
309
|
+
private noteValue;
|
|
276
310
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { LivePublisher } from './publisher';
|
|
2
|
+
export interface CameraPreset {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
fps: number;
|
|
6
|
+
kbps: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const CAMERA_PRESETS: Record<'720p30' | '1080p30', CameraPreset>;
|
|
9
|
+
/** The low layer's default: what small panels and camera walls draw. */
|
|
10
|
+
export declare const LOW_PRESET: CameraPreset;
|
|
11
|
+
export interface CameraLayerStats {
|
|
12
|
+
fps: number;
|
|
13
|
+
kbps: number;
|
|
14
|
+
viewers: number;
|
|
15
|
+
encoding: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** `error`: the encoder's failure message (encoding has stopped), for the page to show; null while healthy.
|
|
18
|
+
* `low`: the low layer's, null without one (or when this browser can't encode it). */
|
|
19
|
+
export interface CameraStats extends CameraLayerStats {
|
|
20
|
+
dropped: number;
|
|
21
|
+
queue: number;
|
|
22
|
+
error: string | null;
|
|
23
|
+
low: CameraLayerStats | null;
|
|
24
|
+
}
|
|
25
|
+
export interface CameraSenderDeps {
|
|
26
|
+
VideoEncoder?: typeof VideoEncoder;
|
|
27
|
+
/** ms epoch */
|
|
28
|
+
now?: () => number;
|
|
29
|
+
/** The frame source (test seam); defaults to MediaStreamTrackProcessor or a hidden <video>. */
|
|
30
|
+
frames?: (track: MediaStreamTrack) => AsyncIterable<VideoFrame>;
|
|
31
|
+
/** The low layer's downscale (test seam); defaults to drawing onto an OffscreenCanvas. The caller closes the result. */
|
|
32
|
+
scale?: (frame: VideoFrame, width: number, height: number) => VideoFrame;
|
|
33
|
+
}
|
|
34
|
+
type Publisher = Pick<LivePublisher, 'send' | 'viewers' | 'onViewers' | 'limits'>;
|
|
35
|
+
export declare class CameraSender {
|
|
36
|
+
private readonly high;
|
|
37
|
+
private readonly lowOpt;
|
|
38
|
+
private low;
|
|
39
|
+
/** When the low layer last took a frame (ms, `now`), for its frame rate. */
|
|
40
|
+
private lowAt;
|
|
41
|
+
private readonly keyframeMs;
|
|
42
|
+
private readonly Encoder;
|
|
43
|
+
private readonly now;
|
|
44
|
+
private readonly frames;
|
|
45
|
+
private readonly scale;
|
|
46
|
+
private stopped;
|
|
47
|
+
private readonly statsFns;
|
|
48
|
+
private lastStats;
|
|
49
|
+
constructor(publisher: Publisher, opts: {
|
|
50
|
+
preset: CameraPreset;
|
|
51
|
+
keyframeS?: number;
|
|
52
|
+
low?: {
|
|
53
|
+
publisher: Publisher;
|
|
54
|
+
preset?: CameraPreset;
|
|
55
|
+
};
|
|
56
|
+
}, deps?: CameraSenderDeps);
|
|
57
|
+
/** Encode frames from `track` (a getUserMedia video track). Resolves once the encoder is configured;
|
|
58
|
+
* frames then flow until stop(). Throws KymoscopeError('camera_codec_unsupported'). A low layer this
|
|
59
|
+
* browser can't encode is left out (stats().low null); the camera still runs. */
|
|
60
|
+
start(track: MediaStreamTrack): Promise<void>;
|
|
61
|
+
private loop;
|
|
62
|
+
private onFrame;
|
|
63
|
+
stats(): CameraStats;
|
|
64
|
+
/** At most about once a second, while frames flow. */
|
|
65
|
+
onStats(fn: (s: CameraStats) => void): () => void;
|
|
66
|
+
private emitStats;
|
|
67
|
+
/** Stop encoding. The publishers and the track belong to the caller and keep running. */
|
|
68
|
+
stop(): void;
|
|
69
|
+
}
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-stream time → wall time (Unix seconds), for syncing a camera with a data stream.
|
|
3
|
+
* Senders that stamp samples with Unix seconds (t > 1e9) need no map: "exact". Others
|
|
4
|
+
* get an estimate: the minimum over a sliding window of (arrival − last sample time of
|
|
5
|
+
* the message), i.e. the clock offset plus the smallest network delay seen: "approx".
|
|
6
|
+
*/
|
|
7
|
+
export type ClockQuality = 'exact' | 'approx' | 'none';
|
|
8
|
+
export declare class ClockMap {
|
|
9
|
+
private samples;
|
|
10
|
+
private exact;
|
|
11
|
+
private windowS;
|
|
12
|
+
constructor(windowS?: number);
|
|
13
|
+
/** A data message arrived at wall time `arrival` (s) whose last sample has stream time `t`. */
|
|
14
|
+
observe(t: number, arrival: number): void;
|
|
15
|
+
quality(): ClockQuality;
|
|
16
|
+
/** Stream time → wall seconds; `offsetMs` is the user's manual correction. */
|
|
17
|
+
toWall(t: number, offsetMs?: number): number | null;
|
|
18
|
+
/** Wall seconds → stream time (to draw the "frame shown" marker on a chart). */
|
|
19
|
+
toStream(wall: number, offsetMs?: number): number | null;
|
|
20
|
+
}
|
|
@@ -117,6 +117,13 @@ export declare function deviceFingerprint(publicKey: string): Promise<string>;
|
|
|
117
117
|
export declare const WORKBOOK_EXPORT_AAD = "kymoscope-workbook-export-v2";
|
|
118
118
|
export declare const workbookBodyAad: (workbookId: string, version: number) => string;
|
|
119
119
|
export declare const workbookKeyAad: (workbookId: string, projectId: string, epoch: number) => string;
|
|
120
|
+
/** A collector's day-history key sealed under the project key (resource_keys 'history_key'). Python twin:
|
|
121
|
+
* kymoscope.crypto.history_key_aad. */
|
|
122
|
+
export declare const historyKeyAad: (keyId: string, projectId: string, epoch: number) => string;
|
|
123
|
+
/** A project's config key (alert rules) sealed under the project key: resource_keys 'project_config', id = the project. */
|
|
124
|
+
export declare const projectConfigKeyAad: (projectId: string, epoch: number) => string;
|
|
125
|
+
/** An alert rule's kyr/1 body sealed under its project's config key: bound to the rule, its version and project. */
|
|
126
|
+
export declare const alertRuleAad: (ruleId: string, version: number, projectId: string) => string;
|
|
120
127
|
export declare const recoveryKeyAad: (orgId: string, deviceId: string) => string;
|
|
121
128
|
/** The recovery attestation: SHA-256 of the recovery public key sealed under a project key (plan Decision 30). */
|
|
122
129
|
export declare const recoveryAttestationAad: (orgId: string, projectId: string, epoch: number) => string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A browser sender of Live Streams: the TypeScript port of the Python sender's protocol
|
|
3
|
+
* (packages/python/src/kymoscope/stream.py LiveStream + _RelayTransport, client.py). Generic over the
|
|
4
|
+
* message kind: the caller hands plaintext to send(kind, plaintext) and the publisher seals and publishes it.
|
|
5
|
+
*
|
|
6
|
+
* stream-token (`x-api-key`: the key's auth part) gives a `pt.` token; register and every heartbeat
|
|
7
|
+
* (stream-publisher) give the `pp.` ticket the relay's publish socket takes. A password stream's key
|
|
8
|
+
* derives from the API key's secret part, which never leaves this side, and goes to the owner's browsers
|
|
9
|
+
* sealed to each (device wraps). A key of an end-to-end encrypted project makes this sender a device of
|
|
10
|
+
* the project: nothing is published until a member's browser approves it, then a random stream key goes
|
|
11
|
+
* up sealed under the project key and messages carry the key id (epoch & 0xff).
|
|
12
|
+
*
|
|
13
|
+
* Persisted key material stays as non-extractable CryptoKeys: an e2ee stream key is kept with its
|
|
14
|
+
* project wrap ({iv, ct}, not secret), which a restart within the epoch uploads again instead of
|
|
15
|
+
* re-sealing raw bytes it no longer has.
|
|
16
|
+
*/
|
|
17
|
+
import { type SealedKey } from './crypto';
|
|
18
|
+
/** What a sender keeps across restarts (per API key). Device and stream keys only count for `apiKeyId`. */
|
|
19
|
+
export interface PublisherState {
|
|
20
|
+
apiKeyId: string | null;
|
|
21
|
+
streamId: string;
|
|
22
|
+
salt: string;
|
|
23
|
+
/** An e2ee project's sender device (non-extractable private key). */
|
|
24
|
+
device: {
|
|
25
|
+
privateKey: CryptoKey;
|
|
26
|
+
publicKey: string;
|
|
27
|
+
} | null;
|
|
28
|
+
/** An e2ee stream key, reused only within its epoch, with its project wrap. */
|
|
29
|
+
streamKey: {
|
|
30
|
+
epoch: number;
|
|
31
|
+
key: CryptoKey;
|
|
32
|
+
sealed: SealedKey;
|
|
33
|
+
} | null;
|
|
34
|
+
}
|
|
35
|
+
export interface PublisherDeps {
|
|
36
|
+
fetch: typeof fetch;
|
|
37
|
+
WebSocket: typeof WebSocket;
|
|
38
|
+
/** ms epoch */
|
|
39
|
+
now: () => number;
|
|
40
|
+
store: {
|
|
41
|
+
load(): Promise<PublisherState | null>;
|
|
42
|
+
save(s: PublisherState): Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface PublisherOptions {
|
|
46
|
+
supabaseUrl: string;
|
|
47
|
+
apiKey: string;
|
|
48
|
+
name: string;
|
|
49
|
+
kind: 'data' | 'camera';
|
|
50
|
+
/** Where the stream goes is the API key's: a project's key publishes into that project, a personal key a personal stream. */
|
|
51
|
+
visibility?: 'private' | 'public';
|
|
52
|
+
/** The caller's own data stream a camera belongs to (required for kind 'camera'); its placement comes from it. */
|
|
53
|
+
parentStreamId?: string;
|
|
54
|
+
/** A camera's low layer (360p, for small panels and camera walls): `layer: 'low'` with `lowOf`, its high camera's
|
|
55
|
+
* stream id (same parent). Both or neither. */
|
|
56
|
+
layer?: 'low';
|
|
57
|
+
lowOf?: string;
|
|
58
|
+
}
|
|
59
|
+
export type PublisherStatus = {
|
|
60
|
+
phase: 'registering';
|
|
61
|
+
} | {
|
|
62
|
+
phase: 'waiting_approval';
|
|
63
|
+
} | {
|
|
64
|
+
phase: 'live';
|
|
65
|
+
viewers: number;
|
|
66
|
+
} | {
|
|
67
|
+
phase: 'reconnecting';
|
|
68
|
+
} | {
|
|
69
|
+
phase: 'refused';
|
|
70
|
+
code: string;
|
|
71
|
+
message: string;
|
|
72
|
+
};
|
|
73
|
+
export declare class LivePublisher {
|
|
74
|
+
private readonly opts;
|
|
75
|
+
private readonly deps;
|
|
76
|
+
private readonly auth;
|
|
77
|
+
private readonly secret;
|
|
78
|
+
private token;
|
|
79
|
+
private ticket;
|
|
80
|
+
/** The current ticket says rec (the reply's `recording`); `authRec`: the one the open socket authenticated with. */
|
|
81
|
+
private ticketRec;
|
|
82
|
+
private authRec;
|
|
83
|
+
private relayUrl;
|
|
84
|
+
private apiKeyId;
|
|
85
|
+
private projectId;
|
|
86
|
+
private e2ee;
|
|
87
|
+
private sid;
|
|
88
|
+
private salt;
|
|
89
|
+
private key;
|
|
90
|
+
/** A password stream's raw key, for device wraps (derivable from the secret anyway). */
|
|
91
|
+
private raw;
|
|
92
|
+
private epoch;
|
|
93
|
+
private haveKey;
|
|
94
|
+
private sealed;
|
|
95
|
+
private device;
|
|
96
|
+
private senderDeviceId;
|
|
97
|
+
private deviceRetried;
|
|
98
|
+
private maxBytesPerSec;
|
|
99
|
+
private maxMessageBytes;
|
|
100
|
+
private ws;
|
|
101
|
+
private open;
|
|
102
|
+
private everOpen;
|
|
103
|
+
private connectedAt;
|
|
104
|
+
private authAt;
|
|
105
|
+
private delay;
|
|
106
|
+
private lastToken;
|
|
107
|
+
private viewerCount;
|
|
108
|
+
private busy;
|
|
109
|
+
private stopped;
|
|
110
|
+
private refusal;
|
|
111
|
+
private heartbeatTimer;
|
|
112
|
+
private pingTimer;
|
|
113
|
+
private retryTimer;
|
|
114
|
+
private readonly statusFns;
|
|
115
|
+
private readonly viewerFns;
|
|
116
|
+
private lastStatus;
|
|
117
|
+
constructor(opts: PublisherOptions, deps: PublisherDeps);
|
|
118
|
+
/** The registered stream id (null until start() registers). */
|
|
119
|
+
get streamId(): string | null;
|
|
120
|
+
/** The relay's viewer count (0 while disconnected). */
|
|
121
|
+
get viewers(): number;
|
|
122
|
+
/** Bytes/second the account negotiated (0: not limited) and the relay's message size limit. */
|
|
123
|
+
get limits(): {
|
|
124
|
+
maxBytesPerSec: number;
|
|
125
|
+
maxMessageBytes: number;
|
|
126
|
+
};
|
|
127
|
+
onStatus(fn: (s: PublisherStatus) => void): () => void;
|
|
128
|
+
onViewers(fn: (n: number) => void): () => void;
|
|
129
|
+
/** Mint, register (reusing a persisted stream id and salt), open the socket, heartbeat. Rejects when
|
|
130
|
+
* registration fails; a 4xx (a refusal) also sets status `refused`. */
|
|
131
|
+
start(): Promise<void>;
|
|
132
|
+
/** Seal with the current stream key (the key id of its epoch when e2ee) and send; false if not sent
|
|
133
|
+
* (no key yet, socket down, stopped). */
|
|
134
|
+
send(kind: number, plaintext: Uint8Array): Promise<boolean>;
|
|
135
|
+
/** Close the socket and stop the timers. The stream stays registered (never deleted here). */
|
|
136
|
+
stop(): void;
|
|
137
|
+
private post;
|
|
138
|
+
private mint;
|
|
139
|
+
private call;
|
|
140
|
+
private register;
|
|
141
|
+
private senderDevice;
|
|
142
|
+
/** Token refresh, then the heartbeat (each new ticket also refreshes the socket). 401/403 stop the
|
|
143
|
+
* publisher (revoked key, plan, stale_epoch); anything else is retried on the next interval. */
|
|
144
|
+
private housekeeping;
|
|
145
|
+
/** Seal the stream key to the owner's browsers that lack it (password streams), at most 50 per call. */
|
|
146
|
+
private wrapFor;
|
|
147
|
+
/** The project key arrived: seal the stream key under it; a new epoch (rotation) gets a new stream key.
|
|
148
|
+
* A key this device can't open re-registers once with a new device key. */
|
|
149
|
+
private applyProject;
|
|
150
|
+
private reregister;
|
|
151
|
+
private save;
|
|
152
|
+
private connect;
|
|
153
|
+
private disconnect;
|
|
154
|
+
/** Reconnect: at once after a stable connection, else after the backoff (1 s doubling to 10 s). */
|
|
155
|
+
private dropped;
|
|
156
|
+
/** Use `ticket` from now on; re-authenticate the open socket in-band when due, or at once when it
|
|
157
|
+
* says rec and the socket's ticket didn't. */
|
|
158
|
+
private setTicket;
|
|
159
|
+
private setViewers;
|
|
160
|
+
private refuse;
|
|
161
|
+
private status;
|
|
162
|
+
private emit;
|
|
163
|
+
}
|
|
@@ -3,6 +3,8 @@ import type { EncryptedEnvelope } from './crypto';
|
|
|
3
3
|
import type { LiveColumnsBlock, LiveMessage } from './buffer';
|
|
4
4
|
import type { LiveDataSource } from '../data/liveDataSource';
|
|
5
5
|
import type { Dataset } from '../parseDataset';
|
|
6
|
+
import type { VideoTrack } from './videoTrack';
|
|
7
|
+
import type { ClockMap } from './clockMap';
|
|
6
8
|
/** What a viewer needs to join an encrypted relay stream (from the stream registry). */
|
|
7
9
|
export interface LiveStreamDescriptor {
|
|
8
10
|
id: string;
|
|
@@ -21,6 +23,12 @@ export interface LiveStreamDescriptor {
|
|
|
21
23
|
* owner hosts). The engine's `getToken` is still asked for the credential.
|
|
22
24
|
*/
|
|
23
25
|
relay_url?: string | null;
|
|
26
|
+
/** A camera stream carries encoded video (KIND_VIDEO), not samples. Default 'data'. */
|
|
27
|
+
kind?: 'data' | 'camera';
|
|
28
|
+
/** A camera's data stream (a camera is a channel of it); null/absent for data streams. */
|
|
29
|
+
parent_stream_id?: string | null;
|
|
30
|
+
/** A camera's low layer (360p, for small panels and walls): its high camera's id; null/absent otherwise. */
|
|
31
|
+
low_of_stream_id?: string | null;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* The keys of a stream in an end-to-end encrypted project: frames name their key id
|
|
@@ -60,12 +68,22 @@ export interface LiveConnection {
|
|
|
60
68
|
visibility: 'private' | 'public' | 'org' | 'project';
|
|
61
69
|
orgName: string | null;
|
|
62
70
|
projectName?: string | null;
|
|
71
|
+
kind: 'data' | 'camera';
|
|
72
|
+
/** A camera's data stream (LiveStreamDescriptor.parent_stream_id); null for data streams. */
|
|
73
|
+
parentId: string | null;
|
|
74
|
+
/** A camera's low layer: its high camera's id (LiveStreamDescriptor.low_of_stream_id); null otherwise. */
|
|
75
|
+
lowOf: string | null;
|
|
76
|
+
/** A camera's encoded video (the same instance for the connection's life); null for data. */
|
|
77
|
+
video: VideoTrack | null;
|
|
78
|
+
/** A data stream's time → wall time map (to sync cameras with it); null for cameras. */
|
|
79
|
+
clock: ClockMap | null;
|
|
63
80
|
/** Slow (~1 Hz) snapshot for column discovery / count / non-multiband charts. */
|
|
64
81
|
dataset: Dataset;
|
|
65
82
|
/** Live decimation source read by the multiband chart every frame (no React). */
|
|
66
83
|
source: LiveDataSource;
|
|
67
84
|
rowCount: number;
|
|
68
|
-
|
|
85
|
+
/** 'idle': a camera no panel shows (connected, not downloading). */
|
|
86
|
+
status: 'idle' | 'connecting' | 'live' | 'stale' | 'error';
|
|
69
87
|
error: string | null;
|
|
70
88
|
/** When a recording is running: its start time (ms since epoch). */
|
|
71
89
|
recordingSince: number | null;
|