@weborigami/async-tree 0.0.61 → 0.0.63

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/index.ts CHANGED
@@ -29,7 +29,7 @@ export type ReduceFn = (values: any[], keys: any[]) => Promise<any>;
29
29
 
30
30
  export type StringLike = string | HasString;
31
31
 
32
- type NativeTreelike =
32
+ export type NativeTreelike =
33
33
  any[] |
34
34
  AsyncTree |
35
35
  Function |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.61",
3
+ "version": "0.0.63",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,7 +11,7 @@
11
11
  "typescript": "5.5.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.0.61"
14
+ "@weborigami/types": "0.0.63"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
@@ -1,5 +1,10 @@
1
1
  import { Tree } from "./internal.js";
2
- import { hiddenFileNames, isStringLike, naturalOrder } from "./utilities.js";
2
+ import {
3
+ hiddenFileNames,
4
+ isStringLike,
5
+ naturalOrder,
6
+ setParent,
7
+ } from "./utilities.js";
3
8
 
4
9
  const TypedArray = Object.getPrototypeOf(Uint8Array);
5
10
 
@@ -34,7 +39,9 @@ export default class BrowserFileTree {
34
39
  // Try the key as a subfolder name.
35
40
  try {
36
41
  const subfolderHandle = await directory.getDirectoryHandle(key);
37
- return Reflect.construct(this.constructor, [subfolderHandle]);
42
+ const value = Reflect.construct(this.constructor, [subfolderHandle]);
43
+ setParent(value, this);
44
+ return value;
38
45
  } catch (error) {
39
46
  if (
40
47
  !(
@@ -50,7 +57,9 @@ export default class BrowserFileTree {
50
57
  try {
51
58
  const fileHandle = await directory.getFileHandle(key);
52
59
  const file = await fileHandle.getFile();
53
- return file.arrayBuffer();
60
+ const buffer = file.arrayBuffer();
61
+ setParent(buffer, this);
62
+ return buffer;
54
63
  } catch (error) {
55
64
  if (!(error instanceof DOMException && error.name === "NotFoundError")) {
56
65
  throw error;
@@ -12,15 +12,11 @@ export default class DeepObjectTree extends ObjectTree {
12
12
  value = Reflect.construct(this.constructor, [value]);
13
13
  }
14
14
 
15
- if (Tree.isAsyncTree(value) && !value.parent) {
16
- value.parent = this;
17
- }
18
-
19
15
  return value;
20
16
  }
21
17
 
22
18
  async isKeyForSubtree(key) {
23
- const value = this.object[key];
19
+ const value = await this.object[key];
24
20
  return isPlainObject(value) || Tree.isAsyncTree(value);
25
21
  }
26
22
  }
package/src/FileTree.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  isPacked,
9
9
  isPlainObject,
10
10
  naturalOrder,
11
+ setParent,
11
12
  } from "./utilities.js";
12
13
 
13
14
  /**
@@ -65,16 +66,18 @@ export default class FileTree {
65
66
  throw error;
66
67
  }
67
68
 
69
+ let value;
68
70
  if (stats.isDirectory()) {
69
71
  // Return subdirectory as a tree
70
- const subtree = Reflect.construct(this.constructor, [filePath]);
71
- subtree.parent = this;
72
- return subtree;
72
+ value = Reflect.construct(this.constructor, [filePath]);
73
73
  } else {
74
74
  // Return file contents as a standard Uint8Array.
75
75
  const buffer = await fs.readFile(filePath);
76
- return Uint8Array.from(buffer);
76
+ value = Uint8Array.from(buffer);
77
77
  }
78
+
79
+ setParent(value, this);
80
+ return value;
78
81
  }
79
82
 
80
83
  async isKeyForSubtree(key) {
@@ -1,3 +1,5 @@
1
+ import { setParent } from "./utilities.js";
2
+
1
3
  /**
2
4
  * A tree defined by a function and an optional domain.
3
5
  *
@@ -30,7 +32,7 @@ export default class FunctionTree {
30
32
  // eventually bind all parameters until only one remains. At that point,
31
33
  // the above condition will apply and the function will be invoked.
32
34
  Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
33
-
35
+ setParent(value, this);
34
36
  return value;
35
37
  }
36
38
 
package/src/MapTree.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "./internal.js";
2
+ import { setParent } from "./utilities.js";
2
3
 
3
4
  /**
4
5
  * A tree backed by a JavaScript `Map` object.
@@ -22,12 +23,8 @@ export default class MapTree {
22
23
  }
23
24
 
24
25
  async get(key) {
25
- let value = this.map.get(key);
26
-
27
- if (Tree.isAsyncTree(value) && !value.parent) {
28
- value.parent = this;
29
- }
30
-
26
+ const value = this.map.get(key);
27
+ setParent(value, this);
31
28
  return value;
32
29
  }
33
30
 
package/src/ObjectTree.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Tree } from "./internal.js";
2
- import { getRealmObjectPrototype } from "./utilities.js";
2
+ import * as symbols from "./symbols.js";
3
+ import { getRealmObjectPrototype, setParent } from "./utilities.js";
3
4
 
4
5
  /**
5
6
  * A tree defined by a plain object or array.
@@ -15,7 +16,7 @@ export default class ObjectTree {
15
16
  */
16
17
  constructor(object) {
17
18
  this.object = object;
18
- this.parent = null;
19
+ this.parent = object[symbols.parent] ?? null;
19
20
  }
20
21
 
21
22
  /**
@@ -35,11 +36,8 @@ export default class ObjectTree {
35
36
  return undefined;
36
37
  }
37
38
 
38
- let value = this.object[key];
39
-
40
- if (Tree.isAsyncTree(value) && !value.parent) {
41
- value.parent = this;
42
- }
39
+ let value = await this.object[key];
40
+ setParent(value, this);
43
41
 
44
42
  if (typeof value === "function" && !Object.hasOwn(this.object, key)) {
45
43
  // Value is an inherited method; bind it to the object.
@@ -50,7 +48,7 @@ export default class ObjectTree {
50
48
  }
51
49
 
52
50
  async isKeyForSubtree(key) {
53
- const value = this.object[key];
51
+ const value = await this.object[key];
54
52
  return Tree.isAsyncTree(value);
55
53
  }
56
54
 
package/src/SetTree.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "./internal.js";
2
+ import { setParent } from "./utilities.js";
2
3
 
3
4
  /**
4
5
  * A tree of Set objects.
@@ -16,12 +17,8 @@ export default class SetTree {
16
17
  }
17
18
 
18
19
  async get(key) {
19
- let value = this.values[key];
20
-
21
- if (Tree.isAsyncTree(value) && !value.parent) {
22
- value.parent = this;
23
- }
24
-
20
+ const value = this.values[key];
21
+ setParent(value, this);
25
22
  return value;
26
23
  }
27
24
 
package/src/SiteTree.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Tree } from "./internal.js";
2
2
  import * as keysJson from "./keysJson.js";
3
+ import { setParent } from "./utilities.js";
3
4
 
4
5
  /**
5
6
  * A tree of values obtained via HTTP/HTTPS calls. These values will be strings
@@ -44,7 +45,9 @@ export default class SiteTree {
44
45
  // If the (possibly adjusted) route ends with a slash and the site is an
45
46
  // explorable site, we return a tree for the indicated route.
46
47
  if (href.endsWith("/") && (await this.hasKeysJson())) {
47
- return Reflect.construct(this.constructor, [href]);
48
+ const value = Reflect.construct(this.constructor, [href]);
49
+ setParent(value, this);
50
+ return value;
48
51
  }
49
52
 
50
53
  // Fetch the data at the given route.
@@ -67,9 +70,13 @@ export default class SiteTree {
67
70
  }
68
71
 
69
72
  const mediaType = response.headers?.get("Content-Type");
70
- return SiteTree.mediaTypeIsText(mediaType)
71
- ? response.text()
72
- : response.arrayBuffer();
73
+ if (SiteTree.mediaTypeIsText(mediaType)) {
74
+ return response.text();
75
+ } else {
76
+ const buffer = response.arrayBuffer();
77
+ setParent(buffer, this);
78
+ return buffer;
79
+ }
73
80
  }
74
81
 
75
82
  async getKeyDictionary() {
package/src/Tree.d.ts CHANGED
@@ -5,7 +5,7 @@ export function assign(target: Treelike, source: Treelike): Promise<AsyncTree>;
5
5
  export function clear(AsyncTree: AsyncMutableTree): Promise<void>;
6
6
  export function entries(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
7
7
  export function forEach(AsyncTree: AsyncTree, callbackfn: (value: any, key: any) => Promise<void>): Promise<void>;
8
- export function from(obj: any): AsyncTree;
8
+ export function from(obj: any, options?: { deep: boolean }): AsyncTree;
9
9
  export function has(AsyncTree: AsyncTree, key: any): Promise<boolean>;
10
10
  export function isAsyncMutableTree(obj: any): obj is AsyncMutableTree;
11
11
  export function isAsyncTree(obj: any): obj is AsyncTree;
@@ -13,7 +13,8 @@ export function isKeyForSubtree(tree: AsyncTree, obj: any): Promise<boolean>;
13
13
  export function isTraversable(obj: any): boolean;
14
14
  export function isTreelike(obj: any): obj is Treelike;
15
15
  export function map(tree: Treelike, valueFn: ValueKeyFn): AsyncTree;
16
- export function mapReduce(tree: Treelike, mapFn: ValueKeyFn|null, reduceFn: ReduceFn): Promise<any>;
16
+ export function mapReduce(tree: Treelike, mapFn: ValueKeyFn | null, reduceFn: ReduceFn): Promise<any>;
17
+ export function paths(tree: Treelike, base: string): string[];
17
18
  export function plain(tree: Treelike): Promise<PlainObject>;
18
19
  export function remove(AsyncTree: AsyncMutableTree, key: any): Promise<boolean>;
19
20
  export function toFunction(tree: Treelike): Function;
package/src/Tree.js CHANGED
@@ -103,32 +103,45 @@ export async function forEach(tree, callbackFn) {
103
103
  /**
104
104
  * Attempts to cast the indicated object to an async tree.
105
105
  *
106
- * @param {Treelike | Object} obj
106
+ * If the object is a plain object, it will be converted to an ObjectTree. The
107
+ * optional `deep` option can be set to `true` to convert a plain object to a
108
+ * DeepObjectTree.
109
+ *
110
+ * @param {Treelike | Object} object
111
+ * @param {{ deep?: true }} [options]
107
112
  * @returns {AsyncTree}
108
113
  */
109
- export function from(obj) {
110
- if (isAsyncTree(obj)) {
114
+ export function from(object, options = {}) {
115
+ if (isAsyncTree(object)) {
111
116
  // Argument already supports the tree interface.
112
117
  // @ts-ignore
113
- return obj;
114
- } else if (typeof obj === "function") {
115
- return new FunctionTree(obj);
116
- } else if (obj instanceof Map) {
117
- return new MapTree(obj);
118
- } else if (obj instanceof Set) {
119
- return new SetTree(obj);
120
- } else if (isPlainObject(obj)) {
121
- return new DeepObjectTree(obj);
122
- } else if (isUnpackable(obj)) {
118
+ return object;
119
+ } else if (typeof object === "function") {
120
+ return new FunctionTree(object);
121
+ } else if (object instanceof Map) {
122
+ return new MapTree(object);
123
+ } else if (object instanceof Set) {
124
+ return new SetTree(object);
125
+ } else if (isPlainObject(object) || object instanceof Array) {
126
+ return options.deep ? new DeepObjectTree(object) : new ObjectTree(object);
127
+ } else if (isUnpackable(object)) {
123
128
  async function AsyncFunction() {} // Sample async function
124
- return obj.unpack instanceof AsyncFunction.constructor
129
+ return object.unpack instanceof AsyncFunction.constructor
125
130
  ? // Async unpack: return a deferred tree.
126
- new DeferredTree(obj.unpack)
131
+ new DeferredTree(object.unpack)
127
132
  : // Synchronous unpack: cast the result of unpack() to a tree.
128
- from(obj.unpack());
129
- } else if (obj && typeof obj === "object") {
133
+ from(object.unpack());
134
+ } else if (object && typeof object === "object") {
130
135
  // An instance of some class.
131
- return new ObjectTree(obj);
136
+ return new ObjectTree(object);
137
+ } else if (
138
+ typeof object === "string" ||
139
+ typeof object === "number" ||
140
+ typeof object === "boolean"
141
+ ) {
142
+ // A primitive value; box it into an object and construct a tree.
143
+ const boxed = utilities.box(object);
144
+ return new ObjectTree(boxed);
132
145
  }
133
146
 
134
147
  throw new TypeError("Couldn't convert argument to an async tree");
@@ -149,25 +162,23 @@ export async function has(tree, key) {
149
162
  /**
150
163
  * Return true if the indicated object is an async tree.
151
164
  *
152
- * @param {any} object
165
+ * @param {any} obj
153
166
  * @returns {obj is AsyncTree}
154
167
  */
155
- export function isAsyncTree(object) {
156
- return (
157
- object &&
158
- typeof object.get === "function" &&
159
- typeof object.keys === "function"
160
- );
168
+ export function isAsyncTree(obj) {
169
+ return obj && typeof obj.get === "function" && typeof obj.keys === "function";
161
170
  }
162
171
 
163
172
  /**
164
173
  * Return true if the indicated object is an async mutable tree.
165
174
  *
166
- * @param {any} object
175
+ * @param {any} obj
167
176
  * @returns {obj is AsyncMutableTree}
168
177
  */
169
- export function isAsyncMutableTree(object) {
170
- return isAsyncTree(object) && typeof object.set === "function";
178
+ export function isAsyncMutableTree(obj) {
179
+ return (
180
+ isAsyncTree(obj) && typeof (/** @type {any} */ (obj).set) === "function"
181
+ );
171
182
  }
172
183
 
173
184
  /**
@@ -214,16 +225,16 @@ export function isTraversable(object) {
214
225
  * Note: the `from()` method accepts any JavaScript object, but `isTreelike`
215
226
  * returns `false` for an object that isn't one of the above types.
216
227
  *
217
- * @param {any} object
228
+ * @param {any} obj
218
229
  * @returns {obj is Treelike}
219
230
  */
220
- export function isTreelike(object) {
231
+ export function isTreelike(obj) {
221
232
  return (
222
- isAsyncTree(object) ||
223
- object instanceof Function ||
224
- object instanceof Array ||
225
- object instanceof Set ||
226
- isPlainObject(object)
233
+ isAsyncTree(obj) ||
234
+ obj instanceof Function ||
235
+ obj instanceof Array ||
236
+ obj instanceof Set ||
237
+ isPlainObject(obj)
227
238
  );
228
239
  }
229
240
 
@@ -278,6 +289,27 @@ export async function mapReduce(treelike, valueFn, reduceFn) {
278
289
  return reduceFn(values, keys);
279
290
  }
280
291
 
292
+ /**
293
+ * Returns slash-separated paths for all values in the tree.
294
+ *
295
+ * @param {Treelike} treelike
296
+ * @param {string} base
297
+ */
298
+ export async function paths(treelike, base = "") {
299
+ const tree = from(treelike);
300
+ const result = [];
301
+ for (const key of await tree.keys()) {
302
+ const valuePath = base ? `${base}/${key}` : key;
303
+ const value = await tree.get(key);
304
+ if (await isAsyncTree(value)) {
305
+ const subPaths = await paths(value, valuePath);
306
+ result.push(...subPaths);
307
+ } else {
308
+ result.push(valuePath);
309
+ }
310
+ }
311
+ return result;
312
+ }
281
313
  /**
282
314
  * Converts an asynchronous tree into a synchronous plain JavaScript object.
283
315
  *
@@ -7,7 +7,7 @@ import deepValuesIterator from "./deepValuesIterator.js";
7
7
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
8
  *
9
9
  * @this {AsyncTree|null}
10
- * @param {import("@weborigami/async-tree").Treelike} treelike
10
+ * @param {import("../../index.ts").Treelike} treelike
11
11
  */
12
12
  export default async function concatTreeValues(treelike) {
13
13
  const strings = [];
@@ -8,7 +8,7 @@ import { Tree } from "../internal.js";
8
8
  * @returns {AsyncTree & { description: string }}
9
9
  */
10
10
  export default function deepMerge(...sources) {
11
- let trees = sources.map((treelike) => Tree.from(treelike));
11
+ let trees = sources.map((treelike) => Tree.from(treelike, { deep: true }));
12
12
  let mergeParent;
13
13
  return {
14
14
  description: "deepMerge",
@@ -11,7 +11,7 @@ export default function deepTakeFn(count) {
11
11
  * @param {import("../../index.ts").Treelike} treelike
12
12
  */
13
13
  return async function deepTakeFn(treelike) {
14
- const tree = await Tree.from(treelike);
14
+ const tree = await Tree.from(treelike, { deep: true });
15
15
  const { values } = await traverse(tree, count);
16
16
  return Tree.from(values);
17
17
  };
@@ -14,7 +14,7 @@ export default async function* deepValuesIterator(
14
14
  treelike,
15
15
  options = { expand: false }
16
16
  ) {
17
- const tree = Tree.from(treelike);
17
+ const tree = Tree.from(treelike, { deep: true });
18
18
  for (const key of await tree.keys()) {
19
19
  let value = await tree.get(key);
20
20
 
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import * as symbols from "../symbols.js";
2
3
 
3
4
  /**
4
5
  * Return a tree that performs a shallow merge of the given trees.
@@ -14,18 +15,24 @@ import { Tree } from "../internal.js";
14
15
  * @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
15
16
  */
16
17
  export default function merge(...sources) {
18
+ const trees = sources.map((treelike) => Tree.from(treelike));
17
19
  return {
18
20
  description: "merge",
19
21
 
20
22
  async get(key) {
21
23
  // Check trees for the indicated key in reverse order.
22
- for (let index = this.trees.length - 1; index >= 0; index--) {
23
- const tree = this.trees[index];
24
+ for (let index = trees.length - 1; index >= 0; index--) {
25
+ const tree = trees[index];
24
26
  const value = await tree.get(key);
25
27
  if (value !== undefined) {
28
+ // Merged tree acts as parent instead of the source tree.
26
29
  if (Tree.isAsyncTree(value) && value.parent === tree) {
27
- // Merged tree acts as parent instead of the source tree.
28
30
  value.parent = this;
31
+ } else if (
32
+ typeof value === "object" &&
33
+ value?.[symbols.parent] === tree
34
+ ) {
35
+ value[symbols.parent] = this;
29
36
  }
30
37
  return value;
31
38
  }
@@ -35,8 +42,8 @@ export default function merge(...sources) {
35
42
 
36
43
  async isKeyForSubtree(key) {
37
44
  // Check trees for the indicated key in reverse order.
38
- for (let index = this.trees.length - 1; index >= 0; index--) {
39
- if (await Tree.isKeyForSubtree(this.trees[index], key)) {
45
+ for (let index = trees.length - 1; index >= 0; index--) {
46
+ if (await Tree.isKeyForSubtree(trees[index], key)) {
40
47
  return true;
41
48
  }
42
49
  }
@@ -46,7 +53,7 @@ export default function merge(...sources) {
46
53
  async keys() {
47
54
  const keys = new Set();
48
55
  // Collect keys in the order the trees were provided.
49
- for (const tree of this.trees) {
56
+ for (const tree of trees) {
50
57
  for (const key of await tree.keys()) {
51
58
  keys.add(key);
52
59
  }
@@ -54,6 +61,8 @@ export default function merge(...sources) {
54
61
  return keys;
55
62
  },
56
63
 
57
- trees: sources.map((treelike) => Tree.from(treelike)),
64
+ get trees() {
65
+ return trees;
66
+ },
58
67
  };
59
68
  }
@@ -1,11 +1,11 @@
1
- import { Tree } from "@weborigami/async-tree";
1
+ import { Tree } from "../internal.js";
2
2
 
3
3
  /**
4
4
  * A tree's "scope" is the collection of everything in that tree and all of its
5
5
  * ancestors.
6
6
  *
7
7
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
- * @typedef {import("@weborigami/async-tree").Treelike} Treelike
8
+ * @typedef {import("../../index.ts").Treelike} Treelike
9
9
  *
10
10
  * @param {Treelike} treelike
11
11
  * @returns {AsyncTree & {trees: AsyncTree[]}}
@@ -1,5 +1,4 @@
1
- import { ObjectTree, Tree } from "../internal.js";
2
- import { isPlainObject } from "../utilities.js";
1
+ import { Tree } from "../internal.js";
3
2
 
4
3
  /**
5
4
  * Return a transform function that maps the keys and/or values of a tree.
@@ -47,10 +46,7 @@ export default function createMapTransform(options = {}) {
47
46
  * @type {import("../../index.ts").TreeTransform}
48
47
  */
49
48
  return function map(treelike) {
50
- const tree =
51
- !deep && isPlainObject(treelike) && !Tree.isAsyncTree(treelike)
52
- ? new ObjectTree(treelike)
53
- : Tree.from(treelike);
49
+ const tree = Tree.from(treelike, { deep });
54
50
 
55
51
  // The transformed tree is actually an extension of the original tree's
56
52
  // prototype chain. This allows the transformed tree to inherit any
@@ -50,16 +50,14 @@ export default function sortFn(options) {
50
50
  const defaultCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
51
51
  const originalCompare = compare ?? defaultCompare;
52
52
  // Sort by the sort key.
53
- const sortedTuples = tuples.toSorted((a, b) =>
54
- originalCompare(a.sort, b.sort)
55
- );
53
+ tuples.sort((a, b) => originalCompare(a.sort, b.sort));
56
54
  // Map back to the original keys.
57
- const sorted = sortedTuples.map((pair) => pair.key);
55
+ const sorted = tuples.map((pair) => pair.key);
58
56
  return sorted;
59
57
  } else {
60
58
  // Use original keys as sort keys.
61
59
  // If compare is undefined, this uses default sort order.
62
- return keys.toSorted(compare);
60
+ return keys.slice().sort(compare);
63
61
  }
64
62
  };
65
63
  return transformed;
@@ -1,14 +1,17 @@
1
+ import { AsyncTree } from "@weborigami/types";
1
2
  import { Packed, PlainObject, StringLike } from "../index.ts";
2
3
 
4
+ export function box(value: any): any;
3
5
  export function castArrayLike(object: any): any;
4
6
  export function getRealmObjectPrototype(object: any): any;
5
7
  export const hiddenFileNames: string[];
6
- export function isPacked(object: any): object is Packed;
7
- export function isPlainObject(object: any): object is PlainObject;
8
- export function isUnpackable(object): object is { unpack: () => any };
9
- export function isStringLike(object: any): object is StringLike;
8
+ export function isPacked(obj: any): obj is Packed;
9
+ export function isPlainObject(obj: any): obj is PlainObject;
10
+ export function isStringLike(obj: any): obj is StringLike;
11
+ export function isUnpackable(obj): obj is { unpack: () => any };
10
12
  export function keysFromPath(path: string): string[];
11
13
  export const naturalOrder: (a: string, b: string) => number;
12
14
  export function pipeline(start: any, ...functions: Function[]): Promise<any>;
15
+ export function setParent(child: any, parent: AsyncTree): void;
13
16
  export function toPlainValue(object: any): Promise<any>;
14
- export function toString(object: any): string;
17
+ export function toString(object: any): string;
package/src/utilities.js CHANGED
@@ -1,8 +1,30 @@
1
1
  import { Tree } from "./internal.js";
2
+ import * as symbols from "./symbols.js";
2
3
 
3
4
  const textDecoder = new TextDecoder();
4
5
  const TypedArray = Object.getPrototypeOf(Uint8Array);
5
6
 
7
+ /**
8
+ * Return the value as an object. If the value is already an object it will be
9
+ * returned as is. If the value is a primitive, it will be wrapped in an object:
10
+ * a string will be wrapped in a String object, a number will be wrapped in a
11
+ * Number object, and a boolean will be wrapped in a Boolean object.
12
+ *
13
+ * @param {any} value
14
+ */
15
+ export function box(value) {
16
+ switch (typeof value) {
17
+ case "string":
18
+ return new String(value);
19
+ case "number":
20
+ return new Number(value);
21
+ case "boolean":
22
+ return new Boolean(value);
23
+ default:
24
+ return value;
25
+ }
26
+ }
27
+
6
28
  /**
7
29
  * If the given plain object has only sequential integer keys, return it as an
8
30
  * array. Otherwise return it as is.
@@ -51,16 +73,16 @@ export const hiddenFileNames = [".DS_Store"];
51
73
  * Return true if the object is in a packed form (or can be readily packed into
52
74
  * a form) that can be given to fs.writeFile or response.write().
53
75
  *
54
- * @param {any} object
55
- * @returns {object is import("../index.ts").Packed}
76
+ * @param {any} obj
77
+ * @returns {obj is import("../index.ts").Packed}
56
78
  */
57
- export function isPacked(object) {
79
+ export function isPacked(obj) {
58
80
  return (
59
- typeof object === "string" ||
60
- object instanceof ArrayBuffer ||
61
- object instanceof ReadableStream ||
62
- object instanceof String ||
63
- object instanceof TypedArray
81
+ typeof obj === "string" ||
82
+ obj instanceof ArrayBuffer ||
83
+ obj instanceof ReadableStream ||
84
+ obj instanceof String ||
85
+ obj instanceof TypedArray
64
86
  );
65
87
  }
66
88
 
@@ -71,23 +93,23 @@ export function isPacked(object) {
71
93
  * This function also considers object-like things with no prototype (like a
72
94
  * `Module`) as plain objects.
73
95
  *
74
- * @param {any} object
75
- * @returns {object is import("../index.ts").PlainObject}
96
+ * @param {any} obj
97
+ * @returns {obj is import("../index.ts").PlainObject}
76
98
  */
77
- export function isPlainObject(object) {
99
+ export function isPlainObject(obj) {
78
100
  // From https://stackoverflow.com/q/51722354/76472
79
- if (typeof object !== "object" || object === null) {
101
+ if (typeof obj !== "object" || obj === null) {
80
102
  return false;
81
103
  }
82
104
 
83
105
  // We treat object-like things with no prototype (like a Module) as plain
84
106
  // objects.
85
- if (Object.getPrototypeOf(object) === null) {
107
+ if (Object.getPrototypeOf(obj) === null) {
86
108
  return true;
87
109
  }
88
110
 
89
111
  // Do we inherit directly from Object in this realm?
90
- return Object.getPrototypeOf(object) === getRealmObjectPrototype(object);
112
+ return Object.getPrototypeOf(obj) === getRealmObjectPrototype(obj);
91
113
  }
92
114
 
93
115
  /**
@@ -108,15 +130,15 @@ export function isPrimitive(value) {
108
130
  * Return true if the object is a string or object with a non-trival `toString`
109
131
  * method.
110
132
  *
111
- * @param {any} object
133
+ * @param {any} obj
112
134
  * @returns {obj is import("../index.ts").StringLike}
113
135
  */
114
- export function isStringLike(object) {
115
- if (typeof object === "string") {
136
+ export function isStringLike(obj) {
137
+ if (typeof obj === "string") {
116
138
  return true;
117
- } else if (object?.toString === undefined) {
139
+ } else if (obj?.toString === undefined) {
118
140
  return false;
119
- } else if (object.toString === getRealmObjectPrototype(object)?.toString) {
141
+ } else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
120
142
  // The stupid Object.prototype.toString implementation always returns
121
143
  // "[object Object]", so if that's the only toString method the object has,
122
144
  // we return false.
@@ -126,10 +148,9 @@ export function isStringLike(object) {
126
148
  }
127
149
  }
128
150
 
129
- export function isUnpackable(object) {
151
+ export function isUnpackable(obj) {
130
152
  return (
131
- isPacked(object) &&
132
- typeof (/** @type {any} */ (object).unpack) === "function"
153
+ isPacked(obj) && typeof (/** @type {any} */ (obj).unpack) === "function"
133
154
  );
134
155
  }
135
156
 
@@ -170,6 +191,35 @@ export async function pipeline(start, ...fns) {
170
191
  return fns.reduce(async (acc, fn) => fn(await acc), start);
171
192
  }
172
193
 
194
+ /**
195
+ * If the child object doesn't have a parent yet, set it to the indicated
196
+ * parent. If the child is an AsyncTree, set the `parent` property. Otherwise,
197
+ * set the `symbols.parent` property.
198
+ *
199
+ * @param {*} child
200
+ * @param {*} parent
201
+ */
202
+ export function setParent(child, parent) {
203
+ if (Tree.isAsyncTree(child)) {
204
+ // Value is a subtree; set its parent to this tree.
205
+ if (!child.parent) {
206
+ child.parent = parent;
207
+ }
208
+ } else if (Object.isExtensible(child) && !child[symbols.parent]) {
209
+ // Add parent reference as a symbol to avoid polluting the object. This
210
+ // reference will be used if the object is later used as a tree. We set
211
+ // `enumerable` to false even thought this makes no practical difference
212
+ // (symbols are never enumerated) because it can provide a hint in the
213
+ // debugger that the property is for internal use.
214
+ Object.defineProperty(child, symbols.parent, {
215
+ configurable: true,
216
+ enumerable: false,
217
+ value: parent,
218
+ writable: true,
219
+ });
220
+ }
221
+ }
222
+
173
223
  /**
174
224
  * Convert the given input to the plainest possible JavaScript value. This
175
225
  * helper is intended for functions that want to accept an argument from the ori
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { ObjectTree, Tree } from "../src/internal.js";
4
+ import * as symbols from "../src/symbols.js";
4
5
 
5
6
  describe("ObjectTree", () => {
6
7
  test("can get the keys of the tree", async () => {
@@ -76,6 +77,14 @@ describe("ObjectTree", () => {
76
77
  assert.equal(await fixture.get("prop"), "Goodbye");
77
78
  });
78
79
 
80
+ test("sets parent symbol on subobjects", async () => {
81
+ const fixture = new ObjectTree({
82
+ sub: {},
83
+ });
84
+ const sub = await fixture.get("sub");
85
+ assert.equal(sub[symbols.parent], fixture);
86
+ });
87
+
79
88
  test("sets parent on subtrees", async () => {
80
89
  const fixture = new ObjectTree({
81
90
  a: 1,
package/test/Tree.test.js CHANGED
@@ -97,6 +97,16 @@ describe("Tree", () => {
97
97
  });
98
98
  });
99
99
 
100
+ test("from returns a deep object tree if deep option is true", async () => {
101
+ const obj = {
102
+ sub: {
103
+ a: 1,
104
+ },
105
+ };
106
+ const tree = Tree.from(obj, { deep: true });
107
+ assert(tree instanceof DeepObjectTree);
108
+ });
109
+
100
110
  test("from() creates a deferred tree if unpack() returns a promise", async () => {
101
111
  const obj = new String();
102
112
  /** @type {any} */ (obj).unpack = async () => ({
@@ -108,6 +118,13 @@ describe("Tree", () => {
108
118
  });
109
119
  });
110
120
 
121
+ test("from() autoboxes primitive values", async () => {
122
+ const tree = Tree.from("Hello, world.");
123
+ const slice = await tree.get("slice");
124
+ const result = await slice(0, 5);
125
+ assert.equal(result, "Hello");
126
+ });
127
+
111
128
  test("has returns true if the key exists", async () => {
112
129
  const fixture = createFixture();
113
130
  assert.equal(await Tree.has(fixture, "Alice.md"), true);
@@ -198,6 +215,18 @@ describe("Tree", () => {
198
215
  assert.deepEqual(reduced, "1234");
199
216
  });
200
217
 
218
+ test("paths returns an array of paths to the values in the tree", async () => {
219
+ const tree = new DeepObjectTree({
220
+ a: 1,
221
+ b: 2,
222
+ c: {
223
+ d: 3,
224
+ e: 4,
225
+ },
226
+ });
227
+ assert.deepEqual(await Tree.paths(tree), ["a", "b", "c/d", "c/e"]);
228
+ });
229
+
201
230
  test("plain() produces a plain object version of a tree", async () => {
202
231
  const original = {
203
232
  a: 1,
@@ -232,6 +261,15 @@ describe("Tree", () => {
232
261
  assert.deepEqual(plain, original);
233
262
  });
234
263
 
264
+ test("plain() awaits async properties", async () => {
265
+ const object = {
266
+ get name() {
267
+ return Promise.resolve("Alice");
268
+ },
269
+ };
270
+ assert.deepEqual(await Tree.plain(object), { name: "Alice" });
271
+ });
272
+
235
273
  test("remove method removes a value", async () => {
236
274
  const fixture = createFixture();
237
275
  await Tree.remove(fixture, "Alice.md");
@@ -19,4 +19,10 @@ describe("deepValues", () => {
19
19
  const values = await deepValues(tree);
20
20
  assert.deepEqual(values, [1, 2, 3, 4]);
21
21
  });
22
+
23
+ test("returns in-order array of values in nested arrays", async () => {
24
+ const tree = [1, [2, 3], 4];
25
+ const values = await deepValues(tree);
26
+ assert.deepEqual(values, [1, 2, 3, 4]);
27
+ });
22
28
  });
@@ -2,6 +2,7 @@ import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { Tree } from "../../src/internal.js";
4
4
  import merge from "../../src/operations/merge.js";
5
+ import * as symbols from "../../src/symbols.js";
5
6
 
6
7
  describe("merge", () => {
7
8
  test("performs a shallow merge", async () => {
@@ -38,7 +39,7 @@ describe("merge", () => {
38
39
  assert.equal(c, undefined);
39
40
 
40
41
  // Parent of a subvalue is the merged tree
41
- const b = await Tree.traverse(fixture, "b");
42
- assert.equal(b.parent, fixture);
42
+ const b = await fixture.get("b");
43
+ assert.equal(b[symbols.parent], fixture);
43
44
  });
44
45
  });
@@ -1,8 +1,31 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import { ObjectTree } from "../src/internal.js";
4
+ import * as symbols from "../src/symbols.js";
3
5
  import * as utilities from "../src/utilities.js";
4
6
 
5
7
  describe("utilities", () => {
8
+ test("box returns a boxed value", () => {
9
+ const string = "string";
10
+ const stringObject = utilities.box(string);
11
+ assert(stringObject instanceof String);
12
+ assert.equal(stringObject, string);
13
+
14
+ const number = 1;
15
+ const numberObject = utilities.box(number);
16
+ assert(numberObject instanceof Number);
17
+ assert.equal(numberObject, number);
18
+
19
+ const boolean = true;
20
+ const booleanObject = utilities.box(boolean);
21
+ assert(booleanObject instanceof Boolean);
22
+ assert.equal(booleanObject, boolean);
23
+
24
+ const object = {};
25
+ const boxedObject = utilities.box(object);
26
+ assert.equal(boxedObject, object);
27
+ });
28
+
6
29
  test("getRealmObjectPrototype returns the object's root prototype", () => {
7
30
  const object = {};
8
31
  const proto = utilities.getRealmObjectPrototype(object);
@@ -36,6 +59,33 @@ describe("utilities", () => {
36
59
  assert.equal(result, 16);
37
60
  });
38
61
 
62
+ test("setParent sets a child's parent", () => {
63
+ const parent = new ObjectTree({});
64
+
65
+ // Set [symbols.parent] on a plain object.
66
+ const object = {};
67
+ utilities.setParent(object, parent);
68
+ assert.equal(object[symbols.parent], parent);
69
+
70
+ // Leave [symbols.parent] alone if it's already set.
71
+ const childWithParent = {
72
+ [symbols.parent]: "parent",
73
+ };
74
+ utilities.setParent(childWithParent, parent);
75
+ assert.equal(childWithParent[symbols.parent], "parent");
76
+
77
+ // Set `parent` on a tree.
78
+ const tree = new ObjectTree({});
79
+ utilities.setParent(tree, parent);
80
+ assert.equal(tree.parent, parent);
81
+
82
+ // Leave `parent` alone if it's already set.
83
+ const treeWithParent = new ObjectTree({});
84
+ treeWithParent.parent = "parent";
85
+ utilities.setParent(treeWithParent, parent);
86
+ assert.equal(treeWithParent.parent, "parent");
87
+ });
88
+
39
89
  test("toPlainValue returns the plainest representation of an object", async () => {
40
90
  class User {
41
91
  constructor(name) {