@woosh/meep-engine 2.48.22 → 2.49.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 (230) hide show
  1. package/editor/ecs/component/editors/three/BufferGeometryEditor.js +1 -1
  2. package/editor/ecs/component/editors/three/MeshEditor.js +1 -1
  3. package/editor/tools/GridPaintTool.js +1 -1
  4. package/editor/tools/SelectionTool.js +1 -1
  5. package/editor/tools/paint/TerrainPaintTool.js +1 -1
  6. package/editor/view/GridPickCoordinateView.js +1 -1
  7. package/editor/view/library/MeshLibraryView.js +1 -1
  8. package/package.json +1 -1
  9. package/src/core/UUID.js +2 -0
  10. package/src/core/assert.js +4 -1
  11. package/src/core/binary/32BitEncoder.js +1 -1
  12. package/src/core/binary/ctz32.js +1 -1
  13. package/src/core/binary/operations/bitCount.spec.js +19 -0
  14. package/src/core/binary/uint82float.spec.js +7 -0
  15. package/src/core/bvh2/LeafNode.js +2 -2
  16. package/src/core/bvh2/Node.d.ts +1 -1
  17. package/src/core/bvh2/Node.js +1 -1
  18. package/src/core/bvh2/NodeValidator.js +1 -1
  19. package/src/core/bvh2/bvh3/EBBVHLeafProxy.js +3 -0
  20. package/src/core/bvh2/bvh3/query/compute_tight_near_far_clipping_planes.js +5 -4
  21. package/src/core/bvh2/serialization/deserializeBinaryNode.js +1 -1
  22. package/src/core/bvh2/serialization/deserializeBinaryNodeFromBinaryBuffer.js +2 -2
  23. package/src/core/bvh2/serialization/serializeBinaryNode.js +1 -1
  24. package/src/core/bvh2/serialization/serializeBinaryNodeToBinaryBuffer.js +2 -2
  25. package/src/core/bvh2/transform/RotationOptimizer.spec.js +161 -155
  26. package/src/core/cache/Cache.js +4 -2
  27. package/src/core/codegen/LineBuilder.js +15 -3
  28. package/src/core/codegen/LineBuilder.spec.js +7 -0
  29. package/src/core/collection/HashMap.js +491 -236
  30. package/src/core/collection/HashMap.spec.js +110 -1
  31. package/src/core/collection/array/{typedArrayToDataType.js → typed/typedArrayToDataType.js} +1 -1
  32. package/src/core/collection/array/weightedRandomFromArray.spec.js +20 -0
  33. package/src/core/collection/heap/Uint32Heap.js +1 -0
  34. package/src/core/debug/matchers/AnyOf.js +1 -2
  35. package/src/core/events/signal/Signal.js +7 -5
  36. package/src/core/events/signal/SignalBinding.js +56 -54
  37. package/src/core/events/signal/SignalFlags.js +7 -0
  38. package/src/core/function/Functions.js +1 -1
  39. package/src/core/geom/{Rectangle.js → 2d/Rectangle.js} +5 -5
  40. package/src/core/geom/{AABB2.js → 2d/aabb/AABB2.js} +10 -78
  41. package/src/core/geom/2d/aabb/aabb2_compute_center_from_multiple.js +19 -0
  42. package/src/core/geom/2d/aabb/aabb2_compute_overlap.js +42 -0
  43. package/src/core/geom/2d/aabb/aabb2_contains.js +23 -0
  44. package/src/core/geom/2d/aabb/aabb2_distance_sqr_to_point.js +25 -0
  45. package/src/core/geom/2d/aabb/aabb2_distance_to_point.js +17 -0
  46. package/src/core/geom/2d/aabb/aabb2_distance_to_point.spec.js +17 -0
  47. package/src/core/geom/2d/aabb/aabb2_overlap_exists.js +18 -0
  48. package/src/core/geom/2d/aabb/aabb2_signed_distance_sqr_to_point.js +45 -0
  49. package/src/core/geom/2d/aabb/aabb2_signed_distance_sqr_to_point.spec.js +40 -0
  50. package/src/core/geom/2d/aabb/aabb2_signed_distance_to_point.js +27 -0
  51. package/src/core/geom/2d/convex-hull/fixed_convex_hull_humus.js +4 -0
  52. package/src/core/geom/2d/quad-tree/PointQuadTree.js +3 -0
  53. package/src/core/geom/2d/quad-tree/QuadTreeDatum.js +1 -1
  54. package/src/core/geom/2d/quad-tree/QuadTreeNode.js +3 -3
  55. package/src/core/geom/2d/quad-tree/qt_collect_by_circle.js +6 -8
  56. package/src/core/geom/2d/quad-tree/qt_match_data_by_circle.js +3 -3
  57. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.js +8 -10
  58. package/src/core/{bvh2/aabb3 → geom/3d/aabb}/AABB3.d.ts +1 -1
  59. package/src/core/{bvh2/aabb3 → geom/3d/aabb}/AABB3.js +13 -13
  60. package/src/core/{bvh2/aabb3 → geom/3d/aabb}/AABB3.spec.js +1 -1
  61. package/src/core/geom/3d/aabb/aabb3_compute_plane_side.js +17 -15
  62. package/src/core/geom/3d/aabb/aabb3_compute_plane_side.spec.js +25 -0
  63. package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.js +1 -1
  64. package/src/core/geom/3d/aabb/aabb3_from_v3_array.js +3 -0
  65. package/src/core/geom/3d/aabb/aabb3_from_v3_array.spec.js +32 -0
  66. package/src/core/geom/3d/aabb/aabb3_intersects_aabb3.spec.js +115 -0
  67. package/src/core/geom/3d/aabb/aabb3_raycast.js +6 -1
  68. package/src/core/geom/3d/aabb/aabb3_signed_distance_sqr_to_point.js +13 -9
  69. package/src/core/{bvh2/aabb3 → geom/3d/aabb}/serializeAABB3Encoded_v0.js +6 -6
  70. package/src/core/geom/3d/{CircleMath.js → compute_circle_bounding_box.js} +1 -1
  71. package/src/core/geom/3d/decompose_matrix_4_array.js +18 -19
  72. package/src/core/geom/3d/frustum/frustum3_computeNearestPointToPoint.js +1 -1
  73. package/src/{engine/graphics/ecs/mesh-v2 → core/geom/3d/matrix}/allocate_transform_m4.js +1 -1
  74. package/src/core/geom/3d/normal/hemioct/decode_hemioct_to_unit.js +26 -0
  75. package/src/core/geom/3d/normal/hemioct/encode_unit3_hemioct.js +0 -26
  76. package/src/core/geom/3d/normal/hemioct/unit_hemioct.spec.js +2 -1
  77. package/src/core/geom/3d/plane/computePlaneLineIntersection.js +51 -0
  78. package/src/core/geom/3d/plane/computePlanePlaneIntersection.js +77 -0
  79. package/src/core/geom/3d/plane/computePlaneRayIntersection.js +55 -0
  80. package/src/core/geom/3d/plane/plane3_computeLineSegmentIntersection.js +50 -0
  81. package/src/core/geom/3d/plane/planeRayIntersection.js +14 -0
  82. package/src/core/geom/3d/{tetrahedra/in_sphere_fast.js → sphere/in_sphere3d_fast.js} +1 -1
  83. package/src/core/geom/3d/{tetrahedra/in_sphere_robust.js → sphere/in_sphere3d_robust.js} +1 -1
  84. package/src/core/geom/3d/sphere/sphere_array_intersects_point.js +2 -2
  85. package/src/core/geom/3d/sphere/{sphereIntersectsPoint.js → sphere_intersects_point.js} +7 -4
  86. package/src/core/geom/3d/sphere/sphere_intersects_point.spec.js +134 -0
  87. package/src/core/geom/3d/sphere/sphere_intersects_ray.spec.js +49 -0
  88. package/src/core/geom/3d/sphere/sphere_radius_sqr_from_v3_array_transformed.js +11 -7
  89. package/src/core/geom/3d/tetrahedra/delaunay/{debug_validate_mesh.js → debug/debug_validate_mesh.js} +1 -1
  90. package/src/core/geom/3d/tetrahedra/delaunay/{push_boundary_with_validation.js → debug/push_boundary_with_validation.js} +1 -1
  91. package/src/core/geom/3d/tetrahedra/delaunay/{validate_cavity_boundary.js → debug/validate_cavity_boundary.js} +2 -2
  92. package/src/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity.js +2 -2
  93. package/src/core/geom/3d/triangle/computeTriangleRayIntersection.js +0 -164
  94. package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js +87 -0
  95. package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentricEdge.js +81 -0
  96. package/src/core/geom/{GeometryMath.js → 3d/triangle/rayTriangleIntersection.js} +7 -3
  97. package/src/core/geom/ConicRay.js +160 -152
  98. package/src/core/geom/Matrix4.js +2 -0
  99. package/src/core/geom/Quaternion.js +19 -1
  100. package/src/core/geom/packing/max-rect/MaxRectangles.js +5 -215
  101. package/src/core/geom/packing/max-rect/cost/costByBestShortSide.js +11 -0
  102. package/src/core/geom/packing/max-rect/cost/costByRemainingArea.js +14 -0
  103. package/src/core/geom/packing/max-rect/cutArea.js +79 -0
  104. package/src/core/geom/packing/max-rect/findBestContainer.js +58 -0
  105. package/src/core/geom/packing/max-rect/packOneBox.js +49 -0
  106. package/src/core/geom/v3_dot.js +1 -1
  107. package/src/core/graph/GraphUtils.js +1 -1
  108. package/src/core/graph/build_face_graph_from_mesh.js +1 -3
  109. package/src/core/graph/layout/CircleLayout.js +3 -3
  110. package/src/core/graph/layout/{BoxLayouter.js → box/BoxLayouter.js} +6 -50
  111. package/src/core/graph/layout/box/applyCentralGravityAABB2.js +29 -0
  112. package/src/core/json/resolvePath.spec.js +14 -0
  113. package/src/core/land/reactive/{compiler/ReactiveCompiler.spec.js → compileReactiveExpression.spec.js} +17 -17
  114. package/src/core/math/random/MersenneTwister.spec.js +19 -0
  115. package/src/core/math/random/randomGaussian.spec.js +9 -0
  116. package/src/core/math/statistics/computeStatisticalMean.js +2 -2
  117. package/src/core/model/node-graph/visual/NodeVisualData.js +1 -1
  118. package/src/core/model/reactive/model/arithmetic/ReactiveAdd.js +1 -1
  119. package/src/core/model/reactive/model/arithmetic/ReactiveDivide.js +3 -1
  120. package/src/core/model/reactive/model/arithmetic/ReactiveMultiply.js +1 -1
  121. package/src/core/model/reactive/model/arithmetic/ReactiveNegate.js +3 -1
  122. package/src/core/model/reactive/model/arithmetic/ReactiveSubtract.js +1 -1
  123. package/src/core/model/reactive/model/comparative/ReactiveEquals.js +1 -1
  124. package/src/core/model/reactive/model/comparative/ReactiveGreaterThan.js +3 -1
  125. package/src/core/model/reactive/model/comparative/ReactiveGreaterThanOrEqual.js +3 -1
  126. package/src/core/model/reactive/model/comparative/ReactiveLessThan.js +3 -1
  127. package/src/core/model/reactive/model/comparative/ReactiveLessThanOrEqual.js +3 -1
  128. package/src/core/model/reactive/model/comparative/ReactiveNotEquals.js +1 -1
  129. package/src/core/model/reactive/model/logic/ReactiveAnd.js +1 -1
  130. package/src/core/model/reactive/model/logic/ReactiveNot.js +3 -1
  131. package/src/core/model/reactive/model/logic/ReactiveOr.js +1 -1
  132. package/src/core/primitives/numbers/computeHashFloat.spec.js +7 -0
  133. package/src/core/process/task/util/iteratorTask.js +3 -1
  134. package/src/engine/animation/curve/AnimationCurve.js +34 -5
  135. package/src/engine/animation/curve/AnimationCurve.spec.js +100 -0
  136. package/src/engine/asset/AssetTransformer.js +1 -0
  137. package/src/engine/asset/loaders/GLTFAssetLoader.js +1 -1
  138. package/src/engine/asset/loaders/image/ImageRGBADataLoader.js +2 -2
  139. package/src/engine/asset/preloader/Preloader.js +3 -3
  140. package/src/engine/computeStridedIntegerArrayHash.js +4 -2
  141. package/src/engine/ecs/components/Renderable.d.ts +2 -2
  142. package/src/engine/ecs/components/Renderable.js +1 -1
  143. package/src/{ecs → engine/ecs}/grid/pick.js +4 -4
  144. package/src/engine/ecs/gui/position/ViewportPositionSystem.js +1 -1
  145. package/src/engine/ecs/parent/entity_node_compute_bounding_box.js +1 -1
  146. package/src/engine/ecs/storage/binary/collection/BinaryCollectionSerializer.js +1 -18
  147. package/src/engine/ecs/systems/MotionSystem.js +7 -1
  148. package/src/engine/ecs/systems/SynchronizePositionSystem.js +8 -2
  149. package/src/engine/ecs/terrain/tiles/TerrainTile.js +1 -1
  150. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +2 -2
  151. package/src/engine/ecs/tooltip/TooltipComponentSystem.js +1 -1
  152. package/src/engine/ecs/transform/Transform.js +1 -1
  153. package/src/engine/graphics/camera/makeScreenScissorFrustum.d.ts +1 -1
  154. package/src/engine/graphics/camera/makeScreenScissorFrustum.js +3 -3
  155. package/src/engine/graphics/camera/testClippingPlaneComputation.js +13 -13
  156. package/src/engine/graphics/ecs/camera/Camera.js +1 -1
  157. package/src/engine/graphics/ecs/light/LightSystem.js +1 -1
  158. package/src/engine/graphics/ecs/mesh/Mesh.d.ts +1 -1
  159. package/src/engine/graphics/ecs/mesh/Mesh.js +1 -1
  160. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.d.ts +1 -1
  161. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js +1 -1
  162. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.d.ts +1 -1
  163. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js +1 -1
  164. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js +9 -0
  165. package/src/engine/graphics/ecs/mesh-v2/sg_hierarchy_compute_bounding_box_via_parent_entity.d.ts +1 -1
  166. package/src/engine/graphics/geometry/MikkT/MikkTSpace.js +1 -1
  167. package/src/engine/graphics/geometry/MikkT/STSpace.js +1 -1
  168. package/src/engine/graphics/geometry/bvh/buffered/BVHGeometryRaycaster.js +1 -1
  169. package/src/engine/graphics/geometry/skining/computeSkinnedMeshBoundingVolumes.js +1 -1
  170. package/src/engine/graphics/material/optimization/MaterialOptimizationContext.js +1 -1
  171. package/src/engine/graphics/particles/particular/engine/MovingBoundingBox.js +1 -1
  172. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js +1 -1
  173. package/src/engine/graphics/particles/particular/engine/emitter/ParticleLayer.js +1 -1
  174. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTable.js +1 -0
  175. package/src/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +1 -1
  176. package/src/engine/graphics/postprocess/threejs/postprocessing/TexturePass.js +2 -2
  177. package/src/engine/graphics/render/forward_plus/debug/createScreenGrid.js +1 -1
  178. package/src/engine/graphics/render/view/CameraView.js +1 -1
  179. package/src/engine/graphics/sh3/path_tracer/GeometryBVHBatched.js +2 -2
  180. package/src/engine/graphics/texture/atlas/AtlasPatch.js +2 -2
  181. package/src/engine/graphics/texture/atlas/TextureAtlas.spec.js +2 -2
  182. package/src/engine/graphics/texture/sampler/Sampler2D.js +1 -1
  183. package/src/engine/graphics/texture/sampler/sampler2d_compute_texel_value_conversion_scale_to_uint8.js +1 -1
  184. package/src/engine/graphics/util/makeMeshPreviewScene.js +1 -1
  185. package/src/engine/graphics/util/renderObjectToSampler2D.js +1 -1
  186. package/src/engine/intelligence/behavior/Behavior.spec.js +15 -0
  187. package/src/engine/intelligence/mcts/MoveEdge.js +1 -1
  188. package/src/engine/reference/v1/ReferenceManager.js +3 -0
  189. package/src/engine/reference/v2/Reference.js +33 -37
  190. package/src/engine/sound/sopra/README.md +6 -0
  191. package/src/engine/ui/tiles2d/computeTileGridMove.js +3 -2
  192. package/src/engine/ui/tiles2d/computeTileGridMove.spec.js +1 -1
  193. package/src/generation/automata/CaveGeneratorCellularAutomata.js +10 -7
  194. package/src/generation/automata/CaveGeneratorCellularAutomata.spec.js +12 -0
  195. package/src/generation/automata/CellularAutomata.js +5 -4
  196. package/src/generation/filtering/numeric/complex/CellFilterGaussianBlur.js +25 -9
  197. package/src/generation/theme/AreaMask.js +1 -1
  198. package/src/view/View.js +1 -1
  199. package/src/view/elements/progress/RectangularPieProgressView.js +1 -1
  200. package/src/view/minimap/Minimap.js +1 -1
  201. package/src/view/minimap/dom/MinimapCameraView.js +1 -1
  202. package/src/view/minimap/gl/MinimapFogOfWar.js +1 -1
  203. package/src/view/tooltip/DomTooltipObserver.js +1 -1
  204. package/src/view/tooltip/TooltipManager.js +1 -1
  205. package/src/view/tooltip/TooltipView.js +1 -1
  206. package/src/view/util/DomSizeObserver.js +2 -2
  207. package/src/core/geom/2d/AABB2Math.js +0 -40
  208. package/src/core/geom/2d/AABB2Math.spec.js +0 -17
  209. package/src/core/geom/Plane.js +0 -250
  210. package/src/core/land/reactive/ReactiveLexer.js +0 -158
  211. package/src/core/land/reactive/ReactiveLexer.ts +0 -181
  212. package/src/core/land/reactive/ReactiveListener.ts +0 -323
  213. package/src/core/land/reactive/ReactiveParser.js +0 -1573
  214. package/src/core/land/reactive/ReactiveParser.ts +0 -1776
  215. package/src/core/land/reactive/ReactiveVisitor.js +0 -1
  216. package/src/core/land/reactive/ReactiveVisitor.ts +0 -218
  217. package/src/core/land/reactive/compiler/ReactiveCompiler.js +0 -350
  218. package/src/core/land/reactive/compiler/ReactiveNearlyCompiler.js +0 -166
  219. package/src/core/land/reactive/compiler/ReactiveParser.js +0 -34
  220. package/src/core/land/reactive/nearley/ReactiveNearley.js +0 -187
  221. /package/src/core/geom/{LineSegment2.js → 2d/LineSegment2.js} +0 -0
  222. /package/src/core/geom/{Rectangle.spec.js → 2d/Rectangle.spec.js} +0 -0
  223. /package/src/core/geom/{AABB2.d.ts → 2d/aabb/AABB2.d.ts} +0 -0
  224. /package/src/core/geom/{AABB2.spec.js → 2d/aabb/AABB2.spec.js} +0 -0
  225. /package/src/core/{bvh2/aabb3 → geom/3d/aabb}/deserializeAABB3.js +0 -0
  226. /package/src/core/{bvh2/aabb3 → geom/3d/aabb}/deserializeAABB3Encoded_v0.js +0 -0
  227. /package/src/core/{bvh2/aabb3 → geom/3d/aabb}/deserializeAABB3Quantized16Uint.js +0 -0
  228. /package/src/core/{bvh2/aabb3 → geom/3d/aabb}/serializeAABB3.js +0 -0
  229. /package/src/core/{bvh2/aabb3 → geom/3d/aabb}/serializeAABB3Quantized16Uint.js +0 -0
  230. /package/src/{engine/graphics/ecs/mesh-v2 → core/geom/3d/vector}/allocate_v3.js +0 -0
