@weborigami/async-tree 0.3.3-jse.3 → 0.3.4-jse.4

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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.3.3-jse.3",
3
+ "version": "0.3.4-jse.4",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
7
7
  "browser": "./browser.js",
8
8
  "types": "./index.ts",
9
9
  "dependencies": {
10
- "@weborigami/types": "0.3.3-jse.3"
10
+ "@weborigami/types": "0.3.4-jse.4"
11
11
  },
12
12
  "devDependencies": {
13
13
  "@types/node": "22.13.13",
@@ -11,6 +11,13 @@ import {
11
11
  naturalOrder,
12
12
  setParent,
13
13
  } from "../utilities.js";
14
+ import limitConcurrency from "./limitConcurrency.js";
15
+
16
+ // As of July 2025, Node doesn't provide any way to limit the number of
17
+ // concurrent calls to readFile, so we wrap readFile in a function that
18
+ // arbitrarily limits the number of concurrent calls to it.
19
+ const MAX_CONCURRENT_READS = 256;
20
+ const limitReadFile = limitConcurrency(fs.readFile, MAX_CONCURRENT_READS);
14
21
 
15
22
  /**
16
23
  * A file system tree via the Node file system API.
@@ -81,7 +88,7 @@ export default class FileTree {
81
88
  value = Reflect.construct(this.constructor, [filePath]);
82
89
  } else {
83
90
  // Return file contents as a standard Uint8Array
84
- const buffer = await fs.readFile(filePath);
91
+ const buffer = await limitReadFile(filePath);
85
92
  value = Uint8Array.from(buffer);
86
93
  }
87
94
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Wrap an async function with a function that limits the number of concurrent
3
+ * calls to that function.
4
+ */
5
+ export default function limitConcurrency(fn, maxConcurrency) {
6
+ const queue = [];
7
+ const activeCallPool = new Set();
8
+
9
+ return async function limitedFunction(...args) {
10
+ // Our turn is represented by a promise that can be externally resolved
11
+ const turnWithResolvers = withResolvers();
12
+
13
+ // Construct a promise for the result of the function call
14
+ const resultPromise =
15
+ // Block until its our turn
16
+ turnWithResolvers.promise
17
+ .then(() => fn(...args)) // Call the function and return its result
18
+ .finally(() => {
19
+ // Remove the promise from the active pool
20
+ activeCallPool.delete(resultPromise);
21
+ // Tell the next call in the queue it can proceed
22
+ next();
23
+ });
24
+
25
+ // Join the queue
26
+ queue.push({
27
+ promise: resultPromise,
28
+ unblock: turnWithResolvers.resolve,
29
+ });
30
+
31
+ if (activeCallPool.size >= maxConcurrency) {
32
+ // The pool is full; wait for the next active call to complete. The call
33
+ // will remove its own completed promise from the active pool.
34
+ await Promise.any(activeCallPool);
35
+ } else {
36
+ next();
37
+ }
38
+
39
+ return resultPromise;
40
+ };
41
+
42
+ // If there are calls in the queue and the active pool is not full, start
43
+ // the next call in the queue.
44
+ function next() {
45
+ if (queue.length > 0 && activeCallPool.size < maxConcurrency) {
46
+ const { promise, unblock } = queue.shift();
47
+ activeCallPool.add(promise);
48
+ // Resolve the turn promise to allow the call to proceed
49
+ unblock();
50
+ }
51
+ }
52
+ }
53
+
54
+ // Polyfill Promise.withResolvers until Node LTS supports it
55
+ function withResolvers() {
56
+ let resolve;
57
+ let reject;
58
+ const promise = new Promise((res, rej) => {
59
+ resolve = res;
60
+ reject = rej;
61
+ });
62
+ return { promise, resolve, reject };
63
+ }
package/src/utilities.js CHANGED
@@ -279,17 +279,22 @@ export function setParent(child, parent) {
279
279
  child.parent = parent;
280
280
  }
281
281
  } else if (Object.isExtensible(child) && !child[symbols.parent]) {
282
- // Add parent reference as a symbol to avoid polluting the object. This
283
- // reference will be used if the object is later used as a tree. We set
284
- // `enumerable` to false even thought this makes no practical difference
285
- // (symbols are never enumerated) because it can provide a hint in the
286
- // debugger that the property is for internal use.
287
- Object.defineProperty(child, symbols.parent, {
288
- configurable: true,
289
- enumerable: false,
290
- value: parent,
291
- writable: true,
292
- });
282
+ try {
283
+ // Add parent reference as a symbol to avoid polluting the object. This
284
+ // reference will be used if the object is later used as a tree. We set
285
+ // `enumerable` to false even thought this makes no practical difference
286
+ // (symbols are never enumerated) because it can provide a hint in the
287
+ // debugger that the property is for internal use.
288
+ Object.defineProperty(child, symbols.parent, {
289
+ configurable: true,
290
+ enumerable: false,
291
+ value: parent,
292
+ writable: true,
293
+ });
294
+ } catch (error) {
295
+ // Ignore exceptions. Some esoteric objects don't allow adding properties.
296
+ // We can still treat them as trees, but they won't have a parent.
297
+ }
293
298
  }
294
299
  }
295
300
 
@@ -0,0 +1,41 @@
1
+ import assert from "node:assert";
2
+ import { before, describe, test } from "node:test";
3
+ import limitConcurrency from "../../src/drivers/limitConcurrency.js";
4
+
5
+ describe("limitConcurrency", async () => {
6
+ before(async () => {
7
+ // Confirm our limited functions throws on too many calls
8
+ const fn = createFixture();
9
+ try {
10
+ const array = Array.from({ length: 10 }, (_, index) => index);
11
+ await Promise.all(array.map((index) => fn(index)));
12
+ } catch (/** @type {any} */ error) {
13
+ assert.equal(error.message, "Too many calls");
14
+ }
15
+ });
16
+
17
+ test("limits the number of concurrent calls", async () => {
18
+ const fn = createFixture();
19
+ const limitedFn = limitConcurrency(fn, 3);
20
+ const array = Array.from({ length: 10 }, (_, index) => index);
21
+ const result = await Promise.all(array.map((index) => limitedFn(index)));
22
+ assert.deepEqual(result, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
23
+ });
24
+ });
25
+
26
+ // Return a function that only permits a limited number of concurrent calls and
27
+ // simulates a delay for each request.
28
+ function createFixture() {
29
+ let activeCalls = 0;
30
+ const maxActiveCalls = 3;
31
+
32
+ return async function (n) {
33
+ if (activeCalls >= maxActiveCalls) {
34
+ throw new Error("Too many calls");
35
+ }
36
+ activeCalls++;
37
+ await new Promise((resolve) => setTimeout(resolve, 10));
38
+ activeCalls--;
39
+ return n;
40
+ };
41
+ }