@bldrs-ai/conway 1.418.1264 → 1.419.1268

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 (34) hide show
  1. package/compiled/examples/browser-bundled.cjs +34 -1
  2. package/compiled/examples/cli-bundled.cjs +34 -1
  3. package/compiled/examples/cli-step-bundled.cjs +34 -1
  4. package/compiled/examples/validator-bundled.cjs +34 -1
  5. package/compiled/src/compat/web-ifc/ifc_api_proxy_ifc.d.ts +7 -6
  6. package/compiled/src/compat/web-ifc/ifc_api_proxy_ifc.d.ts.map +1 -1
  7. package/compiled/src/compat/web-ifc/ifc_api_proxy_ifc.js +11 -8
  8. package/compiled/src/demand/index.d.ts +23 -0
  9. package/compiled/src/demand/index.d.ts.map +1 -0
  10. package/compiled/src/demand/index.js +22 -0
  11. package/compiled/src/index.d.ts +1 -0
  12. package/compiled/src/index.d.ts.map +1 -1
  13. package/compiled/src/index.js +6 -0
  14. package/compiled/src/mem/index.d.ts +17 -0
  15. package/compiled/src/mem/index.d.ts.map +1 -0
  16. package/compiled/src/mem/index.js +16 -0
  17. package/compiled/src/namespace_surface.test.d.ts +2 -0
  18. package/compiled/src/namespace_surface.test.d.ts.map +1 -0
  19. package/compiled/src/namespace_surface.test.js +35 -0
  20. package/compiled/src/step/parsing/step_parser.d.ts +19 -0
  21. package/compiled/src/step/parsing/step_parser.d.ts.map +1 -1
  22. package/compiled/src/step/parsing/step_parser.js +33 -0
  23. package/compiled/src/step/parsing/streaming_index_builder.d.ts +36 -0
  24. package/compiled/src/step/parsing/streaming_index_builder.d.ts.map +1 -1
  25. package/compiled/src/step/parsing/streaming_index_builder.js +109 -0
  26. package/compiled/src/step/parsing/streaming_index_builder_async.test.d.ts +2 -0
  27. package/compiled/src/step/parsing/streaming_index_builder_async.test.d.ts.map +1 -0
  28. package/compiled/src/step/parsing/streaming_index_builder_async.test.js +97 -0
  29. package/compiled/src/stream/index.d.ts +26 -0
  30. package/compiled/src/stream/index.d.ts.map +1 -0
  31. package/compiled/src/stream/index.js +24 -0
  32. package/compiled/src/version/version.js +1 -1
  33. package/compiled/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +16 -1
@@ -125,6 +125,95 @@ export function buildIndexStreaming(source, parser, pool, onRecordIndexed, sink)
125
125
  // many slides on a small fixture; the pool sweep uses ≥ 128 KB anyway.
126
126
  // eslint-disable-next-line no-magic-numbers
127
127
  const MIN_WINDOW = 4 * 1024;