@@ -1,6 +1,38 @@
1
1
  import { assert } from "../assert.js";
2
2
  import { invokeObjectEquals } from "../model/object/invokeObjectEquals.js";
3
3
  import { invokeObjectHash } from "../model/object/invokeObjectHash.js";
4
+ import { isPowerOfTwo } from "../math/isPowerOrTwo.js";
5
+ import { ctz32 } from "../binary/ctz32.js";
6
+ import { ceilPowerOfTwo } from "../binary/operations/ceilPowerOfTwo.js";
7
+ import { UintArrayForCount } from "./array/typed/uint_array_for_count.js";
8
+ import { array_copy } from "./array/copyArray.js";
9
+ import { min2 } from "../math/min2.js";
10
+ import { arraySwapElements } from "./array/arraySwapElements.js";
11
+
12
+ /*
13
+ * Heavily inspired by ruby's "new" (circa 2016) hash table implementation
14
+ * @see https://github.com/ruby/ruby/blob/82995d4615e993f1d13f3e826b93fbd65c47e19e/st.c
15
+ * @see https://blog.heroku.com/ruby-2-4-features-hashes-integers-rounding#hash-changes
16
+ */
17
+
18
+ /**
19
+ * Formula: Xn+1 = (a * Xn + c ) % m
20
+ *
21
+ * According the Hull-Dobell theorem a generator
22
+ * "Xnext = (a*Xprev + c) mod m" is a full cycle generator if and only if
23
+ * o m and c are relatively prime
24
+ * o a-1 is divisible by all prime factors of m
25
+ * o a-1 is divisible by 4 if m is divisible by 4.
26
+ * For our case a is 5, c is 1, and m is a power of two.
27
+ * @param index
28
+ * @param mask used to execute division, this is a number equal to (2^K - 1) where 2^K is the size of the "bins" array
29
+ * @see https://en.wikipedia.org/wiki/Linear_congruential_generator
30
+ */
31
+ export function generate_next_linear_congruential_index(index, mask) {
32
+ const index5 = (index << 2) + index; // this is just a faster version of index*5
33
+
34
+ return (index5 + 1) & mask;
35
+ }
4
36
 
