@woosh/meep-engine 2.165.7 → 2.167.0

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 (146) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/package.json +1 -1
  3. package/src/CORRECTNESS_HUNT_2026_07_24.md +142 -0
  4. package/src/core/binary/BinaryBuffer.js +1 -1
  5. package/src/core/binary/EncodingBinaryBuffer.d.ts.map +1 -1
  6. package/src/core/binary/EncodingBinaryBuffer.js +3 -1
  7. package/src/core/cache/Cache.d.ts.map +1 -1
  8. package/src/core/cache/Cache.js +0 -4
  9. package/src/core/cache/CacheElement.d.ts.map +1 -1
  10. package/src/core/cache/CacheElement.js +4 -0
  11. package/src/core/cache/FrequencySketch.d.ts +0 -21
  12. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  13. package/src/core/cache/FrequencySketch.js +66 -65
  14. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts +17 -0
  15. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts.map +1 -0
  16. package/src/core/cache/wtinylfu/CacheElementWTinylfu.js +18 -0
  17. package/src/core/cache/wtinylfu/CacheRegionType.d.ts +10 -0
  18. package/src/core/cache/wtinylfu/CacheRegionType.d.ts.map +1 -0
  19. package/src/core/cache/wtinylfu/CacheRegionType.js +10 -0
  20. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts +66 -0
  21. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts.map +1 -0
  22. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.js +183 -0
  23. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts +279 -0
  24. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -0
  25. package/src/core/cache/wtinylfu/CacheWTinylfu.js +807 -0
  26. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +243 -0
  27. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts +80 -0
  28. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts.map +1 -0
  29. package/src/core/cache/wtinylfu/wtinylfuDecisions.js +102 -0
  30. package/src/core/collection/RingBuffer.d.ts.map +1 -1
  31. package/src/core/collection/RingBuffer.js +15 -3
  32. package/src/core/collection/map/HashMap.d.ts +1 -1
  33. package/src/core/collection/map/HashMap.d.ts.map +1 -1
  34. package/src/core/collection/map/HashMap.js +5 -17
  35. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.d.ts.map +1 -1
  36. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js +5 -0
  37. package/src/core/geom/ConicRay.d.ts.map +1 -1
  38. package/src/core/geom/ConicRay.js +9 -2
  39. package/src/core/geom/Quaternion.d.ts.map +1 -1
  40. package/src/core/geom/Quaternion.js +7 -3
  41. package/src/core/math/build_gaussian_kernel_1d.d.ts +2 -2
  42. package/src/core/math/build_gaussian_kernel_1d.d.ts.map +1 -1
  43. package/src/core/math/build_gaussian_kernel_1d.js +4 -2
  44. package/src/core/math/build_gaussian_kernel_2d.d.ts +2 -2
  45. package/src/core/math/build_gaussian_kernel_2d.d.ts.map +1 -1
  46. package/src/core/math/build_gaussian_kernel_2d.js +4 -2
  47. package/src/core/math/gaussian.d.ts +2 -2
  48. package/src/core/math/gaussian.js +2 -2
  49. package/src/core/math/spline/spline3_bezier_bounds.d.ts.map +1 -1
  50. package/src/core/math/spline/spline3_bezier_bounds.js +10 -3
  51. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts +8 -7
  52. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts.map +1 -1
  53. package/src/core/math/spline/spline3_bezier_to_hermite.js +13 -12
  54. package/src/core/math/spline/spline3_hermite_bounds.d.ts.map +1 -1
  55. package/src/core/math/spline/spline3_hermite_bounds.js +10 -3
  56. package/src/core/math/spline/spline3_hermite_bounds_t.d.ts.map +1 -1
  57. package/src/core/math/spline/spline3_hermite_bounds_t.js +115 -109
  58. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts +2 -0
  59. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts.map +1 -1
  60. package/src/core/math/spline/spline3_hermite_to_bezier.js +2 -0
  61. package/src/core/math/statistics/computeStatisticalMean.js +1 -1
  62. package/src/core/math/statistics/gaussian_amplitude.d.ts.map +1 -1
  63. package/src/core/math/statistics/gaussian_amplitude.js +3 -1
  64. package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
  65. package/src/engine/animation/clip/ecd_bind_animation_curve.js +10 -1
  66. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts +20 -0
  67. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts.map +1 -0
  68. package/src/engine/animation/clip/hemisphere_align_quaternion_values.js +37 -0
  69. package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts.map +1 -1
  70. package/src/engine/animation/keyed2/AnimationTrackPlayback.js +5 -1
  71. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  72. package/src/engine/ecs/EntityComponentDataset.js +5 -21
  73. package/src/engine/ecs/fow/FogOfWar.d.ts +22 -0
  74. package/src/engine/ecs/fow/FogOfWar.d.ts.map +1 -1
  75. package/src/engine/ecs/fow/FogOfWar.js +202 -54
  76. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts +16 -0
  77. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts.map +1 -0
  78. package/src/engine/ecs/fow/FogOfWarEdgeMode.js +39 -0
  79. package/src/engine/ecs/fow/FogOfWarSystem.d.ts.map +1 -1
  80. package/src/engine/ecs/fow/FogOfWarSystem.js +1 -0
  81. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.d.ts.map +1 -1
  82. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.js +3 -0
  83. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts +7 -0
  84. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts.map +1 -1
  85. package/src/engine/ecs/fow/shader/FogOfWarRenderer.js +155 -145
  86. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.d.ts.map +1 -1
  87. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.js +33 -8
  88. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts.map +1 -1
  89. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +3 -1
  90. package/src/engine/graphics/GraphicsEngine.d.ts +5 -1
  91. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  92. package/src/engine/graphics/GraphicsEngine.js +14 -3
  93. package/src/engine/graphics/composit/CompositLayer.d.ts.map +1 -1
  94. package/src/engine/graphics/composit/CompositLayer.js +258 -255
  95. package/src/engine/graphics/composit/LayerCompositer.d.ts +12 -0
  96. package/src/engine/graphics/composit/LayerCompositer.d.ts.map +1 -1
  97. package/src/engine/graphics/composit/LayerCompositer.js +19 -10
  98. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts +1 -1
  99. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts.map +1 -1
  100. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.js +10 -4
  101. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts +6 -0
  102. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts.map +1 -1
  103. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js +21 -0
  104. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.d.ts.map +1 -1
  105. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.js +5 -23
  106. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.d.ts.map +1 -1
  107. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js +6 -4
  108. package/src/engine/graphics/ecs/water/WaterSystem.d.ts.map +1 -1
  109. package/src/engine/graphics/ecs/water/WaterSystem.js +9 -3
  110. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts +12 -0
  111. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts.map +1 -1
  112. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js +46 -9
  113. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts +5 -0
  114. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts.map +1 -1
  115. package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +8 -0
  116. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts +7 -0
  117. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts.map +1 -1
  118. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.js +8 -0
  119. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts +10 -2
  120. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts.map +1 -1
  121. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +42 -10
  122. package/src/engine/graphics/render/buffer/FrameBuffer.d.ts.map +1 -1
  123. package/src/engine/graphics/render/buffer/FrameBuffer.js +5 -2
  124. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts +13 -1
  125. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  126. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +40 -7
  127. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.d.ts.map +1 -1
  128. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.js +10 -4
  129. package/src/engine/graphics/render/forward_plus/LightManager.d.ts.map +1 -1
  130. package/src/engine/graphics/render/forward_plus/LightManager.js +1235 -1232
  131. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts +5 -0
  132. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts.map +1 -1
  133. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js +11 -10
  134. package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
  135. package/src/engine/graphics/trail/tube/TubeX.js +557 -555
  136. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts +12 -1
  137. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts.map +1 -1
  138. package/src/engine/graphics/trail/x/RibbonXPlugin.js +15 -8
  139. package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
  140. package/src/engine/input/ecs/systems/InputControllerSystem.js +9 -6
  141. package/src/engine/navigation/ecs/components/Path.d.ts +5 -2
  142. package/src/engine/navigation/ecs/components/Path.d.ts.map +1 -1
  143. package/src/engine/navigation/ecs/components/Path.js +10 -4
  144. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +1 -1
  145. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  146. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
