@weborigami/async-tree 0.5.7 → 0.6.0

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.
Files changed (178) hide show
  1. package/browser.js +1 -1
  2. package/index.ts +31 -35
  3. package/main.js +1 -2
  4. package/package.json +4 -7
  5. package/shared.js +77 -12
  6. package/src/Tree.js +8 -2
  7. package/src/drivers/AsyncMap.js +210 -0
  8. package/src/drivers/{BrowserFileTree.js → BrowserFileMap.js} +36 -27
  9. package/src/drivers/{calendarTree.js → CalendarMap.js} +81 -62
  10. package/src/drivers/ConstantMap.js +30 -0
  11. package/src/drivers/DeepObjectMap.js +27 -0
  12. package/src/drivers/{ExplorableSiteTree.js → ExplorableSiteMap.js} +7 -7
  13. package/src/drivers/FileMap.js +245 -0
  14. package/src/drivers/{FunctionTree.js → FunctionMap.js} +19 -22
  15. package/src/drivers/ObjectMap.js +139 -0
  16. package/src/drivers/SetMap.js +13 -0
  17. package/src/drivers/{SiteTree.js → SiteMap.js} +16 -17
  18. package/src/drivers/SyncMap.js +245 -0
  19. package/src/jsonKeys.d.ts +2 -2
  20. package/src/jsonKeys.js +6 -5
  21. package/src/operations/addNextPrevious.js +35 -36
  22. package/src/operations/assign.js +30 -21
  23. package/src/operations/cache.js +29 -35
  24. package/src/operations/cachedKeyFunctions.js +1 -1
  25. package/src/operations/calendar.js +5 -0
  26. package/src/operations/clear.js +13 -12
  27. package/src/operations/constant.js +5 -0
  28. package/src/operations/deepEntries.js +23 -0
  29. package/src/operations/deepMap.js +9 -9
  30. package/src/operations/deepMerge.js +36 -25
  31. package/src/operations/deepReverse.js +23 -16
  32. package/src/operations/deepTake.js +7 -7
  33. package/src/operations/deepText.js +4 -4
  34. package/src/operations/deepValues.js +3 -6
  35. package/src/operations/deepValuesIterator.js +11 -11
  36. package/src/operations/delete.js +8 -12
  37. package/src/operations/entries.js +17 -10
  38. package/src/operations/filter.js +9 -7
  39. package/src/operations/first.js +12 -10
  40. package/src/operations/forEach.js +10 -13
  41. package/src/operations/from.js +31 -39
  42. package/src/operations/globKeys.js +22 -17
  43. package/src/operations/group.js +2 -2
  44. package/src/operations/groupBy.js +24 -22
  45. package/src/operations/has.js +7 -9
  46. package/src/operations/indent.js +2 -2
  47. package/src/operations/inners.js +19 -15
  48. package/src/operations/invokeFunctions.js +22 -10
  49. package/src/operations/isAsyncMutableTree.js +5 -12
  50. package/src/operations/isAsyncTree.js +5 -20
  51. package/src/operations/isMap.js +39 -0
  52. package/src/operations/isMaplike.js +34 -0
  53. package/src/operations/isReadOnlyMap.js +14 -0
  54. package/src/operations/isTraversable.js +3 -3
  55. package/src/operations/isTreelike.js +5 -30
  56. package/src/operations/json.js +4 -12
  57. package/src/operations/keys.js +17 -8
  58. package/src/operations/length.js +9 -8
  59. package/src/operations/map.js +27 -30
  60. package/src/operations/mapExtension.js +20 -16
  61. package/src/operations/mapReduce.js +22 -17
  62. package/src/operations/mask.js +31 -22
  63. package/src/operations/match.js +13 -9
  64. package/src/operations/merge.js +43 -35
  65. package/src/operations/paginate.js +26 -18
  66. package/src/operations/parent.js +7 -7
  67. package/src/operations/paths.js +8 -8
  68. package/src/operations/plain.js +6 -6
  69. package/src/operations/regExpKeys.js +21 -12
  70. package/src/operations/reverse.js +21 -15
  71. package/src/operations/root.js +6 -5
  72. package/src/operations/scope.js +31 -26
  73. package/src/operations/shuffle.js +23 -16
  74. package/src/operations/size.js +13 -0
  75. package/src/operations/sort.js +55 -40
  76. package/src/operations/sync.js +21 -0
  77. package/src/operations/take.js +23 -11
  78. package/src/operations/text.js +4 -4
  79. package/src/operations/toFunction.js +7 -7
  80. package/src/operations/traverse.js +4 -4
  81. package/src/operations/traverseOrThrow.js +13 -9
  82. package/src/operations/traversePath.js +2 -2
  83. package/src/operations/values.js +18 -9
  84. package/src/operations/withKeys.js +22 -16
  85. package/src/symbols.js +1 -0
  86. package/src/utilities/castArraylike.js +10 -2
  87. package/src/utilities/getMapArgument.js +38 -0
  88. package/src/utilities/getParent.js +2 -2
  89. package/src/utilities/isStringlike.js +7 -5
  90. package/src/utilities/setParent.js +7 -7
  91. package/src/utilities/toFunction.js +2 -2
  92. package/src/utilities/toPlainValue.js +22 -18
  93. package/test/SampleAsyncMap.js +34 -0
  94. package/test/browser/assert.js +20 -0
  95. package/test/browser/index.html +54 -21
  96. package/test/drivers/AsyncMap.test.js +119 -0
  97. package/test/drivers/{BrowserFileTree.test.js → BrowserFileMap.test.js} +42 -23
  98. package/test/drivers/{calendarTree.test.js → CalendarMap.test.js} +17 -19
  99. package/test/drivers/ConstantMap.test.js +15 -0
  100. package/test/drivers/DeepObjectMap.test.js +36 -0
  101. package/test/drivers/{ExplorableSiteTree.test.js → ExplorableSiteMap.test.js} +29 -14
  102. package/test/drivers/FileMap.test.js +185 -0
  103. package/test/drivers/FunctionMap.test.js +56 -0
  104. package/test/drivers/ObjectMap.test.js +166 -0
  105. package/test/drivers/SetMap.test.js +35 -0
  106. package/test/drivers/{SiteTree.test.js → SiteMap.test.js} +14 -10
  107. package/test/drivers/SyncMap.test.js +321 -0
  108. package/test/jsonKeys.test.js +2 -2
  109. package/test/operations/addNextPrevious.test.js +3 -2
  110. package/test/operations/assign.test.js +30 -35
  111. package/test/operations/cache.test.js +8 -6
  112. package/test/operations/cachedKeyFunctions.test.js +6 -5
  113. package/test/operations/clear.test.js +6 -27
  114. package/test/operations/deepEntries.test.js +32 -0
  115. package/test/operations/deepMerge.test.js +6 -5
  116. package/test/operations/deepReverse.test.js +2 -2
  117. package/test/operations/deepTake.test.js +2 -2
  118. package/test/operations/deepText.test.js +4 -4
  119. package/test/operations/deepValuesIterator.test.js +2 -2
  120. package/test/operations/delete.test.js +2 -2
  121. package/test/operations/extensionKeyFunctions.test.js +6 -5
  122. package/test/operations/filter.test.js +3 -3
  123. package/test/operations/from.test.js +23 -31
  124. package/test/operations/globKeys.test.js +9 -9
  125. package/test/operations/groupBy.test.js +6 -5
  126. package/test/operations/inners.test.js +4 -4
  127. package/test/operations/invokeFunctions.test.js +2 -2
  128. package/test/operations/isMap.test.js +15 -0
  129. package/test/operations/isMaplike.test.js +15 -0
  130. package/test/operations/json.test.js +2 -2
  131. package/test/operations/keys.test.js +16 -3
  132. package/test/operations/map.test.js +20 -18
  133. package/test/operations/mapExtension.test.js +6 -6
  134. package/test/operations/mapReduce.test.js +2 -2
  135. package/test/operations/mask.test.js +4 -3
  136. package/test/operations/match.test.js +2 -2
  137. package/test/operations/merge.test.js +15 -11
  138. package/test/operations/paginate.test.js +5 -5
  139. package/test/operations/parent.test.js +3 -3
  140. package/test/operations/paths.test.js +6 -6
  141. package/test/operations/plain.test.js +8 -8
  142. package/test/operations/regExpKeys.test.js +12 -11
  143. package/test/operations/reverse.test.js +4 -3
  144. package/test/operations/scope.test.js +6 -5
  145. package/test/operations/shuffle.test.js +3 -2
  146. package/test/operations/sort.test.js +7 -10
  147. package/test/operations/sync.test.js +43 -0
  148. package/test/operations/take.test.js +2 -2
  149. package/test/operations/toFunction.test.js +2 -2
  150. package/test/operations/traverse.test.js +4 -5
  151. package/test/operations/withKeys.test.js +2 -2
  152. package/test/utilities/setParent.test.js +6 -6
  153. package/test/utilities/toFunction.test.js +2 -2
  154. package/test/utilities/toPlainValue.test.js +51 -12
  155. package/src/drivers/DeepMapTree.js +0 -23
  156. package/src/drivers/DeepObjectTree.js +0 -18
  157. package/src/drivers/DeferredTree.js +0 -81
  158. package/src/drivers/FileTree.js +0 -276
  159. package/src/drivers/MapTree.js +0 -70
  160. package/src/drivers/ObjectTree.js +0 -158
  161. package/src/drivers/SetTree.js +0 -34
  162. package/src/drivers/constantTree.js +0 -19
  163. package/src/drivers/limitConcurrency.js +0 -63
  164. package/src/internal.js +0 -16
  165. package/src/utilities/getTreeArgument.js +0 -43
  166. package/test/drivers/DeepMapTree.test.js +0 -17
  167. package/test/drivers/DeepObjectTree.test.js +0 -35
  168. package/test/drivers/DeferredTree.test.js +0 -22
  169. package/test/drivers/FileTree.test.js +0 -192
  170. package/test/drivers/FunctionTree.test.js +0 -46
  171. package/test/drivers/MapTree.test.js +0 -59
  172. package/test/drivers/ObjectTree.test.js +0 -163
  173. package/test/drivers/SetTree.test.js +0 -44
  174. package/test/drivers/constantTree.test.js +0 -13
  175. package/test/drivers/limitConcurrency.test.js +0 -41
  176. package/test/operations/isAsyncMutableTree.test.js +0 -17
  177. package/test/operations/isAsyncTree.test.js +0 -26
  178. package/test/operations/isTreelike.test.js +0 -13
