@weborigami/async-tree 0.6.12 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
package/src/Tree.js CHANGED
@@ -8,8 +8,11 @@ export { default as cache } from "./operations/cache.js";
8
8
  export { default as calendar } from "./operations/calendar.js";
9
9
  export { default as child } from "./operations/child.js";
10
10
  export { default as clear } from "./operations/clear.js";
11
+ export { default as concat } from "./operations/concat.js";
11
12
  export { default as constant } from "./operations/constant.js";
13
+ export { default as deepArrays } from "./operations/deepArrays.js";
12
14
  export { default as deepEntries } from "./operations/deepEntries.js";
15
+ export { default as deepEntriesIterator } from "./operations/deepEntriesIterator.js";
13
16
  export { default as deepMap } from "./operations/deepMap.js";
14
17
  export { default as deepMerge } from "./operations/deepMerge.js";
15
18
  export { default as deepReverse } from "./operations/deepReverse.js";
@@ -21,6 +24,7 @@ export { default as delete } from "./operations/delete.js";
21
24
  export { default as entries } from "./operations/entries.js";
22
25
  export { default as filter } from "./operations/filter.js";
23
26
  export { default as first } from "./operations/first.js";
27
+ export { default as flat } from "./operations/flat.js";
24
28
  export { default as forEach } from "./operations/forEach.js";
25
29
  export { default as from } from "./operations/from.js";
26
30
  export { default as globKeys } from "./operations/globKeys.js";
@@ -106,6 +106,49 @@ export default class AsyncMap {
106
106
  throw new Error("get() not implemented");
107
107
  }
108
108
 
109
+ /**
110
+ * Returns the value associated with the key, or defaultValue if there is
111
+ * none. If defaultValue is returned, it is also inserted into the map for the
112
+ * given key. If the `readOnly` property is true, calling this method throws a
113
+ * `TypeError` if the key is not already present in the map.
114
+ */
115
+ async getOrInsert(key, defaultValue) {
116
+ let value = await this.get(key);
117
+ if (value === undefined) {
118
+ if (this.readOnly) {
119
+ throw new TypeError(
120
+ "getOrInsert() can't insert into a new value into a read-only map.",
121
+ );
122
+ }
123
+ await this.set(key, defaultValue);
124
+ value = defaultValue;
125
+ }
126
+ return value;
127
+ }
128
+
129
+ /**
130
+ * Returns the value associated with the key, or the result of calling
131
+ * `defaultValueFn` if there is none. If the `readOnly` property is true,
132
+ * calling this method throws a `TypeError` if the key is not already present
133
+ * in the map.
134
+ *
135
+ * @param {any} key
136
+ * @param {() => any} defaultValueFn
137
+ */
138
+ async getOrInsertComputed(key, defaultValueFn) {
139
+ let value = await this.get(key);
140
+ if (value === undefined) {
141
+ if (this.readOnly) {
142
+ throw new TypeError(
143
+ "getOrInsertComputed() can't insert into a new value into a read-only map.",
144
+ );
145
+ }
146
+ const defaultValue = await defaultValueFn();
147
+ await this.set(key, defaultValue);
148
+ value = defaultValue;
149
+ }
150
+ return value;
151
+ }
109
152
  /**
110
153
  * Groups items from an async iterable into an AsyncMap according to the keys
111
154
  * returned by the given function.
@@ -139,6 +139,50 @@ export default class SyncMap extends Map {
139
139
  return value;
140
140
  }
141
141
 
142
+ /**
143
+ * Returns the value associated with the key, or defaultValue if there is
144
+ * none. If defaultValue is returned, it is also inserted into the map for the
145
+ * given key. If the `readOnly` property is true, calling this method throws a
146
+ * `TypeError` if the key is not already present in the map.
147
+ */
148
+ getOrInsert(key, defaultValue) {
149
+ let value = this.get(key);
150
+ if (value === undefined) {
151
+ if (this.readOnly) {
152
+ throw new TypeError(
153
+ "getOrInsert() can't insert into a new value into a read-only map.",
154
+ );
155
+ }
156
+ this.set(key, defaultValue);
157
+ value = defaultValue;
158
+ }
159
+ return value;
160
+ }
161
+
162
+ /**
163
+ * Returns the value associated with the key, or the result of calling
164
+ * `defaultValueFn` if there is none. If the `readOnly` property is true,
165
+ * calling this method throws a `TypeError` if the key is not already present
166
+ * in the map.
167
+ *
168
+ * @param {any} key
169
+ * @param {() => any} defaultValueFn
170
+ */
171
+ getOrInsertComputed(key, defaultValueFn) {
172
+ let value = this.get(key);
173
+ if (value === undefined) {
174
+ if (this.readOnly) {
175
+ throw new TypeError(
176
+ "getOrInsertComputed() can't insert into a new value into a read-only map.",
177
+ );
178
+ }
179
+ const defaultValue = defaultValueFn();
180
+ this.set(key, defaultValue);
181
+ value = defaultValue;
182
+ }
183
+ return value;
184
+ }
185
+
142
186
  /**
143
187
  * Returns true if the given key appears in the set returned by keys().
144
188
  *
@@ -51,6 +51,8 @@ export default async function addNextPrevious(maplike) {
51
51
  yield* sourceKeys;
52
52
  },
53
53
 
54
+ source,
55
+
54
56
  trailingSlashKeys: /** @type {any} */ (source).trailingSlashKeys,
