@weborigami/async-tree 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +13 -1
- package/package.json +1 -1
- package/shared.js +13 -16
- package/src/Tree.js +3 -0
- package/src/drivers/AsyncMap.js +30 -3
- package/src/drivers/BrowserFileMap.js +30 -23
- package/src/drivers/CalendarMap.js +0 -2
- package/src/drivers/ConstantMap.js +1 -3
- package/src/drivers/ExplorableSiteMap.js +2 -0
- package/src/drivers/FileMap.js +50 -57
- package/src/drivers/ObjectMap.js +22 -10
- package/src/drivers/SiteMap.js +4 -6
- package/src/drivers/SyncMap.js +22 -7
- package/src/jsonKeys.js +15 -1
- package/src/operations/assign.js +7 -12
- package/src/operations/cache.js +2 -2
- package/src/operations/child.js +35 -0
- package/src/operations/from.js +5 -6
- package/src/operations/globKeys.js +2 -3
- package/src/operations/isMaplike.js +1 -1
- package/src/operations/map.js +22 -3
- package/src/operations/mapReduce.js +28 -19
- package/src/operations/mask.js +34 -18
- package/src/operations/paths.js +14 -16
- package/src/operations/reduce.js +16 -0
- package/src/operations/root.js +2 -2
- package/src/operations/set.js +20 -0
- package/src/operations/sync.js +2 -9
- package/src/operations/traverseOrThrow.js +5 -0
- package/src/utilities/castArraylike.js +23 -20
- package/src/utilities/toPlainValue.js +6 -8
- package/test/browser/index.html +0 -1
- package/test/drivers/BrowserFileMap.test.js +21 -23
- package/test/drivers/FileMap.test.js +2 -31
- package/test/drivers/ObjectMap.test.js +28 -0
- package/test/drivers/SyncMap.test.js +19 -5
- package/test/jsonKeys.test.js +18 -6
- package/test/operations/cache.test.js +11 -8
- package/test/operations/cachedKeyFunctions.test.js +8 -6
- package/test/operations/child.test.js +34 -0
- package/test/operations/deepMerge.test.js +20 -14
- package/test/operations/from.test.js +6 -4
- package/test/operations/inners.test.js +15 -12
- package/test/operations/map.test.js +24 -16
- package/test/operations/mapReduce.test.js +14 -12
- package/test/operations/mask.test.js +12 -0
- package/test/operations/merge.test.js +7 -5
- package/test/operations/paths.test.js +20 -27
- package/test/operations/regExpKeys.test.js +12 -9
- package/test/operations/root.test.js +23 -0
- package/test/operations/set.test.js +11 -0
- package/test/operations/traverse.test.js +13 -0
- package/test/utilities/castArrayLike.test.js +53 -0
- package/src/drivers/DeepObjectMap.js +0 -27
- package/test/drivers/DeepObjectMap.test.js +0 -36
package/src/operations/assign.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import getMapArgument from "../utilities/getMapArgument.js";
|
|
2
|
+
import child from "./child.js";
|
|
2
3
|
import isMaplike from "./isMaplike.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -17,26 +18,18 @@ export default async function assign(target, source) {
|
|
|
17
18
|
const targetTree = await getMapArgument(target, "assign", { position: 0 });
|
|
18
19
|
const sourceTree = await getMapArgument(source, "assign", { position: 1 });
|
|
19
20
|
if ("readOnly" in targetTree && targetTree.readOnly) {
|
|
20
|
-
throw new TypeError("Target must be a
|
|
21
|
+
throw new TypeError("assign: Target must be a read/write map");
|
|
21
22
|
}
|
|
23
|
+
|
|
22
24
|
// Fire off requests to update all keys, then wait for all of them to finish.
|
|
23
25
|
const promises = [];
|
|
24
26
|
for await (const key of sourceTree.keys()) {
|
|
25
27
|
const promise = (async () => {
|
|
26
28
|
const sourceValue = await sourceTree.get(key);
|
|
27
|
-
|
|
28
29
|
if (isMaplike(sourceValue)) {
|
|
29
|
-
let targetValue = await targetTree.get(key);
|
|
30
|
-
if (targetValue === undefined) {
|
|
31
|
-
// Target key doesn't exist; create empty subtree
|
|
32
|
-
/** @type {any} */
|
|
33
|
-
const targetClass = targetTree.constructor;
|
|
34
|
-
const empty = targetClass.EMPTY ?? new targetClass();
|
|
35
|
-
await targetTree.set(key, empty);
|
|
36
|
-
targetValue = await targetTree.get(key);
|
|
37
|
-
}
|
|
38
30
|
// Recurse to copy subtree
|
|
39
|
-
await
|
|
31
|
+
const targetChild = await child(targetTree, key);
|
|
32
|
+
await assign(targetChild, sourceValue);
|
|
40
33
|
} else {
|
|
41
34
|
// Copy the value from the source to the target.
|
|
42
35
|
await targetTree.set(key, sourceValue);
|
|
@@ -44,6 +37,8 @@ export default async function assign(target, source) {
|
|
|
44
37
|
})();
|
|
45
38
|
promises.push(promise);
|
|
46
39
|
}
|
|
40
|
+
|
|
47
41
|
await Promise.all(promises);
|
|
42
|
+
|
|
48
43
|
return targetTree;
|
|
49
44
|
}
|
package/src/operations/cache.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import AsyncMap from "../drivers/AsyncMap.js";
|
|
2
2
|
import SyncMap from "../drivers/SyncMap.js";
|
|
3
3
|
import getMapArgument from "../utilities/getMapArgument.js";
|
|
4
|
+
import child from "./child.js";
|
|
4
5
|
import isMap from "./isMap.js";
|
|
5
6
|
import isReadOnlyMap from "./isReadOnlyMap.js";
|
|
6
7
|
import keys from "./keys.js";
|
|
@@ -54,8 +55,7 @@ export default async function treeCache(sourceMaplike, cacheMaplike) {
|
|
|
54
55
|
// Construct merged tree for a tree result.
|
|
55
56
|
if (cacheValue === undefined) {
|
|
56
57
|
// Construct new empty container in cache
|
|
57
|
-
await cache
|
|
58
|
-
cacheValue = await cache.get(key);
|
|
58
|
+
cacheValue = await child(cache, key);
|
|
59
59
|
}
|
|
60
60
|
value = treeCache(value, cacheValue);
|
|
61
61
|
} else if (value !== undefined) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import getMapArgument from "../utilities/getMapArgument.js";
|
|
2
|
+
import setParent from "../utilities/setParent.js";
|
|
3
|
+
import isMap from "./isMap.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return the child node with the indicated key, creating it if necessary.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
9
|
+
*
|
|
10
|
+
* @param {Maplike} maplike
|
|
11
|
+
*/
|
|
12
|
+
export default async function child(maplike, key) {
|
|
13
|
+
const map = await getMapArgument(maplike, "assign", { position: 0 });
|
|
14
|
+
|
|
15
|
+
let result;
|
|
16
|
+
|
|
17
|
+
const hasChildMethod = "child" in map && typeof map.child === "function";
|
|
18
|
+
if (hasChildMethod) {
|
|
19
|
+
// Use tree's own child() method
|
|
20
|
+
result = map.child(key);
|
|
21
|
+
} else {
|
|
22
|
+
// Default implementation
|
|
23
|
+
result = await map.get(key);
|
|
24
|
+
|
|
25
|
+
// If child is already a map we can use it as is
|
|
26
|
+
if (!isMap(result)) {
|
|
27
|
+
// Create new child node using no-arg constructor
|
|
28
|
+
result = new /** @type {any} */ (map).constructor();
|
|
29
|
+
await map.set(key, result);
|
|
30
|
+
setParent(result, map);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
package/src/operations/from.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import AsyncMap from "../drivers/AsyncMap.js";
|
|
2
|
-
import DeepObjectMap from "../drivers/DeepObjectMap.js";
|
|
3
2
|
import FunctionMap from "../drivers/FunctionMap.js";
|
|
4
3
|
import ObjectMap from "../drivers/ObjectMap.js";
|
|
5
4
|
import SetMap from "../drivers/SetMap.js";
|
|
@@ -14,7 +13,7 @@ import isMap from "./isMap.js";
|
|
|
14
13
|
*
|
|
15
14
|
* If the object is a plain object, it will be converted to an ObjectMap. The
|
|
16
15
|
* optional `deep` option can be set to `true` to convert a plain object to a
|
|
17
|
-
*
|
|
16
|
+
* deep ObjectMap. The optional `parent` parameter will be used as the default
|
|
18
17
|
* parent of the new tree.
|
|
19
18
|
*
|
|
20
19
|
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
@@ -41,14 +40,14 @@ export default function from(object, options = {}) {
|
|
|
41
40
|
} else if (object instanceof Set) {
|
|
42
41
|
map = new SetMap(object);
|
|
43
42
|
} else if (isPlainObject(object) || object instanceof Array) {
|
|
44
|
-
map =
|
|
43
|
+
map = new ObjectMap(object, { deep });
|
|
45
44
|
// @ts-ignore
|
|
46
|
-
} else if (object instanceof Iterator) {
|
|
45
|
+
} else if (globalThis.Iterator && object instanceof Iterator) {
|
|
47
46
|
const array = Array.from(object);
|
|
48
|
-
map = new ObjectMap(array);
|
|
47
|
+
map = new ObjectMap(array, { deep });
|
|
49
48
|
} else if (object && typeof object === "object") {
|
|
50
49
|
// An instance of some class.
|
|
51
|
-
map = new ObjectMap(object);
|
|
50
|
+
map = new ObjectMap(object, { deep });
|
|
52
51
|
} else if (
|
|
53
52
|
typeof object === "string" ||
|
|
54
53
|
typeof object === "number" ||
|
|
@@ -24,9 +24,8 @@ export default async function globKeys(maplike) {
|
|
|
24
24
|
},
|
|
25
25
|
|
|
26
26
|
async *keys() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
27
|
+
// Don't return globstar keys. When used, e.g., with a Tree.mask, we don't
|
|
28
|
+
// want the globstar keys to appear in the result.
|
|
30
29
|
},
|
|
31
30
|
|
|
32
31
|
trailingSlashKeys: /** @type {any} */ (source).trailingSlashKeys,
|
|
@@ -26,7 +26,7 @@ export default function isMaplike(object) {
|
|
|
26
26
|
object instanceof Array ||
|
|
27
27
|
object instanceof Function ||
|
|
28
28
|
// @ts-ignore
|
|
29
|
-
object instanceof Iterator ||
|
|
29
|
+
(globalThis.Iterator && object instanceof Iterator) ||
|
|
30
30
|
object instanceof Set ||
|
|
31
31
|
isMap(object) ||
|
|
32
32
|
isPlainObject(object)
|
package/src/operations/map.js
CHANGED
|
@@ -124,6 +124,7 @@ function createMapFn(options) {
|
|
|
124
124
|
transformed.source = tree;
|
|
125
125
|
transformed.get = createGet(tree, options, mapFn);
|
|
126
126
|
transformed.keys = createKeys(tree, options);
|
|
127
|
+
transformed.trailingSlashKeys = /** @type {any} */ (tree).trailingSlashKeys;
|
|
127
128
|
return transformed;
|
|
128
129
|
};
|
|
129
130
|
}
|
|
@@ -166,9 +167,17 @@ function validateOptions(options) {
|
|
|
166
167
|
valueFn = validateOption(options, "value");
|
|
167
168
|
|
|
168
169
|
// Cast function options to functions
|
|
169
|
-
inverseKeyFn &&=
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
inverseKeyFn &&= castToFunction(inverseKeyFn, "inverseKey");
|
|
171
|
+
if (
|
|
172
|
+
typeof keyFn === "string" &&
|
|
173
|
+
(keyFn.includes("=>") || keyFn.includes("→"))
|
|
174
|
+
) {
|
|
175
|
+
throw new TypeError(
|
|
176
|
+
`map: The key option appears to be an extension mapping. Did you mean to call Tree.mapExtension() ?`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
keyFn &&= castToFunction(keyFn, "key");
|
|
180
|
+
valueFn &&= castToFunction(valueFn, "value");
|
|
172
181
|
} else if (options === undefined) {
|
|
173
182
|
/** @type {any} */
|
|
174
183
|
const error = new TypeError(`map: The second parameter was undefined.`);
|
|
@@ -248,3 +257,13 @@ function validateOptions(options) {
|
|
|
248
257
|
valueFn,
|
|
249
258
|
};
|
|
250
259
|
}
|
|
260
|
+
|
|
261
|
+
function castToFunction(object, name) {
|
|
262
|
+
const fn = toFunction(object);
|
|
263
|
+
if (!fn) {
|
|
264
|
+
throw new TypeError(
|
|
265
|
+
`map: The ${name} option must be a function but couldn't be treated as one.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return fn;
|
|
269
|
+
}
|
|
@@ -2,39 +2,42 @@ import getMapArgument from "../utilities/getMapArgument.js";
|
|
|
2
2
|
import isMap from "./isMap.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Map and reduce a tree.
|
|
5
|
+
* Map and reduce a `source` tree.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* will be
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Each of the tree's values will be requested in an asynchronous call, then
|
|
8
|
+
* those results will be awaited collectively. If a valueFn is provided, it will
|
|
9
|
+
* be invoked to convert each value to a mapped value; if the valueFn is null or
|
|
10
|
+
* undefined, values will be used as is, although any Promise values will be
|
|
11
|
+
* awaited.
|
|
12
|
+
*
|
|
13
|
+
* The resolved values will be added to a regular `Map` with the same keys as
|
|
14
|
+
* the source. This `Map` will be passed to the reduceFn, along with the
|
|
15
|
+
* original `source`.
|
|
13
16
|
*
|
|
14
17
|
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
15
18
|
* @typedef {import("../../index.ts").ReduceFn} ReduceFn
|
|
16
19
|
* @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
|
|
17
20
|
*
|
|
18
|
-
* @param {Maplike}
|
|
19
|
-
* @param {ValueKeyFn|null}
|
|
21
|
+
* @param {Maplike} source
|
|
22
|
+
* @param {ValueKeyFn|null} valueFn
|
|
20
23
|
* @param {ReduceFn} reduceFn
|
|
21
24
|
*/
|
|
22
|
-
export default async function mapReduce(
|
|
23
|
-
const
|
|
25
|
+
export default async function mapReduce(source, valueFn, reduceFn) {
|
|
26
|
+
const sourceMap = await getMapArgument(source, "mapReduce");
|
|
24
27
|
|
|
25
28
|
// We're going to fire off all the get requests in parallel, as quickly as
|
|
26
29
|
// the keys come in. We call the tree's `get` method for each key, but
|
|
27
30
|
// *don't* wait for it yet.
|
|
28
|
-
const
|
|
31
|
+
const mapped = new Map();
|
|
29
32
|
const promises = [];
|
|
30
|
-
for await (const key of
|
|
31
|
-
|
|
33
|
+
for await (const key of sourceMap.keys()) {
|
|
34
|
+
mapped.set(key, null); // placeholder
|
|
32
35
|
const promise = (async () => {
|
|
33
|
-
const value = await
|
|
36
|
+
const value = await sourceMap.get(key);
|
|
34
37
|
return isMap(value)
|
|
35
|
-
? mapReduce(value,
|
|
36
|
-
:
|
|
37
|
-
?
|
|
38
|
+
? mapReduce(value, valueFn, reduceFn) // subtree; recurse
|
|
39
|
+
: valueFn
|
|
40
|
+
? valueFn(value, key, sourceMap)
|
|
38
41
|
: value;
|
|
39
42
|
})();
|
|
40
43
|
promises.push(promise);
|
|
@@ -44,6 +47,12 @@ export default async function mapReduce(maplike, mapFn, reduceFn) {
|
|
|
44
47
|
// in the same order as the keys, the values will also be in the same order.
|
|
45
48
|
const values = await Promise.all(promises);
|
|
46
49
|
|
|
50
|
+
// Replace the placeholders with the actual values.
|
|
51
|
+
const keys = Array.from(mapped.keys());
|
|
52
|
+
for (let i = 0; i < values.length; i++) {
|
|
53
|
+
mapped.set(keys[i], values[i]);
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
// Reduce the values to a single result.
|
|
48
|
-
return reduceFn(
|
|
57
|
+
return reduceFn(mapped, sourceMap);
|
|
49
58
|
}
|
package/src/operations/mask.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import AsyncMap from "../drivers/AsyncMap.js";
|
|
2
2
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
3
|
import getMapArgument from "../utilities/getMapArgument.js";
|
|
4
|
-
import isMap from "./isMap.js";
|
|
5
4
|
import isMaplike from "./isMaplike.js";
|
|
6
5
|
import keys from "./keys.js";
|
|
7
6
|
|
|
@@ -17,7 +16,10 @@ import keys from "./keys.js";
|
|
|
17
16
|
* @returns {Promise<AsyncMap>}
|
|
18
17
|
*/
|
|
19
18
|
export default async function mask(aMaplike, bMaplike) {
|
|
20
|
-
const aMap = await getMapArgument(aMaplike, "filter", {
|
|
19
|
+
const aMap = await getMapArgument(aMaplike, "filter", {
|
|
20
|
+
deep: true,
|
|
21
|
+
position: 0,
|
|
22
|
+
});
|
|
21
23
|
const bMap = await getMapArgument(bMaplike, "filter", {
|
|
22
24
|
deep: true,
|
|
23
25
|
position: 1,
|
|
@@ -32,7 +34,10 @@ export default async function mask(aMaplike, bMaplike) {
|
|
|
32
34
|
if (!bValue) {
|
|
33
35
|
return undefined;
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
const normalized = /** @type {any} */ (aMap).trailingSlashKeys
|
|
38
|
+
? key
|
|
39
|
+
: trailingSlash.remove(key);
|
|
40
|
+
let aValue = await aMap.get(normalized);
|
|
36
41
|
if (isMaplike(aValue)) {
|
|
37
42
|
// Filter the subtree
|
|
38
43
|
return mask(aValue, bValue);
|
|
@@ -42,25 +47,36 @@ export default async function mask(aMaplike, bMaplike) {
|
|
|
42
47
|
},
|
|
43
48
|
|
|
44
49
|
async *keys() {
|
|
45
|
-
//
|
|
46
|
-
const aKeys = await keys(aMap);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Get keys from a and b
|
|
51
|
+
const [aKeys, bKeys] = await Promise.all([keys(aMap), keys(bMap)]);
|
|
52
|
+
|
|
53
|
+
const combined = Array.from(new Set([...aKeys, ...bKeys]));
|
|
54
|
+
|
|
55
|
+
// Get all the values from b. Because a and b may be defined by functions,
|
|
56
|
+
// they might have values that are not represented in their own keys.
|
|
57
|
+
const bValues = await Promise.all(combined.map((key) => bMap.get(key)));
|
|
58
|
+
|
|
59
|
+
// Find keys that have truthy values in b. While we're at it, we can add
|
|
60
|
+
// slashes even if a or b didn't have them.
|
|
61
|
+
const withSlashes = combined.map((key, index) => {
|
|
62
|
+
const bValue = bValues[index];
|
|
63
|
+
if (!bValue) {
|
|
64
|
+
// Mark for removal
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return trailingSlash.toggle(
|
|
51
68
|
key,
|
|
52
|
-
trailingSlash.has(key) ||
|
|
53
|
-
)
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
59
|
-
yield* treeKeys;
|
|
69
|
+
trailingSlash.has(key) || isMaplike(bValue)
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Yield only the keys that have truthy values in b
|
|
74
|
+
const filtered = withSlashes.filter((value) => value !== undefined);
|
|
75
|
+
yield* new Set(filtered);
|
|
60
76
|
},
|
|
61
77
|
|
|
62
78
|
source: aMap,
|
|
63
79
|
|
|
64
|
-
trailingSlashKeys:
|
|
80
|
+
trailingSlashKeys: true,
|
|
65
81
|
});
|
|
66
82
|
}
|
package/src/operations/paths.js
CHANGED
|
@@ -1,47 +1,45 @@
|
|
|
1
1
|
import * as trailingSlash from "../trailingSlash.js";
|
|
2
2
|
import getMapArgument from "../utilities/getMapArgument.js";
|
|
3
|
+
import from from "./from.js";
|
|
3
4
|
import isMap from "./isMap.js";
|
|
5
|
+
import isMaplike from "./isMaplike.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Returns slash-separated paths for all values in the tree.
|
|
7
9
|
*
|
|
8
10
|
* The `base` argument is prepended to all paths.
|
|
9
11
|
*
|
|
10
|
-
* If `assumeSlashes` is true, then keys are assumed to have trailing slashes to
|
|
11
|
-
* indicate subtrees. The default value of this option is false.
|
|
12
|
-
*
|
|
13
12
|
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
14
13
|
*
|
|
15
14
|
* @param {Maplike} maplike
|
|
16
|
-
* @param {{
|
|
15
|
+
* @param {{ base?: string }} options
|
|
17
16
|
*/
|
|
18
17
|
export default async function paths(maplike, options = {}) {
|
|
19
18
|
const tree = await getMapArgument(maplike, "paths");
|
|
20
19
|
const base = options.base ?? "";
|
|
21
|
-
const assumeSlashes = options.assumeSlashes ?? false;
|
|
22
20
|
const result = [];
|
|
23
21
|
for await (const key of tree.keys()) {
|
|
24
22
|
const separator = trailingSlash.has(base) ? "" : "/";
|
|
25
23
|
const valuePath = base ? `${base}${separator}${key}` : key;
|
|
26
|
-
let isSubtree;
|
|
27
24
|
let value;
|
|
28
|
-
if (
|
|
25
|
+
if (/** @type {any} */ (tree).trailingSlashKeys) {
|
|
29
26
|
// Subtree needs to have a trailing slash
|
|
30
|
-
|
|
31
|
-
if (isSubtree) {
|
|
27
|
+
if (trailingSlash.has(key)) {
|
|
32
28
|
// We'll need the value to recurse
|
|
33
29
|
value = await tree.get(key);
|
|
30
|
+
// If it's maplike, treat as subtree
|
|
31
|
+
if (isMaplike(value)) {
|
|
32
|
+
value = from(value);
|
|
33
|
+
}
|
|
34
34
|
}
|
|
35
35
|
} else {
|
|
36
|
-
// Get value
|
|
36
|
+
// Get value
|
|
37
37
|
value = await tree.get(key);
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (isSubtree) {
|
|
44
|
-
const subPaths = await paths(value, { assumeSlashes, base: valuePath });
|
|
39
|
+
|
|
40
|
+
if (isMap(value)) {
|
|
41
|
+
// Subtree; recurse
|
|
42
|
+
const subPaths = await paths(value, { base: valuePath });
|
|
45
43
|
result.push(...subPaths);
|
|
46
44
|
} else {
|
|
47
45
|
result.push(valuePath);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import getMapArgument from "../utilities/getMapArgument.js";
|
|
2
|
+
import mapReduce from "./mapReduce.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reduce a tree by recursively applying a reducer function to its nodes.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
8
|
+
* @typedef {import("../../index.ts").ReduceFn} ReduceFn
|
|
9
|
+
*
|
|
10
|
+
* @param {Maplike} maplike
|
|
11
|
+
* @param {ReduceFn} reduceFn
|
|
12
|
+
*/
|
|
13
|
+
export default async function reduce(maplike, reduceFn) {
|
|
14
|
+
const map = await getMapArgument(maplike, "reduce");
|
|
15
|
+
return mapReduce(map, null, reduceFn);
|
|
16
|
+
}
|
package/src/operations/root.js
CHANGED
|
@@ -8,9 +8,9 @@ import getMapArgument from "../utilities/getMapArgument.js";
|
|
|
8
8
|
*
|
|
9
9
|
* @param {Maplike} maplike
|
|
10
10
|
*/
|
|
11
|
-
export default function root(maplike) {
|
|
11
|
+
export default async function root(maplike) {
|
|
12
12
|
/** @type {any} */
|
|
13
|
-
let current = getMapArgument(maplike, "root");
|
|
13
|
+
let current = await getMapArgument(maplike, "root");
|
|
14
14
|
while (current.parent || current[symbols.parent]) {
|
|
15
15
|
current = current.parent || current[symbols.parent];
|
|
16
16
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import getMapArgument from "../utilities/getMapArgument.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Set a key/value pair in a map.
|
|
5
|
+
*
|
|
6
|
+
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
7
|
+
*
|
|
8
|
+
* @param {Maplike} maplike
|
|
9
|
+
* @param {any} key
|
|
10
|
+
* @param {any} value
|
|
11
|
+
*/
|
|
12
|
+
export default async function set(maplike, key, value) {
|
|
13
|
+
const map = await getMapArgument(maplike, "set");
|
|
14
|
+
await map.set(key, value);
|
|
15
|
+
|
|
16
|
+
// Unlike Map.prototype.set, we return undefined. This is more useful when
|
|
17
|
+
// calling set in the console -- return the complete tree would result in it
|
|
18
|
+
// being dumped to the console.
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
package/src/operations/sync.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import SyncMap from "../drivers/SyncMap.js";
|
|
2
1
|
import getMapArgument from "../utilities/getMapArgument.js";
|
|
3
|
-
import
|
|
2
|
+
import reduce from "./reduce.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Resolve the async tree to a synchronous tree.
|
|
@@ -11,11 +10,5 @@ import mapReduce from "./mapReduce.js";
|
|
|
11
10
|
*/
|
|
12
11
|
export default async function sync(source) {
|
|
13
12
|
const tree = await getMapArgument(source, "sync");
|
|
14
|
-
return
|
|
15
|
-
const entries = [];
|
|
16
|
-
for (let i = 0; i < keys.length; i++) {
|
|
17
|
-
entries.push([keys[i], values[i]]);
|
|
18
|
-
}
|
|
19
|
-
return new SyncMap(entries);
|
|
20
|
-
});
|
|
13
|
+
return reduce(tree, (mapped) => mapped);
|
|
21
14
|
}
|
|
@@ -59,5 +59,10 @@ export default async function traverseOrThrow(maplike, ...keys) {
|
|
|
59
59
|
position++;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// If last key ended in a slash and value is unpackable, unpack it.
|
|
63
|
+
if (key && trailingSlash.has(key) && isUnpackable(value)) {
|
|
64
|
+
value = await value.unpack();
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
return value;
|
|
63
68
|
}
|
|
@@ -1,29 +1,30 @@
|
|
|
1
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Cast the given map to a plain object or array.
|
|
3
5
|
*
|
|
4
6
|
* If the given plain object has only integer keys, and the set of integers is
|
|
5
7
|
* complete from 0 to length-1, assume the values are a result of array
|
|
6
8
|
* transformations and the values are the desired result; return them as is.
|
|
7
|
-
* Otherwise, create a plain object with the keys and values.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Otherwise, call the createFn to create an object. By default, this will
|
|
11
|
+
* create a plain object from the map's entries.
|
|
12
|
+
*
|
|
13
|
+
* @param {Map} map
|
|
14
|
+
* @param {Function} [createFn]
|
|
11
15
|
*/
|
|
12
|
-
export default function castArraylike(
|
|
13
|
-
|
|
14
|
-
values,
|
|
15
|
-
createFn = Object.fromEntries
|
|
16
|
-
) {
|
|
17
|
-
if (keys.length === 0) {
|
|
16
|
+
export default function castArraylike(map, createFn = Object.fromEntries) {
|
|
17
|
+
if (map.size === 0) {
|
|
18
18
|
// Empty keys/values means an empty object, not an empty array
|
|
19
19
|
return {};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
let onlyNumericKeys = true;
|
|
23
|
-
const numberSeen = new Array(
|
|
24
|
-
for (const key of keys) {
|
|
25
|
-
const
|
|
26
|
-
|
|
23
|
+
const numberSeen = new Array(map.size).fill(false);
|
|
24
|
+
for (const key of map.keys()) {
|
|
25
|
+
const normalized = trailingSlash.remove(key);
|
|
26
|
+
const n = Number(normalized);
|
|
27
|
+
if (isNaN(n) || !Number.isInteger(n) || n < 0 || n >= map.size) {
|
|
27
28
|
onlyNumericKeys = false;
|
|
28
29
|
break;
|
|
29
30
|
} else {
|
|
@@ -34,13 +35,15 @@ export default function castArraylike(
|
|
|
34
35
|
// If any number from 0..length-1 is missing, we can't treat this as an array
|
|
35
36
|
const allNumbersSeen = onlyNumericKeys && numberSeen.every((v) => v);
|
|
36
37
|
if (allNumbersSeen) {
|
|
37
|
-
return values;
|
|
38
|
+
return Array.from(map.values());
|
|
38
39
|
} else {
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// Create a map with normalized keys, then call the createFn to build the
|
|
41
|
+
// result. By default this will create a plain object from the entries.
|
|
42
|
+
const normalizedMap = new Map();
|
|
43
|
+
for (const [key, value] of map.entries()) {
|
|
44
|
+
const normalized = trailingSlash.remove(key);
|
|
45
|
+
normalizedMap.set(normalized, value);
|
|
43
46
|
}
|
|
44
|
-
return createFn(
|
|
47
|
+
return createFn(normalizedMap);
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import ObjectMap from "../drivers/ObjectMap.js";
|
|
2
2
|
import isMaplike from "../operations/isMaplike.js";
|
|
3
3
|
import mapReduce from "../operations/mapReduce.js";
|
|
4
|
-
import * as trailingSlash from "../trailingSlash.js";
|
|
5
4
|
import castArraylike from "./castArraylike.js";
|
|
6
5
|
import isPrimitive from "./isPrimitive.js";
|
|
7
6
|
import isStringlike from "./isStringlike.js";
|
|
@@ -72,14 +71,13 @@ export default async function toPlainValue(
|
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
function reduceToPlainObject(
|
|
76
|
-
// Special case for an empty tree: if based on array, return array.
|
|
77
|
-
if (map instanceof ObjectMap && keys.length === 0) {
|
|
78
|
-
return /** @type {any} */ (map).object instanceof Array ? [] : {};
|
|
79
|
-
}
|
|
74
|
+
function reduceToPlainObject(mapped, source) {
|
|
80
75
|
// Normalize slashes in keys.
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
// Special case for an empty map: if based on array, return array.
|
|
77
|
+
if (mapped.size === 0 && source instanceof ObjectMap) {
|
|
78
|
+
return /** @type {any} */ (source).object instanceof Array ? [] : {};
|
|
79
|
+
}
|
|
80
|
+
return castArraylike(mapped);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
function toBase64(object) {
|
package/test/browser/index.html
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
<script type="module" src="../drivers/BrowserFileMap.test.js"></script>
|
|
19
19
|
<script type="module" src="../drivers/CalendarMap.test.js"></script>
|
|
20
20
|
<script type="module" src="../drivers/ConstantMap.test.js"></script>
|
|
21
|
-
<script type="module" src="../drivers/DeepObjectMap.test.js"></script>
|
|
22
21
|
<script type="module" src="../drivers/FunctionMap.test.js"></script>
|
|
23
22
|
<script type="module" src="../drivers/ObjectMap.test.js"></script>
|
|
24
23
|
<script type="module" src="../drivers/SetMap.test.js"></script>
|