@oh-my-pi/pi-natives 8.12.4

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/src/pool.ts ADDED
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Generic worker pool for WASM-based operations.
3
+ *
4
+ * Supports both single-worker (maxWorkers: 1) and multi-worker scenarios.
5
+ * Workers are lazily created and auto-terminated after idle timeout.
6
+ */
7
+
8
+ /** Base request type - workers must accept messages with this shape. */
9
+ export interface BaseRequest {
10
+ type: string;
11
+ id?: number;
12
+ }
13
+
14
+ /** Base response type - workers must respond with this shape. */
15
+ export interface BaseResponse {
16
+ type: string;
17
+ id: number;
18
+ error?: string;
19
+ }
20
+
21
+ export interface WorkerPoolOptions {
22
+ /** URL to the worker script. */
23
+ workerUrl: string | URL;
24
+ /** Maximum number of workers (default: 4). */
25
+ maxWorkers?: number;
26
+ /** Idle timeout in ms before terminating unused workers (0 = never, default: 30000). */
27
+ idleTimeoutMs?: number;
28
+ /** Timeout for worker initialization in ms (default: 10000). */
29
+ initTimeoutMs?: number;
30
+ }
31
+
32
+ interface PooledWorker {
33
+ worker: Worker;
34
+ busy: boolean;
35
+ lastUsed: number;
36
+ currentRequestId: number | null;
37
+ }
38
+
39
+ interface PendingRequest<T> {
40
+ resolve: (result: T) => void;
41
+ reject: (error: Error) => void;
42
+ timeout?: ReturnType<typeof setTimeout>;
43
+ }
44
+
45
+ /**
46
+ * A pool of workers that process requests in parallel.
47
+ *
48
+ * @typeParam TReq - Request message type (must extend BaseRequest)
49
+ * @typeParam TRes - Response message type (must extend BaseResponse)
50
+ */
51
+ export class WorkerPool<TReq extends BaseRequest, TRes extends BaseResponse> {
52
+ readonly #options: Required<WorkerPoolOptions>;
53
+ readonly #pool: PooledWorker[] = [];
54
+ readonly #waiters: Array<(worker: PooledWorker) => void> = [];
55
+ readonly #pending = new Map<number, PendingRequest<TRes>>();
56
+ #nextRequestId = 1;
57
+ #idleCheckInterval: ReturnType<typeof setInterval> | null = null;
58
+
59
+ constructor(options: WorkerPoolOptions) {
60
+ this.#options = {
61
+ workerUrl: options.workerUrl,
62
+ maxWorkers: options.maxWorkers ?? 4,
63
+ idleTimeoutMs: options.idleTimeoutMs ?? 30_000,
64
+ initTimeoutMs: options.initTimeoutMs ?? 10_000,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Send a request to a worker and wait for the response.
70
+ * Workers are acquired from the pool (or created if under limit).
71
+ */
72
+ async request<T extends TRes = TRes>(
73
+ msg: TReq | (Omit<TReq, "id"> & { id?: number }),
74
+ transfer?: ArrayBufferLike[],
75
+ ): Promise<T> {
76
+ const worker = await this.#acquireWorker();
77
+ const id = msg.id ?? this.#nextRequestId++;
78
+ const fullMsg = { ...msg, id } as TReq;
79
+
80
+ const { promise, resolve, reject } = Promise.withResolvers<T>();
81
+ this.#pending.set(id, {
82
+ resolve: resolve as (result: TRes) => void,
83
+ reject,
84
+ });
85
+
86
+ worker.currentRequestId = id;
87
+ if (transfer) {
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ worker.worker.postMessage(fullMsg, transfer as any);
90
+ } else {
91
+ worker.worker.postMessage(fullMsg);
92
+ }
93
+
94
+ return promise;
95
+ }
96
+
97
+ /** Terminate all workers and clear pending requests. */
98
+ terminate(): void {
99
+ if (this.#idleCheckInterval) {
100
+ clearInterval(this.#idleCheckInterval);
101
+ this.#idleCheckInterval = null;
102
+ }
103
+
104
+ for (const w of [...this.#pool]) {
105
+ this.#removeWorker(w);
106
+ }
107
+
108
+ this.#waiters.length = 0;
109
+
110
+ for (const pending of this.#pending.values()) {
111
+ pending.reject(new Error("Worker pool terminated"));
112
+ if (pending.timeout) clearTimeout(pending.timeout);
113
+ }
114
+ this.#pending.clear();
115
+ }
116
+
117
+ #createWorker(): PooledWorker {
118
+ const worker = new Worker(this.#options.workerUrl);
119
+
120
+ const pooledWorker: PooledWorker = {
121
+ worker,
122
+ busy: false,
123
+ lastUsed: Date.now(),
124
+ currentRequestId: null,
125
+ };
126
+
127
+ worker.onmessage = (e: MessageEvent<TRes>) => {
128
+ this.#handleMessage(pooledWorker, e.data);
129
+ };
130
+
131
+ worker.onerror = (e: ErrorEvent) => {
132
+ const requestId = pooledWorker.currentRequestId;
133
+ if (requestId !== null) {
134
+ this.#rejectRequest(requestId, new Error(`Worker error: ${e.message}`));
135
+ }
136
+ this.#removeWorker(pooledWorker);
137
+ };
138
+
139
+ return pooledWorker;
140
+ }
141
+
142
+ #handleMessage(pooledWorker: PooledWorker, msg: TRes): void {
143
+ const pending = this.#pending.get(msg.id);
144
+ if (!pending) return;
145
+
146
+ this.#pending.delete(msg.id);
147
+ if (pending.timeout) clearTimeout(pending.timeout);
148
+
149
+ if (msg.type === "error" && "error" in msg) {
150
+ pending.reject(new Error(msg.error ?? "Unknown error"));
151
+ } else {
152
+ pending.resolve(msg);
153
+ }
154
+
155
+ // Release worker back to pool (unless it was the init request)
156
+ if (msg.type !== "ready") {
157
+ pooledWorker.currentRequestId = null;
158
+ this.#releaseWorker(pooledWorker);
159
+ }
160
+ }
161
+
162
+ #rejectRequest(id: number, error: Error): void {
163
+ const pending = this.#pending.get(id);
164
+ if (pending) {
165
+ this.#pending.delete(id);
166
+ if (pending.timeout) clearTimeout(pending.timeout);
167
+ pending.reject(error);
168
+ }
169
+ }
170
+
171
+ #removeWorker(pooledWorker: PooledWorker): void {
172
+ const idx = this.#pool.indexOf(pooledWorker);
173
+ if (idx !== -1) {
174
+ this.#pool.splice(idx, 1);
175
+ }
176
+ pooledWorker.worker.postMessage({ type: "destroy" } satisfies BaseRequest);
177
+ pooledWorker.worker.terminate();
178
+ }
179
+
180
+ #releaseWorker(pooledWorker: PooledWorker): void {
181
+ pooledWorker.busy = false;
182
+ pooledWorker.lastUsed = Date.now();
183
+
184
+ if (this.#waiters.length) {
185
+ const waiter = this.#waiters.shift()!;
186
+ pooledWorker.busy = true;
187
+ waiter(pooledWorker);
188
+ }
189
+ }
190
+
191
+ #checkIdleWorkers(): void {
192
+ if (this.#options.idleTimeoutMs === 0) return;
193
+
194
+ const now = Date.now();
195
+ const toRemove: PooledWorker[] = [];
196
+
197
+ for (const w of this.#pool) {
198
+ if (!w.busy && now - w.lastUsed > this.#options.idleTimeoutMs && !this.#waiters.length) {
199
+ toRemove.push(w);
200
+ }
201
+ }
202
+
203
+ for (const w of toRemove) {
204
+ this.#removeWorker(w);
205
+ }
206
+
207
+ if (this.#pool.length === 0 && this.#idleCheckInterval) {
208
+ clearInterval(this.#idleCheckInterval);
209
+ this.#idleCheckInterval = null;
210
+ }
211
+ }
212
+
213
+ #ensureIdleCheck(): void {
214
+ if (this.#options.idleTimeoutMs > 0 && !this.#idleCheckInterval) {
215
+ this.#idleCheckInterval = setInterval(() => this.#checkIdleWorkers(), 10_000);
216
+ }
217
+ }
218
+
219
+ async #initializeWorker(pooledWorker: PooledWorker): Promise<void> {
220
+ const id = this.#nextRequestId++;
221
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
222
+
223
+ const timeout = setTimeout(() => {
224
+ this.#rejectRequest(id, new Error("Worker initialization timeout"));
225
+ }, this.#options.initTimeoutMs);
226
+
227
+ this.#pending.set(id, {
228
+ resolve: () => resolve(),
229
+ reject,
230
+ timeout,
231
+ } as PendingRequest<TRes>);
232
+
233
+ pooledWorker.worker.postMessage({ type: "init", id } satisfies BaseRequest);
234
+ return promise;
235
+ }
236
+
237
+ async #acquireWorker(): Promise<PooledWorker> {
238
+ // Try to find an idle worker
239
+ for (const w of this.#pool) {
240
+ if (!w.busy) {
241
+ w.busy = true;
242
+ return w;
243
+ }
244
+ }
245
+
246
+ // Create new worker if under limit
247
+ if (this.#pool.length < this.#options.maxWorkers) {
248
+ const worker = this.#createWorker();
249
+ worker.busy = true;
250
+ this.#pool.push(worker);
251
+ this.#ensureIdleCheck();
252
+ await this.#initializeWorker(worker);
253
+ return worker;
254
+ }
255
+
256
+ // Wait for a worker to become available
257
+ const { promise, resolve } = Promise.withResolvers<PooledWorker>();
258
+ this.#waiters.push(w => {
259
+ w.busy = true;
260
+ resolve(w);
261
+ });
262
+ return promise;
263
+ }
264
+ }