@cubingcenter/scrambler 0.6.3 → 0.8.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 (191) hide show
  1. package/README.md +17 -4
  2. package/dist/core/api.d.ts.map +1 -1
  3. package/dist/core/api.js +10 -2
  4. package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
  5. package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
  7. package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
  8. package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
  9. package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
  10. package/dist/puzzles/cube2/solver/search.js +3 -3
  11. package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
  12. package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
  13. package/dist/puzzles/cube2/solver/tables.js +16 -2
  14. package/dist/puzzles/cube3/index.d.ts +1 -0
  15. package/dist/puzzles/cube3/index.d.ts.map +1 -1
  16. package/dist/puzzles/cube3/moves.d.ts.map +1 -1
  17. package/dist/puzzles/cube3/moves.js +104 -18
  18. package/dist/puzzles/cube3/scrambler.d.ts +16 -1
  19. package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/scrambler.js +1 -1
  21. package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
  22. package/dist/puzzles/cube3/solver/browser-loader.js +48 -22
  23. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  24. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  25. package/dist/puzzles/cube3/solver/index.d.ts +5 -3
  26. package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
  27. package/dist/puzzles/cube3/solver/index.js +2 -1
  28. package/dist/puzzles/cube3/solver/node-loader.js +9 -2
  29. package/dist/puzzles/cube3/solver/phase1.d.ts +37 -0
  30. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  31. package/dist/puzzles/cube3/solver/phase1.js +156 -15
  32. package/dist/puzzles/cube3/solver/phase2.d.ts +75 -0
  33. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  34. package/dist/puzzles/cube3/solver/phase2.js +279 -68
  35. package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
  36. package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
  37. package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
  38. package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
  39. package/dist/puzzles/cube3/solver/symmetry.js +74 -0
  40. package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
  41. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  42. package/dist/puzzles/cube3/solver/tables.js +14 -12
  43. package/dist/puzzles/cube3/validation.d.ts.map +1 -1
  44. package/dist/puzzles/cube3/validation.js +15 -7
  45. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  46. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  47. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  48. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  49. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/browser-worker.js +31 -0
  51. package/dist/puzzles/cube4/index.d.ts +10 -2
  52. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/index.js +6 -2
  54. package/dist/puzzles/cube4/moves.d.ts +5 -0
  55. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  56. package/dist/puzzles/cube4/moves.js +212 -11
  57. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +29 -1
  58. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/browser-loader.js +227 -32
  60. package/dist/puzzles/cube4/reduction/center2.d.ts +44 -3
  61. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  62. package/dist/puzzles/cube4/reduction/center2.js +152 -10
  63. package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
  64. package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/reduction/edge3.js +23 -3
  66. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  67. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  69. package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  70. package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  71. package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  72. package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  73. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
  74. package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  75. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +52 -0
  76. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
  77. package/dist/puzzles/cube4/reduction/hc-pdb.js +220 -0
  78. package/dist/puzzles/cube4/reduction/node-loader.d.ts +15 -1
  79. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  80. package/dist/puzzles/cube4/reduction/node-loader.js +199 -25
  81. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  82. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  83. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  84. package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  85. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +116 -7
  86. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  87. package/dist/puzzles/cube4/reduction/phase3-search.js +458 -32
  88. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  89. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  90. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  91. package/dist/puzzles/cube4/reduction/solve.d.ts +172 -2
  92. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  93. package/dist/puzzles/cube4/reduction/solve.js +1201 -180
  94. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  95. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  96. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  97. package/dist/puzzles/cube4/scrambler.d.ts +201 -6
  98. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  99. package/dist/puzzles/cube4/scrambler.js +414 -176
  100. package/dist/puzzles/cube4/state.d.ts.map +1 -1
  101. package/dist/puzzles/cube4/state.js +4 -1
  102. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  103. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  104. package/dist/puzzles/cube4/worker-pool.js +405 -0
  105. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  106. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  107. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  108. package/dist/puzzles/cube4/worker.d.ts +26 -0
  109. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  110. package/dist/puzzles/cube4/worker.js +49 -0
  111. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  112. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  113. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  114. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  115. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  116. package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
  117. package/dist/puzzles/pyraminx/solver/search.js +3 -6
  118. package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
  119. package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
  120. package/dist/puzzles/pyraminx/solver/tables.js +17 -0
  121. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  122. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  123. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  124. package/dist/puzzles/shared/load-once.d.ts +16 -0
  125. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  126. package/dist/puzzles/shared/load-once.js +11 -0
  127. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  128. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  129. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  130. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  131. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  132. package/dist/puzzles/skewb/solver/search.js +3 -3
  133. package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
  134. package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
  135. package/dist/puzzles/skewb/solver/tables.js +17 -0
  136. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  137. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  138. package/dist-cjs/core/api.js +10 -2
  139. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  140. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  141. package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
  142. package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
  143. package/dist-cjs/puzzles/cube3/moves.js +104 -18
  144. package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
  145. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +48 -22
  146. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  147. package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
  148. package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
  149. package/dist-cjs/puzzles/cube3/solver/phase1.js +157 -15
  150. package/dist-cjs/puzzles/cube3/solver/phase2.js +276 -65
  151. package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
  152. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  153. package/dist-cjs/puzzles/cube3/validation.js +15 -7
  154. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  155. package/dist-cjs/puzzles/cube4/index.js +23 -1
  156. package/dist-cjs/puzzles/cube4/moves.js +213 -11
  157. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +227 -32
  158. package/dist-cjs/puzzles/cube4/reduction/center2.js +155 -10
  159. package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
  160. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  161. package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  162. package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  163. package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  164. package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  165. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
  166. package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  167. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +232 -0
  168. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +200 -25
  169. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  170. package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  171. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +460 -33
  172. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  173. package/dist-cjs/puzzles/cube4/reduction/solve.js +1203 -172
  174. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  175. package/dist-cjs/puzzles/cube4/scrambler.js +416 -174
  176. package/dist-cjs/puzzles/cube4/state.js +4 -1
  177. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  178. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  179. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  180. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  181. package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
  182. package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
  183. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  184. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  185. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  186. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  187. package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
  188. package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
  189. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  190. package/docs/development.md +19 -7
  191. package/package.json +17 -7
