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

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.3",
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.3"
11
11
  },
12
12
  "devDependencies": {
13
13
  "@types/node": "22.13.13",
package/shared.js CHANGED
@@ -14,6 +14,7 @@ export * as jsonKeys from "./src/jsonKeys.js";
14
14
  export { default as addNextPrevious } from "./src/operations/addNextPrevious.js";
15
15
  export { default as cache } from "./src/operations/cache.js";
16
16
  export { default as cachedKeyFunctions } from "./src/operations/cachedKeyFunctions.js";
17
+ export { default as concat } from "./src/operations/concat.js";
17
18
  export { default as deepMerge } from "./src/operations/deepMerge.js";
18
19
  export { default as deepReverse } from "./src/operations/deepReverse.js";
19
20
  export { default as deepTake } from "./src/operations/deepTake.js";
@@ -33,7 +34,6 @@ export { default as reverse } from "./src/operations/reverse.js";
33
34
  export { default as scope } from "./src/operations/scope.js";
34
35
  export { default as sort } from "./src/operations/sort.js";
35
36
  export { default as take } from "./src/operations/take.js";
36
- export { default as text } from "./src/operations/text.js";
37
37
  export * as symbols from "./src/symbols.js";
38
38
  export * as trailingSlash from "./src/trailingSlash.js";
39
39
  export { default as TraverseError } from "./src/TraverseError.js";
package/src/Tree.d.ts CHANGED
@@ -21,4 +21,4 @@ export function toFunction(tree: Treelike): Function;
21
21
  export function traverse(tree: Treelike, ...keys: any[]): Promise<any>;
22
22
  export function traverseOrThrow(tree: Treelike, ...keys: any[]): Promise<any>;
23
23
  export function traversePath(tree: Treelike, path: string): Promise<any>;
24
- export function values(tree: Treelike): Promise<IterableIterator<any>>;
24
+ export function values(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
package/src/Tree.js CHANGED
@@ -26,6 +26,8 @@ import {
26
26
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
27
27
  */
28
28
 
29
+ const treeModule = this;
30
+
29
31
  /**
30
32
  * Apply the key/values pairs from the source tree to the target tree.
31
33
  *
@@ -360,8 +362,8 @@ export async function remove(tree, key) {
360
362
  */
361
363
  export function root(tree) {
362
364
  let current = from(tree);
363
- while (current.parent || current[symbols.parent]) {
364
- current = current.parent || current[symbols.parent];
365
+ while (current.parent) {
366
+ current = current.parent;
365
367
  }
366
368
  return current;
367
369
  }
@@ -414,7 +416,7 @@ export async function traverseOrThrow(treelike, ...keys) {
414
416
 
415
417
  // If traversal operation was called with a `this` context, use that as the
416
418
  // target for function calls.
417
- const target = this;
419
+ const target = this === treeModule ? undefined : this;
418
420
 
419
421
  // Process all the keys.
420
422
  const remainingKeys = keys.slice();
@@ -471,10 +473,9 @@ export async function traversePath(tree, path) {
471
473
  /**
472
474
  * Return the values in the specific node of the tree.
473
475
  *
474
- * @param {Treelike} treelike
476
+ * @param {AsyncTree} tree
475
477
  */
476
- export async function values(treelike) {
477
- const tree = from(treelike);
478
+ export async function values(tree) {
478
479
  const keys = Array.from(await tree.keys());
479
480
  const promises = keys.map(async (key) => tree.get(key));
480
481
  return Promise.all(promises);
@@ -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,16 +88,11 @@ 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
 
88
- const parent =
89
- key === ".."
90
- ? // Special case: ".." parent is the grandparent (if it exists)
91
- this.parent?.parent
92
- : this;
93
- setParent(value, parent);
95
+ setParent(value, this);
94
96
  return value;
95
97
  }
96
98
 
@@ -0,0 +1,62 @@
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
+ resolve: 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, resolve
43
+ // the next call in the queue and add it to the active pool.
44
+ function next() {
45
+ if (queue.length > 0 && activeCallPool.size < maxConcurrency) {
46
+ const { promise, resolve } = queue.shift();
47
+ activeCallPool.add(promise);
48
+ resolve();
49
+ }
50
+ }
51
+ }
52
+
53
+ // Polyfill Promise.withResolvers until Node LTS supports it
54
+ function withResolvers() {
55
+ let resolve;
56
+ let reject;
57
+ const promise = new Promise((res, rej) => {
58
+ resolve = res;
59
+ reject = rej;
60
+ });
61
+ return { promise, resolve, reject };
62
+ }
@@ -0,0 +1,29 @@
1
+ import { assertIsTreelike, toString } from "../utilities.js";
2
+ import deepValuesIterator from "./deepValuesIterator.js";
3
+
4
+ /**
5
+ * Concatenate the deep text values in a tree.
6
+ *
7
+ * @param {import("../../index.ts").Treelike} treelike
8
+ */
9
+ export default async function concat(treelike) {
10
+ assertIsTreelike(treelike, "concat");
11
+
12
+ const strings = [];
13
+ for await (const value of deepValuesIterator(treelike, { expand: true })) {
14
+ let string;
15
+ if (value === null) {
16
+ string = "null";
17
+ } else if (value === undefined) {
18
+ string = "undefined";
19
+ } else {
20
+ string = toString(value);
21
+ }
22
+ if (value === null || value === undefined) {
23
+ const message = `Warning: Origami template encountered a ${string} value. To locate where this happened, build your project and search your build output for the text "${string}".`;
24
+ console.warn(message);
25
+ }
26
+ strings.push(string);
27
+ }
28
+ return strings.join("");
29
+ }
@@ -1,29 +1,25 @@
1
- import { assertIsTreelike, toString } from "../utilities.js";
2
- import deepValuesIterator from "./deepValuesIterator.js";
1
+ import { Tree } from "../internal.js";
2
+ import { toString } from "../utilities.js";
3
+ import concat from "./concat.js";
3
4
 
4
5
  /**
5
- * Concatenate the deep text values in a tree.
6
+ * A tagged template literal function that concatenate the deep text values in a
7
+ * tree. Any treelike values will be concatenated using `concat`.
6
8
  *
7
- * @param {import("../../index.ts").Treelike} treelike
9
+ * @param {TemplateStringsArray} strings
10
+ * @param {...any} values
8
11
  */
9
- export default async function deepText(treelike) {
10
- assertIsTreelike(treelike, "text");
11
-
12
- const strings = [];
13
- for await (const value of deepValuesIterator(treelike, { expand: true })) {
14
- let string;
15
- if (value === null) {
16
- string = "null";
17
- } else if (value === undefined) {
18
- string = "undefined";
19
- } else {
20
- string = toString(value);
21
- }
22
- if (value === null || value === undefined) {
23
- const message = `Warning: a template encountered a ${string} value. To locate where this happened, build your project and search your build output for the text "${string}".`;
24
- console.warn(message);
25
- }
26
- strings.push(string);
12
+ export default async function deepText(strings, ...values) {
13
+ // Convert all the values to strings
14
+ const valueTexts = await Promise.all(
15
+ values.map((value) =>
16
+ Tree.isTreelike(value) ? concat(value) : toString(value)
17
+ )
18
+ );
19
+ // Splice all the strings together
20
+ let result = strings[0];
21
+ for (let i = 0; i < valueTexts.length; i++) {
22
+ result += valueTexts[i] + strings[i + 1];
27
23
  }
28
- return strings.join("");
24
+ return result;
29
25
  }
@@ -16,17 +16,7 @@ import * as trailingSlash from "../trailingSlash.js";
16
16
  * @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
17
17
  */
18
18
  export default function merge(...sources) {
19
- const trees = sources
20
- .filter((source) => source)
21
- .map((treelike) => Tree.from(treelike));
22
-
23
- if (trees.length === 0) {
24
- throw new TypeError("merge: all trees are null or undefined");
25
- } else if (trees.length === 1) {
26
- // Only one tree, no need to merge
27
- return trees[0];
28
- }
29
-
19
+ const trees = sources.map((treelike) => Tree.from(treelike));
30
20
  return {
31
21
  description: "merge",
32
22
 
@@ -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
+ }
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import FunctionTree from "../../src/drivers/FunctionTree.js";
4
+ import { Tree } from "../../src/internal.js";
5
+ import concat from "../../src/operations/concat.js";
6
+
7
+ describe("concat", () => {
8
+ test("concatenates deep tree values", async () => {
9
+ const tree = Tree.from({
10
+ a: "A",
11
+ b: "B",
12
+ c: "C",
13
+ more: {
14
+ d: "D",
15
+ e: "E",
16
+ },
17
+ });
18
+ const result = await concat.call(null, tree);
19
+ assert.equal(result, "ABCDE");
20
+ });
21
+
22
+ test("concatenates deep tree-like values", async () => {
23
+ const letters = ["a", "b", "c"];
24
+ const specimens = new FunctionTree(
25
+ (letter) => ({
26
+ lowercase: letter,
27
+ uppercase: letter.toUpperCase(),
28
+ }),
29
+ letters
30
+ );
31
+ const result = await concat.call(null, specimens);
32
+ assert.equal(result, "aAbBcC");
33
+ });
34
+ });
@@ -1,34 +1,12 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
- import FunctionTree from "../../src/drivers/FunctionTree.js";
4
- import { Tree } from "../../src/internal.js";
5
3
  import deepText from "../../src/operations/deepText.js";
6
4
 
7
5
  describe("deepText", () => {
8
- test("concatenates deep tree values", async () => {
9
- const tree = Tree.from({
10
- a: "A",
11
- b: "B",
12
- c: "C",
13
- more: {
14
- d: "D",
15
- e: "E",
16
- },
17
- });
18
- const result = await deepText(tree);
19
- assert.equal(result, "ABCDE");
20
- });
21
-
22
- test("concatenates deep tree-like values", async () => {
23
- const letters = ["a", "b", "c"];
24
- const specimens = new FunctionTree(
25
- (letter) => ({
26
- lowercase: letter,
27
- uppercase: letter.toUpperCase(),
28
- }),
29
- letters
30
- );
31
- const result = await deepText(specimens);
32
- assert.equal(result, "aAbBcC");
6
+ test("joins strings and values together", async () => {
7
+ const array = [1, 2, 3];
8
+ const object = { person1: "Alice", person2: "Bob" };
9
+ const result = await deepText`a ${array} b ${object} c`;
10
+ assert.equal(result, "a 123 b AliceBob c");
33
11
  });
34
12
  });
@@ -1,25 +0,0 @@
1
- import { Tree } from "../internal.js";
2
- import { toString } from "../utilities.js";
3
- import deepText from "./deepText.js";
4
-
5
- /**
6
- * A tagged template literal function that concatenate the deep text values in a
7
- * tree. Any treelike values will be concatenated using `deepText`.
8
- *
9
- * @param {TemplateStringsArray} strings
10
- * @param {...any} values
11
- */
12
- export default async function text(strings, ...values) {
13
- // Convert all the values to strings
14
- const valueTexts = await Promise.all(
15
- values.map((value) =>
16
- Tree.isTreelike(value) ? deepText(value) : toString(value)
17
- )
18
- );
19
- // Splice all the strings together
20
- let result = strings[0];
21
- for (let i = 0; i < valueTexts.length; i++) {
22
- result += valueTexts[i] + strings[i + 1];
23
- }
24
- return result;
25
- }
@@ -1,12 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe, test } from "node:test";
3
- import text from "../../src/operations/text.js";
4
-
5
- describe("text template literal function", () => {
6
- test("joins strings and values together", async () => {
7
- const array = [1, 2, 3];
8
- const object = { person1: "Alice", person2: "Bob" };
9
- const result = await text`a ${array} b ${object} c`;
10
- assert.equal(result, "a 123 b AliceBob c");
11
- });
12
- });