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