@dmop/puru 0.1.12 → 0.1.14

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/README.md CHANGED
@@ -58,7 +58,11 @@ const { result } = spawn(() => {
58
58
  return fibonacci(40)
59
59
  })
60
60
 
61
- console.log(await result)
61
+ try {
62
+ console.log(await result)
63
+ } catch (err) {
64
+ console.error(err)
65
+ }
62
66
  ```
63
67
 
64
68
  </td>
package/dist/index.cjs CHANGED
@@ -56,6 +56,10 @@ var NATIVE_CODE_RE = /\[native code\]/;
56
56
  var METHOD_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/;
57
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
58
  var serializeCache = /* @__PURE__ */ new WeakMap();
59
+ var functionRefCache = /* @__PURE__ */ new WeakMap();
60
+ var functionIdCache = /* @__PURE__ */ new Map();
61
+ var FUNCTION_ID_CACHE_MAX = 512;
62
+ var functionIdCounter = 0;
59
63
  function serializeFunction(fn) {
60
64
  if (typeof fn !== "function") {
61
65
  throw new TypeError("Expected a function");
@@ -86,6 +90,25 @@ function serializeFunction(fn) {
86
90
  serializeCache.set(fn, str);
87
91
  return str;
88
92
  }
93
+ function getSerializedFunctionRef(fn) {
94
+ const cached = functionRefCache.get(fn);
95
+ if (cached) return cached;
96
+ const fnStr = serializeFunction(fn);
97
+ let fnId = functionIdCache.get(fnStr);
98
+ if (fnId) {
99
+ functionIdCache.delete(fnStr);
100
+ } else {
101
+ fnId = `fn_${++functionIdCounter}`;
102
+ }
103
+ functionIdCache.set(fnStr, fnId);
104
+ if (functionIdCache.size > FUNCTION_ID_CACHE_MAX) {
105
+ const oldestFnStr = functionIdCache.keys().next().value;
106
+ if (oldestFnStr !== void 0) functionIdCache.delete(oldestFnStr);
107
+ }
108
+ const ref = { fnId, fnStr };
109
+ functionRefCache.set(fn, ref);
110
+ return ref;
111
+ }
89
112
 
90
113
  // src/configure.ts
91
114
  var import_node_os = require("os");
@@ -207,12 +230,15 @@ function __buildChannelProxies(channels) {
207
230
  const __fnCache = new Map();
208
231
  const __FN_CACHE_MAX = 1000;
209
232
 
210
- function __execFn(fnStr, channels, args) {
211
- let parsedFn = __fnCache.get(fnStr);
233
+ function __execFn(fnId, fnStr, channels, args) {
234
+ let parsedFn = __fnCache.get(fnId);
212
235
  if (!parsedFn) {
236
+ if (!fnStr) {
237
+ throw new Error('Worker function was not registered on this worker');
238
+ }
213
239
  parsedFn = (new Function('return (' + fnStr + ')'))();
214
240
  if (__fnCache.size >= __FN_CACHE_MAX) __fnCache.delete(__fnCache.keys().next().value);
215
- __fnCache.set(fnStr, parsedFn);
241
+ __fnCache.set(fnId, parsedFn);
216
242
  }
217
243
  if (args) {
218
244
  return parsedFn(...args);
@@ -246,7 +272,7 @@ parentPort.on('message', async (msg) => {
246
272
  return;
247
273
  }
248
274
  try {
249
- const result = await __execFn(msg.fnStr, msg.channels, msg.args);
275
+ const result = await __execFn(msg.fnId, msg.fnStr, msg.channels, msg.args);
250
276
  if (!cancelledTasks.has(msg.taskId)) {
251
277
  parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
252
278
  }
@@ -265,7 +291,7 @@ parentPort.on('message', async (msg) => {
265
291
  })();
266
292
  } else {
267
293
  try {
268
- const result = await __execFn(msg.fnStr, msg.channels, msg.args);
294
+ const result = await __execFn(msg.fnId, msg.fnStr, msg.channels, msg.args);
269
295
  parentPort.postMessage({ type: 'result', taskId: msg.taskId, value: result });
270
296
  } catch (error) {
271
297
  parentPort.postMessage({
@@ -306,7 +332,7 @@ self.onmessage = async (event) => {
306
332
  return;
307
333
  }
308
334
  try {
309
- const result = await __execFn(msg.fnStr, msg.channels, msg.args);
335
+ const result = await __execFn(msg.fnId, msg.fnStr, msg.channels, msg.args);
310
336
  if (!cancelledTasks.has(msg.taskId)) {
311
337
  self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
312
338
  }
@@ -325,7 +351,7 @@ self.onmessage = async (event) => {
325
351
  })();
326
352
  } else {
327
353
  try {
328
- const result = await __execFn(msg.fnStr, msg.channels, msg.args);
354
+ const result = await __execFn(msg.fnId, msg.fnStr, msg.channels, msg.args);
329
355
  self.postMessage({ type: 'result', taskId: msg.taskId, value: result });
330
356
  } catch (error) {
331
357
  self.postMessage({
@@ -441,8 +467,80 @@ var BunWorkerAdapter = class {
441
467
  }
442
468
  };
443
469
 
470
+ // src/queue.ts
471
+ var RingBuffer = class {
472
+ items;
473
+ head = 0;
474
+ tail = 0;
475
+ size = 0;
476
+ cap;
477
+ constructor(capacity) {
478
+ this.cap = capacity;
479
+ this.items = new Array(capacity);
480
+ }
481
+ get length() {
482
+ return this.size;
483
+ }
484
+ push(value) {
485
+ this.items[this.tail] = value;
486
+ this.tail = (this.tail + 1) % this.cap;
487
+ this.size++;
488
+ }
489
+ shift() {
490
+ if (this.size === 0) return void 0;
491
+ const value = this.items[this.head];
492
+ this.items[this.head] = void 0;
493
+ this.head = (this.head + 1) % this.cap;
494
+ this.size--;
495
+ return value;
496
+ }
497
+ };
498
+ var FifoQueue = class {
499
+ head = null;
500
+ tail = null;
501
+ size = 0;
502
+ get length() {
503
+ return this.size;
504
+ }
505
+ push(value) {
506
+ const node = { value, next: null };
507
+ if (this.tail) {
508
+ this.tail.next = node;
509
+ } else {
510
+ this.head = node;
511
+ }
512
+ this.tail = node;
513
+ this.size++;
514
+ }
515
+ shift() {
516
+ if (!this.head) return void 0;
517
+ const node = this.head;
518
+ this.head = node.next;
519
+ if (!this.head) this.tail = null;
520
+ this.size--;
521
+ const value = node.value;
522
+ node.value = void 0;
523
+ node.next = null;
524
+ return value;
525
+ }
526
+ clear() {
527
+ this.head = null;
528
+ this.tail = null;
529
+ this.size = 0;
530
+ }
531
+ *[Symbol.iterator]() {
532
+ let current = this.head;
533
+ while (current) {
534
+ yield current.value;
535
+ current = current.next;
536
+ }
537
+ }
538
+ };
539
+
444
540
  // src/channel.ts
445
541
  var CLOSED = /* @__PURE__ */ Symbol("puru.channel.closed");
542
+ var RESOLVED_VOID = Promise.resolve();
543
+ var RESOLVED_NULL = Promise.resolve(null);
446
544
  var channelIdCounter = 0;
447
545
  var channelRegistry = /* @__PURE__ */ new Map();
448
546
  var ChannelImpl = class {
@@ -451,14 +549,15 @@ var ChannelImpl = class {
451
549
  _id;
452
550
  /** @internal — true once the channel ID has been sent to a worker */
453
551
  _shared = false;
454
- buffer = [];
552
+ buffer;
455
553
  capacity;
456
554
  closed = false;
457
- recvQueue = [];
458
- sendQueue = [];
555
+ recvQueue = new FifoQueue();
556
+ sendQueue = new FifoQueue();
459
557
  constructor(capacity) {
460
558
  this._id = `__ch_${++channelIdCounter}`;
461
559
  this.capacity = capacity;
560
+ this.buffer = new RingBuffer(capacity);
462
561
  channelRegistry.set(this._id, this);
463
562
  }
464
563
  get len() {
@@ -474,11 +573,11 @@ var ChannelImpl = class {
474
573
  const receiver = this.recvQueue.shift();
475
574
  if (receiver) {
476
575
  receiver.resolve(value);
477
- return Promise.resolve();
576
+ return RESOLVED_VOID;
478
577
  }
479
578
  if (this.buffer.length < this.capacity) {
480
579
  this.buffer.push(value);
481
- return Promise.resolve();
580
+ return RESOLVED_VOID;
482
581
  }
483
582
  return new Promise((resolve, reject) => {
484
583
  this.sendQueue.push({ value, resolve, reject });
@@ -502,7 +601,7 @@ var ChannelImpl = class {
502
601
  }
503
602
  if (this.closed) {
504
603
  this.maybeUnregister();
505
- return Promise.resolve(null);
604
+ return RESOLVED_NULL;
506
605
  }
507
606
  return new Promise((resolve) => {
508
607
  this.recvQueue.push({
@@ -516,11 +615,11 @@ var ChannelImpl = class {
516
615
  for (const receiver of this.recvQueue) {
517
616
  receiver.resolve(CLOSED);
518
617
  }
519
- this.recvQueue = [];
618
+ this.recvQueue.clear();
520
619
  for (const sender of this.sendQueue) {
521
620
  sender.reject(new Error("send on closed channel"));
522
621
  }
523
- this.sendQueue = [];
622
+ this.sendQueue.clear();
524
623
  }
525
624
  maybeUnregister() {
526
625
  if (!this._shared && this.closed && this.buffer.length === 0 && this.recvQueue.length === 0) {
@@ -603,7 +702,7 @@ var InlineManagedWorker = class {
603
702
  postMessage(msg) {
604
703
  if (this.terminated) return;
605
704
  if (msg.type === "execute") {
606
- this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels, msg.args);
705
+ this.executeTask(msg.taskId, msg.fnId, msg.fnStr, msg.concurrent, msg.channels, msg.args);
607
706
  } else if (msg.type === "cancel") {
608
707
  this.cancelledTasks.add(msg.taskId);
609
708
  } else if (msg.type === "channel-result") {
@@ -664,7 +763,7 @@ var InlineManagedWorker = class {
664
763
  }
665
764
  return proxies;
666
765
  }
667
- executeTask(taskId, fnStr, concurrent, channels, args) {
766
+ executeTask(taskId, fnId, fnStr, concurrent, channels, args) {
668
767
  queueMicrotask(async () => {
669
768
  if (this.terminated) return;
670
769
  if (concurrent && this.cancelledTasks.has(taskId)) {
@@ -672,11 +771,14 @@ var InlineManagedWorker = class {
672
771
  return;
673
772
  }
674
773
  try {
675
- let parsedFn = this.fnCache.get(fnStr);
774
+ let parsedFn = this.fnCache.get(fnId);
676
775
  if (!parsedFn) {
776
+ if (!fnStr) {
777
+ throw new Error("Worker function was not registered on this worker");
778
+ }
677
779
  parsedFn = new Function("return (" + fnStr + ")")();
678
780
  if (this.fnCache.size >= 1e3) this.fnCache.clear();
679
- this.fnCache.set(fnStr, parsedFn);
781
+ this.fnCache.set(fnId, parsedFn);
680
782
  }
681
783
  let result;
682
784
  if (args) {
@@ -738,6 +840,7 @@ var WorkerPool = class {
738
840
  totalCompleted = 0;
739
841
  totalFailed = 0;
740
842
  taskMap = /* @__PURE__ */ new Map();
843
+ workerFunctionIds = /* @__PURE__ */ new Map();
741
844
  // Per-worker deques for work-stealing strategy
742
845
  workerDeques = /* @__PURE__ */ new Map();
743
846
  constructor(config, adapter) {
@@ -949,7 +1052,8 @@ var WorkerPool = class {
949
1052
  const msg = {
950
1053
  type: "execute",
951
1054
  taskId: task2.id,
952
- fnStr: task2.fnStr,
1055
+ fnId: task2.fnId,
1056
+ fnStr: this.getWorkerFunctionString(worker, task2),
953
1057
  args: task2.args,
954
1058
  concurrent: false,
955
1059
  channels: task2.channels
@@ -971,13 +1075,24 @@ var WorkerPool = class {
971
1075
  const msg = {
972
1076
  type: "execute",
973
1077
  taskId: task2.id,
974
- fnStr: task2.fnStr,
1078
+ fnId: task2.fnId,
1079
+ fnStr: this.getWorkerFunctionString(worker, task2),
975
1080
  args: task2.args,
976
1081
  concurrent: true,
977
1082
  channels: task2.channels
978
1083
  };
979
1084
  worker.postMessage(msg);
980
1085
  }
1086
+ getWorkerFunctionString(worker, task2) {
1087
+ let knownFunctions = this.workerFunctionIds.get(worker);
1088
+ if (!knownFunctions) {
1089
+ knownFunctions = /* @__PURE__ */ new Set();
1090
+ this.workerFunctionIds.set(worker, knownFunctions);
1091
+ }
1092
+ if (knownFunctions.has(task2.fnId)) return void 0;
1093
+ knownFunctions.add(task2.fnId);
1094
+ return task2.fnStr;
1095
+ }
981
1096
  // --- Task completion ---
982
1097
  handleWorkerMessage(worker, response) {
983
1098
  if (response.type === "channel-op") {
@@ -1242,6 +1357,7 @@ var WorkerPool = class {
1242
1357
  this.sharedWorkers.clear();
1243
1358
  this.allWorkers.clear();
1244
1359
  this.idleTimers.clear();
1360
+ this.workerFunctionIds.clear();
1245
1361
  }
1246
1362
  resize(maxThreads) {
1247
1363
  this.config = { ...this.config, maxThreads };
@@ -1301,6 +1417,7 @@ var WorkerPool = class {
1301
1417
  });
1302
1418
  worker.on("exit", (_code) => {
1303
1419
  this.allWorkers.delete(worker);
1420
+ this.workerFunctionIds.delete(worker);
1304
1421
  const timer = this.idleTimers.get(worker);
1305
1422
  if (timer) {
1306
1423
  clearTimeout(timer);
@@ -1402,7 +1519,7 @@ async function shutdown() {
1402
1519
  // src/spawn.ts
1403
1520
  var taskCounter = 0;
1404
1521
  function spawn(fn, opts) {
1405
- const fnStr = serializeFunction(fn);
1522
+ const { fnId, fnStr } = getSerializedFunctionRef(fn);
1406
1523
  const taskId = String(++taskCounter);
1407
1524
  const spawnError = new Error();
1408
1525
  let resolveFn;
@@ -1421,6 +1538,7 @@ function spawn(fn, opts) {
1421
1538
  }
1422
1539
  const task2 = {
1423
1540
  id: taskId,
1541
+ fnId,
1424
1542
  fnStr,
1425
1543
  priority: opts?.priority ?? "normal",
1426
1544
  concurrent: opts?.concurrent ?? false,
@@ -2070,7 +2188,7 @@ var Timer = class {
2070
2188
  // src/registry.ts
2071
2189
  var taskCounter2 = 0;
2072
2190
  function task(fn) {
2073
- const fnStr = serializeFunction(fn);
2191
+ const { fnId, fnStr } = getSerializedFunctionRef(fn);
2074
2192
  return (...args) => {
2075
2193
  for (const a of args) {
2076
2194
  if (JSON.stringify(a) === void 0) {
@@ -2089,6 +2207,7 @@ function task(fn) {
2089
2207
  });
2090
2208
  const taskObj = {
2091
2209
  id: taskId,
2210
+ fnId,
2092
2211
  fnStr,
2093
2212
  args,
2094
2213
  priority: "normal",