@@ -1,4 +1,5 @@
1
1
  import * as trailingSlash from "../trailingSlash.js";
2
+ import SyncMap from "./SyncMap.js";
2
3
 
3
4
  /**
4
5
  * Return a tree of years, months, and days from a start date to an end date.
@@ -14,41 +15,73 @@ import * as trailingSlash from "../trailingSlash.js";
14
15
  *
15
16
  * @typedef {string|undefined} CalendarOptionsDate
16
17
  * @typedef {( year: string, month: string, day: string ) => any} CalendarOptionsFn
18
+ * @typedef {{ year: number, month: number, day: number}} CalendarDateParts}
19
+ *
17
20
  * @param {{ end?: CalendarOptionsDate, start?: CalendarOptionsDate, value: CalendarOptionsFn }} options
18
21
  */
19
- export default function calendarTree(options) {
20
- const start = dateParts(options.start);
21
- const end = dateParts(options.end);
22
- const valueFn = options.value;
23
-
24
- // Fill in the missing parts of the start and end dates.
25
- const today = new Date();
26
-
27
- if (start.day === undefined) {
28
- start.day = start.year ? 1 : today.getDate();
29
- }
30
- if (start.month === undefined) {
31
- start.month = start.year ? 1 : today.getMonth() + 1;
32
- }
33
- if (start.year === undefined) {
34
- start.year = today.getFullYear();
22
+ export default class CalendarMap extends SyncMap {
23
+ constructor(options = {}) {
24
+ super();
25
+
26
+ /** @type {CalendarDateParts} */
27
+ // @ts-ignore
28
+ const start = dateParts(options.start);
29
+ /** @type {CalendarDateParts} */
30
+ // @ts-ignore
31
+ const end = dateParts(options.end);
32
+ const valueFn = options.value;
33
+
34
+ // Fill in the missing parts of the start and end dates.
35
+ const today = new Date();
36
+
37
+ if (start.day === undefined) {
38
+ start.day = start.year ? 1 : today.getDate();
39
+ }
40
+ if (start.month === undefined) {
41
+ start.month = start.year ? 1 : today.getMonth() + 1;
42
+ }
43
+ if (start.year === undefined) {
44
+ start.year = today.getFullYear();
45
+ }
46
+
47
+ if (end.day === undefined) {
48
+ end.day = end.month
49
+ ? daysInMonth(end.year, end.month)
50
+ : end.year
51
+ ? 31 // Last day of December
52
+ : today.getDate();
53
+ }
54
+ if (end.month === undefined) {
55
+ end.month = end.year ? 12 : today.getMonth() + 1;
56
+ }
57
+ if (end.year === undefined) {
58
+ end.year = today.getFullYear();
59
+ }
60
+
61
+ this.start = start;
62
+ this.end = end;
63
+ this.valueFn = valueFn ?? defaultValueFn;
35
64
  }
36
65
 
37
- if (end.day === undefined) {
38
- end.day = end.month
39
- ? daysInMonth(end.year, end.month)
40
- : end.year
41
- ? 31 // Last day of December
42
- : today.getDate();
66
+ get(year) {
67
+ year = parseInt(trailingSlash.remove(year));
68
+ return this.inRange(year)
69
+ ? monthsForYearMap(year, this.start, this.end, this.valueFn)
70
+ : undefined;
43
71
  }
44
- if (end.month === undefined) {
45
- end.month = end.year ? 12 : today.getMonth() + 1;
72
+
73
+ inRange(year) {
74
+ return year >= this.start.year && year <= this.end.year;
46
75
  }
47
- if (end.year === undefined) {
48
- end.year = today.getFullYear();
76
+
77
+ keys() {
78
+ return Array.from(
79
+ { length: this.end.year - this.start.year + 1 },
80
+ (_, i) => this.start.year + i
81
+ )[Symbol.iterator]();
49
82
  }
50
83
 
51
- return yearsTree(start, end, valueFn);
84
+ trailingSlashKeys = false;
52
85
  }
53
86
 
54
87
  function dateParts(date) {
@@ -64,9 +97,9 @@ function dateParts(date) {
64
97
  return { year, month, day };
65
98
  }
66
99
 
67
- function daysForMonthTree(year, month, start, end, valueFn) {
68
- return {
69
- async get(day) {
100
+ function daysForMonthMap(year, month, start, end, valueFn) {
101
+ return Object.assign(new SyncMap(), {
102
+ get(day) {
70
103
  day = parseInt(trailingSlash.remove(day));
71
104
  return this.inRange(day)
72
105
  ? valueFn(year.toString(), twoDigits(month), twoDigits(day))
@@ -101,28 +134,34 @@ function daysForMonthTree(year, month, start, end, valueFn) {
101
134
  }
102
135
  },
103
136
 
104
- async keys() {
137
+ *keys() {
105
138
  const days = Array.from(
106
139
  { length: daysInMonth(year, month) },
107
140
  (_, i) => i + 1
108
141
  );
109
- return days
142
+ yield* days
110
143
  .filter((day) => this.inRange(day))
111
144
  .map((day) => twoDigits(day));
112
145
  },
113
- };
146
+
147
+ trailingSlashKeys: false,
148
+ });
114
149
  }
115
150
 
116
151
  function daysInMonth(year, month) {
117
152
  return new Date(year, month, 0).getDate();
118
153
  }
119
154
 
120
- function monthsForYearTree(year, start, end, valueFn) {
121
- return {
122
- async get(month) {
155
+ function defaultValueFn(year, month, day) {
156
+ return `${year}-${month}-${day}`;
157
+ }
158
+
159
+ function monthsForYearMap(year, start, end, valueFn) {
160
+ return Object.assign(new SyncMap(), {
161
+ get(month) {
123
162
  month = parseInt(trailingSlash.remove(month));
124
163
  return this.inRange(month)
125
- ? daysForMonthTree(year, month, start, end, valueFn)
164
+ ? daysForMonthMap(year, month, start, end, valueFn)
126
165
  : undefined;
127
166
  },
128
167
 
@@ -138,37 +177,17 @@ function monthsForYearTree(year, start, end, valueFn) {
138
177
  }
139
178
  },
140
179
 
141
- async keys() {
180
+ *keys() {
142
181
  const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
143
- return months
182
+ yield* months
144
183
  .filter((month) => this.inRange(month))
145
184
  .map((month) => twoDigits(month));
146
185
  },
147
- };
186
+
187
+ trailingSlashKeys: false,
188
+ });
148
189
  }
149
190
 
150
191
  function twoDigits(number) {
151
192
  return number.toString().padStart(2, "0");
152
193
  }
153
-
154
- function yearsTree(start, end, valueFn) {
155
- return {
156
- async get(year) {
157
- year = parseInt(trailingSlash.remove(year));
158
- return this.inRange(year)
159
- ? monthsForYearTree(year, start, end, valueFn)
160
- : undefined;
161
- },
162
-
163
- inRange(year) {
164
- return year >= start.year && year <= end.year;
165
- },
166
-
167
- async keys() {
168
- return Array.from(
169
- { length: end.year - start.year + 1 },
170
- (_, i) => start.year + i
171
- );
172
- },
173
- };
174
- }
@@ -0,0 +1,30 @@
1
+ import * as trailingSlash from "../trailingSlash.js";
2
+ import SyncMap from "./SyncMap.js";
3
+
4
+ /**
5
+ * A tree that returns a constant value for any key. If the key ends with a
6
+ * slash, then the same type of subtree is returned.
7
+ *
8
+ * @param {any} constant
9
+ * @returns {SyncMap}
10
+ */
11
+ export default class ConstantTree extends SyncMap {
12
+ constructor(constant) {
13
+ super();
14
+ this.constant = constant;
15
+ }
16
+
17
+ get(key) {
18
+ return trailingSlash.has(key)
19
+ ? new ConstantTree(this.constant)
20
+ : this.constant;
21
+ }
22
+
23
+ keys() {
24
+ return [][Symbol.iterator]();
25
+ }
26
+
27
+ get trailingSlashKeys() {
28
+ return true;
29
+ }
30
+ }
@@ -0,0 +1,27 @@
1
+ import isMap from "../operations/isMap.js";
2
+ import isPlainObject from "../utilities/isPlainObject.js";
3
+ import ObjectMap from "./ObjectMap.js";
4
+
5
+ export default class DeepObjectMap extends ObjectMap {
6
+ // Implement delete (and set) to keep the Map read-write
7
+ delete(key) {
8
+ return super.delete(key);
9
+ }
10
+
11
+ get(key) {
12
+ let value = super.get(key);
13
+ if (value instanceof Array || isPlainObject(value)) {
14
+ value = Reflect.construct(this.constructor, [value]);
15
+ }
16
+ return value;
17
+ }
18
+
19
+ isSubtree(value) {
20
+ return value instanceof Array || isPlainObject(value) || isMap(value);
21
+ }
22
+
23
+ // See delete()
24
+ set(key, value) {
25
+ return super.set(key, value);
26
+ }
27
+ }
@@ -1,11 +1,11 @@
1
- import SiteTree from "./SiteTree.js";
1
+ import SiteMap from "./SiteMap.js";
2
2
 
3
3
  /**
4
- * A [SiteTree](SiteTree.html) that implements the [JSON Keys](jsonKeys.html)
4
+ * A [SiteMap](SiteMap.html) that implements the [JSON Keys](jsonKeys.html)
5
5
  * protocol. This enables a `keys()` method that can return the keys of a site
6
6
  * route even though such a mechanism is not built into the HTTP protocol.
7
7
  */
8
- export default class ExplorableSiteTree extends SiteTree {
8
+ export default class ExplorableSiteMap extends SiteMap {
9
9
  /**
10
10
  * @param {string} href
11
11
  */
@@ -24,12 +24,12 @@ export default class ExplorableSiteTree extends SiteTree {
24
24
  .then((response) => (response.ok ? response.text() : null))
25
25
  .then((text) => {
26
26
  try {
27
- return text ? JSON.parse(text) : null;
27
+ return text ? JSON.parse(text) : [];
28
28
  } catch (error) {
29
29
  // Got a response, but it's not JSON. Most likely the site doesn't
30
30
  // actually have a .keys.json file, and is returning a Not Found page,
31
31
  // but hasn't set the correct 404 status code.
32
- return null;
32
+ return [];
33
33
  }
34
34
  });
35
35
  return this.serverKeysPromise;
@@ -39,9 +39,9 @@ export default class ExplorableSiteTree extends SiteTree {
39
39
  * Returns the keys of the site route. For this to work, the route must have a
40
40
  * `.keys.json` file that contains a JSON array of string keys.
41
41
  */
42
- async keys() {
42
+ async *keys() {
43
43
  const serverKeys = await this.getServerKeys();
44
- return serverKeys ?? [];
44
+ yield* serverKeys;
45
45
  }
46
46
 
47
47
  processResponse(response) {
@@ -0,0 +1,245 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { hiddenFileNames } from "../constants.js";
5
+ import from from "../operations/from.js";
6
+ import isMaplike from "../operations/isMaplike.js";
7
+ import * as trailingSlash from "../trailingSlash.js";
8
+ import isPacked from "../utilities/isPacked.js";
9
+ import isStringlike from "../utilities/isStringlike.js";
10
+ import naturalOrder from "../utilities/naturalOrder.js";
11
+ import setParent from "../utilities/setParent.js";
12
+ import SyncMap from "./SyncMap.js";
13
+
14
+ /**
15
+ * A file system folder as a Map.
16
+ *
17
+ * File values are returned as Uint8Array instances. The underlying Node fs API
18
+ * returns file contents as instances of the Node-specific Buffer class, but
19
+ * that class has some incompatible method implementations; see
20
+ * https://nodejs.org/api/buffer.html#buffers-and-typedarrays. For greater
21
+ * compatibility, files are returned as standard Uint8Array instances instead.
22
+ */
23
+ export default class FileMap extends SyncMap {
24
+ constructor(location) {
25
+ if (location instanceof URL) {
26
+ location = location.href;
27
+ } else if (
28
+ !(
29
+ typeof location === "string" ||
30
+ /** @type {any} */ (location) instanceof String
31
+ )
32
+ ) {
33
+ throw new TypeError("FileMap constructor needs a string or URL");
34
+ }
35
+
36
+ super();
37
+ this.dirname = location.startsWith("file://")
38
+ ? fileURLToPath(location)
39
+ : path.resolve(process.cwd(), location);
40
+ }
41
+
42
+ delete(key) {
43
+ if (key === "" || key == null) {
44
+ // Can't have a file with no name or a nullish name
45
+ throw new Error("delete: key was empty or nullish");
46
+ }
47
+
48
+ // What file or directory are we going to delete?
49
+ const stringKey = key != null ? String(key) : "";
50
+ const baseKey = trailingSlash.remove(stringKey);
51
+ const destPath = path.resolve(this.dirname, baseKey);
52
+
53
+ try {
54
+ fs.rmSync(destPath, { recursive: true });
55
+ return true;
56
+ } catch (/** @type {any} */ error) {
57
+ if (error.code === "ENOENT") {
58
+ return false; // File or directory didn't exist
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ get(key) {
65
+ if (key == null) {
66
+ // Reject nullish key
67
+ throw new ReferenceError(
68
+ `${this.constructor.name}: Cannot get a null or undefined key.`
69
+ );
70
+ }
71
+ if (key === "") {
72
+ // Can't have a file with no name
73
+ return undefined;
74
+ }
75
+
76
+ key = trailingSlash.remove(key); // normalize key
77
+ const filePath = path.resolve(this.dirname, key);
78
+
79
+ let stats;
80
+ try {
81
+ stats = fs.statSync(filePath);
82
+ } catch (/** @type {any} */ error) {
83
+ if (error.code === "ENOENT" /* File not found */) {
84
+ return undefined;
85
+ }
86
+ throw error;
87
+ }
88
+
89
+ let value;
90
+ if (stats.isDirectory()) {
91
+ // Return subdirectory as an instance of this class
92
+ value = Reflect.construct(this.constructor, [filePath]);
93
+ } else {
94
+ // Return file contents as a standard Uint8Array
95
+ const buffer = fs.readFileSync(filePath);
96
+ value = Uint8Array.from(buffer);
97
+ }
98
+
99
+ value.parent =
100
+ key === ".."
101
+ ? // Special case: ".." parent is the grandparent (if it exists)
102
+ this.parent?.parent
103
+ : this;
104
+ setParent(value, this);
105
+
106
+ return value;
107
+ }
108
+
109
+ keys() {
110
+ let dirEntries;
111
+ try {
112
+ dirEntries = fs.readdirSync(this.dirname, { withFileTypes: true });
113
+ } catch (/** @type {any} */ error) {
114
+ if (error.code !== "ENOENT") {
115
+ throw error;
116
+ }
117
+ // Directory doesn't exist yet; treat as empty
118
+ dirEntries = [];
119
+ }
120
+
121
+ // Add slashes to directory names.
122
+ let names = dirEntries.map((dirEntry) =>
123
+ trailingSlash.toggle(dirEntry.name, dirEntry.isDirectory())
124
+ );
125
+
126
+ // Filter out unhelpful file names.
127
+ names = names.filter((name) => !hiddenFileNames.includes(name));
128
+
129
+ // Node fs.readdir sort order appears to be unreliable; see, e.g.,
130
+ // https://github.com/nodejs/node/issues/3232.
131
+ names.sort(naturalOrder);
132
+
133
+ return names[Symbol.iterator]();
134
+ }
135
+
136
+ get path() {
137
+ return this.dirname;
138
+ }
139
+
140
+ set(key, value) {
141
+ // Where are we going to write this value?
142
+ const stringKey = key != null ? String(key) : "";
143
+ const baseKey = trailingSlash.remove(stringKey);
144
+ const destPath = path.resolve(this.dirname, baseKey);
145
+
146
+ // Ensure this directory exists.
147
+ const dirname = path.dirname(destPath);
148
+ fs.mkdirSync(dirname, { recursive: true });
149
+
150
+ if (typeof value === "function") {
151
+ // Invoke function; write out the result.
152
+ value = value();
153
+ }
154
+
155
+ let packed = false;
156
+ if (value === null) {
157
+ // Treat null value as empty string; will create an empty file.
158
+ value = "";
159
+ packed = true;
160
+ } else if (value instanceof ArrayBuffer) {
161
+ // Convert ArrayBuffer to Uint8Array, which Node.js can write directly.
162
+ value = new Uint8Array(value);
163
+ packed = true;
164
+ } else if (!(value instanceof String) && isPacked(value)) {
165
+ // As of Node 22, fs.writeFile is incredibly slow for large String
166
+ // instances. Instead of treating a String instance as a Packed value, we
167
+ // want to consider it as a stringlike below. That will convert it to a
168
+ // primitive string before writing — which is orders of magnitude faster.
169
+ packed = true;
170
+ } else if (typeof value.pack === "function") {
171
+ // Pack the value for writing.
172
+ value = value.pack();
173
+ packed = true;
174
+ } else if (isStringlike(value)) {
175
+ // Value has a meaningful `toString` method, use that.
176
+ value = String(value);
177
+ packed = true;
178
+ }
179
+
180
+ if (packed) {
181
+ writeFile(value, destPath);
182
+ } else if (value === /** @type {any} */ (this.constructor).EMPTY) {
183
+ clearDirectory(destPath, this);
184
+ } else if (isMaplike(value)) {
185
+ writeDirectory(value, destPath, this);
186
+ } else {
187
+ const typeName = value?.constructor?.name ?? "unknown";
188
+ throw new TypeError(
189
+ `Cannot write a value of type ${typeName} as ${stringKey}`
190
+ );
191
+ }
192
+
193
+ return this;
194
+ }
195
+
196
+ get trailingSlashKeys() {
197
+ return true;
198
+ }
199
+ }
200
+
201
+ // Create the indicated directory.
202
+ function clearDirectory(destPath, parent) {
203
+ const destTree = Reflect.construct(parent.constructor, [destPath]);
204
+
205
+ // Ensure the directory exists.
206
+ fs.mkdirSync(destPath, { recursive: true });
207
+
208
+ // Clear any existing files
209
+ destTree.clear();
210
+
211
+ return destTree;
212
+ }
213
+
214
+ /**
215
+ * Treat value as a subtree and write it out as a subdirectory.
216
+ *
217
+ * @param {import("../../index.ts").Maplike} value
218
+ */
219
+ function writeDirectory(value, destPath, parent) {
220
+ // Since value is Maplike, result will be a Map
221
+ /** @type {Map} */
222
+ // @ts-ignore
223
+ const valueMap = from(value);
224
+ const destTree = clearDirectory(destPath, parent);
225
+
226
+ // Write out the subtree.
227
+ for (const key of valueMap.keys()) {
228
+ const childValue = valueMap.get(key);
229
+ destTree.set(key, childValue);
230
+ }
231
+ }
232
+
233
+ // Write a value to a file.
234
+ function writeFile(value, destPath) {
235
+ // Write out the value as the contents of a file.
236
+ try {
237
+ fs.writeFileSync(destPath, value);
238
+ } catch (/** @type {any} */ error) {
239
+ if (error.code === "EISDIR" /* Is a directory */) {
240
+ throw new Error(
241
+ `Tried to overwrite a directory with a single file: ${destPath}`
242
+ );
243
+ }
244
+ }
245
+ }
@@ -1,20 +1,14 @@
1
1
  import setParent from "../utilities/setParent.js";
2
+ import SyncMap from "./SyncMap.js";
2
3
 
3
- /**
4
- * A tree defined by a function and an optional domain.
5
- *
6
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
- * @implements {AsyncTree}
8
- */
9
- export default class FunctionTree {
10
- /**
11
- * @param {function} fn the key->value function
12
- * @param {Iterable<any>} [domain] optional domain of the function
13
- */
4
+ export default class FunctionMap extends SyncMap {
14
5
  constructor(fn, domain = []) {
6
+ if (typeof fn !== "function") {
7
+ throw new TypeError("FunctionMap: first argument must be a function");
8
+ }
9
+ super();
15
10
  this.fn = fn;
16
11
  this.domain = domain;
17
- this.parent = null;
18
12
  }
19
13
 
20
14
  /**
@@ -22,25 +16,28 @@ export default class FunctionTree {
22
16
  *
23
17
  * @param {any} key
24
18
  */
25
- async get(key) {
26
- const value =
19
+ get(key) {
20
+ let value =
27
21
  this.fn.length <= 1
28
22
  ? // Function takes no arguments, one argument, or a variable number of
29
23
  // arguments: invoke it.
30
- await this.fn(key)
24
+ this.fn(key)
31
25
  : // Bind the key to the first parameter. Subsequent get calls will
32
26
  // eventually bind all parameters until only one remains. At that point,
33
27
  // the above condition will apply and the function will be invoked.
34
28
  Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
35
- setParent(value, this);
29
+ if (value instanceof Promise) {
30
+ value = value.then((v) => {
31
+ setParent(v, this);
32
+ return v;
33
+ });
34
+ } else {
35
+ setParent(value, this);
36
+ }
36
37
  return value;
37
38
  }
38
39
 
39
- /**
40
- * Enumerates the function's domain (if defined) as the tree's keys. If no domain
41
- * was defined, this returns an empty iterator.
42
- */
43
- async keys() {
44
- return this.domain;
40
+ keys() {
41
+ return this.domain[Symbol.iterator]();
45
42
  }
46
43
  }