@dmop/puru 0.1.4 → 0.1.5

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
@@ -30,12 +30,12 @@ __export(index_exports, {
30
30
  configure: () => configure,
31
31
  detectCapability: () => detectCapability,
32
32
  detectRuntime: () => detectRuntime,
33
- register: () => register,
34
33
  resize: () => resize,
35
- run: () => run,
36
34
  select: () => select,
35
+ shutdown: () => shutdown,
37
36
  spawn: () => spawn,
38
37
  stats: () => stats,
38
+ task: () => task,
39
39
  ticker: () => ticker
40
40
  });
41
41
  module.exports = __toCommonJS(index_exports);
@@ -418,9 +418,11 @@ var BunWorkerAdapter = class {
418
418
  };
419
419
 
420
420
  // src/channel.ts
421
+ var CLOSED = /* @__PURE__ */ Symbol("puru.channel.closed");
421
422
  var channelIdCounter = 0;
422
423
  var channelRegistry = /* @__PURE__ */ new Map();
423
424
  var ChannelImpl = class {
425
+ // constraint: can't create channels of nullable type
424
426
  /** @internal */
425
427
  _id;
426
428
  buffer = [];
@@ -469,14 +471,16 @@ var ChannelImpl = class {
469
471
  return Promise.resolve(null);
470
472
  }
471
473
  return new Promise((resolve) => {
472
- this.recvQueue.push({ resolve });
474
+ this.recvQueue.push({
475
+ resolve: (v) => resolve(v === CLOSED ? null : v)
476
+ });
473
477
  });
474
478
  }