128
+ /**
129
+ * Cooperative twin of {@link buildIndexStreaming}: identical parse, window
130
+ * and grow-and-restart behaviour (mirrored the same way the parser mirrors
131
+ * parseDataBlock/parseDataBlockAsync), but the parse periodically yields to
132
+ * the event loop so browsers repaint progress UI mid-parse instead of
133
+ * flagging the tab as stalled (issue #301 §2 for the streamed path).
134
+ *
135
+ * @param source The byte source.
136
+ * @param parser The STEP parser (typed to the schema).
137
+ * @param pool Target window size in bytes.
138
+ * @param onRecordIndexed Optional per-record event (see buildIndexStreaming).
139
+ * @param sink Optional index sink (columnar builds).
140
+ * @param onProgress Optional progress callback with the ABSOLUTE source byte
141
+ * cursor (unlike the parser's window-relative cursor), so callers can report
142
+ * `cursor / source.byteLength` directly.
143
+ * @param yieldIntervalMs Minimum ms between event-loop yields.
144
+ * @return {Promise<StreamingIndexResult>} The index, header, result and
145
+ * diagnostics.
146
+ */
147
+ export async function buildIndexStreamingAsync(source, parser, pool, onRecordIndexed, sink, onProgress, yieldIntervalMs) {
148
+ const fileSize = source.byteLength;
149
+ let windowBytes = Math.max(pool, MIN_WINDOW);
150
+ for (;;) {
151
+ const window = new Uint8Array(windowBytes);
152
+ let windowStartFile = 0;
153
+ let windowLen = source.read(0, windowBytes, window, 0);
154
+ let bytesRead = windowLen;
155
+ const input = new ParsingBuffer(window, 0, windowLen);
156
+ const [header, headerResult] = parser.parseHeader(input);
157
+ if (headerResult !== ParseResult.COMPLETE) {
158
+ return {
159
+ header,
160
+ elements: [],
161
+ result: headerResult,
162
+ stats: { pool, windowBytes, slides: 0, maxRecordLen: 0, bytesRead },
163
+ };
164
+ }
165
+ const slideThreshold = windowBytes >> 1;
166
+ let slides = 0;
167
+ let maxRecordLen = 0;
168
+ let prevBoundaryFile = windowStartFile + input.cursor;
169
+ const onRecordBoundary = (buffer) => {
170
+ const cursor = buffer.cursor;
171
+ const recordFileStart = windowStartFile + cursor;
172
+ const recordLen = recordFileStart - prevBoundaryFile;
173
+ if (recordLen > maxRecordLen) {
174
+ maxRecordLen = recordLen;
175
+ }
176
+ prevBoundaryFile = recordFileStart;
177
+ if (windowStartFile + windowLen >= fileSize) {
178
+ return;
179
+ }
180
+ if (cursor < slideThreshold) {
181
+ return;
182
+ }
183
+ const tail = windowLen - cursor;
184
+ window.copyWithin(0, cursor, windowLen);
185
+ const want = windowBytes - tail;
186
+ const got = source.read(windowStartFile + windowLen, want, window, tail);
187
+ bytesRead += got;
188
+ windowLen = tail + got;
189
+ windowStartFile = recordFileStart;
190
+ buffer.rebaseWindow(window, 0, windowLen, windowStartFile);
191
+ ++slides;
192
+ };
193
+ // Translate the parser's window-relative cursor to an absolute source
194
+ // cursor (windowStartFile advances as the window slides).
195
+ const progressTick = onProgress !== void 0 ?
196
+ (cursor) => onProgress(windowStartFile + cursor) : void 0;
197
+ const [index, result] = await parser.parseDataBlockStreamedAsync(input, onRecordBoundary, onRecordIndexed, progressTick, sink, yieldIntervalMs);
198
+ const stoppedShort = result !== ParseResult.COMPLETE;
199
+ const notAtEof = windowStartFile + windowLen < fileSize;
200
+ if (stoppedShort && notAtEof) {
201
+ windowBytes *= 2;
202
+ sink?.reset();
203
+ continue;
204
+ }
205
+ const lastRecordLen = (windowStartFile + input.cursor) - prevBoundaryFile;
206
+ if (lastRecordLen > maxRecordLen) {
207
+ maxRecordLen = lastRecordLen;
208
+ }
209
+ return {
210
+ header,
211
+ elements: index.elements,
212
+ result,
213
+ stats: { pool, windowBytes, slides, maxRecordLen, bytesRead },
214
+ };
215
+ }
216
+ }
128
217
  /**
129
218
  * Build the entity index by streaming, **directly into typed-array columns**
130
219
  * (M7): identical parse and window behaviour to {@link buildIndexStreaming},
@@ -144,3 +233,23 @@ export function buildColumnarIndexStreaming(source, parser, pool, onRecordIndexe
144
233
  const { header, result, stats } = buildIndexStreaming(source, parser, pool, onRecordIndexed, sink);
145
234
  return { header, columns: sink.finalize(), result, stats };
146
235
  }
236
+ /**
237
+ * Cooperative twin of {@link buildColumnarIndexStreaming}: identical
238
+ * columnar build, but the parse periodically yields to the event loop (see
239
+ * {@link buildIndexStreamingAsync}) and reports absolute byte-cursor
240
+ * progress — the browser-facing variant for large models.
241
+ *
242
+ * @param source The byte source.
243
+ * @param parser The STEP parser (typed to the schema).
244
+ * @param pool Target window size in bytes.
245
+ * @param onRecordIndexed Optional per-record event (see buildIndexStreaming).
246
+ * @param onProgress Optional absolute byte-cursor progress callback.
247
+ * @param yieldIntervalMs Minimum ms between event-loop yields.
248
+ * @return {Promise<StreamingColumnarIndexResult>} Columns, header, result,
249
+ * stats.
250
+ */
251
+ export async function buildColumnarIndexStreamingAsync(source, parser, pool, onRecordIndexed, onProgress, yieldIntervalMs) {
252
+ const sink = new ColumnarIndexSink();
253
+ const { header, result, stats } = await buildIndexStreamingAsync(source, parser, pool, onRecordIndexed, sink, onProgress, yieldIntervalMs);
254
+ return { header, columns: sink.finalize(), result, stats };
255
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=streaming_index_builder_async.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming_index_builder_async.test.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/streaming_index_builder_async.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,97 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // The cooperative streamed columnar build must be byte-identical to the
3
+ // synchronous build (same columns, same window mechanics) AND actually
4
+ // yield to the event loop mid-parse — the repaint/no-stall property
5
+ // (#301 §2) the streamed open path relies on. The parse generator only
6
+ // suspends every PARSE_PROGRESS_ELEMENT_MASK+1 (4096) records, so the
7
+ // yield/progress assertions use a synthesized >4096-record model; the
8
+ // column-parity assertions use the real fixture.
9
+ import fs from "fs";
10
+ import { describe, expect, test } from "@jest/globals";
11
+ import IfcStepParser from "../../ifc/ifc_step_parser.js";
12
+ import { BufferByteSource } from "./byte_source.js";
13
+ import { buildColumnarIndexStreaming, buildColumnarIndexStreamingAsync, } from "./streaming_index_builder.js";
14
+ import { ParseResult } from "./step_parser.js";
15
+ const POOL = 8 * 1024; // small window → forces slides on both inputs
16
+ /**
17
+ * Synthesize a valid IFC4 file with `recordCount` simple records — enough
18
+ * to cross the parser's progress-suspension mask several times.
19
+ *
20
+ * @param recordCount Number of data records.
21
+ * @return {Uint8Array} The encoded file.
22
+ */
23
+ function syntheticIfc(recordCount) {
24
+ const parts = [
25
+ "ISO-10303-21;\nHEADER;\n" +
26
+ "FILE_DESCRIPTION((''),'2;1');\n" +
27
+ "FILE_NAME('','',(''),(''),'','','');\n" +
28
+ "FILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n",
29
+ ];
30
+ for (let where = 1; where <= recordCount; ++where) {
31
+ parts.push(`#${where}=IFCCARTESIANPOINT((${where}.,0.,0.));\n`);
32
+ }
33
+ parts.push("ENDSEC;\nEND-ISO-10303-21;\n");
34
+ return new TextEncoder().encode(parts.join(""));
35
+ }
36
+ describe("buildColumnarIndexStreamingAsync", () => {
37
+ test("produces identical columns to the sync build (real fixture)", async () => {
38
+ const bytes = new Uint8Array(fs.readFileSync("data/index.ifc"));
39
+ const parser = IfcStepParser.Instance;
40
+ const sync = buildColumnarIndexStreaming(new BufferByteSource(bytes), parser, POOL);
41
+ const cooperative = await buildColumnarIndexStreamingAsync(new BufferByteSource(bytes), parser, POOL, void 0, void 0, 0);
42
+ expect(sync.result).toBe(ParseResult.COMPLETE);
43
+ expect(cooperative.result).toBe(ParseResult.COMPLETE);
44
+ const a = sync.columns;
45
+ const b = cooperative.columns;
46
+ expect(b.count).toBe(a.count);
47
+ expect(b.firstInlineElement).toBe(a.firstInlineElement);
48
+ expect(Array.from(b.address)).toEqual(Array.from(a.address));
49
+ expect(Array.from(b.length)).toEqual(Array.from(a.length));
50
+ expect(Array.from(b.typeID)).toEqual(Array.from(a.typeID));
51
+ expect(Array.from(b.expressID)).toEqual(Array.from(a.expressID));
52
+ // Same window mechanics (slides prove the moving window really moved).
53
+ expect(cooperative.stats.slides).toBe(sync.stats.slides);
54
+ expect(cooperative.stats.slides).toBeGreaterThan(0);
55
+ }, 60000);
56
+ test("yields to the event loop and reports absolute progress mid-parse", async () => {
57
+ const bytes = syntheticIfc(10000);
58
+ const parser = IfcStepParser.Instance;
59
+ // Count macrotask turns that run while the build is in flight — with a
60
+ // zero yield interval the parse must give the event loop real turns.
61
+ let turns = 0;
62
+ let pumping = true;
63
+ const pump = () => {
64
+ if (!pumping) {
65
+ return;
66
+ }
67
+ ++turns;
68
+ setTimeout(pump, 0);
69
+ };
70
+ setTimeout(pump, 0);
71
+ const progressCursors = [];
72
+ const cooperative = await buildColumnarIndexStreamingAsync(new BufferByteSource(bytes), parser, POOL, void 0, (cursor) => progressCursors.push(cursor), 0);
73
+ pumping = false;
74
+ expect(cooperative.result).toBe(ParseResult.COMPLETE);
75
+ expect(cooperative.columns.count).toBe(10000);
76
+ expect(turns).toBeGreaterThan(0);
77
+ // Progress reports absolute source cursors: monotonically
78
+ // non-decreasing, bounded by the file size, and past the first window —
79
+ // the window-relative → absolute translation must hold across slides.
80
+ expect(progressCursors.length).toBeGreaterThan(1);
81
+ for (let where = 1; where < progressCursors.length; ++where) {
82
+ expect(progressCursors[where])
83
+ .toBeGreaterThanOrEqual(progressCursors[where - 1]);
84
+ }
85
+ expect(progressCursors[progressCursors.length - 1])
86
+ .toBeLessThanOrEqual(bytes.byteLength);
87
+ expect(progressCursors[progressCursors.length - 1])
88
+ .toBeGreaterThan(POOL);
89
+ // And the synthetic build matches its own sync twin exactly.
90
+ const sync = buildColumnarIndexStreaming(new BufferByteSource(bytes), parser, POOL);
91
+ expect(Array.from(cooperative.columns.address))
92
+ .toEqual(Array.from(sync.columns.address));
93
+ expect(Array.from(cooperative.columns.expressID))
94
+ .toEqual(Array.from(sync.columns.expressID));
95
+ expect(cooperative.stats.slides).toBe(sync.stats.slides);
96
+ }, 60000);
97
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `@bldrs-ai/conway/stream` — the streaming parse/index plane (epic #390;
3
+ * design/new/streaming-federated-loader.md).
4
+ *
5
+ * Fixed-memory opens: a model parses through a bounded moving window over
6
+ * a {@link ByteSource} into a columnar record index ({@link StepIndexColumns}
7
+ * — no per-record object phase), reads its source bytes on demand through a
8
+ * windowed provider over a {@link StepExternalByteStore} (e.g. OPFS), and
9
+ * serializes its index to a revisit sidecar whose payload IS the columns.
10
+ * Incremental consumers (type index, cross-refs) subscribe to per-record
11
+ * events via {@link StreamingRecordDispatcher} while the parse runs.
12
+ *
13
+ * This is the conway-native namespace for new-era consumers. The web-ifc
14
+ * compat shim (`@bldrs-ai/conway/web-ifc`) adapts parts of this surface to
15
+ * web-ifc's API (e.g. `OpenModelStreamed`) and is headed for retirement;
16
+ * new integrations should build against this module directly.
17
+ */
18
+ export { openStreamedIfcModel, StreamedIfcOpen, StreamedIfcOpenOptions, } from "../ifc/ifc_stream_open.js";
19
+ export { buildColumnarIndexStreaming, buildColumnarIndexStreamingAsync, StreamingColumnarIndexResult, StreamingIndexStats, } from "../step/parsing/streaming_index_builder.js";
20
+ export { ByteSource, BufferByteSource } from "../step/parsing/byte_source.js";
21
+ export { StepExternalByteStore, InMemoryStepByteStore, StepBufferProvider, WindowedStepBufferProvider, } from "../step/step_buffer_provider.js";
22
+ export { StepIndexColumns } from "../step/parsing/columnar_index.js";
23
+ export { serializeIndexSidecarFromColumns, deserializeIndexSidecarToColumns, sidecarMatchesSource, hashSource, } from "../step/parsing/index_sidecar.js";
24
+ export { StreamingRecordDispatcher, RecordHandler, } from "../step/parsing/streaming_record_dispatcher.js";
25
+ export { IncrementalTypeIndex } from "../step/parsing/incremental_type_index.js";
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/stream/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,sBAAsB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,2BAA2B,EAC3B,gCAAgC,EAChC,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,yCAAyC,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AACjE,OAAO,EACL,gCAAgC,EAChC,gCAAgC,EAChC,oBAAoB,EACpB,UAAU,GACX,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACL,yBAAyB,EACzB,aAAa,GACd,MAAM,6CAA6C,CAAA;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAA"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@bldrs-ai/conway/stream` — the streaming parse/index plane (epic #390;
3
+ * design/new/streaming-federated-loader.md).
4
+ *
5
+ * Fixed-memory opens: a model parses through a bounded moving window over
6
+ * a {@link ByteSource} into a columnar record index ({@link StepIndexColumns}
7
+ * — no per-record object phase), reads its source bytes on demand through a
8
+ * windowed provider over a {@link StepExternalByteStore} (e.g. OPFS), and
9
+ * serializes its index to a revisit sidecar whose payload IS the columns.
10
+ * Incremental consumers (type index, cross-refs) subscribe to per-record
11
+ * events via {@link StreamingRecordDispatcher} while the parse runs.
12
+ *
13
+ * This is the conway-native namespace for new-era consumers. The web-ifc
14
+ * compat shim (`@bldrs-ai/conway/web-ifc`) adapts parts of this surface to
15
+ * web-ifc's API (e.g. `OpenModelStreamed`) and is headed for retirement;
16
+ * new integrations should build against this module directly.
17
+ */
18
+ export { openStreamedIfcModel, } from "../ifc/ifc_stream_open.js";
19
+ export { buildColumnarIndexStreaming, buildColumnarIndexStreamingAsync, } from "../step/parsing/streaming_index_builder.js";
20
+ export { BufferByteSource } from "../step/parsing/byte_source.js";
21
+ export { InMemoryStepByteStore, WindowedStepBufferProvider, } from "../step/step_buffer_provider.js";
22
+ export { serializeIndexSidecarFromColumns, deserializeIndexSidecarToColumns, sidecarMatchesSource, hashSource, } from "../step/parsing/index_sidecar.js";
23
+ export { StreamingRecordDispatcher, } from "../step/parsing/streaming_record_dispatcher.js";
24
+ export { IncrementalTypeIndex } from "../step/parsing/incremental_type_index.js";
@@ -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.418.1264';
8
+ const versionString = 'Conway v1.419.1268';
9
9
  export { versionString };