@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/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  Mutex: () => Mutex,
28
28
  Once: () => Once,
29
29
  RWMutex: () => RWMutex,
30
+ Semaphore: () => Semaphore,
30
31
  Ticker: () => Ticker,
31
32
  Timer: () => Timer,
32
33
  WaitGroup: () => WaitGroup,
@@ -54,10 +55,13 @@ module.exports = __toCommonJS(index_exports);
54
55
  var NATIVE_CODE_RE = /\[native code\]/;
55
56
  var METHOD_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/;
56
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();
57
59
  function serializeFunction(fn) {
58
60
  if (typeof fn !== "function") {
59
61
  throw new TypeError("Expected a function");
60
62
  }
63
+ const cached = serializeCache.get(fn);
64
+ if (cached) return cached;
61
65
  const str = fn.toString();
62
66
  if (typeof str !== "string" || str.length === 0) {
63
67
  throw new TypeError(
@@ -79,6 +83,7 @@ function serializeFunction(fn) {
79
83
  "Class methods cannot be serialized. Use an arrow function wrapper instead."
80
84
  );
81
85
  }
86
+ serializeCache.set(fn, str);
82
87
  return str;
83
88
  }
84
89
 
@@ -199,15 +204,23 @@ function __buildChannelProxies(channels) {
199
204
  return proxies;
200
205
  }
201
206
 
202
- 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
+ }
203
220
  if (channels) {
204
- const __ch = __buildChannelProxies(channels);
205
- const fn = new Function('__ch', 'return (' + fnStr + ')(__ch)');
206
- return fn(__ch);
207
- } else {
208
- const fn = new Function('return (' + fnStr + ')()');
209
- return fn();
221
+ return parsedFn(__buildChannelProxies(channels));
210
222
  }
223
+ return parsedFn();
211
224
  }
212
225
  `;
213
226
  var NODE_BOOTSTRAP_CODE = `
