@aurora-blue/signalk-connector 0.2.0-dev.20

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/VERSION +1 -0
  5. package/dist/src/backoff.d.ts +1 -0
  6. package/dist/src/backoff.js +9 -0
  7. package/dist/src/config.d.ts +192 -0
  8. package/dist/src/config.js +317 -0
  9. package/dist/src/connector.d.ts +69 -0
  10. package/dist/src/connector.js +487 -0
  11. package/dist/src/delta.d.ts +4 -0
  12. package/dist/src/delta.js +137 -0
  13. package/dist/src/geometry.d.ts +3 -0
  14. package/dist/src/geometry.js +25 -0
  15. package/dist/src/http-client.d.ts +22 -0
  16. package/dist/src/http-client.js +158 -0
  17. package/dist/src/index.d.ts +3 -0
  18. package/dist/src/index.js +93 -0
  19. package/dist/src/safety/anchor-alarm.d.ts +33 -0
  20. package/dist/src/safety/anchor-alarm.js +79 -0
  21. package/dist/src/state-store.d.ts +45 -0
  22. package/dist/src/state-store.js +491 -0
  23. package/dist/src/storage-budget.d.ts +35 -0
  24. package/dist/src/storage-budget.js +88 -0
  25. package/dist/src/telemetry-catalog.d.ts +19 -0
  26. package/dist/src/telemetry-catalog.js +93 -0
  27. package/dist/src/telemetry-reducer.d.ts +22 -0
  28. package/dist/src/telemetry-reducer.js +150 -0
  29. package/dist/src/types.d.ts +115 -0
  30. package/dist/src/types.js +5 -0
  31. package/dist/src/version.d.ts +1 -0
  32. package/dist/src/version.js +4 -0
  33. package/package.json +60 -0
  34. package/public/aurora-blue-aboard-mark.svg +19 -0
  35. package/public/aurora-blue-archipelago-dark.svg +32 -0
  36. package/public/aurora-blue-archipelago-light.svg +81 -0
  37. package/public/ci-brand.css +77 -0
  38. package/public/ci-fonts.css +28 -0
  39. package/public/ci-tokens.css +139 -0
  40. package/public/favicon.svg +19 -0
  41. package/public/fonts/LICENSE +96 -0
  42. package/public/fonts/ubuntu-latin-300-normal.woff2 +0 -0
  43. package/public/fonts/ubuntu-latin-400-normal.woff2 +0 -0
  44. package/public/fonts/ubuntu-latin-500-normal.woff2 +0 -0
  45. package/public/fonts/ubuntu-latin-700-normal.woff2 +0 -0
  46. package/public/index.html +177 -0
  47. package/public/locales/de.json +82 -0
  48. package/public/locales/en.json +82 -0
  49. package/public/locales/es.json +82 -0
  50. package/public/locales/fr.json +82 -0
  51. package/public/status.css +262 -0
  52. package/public/status.js +237 -0