55
57
  });
56
58
  }
@@ -0,0 +1,59 @@
1
+ import SyncMap from "../drivers/SyncMap.js";
2
+ import * as trailingSlash from "../trailingSlash.js";
3
+ import isUnpackable from "../utilities/isUnpackable.js";
4
+ import entries from "./entries.js";
5
+
6
+ /**
7
+ * Concatenate the given trees. This is similar to a merge, but numeric keys
8
+ * will be renumbered starting with 0 and incrementing by 1.
9
+ *
10
+ * If the final result is array-like, returns an array, otherwise returns a map.
11
+ *
12
+ * @typedef {import("../../index.ts").Maplike} Maplike
13
+ *
14
+ * @param {(Maplike|null)[]} trees
15
+ */
16
+ export default async function concat(...trees) {
17
+ // Filter out null or undefined trees.
18
+ /** @type {Maplike[]}
19
+ * @ts-ignore */
20
+ const filtered = trees.filter((tree) => tree);
21
+
22
+ // Unpack any packed objects.
23
+ const sources = await Promise.all(
24
+ filtered.map((obj) =>
25
+ isUnpackable(obj) ? /** @type {any} */ (obj).unpack() : obj,
26
+ ),
27
+ );
28
+
29
+ if (sources.length === 0) {
30
+ throw new TypeError("Tree.concat: all arguments are null or undefined");
31
+ }
32
+
33
+ let index = 0;
34
+ let onlyNumericKeys = true;
35
+ const map = new SyncMap();
36
+ for (const source of sources) {
37
+ const sourceEntries = await entries(source);
38
+ for (let [key, value] of sourceEntries) {
39
+ key = trailingSlash.remove(key);
40
+ if (typeof key === "number" || /^\d+$/.test(key)) {
41
+ // Numeric key, renumber it.
42
+ key = String(index);
43
+ index++;
44
+ } else {
45
+ // Non-numeric key, keep it as is.
46
+ onlyNumericKeys = false;
47
+ }
48
+ map.set(key, value);
49
+ }
50
+ }
51
+
52
+ if (onlyNumericKeys) {
53
+ // All keys are numeric, return an array.
54
+ return [...map.values()];
55
+ } else {
56
+ // Some keys are non-numeric, return a map.
57
+ return map;
58
+ }
59
+ }
@@ -0,0 +1,23 @@
1
+ import * as args from "../utilities/args.js";
2
+ import entries from "./entries.js";
3
+ import isMap from "./isMap.js";
4
+
5
+ /**
6
+ * Return the deep nested entries in the tree as arrays of [key, value] pairs.
7
+ *
8
+ * @typedef {import("../../index.ts").Maplike} Maplike
9
+ *
10
+ * @param {Maplike} maplike
11
+ */
12
+ export default async function deepEntries(maplike) {
13
+ const tree = await args.map(maplike, "Tree.deepEntries");
14
+
15
+ const treeEntries = await entries(tree);
16
+ const result = await Promise.all(
17
+ treeEntries.map(async ([key, value]) => {
18
+ const resolvedValue = isMap(value) ? await deepEntries(value) : value;
19
+ return [key, resolvedValue];
20
+ }),
21
+ );
22
+ return result;
23
+ }
@@ -1,6 +1,5 @@
1
1
  import * as args from "../utilities/args.js";