@@ -233,7 +246,7 @@ parentPort.on('message', async (msg) => {
233
246
  return;
234
247
  }
235
248
  try {
236
- const result = await __execFn(msg.fnStr, msg.channels);
249
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
237
250
  if (!cancelledTasks.has(msg.taskId)) {
238
251
  parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
239
252
  }
@@ -252,7 +265,7 @@ parentPort.on('message', async (msg) => {
252
265
  })();
253
266
  } else {
254
267
  try {
255
- const result = await __execFn(msg.fnStr, msg.channels);
268
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
256
269
  parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
257
270
  } catch (error) {
258
271
  parentPort.postMessage({
@@ -293,7 +306,7 @@ self.onmessage = async (event) => {
293
306
  return;
294
307
  }
295
308
  try {
296
- const result = await __execFn(msg.fnStr, msg.channels);
309
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
297
310
  if (!cancelledTasks.has(msg.taskId)) {
298
311
  self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
299
312
  }
@@ -312,7 +325,7 @@ self.onmessage = async (event) => {
312
325
  })();
313
326
  } else {
314
327
  try {
315
- const result = await __execFn(msg.fnStr, msg.channels);
328
+ const result = await __execFn(msg.fnStr, msg.channels, msg.args);
316
329
  self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
317
330
  } catch (error) {
318
331
  self.postMessage({
@@ -399,17 +412,14 @@ var BunManagedWorker = class {
399
412
  on(event, handler) {
400
413
  if (event === "message") {
401
414
  this.worker.addEventListener("message", (e) => {
402
- ;
403
415
  handler(e.data);
404
416
  });
405
417
  } else if (event === "error") {
406
418
  this.worker.addEventListener("error", (e) => {
407
- ;
408
419
  handler(e.error ?? new Error(e.message));
409
420
  });
410
421
  } else if (event === "exit") {
411
422
  this.worker.addEventListener("close", (e) => {
412
- ;
413
423
  handler(e.code ?? 0);
414
424
  });
415
425
  }
@@ -439,6 +449,8 @@ var ChannelImpl = class {
439
449
  // constraint: can't create channels of nullable type
440
450
  /** @internal */
441
451
  _id;
452
+ /** @internal — true once the channel ID has been sent to a worker */
453
+ _shared = false;
442
454
  buffer = [];
443
455
  capacity;
444
456
  closed = false;
@@ -480,6 +492,7 @@ var ChannelImpl = class {
480
492
  this.buffer.push(sender2.value);
481
493
  sender2.resolve();
482
494
  }
495
+ this.maybeUnregister();
483
496
  return Promise.resolve(value);
484
497
  }
485
498
  const sender = this.sendQueue.shift();
@@ -488,6 +501,7 @@ var ChannelImpl = class {
488
501
  return Promise.resolve(sender.value);
489
502
  }
490
503
  if (this.closed) {
504
+ this.maybeUnregister();
491
505
  return Promise.resolve(null);
492
506
  }
493
507
  return new Promise((resolve) => {
@@ -508,6 +522,11 @@ var ChannelImpl = class {
508
522
  }
509
523
  this.sendQueue = [];
510
524
  }
525
+ maybeUnregister() {
526
+ if (!this._shared && this.closed && this.buffer.length === 0 && this.recvQueue.length === 0) {
527
+ channelRegistry.delete(this._id);
528
+ }
529
+ }
511
530
  sendOnly() {
512
531
  const send = (value) => this.send(value);
513
532
  const close = () => this.close();
@@ -560,7 +579,9 @@ function getChannelById(id) {
560
579
  return channelRegistry.get(id);
561
580
  }
562
581
  function getChannelId(channel) {
563
- return channel._id;
582
+ const impl = channel;
583
+ impl._shared = true;
584
+ return impl._id;
564
585
  }
565
586
 
566
587
  // src/adapters/inline.ts
@@ -572,6 +593,7 @@ var InlineManagedWorker = class {
572
593
  exitHandlers = [];
573
594
  terminated = false;
574
595
  cancelledTasks = /* @__PURE__ */ new Set();
596
+ fnCache = /* @__PURE__ */ new Map();
575
597
  constructor() {
576
598
  this.id = ++inlineIdCounter;
577
599
  queueMicrotask(() => {
@@ -581,7 +603,7 @@ var InlineManagedWorker = class {
581
603
  postMessage(msg) {
582
604
  if (this.terminated) return;
583
605
  if (msg.type === "execute") {
584
- this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels);
606
+ this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels, msg.args);
585
607
  } else if (msg.type === "cancel") {
586
608
  this.cancelledTasks.add(msg.taskId);
587
609
  } else if (msg.type === "channel-result") {
@@ -642,7 +664,7 @@ var InlineManagedWorker = class {
642
664
  }
643
665
  return proxies;
644
666
  }
645
- executeTask(taskId, fnStr, concurrent, channels) {
667
+ executeTask(taskId, fnStr, concurrent, channels, args) {
646
668
  queueMicrotask(async () => {
647
669
  if (this.terminated) return;
648
670
  if (concurrent && this.cancelledTasks.has(taskId)) {
@@ -650,14 +672,20 @@ var InlineManagedWorker = class {
650
672
  return;
651
673
  }
652
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
+ }
653
681
  let result;
654
- if (channels) {
682
+ if (args) {
683
+ result = await parsedFn(...args);
684
+ } else if (channels) {
655
685
  const proxies = this.buildChannelProxies(channels);
656
- const fn = new Function("__ch", "return (" + fnStr + ")(__ch)");
657
- result = await fn(proxies);
686
+ result = await parsedFn(proxies);
658
687
  } else {
659
- const fn = new Function("return (" + fnStr + ")()");
660
- result = await fn();
688
+ result = await parsedFn();
661
689
  }
662
690
  if (concurrent && this.cancelledTasks.has(taskId)) {
663
691
  this.cancelledTasks.delete(taskId);
@@ -710,6 +738,8 @@ var WorkerPool = class {
710
738
  totalCompleted = 0;
711
739
  totalFailed = 0;
712
740
  taskMap = /* @__PURE__ */ new Map();
741
+ // Per-worker deques for work-stealing strategy
742
+ workerDeques = /* @__PURE__ */ new Map();
713
743
  constructor(config, adapter) {
714
744
  this.config = config;
715
745
  this.adapter = adapter;
@@ -747,6 +777,110 @@ var WorkerPool = class {
747
777
  }
748
778
  return void 0;
749
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
+ }
750
884
  // --- Submit ---
751
885
  submit(task2) {
752
886
  if (this.draining) {
@@ -772,6 +906,13 @@ var WorkerPool = class {
772
906
  this.createAndReadyWorker();
773
907
  return;
774
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
+ }
775
916
  this.enqueue(task2);
776
917
  }
777
918
  submitConcurrent(task2) {
@@ -809,6 +950,7 @@ var WorkerPool = class {
809
950
  type: "execute",
810
951
  taskId: task2.id,
811
952
  fnStr: task2.fnStr,
953
+ args: task2.args,
812
954
  concurrent: false,
813
955
  channels: task2.channels
814
956
  };
@@ -830,6 +972,7 @@ var WorkerPool = class {
830
972
  type: "execute",
831
973
  taskId: task2.id,
832
974
  fnStr: task2.fnStr,
975
+ args: task2.args,
833
976
  concurrent: true,
834
977
  channels: task2.channels
835
978
  };
@@ -876,6 +1019,19 @@ var WorkerPool = class {
876
1019
  }
877
1020
  assignNextOrIdle(worker) {
878
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
+ }
879
1035
  const next = this.dequeue();
880
1036
  if (next) {
881
1037
  this.dispatch(worker, next);
@@ -886,6 +1042,7 @@ var WorkerPool = class {
886
1042
  this.dispatchConcurrent(worker, concurrentNext);
887
1043
  return;
888
1044
  }
1045
+ this.cleanupDeque(worker);
889
1046
  this.makeIdle(worker);
890
1047
  }
891
1048
  assignNextConcurrentOrIdle(worker) {
@@ -905,6 +1062,13 @@ var WorkerPool = class {
905
1062
  const exclusiveTask = this.dequeue();
906
1063
  if (exclusiveTask) {
907
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
+ }
908
1072
  } else {
909
1073
  this.makeIdle(worker);
910
1074
  }
@@ -1000,6 +1164,13 @@ var WorkerPool = class {
1000
1164
  removed.reject(new DOMException("Task was cancelled", "AbortError"));
1001
1165
  return;
1002
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
+ }
1003
1174
  const removedConcurrent = this.removeFromConcurrentQueue(taskId);
1004
1175
  if (removedConcurrent) {
1005
1176
  removedConcurrent.reject(new DOMException("Task was cancelled", "AbortError"));
@@ -1010,6 +1181,7 @@ var WorkerPool = class {
1010
1181
  this.exclusiveWorkers.delete(taskId);
1011
1182
  this.allWorkers.delete(exclusiveWorker);
1012
1183
  this.taskMap.delete(taskId);
1184
+ this.flushDeque(exclusiveWorker);
1013
1185
  exclusiveWorker.terminate();
1014
1186
  return;
1015
1187
  }
@@ -1041,6 +1213,14 @@ var WorkerPool = class {
1041
1213
  }
1042
1214
  this.concurrentQueues[priority] = [];
1043
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();
1044
1224
  for (const [taskId] of this.exclusiveWorkers) {
1045
1225
  this.taskMap.delete(taskId);
1046
1226
  }
@@ -1068,7 +1248,7 @@ var WorkerPool = class {
1068
1248
  while (true) {
1069
1249
  const totalWorkers = this.allWorkers.size + this.pendingWorkerCount;
1070
1250
  if (totalWorkers >= maxThreads) break;
1071
- const task2 = this.dequeue() ?? this.dequeueConcurrent();
1251
+ const task2 = this.dequeue() ?? (this.config.strategy === "work-stealing" ? this.stealFromAny() : void 0) ?? this.dequeueConcurrent();
1072
1252
  if (!task2) break;
1073
1253
  this.pendingWorkerCount++;
1074
1254
  this.pendingTasksForWorkers.push(task2);
@@ -1131,6 +1311,7 @@ var WorkerPool = class {
1131
1311
  this.idleWorkers.splice(idleIdx, 1);
1132
1312
  }
1133
1313
  this.rejectExclusiveTaskForWorker(worker, new Error("Worker exited unexpectedly"));
1314
+ this.flushDeque(worker);
1134
1315
  const taskSet = this.sharedWorkers.get(worker);
1135
1316
  if (taskSet) {
1136
1317
  for (const taskId of taskSet) {
@@ -1146,6 +1327,17 @@ var WorkerPool = class {
1146
1327
  for (const taskSet of this.sharedWorkers.values()) {
1147
1328
  concurrentTasks += taskSet.size;
1148
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;
1149
1341
  return {
1150
1342
  totalWorkers: this.allWorkers.size,
1151
1343
  idleWorkers: this.idleWorkers.length,
@@ -1154,10 +1346,10 @@ var WorkerPool = class {
1154
1346
  concurrentTasks,
1155
1347
  pendingWorkers: this.pendingWorkerCount,
1156
1348
  queuedTasks: {
1157
- high: this.queues.high.length,
1158
- normal: this.queues.normal.length,
1159
- low: this.queues.low.length,
1160
- 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
1161
1353
  },
1162
1354
  queuedConcurrentTasks: {
1163
1355
  high: this.concurrentQueues.high.length,
@@ -1212,7 +1404,7 @@ var taskCounter = 0;
1212
1404
  function spawn(fn, opts) {
1213
1405
  const fnStr = serializeFunction(fn);
1214
1406
  const taskId = String(++taskCounter);
1215
- const spawnStack = new Error().stack;
1407
+ const spawnError = new Error();
1216
1408
  let resolveFn;
1217
1409
  let rejectFn;
1218
1410
  let settled = false;
@@ -1242,9 +1434,12 @@ function spawn(fn, opts) {
1242
1434
  reject: (reason) => {
1243
1435
  if (!settled) {
1244
1436
  settled = true;
1245
- if (reason instanceof Error && spawnStack) {
1246
- const callerLine = spawnStack.split("\n").slice(2).join("\n");
1247
- 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
+ }
1248
1443
  }
1249
1444
  rejectFn(reason);
1250
1445
  }
@@ -1383,16 +1578,26 @@ var ErrGroup = class {
1383
1578
  throw new Error("ErrGroup has been cancelled");
1384
1579
  }
1385
1580
  if (this.limit > 0 && this.inFlight >= this.limit) {
1386
- const result2 = new Promise((resolve) => {
1581
+ let innerCancel = () => {
1582
+ };
1583
+ let cancelled = false;
1584
+ const cancel = () => {
1585
+ cancelled = true;
1586
+ innerCancel();
1587
+ };
1588
+ const result = new Promise((resolve) => {
1387
1589
  this.waiting.push(resolve);
1388
- }).then(() => this.doSpawn(fn, opts));
1389
- this.tasks.push({ result: result2, cancel: () => {
1390
- } });
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 });
1391
1597
  return;
1392
1598
  }
1393
- const result = this.doSpawn(fn, opts);
1394
- this.tasks.push({ result, cancel: () => {
1395
- } });
1599
+ const handle = this.doSpawn(fn, opts);
1600
+ this.tasks.push(handle);
1396
1601
  }
1397
1602
  doSpawn(fn, opts) {
1398
1603
  this.inFlight++;
@@ -1410,7 +1615,7 @@ var ErrGroup = class {
1410
1615
  this.cancel();
1411
1616
  }
1412
1617
  });
1413
- return handle.result;
1618
+ return handle;
1414
1619
  }
1415
1620
  async wait() {
1416
1621
  const settled = await Promise.allSettled(this.tasks.map((t) => t.result));
@@ -1547,6 +1752,97 @@ var RWMutex = class {
1547
1752
  }
1548
1753
  };
1549
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
+
1550
1846
  // src/cond.ts
1551
1847
  var Cond = class {
1552
1848
  mu;
@@ -1619,7 +1915,6 @@ function select(cases, opts) {
1619
1915
  if (settled) return;
1620
1916
  settled = true;
1621
1917
  try {
1622
- ;
1623
1918
  handler(value);
1624
1919
  resolve();
1625
1920
  } catch (err) {
@@ -1653,7 +1948,6 @@ function select(cases, opts) {
1653
1948
  if (settled) return;
1654
1949
  settled = true;
1655
1950
  try {
1656
- ;
1657
1951
  handler(value);
1658
1952
  resolve();
1659
1953
  } catch (err) {
@@ -1778,18 +2072,15 @@ var taskCounter2 = 0;
1778
2072
  function task(fn) {
1779
2073
  const fnStr = serializeFunction(fn);
1780
2074
  return (...args) => {
1781
- const serializedArgs = args.map((a) => {
1782
- const json = JSON.stringify(a);
1783
- if (json === void 0) {
2075
+ for (const a of args) {
2076
+ if (JSON.stringify(a) === void 0) {
1784
2077
  throw new TypeError(
1785
2078
  `Argument of type ${typeof a} is not JSON-serializable. task() args must be JSON-serializable (no undefined, functions, symbols, or BigInt).`
1786
2079
  );
1787
2080
  }
1788
- return json;
1789
- });
1790
- const wrapperStr = `() => (${fnStr})(${serializedArgs.join(", ")})`;
2081
+ }
1791
2082
  const taskId = `task_${++taskCounter2}`;
1792
- const spawnStack = new Error().stack;
2083
+ const spawnError = new Error();
1793
2084
  let resolveFn;
1794
2085
  let rejectFn;
1795
2086
  const result = new Promise((resolve, reject) => {
@@ -1798,14 +2089,18 @@ function task(fn) {
1798
2089
  });
1799
2090
  const taskObj = {
1800
2091
  id: taskId,
1801
- fnStr: wrapperStr,
2092
+ fnStr,
2093
+ args,
1802
2094
  priority: "normal",
1803
2095
  concurrent: false,
1804
2096
  resolve: (value) => resolveFn(value),
1805
2097
  reject: (reason) => {
1806
- if (reason instanceof Error && spawnStack) {
1807
- const callerLine = spawnStack.split("\n").slice(2).join("\n");
1808
- 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
+ }
1809
2104
  }
1810
2105
  rejectFn(reason);
1811
2106
  }
@@ -1982,6 +2277,7 @@ function withValue(parent, key, value) {
1982
2277
  Mutex,
1983
2278
  Once,
1984
2279
  RWMutex,
2280
+ Semaphore,
1985
2281
  Ticker,
1986
2282
  Timer,
1987
2283
  WaitGroup,