@weborigami/async-tree 0.0.39 → 0.0.41

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/main.js CHANGED
@@ -6,6 +6,7 @@ export { default as FunctionTree } from "./src/FunctionTree.js";
6
6
  export { default as MapTree } from "./src/MapTree.js";
7
7
  export { default as ObjectTree } from "./src/ObjectTree.js";
8
8
  // Skip BrowserFileTree.js, which is browser-only.
9
+ export { default as DeepObjectTree } from "./src/DeepObjectTree.js";
9
10
  export { default as SetTree } from "./src/SetTree.js";
10
11
  export { default as SiteTree } from "./src/SiteTree.js";
11
12
  export * as Tree from "./src/Tree.js";
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
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
  "devDependencies": {
10
- "@types/node": "20.11.3",
10
+ "@types/node": "20.11.7",
11
11
  "typescript": "5.3.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "*"
14
+ "@weborigami/types": "0.0.41"
15
15
  },
16
16
  "scripts": {
17
- "test": "node --test",
17
+ "test": "node --test --test-reporter=spec",
18
18
  "typecheck": "tsc"
19
19
  }
20
20
  }
@@ -0,0 +1,27 @@
1
+ import ObjectTree from "./ObjectTree.js";
2
+ import * as Tree from "./Tree.js";
3
+ import { isPlainObject } from "./utilities.js";
4
+
5
+ export default class DeepObjectTree extends ObjectTree {
6
+ async get(key) {
7
+ let value = await super.get(key);
8
+
9
+ const isPlain =
10
+ value instanceof Array ||
11
+ (isPlainObject(value) && !Tree.isAsyncTree(value));
12
+ if (isPlain) {
13
+ value = Reflect.construct(this.constructor, [value]);
14
+ }
15
+
16
+ if (Tree.isAsyncTree(value) && !value.parent) {
17
+ value.parent = this;
18
+ }
19
+
20
+ return value;
21
+ }
22
+
23
+ async isKeyForSubtree(key) {
24
+ const value = this.object[key];
25
+ return isPlainObject(value) || Tree.isAsyncTree(value);
26
+ }
27
+ }
@@ -23,7 +23,8 @@ export default class FunctionTree {
23
23
  async get(key) {
24
24
  const value =
25
25
  this.fn.length <= 1
26
- ? // Function takes no arguments or only one argument: invoke
26
+ ? // Function takes no arguments, one argument, or a variable number of
27
+ // arguments: invoke it.
27
28
  await this.fn.call(null, key)
28
29
  : // Bind the key to the first parameter. Subsequent get calls will
29
30
  // eventually bind all parameters until only one remains. At that point,
package/src/MapTree.js CHANGED
@@ -24,10 +24,6 @@ export default class MapTree {
24
24
  async get(key) {
25
25
  let value = this.map.get(key);
26
26
 
27
- if (value instanceof Map) {
28
- value = Reflect.construct(this.constructor, [value]);
29
- }
30
-
31
27
  if (Tree.isAsyncTree(value) && !value.parent) {
32
28
  value.parent = this;
33
29
  }
@@ -37,7 +33,7 @@ export default class MapTree {
37
33
 
38
34
  async isKeyForSubtree(key) {
39
35
  const value = this.map.get(key);
40
- return value instanceof Map || Tree.isAsyncTree(value);
36
+ return Tree.isAsyncTree(value);
41
37
  }
42
38
 
43
39
  async keys() {
package/src/ObjectTree.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as Tree from "./Tree.js";
2
- import { getRealmObjectPrototype, isPlainObject } from "./utilities.js";
2
+ import { getRealmObjectPrototype } from "./utilities.js";
3
3
 
4
4
  /**
5
5
  * A tree defined by a plain object or array.
@@ -37,13 +37,6 @@ export default class ObjectTree {
37
37
 
38
38
  let value = this.object[key];
39
39
 
40
- const isPlain =
41
- value instanceof Array ||
42
- (isPlainObject(value) && !Tree.isAsyncTree(value));
43
- if (isPlain) {
44
- value = Reflect.construct(this.constructor, [value]);
45
- }
46
-
47
40
  if (Tree.isAsyncTree(value) && !value.parent) {
48
41
  value.parent = this;
49
42
  }
@@ -53,7 +46,7 @@ export default class ObjectTree {
53
46
 
54
47
  async isKeyForSubtree(key) {
55
48
  const value = this.object[key];
56
- return isPlainObject(value) || Tree.isAsyncTree(value);
49
+ return Tree.isAsyncTree(value);
57
50
  }
58
51
 
59
52
  /**
package/src/SetTree.js CHANGED
@@ -18,10 +18,6 @@ export default class SetTree {
18
18
  async get(key) {
19
19
  let value = this.values[key];
20
20
 
21
- if (value instanceof Set) {
22
- value = Reflect.construct(this.constructor, [value]);
23
- }
24
-
25
21
  if (Tree.isAsyncTree(value) && !value.parent) {
26
22
  value.parent = this;
27
23
  }
@@ -31,7 +27,7 @@ export default class SetTree {
31
27
 
32
28
  async isKeyForSubtree(key) {
33
29
  const value = this.values[key];
34
- return value instanceof Set || Tree.isAsyncTree(value);
30
+ return Tree.isAsyncTree(value);
35
31
  }
36
32
 
37
33
  async keys() {
package/src/Tree.js CHANGED
@@ -18,6 +18,8 @@ import { castArrayLike, isPlainObject } from "./utilities.js";
18
18
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
19
19
  */
20
20
 
21
+ const treeModule = this;
22
+
21
23
  /**
22
24
  * Apply the key/values pairs from the source tree to the target tree.
23
25
  *
@@ -111,9 +113,12 @@ export function from(obj) {
111
113
  } else if (obj instanceof Set) {
112
114
  return new SetTree(obj);
113
115
  } else if (obj && typeof obj === "object" && "unpack" in obj) {
114
- // Invoke unpack and convert the result to a tree.
115
- let result = obj.unpack();
116
- return result instanceof Promise ? new DeferredTree(result) : from(result);
116
+ async function AsyncFunction() {} // Sample async function
117
+ return obj.unpack instanceof AsyncFunction.constructor
118
+ ? // Async unpack: return a deferred tree.
119
+ new DeferredTree(obj.unpack)
120
+ : // Synchronous unpack: cast the result of unpack() to a tree.
121
+ from(obj.unpack());
117
122
  } else if (obj && typeof obj === "object") {
118
123
  // An instance of some class.
119
124
  return new ObjectTree(obj);
@@ -328,6 +333,7 @@ export async function traverse(treelike, ...keys) {
328
333
  * Return the value at the corresponding path of keys. Throw if any interior
329
334
  * step of the path doesn't lead to a result.
330
335
  *
336
+ * @this {AsyncTree|null|undefined}
331
337
  * @param {Treelike} treelike
332
338
  * @param {...any} keys
333
339
  */
@@ -336,11 +342,15 @@ export async function traverseOrThrow(treelike, ...keys) {
336
342
  /** @type {any} */
337
343
  let value = treelike;
338
344
 
339
- // Process each key in turn.
340
- // If the value is ever undefined, short-circuit the traversal.
345
+ // If traversal operation was called with a `this` context, use that as the
346
+ // target for function calls.
347
+ const target = this === treeModule ? undefined : this;
348
+
349
+ // Process all the keys.
341
350
  const remainingKeys = keys.slice();
342
351
  while (remainingKeys.length > 0) {
343
352
  if (value === undefined) {
353
+ // Attempted to traverse an undefined value
344
354
  const keyStrings = keys.map((key) => String(key));
345
355
  throw new TraverseError(
346
356
  `Couldn't traverse the path: ${keyStrings.join("/")}`,
@@ -349,24 +359,40 @@ export async function traverseOrThrow(treelike, ...keys) {
349
359
  );
350
360
  }
351
361
 
352
- // Get the next key.
353
- const key = remainingKeys.shift();
354
-
355
- // An empty string as the last key is a special case.
356
- if (key === "" && remainingKeys.length === 0) {
362
+ // Special case: one key left that's an empty string
363
+ if (remainingKeys.length === 1 && remainingKeys[0] === "") {
357
364
  // Unpack the value if it defines an `unpack` function, otherwise return
358
365
  // the value itself.
359
- value = typeof value.unpack === "function" ? await value.unpack() : value;
360
- continue;
366
+ return typeof value.unpack === "function" ? await value.unpack() : value;
361
367
  }
362
368
 
363
- // Someone is trying to traverse the value, so they mean to treat it as a
364
- // tree. If it's not already a tree, cast it to one.
365
- const tree = from(value);
369
+ // If the value is not a function or async tree already, but can be
370
+ // unpacked, unpack it.
371
+ if (
372
+ !(value instanceof Function) &&
373
+ !isAsyncTree(value) &&
374
+ value.unpack instanceof Function
375
+ ) {
376
+ value = await value.unpack();
377
+ }
366
378
 
367
- // Get the value for the key.
368
- value = await tree.get(key);
379
+ if (value instanceof Function) {
380
+ // Value is a function: call it with the remaining keys.
381
+ const fn = value;
382
+ // We'll take as many keys as the function's length, but at least one.
383
+ let fnKeyCount = Math.max(fn.length, 1);
384
+ const args = remainingKeys.splice(0, fnKeyCount);
385
+ value = await fn.call(target, ...args);
386
+ } else {
387
+ // Value is some other treelike object: cast it to a tree.
388
+ const tree = from(value);
389
+ // Get the next key.
390
+ const key = remainingKeys.shift();
391
+ // Get the value for the key.
392
+ value = await tree.get(key);
393
+ }
369
394
  }
395
+
370
396
  return value;
371
397
  }
372
398
 
@@ -26,6 +26,7 @@ export default function createGroupByTransform(groupKeyFn) {
26
26
  // A single value was returned
27
27
  groups = [groups];
28
28
  }
29
+ groups = Tree.from(groups);
29
30
 
30
31
  // Add the value to each group.
31
32
  for (const groupKey of await groups.keys()) {
@@ -0,0 +1,24 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import DeepObjectTree from "../src/DeepObjectTree.js";
4
+ import * as Tree from "../src/Tree.js";
5
+
6
+ describe("DeepObjectTree", () => {
7
+ test("returns an ObjectTree for value that's a plain sub-object or sub-array", async () => {
8
+ const tree = new DeepObjectTree({
9
+ a: 1,
10
+ object: {
11
+ b: 2,
12
+ },
13
+ array: [3],
14
+ });
15
+
16
+ const object = await tree.get("object");
17
+ assert.equal(object instanceof DeepObjectTree, true);
18
+ assert.deepEqual(await Tree.plain(object), { b: 2 });
19
+
20
+ const array = await tree.get("array");
21
+ assert.equal(array instanceof DeepObjectTree, true);
22
+ assert.deepEqual(await Tree.plain(array), [3]);
23
+ });
24
+ });
@@ -76,28 +76,13 @@ describe("ObjectTree", () => {
76
76
  assert.equal(await fixture.get("prop"), "Goodbye");
77
77
  });
78
78
 
79
- test("creates an ObjectTree for subtrees", async () => {
80
- const object = {
81
- a: 1,
82
- more: {
83
- b: 2,
84
- },
85
- };
86
- const fixture = new ObjectTree(object);
87
- const more = await fixture.get("more");
88
- assert.equal(more.constructor, ObjectTree);
89
- const b = await more.get("b");
90
- assert.equal(b, 2);
91
- });
92
-
93
79
  test("sets parent on subtrees", async () => {
94
- const object = {
80
+ const fixture = new ObjectTree({
95
81
  a: 1,
96
- more: {
82
+ more: new ObjectTree({
97
83
  b: 2,
98
- },
99
- };
100
- const fixture = new ObjectTree(object);
84
+ }),
85
+ });
101
86
  const more = await fixture.get("more");
102
87
  assert.equal(more.parent, fixture);
103
88
  });
@@ -105,13 +90,13 @@ describe("ObjectTree", () => {
105
90
  test("isKeyForSubtree() indicates which values are subtrees", async () => {
106
91
  const tree = new ObjectTree({
107
92
  a1: 1,
108
- a2: {
93
+ a2: new ObjectTree({
109
94
  b1: 2,
110
- },
95
+ }),
111
96
  a3: 3,
112
- a4: {
97
+ a4: new ObjectTree({
113
98
  b2: 4,
114
- },
99
+ }),
115
100
  });
116
101
  const keys = Array.from(await tree.keys());
117
102
  const subtrees = await Promise.all(
@@ -120,24 +105,6 @@ describe("ObjectTree", () => {
120
105
  assert.deepEqual(subtrees, [false, true, false, true]);
121
106
  });
122
107
 
123
- test("returns an ObjectTree for value that's a plain sub-object or sub-array", async () => {
124
- const tree = new ObjectTree({
125
- a: 1,
126
- object: {
127
- b: 2,
128
- },
129
- array: [3],
130
- });
131
-
132
- const object = await tree.get("object");
133
- assert.equal(object instanceof ObjectTree, true);
134
- assert.deepEqual(await Tree.plain(object), { b: 2 });
135
-
136
- const array = await tree.get("array");
137
- assert.equal(array instanceof ObjectTree, true);
138
- assert.deepEqual(await Tree.plain(array), [3]);
139
- });
140
-
141
108
  test("returns an async tree value as is", async () => {
142
109
  const subtree = {
143
110
  async get(key) {},
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import ObjectTree from "../src/ObjectTree.js";
3
4
  import SetTree from "../src/SetTree.js";
4
5
 
5
6
  describe("SetTree", () => {
@@ -24,7 +25,7 @@ describe("SetTree", () => {
24
25
 
25
26
  test("sets parent on subtrees", async () => {
26
27
  const set = new Set();
27
- set.add(new Set("a"));
28
+ set.add(new ObjectTree({}));
28
29
  const fixture = new SetTree(set);
29
30
  const subtree = await fixture.get(0);
30
31
  assert.equal(subtree.parent, fixture);
package/test/Tree.test.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import { DeepObjectTree } from "../main.js";
3
4
  import MapTree from "../src/MapTree.js";
4
5
  import ObjectTree from "../src/ObjectTree.js";
5
6
  import * as Tree from "../src/Tree.js";
6
7
 
7
8
  describe("Tree", () => {
8
9
  test("assign applies one tree to another", async () => {
9
- const target = new ObjectTree({
10
+ const target = new DeepObjectTree({
10
11
  a: 1,
11
12
  b: 2,
12
13
  more: {
@@ -14,7 +15,7 @@ describe("Tree", () => {
14
15
  },
15
16
  });
16
17
 
17
- const source = {
18
+ const source = new DeepObjectTree({
18
19
  a: 4, // Overwrite existing value
19
20
  b: undefined, // Delete
20
21
  c: 5, // Add
@@ -26,7 +27,7 @@ describe("Tree", () => {
26
27
  extra: {
27
28
  f: 7,
28
29
  },
29
- };
30
+ });
30
31
 
31
32
  // Apply changes.
32
33
  const result = await Tree.assign(target, source);
@@ -165,7 +166,7 @@ describe("Tree", () => {
165
166
  });
166
167
 
167
168
  test("isKeyForSubtree() returns true if the key is for a subtree", async () => {
168
- const tree = new ObjectTree({
169
+ const tree = new DeepObjectTree({
169
170
  a: 1,
170
171
  more: {
171
172
  b: 2,
@@ -176,12 +177,12 @@ describe("Tree", () => {
176
177
  });
177
178
 
178
179
  test("map() maps values", async () => {
179
- const tree = {
180
+ const tree = new DeepObjectTree({
180
181
  a: "Alice",
181
182
  more: {
182
183
  b: "Bob",
183
184
  },
184
- };
185
+ });
185
186
  const mapped = Tree.map(tree, (value) => value.toUpperCase());
186
187
  assert.deepEqual(await Tree.plain(mapped), {
187
188
  a: "ALICE",
@@ -192,14 +193,14 @@ describe("Tree", () => {
192
193
  });
193
194
 
194
195
  test("mapReduce() can map values and reduce them", async () => {
195
- const tree = {
196
+ const tree = new DeepObjectTree({
196
197
  a: 1,
197
198
  b: 2,
198
199
  more: {
199
200
  c: 3,
200
201
  },
201
202
  d: 4,
202
- };
203
+ });
203
204
  const reduced = await Tree.mapReduce(
204
205
  tree,
205
206
  (value) => value,
@@ -284,6 +285,13 @@ describe("Tree", () => {
284
285
  );
285
286
  });
286
287
 
288
+ test("traverse() a function with fixed number of arguments", async () => {
289
+ const tree = (a, b) => ({
290
+ c: "Result",
291
+ });
292
+ assert.equal(await Tree.traverse(tree, "a", "b", "c"), "Result");
293
+ });
294
+
287
295
  test("traverse() from one tree into another", async () => {
288
296
  const tree = new ObjectTree({
289
297
  a: {
@@ -297,15 +305,13 @@ describe("Tree", () => {
297
305
  });
298
306
 
299
307
  test("traversing a final empty string can unpack the last value", async () => {
300
- class Unpackable {
301
- unpack() {
302
- return "Content";
303
- }
304
- }
305
- const unpackable = new Unpackable();
306
- const tree = new ObjectTree({
307
- unpackable,
308
- });
308
+ const tree = {
309
+ unpackable: {
310
+ unpack() {
311
+ return "Content";
312
+ },
313
+ },
314
+ };
309
315
  const result = await Tree.traverse(tree, "unpackable", "");
310
316
  assert.equal(result, "Content");
311
317
  });
@@ -1,12 +1,13 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import DeepObjectTree from "../../src/DeepObjectTree.js";
3
4
  import * as Tree from "../../src/Tree.js";
4
5
  import mergeDeep from "../../src/operations/mergeDeep.js";
5
6
 
6
7
  describe("mergeDeep", () => {
7
8
  test("can merge deep", async () => {
8
9
  const fixture = mergeDeep(
9
- Tree.from({
10
+ new DeepObjectTree({
10
11
  a: {
11
12
  b: 1,
12
13
  c: {
@@ -14,7 +15,7 @@ describe("mergeDeep", () => {
14
15
  },
15
16
  },
16
17
  }),
17
- Tree.from({
18
+ new DeepObjectTree({
18
19
  a: {
19
20
  b: 0, // Will be obscured by `b` above
20
21
  c: {
@@ -12,9 +12,7 @@ describe("groupBy transform", () => {
12
12
  { name: "Work Sans", tags: ["Grotesque", "Sans Serif"] },
13
13
  ];
14
14
  const tree = Tree.from(fonts);
15
- const grouped = await groupBy((value, key, tree) => value.get("tags"))(
16
- tree
17
- );
15
+ const grouped = await groupBy((value, key, tree) => value.tags)(tree);
18
16
  assert.deepEqual(await Tree.plain(grouped), {
19
17
  Geometric: [{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] }],
20
18
  Grotesque: [{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] }],
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import DeepObjectTree from "../../src/DeepObjectTree.js";
3
4
  import FunctionTree from "../../src/FunctionTree.js";
4
5
  import ObjectTree from "../../src/ObjectTree.js";
5
6
  import * as Tree from "../../src/Tree.js";
@@ -125,7 +126,7 @@ describe("map", () => {
125
126
  });
126
127
 
127
128
  test("deep maps values", async () => {
128
- const tree = new ObjectTree({
129
+ const tree = new DeepObjectTree({
129
130
  a: "letter a",
130
131
  more: {
131
132
  b: "letter b",
@@ -145,7 +146,7 @@ describe("map", () => {
145
146
  });
146
147
 
147
148
  test("deep maps leaf keys", async () => {
148
- const tree = new ObjectTree({
149
+ const tree = new DeepObjectTree({
149
150
  a: "letter a",
150
151
  more: {
151
152
  b: "letter b",
@@ -166,7 +167,7 @@ describe("map", () => {
166
167
  });
167
168
 
168
169
  test("deep maps leaf keys and values", async () => {
169
- const tree = new ObjectTree({
170
+ const tree = new DeepObjectTree({
170
171
  a: "letter a",
171
172
  more: {
172
173
  b: "letter b",
@@ -1,12 +1,13 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import DeepObjectTree from "../../src/DeepObjectTree.js";
3
4
  import * as Tree from "../../src/Tree.js";
4
5
  import regExpKeys from "../../src/transforms/regExpKeys.js";
5
6
 
6
7
  describe("regExpKeys", () => {
7
8
  test("matches keys using regular expressions", async () => {
8
9
  const fixture = await regExpKeys(
9
- Tree.from({
10
+ new DeepObjectTree({
10
11
  a: true,
11
12
  "b.*": true,
12
13
  c: {