@pixagram/pixahash 0.1.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/pool.js ADDED
@@ -0,0 +1,193 @@
1
+ // @pixagram/pixahash/pool — data-parallel hashing across a pool of workers.
2
+ //
3
+ // For a single 5–30 kB hash the worker round-trip costs more than the hash
4
+ // itself (microseconds). A pool wins when you (a) want to keep the UI thread
5
+ // free during a burst of work, or (b) need to hash *many* items in parallel.
6
+ //
7
+ // import { createPool } from '@pixagram/pixahash/pool';
8
+ // const pool = await createPool(); // size = CPU count
9
+ // const id = await pool.hash128(bytes); // -> BigInt
10
+ // const ids = await Promise.all(items.map((x) => pool.hashHex(x, 0, 256)));
11
+ // pool.destroy();
12
+ //
13
+ // Data is copied to the worker by default (your buffers stay intact). Pass
14
+ // `{ transfer: true }` at construction to move buffers instead (faster for
15
+ // large inputs, but it neuters the caller's ArrayBuffer).
16
+
17
+ import { toSeed64 } from "./index.js";
18
+
19
+ const isNode =
20
+ typeof process !== "undefined" &&
21
+ process.versions != null &&
22
+ process.versions.node != null;
23
+
24
+ const workerURL = new URL("./worker.js", import.meta.url);
25
+
26
+ function defaultSize() {
27
+ if (!isNode && typeof navigator !== "undefined" && navigator.hardwareConcurrency) {
28
+ return Math.max(1, navigator.hardwareConcurrency);
29
+ }
30
+ if (isNode) {
31
+ // Lazy: avoid a static import of os just for this.
32
+ try {
33
+ // eslint-disable-next-line no-undef
34
+ return Math.max(1, require("node:os").cpus().length);
35
+ } catch {
36
+ /* ESM: fall through */
37
+ }
38
+ }
39
+ return 4;
40
+ }
41
+
42
+ async function spawn(onMessage, onError) {
43
+ if (isNode) {
44
+ const { Worker } = await import("node:worker_threads");
45
+ const wk = new Worker(workerURL);
46
+ wk.on("message", onMessage);
47
+ wk.on("error", onError);
48
+ return {
49
+ post: (msg, transfer) => wk.postMessage(msg, transfer),
50
+ terminate: () => wk.terminate(),
51
+ };
52
+ }
53
+ const wk = new Worker(workerURL, { type: "module" });
54
+ wk.onmessage = (e) => onMessage(e.data);
55
+ wk.onerror = onError;
56
+ return {
57
+ post: (msg, transfer) => wk.postMessage(msg, transfer),
58
+ terminate: () => wk.terminate(),
59
+ };
60
+ }
61
+
62
+ export class HashPool {
63
+ constructor(opts = {}) {
64
+ this.size = Math.max(1, opts.size || defaultSize());
65
+ this.transfer = !!opts.transfer;
66
+ this._workers = [];
67
+ this._idle = [];
68
+ this._queue = [];
69
+ this._pending = new Map(); // id -> { resolve, reject }
70
+ this._seq = 0;
71
+ this._started = null;
72
+ this._destroyed = false;
73
+ }
74
+
75
+ /** Spawn the workers. Idempotent; awaited automatically by the hash methods. */
76
+ start() {
77
+ if (this._started) return this._started;
78
+ this._started = (async () => {
79
+ const onError = (err) => this._failAll(err);
80
+ for (let i = 0; i < this.size; i++) {
81
+ const worker = await spawn((m) => this._onMessage(worker, m), onError);
82
+ worker.busy = false;
83
+ this._workers.push(worker);
84
+ this._idle.push(worker);
85
+ }
86
+ this._drain();
87
+ })();
88
+ return this._started;
89
+ }
90
+
91
+ _onMessage(worker, msg) {
92
+ const entry = this._pending.get(msg.id);
93
+ if (entry) {
94
+ this._pending.delete(msg.id);
95
+ if (msg.error) entry.reject(new Error(msg.error));
96
+ else entry.resolve(msg.result);
97
+ }
98
+ worker.busy = false;
99
+ this._idle.push(worker);
100
+ this._drain();
101
+ }
102
+
103
+ _failAll(err) {
104
+ const e = err instanceof Error ? err : new Error(String(err));
105
+ for (const { reject } of this._pending.values()) reject(e);
106
+ this._pending.clear();
107
+ for (const t of this._queue.splice(0)) t.reject(e);
108
+ }
109
+
110
+ _drain() {
111
+ while (this._idle.length && this._queue.length) {
112
+ const worker = this._idle.pop();
113
+ const task = this._queue.shift();
114
+ worker.busy = true;
115
+ this._pending.set(task.id, task);
116
+ worker.post(task.msg, task.transfer);
117
+ }
118
+ }
119
+
120
+ _submit(op, data, seed, bits, extra) {
121
+ if (this._destroyed) {
122
+ return Promise.reject(new Error("pixahash: pool has been destroyed."));
123
+ }
124
+ const id = ++this._seq;
125
+ const msg = { id, op, data, seed: toSeed64(seed), bits, ...extra };
126
+
127
+ let transfer;
128
+ if (this.transfer) {
129
+ if (data instanceof ArrayBuffer) transfer = [data];
130
+ else if (ArrayBuffer.isView(data)) transfer = [data.buffer];
131
+ }
132
+
133
+ const p = new Promise((resolve, reject) => {
134
+ this._queue.push({ id, msg, transfer, resolve, reject });
135
+ });
136
+ // Ensure workers exist, then dispatch.
137
+ this.start().then(() => this._drain());
138
+ return p;
139
+ }
140
+
141
+ hash32(data, seed) {
142
+ return this._submit("hash32", data, seed);
143
+ }
144
+ hash64(data, seed) {
145
+ return this._submit("hash64", data, seed);
146
+ }
147
+ hash128(data, seed) {
148
+ return this._submit("hash128", data, seed);
149
+ }
150
+ hashHex(data, seed, bits) {
151
+ return this._submit("hashHex", data, seed, bits);
152
+ }
153
+ hashBase58(data, seed, bits) {
154
+ return this._submit("hashBase58", data, seed, bits);
155
+ }
156
+ digest(data, seed) {
157
+ return this._submit("digest", data, seed);
158
+ }
159
+
160
+ /**
161
+ * Hash an array of items in a single round-trip on one worker. Cheaper than
162
+ * one message per item when you have many small inputs; less parallel than
163
+ * dispatching them individually. `op` defaults to "hash64".
164
+ */
165
+ hashMany(items, { op = "hash64", seed, bits } = {}) {
166
+ return this._submit("hashMany", items, seed, bits, { innerOp: op });
167
+ }
168
+
169
+ /** Terminate all workers and reject anything still in flight. */
170
+ async destroy() {
171
+ if (this._destroyed) return;
172
+ this._destroyed = true;
173
+ this._failAll(new Error("pixahash: pool destroyed."));
174
+ await this.start().catch(() => {});
175
+ await Promise.all(this._workers.map((w) => w.terminate()));
176
+ this._workers = [];
177
+ this._idle = [];
178
+ }
179
+
180
+ /** Alias for destroy(). */
181
+ terminate() {
182
+ return this.destroy();
183
+ }
184
+ }
185
+
186
+ /** Construct and fully start a HashPool. */
187
+ export async function createPool(opts = {}) {
188
+ const pool = new HashPool(opts);
189
+ await pool.start();
190
+ return pool;
191
+ }
192
+
193
+ export default { HashPool, createPool };
package/worker.js ADDED
@@ -0,0 +1,70 @@
1
+ // @pixagram/pixahash/worker — isomorphic worker entry.
2
+ //
3
+ // Runs hashing off the calling thread. Used by HashPool, but you can also drive
4
+ // it directly. Works as a browser Module Worker and as a Node worker_threads
5
+ // worker. Protocol (one message per request):
6
+ //
7
+ // in: { id, op, data, seed, bits }
8
+ // out: { id, result } on success
9
+ // { id, error: string } on failure
10
+ //
11
+ // `op` is one of: hash32 | hash64 | hash128 | hashHex | hashBase58 | digest
12
+ // | hashMany (data = array of items; result = array)
13
+ // `data` is a string | ArrayBuffer | Uint8Array (or array thereof for hashMany).
14
+
15
+ import { ready, hash32, hash64, hash128, hashHex, hashBase58, digest } from "./index.js";
16
+
17
+ const OPS = {
18
+ hash32: (d, s) => hash32(d, s),
19
+ hash64: (d, s) => hash64(d, s),
20
+ hash128: (d, s) => hash128(d, s),
21
+ hashHex: (d, s, b) => hashHex(d, s, b),
22
+ hashBase58: (d, s, b) => hashBase58(d, s, b),
23
+ digest: (d, s) => digest(d, s),
24
+ };
25
+
26
+ const isNode =
27
+ typeof process !== "undefined" &&
28
+ process.versions != null &&
29
+ process.versions.node != null;
30
+
31
+ let post;
32
+
33
+ if (isNode) {
34
+ const { parentPort } = await import("node:worker_threads");
35
+ post = (msg, transfer) => parentPort.postMessage(msg, transfer);
36
+ parentPort.on("message", handle);
37
+ } else {
38
+ post = (msg, transfer) => self.postMessage(msg, transfer);
39
+ self.onmessage = (e) => handle(e.data);
40
+ }
41
+
42
+ // Pre-warm so the first real request doesn't pay the init cost.
43
+ ready();
44
+
45
+ function run(op, data, seed, bits) {
46
+ const fn = OPS[op];
47
+ if (!fn) throw new Error(`pixahash worker: unknown op "${op}"`);
48
+ return fn(data, seed, bits);
49
+ }
50
+
51
+ async function handle(msg) {
52
+ const { id, op, data, seed, bits } = msg;
53
+ try {
54
+ await ready();
55
+ let result;
56
+ if (op === "hashMany") {
57
+ const inner = OPS[msg.innerOp || "hash64"];
58
+ if (!inner) throw new Error(`pixahash worker: unknown op "${msg.innerOp}"`);
59
+ result = data.map((d) => inner(d, seed, bits));
60
+ } else {
61
+ result = run(op, data, seed, bits);
62
+ }
63
+
64
+ // Transfer digest buffers back to avoid a copy.
65
+ const transfer = result instanceof Uint8Array ? [result.buffer] : undefined;
66
+ post({ id, result }, transfer);
67
+ } catch (err) {
68
+ post({ id, error: err && err.message ? err.message : String(err) });
69
+ }
70
+ }