@synnaxlabs/client 0.57.0 → 0.57.1

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.
@@ -1,5 +1,5 @@
1
1
  import { alamos } from '@synnaxlabs/alamos';
2
- import { MultiSeries, Series, TimeSpan, TimeStamp } from '@synnaxlabs/x';
2
+ import { MultiSeries, Series, TimeRange, TimeSpan, TimeStamp } from '@synnaxlabs/x';
3
3
  import { Transform } from './transform';
4
4
  /** Response from a write to the {@link Dynamic} cache. */
5
5
  export interface WriteResponse {
@@ -47,15 +47,35 @@ export declare class Dynamic {
47
47
  * should this be modified by the caller.
48
48
  */
49
49
  get leadingBuffer(): Series | null;
50
+ /**
51
+ * @returns the time range the buffer's samples actually cover: its start to the last
52
+ * stamped write, or the wall clock when the buffer holds unstamped data. Unlike the
53
+ * buffer's provisional time range, the end never claims future time. Null when there
54
+ * is no buffer.
55
+ */
56
+ get dataTimeRange(): TimeRange | null;
50
57
  /**
51
58
  * @returns a list of buffers that were filled by the cache during the write. If the
52
59
  * current buffer is able to fit all writes, no buffers will be returned.
53
60
  */
54
61
  write(series: MultiSeries): WriteResponse;
55
62
  private allocate;
63
+ /**
64
+ * @returns the sample capacity for a buffer that must hold the given series whole:
65
+ * the configured size, or the series' own size when that is larger.
66
+ */
67
+ private capacityFor;
56
68
  private allocStart;
57
69
  private allocCurr;
70
+ /** @returns the timestamp the flushed buffer's end was stamped with, or null when
71
+ * there was no buffer. */
58
72
  private flushCurr;
73
+ /**
74
+ * @returns a right-sized copy of a buffer that rotated with most of its space
75
+ * unused, or the buffer itself. A rotated buffer holds its whole allocation until
76
+ * it is collected, which is wasteful when an oversized series ended it early.
77
+ */
78
+ private compacted;
59
79
  private _write;
60
80
  private updateAvgRate;
61
81
  private nextBufferSize;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic.d.ts","sourceRoot":"","sources":["../../../../src/framer/cache/dynamic.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAGL,WAAW,EACX,MAAM,EACN,KAAK,QAAQ,EACb,SAAS,EACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE9E,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,OAAO,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,SAAS,EAAE,WAAW,CAAC;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,iBAAiB,EAAE,MAAM,GAAG,QAAQ,CAAC;IACrC,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,wBAAwB;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,eAAe,CAAC;IACzC,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,SAAS,CAAC;CACvB;AAYD;;;;GAIG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsC;IAE5D,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,IAAI,CAAuB;IACnC;;iDAE6C;IAC7C,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;gBAE1B,KAAK,EAAE,YAAY;IAY/B,kEAAkE;IAClE,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;OAGG;IACH,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa;IAUzC,OAAO,CAAC,QAAQ;IAgChB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,MAAM;IAmEd,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,cAAc;IAQtB;;;;OAIG;IACH,KAAK,IAAI,MAAM,GAAG,IAAI;IAStB,wDAAwD;IACxD,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"dynamic.d.ts","sourceRoot":"","sources":["../../../../src/framer/cache/dynamic.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAGL,WAAW,EACX,MAAM,EAEN,SAAS,EACT,KAAK,QAAQ,EACb,SAAS,EACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE9E,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,OAAO,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,SAAS,EAAE,WAAW,CAAC;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,iBAAiB,EAAE,MAAM,GAAG,QAAQ,CAAC;IACrC,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,wBAAwB;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,eAAe,CAAC;IACzC,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,SAAS,CAAC;CACvB;AAwCD;;;;GAIG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsC;IAE5D,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,IAAI,CAAuB;IACnC;;iDAE6C;IAC7C,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;gBAE1B,KAAK,EAAE,YAAY;IAY/B,kEAAkE;IAClE,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;OAGG;IACH,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjC;IAED;;;;;OAKG;IACH,IAAI,aAAa,IAAI,SAAS,GAAG,IAAI,CAGpC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa;IAUzC,OAAO,CAAC,QAAQ;IA4BhB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAejB;8BAC0B;IAC1B,OAAO,CAAC,SAAS;IAUjB;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,MAAM;IAgEd,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,cAAc;IAQtB;;;;OAIG;IACH,KAAK,IAAI,MAAM,GAAG,IAAI;IAStB,wDAAwD;IACxD,KAAK,IAAI,IAAI;CAGd"}
@@ -22,22 +22,36 @@ export interface ReaderProps {
22
22
  * @default TimeSpan.milliseconds(5)
23
23
  */
24
24
  overlapThreshold?: TimeSpan;
25
+ /**
26
+ * Deadline for a single remote fetch. A fetch that does not settle within it rejects
27
+ * the reads it serves with an Unreachable error.
28
+ * @default TimeSpan.seconds(30)
29
+ */
30
+ fetchTimeout?: TimeSpan;
25
31
  }
26
32
  /**
27
33
  * Reads historical telemetry, serving from the cache and batch-filling gaps from the
28
- * server. A batch failure rejects only the reads whose gaps were in the failed fetch.
34
+ * server. A fetch failure rejects only the reads whose gaps were in the failed fetch.
29
35
  */
30
36
  export declare class Reader {
31
37
  private readonly props;
32
38
  private readonly batcher;
33
- private readonly mu;
39
+ private closed;
40
+ private readonly inFlight;
34
41
  constructor(props: ReaderProps);
35
42
  /**
36
43
  * Reads the given time range for the given channel.
37
44
  * @throws {UnexpectedError} if the reader is closed while the read is pending.
45
+ * @throws {Unreachable} if a fetch serving the read exceeds the fetch timeout.
38
46
  */
39
47
  read(tr: TimeRange, channel: channel.Key): Promise<MultiSeries>;
40
48
  private batchRead;
49
+ private fetchGap;
50
+ private fetchWithDeadline;
51
+ /**
52
+ * Closes the reader, rejecting every pending read with an UnexpectedError. In-flight
53
+ * fetch results are discarded when they arrive.
54
+ */
41
55
  close(): Promise<void>;
42
56
  }
43
57
  //# sourceMappingURL=reader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../../../src/framer/cache/reader.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,WAAW,EAGX,SAAS,EACT,QAAQ,EACT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CACtD;AAaD,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,UAAU,EAAE,YAAY,CAAC;IACzB,qEAAqE;IACrE,KAAK,EAAE,KAAK,CAAC;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,QAAQ,CAAC;CAC7B;AAED;;;GAGG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+C;IACrE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IAExD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoC;gBAE3C,KAAK,EAAE,WAAW;IAkB9B;;;OAGG;IACG,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC;YAUvD,SAAS;IAsDjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAM7B"}
1
+ {"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../../../src/framer/cache/reader.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,WAAW,EAEX,SAAS,EACT,QAAQ,EACT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CACtD;AAqBD,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,UAAU,EAAE,YAAY,CAAC;IACzB,qEAAqE;IACrE,KAAK,EAAE,KAAK,CAAC;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,QAAQ,CAAC;IAC5B;;;;OAIG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED;;;GAGG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+C;IACrE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,MAAM,CAAS;IAGvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;gBAEnC,KAAK,EAAE,WAAW;IAkB9B;;;;OAIG;IACG,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC;YAUvD,SAAS;YA2CT,QAAQ;YAiCR,iBAAiB;IA0B/B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAO7B"}
@@ -23,10 +23,12 @@ export declare class Unary {
23
23
  flushDynamic(): void;
24
24
  writeStatic(series: MultiSeries): void;
25
25
  /**
26
- * Reads cached data overlapping the given time range. The result includes the
27
- * live leading buffer when it overlaps, but the buffer never claims coverage:
28
- * its samples carry provisional leading alignments that cannot pair with fetched
29
- * data on another channel, so its span is still reported as a gap to fetch. A
26
+ * Reads cached data overlapping the given time range. The result includes the live
27
+ * leading buffer when the data it actually holds overlaps the range, judged by the
28
+ * last stamped write rather than the buffer's provisional MAX end, so the buffer
29
+ * never leaks into reads of spans it has no samples for. It never claims coverage
30
+ * either: its samples carry provisional leading alignments that cannot pair with
31
+ * fetched data on another channel, so its span is still reported as a gap to fetch. A
30
32
  * span may therefore return in both its streamed and fetched representation.
31
33
  */
32
34
  read(tr: TimeRange): DirtyReadResult;
@@ -1 +1 @@
1
- {"version":3,"file":"unary.d.ts","sourceRoot":"","sources":["../../../../src/framer/cache/unary.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAGzE,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,SAAS,EAEd,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,UACf,SAAQ,WAAW,EAAE,IAAI,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAAG;AAEjE;;;;GAIG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,KAAK,EAAE,UAAU;IAS7B,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW;IAQ9C,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAGjC;IAED;;;;OAIG;IACH,YAAY,IAAI,IAAI;IAOpB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAKtC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,EAAE,SAAS,GAAG,eAAe;IASpC,EAAE,IAAI,SAAS;IAKf,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,SAAS;CAIlB"}
1
+ {"version":3,"file":"unary.d.ts","sourceRoot":"","sources":["../../../../src/framer/cache/unary.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAGzE,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,SAAS,EAEd,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,UACf,SAAQ,WAAW,EAAE,IAAI,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAAG;AAEjE;;;;GAIG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,KAAK,EAAE,UAAU;IAS7B,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW;IAQ9C,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAGjC;IAED;;;;OAIG;IACH,YAAY,IAAI,IAAI;IAOpB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAKtC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,EAAE,SAAS,GAAG,eAAe;IAWpC,EAAE,IAAI,SAAS;IAKf,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,SAAS;CAIlB"}
@@ -10,7 +10,7 @@ export { type ConnectOptions, default as Synnax, type SynnaxParams, synnaxParams
10
10
  export * from './connection';
11
11
  export { control } from './control';
12
12
  export { device } from './device';
13
- export { AuthError, ContiguityError, DisconnectedError, isConnectionError, MultipleFoundError, NotFoundError, QueryError, RouteError, UnexpectedError, ValidationError, } from './errors';
13
+ export { AccessDeniedError, AuthError, ContiguityError, DisconnectedError, isConnectionError, MultipleFoundError, NotFoundError, QueryError, RouteError, UnexpectedError, ValidationError, } from './errors';
14
14
  export { ethercat } from './ethercat';
15
15
  export { framer } from './framer';
16
16
  export { Frame } from './framer/frame';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/dist/testutil.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as e, G as t, K as n, V as r, W as i, Z as a, d as o, t as s, z as c } from "./client-B08VDLSA.js";
1
+ import { B as e, G as t, K as n, V as r, W as i, Z as a, d as o, t as s, z as c } from "./client-DDACDVZ-.js";
2
2
  import { DataType as l, TimeSpan as u, TimeStamp as d, array as f, errors as p, id as m } from "@synnaxlabs/x";
3
3
  import { afterAll as h } from "vitest";
4
4
  import { connect as g, createServer as _ } from "node:net";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synnaxlabs/client",
3
- "version": "0.57.0",
3
+ "version": "0.57.1",
4
4
  "description": "The Synnax Client Library",
5
5
  "keywords": [
6
6
  "synnax",
@@ -27,7 +27,7 @@
27
27
  "zod": "^4.4.3",
28
28
  "@synnaxlabs/alamos": "^0.57.0",
29
29
  "@synnaxlabs/freighter": "^0.57.0",
30
- "@synnaxlabs/x": "^0.57.0"
30
+ "@synnaxlabs/x": "^0.57.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "vitest": "^4.1.10"
@@ -44,10 +44,10 @@
44
44
  "typescript": "^7.0.2",
45
45
  "vite": "^8.2.0",
46
46
  "vitest": "^4.1.10",
47
- "@synnaxlabs/madge": "^0.0.0",
48
- "@synnaxlabs/tsconfig": "^0.0.0",
49
47
  "@synnaxlabs/eslint-config": "^0.0.0",
50
- "@synnaxlabs/vite-plugin": "^0.0.0"
48
+ "@synnaxlabs/madge": "^0.0.0",
49
+ "@synnaxlabs/vite-plugin": "^0.0.0",
50
+ "@synnaxlabs/tsconfig": "^0.0.0"
51
51
  },
52
52
  "type": "module",
53
53
  "files": [
@@ -13,6 +13,8 @@ import {
13
13
  math,
14
14
  MultiSeries,
15
15
  Series,
16
+ Size,
17
+ TimeRange,
16
18
  type TimeSpan,
17
19
  TimeStamp,
18
20
  } from "@synnaxlabs/x";
@@ -52,6 +54,34 @@ const MAX_DEF_WRITES = 100;
52
54
  // Variable-rate types allocate bytes, not samples. Rough bytes-per-sample estimate.
53
55
  const VARIABLE_DT_MULTIPLIER = 40;
54
56
 
57
+ /**
58
+ * @returns true when every byte of the series fits in the buffer's free space. Measured
59
+ * in bytes because a variable-length buffer counts its capacity that way.
60
+ */
61
+ const fits = (buffer: Series, series: Series): boolean =>
62
+ series.byteLength.valueOf() <=
63
+ buffer.byteCapacity.valueOf() - buffer.byteLength.valueOf();
64
+
65
+ // Bounds on compacting a buffer that rotated early. Both have to trip: the fraction
66
+ // spares a nearly full buffer, the floor one whose waste is not worth a copy.
67
+ const COMPACT_MAX_FILL = 0.25;
68
+ const COMPACT_MIN_WASTE = Size.kilobytes(256);
69
+
70
+ /**
71
+ * @returns true when copying the buffer's samples into a right-sized series is worth
72
+ * the copy. A held buffer never qualifies: the holder keeps the original alive, so a
73
+ * copy would only add a second allocation, and it would break the object identity a
74
+ * consumer that both reads and streams deduplicates on.
75
+ */
76
+ const shouldCompact = (buffer: Series): boolean => {
77
+ if (buffer.refCount > 0 || buffer.length === 0) return false;
78
+ const used = buffer.byteLength.valueOf();
79
+ const capacity = buffer.byteCapacity.valueOf();
80
+ return (
81
+ capacity - used > COMPACT_MIN_WASTE.valueOf() && used < capacity * COMPACT_MAX_FILL
82
+ );
83
+ };
84
+
55
85
  /**
56
86
  * A cache for channel data that maintains a single, rolling Series as a buffer
57
87
  * for channel data. The buffer's data type is derived from the first written series,
@@ -96,6 +126,17 @@ export class Dynamic {
96
126
  return this.curr;
97
127
  }
98
128
 
129
+ /**
130
+ * @returns the time range the buffer's samples actually cover: its start to the last
131
+ * stamped write, or the wall clock when the buffer holds unstamped data. Unlike the
132
+ * buffer's provisional time range, the end never claims future time. Null when there
133
+ * is no buffer.
134
+ */
135
+ get dataTimeRange(): TimeRange | null {
136
+ if (this.curr == null) return null;
137
+ return new TimeRange(this.curr.timeRange.start, this.currDataEnd ?? this.now());
138
+ }
139
+
99
140
  /**
100
141
  * @returns a list of buffers that were filled by the cache during the write. If the
101
142
  * current buffer is able to fit all writes, no buffers will be returned.
@@ -115,7 +156,6 @@ export class Dynamic {
115
156
  alignment: bigint,
116
157
  start: TimeStamp,
117
158
  source: Series,
118
- sampleIndex: number,
119
159
  ): Series {
120
160
  this.counter++;
121
161
  const dt = this.props.transform.resolveDataType(source.dataType);
@@ -127,8 +167,7 @@ export class Dynamic {
127
167
  let sampleOffset: math.Numeric = 0;
128
168
  if (narrowing)
129
169
  if (source.dataType.equals(DataType.TIMESTAMP)) sampleOffset = start.valueOf();
130
- else if (sampleIndex < source.length)
131
- sampleOffset = BigInt(source.data[sampleIndex].valueOf());
170
+ else if (source.length > 0) sampleOffset = BigInt(source.data[0].valueOf());
132
171
  return Series.alloc({
133
172
  capacity: dt.isVariable ? capacity * VARIABLE_DT_MULTIPLIER : capacity,
134
173
  dataType: dt,
@@ -140,6 +179,18 @@ export class Dynamic {
140
179
  });
141
180
  }
142
181
 
182
+ /**
183
+ * @returns the sample capacity for a buffer that must hold the given series whole:
184
+ * the configured size, or the series' own size when that is larger.
185
+ */
186
+ private capacityFor(source: Series): number {
187
+ const dt = this.props.transform.resolveDataType(source.dataType);
188
+ const required = dt.isVariable
189
+ ? Math.ceil(source.byteLength.valueOf() / VARIABLE_DT_MULTIPLIER)
190
+ : source.length;
191
+ return Math.max(this.nextBufferSize(), required);
192
+ }
193
+
143
194
  // Unstamped series (virtual channels, writes without an in-frame index) fall back
144
195
  // to the wall clock.
145
196
  private allocStart(series: Series): TimeStamp {
@@ -148,28 +199,41 @@ export class Dynamic {
148
199
 
149
200
  private allocCurr(
150
201
  res: WriteResponse,
151
- alignment: bigint,
152
- start: TimeStamp,
153
202
  source: Series,
154
- sampleIndex: number,
203
+ start: TimeStamp | null,
155
204
  ): Series {
156
205
  this.curr = this.allocate(
157
- this.nextBufferSize(),
158
- alignment,
159
- start,
206
+ this.capacityFor(source),
207
+ source.alignment,
208
+ start ?? this.allocStart(source),
160
209
  source,
161
- sampleIndex,
162
210
  );
163
211
  res.allocated.push(this.curr);
164
212
  return this.curr;
165
213
  }
166
214
 
167
- private flushCurr(res: WriteResponse): void {
168
- if (this.curr == null) return;
169
- this.curr.timeRange.end = this.currDataEnd ?? this.now();
215
+ /** @returns the timestamp the flushed buffer's end was stamped with, or null when
216
+ * there was no buffer. */
217
+ private flushCurr(res: WriteResponse): TimeStamp | null {
218
+ if (this.curr == null) return null;
219
+ const end = this.currDataEnd ?? this.now();
220
+ this.curr.timeRange.end = end;
170
221
  this.currDataEnd = null;
171
- res.flushed.push(this.curr);
222
+ res.flushed.push(this.compacted(this.curr, res));
172
223
  this.curr = null;
224
+ return end;
225
+ }
226
+
227
+ /**
228
+ * @returns a right-sized copy of a buffer that rotated with most of its space
229
+ * unused, or the buffer itself. A rotated buffer holds its whole allocation until
230
+ * it is collected, which is wasteful when an oversized series ended it early.
231
+ */
232
+ private compacted(buffer: Series, res: WriteResponse): Series {
233
+ // A buffer allocated during this same write has not reached its subscribers yet,
234
+ // so its reference count cannot yet report who is about to hold it.
235
+ if (!shouldCompact(buffer) || res.allocated.series.includes(buffer)) return buffer;
236
+ return buffer.compact();
173
237
  }
174
238
 
175
239
  private _write(series: Series, res: WriteResponse): void {
@@ -194,9 +258,8 @@ export class Dynamic {
194
258
  }
195
259
  }
196
260
  let curr = this.curr;
197
- if (curr == null)
198
- curr = this.allocCurr(res, series.alignment, this.allocStart(series), series, 0);
199
- else {
261
+ let trimmed = false;
262
+ if (curr != null) {
200
263
  // overlap > 0: the incoming series steps back into samples the current buffer
201
264
  // already holds. overlap < 0: there is a gap between the buffer and the series.
202
265
  const overlap = Number(curr.alignment + BigInt(curr.length) - series.alignment);
@@ -205,38 +268,36 @@ export class Dynamic {
205
268
  // the cache with overlapping series. sub() is zero-copy.
206
269
  if (overlap >= series.length) return;
207
270
  series = series.sub(overlap);
271
+ trimmed = true;
208
272
  } else if (overlap < 0) {
209
273
  this.flushCurr(res);
210
- curr = this.allocCurr(
211
- res,
212
- series.alignment,
213
- this.allocStart(series),
214
- series,
215
- 0,
216
- );
274
+ curr = null;
217
275
  }
218
276
  }
219
- while (true) {
220
- const converted = transform.convert(series, curr.sampleOffset);
221
- const amountWritten = curr.write(converted);
222
- if (amountWritten === series.length) {
223
- this.currDataEnd = series.timeRange.isZero ? null : series.timeRange.end;
224
- this.updateAvgRate(series);
225
- return;
277
+ const convert = (buffer: Series): Series =>
278
+ transform.convert(series, buffer.sampleOffset);
279
+ let rotationStart: TimeStamp | null = null;
280
+ if (curr != null) {
281
+ const converted = convert(curr);
282
+ // A series never splits across two buffers. The timestamp where a split falls is
283
+ // not in the data, so both sides would have to guess the boundary they share, and
284
+ // a guess lands outside the data whenever samples are irregular.
285
+ if (fits(curr, converted)) curr.write(converted);
286
+ else {
287
+ const end = this.flushCurr(res);
288
+ // A trimmed series keeps the whole frame's start, which is earlier than the
289
+ // samples it still holds. Those samples begin where the buffer they were
290
+ // trimmed against ends, so the next buffer starts there.
291
+ if (trimmed) rotationStart = end;
292
+ curr = null;
226
293
  }
227
- // The timestamp at the split point is unknowable from the data series, so
228
- // both sides fall back to the wall clock.
229
- this.currDataEnd = null;
230
- this.flushCurr(res);
231
- curr = this.allocCurr(
232
- res,
233
- series.alignment + BigInt(amountWritten),
234
- this.now(),
235
- series,
236
- amountWritten,
237
- );
238
- series = series.slice(amountWritten);
239
294
  }
295
+ if (curr == null) {
296
+ curr = this.allocCurr(res, series, rotationStart);
297
+ curr.write(convert(curr));
298
+ }
299
+ this.currDataEnd = series.timeRange.isZero ? null : series.timeRange.end;
300
+ this.updateAvgRate(series);
240
301
  }
241
302
 
242
303
  private updateAvgRate(series: Series): void {
@@ -7,11 +7,12 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
+ import { Unreachable } from "@synnaxlabs/freighter";
10
11
  import {
11
12
  debounce,
13
+ errors,
12
14
  MultiSeries,
13
15
  type Series,
14
- sync,
15
16
  TimeRange,
16
17
  TimeSpan,
17
18
  } from "@synnaxlabs/x";
@@ -31,10 +32,18 @@ interface ReadRequest {
31
32
  gaps: TimeRange[];
32
33
  }
33
34
 
35
+ // Tracks one read's outstanding fetches, so the read settles when its last fetch lands.
36
+ // The first failure among them wins.
37
+ interface Tracker {
38
+ entry: debounce.Entry<ReadRequest>;
39
+ remaining: number;
40
+ failure?: unknown;
41
+ }
42
+
34
43
  interface BatchFetch {
35
44
  gap: TimeRange;
36
45
  channels: Set<channel.Key>;
37
- entries: Set<debounce.Entry<ReadRequest>>;
46
+ trackers: Set<Tracker>;
38
47
  }
39
48
 
40
49
  export interface ReaderProps {
@@ -53,17 +62,25 @@ export interface ReaderProps {
53
62
  * @default TimeSpan.milliseconds(5)
54
63
  */
55
64
  overlapThreshold?: TimeSpan;
65
+ /**
66
+ * Deadline for a single remote fetch. A fetch that does not settle within it rejects
67
+ * the reads it serves with an Unreachable error.
68
+ * @default TimeSpan.seconds(30)
69
+ */
70
+ fetchTimeout?: TimeSpan;
56
71
  }
57
72
 
58
73
  /**
59
74
  * Reads historical telemetry, serving from the cache and batch-filling gaps from the
60
- * server. A batch failure rejects only the reads whose gaps were in the failed fetch.
75
+ * server. A fetch failure rejects only the reads whose gaps were in the failed fetch.
61
76
  */
62
77
  export class Reader {
63
78
  private readonly props: Required<Omit<ReaderProps, "batchDebounce">>;
64
79
  private readonly batcher: debounce.Batcher<ReadRequest>;
65
- // Serializes batch execution against close, so close waits for the batch in flight.
66
- private readonly mu = sync.newMutex({ closed: false });
80
+ private closed = false;
81
+ // Reads whose fetches are in flight, so close can reject them without waiting on the
82
+ // fetches themselves.
83
+ private readonly inFlight = new Set<Tracker>();
67
84
 
68
85
  constructor(props: ReaderProps) {
69
86
  const {
@@ -71,90 +88,144 @@ export class Reader {
71
88
  cache,
72
89
  batchDebounce = TimeSpan.milliseconds(50),
73
90
  overlapThreshold = TimeSpan.milliseconds(5),
91
+ fetchTimeout = TimeSpan.seconds(30),
74
92
  } = props;
75
- this.props = { readRemote, cache, overlapThreshold };
93
+ this.props = { readRemote, cache, overlapThreshold, fetchTimeout };
76
94
  this.batcher = new debounce.Batcher({
77
95
  interval: batchDebounce,
78
- exec: async (entries) =>
79
- await this.mu.runExclusive(async () => {
80
- if (this.mu.closed) throw new UnexpectedError("telemetry reader is closed");
81
- await this.batchRead(entries);
82
- }),
96
+ exec: async (entries) => {
97
+ if (this.closed) throw new UnexpectedError("telemetry reader is closed");
98
+ await this.batchRead(entries);
99
+ },
83
100
  });
84
101
  }
85
102
 
86
103
  /**
87
104
  * Reads the given time range for the given channel.
88
105
  * @throws {UnexpectedError} if the reader is closed while the read is pending.
106
+ * @throws {Unreachable} if a fetch serving the read exceeds the fetch timeout.
89
107
  */
90
108
  async read(tr: TimeRange, channel: channel.Key): Promise<MultiSeries> {
91
109
  const { cache } = this.props;
92
110
  const unary = cache.get(channel);
93
111
  const { series, gaps } = unary.read(tr);
94
112
  if (gaps.length === 0) return series;
95
- if (this.mu.closed) throw new UnexpectedError("telemetry reader is closed");
113
+ if (this.closed) throw new UnexpectedError("telemetry reader is closed");
96
114
  await this.batcher.enqueue({ channel, gaps });
97
115
  return unary.read(tr).series;
98
116
  }
99
117
 
100
118
  private async batchRead(entries: Array<debounce.Entry<ReadRequest>>): Promise<void> {
101
- const { readRemote, cache, overlapThreshold } = this.props;
119
+ const { cache, overlapThreshold } = this.props;
102
120
  const batched: BatchFetch[] = [];
121
+ const trackers = new Map<debounce.Entry<ReadRequest>, Tracker>();
103
122
  entries.forEach((entry) => {
104
123
  const unary = cache.get(entry.req.channel);
105
124
  entry.req.gaps.forEach((rawGap) => {
106
- // A concurrent batch or a streaming write may have filled part of the gap
107
- // while this entry sat in the debounce window.
125
+ // An earlier batch or a streaming write may have filled part of the gap while
126
+ // this entry sat in the debounce window.
108
127
  unary.read(rawGap).gaps.forEach((gap) => {
128
+ let tracker = trackers.get(entry);
129
+ if (tracker == null) {
130
+ tracker = { entry, remaining: 0 };
131
+ trackers.set(entry, tracker);
132
+ this.inFlight.add(tracker);
133
+ }
109
134
  const g = batched.find((r) => r.gap.equals(gap, overlapThreshold));
110
- if (g == null)
135
+ if (g == null) {
111
136
  batched.push({
112
137
  gap,
113
138
  channels: new Set([entry.req.channel]),
114
- entries: new Set([entry]),
139
+ trackers: new Set([tracker]),
115
140
  });
116
- else {
141
+ tracker.remaining++;
142
+ } else {
117
143
  g.channels.add(entry.req.channel);
118
144
  g.gap = TimeRange.max(g.gap, gap);
119
- g.entries.add(entry);
145
+ if (!g.trackers.has(tracker)) {
146
+ g.trackers.add(tracker);
147
+ tracker.remaining++;
148
+ }
120
149
  }
121
150
  });
122
151
  });
123
152
  });
124
- const failures = new Map<debounce.Entry<ReadRequest>, unknown>();
125
- await Promise.all(
126
- batched.map(async ({ gap, channels, entries: served }) => {
127
- try {
128
- const frame = await readRemote(gap, Array.from(channels));
129
- // Group series by key in one pass; per-key get() scans the whole frame.
130
- const grouped = new Map<channel.Key, Series[]>();
131
- frame.forEach((k, s) => {
132
- const key = k as channel.Key;
133
- const existing = grouped.get(key);
134
- if (existing == null) grouped.set(key, [s]);
135
- else existing.push(s);
136
- });
137
- channels.forEach((key) =>
138
- cache.get(key).writeStatic(new MultiSeries(grouped.get(key) ?? [])),
139
- );
140
- } catch (err) {
141
- served.forEach((entry) => {
142
- if (!failures.has(entry)) failures.set(entry, err);
143
- });
144
- }
145
- }),
146
- );
147
153
  entries.forEach((entry) => {
148
- const err = failures.get(entry);
149
- if (err == null) entry.resolve();
150
- else entry.reject(err);
154
+ if (!trackers.has(entry)) entry.resolve();
151
155
  });
156
+ await Promise.all(batched.map(async (b) => await this.fetchGap(b)));
152
157
  }
153
158
 
154
- async close(): Promise<void> {
155
- this.batcher.close(new UnexpectedError("telemetry reader is closed"));
156
- await this.mu.runExclusive(async () => {
157
- this.mu.closed = true;
159
+ // Never throws: every outcome, including a deadline expiry and a post-close arrival,
160
+ // settles through the trackers.
161
+ private async fetchGap({ gap, channels, trackers }: BatchFetch): Promise<void> {
162
+ const { cache } = this.props;
163
+ let failure: unknown;
164
+ try {
165
+ const frame = await this.fetchWithDeadline(gap, Array.from(channels));
166
+ if (!this.closed) {
167
+ // Group series by key in one pass; per-key get() scans the whole frame.
168
+ const grouped = new Map<channel.Key, Series[]>();
169
+ frame.forEach((k, s) => {
170
+ const key = k as channel.Key;
171
+ const existing = grouped.get(key);
172
+ if (existing == null) grouped.set(key, [s]);
173
+ else existing.push(s);
174
+ });
175
+ channels.forEach((key) =>
176
+ cache.get(key).writeStatic(new MultiSeries(grouped.get(key) ?? [])),
177
+ );
178
+ }
179
+ } catch (err) {
180
+ failure = err;
181
+ }
182
+ trackers.forEach((tracker) => {
183
+ // close() already settled it.
184
+ if (!this.inFlight.has(tracker)) return;
185
+ if (failure != null) tracker.failure ??= failure;
186
+ tracker.remaining--;
187
+ if (tracker.remaining > 0) return;
188
+ this.inFlight.delete(tracker);
189
+ if (tracker.failure == null) tracker.entry.resolve();
190
+ else tracker.entry.reject(tracker.failure);
158
191
  });
159
192
  }
193
+
194
+ private async fetchWithDeadline(
195
+ gap: TimeRange,
196
+ channels: channel.Key[],
197
+ ): Promise<Frame> {
198
+ const { readRemote, fetchTimeout } = this.props;
199
+ let timer: ReturnType<typeof setTimeout> | undefined;
200
+ const deadline = new Promise<never>((_, reject) => {
201
+ timer = setTimeout(() => {
202
+ const timeout = fetchTimeout.toString();
203
+ const message = `gap read for ${gap.toString()} timed out after ${timeout}`;
204
+ reject(new Unreachable({ message }));
205
+ }, fetchTimeout.milliseconds);
206
+ });
207
+ const fetched = readRemote(gap, channels);
208
+ try {
209
+ return await Promise.race([fetched, deadline]);
210
+ } catch (err) {
211
+ // The read already failed for its callers; a late settle of the losing fetch must
212
+ // not surface as an unhandled rejection.
213
+ fetched.catch(() => {});
214
+ throw errors.fromUnknown(err);
215
+ } finally {
216
+ clearTimeout(timer);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Closes the reader, rejecting every pending read with an UnexpectedError. In-flight
222
+ * fetch results are discarded when they arrive.
223
+ */
224
+ async close(): Promise<void> {
225
+ this.closed = true;
226
+ const err = new UnexpectedError("telemetry reader is closed");
227
+ this.batcher.close(err);
228
+ this.inFlight.forEach(({ entry }) => entry.reject(err));
229
+ this.inFlight.clear();
230
+ }
160
231
  }