2
- import entries from "./entries.js";
3
- import isMap from "./isMap.js";
2
+ import deepEntriesIterator from "./deepEntriesIterator.js";
4
3
 
5
4
  /**
6
5
  * Return the deep nested entries in the tree as arrays of [key, value] pairs.
@@ -12,12 +11,10 @@ import isMap from "./isMap.js";
12
11
  export default async function deepEntries(maplike) {
13
12
  const tree = await args.map(maplike, "Tree.deepEntries");
14
13
 
15
- const treeEntries = await entries(tree);
16
- const result = await Promise.all(
17
- treeEntries.map(async ([key, value]) => {
18
- const resolvedValue = isMap(value) ? await deepEntries(value) : value;
19
- return [key, resolvedValue];
20
- }),
21
- );
22
- return result;
14
+ const iterator = deepEntriesIterator(tree, { depth: Infinity });
15
+ const entries = [];
16
+ for await (const entry of iterator) {
17
+ entries.push(entry);
18
+ }
19
+ return entries;
23
20
  }
@@ -0,0 +1,45 @@
1
+ import * as args from "../utilities/args.js";
2
+ import isUnpackable from "../utilities/isUnpackable.js";
3
+ import isMap from "./isMap.js";
4
+ import isMaplike from "./isMaplike.js";
5
+ /**
6
+ * Return an iterator that yields all entries in a tree, including nested trees.
7
+ *
8
+ * If the `expand` option is true, maplike values (but not functions) will be
9
+ * expanded into nested trees and their values will be yielded. Packed values
10
+ * will be unpacked before expanding.
11
+ *
12
+ * If the `depth` option is specified, the iterator will only descend to the
13
+ * specified depth. A depth of 0 will yield values only at the tree's top level.
14
+ *
15
+ * @param {import("../../index.ts").Maplike} maplike
16
+ * @param {{ depth?: number, expand?: boolean }} [options]
17
+ * @returns {AsyncGenerator<[any, any], void, undefined>}
18
+ */
19
+ export default async function* deepEntriesIterator(maplike, options = {}) {
20
+ const tree = await args.map(maplike, "Tree.deepEntriesIterator", {
21
+ deep: !options.expand,
22
+ });
23
+
24
+ const depth = options.depth ?? Infinity;
25
+ const expand = options.expand ?? false;
26
+
27
+ for await (let [key, value] of tree.entries()) {
28
+ value = await value;
29
+
30
+ if (expand && isUnpackable(value)) {
31
+ value = await value.unpack();
32
+ }
33
+
34
+ // Recurse into child trees, but don't expand functions.
35
+ const recurse =
36
+ depth > 0 &&
37
+ (isMap(value) ||
38
+ (expand && typeof value !== "function" && isMaplike(value)));
39
+ if (recurse) {
40
+ yield* deepEntriesIterator(value, { depth: depth - 1, expand });
41
+ } else {
42
+ yield [key, value];
43
+ }
44
+ }
45
+ }
@@ -1,4 +1,5 @@
1
1
  import * as args from "../utilities/args.js";
2
+ import isUnpackable from "../utilities/isUnpackable.js";
2
3
  import isMap from "./isMap.js";
3
4
  import isMaplike from "./isMaplike.js";
4
5
 
@@ -6,29 +7,38 @@ import isMaplike from "./isMaplike.js";
6
7
  * Return an iterator that yields all values in a tree, including nested trees.
7
8
  *
8
9
  * If the `expand` option is true, maplike values (but not functions) will be
