@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.cjs CHANGED
@@ -20,12 +20,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ CancelledError: () => CancelledError,
24
+ Cond: () => Cond,
25
+ DeadlineExceededError: () => DeadlineExceededError,
23
26
  ErrGroup: () => ErrGroup,
24
27
  Mutex: () => Mutex,
25
28
  Once: () => Once,
29
+ RWMutex: () => RWMutex,
30
+ Semaphore: () => Semaphore,
26
31
  Ticker: () => Ticker,
32
+ Timer: () => Timer,
27
33
  WaitGroup: () => WaitGroup,
28
34
  after: () => after,
35
+ background: () => background,
29
36
  chan: () => chan,
30
37
  configure: () => configure,
31
38
  detectCapability: () => detectCapability,
@@ -36,7 +43,11 @@ __export(index_exports, {
36
43
  spawn: () => spawn,
37
44
  stats: () => stats,
38
45
  task: () => task,
39
- ticker: () => ticker
46
+ ticker: () => ticker,
47
+ withCancel: () => withCancel,
48
+ withDeadline: () => withDeadline,
49
+ withTimeout: () => withTimeout,
50
+ withValue: () => withValue
40
51
  });
41
52
  module.exports = __toCommonJS(index_exports);
42
53
 
@@ -44,10 +55,13 @@ module.exports = __toCommonJS(index_exports);
44
55
  var NATIVE_CODE_RE = /\[native code\]/;
45
56
  var METHOD_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/;
46
57
  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_$])/;
