@weborigami/async-tree 0.0.59 → 0.0.60
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 +1 -0
- package/package.json +2 -2
- package/src/DeferredTree.js +2 -27
- package/src/operations/cache.js +1 -1
- package/src/operations/deepMerge.js +15 -2
- package/src/operations/deepTakeFn.js +1 -1
- package/src/operations/merge.js +12 -21
- package/src/operations/scope.js +65 -0
- package/src/transforms/regExpKeys.js +3 -1
- package/src/utilities.js +14 -1
- package/test/DeepObjectTree.test.js +2 -0
- package/test/operations/deepMerge.test.js +4 -0
- package/test/operations/deepTakeFn.test.js +2 -1
- package/test/operations/merge.test.js +5 -0
- package/test/operations/scope.test.js +25 -0
- package/test/utilities.test.js +5 -0
package/main.js
CHANGED
|
@@ -21,6 +21,7 @@ export { default as group } from "./src/operations/group.js";
|
|
|
21
21
|
export { default as groupFn } from "./src/operations/groupFn.js";
|
|
22
22
|
export { default as map } from "./src/operations/map.js";
|
|
23
23
|
export { default as merge } from "./src/operations/merge.js";
|
|
24
|
+
export { default as scope } from "./src/operations/scope.js";
|
|
24
25
|
export { default as sort } from "./src/operations/sort.js";
|
|
25
26
|
export { default as take } from "./src/operations/take.js";
|
|
26
27
|
export * as symbols from "./src/symbols.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.60",
|
|
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.
|
|
14
|
+
"@weborigami/types": "0.0.60"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/src/DeferredTree.js
CHANGED
|
@@ -20,7 +20,6 @@ export default class DeferredTree {
|
|
|
20
20
|
this.treePromise = null;
|
|
21
21
|
this._tree = null;
|
|
22
22
|
this._parentUntilLoaded = null;
|
|
23
|
-
this._scopeUntilLoaded = null;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
async get(key) {
|
|
@@ -54,25 +53,6 @@ export default class DeferredTree {
|
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
// HACK: The concept of scope is defined in Origami, not at the AsyncTree
|
|
58
|
-
// level. If a DeferredTree is used to wrap an OrigamiTree, the inner
|
|
59
|
-
// OrigamiTree will have a `scope` but not a `parent`. If someone asks the
|
|
60
|
-
// outer deferrred tree for a scope, they'd otherwise get `undefined`, which
|
|
61
|
-
// is incorrect. As a workaround, we introduce a `scope` getter here that
|
|
62
|
-
// defers to the inner tree, but we need to find a way to avoid having to
|
|
63
|
-
// introduce the concept of scope here.
|
|
64
|
-
get scope() {
|
|
65
|
-
return /** @type {any} */ (this._tree)?.scope;
|
|
66
|
-
}
|
|
67
|
-
set scope(scope) {
|
|
68
|
-
// As with `parent`, we can defer setting of scope.
|
|
69
|
-
if (this._tree && !(/** @type {any} */ (this._tree).scope)) {
|
|
70
|
-
/** @type {any} */ (this._tree).scope = scope;
|
|
71
|
-
} else {
|
|
72
|
-
this._scopeUntilLoaded = scope;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
56
|
async tree() {
|
|
77
57
|
if (this._tree) {
|
|
78
58
|
return this._tree;
|
|
@@ -82,18 +62,13 @@ export default class DeferredTree {
|
|
|
82
62
|
this.treePromise ??= this.loadResult().then((treelike) => {
|
|
83
63
|
this._tree = Tree.from(treelike);
|
|
84
64
|
if (this._parentUntilLoaded) {
|
|
85
|
-
// Now that the tree has been loaded, we can set its parent
|
|
65
|
+
// Now that the tree has been loaded, we can set its parent if it hasn't
|
|
66
|
+
// already been set.
|
|
86
67
|
if (!this._tree.parent) {
|
|
87
68
|
this._tree.parent = this._parentUntilLoaded;
|
|
88
69
|
}
|
|
89
70
|
this._parentUntilLoaded = null;
|
|
90
71
|
}
|
|
91
|
-
if (this._scopeUntilLoaded) {
|
|
92
|
-
if (!(/** @type {any} */ (this._tree).scope)) {
|
|
93
|
-
/** @type {any} */ (this._tree).scope = this._scopeUntilLoaded;
|
|
94
|
-
}
|
|
95
|
-
this._scopeUntilLoaded = null;
|
|
96
|
-
}
|
|
97
72
|
return this._tree;
|
|
98
73
|
});
|
|
99
74
|
|
package/src/operations/cache.js
CHANGED
|
@@ -18,15 +18,28 @@ export default function deepMerge(...sources) {
|
|
|
18
18
|
|
|
19
19
|
// Check trees for the indicated key in reverse order.
|
|
20
20
|
for (let index = trees.length - 1; index >= 0; index--) {
|
|
21
|
-
const
|
|
21
|
+
const tree = trees[index];
|
|
22
|
+
const value = await tree.get(key);
|
|
22
23
|
if (Tree.isAsyncTree(value)) {
|
|
24
|
+
if (value.parent === tree) {
|
|
25
|
+
// Merged tree acts as parent instead of the source tree.
|
|
26
|
+
value.parent = this;
|
|
27
|
+
}
|
|
23
28
|
subtrees.unshift(value);
|
|
24
29
|
} else if (value !== undefined) {
|
|
25
30
|
return value;
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
if (subtrees.length > 1) {
|
|
35
|
+
const merged = deepMerge(...subtrees);
|
|
36
|
+
merged.parent = this;
|
|
37
|
+
return merged;
|
|
38
|
+
} else if (subtrees.length === 1) {
|
|
39
|
+
return subtrees[0];
|
|
40
|
+
} else {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
30
43
|
},
|
|
31
44
|
|
|
32
45
|
async isKeyForSubtree(key) {
|
package/src/operations/merge.js
CHANGED
|
@@ -11,19 +11,22 @@ import { Tree } from "../internal.js";
|
|
|
11
11
|
*
|
|
12
12
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
13
13
|
* @param {import("../../index.ts").Treelike[]} sources
|
|
14
|
-
* @returns {AsyncTree & { description: string }}
|
|
14
|
+
* @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
|
|
15
15
|
*/
|
|
16
16
|
export default function merge(...sources) {
|
|
17
|
-
let trees = sources.map((treelike) => Tree.from(treelike));
|
|
18
|
-
let mergeParent;
|
|
19
17
|
return {
|
|
20
18
|
description: "merge",
|
|
21
19
|
|
|
22
20
|
async get(key) {
|
|
23
21
|
// Check trees for the indicated key in reverse order.
|
|
24
|
-
for (let index = trees.length - 1; index >= 0; index--) {
|
|
25
|
-
const
|
|
22
|
+
for (let index = this.trees.length - 1; index >= 0; index--) {
|
|
23
|
+
const tree = this.trees[index];
|
|
24
|
+
const value = await tree.get(key);
|
|
26
25
|
if (value !== undefined) {
|
|
26
|
+
if (Tree.isAsyncTree(value) && value.parent === tree) {
|
|
27
|
+
// Merged tree acts as parent instead of the source tree.
|
|
28
|
+
value.parent = this;
|
|
29
|
+
}
|
|
27
30
|
return value;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -32,8 +35,8 @@ export default function merge(...sources) {
|
|
|
32
35
|
|
|
33
36
|
async isKeyForSubtree(key) {
|
|
34
37
|
// Check trees for the indicated key in reverse order.
|
|
35
|
-
for (let index = trees.length - 1; index >= 0; index--) {
|
|
36
|
-
if (await Tree.isKeyForSubtree(trees[index], key)) {
|
|
38
|
+
for (let index = this.trees.length - 1; index >= 0; index--) {
|
|
39
|
+
if (await Tree.isKeyForSubtree(this.trees[index], key)) {
|
|
37
40
|
return true;
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -43,7 +46,7 @@ export default function merge(...sources) {
|
|
|
43
46
|
async keys() {
|
|
44
47
|
const keys = new Set();
|
|
45
48
|
// Collect keys in the order the trees were provided.
|
|
46
|
-
for (const tree of trees) {
|
|
49
|
+
for (const tree of this.trees) {
|
|
47
50
|
for (const key of await tree.keys()) {
|
|
48
51
|
keys.add(key);
|
|
49
52
|
}
|
|
@@ -51,18 +54,6 @@ export default function merge(...sources) {
|
|
|
51
54
|
return keys;
|
|
52
55
|
},
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
return mergeParent;
|
|
56
|
-
},
|
|
57
|
-
set parent(parent) {
|
|
58
|
-
mergeParent = parent;
|
|
59
|
-
trees = sources.map((treelike) => {
|
|
60
|
-
const tree = Tree.isAsyncTree(treelike)
|
|
61
|
-
? Object.create(treelike)
|
|
62
|
-
: Tree.from(treelike);
|
|
63
|
-
tree.parent = parent;
|
|
64
|
-
return tree;
|
|
65
|
-
});
|
|
66
|
-
},
|
|
57
|
+
trees: sources.map((treelike) => Tree.from(treelike)),
|
|
67
58
|
};
|
|
68
59
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tree's "scope" is the collection of everything in that tree and all of its
|
|
5
|
+
* ancestors.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
+
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
9
|
+
*
|
|
10
|
+
* @param {Treelike} treelike
|
|
11
|
+
* @returns {AsyncTree & {trees: AsyncTree[]}}
|
|
12
|
+
*/
|
|
13
|
+
export default function scope(treelike) {
|
|
14
|
+
const tree = Tree.from(treelike);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
// Starting with this tree, search up the parent hierarchy.
|
|
18
|
+
async get(key) {
|
|
19
|
+
/** @type {AsyncTree|null|undefined} */
|
|
20
|
+
let current = tree;
|
|
21
|
+
let value;
|
|
22
|
+
while (current) {
|
|
23
|
+
value = await current.get(key);
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
current = current.parent;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Collect all keys for this tree and all parents
|
|
33
|
+
async keys() {
|
|
34
|
+
const keys = new Set();
|
|
35
|
+
|
|
36
|
+
/** @type {AsyncTree|null|undefined} */
|
|
37
|
+
let current = tree;
|
|
38
|
+
while (current) {
|
|
39
|
+
for (const key of await current.keys()) {
|
|
40
|
+
keys.add(key);
|
|
41
|
+
}
|
|
42
|
+
current = current.parent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return keys;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Collect all keys for this tree and all parents.
|
|
49
|
+
//
|
|
50
|
+
// This method exists for debugging purposes, as it's helpful to be able to
|
|
51
|
+
// quickly flatten and view the entire scope chain.
|
|
52
|
+
get trees() {
|
|
53
|
+
const result = [];
|
|
54
|
+
|
|
55
|
+
/** @type {AsyncTree|null|undefined} */
|
|
56
|
+
let current = tree;
|
|
57
|
+
while (current) {
|
|
58
|
+
result.push(current);
|
|
59
|
+
current = current.parent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -56,7 +56,9 @@ export default async function regExpKeys(treelike) {
|
|
|
56
56
|
let value = await tree.get(key);
|
|
57
57
|
if (Tree.isAsyncTree(value)) {
|
|
58
58
|
value = regExpKeys(value);
|
|
59
|
-
value.parent
|
|
59
|
+
if (!value.parent) {
|
|
60
|
+
value.parent = result;
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
map.set(regExp, value);
|
package/src/utilities.js
CHANGED
|
@@ -188,7 +188,8 @@ export async function pipeline(start, ...fns) {
|
|
|
188
188
|
* If the input is stringlike, its text will be returned.
|
|
189
189
|
*
|
|
190
190
|
* If the input is a ArrayBuffer or typed array, it will be interpreted as UTF-8
|
|
191
|
-
* text.
|
|
191
|
+
* text if it does not contain unprintable characters. If it does, it will be
|
|
192
|
+
* returned as a base64-encoded string.
|
|
192
193
|
*
|
|
193
194
|
* If the input has a custom class instance, its public properties will be
|
|
194
195
|
* returned as a plain object.
|
|
@@ -213,6 +214,14 @@ export async function toPlainValue(input) {
|
|
|
213
214
|
return Tree.plain(mapped);
|
|
214
215
|
} else if (isStringLike(input)) {
|
|
215
216
|
return toString(input);
|
|
217
|
+
} else if (input instanceof ArrayBuffer || input instanceof TypedArray) {
|
|
218
|
+
// Try to interpret the buffer as UTF-8 text, otherwise use base64.
|
|
219
|
+
const text = toString(input);
|
|
220
|
+
if (text !== null) {
|
|
221
|
+
return text;
|
|
222
|
+
} else {
|
|
223
|
+
return toBase64(input);
|
|
224
|
+
}
|
|
216
225
|
} else {
|
|
217
226
|
// Some other kind of class instance; return its public properties.
|
|
218
227
|
const plain = {};
|
|
@@ -223,6 +232,10 @@ export async function toPlainValue(input) {
|
|
|
223
232
|
}
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
function toBase64(object) {
|
|
236
|
+
return Buffer.from(object).toString("base64");
|
|
237
|
+
}
|
|
238
|
+
|
|
226
239
|
/**
|
|
227
240
|
* Return a string form of the object, handling cases not generally handled by
|
|
228
241
|
* the standard JavaScript `toString()` method:
|
|
@@ -15,9 +15,11 @@ describe("DeepObjectTree", () => {
|
|
|
15
15
|
const object = await tree.get("object");
|
|
16
16
|
assert.equal(object instanceof DeepObjectTree, true);
|
|
17
17
|
assert.deepEqual(await Tree.plain(object), { b: 2 });
|
|
18
|
+
assert.equal(object.parent, tree);
|
|
18
19
|
|
|
19
20
|
const array = await tree.get("array");
|
|
20
21
|
assert.equal(array instanceof DeepObjectTree, true);
|
|
21
22
|
assert.deepEqual(await Tree.plain(array), [3]);
|
|
23
|
+
assert.equal(array.parent, tree);
|
|
22
24
|
});
|
|
23
25
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
3
4
|
import deepTakeFn from "../../src/operations/deepTakeFn.js";
|
|
4
5
|
|
|
5
6
|
describe("deepTakeFn", () => {
|
|
@@ -16,6 +17,6 @@ describe("deepTakeFn", () => {
|
|
|
16
17
|
g: 5,
|
|
17
18
|
};
|
|
18
19
|
const result = await deepTakeFn(4)(tree);
|
|
19
|
-
assert.deepEqual(result, [1, 2, 3, 4]);
|
|
20
|
+
assert.deepEqual(await Tree.plain(result), [1, 2, 3, 4]);
|
|
20
21
|
});
|
|
21
22
|
});
|
|
@@ -33,7 +33,12 @@ describe("merge", () => {
|
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
// Merge is shallow, and last tree wins, so `b/c` doesn't exist
|
|
36
37
|
const c = await Tree.traverse(fixture, "b", "c");
|
|
37
38
|
assert.equal(c, undefined);
|
|
39
|
+
|
|
40
|
+
// Parent of a subvalue is the merged tree
|
|
41
|
+
const b = await Tree.traverse(fixture, "b");
|
|
42
|
+
assert.equal(b.parent, fixture);
|
|
38
43
|
});
|
|
39
44
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { ObjectTree } from "../../src/internal.js";
|
|
4
|
+
import scope from "../../src/operations/scope.js";
|
|
5
|
+
|
|
6
|
+
describe("scope", () => {
|
|
7
|
+
test("gets the first defined value from the scope trees", async () => {
|
|
8
|
+
const outer = new ObjectTree({
|
|
9
|
+
a: 1,
|
|
10
|
+
b: 2,
|
|
11
|
+
});
|
|
12
|
+
const inner = new ObjectTree({
|
|
13
|
+
a: 3,
|
|
14
|
+
});
|
|
15
|
+
inner.parent = outer;
|
|
16
|
+
const innerScope = scope(inner);
|
|
17
|
+
assert.deepEqual([...(await innerScope.keys())], ["a", "b"]);
|
|
18
|
+
// Inner tree has precedence
|
|
19
|
+
assert.equal(await innerScope.get("a"), 3);
|
|
20
|
+
// If tree doesn't have value, finds value from parent
|
|
21
|
+
assert.equal(await innerScope.get("b"), 2);
|
|
22
|
+
assert.equal(await innerScope.get("c"), undefined);
|
|
23
|
+
assert.deepEqual(innerScope.trees, [inner, outer]);
|
|
24
|
+
});
|
|
25
|
+
});
|
package/test/utilities.test.js
CHANGED
|
@@ -50,6 +50,11 @@ describe("utilities", () => {
|
|
|
50
50
|
await utilities.toPlainValue(new TextEncoder().encode("bytes")),
|
|
51
51
|
"bytes"
|
|
52
52
|
);
|
|
53
|
+
// ArrayBuffer with non-printable characters should be returned as base64
|
|
54
|
+
assert.equal(
|
|
55
|
+
await utilities.toPlainValue(new Uint8Array([1, 2, 3]).buffer),
|
|
56
|
+
"AQID"
|
|
57
|
+
);
|
|
53
58
|
assert.equal(await utilities.toPlainValue(async () => "result"), "result");
|
|
54
59
|
assert.deepEqual(await utilities.toPlainValue(new User("Alice")), {
|
|
55
60
|
name: "Alice",
|