@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.
- package/README.md +17 -4
- package/dist/core/api.d.ts.map +1 -1
- package/dist/core/api.js +10 -2
- package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
- package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
- package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
- package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/search.js +3 -3
- package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
- package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/tables.js +16 -2
- package/dist/puzzles/cube3/index.d.ts +1 -0
- package/dist/puzzles/cube3/index.d.ts.map +1 -1
- package/dist/puzzles/cube3/moves.d.ts.map +1 -1
- package/dist/puzzles/cube3/moves.js +104 -18
- package/dist/puzzles/cube3/scrambler.d.ts +16 -1
- package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube3/scrambler.js +1 -1
- package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/browser-loader.js +48 -22
- package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
- package/dist/puzzles/cube3/solver/index.d.ts +5 -3
- package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/index.js +2 -1
- package/dist/puzzles/cube3/solver/node-loader.js +9 -2
- package/dist/puzzles/cube3/solver/phase1.d.ts +37 -0
- package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase1.js +156 -15
- package/dist/puzzles/cube3/solver/phase2.d.ts +75 -0
- package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase2.js +279 -68
- package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
- package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
- package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
- package/dist/puzzles/cube3/solver/symmetry.js +74 -0
- package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
- package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/tables.js +14 -12
- package/dist/puzzles/cube3/validation.d.ts.map +1 -1
- package/dist/puzzles/cube3/validation.js +15 -7
- package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
- package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
- package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
- package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
- package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
- package/dist/puzzles/cube4/browser-worker.js +31 -0
- package/dist/puzzles/cube4/index.d.ts +10 -2
- package/dist/puzzles/cube4/index.d.ts.map +1 -1
- package/dist/puzzles/cube4/index.js +6 -2
- package/dist/puzzles/cube4/moves.d.ts +5 -0
- package/dist/puzzles/cube4/moves.d.ts.map +1 -1
- package/dist/puzzles/cube4/moves.js +212 -11
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts +29 -1
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/browser-loader.js +227 -32
- package/dist/puzzles/cube4/reduction/center2.d.ts +44 -3
- package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/center2.js +152 -10
- package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
- package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/edge3.js +23 -3
- package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
- package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/finish3.js +393 -0
- package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
- package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
- package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +52 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.js +220 -0
- package/dist/puzzles/cube4/reduction/node-loader.d.ts +15 -1
- package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/node-loader.js +199 -25
- package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
- package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
- package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts +116 -7
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/phase3-search.js +458 -32
- package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
- package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
- package/dist/puzzles/cube4/reduction/solve.d.ts +172 -2
- package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/solve.js +1201 -180
- package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
- package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
- package/dist/puzzles/cube4/scramble-cache.js +150 -0
- package/dist/puzzles/cube4/scrambler.d.ts +201 -6
- package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube4/scrambler.js +414 -176
- package/dist/puzzles/cube4/state.d.ts.map +1 -1
- package/dist/puzzles/cube4/state.js +4 -1
- package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
- package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker-pool.js +405 -0
- package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
- package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker-protocol.js +58 -0
- package/dist/puzzles/cube4/worker.d.ts +26 -0
- package/dist/puzzles/cube4/worker.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker.js +49 -0
- package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
- package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
- package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
- package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/search.js +3 -6
- package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
- package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/tables.js +17 -0
- package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
- package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
- package/dist/puzzles/shared/compressed-fetch.js +226 -31
- package/dist/puzzles/shared/load-once.d.ts +16 -0
- package/dist/puzzles/shared/load-once.d.ts.map +1 -0
- package/dist/puzzles/shared/load-once.js +11 -0
- package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
- package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
- package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
- package/dist/puzzles/skewb/solver/search.js +3 -3
- package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
- package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/tables.js +17 -0
- package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
- package/dist-cjs/core/api.js +10 -2
- package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
- package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
- package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
- package/dist-cjs/puzzles/cube3/moves.js +104 -18
- package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
- package/dist-cjs/puzzles/cube3/solver/browser-loader.js +48 -22
- package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
- package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
- package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
- package/dist-cjs/puzzles/cube3/solver/phase1.js +157 -15
- package/dist-cjs/puzzles/cube3/solver/phase2.js +276 -65
- package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
- package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
- package/dist-cjs/puzzles/cube3/validation.js +15 -7
- package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
- package/dist-cjs/puzzles/cube4/index.js +23 -1
- package/dist-cjs/puzzles/cube4/moves.js +213 -11
- package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +227 -32
- package/dist-cjs/puzzles/cube4/reduction/center2.js +155 -10
- package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
- package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +232 -0
- package/dist-cjs/puzzles/cube4/reduction/node-loader.js +200 -25
- package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
- package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
- package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +460 -33
- package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
- package/dist-cjs/puzzles/cube4/reduction/solve.js +1203 -172
- package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
- package/dist-cjs/puzzles/cube4/scrambler.js +416 -174
- package/dist-cjs/puzzles/cube4/state.js +4 -1
- package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
- package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
- package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
- package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
- package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
- package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
- package/dist-cjs/puzzles/shared/load-once.js +14 -0
- package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
- package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
- package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
- package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
- package/docs/development.md +19 -7
- package/package.json +17 -7
|
@@ -36,8 +36,11 @@ export function cube4StatesEqual(a, b) {
|
|
|
36
36
|
typedArrayEqual(a.wp, b.wp) &&
|
|
37
37
|
typedArrayEqual(a.centers, b.centers));
|
|
38
38
|
}
|
|
39
|
+
// Read-only canonical solved state for comparisons (never mutated, so a
|
|
40
|
+
// single shared instance is safe and avoids allocating 4 arrays per call).
|
|
41
|
+
const SOLVED_STATE = createSolvedCube4State();
|
|
39
42
|
export function isSolvedCube4State(state) {
|
|
40
|
-
return cube4StatesEqual(state,
|
|
43
|
+
return cube4StatesEqual(state, SOLVED_STATE);
|
|
41
44
|
}
|
|
42
45
|
// ---------------------------------------------------------------------------
|
|
43
46
|
// Helpers
|
|
@@ -0,0 +1,139 @@
|
|
|
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 type { ScrambleResult } from "../../core/types.js";
|
|
39
|
+
import type { Cube4ScrambleOptions } from "./scrambler.js";
|
|
40
|
+
import { type Cube4WorkerGenerationOptions } from "./worker-protocol.js";
|
|
41
|
+
import type { ScrambleOptions } from "../../core/types.js";
|
|
42
|
+
/** Full option surface accepted by the pool and the batch helper. */
|
|
43
|
+
export type Cube4PoolOptions = Cube4WorkerGenerationOptions;
|
|
44
|
+
export interface Cube4WorkerPoolOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Maximum number of worker threads. Defaults to
|
|
47
|
+
* `os.availableParallelism() - 1` (min 1), resolved lazily so importing
|
|
48
|
+
* this module costs nothing.
|
|
49
|
+
*/
|
|
50
|
+
size?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Explicit path to the worker entry (`worker.ts` / `worker.js`).
|
|
53
|
+
* Auto-resolved normally; pass this only if the automatic location
|
|
54
|
+
* fails (e.g. a bundler renamed this module).
|
|
55
|
+
*/
|
|
56
|
+
workerPath?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Typed rejection used for post-terminate operations and shutdown itself. */
|
|
59
|
+
export declare class Cube4WorkerPoolClosedError extends Error {
|
|
60
|
+
readonly code: "cube4-pool-closed";
|
|
61
|
+
constructor(action?: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Batch seed derivation — byte-for-byte the semantics of
|
|
65
|
+
* `deriveBatchScrambleOptions` in `src/core/api.ts` (`generateScrambles`):
|
|
66
|
+
* with a `seed` and `count > 1`, item i uses `${seed}:${i}`; otherwise the
|
|
67
|
+
* options pass through untouched. Exported so tests can assert the pool
|
|
68
|
+
* batch equals the sequential/core batch for the same inputs.
|
|
69
|
+
*/
|
|
70
|
+
export declare function deriveBatchCube4Options(options: Cube4PoolOptions, index: number, count: number): Cube4PoolOptions;
|
|
71
|
+
export declare class Cube4WorkerPool {
|
|
72
|
+
private readonly workers;
|
|
73
|
+
private readonly queue;
|
|
74
|
+
private readonly explicitSize;
|
|
75
|
+
private readonly workerPathOption;
|
|
76
|
+
private sizePromise;
|
|
77
|
+
/** Last resolved size, kept sync-readable for the pump's headroom check. */
|
|
78
|
+
private sizeSync;
|
|
79
|
+
private pumping;
|
|
80
|
+
private terminatePromise;
|
|
81
|
+
private nextId;
|
|
82
|
+
private terminated;
|
|
83
|
+
constructor(options?: Cube4WorkerPoolOptions);
|
|
84
|
+
/** Maximum thread count (resolves the default lazily on first use). */
|
|
85
|
+
get size(): Promise<number>;
|
|
86
|
+
/** Threads spawned so far (lazily grown; 0 before the first job). */
|
|
87
|
+
get workerCount(): number;
|
|
88
|
+
get queuedCount(): number;
|
|
89
|
+
get busyCount(): number;
|
|
90
|
+
get closed(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Generates one scramble on an available thread. Rejects (typed) if the
|
|
93
|
+
* pool is closed, the worker crashes, the options cannot be
|
|
94
|
+
* structured-cloned, or generation fails inside the worker.
|
|
95
|
+
*/
|
|
96
|
+
generate(options?: Cube4PoolOptions): Promise<ScrambleResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Batch generation with EXACTLY the seed derivation of core
|
|
99
|
+
* `generateScrambles` (see `deriveBatchCube4Options`), so results are
|
|
100
|
+
* identical — move for move — to the sequential path for the same
|
|
101
|
+
* inputs. Order of the returned array matches request order regardless
|
|
102
|
+
* of completion order.
|
|
103
|
+
*/
|
|
104
|
+
generateBatch(count: number, options?: Cube4PoolOptions): Promise<ScrambleResult[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Prompt shutdown: rejects queued and in-flight tasks with
|
|
107
|
+
* `Cube4WorkerPoolClosedError`, terminates every thread and resolves
|
|
108
|
+
* when none is left. Idempotent.
|
|
109
|
+
*/
|
|
110
|
+
terminate(): Promise<void>;
|
|
111
|
+
private resolveSize;
|
|
112
|
+
/**
|
|
113
|
+
* Serialised dispatcher: grows the pool (lazy spawn), hands queued jobs
|
|
114
|
+
* to idle threads, and stops when every thread is busy at capacity.
|
|
115
|
+
* Re-entry while a spawn is awaited is a no-op — the running loop's
|
|
116
|
+
* `while` re-checks the queue, and every completion event (`response`,
|
|
117
|
+
* `crash`) calls `pump()` again.
|
|
118
|
+
*/
|
|
119
|
+
private pump;
|
|
120
|
+
private hasDispatchHeadroom;
|
|
121
|
+
private runPump;
|
|
122
|
+
private spawnWorker;
|
|
123
|
+
private dispatch;
|
|
124
|
+
private handleResponse;
|
|
125
|
+
private handleWorkerGone;
|
|
126
|
+
private resolveWorkerEntry;
|
|
127
|
+
}
|
|
128
|
+
/** Shared lazily-created pool (used by `generateCube4ScrambleBatch`). */
|
|
129
|
+
export declare function getDefaultCube4WorkerPool(): Cube4WorkerPool;
|
|
130
|
+
/** Terminates (and forgets) the shared pool — call on process shutdown. */
|
|
131
|
+
export declare function closeDefaultCube4WorkerPool(): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Batch convenience mirroring core `generateScrambles` for `444`:
|
|
134
|
+
* - `count <= 1` runs INLINE in the caller (no thread spawn, no cold
|
|
135
|
+
* start — a single scramble must never get slower);
|
|
136
|
+
* - `count > 1` fans out over the shared pool (same seed derivation).
|
|
137
|
+
*/
|
|
138
|
+
export declare function generateCube4ScrambleBatch(count: number, options?: ScrambleOptions & Cube4ScrambleOptions): Promise<ScrambleResult[]>;
|
|
139
|
+
//# sourceMappingURL=worker-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-pool.d.ts","sourceRoot":"","sources":["../../../src/puzzles/cube4/worker-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAEL,KAAK,4BAA4B,EAGlC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,8EAA8E;AAC9E,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAG,mBAAmB,CAAU;gBAEjC,MAAM,SAAc;CAIjC;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,gBAAgB,CAKlB;AAsED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,WAAW,CAA8B;IACjD,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,UAAU,CAAS;gBAEf,OAAO,GAAE,sBAA2B;IAWhD,uEAAuE;IACvE,IAAI,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAE1B;IAED,qEAAqE;IACrE,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;;;OAIG;IACH,QAAQ,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IAWjE;;;;;;OAMG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAWvF;;;;OAIG;IACH,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB1B,OAAO,CAAC,WAAW;IAmBnB;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;IAiBZ,OAAO,CAAC,mBAAmB;YAKb,OAAO;YAuBP,WAAW;IA6BzB,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,cAAc;IAetB,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,kBAAkB;CAkC3B;AAMD,yEAAyE;AACzE,wBAAgB,yBAAyB,IAAI,eAAe,CAG3D;AAED,2EAA2E;AAC3E,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC,CAIjE;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAe,GAAG,oBAAyB,GACnD,OAAO,CAAC,cAAc,EAAE,CAAC,CAQ3B"}
|
|
@@ -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"}
|