@bldrs-ai/conway 1.400.1226 → 1.403.1239

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 (41) hide show
  1. package/compiled/Dist/ConwayGeomWasmNode.js +0 -0
  2. package/compiled/Dist/ConwayGeomWasmNodeMT.js +0 -0
  3. package/compiled/Dist/ConwayGeomWasmWeb.js +0 -0
  4. package/compiled/Dist/ConwayGeomWasmWebMT.js +1 -1
  5. package/compiled/Dist/ConwayGeomWasmWebMT.wasm +0 -0
  6. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNode.js +0 -0
  7. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNodeMT.js +0 -0
  8. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWeb.js +0 -0
  9. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.js +1 -1
  10. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.wasm +0 -0
  11. package/compiled/examples/browser-bundled.cjs +13 -6
  12. package/compiled/examples/cli-bundled.cjs +42 -35
  13. package/compiled/examples/cli-step-bundled.cjs +40 -33
  14. package/compiled/examples/validator-bundled.cjs +13 -6
  15. package/compiled/src/core/demand_geometry_queue.d.ts +147 -0
  16. package/compiled/src/core/demand_geometry_queue.d.ts.map +1 -0
  17. package/compiled/src/core/demand_geometry_queue.js +223 -0
  18. package/compiled/src/core/demand_geometry_queue.test.d.ts +2 -0
  19. package/compiled/src/core/demand_geometry_queue.test.d.ts.map +1 -0
  20. package/compiled/src/core/demand_geometry_queue.test.js +133 -0
  21. package/compiled/src/step/parsing/incremental_type_index.d.ts +64 -0
  22. package/compiled/src/step/parsing/incremental_type_index.d.ts.map +1 -0
  23. package/compiled/src/step/parsing/incremental_type_index.js +92 -0
  24. package/compiled/src/step/parsing/incremental_type_index.test.d.ts +2 -0
  25. package/compiled/src/step/parsing/incremental_type_index.test.d.ts.map +1 -0
  26. package/compiled/src/step/parsing/incremental_type_index.test.js +55 -0
  27. package/compiled/src/step/parsing/step_parser.d.ts +6 -1
  28. package/compiled/src/step/parsing/step_parser.d.ts.map +1 -1
  29. package/compiled/src/step/parsing/step_parser.js +10 -3
  30. package/compiled/src/step/parsing/streaming_index_builder.d.ts +11 -1
  31. package/compiled/src/step/parsing/streaming_index_builder.d.ts.map +1 -1
  32. package/compiled/src/step/parsing/streaming_index_builder.js +12 -2
  33. package/compiled/src/step/parsing/streaming_record_dispatcher.d.ts +55 -0
  34. package/compiled/src/step/parsing/streaming_record_dispatcher.d.ts.map +1 -0
  35. package/compiled/src/step/parsing/streaming_record_dispatcher.js +67 -0
  36. package/compiled/src/step/parsing/streaming_record_dispatcher.test.d.ts +2 -0
  37. package/compiled/src/step/parsing/streaming_record_dispatcher.test.d.ts.map +1 -0
  38. package/compiled/src/step/parsing/streaming_record_dispatcher.test.js +66 -0
  39. package/compiled/src/version/version.js +1 -1
  40. package/compiled/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +1 -1
@@ -944,7 +944,7 @@ var EntityTypesIfcCount = 909;
944
944
  var entity_types_ifc_gen_default = EntityTypesIfc;
945
945
 
946
946
  // compiled/src/version/version.js
947
- var versionString = "Conway v1.400.1226";
947
+ var versionString = "Conway v1.403.1239";
948
948
 
949
949
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
950
950
  var wasmType = "";
