@nadrif/thread 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/LICENSE +21 -0
- package/README.md +1156 -0
- package/package.json +133 -0
- package/src/adapters.js +404 -0
- package/src/config/adapters.js +188 -0
- package/src/config/define.js +171 -0
- package/src/config/env.js +218 -0
- package/src/config/frameworks.js +268 -0
- package/src/config/index.js +256 -0
- package/src/config/schema.js +375 -0
- package/src/config.js +52 -0
- package/src/deno.js +29 -0
- package/src/edge.js +38 -0
- package/src/env.js +361 -0
- package/src/error.js +340 -0
- package/src/factory.js +591 -0
- package/src/gpu/adapters.js +227 -0
- package/src/gpu/chains.js +261 -0
- package/src/gpu/env.js +372 -0
- package/src/gpu/gpu.js +1199 -0
- package/src/gpu/helpers.js +240 -0
- package/src/gpu/hooks.js +284 -0
- package/src/gpu/index.js +16 -0
- package/src/gpu/shaders.js +834 -0
- package/src/gpu/special.js +493 -0
- package/src/hooks.js +448 -0
- package/src/index.js +557 -0
- package/src/metrix.js +222 -0
- package/src/node.js +36 -0
- package/src/pool.js +740 -0
- package/src/serializer.js +139 -0
- package/src/thread.js +1246 -0
- package/src/types.js +1354 -0
- package/src/worker-factory.js +347 -0
package/package.json
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nadrif/thread",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "Nadrif LLC",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"module": "./index.js",
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"preact": ">=10.0.0",
|
|
9
|
+
"react": ">=18.0.0"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"browser": {
|
|
14
|
+
"import": "./src/index.js",
|
|
15
|
+
"types": "./src/types.js"
|
|
16
|
+
},
|
|
17
|
+
"node": {
|
|
18
|
+
"import": "./src/node.js",
|
|
19
|
+
"types": "./src/types.js"
|
|
20
|
+
},
|
|
21
|
+
"bun": {
|
|
22
|
+
"import": "./src/node.js",
|
|
23
|
+
"types": "./src/types.js"
|
|
24
|
+
},
|
|
25
|
+
"deno": {
|
|
26
|
+
"import": "./src/deno.js",
|
|
27
|
+
"types": "./src/types.js"
|
|
28
|
+
},
|
|
29
|
+
"edge": {
|
|
30
|
+
"import": "./src/edge.js",
|
|
31
|
+
"types": "./src/types.js"
|
|
32
|
+
},
|
|
33
|
+
"import": "./src/index.js",
|
|
34
|
+
"types": "./src/types.js"
|
|
35
|
+
},
|
|
36
|
+
"./node": {
|
|
37
|
+
"import": "./src/node.js",
|
|
38
|
+
"types": "./src/types.js"
|
|
39
|
+
},
|
|
40
|
+
"./deno": {
|
|
41
|
+
"import": "./src/deno.js",
|
|
42
|
+
"types": "./src/types.js"
|
|
43
|
+
},
|
|
44
|
+
"./edge": {
|
|
45
|
+
"import": "./src/edge.js",
|
|
46
|
+
"types": "./src/types.js"
|
|
47
|
+
},
|
|
48
|
+
"./config": {
|
|
49
|
+
"import": "./src/config.js",
|
|
50
|
+
"types": "./src/types.js"
|
|
51
|
+
},
|
|
52
|
+
"./env": {
|
|
53
|
+
"import": "./src/env.js",
|
|
54
|
+
"types": "./src/types.js"
|
|
55
|
+
},
|
|
56
|
+
"./gpu": {
|
|
57
|
+
"import": "./src/gpu/index.js",
|
|
58
|
+
"types": "./src/types.js"
|
|
59
|
+
},
|
|
60
|
+
"./gpu/env": {
|
|
61
|
+
"import": "./src/gpu/env.js",
|
|
62
|
+
"types": "./src/types.js"
|
|
63
|
+
},
|
|
64
|
+
"./gpu/core": "./src/gpu/gpu.js",
|
|
65
|
+
"./gpu/shaders": "./src/gpu/shaders.js",
|
|
66
|
+
"./gpu/helpers": "./src/gpu/helpers.js",
|
|
67
|
+
"./gpu/chains": "./src/gpu/chains.js",
|
|
68
|
+
"./gpu/special": "./src/gpu/special.js",
|
|
69
|
+
"./gpu/hooks": "./src/gpu/hooks.js",
|
|
70
|
+
"./gpu/adapters": "./src/gpu/adapters.js",
|
|
71
|
+
"./thread": "./src/thread.js",
|
|
72
|
+
"./pool": "./src/pool.js",
|
|
73
|
+
"./hooks": "./src/hooks.js",
|
|
74
|
+
"./adapters": "./src/adapters.js",
|
|
75
|
+
"./factory": "./src/factory.js",
|
|
76
|
+
"./error": "./src/error.js",
|
|
77
|
+
"./metrix": "./src/metrix.js",
|
|
78
|
+
"./serializer": "./src/serializer.js",
|
|
79
|
+
"./workers": "./src/worker-factory.js",
|
|
80
|
+
"./types": "./src/types.js"
|
|
81
|
+
},
|
|
82
|
+
"description": "Enterprise Web Worker & GPU Compute framework — cross-platform thread pools, work-stealing, WebGPU shaders, with framework adapters for Preact/React/Svelte/Vue/Solid and state managers Zustand/Redux/Jotai/MobX/Signals.",
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=18.0.0",
|
|
85
|
+
"bun": ">=1.0.0",
|
|
86
|
+
"deno": ">=1.25.0"
|
|
87
|
+
},
|
|
88
|
+
"files": [
|
|
89
|
+
"src/"
|
|
90
|
+
],
|
|
91
|
+
"keywords": [
|
|
92
|
+
"web-worker",
|
|
93
|
+
"thread-pool",
|
|
94
|
+
"webgpu",
|
|
95
|
+
"compute-shader",
|
|
96
|
+
"gpu",
|
|
97
|
+
"parallel",
|
|
98
|
+
"work-stealing",
|
|
99
|
+
"preact",
|
|
100
|
+
"react",
|
|
101
|
+
"svelte",
|
|
102
|
+
"vue",
|
|
103
|
+
"solid",
|
|
104
|
+
"zustand",
|
|
105
|
+
"redux",
|
|
106
|
+
"jotai",
|
|
107
|
+
"mobx",
|
|
108
|
+
"signals",
|
|
109
|
+
"deno",
|
|
110
|
+
"bun",
|
|
111
|
+
"node",
|
|
112
|
+
"edge",
|
|
113
|
+
"cloudflare-workers"
|
|
114
|
+
],
|
|
115
|
+
"license": "MIT",
|
|
116
|
+
"peerDependenciesMeta": {
|
|
117
|
+
"preact": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"react": {
|
|
121
|
+
"optional": true
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"trustedDependencies": [
|
|
125
|
+
"webgpu"
|
|
126
|
+
],
|
|
127
|
+
"type": "module",
|
|
128
|
+
"types": "./src/types.js",
|
|
129
|
+
"dependencies": {},
|
|
130
|
+
"optionalDependencies": {
|
|
131
|
+
"bun-webgpu": "^0.1.7"
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/adapters.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Framework-agnostic adapters for connecting threads to state
|
|
3
|
+
* management libraries.
|
|
4
|
+
*
|
|
5
|
+
* These adapters let you pipe thread/pool results into **Zustand** stores,
|
|
6
|
+
* **Preact Signals**, or any custom state manager – without coupling
|
|
7
|
+
* the thread to a specific framework.
|
|
8
|
+
*
|
|
9
|
+
* Every adapter follows the same pattern:
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* adapter(thread, ...) → { run, destroy }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* - `run(...args)` – execute the task **and** push the result to the store.
|
|
16
|
+
* - `destroy()` – clean up event listeners.
|
|
17
|
+
*
|
|
18
|
+
* **Why adapters instead of hooks?**
|
|
19
|
+
*
|
|
20
|
+
* Hooks are great inside components, but sometimes you need to wire a
|
|
21
|
+
* thread to a store **outside** the component tree (e.g. in a module,
|
|
22
|
+
* a web worker manager, or a service layer). Adapters work anywhere.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```js
|
|
26
|
+
* import { createZustandBinder } from './adapters.js';
|
|
27
|
+
* import { useDataStore } from '../stores/useDataStore.js';
|
|
28
|
+
* import { createThread } from './factory.js';
|
|
29
|
+
*
|
|
30
|
+
* const thread = createThread((query) => db.query(query));
|
|
31
|
+
*
|
|
32
|
+
* // Bind thread results to Zustand store
|
|
33
|
+
* const binder = createZustandBinder(thread, useDataStore, 'setData');
|
|
34
|
+
*
|
|
35
|
+
* // Now every thread.run() result is pushed to the store
|
|
36
|
+
* await binder.run('SELECT * FROM users');
|
|
37
|
+
* // → useDataStore.getState().data is now the result
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @module adapters
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Zustand adapter
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bind a thread's results to a **Zustand** store action.
|
|
49
|
+
*
|
|
50
|
+
* Every time `run()` resolves, the result is passed to the specified
|
|
51
|
+
* store action (typically a `set` call). Errors are also forwarded to
|
|
52
|
+
* an optional error action.
|
|
53
|
+
*
|
|
54
|
+
* @param {import('./thread.js').Thread} thread
|
|
55
|
+
* The thread to bind.
|
|
56
|
+
* @param {Function} store
|
|
57
|
+
* The Zustand store hook (the return value of `create()`).
|
|
58
|
+
* @param {string} action
|
|
59
|
+
* Name of the store action to call with the result. The action
|
|
60
|
+
* receives `(result)` as its argument.
|
|
61
|
+
* @param {Object} [options={}]
|
|
62
|
+
* @param {string} [options.errorAction]
|
|
63
|
+
* Name of the store action to call on error. Receives
|
|
64
|
+
* `(errorMessage)`. If omitted, errors are thrown (not swallowed).
|
|
65
|
+
* @param {boolean} [options.append=false]
|
|
66
|
+
* If `true`, the result is **appended** to an array in the store
|
|
67
|
+
* instead of replacing it. Useful for log-style stores.
|
|
68
|
+
* @param {string} [options.metricsAction]
|
|
69
|
+
* Name of the store action to call with every metrics snapshot.
|
|
70
|
+
* @returns {{ run: Function, destroy: Function }}
|
|
71
|
+
* `run(...args)` – execute the task and push to the store.
|
|
72
|
+
* `destroy()` – stop listening to thread events.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```js
|
|
76
|
+
* import { create } from 'zustand';
|
|
77
|
+
* import { createZustandBinder } from './adapters.js';
|
|
78
|
+
*
|
|
79
|
+
* const useStore = create((set) => ({
|
|
80
|
+
* data: null,
|
|
81
|
+
* loading: false,
|
|
82
|
+
* error: null,
|
|
83
|
+
* setData: (data) => set({ data, loading: false }),
|
|
84
|
+
* setError: (error) => set({ error, loading: false }),
|
|
85
|
+
* setLoading: () => set({ loading: true, error: null }),
|
|
86
|
+
* }));
|
|
87
|
+
*
|
|
88
|
+
* const thread = createThread((query) => db.query(query));
|
|
89
|
+
* const binder = createZustandBinder(thread, useStore, 'setData', {
|
|
90
|
+
* errorAction: 'setError',
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* // In a component:
|
|
94
|
+
* await binder.run('SELECT * FROM users');
|
|
95
|
+
* const data = useStore.getState().data;
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```js
|
|
100
|
+
* // Append mode – for streaming logs
|
|
101
|
+
* const logThread = createWorker((msg) => ({ text: msg, ts: Date.now() }));
|
|
102
|
+
* const binder = createZustandBinder(logThread, useLogStore, 'appendLog', {
|
|
103
|
+
* append: true,
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* binder.run('User logged in');
|
|
107
|
+
* binder.run('Page loaded');
|
|
108
|
+
* // useLogStore.getState().logs === [{ text: '...', ts: ... }, ...]
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function createZustandBinder(thread, store, action, options = {}) {
|
|
112
|
+
const { errorAction = null, append = false, metricsAction = null } = options;
|
|
113
|
+
|
|
114
|
+
const onResult = (result) => {
|
|
115
|
+
const state = store.getState();
|
|
116
|
+
if (append) {
|
|
117
|
+
const current = state[action] || state[Object.keys(state).find((k) => Array.isArray(state[k]))];
|
|
118
|
+
// If append mode, the action should accept (item) and handle appending
|
|
119
|
+
store.getState()[action](result);
|
|
120
|
+
} else {
|
|
121
|
+
store.getState()[action](result);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const onError = (info) => {
|
|
126
|
+
if (errorAction && store.getState()[errorAction]) {
|
|
127
|
+
store.getState()[errorAction](info.error || String(info));
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const onMetrics = metricsAction
|
|
132
|
+
? (snap) => {
|
|
133
|
+
if (store.getState()[metricsAction]) {
|
|
134
|
+
store.getState()[metricsAction](snap);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
: null;
|
|
138
|
+
|
|
139
|
+
thread.on('result', onResult);
|
|
140
|
+
thread.on('error', onError);
|
|
141
|
+
if (onMetrics) thread.on('metrics', onMetrics);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
run: (...args) => thread.run(...args),
|
|
145
|
+
destroy: () => {
|
|
146
|
+
thread.off('result', onResult);
|
|
147
|
+
thread.off('error', onError);
|
|
148
|
+
if (onMetrics) thread.off('metrics', onMetrics);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Preact Signals adapter
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Bind a thread's results to a **Preact Signal**.
|
|
159
|
+
*
|
|
160
|
+
* Every time `run()` resolves, the signal's value is updated. Errors
|
|
161
|
+
* can optionally be written to a separate signal.
|
|
162
|
+
*
|
|
163
|
+
* @param {import('./thread.js').Thread} thread
|
|
164
|
+
* The thread to bind.
|
|
165
|
+
* @param {Object} signal
|
|
166
|
+
* A Preact Signal (or any object with `.value` getter/setter).
|
|
167
|
+
* @param {Object} [options={}]
|
|
168
|
+
* @param {Object} [options.errorSignal]
|
|
169
|
+
* A signal to write errors into (the signal's `.value` is set to
|
|
170
|
+
* the error message string, or `null` on success).
|
|
171
|
+
* @param {Function} [options.transform]
|
|
172
|
+
* Optional transform applied to the result before writing to the
|
|
173
|
+
* signal: `(result) => transformedResult`.
|
|
174
|
+
* @returns {{ run: Function, destroy: Function }}
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```js
|
|
178
|
+
* import { signal } from '@preact/signals';
|
|
179
|
+
* import { createSignalBinder } from './adapters.js';
|
|
180
|
+
*
|
|
181
|
+
* const dataSignal = signal(null);
|
|
182
|
+
* const errorSignal = signal(null);
|
|
183
|
+
*
|
|
184
|
+
* const thread = createThread((x) => x * 2);
|
|
185
|
+
* const binder = createSignalBinder(thread, dataSignal, {
|
|
186
|
+
* errorSignal,
|
|
187
|
+
* });
|
|
188
|
+
*
|
|
189
|
+
* await binder.run(21);
|
|
190
|
+
* console.log(dataSignal.value); // 42
|
|
191
|
+
* console.log(errorSignal.value); // null
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```js
|
|
196
|
+
* // With transform
|
|
197
|
+
* const countSignal = signal(0);
|
|
198
|
+
* const binder = createSignalBinder(thread, countSignal, {
|
|
199
|
+
* transform: (result) => result.length,
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* await binder.run(['a', 'b', 'c']);
|
|
203
|
+
* console.log(countSignal.value); // 3
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function createSignalBinder(thread, signal, options = {}) {
|
|
207
|
+
const { errorSignal = null, transform = null } = options;
|
|
208
|
+
|
|
209
|
+
const onResult = (result) => {
|
|
210
|
+
signal.value = transform ? transform(result) : result;
|
|
211
|
+
if (errorSignal) errorSignal.value = null;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const onError = (info) => {
|
|
215
|
+
if (errorSignal) {
|
|
216
|
+
errorSignal.value = info.error || String(info);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
thread.on('result', onResult);
|
|
221
|
+
thread.on('error', onError);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
run: (...args) => thread.run(...args),
|
|
225
|
+
destroy: () => {
|
|
226
|
+
thread.off('result', onResult);
|
|
227
|
+
thread.off('error', onError);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Generic reactive adapter
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Bind a thread's results to **any** reactive state system via a setter
|
|
238
|
+
* callback.
|
|
239
|
+
*
|
|
240
|
+
* This is the most generic adapter – it works with MobX, Redux, Vue
|
|
241
|
+
* refs, Svelte stores, or plain `setState` functions.
|
|
242
|
+
*
|
|
243
|
+
* @param {import('./thread.js').Thread} thread
|
|
244
|
+
* The thread to bind.
|
|
245
|
+
* @param {Function} setter
|
|
246
|
+
* Called with `(result)` on success. Typically a state setter like
|
|
247
|
+
* `setState` or `store.set`.
|
|
248
|
+
* @param {Object} [options={}]
|
|
249
|
+
* @param {Function} [options.onError]
|
|
250
|
+
* Called with `(error)` on failure.
|
|
251
|
+
* @param {Function} [options.transform]
|
|
252
|
+
* Transform the result before passing to the setter.
|
|
253
|
+
* @param {Function} [options.onMetrics]
|
|
254
|
+
* Called with every metrics snapshot.
|
|
255
|
+
* @returns {{ run: Function, destroy: Function }}
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```js
|
|
259
|
+
* import { createStoreBinder } from './adapters.js';
|
|
260
|
+
*
|
|
261
|
+
* // With React/Preact setState
|
|
262
|
+
* function MyComponent() {
|
|
263
|
+
* const [data, setData] = useState(null);
|
|
264
|
+
* const thread = useMemo(() => createThread(process), []);
|
|
265
|
+
*
|
|
266
|
+
* useEffect(() => {
|
|
267
|
+
* const binder = createStoreBinder(thread, setData, {
|
|
268
|
+
* onError: (err) => console.error(err),
|
|
269
|
+
* });
|
|
270
|
+
* return binder.destroy;
|
|
271
|
+
* }, [thread]);
|
|
272
|
+
*
|
|
273
|
+
* return <div>{JSON.stringify(data)}</div>;
|
|
274
|
+
* }
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```js
|
|
279
|
+
* // With a plain object
|
|
280
|
+
* const state = { items: [] };
|
|
281
|
+
* const binder = createStoreBinder(thread, (result) => {
|
|
282
|
+
* state.items = result;
|
|
283
|
+
* render(); // re-render
|
|
284
|
+
* });
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export function createStoreBinder(thread, setter, options = {}) {
|
|
288
|
+
const { onError = null, transform = null, onMetrics = null } = options;
|
|
289
|
+
|
|
290
|
+
const onResult = (result) => {
|
|
291
|
+
setter(transform ? transform(result) : result);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const errorHandler = (info) => {
|
|
295
|
+
if (onError) onError(info.error || String(info));
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
thread.on('result', onResult);
|
|
299
|
+
thread.on('error', errorHandler);
|
|
300
|
+
if (onMetrics) thread.on('metrics', onMetrics);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
run: (...args) => thread.run(...args),
|
|
304
|
+
destroy: () => {
|
|
305
|
+
thread.off('result', onResult);
|
|
306
|
+
thread.off('error', errorHandler);
|
|
307
|
+
if (onMetrics) thread.off('metrics', onMetrics);
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Pool adapter
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Bind a thread pool's results to a reactive state system.
|
|
318
|
+
*
|
|
319
|
+
* Works like {@link createStoreBinder} but for pools. Additionally
|
|
320
|
+
* supports batching – multiple pool results can be accumulated and
|
|
321
|
+
* flushed together.
|
|
322
|
+
*
|
|
323
|
+
* @param {import('./pool.js').ThreadPool} pool
|
|
324
|
+
* The thread pool to bind.
|
|
325
|
+
* @param {Function} setter
|
|
326
|
+
* Called with `(result)` on each task completion.
|
|
327
|
+
* @param {Object} [options={}]
|
|
328
|
+
* @param {Function} [options.onError]
|
|
329
|
+
* Called with `(error)` on failure.
|
|
330
|
+
* @param {Function} [options.onMetrics]
|
|
331
|
+
* Called with pool metrics updates.
|
|
332
|
+
* @param {number} [options.batchInterval]
|
|
333
|
+
* If set, results are accumulated for this many ms and then flushed
|
|
334
|
+
* as an array to the setter. Useful for high-throughput scenarios.
|
|
335
|
+
* @returns {{ run: Function, destroy: Function, flush: Function }}
|
|
336
|
+
* `flush()` – manually flush accumulated batch results.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```js
|
|
340
|
+
* import { createPoolBinder } from './adapters.js';
|
|
341
|
+
*
|
|
342
|
+
* const pool = createPool(4, heavyComputation);
|
|
343
|
+
* const results = [];
|
|
344
|
+
*
|
|
345
|
+
* const binder = createPoolBinder(pool, (result) => {
|
|
346
|
+
* results.push(result);
|
|
347
|
+
* renderDashboard(results);
|
|
348
|
+
* }, {
|
|
349
|
+
* batchInterval: 500, // flush every 500ms
|
|
350
|
+
* onMetrics: (snap) => updateMetrics(snap),
|
|
351
|
+
* });
|
|
352
|
+
*
|
|
353
|
+
* items.forEach((item) => binder.run(item));
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
export function createPoolBinder(pool, setter, options = {}) {
|
|
357
|
+
const { onError = null, onMetrics = null, batchInterval = null } = options;
|
|
358
|
+
|
|
359
|
+
let batch = [];
|
|
360
|
+
let batchTimer = null;
|
|
361
|
+
|
|
362
|
+
const flush = () => {
|
|
363
|
+
if (batch.length > 0) {
|
|
364
|
+
setter(batch);
|
|
365
|
+
batch = [];
|
|
366
|
+
}
|
|
367
|
+
if (batchTimer) {
|
|
368
|
+
clearInterval(batchTimer);
|
|
369
|
+
batchTimer = null;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const onTaskResult = (result) => {
|
|
374
|
+
if (batchInterval) {
|
|
375
|
+
batch.push(result);
|
|
376
|
+
} else {
|
|
377
|
+
setter(result);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const onTaskError = (info) => {
|
|
382
|
+
if (onError) onError(info.error || String(info));
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
pool.on?.('result', onTaskResult);
|
|
386
|
+
pool.on?.('error', onTaskError);
|
|
387
|
+
if (onMetrics) pool.on?.('metrics', onMetrics);
|
|
388
|
+
|
|
389
|
+
if (batchInterval) {
|
|
390
|
+
batchTimer = setInterval(flush, batchInterval);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
run: (...args) => pool.run(...args),
|
|
395
|
+
flush,
|
|
396
|
+
destroy: () => {
|
|
397
|
+
flush();
|
|
398
|
+
pool.off?.('result', onTaskResult);
|
|
399
|
+
pool.off?.('error', onTaskError);
|
|
400
|
+
if (onMetrics) pool.off?.('metrics', onMetrics);
|
|
401
|
+
if (batchTimer) clearInterval(batchTimer);
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|