@@ -1,29 +1,48 @@
1
1
  import { SQ1_SOLVER_TABLE_METADATA } from "./table-metadata.js";
2
2
  import { fetchTableBuffer } from "../../shared/compressed-fetch.js";
3
3
  const TABLES_DIR = "puzzles/sq1/solver/generated";
4
+ const CACHE_NAME = "cubingcenter-scrambler-tables-v1";
4
5
  let cachedBrowserTables;
6
+ let cachedBrowserTablesPromise;
5
7
  export function getCachedBrowserTables() {
6
8
  return cachedBrowserTables;
7
9
  }
8
10
  export function clearCachedBrowserTables() {
9
11
  cachedBrowserTables = undefined;
12
+ cachedBrowserTablesPromise = undefined;
10
13
  }
11
14
  export async function loadBrowserTables(baseUrl) {
12
15
  if (cachedBrowserTables) {
13
16
  return cachedBrowserTables;
14
17
  }
18
+ if (cachedBrowserTablesPromise) {
19
+ // One shared in-flight promise: concurrent calls trigger exactly one
20
+ // fetch pass (dedup).
21
+ return cachedBrowserTablesPromise;
22
+ }
23
+ const promise = fetchTables(baseUrl);
24
+ cachedBrowserTablesPromise = promise;
25
+ promise.then(() => { }, () => {
26
+ // A failed load must not poison the dedup slot: purge the rejected
27
+ // promise so a later call retries from scratch.
28
+ if (cachedBrowserTablesPromise === promise)
29
+ cachedBrowserTablesPromise = undefined;
30
+ });
31
+ return promise;
32
+ }
33
+ async function fetchTables(baseUrl) {
15
34
  const resolved = resolveBaseUrl(baseUrl);
16
35
  const manifest = await fetchManifest(resolved);
17
36
  const [shapeIdx, shapePrun, shapeTopMove, shapeBottomMove, shapeTwistMove, squareTwistMove, squareTopMove, squareBottomMove, squarePrun] = await Promise.all([
18
- fetchInt32Table(resolved, manifest.files.shapeIdx, SQ1_SOLVER_TABLE_METADATA.shapeIdx.entries),
19
- fetchInt8Table(resolved, manifest.files.shapePrun, SQ1_SOLVER_TABLE_METADATA.shapePrun.entries),
20
- fetchInt32Table(resolved, manifest.files.shapeTopMove, SQ1_SOLVER_TABLE_METADATA.shapeTopMove.entries),
21
- fetchInt32Table(resolved, manifest.files.shapeBottomMove, SQ1_SOLVER_TABLE_METADATA.shapeBottomMove.entries),
22
- fetchInt32Table(resolved, manifest.files.shapeTwistMove, SQ1_SOLVER_TABLE_METADATA.shapeTwistMove.entries),
23
- fetchUint16Table(resolved, manifest.files.squareTwistMove, SQ1_SOLVER_TABLE_METADATA.squareTwistMove.entries),
24
- fetchUint16Table(resolved, manifest.files.squareTopMove, SQ1_SOLVER_TABLE_METADATA.squareTopMove.entries),
25
- fetchUint16Table(resolved, manifest.files.squareBottomMove, SQ1_SOLVER_TABLE_METADATA.squareBottomMove.entries),
26
- fetchInt8Table(resolved, manifest.files.squarePrun, SQ1_SOLVER_TABLE_METADATA.squarePrun.entries),
37
+ fetchInt32Table(resolved, manifest.files.shapeIdx, SQ1_SOLVER_TABLE_METADATA.shapeIdx),
38
+ fetchInt8Table(resolved, manifest.files.shapePrun, SQ1_SOLVER_TABLE_METADATA.shapePrun),
39
+ fetchInt32Table(resolved, manifest.files.shapeTopMove, SQ1_SOLVER_TABLE_METADATA.shapeTopMove),
40
+ fetchInt32Table(resolved, manifest.files.shapeBottomMove, SQ1_SOLVER_TABLE_METADATA.shapeBottomMove),
41
+ fetchInt32Table(resolved, manifest.files.shapeTwistMove, SQ1_SOLVER_TABLE_METADATA.shapeTwistMove),
42
+ fetchUint16Table(resolved, manifest.files.squareTwistMove, SQ1_SOLVER_TABLE_METADATA.squareTwistMove),
43
+ fetchUint16Table(resolved, manifest.files.squareTopMove, SQ1_SOLVER_TABLE_METADATA.squareTopMove),
44
+ fetchUint16Table(resolved, manifest.files.squareBottomMove, SQ1_SOLVER_TABLE_METADATA.squareBottomMove),
45
+ fetchInt8Table(resolved, manifest.files.squarePrun, SQ1_SOLVER_TABLE_METADATA.squarePrun),
27
46
  ]);
28
47
  cachedBrowserTables = {
29
48
  shapeIdx,
@@ -59,34 +78,47 @@ async function fetchManifest(baseUrl) {
59
78
  }
60
79
  return (await response.json());
61
80
  }
62
- async function fetchInt32Table(baseUrl, entry, expectedEntries) {
63
- const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, { compressed: entry.compressed !== false });
64
- if (buffer.byteLength !== expectedEntries * 4) {
65
- throw new Error(`Invalid byte size for ${entry.file}`);
66
- }
67
- const values = new Int32Array(expectedEntries);
81
+ /**
82
+ * Fetch + verify with the SAME checks as node-loader.ts: byte length from
83
+ * the manifest entries, FNV-1a checksum against the table-metadata
84
+ * constants (node skips the checksum when that constant is 0 = "unchecked";
85
+ * the browser mirrors the guard).
86
+ */
87
+ async function fetchInt32Table(baseUrl, entry, meta) {
88
+ const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, {
89
+ cacheName: CACHE_NAME,
90
+ compressed: entry.compressed !== false,
91
+ expectedBytes: entry.entries * 4,
92
+ ...(meta.checksum !== 0 ? { expectedFnv1a: { value: meta.checksum, elementSize: 4 } } : {})
93
+ });
94
+ const values = new Int32Array(entry.entries);
68
95
  const view = new DataView(buffer);
69
96
  for (let i = 0; i < values.length; i++) {
70
97
  values[i] = view.getInt32(i * 4, true);
71
98
  }
72
99
  return values;
73
100
  }
