@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.
- package/browser.js +1 -1
- package/index.ts +31 -35
- package/main.js +1 -2
- package/package.json +4 -7
- package/shared.js +77 -12
- package/src/Tree.js +8 -2
- package/src/drivers/AsyncMap.js +210 -0
- package/src/drivers/{BrowserFileTree.js → BrowserFileMap.js} +36 -27
- package/src/drivers/{calendarTree.js → CalendarMap.js} +81 -62
- package/src/drivers/ConstantMap.js +30 -0
- package/src/drivers/DeepObjectMap.js +27 -0
- package/src/drivers/{ExplorableSiteTree.js → ExplorableSiteMap.js} +7 -7
- package/src/drivers/FileMap.js +245 -0
- package/src/drivers/{FunctionTree.js → FunctionMap.js} +19 -22
- package/src/drivers/ObjectMap.js +139 -0
- package/src/drivers/SetMap.js +13 -0
- package/src/drivers/{SiteTree.js → SiteMap.js} +16 -17
- package/src/drivers/SyncMap.js +245 -0
- package/src/jsonKeys.d.ts +2 -2
- package/src/jsonKeys.js +6 -5
- package/src/operations/addNextPrevious.js +35 -36
- package/src/operations/assign.js +30 -21
- package/src/operations/cache.js +29 -35
- package/src/operations/cachedKeyFunctions.js +1 -1
- package/src/operations/calendar.js +5 -0
- package/src/operations/clear.js +13 -12
- package/src/operations/constant.js +5 -0
- package/src/operations/deepEntries.js +23 -0
- package/src/operations/deepMap.js +9 -9
- package/src/operations/deepMerge.js +36 -25
- package/src/operations/deepReverse.js +23 -16
- package/src/operations/deepTake.js +7 -7
- package/src/operations/deepText.js +4 -4
- package/src/operations/deepValues.js +3 -6
- package/src/operations/deepValuesIterator.js +11 -11
- package/src/operations/delete.js +8 -12
- package/src/operations/entries.js +17 -10
- package/src/operations/filter.js +9 -7
- package/src/operations/first.js +12 -10
- package/src/operations/forEach.js +10 -13
- package/src/operations/from.js +31 -39
- package/src/operations/globKeys.js +22 -17
- package/src/operations/group.js +2 -2
- package/src/operations/groupBy.js +24 -22
- package/src/operations/has.js +7 -9
- package/src/operations/indent.js +2 -2
- package/src/operations/inners.js +19 -15
- package/src/operations/invokeFunctions.js +22 -10
- package/src/operations/isAsyncMutableTree.js +5 -12
- package/src/operations/isAsyncTree.js +5 -20
- package/src/operations/isMap.js +39 -0
- package/src/operations/isMaplike.js +34 -0
- package/src/operations/isReadOnlyMap.js +14 -0
- package/src/operations/isTraversable.js +3 -3
- package/src/operations/isTreelike.js +5 -30
- package/src/operations/json.js +4 -12
- package/src/operations/keys.js +17 -8
- package/src/operations/length.js +9 -8
- package/src/operations/map.js +27 -30
- package/src/operations/mapExtension.js +20 -16
- package/src/operations/mapReduce.js +22 -17
- package/src/operations/mask.js +31 -22
- package/src/operations/match.js +13 -9
- package/src/operations/merge.js +43 -35
- package/src/operations/paginate.js +26 -18
- package/src/operations/parent.js +7 -7
- package/src/operations/paths.js +8 -8
- package/src/operations/plain.js +6 -6
- package/src/operations/regExpKeys.js +21 -12
- package/src/operations/reverse.js +21 -15
- package/src/operations/root.js +6 -5
- package/src/operations/scope.js +31 -26
- package/src/operations/shuffle.js +23 -16
- package/src/operations/size.js +13 -0
- package/src/operations/sort.js +55 -40
- package/src/operations/sync.js +21 -0
- package/src/operations/take.js +23 -11
- package/src/operations/text.js +4 -4
- package/src/operations/toFunction.js +7 -7
- package/src/operations/traverse.js +4 -4
- package/src/operations/traverseOrThrow.js +13 -9
- package/src/operations/traversePath.js +2 -2
- package/src/operations/values.js +18 -9
- package/src/operations/withKeys.js +22 -16
- package/src/symbols.js +1 -0
- package/src/utilities/castArraylike.js +10 -2
- package/src/utilities/getMapArgument.js +38 -0
- package/src/utilities/getParent.js +2 -2
- package/src/utilities/isStringlike.js +7 -5
- package/src/utilities/setParent.js +7 -7
- package/src/utilities/toFunction.js +2 -2
- package/src/utilities/toPlainValue.js +22 -18
- package/test/SampleAsyncMap.js +34 -0
- package/test/browser/assert.js +20 -0
- package/test/browser/index.html +54 -21
- package/test/drivers/AsyncMap.test.js +119 -0
- package/test/drivers/{BrowserFileTree.test.js → BrowserFileMap.test.js} +42 -23
- package/test/drivers/{calendarTree.test.js → CalendarMap.test.js} +17 -19
- package/test/drivers/ConstantMap.test.js +15 -0
- package/test/drivers/DeepObjectMap.test.js +36 -0
- package/test/drivers/{ExplorableSiteTree.test.js → ExplorableSiteMap.test.js} +29 -14
- package/test/drivers/FileMap.test.js +185 -0
- package/test/drivers/FunctionMap.test.js +56 -0
- package/test/drivers/ObjectMap.test.js +166 -0
- package/test/drivers/SetMap.test.js +35 -0
- package/test/drivers/{SiteTree.test.js → SiteMap.test.js} +14 -10
- package/test/drivers/SyncMap.test.js +321 -0
- package/test/jsonKeys.test.js +2 -2
- package/test/operations/addNextPrevious.test.js +3 -2
- package/test/operations/assign.test.js +30 -35
- package/test/operations/cache.test.js +8 -6
- package/test/operations/cachedKeyFunctions.test.js +6 -5
- package/test/operations/clear.test.js +6 -27
- package/test/operations/deepEntries.test.js +32 -0
- package/test/operations/deepMerge.test.js +6 -5
- package/test/operations/deepReverse.test.js +2 -2
- package/test/operations/deepTake.test.js +2 -2
- package/test/operations/deepText.test.js +4 -4
- package/test/operations/deepValuesIterator.test.js +2 -2
- package/test/operations/delete.test.js +2 -2
- package/test/operations/extensionKeyFunctions.test.js +6 -5
- package/test/operations/filter.test.js +3 -3
- package/test/operations/from.test.js +23 -31
- package/test/operations/globKeys.test.js +9 -9
- package/test/operations/groupBy.test.js +6 -5
- package/test/operations/inners.test.js +4 -4
- package/test/operations/invokeFunctions.test.js +2 -2
- package/test/operations/isMap.test.js +15 -0
- package/test/operations/isMaplike.test.js +15 -0
- package/test/operations/json.test.js +2 -2
- package/test/operations/keys.test.js +16 -3
- package/test/operations/map.test.js +20 -18
- package/test/operations/mapExtension.test.js +6 -6
- package/test/operations/mapReduce.test.js +2 -2
- package/test/operations/mask.test.js +4 -3
- package/test/operations/match.test.js +2 -2
- package/test/operations/merge.test.js +15 -11
- package/test/operations/paginate.test.js +5 -5
- package/test/operations/parent.test.js +3 -3
- package/test/operations/paths.test.js +6 -6
- package/test/operations/plain.test.js +8 -8
- package/test/operations/regExpKeys.test.js +12 -11
- package/test/operations/reverse.test.js +4 -3
- package/test/operations/scope.test.js +6 -5
- package/test/operations/shuffle.test.js +3 -2
- package/test/operations/sort.test.js +7 -10
- package/test/operations/sync.test.js +43 -0
- package/test/operations/take.test.js +2 -2
- package/test/operations/toFunction.test.js +2 -2
- package/test/operations/traverse.test.js +4 -5
- package/test/operations/withKeys.test.js +2 -2
- package/test/utilities/setParent.test.js +6 -6
- package/test/utilities/toFunction.test.js +2 -2
- package/test/utilities/toPlainValue.test.js +51 -12
- package/src/drivers/DeepMapTree.js +0 -23
- package/src/drivers/DeepObjectTree.js +0 -18
- package/src/drivers/DeferredTree.js +0 -81
- package/src/drivers/FileTree.js +0 -276
- package/src/drivers/MapTree.js +0 -70
- package/src/drivers/ObjectTree.js +0 -158
- package/src/drivers/SetTree.js +0 -34
- package/src/drivers/constantTree.js +0 -19
- package/src/drivers/limitConcurrency.js +0 -63
- package/src/internal.js +0 -16
- package/src/utilities/getTreeArgument.js +0 -43
- package/test/drivers/DeepMapTree.test.js +0 -17
- package/test/drivers/DeepObjectTree.test.js +0 -35
- package/test/drivers/DeferredTree.test.js +0 -22
- package/test/drivers/FileTree.test.js +0 -192
- package/test/drivers/FunctionTree.test.js +0 -46
- package/test/drivers/MapTree.test.js +0 -59
- package/test/drivers/ObjectTree.test.js +0 -163
- package/test/drivers/SetTree.test.js +0 -44
- package/test/drivers/constantTree.test.js +0 -13
- package/test/drivers/limitConcurrency.test.js +0 -41
- package/test/operations/isAsyncMutableTree.test.js +0 -17
- package/test/operations/isAsyncTree.test.js +0 -26
- package/test/operations/isTreelike.test.js +0 -13
package/src/drivers/FileTree.js
DELETED
|
@@ -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
|
-
}
|
package/src/drivers/MapTree.js
DELETED
|
@@ -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
|
-
}
|
package/src/drivers/SetTree.js
DELETED
|
@@ -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
|
-
});
|