@weborigami/async-tree 0.0.66-beta.2 → 0.0.67-beta.1
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 -1
- package/package.json +2 -2
- package/src/DeferredTree.js +4 -2
- package/src/{OpenSiteTree.js → ExplorableSiteTree.js} +12 -1
- package/src/FileTree.js +4 -1
- package/src/SiteTree.js +16 -2
- package/src/Tree.js +6 -3
- package/src/operations/cache.js +11 -5
- package/test/{OpenSiteTree.test.js → ExplorableSiteTree.test.js} +9 -9
- package/test/FileTree.test.js +19 -2
package/main.js
CHANGED
|
@@ -6,9 +6,9 @@ export { default as FunctionTree } from "./src/FunctionTree.js";
|
|
|
6
6
|
export { default as MapTree } from "./src/MapTree.js";
|
|
7
7
|
// Skip BrowserFileTree.js, which is browser-only.
|
|
8
8
|
export { default as DeepMapTree } from "./src/DeepMapTree.js";
|
|
9
|
+
export { default as ExplorableSiteTree } from "./src/ExplorableSiteTree.js";
|
|
9
10
|
export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
|
|
10
11
|
export * as jsonKeys from "./src/jsonKeys.js";
|
|
11
|
-
export { default as OpenSiteTree } from "./src/OpenSiteTree.js";
|
|
12
12
|
export { default as cache } from "./src/operations/cache.js";
|
|
13
13
|
export { default as concat } from "./src/operations/concat.js";
|
|
14
14
|
export { default as deepMerge } from "./src/operations/deepMerge.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.67-beta.1",
|
|
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.6.2"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.0.
|
|
14
|
+
"@weborigami/types": "0.0.67-beta.1"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/src/DeferredTree.js
CHANGED
|
@@ -14,12 +14,14 @@ import { Tree } from "./internal.js";
|
|
|
14
14
|
export default class DeferredTree {
|
|
15
15
|
/**
|
|
16
16
|
* @param {Function|Promise<any>} loader
|
|
17
|
+
* @param {{ deep?: boolean }} [options]
|
|
17
18
|
*/
|
|
18
|
-
constructor(loader) {
|
|
19
|
+
constructor(loader, options) {
|
|
19
20
|
this.loader = loader;
|
|
20
21
|
this.treePromise = null;
|
|
21
22
|
this._tree = null;
|
|
22
23
|
this._parentUntilLoaded = null;
|
|
24
|
+
this._deep = options?.deep ?? false;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
async get(key) {
|
|
@@ -60,7 +62,7 @@ export default class DeferredTree {
|
|
|
60
62
|
|
|
61
63
|
// Use a promise to ensure the treelike is only converted to a tree once.
|
|
62
64
|
this.treePromise ??= this.loadResult().then((treelike) => {
|
|
63
|
-
this._tree = Tree.from(treelike);
|
|
65
|
+
this._tree = Tree.from(treelike, { deep: this._deep });
|
|
64
66
|
if (this._parentUntilLoaded) {
|
|
65
67
|
// Now that the tree has been loaded, we can set its parent if it hasn't
|
|
66
68
|
// already been set.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import SiteTree from "./SiteTree.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A [SiteTree](SiteTree.html) that implements the [JSON Keys](jsonKeys.html)
|
|
5
|
+
* protocol. This enables a `keys()` method that can return the keys of a site
|
|
6
|
+
* route even though such a mechanism is not built into the HTTP protocol.
|
|
7
|
+
*/
|
|
8
|
+
export default class ExplorableSiteTree extends SiteTree {
|
|
4
9
|
constructor(...args) {
|
|
5
10
|
super(...args);
|
|
6
11
|
this.serverKeysPromise = undefined;
|
|
@@ -24,6 +29,12 @@ export default class OpenSiteTree extends SiteTree {
|
|
|
24
29
|
return this.serverKeysPromise;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Returns the keys of the site route. For this to work, the route must have a
|
|
34
|
+
* `.keys.json` file that contains a JSON array of string keys.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Promise<Iterable<string>>}
|
|
37
|
+
*/
|
|
27
38
|
async keys() {
|
|
28
39
|
const serverKeys = await this.getServerKeys();
|
|
29
40
|
return serverKeys ?? [];
|
package/src/FileTree.js
CHANGED
|
@@ -190,8 +190,11 @@ export default class FileTree {
|
|
|
190
190
|
// Special case: empty object means create an empty directory.
|
|
191
191
|
await fs.mkdir(destPath, { recursive: true });
|
|
192
192
|
} else if (Tree.isTreelike(value)) {
|
|
193
|
-
// Treat value as a
|
|
193
|
+
// Treat value as a subtree and write it out as a subdirectory.
|
|
194
194
|
const destTree = Reflect.construct(this.constructor, [destPath]);
|
|
195
|
+
// Create the directory here, even if the subtree is empty.
|
|
196
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
197
|
+
// Write out the subtree.
|
|
195
198
|
await Tree.assign(destTree, value);
|
|
196
199
|
} else {
|
|
197
200
|
const typeName = value?.constructor?.name ?? "unknown";
|
package/src/SiteTree.js
CHANGED
|
@@ -35,14 +35,20 @@ export default class SiteTree {
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
|
|
38
|
+
// A key with a trailing slash and no extension is for a folder; return a
|
|
39
|
+
// subtree without making a network request.
|
|
40
|
+
if (trailingSlash.has(key) && !key.includes(".")) {
|
|
40
41
|
const href = new URL(key, this.href).href;
|
|
41
42
|
const value = Reflect.construct(this.constructor, [href]);
|
|
42
43
|
setParent(value, this);
|
|
43
44
|
return value;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// HACK: For now we don't allow lookup of Origami extension handlers.
|
|
48
|
+
if (key.endsWith("_handler")) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
const href = new URL(key, this.href).href;
|
|
47
53
|
|
|
48
54
|
// Fetch the data at the given route.
|
|
@@ -56,6 +62,14 @@ export default class SiteTree {
|
|
|
56
62
|
return this.processResponse(response);
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Returns an empty set of keys.
|
|
67
|
+
*
|
|
68
|
+
* For a variation of `SiteTree` that can return the keys for a site route,
|
|
69
|
+
* see [ExplorableSiteTree](ExplorableSiteTree.html).
|
|
70
|
+
*
|
|
71
|
+
* @returns {Promise<Iterable<string>>}
|
|
72
|
+
*/
|
|
59
73
|
async keys() {
|
|
60
74
|
return [];
|
|
61
75
|
}
|
package/src/Tree.js
CHANGED
|
@@ -110,10 +110,11 @@ export async function forEach(tree, callbackFn) {
|
|
|
110
110
|
* parent of the new tree.
|
|
111
111
|
*
|
|
112
112
|
* @param {Treelike | Object} object
|
|
113
|
-
* @param {{ deep?:
|
|
113
|
+
* @param {{ deep?: boolean, parent?: AsyncTree|null }} [options]
|
|
114
114
|
* @returns {AsyncTree}
|
|
115
115
|
*/
|
|
116
116
|
export function from(object, options = {}) {
|
|
117
|
+
const deep = options.deep ?? false;
|
|
117
118
|
let tree;
|
|
118
119
|
if (isAsyncTree(object)) {
|
|
119
120
|
// Argument already supports the tree interface.
|
|
@@ -126,13 +127,13 @@ export function from(object, options = {}) {
|
|
|
126
127
|
} else if (object instanceof Set) {
|
|
127
128
|
tree = new SetTree(object);
|
|
128
129
|
} else if (isPlainObject(object) || object instanceof Array) {
|
|
129
|
-
tree =
|
|
130
|
+
tree = deep ? new DeepObjectTree(object) : new ObjectTree(object);
|
|
130
131
|
} else if (isUnpackable(object)) {
|
|
131
132
|
async function AsyncFunction() {} // Sample async function
|
|
132
133
|
tree =
|
|
133
134
|
object.unpack instanceof AsyncFunction.constructor
|
|
134
135
|
? // Async unpack: return a deferred tree.
|
|
135
|
-
new DeferredTree(object.unpack)
|
|
136
|
+
new DeferredTree(object.unpack, { deep })
|
|
136
137
|
: // Synchronous unpack: cast the result of unpack() to a tree.
|
|
137
138
|
from(object.unpack());
|
|
138
139
|
} else if (object && typeof object === "object") {
|
|
@@ -320,6 +321,8 @@ export async function paths(treelike, base = "") {
|
|
|
320
321
|
* The result's keys will be the tree's keys cast to strings. Any tree value
|
|
321
322
|
* that is itself a tree will be similarly converted to a plain object.
|
|
322
323
|
*
|
|
324
|
+
* Any trailing slashes in keys will be removed.
|
|
325
|
+
*
|
|
323
326
|
* @param {Treelike} treelike
|
|
324
327
|
* @returns {Promise<PlainObject|Array>}
|
|
325
328
|
*/
|
package/src/operations/cache.js
CHANGED
|
@@ -60,16 +60,22 @@ export default function treeCache(
|
|
|
60
60
|
let value = await source.get(key);
|
|
61
61
|
if (value !== undefined) {
|
|
62
62
|
// If a filter is defined, does the key match the filter?
|
|
63
|
-
const filterValue = await filter
|
|
63
|
+
const filterValue = filter ? await filter.get(key) : undefined;
|
|
64
64
|
const filterMatch = !filter || filterValue !== undefined;
|
|
65
65
|
if (filterMatch) {
|
|
66
66
|
if (Tree.isAsyncTree(value)) {
|
|
67
67
|
// Construct merged tree for a tree result.
|
|
68
68
|
if (cacheValue === undefined) {
|
|
69
|
-
// Construct new container in cache
|
|
70
|
-
|
|
71
|
-
cacheValue
|
|
72
|
-
|
|
69
|
+
// Construct new empty container in cache
|
|
70
|
+
await cache.set(key, {});
|
|
71
|
+
cacheValue = await cache.get(key);
|
|
72
|
+
if (!Tree.isAsyncTree(cacheValue)) {
|
|
73
|
+
// Coerce to tree and then save it back to the cache. This is
|
|
74
|
+
// necessary, e.g., if cache is an ObjectTree; we want the
|
|
75
|
+
// subtree to also be an ObjectTree, not a plain object.
|
|
76
|
+
cacheValue = Tree.from(cacheValue);
|
|
77
|
+
await cache.set(key, cacheValue);
|
|
78
|
+
}
|
|
73
79
|
}
|
|
74
80
|
value = treeCache(value, cacheValue, filterValue);
|
|
75
81
|
} else {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { beforeEach, describe, mock, test } from "node:test";
|
|
3
|
-
import
|
|
3
|
+
import ExplorableSiteTree from "../src/ExplorableSiteTree.js";
|
|
4
4
|
import { Tree } from "../src/internal.js";
|
|
5
5
|
|
|
6
6
|
const textDecoder = new TextDecoder();
|
|
@@ -34,31 +34,31 @@ const mockResponses = {
|
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
describe("
|
|
37
|
+
describe("ExplorableSiteTree", () => {
|
|
38
38
|
beforeEach(() => {
|
|
39
39
|
mock.method(global, "fetch", mockFetch);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test("can get the keys of a tree", async () => {
|
|
43
|
-
const fixture = new
|
|
43
|
+
const fixture = new ExplorableSiteTree(mockHost);
|
|
44
44
|
const keys = await fixture.keys();
|
|
45
45
|
assert.deepEqual(Array.from(keys), ["about/", "index.html"]);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test("can get a plain value for a key", async () => {
|
|
49
|
-
const fixture = new
|
|
49
|
+
const fixture = new ExplorableSiteTree(mockHost);
|
|
50
50
|
const arrayBuffer = await fixture.get("index.html");
|
|
51
51
|
const text = textDecoder.decode(arrayBuffer);
|
|
52
52
|
assert.equal(text, "Home page");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
test("getting an unsupported key returns undefined", async () => {
|
|
56
|
-
const fixture = new
|
|
56
|
+
const fixture = new ExplorableSiteTree(mockHost);
|
|
57
57
|
assert.equal(await fixture.get("xyz"), undefined);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("getting a null/undefined key throws an exception", async () => {
|
|
61
|
-
const fixture = new
|
|
61
|
+
const fixture = new ExplorableSiteTree(mockHost);
|
|
62
62
|
await assert.rejects(async () => {
|
|
63
63
|
await fixture.get(null);
|
|
64
64
|
});
|
|
@@ -68,14 +68,14 @@ describe("OpenSiteTree", () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test("can return a new tree for a key that redirects", async () => {
|
|
71
|
-
const fixture = new
|
|
71
|
+
const fixture = new ExplorableSiteTree(mockHost);
|
|
72
72
|
const about = await fixture.get("about");
|
|
73
|
-
assert(about instanceof
|
|
73
|
+
assert(about instanceof ExplorableSiteTree);
|
|
74
74
|
assert.equal(about.href, "https://mock/about/");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
test("can convert a site to a plain object", async () => {
|
|
78
|
-
const fixture = new
|
|
78
|
+
const fixture = new ExplorableSiteTree(mockHost);
|
|
79
79
|
// Convert buffers to strings.
|
|
80
80
|
const strings = Tree.map(fixture, (value) => textDecoder.decode(value));
|
|
81
81
|
assert.deepEqual(await Tree.plain(strings), {
|
package/test/FileTree.test.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { describe, test } from "node:test";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import FileTree from "../src/FileTree.js";
|
|
7
|
-
import { Tree } from "../src/internal.js";
|
|
7
|
+
import { ObjectTree, Tree } from "../src/internal.js";
|
|
8
8
|
|
|
9
9
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const tempDirectory = path.join(dirname, "fixtures/temp");
|
|
@@ -81,7 +81,7 @@ describe("FileTree", async () => {
|
|
|
81
81
|
await removeTempDirectory();
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
test("
|
|
84
|
+
test("create subfolder via set() with empty object value", async () => {
|
|
85
85
|
await createTempDirectory();
|
|
86
86
|
|
|
87
87
|
// Write out new, empty folder called "empty".
|
|
@@ -98,6 +98,23 @@ describe("FileTree", async () => {
|
|
|
98
98
|
await removeTempDirectory();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
test("create subfolder via set() with empty tree value", async () => {
|
|
102
|
+
await createTempDirectory();
|
|
103
|
+
|
|
104
|
+
// Write out new, empty folder called "empty".
|
|
105
|
+
const tempFiles = new FileTree(tempDirectory);
|
|
106
|
+
await tempFiles.set("empty", new ObjectTree({}));
|
|
107
|
+
|
|
108
|
+
// Verify folder exists and has no contents.
|
|
109
|
+
const folderPath = path.join(tempDirectory, "empty");
|
|
110
|
+
const stats = await fs.stat(folderPath);
|
|
111
|
+
assert(stats.isDirectory());
|
|
112
|
+
const files = await fs.readdir(folderPath);
|
|
113
|
+
assert.deepEqual(files, []);
|
|
114
|
+
|
|
115
|
+
await removeTempDirectory();
|
|
116
|
+
});
|
|
117
|
+
|
|
101
118
|
test("can write out subfolder via set()", async () => {
|
|
102
119
|
await createTempDirectory();
|
|
103
120
|
|