5
37
 
6
38
  /**
@@ -40,28 +72,151 @@ class MapEntry {
40
72
  * @readonly
41
73
  * @type {number}
42
74
  */
43
- const DEFAULT_INITIAL_CAPACITY = 16;
75
+ const DEFAULT_INITIAL_CAPACITY_POWER = 4;
44
76
 
45
77
  /**
46
78
  * @readonly
47
79
  * @type {number}
48
80
  */
49
- const DEFAULT_LOAD_FACTOR = 0.75;
81
+ const DEFAULT_INITIAL_CAPACITY = 2 ** DEFAULT_INITIAL_CAPACITY_POWER;
50
82
 
51
83
  /**
52
- * Multiply previous size by this number when growing the table
53
84
  * @readonly
54
85
  * @type {number}
55
86
  */
56
- const GROWTH_FACTOR = 2;
87
+ const DEFAULT_LOAD_FACTOR = 0.75;
88
+
89
+
90
+ /**
91
+ * Reserved value that we store in "bins" array to indicate an empty slot
92
+ * @type {number}
93
+ */
94
+ const BIN_RESERVED_VALUE_EMPTY = 0;
95
+
96
+ /**
97
+ * Reserved value that we store in "bins" array to indicate a deleted entry
98
+ * @type {number}
99
+ */
100
+ const BIN_RESERVED_VALUE_DELETED = 1;
101
+
102
+ /**
103
+ * Real index offset into entry array
104
+ * @type {number}
105
+ */
106
+ const ENTRY_BASE = 2;
107
+
108
+ /**
109
+ * Special hash value used to indicate "dead" entries
110
+ * If key hashes to this value - we will replace it
111
+ * @type {number}
112
+ */
113
+ const RESERVED_HASH = 4294967295;
114
+ /**
115
+ * Used as a replacement for reserved hash
116
+ * @type {number}
117
+ */
118
+ const RESERVED_HASH_SUBSTITUTE = 0;
119
+
120
+ /**
121
+ *
122
+ * @type {number}
123
+ */
124
+ const UNDEFINED_BIN_INDEX = ~0;
125
+
126
+
127
+ /**
128
+ * @template K,V
129
+ * @param {MapEntry<K,V>} record
130
+ * @param {number} hash
131
+ * @param {K} key
132
+ * @param {function(a:K,b:K):boolean} equality_op
133
+ */
134
+ function entry_equality_check(record, hash, key, equality_op) {
135
+ if (record.hash !== hash) {
136
+ return false;
137
+ }
138
+
139
+ if (record.key === key) {
140
+ return true;
141
+ }
142
+
143
+ const result = equality_op(record.key, key);
144
+
145
+ assert.isBoolean(result, `result(a=${record.key},b=${key})`);
146
+
147
+ return result;
148
+ }
149
+
150
+
151
+ const EMPTY_BINS = new Uint32Array(0);
57
152
 
