@dmop/puru 0.1.11 → 0.1.13
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/AGENTS.md +2 -2
- package/README.md +11 -2
- package/dist/index.cjs +348 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -6
- package/dist/index.d.ts +77 -6
- package/dist/index.js +347 -52
- package/dist/index.js.map +1 -1
- package/llms-full.txt +2 -2
- package/llms.txt +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
var NATIVE_CODE_RE = /\[native code\]/;
|
|
3
3
|
var METHOD_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/;
|
|
4
4
|
var VALID_FN_START_RE = /^(?:function\b|async\s+function\b|async\s*\(|\(|[a-zA-Z_$][a-zA-Z0-9_$]*\s*=>|async\s+[a-zA-Z_$])/;
|
|
5
|
+
var serializeCache = /* @__PURE__ */ new WeakMap();
|
|
5
6
|
function serializeFunction(fn) {
|
|
6
7
|
if (typeof fn !== "function") {
|
|
7
8
|
throw new TypeError("Expected a function");
|
|
8
9
|
}
|
|
10
|
+
const cached = serializeCache.get(fn);
|
|
11
|
+
if (cached) return cached;
|
|
9
12
|
const str = fn.toString();
|
|
10
13
|
if (typeof str !== "string" || str.length === 0) {
|
|
11
14
|
throw new TypeError(
|
|
@@ -27,6 +30,7 @@ function serializeFunction(fn) {
|
|
|
27
30
|
"Class methods cannot be serialized. Use an arrow function wrapper instead."
|
|
28
31
|
);
|
|
29
32
|
}
|
|
33
|
+
serializeCache.set(fn, str);
|
|
30
34
|
return str;
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -147,15 +151,23 @@ function __buildChannelProxies(channels) {
|
|
|
147
151
|
return proxies;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
|
|
154
|
+
const __fnCache = new Map();
|
|
155
|
+
const __FN_CACHE_MAX = 1000;
|
|
156
|
+
|
|
157
|
+
function __execFn(fnStr, channels, args) {
|
|
158
|
+
let parsedFn = __fnCache.get(fnStr);
|
|
159
|
+
if (!parsedFn) {
|
|
160
|
+
parsedFn = (new Function('return (' + fnStr + ')'))();
|
|
161
|
+
if (__fnCache.size >= __FN_CACHE_MAX) __fnCache.delete(__fnCache.keys().next().value);
|
|
162
|
+
__fnCache.set(fnStr, parsedFn);
|
|
163
|
+
}
|
|
164
|
+
if (args) {
|
|
165
|
+
return parsedFn(...args);
|
|
166
|
+
}
|
|
151
167
|
if (channels) {
|
|
152
|
-
|
|
153
|
-
const fn = new Function('__ch', 'return (' + fnStr + ')(__ch)');
|
|
154
|
-
return fn(__ch);
|
|
155
|
-
} else {
|
|
156
|
-
const fn = new Function('return (' + fnStr + ')()');
|
|
157
|
-
return fn();
|
|
168
|
+
return parsedFn(__buildChannelProxies(channels));
|
|
158
169
|
}
|
|
170
|
+
return parsedFn();
|
|
159
171
|
}
|
|
160
172
|
`;
|
|
161
173
|
var NODE_BOOTSTRAP_CODE = `
|
|
@@ -181,7 +193,7 @@ parentPort.on('message', async (msg) => {
|
|
|
181
193
|
return;
|
|
182
194
|
}
|
|
183
195
|
try {
|
|
184
|
-
const result = await __execFn(msg.fnStr, msg.channels);
|
|
196
|
+
const result = await __execFn(msg.fnStr, msg.channels, msg.args);
|
|
185
197
|
if (!cancelledTasks.has(msg.taskId)) {
|
|
186
198
|
parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
|
|
187
199
|
}
|
|
@@ -200,7 +212,7 @@ parentPort.on('message', async (msg) => {
|
|
|
200
212
|
})();
|
|
201
213
|
} else {
|
|
202
214
|
try {
|
|
203
|
-
const result = await __execFn(msg.fnStr, msg.channels);
|
|
215
|
+
const result = await __execFn(msg.fnStr, msg.channels, msg.args);
|
|
204
216
|
parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
|
|
205
217
|
} catch (error) {
|
|
206
218
|
parentPort.postMessage({
|
|
@@ -241,7 +253,7 @@ self.onmessage = async (event) => {
|
|
|
241
253
|
return;
|
|
242
254
|
}
|
|
243
255
|
try {
|
|
244
|
-
const result = await __execFn(msg.fnStr, msg.channels);
|
|
256
|
+
const result = await __execFn(msg.fnStr, msg.channels, msg.args);
|
|
245
257
|
if (!cancelledTasks.has(msg.taskId)) {
|
|
246
258
|
self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
|
|
247
259
|
}
|
|
@@ -260,7 +272,7 @@ self.onmessage = async (event) => {
|
|
|
260
272
|
})();
|
|
261
273
|
} else {
|
|
262
274
|
try {
|
|
263
|
-
const result = await __execFn(msg.fnStr, msg.channels);
|
|
275
|
+
const result = await __execFn(msg.fnStr, msg.channels, msg.args);
|
|
264
276
|
self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
|
|
265
277
|
} catch (error) {
|
|
266
278
|
self.postMessage({
|
|
@@ -347,17 +359,14 @@ var BunManagedWorker = class {
|
|
|
347
359
|
on(event, handler) {
|
|
348
360
|
if (event === "message") {
|
|
349
361
|
this.worker.addEventListener("message", (e) => {
|
|
350
|
-
;
|
|
351
362
|
handler(e.data);
|
|
352
363
|
});
|
|
353
364
|
} else if (event === "error") {
|
|
354
365
|
this.worker.addEventListener("error", (e) => {
|
|
355
|
-
;
|
|
356
366
|
handler(e.error ?? new Error(e.message));
|
|
357
367
|
});
|
|
358
368
|
} else if (event === "exit") {
|
|
359
369
|
this.worker.addEventListener("close", (e) => {
|
|
360
|
-
;
|
|
361
370
|
handler(e.code ?? 0);
|
|
362
371
|
});
|
|
363
372
|
}
|
|
@@ -387,6 +396,8 @@ var ChannelImpl = class {
|
|
|
387
396
|
// constraint: can't create channels of nullable type
|
|
388
397
|
/** @internal */
|
|
389
398
|
_id;
|
|
399
|
+
/** @internal — true once the channel ID has been sent to a worker */
|
|
400
|
+
_shared = false;
|
|
390
401
|
buffer = [];
|
|
391
402
|
capacity;
|
|
392
403
|
closed = false;
|
|
@@ -428,6 +439,7 @@ var ChannelImpl = class {
|
|
|
428
439
|
this.buffer.push(sender2.value);
|
|
429
440
|
sender2.resolve();
|
|
430
441
|
}
|
|
442
|
+
this.maybeUnregister();
|
|
431
443
|
return Promise.resolve(value);
|
|
432
444
|
}
|
|
433
445
|
const sender = this.sendQueue.shift();
|
|
@@ -436,6 +448,7 @@ var ChannelImpl = class {
|
|
|
436
448
|
return Promise.resolve(sender.value);
|
|
437
449
|
}
|
|
438
450
|
if (this.closed) {
|
|
451
|
+
this.maybeUnregister();
|
|
439
452
|
return Promise.resolve(null);
|
|
440
453
|
}
|
|
441
454
|
return new Promise((resolve) => {
|
|
@@ -456,6 +469,11 @@ var ChannelImpl = class {
|
|
|
456
469
|
}
|
|
457
470
|
this.sendQueue = [];
|
|
458
471
|
}
|
|
472
|
+
maybeUnregister() {
|
|
473
|
+
if (!this._shared && this.closed && this.buffer.length === 0 && this.recvQueue.length === 0) {
|
|
474
|
+
channelRegistry.delete(this._id);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
459
477
|
sendOnly() {
|
|
460
478
|
const send = (value) => this.send(value);
|
|
461
479
|
const close = () => this.close();
|
|
@@ -508,7 +526,9 @@ function getChannelById(id) {
|
|
|
508
526
|
return channelRegistry.get(id);
|
|
509
527
|
}
|
|
510
528
|
function getChannelId(channel) {
|
|
511
|
-
|
|
529
|
+
const impl = channel;
|
|
530
|
+
impl._shared = true;
|
|
531
|
+
return impl._id;
|
|
512
532
|
}
|
|
513
533
|
|
|
514
534
|
// src/adapters/inline.ts
|
|
@@ -520,6 +540,7 @@ var InlineManagedWorker = class {
|
|
|
520
540
|
exitHandlers = [];
|
|
521
541
|
terminated = false;
|
|
522
542
|
cancelledTasks = /* @__PURE__ */ new Set();
|
|
543
|
+
fnCache = /* @__PURE__ */ new Map();
|
|
523
544
|
constructor() {
|
|
524
545
|
this.id = ++inlineIdCounter;
|
|
525
546
|
queueMicrotask(() => {
|
|
@@ -529,7 +550,7 @@ var InlineManagedWorker = class {
|
|
|
529
550
|
postMessage(msg) {
|
|
530
551
|
if (this.terminated) return;
|
|
531
552
|
if (msg.type === "execute") {
|
|
532
|
-
this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels);
|
|
553
|
+
this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels, msg.args);
|
|
533
554
|
} else if (msg.type === "cancel") {
|
|
534
555
|
this.cancelledTasks.add(msg.taskId);
|
|
535
556
|
} else if (msg.type === "channel-result") {
|
|
@@ -590,7 +611,7 @@ var InlineManagedWorker = class {
|
|
|
590
611
|
}
|
|
591
612
|
return proxies;
|
|
592
613
|
}
|
|
593
|
-
executeTask(taskId, fnStr, concurrent, channels) {
|
|
614
|
+
executeTask(taskId, fnStr, concurrent, channels, args) {
|
|
594
615
|
queueMicrotask(async () => {
|
|
595
616
|
if (this.terminated) return;
|
|
596
617
|
if (concurrent && this.cancelledTasks.has(taskId)) {
|
|
@@ -598,14 +619,20 @@ var InlineManagedWorker = class {
|
|
|
598
619
|
return;
|
|
599
620
|
}
|
|
600
621
|
try {
|
|
622
|
+
let parsedFn = this.fnCache.get(fnStr);
|
|
623
|
+
if (!parsedFn) {
|
|
624
|
+
parsedFn = new Function("return (" + fnStr + ")")();
|
|
625
|
+
if (this.fnCache.size >= 1e3) this.fnCache.clear();
|
|
626
|
+
this.fnCache.set(fnStr, parsedFn);
|
|
627
|
+
}
|
|
601
628
|
let result;
|
|
602
|
-
if (
|
|
629
|
+
if (args) {
|
|
630
|
+
result = await parsedFn(...args);
|
|
631
|
+
} else if (channels) {
|
|
603
632
|
const proxies = this.buildChannelProxies(channels);
|
|
604
|
-
|
|
605
|
-
result = await fn(proxies);
|
|
633
|
+
result = await parsedFn(proxies);
|
|
606
634
|
} else {
|
|
607
|
-
|
|
608
|
-
result = await fn();
|
|
635
|
+
result = await parsedFn();
|
|
609
636
|
}
|
|
610
637
|
if (concurrent && this.cancelledTasks.has(taskId)) {
|
|
611
638
|
this.cancelledTasks.delete(taskId);
|
|
@@ -658,6 +685,8 @@ var WorkerPool = class {
|
|
|
658
685
|
totalCompleted = 0;
|
|
659
686
|
totalFailed = 0;
|
|
660
687
|
taskMap = /* @__PURE__ */ new Map();
|
|
688
|
+
// Per-worker deques for work-stealing strategy
|
|
689
|
+
workerDeques = /* @__PURE__ */ new Map();
|
|
661
690
|
constructor(config, adapter) {
|
|
662
691
|
this.config = config;
|
|
663
692
|
this.adapter = adapter;
|
|
@@ -695,6 +724,110 @@ var WorkerPool = class {
|
|
|
695
724
|
}
|
|
696
725
|
return void 0;
|
|
697
726
|
}
|
|
727
|
+
// --- Work-stealing helpers ---
|
|
728
|
+
getOrCreateDeque(worker) {
|
|
729
|
+
let deque = this.workerDeques.get(worker);
|
|
730
|
+
if (!deque) {
|
|
731
|
+
deque = { high: [], normal: [], low: [] };
|
|
732
|
+
this.workerDeques.set(worker, deque);
|
|
733
|
+
}
|
|
734
|
+
return deque;
|
|
735
|
+
}
|
|
736
|
+
dequeSize(worker) {
|
|
737
|
+
const deque = this.workerDeques.get(worker);
|
|
738
|
+
if (!deque) return 0;
|
|
739
|
+
return deque.high.length + deque.normal.length + deque.low.length;
|
|
740
|
+
}
|
|
741
|
+
enqueueToWorker(worker, task2) {
|
|
742
|
+
this.getOrCreateDeque(worker)[task2.priority].push(task2);
|
|
743
|
+
}
|
|
744
|
+
/** Pop from own deque — FIFO within each priority level. */
|
|
745
|
+
dequeueFromOwn(worker) {
|
|
746
|
+
const deque = this.workerDeques.get(worker);
|
|
747
|
+
if (!deque) return void 0;
|
|
748
|
+
return deque.high.shift() ?? deque.normal.shift() ?? deque.low.shift();
|
|
749
|
+
}
|
|
750
|
+
/** Steal from a victim's deque — takes lowest-priority work from the back. */
|
|
751
|
+
stealFrom(victim) {
|
|
752
|
+
const deque = this.workerDeques.get(victim);
|
|
753
|
+
if (!deque) return void 0;
|
|
754
|
+
return deque.low.pop() ?? deque.normal.pop() ?? deque.high.pop();
|
|
755
|
+
}
|
|
756
|
+
/** Find the exclusive worker with the shortest deque to push a new task to. */
|
|
757
|
+
findShortestDequeWorker() {
|
|
758
|
+
let best;
|
|
759
|
+
let bestSize = Infinity;
|
|
760
|
+
const seen = /* @__PURE__ */ new Set();
|
|
761
|
+
for (const worker of this.exclusiveWorkers.values()) {
|
|
762
|
+
if (seen.has(worker)) continue;
|
|
763
|
+
seen.add(worker);
|
|
764
|
+
const size = this.dequeSize(worker);
|
|
765
|
+
if (size < bestSize) {
|
|
766
|
+
bestSize = size;
|
|
767
|
+
best = worker;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return best;
|
|
771
|
+
}
|
|
772
|
+
/** Steal a task from the busiest worker's deque, excluding the thief. */
|
|
773
|
+
stealFromBusiest(thief) {
|
|
774
|
+
let victim;
|
|
775
|
+
let maxSize = 0;
|
|
776
|
+
for (const [worker, deque] of this.workerDeques) {
|
|
777
|
+
if (worker === thief) continue;
|
|
778
|
+
const size = deque.high.length + deque.normal.length + deque.low.length;
|
|
779
|
+
if (size > maxSize) {
|
|
780
|
+
maxSize = size;
|
|
781
|
+
victim = worker;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (!victim || maxSize === 0) return void 0;
|
|
785
|
+
return this.stealFrom(victim);
|
|
786
|
+
}
|
|
787
|
+
/** Steal from any deque (no thief exclusion — used by resize). */
|
|
788
|
+
stealFromAny() {
|
|
789
|
+
let victim;
|
|
790
|
+
let maxSize = 0;
|
|
791
|
+
for (const [worker, deque] of this.workerDeques) {
|
|
792
|
+
const size = deque.high.length + deque.normal.length + deque.low.length;
|
|
793
|
+
if (size > maxSize) {
|
|
794
|
+
maxSize = size;
|
|
795
|
+
victim = worker;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (!victim || maxSize === 0) return void 0;
|
|
799
|
+
return this.stealFrom(victim);
|
|
800
|
+
}
|
|
801
|
+
/** Remove a task by ID from any worker's deque. */
|
|
802
|
+
removeFromDeques(taskId) {
|
|
803
|
+
for (const [, deque] of this.workerDeques) {
|
|
804
|
+
for (const priority of ["high", "normal", "low"]) {
|
|
805
|
+
const queue = deque[priority];
|
|
806
|
+
const idx = queue.findIndex((t) => t.id === taskId);
|
|
807
|
+
if (idx !== -1) {
|
|
808
|
+
return queue.splice(idx, 1)[0];
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return void 0;
|
|
813
|
+
}
|
|
814
|
+
/** Flush a worker's deque back to the global queue (for redistribution). */
|
|
815
|
+
flushDeque(worker) {
|
|
816
|
+
const deque = this.workerDeques.get(worker);
|
|
817
|
+
if (!deque) return;
|
|
818
|
+
for (const priority of ["high", "normal", "low"]) {
|
|
819
|
+
for (const task2 of deque[priority]) {
|
|
820
|
+
this.queues[priority].push(task2);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
this.workerDeques.delete(worker);
|
|
824
|
+
}
|
|
825
|
+
/** Clean up a deque if it's empty. */
|
|
826
|
+
cleanupDeque(worker) {
|
|
827
|
+
if (this.dequeSize(worker) === 0) {
|
|
828
|
+
this.workerDeques.delete(worker);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
698
831
|
// --- Submit ---
|
|
699
832
|
submit(task2) {
|
|
700
833
|
if (this.draining) {
|
|
@@ -720,6 +853,13 @@ var WorkerPool = class {
|
|
|
720
853
|
this.createAndReadyWorker();
|
|
721
854
|
return;
|
|
722
855
|
}
|
|
856
|
+
if (this.config.strategy === "work-stealing") {
|
|
857
|
+
const target = this.findShortestDequeWorker();
|
|
858
|
+
if (target) {
|
|
859
|
+
this.enqueueToWorker(target, task2);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
723
863
|
this.enqueue(task2);
|
|
724
864
|
}
|
|
725
865
|
submitConcurrent(task2) {
|
|
@@ -757,6 +897,7 @@ var WorkerPool = class {
|
|
|
757
897
|
type: "execute",
|
|
758
898
|
taskId: task2.id,
|
|
759
899
|
fnStr: task2.fnStr,
|
|
900
|
+
args: task2.args,
|
|
760
901
|
concurrent: false,
|
|
761
902
|
channels: task2.channels
|
|
762
903
|
};
|
|
@@ -778,6 +919,7 @@ var WorkerPool = class {
|
|
|
778
919
|
type: "execute",
|
|
779
920
|
taskId: task2.id,
|
|
780
921
|
fnStr: task2.fnStr,
|
|
922
|
+
args: task2.args,
|
|
781
923
|
concurrent: true,
|
|
782
924
|
channels: task2.channels
|
|
783
925
|
};
|
|
@@ -824,6 +966,19 @@ var WorkerPool = class {
|
|
|
824
966
|
}
|
|
825
967
|
assignNextOrIdle(worker) {
|
|
826
968
|
if (!this.allWorkers.has(worker)) return;
|
|
969
|
+
if (this.config.strategy === "work-stealing") {
|
|
970
|
+
const own = this.dequeueFromOwn(worker);
|
|
971
|
+
if (own) {
|
|
972
|
+
this.cleanupDeque(worker);
|
|
973
|
+
this.dispatch(worker, own);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const stolen = this.stealFromBusiest(worker);
|
|
977
|
+
if (stolen) {
|
|
978
|
+
this.dispatch(worker, stolen);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
827
982
|
const next = this.dequeue();
|
|
828
983
|
if (next) {
|
|
829
984
|
this.dispatch(worker, next);
|
|
@@ -834,6 +989,7 @@ var WorkerPool = class {
|
|
|
834
989
|
this.dispatchConcurrent(worker, concurrentNext);
|
|
835
990
|
return;
|
|
836
991
|
}
|
|
992
|
+
this.cleanupDeque(worker);
|
|
837
993
|
this.makeIdle(worker);
|
|
838
994
|
}
|
|
839
995
|
assignNextConcurrentOrIdle(worker) {
|
|
@@ -853,6 +1009,13 @@ var WorkerPool = class {
|
|
|
853
1009
|
const exclusiveTask = this.dequeue();
|
|
854
1010
|
if (exclusiveTask) {
|
|
855
1011
|
this.dispatch(worker, exclusiveTask);
|
|
1012
|
+
} else if (this.config.strategy === "work-stealing") {
|
|
1013
|
+
const stolen = this.stealFromBusiest(worker);
|
|
1014
|
+
if (stolen) {
|
|
1015
|
+
this.dispatch(worker, stolen);
|
|
1016
|
+
} else {
|
|
1017
|
+
this.makeIdle(worker);
|
|
1018
|
+
}
|
|
856
1019
|
} else {
|
|
857
1020
|
this.makeIdle(worker);
|
|
858
1021
|
}
|
|
@@ -948,6 +1111,13 @@ var WorkerPool = class {
|
|
|
948
1111
|
removed.reject(new DOMException("Task was cancelled", "AbortError"));
|
|
949
1112
|
return;
|
|
950
1113
|
}
|
|
1114
|
+
if (this.config.strategy === "work-stealing") {
|
|
1115
|
+
const removedFromDeque = this.removeFromDeques(taskId);
|
|
1116
|
+
if (removedFromDeque) {
|
|
1117
|
+
removedFromDeque.reject(new DOMException("Task was cancelled", "AbortError"));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
951
1121
|
const removedConcurrent = this.removeFromConcurrentQueue(taskId);
|
|
952
1122
|
if (removedConcurrent) {
|
|
953
1123
|
removedConcurrent.reject(new DOMException("Task was cancelled", "AbortError"));
|
|
@@ -958,6 +1128,7 @@ var WorkerPool = class {
|
|
|
958
1128
|
this.exclusiveWorkers.delete(taskId);
|
|
959
1129
|
this.allWorkers.delete(exclusiveWorker);
|
|
960
1130
|
this.taskMap.delete(taskId);
|
|
1131
|
+
this.flushDeque(exclusiveWorker);
|
|
961
1132
|
exclusiveWorker.terminate();
|
|
962
1133
|
return;
|
|
963
1134
|
}
|
|
@@ -989,6 +1160,14 @@ var WorkerPool = class {
|
|
|
989
1160
|
}
|
|
990
1161
|
this.concurrentQueues[priority] = [];
|
|
991
1162
|
}
|
|
1163
|
+
for (const [, deque] of this.workerDeques) {
|
|
1164
|
+
for (const priority of ["high", "normal", "low"]) {
|
|
1165
|
+
for (const task2 of deque[priority]) {
|
|
1166
|
+
task2.reject(new Error("Pool is shutting down"));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
this.workerDeques.clear();
|
|
992
1171
|
for (const [taskId] of this.exclusiveWorkers) {
|
|
993
1172
|
this.taskMap.delete(taskId);
|
|
994
1173
|
}
|
|
@@ -1016,7 +1195,7 @@ var WorkerPool = class {
|
|
|
1016
1195
|
while (true) {
|
|
1017
1196
|
const totalWorkers = this.allWorkers.size + this.pendingWorkerCount;
|
|
1018
1197
|
if (totalWorkers >= maxThreads) break;
|
|
1019
|
-
const task2 = this.dequeue() ?? this.dequeueConcurrent();
|
|
1198
|
+
const task2 = this.dequeue() ?? (this.config.strategy === "work-stealing" ? this.stealFromAny() : void 0) ?? this.dequeueConcurrent();
|
|
1020
1199
|
if (!task2) break;
|
|
1021
1200
|
this.pendingWorkerCount++;
|
|
1022
1201
|
this.pendingTasksForWorkers.push(task2);
|
|
@@ -1079,6 +1258,7 @@ var WorkerPool = class {
|
|
|
1079
1258
|
this.idleWorkers.splice(idleIdx, 1);
|
|
1080
1259
|
}
|
|
1081
1260
|
this.rejectExclusiveTaskForWorker(worker, new Error("Worker exited unexpectedly"));
|
|
1261
|
+
this.flushDeque(worker);
|
|
1082
1262
|
const taskSet = this.sharedWorkers.get(worker);
|
|
1083
1263
|
if (taskSet) {
|
|
1084
1264
|
for (const taskId of taskSet) {
|
|
@@ -1094,6 +1274,17 @@ var WorkerPool = class {
|
|
|
1094
1274
|
for (const taskSet of this.sharedWorkers.values()) {
|
|
1095
1275
|
concurrentTasks += taskSet.size;
|
|
1096
1276
|
}
|
|
1277
|
+
let dequeHigh = 0;
|
|
1278
|
+
let dequeNormal = 0;
|
|
1279
|
+
let dequeLow = 0;
|
|
1280
|
+
for (const deque of this.workerDeques.values()) {
|
|
1281
|
+
dequeHigh += deque.high.length;
|
|
1282
|
+
dequeNormal += deque.normal.length;
|
|
1283
|
+
dequeLow += deque.low.length;
|
|
1284
|
+
}
|
|
1285
|
+
const queuedHigh = this.queues.high.length + dequeHigh;
|
|
1286
|
+
const queuedNormal = this.queues.normal.length + dequeNormal;
|
|
1287
|
+
const queuedLow = this.queues.low.length + dequeLow;
|
|
1097
1288
|
return {
|
|
1098
1289
|
totalWorkers: this.allWorkers.size,
|
|
1099
1290
|
idleWorkers: this.idleWorkers.length,
|
|
@@ -1102,10 +1293,10 @@ var WorkerPool = class {
|
|
|
1102
1293
|
concurrentTasks,
|
|
1103
1294
|
pendingWorkers: this.pendingWorkerCount,
|
|
1104
1295
|
queuedTasks: {
|
|
1105
|
-
high:
|
|
1106
|
-
normal:
|
|
1107
|
-
low:
|
|
1108
|
-
total:
|
|
1296
|
+
high: queuedHigh,
|
|
1297
|
+
normal: queuedNormal,
|
|
1298
|
+
low: queuedLow,
|
|
1299
|
+
total: queuedHigh + queuedNormal + queuedLow
|
|
1109
1300
|
},
|
|
1110
1301
|
queuedConcurrentTasks: {
|
|
1111
1302
|
high: this.concurrentQueues.high.length,
|
|
@@ -1160,7 +1351,7 @@ var taskCounter = 0;
|
|
|
1160
1351
|
function spawn(fn, opts) {
|
|
1161
1352
|
const fnStr = serializeFunction(fn);
|
|
1162
1353
|
const taskId = String(++taskCounter);
|
|
1163
|
-
const
|
|
1354
|
+
const spawnError = new Error();
|
|
1164
1355
|
let resolveFn;
|
|
1165
1356
|
let rejectFn;
|
|
1166
1357
|
let settled = false;
|
|
@@ -1190,9 +1381,12 @@ function spawn(fn, opts) {
|
|
|
1190
1381
|
reject: (reason) => {
|
|
1191
1382
|
if (!settled) {
|
|
1192
1383
|
settled = true;
|
|
1193
|
-
if (reason instanceof Error
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1384
|
+
if (reason instanceof Error) {
|
|
1385
|
+
const spawnStack = spawnError.stack;
|
|
1386
|
+
if (spawnStack) {
|
|
1387
|
+
const callerLine = spawnStack.split("\n").slice(2).join("\n");
|
|
1388
|
+
reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
|
|
1389
|
+
}
|
|
1196
1390
|
}
|
|
1197
1391
|
rejectFn(reason);
|
|
1198
1392
|
}
|
|
@@ -1331,16 +1525,26 @@ var ErrGroup = class {
|
|
|
1331
1525
|
throw new Error("ErrGroup has been cancelled");
|
|
1332
1526
|
}
|
|
1333
1527
|
if (this.limit > 0 && this.inFlight >= this.limit) {
|
|
1334
|
-
|
|
1528
|
+
let innerCancel = () => {
|
|
1529
|
+
};
|
|
1530
|
+
let cancelled = false;
|
|
1531
|
+
const cancel = () => {
|
|
1532
|
+
cancelled = true;
|
|
1533
|
+
innerCancel();
|
|
1534
|
+
};
|
|
1535
|
+
const result = new Promise((resolve) => {
|
|
1335
1536
|
this.waiting.push(resolve);
|
|
1336
|
-
}).then(() =>
|
|
1337
|
-
|
|
1338
|
-
|
|
1537
|
+
}).then(() => {
|
|
1538
|
+
if (cancelled) throw new DOMException("Task was cancelled", "AbortError");
|
|
1539
|
+
const handle2 = this.doSpawn(fn, opts);
|
|
1540
|
+
innerCancel = handle2.cancel;
|
|
1541
|
+
return handle2.result;
|
|
1542
|
+
});
|
|
1543
|
+
this.tasks.push({ result, cancel });
|
|
1339
1544
|
return;
|
|
1340
1545
|
}
|
|
1341
|
-
const
|
|
1342
|
-
this.tasks.push(
|
|
1343
|
-
} });
|
|
1546
|
+
const handle = this.doSpawn(fn, opts);
|
|
1547
|
+
this.tasks.push(handle);
|
|
1344
1548
|
}
|
|
1345
1549
|
doSpawn(fn, opts) {
|
|
1346
1550
|
this.inFlight++;
|
|
@@ -1358,7 +1562,7 @@ var ErrGroup = class {
|
|
|
1358
1562
|
this.cancel();
|
|
1359
1563
|
}
|
|
1360
1564
|
});
|
|
1361
|
-
return handle
|
|
1565
|
+
return handle;
|
|
1362
1566
|
}
|
|
1363
1567
|
async wait() {
|
|
1364
1568
|
const settled = await Promise.allSettled(this.tasks.map((t) => t.result));
|
|
@@ -1495,6 +1699,97 @@ var RWMutex = class {
|
|
|
1495
1699
|
}
|
|
1496
1700
|
};
|
|
1497
1701
|
|
|
1702
|
+
// src/semaphore.ts
|
|
1703
|
+
var Semaphore = class {
|
|
1704
|
+
max;
|
|
1705
|
+
cur;
|
|
1706
|
+
queue = [];
|
|
1707
|
+
constructor(n) {
|
|
1708
|
+
if (n <= 0 || !Number.isInteger(n)) {
|
|
1709
|
+
throw new Error("Semaphore capacity must be a positive integer");
|
|
1710
|
+
}
|
|
1711
|
+
this.max = n;
|
|
1712
|
+
this.cur = 0;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Acquire `n` permits, waiting if necessary until they are available.
|
|
1716
|
+
* Acquires are served in FIFO order.
|
|
1717
|
+
*/
|
|
1718
|
+
async acquire(n = 1) {
|
|
1719
|
+
if (n <= 0 || !Number.isInteger(n)) {
|
|
1720
|
+
throw new Error("Acquire count must be a positive integer");
|
|
1721
|
+
}
|
|
1722
|
+
if (n > this.max) {
|
|
1723
|
+
throw new Error(`Acquire count ${n} exceeds semaphore capacity ${this.max}`);
|
|
1724
|
+
}
|
|
1725
|
+
if (this.cur + n <= this.max && this.queue.length === 0) {
|
|
1726
|
+
this.cur += n;
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
return new Promise((resolve) => {
|
|
1730
|
+
this.queue.push({ n, resolve });
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Try to acquire `n` permits without blocking.
|
|
1735
|
+
* Returns `true` if successful, `false` if not enough permits are available.
|
|
1736
|
+
*/
|
|
1737
|
+
tryAcquire(n = 1) {
|
|
1738
|
+
if (n <= 0 || !Number.isInteger(n)) {
|
|
1739
|
+
throw new Error("Acquire count must be a positive integer");
|
|
1740
|
+
}
|
|
1741
|
+
if (n > this.max) {
|
|
1742
|
+
throw new Error(`Acquire count ${n} exceeds semaphore capacity ${this.max}`);
|
|
1743
|
+
}
|
|
1744
|
+
if (this.cur + n <= this.max && this.queue.length === 0) {
|
|
1745
|
+
this.cur += n;
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Release `n` permits, potentially waking queued acquirers.
|
|
1752
|
+
*/
|
|
1753
|
+
release(n = 1) {
|
|
1754
|
+
if (n <= 0 || !Number.isInteger(n)) {
|
|
1755
|
+
throw new Error("Release count must be a positive integer");
|
|
1756
|
+
}
|
|
1757
|
+
if (this.cur - n < 0) {
|
|
1758
|
+
throw new Error("Released more permits than acquired");
|
|
1759
|
+
}
|
|
1760
|
+
this.cur -= n;
|
|
1761
|
+
this.wake();
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Acquire `n` permits, run `fn`, then release automatically — even if `fn` throws.
|
|
1765
|
+
*/
|
|
1766
|
+
async withAcquire(fn, n = 1) {
|
|
1767
|
+
await this.acquire(n);
|
|
1768
|
+
try {
|
|
1769
|
+
return await fn();
|
|
1770
|
+
} finally {
|
|
1771
|
+
this.release(n);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
/** Number of permits currently available. */
|
|
1775
|
+
get available() {
|
|
1776
|
+
return this.max - this.cur;
|
|
1777
|
+
}
|
|
1778
|
+
/** Total capacity of the semaphore. */
|
|
1779
|
+
get capacity() {
|
|
1780
|
+
return this.max;
|
|
1781
|
+
}
|
|
1782
|
+
wake() {
|
|
1783
|
+
while (this.queue.length > 0) {
|
|
1784
|
+
const head = this.queue[0];
|
|
1785
|
+
if (this.cur + head.n > this.max) break;
|
|
1786
|
+
this.queue.shift();
|
|
1787
|
+
this.cur += head.n;
|
|
1788
|
+
head.resolve();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1498
1793
|
// src/cond.ts
|
|
1499
1794
|
var Cond = class {
|
|
1500
1795
|
mu;
|
|
@@ -1567,7 +1862,6 @@ function select(cases, opts) {
|
|
|
1567
1862
|
if (settled) return;
|
|
1568
1863
|
settled = true;
|
|
1569
1864
|
try {
|
|
1570
|
-
;
|
|
1571
1865
|
handler(value);
|
|
1572
1866
|
resolve();
|
|
1573
1867
|
} catch (err) {
|
|
@@ -1601,7 +1895,6 @@ function select(cases, opts) {
|
|
|
1601
1895
|
if (settled) return;
|
|
1602
1896
|
settled = true;
|
|
1603
1897
|
try {
|
|
1604
|
-
;
|
|
1605
1898
|
handler(value);
|
|
1606
1899
|
resolve();
|
|
1607
1900
|
} catch (err) {
|
|
@@ -1726,18 +2019,15 @@ var taskCounter2 = 0;
|
|
|
1726
2019
|
function task(fn) {
|
|
1727
2020
|
const fnStr = serializeFunction(fn);
|
|
1728
2021
|
return (...args) => {
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
if (json === void 0) {
|
|
2022
|
+
for (const a of args) {
|
|
2023
|
+
if (JSON.stringify(a) === void 0) {
|
|
1732
2024
|
throw new TypeError(
|
|
1733
2025
|
`Argument of type ${typeof a} is not JSON-serializable. task() args must be JSON-serializable (no undefined, functions, symbols, or BigInt).`
|
|
1734
2026
|
);
|
|
1735
2027
|
}
|
|
1736
|
-
|
|
1737
|
-
});
|
|
1738
|
-
const wrapperStr = `() => (${fnStr})(${serializedArgs.join(", ")})`;
|
|
2028
|
+
}
|
|
1739
2029
|
const taskId = `task_${++taskCounter2}`;
|
|
1740
|
-
const
|
|
2030
|
+
const spawnError = new Error();
|
|
1741
2031
|
let resolveFn;
|
|
1742
2032
|
let rejectFn;
|
|
1743
2033
|
const result = new Promise((resolve, reject) => {
|
|
@@ -1746,14 +2036,18 @@ function task(fn) {
|
|
|
1746
2036
|
});
|
|
1747
2037
|
const taskObj = {
|
|
1748
2038
|
id: taskId,
|
|
1749
|
-
fnStr
|
|
2039
|
+
fnStr,
|
|
2040
|
+
args,
|
|
1750
2041
|
priority: "normal",
|
|
1751
2042
|
concurrent: false,
|
|
1752
2043
|
resolve: (value) => resolveFn(value),
|
|
1753
2044
|
reject: (reason) => {
|
|
1754
|
-
if (reason instanceof Error
|
|
1755
|
-
const
|
|
1756
|
-
|
|
2045
|
+
if (reason instanceof Error) {
|
|
2046
|
+
const spawnStack = spawnError.stack;
|
|
2047
|
+
if (spawnStack) {
|
|
2048
|
+
const callerLine = spawnStack.split("\n").slice(2).join("\n");
|
|
2049
|
+
reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
|
|
2050
|
+
}
|
|
1757
2051
|
}
|
|
1758
2052
|
rejectFn(reason);
|
|
1759
2053
|
}
|
|
@@ -1929,6 +2223,7 @@ export {
|
|
|
1929
2223
|
Mutex,
|
|
1930
2224
|
Once,
|
|
1931
2225
|
RWMutex,
|
|
2226
|
+
Semaphore,
|
|
1932
2227
|
Ticker,
|
|
1933
2228
|
Timer,
|
|
1934
2229
|
WaitGroup,
|