@weborigami/async-tree 0.5.8 → 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,276 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
- import { hiddenFileNames } from "../constants.js";
5
- import assign from "../operations/assign.js";
6
- import isTreelike from "../operations/isTreelike.js";
7
- import * as trailingSlash from "../trailingSlash.js";
8
- import isPacked from "../utilities/isPacked.js";
9
- import isPlainObject from "../utilities/isPlainObject.js";
10
- import isStringlike from "../utilities/isStringlike.js";
11
- import naturalOrder from "../utilities/naturalOrder.js";
12
- import setParent from "../utilities/setParent.js";
13
- import limitConcurrency from "./limitConcurrency.js";
14
-
15
- // As of July 2025, Node doesn't provide any way to limit the number of
16
- // concurrent calls to readFile, so we wrap readFile in a function that
17
- // arbitrarily limits the number of concurrent calls to it.
18
- const MAX_CONCURRENT_READS = 4096;
19
- const limitReadFile = limitConcurrency(fs.readFile, MAX_CONCURRENT_READS);
20
-
21
- /**
22
- * A file system tree via the Node file system API.
23
- *
24
- * File values are returned as Uint8Array instances. The underlying Node fs API
25
- * returns file contents as instances of the node-specific Buffer class, but
26
- * that class has some incompatible method implementations; see
27
- * https://nodejs.org/api/buffer.html#buffers-and-typedarrays. For greater
28
- * compatibility, files are returned as standard Uint8Array instances instead.
29
- *
30
- * @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
31
- * @implements {AsyncMutableTree}
32
- */
33
- export default class FileTree {
34
- /**
35
- * @param {string|URL} location
36
- */
37
- constructor(location) {
38
- if (location instanceof URL) {
39
- location = location.href;
40
- } else if (
41
- !(
42
- typeof location === "string" ||
43
- /** @type {any} */ (location) instanceof String
44
- )
45
- ) {
46
- throw new TypeError(
47
- `FileTree constructor needs a string or URL, received an instance of ${
48
- /** @type {any} */ (location)?.constructor?.name
49
- }`
50
- );
51
- }
52
- this.dirname = location.startsWith("file://")
53
- ? fileURLToPath(location)
54
- : path.resolve(process.cwd(), location);
55
- this.parent = null;
56
- }
57
-
58
- async get(key) {
59
- if (key == null) {
60
- // Reject nullish key
61
- throw new ReferenceError(
62
- `${this.constructor.name}: Cannot get a null or undefined key.`
63
- );
64
- }
65
- if (key === "") {
66
- // Can't have a file with no name
67
- return undefined;
68
- }
69
-
70
- // Remove trailing slash if present
71
- key = trailingSlash.remove(key);
72
- const filePath = path.resolve(this.dirname, key);
73
-
74
- let stats;
75
- try {
76
- stats = await fs.stat(filePath);
77
- } catch (/** @type {any} */ error) {
78
- if (error.code === "ENOENT" /* File not found */) {
79
- return undefined;
80
- }
81
- throw error;
82
- }
83
-
84
- let value;
85
- if (stats.isDirectory()) {
86
- // Return subdirectory as a tree
87
- value = Reflect.construct(this.constructor, [filePath]);
88
- } else {
89
- // Return file contents as a standard Uint8Array
90
- const buffer = await limitReadFile(filePath);
91
- // const buffer = await fs.readFile(filePath);
92
- value = Uint8Array.from(buffer);
93
- }
94
-
95
- const parent =
96
- key === ".."
97
- ? // Special case: ".." parent is the grandparent (if it exists)
98
- this.parent?.parent
99
- : this;
100
- setParent(value, parent);
101
- return value;
102
- }
103
-
104
- /**
105
- * Enumerate the names of the files/subdirectories in this directory.
106
- *
107
- * @returns {Promise<string[]>}
108
- */
109
- async keys() {
110
- let entries;
111
- try {
112
- entries = await fs.readdir(this.dirname, { withFileTypes: true });
113
- } catch (/** @type {any} */ error) {
114
- if (error.code !== "ENOENT") {
115
- throw error;
116
- }
117
- entries = [];
118
- }
119
-
120
- // Add slashes to directory names.
121
- let names = await Promise.all(
122
- entries.map(async (entry) =>
123
- trailingSlash.toggle(entry.name, await isDirectory(entry, this.dirname))
124
- )
125
- );
126
-
127
- // Filter out unhelpful file names.
128
- names = names.filter((name) => !hiddenFileNames.includes(name));
129
-
130
- // Node fs.readdir sort order appears to be unreliable; see, e.g.,
131
- // https://github.com/nodejs/node/issues/3232.
132
- names.sort(naturalOrder);
133
-
134
- return names;
135
- }
136
-
137
- get path() {
138
- return this.dirname;
139
- }
140
-
141
- async set(key, value) {
142
- // Where are we going to write this value?
143
- const stringKey = key != null ? String(key) : "";
144
- const baseKey = trailingSlash.remove(stringKey);
145
- const destPath = path.resolve(this.dirname, baseKey);
146
-
147
- if (value === undefined) {
148
- // Delete the file or directory.
149
- let stats;
150
- try {
151
- stats = await stat(destPath);
152
- } catch (/** @type {any} */ error) {
153
- if (error.code === "ENOENT" /* File not found */) {
154
- return this;
155
- }
156
- throw error;
157
- }
158
-
159
- if (stats?.isDirectory()) {
160
- // Delete directory.
161
- await fs.rm(destPath, { recursive: true });
162
- } else if (stats) {
163
- // Delete file.
164
- await fs.unlink(destPath);
165
- }
166
-
167
- return this;
168
- }
169
-
170
- if (typeof value === "function") {
171
- // Invoke function; write out the result.
172
- value = await value();
173
- }
174
-
175
- let packed = false;
176
- if (value === null) {
177
- // Treat null value as empty string; will create an empty file.
178
- value = "";
179
- packed = true;
180
- } else if (!(value instanceof String) && isPacked(value)) {
181
- // As of Node 22, fs.writeFile is incredibly slow for large String
182
- // instances. Instead of treating a String instance as a Packed value, we
183
- // want to consider it as a stringlike below. That will convert it to a
184
- // primitive string before writing — which is orders of magnitude faster.
185
- packed = true;
186
- } else if (typeof value.pack === "function") {
187
- // Pack the value for writing.
188
- value = await value.pack();
189
- packed = true;
190
- } else if (isStringlike(value)) {
191
- // Value has a meaningful `toString` method, use that.
192
- value = String(value);
193
- packed = true;
194
- }
195
-
196
- if (packed) {
197
- await writeFile(value, destPath);
198
- } else if (isPlainObject(value) && Object.keys(value).length === 0) {
199
- // Special case: empty object means create an empty directory.
200
- await fs.mkdir(destPath, { recursive: true });
201
- } else if (isTreelike(value)) {
202
- // Treat value as a subtree and write it out as a subdirectory.
203
- const destTree = Reflect.construct(this.constructor, [destPath]);
204
- // Create the directory here, even if the subtree is empty.
205
- await fs.mkdir(destPath, { recursive: true });
206
- // Write out the subtree.
207
- await assign(destTree, value);
208
- } else {
209
- const typeName = value?.constructor?.name ?? "unknown";
210
- throw new TypeError(
211
- `Cannot write a value of type ${typeName} as ${stringKey}`
212
- );
213
- }
214
-
215
- return this;
216
- }
217
-
218
- get url() {
219
- return pathToFileURL(this.dirname);
220
- }
221
- }
222
-
223
- /**
224
- * Return true if the entry is a directory or is a symbolic link to a directory.
225
- */
226
- async function isDirectory(entry, dirname) {
227
- if (entry.isSymbolicLink()) {
228
- const entryPath = path.resolve(dirname, entry.name);
229
- try {
230
- const realPath = await fs.realpath(entryPath);
231
- entry = await fs.stat(realPath);
232
- } catch (error) {
233
- // The slash isn't crucial, so if link doesn't work that's okay
234
- return false;
235
- }
236
- }
237
- return entry.isDirectory();
238
- }
239
-
240
- // Return the file information for the file/folder at the given path.
241
- // If it does not exist, return undefined.
242
- async function stat(filePath) {
243
- try {
244
- // Await the result here so that, if the file doesn't exist, the catch block
245
- // below will catch the exception.
246
- return await fs.stat(filePath);
247
- } catch (/** @type {any} */ error) {
248
- if (error.code === "ENOENT" /* File not found */) {
249
- return undefined;
250
- }
251
- throw error;
252
- }
253
- }
254
-
255
- // Write a value to a file.
256
- async function writeFile(value, destPath) {
257
- // Single writeable value.
258
- if (value instanceof ArrayBuffer) {
259
- // Convert ArrayBuffer to Uint8Array, which Node.js can write directly.
260
- value = new Uint8Array(value);
261
- }
262
- // Ensure this directory exists.
263
- const dirname = path.dirname(destPath);
264
- await fs.mkdir(dirname, { recursive: true });
265
-
266
- // Write out the value as the contents of a file.
267
- try {
268
- await fs.writeFile(destPath, value);
269
- } catch (/** @type {any} */ error) {
270
- if (error.code === "EISDIR" /* Is a directory */) {
271
- throw new Error(
272
- `Tried to overwrite a directory with a single file: ${destPath}`
273
- );
274
- }
275
- }
276
- }
@@ -1,70 +0,0 @@
1
- import isAsyncTree from "../operations/isAsyncTree.js";
2
- import * as trailingSlash from "../trailingSlash.js";
3
- import setParent from "../utilities/setParent.js";
4
-
5
- /**
6
- * A tree backed by a JavaScript `Map` object.
7
- *
8
- * Note: By design, the standard `Map` class already complies with the
9
- * `AsyncTree` interface. This class adds some additional tree behavior, such as
10
- * constructing subtree instances and setting their `parent` property. While
11
- * we'd like to construct this by subclassing `Map`, that class appears
12
- * puzzingly and deliberately implemented to break subclasses.
13
- *
14
- * @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
15
- * @implements {AsyncMutableTree}
16
- */
17
- export default class MapTree {
18
- /**
19
- * Constructs a new `MapTree` instance. This `iterable` parameter can be a
20
- * `Map` instance, or any other iterable of key-value pairs.
21
- *
22
- * @param {Iterable} [source]
23
- */
24
- constructor(source = []) {
25
- this.map = source instanceof Map ? source : new Map(source);
26
- this.parent = null;
27
- }
28
-
29
- async get(key) {
30
- // Try key as is
31
- let value = this.map.get(key);
32
- if (value === undefined) {
33
- // Try the other variation of the key
34
- const alternateKey = trailingSlash.toggle(key);
35
- value = this.map.get(alternateKey);
36
- if (value === undefined) {
37
- // Key doesn't exist
38
- return undefined;
39
- }
40
- }
41
-
42
- value = await value;
43
-
44
- if (value === undefined) {
45
- // Key exists but value is undefined
46
- return undefined;
47
- }
48
-
49
- setParent(value, this);
50
- return value;
51
- }
52
-
53
- /** @returns {boolean} */
54
- isSubtree(value) {
55
- return isAsyncTree(value);
56
- }
57
-
58
- async keys() {
59
- const keys = [];
60
- for (const [key, value] of this.map.entries()) {
61
- keys.push(trailingSlash.toggle(key, this.isSubtree(value)));
62
- }
63
- return keys;
64
- }
65
-
66
- async set(key, value) {
67
- this.map.set(key, value);
68
- return this;
69
- }
70
- }
@@ -1,158 +0,0 @@
1
- import isAsyncTree from "../operations/isAsyncTree.js";
2
- import * as symbols from "../symbols.js";
3
- import * as trailingSlash from "../trailingSlash.js";
4
- import getRealmObjectPrototype from "../utilities/getRealmObjectPrototype.js";
5
- import setParent from "../utilities/setParent.js";
6
-
7
- /**
8
- * A tree defined by a plain object or array.
9
- *
10
- * @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
11
- * @implements {AsyncMutableTree}
12
- */
13
- export default class ObjectTree {
14
- /**
15
- * Create a tree wrapping a given plain object or array.
16
- *
17
- * @param {object|any[]} object The object/array to wrap.
18
- */
19
- constructor(object) {
20
- // Note: we use `typeof` here instead of `instanceof Object` to allow for
21
- // objects such as Node's `Module` class for representing an ES module.
22
- if (typeof object !== "object" || object === null) {
23
- throw new TypeError(
24
- `${this.constructor.name}: Expected an object or array.`
25
- );
26
- }
27
- this.object = object;
28
- this.parent = object[symbols.parent] ?? null;
29
- }
30
-
31
- /**
32
- * Return the value for the given key.
33
- *
34
- * @param {any} key
35
- */
36
- async get(key) {
37
- if (key == null) {
38
- // Reject nullish key.
39
- throw new ReferenceError(
40
- `${this.constructor.name}: Cannot get a null or undefined key.`
41
- );
42
- }
43
-
44
- // Does the object have the key with or without a trailing slash?
45
- const existingKey = findExistingKey(this.object, key);
46
- if (existingKey === null) {
47
- // Key doesn't exist
48
- return undefined;
49
- }
50
-
51
- let value = await this.object[existingKey];
52
-
53
- if (value === undefined) {
54
- // Key exists but value is undefined
55
- return undefined;
56
- }
57
-
58
- setParent(value, this);
59
-
60
- // Is value an instance method?
61
- const isInstanceMethod =
62
- value instanceof Function && !Object.hasOwn(this.object, key);
63
- if (isInstanceMethod) {
64
- // Bind it to the object
65
- value = value.bind(this.object);
66
- }
67
-
68
- return value;
69
- }
70
-
71
- /** @returns {boolean} */
72
- isSubtree(value) {
73
- return isAsyncTree(value);
74
- }
75
-
76
- /**
77
- * Enumerate the object's keys.
78
- */
79
- async keys() {
80
- // Defer to symbols.keys if defined
81
- if (typeof this.object[symbols.keys] === "function") {
82
- return this.object[symbols.keys]();
83
- }
84
-
85
- // Walk up the prototype chain to realm's Object.prototype.
86
- let obj = this.object;
87
- const objectPrototype = getRealmObjectPrototype(obj);
88
-
89
- const result = new Set();
90
- while (obj && obj !== objectPrototype) {
91
- // Get the enumerable instance properties and the get/set properties.
92
- const descriptors = Object.getOwnPropertyDescriptors(obj);
93
- const propertyNames = Object.entries(descriptors)
94
- .filter(
95
- ([name, descriptor]) =>
96
- name !== "constructor" &&
97
- (descriptor.enumerable ||
98
- (descriptor.get !== undefined && descriptor.set !== undefined))
99
- )
100
- .map(([name, descriptor]) =>
101
- trailingSlash.has(name)
102
- ? // Preserve existing slash
103
- name
104
- : // Add a slash if the value is a plain property and a subtree
105
- trailingSlash.toggle(
106
- name,
107
- descriptor.value !== undefined &&
108
- this.isSubtree(descriptor.value)
109
- )
110
- );
111
- for (const name of propertyNames) {
112
- result.add(name);
113
- }
114
- obj = Object.getPrototypeOf(obj);
115
- }
116
- return result;
117
- }
118
-
119
- /**
120
- * Set the value for the given key. If the value is undefined, delete the key.
121
- *
122
- * @param {any} key
123
- * @param {any} value
124
- */
125
- async set(key, value) {
126
- const existingKey = findExistingKey(this.object, key);
127
-
128
- if (value === undefined) {
129
- // Delete the key if it exists.
130
- if (existingKey !== null) {
131
- delete this.object[existingKey];
132
- }
133
- } else {
134
- // If the key exists under a different form, delete the existing key.
135
- if (existingKey !== null && existingKey !== key) {
136
- delete this.object[existingKey];
137
- }
138
-
139
- // Set the value for the key.
140
- this.object[key] = value;
141
- }
142
-
143
- return this;
144
- }
145
- }
146
-
147
- function findExistingKey(object, key) {
148
- // First try key as is
149
- if (key in object) {
150
- return key;
151
- }
152
- // Try alternate form
153
- const alternateKey = trailingSlash.toggle(key);
154
- if (alternateKey in object) {
155
- return alternateKey;
156
- }
157
- return null;
158
- }
@@ -1,34 +0,0 @@
1
- import setParent from "../utilities/setParent.js";
2
-
3
- /**
4
- * A tree of Set objects.
5
- *
6
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
- * @implements {AsyncTree}
8
- */
9
- export default class SetTree {
10
- /**
11
- * @param {Set} set
12
- */
13
- constructor(set) {
14
- this.values = Array.from(set);
15
- this.parent = null;
16
- }
17
-
18
- async get(key) {
19
- if (key == null) {
20
- // Reject nullish key.
21
- throw new ReferenceError(
22
- `${this.constructor.name}: Cannot get a null or undefined key.`
23
- );
24
- }
25
-
26
- const value = this.values[key];
27
- setParent(value, this);
28
- return value;
29
- }
30
-
31
- async keys() {
32
- return this.values.keys();
33
- }
34
- }
@@ -1,19 +0,0 @@
1
- import * as trailingSlash from "../trailingSlash.js";
2
-
3
- /**
4
- * A tree that returns a constant value for any key. If the key ends with a
5
- * slash, then the same type of subtree is returned.
6
- *
7
- * @param {any} constant
8
- */
9
- export default function constantTree(constant) {
10
- return {
11
- async get(key) {
12
- return trailingSlash.has(key) ? constantTree(constant) : constant;
13
- },
14
-
15
- async keys() {
16
- return [];
17
- },
18
- };
19
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * Wrap an async function with a function that limits the number of concurrent
3
- * calls to that function.
4
- */
5
- export default function limitConcurrency(fn, maxConcurrency) {
6
- const queue = [];
7
- const activeCallPool = new Set();
8
-
9
- return async function limitedFunction(...args) {
10
- // Our turn is represented by a promise that can be externally resolved
11
- const turnWithResolvers = withResolvers();
12
-
13
- // Construct a promise for the result of the function call
14
- const resultPromise =
15
- // Block until its our turn
16
- turnWithResolvers.promise
17
- .then(() => fn(...args)) // Call the function and return its result
18
- .finally(() => {
19
- // Remove the promise from the active pool
20
- activeCallPool.delete(resultPromise);
21
- // Tell the next call in the queue it can proceed
22
- next();
23
- });
24
-
25
- // Join the queue
26
- queue.push({
27
- promise: resultPromise,
28
- unblock: turnWithResolvers.resolve,
29
- });
30
-
31
- if (activeCallPool.size >= maxConcurrency) {
32
- // The pool is full; wait for the next active call to complete. The call
33
- // will remove its own completed promise from the active pool.
34
- await Promise.any(activeCallPool);
35
- } else {
36
- next();
37
- }
38
-
39
- return resultPromise;
40
- };
41
-
42
- // If there are calls in the queue and the active pool is not full, start
43
- // the next call in the queue.
44
- function next() {
45
- if (queue.length > 0 && activeCallPool.size < maxConcurrency) {
46
- const { promise, unblock } = queue.shift();
47
- activeCallPool.add(promise);
48
- // Resolve the turn promise to allow the call to proceed
49
- unblock();
50
- }
51
- }
52
- }
53
-
54
- // Polyfill Promise.withResolvers until Node LTS supports it
55
- function withResolvers() {
56
- let resolve;
57
- let reject;
58
- const promise = new Promise((res, rej) => {
59
- resolve = res;
60
- reject = rej;
61
- });
62
- return { promise, resolve, reject };
63
- }
package/src/internal.js DELETED
@@ -1,16 +0,0 @@
1
- //
2
- // This library includes a number of modules with circular dependencies. This
3
- // module exists to explicitly set the loading order for those modules. To
4
- // enforce use of this loading order, other modules should only load the modules
5
- // below via this module.
6
- //
7
- // About this pattern:
8
- // https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
9
- //
10
- // Note: to avoid having VS Code auto-sort the imports, keep lines between them.
11
-
12
- export * as Tree from "./Tree.js";
13
-
14
- export { default as ObjectTree } from "./drivers/ObjectTree.js";
15
-
16
- export { default as DeepObjectTree } from "./drivers/DeepObjectTree.js";
@@ -1,43 +0,0 @@
1
- import from from "../operations/from.js";
2
- import isUnpackable from "./isUnpackable.js";
3
-
4
- /**
5
- * Convert the indicated argument to a tree, or throw an exception.
6
- *
7
- * Tree operations can use this to validate the tree argument and provide more
8
- * helpful error messages. This also unpacks a unpackable tree argument so that
9
- * the caller can work with a simpler tree instead of a DeferredTree.
10
- *
11
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
12
- * @typedef {import("../../index.ts").Treelike} Treelike
13
- * @typedef {import("../../index.ts").Unpackable} Unpackable
14
- *
15
- * @param {Treelike|Unpackable} treelike
16
- * @param {string} operation
17
- * @param {{ deep?: boolean, position?: number }} [options]
18
- * @returns {Promise<AsyncTree>}
19
- */
20
- export default async function getTreeArgument(
21
- treelike,
22
- operation,
23
- options = {}
24
- ) {
25
- const deep = options.deep;
26
- const position = options.position ?? 0;
27
-
28
- if (isUnpackable(treelike)) {
29
- treelike = await treelike.unpack();
30
- }
31
-
32
- let tree;
33
- try {
34
- tree = from(treelike, { deep });
35
- } catch (/** @type {any} */ error) {
36
- let message = error.message ?? error;
37
- message = `${operation}: ${message}`;
38
- const newError = new TypeError(message);
39
- /** @type {any} */ (newError).position = position;
40
- throw newError;
41
- }
42
- return tree;
43
- }
@@ -1,17 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe, test } from "node:test";
3
- import DeepMapTree from "../../src/drivers/DeepMapTree.js";
4
- import { Tree } from "../../src/internal.js";
5
-
6
- describe("DeepMapTree", () => {
7
- test("returns a DeepMapTree for value that's a Map", async () => {
8
- const tree = new DeepMapTree([
9
- ["a", 1],
10
- ["map", new Map([["b", 2]])],
11
- ]);
12
- const map = await tree.get("map");
13
- assert.equal(map instanceof DeepMapTree, true);
14
- assert.deepEqual(await Tree.plain(map), { b: 2 });
15
- assert.equal(map.parent, tree);
16
- });
17
- });