74
- async function fetchUint16Table(baseUrl, entry, expectedEntries) {
75
- const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, { compressed: entry.compressed !== false });
76
- if (buffer.byteLength !== expectedEntries * 2) {
77
- throw new Error(`Invalid byte size for ${entry.file}`);
78
- }
79
- const values = new Uint16Array(expectedEntries);
101
+ async function fetchUint16Table(baseUrl, entry, meta) {
102
+ const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, {
103
+ cacheName: CACHE_NAME,
104
+ compressed: entry.compressed !== false,
105
+ expectedBytes: entry.entries * 2,
106
+ ...(meta.checksum !== 0 ? { expectedFnv1a: { value: meta.checksum, elementSize: 2 } } : {})
107
+ });
108
+ const values = new Uint16Array(entry.entries);
80
109
  const view = new DataView(buffer);
81
110
  for (let i = 0; i < values.length; i++) {
82
111
  values[i] = view.getUint16(i * 2, true);
83
112
  }
84
113
  return values;
85
114
  }
86
- async function fetchInt8Table(baseUrl, entry, expectedEntries) {
87
- const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, { compressed: entry.compressed !== false });
88
- if (buffer.byteLength !== expectedEntries) {
89
- throw new Error(`Invalid byte size for ${entry.file}`);
90
- }
115
+ async function fetchInt8Table(baseUrl, entry, meta) {
116
+ const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, {
117
+ cacheName: CACHE_NAME,
118
+ compressed: entry.compressed !== false,
119
+ expectedBytes: entry.entries,
120
+ // Square-1's int8 tables hash SIGNED values in node (Int8Array).
121
+ ...(meta.checksum !== 0 ? { expectedFnv1a: { value: meta.checksum, elementSize: 1, signed: true } } : {})
122
+ });
91
123
  return new Int8Array(buffer);
92
124
  }
@@ -80,8 +80,16 @@ async function initializeScrambler(baseUrl, eventId) {
80
80
  await loadBrowserTables(baseUrl);
81
81
  }
82
82
  else if (eventId === "444") {
83
- const { loadCube4ReductionTablesBrowser } = await Promise.resolve().then(() => __importStar(require("../puzzles/cube4/reduction/browser-loader.js")));
84
- await loadCube4ReductionTablesBrowser(baseUrl);
83
+ // Reduction ends with a 3x3 solve in the same worker. Initializing 444
84
+ // must also load those required tables, including the caller's base URL.
85
+ const [cube4, cube3] = await Promise.all([
86
+ Promise.resolve().then(() => __importStar(require("../puzzles/cube4/reduction/browser-loader.js"))),
87
+ Promise.resolve().then(() => __importStar(require("../puzzles/cube3/solver/browser-loader.js")))
88
+ ]);
89
+ await Promise.all([
90
+ cube4.loadCube4ReductionTablesBrowser(baseUrl),
91
+ cube3.loadBrowserTables(baseUrl)
92
+ ]);
85
93
  }
