@dmop/puru 0.1.10 → 0.1.12

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/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
- function __execFn(fnStr, channels) {
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
- const __ch = __buildChannelProxies(channels);
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;
@@ -397,6 +408,12 @@ var ChannelImpl = class {
397
408
  this.capacity = capacity;
398
409
  channelRegistry.set(this._id, this);
399
410
  }
411
+ get len() {
412
+ return this.buffer.length;
413
+ }
414
+ get cap() {
415
+ return this.capacity;
416
+ }
400
417
  send(value) {
401
418
  if (this.closed) {
402
419
  return Promise.reject(new Error("send on closed channel"));
@@ -422,6 +439,7 @@ var ChannelImpl = class {
422
439
  this.buffer.push(sender2.value);
423
440
  sender2.resolve();
424
441
  }
442
+ this.maybeUnregister();
425
443
  return Promise.resolve(value);
426
444
  }
427
445
  const sender = this.sendQueue.shift();
@@ -430,6 +448,7 @@ var ChannelImpl = class {
430
448
  return Promise.resolve(sender.value);
431
449
  }
432
450
  if (this.closed) {
451
+ this.maybeUnregister();
433
452
  return Promise.resolve(null);
434
453
  }
435
454
  return new Promise((resolve) => {
@@ -450,6 +469,45 @@ var ChannelImpl = class {
450
469
  }
451
470
  this.sendQueue = [];
452
471
  }
472
+ maybeUnregister() {
473
+ if (!this._shared && this.closed && this.buffer.length === 0 && this.recvQueue.length === 0) {
474
+ channelRegistry.delete(this._id);
475
+ }
476
+ }
477
+ sendOnly() {
478
+ const send = (value) => this.send(value);
479
+ const close = () => this.close();
480
+ const getLen = () => this.len;
481
+ const getCap = () => this.cap;
482
+ return {
483
+ send,
484
+ close,
485
+ get len() {
486
+ return getLen();
487
+ },
488
+ get cap() {
489
+ return getCap();
490
+ }
491
+ };
492
+ }
493
+ recvOnly() {
494
+ const recv = () => this.recv();
495
+ const getLen = () => this.len;
496
+ const getCap = () => this.cap;
497
+ const getIter = () => this[Symbol.asyncIterator]();
498
+ return {
499
+ recv,
500
+ get len() {
501
+ return getLen();
502
+ },
503
+ get cap() {
504
+ return getCap();
505
+ },
506
+ [Symbol.asyncIterator]() {
507
+ return getIter();
508
+ }
509
+ };
510
+ }
453
511
  async *[Symbol.asyncIterator]() {
454
512
  while (true) {
455
513
  const value = await this.recv();
@@ -468,7 +526,9 @@ function getChannelById(id) {
468
526
  return channelRegistry.get(id);
469
527
  }
470
528
  function getChannelId(channel) {
471
- return channel._id;
529
+ const impl = channel;
530
+ impl._shared = true;
531
+ return impl._id;
472
532
  }
473
533
 
474
534
  // src/adapters/inline.ts
@@ -480,6 +540,7 @@ var InlineManagedWorker = class {
480
540
  exitHandlers = [];
481
541
  terminated = false;
482
542
  cancelledTasks = /* @__PURE__ */ new Set();
543
+ fnCache = /* @__PURE__ */ new Map();
483
544
  constructor() {
484
545
  this.id = ++inlineIdCounter;
485
546
  queueMicrotask(() => {
@@ -489,7 +550,7 @@ var InlineManagedWorker = class {
489
550
  postMessage(msg) {
490
551
  if (this.terminated) return;
491
552
  if (msg.type === "execute") {
492
- this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels);
553
+ this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels, msg.args);
493
554
  } else if (msg.type === "cancel") {
494
555
  this.cancelledTasks.add(msg.taskId);
495
556
  } else if (msg.type === "channel-result") {
@@ -550,7 +611,7 @@ var InlineManagedWorker = class {
550
611
  }
551
612
  return proxies;
552
613
  }
553
- executeTask(taskId, fnStr, concurrent, channels) {
614
+ executeTask(taskId, fnStr, concurrent, channels, args) {
554
615
  queueMicrotask(async () => {
555
616
  if (this.terminated) return;
556
617
  if (concurrent && this.cancelledTasks.has(taskId)) {
@@ -558,14 +619,20 @@ var InlineManagedWorker = class {
558
619
  return;
559
620
  }
560
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
+ }
561
628
  let result;
562
- if (channels) {
629
+ if (args) {
630
+ result = await parsedFn(...args);
631
+ } else if (channels) {
563
632
  const proxies = this.buildChannelProxies(channels);
564
- const fn = new Function("__ch", "return (" + fnStr + ")(__ch)");
565
- result = await fn(proxies);
633
+ result = await parsedFn(proxies);
566
634
  } else {
567
- const fn = new Function("return (" + fnStr + ")()");
568
- result = await fn();
635
+ result = await parsedFn();
569
636
  }
570
637
  if (concurrent && this.cancelledTasks.has(taskId)) {
571
638
  this.cancelledTasks.delete(taskId);
@@ -618,6 +685,8 @@ var WorkerPool = class {
618
685
  totalCompleted = 0;
619
686
  totalFailed = 0;
620
687
  taskMap = /* @__PURE__ */ new Map();
688
+ // Per-worker deques for work-stealing strategy
689
+ workerDeques = /* @__PURE__ */ new Map();
621
690
  constructor(config, adapter) {
622
691
  this.config = config;
623
692
  this.adapter = adapter;
@@ -655,6 +724,110 @@ var WorkerPool = class {
655
724
  }
656
725
  return void 0;
657
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
+ }
658
831
  // --- Submit ---
659
832
  submit(task2) {
660
833
  if (this.draining) {
@@ -680,6 +853,13 @@ var WorkerPool = class {
680
853
  this.createAndReadyWorker();
681
854
  return;
682
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
+ }
683
863
  this.enqueue(task2);
684
864
  }
685
865
  submitConcurrent(task2) {
@@ -717,6 +897,7 @@ var WorkerPool = class {
717
897
  type: "execute",
718
898
  taskId: task2.id,
719
899
  fnStr: task2.fnStr,
900
+ args: task2.args,
720
901
  concurrent: false,
721
902
  channels: task2.channels
722
903
  };
@@ -738,6 +919,7 @@ var WorkerPool = class {
738
919
  type: "execute",
739
920
  taskId: task2.id,
740
921
  fnStr: task2.fnStr,
922
+ args: task2.args,
741
923
  concurrent: true,
742
924
  channels: task2.channels
743
925
  };
@@ -784,6 +966,19 @@ var WorkerPool = class {
784
966
  }
785
967
  assignNextOrIdle(worker) {
786
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
+ }
787
982
  const next = this.dequeue();
788
983
  if (next) {
789
984
  this.dispatch(worker, next);
@@ -794,6 +989,7 @@ var WorkerPool = class {
794
989
  this.dispatchConcurrent(worker, concurrentNext);
795
990
  return;
796
991
  }
992
+ this.cleanupDeque(worker);
797
993
  this.makeIdle(worker);
798
994
  }
799
995
  assignNextConcurrentOrIdle(worker) {
@@ -813,6 +1009,13 @@ var WorkerPool = class {
813
1009
  const exclusiveTask = this.dequeue();
814
1010
  if (exclusiveTask) {
815
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
+ }
816
1019
  } else {
817
1020
  this.makeIdle(worker);
818
1021
  }
@@ -908,6 +1111,13 @@ var WorkerPool = class {
908
1111
  removed.reject(new DOMException("Task was cancelled", "AbortError"));
909
1112
  return;
910
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
+ }
911
1121
  const removedConcurrent = this.removeFromConcurrentQueue(taskId);
912
1122
  if (removedConcurrent) {
913
1123
  removedConcurrent.reject(new DOMException("Task was cancelled", "AbortError"));
@@ -918,6 +1128,7 @@ var WorkerPool = class {
918
1128
  this.exclusiveWorkers.delete(taskId);
919
1129
  this.allWorkers.delete(exclusiveWorker);
920
1130
  this.taskMap.delete(taskId);
1131
+ this.flushDeque(exclusiveWorker);
921
1132
  exclusiveWorker.terminate();
922
1133
  return;
923
1134
  }
@@ -949,6 +1160,14 @@ var WorkerPool = class {
949
1160
  }
950
1161
  this.concurrentQueues[priority] = [];
951
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();
952
1171
  for (const [taskId] of this.exclusiveWorkers) {
953
1172
  this.taskMap.delete(taskId);
954
1173
  }
@@ -976,7 +1195,7 @@ var WorkerPool = class {
976
1195
  while (true) {
977
1196
  const totalWorkers = this.allWorkers.size + this.pendingWorkerCount;
978
1197
  if (totalWorkers >= maxThreads) break;
979
- const task2 = this.dequeue() ?? this.dequeueConcurrent();
1198
+ const task2 = this.dequeue() ?? (this.config.strategy === "work-stealing" ? this.stealFromAny() : void 0) ?? this.dequeueConcurrent();
980
1199
  if (!task2) break;
981
1200
  this.pendingWorkerCount++;
982
1201
  this.pendingTasksForWorkers.push(task2);
@@ -1039,6 +1258,7 @@ var WorkerPool = class {
1039
1258
  this.idleWorkers.splice(idleIdx, 1);
1040
1259
  }
1041
1260
  this.rejectExclusiveTaskForWorker(worker, new Error("Worker exited unexpectedly"));
1261
+ this.flushDeque(worker);
1042
1262
  const taskSet = this.sharedWorkers.get(worker);
1043
1263
  if (taskSet) {
1044
1264
  for (const taskId of taskSet) {
@@ -1054,6 +1274,17 @@ var WorkerPool = class {
1054
1274
  for (const taskSet of this.sharedWorkers.values()) {
1055
1275
  concurrentTasks += taskSet.size;
1056
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;
1057
1288
  return {
1058
1289
  totalWorkers: this.allWorkers.size,
1059
1290
  idleWorkers: this.idleWorkers.length,
@@ -1062,10 +1293,10 @@ var WorkerPool = class {
1062
1293
  concurrentTasks,
1063
1294
  pendingWorkers: this.pendingWorkerCount,
1064
1295
  queuedTasks: {
1065
- high: this.queues.high.length,
1066
- normal: this.queues.normal.length,
1067
- low: this.queues.low.length,
1068
- total: this.queues.high.length + this.queues.normal.length + this.queues.low.length
1296
+ high: queuedHigh,
1297
+ normal: queuedNormal,
1298
+ low: queuedLow,
1299
+ total: queuedHigh + queuedNormal + queuedLow
1069
1300
  },
1070
1301
  queuedConcurrentTasks: {
1071
1302
  high: this.concurrentQueues.high.length,
@@ -1120,7 +1351,7 @@ var taskCounter = 0;
1120
1351
  function spawn(fn, opts) {
1121
1352
  const fnStr = serializeFunction(fn);
1122
1353
  const taskId = String(++taskCounter);
1123
- const spawnStack = new Error().stack;
1354
+ const spawnError = new Error();
1124
1355
  let resolveFn;
1125
1356
  let rejectFn;
1126
1357
  let settled = false;
@@ -1150,14 +1381,29 @@ function spawn(fn, opts) {
1150
1381
  reject: (reason) => {
1151
1382
  if (!settled) {
1152
1383
  settled = true;
1153
- if (reason instanceof Error && spawnStack) {
1154
- const callerLine = spawnStack.split("\n").slice(2).join("\n");
1155
- reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
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
+ }
1156
1390
  }
1157
1391
  rejectFn(reason);
1158
1392
  }
1159
1393
  }
1160
1394
  };
1395
+ const ctx = opts?.ctx;
1396
+ if (ctx) {
1397
+ if (ctx.signal.aborted) {
1398
+ settled = true;
1399
+ rejectFn(ctx.err ?? new DOMException("Task was cancelled", "AbortError"));
1400
+ return {
1401
+ result,
1402
+ cancel: () => {
1403
+ }
1404
+ };
1405
+ }
1406
+ }
1161
1407
  getPool().submit(task2);
1162
1408
  const cancel = () => {
1163
1409
  if (settled) return;
@@ -1165,6 +1411,14 @@ function spawn(fn, opts) {
1165
1411
  getPool().cancelTask(taskId);
1166
1412
  rejectFn(new DOMException("Task was cancelled", "AbortError"));
1167
1413
  };
1414
+ if (ctx) {
1415
+ const onAbort = () => cancel();
1416
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
1417
+ result.then(
1418
+ () => ctx.signal.removeEventListener("abort", onAbort),
1419
+ () => ctx.signal.removeEventListener("abort", onAbort)
1420
+ );
1421
+ }
1168
1422
  return { result, cancel };
1169
1423
  }
1170
1424
 
@@ -1172,6 +1426,17 @@ function spawn(fn, opts) {
1172
1426
  var WaitGroup = class {
1173
1427
  tasks = [];
1174
1428
  controller = new AbortController();
1429
+ ctx;
1430
+ constructor(ctx) {
1431
+ this.ctx = ctx;
1432
+ if (ctx) {
1433
+ if (ctx.signal.aborted) {
1434
+ this.controller.abort();
1435
+ } else {
1436
+ ctx.signal.addEventListener("abort", () => this.cancel(), { once: true });
1437
+ }
1438
+ }
1439
+ }
1175
1440
  /**
1176
1441
  * An `AbortSignal` shared across all tasks in this group.
1177
1442
  * Pass it into spawned functions so they can stop early when `cancel()` is called.
@@ -1188,7 +1453,7 @@ var WaitGroup = class {
1188
1453
  if (this.controller.signal.aborted) {
1189
1454
  throw new Error("WaitGroup has been cancelled");
1190
1455
  }
1191
- const handle = spawn(fn, opts);
1456
+ const handle = spawn(fn, { ...opts, ctx: this.ctx });
1192
1457
  this.tasks.push(handle);
1193
1458
  }
1194
1459
  /**
@@ -1223,22 +1488,81 @@ var ErrGroup = class {
1223
1488
  controller = new AbortController();
1224
1489
  firstError = null;
1225
1490
  hasError = false;
1491
+ ctx;
1492
+ limit = 0;
1493
+ // 0 = unlimited
1494
+ inFlight = 0;
1495
+ waiting = [];
1496
+ constructor(ctx) {
1497
+ this.ctx = ctx;
1498
+ if (ctx) {
1499
+ if (ctx.signal.aborted) {
1500
+ this.controller.abort();
1501
+ } else {
1502
+ ctx.signal.addEventListener("abort", () => this.cancel(), { once: true });
1503
+ }
1504
+ }
1505
+ }
1226
1506
  get signal() {
1227
1507
  return this.controller.signal;
1228
1508
  }
1509
+ /**
1510
+ * Set the maximum number of tasks that can run concurrently.
1511
+ * Like Go's `errgroup.SetLimit()`. Must be called before any `spawn()`.
1512
+ * A value of 0 (default) means unlimited.
1513
+ */
1514
+ setLimit(n) {
1515
+ if (this.tasks.length > 0) {
1516
+ throw new Error("SetLimit must be called before any spawn()");
1517
+ }
1518
+ if (n < 0 || !Number.isInteger(n)) {
1519
+ throw new RangeError("Limit must be a non-negative integer");
1520
+ }
1521
+ this.limit = n;
1522
+ }
1229
1523
  spawn(fn, opts) {
1230
1524
  if (this.controller.signal.aborted) {
1231
1525
  throw new Error("ErrGroup has been cancelled");
1232
1526
  }
1233
- const handle = spawn(fn, opts);
1234
- handle.result.catch((err) => {
1527
+ if (this.limit > 0 && this.inFlight >= this.limit) {
1528
+ let innerCancel = () => {
1529
+ };
1530
+ let cancelled = false;
1531
+ const cancel = () => {
1532
+ cancelled = true;
1533
+ innerCancel();
1534
+ };
1535
+ const result = new Promise((resolve) => {
1536
+ this.waiting.push(resolve);
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 });
1544
+ return;
1545
+ }
1546
+ const handle = this.doSpawn(fn, opts);
1547
+ this.tasks.push(handle);
1548
+ }
1549
+ doSpawn(fn, opts) {
1550
+ this.inFlight++;
1551
+ const handle = spawn(fn, { ...opts, ctx: this.ctx });
1552
+ const onSettle = () => {
1553
+ this.inFlight--;
1554
+ const next = this.waiting.shift();
1555
+ if (next) next();
1556
+ };
1557
+ handle.result.then(onSettle, (err) => {
1558
+ onSettle();
1235
1559
  if (!this.hasError) {
1236
1560
  this.hasError = true;
1237
1561
  this.firstError = err;
1238
1562
  this.cancel();
1239
1563
  }
1240
1564
  });
1241
- this.tasks.push(handle);
1565
+ return handle;
1242
1566
  }
1243
1567
  async wait() {
1244
1568
  const settled = await Promise.allSettled(this.tasks.map((t) => t.result));
@@ -1294,6 +1618,212 @@ var Mutex = class {
1294
1618
  return this.locked;
1295
1619
  }
1296
1620
  };
1621
+ var RWMutex = class {
1622
+ readers = 0;
1623
+ writing = false;
1624
+ readQueue = [];
1625
+ writeQueue = [];
1626
+ async rLock() {
1627
+ if (!this.writing && this.writeQueue.length === 0) {
1628
+ this.readers++;
1629
+ return;
1630
+ }
1631
+ return new Promise((resolve) => {
1632
+ this.readQueue.push(() => {
1633
+ this.readers++;
1634
+ resolve();
1635
+ });
1636
+ });
1637
+ }
1638
+ rUnlock() {
1639
+ if (this.readers <= 0) {
1640
+ throw new Error("Cannot rUnlock a RWMutex that is not read-locked");
1641
+ }
1642
+ this.readers--;
1643
+ if (this.readers === 0) {
1644
+ this.wakeWriter();
1645
+ }
1646
+ }
1647
+ async lock() {
1648
+ if (!this.writing && this.readers === 0) {
1649
+ this.writing = true;
1650
+ return;
1651
+ }
1652
+ return new Promise((resolve) => {
1653
+ this.writeQueue.push(() => {
1654
+ this.writing = true;
1655
+ resolve();
1656
+ });
1657
+ });
1658
+ }
1659
+ unlock() {
1660
+ if (!this.writing) {
1661
+ throw new Error("Cannot unlock a RWMutex that is not write-locked");
1662
+ }
1663
+ this.writing = false;
1664
+ if (this.readQueue.length > 0) {
1665
+ this.wakeReaders();
1666
+ } else {
1667
+ this.wakeWriter();
1668
+ }
1669
+ }
1670
+ async withRLock(fn) {
1671
+ await this.rLock();
1672
+ try {
1673
+ return await fn();
1674
+ } finally {
1675
+ this.rUnlock();
1676
+ }
1677
+ }
1678
+ async withLock(fn) {
1679
+ await this.lock();
1680
+ try {
1681
+ return await fn();
1682
+ } finally {
1683
+ this.unlock();
1684
+ }
1685
+ }
1686
+ get isLocked() {
1687
+ return this.writing || this.readers > 0;
1688
+ }
1689
+ wakeReaders() {
1690
+ const queue = this.readQueue;
1691
+ this.readQueue = [];
1692
+ for (const wake of queue) {
1693
+ wake();
1694
+ }
1695
+ }
1696
+ wakeWriter() {
1697
+ const next = this.writeQueue.shift();
1698
+ if (next) next();
1699
+ }
1700
+ };
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
+
1793
+ // src/cond.ts
1794
+ var Cond = class {
1795
+ mu;
1796
+ waiters = [];
1797
+ constructor(mu) {
1798
+ this.mu = mu;
1799
+ }
1800
+ /**
1801
+ * Atomically releases the mutex, suspends the caller until `signal()` or `broadcast()`
1802
+ * is called, then re-acquires the mutex before returning.
1803
+ *
1804
+ * Must be called while holding the mutex.
1805
+ */
1806
+ async wait() {
1807
+ this.mu.unlock();
1808
+ await new Promise((resolve) => {
1809
+ this.waiters.push(resolve);
1810
+ });
1811
+ await this.mu.lock();
1812
+ }
1813
+ /** Wake one waiting task (if any). */
1814
+ signal() {
1815
+ const next = this.waiters.shift();
1816
+ if (next) next();
1817
+ }
1818
+ /** Wake all waiting tasks. */
1819
+ broadcast() {
1820
+ const queue = this.waiters;
1821
+ this.waiters = [];
1822
+ for (const wake of queue) {
1823
+ wake();
1824
+ }
1825
+ }
1826
+ };
1297
1827
 
1298
1828
  // src/once.ts
1299
1829
  var Once = class {
@@ -1435,23 +1965,69 @@ function ticker(ms) {
1435
1965
  return new Ticker(ms);
1436
1966
  }
1437
1967
 
1968
+ // src/timer.ts
1969
+ var Timer = class {
1970
+ timer = null;
1971
+ _stopped = false;
1972
+ /** Promise that resolves when the timer fires. Replaced on `reset()`. */
1973
+ channel;
1974
+ constructor(ms) {
1975
+ this.channel = this.schedule(ms);
1976
+ }
1977
+ schedule(ms) {
1978
+ return new Promise((resolve) => {
1979
+ this.timer = setTimeout(() => {
1980
+ this._stopped = true;
1981
+ this.timer = null;
1982
+ resolve();
1983
+ }, ms);
1984
+ if (typeof this.timer === "object" && "unref" in this.timer) {
1985
+ this.timer.unref();
1986
+ }
1987
+ });
1988
+ }
1989
+ /**
1990
+ * Stop the timer. Returns `true` if the timer was pending (stopped before firing),
1991
+ * `false` if it had already fired or was already stopped.
1992
+ *
1993
+ * After stopping, the current `channel` promise will never resolve.
1994
+ */
1995
+ stop() {
1996
+ if (this.timer === null) return false;
1997
+ clearTimeout(this.timer);
1998
+ this.timer = null;
1999
+ this._stopped = true;
2000
+ return true;
2001
+ }
2002
+ /**
2003
+ * Reset the timer to fire after `ms` milliseconds.
2004
+ * If the timer was pending, it is stopped first. Creates a new `channel` promise.
2005
+ */
2006
+ reset(ms) {
2007
+ this.stop();
2008
+ this._stopped = false;
2009
+ this.channel = this.schedule(ms);
2010
+ }
2011
+ /** Whether the timer has fired or been stopped. */
2012
+ get stopped() {
2013
+ return this._stopped;
2014
+ }
2015
+ };
2016
+
1438
2017
  // src/registry.ts
1439
2018
  var taskCounter2 = 0;
1440
2019
  function task(fn) {
2020
+ const fnStr = serializeFunction(fn);
1441
2021
  return (...args) => {
1442
- const fnStr = serializeFunction(fn);
1443
- const serializedArgs = args.map((a) => {
1444
- const json = JSON.stringify(a);
1445
- if (json === void 0) {
2022
+ for (const a of args) {
2023
+ if (JSON.stringify(a) === void 0) {
1446
2024
  throw new TypeError(
1447
2025
  `Argument of type ${typeof a} is not JSON-serializable. task() args must be JSON-serializable (no undefined, functions, symbols, or BigInt).`
1448
2026
  );
1449
2027
  }
1450
- return json;
1451
- });
1452
- const wrapperStr = `() => (${fnStr})(${serializedArgs.join(", ")})`;
2028
+ }
1453
2029
  const taskId = `task_${++taskCounter2}`;
1454
- const spawnStack = new Error().stack;
2030
+ const spawnError = new Error();
1455
2031
  let resolveFn;
1456
2032
  let rejectFn;
1457
2033
  const result = new Promise((resolve, reject) => {
@@ -1460,14 +2036,18 @@ function task(fn) {
1460
2036
  });
1461
2037
  const taskObj = {
1462
2038
  id: taskId,
1463
- fnStr: wrapperStr,
2039
+ fnStr,
2040
+ args,
1464
2041
  priority: "normal",
1465
2042
  concurrent: false,
1466
2043
  resolve: (value) => resolveFn(value),
1467
2044
  reject: (reason) => {
1468
- if (reason instanceof Error && spawnStack) {
1469
- const callerLine = spawnStack.split("\n").slice(2).join("\n");
1470
- reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
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
+ }
1471
2051
  }
1472
2052
  rejectFn(reason);
1473
2053
  }
@@ -1476,13 +2056,179 @@ function task(fn) {
1476
2056
  return result;
1477
2057
  };
1478
2058
  }
2059
+
2060
+ // src/context.ts
2061
+ var CancelledError = class extends Error {
2062
+ constructor(message = "context cancelled") {
2063
+ super(message);
2064
+ this.name = "CancelledError";
2065
+ }
2066
+ };
2067
+ var DeadlineExceededError = class extends Error {
2068
+ constructor() {
2069
+ super("context deadline exceeded");
2070
+ this.name = "DeadlineExceededError";
2071
+ }
2072
+ };
2073
+ var BaseContext = class {
2074
+ _err = null;
2075
+ controller;
2076
+ parent;
2077
+ constructor(parent) {
2078
+ this.parent = parent;
2079
+ this.controller = new AbortController();
2080
+ if (parent) {
2081
+ if (parent.signal.aborted) {
2082
+ this._err = parent.err ?? new CancelledError();
2083
+ this.controller.abort();
2084
+ } else {
2085
+ parent.signal.addEventListener(
2086
+ "abort",
2087
+ () => {
2088
+ if (!this.controller.signal.aborted) {
2089
+ this._err = parent.err ?? new CancelledError();
2090
+ this.controller.abort();
2091
+ }
2092
+ },
2093
+ { once: true }
2094
+ );
2095
+ }
2096
+ }
2097
+ }
2098
+ get signal() {
2099
+ return this.controller.signal;
2100
+ }
2101
+ get deadline() {
2102
+ return this.parent?.deadline ?? null;
2103
+ }
2104
+ get err() {
2105
+ return this._err;
2106
+ }
2107
+ value(_key) {
2108
+ return this.parent?.value(_key);
2109
+ }
2110
+ done() {
2111
+ if (this.controller.signal.aborted) return Promise.resolve();
2112
+ return new Promise((resolve) => {
2113
+ this.controller.signal.addEventListener("abort", () => resolve(), { once: true });
2114
+ });
2115
+ }
2116
+ };
2117
+ var BackgroundContext = class {
2118
+ _signal = new AbortController().signal;
2119
+ get signal() {
2120
+ return this._signal;
2121
+ }
2122
+ get deadline() {
2123
+ return null;
2124
+ }
2125
+ get err() {
2126
+ return null;
2127
+ }
2128
+ value(_key) {
2129
+ return void 0;
2130
+ }
2131
+ done() {
2132
+ return new Promise(() => {
2133
+ });
2134
+ }
2135
+ };
2136
+ var bg = null;
2137
+ function background() {
2138
+ if (!bg) bg = new BackgroundContext();
2139
+ return bg;
2140
+ }
2141
+ var CancelContext = class extends BaseContext {
2142
+ cancel(reason) {
2143
+ if (!this.controller.signal.aborted) {
2144
+ this._err = new CancelledError(reason ?? "context cancelled");
2145
+ this.controller.abort();
2146
+ }
2147
+ }
2148
+ };
2149
+ function withCancel(parent) {
2150
+ const ctx = new CancelContext(parent);
2151
+ return [ctx, (reason) => ctx.cancel(reason)];
2152
+ }
2153
+ var DeadlineContext = class extends BaseContext {
2154
+ _deadline;
2155
+ timer = null;
2156
+ constructor(parent, deadline) {
2157
+ super(parent);
2158
+ this._deadline = deadline;
2159
+ if (parent.deadline && parent.deadline < deadline) {
2160
+ this._deadline = parent.deadline;
2161
+ }
2162
+ if (this.controller.signal.aborted) {
2163
+ return;
2164
+ }
2165
+ const ms = this._deadline.getTime() - Date.now();
2166
+ if (ms <= 0) {
2167
+ this._err = new DeadlineExceededError();
2168
+ this.controller.abort();
2169
+ } else {
2170
+ this.timer = setTimeout(() => {
2171
+ if (!this.controller.signal.aborted) {
2172
+ this._err = new DeadlineExceededError();
2173
+ this.controller.abort();
2174
+ }
2175
+ }, ms);
2176
+ if (typeof this.timer === "object" && "unref" in this.timer) {
2177
+ this.timer.unref();
2178
+ }
2179
+ }
2180
+ }
2181
+ get deadline() {
2182
+ return this._deadline;
2183
+ }
2184
+ cancel(reason) {
2185
+ if (this.timer !== null) {
2186
+ clearTimeout(this.timer);
2187
+ this.timer = null;
2188
+ }
2189
+ if (!this.controller.signal.aborted) {
2190
+ this._err = new CancelledError(reason ?? "context cancelled");
2191
+ this.controller.abort();
2192
+ }
2193
+ }
2194
+ };
2195
+ function withDeadline(parent, deadline) {
2196
+ const ctx = new DeadlineContext(parent, deadline);
2197
+ return [ctx, (reason) => ctx.cancel(reason)];
2198
+ }
2199
+ function withTimeout(parent, ms) {
2200
+ return withDeadline(parent, new Date(Date.now() + ms));
2201
+ }
2202
+ var ValueContext = class extends BaseContext {
2203
+ key;
2204
+ val;
2205
+ constructor(parent, key, val) {
2206
+ super(parent);
2207
+ this.key = key;
2208
+ this.val = val;
2209
+ }
2210
+ value(key) {
2211
+ if (key === this.key) return this.val;
2212
+ return this.parent?.value(key);
2213
+ }
2214
+ };
2215
+ function withValue(parent, key, value) {
2216
+ return new ValueContext(parent, key, value);
2217
+ }
1479
2218
  export {
2219
+ CancelledError,
2220
+ Cond,
2221
+ DeadlineExceededError,
1480
2222
  ErrGroup,
1481
2223
  Mutex,
1482
2224
  Once,
2225
+ RWMutex,
2226
+ Semaphore,
1483
2227
  Ticker,
2228
+ Timer,
1484
2229
  WaitGroup,
1485
2230
  after,
2231
+ background,
1486
2232
  chan,
1487
2233
  configure,
1488
2234
  detectCapability,
@@ -1493,6 +2239,10 @@ export {
1493
2239
  spawn,
1494
2240
  stats,
1495
2241
  task,
1496
- ticker
2242
+ ticker,
2243
+ withCancel,
2244
+ withDeadline,
2245
+ withTimeout,
2246
+ withValue
1497
2247
  };
1498
2248
  //# sourceMappingURL=index.js.map