58
+ var serializeCache = /* @__PURE__ */ new WeakMap();
47
59
  function serializeFunction(fn) {
48
60
  if (typeof fn !== "function") {
49
61
  throw new TypeError("Expected a function");
50
62
  }
63
+ const cached = serializeCache.get(fn);
64
+ if (cached) return cached;
51
65
  const str = fn.toString();
52
66
  if (typeof str !== "string" || str.length === 0) {
53
67
  throw new TypeError(
@@ -69,6 +83,7 @@ function serializeFunction(fn) {
69
83
  "Class methods cannot be serialized. Use an arrow function wrapper instead."
70
84
  );
71
85
  }
86
+ serializeCache.set(fn, str);
72
87
  return str;
73
88
  }
74
89
 
@@ -189,15 +204,23 @@ function __buildChannelProxies(channels) {
189
204
  return proxies;
190
205
  }
191
206
 
192
- function __execFn(fnStr, channels) {
207
+ const __fnCache = new Map();
208
+ const __FN_CACHE_MAX = 1000;
209
+
210
+ function __execFn(fnStr, channels, args) {
211
+ let parsedFn = __fnCache.get(fnStr);
212
+ if (!parsedFn) {
213
+ parsedFn = (new Function('return (' + fnStr + ')'))();
214
+ if (__fnCache.size >= __FN_CACHE_MAX) __fnCache.delete(__fnCache.keys().next().value);
215
+ __fnCache.set(fnStr, parsedFn);
216
+ }
217
+ if (args) {
218
+ return parsedFn(...args);
219
+ }
193
220
  if (channels) {
194
- const __ch = __buildChannelProxies(channels);
195
- const fn = new Function('__ch', 'return (' + fnStr + ')(__ch)');
196
- return fn(__ch);
197
- } else {
198
- const fn = new Function('return (' + fnStr + ')()');
199
- return fn();
221
+ return parsedFn(__buildChannelProxies(channels));
200
222
  }
223
+ return parsedFn();
201
224
  }
202
225
  `;
203
226
  var NODE_BOOTSTRAP_CODE = `
@@ -223,7 +246,7 @@ parentPort.on('message', async (msg) => {
223
246
  return;
224
247
  }
225
248
  try {
226
- const result = await __execFn(msg.fnStr, msg.channels);
249
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
227
250
  if (!cancelledTasks.has(msg.taskId)) {
228
251
  parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
229
252
  }
@@ -242,7 +265,7 @@ parentPort.on('message', async (msg) => {
242
265
  })();
243
266
  } else {
244
267
  try {
245
- const result = await __execFn(msg.fnStr, msg.channels);
268
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
246
269
  parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
247
270
  } catch (error) {
248
271
  parentPort.postMessage({
@@ -283,7 +306,7 @@ self.onmessage = async (event) => {
283
306
  return;
284
307
  }
285
308
  try {
286
- const result = await __execFn(msg.fnStr, msg.channels);
309
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
287
310
  if (!cancelledTasks.has(msg.taskId)) {
288
311
  self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
289
312
  }
@@ -302,7 +325,7 @@ self.onmessage = async (event) => {
302
325
  })();
303
326
  } else {
304
327
  try {
305
- const result = await __execFn(msg.fnStr, msg.channels);
328
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
306
329
  self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
307
330
  } catch (error) {
308
331
  self.postMessage({
@@ -389,17 +412,14 @@ var BunManagedWorker = class {
389
412
  on(event, handler) {
390
413
  if (event === "message") {
391
414
  this.worker.addEventListener("message", (e) => {
392
- ;
393
415
  handler(e.data);
394
416
  });
395
417
  } else if (event === "error") {
396
418
  this.worker.addEventListener("error", (e) => {
397
- ;
398
419
  handler(e.error ?? new Error(e.message));
399
420
  });
400
421
  } else if (event === "exit") {
401
422
  this.worker.addEventListener("close", (e) => {
402
- ;
403
423
  handler(e.code ?? 0);
404
424
  });
405
425
  }
@@ -429,6 +449,8 @@ var ChannelImpl = class {
429
449
  // constraint: can't create channels of nullable type
430
450
  /** @internal */
431
451
  _id;
452
+ /** @internal — true once the channel ID has been sent to a worker */
453
+ _shared = false;
432
454
  buffer = [];
433
455
  capacity;
434
456
  closed = false;
@@ -439,6 +461,12 @@ var ChannelImpl = class {
439
461
  this.capacity = capacity;
440
462
  channelRegistry.set(this._id, this);
441
463
  }
464
+ get len() {
465
+ return this.buffer.length;
466
+ }
467
+ get cap() {
468
+ return this.capacity;
469
+ }
442
470
  send(value) {
443
471
  if (this.closed) {
444
472
  return Promise.reject(new Error("send on closed channel"));
@@ -464,6 +492,7 @@ var ChannelImpl = class {
464
492
  this.buffer.push(sender2.value);
465
493
  sender2.resolve();
466
494
  }
495
+ this.maybeUnregister();
467
496
  return Promise.resolve(value);
468
497
  }
469
498
  const sender = this.sendQueue.shift();
@@ -472,6 +501,7 @@ var ChannelImpl = class {
472
501
  return Promise.resolve(sender.value);
473
502
  }
474
503
  if (this.closed) {
504
+ this.maybeUnregister();
475
505
  return Promise.resolve(null);
476
506
  }
477
507
  return new Promise((resolve) => {
@@ -492,6 +522,45 @@ var ChannelImpl = class {
492
522
  }
493
523
  this.sendQueue = [];
494
524
  }
525
+ maybeUnregister() {
526
+ if (!this._shared && this.closed && this.buffer.length === 0 && this.recvQueue.length === 0) {
527
+ channelRegistry.delete(this._id);
528
+ }
529
+ }
530
+ sendOnly() {
531
+ const send = (value) => this.send(value);
532
+ const close = () => this.close();
533
+ const getLen = () => this.len;
534
+ const getCap = () => this.cap;
535
+ return {
536
+ send,
537
+ close,
538
+ get len() {
539
+ return getLen();
540
+ },
541
+ get cap() {
542
+ return getCap();
543
+ }
544
+ };
545
+ }
546
+ recvOnly() {
547
+ const recv = () => this.recv();
548
+ const getLen = () => this.len;
549
+ const getCap = () => this.cap;
550
+ const getIter = () => this[Symbol.asyncIterator]();
551
+ return {
552
+ recv,
553
+ get len() {
554
+ return getLen();
555
+ },
556
+ get cap() {
557
+ return getCap();
558
+ },
559
+ [Symbol.asyncIterator]() {
560
+ return getIter();
561
+ }
562
+ };
563
+ }
495
564
  async *[Symbol.asyncIterator]() {
496
565
  while (true) {
497
566
  const value = await this.recv();
@@ -510,7 +579,9 @@ function getChannelById(id) {
510
579
  return channelRegistry.get(id);
511
580
  }
512
581
  function getChannelId(channel) {
513
- return channel._id;
582
+ const impl = channel;
583
+ impl._shared = true;
584
+ return impl._id;
514
585
  }
515
586
 
516
587
  // src/adapters/inline.ts
@@ -522,6 +593,7 @@ var InlineManagedWorker = class {
522
593
  exitHandlers = [];
523
594
  terminated = false;
524
595
  cancelledTasks = /* @__PURE__ */ new Set();
596
+ fnCache = /* @__PURE__ */ new Map();
525
597
  constructor() {
526
598
  this.id = ++inlineIdCounter;
527
599
  queueMicrotask(() => {
@@ -531,7 +603,7 @@ var InlineManagedWorker = class {
531
603
  postMessage(msg) {
532
604
  if (this.terminated) return;
533
605
  if (msg.type === "execute") {
534
- this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels);
606
+ this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels, msg.args);
535
607
  } else if (msg.type === "cancel") {
536
608
  this.cancelledTasks.add(msg.taskId);
537
609
  } else if (msg.type === "channel-result") {
@@ -592,7 +664,7 @@ var InlineManagedWorker = class {
592
664
  }
593
665
  return proxies;
594
666
  }
595
- executeTask(taskId, fnStr, concurrent, channels) {
667
+ executeTask(taskId, fnStr, concurrent, channels, args) {
596
668
  queueMicrotask(async () => {
597
669
  if (this.terminated) return;
598
670
  if (concurrent && this.cancelledTasks.has(taskId)) {
@@ -600,14 +672,20 @@ var InlineManagedWorker = class {
600
672
  return;
601
673
  }
602
674
  try {
675
+ let parsedFn = this.fnCache.get(fnStr);
676
+ if (!parsedFn) {
677
+ parsedFn = new Function("return (" + fnStr + ")")();
678
+ if (this.fnCache.size >= 1e3) this.fnCache.clear();
679
+ this.fnCache.set(fnStr, parsedFn);
680
+ }
603
681
  let result;
604
- if (channels) {
682
+ if (args) {
683
+ result = await parsedFn(...args);
684
+ } else if (channels) {
605
685
  const proxies = this.buildChannelProxies(channels);
606
- const fn = new Function("__ch", "return (" + fnStr + ")(__ch)");
607
- result = await fn(proxies);
686
+ result = await parsedFn(proxies);
608
687
  } else {
609
- const fn = new Function("return (" + fnStr + ")()");
610
- result = await fn();
688
+ result = await parsedFn();
611
689
  }
612
690
  if (concurrent && this.cancelledTasks.has(taskId)) {
613
691
  this.cancelledTasks.delete(taskId);
@@ -660,6 +738,8 @@ var WorkerPool = class {
660
738
  totalCompleted = 0;
661
739
  totalFailed = 0;
662
740
  taskMap = /* @__PURE__ */ new Map();
741
+ // Per-worker deques for work-stealing strategy
742
+ workerDeques = /* @__PURE__ */ new Map();
663
743
  constructor(config, adapter) {
664
744
  this.config = config;
665
745
  this.adapter = adapter;
@@ -697,6 +777,110 @@ var WorkerPool = class {
697
777
  }
698
778
  return void 0;
699
779
  }
780
+ // --- Work-stealing helpers ---
781
+ getOrCreateDeque(worker) {
782
+ let deque = this.workerDeques.get(worker);
783
+ if (!deque) {
784
+ deque = { high: [], normal: [], low: [] };
785
+ this.workerDeques.set(worker, deque);
786
+ }
787
+ return deque;
788
+ }
789
+ dequeSize(worker) {
790
+ const deque = this.workerDeques.get(worker);
791
+ if (!deque) return 0;
792
+ return deque.high.length + deque.normal.length + deque.low.length;
793
+ }
794
+ enqueueToWorker(worker, task2) {
795
+ this.getOrCreateDeque(worker)[task2.priority].push(task2);
796
+ }
797
+ /** Pop from own deque — FIFO within each priority level. */
798
+ dequeueFromOwn(worker) {
799
+ const deque = this.workerDeques.get(worker);
800
+ if (!deque) return void 0;
801
+ return deque.high.shift() ?? deque.normal.shift() ?? deque.low.shift();
802
+ }
803
+ /** Steal from a victim's deque — takes lowest-priority work from the back. */
804
+ stealFrom(victim) {
805
+ const deque = this.workerDeques.get(victim);
806
+ if (!deque) return void 0;
807
+ return deque.low.pop() ?? deque.normal.pop() ?? deque.high.pop();
808
+ }
809
+ /** Find the exclusive worker with the shortest deque to push a new task to. */
810
+ findShortestDequeWorker() {
811
+ let best;
812
+ let bestSize = Infinity;
813
+ const seen = /* @__PURE__ */ new Set();
814
+ for (const worker of this.exclusiveWorkers.values()) {
815
+ if (seen.has(worker)) continue;
816
+ seen.add(worker);
817
+ const size = this.dequeSize(worker);
818
+ if (size < bestSize) {
819
+ bestSize = size;
820
+ best = worker;
821
+ }
822
+ }
823
+ return best;
824
+ }
825
+ /** Steal a task from the busiest worker's deque, excluding the thief. */
826
+ stealFromBusiest(thief) {
827
+ let victim;
828
+ let maxSize = 0;
829
+ for (const [worker, deque] of this.workerDeques) {
830
+ if (worker === thief) continue;
831
+ const size = deque.high.length + deque.normal.length + deque.low.length;
832
+ if (size > maxSize) {
833
+ maxSize = size;
834
+ victim = worker;
835
+ }
836
+ }
837
+ if (!victim || maxSize === 0) return void 0;
838
+ return this.stealFrom(victim);
839
+ }
840
+ /** Steal from any deque (no thief exclusion — used by resize). */
841
+ stealFromAny() {
842
+ let victim;
843
+ let maxSize = 0;
844
+ for (const [worker, deque] of this.workerDeques) {
845
+ const size = deque.high.length + deque.normal.length + deque.low.length;
846
+ if (size > maxSize) {
847
+ maxSize = size;
848
+ victim = worker;
849
+ }
850
+ }
851
+ if (!victim || maxSize === 0) return void 0;
852
+ return this.stealFrom(victim);
853
+ }
854
+ /** Remove a task by ID from any worker's deque. */
855
+ removeFromDeques(taskId) {
856
+ for (const [, deque] of this.workerDeques) {
857
+ for (const priority of ["high", "normal", "low"]) {
858
+ const queue = deque[priority];
859
+ const idx = queue.findIndex((t) => t.id === taskId);
860
+ if (idx !== -1) {
861
+ return queue.splice(idx, 1)[0];
862
+ }
863
+ }
864
+ }
865
+ return void 0;
866
+ }
867
+ /** Flush a worker's deque back to the global queue (for redistribution). */
868
+ flushDeque(worker) {
869
+ const deque = this.workerDeques.get(worker);
870
+ if (!deque) return;
871
+ for (const priority of ["high", "normal", "low"]) {
872
+ for (const task2 of deque[priority]) {
873
+ this.queues[priority].push(task2);
874
+ }
875
+ }
876
+ this.workerDeques.delete(worker);
877
+ }
878
+ /** Clean up a deque if it's empty. */
879
+ cleanupDeque(worker) {
880
+ if (this.dequeSize(worker) === 0) {
881
+ this.workerDeques.delete(worker);
882
+ }
883
+ }
700
884
  // --- Submit ---
701
885
  submit(task2) {
702
886
  if (this.draining) {
@@ -722,6 +906,13 @@ var WorkerPool = class {
722
906
  this.createAndReadyWorker();
723
907
  return;
724
908
  }
909
+ if (this.config.strategy === "work-stealing") {
910
+ const target = this.findShortestDequeWorker();
911
+ if (target) {
912
+ this.enqueueToWorker(target, task2);
913
+ return;
914
+ }
915
+ }
725
916
  this.enqueue(task2);
726
917
  }
727
918
  submitConcurrent(task2) {
@@ -759,6 +950,7 @@ var WorkerPool = class {
759
950
  type: "execute",
760
951
  taskId: task2.id,
761
952
  fnStr: task2.fnStr,
953
+ args: task2.args,
762
954
  concurrent: false,
763
955
  channels: task2.channels
764
956
  };
@@ -780,6 +972,7 @@ var WorkerPool = class {
780
972
  type: "execute",
781
973
  taskId: task2.id,
782
974
  fnStr: task2.fnStr,
975
+ args: task2.args,
783
976
  concurrent: true,
784
977
  channels: task2.channels
785
978
  };
@@ -826,6 +1019,19 @@ var WorkerPool = class {
826
1019
  }
827
1020
  assignNextOrIdle(worker) {
828
1021
  if (!this.allWorkers.has(worker)) return;
1022
+ if (this.config.strategy === "work-stealing") {
1023
+ const own = this.dequeueFromOwn(worker);
1024
+ if (own) {
1025
+ this.cleanupDeque(worker);
1026
+ this.dispatch(worker, own);
1027
+ return;
1028
+ }
1029
+ const stolen = this.stealFromBusiest(worker);
1030
+ if (stolen) {
1031
+ this.dispatch(worker, stolen);
1032
+ return;
1033
+ }
1034
+ }
829
1035
  const next = this.dequeue();
830
1036
  if (next) {
831
1037
  this.dispatch(worker, next);
@@ -836,6 +1042,7 @@ var WorkerPool = class {
836
1042
  this.dispatchConcurrent(worker, concurrentNext);
837
1043
  return;
838
1044
  }
1045
+ this.cleanupDeque(worker);
839
1046
  this.makeIdle(worker);
840
1047
  }
841
1048
  assignNextConcurrentOrIdle(worker) {
@@ -855,6 +1062,13 @@ var WorkerPool = class {
855
1062
  const exclusiveTask = this.dequeue();
856
1063
  if (exclusiveTask) {
857
1064
  this.dispatch(worker, exclusiveTask);
1065
+ } else if (this.config.strategy === "work-stealing") {
1066
+ const stolen = this.stealFromBusiest(worker);
1067
+ if (stolen) {
1068
+ this.dispatch(worker, stolen);
1069
+ } else {
1070
+ this.makeIdle(worker);
1071
+ }
858
1072
  } else {
859
1073
  this.makeIdle(worker);
860
1074
  }
@@ -950,6 +1164,13 @@ var WorkerPool = class {
950
1164
  removed.reject(new DOMException("Task was cancelled", "AbortError"));
951
1165
  return;
952
1166
  }
1167
+ if (this.config.strategy === "work-stealing") {
1168
+ const removedFromDeque = this.removeFromDeques(taskId);
1169
+ if (removedFromDeque) {
1170
+ removedFromDeque.reject(new DOMException("Task was cancelled", "AbortError"));
1171
+ return;
1172
+ }
1173
+ }
953
1174
  const removedConcurrent = this.removeFromConcurrentQueue(taskId);
954
1175
  if (removedConcurrent) {
955
1176
  removedConcurrent.reject(new DOMException("Task was cancelled", "AbortError"));
@@ -960,6 +1181,7 @@ var WorkerPool = class {
960
1181
  this.exclusiveWorkers.delete(taskId);
961
1182
  this.allWorkers.delete(exclusiveWorker);
962
1183
  this.taskMap.delete(taskId);
1184
+ this.flushDeque(exclusiveWorker);
963
1185
  exclusiveWorker.terminate();
964
1186
  return;
965
1187
  }
@@ -991,6 +1213,14 @@ var WorkerPool = class {
991
1213
  }
992
1214
  this.concurrentQueues[priority] = [];
993
1215
  }
1216
+ for (const [, deque] of this.workerDeques) {
1217
+ for (const priority of ["high", "normal", "low"]) {
1218
+ for (const task2 of deque[priority]) {
1219
+ task2.reject(new Error("Pool is shutting down"));
1220
+ }
1221
+ }
1222
+ }
1223
+ this.workerDeques.clear();
994
1224
  for (const [taskId] of this.exclusiveWorkers) {
995
1225
  this.taskMap.delete(taskId);
996
1226
  }
@@ -1018,7 +1248,7 @@ var WorkerPool = class {
1018
1248
  while (true) {
1019
1249
  const totalWorkers = this.allWorkers.size + this.pendingWorkerCount;
1020
1250
  if (totalWorkers >= maxThreads) break;
1021
- const task2 = this.dequeue() ?? this.dequeueConcurrent();
1251
+ const task2 = this.dequeue() ?? (this.config.strategy === "work-stealing" ? this.stealFromAny() : void 0) ?? this.dequeueConcurrent();
1022
1252
  if (!task2) break;
1023
1253
  this.pendingWorkerCount++;
1024
1254
  this.pendingTasksForWorkers.push(task2);
@@ -1081,6 +1311,7 @@ var WorkerPool = class {
1081
1311
  this.idleWorkers.splice(idleIdx, 1);
1082
1312
  }
1083
1313
  this.rejectExclusiveTaskForWorker(worker, new Error("Worker exited unexpectedly"));
1314
+ this.flushDeque(worker);
1084
1315
  const taskSet = this.sharedWorkers.get(worker);
1085
1316
  if (taskSet) {
1086
1317
  for (const taskId of taskSet) {
@@ -1096,6 +1327,17 @@ var WorkerPool = class {
1096
1327
  for (const taskSet of this.sharedWorkers.values()) {
1097
1328
  concurrentTasks += taskSet.size;
1098
1329
  }
1330
+ let dequeHigh = 0;
1331
+ let dequeNormal = 0;
1332
+ let dequeLow = 0;
1333
+ for (const deque of this.workerDeques.values()) {
1334
+ dequeHigh += deque.high.length;
1335
+ dequeNormal += deque.normal.length;
1336
+ dequeLow += deque.low.length;
1337
+ }
1338
+ const queuedHigh = this.queues.high.length + dequeHigh;
1339
+ const queuedNormal = this.queues.normal.length + dequeNormal;
1340
+ const queuedLow = this.queues.low.length + dequeLow;
1099
1341
  return {
1100
1342
  totalWorkers: this.allWorkers.size,
1101
1343
  idleWorkers: this.idleWorkers.length,
@@ -1104,10 +1346,10 @@ var WorkerPool = class {
1104
1346
  concurrentTasks,
1105
1347
  pendingWorkers: this.pendingWorkerCount,
1106
1348
  queuedTasks: {
1107
- high: this.queues.high.length,
1108
- normal: this.queues.normal.length,
1109
- low: this.queues.low.length,
1110
- total: this.queues.high.length + this.queues.normal.length + this.queues.low.length
1349
+ high: queuedHigh,
1350
+ normal: queuedNormal,
1351
+ low: queuedLow,
1352
+ total: queuedHigh + queuedNormal + queuedLow
1111
1353
  },
1112
1354
  queuedConcurrentTasks: {
1113
1355
  high: this.concurrentQueues.high.length,
@@ -1162,7 +1404,7 @@ var taskCounter = 0;
1162
1404
  function spawn(fn, opts) {
1163
1405
  const fnStr = serializeFunction(fn);
1164
1406
  const taskId = String(++taskCounter);
1165
- const spawnStack = new Error().stack;
1407
+ const spawnError = new Error();
1166
1408
  let resolveFn;
1167
1409
  let rejectFn;
1168
1410
  let settled = false;
@@ -1192,14 +1434,29 @@ function spawn(fn, opts) {
1192
1434
  reject: (reason) => {
1193
1435
  if (!settled) {
1194
1436
  settled = true;
1195
- if (reason instanceof Error && spawnStack) {
1196
- const callerLine = spawnStack.split("\n").slice(2).join("\n");
1197
- reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
1437
+ if (reason instanceof Error) {
1438
+ const spawnStack = spawnError.stack;
1439
+ if (spawnStack) {
1440
+ const callerLine = spawnStack.split("\n").slice(2).join("\n");
1441
+ reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
1442
+ }
1198
1443
  }
1199
1444
  rejectFn(reason);
1200
1445
  }
1201
1446
  }
1202
1447
  };
1448
+ const ctx = opts?.ctx;
1449
+ if (ctx) {
1450
+ if (ctx.signal.aborted) {
1451
+ settled = true;
1452
+ rejectFn(ctx.err ?? new DOMException("Task was cancelled", "AbortError"));
1453
+ return {
1454
+ result,
1455
+ cancel: () => {
1456
+ }
1457
+ };
1458
+ }
1459
+ }
1203
1460
  getPool().submit(task2);
1204
1461
  const cancel = () => {
1205
1462
  if (settled) return;
@@ -1207,6 +1464,14 @@ function spawn(fn, opts) {
1207
1464
  getPool().cancelTask(taskId);
1208
1465
  rejectFn(new DOMException("Task was cancelled", "AbortError"));
1209
1466
  };
1467
+ if (ctx) {
1468
+ const onAbort = () => cancel();
1469
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
1470
+ result.then(
1471
+ () => ctx.signal.removeEventListener("abort", onAbort),
1472
+ () => ctx.signal.removeEventListener("abort", onAbort)
1473
+ );
1474
+ }
1210
1475
  return { result, cancel };
1211
1476
  }
1212
1477
 
@@ -1214,6 +1479,17 @@ function spawn(fn, opts) {
1214
1479
  var WaitGroup = class {
1215
1480
  tasks = [];
1216
1481
  controller = new AbortController();
1482
+ ctx;
1483
+ constructor(ctx) {
1484
+ this.ctx = ctx;
1485
+ if (ctx) {
1486
+ if (ctx.signal.aborted) {
1487
+ this.controller.abort();
1488
+ } else {
1489
+ ctx.signal.addEventListener("abort", () => this.cancel(), { once: true });
1490
+ }
1491
+ }
1492
+ }
1217
1493
  /**
1218
1494
  * An `AbortSignal` shared across all tasks in this group.
1219
1495
  * Pass it into spawned functions so they can stop early when `cancel()` is called.
@@ -1230,7 +1506,7 @@ var WaitGroup = class {
1230
1506
  if (this.controller.signal.aborted) {
1231
1507
  throw new Error("WaitGroup has been cancelled");
1232
1508
  }
1233
- const handle = spawn(fn, opts);
1509
+ const handle = spawn(fn, { ...opts, ctx: this.ctx });
1234
1510
  this.tasks.push(handle);
1235
1511
  }
1236
1512
  /**
@@ -1265,22 +1541,81 @@ var ErrGroup = class {
1265
1541
  controller = new AbortController();
1266
1542
  firstError = null;
1267
1543
  hasError = false;
1544
+ ctx;
1545
+ limit = 0;
1546
+ // 0 = unlimited
1547
+ inFlight = 0;
1548
+ waiting = [];
1549
+ constructor(ctx) {
1550
+ this.ctx = ctx;
1551
+ if (ctx) {
1552
+ if (ctx.signal.aborted) {
1553
+ this.controller.abort();
1554
+ } else {
1555
+ ctx.signal.addEventListener("abort", () => this.cancel(), { once: true });
1556
+ }
1557
+ }
1558
+ }
1268
1559
  get signal() {
1269
1560
  return this.controller.signal;
1270
1561
  }
1562
+ /**
1563
+ * Set the maximum number of tasks that can run concurrently.
1564
+ * Like Go's `errgroup.SetLimit()`. Must be called before any `spawn()`.
1565
+ * A value of 0 (default) means unlimited.
1566
+ */
1567
+ setLimit(n) {
1568
+ if (this.tasks.length > 0) {
1569
+ throw new Error("SetLimit must be called before any spawn()");
1570
+ }
1571
+ if (n < 0 || !Number.isInteger(n)) {
1572
+ throw new RangeError("Limit must be a non-negative integer");
1573
+ }
1574
+ this.limit = n;
1575
+ }
1271
1576
  spawn(fn, opts) {
1272
1577
  if (this.controller.signal.aborted) {
1273
1578
  throw new Error("ErrGroup has been cancelled");
1274
1579
  }
1275
- const handle = spawn(fn, opts);
1276
- handle.result.catch((err) => {
1580
+ if (this.limit > 0 && this.inFlight >= this.limit) {
1581
+ let innerCancel = () => {
1582
+ };
1583
+ let cancelled = false;
1584
+ const cancel = () => {
1585
+ cancelled = true;
1586
+ innerCancel();
1587
+ };
1588
+ const result = new Promise((resolve) => {
1589
+ this.waiting.push(resolve);
1590
+ }).then(() => {
1591
+ if (cancelled) throw new DOMException("Task was cancelled", "AbortError");
1592
+ const handle2 = this.doSpawn(fn, opts);
1593
+ innerCancel = handle2.cancel;
1594
+ return handle2.result;
1595
+ });
1596
+ this.tasks.push({ result, cancel });
1597
+ return;
1598
+ }
1599
+ const handle = this.doSpawn(fn, opts);
1600
+ this.tasks.push(handle);
1601
+ }
1602
+ doSpawn(fn, opts) {
1603
+ this.inFlight++;
1604
+ const handle = spawn(fn, { ...opts, ctx: this.ctx });
1605
+ const onSettle = () => {
1606
+ this.inFlight--;
1607
+ const next = this.waiting.shift();
1608
+ if (next) next();
1609
+ };
1610
+ handle.result.then(onSettle, (err) => {
1611
+ onSettle();
1277
1612
  if (!this.hasError) {
1278
1613
  this.hasError = true;
1279
1614
  this.firstError = err;
1280
1615
  this.cancel();
1281
1616
  }
1282
1617
  });
1283
- this.tasks.push(handle);
1618
+ return handle;
1284
1619
  }
1285
1620
  async wait() {
1286
1621
  const settled = await Promise.allSettled(this.tasks.map((t) => t.result));
@@ -1336,6 +1671,212 @@ var Mutex = class {
1336
1671
  return this.locked;
1337
1672
  }
1338
1673
  };
1674
+ var RWMutex = class {
1675
+ readers = 0;
1676
+ writing = false;
1677
+ readQueue = [];
1678
+ writeQueue = [];
1679
+ async rLock() {
1680
+ if (!this.writing && this.writeQueue.length === 0) {
1681
+ this.readers++;
1682
+ return;
1683
+ }
1684
+ return new Promise((resolve) => {
1685
+ this.readQueue.push(() => {
1686
+ this.readers++;
1687
+ resolve();
1688
+ });
1689
+ });
1690
+ }
1691
+ rUnlock() {
1692
+ if (this.readers <= 0) {
1693
+ throw new Error("Cannot rUnlock a RWMutex that is not read-locked");
1694
+ }
1695
+ this.readers--;
1696
+ if (this.readers === 0) {
1697
+ this.wakeWriter();
1698
+ }
1699
+ }
1700
+ async lock() {
1701
+ if (!this.writing && this.readers === 0) {
1702
+ this.writing = true;
1703
+ return;
1704
+ }
1705
+ return new Promise((resolve) => {
1706
+ this.writeQueue.push(() => {
1707
+ this.writing = true;
1708
+ resolve();
1709
+ });
1710
+ });
1711
+ }
1712
+ unlock() {
1713
+ if (!this.writing) {
1714
+ throw new Error("Cannot unlock a RWMutex that is not write-locked");
1715
+ }
1716
+ this.writing = false;
1717
+ if (this.readQueue.length > 0) {
1718
+ this.wakeReaders();
1719
+ } else {
1720
+ this.wakeWriter();
1721
+ }
1722
+ }
1723
+ async withRLock(fn) {
1724
+ await this.rLock();
1725
+ try {
1726
+ return await fn();
1727
+ } finally {
1728
+ this.rUnlock();
1729
+ }
1730
+ }
1731
+ async withLock(fn) {
1732
+ await this.lock();
1733
+ try {
1734
+ return await fn();
1735
+ } finally {
1736
+ this.unlock();
1737
+ }
1738
+ }
1739
+ get isLocked() {
1740
+ return this.writing || this.readers > 0;
1741
+ }
1742
+ wakeReaders() {
1743
+ const queue = this.readQueue;
1744
+ this.readQueue = [];
1745
+ for (const wake of queue) {
1746
+ wake();
1747
+ }
1748
+ }
1749
+ wakeWriter() {
1750
+ const next = this.writeQueue.shift();
1751
+ if (next) next();
1752
+ }
1753
+ };
1754
+
1755
+ // src/semaphore.ts
1756
+ var Semaphore = class {
1757
+ max;
1758
+ cur;
1759
+ queue = [];
1760
+ constructor(n) {
1761
+ if (n <= 0 || !Number.isInteger(n)) {
1762
+ throw new Error("Semaphore capacity must be a positive integer");
1763
+ }
1764
+ this.max = n;
1765
+ this.cur = 0;
1766
+ }
1767
+ /**
1768
+ * Acquire `n` permits, waiting if necessary until they are available.
1769
+ * Acquires are served in FIFO order.
1770
+ */
1771
+ async acquire(n = 1) {
1772
+ if (n <= 0 || !Number.isInteger(n)) {
1773
+ throw new Error("Acquire count must be a positive integer");
1774
+ }
1775
+ if (n > this.max) {
1776
+ throw new Error(`Acquire count ${n} exceeds semaphore capacity ${this.max}`);
1777
+ }
1778
+ if (this.cur + n <= this.max && this.queue.length === 0) {
1779
+ this.cur += n;
1780
+ return;
1781
+ }
1782
+ return new Promise((resolve) => {
1783
+ this.queue.push({ n, resolve });
1784
+ });
1785
+ }
1786
+ /**
1787
+ * Try to acquire `n` permits without blocking.
1788
+ * Returns `true` if successful, `false` if not enough permits are available.
1789
+ */
1790
+ tryAcquire(n = 1) {
1791
+ if (n <= 0 || !Number.isInteger(n)) {
1792
+ throw new Error("Acquire count must be a positive integer");
1793
+ }
1794
+ if (n > this.max) {
1795
+ throw new Error(`Acquire count ${n} exceeds semaphore capacity ${this.max}`);
1796
+ }
1797
+ if (this.cur + n <= this.max && this.queue.length === 0) {
1798
+ this.cur += n;
1799
+ return true;
1800
+ }
1801
+ return false;
1802
+ }
1803
+ /**
1804
+ * Release `n` permits, potentially waking queued acquirers.
1805
+ */
1806
+ release(n = 1) {
1807
+ if (n <= 0 || !Number.isInteger(n)) {
1808
+ throw new Error("Release count must be a positive integer");
1809
+ }
1810
+ if (this.cur - n < 0) {
1811
+ throw new Error("Released more permits than acquired");
1812
+ }
1813
+ this.cur -= n;
1814
+ this.wake();
1815
+ }
1816
+ /**
1817
+ * Acquire `n` permits, run `fn`, then release automatically — even if `fn` throws.
1818
+ */
1819
+ async withAcquire(fn, n = 1) {
1820
+ await this.acquire(n);
1821
+ try {
1822
+ return await fn();
1823
+ } finally {
1824
+ this.release(n);
1825
+ }
1826
+ }
1827
+ /** Number of permits currently available. */
1828
+ get available() {
1829
+ return this.max - this.cur;
1830
+ }
1831
+ /** Total capacity of the semaphore. */
1832
+ get capacity() {
1833
+ return this.max;
1834
+ }
1835
+ wake() {
1836
+ while (this.queue.length > 0) {
1837
+ const head = this.queue[0];
1838
+ if (this.cur + head.n > this.max) break;
1839
+ this.queue.shift();
1840
+ this.cur += head.n;
1841
+ head.resolve();
1842
+ }
1843
+ }
1844
+ };
1845
+
1846
+ // src/cond.ts
1847
+ var Cond = class {
1848
+ mu;
1849
+ waiters = [];
1850
+ constructor(mu) {
1851
+ this.mu = mu;
1852
+ }
1853
+ /**
1854
+ * Atomically releases the mutex, suspends the caller until `signal()` or `broadcast()`
1855
+ * is called, then re-acquires the mutex before returning.
1856
+ *
1857
+ * Must be called while holding the mutex.
1858
+ */
1859
+ async wait() {
1860
+ this.mu.unlock();
1861
+ await new Promise((resolve) => {
1862
+ this.waiters.push(resolve);
1863
+ });
1864
+ await this.mu.lock();
1865
+ }
1866
+ /** Wake one waiting task (if any). */
1867
+ signal() {
1868
+ const next = this.waiters.shift();
1869
+ if (next) next();
1870
+ }
1871
+ /** Wake all waiting tasks. */
1872
+ broadcast() {
1873
+ const queue = this.waiters;
1874
+ this.waiters = [];
1875
+ for (const wake of queue) {
1876
+ wake();
1877
+ }
1878
+ }
1879
+ };
1339
1880
 
1340
1881
  // src/once.ts
1341
1882
  var Once = class {
@@ -1477,23 +2018,69 @@ function ticker(ms) {
1477
2018
  return new Ticker(ms);
1478
2019
  }
1479
2020
 
2021
+ // src/timer.ts
2022
+ var Timer = class {
2023
+ timer = null;
2024
+ _stopped = false;
2025
+ /** Promise that resolves when the timer fires. Replaced on `reset()`. */
2026
+ channel;
2027
+ constructor(ms) {
2028
+ this.channel = this.schedule(ms);
2029
+ }
2030
+ schedule(ms) {
2031
+ return new Promise((resolve) => {
2032
+ this.timer = setTimeout(() => {
2033
+ this._stopped = true;
2034
+ this.timer = null;
2035
+ resolve();
2036
+ }, ms);
2037
+ if (typeof this.timer === "object" && "unref" in this.timer) {
2038
+ this.timer.unref();
2039
+ }
2040
+ });
2041
+ }
2042
+ /**
2043
+ * Stop the timer. Returns `true` if the timer was pending (stopped before firing),
2044
+ * `false` if it had already fired or was already stopped.
2045
+ *
2046
+ * After stopping, the current `channel` promise will never resolve.
2047
+ */
2048
+ stop() {
2049
+ if (this.timer === null) return false;
2050
+ clearTimeout(this.timer);
2051
+ this.timer = null;
2052
+ this._stopped = true;
2053
+ return true;
2054
+ }
2055
+ /**
2056
+ * Reset the timer to fire after `ms` milliseconds.
2057
+ * If the timer was pending, it is stopped first. Creates a new `channel` promise.
2058
+ */
2059
+ reset(ms) {
2060
+ this.stop();
2061
+ this._stopped = false;
2062
+ this.channel = this.schedule(ms);
2063
+ }
2064
+ /** Whether the timer has fired or been stopped. */
2065
+ get stopped() {
2066
+ return this._stopped;
2067
+ }
2068
+ };
2069
+
1480
2070
  // src/registry.ts
1481
2071
  var taskCounter2 = 0;
1482
2072
  function task(fn) {
2073
+ const fnStr = serializeFunction(fn);
1483
2074
  return (...args) => {
1484
- const fnStr = serializeFunction(fn);
1485
- const serializedArgs = args.map((a) => {
1486
- const json = JSON.stringify(a);
1487
- if (json === void 0) {
2075
+ for (const a of args) {
2076
+ if (JSON.stringify(a) === void 0) {
1488
2077
  throw new TypeError(
1489
2078
  `Argument of type ${typeof a} is not JSON-serializable. task() args must be JSON-serializable (no undefined, functions, symbols, or BigInt).`
1490
2079
  );
1491
2080
  }
1492
- return json;
1493
- });
1494
- const wrapperStr = `() => (${fnStr})(${serializedArgs.join(", ")})`;
2081
+ }
1495
2082
  const taskId = `task_${++taskCounter2}`;
1496
- const spawnStack = new Error().stack;
2083
+ const spawnError = new Error();
1497
2084
  let resolveFn;
1498
2085
  let rejectFn;
1499
2086
  const result = new Promise((resolve, reject) => {
@@ -1502,14 +2089,18 @@ function task(fn) {
1502
2089
  });
1503
2090
  const taskObj = {
1504
2091
  id: taskId,
1505
- fnStr: wrapperStr,
2092
+ fnStr,
2093
+ args,
1506
2094
  priority: "normal",
1507
2095
  concurrent: false,
1508
2096
  resolve: (value) => resolveFn(value),
1509
2097
  reject: (reason) => {
1510
- if (reason instanceof Error && spawnStack) {
1511
- const callerLine = spawnStack.split("\n").slice(2).join("\n");
1512
- reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
2098
+ if (reason instanceof Error) {
2099
+ const spawnStack = spawnError.stack;
2100
+ if (spawnStack) {
2101
+ const callerLine = spawnStack.split("\n").slice(2).join("\n");
2102
+ reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
2103
+ }
1513
2104
  }
1514
2105
  rejectFn(reason);
1515
2106
  }
@@ -1518,14 +2109,180 @@ function task(fn) {
1518
2109
  return result;
1519
2110
  };
1520
2111
  }
2112
+
2113
+ // src/context.ts
2114
+ var CancelledError = class extends Error {
2115
+ constructor(message = "context cancelled") {
2116
+ super(message);
2117
+ this.name = "CancelledError";
2118
+ }
2119
+ };
2120
+ var DeadlineExceededError = class extends Error {
2121
+ constructor() {
2122
+ super("context deadline exceeded");
2123
+ this.name = "DeadlineExceededError";
2124
+ }
2125
+ };
2126
+ var BaseContext = class {
2127
+ _err = null;
2128
+ controller;
2129
+ parent;
2130
+ constructor(parent) {
2131
+ this.parent = parent;
2132
+ this.controller = new AbortController();
2133
+ if (parent) {
2134
+ if (parent.signal.aborted) {
2135
+ this._err = parent.err ?? new CancelledError();
2136
+ this.controller.abort();
2137
+ } else {
2138
+ parent.signal.addEventListener(
2139
+ "abort",
2140
+ () => {
2141
+ if (!this.controller.signal.aborted) {
2142
+ this._err = parent.err ?? new CancelledError();
2143
+ this.controller.abort();
2144
+ }
2145
+ },
2146
+ { once: true }
2147
+ );
2148
+ }
2149
+ }
2150
+ }
2151
+ get signal() {
2152
+ return this.controller.signal;
2153
+ }
2154
+ get deadline() {
2155
+ return this.parent?.deadline ?? null;
2156
+ }
2157
+ get err() {
2158
+ return this._err;
2159
+ }
2160
+ value(_key) {
2161
+ return this.parent?.value(_key);
2162
+ }
2163
+ done() {
2164
+ if (this.controller.signal.aborted) return Promise.resolve();
2165
+ return new Promise((resolve) => {
2166
+ this.controller.signal.addEventListener("abort", () => resolve(), { once: true });
2167
+ });
2168
+ }
2169
+ };
2170
+ var BackgroundContext = class {
2171
+ _signal = new AbortController().signal;
2172
+ get signal() {
2173
+ return this._signal;
2174
+ }
2175
+ get deadline() {
2176
+ return null;
2177
+ }
2178
+ get err() {
2179
+ return null;
2180
+ }
2181
+ value(_key) {
2182
+ return void 0;
2183
+ }
2184
+ done() {
2185
+ return new Promise(() => {
2186
+ });
2187
+ }
2188
+ };
2189
+ var bg = null;
2190
+ function background() {
2191
+ if (!bg) bg = new BackgroundContext();
2192
+ return bg;
2193
+ }
2194
+ var CancelContext = class extends BaseContext {
2195
+ cancel(reason) {
2196
+ if (!this.controller.signal.aborted) {
2197
+ this._err = new CancelledError(reason ?? "context cancelled");
2198
+ this.controller.abort();
2199
+ }
2200
+ }
2201
+ };
2202
+ function withCancel(parent) {
2203
+ const ctx = new CancelContext(parent);
2204
+ return [ctx, (reason) => ctx.cancel(reason)];
2205
+ }
2206
+ var DeadlineContext = class extends BaseContext {
2207
+ _deadline;
2208
+ timer = null;
2209
+ constructor(parent, deadline) {
2210
+ super(parent);
2211
+ this._deadline = deadline;
2212
+ if (parent.deadline && parent.deadline < deadline) {
2213
+ this._deadline = parent.deadline;
2214
+ }
2215
+ if (this.controller.signal.aborted) {
2216
+ return;
2217
+ }
2218
+ const ms = this._deadline.getTime() - Date.now();
2219
+ if (ms <= 0) {
2220
+ this._err = new DeadlineExceededError();
2221
+ this.controller.abort();
2222
+ } else {
2223
+ this.timer = setTimeout(() => {
2224
+ if (!this.controller.signal.aborted) {
2225
+ this._err = new DeadlineExceededError();
2226
+ this.controller.abort();
2227
+ }
2228
+ }, ms);
2229
+ if (typeof this.timer === "object" && "unref" in this.timer) {
2230
+ this.timer.unref();
2231
+ }
2232
+ }
2233
+ }
2234
+ get deadline() {
2235
+ return this._deadline;
2236
+ }
2237
+ cancel(reason) {
2238
+ if (this.timer !== null) {
2239
+ clearTimeout(this.timer);
2240
+ this.timer = null;
2241
+ }
2242
+ if (!this.controller.signal.aborted) {
2243
+ this._err = new CancelledError(reason ?? "context cancelled");
2244
+ this.controller.abort();
2245
+ }
2246
+ }
2247
+ };
2248
+ function withDeadline(parent, deadline) {
2249
+ const ctx = new DeadlineContext(parent, deadline);
2250
+ return [ctx, (reason) => ctx.cancel(reason)];
2251
+ }
2252
+ function withTimeout(parent, ms) {
2253
+ return withDeadline(parent, new Date(Date.now() + ms));
2254
+ }
2255
+ var ValueContext = class extends BaseContext {
2256
+ key;
2257
+ val;
2258
+ constructor(parent, key, val) {
2259
+ super(parent);
2260
+ this.key = key;
2261
+ this.val = val;
2262
+ }
2263
+ value(key) {
2264
+ if (key === this.key) return this.val;
2265
+ return this.parent?.value(key);
2266
+ }
2267
+ };
2268
+ function withValue(parent, key, value) {
2269
+ return new ValueContext(parent, key, value);
2270
+ }
1521
2271
  // Annotate the CommonJS export names for ESM import in node:
1522
2272
  0 && (module.exports = {
2273
+ CancelledError,
2274
+ Cond,
2275
+ DeadlineExceededError,
1523
2276
  ErrGroup,
1524
2277
  Mutex,
1525
2278
  Once,
2279
+ RWMutex,
2280
+ Semaphore,
1526
2281
  Ticker,
2282
+ Timer,
1527
2283
  WaitGroup,
1528
2284
  after,
2285
+ background,
1529
2286
  chan,
1530
2287
  configure,
1531
2288
  detectCapability,
@@ -1536,6 +2293,10 @@ function task(fn) {
1536
2293
  spawn,
1537
2294
  stats,
1538
2295
  task,
1539
- ticker
2296
+ ticker,
2297
+ withCancel,
2298
+ withDeadline,
2299
+ withTimeout,
2300
+ withValue
1540
2301
  });
1541
2302
  //# sourceMappingURL=index.cjs.map