475
479
  close() {
476
480
  if (this.closed) return;
477
481
  this.closed = true;
478
482
  for (const receiver of this.recvQueue) {
479
- receiver.resolve(null);
483
+ receiver.resolve(CLOSED);
480
484
  }
481
485
  this.recvQueue = [];
482
486
  for (const sender of this.sendQueue) {
@@ -662,14 +666,14 @@ var WorkerPool = class {
662
666
  this.adapter = adapter;
663
667
  }
664
668
  // --- Queue helpers ---
665
- enqueue(task) {
666
- this.queues[task.priority].push(task);
669
+ enqueue(task2) {
670
+ this.queues[task2.priority].push(task2);
667
671
  }
668
672
  dequeue() {
669
673
  return this.queues.high.shift() ?? this.queues.normal.shift() ?? this.queues.low.shift();
670
674
  }
671
- enqueueConcurrent(task) {
672
- this.concurrentQueues[task.priority].push(task);
675
+ enqueueConcurrent(task2) {
676
+ this.concurrentQueues[task2.priority].push(task2);
673
677
  }
674
678
  dequeueConcurrent() {
675
679
  return this.concurrentQueues.high.shift() ?? this.concurrentQueues.normal.shift() ?? this.concurrentQueues.low.shift();
@@ -695,73 +699,73 @@ var WorkerPool = class {
695
699
  return void 0;
696
700
  }
697
701
  // --- Submit ---
698
- submit(task) {
702
+ submit(task2) {
699
703
  if (this.draining) {
700
- task.reject(new Error("Pool is shutting down"));
704
+ task2.reject(new Error("Pool is shutting down"));
701
705
  return;
702
706
  }
703
- if (task.concurrent) {
704
- this.submitConcurrent(task);
707
+ if (task2.concurrent) {
708
+ this.submitConcurrent(task2);
705
709
  } else {
706
- this.submitExclusive(task);
710
+ this.submitExclusive(task2);
707
711
  }
708
712
  }
709
- submitExclusive(task) {
713
+ submitExclusive(task2) {
710
714
  const worker = this.idleWorkers.pop();
711
715
  if (worker) {
712
- this.dispatch(worker, task);
716
+ this.dispatch(worker, task2);
713
717
  return;
714
718
  }
715
719
  const totalWorkers = this.allWorkers.size + this.pendingWorkerCount;
716
720
  if (totalWorkers < this.config.maxThreads) {
717
721
  this.pendingWorkerCount++;
718
- this.pendingTasksForWorkers.push(task);
722
+ this.pendingTasksForWorkers.push(task2);
719
723
  this.createAndReadyWorker();
720
724
  return;
721
725
  }
722
- this.enqueue(task);
726
+ this.enqueue(task2);
723
727
  }
724
- submitConcurrent(task) {
728
+ submitConcurrent(task2) {
725
729
  for (const [worker, tasks] of this.sharedWorkers) {
726
730
  if (tasks.size < this.config.concurrency) {
727
- this.dispatchConcurrent(worker, task);
731
+ this.dispatchConcurrent(worker, task2);
728
732
  return;
729
733
  }
730
734
  }
731
735
  const idleWorker = this.idleWorkers.pop();
732
736
  if (idleWorker) {
733
- this.dispatchConcurrent(idleWorker, task);
737
+ this.dispatchConcurrent(idleWorker, task2);
734
738
  return;
735
739
  }
736
740
  const totalWorkers = this.allWorkers.size + this.pendingWorkerCount;
737
741
  if (totalWorkers < this.config.maxThreads) {
738
742
  this.pendingWorkerCount++;
739
- this.pendingTasksForWorkers.push(task);
743
+ this.pendingTasksForWorkers.push(task2);
740
744
  this.createAndReadyWorker();
741
745
  return;
742
746
  }
743
- this.enqueueConcurrent(task);
747
+ this.enqueueConcurrent(task2);
744
748
  }
745
749
  // --- Dispatch ---
746
- dispatch(worker, task) {
750
+ dispatch(worker, task2) {
747
751
  const timer = this.idleTimers.get(worker);
748
752
  if (timer) {
749
753
  clearTimeout(timer);
750
754
  this.idleTimers.delete(worker);
751
755
  }
752
756
  worker.ref();
753
- this.exclusiveWorkers.set(task.id, worker);
754
- this.taskMap.set(task.id, task);
757
+ this.exclusiveWorkers.set(task2.id, worker);
758
+ this.taskMap.set(task2.id, task2);
755
759
  const msg = {
756
760
  type: "execute",
757
- taskId: task.id,
758
- fnStr: task.fnStr,
761
+ taskId: task2.id,
762
+ fnStr: task2.fnStr,
759
763
  concurrent: false,
760
- channels: task.channels
764
+ channels: task2.channels
761
765
  };
762
766
  worker.postMessage(msg);
763
767
  }
764
- dispatchConcurrent(worker, task) {
768
+ dispatchConcurrent(worker, task2) {
765
769
  const timer = this.idleTimers.get(worker);
766
770
  if (timer) {
767
771
  clearTimeout(timer);
@@ -771,14 +775,14 @@ var WorkerPool = class {
771
775
  if (!this.sharedWorkers.has(worker)) {
772
776
  this.sharedWorkers.set(worker, /* @__PURE__ */ new Set());
773
777
  }
774
- this.sharedWorkers.get(worker).add(task.id);
775
- this.taskMap.set(task.id, task);
778
+ this.sharedWorkers.get(worker).add(task2.id);
779
+ this.taskMap.set(task2.id, task2);
776
780
  const msg = {
777
781
  type: "execute",
778
- taskId: task.id,
779
- fnStr: task.fnStr,
782
+ taskId: task2.id,
783
+ fnStr: task2.fnStr,
780
784
  concurrent: true,
781
- channels: task.channels
785
+ channels: task2.channels
782
786
  };
783
787
  worker.postMessage(msg);
784
788
  }
@@ -805,6 +809,9 @@ var WorkerPool = class {
805
809
  const taskId = response.taskId;
806
810
  const err = new Error(response.message);
807
811
  if (response.stack) err.stack = response.stack;
812
+ if (response.message.match(/^ReferenceError:/) || response.message.match(/ is not defined$/)) {
813
+ err.message += "\n Hint: functions passed to spawn() cannot access variables from the enclosing scope. Inline all required values directly in the function body, or pass them via the channels option.";
814
+ }
808
815
  this.rejectTask(taskId, err);
809
816
  if (this.exclusiveWorkers.has(taskId)) {
810
817
  this.exclusiveWorkers.delete(taskId);
@@ -913,19 +920,19 @@ var WorkerPool = class {
913
920
  }
914
921
  // --- Task resolution ---
915
922
  resolveTask(taskId, value) {
916
- const task = this.taskMap.get(taskId);
917
- if (task) {
923
+ const task2 = this.taskMap.get(taskId);
924
+ if (task2) {
918
925
  this.taskMap.delete(taskId);
919
926
  this.totalCompleted++;
920
- task.resolve(value);
927
+ task2.resolve(value);
921
928
  }
922
929
  }
923
930
  rejectTask(taskId, reason) {
924
- const task = this.taskMap.get(taskId);
925
- if (task) {
931
+ const task2 = this.taskMap.get(taskId);
932
+ if (task2) {
926
933
  this.taskMap.delete(taskId);
927
934
  this.totalFailed++;
928
- task.reject(reason);
935
+ task2.reject(reason);
929
936
  }
930
937
  }
931
938
  // --- Cancellation ---
@@ -965,14 +972,14 @@ var WorkerPool = class {
965
972
  async drain() {
966
973
  this.draining = true;
967
974
  for (const priority of ["high", "normal", "low"]) {
968
- for (const task of this.queues[priority]) {
969
- task.reject(new Error("Pool is shutting down"));
975
+ for (const task2 of this.queues[priority]) {
976
+ task2.reject(new Error("Pool is shutting down"));
970
977
  }
971
978
  this.queues[priority] = [];
972
979
  }
973
980
  for (const priority of ["high", "normal", "low"]) {
974
- for (const task of this.concurrentQueues[priority]) {
975
- task.reject(new Error("Pool is shutting down"));
981
+ for (const task2 of this.concurrentQueues[priority]) {
982
+ task2.reject(new Error("Pool is shutting down"));
976
983
  }
977
984
  this.concurrentQueues[priority] = [];
978
985
  }
@@ -999,10 +1006,10 @@ var WorkerPool = class {
999
1006
  while (true) {
1000
1007
  const totalWorkers = this.allWorkers.size + this.pendingWorkerCount;
1001
1008
  if (totalWorkers >= maxThreads) break;
1002
- const task = this.dequeue() ?? this.dequeueConcurrent();
1003
- if (!task) break;
1009
+ const task2 = this.dequeue() ?? this.dequeueConcurrent();
1010
+ if (!task2) break;
1004
1011
  this.pendingWorkerCount++;
1005
- this.pendingTasksForWorkers.push(task);
1012
+ this.pendingTasksForWorkers.push(task2);
1006
1013
  this.createAndReadyWorker();
1007
1014
  }
1008
1015
  while (this.allWorkers.size > maxThreads && this.idleWorkers.length > 0) {
@@ -1022,12 +1029,12 @@ var WorkerPool = class {
1022
1029
  const onReady = () => {
1023
1030
  this.pendingWorkerCount--;
1024
1031
  this.allWorkers.add(worker);
1025
- const task = this.pendingTasksForWorkers.shift();
1026
- if (task) {
1027
- if (task.concurrent) {
1028
- this.dispatchConcurrent(worker, task);
1032
+ const task2 = this.pendingTasksForWorkers.shift();
1033
+ if (task2) {
1034
+ if (task2.concurrent) {
1035
+ this.dispatchConcurrent(worker, task2);
1029
1036
  } else {
1030
- this.dispatch(worker, task);
1037
+ this.dispatch(worker, task2);
1031
1038
  }
1032
1039
  } else {
1033
1040
  this.makeIdle(worker);
@@ -1142,6 +1149,12 @@ function stats() {
1142
1149
  function resize(maxThreads) {
1143
1150
  getPool().resize(maxThreads);
1144
1151
  }
1152
+ async function shutdown() {
1153
+ if (poolInstance) {
1154
+ await poolInstance.drain();
1155
+ poolInstance = null;
1156
+ }
1157
+ }
1145
1158
 
1146
1159
  // src/spawn.ts
1147
1160
  var taskCounter = 0;
@@ -1167,7 +1180,7 @@ function spawn(fn, opts) {
1167
1180
  channelMap[name] = impl._id;
1168
1181
  }
1169
1182
  }
1170
- const task = {
1183
+ const task2 = {
1171
1184
  id: taskId,
1172
1185
  fnStr,
1173
1186
  priority: opts?.priority ?? "normal",
@@ -1190,7 +1203,7 @@ function spawn(fn, opts) {
1190
1203
  }
1191
1204
  }
1192
1205
  };
1193
- getPool().submit(task);
1206
+ getPool().submit(task2);
1194
1207
  const cancel = () => {
1195
1208
  if (settled) return;
1196
1209
  settled = true;
@@ -1204,9 +1217,18 @@ function spawn(fn, opts) {
1204
1217
  var WaitGroup = class {
1205
1218
  tasks = [];
1206
1219
  controller = new AbortController();
1220
+ /**
1221
+ * An `AbortSignal` shared across all tasks in this group.
1222
+ * Pass it into spawned functions so they can stop early when `cancel()` is called.
1223
+ */
1207
1224
  get signal() {
1208
1225
  return this.controller.signal;
1209
1226
  }
1227
+ /**
1228
+ * Spawns a function on a worker thread and adds it to the group.
1229
+ *
1230
+ * @throws If the group has already been cancelled.
1231
+ */
1210
1232
  spawn(fn, opts) {
1211
1233
  if (this.controller.signal.aborted) {
1212
1234
  throw new Error("WaitGroup has been cancelled");
@@ -1214,16 +1236,28 @@ var WaitGroup = class {
1214
1236
  const handle = spawn(fn, opts);
1215
1237
  this.tasks.push(handle);
1216
1238
  }
1239
+ /**
1240
+ * Waits for all tasks to complete successfully.
1241
+ * Rejects as soon as any task throws.
1242
+ */
1217
1243
  async wait() {
1218
1244
  return Promise.all(this.tasks.map((t) => t.result));
1219
1245
  }
1246
+ /**
1247
+ * Waits for all tasks to settle (fulfilled or rejected) and returns each outcome.
1248
+ * Never rejects — inspect each `PromiseSettledResult` to handle failures individually.
1249
+ */
1220
1250
  async waitSettled() {
1221
1251
  return Promise.allSettled(this.tasks.map((t) => t.result));
1222
1252
  }
1253
+ /**
1254
+ * Cancels all tasks in the group and signals the shared `AbortSignal`.
1255
+ * Already-settled tasks are unaffected.
1256
+ */
1223
1257
  cancel() {
1224
1258
  this.controller.abort();
1225
- for (const task of this.tasks) {
1226
- task.cancel();
1259
+ for (const task2 of this.tasks) {
1260
+ task2.cancel();
1227
1261
  }
1228
1262
  }
1229
1263
  };
@@ -1263,8 +1297,8 @@ var ErrGroup = class {
1263
1297
  }
1264
1298
  cancel() {
1265
1299
  this.controller.abort();
1266
- for (const task of this.tasks) {
1267
- task.cancel();
1300
+ for (const task2 of this.tasks) {
1301
+ task2.cancel();
1268
1302
  }
1269
1303
  }
1270
1304
  };
@@ -1409,7 +1443,7 @@ var Ticker = class {
1409
1443
  if (this.resolve) {
1410
1444
  const r = this.resolve;
1411
1445
  this.resolve = null;
1412
- r();
1446
+ r(true);
1413
1447
  }
1414
1448
  }, ms);
1415
1449
  if (this.interval.unref) this.interval.unref();
@@ -1417,7 +1451,11 @@ var Ticker = class {
1417
1451
  async tick() {
1418
1452
  if (this.stopped) return false;
1419
1453
  return new Promise((resolve) => {
1420
- this.resolve = () => resolve(true);
1454
+ this.resolve = resolve;
1455
+ if (this.stopped) {
1456
+ this.resolve = null;
1457
+ resolve(false);
1458
+ }
1421
1459
  });
1422
1460
  }
1423
1461
  stop() {
@@ -1429,7 +1467,7 @@ var Ticker = class {
1429
1467
  if (this.resolve) {
1430
1468
  const r = this.resolve;
1431
1469
  this.resolve = null;
1432
- r();
1470
+ r(false);
1433
1471
  }
1434
1472
  }
1435
1473
  async *[Symbol.asyncIterator]() {
@@ -1443,54 +1481,45 @@ function ticker(ms) {
1443
1481
  }
1444
1482
 
1445
1483
  // src/registry.ts
1446
- var registry = /* @__PURE__ */ new Map();
1447
1484
  var taskCounter2 = 0;
1448
- function register(name, fn) {
1449
- if (registry.has(name)) {
1450
- throw new Error(`Task "${name}" is already registered`);
1451
- }
1452
- registry.set(name, fn);
1453
- }
1454
- function run(name, ...args) {
1455
- const fn = registry.get(name);
1456
- if (!fn) {
1457
- throw new Error(`Task "${name}" is not registered. Call register() first.`);
1458
- }
1459
- const fnStr = serializeFunction(fn);
1460
- const serializedArgs = args.map((a) => {
1461
- const json = JSON.stringify(a);
1462
- if (json === void 0) {
1463
- throw new TypeError(
1464
- `Argument of type ${typeof a} is not JSON-serializable. run() args must be JSON-serializable (no undefined, functions, symbols, or BigInt).`
1465
- );
1466
- }
1467
- return json;
1468
- });
1469
- const wrapperStr = `() => (${fnStr})(${serializedArgs.join(", ")})`;
1470
- const taskId = `reg_${++taskCounter2}`;
1471
- const spawnStack = new Error().stack;
1472
- let resolveFn;
1473
- let rejectFn;
1474
- const result = new Promise((resolve, reject) => {
1475
- resolveFn = resolve;
1476
- rejectFn = reject;
1477
- });
1478
- const task = {
1479
- id: taskId,
1480
- fnStr: wrapperStr,
1481
- priority: "normal",
1482
- concurrent: false,
1483
- resolve: (value) => resolveFn(value),
1484
- reject: (reason) => {
1485
- if (reason instanceof Error && spawnStack) {
1486
- const callerLine = spawnStack.split("\n").slice(2).join("\n");
1487
- reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
1485
+ function task(fn) {
1486
+ return (...args) => {
1487
+ const fnStr = serializeFunction(fn);
1488
+ const serializedArgs = args.map((a) => {
1489
+ const json = JSON.stringify(a);
1490
+ if (json === void 0) {
1491
+ throw new TypeError(
1492
+ `Argument of type ${typeof a} is not JSON-serializable. task() args must be JSON-serializable (no undefined, functions, symbols, or BigInt).`
1493
+ );
1488
1494
  }
1489
- rejectFn(reason);
1490
- }
1495
+ return json;
1496
+ });
1497
+ const wrapperStr = `() => (${fnStr})(${serializedArgs.join(", ")})`;
1498
+ const taskId = `task_${++taskCounter2}`;
1499
+ const spawnStack = new Error().stack;
1500
+ let resolveFn;
1501
+ let rejectFn;
1502
+ const result = new Promise((resolve, reject) => {
1503
+ resolveFn = resolve;
1504
+ rejectFn = reject;
1505
+ });
1506
+ const taskObj = {
1507
+ id: taskId,
1508
+ fnStr: wrapperStr,
1509
+ priority: "normal",
1510
+ concurrent: false,
1511
+ resolve: (value) => resolveFn(value),
1512
+ reject: (reason) => {
1513
+ if (reason instanceof Error && spawnStack) {
1514
+ const callerLine = spawnStack.split("\n").slice(2).join("\n");
1515
+ reason.stack = (reason.stack ?? reason.message) + "\n --- spawned at ---\n" + callerLine;
1516
+ }
1517
+ rejectFn(reason);
1518
+ }
1519
+ };
1520
+ getPool().submit(taskObj);
1521
+ return result;
1491
1522
  };
1492
- getPool().submit(task);
1493
- return result;
1494
1523
  }
1495
1524
  // Annotate the CommonJS export names for ESM import in node:
1496
1525
  0 && (module.exports = {
@@ -1504,12 +1533,12 @@ function run(name, ...args) {
1504
1533
  configure,
1505
1534
  detectCapability,
1506
1535
  detectRuntime,
1507
- register,
1508
1536
  resize,
1509
- run,
1510
1537
  select,
1538
+ shutdown,
1511
1539
  spawn,
1512
1540
  stats,
1541
+ task,
1513
1542
  ticker
1514
1543
  });
1515
1544
  //# sourceMappingURL=index.cjs.map