@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 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-beta.1",
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-beta.1"
14
+ "@weborigami/types": "0.0.64"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
@@ -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(null, key)
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(null, key)]);
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: string): string[];
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 obj && typeof obj.get === "function" && typeof obj.keys === "function";
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
+ }
@@ -1,4 +1,4 @@
1
- import { DeepObjectTree, Tree } from "../internal.js";
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} [cacheTree]
17
+ * @param {AsyncMutableTree} [cacheTreelike]
18
18
  * @param {Treelike} [filterTreelike]
19
19
  * @returns {AsyncTree & { description: string }}
20
20
  */
21
- export default function treeCache(sourceTreelike, cacheTree, filterTreelike) {
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
- const cache = cacheTree ?? new DeepObjectTree({});
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
- await cache.set(key, {});
51
- cacheValue = await cache.get(key);
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 {
@@ -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
- Tree.from({
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 = Array.from(await fixture.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 () => {