@draco-rust/decoder 0.1.2 → 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/README.md +27 -21
- package/index.d.ts +21 -25
- package/index.js +12 -7
- package/index_bg.wasm +0 -0
- package/index_bg.wasm.d.ts +1 -1
- package/legacy/index.d.ts +21 -25
- package/legacy/index.js +12 -7
- package/legacy/index_bg.wasm +0 -0
- package/legacy/index_bg.wasm.d.ts +1 -1
- package/legacy/pool-core.d.ts +44 -0
- package/legacy/pool-core.js +250 -0
- package/legacy/pool.d.ts +6 -29
- package/legacy/pool.js +15 -184
- package/legacy/tasks.js +2 -0
- package/legacy/three.d.ts +11 -57
- package/legacy/three.js +66 -107
- package/legacy/worker.js +24 -10
- package/mesh/index.d.ts +21 -25
- package/mesh/index.js +12 -7
- package/mesh/index_bg.wasm +0 -0
- package/mesh/index_bg.wasm.d.ts +1 -1
- package/mesh/pool-core.d.ts +44 -0
- package/mesh/pool-core.js +250 -0
- package/mesh/pool.d.ts +6 -29
- package/mesh/pool.js +15 -184
- package/mesh/tasks.js +2 -0
- package/mesh/three.d.ts +11 -57
- package/mesh/three.js +66 -107
- package/mesh/worker.js +24 -10
- package/package.json +10 -2
- package/point-cloud/index.d.ts +21 -25
- package/point-cloud/index.js +12 -7
- package/point-cloud/index_bg.wasm +0 -0
- package/point-cloud/index_bg.wasm.d.ts +1 -1
- package/point-cloud/pool-core.d.ts +44 -0
- package/point-cloud/pool-core.js +250 -0
- package/point-cloud/pool.d.ts +6 -29
- package/point-cloud/pool.js +15 -184
- package/point-cloud/tasks.js +2 -0
- package/point-cloud/three.d.ts +11 -57
- package/point-cloud/three.js +66 -107
- package/point-cloud/worker.js +24 -10
- package/pool-core.d.ts +44 -0
- package/pool-core.js +250 -0
- package/pool.d.ts +6 -29
- package/pool.js +15 -184
- package/tasks.js +2 -0
- package/three.d.ts +11 -57
- package/three.js +66 -107
- package/worker.js +24 -10
package/point-cloud/three.js
CHANGED
|
@@ -1,123 +1,82 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// three.js's DRACOLoader, decoding on this entry's worker pool. Everything a
|
|
2
|
+
// caller sees -- load, parse, decodeDracoFile, GLTFLoader's use of it, the
|
|
3
|
+
// geometry it builds -- is three's own class; only the decode is replaced, so
|
|
4
|
+
// the result follows whichever three the application has.
|
|
5
|
+
import { REVISION } from 'three';
|
|
6
|
+
import { DRACOLoader as ThreeDRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
|
4
7
|
import { createDecoderPool } from './pool.js';
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
// From r181 DRACOLoader pads an attribute whose items are not a multiple of
|
|
10
|
+
// four bytes and builds it interleaved, from the stride its worker reports.
|
|
11
|
+
const PADS = Number.parseInt(REVISION, 10) >= 181;
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export function createDracoLoader(three, options = {}) {
|
|
18
|
-
const { BufferGeometry, BufferAttribute, InterleavedBuffer, InterleavedBufferAttribute, Color, ColorManagement } =
|
|
19
|
-
three;
|
|
20
|
-
if (typeof BufferGeometry !== 'function' || typeof BufferAttribute !== 'function') {
|
|
21
|
-
throw new TypeError("createDracoLoader needs three.js's BufferGeometry and BufferAttribute");
|
|
13
|
+
function padded(array, itemSize) {
|
|
14
|
+
const bytes = array.BYTES_PER_ELEMENT;
|
|
15
|
+
const stride = (Math.ceil((itemSize * bytes) / 4) * 4) / bytes;
|
|
16
|
+
if (!PADS || stride === itemSize) return { array, stride: itemSize };
|
|
17
|
+
const count = array.length / itemSize;
|
|
18
|
+
const out = new array.constructor(count * stride);
|
|
19
|
+
for (let i = 0; i < count; i += 1) {
|
|
20
|
+
out.set(array.subarray(i * itemSize, (i + 1) * itemSize), i * stride);
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// decode a second time; GLTFLoader asks again for a primitive it shares, and
|
|
26
|
-
// gets the first answer, as DRACOLoader gives it.
|
|
27
|
-
const tasks = new WeakMap();
|
|
22
|
+
return { array: out, stride };
|
|
23
|
+
}
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function toWorkingColorSpace(attribute) {
|
|
32
|
-
const convert = ColorManagement?.colorSpaceToWorking ?? ColorManagement?.toWorkingColorSpace;
|
|
33
|
-
if (!Color || !convert) return;
|
|
34
|
-
const color = new Color();
|
|
35
|
-
for (let i = 0; i < attribute.count; i += 1) {
|
|
36
|
-
color.fromBufferAttribute(attribute, i);
|
|
37
|
-
convert.call(ColorManagement, color, SRGB);
|
|
38
|
-
attribute.setXYZ(i, color.r, color.g, color.b);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
25
|
+
export class DRACOLoader extends ThreeDRACOLoader {
|
|
26
|
+
#tasks = new WeakMap();
|
|
41
27
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function attributeOf(array, itemSize) {
|
|
46
|
-
const bytes = array.BYTES_PER_ELEMENT;
|
|
47
|
-
const stride = Math.ceil((itemSize * bytes) / 4) * (4 / bytes);
|
|
48
|
-
if (stride === itemSize || !InterleavedBuffer || !InterleavedBufferAttribute) {
|
|
49
|
-
return new BufferAttribute(array, itemSize);
|
|
50
|
-
}
|
|
51
|
-
const count = array.length / itemSize;
|
|
52
|
-
const padded = new array.constructor(count * stride);
|
|
53
|
-
for (let i = 0; i < count; i += 1) {
|
|
54
|
-
padded.set(array.subarray(i * itemSize, (i + 1) * itemSize), i * stride);
|
|
55
|
-
}
|
|
56
|
-
return new InterleavedBufferAttribute(new InterleavedBuffer(padded, stride), itemSize, 0);
|
|
28
|
+
constructor(manager, options = {}) {
|
|
29
|
+
super(manager);
|
|
30
|
+
this.pool = options.pool ?? createDecoderPool(options);
|
|
57
31
|
}
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
33
|
+
// The buffer goes to a worker by transfer, which leaves nothing here to
|
|
34
|
+
// decode again; a second request for it gets the first answer, as three's
|
|
35
|
+
// own task cache gives it.
|
|
36
|
+
decodeGeometry(buffer, taskConfig) {
|
|
37
|
+
const key = JSON.stringify(taskConfig);
|
|
38
|
+
const cached = this.#tasks.get(buffer);
|
|
39
|
+
if (cached?.key === key) return cached.promise;
|
|
40
|
+
if (cached && buffer.byteLength === 0) {
|
|
41
|
+
return Promise.reject(new Error('THREE.DRACOLoader: Unable to re-decode a buffer with different settings. Buffer has already been transferred.'));
|
|
42
|
+
}
|
|
43
|
+
const { attributeIDs, attributeTypes, useUniqueIDs, vertexColorSpace } = taskConfig;
|
|
44
|
+
const requests = Object.keys(attributeIDs).map((name) => ({
|
|
62
45
|
name,
|
|
63
|
-
...(
|
|
64
|
-
type: attributeTypes
|
|
46
|
+
...(useUniqueIDs ? { id: attributeIDs[name] } : { semantic: attributeIDs[name] }),
|
|
47
|
+
type: attributeTypes[name],
|
|
65
48
|
}));
|
|
66
|
-
const
|
|
67
|
-
|
|
49
|
+
const promise = this.pool.decode(buffer, requests, { transfer: buffer instanceof ArrayBuffer }).then((result) => {
|
|
50
|
+
if (!result.success) throw new Error(`THREE.DRACOLoader: Decoding failed: ${result.error}`);
|
|
51
|
+
return this._createGeometry({
|
|
52
|
+
index: result.index ? { array: result.index, itemSize: 1 } : null,
|
|
53
|
+
attributes: result.attributes.map(({ name, array, itemSize }) => ({
|
|
54
|
+
name,
|
|
55
|
+
itemSize,
|
|
56
|
+
...padded(array, itemSize),
|
|
57
|
+
...(name === 'color' ? { vertexColorSpace } : {}),
|
|
58
|
+
})),
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
if (buffer instanceof ArrayBuffer) this.#tasks.set(buffer, { key, promise });
|
|
62
|
+
return promise;
|
|
63
|
+
}
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (name === 'color') {
|
|
74
|
-
if (vertexColorSpace === SRGB) toWorkingColorSpace(attribute);
|
|
75
|
-
attribute.normalized = !(array instanceof Float32Array);
|
|
76
|
-
}
|
|
77
|
-
geometry.setAttribute(name, attribute);
|
|
78
|
-
}
|
|
79
|
-
return geometry;
|
|
65
|
+
setWorkerLimit(workerLimit) {
|
|
66
|
+
super.setWorkerLimit(workerLimit);
|
|
67
|
+
this.pool.setWorkerLimit(workerLimit);
|
|
68
|
+
return this;
|
|
80
69
|
}
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (cached && buffer.byteLength === 0) {
|
|
87
|
-
return Promise.reject(new Error('this buffer was already decoded with other settings and handed to a worker'));
|
|
88
|
-
}
|
|
89
|
-
const promise = decode(buffer, attributeIDs, attributeTypes, vertexColorSpace);
|
|
90
|
-
if (buffer instanceof ArrayBuffer) tasks.set(buffer, { key, promise });
|
|
91
|
-
return promise;
|
|
71
|
+
preload() {
|
|
72
|
+
// A failure shows again on the first decode, which is where it is handled.
|
|
73
|
+
this.pool.preload().catch(() => {});
|
|
74
|
+
return this;
|
|
92
75
|
}
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return decodeGeometry(buffer, { attributeIDs, attributeTypes, vertexColorSpace }).then(callback).catch(onError);
|
|
100
|
-
},
|
|
101
|
-
/** A `.drc` file's contents, as DRACOLoader's `parse`. */
|
|
102
|
-
parse(buffer, onLoad, onError = () => {}) {
|
|
103
|
-
loader.decodeDracoFile(buffer, onLoad, null, null, SRGB, onError);
|
|
104
|
-
},
|
|
105
|
-
parseAsync(buffer) {
|
|
106
|
-
return decodeGeometry(buffer, { vertexColorSpace: SRGB });
|
|
107
|
-
},
|
|
108
|
-
preload() {
|
|
109
|
-
// A failure shows again on the first decode, which is where it is handled.
|
|
110
|
-
pool.preload().catch(() => {});
|
|
111
|
-
return loader;
|
|
112
|
-
},
|
|
113
|
-
setWorkerLimit(workers) {
|
|
114
|
-
pool.setWorkerLimit(workers);
|
|
115
|
-
return loader;
|
|
116
|
-
},
|
|
117
|
-
dispose() {
|
|
118
|
-
pool.dispose();
|
|
119
|
-
return loader;
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
return loader;
|
|
77
|
+
dispose() {
|
|
78
|
+
super.dispose();
|
|
79
|
+
this.pool.dispose();
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
123
82
|
}
|
package/point-cloud/worker.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// A
|
|
2
|
-
//
|
|
3
|
-
// transfer. Runs as a Web Worker and as a Node
|
|
4
|
-
|
|
1
|
+
// A pool worker. The pool sends it the compiled module once, then tasks by
|
|
2
|
+
// name from this entry's `tasks.js`; it answers each with its result, handing
|
|
3
|
+
// every array in it back by transfer. Runs as a Web Worker and as a Node
|
|
4
|
+
// worker thread.
|
|
5
|
+
import init from './index.js';
|
|
6
|
+
import * as tasks from './tasks.js';
|
|
5
7
|
|
|
6
8
|
const web = typeof self !== 'undefined' && typeof self.postMessage === 'function';
|
|
7
9
|
const port = web ? self : process.getBuiltinModule('node:worker_threads').parentPort;
|
|
@@ -10,18 +12,30 @@ const listen = (handler) =>
|
|
|
10
12
|
|
|
11
13
|
let ready = null;
|
|
12
14
|
|
|
15
|
+
/** Every buffer in a result, except the module's own memory, which stays. */
|
|
16
|
+
function buffersOf(value, memory, found = new Set()) {
|
|
17
|
+
if (ArrayBuffer.isView(value)) {
|
|
18
|
+
if (value.buffer instanceof ArrayBuffer && value.buffer !== memory) found.add(value.buffer);
|
|
19
|
+
} else if (value instanceof ArrayBuffer) {
|
|
20
|
+
if (value !== memory) found.add(value);
|
|
21
|
+
} else if (Array.isArray(value)) {
|
|
22
|
+
for (const item of value) buffersOf(item, memory, found);
|
|
23
|
+
} else if (value !== null && typeof value === 'object') {
|
|
24
|
+
for (const item of Object.values(value)) buffersOf(item, memory, found);
|
|
25
|
+
}
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
listen(async (message) => {
|
|
14
30
|
if (message.type === 'init') {
|
|
15
31
|
ready = init({ module_or_path: message.module });
|
|
16
32
|
return;
|
|
17
33
|
}
|
|
18
|
-
const { id,
|
|
34
|
+
const { id, task, args } = message;
|
|
19
35
|
try {
|
|
20
|
-
await ready;
|
|
21
|
-
const result =
|
|
22
|
-
|
|
23
|
-
if (result.index) transfer.push(result.index.buffer);
|
|
24
|
-
port.postMessage({ id, result }, transfer);
|
|
36
|
+
const { memory } = await ready;
|
|
37
|
+
const result = await tasks[task](...args);
|
|
38
|
+
port.postMessage({ id, result }, [...buffersOf(result, memory.buffer)]);
|
|
25
39
|
} catch (error) {
|
|
26
40
|
port.postMessage({ id, error: String(error?.message ?? error) });
|
|
27
41
|
}
|
package/pool-core.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface PoolOptions {
|
|
2
|
+
/**
|
|
3
|
+
* How many workers to run at most; they start as work arrives. Defaults to
|
|
4
|
+
* one less than the cores, between one and four. `0` runs on the calling
|
|
5
|
+
* thread, as does an environment without workers.
|
|
6
|
+
*/
|
|
7
|
+
workers?: number;
|
|
8
|
+
/**
|
|
9
|
+
* The entry's wasm, when it is not next to this module where the pool looks
|
|
10
|
+
* for it: a URL, a `Response`, the bytes, or a compiled module.
|
|
11
|
+
*/
|
|
12
|
+
wasm?: string | URL | Response | BufferSource | WebAssembly.Module;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TaskOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Hand the input's buffers to the worker instead of copying them, leaving
|
|
18
|
+
* them detached here. For buffers nothing else reads afterwards.
|
|
19
|
+
*/
|
|
20
|
+
transfer?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Abandons the task: the promise rejects with the signal's reason at once.
|
|
23
|
+
* A task a worker has already started still runs to its end, and its result
|
|
24
|
+
* is dropped; one not yet sent is never sent.
|
|
25
|
+
*/
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Pool {
|
|
30
|
+
/** Compiles the wasm and starts one worker ahead of the first task. */
|
|
31
|
+
preload(): Promise<void>;
|
|
32
|
+
/** Changes the worker limit; idle workers past it stop now, busy ones when done. */
|
|
33
|
+
setWorkerLimit(workers: number): void;
|
|
34
|
+
readonly workerLimit: number;
|
|
35
|
+
/** Stops every worker and rejects what they held. */
|
|
36
|
+
dispose(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TaskPool extends Pool {
|
|
40
|
+
/** Runs the task `tasks.js` exports under `name` on a worker. */
|
|
41
|
+
run(name: string, args?: unknown[], options?: TaskOptions): Promise<any>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createPool(options?: PoolOptions): TaskPool;
|
package/pool-core.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Work off the calling thread: a pool of workers, each running this entry's
|
|
2
|
+
// own module and the tasks its `tasks.js` names. The wasm is compiled once here
|
|
3
|
+
// and handed to every worker; inputs go over by transfer of a copy, or of the
|
|
4
|
+
// caller's own buffers when asked, and results come back by transfer, so a
|
|
5
|
+
// task copies nothing it does not have to.
|
|
6
|
+
import init from './index.js';
|
|
7
|
+
import * as tasks from './tasks.js';
|
|
8
|
+
|
|
9
|
+
/** Leaves a core for the page, and stops at four, past which a page rarely has
|
|
10
|
+
* that many files in flight at once. */
|
|
11
|
+
const defaultWorkers = () =>
|
|
12
|
+
Math.max(1, Math.min(4, (globalThis.navigator?.hardwareConcurrency ?? 4) - 1));
|
|
13
|
+
|
|
14
|
+
const builtin = (name) =>
|
|
15
|
+
typeof process !== 'undefined' && typeof process.getBuiltinModule === 'function'
|
|
16
|
+
? process.getBuiltinModule(name)
|
|
17
|
+
: undefined;
|
|
18
|
+
|
|
19
|
+
async function compile(wasm) {
|
|
20
|
+
if (wasm instanceof WebAssembly.Module) return wasm;
|
|
21
|
+
let source = wasm ?? new URL('./index_bg.wasm', import.meta.url);
|
|
22
|
+
if (typeof source === 'string' || source instanceof URL) {
|
|
23
|
+
const url = new URL(source, import.meta.url);
|
|
24
|
+
// Node's fetch does not read files.
|
|
25
|
+
const fs = url.protocol === 'file:' ? builtin('node:fs/promises') : undefined;
|
|
26
|
+
if (fs) return WebAssembly.compile(await fs.readFile(url));
|
|
27
|
+
source = await fetch(url);
|
|
28
|
+
}
|
|
29
|
+
if (typeof Response !== 'undefined' && source instanceof Response) {
|
|
30
|
+
if (!source.ok) throw new Error(`failed to fetch ${source.url}: ${source.status}`);
|
|
31
|
+
if (source.headers.get('content-type') === 'application/wasm') {
|
|
32
|
+
return WebAssembly.compileStreaming(source);
|
|
33
|
+
}
|
|
34
|
+
source = await source.arrayBuffer();
|
|
35
|
+
}
|
|
36
|
+
return WebAssembly.compile(source);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What the pool needs of a worker, the same over a Web Worker and a Node
|
|
40
|
+
* worker thread. */
|
|
41
|
+
function spawn(blobUrl) {
|
|
42
|
+
if (typeof Worker === 'function') {
|
|
43
|
+
const url = new URL('./worker.js', import.meta.url);
|
|
44
|
+
// A bundler recognises this exact form and emits the worker as a chunk.
|
|
45
|
+
const worker =
|
|
46
|
+
typeof location === 'undefined' || url.origin === location.origin
|
|
47
|
+
? new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
|
|
48
|
+
: new Worker(blobUrl(url), { type: 'module' });
|
|
49
|
+
return {
|
|
50
|
+
post: (message, transfer) => worker.postMessage(message, transfer),
|
|
51
|
+
listen: (onMessage, onError) => {
|
|
52
|
+
worker.addEventListener('message', (event) => onMessage(event.data));
|
|
53
|
+
worker.addEventListener('error', (event) => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
onError(new Error(event.message || 'the worker failed'));
|
|
56
|
+
});
|
|
57
|
+
worker.addEventListener('messageerror', () => onError(new Error('a result could not be read')));
|
|
58
|
+
},
|
|
59
|
+
busy: () => {},
|
|
60
|
+
terminate: () => worker.terminate(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const threads = builtin('node:worker_threads');
|
|
64
|
+
if (!threads) return null;
|
|
65
|
+
const worker = new threads.Worker(new URL('./worker.js', import.meta.url));
|
|
66
|
+
// An idle worker does not keep Node running; one with work does.
|
|
67
|
+
worker.unref();
|
|
68
|
+
return {
|
|
69
|
+
post: (message, transfer) => worker.postMessage(message, transfer),
|
|
70
|
+
listen: (onMessage, onError) => {
|
|
71
|
+
worker.on('message', onMessage);
|
|
72
|
+
worker.on('error', onError);
|
|
73
|
+
worker.on('exit', (code) => onError(new Error(`the worker exited with code ${code}`)));
|
|
74
|
+
},
|
|
75
|
+
busy: (busy) => (busy ? worker.ref() : worker.unref()),
|
|
76
|
+
terminate: () => worker.terminate(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const workersAvailable = () =>
|
|
81
|
+
typeof Worker === 'function' || builtin('node:worker_threads') !== undefined;
|
|
82
|
+
|
|
83
|
+
const isPlain = (value) => {
|
|
84
|
+
if (value === null || typeof value !== 'object') return false;
|
|
85
|
+
const proto = Object.getPrototypeOf(value);
|
|
86
|
+
return proto === Object.prototype || proto === null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The arguments as they go to a worker, and the buffers that go with them.
|
|
91
|
+
* Without `transfer` every typed array is copied first -- just its own bytes,
|
|
92
|
+
* where structured cloning would copy the whole buffer behind a view -- and
|
|
93
|
+
* the copy is what moves; with it the caller's buffers move and are left
|
|
94
|
+
* detached.
|
|
95
|
+
*/
|
|
96
|
+
function outgoing(args, transfer) {
|
|
97
|
+
const buffers = new Set();
|
|
98
|
+
const visit = (value) => {
|
|
99
|
+
if (ArrayBuffer.isView(value)) {
|
|
100
|
+
const view = transfer ? value : value.slice();
|
|
101
|
+
if (!(view.buffer instanceof ArrayBuffer)) return view;
|
|
102
|
+
buffers.add(view.buffer);
|
|
103
|
+
return view;
|
|
104
|
+
}
|
|
105
|
+
if (value instanceof ArrayBuffer) {
|
|
106
|
+
const buffer = transfer ? value : value.slice(0);
|
|
107
|
+
buffers.add(buffer);
|
|
108
|
+
return buffer;
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(value)) return value.map(visit);
|
|
111
|
+
if (isPlain(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, visit(item)]));
|
|
112
|
+
return value;
|
|
113
|
+
};
|
|
114
|
+
const sent = args.map(visit);
|
|
115
|
+
return { sent, buffers: [...buffers] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const aborted = (signal) => signal.reason ?? new DOMException('The task was aborted', 'AbortError');
|
|
119
|
+
|
|
120
|
+
export function createPool(options = {}) {
|
|
121
|
+
let limit = options.workers ?? defaultWorkers();
|
|
122
|
+
let compiled = null;
|
|
123
|
+
let local = null;
|
|
124
|
+
let blob = null;
|
|
125
|
+
let disposed = false;
|
|
126
|
+
let nextId = 0;
|
|
127
|
+
const slots = [];
|
|
128
|
+
|
|
129
|
+
const load = () => (compiled ??= compile(options.wasm));
|
|
130
|
+
// A worker script must share the page's origin. From a CDN, a worker made
|
|
131
|
+
// here imports it instead, which a module worker may do across origins.
|
|
132
|
+
const blobUrl = (url) =>
|
|
133
|
+
(blob ??= URL.createObjectURL(
|
|
134
|
+
new Blob([`import ${JSON.stringify(url.href)};`], { type: 'text/javascript' }),
|
|
135
|
+
));
|
|
136
|
+
|
|
137
|
+
function retire(slot, error) {
|
|
138
|
+
const at = slots.indexOf(slot);
|
|
139
|
+
if (at >= 0) slots.splice(at, 1);
|
|
140
|
+
slot.worker.terminate();
|
|
141
|
+
for (const { reject } of slot.pending.values()) reject(error);
|
|
142
|
+
slot.pending.clear();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function open(module) {
|
|
146
|
+
const worker = spawn(blobUrl);
|
|
147
|
+
if (!worker) return null;
|
|
148
|
+
const slot = { worker, pending: new Map() };
|
|
149
|
+
worker.listen(
|
|
150
|
+
(message) => {
|
|
151
|
+
const task = slot.pending.get(message.id);
|
|
152
|
+
if (!task) return;
|
|
153
|
+
slot.pending.delete(message.id);
|
|
154
|
+
if (slot.pending.size === 0) {
|
|
155
|
+
worker.busy(false);
|
|
156
|
+
if (slots.indexOf(slot) >= limit) retire(slot);
|
|
157
|
+
}
|
|
158
|
+
if ('error' in message) task.reject(new Error(message.error));
|
|
159
|
+
else task.resolve(message.result);
|
|
160
|
+
},
|
|
161
|
+
// A worker that fails takes only its own tasks down; the next task
|
|
162
|
+
// starts another.
|
|
163
|
+
(error) => {
|
|
164
|
+
if (slots.includes(slot)) retire(slot, error);
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
worker.post({ type: 'init', module });
|
|
168
|
+
slots.push(slot);
|
|
169
|
+
return slot;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The least loaded worker, or a new one while every worker is busy and
|
|
173
|
+
* the limit allows another. */
|
|
174
|
+
function pick(module) {
|
|
175
|
+
let best = null;
|
|
176
|
+
for (const slot of slots.slice(0, limit)) {
|
|
177
|
+
if (!best || slot.pending.size < best.pending.size) best = slot;
|
|
178
|
+
}
|
|
179
|
+
if ((!best || best.pending.size > 0) && slots.length < limit) return open(module) ?? best;
|
|
180
|
+
return best;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const onThisThread = () =>
|
|
184
|
+
(local ??= load().then((module) => init({ module_or_path: module })));
|
|
185
|
+
|
|
186
|
+
async function run(name, args = [], { transfer = false, signal } = {}) {
|
|
187
|
+
if (disposed) throw new Error('the pool was disposed');
|
|
188
|
+
if (typeof tasks[name] !== 'function') throw new TypeError(`no task named ${name}`);
|
|
189
|
+
signal?.throwIfAborted();
|
|
190
|
+
if (limit > 0 && workersAvailable()) {
|
|
191
|
+
const module = await load();
|
|
192
|
+
if (disposed) throw new Error('the pool was disposed');
|
|
193
|
+
signal?.throwIfAborted();
|
|
194
|
+
const slot = pick(module);
|
|
195
|
+
if (slot) {
|
|
196
|
+
const id = nextId++;
|
|
197
|
+
const { sent, buffers } = outgoing(args, transfer);
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const task = { resolve, reject };
|
|
200
|
+
if (signal) {
|
|
201
|
+
// A task a worker has started runs to its end. Aborting it settles
|
|
202
|
+
// the caller now and drops the result when it comes; the task stays
|
|
203
|
+
// counted against its worker until then, so the load stays true.
|
|
204
|
+
const onAbort = () => {
|
|
205
|
+
task.resolve = task.reject = () => {};
|
|
206
|
+
reject(aborted(signal));
|
|
207
|
+
};
|
|
208
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
209
|
+
const settle = (done) => (value) => {
|
|
210
|
+
signal.removeEventListener('abort', onAbort);
|
|
211
|
+
done(value);
|
|
212
|
+
};
|
|
213
|
+
task.resolve = settle(resolve);
|
|
214
|
+
task.reject = settle(reject);
|
|
215
|
+
}
|
|
216
|
+
slot.pending.set(id, task);
|
|
217
|
+
slot.worker.busy(true);
|
|
218
|
+
slot.worker.post({ id, task: name, args: sent }, buffers);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
await onThisThread();
|
|
223
|
+
signal?.throwIfAborted();
|
|
224
|
+
return tasks[name](...args);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
run,
|
|
229
|
+
async preload() {
|
|
230
|
+
if (limit > 0 && workersAvailable()) {
|
|
231
|
+
const module = await load();
|
|
232
|
+
if (!disposed && slots.length === 0) open(module);
|
|
233
|
+
} else {
|
|
234
|
+
await onThisThread();
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
setWorkerLimit(workers) {
|
|
238
|
+
limit = workers;
|
|
239
|
+
for (const slot of slots.slice(limit)) if (slot.pending.size === 0) retire(slot);
|
|
240
|
+
},
|
|
241
|
+
get workerLimit() {
|
|
242
|
+
return limit;
|
|
243
|
+
},
|
|
244
|
+
dispose() {
|
|
245
|
+
disposed = true;
|
|
246
|
+
for (const slot of [...slots]) retire(slot, new Error('the pool was disposed'));
|
|
247
|
+
if (blob) URL.revokeObjectURL(blob);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
package/pool.d.ts
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
import type { DracoAttributeRequest, DracoDecodeResult } from './index.js';
|
|
1
|
+
import type { DracoAttributeRequest, DracoDecodeOptions, DracoDecodeResult } from './index.js';
|
|
2
|
+
import type { Pool, PoolOptions, TaskOptions } from './pool-core.js';
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
/**
|
|
5
|
-
* How many workers to run at most; they start as work arrives. Defaults to
|
|
6
|
-
* one less than the cores, between one and four. `0` decodes on the calling
|
|
7
|
-
* thread, as does an environment without workers.
|
|
8
|
-
*/
|
|
9
|
-
workers?: number;
|
|
10
|
-
/**
|
|
11
|
-
* The entry's wasm, when it is not next to this module where the pool looks
|
|
12
|
-
* for it: a URL, a `Response`, the bytes, or a compiled module.
|
|
13
|
-
*/
|
|
14
|
-
wasm?: string | URL | Response | BufferSource | WebAssembly.Module;
|
|
15
|
-
}
|
|
4
|
+
export type DecoderPoolOptions = PoolOptions;
|
|
16
5
|
|
|
17
|
-
export interface DecodeOptions {
|
|
18
|
-
/**
|
|
19
|
-
* Hand the stream's buffer to the worker instead of copying it, leaving it
|
|
20
|
-
* detached here. For a buffer nothing else reads afterwards.
|
|
21
|
-
*/
|
|
22
|
-
transfer?: boolean;
|
|
23
|
-
}
|
|
6
|
+
export interface DecodeOptions extends DracoDecodeOptions, TaskOptions {}
|
|
24
7
|
|
|
25
|
-
export interface DecoderPool {
|
|
8
|
+
export interface DecoderPool extends Pool {
|
|
26
9
|
/**
|
|
27
10
|
* `decode_draco` on a worker. A stream that does not decode resolves with
|
|
28
11
|
* `success: false`, as `decode_draco` returns it; a worker that fails rejects
|
|
@@ -33,13 +16,7 @@ export interface DecoderPool {
|
|
|
33
16
|
attributes?: DracoAttributeRequest[],
|
|
34
17
|
options?: DecodeOptions,
|
|
35
18
|
): Promise<DracoDecodeResult>;
|
|
36
|
-
/** Compiles the wasm and starts one worker ahead of the first decode. */
|
|
37
|
-
preload(): Promise<void>;
|
|
38
|
-
/** Changes the worker limit; idle workers past it stop now, busy ones when done. */
|
|
39
|
-
setWorkerLimit(workers: number): void;
|
|
40
|
-
readonly workerLimit: number;
|
|
41
|
-
/** Stops every worker and rejects what they held. */
|
|
42
|
-
dispose(): void;
|
|
43
19
|
}
|
|
44
20
|
|
|
45
21
|
export function createDecoderPool(options?: DecoderPoolOptions): DecoderPool;
|
|
22
|
+
export { createDecoderPool as createPool };
|