58
153
  /**
59
154
  * Implements part of {@link Map} interface
155
+ * NOTE: as with any hash-based data structure, keys are assumed to be immutable. If you modified keys after inserting them into the map, it will cause the hash table to become invalid. You can fix this by forcing rehashing, but generally - try to avoid changing keys in the first place.
156
+ *
60
157
  * @copyright Alex Goldring (c) 2023
61
158
  * @template K,V
62
159
  * @extends Map<K,V>
63
160
  */
64
161
  export class HashMap {
162
+ /**
163
+ * Index pointers to entries array,
164
+ * number of bins is always power or two
165
+ * @type {Uint32Array}
166
+ */
167
+ #bins = EMPTY_BINS;
168
+
169
+ /**
170
+ * Note that dead entries are marked as such with a special reserved hash values, so records can be reused for new entries
171
+ * @type {MapEntry<K,V>[]}
172
+ */
173
+ #entries = new Array(0);
174
+
175
+ /**
176
+ * Pointer to the end of allocated entries segment
177
+ * @type {number}
178
+ */
179
+ #entries_bound = 0;
180
+
181
+ /**
182
+ *
183
+ * @type {number}
184
+ */
185
+ #entries_start = 0;
186
+
187
+ /**
188
+ * number of records in the map
189
+ * @type {number}
190
+ */
191
+ #size = 0;
192
+
193
+ #bin_count = 0;
194
+
195
+ /**
196
+ * Always exactly half of the number of bins
197
+ * @type {number}
198
+ */
199
+ #entries_allocated_count = 0;
200
+
201
+ #bin_count_power_of_two = 0;
202
+
203
+ #entries_count_power_of_two = 0;
204
+
205
+ /**
206
+ * Mask used to map from hash to a bin index
207
+ * @type {number}
208
+ */
209
+ #bin_count_mask = 0;
210
+
211
+ /**
212
+ * How full the table can get before number of buckets is increased
213
+ * @type {number}
214
+ * @private
215
+ */
216
+ #load_factor = DEFAULT_LOAD_FACTOR;
217
+
218
+ #version = 0;
219
+
65
220
  /**
66
221
  * @template K, V
67
222
  * @param {function(K):number} [keyHashFunction]
@@ -81,45 +236,31 @@ export class HashMap {
81
236
  assert.isNonNegativeInteger(capacity, 'capacity');
82
237
 
83
238
  assert.isNumber(loadFactor, 'loadFactor');
239
+ assert.notNaN(loadFactor, 'loadFactor');
84
240
  assert.greaterThan(loadFactor, 0, 'loadFactor must be > 0');
85
241
 
86
242
  /**
87
243
  *
88
244
  * @type {function(K): number}
245
+ * @readonly
246
+ * @private
89
247
  */
90
248
  this.keyHashFunction = keyHashFunction;
91
249
  /**
92
250
  *
93
251
  * @type {function(K, K): boolean}
252
+ * @readonly
253
+ * @private
94
254
  */
95
255
  this.keyEqualityFunction = keyEqualityFunction;
96
256
 
97
- /**
98
- * Sparse array of map entries
99
- * @type {MapEntry[][]}
100
- */
101
- this.records = [];
102
-
103
- /**
104
- *
105
- * @type {number}
106
- */
107
- this.size = 0;
257
+ this.#load_factor = loadFactor;
108
258
 
259
+ this.#setBinCount(ceilPowerOfTwo(capacity));
260
+ }
109
261
 
