@weborigami/async-tree 0.6.11 → 0.6.13

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,14 +1,14 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
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": "24.10.1",
11
- "puppeteer": "24.30.0",
10
+ "@types/node": "25.3.2",
11
+ "puppeteer": "24.37.5",
12
12
  "typescript": "5.9.3"
13
13
  },
14
14
  "scripts": {
package/src/Tree.js CHANGED
@@ -8,6 +8,7 @@ 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";
12
13
  export { default as deepEntries } from "./operations/deepEntries.js";
13
14
  export { default as deepMap } from "./operations/deepMap.js";
@@ -21,6 +22,7 @@ export { default as delete } from "./operations/delete.js";
21
22
  export { default as entries } from "./operations/entries.js";
22
23
  export { default as filter } from "./operations/filter.js";
23
24
  export { default as first } from "./operations/first.js";
25
+ export { default as flat } from "./operations/flat.js";
24
26
  export { default as forEach } from "./operations/forEach.js";
25
27
  export { default as from } from "./operations/from.js";
26
28
  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
  *
@@ -0,0 +1,56 @@
1
+ import { isUnpackable, SyncMap, Tree } from "@weborigami/async-tree";
2
+ // import assignPropertyDescriptors from "./assignPropertyDescriptors.js";
3
+
4
+ /**
5
+ * Concatenate the given trees. This is similar to a merge, but numeric keys
6
+ * will be renumbered starting with 0 and incrementing by 1.
7
+ *
8
+ * @typedef {import("@weborigami/async-tree").Maplike} Maplike
9
+ *
10
+ * @param {(Maplike|null)[]} trees
11
+ */
12
+ export default async function concat(...trees) {
13
+ // Filter out null or undefined trees.
14
+ /** @type {Maplike[]}
15
+ * @ts-ignore */
16
+ const filtered = trees.filter((tree) => tree);
17
+
18
+ // Unpack any packed objects.
19
+ const sources = await Promise.all(
20
+ filtered.map((obj) =>
21
+ isUnpackable(obj) ? /** @type {any} */ (obj).unpack() : obj,
22
+ ),
23
+ );
24
+
25
+ if (sources.length === 0) {
26
+ throw new TypeError("Tree.concat: all arguments are null or undefined");
27
+ }
28
+
29
+ let index = 0;
30
+ let onlyNumericKeys = true;
31
+ const map = new SyncMap();
32
+ for (const source of sources) {
33
+ const entries = await Tree.entries(source);
34
+ for (const [entryKey, entryValue] of entries) {
35
+ let key;
36
+ if (!isNaN(parseInt(entryKey))) {
37
+ // Numeric key, renumber it.
38
+ key = String(index);
39
+ index++;
40
+ } else {
41
+ // Non-numeric key, keep it as is.
42
+ key = entryKey;
43
+ onlyNumericKeys = false;
44
+ }
45
+ map.set(key, entryValue);
46
+ }
47
+ }
48
+
49
+ if (onlyNumericKeys) {
50
+ // All keys are numeric, return an array.
51
+ return [...map.values()];
52
+ } else {
53
+ // Some keys are non-numeric, return a map.
54
+ return map;
55
+ }
56
+ }
@@ -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 1 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 > 1 &&
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,21 @@
1
+ import * as args from "../utilities/args.js";
2
+ import deepValuesIterator from "./deepValuesIterator.js";
3
+
4
+ /**
5
+ * Flatten the values in the tree to the given depth.
6
+ *
7
+ * @typedef {import("../../index.ts").Maplike} Maplike
8
+ *
9
+ * @param {Maplike} maplike
10
+ * @param {number} [depth] The maximum depth to flatten
11
+ */
12
+ export default async function flat(maplike, depth = 1) {
13
+ const map = await args.map(maplike, "Tree.flat", { deep: true });
14
+ /** @type {any} */
15
+ const iterator = deepValuesIterator(map, { depth, expand: true });
16
+ const result = [];
17
+ for await (const key of iterator) {
18
+ result.push(key);
19
+ }
20
+ return result;
21
+ }
@@ -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);
@@ -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
+ });
@@ -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: 3,
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,58 @@
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 the tree's values into an array", async () => {
9
+ const fixture = {
10
+ a: 1,
11
+ sub: {
12
+ b: 2,
13
+ more: {
14
+ c: 3,
15
+ },
16
+ },
17
+ };
18
+ assert.deepEqual(await flat(fixture, Infinity), [1, 2, 3]);
19
+ });
20
+
21
+ test("flattens one level by default", async () => {
22
+ const fixture = {
23
+ a: 1,
24
+ sub: {
25
+ b: 2,
26
+ more: {
27
+ c: 3,
28
+ },
29
+ },
30
+ };
31
+ assert.deepEqual(await plain(await flat(fixture)), [
32
+ 1,
33
+ { b: 2, more: { c: 3 } },
34
+ ]);
35
+ });
36
+
37
+ test("flattens arrays", async () => {
38
+ assert.deepEqual(await flat([1, 2, [3]], Infinity), [1, 2, 3]);
39
+ });
40
+
41
+ test("flattens maplike objects", async () => {
42
+ const result = await flat(
43
+ [
44
+ {
45
+ a: 1,
46
+ b: 2,
47
+ },
48
+ new ObjectMap({
49
+ c: 3,
50
+ d: 4,
51
+ }),
52
+ [5, 6],
53
+ ],
54
+ Infinity,
55
+ );
56
+ assert.deepEqual(result, [1, 2, 3, 4, 5, 6]);
57
+ });
58
+ });