@@ -0,0 +1,807 @@
1
+ /**
2
+ * Frequency-based admission cache, using W-TinyLFU (Window Tiny Least Frequency)
3
+ *
4
+ * Cache regions:
5
+ * 1. Window (LRU) Burst traffic
6
+ * 2. Main (SLRU) Long-term residency
7
+ * Probation (LRU) Trial area for candidate from Window or demoted from Protected
8
+ * Protected (LRU) VIP area for frequently accessed items
9
+ *
10
+ * Life cycle:
11
+ * 1. Admission: New items enter the Window.
12
+ * 2. Promoting to Probation: If Window is full, the Candidate (oldest window element) attempts to move
13
+ * into the Probation.
14
+ * 3. Frequency Duel: If the Probation is full, the Candidate (oldest window element) competes against the
15
+ * Victim (oldest probation element) using a Count-Min Sketch frequency filter.
16
+ * 4. Promotion to Protected: A Probation item accessed again (or exceeds a threshold) is promoted to the
17
+ * Protected list.
18
+ * 5. Demotion: If the Protected list reaches its capacity, the oldest item is demoted
19
+ * to the Probation list to make room.
20
+ *
21
+ * Capacity management:
22
+ * Can be governed by count or weight
23
+ * Window ~1%
24
+ * Probation ~20%
25
+ * Protected ~80%
26
+ * The Main cache segments are dynamic. Protected is capped at ~80%. Probation will vary
27
+ * as it consumes the remainder, based on promotion/demotion influence.
28
+ */
29
+
30
+ import {CacheRegionWTinylfu} from "./CacheRegionWTinylfu.js";
31
+ import {assert} from "../../assert.js";
32
+ import {CacheElementWTinylfu} from "./CacheElementWTinylfu.js";
33
+ import {CacheRegionType} from "./CacheRegionType.js";
34
+ import {returnZero} from "../../function/returnZero.js";
35
+ import {returnOne} from "../../function/returnOne.js";
36
+ import {FrequencySketch} from "../FrequencySketch.js";
37
+ import {invokeObjectHash} from "../../model/object/invokeObjectHash.js";
38
+ import {invokeObjectEquals} from "../../model/object/invokeObjectEquals.js";
39
+ import {HashMap} from "../../collection/map/HashMap.js";
40
+ import {mainCapacity, exceedsMainWeight, cannotFitMain, winsAdmissionDuel, shouldPromoteToProtected, classifyAdmission, AdmissionRoute} from "./wtinylfuDecisions.js";
41
+ import {Signal} from "../../events/signal/Signal.js";
42
+
43
+ /**
44
+ * Cap on the reuse pool of evicted element objects. Under churn the pool hovers near 1 (one evict per
45
+ * insert), so this only bounds it under remove-heavy bursts; past the cap, elements are left to GC.
46
+ * @type {number}
47
+ */
48
+ const ELEMENT_POOL_MAX = 256;
49
+
50
+ export class CacheWTinylfu {
51
+
52
+ /**
53
+ * @type {number}
54
+ * @private
55
+ */
56
+ #maxWeight = Number.POSITIVE_INFINITY;
57
+
58
+ /**
59
+ * @type {number}
60
+ * @private
61
+ */
62
+ #promotionThreshold = 1;
63
+
64
+ /**
65
+ * @type {function(Key):number}
66
+ * @private
67
+ */
68
+ #keyHashFunction = invokeObjectHash;
69
+
70
+ /**
71
+ * Cache regions — kept as three direct fields (not a {window,trial,protected} record) so each hot-path
72
+ * access is a single property read rather than two.
73
+ * @type {CacheRegionWTinylfu}
74
+ * @private
75
+ */
76
+ #window;
77
+ /** @type {CacheRegionWTinylfu} @private */
78
+ #trial;
79
+ /** @type {CacheRegionWTinylfu} @private */
80
+ #protected;
81
+
82
+ /**
83
+ *
84
+ * @type {FrequencySketch}
85
+ * @private
86
+ */
87
+ #sketch = new FrequencySketch();
88
+
89
+ /**
90
+ * @type {function(Key): number}
91
+ * @private
92
+ */
93
+ #keyWeigher;
94
+
95
+ /**
96
+ * @type {function(Value): number}
97
+ * @private
98
+ */
99
+ #valueWeigher;
100
+
101
+ /**
102
+ * @type {HashMap<Key, CacheElementWTinylfu<Key,Value>>}
103
+ * @private
104
+ */
105
+ #data;
106
+
107
+ /**
108
+ * Reuse pool of evicted element objects. `put` takes one from here instead of allocating a fresh
109
+ * element; under churn this makes steady-state inserts allocation-free. Capped at {@link ELEMENT_POOL_MAX}.
110
+ * @type {CacheElementWTinylfu[]}
111
+ * @private
112
+ */
113
+ #elementPool = [];
114
+
115
+ /**
116
+ * Dispatched when an element is added or its value replaced. send2(key, value)
117
+ * @type {Signal}
118
+ */
119
+ onSet = new Signal();
120
+
121
+ /**
122
+ * Dispatched when an element leaves the cache, by eviction or explicit remove. send2(key, value)
123
+ * @type {Signal}
124
+ */
125
+ onRemoved = new Signal();
126
+
127
+ /**
128
+ * Dispatched when an element leaves due to capacity, immediately after onRemoved. send2(key, value)
129
+ * @type {Signal}
130
+ */
131
+ onEvicted = new Signal();
132
+
133
+ /**
134
+ * Dispatched when admission control turns an element away (reject-guard or lost duel) — it was
135
+ * never stored. Unique to W-TinyLFU. send2(key, value)
136
+ * @type {Signal}
137
+ */
138
+ onRejected = new Signal();
139
+
140
+ /**
141
+ *
142
+ * @param {number} [maxWeight=Number.POSITIVE_INFINITY] total weight budget of the cache; an element's weight is keyWeigher(key) + valueWeigher(value)
143
+ * @param {number} [promotionThreshold=1] frequency an item must exceed to be promoted out of Trial (Probation)
144
+ * @param {number} [capacityWindow=1] weight cap of the Window region (burst buffer)
145
+ * @param {number} [capacityProtected=1] weight cap of the Protected region (VIP segment of Main)
146
+ * @param {function(key:Key):number} [keyWeigher= key=>0] function to compute weight of a key
147
+ * @param {function(value:Value):number} [valueWeigher= value=>1] function to compute weight of a value
148
+ * @param {function(Key):number} [keyHashFunction]
149
+ * @param {function(Key, Key):boolean} [keyEqualityFunction]
150
+ */
151
+ constructor({
152
+ maxWeight = Number.POSITIVE_INFINITY,
153
+ promotionThreshold = 1,
154
+ capacityWindow = 1,
155
+ capacityProtected = 1,
156
+ keyWeigher = returnZero,
157
+ valueWeigher = returnOne,
158
+ keyHashFunction = invokeObjectHash,
159
+ keyEqualityFunction = invokeObjectEquals
160
+ } = {}) {
161
+
162
+ /**
163
+ * @param {number} parameter
164
+ * @param {string} message
165
+ */
166
+ const numberAssert = (parameter, message) => {
167
+ assert.isNumber(parameter, message);
168
+ assert.notNaN(parameter, message);
169
+ assert.greaterThanOrEqual(parameter, 0, message +' < 0');
170
+ }
171
+
172
+ numberAssert(maxWeight, 'maxWeight');
173
+ numberAssert(promotionThreshold, 'promotionThreshold');
174
+ numberAssert(capacityWindow, 'capacityWindow');
175
+ numberAssert(capacityProtected, 'capacityProtected');
176
+
177
+ const trialRegionSize = maxWeight - (capacityWindow + capacityProtected)
178
+ assert.greaterThan(trialRegionSize, 0, "probation region <= 0");
179
+ assert.greaterThanOrEqual(trialRegionSize, capacityWindow, 'capacityWindow > trialRegionSize');
180
+
181
+ assert.isFunction(keyWeigher, 'keyWeigher');
182
+ assert.isFunction(valueWeigher, 'valueWeigher');
183
+ assert.isFunction(keyHashFunction, 'keyHashFunction');
184
+ assert.isFunction(keyEqualityFunction, 'keyEqualityFunction');
185
+
186
+
187
+ this.#maxWeight = maxWeight;
188
+ this.#promotionThreshold = promotionThreshold;
189
+ this.#keyHashFunction = keyHashFunction;
190
+
191
+ this.#window = new CacheRegionWTinylfu({maxWeight: capacityWindow});
192
+ this.#trial = new CacheRegionWTinylfu({});
193
+ this.#protected = new CacheRegionWTinylfu({maxWeight: capacityProtected});
194
+
195
+ this.#keyWeigher = keyWeigher;
196
+ this.#valueWeigher = valueWeigher;
197
+
198
+ this.#data = new HashMap({
199
+ keyHashFunction,
200
+ keyEqualityFunction
201
+ });
202
+
203
+ // The sketch is sized dynamically to the live element count in __admit; seed a minimal table.
204
+ this.#sketch.ensureCapacity(0);
205
+ }
206
+
207
+ /**
208
+ * Update the recency & frequency of the cache element
209
+ * @param {CacheElementWTinylfu} element
210
+ * @private
211
+ */
212
+ __touch(element) {
213
+ this.__incrementSketch(element);
214
+ this.__onAccess(element);
215
+ }
216
+
217
+ /**
218
+ * Recency + promotion side of an access (no frequency increment).
219
+ * @param {CacheElementWTinylfu} element
220
+ * @private
221
+ */
222
+ __onAccess(element) {
223
+ switch (element.region) {
224
+ case CacheRegionType.Window:
225
+ this.#window.touch(element);
226
+ break;
227
+ case CacheRegionType.Trial:
228
+ this.__onTrialAccess(element)
229
+ break;
230
+ case CacheRegionType.Protected:
231
+ this.#protected.touch(element);
232
+ break;
233
+ default:
234
+ throw new Error ("Invalid region found");
235
+ }
236
+ }
237
+ /**
238
+ * Access of a Trial element: promote to Protected if hot enough and it fits, else bump recency.
239
+ * @param {CacheElementWTinylfu} element
240
+ * @private
241
+ */
242
+ __onTrialAccess(element) {
243
+ const trialRegion = this.#trial;
244
+ const protectedRegion = this.#protected;
245
+
246
+ if (this.__shouldPromoteToProtected(element)) {
247
+ trialRegion.remove(element);
248
+
249
+ element.region = CacheRegionType.Protected;
250
+ protectedRegion.add(element);
251
+
252
+ while (protectedRegion.wouldExceedCapacity())
253
+ {
254
+ const demoted = protectedRegion.findEvictionVictim();
255
+ protectedRegion.remove(demoted);
256
+
257
+ demoted.region = CacheRegionType.Trial;
258
+ trialRegion.add(demoted);
259
+ }
260
+ }
261
+ else {
262
+ trialRegion.touch(element);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Record an access to `element` in the frequency sketch (using its cached key hash).
268
+ * @param {CacheElementWTinylfu} element
269
+ * @private
270
+ */
271
+ __incrementSketch(element) {
272
+ this.#sketch.increment(element.keyHash);
273
+ }
274
+
275
+ /**
276
+ * Estimated access frequency of `element` from the sketch (using its cached key hash).
277
+ * @param {CacheElementWTinylfu} element
278
+ * @returns {number}
279
+ * @private
280
+ */
281
+ __frequency(element) {
282
+ return this.#sketch.frequency(element.keyHash);
283
+ }
284
+
285
+ /**
286
+ * Is `element` hot enough and small enough to be promoted from Trial to Protected?
287
+ * (frequency above threshold AND fits Protected — contract in {@link shouldPromoteToProtected})
288
+ * @param {CacheElementWTinylfu} element
289
+ * @returns {boolean}
290
+ * @private
291
+ */
292
+ __shouldPromoteToProtected(element) {
293
+ return shouldPromoteToProtected(this.__frequency(element), this.#promotionThreshold, element.weight, this.#protected.maxWeight);
294
+ }
295
+
296
+ /**
297
+ * The admission duel: does the candidate beat the victim on frequency?
298
+ * (contract in {@link winsAdmissionDuel})
299
+ * @param {CacheElementWTinylfu} candidate
300
+ * @param {CacheElementWTinylfu} victim
301
+ * @returns {boolean}
302
+ * @private
303
+ */
304
+ __admissionDuel(candidate, victim) {
305
+ return winsAdmissionDuel(this.__frequency(candidate), this.__frequency(victim));
306
+ }
307
+
308
+ /**
309
+ * Explicitly remove (invalidate) a key. Fires onRemoved, but not onEvicted (not capacity-driven).
310
+ * @param {Key} key
311
+ * @returns {boolean} true if the key was present and removed
312
+ */
313
+ remove(key) {
314
+ const element = this.#data.get(key);
315
+ if (element === undefined) {
316
+ return false;
317
+ }
318
+
319
+ this.__evict(element, element.weight, false); // manual removal: onRemoved only, not onEvicted
320
+ return true;
321
+ }
322
+
323
+ /**
324
+ * Remove all entries. Fires onRemoved for each (explicit removal, so not onEvicted).
325
+ * The frequency sketch is left intact (history decays on its own).
326
+ */
327
+ clear() {
328
+ for (const element of this.#data.values()) {
329
+ this.onRemoved.send2(element.key, element.value);
330
+ }
331
+
332
+ this.#data.clear();
333
+ this.#window.clear();
334
+ this.#trial.clear();
335
+ this.#protected.clear();
336
+ }
337
+
338
+ /**
339
+ * An element's weight: keyWeigher(key) + valueWeigher(value). Asserts each is a non-negative number.
340
+ * @param {Key} key
341
+ * @param {Value} value
342
+ * @returns {number}
343
+ * @private
344
+ */
345
+ __computeElementWeight(key, value) {
346
+ const keyWeight = this.#keyWeigher(key);
347
+
348
+ assert.greaterThanOrEqual(keyWeight, 0, 'keyWeight');
349
+
350
+ const valueWeight = this.#valueWeigher(value);
351
+
352
+ assert.greaterThanOrEqual(valueWeight, 0, 'valueWeight');
353
+
354
+ return keyWeight + valueWeight;
355
+ }
356
+
357
+ /**
358
+ * Total weight Main (Trial + Protected) is allowed to hold.
359
+ * @returns {number}
360
+ * @private
361
+ */
362
+ __mainCapacity() {
363
+ return mainCapacity(this.#maxWeight, this.#window.maxWeight);
364
+ }
365
+
366
+ /**
367
+ * Would Main exceed its budget once `extraWeight` is added, counting current occupancy?
368
+ * (contract in {@link exceedsMainWeight})
369
+ * @param {number} [extraWeight=0]
370
+ * @returns {boolean}
371
+ * @private
372
+ */
373
+ __exceedsMainWeight(extraWeight = 0) {
374
+ return exceedsMainWeight(this.#trial.weight, this.#protected.weight, extraWeight, this.__mainCapacity());
375
+ }
376
+
377
+ /**
378
+ * Could `weight` never fit Main, even with Trial fully evicted? (Protected's live weight is the
379
+ * immovable floor — contract in {@link cannotFitMain}.)
380
+ * @param {number} weight
381
+ * @returns {boolean}
382
+ * @private
383
+ */
384
+ __cannotFitMain(weight) {
385
+ return cannotFitMain(this.#protected.weight, weight, this.__mainCapacity());
386
+ }
387
+
388
+ /**
389
+ * Decide where an element of `weight` is routed on admission: Reject, Window, or Trial.
390
+ * (contract in {@link classifyAdmission})
391
+ * @param {number} weight
392
+ * @returns {number} one of AdmissionRoute
393
+ * @private
394
+ */
395
+ __classifyAdmission(weight) {
396
+ return classifyAdmission(weight, this.#protected.weight, this.#window.maxWeight, this.__mainCapacity());
397
+ }
398
+
399
+ /**
400
+ * Demote a Protected element to Trial. Pure relocation — the caller restores Main's budget.
401
+ * @param {CacheElementWTinylfu} element
402
+ * @private
403
+ */
404
+ __demoteToTrial(element) {
405
+ this.#protected.remove(element);
406
+ element.region = CacheRegionType.Trial;
407
+ this.#trial.add(element);
408
+ }
409
+
410
+ /**
411
+ * Refit a grown Protected element
412
+ * @param {CacheElementWTinylfu} element
413
+ * @param {number} newWeight
414
+ * @param {number} delta
415
+ * @returns {boolean} whether `element` survived
416
+ * @private
417
+ */
418
+ __refitGrownProtected(element, newWeight, delta) {
419
+ // Too big to ever fit Main, even alone → cannot keep it.
420
+ if (newWeight > this.__mainCapacity()) {
421
+ this.__evict(element);
422
+ return false;
423
+ }
424
+
425
+ element.weight = newWeight;
426
+ this.#protected.adjustWeight(delta);
427
+ this.#protected.touch(element); // keep the updated element (protect from demotion below)
428
+
429
+ while (this.#protected.wouldExceedCapacity()) {
430
+ this.__demoteToTrial(this.#protected.findEvictionVictim());
431
+ }
432
+ while (this.__exceedsMainWeight()) {
433
+ this.__evict(this.__trialEvictionVictim());
434
+ }
435
+
436
+ return true;
437
+ }
438
+
439
+ /**
440
+ * Refit a grown Trial element
441
+ * @param {CacheElementWTinylfu} element
442
+ * @param {number} newWeight
443
+ * @param {number} delta
444
+ * @returns {boolean} whether `element` survived
445
+ * @private
446
+ */
447
+ __refitGrownTrial(element, newWeight, delta) {
448
+ if (this.__cannotFitMain(newWeight)) {
449
+ this.__evict(element);
450
+ return false;
451
+ }
452
+
453
+ element.weight = newWeight;
454
+ this.#trial.adjustWeight(delta);
455
+ this.#trial.touch(element); // keep the updated element (protect from eviction below)
456
+
457
+ while (this.__exceedsMainWeight()) {
458
+ this.__evict(this.__trialEvictionVictim());
459
+ }
460
+
461
+ return true;
462
+ }
463
+
464
+ /**
465
+ * Refit a grown Window element
466
+ * @param {CacheElementWTinylfu} element
467
+ * @param {number} newWeight
468
+ * @param {number} delta
469
+ * @returns {boolean} whether `element` survived
470
+ * @private
471
+ */
472
+ __refitGrownWindow(element, newWeight, delta) {
473
+ const oldWeight = element.weight;
474
+
475
+ // Same placement decision as admission: the grown element either no longer fits Main (evict),
476
+ // outgrew Window (→ Trial), or still fits Window.
477
+ const route = this.__classifyAdmission(newWeight);
478
+
479
+ switch (route) {
480
+ case AdmissionRoute.REJECT:
481
+ this.__evict(element); // at oldWeight — weight not yet changed
482
+ return false;
483
+ case AdmissionRoute.TRIAL:
484
+ element.weight = newWeight;
485
+ return this.__nominateWindowToTrial(element, oldWeight);
486
+ case AdmissionRoute.WINDOW:
487
+ // keep it (MRU) and nominate older Window residents to Trial to absorb the growth
488
+ element.weight = newWeight;
489
+ this.#window.adjustWeight(delta);
490
+ this.#window.touch(element);
491
+ while (this.#window.wouldExceedCapacity()) {
492
+ this.__nominateWindowToTrial(this.#window.findEvictionVictim());
493
+ }
494
+ return true;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Route an update to an existing key: shrink/same-weight applies in place, a growth dispatches to the
500
+ * region's refit handler. On survival, commits the new value and counts the access.
501
+ * @param {CacheElementWTinylfu} element
502
+ * @param {Value} newValue
503
+ * @private
504
+ */
505
+ __routeUpdate(element, newValue) {
506
+ const newWeight = this.__computeElementWeight(element.key, newValue);
507
+ const delta = newWeight - element.weight;
508
+
509
+ // An update is an access — count its frequency up front, so it weighs into any
510
+ // admission duel the refit triggers.
511
+ this.__incrementSketch(element);
512
+
513
+ let survived;
514
+ if (delta <= 0) {
515
+ element.weight = newWeight;
516
+ this.__regionOf(element).adjustWeight(delta);
517
+ survived = true;
518
+ } else {
519
+ switch (element.region) {
520
+ case CacheRegionType.Window:
521
+ survived = this.__refitGrownWindow(element, newWeight, delta);
522
+ break;
523
+ case CacheRegionType.Trial:
524
+ survived = this.__refitGrownTrial(element, newWeight, delta);
525
+ break;
526
+ case CacheRegionType.Protected:
527
+ survived = this.__refitGrownProtected(element, newWeight, delta);
528
+ break;
529
+ default:
530
+ throw new Error("Invalid region found");
531
+ }
532
+ }
533
+
534
+ if (survived) {
535
+ element.value = newValue; // commit payload after the element's place is secured
536
+ this.__onAccess(element); // recency + promotion (frequency already counted above)
537
+ this.onSet.send2(element.key, newValue); // value replaced
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Relocate a resident into Trial (already removed from its prior region by the caller).
543
+ * @param {CacheElementWTinylfu} movedElement
544
+ * @private
545
+ */
546
+ __moveToTrial(movedElement) {
547
+ movedElement.region = CacheRegionType.Trial;
548
+ this.#trial.add(movedElement);
549
+ }
550
+
551
+ /**
552
+ * First-time admission: place a new element in `region`, register it in the lookup map,
553
+ * and record its frequency.
554
+ * @param {CacheElementWTinylfu} element
555
+ * @param {CacheRegionType} region
556
+ * @private
557
+ */
558
+ __admit(element, region) {
559
+ element.region = region;
560
+ this.__regionOf(element).add(element);
561
+ this.#data.set(element.key, element);
562
+ this.#sketch.ensureCapacity(this.#data.size); // grow the sketch to track the live working set
563
+ this.__incrementSketch(element);
564
+
565
+ this.onSet.send2(element.key, element.value);
566
+ }
567
+
568
+ /**
569
+ * Remove an element from its region's list and the lookup map. Always fires onRemoved; also fires
570
+ * onEvicted when capacity-driven (`evicted`, the default), but not for manual removal.
571
+ * @param {CacheElementWTinylfu} element
572
+ * @param {number} [weight=element.weight]
573
+ * @param {boolean} [evicted=true] true for capacity eviction, false for explicit removal
574
+ * @private
575
+ */
576
+ __evict(element, weight = element.weight, evicted = true) {
577
+ this.__regionOf(element).remove(element, weight);
578
+ this.#data.delete(element.key);
579
+
580
+ this.onRemoved.send2(element.key, element.value);
581
+ if (evicted) {
582
+ this.onEvicted.send2(element.key, element.value);
583
+ }
584
+ this.#releaseElement(element); // fully detached now — return it to the reuse pool
585
+ }
586
+
587
+ /**
588
+ * Return a fully-detached element to the reuse pool, clearing its references so it pins no user data.
589
+ * Caller MUST have already removed it from its region list and the lookup map, and fired any signals
590
+ * (which read key/value) — this nulls those out.
591
+ * @param {CacheElementWTinylfu} element
592
+ * @private
593
+ */
594
+ #releaseElement(element) {
595
+ element.key = null;
596
+ element.value = null;
597
+ element.next = null;
598
+ element.previous = null;
599
+
600
+ if (this.#elementPool.length < ELEMENT_POOL_MAX) {
601
+ this.#elementPool.push(element);
602
+ }
603
+ }
604
+
605
+ /**
606
+ * @param {CacheElementWTinylfu} element
607
+ * @returns {CacheRegionWTinylfu}
608
+ * @private
609
+ */
610
+ __regionOf(element) {
611
+ switch (element.region) {
612
+ case CacheRegionType.Window: return this.#window;
613
+ case CacheRegionType.Trial: return this.#trial;
614
+ case CacheRegionType.Protected: return this.#protected;
615
+ default: throw new Error("Invalid region found");
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Earn `element` a place in Trial: if Main is over budget, duel it against the Trial LRU victim and,
621
+ * on a win, evict Trial victims until it fits. Verdict only — the caller owns the element's data entry.
622
+ * @param {CacheElementWTinylfu} element
623
+ * @returns {boolean} true if `element` is admitted and space was made for it;
624
+ * false if it lost the admission duel.
625
+ * @private
626
+ */
627
+ __prepareTrialSpaceFor(element) {
628
+ if (!this.__exceedsMainWeight(element.weight)) {
629
+ return true;
630
+ }
631
+
632
+ if (!this.__admissionDuel(element, this.__trialEvictionVictim())) {
633
+ return false;
634
+ }
635
+
636
+ // by design: the candidate duels only the Trial LRU victim; a win may then cascade several
637
+ // evictions when weighted (the candidate proved it beats the coldest resident — we commit).
638
+ while (this.__exceedsMainWeight(element.weight)) {
639
+ this.__evict(this.__trialEvictionVictim());
640
+ }
641
+
642
+ return true;
643
+ }
644
+
645
+ /**
646
+ * @returns {CacheElementWTinylfu} Trial's LRU eviction victim
647
+ * @throws
648
+ * if Trial is empty while Main is over capacity (invariant: callers guarantee Trial holds enough evictable weight)
649
+ * @private
650
+ */
651
+ __trialEvictionVictim() {
652
+ const victim = this.#trial.findEvictionVictim();
653
+ if (!victim) {
654
+ throw new Error("Cache violation: no evictable victim in Trial while over capacity");
655
+ }
656
+ return victim;
657
+ }
658
+
659
+ /**
660
+ * Move a Window resident toward Trial through the admission duel; on a loss it is evicted
661
+ * (onRemoved + onEvicted).
662
+ * @param {CacheElementWTinylfu} candidate
663
+ * @param {number} [weight=candidate.weight] weight to reclaim from Window (override when it has changed)
664
+ * @returns {boolean} whether the candidate survived into Trial
665
+ * @private
666
+ */
667
+ __nominateWindowToTrial(candidate, weight = candidate.weight) {
668
+ this.#window.remove(candidate, weight);
669
+
670
+ if (this.__prepareTrialSpaceFor(candidate)) {
671
+ this.__moveToTrial(candidate);
672
+ return true;
673
+ } else {
674
+ // lost the duel — a resident is evicted
675
+ this.#data.delete(candidate.key);
676
+ this.onRemoved.send2(candidate.key, candidate.value);
677
+ this.onEvicted.send2(candidate.key, candidate.value);
678
+ this.#releaseElement(candidate);
679
+ return false;
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Evict Window's LRU residents (nominating each to Trial) until `incomingWeight` fits.
685
+ * @param {number} incomingWeight
686
+ * @private
687
+ */
688
+ __makeRoomInWindow(incomingWeight) {
689
+ while (this.#window.wouldExceedCapacity(incomingWeight)) {
690
+ const victim = this.#window.findEvictionVictim();
691
+ if (!victim) {
692
+ // invariant: over capacity implies at least one resident
693
+ throw new Error("Cache violation: no evictable victim in Window while over capacity");
694
+ }
695
+ this.__nominateWindowToTrial(victim);
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Admit a new element that is too big for Window straight into the Trial duel; on a loss it is
701
+ * rejected (onRejected) — it was never stored.
702
+ * @param {CacheElementWTinylfu} newElement
703
+ * @private
704
+ */
705
+ __nominateDirectToTrial(newElement) {
706
+ if (this.__prepareTrialSpaceFor(newElement)) {
707
+ this.__admit(newElement, CacheRegionType.Trial);
708
+ } else {
709
+ // lost the duel — newcomer never admitted
710
+ this.onRejected.send2(newElement.key, newElement.value);
711
+ this.#releaseElement(newElement);
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Route a brand-new element per classifyAdmission: reject outright, insert into Window, or send it to
717
+ * the Trial duel.
718
+ * @param {CacheElementWTinylfu} newElement
719
+ * @private
720
+ */
721
+ __routeAdmission(newElement) {
722
+ const weight = newElement.weight;
723
+
724
+ // Decision: where does this element go? (see classifyAdmission — TRIAL is a lower bar than
725
+ // Protected; an element too big for Protected is still a valid, pinned Trial resident.)
726
+ const route = this.__classifyAdmission(weight);
727
+
728
+ switch (route) {
729
+ case AdmissionRoute.REJECT:
730
+ this.onRejected.send2(newElement.key, newElement.value); // could never fit Main
731
+ return;
732
+ case AdmissionRoute.TRIAL:
733
+ this.__nominateDirectToTrial(newElement);
734
+ break;
735
+ case AdmissionRoute.WINDOW:
736
+ this.__makeRoomInWindow(weight);
737
+ this.__admit(newElement, CacheRegionType.Window);
738
+ break;
739
+ }
740
+ }
741
+
742
+ /**
743
+ * Insert a new entry, or update the value of an existing key. The main write entry point.
744
+ * @param {Key} key
745
+ * @param {Value} value
746
+ */
747
+ put(key, value){
748
+ let element = this.#data.get(key);
749
+
750
+ if (element !== undefined) {
751
+ this.__routeUpdate(element, value);
752
+ return;
753
+ }
754
+
755
+ // Reuse an evicted element if one is pooled, else allocate. Both arrive with next/previous null.
756
+ const pool = this.#elementPool;
757
+ element = pool.length > 0 ? pool.pop() : new CacheElementWTinylfu();
758
+ element.key = key;
759
+ element.keyHash = this.#keyHashFunction(key) | 0; // hash once; `| 0` keeps it an inline SMI (no boxing)
760
+ element.value = value;
761
+ element.region = CacheRegionType.Window;
762
+ element.weight = this.__computeElementWeight(key, value);
763
+
764
+ this.__routeAdmission(element);
765
+ }
766
+
767
+ /**
768
+ * Look up a key. A hit counts as an access (recency + frequency, and may trigger promotion).
769
+ * @param {Key} key
770
+ * @returns {Value|null} the cached value, or null if not found
771
+ */
772
+ get(key) {
773
+ const element = this.#data.get(key);
774
+
775
+ if (element === undefined) {
776
+ return null;
777
+ }
778
+
779
+ this.__touch(element); // a read is an access: frequency + recency + promotion
780
+ return element.value;
781
+ }
782
+
783
+ // ── diagnostics (inspection only; not part of the cache contract) ──
784
+
785
+ /**
786
+ * @internal Diagnostic for tests/monitoring: which region currently holds `key`, or null if absent.
787
+ * Returns the immutable {@link CacheRegionType} — it does NOT expose the element itself.
788
+ * @param {Key} key
789
+ * @returns {CacheRegionType|null}
790
+ */
791
+ inspectRegion(key) {
792
+ const element = this.#data.get(key);
793
+ return element === undefined ? null : element.region;
794
+ }
795
+
796
+ /**
797
+ * @internal Diagnostic for tests/monitoring: the live weight currently held by each region.
798
+ * @returns {{window: number, trial: number, protected: number}}
799
+ */
800
+ inspectWeights() {
801
+ return {
802
+ window: this.#window.weight,
803
+ trial: this.#trial.weight,
804
+ protected: this.#protected.weight,
805
+ };
806
+ }
807
+ }