@@ -0,0 +1,491 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StateStore = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_fs_1 = require("node:fs");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_path_1 = require("node:path");
8
+ const types_1 = require("./types");
9
+ const METADATA_FILE = "metadata.json";
10
+ const LEGACY_STATE_FILE = "state.json";
11
+ const LEGACY_BACKUP_FILE = "state.v1.backup.json";
12
+ const SEGMENT_PATTERN = /^segment-(\d{12})\.jsonl$/;
13
+ const initialState = () => ({
14
+ version: 2,
15
+ protocol_version: types_1.PROTOCOL_VERSION,
16
+ device_id: null,
17
+ access_token: null,
18
+ next_sequence: 1,
19
+ accepted_through: 0,
20
+ events: [],
21
+ pending_batch: null,
22
+ last_success_at: null,
23
+ operating_mode: "active",
24
+ });
25
+ const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
26
+ const isSupportedProtocolVersion = (value) => typeof value === "string" &&
27
+ types_1.SUPPORTED_PROTOCOL_VERSIONS.some((version) => version === value);
28
+ const isOperatingMode = (value) => value === "active" || value === "standby" || value === "disabled";
29
+ const assertState = (state) => {
30
+ if (state.version !== 2 ||
31
+ !isSupportedProtocolVersion(state.protocol_version) ||
32
+ !isPositiveInteger(state.accepted_through) ||
33
+ !isPositiveInteger(state.next_sequence) ||
34
+ state.next_sequence < 1 ||
35
+ !Array.isArray(state.events) ||
36
+ !isOperatingMode(state.operating_mode) ||
37
+ (state.device_id === null) !== (state.access_token === null)) {
38
+ throw new Error("Aurora Blue connector state is invalid.");
39
+ }
40
+ const expectedFirst = state.accepted_through + 1;
41
+ for (const [index, event] of state.events.entries()) {
42
+ if (event.sequence !== expectedFirst + index) {
43
+ throw new Error("Aurora Blue connector event sequence is not contiguous.");
44
+ }
45
+ }
46
+ const expectedNext = expectedFirst + state.events.length;
47
+ if (state.next_sequence !== expectedNext) {
48
+ throw new Error("Aurora Blue connector next sequence is inconsistent.");
49
+ }
50
+ if (state.pending_batch !== null) {
51
+ const pending = state.pending_batch;
52
+ const expectedEvents = state.events.slice(0, pending.events.length);
53
+ if (!isSupportedProtocolVersion(pending.protocol_version) ||
54
+ pending.device_id !== state.device_id ||
55
+ expectedEvents.length !== pending.events.length ||
56
+ pending.first_sequence !== pending.events[0]?.sequence ||
57
+ pending.last_sequence !== pending.events.at(-1)?.sequence ||
58
+ expectedEvents.some((event, index) => event.sequence !== pending.events[index]?.sequence)) {
59
+ throw new Error("Aurora Blue connector pending batch is inconsistent.");
60
+ }
61
+ }
62
+ };
63
+ const assertLegacyState = (state) => {
64
+ assertState({
65
+ ...state,
66
+ version: 2,
67
+ operating_mode: isOperatingMode(state.operating_mode)
68
+ ? state.operating_mode
69
+ : "active",
70
+ });
71
+ };
72
+ const checksum = (event) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(event)).digest("hex");
73
+ const encodeRecord = (event) => `${JSON.stringify({ checksum: checksum(event), event })}\n`;
74
+ const metadataFrom = (state) => ({
75
+ version: 2,
76
+ protocol_version: state.protocol_version,
77
+ device_id: state.device_id,
78
+ access_token: state.access_token,
79
+ next_sequence: state.next_sequence,
80
+ accepted_through: state.accepted_through,
81
+ pending_batch: state.pending_batch,
82
+ last_success_at: state.last_success_at,
83
+ operating_mode: state.operating_mode,
84
+ });
85
+ class StateStore {
86
+ directory;
87
+ metadataPath;
88
+ now;
89
+ uuid;
90
+ segmentMaximumBytes;
91
+ capacityProvider;
92
+ state = initialState();
93
+ segments = [];
94
+ journalBytes = 0;
95
+ tail = Promise.resolve();
96
+ constructor(directory, options = {}) {
97
+ this.directory = directory;
98
+ this.metadataPath = (0, node_path_1.join)(directory, METADATA_FILE);
99
+ this.now = options.now ?? (() => new Date().toISOString());
100
+ this.uuid = options.uuid ?? node_crypto_1.randomUUID;
101
+ this.segmentMaximumBytes = options.segmentMaximumBytes ?? 256 * 1024;
102
+ this.capacityProvider =
103
+ options.filesystemCapacity ?? (() => this.readFilesystemCapacity());
104
+ }
105
+ async load() {
106
+ await (0, promises_1.mkdir)(this.directory, { recursive: true, mode: 0o700 });
107
+ await (0, promises_1.chmod)(this.directory, 0o700);
108
+ if (!(await this.exists(this.metadataPath))) {
109
+ const legacyPath = (0, node_path_1.join)(this.directory, LEGACY_STATE_FILE);
110
+ if (await this.exists(legacyPath)) {
111
+ await this.migrateLegacy(legacyPath);
112
+ }
113
+ else {
114
+ this.state = initialState();
115
+ await this.persistMetadata();
116
+ }
117
+ }
118
+ const metadata = await this.readMetadata();
119
+ const journal = await this.readJournal(metadata.accepted_through);
120
+ this.state = {
121
+ ...metadata,
122
+ protocol_version: types_1.PROTOCOL_VERSION,
123
+ events: journal,
124
+ next_sequence: metadata.accepted_through + journal.length + 1,
125
+ };
126
+ assertState(this.state);
127
+ await (0, promises_1.chmod)(this.metadataPath, 0o600);
128
+ await this.removeAcknowledgedSegments(this.state.accepted_through);
129
+ await this.preserveLegacyBackup();
130
+ await this.removeStaleTemporaryFiles();
131
+ if (metadata.protocol_version !== types_1.PROTOCOL_VERSION) {
132
+ await this.persistMetadata();
133
+ }
134
+ }
135
+ async snapshot() {
136
+ await this.tail;
137
+ return structuredClone(this.state);
138
+ }
139
+ async summary() {
140
+ await this.tail;
141
+ const { pending_batch: _, ...metadata } = metadataFrom(this.state);
142
+ return { ...metadata, bufferedEvents: this.state.events.length };
143
+ }
144
+ async journalMetrics() {
145
+ await this.tail;
146
+ return {
147
+ events: this.state.events.length,
148
+ bytes: this.journalBytes,
149
+ averageRecordBytes: this.state.events.length === 0
150
+ ? null
151
+ : this.journalBytes / this.state.events.length,
152
+ };
153
+ }
154
+ async filesystemCapacity() {
155
+ await this.tail;
156
+ return this.capacityProvider();
157
+ }
158
+ async setPairing(pairing) {
159
+ await this.serialized(async () => {
160
+ if (this.state.device_id !== null || this.state.events.length > 0) {
161
+ throw new Error("Connector is already paired or contains buffered events.");
162
+ }
163
+ this.state.device_id = pairing.device_id;
164
+ this.state.access_token = pairing.access_token;
165
+ this.state.operating_mode = pairing.runtime_configuration.operating_mode;
166
+ await this.persistMetadata();
167
+ });
168
+ }
169
+ async setOperatingMode(operatingMode) {
170
+ await this.serialized(async () => {
171
+ this.state.operating_mode = operatingMode;
172
+ await this.persistMetadata();
173
+ });
174
+ }
175
+ async append(event, eventLimit, byteLimit = Number.MAX_SAFE_INTEGER) {
176
+ let result = null;
177
+ await this.serialized(async () => {
178
+ if (this.state.device_id === null || this.state.access_token === null) {
179
+ throw new Error("Connector must be paired before buffering events.");
180
+ }
181
+ const buffered = { ...event, sequence: this.state.next_sequence };
182
+ const payload = encodeRecord(buffered);
183
+ const payloadBytes = Buffer.byteLength(payload);
184
+ if (this.state.events.length >= eventLimit ||
185
+ this.journalBytes + payloadBytes > byteLimit) {
186
+ return;
187
+ }
188
+ await this.appendRecord(buffered, payload, payloadBytes);
189
+ this.state.events.push(buffered);
190
+ this.state.next_sequence += 1;
191
+ // load() validates the complete journal. This append preserves its
192
+ // contiguous sequence without rescanning every previous record.
193
+ result = structuredClone(buffered);
194
+ });
195
+ return result;
196
+ }
197
+ async pendingBatch(maximumEvents) {
198
+ let result = null;
199
+ await this.serialized(async () => {
200
+ if (this.state.pending_batch !== null) {
201
+ result = structuredClone(this.state.pending_batch);
202
+ return;
203
+ }
204
+ if (this.state.device_id === null || this.state.events.length === 0) {
205
+ return;
206
+ }
207
+ const firstBoot = this.state.events[0]?.boot_id;
208
+ const selected = [];
209
+ for (const event of this.state.events.slice(0, maximumEvents)) {
210
+ if (event.boot_id !== firstBoot) {
211
+ break;
212
+ }
213
+ selected.push(event);
214
+ }
215
+ if (selected.length === 0 || firstBoot === undefined) {
216
+ return;
217
+ }
218
+ const events = selected.map(({ boot_id: _, ...event }) => event);
219
+ const firstSequence = events[0]?.sequence;
220
+ const lastSequence = events.at(-1)?.sequence;
221
+ if (firstSequence === undefined || lastSequence === undefined) {
222
+ return;
223
+ }
224
+ this.state.pending_batch = {
225
+ protocol_version: types_1.PROTOCOL_VERSION,
226
+ device_id: this.state.device_id,
227
+ boot_id: firstBoot,
228
+ batch_id: this.uuid(),
229
+ first_sequence: firstSequence,
230
+ last_sequence: lastSequence,
231
+ created_at: this.now(),
232
+ events,
233
+ };
234
+ assertState(this.state);
235
+ await this.persistMetadata();
236
+ result = structuredClone(this.state.pending_batch);
237
+ });
238
+ return result;
239
+ }
240
+ async acknowledge(acceptedThrough) {
241
+ await this.serialized(async () => {
242
+ const pending = this.state.pending_batch;
243
+ if (pending === null || acceptedThrough !== pending.last_sequence) {
244
+ throw new Error("Aurora Blue acknowledgement does not match the pending batch.");
245
+ }
246
+ this.state.events = this.state.events.filter((event) => event.sequence > acceptedThrough);
247
+ this.state.accepted_through = acceptedThrough;
248
+ this.state.pending_batch = null;
249
+ this.state.last_success_at = this.now();
250
+ assertState(this.state);
251
+ await this.persistMetadata();
252
+ await this.removeAcknowledgedSegments(acceptedThrough);
253
+ });
254
+ }
255
+ async serialized(operation) {
256
+ const current = this.tail.then(operation);
257
+ this.tail = current.catch(() => undefined);
258
+ await current;
259
+ }
260
+ async appendRecord(event, payload, payloadBytes) {
261
+ let segment = this.segments.at(-1);
262
+ if (segment === undefined ||
263
+ (segment.bytes > 0 &&
264
+ segment.bytes + payloadBytes > this.segmentMaximumBytes)) {
265
+ const path = (0, node_path_1.join)(this.directory, `segment-${String(event.sequence).padStart(12, "0")}.jsonl`);
266
+ segment = {
267
+ path,
268
+ firstSequence: event.sequence,
269
+ lastSequence: event.sequence - 1,
270
+ bytes: 0,
271
+ };
272
+ this.segments.push(segment);
273
+ }
274
+ const file = await (0, promises_1.open)(segment.path, "a", 0o600);
275
+ try {
276
+ await file.writeFile(payload, "utf8");
277
+ await file.sync();
278
+ }
279
+ finally {
280
+ await file.close();
281
+ }
282
+ await (0, promises_1.chmod)(segment.path, 0o600);
283
+ segment.lastSequence = event.sequence;
284
+ segment.bytes += payloadBytes;
285
+ this.journalBytes += payloadBytes;
286
+ }
287
+ async readMetadata() {
288
+ let parsed;
289
+ try {
290
+ parsed = JSON.parse(await (0, promises_1.readFile)(this.metadataPath, "utf8"));
291
+ }
292
+ catch (error) {
293
+ throw new Error("Aurora Blue connector metadata cannot be read safely.", {
294
+ cause: error,
295
+ });
296
+ }
297
+ const parsedMetadata = parsed;
298
+ const metadata = {
299
+ ...parsedMetadata,
300
+ operating_mode: isOperatingMode(parsedMetadata.operating_mode)
301
+ ? parsedMetadata.operating_mode
302
+ : "active",
303
+ };
304
+ if (metadata.version !== 2 ||
305
+ !isSupportedProtocolVersion(metadata.protocol_version) ||
306
+ !isPositiveInteger(metadata.accepted_through) ||
307
+ !isPositiveInteger(metadata.next_sequence) ||
308
+ !isOperatingMode(metadata.operating_mode) ||
309
+ (metadata.device_id === null) !== (metadata.access_token === null)) {
310
+ throw new Error("Aurora Blue connector metadata is invalid.");
311
+ }
312
+ return metadata;
313
+ }
314
+ async readJournal(acceptedThrough) {
315
+ const names = (await (0, promises_1.readdir)(this.directory))
316
+ .filter((name) => SEGMENT_PATTERN.test(name))
317
+ .sort();
318
+ const events = [];
319
+ this.segments = [];
320
+ this.journalBytes = 0;
321
+ for (const name of names) {
322
+ const match = SEGMENT_PATTERN.exec(name);
323
+ if (match === null) {
324
+ continue;
325
+ }
326
+ const path = (0, node_path_1.join)(this.directory, name);
327
+ const segmentEvents = await this.readSegment(path);
328
+ const details = await (0, promises_1.stat)(path);
329
+ const firstSequence = Number(match[1]);
330
+ const lastSequence = segmentEvents.at(-1)?.sequence ?? firstSequence - 1;
331
+ this.segments.push({
332
+ path,
333
+ firstSequence,
334
+ lastSequence,
335
+ bytes: details.size,
336
+ });
337
+ this.journalBytes += details.size;
338
+ events.push(...segmentEvents.filter((event) => event.sequence > acceptedThrough));
339
+ }
340
+ return events;
341
+ }
342
+ async readSegment(path) {
343
+ let payload = await (0, promises_1.readFile)(path, "utf8");
344
+ if (payload !== "" && !payload.endsWith("\n")) {
345
+ const lastComplete = payload.lastIndexOf("\n") + 1;
346
+ await (0, promises_1.truncate)(path, Buffer.byteLength(payload.slice(0, lastComplete)));
347
+ payload = payload.slice(0, lastComplete);
348
+ }
349
+ const events = [];
350
+ for (const line of payload.split("\n")) {
351
+ if (line === "") {
352
+ continue;
353
+ }
354
+ let record;
355
+ try {
356
+ record = JSON.parse(line);
357
+ }
358
+ catch (error) {
359
+ throw new Error("Aurora Blue connector journal is corrupted.", {
360
+ cause: error,
361
+ });
362
+ }
363
+ if (typeof record.checksum !== "string" ||
364
+ record.event === null ||
365
+ typeof record.event !== "object" ||
366
+ checksum(record.event) !== record.checksum) {
367
+ throw new Error("Aurora Blue connector journal checksum is invalid.");
368
+ }
369
+ events.push(record.event);
370
+ }
371
+ return events;
372
+ }
373
+ async removeAcknowledgedSegments(acceptedThrough) {
374
+ const retained = [];
375
+ for (const segment of this.segments) {
376
+ if (segment.lastSequence >= segment.firstSequence &&
377
+ segment.lastSequence <= acceptedThrough) {
378
+ await (0, promises_1.unlink)(segment.path);
379
+ this.journalBytes -= segment.bytes;
380
+ }
381
+ else {
382
+ retained.push(segment);
383
+ }
384
+ }
385
+ this.segments = retained;
386
+ }
387
+ async migrateLegacy(path) {
388
+ let legacy;
389
+ try {
390
+ legacy = JSON.parse(await (0, promises_1.readFile)(path, "utf8"));
391
+ assertLegacyState(legacy);
392
+ }
393
+ catch (error) {
394
+ throw new Error("Legacy Aurora Blue connector state is invalid.", {
395
+ cause: error,
396
+ });
397
+ }
398
+ for (const name of await (0, promises_1.readdir)(this.directory)) {
399
+ if (SEGMENT_PATTERN.test(name)) {
400
+ await (0, promises_1.unlink)((0, node_path_1.join)(this.directory, name));
401
+ }
402
+ }
403
+ this.segments = [];
404
+ this.journalBytes = 0;
405
+ this.state = {
406
+ ...legacy,
407
+ version: 2,
408
+ protocol_version: types_1.PROTOCOL_VERSION,
409
+ operating_mode: isOperatingMode(legacy.operating_mode)
410
+ ? legacy.operating_mode
411
+ : "active",
412
+ };
413
+ for (const event of legacy.events) {
414
+ const payload = encodeRecord(event);
415
+ await this.appendRecord(event, payload, Buffer.byteLength(payload));
416
+ }
417
+ await this.persistMetadata();
418
+ await (0, promises_1.rename)(path, (0, node_path_1.join)(this.directory, LEGACY_BACKUP_FILE));
419
+ }
420
+ async preserveLegacyBackup() {
421
+ const legacyPath = (0, node_path_1.join)(this.directory, LEGACY_STATE_FILE);
422
+ const backupPath = (0, node_path_1.join)(this.directory, LEGACY_BACKUP_FILE);
423
+ if ((await this.exists(legacyPath)) && !(await this.exists(backupPath))) {
424
+ await (0, promises_1.rename)(legacyPath, backupPath);
425
+ }
426
+ }
427
+ async removeStaleTemporaryFiles() {
428
+ for (const name of await (0, promises_1.readdir)(this.directory)) {
429
+ if (/^\.(?:state|metadata)-.+\.tmp$/.test(name)) {
430
+ await (0, promises_1.unlink)((0, node_path_1.join)(this.directory, name));
431
+ }
432
+ }
433
+ }
434
+ async persistMetadata() {
435
+ const temporaryPath = (0, node_path_1.join)(this.directory, `.metadata-${this.uuid()}.tmp`);
436
+ const payload = `${JSON.stringify(metadataFrom(this.state))}\n`;
437
+ await (0, promises_1.writeFile)(temporaryPath, payload, {
438
+ encoding: "utf8",
439
+ flag: "wx",
440
+ mode: 0o600,
441
+ });
442
+ const temporary = await (0, promises_1.open)(temporaryPath, "r");
443
+ try {
444
+ await temporary.sync();
445
+ }
446
+ finally {
447
+ await temporary.close();
448
+ }
449
+ await (0, promises_1.rename)(temporaryPath, this.metadataPath);
450
+ await (0, promises_1.chmod)(this.metadataPath, 0o600);
451
+ try {
452
+ const directory = await (0, promises_1.open)(this.directory, "r");
453
+ try {
454
+ await directory.sync();
455
+ }
456
+ finally {
457
+ await directory.close();
458
+ }
459
+ }
460
+ catch {
461
+ // Directory fsync is not available on every supported platform.
462
+ }
463
+ }
464
+ async exists(path) {
465
+ try {
466
+ await (0, promises_1.access)(path, node_fs_1.constants.F_OK);
467
+ return true;
468
+ }
469
+ catch {
470
+ return false;
471
+ }
472
+ }
473
+ async readFilesystemCapacity() {
474
+ try {
475
+ const details = await (0, promises_1.statfs)(this.directory);
476
+ const totalBytes = details.blocks * details.bsize;
477
+ const freeBytes = details.bavail * details.bsize;
478
+ if (!Number.isFinite(totalBytes) ||
479
+ !Number.isFinite(freeBytes) ||
480
+ totalBytes <= 0 ||
481
+ freeBytes < 0) {
482
+ return null;
483
+ }
484
+ return { totalBytes, freeBytes };
485
+ }
486
+ catch {
487
+ return null;
488
+ }
489
+ }
490
+ }
491
+ exports.StateStore = StateStore;
@@ -0,0 +1,35 @@
1
+ export type StorageProfile = "auto" | "constrained" | "standard" | "extended" | "custom";
2
+ export type ResolvedStorageProfile = Exclude<StorageProfile, "auto">;
3
+ export type BufferPressure = "normal" | "elevated" | "critical" | "emergency";
4
+ export interface FilesystemCapacity {
5
+ totalBytes: number;
6
+ freeBytes: number;
7
+ }
8
+ export interface JournalMetrics {
9
+ events: number;
10
+ bytes: number;
11
+ averageRecordBytes: number | null;
12
+ }
13
+ export interface StorageBudgetPolicy {
14
+ requestedProfile: StorageProfile;
15
+ customMaximumBytes: number;
16
+ customReserveBytes: number;
17
+ customMaximumFraction: number;
18
+ eventSafetyLimit: number;
19
+ }
20
+ export interface StorageBudget {
21
+ requestedProfile: StorageProfile;
22
+ resolvedProfile: ResolvedStorageProfile;
23
+ byteLimit: number;
24
+ eventLimit: number;
25
+ reserveBytes: number;
26
+ maximumFraction: number;
27
+ utilization: number;
28
+ pressure: BufferPressure;
29
+ remainingBytes: number;
30
+ remainingEvents: number;
31
+ averageRecordBytes: number;
32
+ filesystem: FilesystemCapacity | null;
33
+ }
34
+ export declare const pressureForUtilization: (utilization: number) => BufferPressure;
35
+ export declare const resolveStorageBudget: (policy: StorageBudgetPolicy, journal: JournalMetrics, filesystem: FilesystemCapacity | null) => StorageBudget;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveStorageBudget = exports.pressureForUtilization = void 0;
4
+ const MEBIBYTE = 1024 * 1024;
5
+ const GIBIBYTE = 1024 * MEBIBYTE;
6
+ const ASSUMED_RECORD_BYTES = 384;
7
+ const PROFILE_POLICIES = {
8
+ constrained: {
9
+ maximumBytes: 16 * MEBIBYTE,
10
+ reserveBytes: 128 * MEBIBYTE,
11
+ maximumFraction: 0.02,
12
+ },
13
+ standard: {
14
+ maximumBytes: 64 * MEBIBYTE,
15
+ reserveBytes: 256 * MEBIBYTE,
16
+ maximumFraction: 0.03,
17
+ },
18
+ extended: {
19
+ maximumBytes: 256 * MEBIBYTE,
20
+ reserveBytes: 512 * MEBIBYTE,
21
+ maximumFraction: 0.05,
22
+ },
23
+ };
24
+ const autoProfile = (filesystem) => {
25
+ if (filesystem === null || filesystem.totalBytes <= 8 * GIBIBYTE) {
26
+ return "constrained";
27
+ }
28
+ return filesystem.totalBytes <= 128 * GIBIBYTE ? "standard" : "extended";
29
+ };
30
+ const profilePolicy = (policy, filesystem) => {
31
+ const resolvedProfile = policy.requestedProfile === "auto"
32
+ ? autoProfile(filesystem)
33
+ : policy.requestedProfile;
34
+ if (resolvedProfile === "custom") {
35
+ return {
36
+ resolvedProfile,
37
+ limits: {
38
+ maximumBytes: policy.customMaximumBytes,
39
+ reserveBytes: policy.customReserveBytes,
40
+ maximumFraction: policy.customMaximumFraction,
41
+ },
42
+ };
43
+ }
44
+ return { resolvedProfile, limits: PROFILE_POLICIES[resolvedProfile] };
45
+ };
46
+ const pressureForUtilization = (utilization) => {
47
+ if (utilization >= 0.9) {
48
+ return "emergency";
49
+ }
50
+ if (utilization >= 0.75) {
51
+ return "critical";
52
+ }
53
+ return utilization >= 0.5 ? "elevated" : "normal";
54
+ };
55
+ exports.pressureForUtilization = pressureForUtilization;
56
+ const resolveStorageBudget = (policy, journal, filesystem) => {
57
+ const { resolvedProfile, limits } = profilePolicy(policy, filesystem);
58
+ let byteLimit = limits.maximumBytes;
59
+ if (filesystem !== null) {
60
+ const fractionLimit = Math.floor(filesystem.totalBytes * limits.maximumFraction);
61
+ const reserveLimit = Math.max(0, filesystem.freeBytes + journal.bytes - limits.reserveBytes);
62
+ byteLimit = Math.min(byteLimit, fractionLimit, reserveLimit);
63
+ }
64
+ byteLimit = Math.max(journal.bytes, Math.floor(byteLimit), 1);
65
+ const averageRecordBytes = journal.averageRecordBytes === null || journal.averageRecordBytes <= 0
66
+ ? ASSUMED_RECORD_BYTES
67
+ : journal.averageRecordBytes;
68
+ const byteDerivedEventLimit = Math.max(1, Math.floor(byteLimit / averageRecordBytes));
69
+ const eventLimit = Math.max(journal.events, Math.min(policy.eventSafetyLimit, byteDerivedEventLimit));
70
+ const byteUtilization = journal.bytes / byteLimit;
71
+ const eventUtilization = journal.events / eventLimit;
72
+ const utilization = Math.min(1, Math.max(byteUtilization, eventUtilization));
73
+ return {
74
+ requestedProfile: policy.requestedProfile,
75
+ resolvedProfile,
76
+ byteLimit,
77
+ eventLimit,
78
+ reserveBytes: limits.reserveBytes,
79
+ maximumFraction: limits.maximumFraction,
80
+ utilization,
81
+ pressure: (0, exports.pressureForUtilization)(utilization),
82
+ remainingBytes: Math.max(0, byteLimit - journal.bytes),
83
+ remainingEvents: Math.max(0, eventLimit - journal.events),
84
+ averageRecordBytes,
85
+ filesystem,
86
+ };
87
+ };
88
+ exports.resolveStorageBudget = resolveStorageBudget;
@@ -0,0 +1,19 @@
1
+ import type { EventContext } from "./types";
2
+ export type TelemetryScope = "self" | "ais";
3
+ export type ValueKind = "number" | "position" | "string" | "aisShipType";
4
+ export type ChangeKind = "scalar" | "angle" | "position" | "identity";
5
+ export interface TelemetryDefinition {
6
+ scope: TelemetryScope;
7
+ path: string;
8
+ valueKind: ValueKind;
9
+ changeKind: ChangeKind;
10
+ minimumIntervalMs: number;
11
+ maximumHeartbeatMs: number;
12
+ changeThreshold: number;
13
+ }
14
+ export declare const SELF_TELEMETRY: readonly TelemetryDefinition[];
15
+ export declare const AIS_TELEMETRY: readonly TelemetryDefinition[];
16
+ export declare const SELF_SUBSCRIPTION_PATHS: string[];
17
+ export declare const AIS_SUBSCRIPTION_PATHS: string[];
18
+ export declare const isAisContext: (context: string) => context is EventContext;
19
+ export declare const telemetryDefinition: (context: string, path: string) => TelemetryDefinition | null;