@bldrs-ai/conway 1.389.1221 → 1.398.1224
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/compiled/examples/browser-bundled.cjs +60 -5
- package/compiled/examples/cli-bundled.cjs +60 -5
- package/compiled/examples/cli-step-bundled.cjs +60 -5
- package/compiled/examples/validator-bundled.cjs +60 -5
- package/compiled/src/parsing/parsing_buffer.d.ts +23 -0
- package/compiled/src/parsing/parsing_buffer.d.ts.map +1 -1
- package/compiled/src/parsing/parsing_buffer.js +31 -0
- package/compiled/src/step/parsing/byte_source.d.ts +55 -0
- package/compiled/src/step/parsing/byte_source.d.ts.map +1 -0
- package/compiled/src/step/parsing/byte_source.js +35 -0
- package/compiled/src/step/parsing/step_parser.d.ts +14 -0
- package/compiled/src/step/parsing/step_parser.d.ts.map +1 -1
- package/compiled/src/step/parsing/step_parser.js +41 -4
- package/compiled/src/step/parsing/streaming_index_builder.d.ts +60 -0
- package/compiled/src/step/parsing/streaming_index_builder.d.ts.map +1 -0
- package/compiled/src/step/parsing/streaming_index_builder.js +115 -0
- package/compiled/src/step/parsing/streaming_index_builder.test.d.ts +2 -0
- package/compiled/src/step/parsing/streaming_index_builder.test.d.ts.map +1 -0
- package/compiled/src/step/parsing/streaming_index_builder.test.js +107 -0
- package/compiled/src/version/version.js +1 -1
- package/compiled/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -116,6 +116,20 @@ export default class StepParser<TypeIDType> extends StepHeaderParser {
|
|
|
116
116
|
* @return {BlockParseResult} The parsing result, including the index and result enum.
|
|
117
117
|
*/
|
|
118
118
|
parseDataBlock(input: ParsingBuffer, onProgress?: ParseProgressCallback): BlockParseResult<TypeIDType>;
|
|
119
|
+
/**
|
|
120
|
+
* Streaming driver over parseDataBlockIncremental: identical parse (same
|
|
121
|
+
* generator body) but invokes `onRecordBoundary` at every top-level record
|
|
122
|
+
* boundary so a caller feeding the parser from a moving window can slide
|
|
123
|
+
* that window forward while the rewind stack is empty. See
|
|
124
|
+
* streaming_index_builder.ts for the coordinator that owns the window.
|
|
125
|
+
*
|
|
126
|
+
* @param input The input parsing buffer, positioned at the data section.
|
|
127
|
+
* @param onRecordBoundary Called at each top-level record boundary with the
|
|
128
|
+
* buffer; the callback may rebase the buffer's window in place.
|
|
129
|
+
* @param onProgress Optional byte-cursor progress callback.
|
|
130
|
+
* @return {BlockParseResult} The parsing result, including the index and result enum.
|
|
131
|
+
*/
|
|
132
|
+
parseDataBlockStreamed(input: ParsingBuffer, onRecordBoundary: (input: ParsingBuffer) => void, onProgress?: ParseProgressCallback): BlockParseResult<TypeIDType>;
|
|
119
133
|
/**
|
|
120
134
|
* Cooperative variant of parseDataBlock: identical parse (same generator
|
|
121
135
|
* body), but periodically awaits a macrotask so the event loop can run —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step_parser.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/step_parser.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"step_parser.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/step_parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -310,6 +310,29 @@ export default class StepParser extends StepHeaderParser {
|
|
|
310
310
|
onProgress?.(next.value);
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Streaming driver over parseDataBlockIncremental: identical parse (same
|
|
315
|
+
* generator body) but invokes `onRecordBoundary` at every top-level record
|
|
316
|
+
* boundary so a caller feeding the parser from a moving window can slide
|
|
317
|
+
* that window forward while the rewind stack is empty. See
|
|
318
|
+
* streaming_index_builder.ts for the coordinator that owns the window.
|
|
319
|
+
*
|
|
320
|
+
* @param input The input parsing buffer, positioned at the data section.
|
|
321
|
+
* @param onRecordBoundary Called at each top-level record boundary with the
|
|
322
|
+
* buffer; the callback may rebase the buffer's window in place.
|
|
323
|
+
* @param onProgress Optional byte-cursor progress callback.
|
|
324
|
+
* @return {BlockParseResult} The parsing result, including the index and result enum.
|
|
325
|
+
*/
|
|
326
|
+
parseDataBlockStreamed(input, onRecordBoundary, onProgress) {
|
|
327
|
+
const parser = this.parseDataBlockIncremental(input, onRecordBoundary);
|
|
328
|
+
while (true) {
|
|
329
|
+
const next = parser.next();
|
|
330
|
+
if (next.done === true) {
|
|
331
|
+
return next.value;
|
|
332
|
+
}
|
|
333
|
+
onProgress?.(next.value);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
313
336
|
/**
|
|
314
337
|
* Cooperative variant of parseDataBlock: identical parse (same generator
|
|
315
338
|
* body), but periodically awaits a macrotask so the event loop can run —
|
|
@@ -347,7 +370,7 @@ export default class StepParser extends StepHeaderParser {
|
|
|
347
370
|
* @yields {number} The current byte cursor within the input buffer.
|
|
348
371
|
* @return {BlockParseResult} The parsing result, including the index and result enum.
|
|
349
372
|
*/
|
|
350
|
-
*parseDataBlockIncremental(input) {
|
|
373
|
+
*parseDataBlockIncremental(input, onRecordBoundary) {
|
|
351
374
|
const indexResult = { elements: [] };
|
|
352
375
|
const match = input.match;
|
|
353
376
|
const comment = () => match(commentParser);
|
|
@@ -394,7 +417,10 @@ export default class StepParser extends StepHeaderParser {
|
|
|
394
417
|
return syntaxError();
|
|
395
418
|
}
|
|
396
419
|
whitespace();
|
|
397
|
-
|
|
420
|
+
// File-absolute (== input.cursor when initialOffset is 0, i.e. every
|
|
421
|
+
// resident parse; file-absolute under a streaming moving window so the
|
|
422
|
+
// recorded address/length stay stable as the window slides).
|
|
423
|
+
const startElement = input.address;
|
|
398
424
|
let stackDepth = 1;
|
|
399
425
|
const savedInlineElements = inlineElements;
|
|
400
426
|
inlineElements = void 0;
|
|
@@ -492,6 +518,11 @@ export default class StepParser extends StepHeaderParser {
|
|
|
492
518
|
if ((++parsedElementCount & PARSE_PROGRESS_ELEMENT_MASK) === 0) {
|
|
493
519
|
yield input.cursor;
|
|
494
520
|
}
|
|
521
|
+
// Top-level record boundary: the rewind stack is empty and the cursor
|
|
522
|
+
// sits at the next record's leading whitespace/`#`. A streaming driver
|
|
523
|
+
// uses this to slide its moving window forward (see
|
|
524
|
+
// streaming_index_builder.ts). No-op on the resident path.
|
|
525
|
+
onRecordBoundary?.(input);
|
|
495
526
|
if (!charws(HASH)) {
|
|
496
527
|
if (tokenws(END_SECTION)) {
|
|
497
528
|
return parseResult(ParseResult.COMPLETE);
|
|
@@ -516,7 +547,10 @@ export default class StepParser extends StepHeaderParser {
|
|
|
516
547
|
}
|
|
517
548
|
whitespace();
|
|
518
549
|
inlineElements = void 0;
|
|
519
|
-
|
|
550
|
+
// File-absolute (== input.cursor when initialOffset is 0, i.e. every
|
|
551
|
+
// resident parse; file-absolute under a streaming moving window so the
|
|
552
|
+
// recorded address/length stay stable as the window slides).
|
|
553
|
+
const startElement = input.address;
|
|
520
554
|
// todo, loop and read inline then add them to a special "External Mapping" node.
|
|
521
555
|
while (!charws(CLOSE_PAREN)) {
|
|
522
556
|
input.begin();
|
|
@@ -547,7 +581,10 @@ export default class StepParser extends StepHeaderParser {
|
|
|
547
581
|
return syntaxError();
|
|
548
582
|
}
|
|
549
583
|
whitespace();
|
|
550
|
-
|
|
584
|
+
// File-absolute (== input.cursor when initialOffset is 0, i.e. every
|
|
585
|
+
// resident parse; file-absolute under a streaming moving window so the
|
|
586
|
+
// recorded address/length stay stable as the window slides).
|
|
587
|
+
const startElement = input.address;
|
|
551
588
|
let stackDepth = 1;
|
|
552
589
|
inlineElements = void 0;
|
|
553
590
|
while (stackDepth > 0) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ByteSource } from "./byte_source.js";
|
|
2
|
+
import StepParser, { ParseResult, StepHeader, StepIndexEntry } from "./step_parser.js";
|
|
3
|
+
/**
|
|
4
|
+
* Diagnostics from a streaming index build — how hard the moving window had
|
|
5
|
+
* to work, so a pool-size sweep can compare cost against a resident parse.
|
|
6
|
+
*/
|
|
7
|
+
export interface StreamingIndexStats {
|
|
8
|
+
/** Target window size in bytes (the pool). */
|
|
9
|
+
pool: number;
|
|
10
|
+
/** Physical window buffer size actually used (grows past `pool` only if a
|
|
11
|
+
* single record exceeds the pool). */
|
|
12
|
+
windowBytes: number;
|
|
13
|
+
/** Number of times the window slid forward. */
|
|
14
|
+
slides: number;
|
|
15
|
+
/** Largest single top-level record observed, in bytes. */
|
|
16
|
+
maxRecordLen: number;
|
|
17
|
+
/** Total bytes read from the source (≈ file size + re-reads on growth). */
|
|
18
|
+
bytesRead: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The output of a streaming index build: the same element index a resident
|
|
22
|
+
* `parseDataBlock` produces, plus the header and window diagnostics.
|
|
23
|
+
*/
|
|
24
|
+
export interface StreamingIndexResult<TypeIDType> {
|
|
25
|
+
header: StepHeader;
|
|
26
|
+
elements: StepIndexEntry<TypeIDType>[];
|
|
27
|
+
result: ParseResult;
|
|
28
|
+
stats: StreamingIndexStats;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the entity index by streaming a `ByteSource` through a fixed-size
|
|
32
|
+
* moving window, instead of parsing one resident buffer. The parse loop is
|
|
33
|
+
* byte-for-byte the resident one (`StepParser.parseDataBlockStreamed` drives
|
|
34
|
+
* the same generator); this coordinator only owns the window: it fills it,
|
|
35
|
+
* parses the header from the first fill, then slides the window forward at
|
|
36
|
+
* top-level record boundaries as the parser advances.
|
|
37
|
+
*
|
|
38
|
+
* Because the parser records file-absolute addresses and its rewind stack is
|
|
39
|
+
* empty at every top-level boundary, sliding there is transparent: the
|
|
40
|
+
* unconsumed tail is copied to the front, fresh bytes are appended, and the
|
|
41
|
+
* buffer is rebased so `address` keeps its file-absolute value. Peak JS heap
|
|
42
|
+
* for the index build is therefore `window + index columns`, independent of
|
|
43
|
+
* file size.
|
|
44
|
+
*
|
|
45
|
+
* The window slides only once the cursor passes `pool / 2`, bounding the
|
|
46
|
+
* memmove frequency; a record up to `pool / 2` bytes always fits after a
|
|
47
|
+
* slide. If a single record exceeds that (never on the current corpus, whose
|
|
48
|
+
* largest STEP record is ~25 KB), the whole parse restarts from the
|
|
49
|
+
* beginning with the window doubled, and repeats until every record fits —
|
|
50
|
+
* correctness over the pathological case, at the cost of a re-scan. (M1's
|
|
51
|
+
* production path will instead grow in place / restart from the last
|
|
52
|
+
* boundary; from-scratch keeps the spike simple.)
|
|
53
|
+
*
|
|
54
|
+
* @param source The byte source.
|
|
55
|
+
* @param parser The STEP parser (typed to the schema).
|
|
56
|
+
* @param pool Target window size in bytes.
|
|
57
|
+
* @return {StreamingIndexResult} The index, header, result and diagnostics.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildIndexStreaming<TypeIDType>(source: ByteSource, parser: StepParser<TypeIDType>, pool: number): StreamingIndexResult<TypeIDType>;
|
|
60
|
+
//# sourceMappingURL=streaming_index_builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming_index_builder.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/streaming_index_builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,UAAU,EAAE,EACjB,WAAW,EACX,UAAU,EACV,cAAc,EACf,MAAM,eAAe,CAAA;AAGtB;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAElC,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAA;IAEZ;0CACsC;IACtC,WAAW,EAAE,MAAM,CAAA;IAEnB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAA;IAEd,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAA;IAEpB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;CAClB;AAGD;;;GAGG;AACH,MAAM,WAAW,oBAAoB,CAAC,UAAU;IAC9C,MAAM,EAAE,UAAU,CAAA;IAClB,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EAAE,CAAA;IACtC,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,mBAAmB,CAAA;CAC3B;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAC1C,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC,EAC9B,IAAI,EAAE,MAAM,GAAI,oBAAoB,CAAC,UAAU,CAAC,CA2GnD"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import ParsingBuffer from "../../parsing/parsing_buffer.js";
|
|
2
|
+
import { ParseResult, } from "./step_parser.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the entity index by streaming a `ByteSource` through a fixed-size
|
|
5
|
+
* moving window, instead of parsing one resident buffer. The parse loop is
|
|
6
|
+
* byte-for-byte the resident one (`StepParser.parseDataBlockStreamed` drives
|
|
7
|
+
* the same generator); this coordinator only owns the window: it fills it,
|
|
8
|
+
* parses the header from the first fill, then slides the window forward at
|
|
9
|
+
* top-level record boundaries as the parser advances.
|
|
10
|
+
*
|
|
11
|
+
* Because the parser records file-absolute addresses and its rewind stack is
|
|
12
|
+
* empty at every top-level boundary, sliding there is transparent: the
|
|
13
|
+
* unconsumed tail is copied to the front, fresh bytes are appended, and the
|
|
14
|
+
* buffer is rebased so `address` keeps its file-absolute value. Peak JS heap
|
|
15
|
+
* for the index build is therefore `window + index columns`, independent of
|
|
16
|
+
* file size.
|
|
17
|
+
*
|
|
18
|
+
* The window slides only once the cursor passes `pool / 2`, bounding the
|
|
19
|
+
* memmove frequency; a record up to `pool / 2` bytes always fits after a
|
|
20
|
+
* slide. If a single record exceeds that (never on the current corpus, whose
|
|
21
|
+
* largest STEP record is ~25 KB), the whole parse restarts from the
|
|
22
|
+
* beginning with the window doubled, and repeats until every record fits —
|
|
23
|
+
* correctness over the pathological case, at the cost of a re-scan. (M1's
|
|
24
|
+
* production path will instead grow in place / restart from the last
|
|
25
|
+
* boundary; from-scratch keeps the spike simple.)
|
|
26
|
+
*
|
|
27
|
+
* @param source The byte source.
|
|
28
|
+
* @param parser The STEP parser (typed to the schema).
|
|
29
|
+
* @param pool Target window size in bytes.
|
|
30
|
+
* @return {StreamingIndexResult} The index, header, result and diagnostics.
|
|
31
|
+
*/
|
|
32
|
+
export function buildIndexStreaming(source, parser, pool) {
|
|
33
|
+
const fileSize = source.byteLength;
|
|
34
|
+
// Grow-and-restart loop: the body runs a full streamed parse at the current
|
|
35
|
+
// window size. It only re-runs if a single record couldn't fit the window
|
|
36
|
+
// (false end-of-file before true EOF), doubling the window each time.
|
|
37
|
+
let windowBytes = Math.max(pool, MIN_WINDOW);
|
|
38
|
+
for (;;) {
|
|
39
|
+
const window = new Uint8Array(windowBytes);
|
|
40
|
+
let windowStartFile = 0;
|
|
41
|
+
let windowLen = source.read(0, windowBytes, window, 0);
|
|
42
|
+
let bytesRead = windowLen;
|
|
43
|
+
const input = new ParsingBuffer(window, 0, windowLen);
|
|
44
|
+
const [header, headerResult] = parser.parseHeader(input);
|
|
45
|
+
if (headerResult !== ParseResult.COMPLETE) {
|
|
46
|
+
// Header didn't complete in the first window: either malformed, or a
|
|
47
|
+
// header larger than the pool. Report it as-is — the resident parse
|
|
48
|
+
// would surface the same header result.
|
|
49
|
+
return {
|
|
50
|
+
header,
|
|
51
|
+
elements: [],
|
|
52
|
+
result: headerResult,
|
|
53
|
+
stats: { pool, windowBytes, slides: 0, maxRecordLen: 0, bytesRead },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Slide once the cursor is past half the window; a record up to this much
|
|
57
|
+
// headroom always fits without a false EOF.
|
|
58
|
+
const slideThreshold = windowBytes >> 1;
|
|
59
|
+
let slides = 0;
|
|
60
|
+
let maxRecordLen = 0;
|
|
61
|
+
let prevBoundaryFile = windowStartFile + input.cursor;
|
|
62
|
+
const onRecordBoundary = (buffer) => {
|
|
63
|
+
const cursor = buffer.cursor;
|
|
64
|
+
const recordFileStart = windowStartFile + cursor;
|
|
65
|
+
const recordLen = recordFileStart - prevBoundaryFile;
|
|
66
|
+
if (recordLen > maxRecordLen) {
|
|
67
|
+
maxRecordLen = recordLen;
|
|
68
|
+
}
|
|
69
|
+
prevBoundaryFile = recordFileStart;
|
|
70
|
+
// Window already spans to EOF — nothing left to slide in.
|
|
71
|
+
if (windowStartFile + windowLen >= fileSize) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Enough headroom remains; defer the slide to bound memmove frequency.
|
|
75
|
+
if (cursor < slideThreshold) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Slide: move the unconsumed tail to the front, refill behind it.
|
|
79
|
+
const tail = windowLen - cursor;
|
|
80
|
+
window.copyWithin(0, cursor, windowLen);
|
|
81
|
+
const want = windowBytes - tail;
|
|
82
|
+
const got = source.read(windowStartFile + windowLen, want, window, tail);
|
|
83
|
+
bytesRead += got;
|
|
84
|
+
windowLen = tail + got;
|
|
85
|
+
windowStartFile = recordFileStart;
|
|
86
|
+
buffer.rebaseWindow(window, 0, windowLen, windowStartFile);
|
|
87
|
+
++slides;
|
|
88
|
+
};
|
|
89
|
+
const [index, result] = parser.parseDataBlockStreamed(input, onRecordBoundary);
|
|
90
|
+
// A parse that stopped short of true EOF, with the window not spanning to
|
|
91
|
+
// EOF, means a single record overflowed the window: grow and retry.
|
|
92
|
+
const stoppedShort = result !== ParseResult.COMPLETE;
|
|
93
|
+
const notAtEof = windowStartFile + windowLen < fileSize;
|
|
94
|
+
if (stoppedShort && notAtEof) {
|
|
95
|
+
windowBytes *= 2;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Capture the final record's length (no boundary fires after the last).
|
|
99
|
+
const lastRecordLen = (windowStartFile + input.cursor) - prevBoundaryFile;
|
|
100
|
+
if (lastRecordLen > maxRecordLen) {
|
|
101
|
+
maxRecordLen = lastRecordLen;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
header,
|
|
105
|
+
elements: index.elements,
|
|
106
|
+
result,
|
|
107
|
+
stats: { pool, windowBytes, slides, maxRecordLen, bytesRead },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Floor on the window so a pathologically tiny pool still holds the header
|
|
112
|
+
// and a record with slide headroom. Tests drive pools down to this to force
|
|
113
|
+
// many slides on a small fixture; the pool sweep uses ≥ 128 KB anyway.
|
|
114
|
+
// eslint-disable-next-line no-magic-numbers
|
|
115
|
+
const MIN_WINDOW = 4 * 1024;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming_index_builder.test.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/streaming_index_builder.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
// M0 spike: streaming index build must be byte-identical to the resident
|
|
3
|
+
// parse. The parser reads file-absolute addresses and only rewinds within a
|
|
4
|
+
// single top-level record, so feeding it from a moving window that slides at
|
|
5
|
+
// record boundaries must produce the exact same index columns — at every
|
|
6
|
+
// pool size, including one small enough to force many slides + straddles.
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import { beforeAll, describe, expect, test } from "@jest/globals";
|
|
9
|
+
import ParsingBuffer from "../../parsing/parsing_buffer.js";
|
|
10
|
+
import IfcStepParser from "../../ifc/ifc_step_parser.js";
|
|
11
|
+
import { BufferByteSource } from "./byte_source.js";
|
|
12
|
+
import { buildIndexStreaming } from "./streaming_index_builder.js";
|
|
13
|
+
import { ParseResult } from "./step_parser.js";
|
|
14
|
+
/**
|
|
15
|
+
* Flatten the index (top-level records + their inline / multi-mapping
|
|
16
|
+
* children) into a stable, comparable column dump: every field the streaming
|
|
17
|
+
* parse must reproduce identically.
|
|
18
|
+
*
|
|
19
|
+
* @param elements The index elements.
|
|
20
|
+
* @return {object[]} A flat list of column tuples in traversal order.
|
|
21
|
+
*/
|
|
22
|
+
function columns(elements) {
|
|
23
|
+
const out = [];
|
|
24
|
+
const visit = (e) => {
|
|
25
|
+
out.push({
|
|
26
|
+
address: e.address,
|
|
27
|
+
length: e.length,
|
|
28
|
+
typeID: e.typeID,
|
|
29
|
+
expressID: e.expressID ?? null,
|
|
30
|
+
});
|
|
31
|
+
for (const child of e.inlineEntities ?? []) {
|
|
32
|
+
visit(child);
|
|
33
|
+
}
|
|
34
|
+
for (const child of e.multiMapping ?? []) {
|
|
35
|
+
visit(child);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
for (const e of elements) {
|
|
39
|
+
visit(e);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
let bytes;
|
|
44
|
+
let residentColumns;
|
|
45
|
+
let residentCount;
|
|
46
|
+
beforeAll(() => {
|
|
47
|
+
bytes = new Uint8Array(fs.readFileSync("data/index.ifc"));
|
|
48
|
+
const input = new ParsingBuffer(bytes);
|
|
49
|
+
IfcStepParser.Instance.parseHeader(input);
|
|
50
|
+
const [index, result] = IfcStepParser.Instance.parseDataBlock(input);
|
|
51
|
+
expect(result).toBe(ParseResult.COMPLETE);
|
|
52
|
+
residentColumns = columns(index.elements);
|
|
53
|
+
residentCount = index.elements.length;
|
|
54
|
+
});
|
|
55
|
+
describe("buildIndexStreaming", () => {
|
|
56
|
+
// A tiny pool relative to the 18 KB fixture forces repeated slides and at
|
|
57
|
+
// least one record straddling a window boundary; a large one takes the
|
|
58
|
+
// whole file in a single window (no slide). Both must match the resident
|
|
59
|
+
// parse exactly.
|
|
60
|
+
const POOLS = [4 * 1024, 8 * 1024, 16 * 1024, 128 * 1024];
|
|
61
|
+
test.each(POOLS)("produces the resident index at pool=%d bytes", (pool) => {
|
|
62
|
+
const source = new BufferByteSource(bytes);
|
|
63
|
+
const result = buildIndexStreaming(source, IfcStepParser.Instance, pool);
|
|
64
|
+
expect(result.result).toBe(ParseResult.COMPLETE);
|
|
65
|
+
expect(result.elements.length).toBe(residentCount);
|
|
66
|
+
expect(columns(result.elements)).toEqual(residentColumns);
|
|
67
|
+
});
|
|
68
|
+
test("the small pool actually slides the window (straddle exercised)", () => {
|
|
69
|
+
const source = new BufferByteSource(bytes);
|
|
70
|
+
const result = buildIndexStreaming(source, IfcStepParser.Instance, 4 * 1024);
|
|
71
|
+
// 18 KB fixture through a 4 KB window must slide several times.
|
|
72
|
+
expect(result.stats.slides).toBeGreaterThan(1);
|
|
73
|
+
// Physical window never grew past the pool (no record exceeds it).
|
|
74
|
+
expect(result.stats.windowBytes).toBe(4 * 1024);
|
|
75
|
+
// Every byte read once (no growth re-reads) — bounded to file size.
|
|
76
|
+
expect(result.stats.bytesRead).toBe(bytes.length);
|
|
77
|
+
});
|
|
78
|
+
test("the large pool takes the whole file in one window (no slide)", () => {
|
|
79
|
+
const source = new BufferByteSource(bytes);
|
|
80
|
+
const result = buildIndexStreaming(source, IfcStepParser.Instance, 128 * 1024);
|
|
81
|
+
expect(result.stats.slides).toBe(0);
|
|
82
|
+
expect(result.stats.maxRecordLen).toBeGreaterThan(0);
|
|
83
|
+
});
|
|
84
|
+
test("grows the window and restarts when a single record exceeds the pool", () => {
|
|
85
|
+
// Synthetic file with one record far larger than a tiny pool — the
|
|
86
|
+
// corpus never hits this (largest STEP record ~25 KB), so exercise the
|
|
87
|
+
// grow-and-restart safety valve directly. A long text literal makes one
|
|
88
|
+
// record ~12 KB; a 4 KB pool must grow past it and still index it.
|
|
89
|
+
const bigValue = "X".repeat(12000);
|
|
90
|
+
const text = "ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n" +
|
|
91
|
+
"#1=IFCWALL('a');\n" +
|
|
92
|
+
`#2=IFCPROPERTYSINGLEVALUE('${bigValue}');\n` +
|
|
93
|
+
"#3=IFCWALL('c');\nENDSEC;\n";
|
|
94
|
+
const synthetic = new TextEncoder().encode(text);
|
|
95
|
+
const resident = new ParsingBuffer(synthetic);
|
|
96
|
+
IfcStepParser.Instance.parseHeader(resident);
|
|
97
|
+
const [residentIndex, residentResult] = IfcStepParser.Instance.parseDataBlock(resident);
|
|
98
|
+
expect(residentResult).toBe(ParseResult.COMPLETE);
|
|
99
|
+
const streamed = buildIndexStreaming(new BufferByteSource(synthetic), IfcStepParser.Instance, 4 * 1024);
|
|
100
|
+
expect(streamed.result).toBe(ParseResult.COMPLETE);
|
|
101
|
+
// Window grew past the 4 KB pool to fit the ~12 KB record.
|
|
102
|
+
expect(streamed.stats.windowBytes).toBeGreaterThan(4 * 1024);
|
|
103
|
+
expect(streamed.stats.maxRecordLen).toBeGreaterThan(12000);
|
|
104
|
+
// ...and the index is still byte-identical to the resident parse.
|
|
105
|
+
expect(columns(streamed.elements)).toEqual(columns(residentIndex.elements));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -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.
|
|
8
|
+
const versionString = 'Conway v1.398.1224';
|
|
9
9
|
export { versionString };
|