9
- * expanded into nested trees and their values will be yielded.
10
+ * expanded into nested trees and their values will be yielded. Packed values
11
+ * will be unpacked before expanding.
12
+ *
13
+ * If the `depth` option is specified, the iterator will only descend to the
14
+ * specified depth. A depth of 0 will yield values only at the tree's top level.
10
15
  *
11
16
  * @param {import("../../index.ts").Maplike} maplike
12
- * @param {{ expand?: boolean }} [options]
17
+ * @param {{ depth?: number, expand?: boolean }} [options]
13
18
  * @returns {AsyncGenerator<any, void, undefined>}
14
19
  */
15
- export default async function* deepValuesIterator(
16
- maplike,
17
- options = { expand: false },
18
- ) {
20
+ export default async function* deepValuesIterator(maplike, options = {}) {
19
21
  const tree = await args.map(maplike, "Tree.deepValuesIterator", {
20
- deep: true,
22
+ deep: !options.expand,
21
23
  });
22
24
 
25
+ const depth = options.depth ?? Infinity;
26
+ const expand = options.expand ?? false;
27
+
23
28
  for await (const key of tree.keys()) {
24
- const value = await tree.get(key);
29
+ let value = await tree.get(key);
30
+
31
+ if (expand && isUnpackable(value)) {
32
+ value = await value.unpack();
33
+ }
25
34
 
26
35
  // Recurse into child trees, but don't expand functions.
27
36
  const recurse =
28
- isMap(value) ||
29
- (options.expand && typeof value !== "function" && isMaplike(value));
37
+ depth > 0 &&
38
+ (isMap(value) ||
39
+ (expand && typeof value !== "function" && isMaplike(value)));
30
40
  if (recurse) {
31
- yield* deepValuesIterator(value, options);
41
+ yield* deepValuesIterator(value, { depth: depth - 1, expand });
32
42
  } else {
33
43
  yield value;
34
44
  }
@@ -0,0 +1,45 @@
1
+ import SyncMap from "../drivers/SyncMap.js";
2
+ import * as trailingSlash from "../trailingSlash.js";
3
+ import * as args from "../utilities/args.js";
4
+ import deepEntriesIterator from "./deepEntriesIterator.js";
5
+
6
+ /**
7
+ * Flatten the entries in the tree to the given depth. If the depth is omitted,
8
+ * the tree will be flattened one level.
9
+ *
10
+ * Numeric keys will be renumbered.
11
+ *
12
+ * If the final result is array-like, returns an array, otherwise returns a map.
13
+ *
14
+ * @typedef {import("../../index.ts").Maplike} Maplike
15
+ *
16
+ * @param {Maplike} maplike
17
+ * @param {number} [depth] The maximum depth to flatten
18
+ */
19
+ export default async function flat(maplike, depth = 1) {
20
+ const map = await args.map(maplike, "Tree.flat", { deep: true });
21
+
22
+ let index = 0;
23
+ let onlyNumericKeys = true;
24
+ const result = new SyncMap();
25
+ for await (let [key, value] of deepEntriesIterator(map, { depth })) {
26
+ key = trailingSlash.remove(key);
27
+ if (typeof key === "number" || /^\d+$/.test(key)) {
28
+ // Numeric key, renumber it.
29
+ key = String(index);
30
+ index++;
31
+ } else {
32
+ // Non-numeric key, keep it as is.
33
+ onlyNumericKeys = false;
34
+ }
35
+ result.set(key, value);
36
+ }
37
+
38
+ if (onlyNumericKeys) {
39
+ // All keys are numeric, return an array.
40
+ return [...result.values()];
41
+ } else {
42
+ // Some keys are non-numeric, return a map.
43
+ return result;
44
+ }
45
+ }
@@ -16,6 +16,7 @@ export default function isMap(object) {
16
16
  }
17
17
 
18
18
  // Check for Map-like interface
19
+ // Note: doesn't require the getOrInsert or getOrInsertComputed members.
19
20
  if (
20
21
  object &&
21
22
  object.clear instanceof Function &&
@@ -48,7 +48,7 @@ export default async function merge(...treelikes) {
48
48
  const sources = unpacked.map((maplike) => from(maplike));
49
49
 
50
50
  if (sources.length === 0) {
51
- throw new TypeError("merge: all trees are null or undefined");
51
+ throw new TypeError("Tree.merge: all trees are null or undefined");
52
52
  } else if (sources.length === 1) {
53
53
  // Only one tree, no need to merge
54
54
  return sources[0];
@@ -19,7 +19,10 @@ export default function getParent(packed, options = {}) {
19
19
 
20
20
  // If the packed object has a `parent` property, use that. Exception: Node
21
21
  // Buffer objects have a `parent` property that we ignore.
22
- if (packed.parent && !(packed instanceof Buffer)) {
22
+ if (
23
+ packed.parent &&
24
+ !(typeof Buffer !== "undefined" && packed instanceof Buffer)
25
+ ) {
23
26
  return packed.parent;
24
27
  }
25
28
 
@@ -44,6 +44,55 @@ describe("AsyncMap", () => {
44
44
  ]);
45
45
  });
46
46
 
47
+ test("getOrInsert", async () => {
48
+ const map = new SampleAsyncMap([
49
+ ["a", 1],
50
+ ["b", 2],
51
+ ]);
52
+ assert.strictEqual(await map.getOrInsert("a", 100), 1);
53
+ assert.strictEqual(await map.getOrInsert("c", 3), 3);
54
+ assert.strictEqual(await map.get("c"), 3);
55
+ });
56
+
57
+ test("getOrInsert on read-only map throws if key doesn't exist", async () => {
58
+ class Fixture extends AsyncMap {
59
+ constructor(entries) {
60
+ super();
61
+ this.map = new Map(entries);
62
+ }
63
+
64
+ async get(key) {
65
+ let value = this.map.get(key);
66
+ if (value instanceof Array) {
67
+ value = Reflect.construct(this.constructor, [value]);
68
+ }
69
+ return value;
70
+ }
71
+ }
72
+
73
+ const map = new Fixture([
74
+ ["a", 1],
75
+ ["b", 2],
76
+ ]);
77
+
78
+ assert.strictEqual(await map.getOrInsert("a", 100), 1);
79
+ await assert.rejects(async () => await map.getOrInsert("c", 3), {
80
+ name: "TypeError",
81
+ message:
82
+ "getOrInsert() can't insert into a new value into a read-only map.",
83
+ });
84
+ });
85
+
86
+ test("getOrInsertComputed", async () => {
87
+ const map = new SampleAsyncMap([
88
+ ["a", 1],
89
+ ["b", 2],
90
+ ]);
91
+ assert.strictEqual(await map.getOrInsertComputed("a", async () => 100), 1);
92
+ assert.strictEqual(await map.getOrInsertComputed("c", async () => 3), 3);
93
+ assert.strictEqual(await map.get("c"), 3);
94
+ });
95
+
47
96
  test("static groupBy", async () => {
48
97
  const items = [
49
98
  { name: "apple", type: "fruit" },
@@ -52,7 +101,7 @@ describe("AsyncMap", () => {
52
101
  ];
53
102
  const map = await AsyncMap.groupBy(
54
103
  items,
55
- async (element, index) => element.type
104
+ async (element, index) => element.type,
56
105
  );
57
106
  assert.deepStrictEqual(Array.from(map.entries()), [
58
107
  [
@@ -133,6 +133,50 @@ describe("SyncMap", () => {
133
133
  assert.strictEqual(map.get("c"), undefined);
134
134
  });
135
135
 
136
+ test("getOrInsert", () => {
137
+ const map = new SyncMap([
138
+ ["a", 1],
139
+ ["b", 2],
140
+ ]);
141
+ assert.strictEqual(map.getOrInsert("a", 100), 1);
142
+ assert.strictEqual(map.getOrInsert("c", 3), 3);
143
+ assert.strictEqual(map.get("c"), 3);
144
+ });
145
+
146
+ test("getOrInsert on read-only map throws if key doesn't exist", () => {
147
+ class Fixture extends SyncMap {
148
+ get(key) {
149
+ return super.get(key);
150
+ }
151
+ }
152
+ const map = new Fixture([
153
+ ["a", 1],
154
+ ["b", 2],
155
+ ]);
156
+ assert.strictEqual(map.getOrInsert("a", 100), 1);
157
+ assert.throws(() => map.getOrInsert("c", 3), {
158
+ name: "TypeError",
159
+ message:
160
+ "getOrInsert() can't insert into a new value into a read-only map.",
161
+ });
162
+ });
163
+
164
+ test("getOrInsertComputed", () => {
165
+ const map = new SyncMap([
166
+ ["a", 1],
167
+ ["b", 2],
168
+ ]);
169
+ assert.strictEqual(
170
+ map.getOrInsertComputed("a", () => 100),
171
+ 1,
172
+ );
173
+ assert.strictEqual(
174
+ map.getOrInsertComputed("c", () => 3),
175
+ 3,
176
+ );
177
+ assert.strictEqual(map.get("c"), 3);
178
+ });
179
+
136
180
  test("has returns true if key exists in keys()", () => {
137
181
  const map = new SyncMap();
138
182
  map.keys = () => {
@@ -321,7 +365,7 @@ describe("SyncMap", () => {
321
365
  ]);
322
366
  // @ts-ignore
323
367
  const grouped = Map.groupBy(map, ([key, value]) =>
324
- value % 2 === 0 ? "even" : "odd"
368
+ value % 2 === 0 ? "even" : "odd",
325
369
  );
326
370
  assert(grouped instanceof Map);
327
371
  assert.strictEqual(grouped.size, 2);
@@ -3,8 +3,8 @@ import { describe, test } from "node:test";
3
3
  import ObjectMap from "../../src/drivers/ObjectMap.js";
4
4
  import SyncMap from "../../src/drivers/SyncMap.js";
5
5
  import assign from "../../src/operations/assign.js";
6
+ import deepArrays from "../../src/operations/deepArrays.js";
6
7
  import plain from "../../src/operations/plain.js";
7
- import { deepEntries } from "../../src/Tree.js";
8
8
  import SampleAsyncMap from "../SampleAsyncMap.js";
9
9
 
10
10
  describe("assign", () => {
@@ -26,7 +26,7 @@ describe("assign", () => {
26
26
  const result = await assign(target, source);
27
27
 
28
28
  assert.equal(result, target);
29
- assert.deepEqual(await deepEntries(target), [
29
+ assert.deepEqual(await deepArrays(target), [
30
30
  ["a", 4],
31
31
  ["b", 2],
32
32
  [
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import ObjectMap from "../../src/drivers/ObjectMap.js";
4
+ import concat from "../../src/operations/concat.js";
5
+
6
+ describe("concat", () => {
7
+ test("concatenates arrays", async () => {
8
+ const result = await concat(["a", "b"], ["c", "d"]);
9
+ assert.deepEqual(result, ["a", "b", "c", "d"]);
10
+ });
11
+
12
+ test("concatenates maplike objects", async () => {
13
+ const result = await concat(
14
+ {
15
+ 1: "a",
16
+ 2: "b",
17
+ },
18
+ new ObjectMap({
19
+ 0: "c",
20
+ 1: "d",
21
+ }),
22
+ ["e", "f"],
23
+ );
24
+ assert.deepEqual(result, ["a", "b", "c", "d", "e", "f"]);
25
+ });
26
+
27
+ test("copes with mixture of numeric and non-numeric keys", async () => {
28
+ const result = await concat(
29
+ ["a", "b"],
30
+ {
31
+ x: 1,
32
+ y: 2,
33
+ },
34
+ ["c", "d"],
35
+ );
36
+ assert.deepEqual(
37
+ [...result.entries()],
38
+ [
39
+ ["0", "a"],
40
+ ["1", "b"],
41
+ ["x", 1],
42
+ ["y", 2],
43
+ ["2", "c"],
44
+ ["3", "d"],
45
+ ],
46
+ );
47
+ });
48
+
49
+ test("can unpack arguments", async () => {
50
+ /** @type {any} */
51
+ const packed1 = new String("Packed array");
52
+ packed1.unpack = async function () {
53
+ return ["a", "b"];
54
+ };
55
+ /** @type {any} */
56
+ const packed2 = new String("Packed object");
57
+ packed2.unpack = async function () {
58
+ return {
59
+ 0: "c",
60
+ 1: "d",
61
+ };
62
+ };
63
+ const result = await concat(packed1, packed2);
64
+ assert.deepEqual(result, ["a", "b", "c", "d"]);
65
+ });
66
+ });
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import deepArrays from "../../src/operations/deepArrays.js";
4
+ import SampleAsyncMap from "../SampleAsyncMap.js";
5
+
6
+ describe("deepArrays", () => {
7
+ test("converts an async tree to an array of nested entries arrays", async () => {
8
+ const fixture = new SampleAsyncMap([
9
+ ["a", 1],
10
+ ["b", 2],
11
+ [
12
+ "more",
13
+ [
14
+ ["c", 3],
15
+ ["d", 4],
16
+ ],
17
+ ],
18
+ ]);
19
+ const result = await deepArrays(fixture);
20
+ assert.deepStrictEqual(result, [
21
+ ["a", 1],
22
+ ["b", 2],
23
+ [
24
+ "more",
25
+ [
26
+ ["c", 3],
27
+ ["d", 4],
28
+ ],
29
+ ],
30
+ ]);
31
+ });
32
+ });
@@ -4,7 +4,7 @@ import deepEntries from "../../src/operations/deepEntries.js";
4
4
  import SampleAsyncMap from "../SampleAsyncMap.js";
5
5
 
6
6
  describe("deepEntries", () => {
7
- test("converts an async tree to an array of nested entries arrays", async () => {
7
+ test("returns an entry of the deep tree entries", async () => {
8
8
  const fixture = new SampleAsyncMap([
9
9
  ["a", 1],
10
10
  ["b", 2],
@@ -20,13 +20,8 @@ describe("deepEntries", () => {
20
20
  assert.deepStrictEqual(result, [
21
21
  ["a", 1],
22
22
  ["b", 2],
23
- [
24
- "more",
25
- [
26
- ["c", 3],
27
- ["d", 4],
28
- ],
29
- ],
23
+ ["c", 3],
24
+ ["d", 4],
30
25
  ]);
31
26
  });
32
27
  });
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import ObjectMap from "../../src/drivers/ObjectMap.js";
4
+ import deepEntriesIterator from "../../src/operations/deepEntriesIterator.js";
5
+
6
+ describe("deepEntriesIterator", () => {
7
+ test("returns an iterator of a tree's deep entries", async () => {
8
+ const tree = new ObjectMap({
9
+ a: 1,
10
+ b: 2,
11
+ more: {
12
+ c: 3,
13
+ d: 4,
14
+ },
15
+ });
16
+ const entries = [];
17
+ // The tree will be shallow, but we'll ask to expand the entries.
18
+ for await (const entry of deepEntriesIterator(tree, { expand: true })) {
19
+ entries.push(entry);
20
+ }
21
+ assert.deepEqual(entries, [
22
+ ["a", 1],
23
+ ["b", 2],
24
+ ["c", 3],
25
+ ["d", 4],
26
+ ]);
27
+ });
28
+
29
+ test("if depth is specified, only descends to specified depth", async () => {
30
+ const tree = new ObjectMap({
31
+ a: 1,
32
+ sub: {
33
+ b: 2,
34
+ more: {
35
+ c: 3,
36
+ deeper: {
37
+ d: 4,
38
+ },
39
+ },
40
+ },
41
+ });
42
+ const entries = [];
43
+ for await (const entry of deepEntriesIterator(tree, {
44
+ depth: 2,
45
+ expand: true,
46
+ })) {
47
+ entries.push(entry);
48
+ }
49
+ assert.deepEqual(entries, [
50
+ ["a", 1],
51
+ ["b", 2],
52
+ ["c", 3],
53
+ ["deeper", { d: 4 }],
54
+ ]);
55
+ });
56
+
57
+ test("can optionally unpack a packed value", async () => {
58
+ /** @type {any} */
59
+ const packed = new String("String that unpacks to data");
60
+ packed.unpack = async function () {
61
+ return {
62
+ message: "Hello",
63
+ };
64
+ };
65
+ const tree = {
66
+ a: 1,
67
+ packed,
68
+ };
69
+ const entries = [];
70
+ for await (const entry of deepEntriesIterator(tree, { expand: true })) {
71
+ entries.push(entry);
72
+ }
73
+ assert.deepEqual(entries, [
74
+ ["a", 1],
75
+ ["message", "Hello"],
76
+ ]);
77
+ });
78
+ });
@@ -20,4 +20,46 @@ describe("deepValuesIterator", () => {
20
20
  }
21
21
  assert.deepEqual(values, [1, 2, 3, 4]);
22
22
  });
23
+
24
+ test("if depth is specified, only descends to specified depth", async () => {
25
+ const tree = new ObjectMap({
26
+ a: 1,
27
+ sub: {
28
+ b: 2,
29
+ more: {
30
+ c: 3,
31
+ deeper: {
32
+ d: 4,
33
+ },
34
+ },
35
+ },
36
+ });
37
+ const values = [];
38
+ for await (const value of deepValuesIterator(tree, {
39
+ depth: 2,
40
+ expand: true,
41
+ })) {
42
+ values.push(value);
43
+ }
44
+ assert.deepEqual(values, [1, 2, 3, { d: 4 }]);
45
+ });
46
+
47
+ test("can optionally unpack a packed value", async () => {
48
+ /** @type {any} */
49
+ const packed = new String("String that unpacks to data");
50
+ packed.unpack = async function () {
51
+ return {
52
+ message: "Hello",
53
+ };
54
+ };
55
+ const tree = {
56
+ a: 1,
57
+ packed,
58
+ };
59
+ const values = [];
60
+ for await (const value of deepValuesIterator(tree, { expand: true })) {
61
+ values.push(value);
62
+ }
63
+ assert.deepEqual(values, [1, "Hello"]);
64
+ });
23
65
  });
@@ -0,0 +1,82 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import ObjectMap from "../../src/drivers/ObjectMap.js";
4
+ import flat from "../../src/operations/flat.js";
5
+ import plain from "../../src/operations/plain.js";
6
+
7
+ describe("flat", () => {
8
+ test("flattens an array one level by default", async () => {
9
+ const result = await flat([1, 2, [3], [[4, [5]]]], 1);
10
+ assert.deepEqual(await plain(result), [1, 2, 3, [4, [5]]]);
11
+ });
12
+
13
+ test("flattens deep arrays", async () => {
14
+ assert.deepEqual(
15
+ await flat([1, 2, [3], [[4, [5]]]], Infinity),
16
+ [1, 2, 3, 4, 5],
17
+ );
18
+ });
19
+
20
+ test("flattens an object one level by default", async () => {
21
+ const fixture = {
22
+ a: 1,
23
+ sub: {
24
+ b: 2,
25
+ more: {
26
+ c: 3,
27
+ },
28
+ },
29
+ };
30
+ const result = await flat(fixture);
31
+ assert.deepEqual(await plain(result), {
32
+ a: 1,
33
+ b: 2,
34
+ more: {
35
+ c: 3,
36
+ },
37
+ });
38
+ });
39
+
40
+ test("flattens deep objects", async () => {
41
+ const fixture = {
42
+ a: 1,
43
+ sub: {
44
+ b: 2,
45
+ more: {
46
+ c: 3,
47
+ },
48
+ },
49
+ };
50
+ const result = await flat(fixture, Infinity);
51
+ assert.deepEqual(await plain(result), {
52
+ a: 1,
53
+ b: 2,
54
+ c: 3,
55
+ });
56
+ });
57
+
58
+ test("flattens maplike objects", async () => {
59
+ const result = await flat(
60
+ [
61
+ {
62
+ a: 1,
63
+ b: 2,
64
+ },
65
+ new ObjectMap({
66
+ c: 3,
67
+ d: 4,
68
+ }),
69
+ [5, 6],
70
+ ],
71
+ Infinity,
72
+ );
73
+ assert.deepEqual(await plain(result), {
74
+ a: 1,
75
+ b: 2,
76
+ c: 3,
77
+ d: 4,
78
+ 0: 5,
79
+ 1: 6,
80
+ });
81
+ });
82
+ });