86
94
  else if (!eventId) {
87
95
  await Promise.all([
@@ -6,23 +6,42 @@ exports.loadBrowserTables = loadBrowserTables;
6
6
  const table_metadata_js_1 = require("./table-metadata.js");
7
7
  const compressed_fetch_js_1 = require("../../shared/compressed-fetch.js");
8
8
  const TABLES_DIR = "puzzles/cube2/solver/generated";
9
+ const CACHE_NAME = "cubingcenter-scrambler-tables-v1";
9
10
  let cachedBrowserTables;
11
+ let cachedBrowserTablesPromise;
10
12
  function getCachedBrowserTables() {
11
13
  return cachedBrowserTables;
12
14
  }
13
15
  function clearCachedBrowserTables() {
14
16
  cachedBrowserTables = undefined;
17
+ cachedBrowserTablesPromise = undefined;
15
18
  }
16
19
  async function loadBrowserTables(baseUrl) {
17
20
  if (cachedBrowserTables) {
18
21
  return cachedBrowserTables;
19
22
  }
23
+ if (cachedBrowserTablesPromise) {
24
+ // One shared in-flight promise: concurrent calls trigger exactly one
25
+ // fetch pass (dedup).
26
+ return cachedBrowserTablesPromise;
27
+ }
28
+ const promise = fetchTables(baseUrl);
29
+ cachedBrowserTablesPromise = promise;
30
+ promise.then(() => { }, () => {
31
+ // A failed load must not poison the dedup slot: purge the rejected
32
+ // promise so a later call retries from scratch.
33
+ if (cachedBrowserTablesPromise === promise)
34
+ cachedBrowserTablesPromise = undefined;
35
+ });
36
+ return promise;
37
+ }
38
+ async function fetchTables(baseUrl) {
20
39
  const resolved = resolveBaseUrl(baseUrl);
21
40
  const manifest = await fetchManifest(resolved);
22
41
  const [permTable, orientTable, dist] = await Promise.all([
23
- fetchUint16Table(resolved, manifest.files.permutationTable, table_metadata_js_1.CUBE2_SOLVER_TABLE_METADATA.permutationTable.entries),
24
- fetchUint16Table(resolved, manifest.files.orientationTable, table_metadata_js_1.CUBE2_SOLVER_TABLE_METADATA.orientationTable.entries),
25
- fetchUint8Table(resolved, manifest.files.policy, table_metadata_js_1.CUBE2_SOLVER_TABLE_METADATA.policy.entries)
42
+ fetchUint16Table(resolved, manifest.files.permutationTable, table_metadata_js_1.CUBE2_SOLVER_TABLE_METADATA.permutationTable),
43
+ fetchUint16Table(resolved, manifest.files.orientationTable, table_metadata_js_1.CUBE2_SOLVER_TABLE_METADATA.orientationTable),
44
+ fetchUint8Table(resolved, manifest.files.policy, table_metadata_js_1.CUBE2_SOLVER_TABLE_METADATA.policy)
26
45
  ]);
27
46
  cachedBrowserTables = {
28
47
  permutationTable: permTable,
@@ -56,22 +75,31 @@ async function fetchManifest(baseUrl) {
56
75
  }
57
76
  return manifest;
58
77
  }
59
- async function fetchUint16Table(baseUrl, entry, expectedEntries) {
60
- const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, { compressed: entry.compressed !== false });
61
- if (buffer.byteLength !== expectedEntries * 2) {
62
- throw new Error(`Invalid byte size for ${entry.file}`);
63
- }
64
- const values = new Uint16Array(expectedEntries);
78
+ /**
79
+ * Fetch + verify with the SAME checks as node-loader.ts: byte length from
80
+ * the manifest entries, FNV-1a checksum against the table-metadata
81
+ * constants (the values node asserts against).
82
+ */
83
+ async function fetchUint16Table(baseUrl, entry, meta) {
84
+ const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, {
85
+ cacheName: CACHE_NAME,
86
+ compressed: entry.compressed !== false,
87
+ expectedBytes: entry.entries * 2,
88
+ expectedFnv1a: { value: meta.checksum, elementSize: 2 }
89
+ });
90
+ const values = new Uint16Array(entry.entries);
65
91
  const view = new DataView(buffer);
66
92
  for (let i = 0; i < values.length; i++) {
67
93
  values[i] = view.getUint16(i * 2, true);
68
94
  }
69
95
  return values;
70
96
  }
71
- async function fetchUint8Table(baseUrl, entry, expectedEntries) {
72
- const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, { compressed: entry.compressed !== false });
73
- if (buffer.byteLength !== expectedEntries) {
74
- throw new Error(`Invalid byte size for ${entry.file}`);
75
- }
97
+ async function fetchUint8Table(baseUrl, entry, meta) {
98
+ const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, {
99
+ cacheName: CACHE_NAME,
100
+ compressed: entry.compressed !== false,
101
+ expectedBytes: entry.entries,
102
+ expectedFnv1a: { value: meta.checksum, elementSize: 1 }
103
+ });
76
104
  return new Uint8Array(buffer);
77
105
  }
@@ -35,7 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadGeneratedCube2SolverTables = loadGeneratedCube2SolverTables;
37
37
  const browser_loader_js_1 = require("./browser-loader.js");
38
- let cachedTables;
38
+ const load_once_js_1 = require("../../shared/load-once.js");
39
+ // One shared in-flight promise for the node fs load (same pattern as
40
+ // cube3/solver/generated-assets.ts): the previous `cached ??= await …` raced —
41
+ // concurrent callers interleaving on the await both ran `loadNodeTables()`
42
+ // (two filesystem reads, last assignment wins). On rejection the slot is
43
+ // purged so the next call can retry.
44
+ const nodeTablesSlot = { promise: undefined };
39
45
  function isBrowser() {
40
46
  return typeof self !== "undefined";
41
47
  }
@@ -49,8 +55,7 @@ async function loadGeneratedCube2SolverTables() {
49
55
  "In browser environments, call `initializeScrambler(baseUrl)` " +
50
56
  "before generating scrambles.");
51
57
  }
52
- cachedTables ??= await loadNodeTables();
53
- return cachedTables;
58
+ return (0, load_once_js_1.loadOnce)(nodeTablesSlot, loadNodeTables);
54
59
  }
