@cubingcenter/scrambler 0.7.0 → 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 (126) hide show
  1. package/README.md +15 -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/cube3/solver/browser-loader.d.ts.map +1 -1
  10. package/dist/puzzles/cube3/solver/browser-loader.js +42 -20
  11. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  12. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  13. package/dist/puzzles/cube3/solver/phase1.d.ts +19 -0
  14. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  15. package/dist/puzzles/cube3/solver/phase1.js +29 -6
  16. package/dist/puzzles/cube3/solver/phase2.d.ts +28 -0
  17. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  18. package/dist/puzzles/cube3/solver/phase2.js +56 -15
  19. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/solver/tables.js +14 -12
  21. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  22. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  23. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  24. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  25. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  26. package/dist/puzzles/cube4/browser-worker.js +31 -0
  27. package/dist/puzzles/cube4/index.d.ts +7 -0
  28. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  29. package/dist/puzzles/cube4/index.js +4 -0
  30. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  31. package/dist/puzzles/cube4/moves.js +122 -30
  32. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +26 -1
  33. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  34. package/dist/puzzles/cube4/reduction/browser-loader.js +212 -48
  35. package/dist/puzzles/cube4/reduction/center2.d.ts +24 -0
  36. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  37. package/dist/puzzles/cube4/reduction/center2.js +99 -1
  38. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  39. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  40. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  41. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  42. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +5 -3
  43. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -1
  44. package/dist/puzzles/cube4/reduction/hc-pdb.js +30 -12
  45. package/dist/puzzles/cube4/reduction/node-loader.d.ts +10 -1
  46. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  47. package/dist/puzzles/cube4/reduction/node-loader.js +170 -36
  48. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  49. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  51. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +33 -30
  52. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/reduction/phase3-search.js +211 -104
  54. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  55. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  56. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  57. package/dist/puzzles/cube4/reduction/solve.d.ts +72 -13
  58. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/solve.js +374 -92
  60. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  61. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  62. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  63. package/dist/puzzles/cube4/scrambler.d.ts +112 -18
  64. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/scrambler.js +271 -83
  66. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  67. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/worker-pool.js +405 -0
  69. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  70. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  71. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  72. package/dist/puzzles/cube4/worker.d.ts +26 -0
  73. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  74. package/dist/puzzles/cube4/worker.js +49 -0
  75. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  76. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  77. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  78. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  79. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  80. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  81. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  82. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  83. package/dist/puzzles/shared/load-once.d.ts +16 -0
  84. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  85. package/dist/puzzles/shared/load-once.js +11 -0
  86. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  87. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  88. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  89. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  90. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  91. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  92. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  93. package/dist-cjs/core/api.js +10 -2
  94. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  95. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  96. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +42 -20
  97. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  98. package/dist-cjs/puzzles/cube3/solver/phase1.js +29 -6
  99. package/dist-cjs/puzzles/cube3/solver/phase2.js +56 -15
  100. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  101. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  102. package/dist-cjs/puzzles/cube4/index.js +21 -1
  103. package/dist-cjs/puzzles/cube4/moves.js +122 -30
  104. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +212 -48
  105. package/dist-cjs/puzzles/cube4/reduction/center2.js +102 -2
  106. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  107. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  108. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +31 -12
  109. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +171 -36
  110. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  111. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +211 -104
  112. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  113. package/dist-cjs/puzzles/cube4/reduction/solve.js +374 -88
  114. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  115. package/dist-cjs/puzzles/cube4/scrambler.js +271 -80
  116. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  117. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  118. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  119. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  120. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  121. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  122. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  123. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  124. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  125. package/docs/development.md +19 -7
  126. package/package.json +4 -3
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Worker pool for 4x4 random-state scrambles (Node.js `worker_threads`).
3
+ *
4
+ * Why: generation is CPU-bound in a single event loop — a `Promise.all`
5
+ * batch of N scrambles does not scale. This pool spreads one batch across
6
+ * N threads so `generateScrambles`-style batches grow roughly linearly
7
+ * with core count, while keeping the (seed, complete-options) → moves
8
+ * contract byte-identical to the inline path: tasks forward the FULL
9
+ * options object verbatim and `worker.ts` calls the very same
10
+ * `generateCube4Scramble`.
11
+ *
12
+ * Lifecycle:
13
+ * - Lazy spawn: threads are created on demand (one per queued job, up to
14
+ * `size`), never in the constructor. A cold first generation pays for
15
+ * table loading (≈37 MB) per spawned thread, once; the thread then
16
+ * reuses its tables for every later job (per-thread promise cache inside
17
+ * `scrambler.ts`).
18
+ * - Idle threads are `unref()`d, so a pool that is never terminated does
19
+ * not keep the process alive; in-flight jobs `ref()` their thread so an
20
+ * awaited batch always completes.
21
+ * - One task in flight per thread; extra tasks queue FIFO.
22
+ * - `terminate()` rejects everything queued/in-flight with
23
+ * `Cube4WorkerPoolClosedError` and joins all threads — after it
24
+ * resolves there are no open handles.
25
+ *
26
+ * Failure: a crashed thread only fails ITS in-flight task and is dropped
27
+ * from the pool; the next dispatch spawns a fresh thread (which reloads
28
+ * tables). Typed solver errors from inside a worker arrive as an `Error`
29
+ * with the original `name`, `message` and `code` preserved (see
30
+ * `worker-protocol.ts`).
31
+ *
32
+ * Dual-build note: this file intentionally contains no bare `import.meta`
33
+ * (it must also compile in the CommonJS build that `index.ts` is part of).
34
+ * The worker entry is located from `__filename` (CJS) or this module's top
35
+ * stack frame (ESM), and `dist-cjs` falls back to the sibling ESM tree,
36
+ * which always ships `worker.js`.
37
+ */
38
+ import { InvalidCountError } from "../../core/errors.js";
39
+ import { generateCube4Scramble } from "./scrambler.js";
40
+ import { deserializeCube4Error } from "./worker-protocol.js";
41
+ /** Typed rejection used for post-terminate operations and shutdown itself. */
42
+ export class Cube4WorkerPoolClosedError extends Error {
43
+ code = "cube4-pool-closed";
44
+ constructor(action = "operation") {
45
+ super(`[cube4] worker pool is closed (${action} rejected).`);
46
+ this.name = "Cube4WorkerPoolClosedError";
47
+ }
48
+ }
49
+ /**
50
+ * Batch seed derivation — byte-for-byte the semantics of
51
+ * `deriveBatchScrambleOptions` in `src/core/api.ts` (`generateScrambles`):
52
+ * with a `seed` and `count > 1`, item i uses `${seed}:${i}`; otherwise the
53
+ * options pass through untouched. Exported so tests can assert the pool
54
+ * batch equals the sequential/core batch for the same inputs.
55
+ */
56
+ export function deriveBatchCube4Options(options, index, count) {
57
+ if (count === 1 || options.seed === undefined) {
58
+ return options;
59
+ }
60
+ return { ...options, seed: `${options.seed}:${index}` };
61
+ }
62
+ const TSX_ARGV = ["--import", "tsx"];
63
+ /**
64
+ * Locates this module's own file (extension included), dual-build safe:
65
+ * CJS knows it as `__filename`; in ESM we take the top stack frame, which
66
+ * V8 reports as the module URL (`file://…`) of `worker-pool.ts|js` — the
67
+ * path is correct whether or not source maps are enabled (they only move
68
+ * line/column, never the module URL).
69
+ */
70
+ function resolveCurrentModuleLocation() {
71
+ if (typeof __filename === "string" && __filename.length > 0) {
72
+ return __filename;
73
+ }
74
+ const holder = new Error();
75
+ Error.captureStackTrace?.(holder);
76
+ const raw = holder.stack;
77
+ const candidates = [];
78
+ if (typeof raw === "string") {
79
+ for (const line of raw.split("\n")) {
80
+ const match = /^\s*at (?:(?:.*?\s+\())?(.+?):\d+:\d+\)?$/.exec(line.trim());
81
+ if (match && match[1] !== undefined)
82
+ candidates.push(match[1]);
83
+ }
84
+ }
85
+ else if (Array.isArray(raw)) {
86
+ for (const frame of raw) {
87
+ const name = frame?.getFileName?.();
88
+ if (typeof name === "string")
89
+ candidates.push(name);
90
+ }
91
+ }
92
+ for (const candidate of candidates) {
93
+ if (candidate.includes("worker-pool.")) {
94
+ return candidate.startsWith("file://") ? fileUrlToPath(candidate) : candidate;
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ /** Minimal `file://` → path conversion (global `URL`, no node:url import). */
100
+ function fileUrlToPath(value) {
101
+ try {
102
+ let pathname = decodeURIComponent(new URL(value).pathname);
103
+ // Windows file URLs keep a leading slash before the drive letter.
104
+ if (/^\/[A-Za-z]:/.test(pathname))
105
+ pathname = pathname.slice(1);
106
+ return pathname;
107
+ }
108
+ catch {
109
+ return value.replace(/^file:\/\//, "");
110
+ }
111
+ }
112
+ export class Cube4WorkerPool {
113
+ workers = [];
114
+ queue = [];
115
+ explicitSize;
116
+ workerPathOption;
117
+ sizePromise;
118
+ /** Last resolved size, kept sync-readable for the pump's headroom check. */
119
+ sizeSync = 0;
120
+ pumping;
121
+ terminatePromise;
122
+ nextId = 0;
123
+ terminated = false;
124
+ constructor(options = {}) {
125
+ if (options.size !== undefined) {
126
+ if (!Number.isInteger(options.size) || options.size < 1) {
127
+ throw new RangeError(`[cube4] pool size must be an integer >= 1 (got ${options.size}).`);
128
+ }
129
+ this.sizeSync = options.size;
130
+ }
131
+ this.explicitSize = options.size;
132
+ this.workerPathOption = options.workerPath;
133
+ }
134
+ /** Maximum thread count (resolves the default lazily on first use). */
135
+ get size() {
136
+ return this.resolveSize();
137
+ }
138
+ /** Threads spawned so far (lazily grown; 0 before the first job). */
139
+ get workerCount() {
140
+ return this.workers.length;
141
+ }
142
+ get queuedCount() {
143
+ return this.queue.length;
144
+ }
145
+ get busyCount() {
146
+ return this.workers.reduce((n, rec) => n + (rec.task !== null ? 1 : 0), 0);
147
+ }
148
+ get closed() {
149
+ return this.terminated;
150
+ }
151
+ /**
152
+ * Generates one scramble on an available thread. Rejects (typed) if the
153
+ * pool is closed, the worker crashes, the options cannot be
154
+ * structured-cloned, or generation fails inside the worker.
155
+ */
156
+ generate(options = {}) {
157
+ if (this.terminated) {
158
+ return Promise.reject(new Cube4WorkerPoolClosedError("generate"));
159
+ }
160
+ const id = this.nextId++;
161
+ return new Promise((resolve, reject) => {
162
+ this.queue.push({ id, options, resolve, reject });
163
+ this.pump();
164
+ });
165
+ }
166
+ /**
167
+ * Batch generation with EXACTLY the seed derivation of core
168
+ * `generateScrambles` (see `deriveBatchCube4Options`), so results are
169
+ * identical — move for move — to the sequential path for the same
170
+ * inputs. Order of the returned array matches request order regardless
171
+ * of completion order.
172
+ */
173
+ generateBatch(count, options = {}) {
174
+ if (!Number.isInteger(count) || count < 1) {
175
+ return Promise.reject(new InvalidCountError(count));
176
+ }
177
+ return Promise.all(Array.from({ length: count }, (_, index) => this.generate(deriveBatchCube4Options(options, index, count))));
178
+ }
179
+ /**
180
+ * Prompt shutdown: rejects queued and in-flight tasks with
181
+ * `Cube4WorkerPoolClosedError`, terminates every thread and resolves
182
+ * when none is left. Idempotent.
183
+ */
184
+ terminate() {
185
+ if (this.terminatePromise !== undefined)
186
+ return this.terminatePromise;
187
+ this.terminated = true;
188
+ for (const task of this.queue.splice(0)) {
189
+ task.reject(new Cube4WorkerPoolClosedError("queued task"));
190
+ }
191
+ const records = this.workers.splice(0);
192
+ const joins = [];
193
+ for (const rec of records) {
194
+ if (rec.task !== null) {
195
+ rec.task.reject(new Cube4WorkerPoolClosedError("in-flight task"));
196
+ rec.task = null;
197
+ }
198
+ joins.push(rec.worker.terminate().then(() => undefined));
199
+ }
200
+ this.terminatePromise = Promise.all(joins).then(() => undefined);
201
+ return this.terminatePromise;
202
+ }
203
+ // ── internals ────────────────────────────────────────────────────────
204
+ resolveSize() {
205
+ if (this.explicitSize !== undefined)
206
+ return Promise.resolve(this.explicitSize);
207
+ this.sizePromise ??= (async () => {
208
+ try {
209
+ // @vite-ignore: Node-only; the browser bundle never executes this.
210
+ const os = await import(/* webpackIgnore: true */ /* @vite-ignore */ "node:os");
211
+ const parallelism = typeof os.availableParallelism === "function"
212
+ ? os.availableParallelism()
213
+ : os.cpus().length;
214
+ this.sizeSync = Math.max(1, parallelism - 1);
215
+ }
216
+ catch {
217
+ this.sizeSync = 4;
218
+ }
219
+ return this.sizeSync;
220
+ })();
221
+ return this.sizePromise;
222
+ }
223
+ /**
224
+ * Serialised dispatcher: grows the pool (lazy spawn), hands queued jobs
225
+ * to idle threads, and stops when every thread is busy at capacity.
226
+ * Re-entry while a spawn is awaited is a no-op — the running loop's
227
+ * `while` re-checks the queue, and every completion event (`response`,
228
+ * `crash`) calls `pump()` again.
229
+ */
230
+ pump() {
231
+ if (this.pumping !== undefined || this.terminated)
232
+ return;
233
+ this.pumping = this.runPump()
234
+ .catch(() => undefined) // runPump handles failures per task; belt and braces
235
+ .finally(() => {
236
+ this.pumping = undefined;
237
+ // A task pushed by a microtask that landed between runPump's last
238
+ // queue check and this finally would have been skipped (pump was
239
+ // still "running"). Re-pump only when dispatch can make actual
240
+ // progress — an idle thread or spawn headroom — so tasks parked at
241
+ // capacity (which a completion event will pump) cannot spin here.
242
+ if (this.queue.length > 0 && !this.terminated && this.hasDispatchHeadroom()) {
243
+ this.pump();
244
+ }
245
+ });
246
+ }
247
+ hasDispatchHeadroom() {
248
+ if (this.workers.some((rec) => rec.task === null))
249
+ return true;
250
+ return this.sizeSync > 0 && this.workers.length < this.sizeSync;
251
+ }
252
+ async runPump() {
253
+ while (!this.terminated && this.queue.length > 0) {
254
+ const idle = this.workers.find((rec) => rec.task === null);
255
+ if (idle !== undefined) {
256
+ this.dispatch(idle, this.queue.shift());
257
+ continue;
258
+ }
259
+ if (this.workers.length < (await this.resolveSize())) {
260
+ let rec;
261
+ try {
262
+ rec = await this.spawnWorker();
263
+ }
264
+ catch (error) {
265
+ // Spawning failed (missing entry, bad workerPath, …): fail THIS
266
+ // task and keep trying for the rest of the queue.
267
+ this.queue.shift()?.reject(error);
268
+ continue;
269
+ }
270
+ continue; // the fresh thread is idle → dispatched on the next pass
271
+ }
272
+ break; // at capacity with every thread busy — wait for a response
273
+ }
274
+ }
275
+ async spawnWorker() {
276
+ // @vite-ignore: Node-only; never executed in a browser bundle.
277
+ const { Worker: WorkerCtor } = await import(
278
+ /* webpackIgnore: true */ /* @vite-ignore */ "node:worker_threads");
279
+ if (this.terminated)
280
+ throw new Cube4WorkerPoolClosedError("spawn");
281
+ const entry = this.resolveWorkerEntry();
282
+ const worker = new WorkerCtor(entry.path, { execArgv: [...entry.execArgv] });
283
+ const rec = { worker, task: null };
284
+ worker.on("message", (response) => {
285
+ this.handleResponse(rec, response);
286
+ });
287
+ worker.on("error", (error) => {
288
+ this.handleWorkerGone(rec, error);
289
+ });
290
+ worker.on("exit", (code) => {
291
+ this.handleWorkerGone(rec, new Error(`[cube4] worker exited unexpectedly (code ${code}).`));
292
+ });
293
+ this.workers.push(rec);
294
+ worker.unref(); // idle threads must not keep the process alive
295
+ return rec;
296
+ }
297
+ dispatch(rec, task) {
298
+ rec.task = task;
299
+ rec.worker.ref(); // an awaited job keeps the event loop alive
300
+ const message = { id: task.id, options: task.options };
301
+ try {
302
+ rec.worker.postMessage(message);
303
+ }
304
+ catch (error) {
305
+ // e.g. DataCloneError for non-cloneable options.metadata — the
306
+ // thread is fine; fail only this task.
307
+ rec.task = null;
308
+ rec.worker.unref();
309
+ task.reject(error);
310
+ this.pump();
311
+ }
312
+ }
313
+ handleResponse(rec, response) {
314
+ const task = rec.task;
315
+ if (task === null || task.id !== response.id)
316
+ return; // stale/unknown id
317
+ rec.task = null;
318
+ if (this.terminated)
319
+ return;
320
+ if (response.ok)
321
+ task.resolve(response.result);
322
+ else
323
+ task.reject(deserializeCube4Error(response.error));
324
+ const next = this.queue.shift();
325
+ if (next !== undefined)
326
+ this.dispatch(rec, next);
327
+ else
328
+ rec.worker.unref();
329
+ this.pump();
330
+ }
331
+ handleWorkerGone(rec, error) {
332
+ const index = this.workers.indexOf(rec);
333
+ if (index === -1)
334
+ return; // already removed ('error' precedes 'exit')
335
+ this.workers.splice(index, 1);
336
+ const task = rec.task;
337
+ rec.task = null;
338
+ if (task !== null && !this.terminated) {
339
+ // Only THIS thread's job fails; the rest of the queue keeps running
340
+ // (a replacement thread is spawned lazily by pump()).
341
+ task.reject(error);
342
+ }
343
+ if (!this.terminated)
344
+ this.pump();
345
+ }
346
+ resolveWorkerEntry() {
347
+ if (this.workerPathOption !== undefined) {
348
+ return {
349
+ path: this.workerPathOption,
350
+ execArgv: this.workerPathOption.endsWith(".ts") ? [...TSX_ARGV] : []
351
+ };
352
+ }
353
+ const hereRaw = resolveCurrentModuleLocation();
354
+ if (hereRaw === undefined) {
355
+ throw new Error("[cube4] cannot locate worker.ts/worker.js next to worker-pool.js; " +
356
+ "pass an explicit `workerPath` to Cube4WorkerPool.");
357
+ }
358
+ // Normalise Windows separators: forward slashes are valid in every
359
+ // Node path/URL consumer here (Worker entry, fs lookups by Node).
360
+ const here = hereRaw.replace(/\\/g, "/");
361
+ const separator = here.lastIndexOf("/");
362
+ const dir = separator === -1 ? "." : here.slice(0, separator);
363
+ if (here.endsWith(".ts")) {
364
+ // Running from source under tsx → the thread needs the tsx loader.
365
+ return { path: `${dir}/worker.ts`, execArgv: [...TSX_ARGV] };
366
+ }
367
+ const distCjs = dir.lastIndexOf("/dist-cjs");
368
+ if (distCjs !== -1) {
369
+ // The CJS build excludes the worker entry (it is Node-ESM-only); the
370
+ // ESM tree is always published alongside and always contains it.
371
+ const esmDir = `${dir.slice(0, distCjs)}/dist${dir.slice(distCjs + "/dist-cjs".length)}`;
372
+ return { path: `${esmDir}/worker.js`, execArgv: [] };
373
+ }
374
+ return { path: `${dir}/worker.js`, execArgv: [] };
375
+ }
376
+ }
377
+ // ── module-level conveniences ──────────────────────────────────────────
378
+ let defaultPool;
379
+ /** Shared lazily-created pool (used by `generateCube4ScrambleBatch`). */
380
+ export function getDefaultCube4WorkerPool() {
381
+ defaultPool ??= new Cube4WorkerPool();
382
+ return defaultPool;
383
+ }
384
+ /** Terminates (and forgets) the shared pool — call on process shutdown. */
385
+ export async function closeDefaultCube4WorkerPool() {
386
+ const pool = defaultPool;
387
+ defaultPool = undefined;
388
+ if (pool !== undefined)
389
+ await pool.terminate();
390
+ }
391
+ /**
392
+ * Batch convenience mirroring core `generateScrambles` for `444`:
393
+ * - `count <= 1` runs INLINE in the caller (no thread spawn, no cold
394
+ * start — a single scramble must never get slower);
395
+ * - `count > 1` fans out over the shared pool (same seed derivation).
396
+ */
397
+ export async function generateCube4ScrambleBatch(count, options = {}) {
398
+ if (!Number.isInteger(count) || count < 1) {
399
+ throw new InvalidCountError(count);
400
+ }
401
+ if (count === 1) {
402
+ return [await generateCube4Scramble(options)];
403
+ }
404
+ return getDefaultCube4WorkerPool().generateBatch(count, options);
405
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Message protocol shared by the 4x4 Node worker (`worker.ts`), its pool
3
+ * (`worker-pool.ts`), and the browser equivalents (`browser-worker.ts`,
4
+ * `browser-worker-pool.ts`).
5
+ *
6
+ * Design notes:
7
+ * - Tasks carry the FULL `generateCube4Scramble` options object. The
8
+ * scrambler is a pure function of (seed, complete options), so forwarding
9
+ * every option verbatim is what makes the pool output bit-identical to
10
+ * the inline path for the same inputs.
11
+ * - Errors cross the thread boundary as data (`Cube4SerializedError`):
12
+ * `name`, `message`, `code` and `stack` survive, so typed solver errors
13
+ * (e.g. `Cube4TargetMismatchError` with `code: "cube4-target-mismatch"`)
14
+ * remain discriminated on the consumer side by name/code — the same
15
+ * classification keys `scrambler.ts` itself uses. Classes cannot be
16
+ * structured-cloned, so `instanceof` does not cross the boundary
17
+ * (documented contract).
18
+ *
19
+ * This module is deliberately free of runtime imports (type-only imports
20
+ * are erased at emit) so it can be bundled for the browser worker without
21
+ * dragging Node-only code into the graph.
22
+ */
23
+ import type { ScrambleOptions, ScrambleResult } from "../../core/types.js";
24
+ import type { Cube4ScrambleOptions } from "./scrambler.js";
25
+ /** Complete options accepted by a worker task. */
26
+ export type Cube4WorkerGenerationOptions = ScrambleOptions & Cube4ScrambleOptions;
27
+ export interface Cube4WorkerTask {
28
+ id: number;
29
+ options: Cube4WorkerGenerationOptions;
30
+ }
31
+ export interface Cube4SerializedError {
32
+ name: string;
33
+ message: string;
34
+ /** Preserved when primitive (string | number); `undefined` otherwise. */
35
+ code?: string | number;
36
+ stack?: string;
37
+ }
38
+ export type Cube4WorkerResponse = {
39
+ id: number;
40
+ ok: true;
41
+ result: ScrambleResult;
42
+ } | {
43
+ id: number;
44
+ ok: false;
45
+ error: Cube4SerializedError;
46
+ };
47
+ /**
48
+ * Flattens an unknown thrown value into the structured-clone-safe shape
49
+ * above. Non-Error values are stringified so nothing is lost silently.
50
+ */
51
+ export declare function serializeCube4Error(error: unknown): Cube4SerializedError;
52
+ /**
53
+ * Rebuilds an Error from the wire shape, restoring `name`, `code` and
54
+ * `stack` (stack kept only as a debugging aid — its file locations point
55
+ * into the worker thread).
56
+ */
57
+ export declare function deserializeCube4Error(serialized: Cube4SerializedError): Error;
58
+ //# sourceMappingURL=worker-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-protocol.d.ts","sourceRoot":"","sources":["../../../src/puzzles/cube4/worker-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,kDAAkD;AAClD,MAAM,MAAM,4BAA4B,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAElF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,4BAA4B,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GAChD;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAcxE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,oBAAoB,GAAG,KAAK,CAU7E"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Message protocol shared by the 4x4 Node worker (`worker.ts`), its pool
3
+ * (`worker-pool.ts`), and the browser equivalents (`browser-worker.ts`,
4
+ * `browser-worker-pool.ts`).
5
+ *
6
+ * Design notes:
7
+ * - Tasks carry the FULL `generateCube4Scramble` options object. The
8
+ * scrambler is a pure function of (seed, complete options), so forwarding
9
+ * every option verbatim is what makes the pool output bit-identical to
10
+ * the inline path for the same inputs.
11
+ * - Errors cross the thread boundary as data (`Cube4SerializedError`):
12
+ * `name`, `message`, `code` and `stack` survive, so typed solver errors
13
+ * (e.g. `Cube4TargetMismatchError` with `code: "cube4-target-mismatch"`)
14
+ * remain discriminated on the consumer side by name/code — the same
15
+ * classification keys `scrambler.ts` itself uses. Classes cannot be
16
+ * structured-cloned, so `instanceof` does not cross the boundary
17
+ * (documented contract).
18
+ *
19
+ * This module is deliberately free of runtime imports (type-only imports
20
+ * are erased at emit) so it can be bundled for the browser worker without
21
+ * dragging Node-only code into the graph.
22
+ */
23
+ /**
24
+ * Flattens an unknown thrown value into the structured-clone-safe shape
25
+ * above. Non-Error values are stringified so nothing is lost silently.
26
+ */
27
+ export function serializeCube4Error(error) {
28
+ if (error instanceof Error) {
29
+ const code = error.code;
30
+ const primitiveCode = typeof code === "string" || typeof code === "number" ? code : undefined;
31
+ const serialized = {
32
+ name: error.name,
33
+ message: error.message
34
+ };
35
+ if (primitiveCode !== undefined)
36
+ serialized.code = primitiveCode;
37
+ if (typeof error.stack === "string")
38
+ serialized.stack = error.stack;
39
+ return serialized;
40
+ }
41
+ return { name: "Error", message: String(error) };
42
+ }
43
+ /**
44
+ * Rebuilds an Error from the wire shape, restoring `name`, `code` and
45
+ * `stack` (stack kept only as a debugging aid — its file locations point
46
+ * into the worker thread).
47
+ */
48
+ export function deserializeCube4Error(serialized) {
49
+ const error = new Error(serialized.message);
50
+ error.name = serialized.name;
51
+ if (serialized.code !== undefined) {
52
+ error.code = serialized.code;
53
+ }
54
+ if (serialized.stack !== undefined) {
55
+ error.stack = serialized.stack;
56
+ }
57
+ return error;
58
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Node.js worker_thread entry for 4x4 scramble generation.
3
+ *
4
+ * Lifecycle / lazy init: the thread boots without doing any work; the
5
+ * reduction + 3x3 tables (≈37 MB) are loaded on the FIRST task and then
6
+ * reused for every later task, because `scrambler.ts` deduplicates table
7
+ * loads per thread via its promise cache. Spawning is driven lazily by
8
+ * `worker-pool.ts` (one thread per queued job, up to the pool size), so a
9
+ * small batch never pays for idle threads.
10
+ *
11
+ * Protocol: see `worker-protocol.ts`. One task in flight per thread (the
12
+ * pool only re-dispatches after a response), but the handler is written to
13
+ * cope with concurrent messages just in case.
14
+ *
15
+ * Source-vs-dist loading: this file is excluded from the CommonJS build,
16
+ * so `import.meta` is safe here. When running from source the thread is
17
+ * booted with `execArgv: ["--import", "tsx"]` by the pool, but that flag
18
+ * alone does NOT register tsx's resolve hooks inside a worker thread
19
+ * (measured: `./x.js` → `./x.ts` mapping missing). Therefore, before any
20
+ * value import of the repo's `.js`-suffixed specifiers, we register the
21
+ * tsx hooks explicitly — `tsx/esm/api` resolves because source mode only
22
+ * happens inside a checkout with node_modules. From `dist/` no tsx is
23
+ * involved: the `.js` files are real.
24
+ */
25
+ export {};
26
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/puzzles/cube4/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Node.js worker_thread entry for 4x4 scramble generation.
3
+ *
4
+ * Lifecycle / lazy init: the thread boots without doing any work; the
5
+ * reduction + 3x3 tables (≈37 MB) are loaded on the FIRST task and then
6
+ * reused for every later task, because `scrambler.ts` deduplicates table
7
+ * loads per thread via its promise cache. Spawning is driven lazily by
8
+ * `worker-pool.ts` (one thread per queued job, up to the pool size), so a
9
+ * small batch never pays for idle threads.
10
+ *
11
+ * Protocol: see `worker-protocol.ts`. One task in flight per thread (the
12
+ * pool only re-dispatches after a response), but the handler is written to
13
+ * cope with concurrent messages just in case.
14
+ *
15
+ * Source-vs-dist loading: this file is excluded from the CommonJS build,
16
+ * so `import.meta` is safe here. When running from source the thread is
17
+ * booted with `execArgv: ["--import", "tsx"]` by the pool, but that flag
18
+ * alone does NOT register tsx's resolve hooks inside a worker thread
19
+ * (measured: `./x.js` → `./x.ts` mapping missing). Therefore, before any
20
+ * value import of the repo's `.js`-suffixed specifiers, we register the
21
+ * tsx hooks explicitly — `tsx/esm/api` resolves because source mode only
22
+ * happens inside a checkout with node_modules. From `dist/` no tsx is
23
+ * involved: the `.js` files are real.
24
+ */
25
+ import { parentPort } from "node:worker_threads";
26
+ if (!parentPort) {
27
+ throw new Error("[cube4] worker.ts must be started as a worker_thread (use Cube4WorkerPool).");
28
+ }
29
+ const port = parentPort;
30
+ if (import.meta.url.endsWith(".ts")) {
31
+ const tsxApi = await import("tsx/esm/api");
32
+ tsxApi.register();
33
+ }
34
+ const { generateCube4Scramble } = await import("./scrambler.js");
35
+ const { serializeCube4Error } = await import("./worker-protocol.js");
36
+ port.on("message", (task) => {
37
+ void (async () => {
38
+ const response = await (async () => {
39
+ try {
40
+ const result = await generateCube4Scramble(task.options);
41
+ return { id: task.id, ok: true, result };
42
+ }
43
+ catch (error) {
44
+ return { id: task.id, ok: false, error: serializeCube4Error(error) };
45
+ }
46
+ })();
47
+ port.postMessage(response);
48
+ })();
49
+ });
@@ -1,14 +1,10 @@
1
- export declare function getCachedBrowserTables(): {
1
+ export interface PyraminxBrowserTables {
2
2
  centersTable: Uint8Array;
3
3
  permutationTable: Uint16Array;
4
4
  orientationTable: Uint8Array;
5
5
  policyTable: Uint8Array;
6
- } | undefined;
6
+ }
7
+ export declare function getCachedBrowserTables(): PyraminxBrowserTables | undefined;
7
8
  export declare function clearCachedBrowserTables(): void;
8
- export declare function loadBrowserTables(baseUrl?: string): Promise<{
9
- centersTable: Uint8Array;
10
- permutationTable: Uint16Array;
11
- orientationTable: Uint8Array;
12
- policyTable: Uint8Array;
13
- }>;
9
+ export declare function loadBrowserTables(baseUrl?: string): Promise<PyraminxBrowserTables>;
14
10
  //# sourceMappingURL=browser-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser-loader.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/pyraminx/solver/browser-loader.ts"],"names":[],"mappings":"AAUA,wBAAgB,sBAAsB;kBANtB,UAAU;sBACN,WAAW;sBACX,UAAU;iBACf,UAAU;cAKxB;AACD,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAsB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACjE,YAAY,EAAE,UAAU,CAAC;IACzB,gBAAgB,EAAE,WAAW,CAAC;IAC9B,gBAAgB,EAAE,UAAU,CAAC;IAC7B,WAAW,EAAE,UAAU,CAAC;CACzB,CAAC,CAYD"}
1
+ {"version":3,"file":"browser-loader.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/pyraminx/solver/browser-loader.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,UAAU,CAAC;IACzB,gBAAgB,EAAE,WAAW,CAAC;IAC9B,gBAAgB,EAAE,UAAU,CAAC;IAC7B,WAAW,EAAE,UAAU,CAAC;CACzB;AAKD,wBAAgB,sBAAsB,IAAI,qBAAqB,GAAG,SAAS,CAE1E;AAED,wBAAgB,wBAAwB,IAAI,IAAI,CAG/C;AAED,wBAAsB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAkBxF"}