@bldrs-ai/conway 1.401.1237 → 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.
@@ -30,7 +30,7 @@ var import_node_process = require("node:process");
30
30
  var readline = __toESM(require("node:readline"), 1);
31
31
 
32
32
  // compiled/src/version/version.js
33
- var versionString = "Conway v1.401.1237";
33
+ var versionString = "Conway v1.403.1239";
34
34
 
35
35
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
36
36
  var wasmType = "";
@@ -14965,7 +14965,7 @@ ${t5.join("\n")}` : "";
14965
14965
  var import_process = require("process");
14966
14966
 
14967
14967
  // compiled/src/version/version.js
14968
- var versionString = "Conway v1.401.1237";
14968
+ var versionString = "Conway v1.403.1239";
14969
14969
 
14970
14970
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
14971
14971
  function pThreadsAllowed() {
@@ -15943,7 +15943,7 @@ var ParsingBuffer = class {
15943
15943
  };
15944
15944
 
15945
15945
  // compiled/src/version/version.js
15946
- var versionString = "Conway v1.401.1237";
15946
+ var versionString = "Conway v1.403.1239";
15947
15947
 
15948
15948
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
15949
15949
  function pThreadsAllowed() {
@@ -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.401.1237";
947
+ var versionString = "Conway v1.403.1239";
948
948
 
949
949
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
950
950
  var wasmType = "";
@@ -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
+ });
@@ -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.403.1239';
9
9
  export { versionString };