110
- /**
111
- *
112
- * @type {number}
113
- * @private
114
- */
115
- this.__bucket_count = capacity;
116
-
117
- /**
118
- * How full the table can get before number of buckets is increased
119
- * @type {number}
120
- * @private
121
- */
122
- this.__load_factor = loadFactor;
262
+ get size() {
263
+ return this.#size;
123
264
  }
124
265
 
125
266
  /**
@@ -127,61 +268,43 @@ export class HashMap {
127
268
  * @returns {number}
128
269
  */
129
270
  getCurrentLoad() {
130
- return this.size / this.__bucket_count;
271
+ return this.#size / this.#bin_count;
131
272
  }
132
273
 
133
274
  /**
134
- * Proportion of buckets that are occupied with at least 1 record
135
- * @returns {number}
275
+ * Note: this method is not intended for public use
276
+ * @param {number} count
136
277
  */
137
- getOccupancy() {
138
- let occupied_bucket_count = 0;
139
-
140
- const c = this.__bucket_count;
141
- for (let i = 0; i < c; i++) {
142
- const bucket = this.records[i];
278
+ #setBinCount(count) {
279
+ assert.greaterThanOrEqual(count, 1, 'bucket count must be at least 1');
280
+ assert.isNonNegativeInteger(count, 'count');
281
+ assert.equal(isPowerOfTwo(count), true, `count must be a power of two, instead was ${count}`);
143
282
 
144
- if (bucket !== undefined && bucket.length > 0) {
145
- occupied_bucket_count++;
146
- }
283
+ if (count < this.#size) {
284
+ throw new Error(`count must be at least equal to must of records in the map (=${this.#size}), instead was ${count}`);
147
285
  }
148
286
 
149
- return occupied_bucket_count / this.__bucket_count;
150
- }
151
287
 
152
- /**
153
- * Number of collisions, that is elements beyond first that occupy the same bucket in the table
154
- * @returns {number}
155
- */
156
- getCollisionCount() {
157
- let count = 0;
288
+ this.#entries_count_power_of_two = ctz32(count);
289
+ this.#bin_count_power_of_two = this.#entries_count_power_of_two + 1;
158
290
 
159
- const c = this.__bucket_count;
160
- for (let i = 0; i < c; i++) {
161
- const bucket = this.records[i];
291
+ this.#bin_count = 2 ** this.#bin_count_power_of_two;
292
+ this.#bin_count_mask = this.#bin_count - 1;
162
293
 
163
- if (bucket !== undefined) {
164
- const occupancy = bucket.length;
165
- const collisions = occupancy - 1;
166
- if (collisions > 0) {
167
- count += collisions;
168
- }
169
- }
170
- }
294
+ this.#entries_allocated_count = 2 ** this.#entries_count_power_of_two;
171
295
 
172
- return count;
173
- }
296
+ const BinsArray = UintArrayForCount(this.#entries_allocated_count + ENTRY_BASE);
174
297
 
175
- /**
176
- *
177
- * @param {number} count
178
- */
179
- setBucketCount(count) {
180
- assert.greaterThanOrEqual(count, 1, 'bucket count must be at least 1');
181
- assert.isNonNegativeInteger(count, 'count');
298
+ this.#bins = new BinsArray(this.#bin_count);
299
+
300
+ const new_entries = new Array(this.#entries_allocated_count);
301
+ const old_entries = this.#entries;
302
+
303
+ this.#entries = new_entries;
304
+
305
+ array_copy(old_entries, 0, new_entries, 0, min2(old_entries.length, this.#entries_allocated_count));
182
306
 
183
- this.__bucket_count = count;
184
- if (this.size > 0) {
307
+ if (this.#size > 0) {
185
308
  // re-hash
186
309
  this.rebuild();
187
310
  }
@@ -193,7 +316,7 @@ export class HashMap {
193
316
  * @returns {number}
194
317
  * @private
195
318
  */
196
- __compute_bucket_index(hash) {
319
+ #compute_bin_index(hash) {
197
320
  assert.isInteger(hash, 'hash');
198
321
 
199
322
  // mix the input hash to minimize potential impact of poor hash function spread
@@ -202,76 +325,141 @@ export class HashMap {
202
325
  // force index to unsigned integer
203
326
  const index = mixed_hash >>> 0;
204
327
 
205
- return index % this.__bucket_count;
328
+ return index & this.#bin_count_mask;
206
329
  }
207
330
 
208
331
  /**
209
332
  *
210
333
  * @param {K} key
211
- * @param {V} value
334
+ * @return {number}
212
335
  */
213
- set(key, value) {
214
- const raw_hash = this.keyHashFunction(key);
215
- const bucket_index = this.__compute_bucket_index(raw_hash);
336
+ #build_key_hash(key) {
337
+ const original = this.keyHashFunction(key);
216
338
 
217
- assert.isFiniteNumber(bucket_index, 'hash');
339
+ return original === RESERVED_HASH ? RESERVED_HASH_SUBSTITUTE : original;
340
+ }
218
341
 
219
- const records = this.records;
342
+ /**
343
+ *
344
+ * @param {K} k
345
+ * @param {V} v
346
+ * @param {number} hash
347
+ * @return {number}
348
+ */
349
+ #allocate_entry(k, v, hash) {
220
350
 
221
- let bucket = records[bucket_index];
351
+ const i = this.#entries_bound;
222
352
 
223
- if (bucket === undefined) {
353
+ this.#entries_bound++;
224
354
 
225
- // bucket for this value doesn't exist
355
+ if (this.#entries[i] !== undefined) {
356
+ const entry = this.#entries[i];
357
+ entry.hash = hash;
358
+ entry.key = k;
359
+ entry.value = v;
360
+ } else {
361
+ this.#entries[i] = new MapEntry(k, v, hash);
362
+ }
226
363
 
227
- bucket = [];
364
+ return i;
365
+ }
228
366
 
229
- records[bucket_index] = bucket;
367
+ /**
368
+ *
369
+ * @param {MapEntry<K,V>} entry
370
+ */
371
+ #deallocate(entry) {
230
372
 
231
- } else {
373
+ // clear out entry to allow values/keys to be garbage collected
374
+ entry.key = null;
375
+ entry.value = null;
376
+ entry.hash = RESERVED_HASH; // mark as dead via hash
232
377
 
233
- // bucket exists
378
+ }
234
379
 
235
- const bucketSize = bucket.length;
380
+ #rebuild_if_necessary() {
381
+ if (this.#entries_bound === this.#entries_allocated_count) {
382
+ if (this.#size === this.#entries_allocated_count) {
383
+ // used up all allocated entries
384
+ // bin count must always be larger than end of the entries table
385
+ this.#grow();
386
+ } else {
387
+ // exhausted entries array, perform compaction
388
+ this.rebuild();
389
+ }
390
+ }
391
+ }
236
392
 
237
- //check if key already exists
238
- for (let i = 0; i < bucketSize; i++) {
239
- const entry = bucket[i];
393
+ /**
394
+ *
395
+ * @param {K} key
396
+ * @param {V} value
397
+ */
398
+ set(key, value) {
399
+ this.#rebuild_if_necessary();
240
400
 
241
- const entryKey = entry.key;
401
+ const raw_hash = this.#build_key_hash(key);
402
+ let bin_index = this.#compute_bin_index(raw_hash);
403
+ assert.isFiniteNumber(bin_index, 'hash');
242
404
 
405
+ let first_deleted_bin_index = UNDEFINED_BIN_INDEX;
243
406
 
244
- if (
245
- entry.hash === raw_hash
246
- && (entryKey === key || this.keyEqualityFunction(entryKey, key))
247
- ) {
407
+ for (; ;) {
408
+ const bin = this.#bins[bin_index];
248
409
 
249
- // found record with matching key, replace the value
410
+ if (bin > BIN_RESERVED_VALUE_DELETED) {
411
+ // bin is occupied
250
412
 
251
- entry.value = value;
413
+ // check if it's the entry that we're looking for
414
+ const entry = this.#entries[bin - ENTRY_BASE];
252
415
 
253
- // done, return
416
+ if (entry_equality_check(entry, raw_hash, key, this.keyEqualityFunction)) {
417
+ // found the right entry
418
+ entry.value = value;
254
419
  return;
420
+ }
421
+ } else if (bin === BIN_RESERVED_VALUE_EMPTY) {
422
+ // bin is empty
255
423
 
424
+ if (first_deleted_bin_index !== UNDEFINED_BIN_INDEX) {
425
+ // reused bin of deleted entity
426
+ bin_index = first_deleted_bin_index;
256
427
  }
257
428
 
429
+ const entry_index = this.#allocate_entry(key, value, raw_hash);
430
+
431
+ assert.defined(this.#entries[entry_index], 'entry');
432
+
433
+ assert.equal(this.#entries[entry_index].hash, raw_hash, 'entry.hash');
434
+ assert.equal(this.#entries[entry_index].value, value, 'entry.value');
435
+ assert.equal(this.#entries[entry_index].key, key, 'entry.key');
436
+
437
+ this.#bins[bin_index] = entry_index + ENTRY_BASE;
438
+
439
+ break;
440
+
441
+ } else if (first_deleted_bin_index === UNDEFINED_BIN_INDEX) {
442
+ // bin is deleted
443
+ first_deleted_bin_index = bin_index;
444
+
258
445
  }
259
446
 
260
- }
447
+ // perform secondary hashing
448
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
261
449
 
262
- bucket.push(new MapEntry(key, value, raw_hash));
450
+ }
263
451
 
264
- const old_size = this.size;
452
+ const old_size = this.#size;
265
453
  const new_size = old_size + 1;
266
- this.size = new_size;
454
+ this.#size = new_size;
267
455
 
268
456
  // compute actual current load
269
- const bucket_count = this.__bucket_count;
457
+ const bucket_count = this.#bin_count;
270
458
  const load = new_size / bucket_count;
271
459
 
272
- if (load > this.__load_factor) {
460
+ if (load > this.#load_factor) {
273
461
  // current load is too high, increase table size
274
- this.setBucketCount(bucket_count * GROWTH_FACTOR);
462
+ this.#grow();
275
463
  }
276
464
  }
277
465
 
@@ -281,33 +469,34 @@ export class HashMap {
281
469
  * @returns {V|undefined}
282
470
  */
283
471
  get(key) {
284
- const raw_hash = this.keyHashFunction(key);
285
- const bucket_index = this.__compute_bucket_index(raw_hash);
286
- const bucket = this.records[bucket_index];
472
+ const raw_hash = this.#build_key_hash(key);
287
473
 
288
- if (bucket !== undefined) {
474
+ let bin_index = this.#compute_bin_index(raw_hash);
289
475
 
290
- const bucketSize = bucket.length;
476
+ for (; ;) {
477
+ const bin = this.#bins[bin_index];
291
478
 
292
- //check if key already exists
293
- for (let i = 0; i < bucketSize; i++) {
294
- const entry = bucket[i];
479
+ if (bin > BIN_RESERVED_VALUE_DELETED) {
480
+ // bin is occupied
295
481
 
296
- const entryKey = entry.key;
482
+ // check if the entry is what we're looking for
483
+ const entry = this.#entries[bin - ENTRY_BASE];
297
484
 
298
- if (
299
- entry.hash === raw_hash
300
- && (entryKey === key || this.keyEqualityFunction(entryKey, key))
301
- ) {
485
+ if (entry_equality_check(entry, raw_hash, key, this.keyEqualityFunction)) {
486
+ // found the right entry
302
487
  return entry.value;
303
-
304
488
  }
489
+
490
+ } else if (bin === BIN_RESERVED_VALUE_EMPTY) {
491
+ // bin is empty
492
+ return undefined;
305
493
  }
306
494
 
495
+ // perform secondary hashing
496
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
497
+
307
498
  }
308
499
 
309
- //not found
310
- return undefined;
311
500
  }
312
501
 
313
502
  /**
@@ -351,6 +540,26 @@ export class HashMap {
351
540
  return value;
352
541
  }
353
542
 
543
+ /**
544
+ *
545
+ * @param {number} bin
546
+ */
547
+ #update_range_for_deleted(bin) {
548
+ if (this.#entries_start === bin) {
549
+ let start = bin + 1;
550
+ let bound = this.#entries_bound;
551
+
552
+ const entries = this.#entries;
553
+
554
+ while (start < bound && entries[start].hash === RESERVED_HASH) {
555
+ start++;
556
+ }
557
+
558
+ assert.greaterThanOrEqual(bound - start, this.#size, `live entity bounds must span at least number of entries equal to map size(=${this.#size}), instead got start(=${start}), and end(=${bound})`)
559
+
560
+ this.#entries_start = start;
561
+ }
562
+ }
354
563
 
355
564
  /**
356
565
  *
@@ -359,38 +568,49 @@ export class HashMap {
359
568
  */
360
569
  delete(key) {
361
570
 
362
- const raw_hash = this.keyHashFunction(key);
363
- const bucket_index = this.__compute_bucket_index(raw_hash);
364
- const bucket = this.records[bucket_index];
571
+ const raw_hash = this.#build_key_hash(key);
572
+ let bin_index = this.#compute_bin_index(raw_hash);
365
573
 
366
- if (bucket === undefined) {
367
- return false;
368
- }
574
+ assert.isFiniteNumber(bin_index, 'hash');
369
575
 
576
+ const bins = this.#bins;
577
+ const entries = this.#entries;
370
578
 
371
- const bucketSize = bucket.length;
579
+ for (; ;) {
580
+ const bin = bins[bin_index];
372
581
 
373
- //check if key already exists
374
- for (let i = 0; i < bucketSize; i++) {
375
- const entry = bucket[i];
582
+ if (bin > BIN_RESERVED_VALUE_DELETED) {
583
+ // bin is occupied
376
584
 
377
- const entryKey = entry.key;
378
- if (entryKey === key || this.keyEqualityFunction(entryKey, key)) {
585
+ // check if the entry is what we're looking for
586
+ const entry_index = bin - ENTRY_BASE;
587
+ const entry = entries[entry_index];
379
588
 
380
- if (bucket.length <= 1) {
381
- //last element in the bucket
382
- delete this.records[bucket_index];
383
- } else {
384
- bucket.splice(i, 1);
589
+ if (entry_equality_check(entry, raw_hash, key, this.keyEqualityFunction)) {
590
+ // found the right entry
591
+
592
+ // record entry as dead
593
+ this.#deallocate(entry);
594
+
595
+ // mark slot as removed
596
+ bins[bin_index] = BIN_RESERVED_VALUE_DELETED;
597
+
598
+ this.#size--;
599
+
600
+ this.#update_range_for_deleted(entry_index);
601
+
602
+ return true;
385
603
  }
386
604
 
387
- this.size--;
388
- return true;
605
+ } else if (bin === BIN_RESERVED_VALUE_EMPTY) {
606
+ // bin is empty
607
+ return false;
389
608
  }
390
- }
391
609
 
392
- //not found
393
- return false;
610
+ // perform secondary hashing
611
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
612
+
613
+ }
394
614
  }
395
615
 
396
616
  /**
@@ -402,130 +622,134 @@ export class HashMap {
402
622
  verifyHashes(callback, thisArg) {
403
623
  let all_hashes_valid = true;
404
624
 
405
- let h, i;
406
-
407
- const records = this.records;
625
+ const count = this.#bin_count;
626
+ for (let j = 0; j < count; j++) {
627
+ const bin = this.#bins[j];
408
628
 
409
- for (h in records) {
410
- if (!records.hasOwnProperty(h)) {
629
+ if (bin <= BIN_RESERVED_VALUE_DELETED) {
630
+ // unoccupied
411
631
  continue;
412
632
  }
413
633
 
414
- const bucket = records[h];
415
-
416
- const bucketSize = bucket.length;
417
-
418
- //check if key already exists
419
- for (i = 0; i < bucketSize; i++) {
420
-
421
- /**
422
- * @type {MapEntry<K,V>}
423
- */
424
- const entry = bucket[i];
634
+ /**
635
+ * @type {MapEntry<K,V>}
636
+ */
637
+ const entry = this.#entries[bin - ENTRY_BASE];
425
638
 
426
- //check hash
427
- const raw_hash = this.keyHashFunction(entry.key);
639
+ //check hash
640
+ const raw_hash = this.#build_key_hash(entry.key);
428
641
 
429
- if (entry.hash !== raw_hash) {
430
- callback.call(thisArg, `Hash stored on the entry(=${entry.hash}) is different from the computed key hash(=${raw_hash}).`, entry.key, entry.value)
642
+ if (entry.hash !== raw_hash) {
643
+ callback.call(thisArg, `Hash stored on the entry(=${entry.hash}) is different from the computed key hash(=${raw_hash}).`, entry.key, entry.value)
431
644
 
432
- all_hashes_valid = false;
433
- }
434
-
435
- const actual_bucket_index = this.__compute_bucket_index(raw_hash);
436
-
437
- const stored_bucket_index = parseInt(h);
438
-
439
- if (actual_bucket_index !== stored_bucket_index) {
440
- callback.call(thisArg, `Hash of key has changed. old=${stored_bucket_index}, new=${actual_bucket_index}`, entry.key, entry.value);
441
-
442
- all_hashes_valid = false;
443
- }
645
+ all_hashes_valid = false;
444
646
  }
647
+
445
648
  }
446
649
 
447
650
  return all_hashes_valid;
448
651
  }
449
652
 
653
+ #grow() {
654
+ this.#setBinCount(this.#entries_allocated_count * 2);
655
+ }
656
+
657
+
450
658
  /**
451
- * Rebuild table, useful for when table is resized or hashes of stored keys change
659
+ * Rebuild table, useful for when table is resized
452
660
  */
453
661
  rebuild() {
662
+ const entries_bound = this.#entries_bound;
663
+ const entries = this.#entries;
664
+
665
+ // reset all bins
666
+ const bins = this.#bins;
667
+ bins.fill(BIN_RESERVED_VALUE_EMPTY);
454
668
 
455
- let string_bucket_index, i, l;
669
+ let written_entries = 0;
456
670
 
457
- const records = this.records;
671
+ for (let existing_entry_index = this.#entries_start; existing_entry_index < entries_bound; existing_entry_index++) {
672
+ const entry = entries[existing_entry_index];
458
673
 
459
- for (string_bucket_index in records) {
460
- const stored_bucket_index = parseInt(string_bucket_index);
674
+ const hash = entry.hash;
461
675
 
462
- if (!records.hasOwnProperty(string_bucket_index)) {
676
+ if (hash === RESERVED_HASH) {
677
+ // entry is dead
463
678
  continue;
464
679
  }
465
680
 
466
- const bucket = records[stored_bucket_index];
681
+ const new_index = written_entries;
682
+ written_entries++;
467
683
 
468
- for (i = 0, l = bucket.length; i < l; i++) {
469
- /**
470
- * @type {MapEntry<K,V>}
471
- */
472
- const entry = bucket[i];
684
+ if (new_index !== existing_entry_index) {
685
+ // move entries to the new position, compacting holes
686
+ arraySwapElements(entries, new_index, existing_entry_index);
687
+ }
473
688
 
474
- //check hash
475
- const raw_hash = this.keyHashFunction(entry.key);
689
+ let bin_index = this.#compute_bin_index(hash);
476
690
 
477
- entry.hash = raw_hash;
691
+ for (; ;) {
692
+ const bin = bins[bin_index];
478
693
 
479
- const actual_bucket_index = this.__compute_bucket_index(raw_hash);
694
+ if (bin === BIN_RESERVED_VALUE_EMPTY) {
695
+ // empty slot, take it
696
+ bins[bin_index] = new_index + ENTRY_BASE;
697
+ break;
698
+ }
480
699
 
700
+ // perform secondary hashing
701
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
481
702
 
482
- if (actual_bucket_index !== stored_bucket_index) {
483
- //remove entry from the bucket
703
+ }
704
+ }
484
705
 
485
- bucket.splice(i, 1);
486
- //update iterator
487
- l--;
488
- i--;
706
+ assert.equal(written_entries, this.#size, `live entries(=${written_entries}) should match size(=${this.#size})`);
489
707
 
490
- if (bucket.length === 0) {
491
- //delete empty bucket
492
- delete records[stored_bucket_index];
493
- }
708
+ this.#entries_start = 0;
709
+ this.#entries_bound = this.#size;
710
+ this.#version++;
711
+ }
494
712
 
495
- const newBucket = records[actual_bucket_index];
713
+ #count_live_entities() {
714
+ let count = 0;
715
+ for (let i = this.#entries_start; i < this.#entries_bound; i++) {
716
+ const entry = this.#entries[i];
496
717
 
497
- if (newBucket === undefined) {
498
- records[actual_bucket_index] = [entry];
499
- } else {
500
- newBucket.push(entry);
501
- }
718
+ if (entry.hash !== RESERVED_HASH) {
719
+ count++;
502
720
 
503
- }
721
+ assert.equal(entry.hash, this.#build_key_hash(entry.key));
722
+ } else {
723
+ assert.isNull(entry.key, 'key');
504
724
  }
505
725
  }
726
+
727
+ return count;
506
728
  }
507
729
 
508
730
  forEach(callback, thisArg) {
509
- let h, i, l;
731
+ const count = this.#bin_count;
732
+ const entries = this.#entries;
733
+ const bins = this.#bins;
734
+ const start_version = this.#version;
735
+
736
+ for (let j = 0; j < count; j++) {
737
+ assert.equal(start_version, this.#version, 'HashMap modified during traversal');
510
738
 
511
- const records = this.records;
739
+ const bin = bins[j];
512
740
 
513
- for (h in records) {
514
- if (!records.hasOwnProperty(h)) {
741
+ if (bin <= BIN_RESERVED_VALUE_DELETED) {
742
+ // unoccupied
515
743
  continue;
516
744
  }
517
745
 
518
- const bucket = records[h];
746
+ /**
747
+ * @type {MapEntry<K,V>}
748
+ */
749
+ const entry = entries[bin - ENTRY_BASE];
519
750
 
520
- for (i = 0, l = bucket.length; i < l; i++) {
521
- /**
522
- * @type {MapEntry<K,V>}
523
- */
524
- const entry = bucket[i];
525
-
526
- // Signature based on MDN docs of Map.prototype.forEach()
527
- callback.call(thisArg, entry.value, entry.key, this);
528
- }
751
+ // Signature based on MDN docs of Map.prototype.forEach()
752
+ callback.call(thisArg, entry.value, entry.key, this);
529
753
  }
530
754
  }
531
755
 
@@ -542,30 +766,61 @@ export class HashMap {
542
766
  * Remove all data from the Map
543
767
  */
544
768
  clear() {
545
- this.records = [];
546
- this.size = 0;
769
+
770
+ // clear out all
771
+ const bins = this.#bins;
772
+ const count = this.#bin_count;
773
+
774
+ for (let i = 0; i < count; i++) {
775
+ const bin = bins[i];
776
+
777
+ if (bin !== BIN_RESERVED_VALUE_EMPTY) {
778
+
779
+ if (bin !== BIN_RESERVED_VALUE_DELETED) {
780
+ // occupied, move to deleted
781
+ const entry_index = bin - ENTRY_BASE;
782
+
783
+ this.#deallocate(this.#entries[entry_index]);
784
+ }
785
+
786
+ // mark as empty
787
+ bins[i] = BIN_RESERVED_VALUE_EMPTY;
788
+ }
789
+
790
+
791
+ }
792
+
793
+ this.#size = 0;
794
+
795
+ this.#entries_start = 0;
796
+ this.#entries_bound = 0;
547
797
  }
548
798
 
549
799
  * [Symbol.iterator]() {
550
- let h, i, l;
551
800
 
552
- const records = this.records;
801
+ const count = this.#bin_count;
553
802
 
554
- for (h in records) {
555
- if (!records.hasOwnProperty(h)) {
556
- continue;
557
- }
803
+ const bins = this.#bins;
804
+ const entries = this.#entries;
805
+
806
+ const start_version = this.#version;
558
807
 
559
- const bucket = records[h];
808
+ for (let j = 0; j < count; j++) {
809
+ assert.equal(start_version, this.#version, 'HashMap modified during traversal');
560
810
 
561
- for (i = 0, l = bucket.length; i < l; i++) {
562
- /**
563
- * @type {MapEntry<K,V>}
564
- */
565
- const entry = bucket[i];
811
+ const bin = bins[j];
566
812
 
567
- yield [entry.key, entry.value];
813
+ if (bin <= BIN_RESERVED_VALUE_DELETED) {
814
+ // unoccupied
815
+ continue;
568
816
  }
817
+
818
+ /**
819
+ * @type {MapEntry<K,V>}
820
+ */
821
+ const entry = entries[bin - ENTRY_BASE];
822
+
823
+ yield [entry.key, entry.value];
569
824
  }
570
825
 
571
826
  }