@weborigami/async-tree 0.0.40 → 0.0.42
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/DeepMapTree.js +23 -0
- package/src/DeferredTree.js +17 -0
- package/src/FileTree.js +21 -3
- package/src/FunctionTree.js +2 -1
- package/src/SiteTree.js +41 -2
- package/src/Tree.js +49 -20
- package/test/Tree.test.js +14 -9
package/main.js
CHANGED
|
@@ -6,6 +6,7 @@ export { default as FunctionTree } from "./src/FunctionTree.js";
|
|
|
6
6
|
export { default as MapTree } from "./src/MapTree.js";
|
|
7
7
|
export { default as ObjectTree } from "./src/ObjectTree.js";
|
|
8
8
|
// Skip BrowserFileTree.js, which is browser-only.
|
|
9
|
+
export { default as DeepMapTree } from "./src/DeepMapTree.js";
|
|
9
10
|
export { default as DeepObjectTree } from "./src/DeepObjectTree.js";
|
|
10
11
|
export { default as SetTree } from "./src/SetTree.js";
|
|
11
12
|
export { default as SiteTree } from "./src/SiteTree.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.42",
|
|
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.3.3"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.0.
|
|
14
|
+
"@weborigami/types": "0.0.42"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import MapTree from "./MapTree.js";
|
|
2
|
+
import * as Tree from "./Tree.js";
|
|
3
|
+
|
|
4
|
+
export default class DeepMapTree extends MapTree {
|
|
5
|
+
async get(key) {
|
|
6
|
+
let value = await super.get(key);
|
|
7
|
+
|
|
8
|
+
if (value instanceof Map) {
|
|
9
|
+
value = Reflect.construct(this.constructor, [value]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
13
|
+
value.parent = this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async isKeyForSubtree(key) {
|
|
20
|
+
const value = this.map.get[key];
|
|
21
|
+
return value instanceof Map || Tree.isAsyncTree(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/DeferredTree.js
CHANGED
|
@@ -50,6 +50,23 @@ export default class DeferredTree {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// HACK: The concept of scope is defined in Origami, not at the AsyncTree
|
|
54
|
+
// level. If a DeferredTree is used to wrap an OrigamiTree, the inner
|
|
55
|
+
// OrigamiTree will have a `scope` but not a `parent`. If someone asks the
|
|
56
|
+
// outer deferrred tree for a scope, they'd otherwise get `undefined`, which
|
|
57
|
+
// is incorrect. As a workaround, we introduce a `scope` getter here that
|
|
58
|
+
// defers to the inner tree, but we need to find a way to avoid having to
|
|
59
|
+
// introduce the concept of scope here.
|
|
60
|
+
get scope() {
|
|
61
|
+
return /** @type {any} */ (this._tree)?.scope;
|
|
62
|
+
}
|
|
63
|
+
set scope(scope) {
|
|
64
|
+
// If tree hasn't been loaded yet, setting scope has no effect.
|
|
65
|
+
if (this._tree) {
|
|
66
|
+
/** @type {any} */ (this._tree).scope = scope;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
async tree() {
|
|
54
71
|
if (this._tree) {
|
|
55
72
|
return this._tree;
|
package/src/FileTree.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import * as Tree from "./Tree.js";
|
|
5
5
|
import {
|
|
6
6
|
getRealmObjectPrototype,
|
|
@@ -18,11 +18,25 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
|
18
18
|
*/
|
|
19
19
|
export default class FileTree {
|
|
20
20
|
/**
|
|
21
|
-
* @param {string} location
|
|
21
|
+
* @param {string|URL} location
|
|
22
22
|
*/
|
|
23
23
|
constructor(location) {
|
|
24
|
+
if (location instanceof URL) {
|
|
25
|
+
location = location.href;
|
|
26
|
+
} else if (
|
|
27
|
+
!(
|
|
28
|
+
typeof location === "string" ||
|
|
29
|
+
/** @type {any} */ (location) instanceof String
|
|
30
|
+
)
|
|
31
|
+
) {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
`FileTree constructor needs a string or URL, received an instance of ${
|
|
34
|
+
/** @type {any} */ (location)?.constructor?.name
|
|
35
|
+
}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
24
38
|
this.dirname = location.startsWith("file://")
|
|
25
|
-
?
|
|
39
|
+
? fileURLToPath(location)
|
|
26
40
|
: path.resolve(process.cwd(), location);
|
|
27
41
|
this.parent = null;
|
|
28
42
|
}
|
|
@@ -154,6 +168,10 @@ export default class FileTree {
|
|
|
154
168
|
|
|
155
169
|
return this;
|
|
156
170
|
}
|
|
171
|
+
|
|
172
|
+
get url() {
|
|
173
|
+
return pathToFileURL(this.dirname);
|
|
174
|
+
}
|
|
157
175
|
}
|
|
158
176
|
|
|
159
177
|
/**
|
package/src/FunctionTree.js
CHANGED
|
@@ -23,7 +23,8 @@ export default class FunctionTree {
|
|
|
23
23
|
async get(key) {
|
|
24
24
|
const value =
|
|
25
25
|
this.fn.length <= 1
|
|
26
|
-
? // Function takes no arguments or
|
|
26
|
+
? // Function takes no arguments, one argument, or a variable number of
|
|
27
|
+
// arguments: invoke it.
|
|
27
28
|
await this.fn.call(null, key)
|
|
28
29
|
: // Bind the key to the first parameter. Subsequent get calls will
|
|
29
30
|
// eventually bind all parameters until only one remains. At that point,
|
package/src/SiteTree.js
CHANGED
|
@@ -2,7 +2,9 @@ import * as Tree from "./Tree.js";
|
|
|
2
2
|
import * as keysJson from "./keysJson.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* A tree of values obtained via HTTP/HTTPS calls. These values will be strings
|
|
6
|
+
* for HTTP responses with a MIME text type; otherwise they will be ArrayBuffer
|
|
7
|
+
* instances.
|
|
6
8
|
*
|
|
7
9
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
10
|
* @implements {AsyncTree}
|
|
@@ -64,7 +66,10 @@ export default class SiteTree {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
const mediaType = response.headers?.get("Content-Type");
|
|
70
|
+
return SiteTree.mediaTypeIsText(mediaType)
|
|
71
|
+
? response.text()
|
|
72
|
+
: response.arrayBuffer();
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
async getKeyDictionary() {
|
|
@@ -106,6 +111,36 @@ export default class SiteTree {
|
|
|
106
111
|
return keyDictionary ? Object.keys(keyDictionary) : [];
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
// Return true if the given media type is a standard text type.
|
|
115
|
+
static mediaTypeIsText(mediaType) {
|
|
116
|
+
if (!mediaType) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const regex = /^(?<type>[^/]+)\/(?<subtype>[^;]+)/;
|
|
120
|
+
const match = mediaType.match(regex);
|
|
121
|
+
if (!match) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const { type, subtype } = match.groups;
|
|
125
|
+
if (type === "text") {
|
|
126
|
+
return true;
|
|
127
|
+
} else if (type === "application") {
|
|
128
|
+
return (
|
|
129
|
+
subtype === "json" ||
|
|
130
|
+
subtype.endsWith("+json") ||
|
|
131
|
+
subtype.endsWith(".json") ||
|
|
132
|
+
subtype === "xml" ||
|
|
133
|
+
subtype.endsWith("+xml") ||
|
|
134
|
+
subtype.endsWith(".xml")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get path() {
|
|
141
|
+
return this.href;
|
|
142
|
+
}
|
|
143
|
+
|
|
109
144
|
/**
|
|
110
145
|
* Returns a new `SiteTree` for the given relative route.
|
|
111
146
|
*
|
|
@@ -121,4 +156,8 @@ export default class SiteTree {
|
|
|
121
156
|
const response = await fetch(this.href);
|
|
122
157
|
return response.ok ? response.arrayBuffer() : undefined;
|
|
123
158
|
}
|
|
159
|
+
|
|
160
|
+
get url() {
|
|
161
|
+
return new URL(this.href);
|
|
162
|
+
}
|
|
124
163
|
}
|
package/src/Tree.js
CHANGED
|
@@ -18,6 +18,8 @@ import { castArrayLike, isPlainObject } from "./utilities.js";
|
|
|
18
18
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
const treeModule = this;
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Apply the key/values pairs from the source tree to the target tree.
|
|
23
25
|
*
|
|
@@ -331,45 +333,72 @@ export async function traverse(treelike, ...keys) {
|
|
|
331
333
|
* Return the value at the corresponding path of keys. Throw if any interior
|
|
332
334
|
* step of the path doesn't lead to a result.
|
|
333
335
|
*
|
|
336
|
+
* @this {AsyncTree|null|undefined}
|
|
334
337
|
* @param {Treelike} treelike
|
|
335
338
|
* @param {...any} keys
|
|
336
339
|
*/
|
|
337
340
|
export async function traverseOrThrow(treelike, ...keys) {
|
|
341
|
+
if (!treelike) {
|
|
342
|
+
throw new TraverseError("Tried to traverse a null or undefined value");
|
|
343
|
+
}
|
|
344
|
+
|
|
338
345
|
// Start our traversal at the root of the tree.
|
|
339
346
|
/** @type {any} */
|
|
340
347
|
let value = treelike;
|
|
341
348
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
349
|
+
// If traversal operation was called with a `this` context, use that as the
|
|
350
|
+
// target for function calls.
|
|
351
|
+
const target = this === treeModule ? undefined : this;
|
|
352
|
+
|
|
353
|
+
// Process all the keys.
|
|
344
354
|
const remainingKeys = keys.slice();
|
|
355
|
+
let key;
|
|
345
356
|
while (remainingKeys.length > 0) {
|
|
346
357
|
if (value === undefined) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
// Attempted to traverse an undefined value
|
|
359
|
+
const message = key
|
|
360
|
+
? `${key} does not exist`
|
|
361
|
+
: `Couldn't traverse the path: ${keys
|
|
362
|
+
.map((key) => String(key))
|
|
363
|
+
.join("/")}`;
|
|
364
|
+
throw new TraverseError(message, treelike, keys);
|
|
353
365
|
}
|
|
354
366
|
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// An empty string as the last key is a special case.
|
|
359
|
-
if (key === "" && remainingKeys.length === 0) {
|
|
367
|
+
// Special case: one key left that's an empty string
|
|
368
|
+
if (remainingKeys.length === 1 && remainingKeys[0] === "") {
|
|
360
369
|
// Unpack the value if it defines an `unpack` function, otherwise return
|
|
361
370
|
// the value itself.
|
|
362
|
-
|
|
363
|
-
continue;
|
|
371
|
+
return typeof value.unpack === "function" ? await value.unpack() : value;
|
|
364
372
|
}
|
|
365
373
|
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
374
|
+
// If the value is not a function or async tree already, but can be
|
|
375
|
+
// unpacked, unpack it.
|
|
376
|
+
if (
|
|
377
|
+
!(value instanceof Function) &&
|
|
378
|
+
!isAsyncTree(value) &&
|
|
379
|
+
value.unpack instanceof Function
|
|
380
|
+
) {
|
|
381
|
+
value = await value.unpack();
|
|
382
|
+
}
|
|
369
383
|
|
|
370
|
-
|
|
371
|
-
|
|
384
|
+
if (value instanceof Function) {
|
|
385
|
+
// Value is a function: call it with the remaining keys.
|
|
386
|
+
const fn = value;
|
|
387
|
+
// We'll take as many keys as the function's length, but at least one.
|
|
388
|
+
let fnKeyCount = Math.max(fn.length, 1);
|
|
389
|
+
const args = remainingKeys.splice(0, fnKeyCount);
|
|
390
|
+
key = null;
|
|
391
|
+
value = await fn.call(target, ...args);
|
|
392
|
+
} else {
|
|
393
|
+
// Value is some other treelike object: cast it to a tree.
|
|
394
|
+
const tree = from(value);
|
|
395
|
+
// Get the next key.
|
|
396
|
+
key = remainingKeys.shift();
|
|
397
|
+
// Get the value for the key.
|
|
398
|
+
value = await tree.get(key);
|
|
399
|
+
}
|
|
372
400
|
}
|
|
401
|
+
|
|
373
402
|
return value;
|
|
374
403
|
}
|
|
375
404
|
|
package/test/Tree.test.js
CHANGED
|
@@ -285,6 +285,13 @@ describe("Tree", () => {
|
|
|
285
285
|
);
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
test("traverse() a function with fixed number of arguments", async () => {
|
|
289
|
+
const tree = (a, b) => ({
|
|
290
|
+
c: "Result",
|
|
291
|
+
});
|
|
292
|
+
assert.equal(await Tree.traverse(tree, "a", "b", "c"), "Result");
|
|
293
|
+
});
|
|
294
|
+
|
|
288
295
|
test("traverse() from one tree into another", async () => {
|
|
289
296
|
const tree = new ObjectTree({
|
|
290
297
|
a: {
|
|
@@ -298,15 +305,13 @@ describe("Tree", () => {
|
|
|
298
305
|
});
|
|
299
306
|
|
|
300
307
|
test("traversing a final empty string can unpack the last value", async () => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
unpackable,
|
|
309
|
-
});
|
|
308
|
+
const tree = {
|
|
309
|
+
unpackable: {
|
|
310
|
+
unpack() {
|
|
311
|
+
return "Content";
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
310
315
|
const result = await Tree.traverse(tree, "unpackable", "");
|
|
311
316
|
assert.equal(result, "Content");
|
|
312
317
|
});
|