55
60
  async function loadNodeTables() {
56
61
  const loader = await Promise.resolve().then(() => __importStar(require(
@@ -37,7 +37,7 @@ function solveWithTablesIndices(state) {
37
37
  }
38
38
  const permTable = tables_js_1.PERMUTATION_TABLE;
39
39
  const orientTable = tables_js_1.ORIENTATION_TABLE;
40
- const policy = tables_js_1.POLICY_TABLE;
40
+ const policy = tables_js_1.POLICY_TABLE_EXPANDED;
41
41
  let permIndex = (0, tables_js_1.encodePermutation)(state.corners.permutation);
42
42
  let orientIndex = (0, tables_js_1.encodeOrientation)(state.corners.orientation);
43
43
  const moves = [];
@@ -45,7 +45,7 @@ function solveWithTablesIndices(state) {
45
45
  // candidate scanning (the old BFS-distance gradient walk) is needed.
46
46
  let combined = permIndex * tables_js_1.ORIENTATION_COUNT + orientIndex;
47
47
  while (combined !== 0) {
48
- const m = (policy[combined >> 1] >> ((combined & 1) << 2)) & 0x0f;
48
+ const m = policy[combined];
49
49
  moves.push(m);
50
50
  permIndex = permTable[permIndex * tables_js_1.MOVE_COUNT + m];
51
51
  orientIndex = orientTable[orientIndex * tables_js_1.MOVE_COUNT + m];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BFS_MOVES = exports.POLICY_TABLE = exports.ORIENTATION_TABLE = exports.PERMUTATION_TABLE = exports.MOVE_COUNT = exports.ORIENTATION_COUNT = exports.PERMUTATION_COUNT = void 0;
3
+ exports.BFS_MOVES = exports.POLICY_TABLE_EXPANDED = exports.POLICY_TABLE = exports.ORIENTATION_TABLE = exports.PERMUTATION_TABLE = exports.MOVE_COUNT = exports.ORIENTATION_COUNT = exports.PERMUTATION_COUNT = void 0;
4
4
  exports.encodePermutation = encodePermutation;
5
5
  exports.encodeOrientation = encodeOrientation;
6
6
  exports.ensureTablesLoaded = ensureTablesLoaded;
@@ -64,6 +64,12 @@ let tablesLoaded = false;
64
64
  exports.PERMUTATION_TABLE = null;
65
65
  exports.ORIENTATION_TABLE = null;
66
66
  exports.POLICY_TABLE = null;
67
+ /**
68
+ * Byte-expanded policy (one optimal move per state, no bit-unpacking in
69
+ * the hot walk). Built at load from the packed table: +3.7 MB RAM,
70
+ * zero wire/format change.
71
+ */
72
+ exports.POLICY_TABLE_EXPANDED = null;
67
73
  async function ensureTablesLoaded() {
68
74
  if (tablesLoaded) {
69
75
  return;
@@ -72,8 +78,16 @@ async function ensureTablesLoaded() {
72
78
  exports.PERMUTATION_TABLE = tables.permutationTable;
73
79
  exports.ORIENTATION_TABLE = tables.orientationTable;
74
80
  exports.POLICY_TABLE = tables.policyTable;
81
+ exports.POLICY_TABLE_EXPANDED = expandNibblePolicy(tables.policyTable, exports.PERMUTATION_COUNT * exports.ORIENTATION_COUNT);
75
82
  tablesLoaded = true;
76
83
  }
84
+ function expandNibblePolicy(packed, count) {
85
+ const expanded = new Uint8Array(count);
86
+ for (let index = 0; index < count; index += 1) {
87
+ expanded[index] = (packed[index >> 1] >> ((index & 1) << 2)) & 0x0f;
88
+ }
89
+ return expanded;
90
+ }
77
91
  function areCube2TablesLoaded() {
78
92
  return tablesLoaded;
79
93
  }
@@ -81,7 +95,7 @@ function areCube2TablesLoaded() {
81
95
  // Public API
82
96
  // ---------------------------------------------------------------------------
83
97
  function getDistance(permIndex, orientIndex) {
84
- if (!exports.POLICY_TABLE) {
98
+ if (!exports.POLICY_TABLE_EXPANDED) {
85
99
  throw new Error("Cube2 solver tables not loaded. Call ensureTablesLoaded() first.");
86
100
  }
87
101
  const permTable = exports.PERMUTATION_TABLE;
@@ -91,7 +105,7 @@ function getDistance(permIndex, orientIndex) {
91
105
  let combined = p * exports.ORIENTATION_COUNT + o;
92
106
  let distance = 0;
93
107
  while (combined !== 0) {
94
- const m = (exports.POLICY_TABLE[combined >> 1] >> ((combined & 1) << 2)) & 0x0f;
108
+ const m = exports.POLICY_TABLE_EXPANDED[combined];
95
109
  p = permTable[p * exports.MOVE_COUNT + m];
96
110
  o = orientTable[o * exports.MOVE_COUNT + m];
97
111
  combined = p * exports.ORIENTATION_COUNT + o;
@@ -35,16 +35,114 @@ const FACE_MOVE_TABLES = {
35
35
  edges: { permutation: [0, 1, 2, 11, 4, 5, 6, 10, 8, 9, 3, 7], orientation: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1] }
36
36
  }
37
37
  };
38
- function applyCube3Move(state, move) {
38
+ function quarterEffect(face) {
39
+ const table = FACE_MOVE_TABLES[face];
40
+ return {
41
+ cornerPermutation: table.corners.permutation,
42
+ cornerOrientation: table.corners.orientation,
43
+ edgePermutation: table.edges.permutation,
44
+ edgeOrientation: table.edges.orientation
45
+ };
46
+ }
47
+ // Compose two effects (apply `first`, then `second`) into one so a move of
48
+ // any suffix costs exactly one pass over the state — no per-quarter-turn
49
+ // cloning, no intermediate states, no closure allocations.
50
+ function composeEffects(first, second) {
51
+ const cornerPermutation = new Array(8);
52
+ const cornerOrientation = new Array(8);
53
+ const edgePermutation = new Array(12);
54
+ const edgeOrientation = new Array(12);
55
+ for (let i = 0; i < 8; i++) {
56
+ const source = second.cornerPermutation[i];
57
+ cornerPermutation[i] = first.cornerPermutation[source];
58
+ cornerOrientation[i] = (first.cornerOrientation[source] + second.cornerOrientation[i]) % 3;
59
+ }
60
+ for (let i = 0; i < 12; i++) {
61
+ const source = second.edgePermutation[i];
62
+ edgePermutation[i] = first.edgePermutation[source];
63
+ edgeOrientation[i] = (first.edgeOrientation[source] + second.edgeOrientation[i]) % 2;
64
+ }
65
+ return { cornerPermutation, cornerOrientation, edgePermutation, edgeOrientation };
66
+ }
67
+ function powerEffect(face, turns) {
68
+ const quarter = quarterEffect(face);
69
+ if (turns === 1)
70
+ return quarter;
71
+ const half = composeEffects(quarter, quarter);
72
+ if (turns === 2)
73
+ return half;
74
+ return composeEffects(half, quarter);
75
+ }
76
+ // Total effect per move string, built once at module load (mirrors the 4x4
77
+ // fast path): one lookup + one pass per move.
78
+ const COMPOSED_EFFECTS = Object.fromEntries(types_js_1.CUBE3_MOVES.map((move) => {
79
+ const face = move[0];
80
+ const suffix = move.slice(1);
81
+ const turns = suffix === "2" ? 2 : suffix === "'" ? 3 : 1;
82
+ return [move, powerEffect(face, turns)];
83
+ }));
84
+ function effectForMove(move) {
85
+ const hit = COMPOSED_EFFECTS[move];
86
+ if (hit !== undefined)
87
+ return hit;
39
88
  const { face, turns } = parseMoveParts(move);
40
- let next = (0, state_js_1.cloneCube3State)(state);
41
- for (let index = 0; index < turns; index += 1) {
42
- next = applyFaceMove(next, FACE_MOVE_TABLES[face]);
89
+ return powerEffect(face, turns);
90
+ }
91
+ function applyCube3Move(state, move) {
92
+ const effect = effectForMove(move);
93
+ const cornerPermutation = new Array(8);
94
+ const cornerOrientation = new Array(8);
95
+ const edgePermutation = new Array(12);
96
+ const edgeOrientation = new Array(12);
97
+ for (let i = 0; i < 8; i++) {
98
+ const source = effect.cornerPermutation[i];
99
+ cornerPermutation[i] = state.corners.permutation[source];
100
+ cornerOrientation[i] =
101
+ (state.corners.orientation[source] + effect.cornerOrientation[i]) % 3;
43
102
  }
44
- return next;
103
+ for (let i = 0; i < 12; i++) {
104
+ const source = effect.edgePermutation[i];
105
+ edgePermutation[i] = state.edges.permutation[source];
106
+ edgeOrientation[i] =
107
+ (state.edges.orientation[source] + effect.edgeOrientation[i]) % 2;
108
+ }
109
+ return {
110
+ corners: { permutation: cornerPermutation, orientation: cornerOrientation },
111
+ edges: { permutation: edgePermutation, orientation: edgeOrientation }
112
+ };
45
113
  }
46
114
  function applyCube3Algorithm(state, moves) {
47
- return moves.reduce((current, move) => applyCube3Move(current, move), state);
115
+ if (moves.length === 0)
116
+ return (0, state_js_1.cloneCube3State)(state);
117
+ let curCp = [...state.corners.permutation];
118
+ let curCo = [...state.corners.orientation];
119
+ let curEp = [...state.edges.permutation];
120
+ let curEo = [...state.edges.orientation];
121
+ let nxtCp = new Array(8);
122
+ let nxtCo = new Array(8);
123
+ let nxtEp = new Array(12);
124
+ let nxtEo = new Array(12);
125
+ for (const move of moves) {
126
+ const effect = effectForMove(move);
127
+ for (let i = 0; i < 8; i++) {
128
+ const source = effect.cornerPermutation[i];
129
+ nxtCp[i] = curCp[source];
130
+ nxtCo[i] = (curCo[source] + effect.cornerOrientation[i]) % 3;
131
+ }
132
+ for (let i = 0; i < 12; i++) {
133
+ const source = effect.edgePermutation[i];
134
+ nxtEp[i] = curEp[source];
135
+ nxtEo[i] = (curEo[source] + effect.edgeOrientation[i]) % 2;
136
+ }
137
+ [curCp, nxtCp] = [nxtCp, curCp];
138
+ [curCo, nxtCo] = [nxtCo, curCo];
139
+ [curEp, nxtEp] = [nxtEp, curEp];
140
+ [curEo, nxtEo] = [nxtEo, curEo];
141
+ }
142
+ return {
143
+ corners: { permutation: curCp, orientation: curCo },
144
+ edges: { permutation: curEp, orientation: curEo }
145
+ };
48
146
  }
49
147
  function invertCube3Algorithm(moves) {
50
148
  return [...moves].reverse().map(invertCube3Move);
@@ -199,18 +297,6 @@ function invertCube3Move(move) {
199
297
  }
200
298
  return `${face}${suffix === "'" ? "" : "'"}`;
201
299
  }
202
- function applyFaceMove(state, table) {
203
- return {
204
- corners: {
205
- permutation: table.corners.permutation.map((source) => state.corners.permutation[source]),
206
- orientation: table.corners.permutation.map((source, target) => ((state.corners.orientation[source] + table.corners.orientation[target]) % 3))
207
- },
208
- edges: {
209
- permutation: table.edges.permutation.map((source) => state.edges.permutation[source]),
210
- orientation: table.edges.permutation.map((source, target) => ((state.edges.orientation[source] + table.edges.orientation[target]) % 2))
211
- }
212
- };
213
- }
214
300
  function parseMoveParts(move) {
215
301
  const face = move[0];
216
302
  const suffix = move.slice(1);
@@ -22,7 +22,7 @@ exports.cube3ScrambleGenerator = {
22
22
  };
23
23
  async function generateCube3Scramble(options = {}) {
24
24
  const state = (0, random_state_js_1.generateRandomCube3State)(options);
25
- const solution = await (0, index_js_1.solveCube3State)(state);
25
+ const solution = await (0, index_js_1.solveCube3State)(state, options.solver ?? {});
26
26
  const scrambleMoves = (0, moves_js_1.invertCube3Algorithm)(solution.moves);
27
27
  if (!options.skipValidation) {
28
28
  const scrambledState = (0, moves_js_1.applyCube3Algorithm)((0, state_js_1.createSolvedCube3State)(), scrambleMoves);
@@ -7,6 +7,7 @@ const compressed_fetch_js_1 = require("../../shared/compressed-fetch.js");
7
7
  const TABLES_DIR = "puzzles/cube3/solver/generated";
8
8
  const CACHE_NAME = "cubingcenter-scrambler-tables-v1";
9
9
  let browserTables;
10
+ let browserTablesPromise;
10
11
  function resolveBaseUrl(provided) {
11
12
  if (provided) {
12
13
  return provided.replace(/\/$/, "");
@@ -25,11 +26,28 @@ function getCachedBrowserTables() {
25
26
  }
26
27
  function clearCachedBrowserTables() {
27
28
  browserTables = undefined;
29
+ browserTablesPromise = undefined;
28
30
  }
29
31
  async function loadBrowserTables(baseUrl) {
30
32
  if (browserTables) {
31
33
  return browserTables;
32
34
  }
35
+ if (browserTablesPromise) {
36
+ // One shared in-flight promise: concurrent calls trigger exactly one
37
+ // fetch pass (dedup).
38
+ return browserTablesPromise;
39
+ }
40
+ const promise = fetchTables(baseUrl);
41
+ browserTablesPromise = promise;
42
+ promise.then(() => { }, () => {
43
+ // A failed load must not poison the dedup slot: purge the rejected
44
+ // promise so a later call retries from scratch.
45
+ if (browserTablesPromise === promise)
46
+ browserTablesPromise = undefined;
47
+ });
48
+ return promise;
49
+ }
50
+ async function fetchTables(baseUrl) {
33
51
  const resolvedBase = resolveBaseUrl(baseUrl);
34
52
  const manifest = await fetchManifest(resolvedBase);
35
53
  browserTables = {
@@ -63,19 +81,15 @@ async function fetchJointTables(baseUrl, entries) {
63
81
  return undefined;
64
82
  }
65
83
  const fetchJoint = async (entry) => {
84
+ // Verified by fetchTableBuffer with the SAME checks as node-loader.ts:
85
+ // nibble-packed byte length + FNV-1a over the packed bytes.
66
86
  const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, {
67
87
  cacheName: CACHE_NAME,
68
- compressed: entry.compressed !== false
88
+ compressed: entry.compressed !== false,
89
+ expectedBytes: Math.ceil((entry.rangeA * entry.rangeB) / 2),
90
+ expectedFnv1a: { value: entry.checksum, elementSize: 1 }
69
91
  });
70
- const expectedBytes = Math.ceil((entry.rangeA * entry.rangeB) / 2);
71
- if (buffer.byteLength !== expectedBytes) {
72
- throw new Error(`Invalid byte size for ${entry.file}.`);
73
- }
74
- const values = new Uint8Array(buffer);
75
- if (checksum(values) !== entry.checksum) {
76
- throw new Error(`Checksum mismatch for ${entry.file}.`);
77
- }
78
- return values;
92
+ return new Uint8Array(buffer);
79
93
  };
80
94
  return {
81
95
  jointUdCo: await fetchJoint(entries.udCo),
@@ -95,15 +109,23 @@ async function fetchManifest(baseUrl) {
95
109
  return manifest;
96
110
  }
97
111
  async function fetchMoveTable(baseUrl, entry) {
98
- const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, { cacheName: CACHE_NAME, compressed: entry.compressed !== false });
99
- const expectedBytes = entry.range * entry.moves.length * 4;
100
- if (buffer.byteLength !== expectedBytes) {
101
- throw new Error(`Invalid byte size for ${entry.coordinate} move table.`);
102
- }
103
- const values = new Int32Array(entry.range * entry.moves.length);
112
+ // Verified by fetchTableBuffer with the SAME checks as node-loader.ts:
113
+ // int32 wire length + FNV-1a over the decoded coordinate values (element
114
+ // size 4 reproduces node's hash over the uint16 values — same numbers).
115
+ const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, {
116
+ cacheName: CACHE_NAME,
117
+ compressed: entry.compressed !== false,
118
+ expectedBytes: entry.range * entry.moves.length * 4,
119
+ expectedFnv1a: { value: entry.checksum, elementSize: 4 }
120
+ });
121
+ const values = new Uint16Array(entry.range * entry.moves.length);
104
122
  const view = new DataView(buffer);
105
123
  for (let index = 0; index < values.length; index += 1) {
106
- values[index] = view.getInt32(index * 4, true);
124
+ const value = view.getInt32(index * 4, true);
125
+ if (value < 0 || value > 0xffff) {
126
+ throw new Error(`Move table value out of range for ${entry.coordinate}: ${value}.`);
127
+ }
128
+ values[index] = value;
107
129
  }
108
130
  return {
109
131
  coordinate: entry.coordinate,
@@ -115,11 +137,15 @@ async function fetchMoveTable(baseUrl, entry) {
115
137
  };
116
138
  }
117
139
  async function fetchPruningTable(baseUrl, entry) {
118
- const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, { cacheName: CACHE_NAME, compressed: entry.compressed !== false });
119
- const expectedBytes = Math.ceil(entry.range / 2);
120
- if (buffer.byteLength !== expectedBytes) {
121
- throw new Error(`Invalid byte size for ${entry.coordinate} pruning table.`);
122
- }
140
+ // Verified by fetchTableBuffer with the SAME checks as node-loader.ts:
141
+ // nibble-packed byte length + FNV-1a over the packed bytes (this browser
142
+ // path did NOT compare the pruning checksum before — only the length).
143
+ const buffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, TABLES_DIR, entry.file, {
144
+ cacheName: CACHE_NAME,
145
+ compressed: entry.compressed !== false,
146
+ expectedBytes: Math.ceil(entry.range / 2),
147
+ expectedFnv1a: { value: entry.checksum, elementSize: 1 }
148
+ });
123
149
  const values = new Uint8Array(buffer);
124
150
  return {
125
151
  coordinate: entry.coordinate,
@@ -37,10 +37,17 @@ exports.loadGeneratedCube3SolverTables = loadGeneratedCube3SolverTables;
37
37
  exports.loadGeneratedCube3Phase1Tables = loadGeneratedCube3Phase1Tables;
38
38
  exports.loadGeneratedCube3Phase2Tables = loadGeneratedCube3Phase2Tables;
39
39
  const browser_loader_js_1 = require("./browser-loader.js");
40
- let cachedGeneratedTables;
40
+ const load_once_js_1 = require("../../shared/load-once.js");
41
41
  function isBrowser() {
42
42
  return typeof self !== "undefined";
43
43
  }
44
+ // One shared in-flight promise for the node fs load: mixed concurrent
45
+ // phase-1 + phase-2 loads (separate slots in tables.ts) converge on ONE
46
+ // `readGeneratedCube3SolverTables()` call. The previous `cached ??= await …`
47
+ // raced — two callers interleaving on the await both ran `loadNodeTables()`
48
+ // (two filesystem reads, two table sets, last assignment wins). On
49
+ // rejection the slot is purged so the next call can retry.
50
+ const nodeTablesSlot = { promise: undefined };
44
51
  async function loadGeneratedCube3SolverTables() {
45
52
  if (isBrowser()) {
46
53
  const browserTables = (0, browser_loader_js_1.getCachedBrowserTables)();
@@ -51,8 +58,7 @@ async function loadGeneratedCube3SolverTables() {
51
58
  "In browser environments, call `initializeScrambler(baseUrl)` " +
52
59
  "before generating scrambles.");
53
60
  }
54
- cachedGeneratedTables ??= await loadNodeTables();
55
- return cachedGeneratedTables;
61
+ return (0, load_once_js_1.loadOnce)(nodeTablesSlot, loadNodeTables);
56
62
  }
57
63
  async function loadNodeTables() {
58
64
  const loader = await Promise.resolve().then(() => __importStar(require(