@@ -3493,11 +3493,16 @@ var StepParser = class extends StepHeaderParser {
3493
3493
  * @param input The input parsing buffer, positioned at the data section.
3494
3494
  * @param onRecordBoundary Called at each top-level record boundary with the
3495
3495
  * buffer; the callback may rebase the buffer's window in place.
3496
+ * @param onRecordIndexed Called as each top-level record is indexed, with
3497
+ * its localID, expressID and typeID (0 for external-mapping records) — the
3498
+ * seam for incremental semantic consumers (type index, roots registry,
3499
+ * names skeleton). Must be synchronous and cheap; expensive work belongs on
3500
+ * a demand queue, not the parse path.
3496
3501
  * @param onProgress Optional byte-cursor progress callback.
3497
3502
  * @return {BlockParseResult} The parsing result, including the index and result enum.
3498
3503
  */
3499
- parseDataBlockStreamed(input, onRecordBoundary, onProgress) {
3500
- const parser211 = this.parseDataBlockIncremental(input, onRecordBoundary);
3504
+ parseDataBlockStreamed(input, onRecordBoundary, onRecordIndexed, onProgress) {
3505
+ const parser211 = this.parseDataBlockIncremental(input, onRecordBoundary, onRecordIndexed);
3501
3506
  while (true) {
3502
3507
  const next = parser211.next();
3503
3508
  if (next.done === true) {
@@ -3543,7 +3548,7 @@ var StepParser = class extends StepHeaderParser {
3543
3548
  * @yields {number} The current byte cursor within the input buffer.
3544
3549
  * @return {BlockParseResult} The parsing result, including the index and result enum.
3545
3550
  */
3546
- *parseDataBlockIncremental(input, onRecordBoundary) {
3551
+ *parseDataBlockIncremental(input, onRecordBoundary, onRecordIndexed) {
3547
3552
  const indexResult = { elements: [] };
3548
3553
  const match = input.match;
3549
3554
  const comment = () => match(commentParser2);
@@ -3720,6 +3725,7 @@ var StepParser = class extends StepHeaderParser {
3720
3725
  if (!charws(SEMICOLON)) {
3721
3726
  return syntaxError();
3722
3727
  }
3728
+ onRecordIndexed?.(indexResult.elements.length, expressID, 0);
3723
3729
  indexResult.elements.push({
3724
3730
  address: startElement2,
3725
3731
  length: input.address - startElement2,
@@ -3811,6 +3817,7 @@ var StepParser = class extends StepHeaderParser {
3811
3817
  if (!charws(SEMICOLON)) {
3812
3818
  return syntaxError();
3813
3819
  }
3820
+ onRecordIndexed?.(indexResult.elements.length, expressID, foundItem);
3814
3821
  indexResult.elements.push({
3815
3822
  address: startElement,
3816
3823
  length: input.address - startElement,
@@ -70774,7 +70781,7 @@ var IfcStepModel = class extends StepModelBase {
70774
70781
  };
70775
70782
 
70776
70783
  // compiled/src/step/parsing/streaming_index_builder.js
70777
- function buildIndexStreaming(source, parser211, pool3) {
70784
+ function buildIndexStreaming(source, parser211, pool3, onRecordIndexed) {
70778
70785
  const fileSize = source.byteLength;
70779
70786
  let windowBytes = Math.max(pool3, MIN_WINDOW);
70780
70787
  for (; ; ) {
@@ -70820,7 +70827,7 @@ function buildIndexStreaming(source, parser211, pool3) {
70820
70827
  buffer.rebaseWindow(window2, 0, windowLen, windowStartFile);
70821
70828
  ++slides;
70822
70829
  };
70823
- const [index, result] = parser211.parseDataBlockStreamed(input, onRecordBoundary);
70830
+ const [index, result] = parser211.parseDataBlockStreamed(input, onRecordBoundary, onRecordIndexed);
70824
70831
  const stoppedShort = result !== ParseResult.COMPLETE;
70825
70832
  const notAtEof = windowStartFile + windowLen < fileSize;
70826
70833
  if (stoppedShort && notAtEof) {
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Demand-driven, budgeted geometry materialisation (M3).
3
+ *
4
+ * The streaming loader inverts geometry: instead of extracting every product
5
+ * up front, extraction becomes a **cache fill keyed by product**, ordered by
6
+ * demand (viewport frustum + distance, explicit selection, prefetch hints)
7
+ * and bounded by an explicit byte budget. This module is the scheduler: a
8
+ * priority queue over product local IDs plus an evictable resident set, with
9
+ * the actual extract / release delegated to a pluggable {@link GeometryTiles}.
10
+ *
11
+ * The heavy half — the wasm-side extract → tessellate → upload and the
12
+ * per-product native reclaim — lives behind {@link GeometryTiles}. Its
13
+ * production implementation needs a **conway-geom API to free one product's
14
+ * native geometry** (today the wasm heap has no per-product reclaim); scoping
15
+ * that C++ surface is the M3 blocker tracked in the design doc. This scheduler
16
+ * is engine-side, fully deterministic, and tested against a mock tiles impl,
17
+ * so the queue/budget/eviction policy is settled independently of the wasm
18
+ * work.
19
+ */
20
+ /**
21
+ * The per-product geometry work behind the queue. Implementations do the
22
+ * expensive materialisation; the queue owns only ordering and the budget.
23
+ */
24
+ export interface GeometryTiles {
25
+ /**
26
+ * Materialise a product's geometry tile and return its resident cost in
27
+ * bytes (GPU/scene + any retained wasm working set the budget should count).
28
+ * Called at most once per product between evictions.
29
+ *
30
+ * @param productLocalID The product's local ID.
31
+ * @return {number} The tile's resident byte cost (≥ 0).
32
+ */
33
+ extract(productLocalID: number): number;
34
+ /**
35
+ * Release a product's materialised tile, freeing its resident bytes — the
36
+ * scene mesh and, crucially, the product's native (wasm) geometry. This is
37
+ * the per-product reclaim the production conway-geom surface must provide.
38
+ *
39
+ * @param productLocalID The product's local ID.
40
+ */
41
+ release(productLocalID: number): void;
42
+ }
43
+ /**
44
+ * Runtime counters for a queue, for tests and telemetry.
45
+ */
46
+ export interface DemandQueueStats {
47
+ extractions: number;
48
+ evictions: number;
49
+ residentBytes: number;
50
+ residentCount: number;
51
+ pendingCount: number;
52
+ }
53
+ /**
54
+ * A budgeted, demand-ordered geometry materialisation queue.
55
+ *
56
+ * Usage: `request(productLocalID, priority)` as demand changes (higher
57
+ * priority = more wanted); `pump()` to materialise the most-wanted pending
58
+ * products until the queue drains or the byte budget forces eviction of
59
+ * lower-priority resident tiles. Re-requesting an evicted product re-fills it.
60
+ * A steady stream of `request` + `pump` keeps a bounded working set of the
61
+ * highest-priority products resident — the "full model navigable under a
62
+ * fixed budget" behaviour.
63
+ */
64
+ export declare class DemandGeometryQueue {
65
+ private readonly tiles_;
66
+ private readonly budgetBytes_;
67
+ /** Highest priority a product currently wants materialising at. */
68
+ private readonly pending_;
69
+ /** Resident tiles by product local ID. */
70
+ private readonly resident_;
71
+ private residentBytes_;
72
+ private extractions_;
73
+ private evictions_;
74
+ /**
75
+ * @param tiles The extract/release backend.
76
+ * @param budgetBytes Maximum resident tile bytes before eviction kicks in.
77
+ */
78
+ constructor(tiles: GeometryTiles, budgetBytes: number);
79
+ /**
80
+ * Request a product be materialised at the given demand priority. Updates
81
+ * the priority if already pending (keeping the higher of the two) or already
82
+ * resident (so eviction ranks it correctly). Requesting a resident product
83
+ * does not re-extract it.
84
+ *
85
+ * @param productLocalID The product to materialise.
86
+ * @param priority The demand priority (higher = more wanted).
87
+ */
88
+ request(productLocalID: number, priority: number): void;
89
+ /**
90
+ * Materialise pending products in descending priority order, evicting the
91
+ * lowest-priority resident tiles whenever the budget is exceeded. A pending
92
+ * product whose priority is below every resident tile AND that can't fit is
93
+ * left pending (it will fill once demand raises it or resident tiles drop).
94
+ *
95
+ * @param maxExtractions Optional cap on how many tiles to extract this pump
96
+ * (e.g. a per-frame budget); unbounded when omitted.
97
+ * @return {number} The number of tiles extracted this pump.
98
+ */
99
+ pump(maxExtractions?: number): number;
100
+ /**
101
+ * Evict every resident tile (e.g. on model close), releasing all bytes.
102
+ */
103
+ evictAll(): void;
104
+ /**
105
+ * @return {boolean} True if the product's tile is resident.
106
+ * @param productLocalID The product to check.
107
+ */
108
+ isResident(productLocalID: number): boolean;
109
+ /**
110
+ * @return {DemandQueueStats} A snapshot of runtime counters.
111
+ */
112
+ get stats(): DemandQueueStats;
113
+ /**
114
+ * Pop the highest-priority pending request. Linear scan — swap for a heap
115
+ * if a real workload shows the pending set growing large per pump.
116
+ *
117
+ * @return {PendingRequest | undefined} The request, or undefined if none.
118
+ */
119
+ private popHighestPending_;
120
+ /**
121
+ * Ensure there is conceptual room for a tile requested at `priority` by
122
+ * evicting resident tiles strictly lower in priority while over budget.
123
+ * Returns false if the budget is full of tiles at least as wanted as this
124
+ * request (so it shouldn't displace them).
125
+ *
126
+ * @param priority The incoming request's priority.
127
+ * @return {boolean} True if extraction should proceed.
128
+ */
129
+ private ensureRoomFor_;
130
+ /**
131
+ * Evict lowest-priority resident tiles until within budget (used after an
132
+ * extraction that pushed us over — e.g. a large tile).
133
+ */
134
+ private evictToBudget_;
135
+ /**
136
+ * @return {{ productLocalID: number, priority: number } | undefined} The
137
+ * lowest-priority resident tile, or undefined if none.
138
+ */
139
+ private lowestPriorityResident_;
140
+ /**
141
+ * Release one resident tile.
142
+ *
143
+ * @param productLocalID The tile to evict.
144
+ */
145
+ private evictOne_;
146
+ }
147
+ //# sourceMappingURL=demand_geometry_queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demand_geometry_queue.d.ts","sourceRoot":"","sources":["../../../src/core/demand_geometry_queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,MAAM,WAAW,aAAa;IAE5B;;;;;;;OAOG;IACH,OAAO,CAAE,cAAc,EAAE,MAAM,GAAI,MAAM,CAAA;IAEzC;;;;;;OAMG;IACH,OAAO,CAAE,cAAc,EAAE,MAAM,GAAI,IAAI,CAAA;CACxC;AAiBD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;CACrB;AAGD;;;;;;;;;;GAUG;AACH,qBAAa,mBAAmB;IAE9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IAErC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IAErD,0CAA0C;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAE5D,OAAO,CAAC,cAAc,CAAI;IAE1B,OAAO,CAAC,YAAY,CAAI;IAExB,OAAO,CAAC,UAAU,CAAI;IAEtB;;;OAGG;gBACU,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM;IAUtD;;;;;;;;OAQG;IACI,OAAO,CAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAI,IAAI;IAiBhE;;;;;;;;;OASG;IACI,IAAI,CAAE,cAAc,GAAE,MAAiC,GAAI,MAAM;IAoCxE;;OAEG;IACI,QAAQ,IAAI,IAAI;IASvB;;;OAGG;IACI,UAAU,CAAE,cAAc,EAAE,MAAM,GAAI,OAAO;IAIpD;;OAEG;IACH,IAAW,KAAK,IAAI,gBAAgB,CAQnC;IAED;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAuBtB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAYtB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAiB/B;;;;OAIG;IACH,OAAO,CAAC,SAAS;CAYlB"}
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Demand-driven, budgeted geometry materialisation (M3).
3
+ *
4
+ * The streaming loader inverts geometry: instead of extracting every product
5
+ * up front, extraction becomes a **cache fill keyed by product**, ordered by
6
+ * demand (viewport frustum + distance, explicit selection, prefetch hints)
7
+ * and bounded by an explicit byte budget. This module is the scheduler: a
8
+ * priority queue over product local IDs plus an evictable resident set, with
9
+ * the actual extract / release delegated to a pluggable {@link GeometryTiles}.
10
+ *
11
+ * The heavy half — the wasm-side extract → tessellate → upload and the
12
+ * per-product native reclaim — lives behind {@link GeometryTiles}. Its
13
+ * production implementation needs a **conway-geom API to free one product's
14
+ * native geometry** (today the wasm heap has no per-product reclaim); scoping
15
+ * that C++ surface is the M3 blocker tracked in the design doc. This scheduler
16
+ * is engine-side, fully deterministic, and tested against a mock tiles impl,
17
+ * so the queue/budget/eviction policy is settled independently of the wasm
18
+ * work.
19
+ */
20
+ /**
21
+ * A budgeted, demand-ordered geometry materialisation queue.
22
+ *
23
+ * Usage: `request(productLocalID, priority)` as demand changes (higher
24
+ * priority = more wanted); `pump()` to materialise the most-wanted pending
25
+ * products until the queue drains or the byte budget forces eviction of
26
+ * lower-priority resident tiles. Re-requesting an evicted product re-fills it.
27
+ * A steady stream of `request` + `pump` keeps a bounded working set of the
28
+ * highest-priority products resident — the "full model navigable under a
29
+ * fixed budget" behaviour.
30
+ */
31
+ export class DemandGeometryQueue {
32
+ /**
33
+ * @param tiles The extract/release backend.
34
+ * @param budgetBytes Maximum resident tile bytes before eviction kicks in.
35
+ */
36
+ constructor(tiles, budgetBytes) {
37
+ /** Highest priority a product currently wants materialising at. */
38
+ this.pending_ = new Map();
39
+ /** Resident tiles by product local ID. */
40
+ this.resident_ = new Map();
41
+ this.residentBytes_ = 0;
42
+ this.extractions_ = 0;
43
+ this.evictions_ = 0;
44
+ if (budgetBytes <= 0) {
45
+ throw new Error(`Invalid budgetBytes ${budgetBytes}`);
46
+ }
47
+ this.tiles_ = tiles;
48
+ this.budgetBytes_ = budgetBytes;
49
+ }
50
+ /**
51
+ * Request a product be materialised at the given demand priority. Updates
52
+ * the priority if already pending (keeping the higher of the two) or already
53
+ * resident (so eviction ranks it correctly). Requesting a resident product
54
+ * does not re-extract it.
55
+ *
56
+ * @param productLocalID The product to materialise.
57
+ * @param priority The demand priority (higher = more wanted).
58
+ */
59
+ request(productLocalID, priority) {
60
+ const tile = this.resident_.get(productLocalID);
61
+ if (tile !== void 0) {
62
+ // Already resident — just refresh its eviction ranking.
63
+ tile.priority = Math.max(tile.priority, priority);
64
+ return;
65
+ }
66
+ const existing = this.pending_.get(productLocalID);
67
+ this.pending_.set(productLocalID, existing === void 0 ? priority : Math.max(existing, priority));
68
+ }
69
+ /**
70
+ * Materialise pending products in descending priority order, evicting the
71
+ * lowest-priority resident tiles whenever the budget is exceeded. A pending
72
+ * product whose priority is below every resident tile AND that can't fit is
73
+ * left pending (it will fill once demand raises it or resident tiles drop).
74
+ *
75
+ * @param maxExtractions Optional cap on how many tiles to extract this pump
76
+ * (e.g. a per-frame budget); unbounded when omitted.
77
+ * @return {number} The number of tiles extracted this pump.
78
+ */
79
+ pump(maxExtractions = Number.POSITIVE_INFINITY) {
80
+ let extractedThisPump = 0;
81
+ while (extractedThisPump < maxExtractions && this.pending_.size > 0) {
82
+ const next = this.popHighestPending_();
83
+ if (next === void 0) {
84
+ break;
85
+ }
86
+ // If the working set is full and this request can't beat the cheapest
87
+ // evictable (lower-priority) resident tile, stop — nothing more fits.
88
+ if (!this.ensureRoomFor_(next.priority)) {
89
+ // Put it back; it stays pending for a future pump.
90
+ this.pending_.set(next.productLocalID, next.priority);
91
+ break;
92
+ }
93
+ const bytes = this.tiles_.extract(next.productLocalID);
94
+ this.resident_.set(next.productLocalID, { bytes, priority: next.priority });
95
+ this.residentBytes_ += bytes;
96
+ ++this.extractions_;
97
+ ++extractedThisPump;
98
+ // A single oversized tile can still exceed budget; evict what we can.
99
+ this.evictToBudget_();
100
+ }
101
+ return extractedThisPump;
102
+ }
103
+ /**
104
+ * Evict every resident tile (e.g. on model close), releasing all bytes.
105
+ */
106
+ evictAll() {
107
+ for (const productLocalID of this.resident_.keys()) {
108
+ this.tiles_.release(productLocalID);
109
+ ++this.evictions_;
110
+ }
111
+ this.resident_.clear();
112
+ this.residentBytes_ = 0;
113
+ }
114
+ /**
115
+ * @return {boolean} True if the product's tile is resident.
116
+ * @param productLocalID The product to check.
117
+ */
118
+ isResident(productLocalID) {
119
+ return this.resident_.has(productLocalID);
120
+ }
121
+ /**
122
+ * @return {DemandQueueStats} A snapshot of runtime counters.
123
+ */
124
+ get stats() {
125
+ return {
126
+ extractions: this.extractions_,
127
+ evictions: this.evictions_,
128
+ residentBytes: this.residentBytes_,
129
+ residentCount: this.resident_.size,
130
+ pendingCount: this.pending_.size,
131
+ };
132
+ }
133
+ /**
134
+ * Pop the highest-priority pending request. Linear scan — swap for a heap
135
+ * if a real workload shows the pending set growing large per pump.
136
+ *
137
+ * @return {PendingRequest | undefined} The request, or undefined if none.
138
+ */
139
+ popHighestPending_() {
140
+ let bestID;
141
+ let bestPriority = Number.NEGATIVE_INFINITY;
142
+ for (const [productLocalID, priority] of this.pending_) {
143
+ if (priority > bestPriority) {
144
+ bestPriority = priority;
145
+ bestID = productLocalID;
146
+ }
147
+ }
148
+ if (bestID === void 0) {
149
+ return void 0;
150
+ }
151
+ this.pending_.delete(bestID);
152
+ return { productLocalID: bestID, priority: bestPriority };
153
+ }
154
+ /**
155
+ * Ensure there is conceptual room for a tile requested at `priority` by
156
+ * evicting resident tiles strictly lower in priority while over budget.
157
+ * Returns false if the budget is full of tiles at least as wanted as this
158
+ * request (so it shouldn't displace them).
159
+ *
160
+ * @param priority The incoming request's priority.
161
+ * @return {boolean} True if extraction should proceed.
162
+ */
163
+ ensureRoomFor_(priority) {
164
+ if (this.residentBytes_ < this.budgetBytes_) {
165
+ return true;
166
+ }
167
+ // Over/at budget: only proceed if there's a lower-priority tile to evict.
168
+ let evictedAny = false;
169
+ while (this.residentBytes_ >= this.budgetBytes_) {
170
+ const victim = this.lowestPriorityResident_();
171
+ if (victim === void 0 || victim.priority >= priority) {
172
+ break;
173
+ }
174
+ this.evictOne_(victim.productLocalID);
175
+ evictedAny = true;
176
+ }
177
+ return this.residentBytes_ < this.budgetBytes_ || evictedAny;
178
+ }
179
+ /**
180
+ * Evict lowest-priority resident tiles until within budget (used after an
181
+ * extraction that pushed us over — e.g. a large tile).
182
+ */
183
+ evictToBudget_() {
184
+ while (this.residentBytes_ > this.budgetBytes_) {
185
+ const victim = this.lowestPriorityResident_();
186
+ if (victim === void 0) {
187
+ break;
188
+ }
189
+ this.evictOne_(victim.productLocalID);
190
+ }
191
+ }
192
+ /**
193
+ * @return {{ productLocalID: number, priority: number } | undefined} The
194
+ * lowest-priority resident tile, or undefined if none.
195
+ */
196
+ lowestPriorityResident_() {
197
+ let worstID;
198
+ let worstPriority = Number.POSITIVE_INFINITY;
199
+ for (const [productLocalID, tile] of this.resident_) {
200
+ if (tile.priority < worstPriority) {
201
+ worstPriority = tile.priority;
202
+ worstID = productLocalID;
203
+ }
204
+ }
205
+ return worstID === void 0 ?
206
+ void 0 : { productLocalID: worstID, priority: worstPriority };
207
+ }
208
+ /**
209
+ * Release one resident tile.
210
+ *
211
+ * @param productLocalID The tile to evict.
212
+ */
213
+ evictOne_(productLocalID) {
214
+ const tile = this.resident_.get(productLocalID);
215
+ if (tile === void 0) {
216
+ return;
217
+ }
218
+ this.tiles_.release(productLocalID);
219
+ this.resident_.delete(productLocalID);
220
+ this.residentBytes_ -= tile.bytes;
221
+ ++this.evictions_;
222
+ }
223
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=demand_geometry_queue.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demand_geometry_queue.test.d.ts","sourceRoot":"","sources":["../../../src/core/demand_geometry_queue.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,133 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // M3: the demand-geometry scheduler — priority ordering, byte budget, and
3
+ // eviction — tested against a mock tiles backend so the policy is settled
4
+ // independently of the wasm extract/reclaim work.
5
+ import { describe, expect, test } from "@jest/globals";
6
+ import { DemandGeometryQueue } from "./demand_geometry_queue.js";
7
+ /**
8
+ * A mock tiles backend: every product costs a fixed number of bytes and
9
+ * records the extract/release calls so tests can assert on them.
10
+ *
11
+ * @param costBytes Per-tile resident cost.
12
+ * @return {object} `{ tiles, extracted, released }`.
13
+ */
14
+ function mockTiles(costBytes) {
15
+ const extracted = [];
16
+ const released = [];
17
+ const tiles = {
18
+ extract(id) {
19
+ extracted.push(id);
20
+ return costBytes;
21
+ },
22
+ release(id) {
23
+ released.push(id);
24
+ },
25
+ };
26
+ return { tiles, extracted, released };
27
+ }
28
+ describe("DemandGeometryQueue", () => {
29
+ test("materialises highest-priority requests first", () => {
30
+ const { tiles, extracted } = mockTiles(10);
31
+ const q = new DemandGeometryQueue(tiles, 1000);
32
+ q.request(1, 5);
33
+ q.request(2, 50);
34
+ q.request(3, 20);
35
+ q.pump();
36
+ // Extracted in descending priority: 2 (50), 3 (20), 1 (5).
37
+ expect(extracted).toEqual([2, 3, 1]);
38
+ expect(q.stats.residentCount).toBe(3);
39
+ expect(q.stats.residentBytes).toBe(30);
40
+ });
41
+ test("evicts the lowest-priority tile when the budget is exceeded", () => {
42
+ const { tiles, released } = mockTiles(100);
43
+ // Budget holds 3 tiles (300).
44
+ const q = new DemandGeometryQueue(tiles, 300);
45
+ for (const [id, pri] of [[1, 10], [2, 20], [3, 30]]) {
46
+ q.request(id, pri);
47
+ }
48
+ q.pump();
49
+ expect(q.stats.residentCount).toBe(3);
50
+ // A 4th, higher-priority request must evict the lowest (id 1, pri 10).
51
+ q.request(4, 40);
52
+ q.pump();
53
+ expect(q.isResident(4)).toBe(true);
54
+ expect(q.isResident(1)).toBe(false);
55
+ expect(released).toContain(1);
56
+ expect(q.stats.residentBytes).toBeLessThanOrEqual(300);
57
+ });
58
+ test("a request below every resident tile does not displace them", () => {
59
+ const { tiles, extracted } = mockTiles(100);
60
+ const q = new DemandGeometryQueue(tiles, 200); // holds 2
61
+ q.request(1, 50);
62
+ q.request(2, 40);
63
+ q.pump();
64
+ expect(q.stats.residentCount).toBe(2);
65
+ // Low-priority newcomer can't beat either resident tile → stays pending.
66
+ q.request(3, 5);
67
+ const n = q.pump();
68
+ expect(n).toBe(0);
69
+ expect(q.isResident(3)).toBe(false);
70
+ expect(extracted).toEqual([1, 2]);
71
+ expect(q.stats.pendingCount).toBe(1);
72
+ });
73
+ test("re-requesting an evicted product re-extracts it", () => {
74
+ const { tiles, extracted } = mockTiles(100);
75
+ const q = new DemandGeometryQueue(tiles, 100); // holds 1
76
+ q.request(1, 10);
77
+ q.pump();
78
+ q.request(2, 20); // evicts 1
79
+ q.pump();
80
+ expect(q.isResident(1)).toBe(false);
81
+ q.request(1, 30); // evicts 2, re-extracts 1
82
+ q.pump();
83
+ expect(q.isResident(1)).toBe(true);
84
+ // 1 extracted twice (first fill, then re-fill after eviction).
85
+ expect(extracted.filter((id) => id === 1).length).toBe(2);
86
+ });
87
+ test("requesting an already-resident product does not re-extract", () => {
88
+ const { tiles, extracted } = mockTiles(10);
89
+ const q = new DemandGeometryQueue(tiles, 1000);
90
+ q.request(1, 10);
91
+ q.pump();
92
+ q.request(1, 99); // already resident — just bumps ranking
93
+ q.pump();
94
+ expect(extracted).toEqual([1]);
95
+ });
96
+ test("a per-pump extraction cap bounds work per call", () => {
97
+ const { tiles } = mockTiles(1);
98
+ const q = new DemandGeometryQueue(tiles, 1000000);
99
+ for (let id = 0; id < 100; ++id) {
100
+ q.request(id, id);
101
+ }
102
+ expect(q.pump(10)).toBe(10);
103
+ expect(q.stats.residentCount).toBe(10);
104
+ expect(q.stats.pendingCount).toBe(90);
105
+ });
106
+ test("evictAll releases everything", () => {
107
+ const { tiles, released } = mockTiles(10);
108
+ const q = new DemandGeometryQueue(tiles, 1000);
109
+ for (let id = 0; id < 5; ++id) {
110
+ q.request(id, id);
111
+ }
112
+ q.pump();
113
+ q.evictAll();
114
+ expect(q.stats.residentCount).toBe(0);
115
+ expect(q.stats.residentBytes).toBe(0);
116
+ expect(released.sort()).toEqual([0, 1, 2, 3, 4]);
117
+ });
118
+ test("keeps the resident set bounded by the budget across a demand stream", () => {
119
+ const { tiles } = mockTiles(100);
120
+ const q = new DemandGeometryQueue(tiles, 500); // holds 5
121
+ // A moving "viewport": 200 products stream past with rising priority.
122
+ for (let id = 0; id < 200; ++id) {
123
+ q.request(id, id);
124
+ q.pump();
125
+ expect(q.stats.residentBytes).toBeLessThanOrEqual(500);
126
+ }
127
+ // The 5 most-wanted (highest ids) remain.
128
+ expect(q.stats.residentCount).toBe(5);
129
+ for (const id of [195, 196, 197, 198, 199]) {
130
+ expect(q.isResident(id)).toBe(true);
131
+ }
132
+ });
133
+ });
@@ -0,0 +1,64 @@
1
+ import { StepEntityConstructorAbstract } from "../step_entity_constructor.js";
2
+ import { RecordHandler } from "./streaming_record_dispatcher.js";
3
+ /**
4
+ * A type index built **incrementally** from the streaming parse's per-record
5
+ * events (M2), rather than from the finished element array. Feed its
6
+ * {@link handler} to a {@link StreamingRecordDispatcher} (or directly to
7
+ * `buildIndexStreaming`'s `onRecordIndexed`) and it accumulates, per concrete
8
+ * type, the set of express IDs seen — so the moment parsing reaches a record
9
+ * it is queryable, without waiting for end-of-parse.
10
+ *
11
+ * Queries take entity constructors and expand to their subtype closure via
12
+ * the generated `query` (conway #383), so `expressIDsOfTypes(IfcProduct)`
13
+ * unions every product subtype exactly as the resident model's type index
14
+ * does.
15
+ *
16
+ * Express IDs are held in per-type `Set`s, so the consumer is idempotent
17
+ * under the streaming builder's rare grow-and-restart (records re-fire from
18
+ * localID 0): re-adding an express ID is a no-op. Memory is O(records) — the
19
+ * same order as the element index it mirrors.
20
+ *
21
+ * Scope: membership is keyed on the raw parse `typeID`. External-mapping /
22
+ * complex records (typeID 0) are therefore not attributed to their mapped
23
+ * classes here — resolving those needs the record's `multiMapping`, which the
24
+ * event stream doesn't carry; that's the model's construction-time index. For
25
+ * the overwhelming majority of records (simple, one type each) this index is
26
+ * exact, which the parity test pins.
27
+ */
28
+ export declare class IncrementalTypeIndex<TypeIDType extends number> {
29
+ /** Concrete typeID → express IDs of records of exactly that type. */
30
+ private readonly byType_;
31
+ /**
32
+ * Record one indexed entity. Bound as {@link handler} for direct use as a
33
+ * dispatcher subscription / `onRecordIndexed` callback.
34
+ *
35
+ * @param localID Unused (kept for the RecordHandler shape).
36
+ * @param expressID The record's express ID.
37
+ * @param typeID The record's concrete type ID (undefined / 0 records are
38
+ * ignored — they carry no concrete queryable type here).
39
+ */
40
+ readonly handler: RecordHandler<TypeIDType>;
41
+ /**
42
+ * The distinct concrete type IDs seen so far.
43
+ *
44
+ * @return {IterableIterator<TypeIDType>} The concrete types.
45
+ */
46
+ concreteTypes(): IterableIterator<TypeIDType>;
47
+ /**
48
+ * Lazily iterate the express IDs of all records of the given types
49
+ * (including subtypes).
50
+ *
51
+ * @param types The entity constructors to query (subtype closures unioned).
52
+ * @return {IterableIterator<number>} Express IDs of matching records.
53
+ * @yields {number} Each matching express ID.
54
+ */
55
+ expressIDsOfTypes(...types: StepEntityConstructorAbstract<TypeIDType>[]): IterableIterator<number>;
56
+ /**
57
+ * Count records of the given types (including subtypes) seen so far.
58
+ *
59
+ * @param types The entity constructors to count.
60
+ * @return {number} The number of matching records.
61
+ */
62
+ count(...types: StepEntityConstructorAbstract<TypeIDType>[]): number;
63
+ }
64
+ //# sourceMappingURL=incremental_type_index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental_type_index.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/incremental_type_index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,oBAAoB,CAAC,UAAU,SAAS,MAAM;IAEzD,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D;;;;;;;;OAQG;IACH,SAAgB,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAe/C;IAEH;;;;OAIG;IACI,aAAa,IAAI,gBAAgB,CAAC,UAAU,CAAC;IAIpD;;;;;;;OAOG;IACK,iBAAiB,CACrB,GAAG,KAAK,EAAE,6BAA6B,CAAC,UAAU,CAAC,EAAE,GACrD,gBAAgB,CAAC,MAAM,CAAC;IAc5B;;;;;OAKG;IACI,KAAK,CAAE,GAAG,KAAK,EAAE,6BAA6B,CAAC,UAAU,CAAC,EAAE,GAAI,MAAM;CAa9E"}