format-png 0.2.0 → 0.3.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/CHANGELOG.md +93 -0
- package/README.md +121 -6
- package/dist/async.d.ts +28 -0
- package/dist/async.js +48 -0
- package/dist/core.d.ts +393 -0
- package/dist/core.js +405 -0
- package/dist/index.d.ts +5 -371
- package/dist/index.js +3 -370
- package/dist/pool.d.ts +82 -0
- package/dist/pool.js +407 -0
- package/dist/port-node.d.ts +3 -0
- package/dist/port-node.js +9 -0
- package/dist/port-web.d.ts +3 -0
- package/dist/port-web.js +7 -0
- package/dist/protocol.d.ts +40 -0
- package/dist/protocol.js +36 -0
- package/dist/spawn-node.d.ts +3 -0
- package/dist/spawn-node.js +9 -0
- package/dist/spawn-web.d.ts +2 -0
- package/dist/spawn-web.js +9 -0
- package/dist/wasm/format_png_wasm.d.ts +127 -68
- package/dist/wasm/format_png_wasm.js +947 -366
- package/dist/wasm/format_png_wasm_bg.wasm.base64.js +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +137 -0
- package/package.json +24 -4
package/dist/pool.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { defaultSize, spawnWorker } from "#spawn";
|
|
2
|
+
import { PngError, } from "./core.js";
|
|
3
|
+
/**
|
|
4
|
+
* A job that failed because of the pool rather than the PNG: "terminated" if
|
|
5
|
+
* the pool was terminated, "worker-crashed" if its worker stopped unexpectedly
|
|
6
|
+
* (the pool replaces it).
|
|
7
|
+
*/
|
|
8
|
+
export class WorkerPoolError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
name = "WorkerPoolError";
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const terminated = () => new WorkerPoolError("terminated", "format-png: the worker pool was terminated");
|
|
17
|
+
/** Creates a pool of workers. Nothing starts until the first job. */
|
|
18
|
+
export function createWorkerPool(options = {}) {
|
|
19
|
+
const size = options.size ?? Math.min(Math.max(defaultSize(), 1), 8);
|
|
20
|
+
if (!Number.isInteger(size) || size < 1)
|
|
21
|
+
throw new RangeError(`format-png: a worker pool's size must be a whole number of at least 1, not ${size}`);
|
|
22
|
+
return new Pool(size, options.createWorker ?? spawnWorker);
|
|
23
|
+
}
|
|
24
|
+
function isNodeWorker(worker) {
|
|
25
|
+
return typeof worker.on === "function";
|
|
26
|
+
}
|
|
27
|
+
function wrap(worker, onResponse, onCrash) {
|
|
28
|
+
if (isNodeWorker(worker)) {
|
|
29
|
+
worker.on("message", onResponse);
|
|
30
|
+
worker.on("error", (error) => onCrash(error instanceof Error ? error.message : String(error)));
|
|
31
|
+
worker.on("messageerror", () => onCrash("a message from the worker couldn't be read"));
|
|
32
|
+
worker.on("exit", (code) => onCrash(`the worker exited with code ${code}`));
|
|
33
|
+
return {
|
|
34
|
+
post: (request, transfer) => worker.postMessage(request, transfer),
|
|
35
|
+
terminate: async () => void (await worker.terminate()),
|
|
36
|
+
keepAlive: (on) => (on ? worker.ref?.() : worker.unref?.()),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
worker.addEventListener("message", (event) => onResponse(event.data));
|
|
40
|
+
worker.addEventListener("error", (event) => {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
onCrash(event.message || "the worker failed to start or threw an uncaught error");
|
|
43
|
+
});
|
|
44
|
+
worker.addEventListener("messageerror", () => onCrash("a message from the worker couldn't be read"));
|
|
45
|
+
return {
|
|
46
|
+
post: (request, transfer) => worker.postMessage(request, transfer),
|
|
47
|
+
terminate: async () => worker.terminate(),
|
|
48
|
+
keepAlive: () => { },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const errorClasses = { TypeError, RangeError, SyntaxError, ReferenceError };
|
|
52
|
+
function toError({ name, message }) {
|
|
53
|
+
if (name === "PngError")
|
|
54
|
+
return new PngError(message);
|
|
55
|
+
const error = new (errorClasses[name] ?? Error)(message);
|
|
56
|
+
error.name = name;
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A copy of `bytes` the pool owns, with a buffer of its own: copied, or moved
|
|
61
|
+
* with `transfer`, which detaches the caller's buffer now rather than when the
|
|
62
|
+
* job starts.
|
|
63
|
+
*/
|
|
64
|
+
function own(bytes, transfer) {
|
|
65
|
+
// Uint8Array or Uint8ClampedArray, never a subclass: a Node Buffer's `slice` doesn't copy.
|
|
66
|
+
const Type = (bytes instanceof Uint8ClampedArray ? Uint8ClampedArray : Uint8Array);
|
|
67
|
+
if (!transfer)
|
|
68
|
+
return new Type(bytes);
|
|
69
|
+
const { buffer, byteOffset, length } = bytes;
|
|
70
|
+
if (!(buffer instanceof ArrayBuffer))
|
|
71
|
+
throw new TypeError("format-png: `transfer` needs an ArrayBuffer; a SharedArrayBuffer can't be transferred");
|
|
72
|
+
return new Type(structuredClone(buffer, { transfer: [buffer] }), byteOffset, length);
|
|
73
|
+
}
|
|
74
|
+
/** The options to send to the worker: without `transfer` and `signal`, which can't be cloned. */
|
|
75
|
+
function workerOptions(options) {
|
|
76
|
+
const { transfer: _transfer, signal: _signal, ...rest } = options;
|
|
77
|
+
return rest;
|
|
78
|
+
}
|
|
79
|
+
const crashed = (message) => new WorkerPoolError("worker-crashed", `format-png: a worker crashed: ${message}`);
|
|
80
|
+
/**
|
|
81
|
+
* Schedules tasks on workers. A worker runs one task at a time. When one is
|
|
82
|
+
* free, it first runs its pinned tasks, then the next task of the next job, in
|
|
83
|
+
* turn: a job split into many segments doesn't hold up the jobs after it.
|
|
84
|
+
* Nothing waits while holding a worker, so nothing can deadlock: a prepared
|
|
85
|
+
* image waiting for its segments only takes memory in the worker that holds
|
|
86
|
+
* it, which runs other tasks meanwhile, including those segments.
|
|
87
|
+
*/
|
|
88
|
+
class Pool {
|
|
89
|
+
size;
|
|
90
|
+
#createWorker;
|
|
91
|
+
#slots = [];
|
|
92
|
+
/** Jobs with queued tasks, in order; `#next` is the one whose turn it is. */
|
|
93
|
+
#jobs = [];
|
|
94
|
+
#next = 0;
|
|
95
|
+
/** Every job not settled yet. */
|
|
96
|
+
#active = new Set();
|
|
97
|
+
#nextId = 1;
|
|
98
|
+
#nextHandle = 1;
|
|
99
|
+
#terminated = false;
|
|
100
|
+
constructor(size, createWorker) {
|
|
101
|
+
this.size = size;
|
|
102
|
+
this.#createWorker = createWorker;
|
|
103
|
+
}
|
|
104
|
+
async decode(bytes, options = {}) {
|
|
105
|
+
this.#check(options);
|
|
106
|
+
const input = own(bytes, options.transfer);
|
|
107
|
+
return this.#single("decode", [input, workerOptions(options)], [input.buffer], options);
|
|
108
|
+
}
|
|
109
|
+
async decodeRgba8(bytes, options = {}) {
|
|
110
|
+
this.#check(options);
|
|
111
|
+
const input = own(bytes, options.transfer);
|
|
112
|
+
return this.#single("decodeRgba8", [input, workerOptions(options)], [input.buffer], options);
|
|
113
|
+
}
|
|
114
|
+
async readChunks(bytes, options = {}) {
|
|
115
|
+
this.#check(options);
|
|
116
|
+
const input = own(bytes, options.transfer);
|
|
117
|
+
const returnInput = options.transfer ?? false;
|
|
118
|
+
return this.#single("readChunks", [input, workerOptions(options)], [input.buffer], options, (value) => {
|
|
119
|
+
// The worker sent each chunk's data as its length. Rebuilt as views
|
|
120
|
+
// into the caller's bytes, as `readChunks` returns, or into the
|
|
121
|
+
// input moved back from the worker.
|
|
122
|
+
const { chunks, input: returned } = value;
|
|
123
|
+
const base = returned ?? bytes;
|
|
124
|
+
for (const chunk of chunks.chunks) {
|
|
125
|
+
const length = chunk.data;
|
|
126
|
+
chunk.data = base.subarray(chunk.offset + 8, chunk.offset + 8 + length);
|
|
127
|
+
}
|
|
128
|
+
return chunks;
|
|
129
|
+
}, returnInput);
|
|
130
|
+
}
|
|
131
|
+
async readHeader(bytes, options = {}) {
|
|
132
|
+
this.#check(options);
|
|
133
|
+
const input = own(bytes, options.transfer);
|
|
134
|
+
return this.#single("readHeader", [input], [input.buffer], options);
|
|
135
|
+
}
|
|
136
|
+
async parseText(type, data, options = {}) {
|
|
137
|
+
this.#check(options);
|
|
138
|
+
const input = own(data, options.transfer);
|
|
139
|
+
return this.#single("parseText", [type, input], [input.buffer], options);
|
|
140
|
+
}
|
|
141
|
+
async encode(image, options = {}) {
|
|
142
|
+
this.#check(options);
|
|
143
|
+
const data = own(image.data, options.transfer);
|
|
144
|
+
return this.#encode("encode", { ...image, data }, data.buffer, options);
|
|
145
|
+
}
|
|
146
|
+
async encodeRgba8(image, options = {}) {
|
|
147
|
+
this.#check(options);
|
|
148
|
+
// Only the fields `encodeRgba8` reads: an `ImageData` would otherwise be cloned whole.
|
|
149
|
+
const { width, height, metadata, chunks, interlaced } = image;
|
|
150
|
+
const data = own(image.data, options.transfer);
|
|
151
|
+
return this.#encode("encodeRgba8", { width, height, data, metadata, chunks, interlaced }, data.buffer, options);
|
|
152
|
+
}
|
|
153
|
+
async terminate() {
|
|
154
|
+
this.#terminated = true;
|
|
155
|
+
for (const job of this.#active)
|
|
156
|
+
this.#fail(job, terminated());
|
|
157
|
+
await Promise.all(this.#slots.splice(0).map((slot) => this.#retire(slot)));
|
|
158
|
+
}
|
|
159
|
+
/** Throws if the job can't start, before its input is copied or detached. */
|
|
160
|
+
#check(options) {
|
|
161
|
+
if (this.#terminated)
|
|
162
|
+
throw terminated();
|
|
163
|
+
if (options.signal?.aborted)
|
|
164
|
+
throw options.signal.reason;
|
|
165
|
+
}
|
|
166
|
+
/** A job of one task, whose answer `finish` turns into the result. */
|
|
167
|
+
#single(op, args, transfer, options, finish = (value) => value, returnInput) {
|
|
168
|
+
return this.#submit(options, (job) => ({
|
|
169
|
+
job, op, args, transfer,
|
|
170
|
+
request: returnInput ? { returnInput } : undefined,
|
|
171
|
+
done: (value) => this.#succeed(job, finish(value)),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Encodes on one worker with `threads: "single"`. Otherwise, by default,
|
|
176
|
+
* prepares the image on one worker and compresses its segments on all of
|
|
177
|
+
* them: the same bytes as `threads: "auto"`.
|
|
178
|
+
*/
|
|
179
|
+
#encode(op, image, buffer, options) {
|
|
180
|
+
const threads = options.threads ?? "auto";
|
|
181
|
+
const args = [image, { ...workerOptions(options), threads }];
|
|
182
|
+
if (threads === "single")
|
|
183
|
+
return this.#single(op, args, [buffer], options);
|
|
184
|
+
const handle = this.#nextHandle++;
|
|
185
|
+
return this.#submit(options, (job) => ({
|
|
186
|
+
job, op, args, transfer: [buffer],
|
|
187
|
+
request: { split: true, handle },
|
|
188
|
+
done: (value, slot) => this.#prepared(job, slot, handle, value),
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
/** A split encode's image is prepared: queues its segments, then its "finish" on the worker that prepared it. */
|
|
192
|
+
#prepared(job, slot, handle, value) {
|
|
193
|
+
if ("png" in value) {
|
|
194
|
+
this.#succeed(job, value.png); // small enough to have no segments
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
job.prepared = { slot, handle };
|
|
198
|
+
slot.holds.add(job);
|
|
199
|
+
if (job.settled) {
|
|
200
|
+
this.#free(job); // cancelled while it was being prepared
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { segments, level, strategy } = value;
|
|
204
|
+
const compressed = new Array(segments.length);
|
|
205
|
+
let remaining = segments.length;
|
|
206
|
+
job.queue = segments.map((segment, i) => ({
|
|
207
|
+
job,
|
|
208
|
+
op: "compressSegment",
|
|
209
|
+
args: [segment, level, strategy],
|
|
210
|
+
transfer: [segment.buffer],
|
|
211
|
+
done: (result) => {
|
|
212
|
+
if (job.settled)
|
|
213
|
+
return;
|
|
214
|
+
compressed[i] = result;
|
|
215
|
+
if (--remaining > 0)
|
|
216
|
+
return;
|
|
217
|
+
job.finishing = true;
|
|
218
|
+
slot.pinned.push({
|
|
219
|
+
job,
|
|
220
|
+
op: "finish",
|
|
221
|
+
args: [handle, compressed],
|
|
222
|
+
transfer: compressed.map((part) => part.buffer),
|
|
223
|
+
after: () => {
|
|
224
|
+
slot.holds.delete(job);
|
|
225
|
+
job.prepared = undefined;
|
|
226
|
+
},
|
|
227
|
+
done: (png) => this.#succeed(job, png),
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
}));
|
|
231
|
+
this.#jobs.push(job);
|
|
232
|
+
}
|
|
233
|
+
/** Frees the job's prepared image, if it has one that "finish" won't free. */
|
|
234
|
+
#free(job) {
|
|
235
|
+
const { prepared } = job;
|
|
236
|
+
if (!prepared || job.finishing)
|
|
237
|
+
return;
|
|
238
|
+
job.prepared = undefined;
|
|
239
|
+
const { slot, handle } = prepared;
|
|
240
|
+
slot.pinned.push({ job, op: "free", args: [handle], transfer: [], after: () => slot.holds.delete(job), done: () => { } });
|
|
241
|
+
}
|
|
242
|
+
#submit(options, first) {
|
|
243
|
+
const { signal } = options;
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
const job = { queue: [], running: new Set(), finishing: false, settled: false, resolve, reject, signal };
|
|
246
|
+
job.queue.push(first(job));
|
|
247
|
+
if (signal) {
|
|
248
|
+
job.onAbort = () => this.#abort(job);
|
|
249
|
+
signal.addEventListener("abort", job.onAbort, { once: true });
|
|
250
|
+
}
|
|
251
|
+
this.#active.add(job);
|
|
252
|
+
this.#jobs.push(job);
|
|
253
|
+
this.#pump();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Starts tasks on idle workers, starting workers as needed. */
|
|
257
|
+
#pump() {
|
|
258
|
+
for (;;) {
|
|
259
|
+
for (const slot of this.#slots) {
|
|
260
|
+
if (!slot.task && slot.pinned.length > 0)
|
|
261
|
+
this.#run(slot, slot.pinned.shift());
|
|
262
|
+
}
|
|
263
|
+
if (this.#jobs.length === 0)
|
|
264
|
+
return;
|
|
265
|
+
let slot = this.#slots.find((candidate) => !candidate.task);
|
|
266
|
+
if (!slot) {
|
|
267
|
+
if (this.#slots.length >= this.size)
|
|
268
|
+
return;
|
|
269
|
+
try {
|
|
270
|
+
slot = this.#spawn();
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.#fail(this.#jobs[0], error);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Round robin: the next job's next task.
|
|
278
|
+
if (this.#next >= this.#jobs.length)
|
|
279
|
+
this.#next = 0;
|
|
280
|
+
const job = this.#jobs[this.#next];
|
|
281
|
+
const task = job.queue.shift();
|
|
282
|
+
if (job.queue.length === 0)
|
|
283
|
+
this.#jobs.splice(this.#next, 1);
|
|
284
|
+
else
|
|
285
|
+
this.#next++;
|
|
286
|
+
this.#run(slot, task);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
#run(slot, task) {
|
|
290
|
+
const id = this.#nextId++;
|
|
291
|
+
slot.task = task;
|
|
292
|
+
slot.taskId = id;
|
|
293
|
+
task.job.running.add(slot);
|
|
294
|
+
slot.worker.keepAlive(true);
|
|
295
|
+
try {
|
|
296
|
+
slot.worker.post({ id, op: task.op, args: task.args, ...task.request }, task.transfer);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
slot.task = undefined;
|
|
300
|
+
task.job.running.delete(slot);
|
|
301
|
+
slot.worker.keepAlive(false);
|
|
302
|
+
task.after?.();
|
|
303
|
+
this.#fail(task.job, error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
#spawn() {
|
|
307
|
+
const slot = { worker: undefined, pinned: [], holds: new Set(), retired: false };
|
|
308
|
+
slot.worker = wrap(this.#createWorker(), (response) => this.#onResponse(slot, response), (message) => this.#onCrash(slot, message));
|
|
309
|
+
slot.worker.keepAlive(false);
|
|
310
|
+
this.#slots.push(slot);
|
|
311
|
+
return slot;
|
|
312
|
+
}
|
|
313
|
+
#onResponse(slot, response) {
|
|
314
|
+
const { task } = slot;
|
|
315
|
+
if (slot.retired || !task || slot.taskId !== response.id)
|
|
316
|
+
return;
|
|
317
|
+
slot.task = undefined;
|
|
318
|
+
task.job.running.delete(slot);
|
|
319
|
+
slot.worker.keepAlive(false);
|
|
320
|
+
task.after?.();
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
this.#fail(task.job, toError(response.error));
|
|
323
|
+
// A trap: the worker goes, with any other job's prepared image.
|
|
324
|
+
if (response.retire)
|
|
325
|
+
void this.#retire(slot, crashed("its WebAssembly instance failed"));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
try {
|
|
329
|
+
task.done(response.value, slot);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
this.#fail(task.job, error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
this.#pump();
|
|
336
|
+
}
|
|
337
|
+
#onCrash(slot, message) {
|
|
338
|
+
if (slot.retired)
|
|
339
|
+
return;
|
|
340
|
+
void this.#retire(slot, crashed(message));
|
|
341
|
+
this.#pump();
|
|
342
|
+
}
|
|
343
|
+
#abort(job) {
|
|
344
|
+
if (job.settled)
|
|
345
|
+
return;
|
|
346
|
+
// Running tasks: wasm can't be interrupted, so their workers go, unless
|
|
347
|
+
// a worker holds prepared images, which would go with it. Then its task
|
|
348
|
+
// finishes, and the result is dropped.
|
|
349
|
+
const stop = [...job.running].filter((slot) => slot.holds.size === 0);
|
|
350
|
+
this.#fail(job, job.signal.reason);
|
|
351
|
+
for (const slot of stop)
|
|
352
|
+
void this.#retire(slot);
|
|
353
|
+
this.#pump();
|
|
354
|
+
}
|
|
355
|
+
/** Rejects the job, drops its queued tasks, and frees its prepared image. */
|
|
356
|
+
#fail(job, error) {
|
|
357
|
+
if (job.settled)
|
|
358
|
+
return;
|
|
359
|
+
this.#settle(job);
|
|
360
|
+
job.reject(error);
|
|
361
|
+
job.queue.length = 0;
|
|
362
|
+
const index = this.#jobs.indexOf(job);
|
|
363
|
+
if (index >= 0) {
|
|
364
|
+
this.#jobs.splice(index, 1);
|
|
365
|
+
if (index < this.#next)
|
|
366
|
+
this.#next--;
|
|
367
|
+
}
|
|
368
|
+
this.#free(job);
|
|
369
|
+
}
|
|
370
|
+
#succeed(job, value) {
|
|
371
|
+
if (job.settled)
|
|
372
|
+
return;
|
|
373
|
+
this.#settle(job);
|
|
374
|
+
job.resolve(value);
|
|
375
|
+
}
|
|
376
|
+
#settle(job) {
|
|
377
|
+
job.settled = true;
|
|
378
|
+
this.#active.delete(job);
|
|
379
|
+
if (job.onAbort)
|
|
380
|
+
job.signal.removeEventListener("abort", job.onAbort);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Stops a worker. With `error`, the job it was running and every job whose
|
|
384
|
+
* prepared image it held reject with it.
|
|
385
|
+
*/
|
|
386
|
+
#retire(slot, error) {
|
|
387
|
+
slot.retired = true;
|
|
388
|
+
const index = this.#slots.indexOf(slot);
|
|
389
|
+
if (index >= 0)
|
|
390
|
+
this.#slots.splice(index, 1);
|
|
391
|
+
const { task } = slot;
|
|
392
|
+
slot.task = undefined;
|
|
393
|
+
if (task) {
|
|
394
|
+
task.job.running.delete(slot);
|
|
395
|
+
if (error)
|
|
396
|
+
this.#fail(task.job, error);
|
|
397
|
+
}
|
|
398
|
+
for (const job of slot.holds) {
|
|
399
|
+
job.prepared = undefined; // gone with the worker
|
|
400
|
+
if (error)
|
|
401
|
+
this.#fail(job, error);
|
|
402
|
+
}
|
|
403
|
+
slot.holds.clear();
|
|
404
|
+
slot.pinned.length = 0;
|
|
405
|
+
return slot.worker.terminate().catch(() => { });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The worker side of a WorkerPool in Node: worker_threads' `parentPort`.
|
|
2
|
+
import { parentPort } from "node:worker_threads";
|
|
3
|
+
const port = parentPort;
|
|
4
|
+
export function listen(handler) {
|
|
5
|
+
port.on("message", handler);
|
|
6
|
+
}
|
|
7
|
+
export function post(message, transfer) {
|
|
8
|
+
port.postMessage(message, transfer);
|
|
9
|
+
}
|
package/dist/port-web.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type Op = "decode" | "decodeRgba8" | "readChunks" | "readHeader" | "parseText" | "encode" | "encodeRgba8" | "compressSegment" | "finish" | "free"
|
|
2
|
+
/** How many prepared images the worker holds: for tests. */
|
|
3
|
+
| "handles";
|
|
4
|
+
export interface Request {
|
|
5
|
+
id: number;
|
|
6
|
+
op: Op;
|
|
7
|
+
args: unknown[];
|
|
8
|
+
/** `readChunks` only: send the input back, as the chunks' data are views into it. */
|
|
9
|
+
returnInput?: boolean;
|
|
10
|
+
/** `encode` and `encodeRgba8`: prepare the image, keeping it as `handle`, rather than encode it. */
|
|
11
|
+
split?: boolean;
|
|
12
|
+
handle?: number;
|
|
13
|
+
}
|
|
14
|
+
/** What an `encode` with `split` returns. */
|
|
15
|
+
export type Prepared = {
|
|
16
|
+
png: Uint8Array;
|
|
17
|
+
} | {
|
|
18
|
+
segments: Uint8Array[];
|
|
19
|
+
level: number;
|
|
20
|
+
strategy: string;
|
|
21
|
+
};
|
|
22
|
+
export interface SerializedError {
|
|
23
|
+
name: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
export type Response = {
|
|
27
|
+
id: number;
|
|
28
|
+
ok: true;
|
|
29
|
+
value: unknown;
|
|
30
|
+
} | {
|
|
31
|
+
id: number;
|
|
32
|
+
ok: false;
|
|
33
|
+
error: SerializedError;
|
|
34
|
+
retire: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The buffers of every typed array in `value`, to transfer rather than copy.
|
|
38
|
+
* Only call it on values whose buffers nothing else uses.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buffersIn(value: unknown, found?: Set<ArrayBuffer>): ArrayBuffer[];
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The messages between a WorkerPool and its workers. The same in browsers and
|
|
2
|
+
// Node; only how a message is posted differs (see spawn-*.ts and port-*.ts).
|
|
3
|
+
//
|
|
4
|
+
// Each worker runs one task at a time:
|
|
5
|
+
// pool → worker { id, op, args, returnInput, split, handle } input buffers transferred
|
|
6
|
+
// worker → pool { id, ok: true, value } output buffers transferred
|
|
7
|
+
// { id, ok: false, error, retire } retire: the worker can't be trusted anymore
|
|
8
|
+
//
|
|
9
|
+
// Most jobs are one task. An encode with `split` is several, so one image is
|
|
10
|
+
// compressed by every worker:
|
|
11
|
+
// 1. "encode"/"encodeRgba8", split, handle h, on any worker A. A prepares the
|
|
12
|
+
// image. With no segments it returns { png }. Otherwise it keeps the
|
|
13
|
+
// prepared image as h and returns { segments, level, strategy }.
|
|
14
|
+
// 2. "compressSegment" [segment, level, strategy] for each, on any worker.
|
|
15
|
+
// 3. "finish" [h, compressed] on A: the PNG. A drops h, whatever happens.
|
|
16
|
+
// Or "free" [h] on A, if the job failed or was cancelled first.
|
|
17
|
+
// A worker holding prepared images still runs other tasks between these.
|
|
18
|
+
/**
|
|
19
|
+
* The buffers of every typed array in `value`, to transfer rather than copy.
|
|
20
|
+
* Only call it on values whose buffers nothing else uses.
|
|
21
|
+
*/
|
|
22
|
+
export function buffersIn(value, found = new Set()) {
|
|
23
|
+
if (ArrayBuffer.isView(value)) {
|
|
24
|
+
if (value.buffer instanceof ArrayBuffer)
|
|
25
|
+
found.add(value.buffer);
|
|
26
|
+
}
|
|
27
|
+
else if (Array.isArray(value)) {
|
|
28
|
+
for (const item of value)
|
|
29
|
+
buffersIn(item, found);
|
|
30
|
+
}
|
|
31
|
+
else if (value !== null && typeof value === "object") {
|
|
32
|
+
for (const item of Object.values(value))
|
|
33
|
+
buffersIn(item, found);
|
|
34
|
+
}
|
|
35
|
+
return [...found];
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Starts a WorkerPool worker in Node, with worker_threads.
|
|
2
|
+
import { availableParallelism } from "node:os";
|
|
3
|
+
import { Worker } from "node:worker_threads";
|
|
4
|
+
export function spawnWorker() {
|
|
5
|
+
return new Worker(new URL("./worker.js", import.meta.url));
|
|
6
|
+
}
|
|
7
|
+
export function defaultSize() {
|
|
8
|
+
return availableParallelism();
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Starts a WorkerPool worker in browsers, bundlers and Web Workers. Written as
|
|
2
|
+
// `new Worker(new URL("./worker.js", import.meta.url), { type: "module" })` so
|
|
3
|
+
// bundlers such as Vite and webpack find and bundle the worker.
|
|
4
|
+
export function spawnWorker() {
|
|
5
|
+
return new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
|
|
6
|
+
}
|
|
7
|
+
export function defaultSize() {
|
|
8
|
+
return globalThis.navigator?.hardwareConcurrency || 4;
|
|
9
|
+
}
|