@weborigami/async-tree 0.0.35
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/ReadMe.md +1 -0
- package/browser.js +13 -0
- package/index.ts +44 -0
- package/main.js +23 -0
- package/package.json +20 -0
- package/src/BrowserFileTree.js +137 -0
- package/src/DeferredTree.js +72 -0
- package/src/FileTree.js +193 -0
- package/src/FunctionTree.js +47 -0
- package/src/MapTree.js +51 -0
- package/src/ObjectTree.js +103 -0
- package/src/SetTree.js +40 -0
- package/src/SiteTree.js +124 -0
- package/src/Tree.d.ts +22 -0
- package/src/Tree.js +404 -0
- package/src/keysJson.d.ts +4 -0
- package/src/keysJson.js +56 -0
- package/src/operations/cache.js +75 -0
- package/src/operations/merge.js +60 -0
- package/src/operations/mergeDeep.js +47 -0
- package/src/transforms/cachedKeyMaps.js +86 -0
- package/src/transforms/groupBy.js +40 -0
- package/src/transforms/keyMapsForExtensions.js +64 -0
- package/src/transforms/map.js +106 -0
- package/src/transforms/regExpKeys.js +65 -0
- package/src/transforms/sort.js +22 -0
- package/src/transforms/sortBy.js +29 -0
- package/src/transforms/sortNatural.js +10 -0
- package/src/utilities.d.ts +10 -0
- package/src/utilities.js +124 -0
- package/test/BrowserFileTree.test.js +119 -0
- package/test/DeferredTree.test.js +23 -0
- package/test/FileTree.test.js +134 -0
- package/test/FunctionTree.test.js +51 -0
- package/test/MapTree.test.js +37 -0
- package/test/ObjectTree.test.js +159 -0
- package/test/SetTree.test.js +32 -0
- package/test/SiteTree.test.js +110 -0
- package/test/Tree.test.js +347 -0
- package/test/browser/assert.js +45 -0
- package/test/browser/index.html +35 -0
- package/test/browser/testRunner.js +51 -0
- package/test/fixtures/markdown/Alice.md +1 -0
- package/test/fixtures/markdown/Bob.md +1 -0
- package/test/fixtures/markdown/Carol.md +1 -0
- package/test/operations/cache.test.js +57 -0
- package/test/operations/merge.test.js +39 -0
- package/test/operations/mergeDeep.test.js +38 -0
- package/test/transforms/cachedKeyMaps.test.js +41 -0
- package/test/transforms/groupBy.test.js +29 -0
- package/test/transforms/keyMapsForExtensions.test.js +70 -0
- package/test/transforms/map.test.js +174 -0
- package/test/transforms/regExpKeys.test.js +25 -0
- package/test/transforms/sort.test.js +30 -0
- package/test/transforms/sortBy.test.js +22 -0
- package/test/transforms/sortNatural.test.js +21 -0
- package/test/utilities.test.js +24 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as Tree from "./Tree.js";
|
|
2
|
+
import { getRealmObjectPrototype, isPlainObject } from "./utilities.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A tree defined by a plain object or array.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
8
|
+
* @implements {AsyncMutableTree}
|
|
9
|
+
*/
|
|
10
|
+
export default class ObjectTree {
|
|
11
|
+
/**
|
|
12
|
+
* Create a tree wrapping a given plain object or array.
|
|
13
|
+
*
|
|
14
|
+
* @param {any} object The object/array to wrap.
|
|
15
|
+
*/
|
|
16
|
+
constructor(object) {
|
|
17
|
+
this.object = object;
|
|
18
|
+
this.parent = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Return the value for the given key.
|
|
23
|
+
*
|
|
24
|
+
* @param {any} key
|
|
25
|
+
*/
|
|
26
|
+
async get(key) {
|
|
27
|
+
// If the value is an array, we require that the key be one of its own
|
|
28
|
+
// properties: we don't want to return Array prototype methods like `map`
|
|
29
|
+
// and `find`.
|
|
30
|
+
if (
|
|
31
|
+
this.object instanceof Array &&
|
|
32
|
+
key &&
|
|
33
|
+
!this.object.hasOwnProperty(key)
|
|
34
|
+
) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let value = this.object[key];
|
|
39
|
+
|
|
40
|
+
const isPlain =
|
|
41
|
+
value instanceof Array ||
|
|
42
|
+
(isPlainObject(value) && !Tree.isAsyncTree(value));
|
|
43
|
+
if (isPlain) {
|
|
44
|
+
value = Reflect.construct(this.constructor, [value]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
48
|
+
value.parent = this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async isKeyForSubtree(key) {
|
|
55
|
+
const value = this.object[key];
|
|
56
|
+
return isPlainObject(value) || Tree.isAsyncTree(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Enumerate the object's keys.
|
|
61
|
+
*/
|
|
62
|
+
async keys() {
|
|
63
|
+
// Walk up the prototype chain to realm's Object.prototype.
|
|
64
|
+
let obj = this.object;
|
|
65
|
+
const objectPrototype = getRealmObjectPrototype(obj);
|
|
66
|
+
|
|
67
|
+
const result = new Set();
|
|
68
|
+
while (obj && obj !== objectPrototype) {
|
|
69
|
+
// Get the enumerable instance properties and the get/set properties.
|
|
70
|
+
const descriptors = Object.getOwnPropertyDescriptors(obj);
|
|
71
|
+
const propertyNames = Object.entries(descriptors)
|
|
72
|
+
.filter(
|
|
73
|
+
([name, descriptor]) =>
|
|
74
|
+
name !== "constructor" &&
|
|
75
|
+
(descriptor.enumerable ||
|
|
76
|
+
(descriptor.get !== undefined && descriptor.set !== undefined))
|
|
77
|
+
)
|
|
78
|
+
.map(([name]) => name);
|
|
79
|
+
for (const name of propertyNames) {
|
|
80
|
+
result.add(name);
|
|
81
|
+
}
|
|
82
|
+
obj = Object.getPrototypeOf(obj);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set the value for the given key. If the value is undefined, delete the key.
|
|
89
|
+
*
|
|
90
|
+
* @param {any} key
|
|
91
|
+
* @param {any} value
|
|
92
|
+
*/
|
|
93
|
+
async set(key, value) {
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
// Delete the key.
|
|
96
|
+
delete this.object[key];
|
|
97
|
+
} else {
|
|
98
|
+
// Set the value for the key.
|
|
99
|
+
this.object[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/SetTree.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Tree from "./Tree.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tree of Set objects.
|
|
5
|
+
*
|
|
6
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
7
|
+
* @implements {AsyncTree}
|
|
8
|
+
*/
|
|
9
|
+
export default class SetTree {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Set} set
|
|
12
|
+
*/
|
|
13
|
+
constructor(set) {
|
|
14
|
+
this.values = [...set];
|
|
15
|
+
this.parent = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get(key) {
|
|
19
|
+
let value = this.values[key];
|
|
20
|
+
|
|
21
|
+
if (value instanceof Set) {
|
|
22
|
+
value = Reflect.construct(this.constructor, [value]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
26
|
+
value.parent = this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async isKeyForSubtree(key) {
|
|
33
|
+
const value = this.values[key];
|
|
34
|
+
return value instanceof Set || Tree.isAsyncTree(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async keys() {
|
|
38
|
+
return this.values.keys();
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/SiteTree.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as Tree from "./Tree.js";
|
|
2
|
+
import * as keysJson from "./keysJson.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An HTTP/HTTPS site as a tree of ArrayBuffers.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
+
* @implements {AsyncTree}
|
|
9
|
+
*/
|
|
10
|
+
export default class SiteTree {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} href
|
|
13
|
+
*/
|
|
14
|
+
constructor(href = window?.location.href) {
|
|
15
|
+
if (href?.startsWith(".") && window?.location !== undefined) {
|
|
16
|
+
// URL represents a relative path; concatenate with current location.
|
|
17
|
+
href = new URL(href, window.location.href).href;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!href.endsWith("/")) {
|
|
21
|
+
// Add trailing slash; the URL is expected to represent a directory.
|
|
22
|
+
href += "/";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.href = href;
|
|
26
|
+
this.keysPromise = undefined;
|
|
27
|
+
this.parent = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async get(key) {
|
|
31
|
+
// If there is only one key and it's the empty string, and the site is
|
|
32
|
+
// explorable, we take the route as "index.html". With this and subsequent
|
|
33
|
+
// checks, we try to avoid sniffing the site to see if it's explorable, as
|
|
34
|
+
// that necessitates an extra network request per SiteTree instance. In many
|
|
35
|
+
// cases, that can be avoided.
|
|
36
|
+
if (key === "" && (await this.hasKeysJson())) {
|
|
37
|
+
key = "index.html";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const href = new URL(key, this.href).href;
|
|
41
|
+
|
|
42
|
+
// If the (possibly adjusted) route ends with a slash and the site is an
|
|
43
|
+
// explorable site, we return a tree for the indicated route.
|
|
44
|
+
if (href.endsWith("/") && (await this.hasKeysJson())) {
|
|
45
|
+
return Reflect.construct(this.constructor, [href]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fetch the data at the given route.
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await fetch(href);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (response.redirected && response.url.endsWith("/")) {
|
|
60
|
+
// If the response is redirected to a route that ends with a slash, and
|
|
61
|
+
// the site is an explorable site, we return a tree for the new route.
|
|
62
|
+
if (await this.hasKeysJson()) {
|
|
63
|
+
return Reflect.construct(this.constructor, [response.url]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return response.arrayBuffer();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async getKeyDictionary() {
|
|
71
|
+
// We use a promise to ensure we only check for keys once.
|
|
72
|
+
const href = new URL(".keys.json", this.href).href;
|
|
73
|
+
this.keysPromise ??= fetch(href)
|
|
74
|
+
.then((response) => (response.ok ? response.text() : null))
|
|
75
|
+
.then((text) => {
|
|
76
|
+
try {
|
|
77
|
+
return text ? keysJson.parse(text) : null;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Got a response, but it's not JSON. Most likely the site doesn't
|
|
80
|
+
// actually have a .keys.json file, and is returning a Not Found page,
|
|
81
|
+
// but hasn't set the correct 404 status code.
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return this.keysPromise;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async hasKeysJson() {
|
|
89
|
+
const keyDictionary = await this.getKeyDictionary();
|
|
90
|
+
return keyDictionary !== null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async isKeyForSubtree(key) {
|
|
94
|
+
const keyDictionary = await this.getKeyDictionary();
|
|
95
|
+
if (keyDictionary) {
|
|
96
|
+
return keyDictionary[key];
|
|
97
|
+
} else {
|
|
98
|
+
// Expensive check, since this fetches the key's value.
|
|
99
|
+
const value = await this.get(key);
|
|
100
|
+
return Tree.isAsyncTree(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async keys() {
|
|
105
|
+
const keyDictionary = await this.getKeyDictionary();
|
|
106
|
+
return keyDictionary ? Object.keys(keyDictionary) : [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns a new `SiteTree` for the given relative route.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} path
|
|
113
|
+
* @returns {SiteTree}
|
|
114
|
+
*/
|
|
115
|
+
resolve(path) {
|
|
116
|
+
const href = new URL(path, this.href).href;
|
|
117
|
+
return Reflect.construct(this.constructor, [href]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async unpack() {
|
|
121
|
+
const response = await fetch(this.href);
|
|
122
|
+
return response.ok ? response.arrayBuffer() : undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/Tree.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AsyncMutableTree, AsyncTree } from "@weborigami/types";
|
|
2
|
+
import { PlainObject, ReduceFn, Treelike, ValueKeyFn } from "../index.ts";
|
|
3
|
+
|
|
4
|
+
export function assign(target: Treelike, source: Treelike): Promise<AsyncTree>;
|
|
5
|
+
export function clear(AsyncTree: AsyncMutableTree): Promise<void>;
|
|
6
|
+
export function entries(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
|
|
7
|
+
export function forEach(AsyncTree: AsyncTree, callbackfn: (value: any, key: any) => Promise<void>): Promise<void>;
|
|
8
|
+
export function from(obj: any): AsyncTree;
|
|
9
|
+
export function has(AsyncTree: AsyncTree, key: any): Promise<boolean>;
|
|
10
|
+
export function isAsyncMutableTree(obj: any): obj is AsyncMutableTree;
|
|
11
|
+
export function isAsyncTree(obj: any): obj is AsyncTree;
|
|
12
|
+
export function isKeyForSubtree(tree: AsyncTree, obj: any): Promise<boolean>;
|
|
13
|
+
export function isTreelike(obj: any): obj is Treelike;
|
|
14
|
+
export function map(tree: Treelike, valueMap: ValueKeyFn): AsyncTree;
|
|
15
|
+
export function mapReduce(tree: Treelike, mapFn: ValueKeyFn|null, reduceFn: ReduceFn): Promise<any>;
|
|
16
|
+
export function plain(tree: Treelike): Promise<PlainObject>;
|
|
17
|
+
export function remove(AsyncTree: AsyncMutableTree, key: any): Promise<boolean>;
|
|
18
|
+
export function toFunction(tree: Treelike): Function;
|
|
19
|
+
export function traverse(tree: Treelike, ...keys: any[]): Promise<any>;
|
|
20
|
+
export function traverseOrThrow(tree: Treelike, ...keys: any[]): Promise<any>;
|
|
21
|
+
export function traversePath(tree: Treelike, path: string): Promise<any>;
|
|
22
|
+
export function values(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
|
package/src/Tree.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import DeferredTree from "./DeferredTree.js";
|
|
2
|
+
import FunctionTree from "./FunctionTree.js";
|
|
3
|
+
import MapTree from "./MapTree.js";
|
|
4
|
+
import ObjectTree from "./ObjectTree.js";
|
|
5
|
+
import SetTree from "./SetTree.js";
|
|
6
|
+
import mapTransform from "./transforms/map.js";
|
|
7
|
+
import * as utilities from "./utilities.js";
|
|
8
|
+
import { castArrayLike, isPlainObject } from "./utilities.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper functions for working with async trees
|
|
12
|
+
*
|
|
13
|
+
* @typedef {import("../index.ts").PlainObject} PlainObject
|
|
14
|
+
* @typedef {import("../index.ts").ReduceFn} ReduceFn
|
|
15
|
+
* @typedef {import("../index.ts").Treelike} Treelike
|
|
16
|
+
* @typedef {import("../index.ts").ValueKeyFn} ValueKeyFn
|
|
17
|
+
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
18
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply the key/values pairs from the source tree to the target tree.
|
|
23
|
+
*
|
|
24
|
+
* If a key exists in both trees, and the values in both trees are
|
|
25
|
+
* subtrees, then the subtrees will be merged recursively. Otherwise, the
|
|
26
|
+
* value from the source tree will overwrite the value in the target tree.
|
|
27
|
+
*
|
|
28
|
+
* @param {AsyncMutableTree} target
|
|
29
|
+
* @param {AsyncTree} source
|
|
30
|
+
*/
|
|
31
|
+
export async function assign(target, source) {
|
|
32
|
+
const targetTree = from(target);
|
|
33
|
+
const sourceTree = from(source);
|
|
34
|
+
if (!isAsyncMutableTree(targetTree)) {
|
|
35
|
+
throw new TypeError("Target must be a mutable asynchronous tree");
|
|
36
|
+
}
|
|
37
|
+
// Fire off requests to update all keys, then wait for all of them to finish.
|
|
38
|
+
const keys = Array.from(await sourceTree.keys());
|
|
39
|
+
const promises = keys.map(async (key) => {
|
|
40
|
+
const sourceValue = await sourceTree.get(key);
|
|
41
|
+
if (isAsyncTree(sourceValue)) {
|
|
42
|
+
const targetValue = await targetTree.get(key);
|
|
43
|
+
if (isAsyncMutableTree(targetValue)) {
|
|
44
|
+
// Both source and target are trees; recurse.
|
|
45
|
+
await assign(targetValue, sourceValue);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Copy the value from the source to the target.
|
|
50
|
+
await /** @type {any} */ (targetTree).set(key, sourceValue);
|
|
51
|
+
});
|
|
52
|
+
await Promise.all(promises);
|
|
53
|
+
return targetTree;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Removes all entries from the tree.
|
|
58
|
+
*
|
|
59
|
+
* @param {AsyncMutableTree} tree
|
|
60
|
+
*/
|
|
61
|
+
export async function clear(tree) {
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
for (const key of await tree.keys()) {
|
|
64
|
+
await tree.set(key, undefined);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns a new `Iterator` object that contains a two-member array of `[key,
|
|
70
|
+
* value]` for each element in the specific node of the tree.
|
|
71
|
+
*
|
|
72
|
+
* @param {AsyncTree} tree
|
|
73
|
+
*/
|
|
74
|
+
export async function entries(tree) {
|
|
75
|
+
const keys = [...(await tree.keys())];
|
|
76
|
+
const promises = keys.map(async (key) => [key, await tree.get(key)]);
|
|
77
|
+
return Promise.all(promises);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calls callbackFn once for each key-value pair present in the specific node of
|
|
82
|
+
* the tree.
|
|
83
|
+
*
|
|
84
|
+
* @param {AsyncTree} tree
|
|
85
|
+
* @param {Function} callbackFn
|
|
86
|
+
*/
|
|
87
|
+
export async function forEach(tree, callbackFn) {
|
|
88
|
+
const keys = [...(await tree.keys())];
|
|
89
|
+
const promises = keys.map(async (key) => {
|
|
90
|
+
const value = await tree.get(key);
|
|
91
|
+
return callbackFn(value, key);
|
|
92
|
+
});
|
|
93
|
+
await Promise.all(promises);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Attempts to cast the indicated object to an async tree.
|
|
98
|
+
*
|
|
99
|
+
* @param {Treelike | Object} obj
|
|
100
|
+
* @returns {AsyncTree}
|
|
101
|
+
*/
|
|
102
|
+
export function from(obj) {
|
|
103
|
+
if (isAsyncTree(obj)) {
|
|
104
|
+
// Argument already supports the tree interface.
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
return obj;
|
|
107
|
+
} else if (typeof obj === "function") {
|
|
108
|
+
return new FunctionTree(obj);
|
|
109
|
+
} else if (obj instanceof Map) {
|
|
110
|
+
return new MapTree(obj);
|
|
111
|
+
} else if (obj instanceof Set) {
|
|
112
|
+
return new SetTree(obj);
|
|
113
|
+
} else if (obj && typeof obj === "object" && "unpack" in obj) {
|
|
114
|
+
// Invoke unpack and convert the result to a tree.
|
|
115
|
+
let result = obj.unpack();
|
|
116
|
+
return result instanceof Promise ? new DeferredTree(result) : from(result);
|
|
117
|
+
} else if (obj && typeof obj === "object") {
|
|
118
|
+
// An instance of some class.
|
|
119
|
+
return new ObjectTree(obj);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new TypeError("Couldn't convert argument to an async tree");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns a boolean indicating whether the specific node of the tree has a
|
|
127
|
+
* value for the given `key`.
|
|
128
|
+
*
|
|
129
|
+
* @param {AsyncTree} tree
|
|
130
|
+
* @param {any} key
|
|
131
|
+
*/
|
|
132
|
+
export async function has(tree, key) {
|
|
133
|
+
const value = await tree.get(key);
|
|
134
|
+
return value !== undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Return true if the indicated object is an async tree.
|
|
139
|
+
*
|
|
140
|
+
* @param {any} object
|
|
141
|
+
* @returns {obj is AsyncTree}
|
|
142
|
+
*/
|
|
143
|
+
export function isAsyncTree(object) {
|
|
144
|
+
return (
|
|
145
|
+
object &&
|
|
146
|
+
typeof object.get === "function" &&
|
|
147
|
+
typeof object.keys === "function"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Return true if the indicated object is an async mutable tree.
|
|
153
|
+
*
|
|
154
|
+
* @param {any} object
|
|
155
|
+
* @returns {obj is AsyncMutableTree}
|
|
156
|
+
*/
|
|
157
|
+
export function isAsyncMutableTree(object) {
|
|
158
|
+
return isAsyncTree(object) && typeof object.set === "function";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns true if the indicated object can be directly treated as an
|
|
163
|
+
* asynchronous tree. This includes:
|
|
164
|
+
*
|
|
165
|
+
* - An object that implements the AsyncTree interface (including
|
|
166
|
+
* AsyncTree instances)
|
|
167
|
+
* - An object that implements the `unpack()` method
|
|
168
|
+
* - A function
|
|
169
|
+
* - An `Array` instance
|
|
170
|
+
* - A `Map` instance
|
|
171
|
+
* - A `Set` instance
|
|
172
|
+
* - A plain object
|
|
173
|
+
*
|
|
174
|
+
* Note: the `from()` method accepts any JavaScript object, but `isTreeable`
|
|
175
|
+
* returns `false` for an object that isn't one of the above types.
|
|
176
|
+
*
|
|
177
|
+
* @param {any} object
|
|
178
|
+
*/
|
|
179
|
+
export function isTreelike(object) {
|
|
180
|
+
return (
|
|
181
|
+
isAsyncTree(object) ||
|
|
182
|
+
object instanceof Function ||
|
|
183
|
+
object instanceof Array ||
|
|
184
|
+
object instanceof Set ||
|
|
185
|
+
object?.unpack instanceof Function ||
|
|
186
|
+
isPlainObject(object)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Return true if the indicated key produces or is expected to produce an
|
|
192
|
+
* async tree.
|
|
193
|
+
*
|
|
194
|
+
* This defers to the tree's own isKeyForSubtree method. If not found, this
|
|
195
|
+
* gets the value of that key and returns true if the value is an async
|
|
196
|
+
* tree.
|
|
197
|
+
*/
|
|
198
|
+
export async function isKeyForSubtree(tree, key) {
|
|
199
|
+
if (tree.isKeyForSubtree) {
|
|
200
|
+
return tree.isKeyForSubtree(key);
|
|
201
|
+
}
|
|
202
|
+
const value = await tree.get(key);
|
|
203
|
+
return isAsyncTree(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Return a new tree with deeply-mapped values of the original tree.
|
|
208
|
+
*
|
|
209
|
+
* @param {Treelike} treelike
|
|
210
|
+
* @param {ValueKeyFn} valueMap
|
|
211
|
+
*/
|
|
212
|
+
export function map(treelike, valueMap) {
|
|
213
|
+
const tree = from(treelike);
|
|
214
|
+
return mapTransform({ deep: true, valueMap })(tree);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Map and reduce a tree.
|
|
219
|
+
*
|
|
220
|
+
* This is done in as parallel fashion as possible. Each of the tree's values
|
|
221
|
+
* will be requested in an async call, then those results will be awaited
|
|
222
|
+
* collectively. If a mapFn is provided, it will be invoked to convert each
|
|
223
|
+
* value to a mapped value; otherwise, values will be used as is. When the
|
|
224
|
+
* values have been obtained, all the values and keys will be passed to the
|
|
225
|
+
* reduceFn, which should consolidate those into a single result.
|
|
226
|
+
*
|
|
227
|
+
* @param {Treelike} treelike
|
|
228
|
+
* @param {ValueKeyFn|null} valueMap
|
|
229
|
+
* @param {ReduceFn} reduceFn
|
|
230
|
+
*/
|
|
231
|
+
export async function mapReduce(treelike, valueMap, reduceFn) {
|
|
232
|
+
const tree = from(treelike);
|
|
233
|
+
|
|
234
|
+
// We're going to fire off all the get requests in parallel, as quickly as
|
|
235
|
+
// the keys come in. We call the tree's `get` method for each key, but
|
|
236
|
+
// *don't* wait for it yet.
|
|
237
|
+
const keys = Array.from(await tree.keys());
|
|
238
|
+
const promises = keys.map((key) =>
|
|
239
|
+
tree.get(key).then((value) =>
|
|
240
|
+
// If the value is a subtree, recurse.
|
|
241
|
+
isAsyncTree(value)
|
|
242
|
+
? mapReduce(value, valueMap, reduceFn)
|
|
243
|
+
: valueMap
|
|
244
|
+
? valueMap(value, key, tree)
|
|
245
|
+
: value
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Wait for all the promises to resolve. Because the promises were captured
|
|
250
|
+
// in the same order as the keys, the values will also be in the same order.
|
|
251
|
+
const values = await Promise.all(promises);
|
|
252
|
+
|
|
253
|
+
// Reduce the values to a single result.
|
|
254
|
+
return reduceFn(values, keys);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Converts an asynchronous tree into a synchronous plain JavaScript object.
|
|
259
|
+
*
|
|
260
|
+
* The result's keys will be the tree's keys cast to strings. Any tree value
|
|
261
|
+
* that is itself a tree will be similarly converted to a plain object.
|
|
262
|
+
*
|
|
263
|
+
* @param {Treelike} treelike
|
|
264
|
+
* @returns {Promise<PlainObject|Array>}
|
|
265
|
+
*/
|
|
266
|
+
export async function plain(treelike) {
|
|
267
|
+
return mapReduce(treelike, null, (values, keys) => {
|
|
268
|
+
const object = {};
|
|
269
|
+
for (let i = 0; i < keys.length; i++) {
|
|
270
|
+
object[keys[i]] = values[i];
|
|
271
|
+
}
|
|
272
|
+
return castArrayLike(object);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Removes the value for the given key from the specific node of the tree.
|
|
278
|
+
*
|
|
279
|
+
* Note: The corresponding `Map` method is `delete`, not `remove`. However,
|
|
280
|
+
* `delete` is a reserved word in JavaScript, so this uses `remove` instead.
|
|
281
|
+
*
|
|
282
|
+
* @param {AsyncMutableTree} tree
|
|
283
|
+
* @param {any} key
|
|
284
|
+
*/
|
|
285
|
+
export async function remove(tree, key) {
|
|
286
|
+
const exists = await has(tree, key);
|
|
287
|
+
if (exists) {
|
|
288
|
+
await tree.set(key, undefined);
|
|
289
|
+
return true;
|
|
290
|
+
} else {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Returns a function that invokes the tree's `get` method.
|
|
297
|
+
*
|
|
298
|
+
* @param {Treelike} treelike
|
|
299
|
+
* @returns {Function}
|
|
300
|
+
*/
|
|
301
|
+
export function toFunction(treelike) {
|
|
302
|
+
const tree = from(treelike);
|
|
303
|
+
return tree.get.bind(tree);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Return the value at the corresponding path of keys.
|
|
308
|
+
*
|
|
309
|
+
* @this {any}
|
|
310
|
+
* @param {Treelike} treelike
|
|
311
|
+
* @param {...any} keys
|
|
312
|
+
*/
|
|
313
|
+
export async function traverse(treelike, ...keys) {
|
|
314
|
+
try {
|
|
315
|
+
// Await the result here so that, if the path doesn't exist, the catch
|
|
316
|
+
// block below will catch the exception.
|
|
317
|
+
return await traverseOrThrow.call(this, treelike, ...keys);
|
|
318
|
+
} catch (/** @type {any} */ error) {
|
|
319
|
+
if (error instanceof TraverseError) {
|
|
320
|
+
return undefined;
|
|
321
|
+
} else {
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Return the value at the corresponding path of keys. Throw if any interior
|
|
329
|
+
* step of the path doesn't lead to a result.
|
|
330
|
+
*
|
|
331
|
+
* @param {Treelike} treelike
|
|
332
|
+
* @param {...any} keys
|
|
333
|
+
*/
|
|
334
|
+
export async function traverseOrThrow(treelike, ...keys) {
|
|
335
|
+
// Start our traversal at the root of the tree.
|
|
336
|
+
/** @type {any} */
|
|
337
|
+
let value = treelike;
|
|
338
|
+
|
|
339
|
+
// Process each key in turn.
|
|
340
|
+
// If the value is ever undefined, short-circuit the traversal.
|
|
341
|
+
const remainingKeys = keys.slice();
|
|
342
|
+
while (remainingKeys.length > 0) {
|
|
343
|
+
if (value === undefined) {
|
|
344
|
+
const keyStrings = keys.map((key) => String(key));
|
|
345
|
+
throw new TraverseError(
|
|
346
|
+
`Couldn't traverse the path: ${keyStrings.join("/")}`,
|
|
347
|
+
value,
|
|
348
|
+
keys
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Get the next key.
|
|
353
|
+
const key = remainingKeys.shift();
|
|
354
|
+
|
|
355
|
+
// An empty string as the last key is a special case.
|
|
356
|
+
if (key === "" && remainingKeys.length === 0) {
|
|
357
|
+
// Unpack the value if it defines an `unpack` function, otherwise return
|
|
358
|
+
// the value itself.
|
|
359
|
+
value = typeof value.unpack === "function" ? await value.unpack() : value;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Someone is trying to traverse the value, so they mean to treat it as a
|
|
364
|
+
// tree. If it's not already a tree, cast it to one.
|
|
365
|
+
const tree = from(value);
|
|
366
|
+
|
|
367
|
+
// Get the value for the key.
|
|
368
|
+
value = await tree.get(key);
|
|
369
|
+
}
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Given a slash-separated path like "foo/bar", traverse the keys "foo" and
|
|
375
|
+
* "bar" and return the resulting value.
|
|
376
|
+
*
|
|
377
|
+
* @param {Treelike} tree
|
|
378
|
+
* @param {string} path
|
|
379
|
+
*/
|
|
380
|
+
export async function traversePath(tree, path) {
|
|
381
|
+
const keys = utilities.keysFromPath(path);
|
|
382
|
+
return traverse(tree, ...keys);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Error class thrown by traverseOrThrow()
|
|
386
|
+
class TraverseError extends ReferenceError {
|
|
387
|
+
constructor(message, tree, keys) {
|
|
388
|
+
super(message);
|
|
389
|
+
this.tree = tree;
|
|
390
|
+
this.name = "TraverseError";
|
|
391
|
+
this.keys = keys;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Return the values in the specific node of the tree.
|
|
397
|
+
*
|
|
398
|
+
* @param {AsyncTree} tree
|
|
399
|
+
*/
|
|
400
|
+
export async function values(tree) {
|
|
401
|
+
const keys = [...(await tree.keys())];
|
|
402
|
+
const promises = keys.map(async (key) => tree.get(key));
|
|
403
|
+
return Promise.all(promises);
|
|
404
|
+
}
|