@bldrs-ai/conway 1.401.1237 → 1.404.1241

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 (25) hide show
  1. package/compiled/examples/browser-bundled.cjs +1 -1
  2. package/compiled/examples/cli-bundled.cjs +1 -1
  3. package/compiled/examples/cli-step-bundled.cjs +1 -1
  4. package/compiled/examples/validator-bundled.cjs +1 -1
  5. package/compiled/src/core/demand_geometry_queue.d.ts +147 -0
  6. package/compiled/src/core/demand_geometry_queue.d.ts.map +1 -0
  7. package/compiled/src/core/demand_geometry_queue.js +223 -0
  8. package/compiled/src/core/demand_geometry_queue.test.d.ts +2 -0
  9. package/compiled/src/core/demand_geometry_queue.test.d.ts.map +1 -0
  10. package/compiled/src/core/demand_geometry_queue.test.js +133 -0
  11. package/compiled/src/step/parsing/index_sidecar.d.ts +53 -0
  12. package/compiled/src/step/parsing/index_sidecar.d.ts.map +1 -0
  13. package/compiled/src/step/parsing/index_sidecar.js +166 -0
  14. package/compiled/src/step/parsing/index_sidecar.test.d.ts +2 -0
  15. package/compiled/src/step/parsing/index_sidecar.test.d.ts.map +1 -0
  16. package/compiled/src/step/parsing/index_sidecar.test.js +92 -0
  17. package/compiled/src/step/parsing/range_byte_source.d.ts +69 -0
  18. package/compiled/src/step/parsing/range_byte_source.d.ts.map +1 -0
  19. package/compiled/src/step/parsing/range_byte_source.js +78 -0
  20. package/compiled/src/step/parsing/range_byte_source.test.d.ts +2 -0
  21. package/compiled/src/step/parsing/range_byte_source.test.d.ts.map +1 -0
  22. package/compiled/src/step/parsing/range_byte_source.test.js +70 -0
  23. package/compiled/src/version/version.js +1 -1
  24. package/compiled/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Index sidecar (M4): a compact, version-stamped binary serialisation of a
