@weborigami/async-tree 0.0.64-beta.1 → 0.0.64
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/index.ts +1 -1
- package/package.json +2 -2
- package/src/FunctionTree.js +2 -2
- package/src/Tree.d.ts +1 -1
- package/src/Tree.js +18 -5
- package/src/calendarTree.js +77 -0
- package/src/operations/cache.js +21 -6
- package/test/MapTree.test.js +2 -1
- package/test/Tree.test.js +7 -0
- package/test/calendarTree.test.js +111 -0
- package/test/operations/cache.test.js +10 -3
package/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type PlainObject = {
|
|
|
25
25
|
[key: string]: any;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export type ReduceFn = (values: any[], keys: any[]) => Promise<any>;
|
|
28
|
+
export type ReduceFn = (values: any[], keys: any[], tree: AsyncTree) => Promise<any>;
|
|
29
29
|
|
|
30
30
|
export type StringLike = string | HasString;
|
|
31
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.0.64
|
|
3
|
+
"version": "0.0.64",
|
|
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.64
|
|
14
|
+
"@weborigami/types": "0.0.64"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/src/FunctionTree.js
CHANGED
|
@@ -27,11 +27,11 @@ export default class FunctionTree {
|
|
|
27
27
|
this.fn.length <= 1
|
|
28
28
|
? // Function takes no arguments, one argument, or a variable number of
|
|
29
29
|
// arguments: invoke it.
|
|
30
|
-
await this.fn.call(
|
|
30
|
+
await this.fn.call(this.parent, key)
|
|
31
31
|
: // Bind the key to the first parameter. Subsequent get calls will
|
|
32
32
|
// eventually bind all parameters until only one remains. At that point,
|
|
33
33
|
// the above condition will apply and the function will be invoked.
|
|
34
|
-
Reflect.construct(this.constructor, [this.fn.bind(
|
|
34
|
+
Reflect.construct(this.constructor, [this.fn.bind(this.parent, key)]);
|
|
35
35
|
setParent(value, this);
|
|
36
36
|
return value;
|
|
37
37
|
}
|
package/src/Tree.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export function isTraversable(obj: any): boolean;
|
|
|
14
14
|
export function isTreelike(obj: any): obj is Treelike;
|
|
15
15
|
export function map(tree: Treelike, valueFn: ValueKeyFn): AsyncTree;
|
|
16
16
|
export function mapReduce(tree: Treelike, mapFn: ValueKeyFn | null, reduceFn: ReduceFn): Promise<any>;
|
|
17
|
-
export function paths(tree: Treelike, base
|
|
17
|
+
export function paths(tree: Treelike, base?: string): string[];
|
|
18
18
|
export function plain(tree: Treelike): Promise<PlainObject>;
|
|
19
19
|
export function remove(AsyncTree: AsyncMutableTree, key: any): Promise<boolean>;
|
|
20
20
|
export function toFunction(tree: Treelike): Function;
|
package/src/Tree.js
CHANGED
|
@@ -166,7 +166,14 @@ export async function has(tree, key) {
|
|
|
166
166
|
* @returns {obj is AsyncTree}
|
|
167
167
|
*/
|
|
168
168
|
export function isAsyncTree(obj) {
|
|
169
|
-
return
|
|
169
|
+
return (
|
|
170
|
+
obj &&
|
|
171
|
+
typeof obj.get === "function" &&
|
|
172
|
+
typeof obj.keys === "function" &&
|
|
173
|
+
// JavaScript Map look like trees but can't be extended the same way, so we
|
|
174
|
+
// report false.
|
|
175
|
+
!(obj instanceof Map)
|
|
176
|
+
);
|
|
170
177
|
}
|
|
171
178
|
|
|
172
179
|
/**
|
|
@@ -231,8 +238,9 @@ export function isTraversable(object) {
|
|
|
231
238
|
export function isTreelike(obj) {
|
|
232
239
|
return (
|
|
233
240
|
isAsyncTree(obj) ||
|
|
234
|
-
obj instanceof Function ||
|
|
235
241
|
obj instanceof Array ||
|
|
242
|
+
obj instanceof Function ||
|
|
243
|
+
obj instanceof Map ||
|
|
236
244
|
obj instanceof Set ||
|
|
237
245
|
isPlainObject(obj)
|
|
238
246
|
);
|
|
@@ -286,14 +294,14 @@ export async function mapReduce(treelike, valueFn, reduceFn) {
|
|
|
286
294
|
const values = await Promise.all(promises);
|
|
287
295
|
|
|
288
296
|
// Reduce the values to a single result.
|
|
289
|
-
return reduceFn(values, keys);
|
|
297
|
+
return reduceFn(values, keys, tree);
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
/**
|
|
293
301
|
* Returns slash-separated paths for all values in the tree.
|
|
294
302
|
*
|
|
295
303
|
* @param {Treelike} treelike
|
|
296
|
-
* @param {string} base
|
|
304
|
+
* @param {string?} base
|
|
297
305
|
*/
|
|
298
306
|
export async function paths(treelike, base = "") {
|
|
299
307
|
const tree = from(treelike);
|
|
@@ -310,6 +318,7 @@ export async function paths(treelike, base = "") {
|
|
|
310
318
|
}
|
|
311
319
|
return result;
|
|
312
320
|
}
|
|
321
|
+
|
|
313
322
|
/**
|
|
314
323
|
* Converts an asynchronous tree into a synchronous plain JavaScript object.
|
|
315
324
|
*
|
|
@@ -320,7 +329,11 @@ export async function paths(treelike, base = "") {
|
|
|
320
329
|
* @returns {Promise<PlainObject|Array>}
|
|
321
330
|
*/
|
|
322
331
|
export async function plain(treelike) {
|
|
323
|
-
return mapReduce(treelike, null, (values, keys) => {
|
|
332
|
+
return mapReduce(treelike, null, (values, keys, tree) => {
|
|
333
|
+
// Special case for an empty tree: if based on array, return array.
|
|
334
|
+
if (tree instanceof ObjectTree && keys.length === 0) {
|
|
335
|
+
return tree.object instanceof Array ? [] : {};
|
|
336
|
+
}
|
|
324
337
|
const object = {};
|
|
325
338
|
for (let i = 0; i < keys.length; i++) {
|
|
326
339
|
object[keys[i]] = values[i];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return a tree of years, months, and days from a start date to an end date.
|
|
3
|
+
*
|
|
4
|
+
* Both the start and end date can be provided in "YYYY-MM-DD", "YYYY-MM", or
|
|
5
|
+
* "YYYY" format. If a start year is provided, but a month is not, then the
|
|
6
|
+
* first month of the year will be used; if a start month is provided, but a day
|
|
7
|
+
* is not, then the first day of that month will be used. Similar logic applies
|
|
8
|
+
* to the end date, using the last month of the year or the last day of the
|
|
9
|
+
* month.
|
|
10
|
+
*
|
|
11
|
+
* If a start date is omitted, today will be used, likewise for the end date.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} [start] - Start date in "YYYY-MM-DD", "YYYY-MM", or "YYYY"
|
|
14
|
+
* format
|
|
15
|
+
* @param {string} [end] - End date in "YYYY-MM-DD", "YYYY-MM", or "YYYY" format
|
|
16
|
+
*/
|
|
17
|
+
export default function calendarTree(start, end) {
|
|
18
|
+
const startParts = start?.split("-") ?? [];
|
|
19
|
+
const endParts = end?.split("-") ?? [];
|
|
20
|
+
|
|
21
|
+
const today = new Date();
|
|
22
|
+
|
|
23
|
+
const startYear = startParts[0]
|
|
24
|
+
? parseInt(startParts[0])
|
|
25
|
+
: today.getFullYear();
|
|
26
|
+
const startMonth = startParts[1]
|
|
27
|
+
? parseInt(startParts[1])
|
|
28
|
+
: startParts[0]
|
|
29
|
+
? 1
|
|
30
|
+
: today.getMonth() + 1;
|
|
31
|
+
const startDay = startParts[2]
|
|
32
|
+
? parseInt(startParts[2])
|
|
33
|
+
: startParts[1]
|
|
34
|
+
? 1
|
|
35
|
+
: today.getDate();
|
|
36
|
+
|
|
37
|
+
const endYear = endParts[0] ? parseInt(endParts[0]) : today.getFullYear();
|
|
38
|
+
const endMonth = endParts[1]
|
|
39
|
+
? parseInt(endParts[1])
|
|
40
|
+
: endParts[0]
|
|
41
|
+
? 12
|
|
42
|
+
: today.getMonth() + 1;
|
|
43
|
+
const endDay = endParts[2]
|
|
44
|
+
? parseInt(endParts[2])
|
|
45
|
+
: endParts[1]
|
|
46
|
+
? daysInMonth(endYear, endMonth)
|
|
47
|
+
: today.getDate();
|
|
48
|
+
|
|
49
|
+
let years = {};
|
|
50
|
+
for (let year = startYear; year <= endYear; year++) {
|
|
51
|
+
let months = new Map();
|
|
52
|
+
const firstMonth = year === startYear ? startMonth : 1;
|
|
53
|
+
const lastMonth = year === endYear ? endMonth : 12;
|
|
54
|
+
for (let month = firstMonth; month <= lastMonth; month++) {
|
|
55
|
+
const monthPadded = month.toString().padStart(2, "0");
|
|
56
|
+
let days = new Map();
|
|
57
|
+
const firstDay =
|
|
58
|
+
year === startYear && month === startMonth ? startDay : 1;
|
|
59
|
+
const lastDay =
|
|
60
|
+
year === endYear && month === endMonth
|
|
61
|
+
? endDay
|
|
62
|
+
: daysInMonth(year, month);
|
|
63
|
+
for (let day = firstDay; day <= lastDay; day++) {
|
|
64
|
+
const dayPadded = day.toString().padStart(2, "0");
|
|
65
|
+
days.set(dayPadded, null);
|
|
66
|
+
}
|
|
67
|
+
months.set(monthPadded, days);
|
|
68
|
+
}
|
|
69
|
+
years[year] = months;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return years;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function daysInMonth(year, month) {
|
|
76
|
+
return new Date(year, month, 0).getDate();
|
|
77
|
+
}
|
package/src/operations/cache.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ObjectTree, Tree } from "../internal.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Caches values from a source tree in a second cache tree. Cache source tree
|
|
@@ -14,16 +14,30 @@ import { DeepObjectTree, Tree } from "../internal.js";
|
|
|
14
14
|
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
15
15
|
*
|
|
16
16
|
* @param {Treelike} sourceTreelike
|
|
17
|
-
* @param {AsyncMutableTree} [
|
|
17
|
+
* @param {AsyncMutableTree} [cacheTreelike]
|
|
18
18
|
* @param {Treelike} [filterTreelike]
|
|
19
19
|
* @returns {AsyncTree & { description: string }}
|
|
20
20
|
*/
|
|
21
|
-
export default function treeCache(
|
|
21
|
+
export default function treeCache(
|
|
22
|
+
sourceTreelike,
|
|
23
|
+
cacheTreelike,
|
|
24
|
+
filterTreelike
|
|
25
|
+
) {
|
|
22
26
|
const source = Tree.from(sourceTreelike);
|
|
23
27
|
const filter = filterTreelike ? Tree.from(filterTreelike) : undefined;
|
|
24
28
|
|
|
25
29
|
/** @type {AsyncMutableTree} */
|
|
26
|
-
|
|
30
|
+
let cache;
|
|
31
|
+
if (cacheTreelike) {
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
cache = Tree.from(cacheTreelike);
|
|
34
|
+
if (!Tree.isAsyncMutableTree(cache)) {
|
|
35
|
+
throw new Error("Cache tree must define a set() method.");
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
cache = new ObjectTree({});
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
let keys;
|
|
28
42
|
return {
|
|
29
43
|
description: "cache",
|
|
@@ -47,8 +61,9 @@ export default function treeCache(sourceTreelike, cacheTree, filterTreelike) {
|
|
|
47
61
|
// Construct merged tree for a tree result.
|
|
48
62
|
if (cacheValue === undefined) {
|
|
49
63
|
// Construct new container in cache
|
|
50
|
-
|
|
51
|
-
cacheValue =
|
|
64
|
+
cacheValue = new ObjectTree({});
|
|
65
|
+
cacheValue.parent = this;
|
|
66
|
+
await cache.set(key, cacheValue);
|
|
52
67
|
}
|
|
53
68
|
value = treeCache(value, cacheValue, filterValue);
|
|
54
69
|
} else {
|
package/test/MapTree.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import MapTree from "../src/MapTree.js";
|
|
4
|
+
import * as symbols from "../src/symbols.js";
|
|
4
5
|
|
|
5
6
|
describe("MapTree", () => {
|
|
6
7
|
test("can get the keys of the tree", async () => {
|
|
@@ -18,7 +19,7 @@ describe("MapTree", () => {
|
|
|
18
19
|
const map = new Map([["more", new Map([["a", 1]])]]);
|
|
19
20
|
const fixture = new MapTree(map);
|
|
20
21
|
const more = await fixture.get("more");
|
|
21
|
-
assert.equal(more.parent, fixture);
|
|
22
|
+
assert.equal(more[symbols.parent], fixture);
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
test("getting an unsupported key returns undefined", async () => {
|
package/test/Tree.test.js
CHANGED
|
@@ -261,6 +261,13 @@ describe("Tree", () => {
|
|
|
261
261
|
assert.deepEqual(plain, original);
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
test("plain() returns empty array or object for ObjectTree as necessary", async () => {
|
|
265
|
+
const tree = new ObjectTree({});
|
|
266
|
+
assert.deepEqual(await Tree.plain(tree), {});
|
|
267
|
+
const arrayTree = new ObjectTree([]);
|
|
268
|
+
assert.deepEqual(await Tree.plain(arrayTree), []);
|
|
269
|
+
});
|
|
270
|
+
|
|
264
271
|
test("plain() awaits async properties", async () => {
|
|
265
272
|
const object = {
|
|
266
273
|
get name() {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { toPlainValue } from "../main.js";
|
|
4
|
+
import calendarTree from "../src/calendarTree.js";
|
|
5
|
+
|
|
6
|
+
describe("calendarTree", () => {
|
|
7
|
+
test("without a start or end, returns a tree for today", async () => {
|
|
8
|
+
const tree = calendarTree();
|
|
9
|
+
const plain = await toPlainValue(tree);
|
|
10
|
+
const today = new Date();
|
|
11
|
+
const year = today.getFullYear();
|
|
12
|
+
const month = (today.getMonth() + 1).toString().padStart(2, "0");
|
|
13
|
+
const day = today.getDate().toString().padStart(2, "0");
|
|
14
|
+
assert.deepEqual(plain, {
|
|
15
|
+
[year]: {
|
|
16
|
+
[month]: {
|
|
17
|
+
[day]: null,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns a tree for a month range", async () => {
|
|
24
|
+
const tree = calendarTree("2025-01", "2025-02");
|
|
25
|
+
const plain = await toPlainValue(tree);
|
|
26
|
+
assert.deepEqual(plain, {
|
|
27
|
+
2025: {
|
|
28
|
+
"01": {
|
|
29
|
+
"01": null,
|
|
30
|
+
"02": null,
|
|
31
|
+
"03": null,
|
|
32
|
+
"04": null,
|
|
33
|
+
"05": null,
|
|
34
|
+
"06": null,
|
|
35
|
+
"07": null,
|
|
36
|
+
"08": null,
|
|
37
|
+
"09": null,
|
|
38
|
+
10: null,
|
|
39
|
+
11: null,
|
|
40
|
+
12: null,
|
|
41
|
+
13: null,
|
|
42
|
+
14: null,
|
|
43
|
+
15: null,
|
|
44
|
+
16: null,
|
|
45
|
+
17: null,
|
|
46
|
+
18: null,
|
|
47
|
+
19: null,
|
|
48
|
+
20: null,
|
|
49
|
+
21: null,
|
|
50
|
+
22: null,
|
|
51
|
+
23: null,
|
|
52
|
+
24: null,
|
|
53
|
+
25: null,
|
|
54
|
+
26: null,
|
|
55
|
+
27: null,
|
|
56
|
+
28: null,
|
|
57
|
+
29: null,
|
|
58
|
+
30: null,
|
|
59
|
+
31: null,
|
|
60
|
+
},
|
|
61
|
+
"02": {
|
|
62
|
+
"01": null,
|
|
63
|
+
"02": null,
|
|
64
|
+
"03": null,
|
|
65
|
+
"04": null,
|
|
66
|
+
"05": null,
|
|
67
|
+
"06": null,
|
|
68
|
+
"07": null,
|
|
69
|
+
"08": null,
|
|
70
|
+
"09": null,
|
|
71
|
+
10: null,
|
|
72
|
+
11: null,
|
|
73
|
+
12: null,
|
|
74
|
+
13: null,
|
|
75
|
+
14: null,
|
|
76
|
+
15: null,
|
|
77
|
+
16: null,
|
|
78
|
+
17: null,
|
|
79
|
+
18: null,
|
|
80
|
+
19: null,
|
|
81
|
+
20: null,
|
|
82
|
+
21: null,
|
|
83
|
+
22: null,
|
|
84
|
+
23: null,
|
|
85
|
+
24: null,
|
|
86
|
+
25: null,
|
|
87
|
+
26: null,
|
|
88
|
+
27: null,
|
|
89
|
+
28: null,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("returns a tree for a day range", async () => {
|
|
96
|
+
const tree = calendarTree("2025-02-27", "2025-03-02");
|
|
97
|
+
const plain = await toPlainValue(tree);
|
|
98
|
+
assert.deepEqual(plain, {
|
|
99
|
+
2025: {
|
|
100
|
+
"02": {
|
|
101
|
+
27: null,
|
|
102
|
+
28: null,
|
|
103
|
+
},
|
|
104
|
+
"03": {
|
|
105
|
+
"01": null,
|
|
106
|
+
"02": null,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
-
import { ObjectTree, Tree } from "../../src/internal.js";
|
|
3
|
+
import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
|
|
4
4
|
import cache from "../../src/operations/cache.js";
|
|
5
5
|
|
|
6
6
|
describe("cache", () => {
|
|
7
7
|
test("caches reads of values from one tree into another", async () => {
|
|
8
8
|
const objectCache = new ObjectTree({});
|
|
9
9
|
const fixture = cache(
|
|
10
|
-
|
|
10
|
+
new DeepObjectTree({
|
|
11
11
|
a: 1,
|
|
12
12
|
b: 2,
|
|
13
13
|
c: 3,
|
|
@@ -18,7 +18,7 @@ describe("cache", () => {
|
|
|
18
18
|
objectCache
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
const keys =
|
|
21
|
+
const keys = [...(await fixture.keys())];
|
|
22
22
|
assert.deepEqual(keys, ["a", "b", "c", "more"]);
|
|
23
23
|
|
|
24
24
|
assert.equal(await objectCache.get("a"), undefined);
|
|
@@ -28,6 +28,13 @@ describe("cache", () => {
|
|
|
28
28
|
assert.equal(await objectCache.get("b"), undefined);
|
|
29
29
|
assert.equal(await fixture.get("b"), 2);
|
|
30
30
|
assert.equal(await objectCache.get("b"), 2);
|
|
31
|
+
|
|
32
|
+
assert.equal(await objectCache.get("more"), undefined);
|
|
33
|
+
const more = await fixture.get("more");
|
|
34
|
+
assert.deepEqual([...(await more.keys())], ["d"]);
|
|
35
|
+
assert.equal(await more.get("d"), 4);
|
|
36
|
+
const moreCache = await objectCache.get("more");
|
|
37
|
+
assert.equal(await moreCache.get("d"), 4);
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
test("if a cache filter is supplied, it only caches values whose keys match the filter", async () => {
|