3
+ * model's parse index, so a revisit (or a first visit with a server-provided
4
+ * sidecar) can open **index-first** — reconstruct the entity index without
5
+ * re-scanning the source, then fetch only the byte ranges demand asks for
6
+ * (see RangeByteSource).
7
+ *
8
+ * Layout (all little-endian):
9
+ *
10
+ * 'CIDX' 4 magic
11
+ * version u32
12
+ * sourceByteLength f64 (length of the source the index was built from)
13
+ * sourceHash u32 (hash of the source bytes — the handshake)
14
+ * recordCount u32
15
+ * address[] f64 × recordCount (file-absolute; f64 tolerates >4 GB)
16
+ * length[] u32 × recordCount
17
+ * typeID[] i32 × recordCount (−1 = undefined type)
18
+ * expressID[] u32 × recordCount
19
+ *
20
+ * This carries the top-level record columns — enough to answer type/express
21
+ * queries and locate every record's bytes. Inline / multi-mapping child
22
+ * serialisation is a v2 extension (records with children re-materialise their
23
+ * children from the record bytes on demand); `hasChildren` flags which
24
+ * records the sidecar under-describes so a consumer can fall back for them.
25
+ *
26
+ * The sidecar is a **cache, not an interchange format**: it is only trusted
27
+ * after `sourceHash` + `sourceByteLength` match the actual source. A mismatch
28
+ * means fall back to a cold scan — never serve wrong bytes. Production should
29
+ * swap the placeholder 32-bit hash for a strong digest (e.g. SHA-256); the
30
+ * format reserves a fixed slot so that is a version bump, not a reshape.
31
+ */
32
+ const MAGIC = 0x58444943; // 'CIDX' little-endian
33
+ const VERSION = 1;
34
+ const UNDEFINED_TYPE = -1;
35
+ // FNV-1a 32-bit parameters (see hashSource).
36
+ const FNV_OFFSET_BASIS = 2166136261;
37
+ const FNV_PRIME = 16777619;
38
+ const HEX = 16;
39
+ // Byte sizes of the fixed header fields, in order.
40
+ const HEADER_BYTES = 4 + // magic
41
+ 4 + // version
42
+ 8 + // sourceByteLength (f64)
43
+ 4 + // sourceHash (u32)
44
+ 4; // recordCount (u32)
45
+ const ADDRESS_BYTES = 8;
46
+ const LENGTH_BYTES = 4;
47
+ const TYPEID_BYTES = 4;
48
+ const EXPRESSID_BYTES = 4;
49
+ /**
50
+ * A placeholder strong-ish hash of the source bytes (FNV-1a, 32-bit). Stands
51
+ * in for a production digest (SHA-256); only the fixed slot matters to the
52
+ * format. Sufficient to demonstrate the mismatch → cold-scan handshake.
53
+ *
54
+ * @param source The source bytes.
55
+ * @return {number} A 32-bit unsigned hash.
56
+ */
57
+ export function hashSource(source) {
58
+ let hash = FNV_OFFSET_BASIS >>> 0;
59
+ for (let where = 0; where < source.length; ++where) {
60
+ hash ^= source[where];
61
+ hash = Math.imul(hash, FNV_PRIME) >>> 0;
62
+ }
63
+ return hash >>> 0;
64
+ }
65
+ /**
66
+ * Serialise a model's top-level element index to a sidecar blob.
67
+ *
68
+ * @param elements The top-level element index.
69
+ * @param sourceByteLength The length of the source it was built from.
70
+ * @param sourceHash The source hash (see {@link hashSource}).
71
+ * @return {Uint8Array} The sidecar bytes.
72
+ */
73
+ export function serializeIndexSidecar(elements, sourceByteLength, sourceHash) {
74
+ const recordCount = elements.length;
75
+ const total = HEADER_BYTES +
76
+ recordCount * (ADDRESS_BYTES + LENGTH_BYTES + TYPEID_BYTES + EXPRESSID_BYTES);
77
+ const bytes = new Uint8Array(total);
78
+ const view = new DataView(bytes.buffer);
79
+ let offset = 0;
80
+ view.setUint32(offset, MAGIC, true);
81
+ offset += 4;
82
+ view.setUint32(offset, VERSION, true);
83
+ offset += 4;
84
+ view.setFloat64(offset, sourceByteLength, true);
85
+ offset += 8;
86
+ view.setUint32(offset, sourceHash >>> 0, true);
87
+ offset += 4;
88
+ view.setUint32(offset, recordCount, true);
89
+ offset += 4;
90
+ // Column-major: better compression locality than row-major, and it mirrors
91
+ // the SoA columns the model rebuilds.
92
+ for (const element of elements) {
93
+ view.setFloat64(offset, element.address, true);
94
+ offset += ADDRESS_BYTES;
95
+ }
96
+ for (const element of elements) {
97
+ view.setUint32(offset, element.length, true);
98
+ offset += LENGTH_BYTES;
99
+ }
100
+ for (const element of elements) {
101
+ const typeID = element.typeID;
102
+ view.setInt32(offset, typeID === void 0 ? UNDEFINED_TYPE : typeID, true);
103
+ offset += TYPEID_BYTES;
104
+ }
105
+ for (const element of elements) {
106
+ view.setUint32(offset, element.expressID, true);
107
+ offset += EXPRESSID_BYTES;
108
+ }
109
+ return bytes;
110
+ }
111
+ /**
112
+ * Deserialise a sidecar blob back to the top-level element index.
113
+ *
114
+ * @param bytes The sidecar bytes.
115
+ * @return {DecodedSidecar} The decoded source identity + element index.
116
+ */
117
+ export function deserializeIndexSidecar(bytes) {
118
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
119
+ let offset = 0;
120
+ const magic = view.getUint32(offset, true);
121
+ offset += 4;
122
+ if (magic !== MAGIC) {
123
+ throw new Error(`Not a sidecar: bad magic 0x${magic.toString(HEX)}`);
124
+ }
125
+ const version = view.getUint32(offset, true);
126
+ offset += 4;
127
+ if (version !== VERSION) {
128
+ throw new Error(`Unsupported sidecar version ${version}`);
129
+ }
130
+ const sourceByteLength = view.getFloat64(offset, true);
131
+ offset += 8;
132
+ const sourceHash = view.getUint32(offset, true);
133
+ offset += 4;
134
+ const recordCount = view.getUint32(offset, true);
135
+ offset += 4;
136
+ const addressBase = offset;
137
+ const lengthBase = addressBase + recordCount * ADDRESS_BYTES;
138
+ const typeIDBase = lengthBase + recordCount * LENGTH_BYTES;
139
+ const expressIDBase = typeIDBase + recordCount * TYPEID_BYTES;
140
+ const elements = new Array(recordCount);
141
+ const hasChildren = new Array(recordCount).fill(false);
142
+ for (let where = 0; where < recordCount; ++where) {
143
+ const typeID = view.getInt32(typeIDBase + where * TYPEID_BYTES, true);
144
+ elements[where] = {
145
+ address: view.getFloat64(addressBase + where * ADDRESS_BYTES, true),
146
+ length: view.getUint32(lengthBase + where * LENGTH_BYTES, true),
147
+ typeID: (typeID === UNDEFINED_TYPE ? void 0 : typeID),
148
+ expressID: view.getUint32(expressIDBase + where * EXPRESSID_BYTES, true),
149
+ };
150
+ }
151
+ return { version, sourceByteLength, sourceHash, elements, hasChildren };
152
+ }
153
+ /**
154
+ * Validate a decoded sidecar against the actual source identity. The
155
+ * index-first open path calls this before trusting the sidecar; on false it
156
+ * must fall back to a cold scan.
157
+ *
158
+ * @param sidecar The decoded sidecar.
159
+ * @param sourceByteLength The actual source length.
160
+ * @param sourceHash The actual source hash.
161
+ * @return {boolean} True if the sidecar matches the source.
162
+ */
163
+ export function sidecarMatchesSource(sidecar, sourceByteLength, sourceHash) {
164
+ return sidecar.sourceByteLength === sourceByteLength &&
165
+ sidecar.sourceHash === (sourceHash >>> 0);
166
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index_sidecar.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index_sidecar.test.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/index_sidecar.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,92 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // M4: the index sidecar must serialise a model's parse index and deserialise
3
+ // it back byte-identically, so an index-first open reconstructs the entity
4
+ // index without re-scanning the source — and it must refuse to be trusted when
5
+ // the source it was built from no longer matches (hash / length handshake).
6
+ import * as fs from "fs";
7
+ import { describe, expect, test } from "@jest/globals";
8
+ import ParsingBuffer from "../../parsing/parsing_buffer.js";
9
+ import IfcStepParser from "../../ifc/ifc_step_parser.js";
10
+ import { deserializeIndexSidecar, hashSource, serializeIndexSidecar, sidecarMatchesSource, } from "./index_sidecar.js";
11
+ /**
12
+ * Parse index.ifc resident and return its top-level element index plus the
13
+ * source bytes it was built from.
14
+ *
15
+ * @return {object} `{ bytes, elements }`.
16
+ */
17
+ function residentIndex() {
18
+ const bytes = new Uint8Array(fs.readFileSync("data/index.ifc"));
19
+ const input = new ParsingBuffer(bytes);
20
+ IfcStepParser.Instance.parseHeader(input);
21
+ const [index,] = IfcStepParser.Instance.parseDataBlock(input);
22
+ return { bytes, elements: index.elements };
23
+ }
24
+ describe("index sidecar", () => {
25
+ test("round-trips the top-level index byte-identically", () => {
26
+ const { bytes, elements } = residentIndex();
27
+ const hash = hashSource(bytes);
28
+ const blob = serializeIndexSidecar(elements, bytes.byteLength, hash);
29
+ const decoded = deserializeIndexSidecar(blob);
30
+ expect(decoded.sourceByteLength).toBe(bytes.byteLength);
31
+ expect(decoded.sourceHash).toBe(hash);
32
+ expect(decoded.elements.length).toBe(elements.length);
33
+ for (let where = 0; where < elements.length; ++where) {
34
+ const original = elements[where];
35
+ const restored = decoded.elements[where];
36
+ expect(restored.address).toBe(original.address);
37
+ expect(restored.length).toBe(original.length);
38
+ expect(restored.expressID).toBe(original.expressID);
39
+ // typeID round-trips including the undefined case (stored as -1).
40
+ expect(restored.typeID).toBe(original.typeID);
41
+ }
42
+ });
43
+ test("accepts a sidecar whose hash and length match the source", () => {
44
+ const { bytes, elements } = residentIndex();
45
+ const hash = hashSource(bytes);
46
+ const decoded = deserializeIndexSidecar(serializeIndexSidecar(elements, bytes.byteLength, hash));
47
+ expect(sidecarMatchesSource(decoded, bytes.byteLength, hash)).toBe(true);
48
+ });
49
+ test("rejects a sidecar when the source bytes changed (hash mismatch)", () => {
50
+ const { bytes, elements } = residentIndex();
51
+ const hash = hashSource(bytes);
52
+ const decoded = deserializeIndexSidecar(serializeIndexSidecar(elements, bytes.byteLength, hash));
53
+ // Same length, one byte flipped → different hash → must not be trusted.
54
+ const mutated = bytes.slice();
55
+ mutated[Math.floor(mutated.length / 2)] ^= 255;
56
+ expect(sidecarMatchesSource(decoded, mutated.byteLength, hashSource(mutated)))
57
+ .toBe(false);
58
+ });
59
+ test("rejects a sidecar when the source length changed", () => {
60
+ const { bytes, elements } = residentIndex();
61
+ const hash = hashSource(bytes);
62
+ const decoded = deserializeIndexSidecar(serializeIndexSidecar(elements, bytes.byteLength, hash));
63
+ expect(sidecarMatchesSource(decoded, bytes.byteLength + 1, hash))
64
+ .toBe(false);
65
+ });
66
+ test("hashSource is deterministic and sensitive to content", () => {
67
+ const a = new Uint8Array([1, 2, 3, 4, 5]);
68
+ const b = new Uint8Array([1, 2, 3, 4, 5]);
69
+ const c = new Uint8Array([1, 2, 3, 4, 6]);
70
+ expect(hashSource(a)).toBe(hashSource(b));
71
+ expect(hashSource(a)).not.toBe(hashSource(c));
72
+ });
73
+ test("preserves undefined typeID through the -1 sentinel", () => {
74
+ const elements = [
75
+ { address: 0, length: 10, typeID: 7, expressID: 1 },
76
+ { address: 10, length: 20, typeID: void 0, expressID: 2 },
77
+ { address: 30, length: 5, typeID: 0, expressID: 3 },
78
+ ];
79
+ const decoded = deserializeIndexSidecar(serializeIndexSidecar(elements, 35, 0));
80
+ expect(decoded.elements[0].typeID).toBe(7);
81
+ expect(decoded.elements[1].typeID).toBe(void 0);
82
+ expect(decoded.elements[2].typeID).toBe(0);
83
+ });
84
+ test("throws on a blob with bad magic", () => {
85
+ const garbage = new Uint8Array(32);
86
+ expect(() => deserializeIndexSidecar(garbage)).toThrow(/magic/);
87
+ });
88
+ test("round-trips an empty index", () => {
89
+ const decoded = deserializeIndexSidecar(serializeIndexSidecar([], 0, 0));
90
+ expect(decoded.elements.length).toBe(0);
91
+ });
92
+ });
@@ -0,0 +1,69 @@
1
+ import { StepExternalByteStore } from "../step_buffer_provider.js";
2
+ /**
3
+ * Fetch accounting for a {@link RangeByteSource}: how many range requests were
4
+ * issued and how many bytes actually crossed the wire, versus how many the
5
+ * caller asked for. The gap between `bytesFetched` and `bytesServed` is the
6
+ * over-read imposed by block alignment (a real HTTP-Range / OPFS-block store
7
+ * can only fetch whole blocks); a small gap means the access pattern has good
8
+ * locality — the property an index-first open (M4) leans on.
9
+ */
10
+ export interface RangeFetchStats {
11
+ /** Number of `read` calls that reached the backing store. */
12
+ requestCount: number;
13
+ /** Total bytes actually fetched from the backing store (aligned spans). */
14
+ bytesFetched: number;
15
+ /** Total bytes handed back to callers (the sum of requested lengths). */
16
+ bytesServed: number;
17
+ }
18
+ /**
19
+ * An asynchronous {@link StepExternalByteStore} that models a **range-fetched**
20
+ * source — an HTTP server honouring `Range:` requests, or an OPFS/blob store
21
+ * read block-by-block — so the index-first open path (M4) can be exercised and
22
+ * measured without a network. It wraps a fully-resident buffer (the "server")
23
+ * but never assumes the client holds it: every `read` is counted as a fetch,
24
+ * and with a `blockBytes` grid the fetch is rounded out to whole blocks, so
25
+ * {@link stats} reports the real transfer an aligned store would incur.
26
+ *
27
+ * This is the source a sidecar-driven open pairs with: reconstruct the entity
28
+ * index from the sidecar (no scan), then pull only the byte ranges demand asks
29
+ * for through a source like this — and read back from `stats` how little of the
30
+ * file that touched.
31
+ */
32
+ export declare class RangeByteSource implements StepExternalByteStore {
33
+ private readonly bytes_;
34
+ private readonly blockBytes_;
35
+ private requestCount_;
36
+ private bytesFetched_;
37
+ private bytesServed_;
38
+ /**
39
+ * @param bytes_ The full backing bytes (the notional server-side file). Not
40
+ * copied; treated as read-only.
41
+ * @param blockBytes Optional fetch granularity: reads are rounded out to a
42
+ * multiple of this so `bytesFetched` reflects whole-block transfer. Omit (or
43
+ * pass 0) for exact, unaligned fetches.
44
+ */
45
+ constructor(bytes_: Uint8Array, blockBytes?: number);
46
+ /**
47
+ * Total size of the stored byte sequence.
48
+ *
49
+ * @return {number} The byte length.
50
+ */
51
+ get byteLength(): number;
52
+ /**
53
+ * A snapshot of the fetch counters.
54
+ *
55
+ * @return {RangeFetchStats} The current stats.
56
+ */
57
+ get stats(): RangeFetchStats;
58
+ /**
59
+ * Read a range, accounting for the (block-aligned) fetch it would incur.
60
+ * Returns exactly the requested `length` bytes as a standalone array, even
61
+ * though the underlying fetch may have pulled a wider aligned span.
62
+ *
63
+ * @param offset Absolute offset of the first byte to read.
64
+ * @param length Number of bytes to read.
65
+ * @return {Promise< Uint8Array >} The requested bytes (byteOffset 0).
66
+ */
67
+ read(offset: number, length: number): Promise<Uint8Array>;
68
+ }
69
+ //# sourceMappingURL=range_byte_source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"range_byte_source.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/range_byte_source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAG/D;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAE9B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAA;IAEpB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAA;IAEpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAA;CACpB;AAQD;;;;;;;;;;;;;GAaG;AACH,qBAAa,eAAgB,YAAW,qBAAqB;IAkBvD,OAAO,CAAC,QAAQ,CAAC,MAAM;IAhB3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IAEpC,OAAO,CAAC,aAAa,CAAI;IAEzB,OAAO,CAAC,aAAa,CAAI;IAEzB,OAAO,CAAC,YAAY,CAAI;IAExB;;;;;;OAMG;gBAEkB,MAAM,EAAE,UAAU,EACnC,UAAU,GAAE,MAAU;IAS1B;;;;OAIG;IACH,IAAW,UAAU,IAAI,MAAM,CAE9B;IAED;;;;OAIG;IACH,IAAW,KAAK,IAAI,eAAe,CAMlC;IAED;;;;;;;;OAQG;IACI,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAI,OAAO,CAAE,UAAU,CAAE;CAkBrE"}
@@ -0,0 +1,78 @@
1
+ // Default block granularity when none is given: reads are unaligned, so
2
+ // `bytesFetched` equals `bytesServed` (each read fetches exactly its range).
3
+ const NO_ALIGNMENT = 1;
4
+ /**
5
+ * An asynchronous {@link StepExternalByteStore} that models a **range-fetched**
6
+ * source — an HTTP server honouring `Range:` requests, or an OPFS/blob store
7
+ * read block-by-block — so the index-first open path (M4) can be exercised and
8
+ * measured without a network. It wraps a fully-resident buffer (the "server")
9
+ * but never assumes the client holds it: every `read` is counted as a fetch,
10
+ * and with a `blockBytes` grid the fetch is rounded out to whole blocks, so
11
+ * {@link stats} reports the real transfer an aligned store would incur.
12
+ *
13
+ * This is the source a sidecar-driven open pairs with: reconstruct the entity
14
+ * index from the sidecar (no scan), then pull only the byte ranges demand asks
15
+ * for through a source like this — and read back from `stats` how little of the
16
+ * file that touched.
17
+ */
18
+ export class RangeByteSource {
19
+ /**
20
+ * @param bytes_ The full backing bytes (the notional server-side file). Not
21
+ * copied; treated as read-only.
22
+ * @param blockBytes Optional fetch granularity: reads are rounded out to a
23
+ * multiple of this so `bytesFetched` reflects whole-block transfer. Omit (or
24
+ * pass 0) for exact, unaligned fetches.
25
+ */
26
+ constructor(bytes_, blockBytes = 0) {
27
+ this.bytes_ = bytes_;
28
+ this.requestCount_ = 0;
29
+ this.bytesFetched_ = 0;
30
+ this.bytesServed_ = 0;
31
+ if (blockBytes < 0) {
32
+ throw new Error(`Invalid blockBytes ${blockBytes}`);
33
+ }
34
+ this.blockBytes_ = blockBytes > 0 ? blockBytes : NO_ALIGNMENT;
35
+ }
36
+ /**
37
+ * Total size of the stored byte sequence.
38
+ *
39
+ * @return {number} The byte length.
40
+ */
41
+ get byteLength() {
42
+ return this.bytes_.byteLength;
43
+ }
44
+ /**
45
+ * A snapshot of the fetch counters.
46
+ *
47
+ * @return {RangeFetchStats} The current stats.
48
+ */
49
+ get stats() {
50
+ return {
51
+ requestCount: this.requestCount_,
52
+ bytesFetched: this.bytesFetched_,
53
+ bytesServed: this.bytesServed_,
54
+ };
55
+ }
56
+ /**
57
+ * Read a range, accounting for the (block-aligned) fetch it would incur.
58
+ * Returns exactly the requested `length` bytes as a standalone array, even
59
+ * though the underlying fetch may have pulled a wider aligned span.
60
+ *
61
+ * @param offset Absolute offset of the first byte to read.
62
+ * @param length Number of bytes to read.
63
+ * @return {Promise< Uint8Array >} The requested bytes (byteOffset 0).
64
+ */
65
+ read(offset, length) {
66
+ const total = this.bytes_.byteLength;
67
+ const start = Math.max(0, Math.min(offset, total));
68
+ const end = Math.max(start, Math.min(offset + length, total));
69
+ // The aligned span a block-granular store would actually transfer.
70
+ const block = this.blockBytes_;
71
+ const fetchStart = start - (start % block);
72
+ const fetchEnd = Math.min(total, Math.ceil(end / block) * block);
73
+ this.requestCount_ += 1;
74
+ this.bytesFetched_ += fetchEnd - fetchStart;
75
+ this.bytesServed_ += end - start;
76
+ return Promise.resolve(this.bytes_.slice(start, end));
77
+ }
78
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=range_byte_source.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"range_byte_source.test.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/range_byte_source.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,70 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // M4: the range-fetched byte source models an HTTP-Range / block store — it
3
+ // returns exactly the requested bytes while accounting for the (possibly
4
+ // wider, block-aligned) fetch it would really incur. Index-first open reads
5
+ // back from those stats to prove it touched only a fraction of the file.
6
+ import { describe, expect, test } from "@jest/globals";
7
+ import { RangeByteSource } from "./range_byte_source.js";
8
+ /**
9
+ * A deterministic ramp buffer: byte i === i mod 256.
10
+ *
11
+ * @param n Length.
12
+ * @return {Uint8Array} The buffer.
13
+ */
14
+ function ramp(n) {
15
+ const bytes = new Uint8Array(n);
16
+ for (let where = 0; where < n; ++where) {
17
+ bytes[where] = where & 255;
18
+ }
19
+ return bytes;
20
+ }
21
+ describe("RangeByteSource", () => {
22
+ test("returns exactly the requested range", async () => {
23
+ const source = new RangeByteSource(ramp(1000));
24
+ const got = await source.read(100, 16);
25
+ expect(got.byteLength).toBe(16);
26
+ expect(Array.from(got)).toEqual(Array.from({ length: 16 }, (_v, i) => (100 + i) & 255));
27
+ });
28
+ test("unaligned reads fetch exactly what is served", async () => {
29
+ const source = new RangeByteSource(ramp(1000));
30
+ await source.read(10, 20);
31
+ await source.read(500, 4);
32
+ const stats = source.stats;
33
+ expect(stats.requestCount).toBe(2);
34
+ expect(stats.bytesServed).toBe(24);
35
+ expect(stats.bytesFetched).toBe(24);
36
+ });
37
+ test("block alignment over-reads to whole blocks", async () => {
38
+ const source = new RangeByteSource(ramp(10000), 4096);
39
+ // A 10-byte read at offset 100 sits inside the first 4096-byte block.
40
+ await source.read(100, 10);
41
+ expect(source.stats.bytesFetched).toBe(4096);
42
+ expect(source.stats.bytesServed).toBe(10);
43
+ // A read straddling the first/second block boundary pulls two blocks.
44
+ await source.read(4090, 20);
45
+ expect(source.stats.bytesFetched).toBe(4096 + 8192);
46
+ expect(source.stats.bytesServed).toBe(30);
47
+ });
48
+ test("clamps reads at end of source", async () => {
49
+ const source = new RangeByteSource(ramp(100));
50
+ const got = await source.read(90, 50);
51
+ expect(got.byteLength).toBe(10);
52
+ expect(source.stats.bytesServed).toBe(10);
53
+ });
54
+ test("a read fully past the end is empty and fetches nothing", async () => {
55
+ const source = new RangeByteSource(ramp(100));
56
+ const got = await source.read(200, 10);
57
+ expect(got.byteLength).toBe(0);
58
+ expect(source.stats.bytesFetched).toBe(0);
59
+ });
60
+ test("block-aligned fetch never exceeds the source length", async () => {
61
+ const source = new RangeByteSource(ramp(5000), 4096);
62
+ // Last block is partial (5000 = 4096 + 904); fetching into it must clamp.
63
+ await source.read(4990, 10);
64
+ expect(source.stats.bytesFetched).toBe(5000 - 4096);
65
+ expect(source.stats.bytesFetched).toBeLessThanOrEqual(5000);
66
+ });
67
+ test("reports byteLength of the backing source", () => {
68
+ expect(new RangeByteSource(ramp(777)).byteLength).toBe(777);
69
+ });
70
+ });
@@ -5,5 +5,5 @@
5
5
  // only the first segment (major) is meaningful and is the one CI carries forward.
6
6
  // Must stay in `vN.N.N` shape: the CI stamp regex, scripts/updateVersion.mjs, and
7
7
  // statistics.ts all match `v\d+\.\d+\.\d+`.
8
- const versionString = 'Conway v1.401.1237';
8
+ const versionString = 'Conway v1.404.1241';
9
9
  export { versionString };