@weborigami/async-tree 0.6.16 → 0.7.0-beta.1
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 +1 -1
- package/package.json +4 -4
- package/shared.js +1 -58
- package/src/Tree.js +1 -5
- package/src/drivers/ObjectMap.js +10 -2
- package/src/drivers/SetMap.js +11 -0
- package/src/drivers/SyncMap.js +1 -1
- package/src/operations/addNextPrevious.js +1 -3
- package/src/operations/cache.js +5 -0
- package/src/operations/combine.js +59 -0
- package/src/operations/deepEntriesIterator.js +1 -0
- package/src/operations/flat.js +1 -1
- package/src/operations/invokeFunctions.js +15 -1
- package/src/operations/map.js +10 -42
- package/src/operations/mapExtension.js +38 -8
- package/src/operations/merge.js +2 -1
- package/src/operations/root.js +3 -3
- package/src/operations/shuffle.js +20 -7
- package/src/operations/traverseOrThrow.js +7 -2
- package/src/utilities/args.js +1 -1
- package/src/utilities/assignPropertyDescriptors.js +26 -0
- package/src/utilities/castArraylike.js +4 -1
- package/test/drivers/SyncMap.test.js +1 -0
- package/test/operations/combine.test.js +35 -0
- package/test/operations/shuffle.test.js +25 -0
- package/test/operations/{length.test.js → size.test.js} +3 -3
- package/test/utilities/castArrayLike.test.js +32 -0
- package/src/operations/group.js +0 -6
- package/src/operations/isAsyncMutableTree.js +0 -8
- package/src/operations/isAsyncTree.js +0 -6
- package/src/operations/isTreelike.js +0 -8
- package/src/operations/length.js +0 -16
package/index.ts
CHANGED
|
@@ -27,7 +27,6 @@ export type MapExtensionOptions = {
|
|
|
27
27
|
deep?: boolean;
|
|
28
28
|
description?: string;
|
|
29
29
|
extension?: string;
|
|
30
|
-
needsSourceValue?: boolean;
|
|
31
30
|
value?: ValueKeyFn;
|
|
32
31
|
};
|
|
33
32
|
|
|
@@ -46,6 +45,7 @@ export type MapOptions = {
|
|
|
46
45
|
inverseKey?: KeyFn;
|
|
47
46
|
key?: ValueKeyFn;
|
|
48
47
|
keyNeedsSourceValue?: boolean;
|
|
48
|
+
needsSourceValue?: boolean;
|
|
49
49
|
value?: ValueKeyFn;
|
|
50
50
|
};
|
|
51
51
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-beta.1",
|
|
4
4
|
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
7
7
|
"browser": "./browser.js",
|
|
8
8
|
"types": "./index.ts",
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/node": "25.
|
|
11
|
-
"puppeteer": "
|
|
12
|
-
"typescript": "
|
|
10
|
+
"@types/node": "25.9.1",
|
|
11
|
+
"puppeteer": "25.1.0",
|
|
12
|
+
"typescript": "6.0.3"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"headlessTest": "node scripts/headlessTest.js",
|
package/shared.js
CHANGED
|
@@ -20,6 +20,7 @@ export * as trailingSlash from "./src/trailingSlash.js";
|
|
|
20
20
|
export { default as TraverseError } from "./src/TraverseError.js";
|
|
21
21
|
export * as Tree from "./src/Tree.js";
|
|
22
22
|
export * as args from "./src/utilities/args.js";
|
|
23
|
+
export { default as assignPropertyDescriptors } from "./src/utilities/assignPropertyDescriptors.js";
|
|
23
24
|
export { default as box } from "./src/utilities/box.js";
|
|
24
25
|
export { default as castArraylike } from "./src/utilities/castArraylike.js";
|
|
25
26
|
export { default as getParent } from "./src/utilities/getParent.js";
|
|
@@ -38,61 +39,3 @@ export { default as toPlainValue } from "./src/utilities/toPlainValue.js";
|
|
|
38
39
|
export { default as toString } from "./src/utilities/toString.js";
|
|
39
40
|
|
|
40
41
|
export { ExplorableSiteMap, FileMap, FunctionMap, ObjectMap, SetMap, SiteMap };
|
|
41
|
-
|
|
42
|
-
export class DeepObjectMap extends ObjectMap {
|
|
43
|
-
constructor(object) {
|
|
44
|
-
super(object, { deep: true });
|
|
45
|
-
console.warn("DeepObjectMap is deprecated. Please use ObjectMap instead.");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export class ObjectTree extends ObjectMap {
|
|
50
|
-
constructor(...args) {
|
|
51
|
-
super(...args);
|
|
52
|
-
console.warn("ObjectTree is deprecated. Please use ObjectMap instead.");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class DeepObjectTree extends ObjectMap {
|
|
57
|
-
constructor(object) {
|
|
58
|
-
super(object, { deep: true });
|
|
59
|
-
console.warn("DeepObjectTree is deprecated. Please use ObjectMap instead.");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export class ExplorableSiteTree extends ExplorableSiteMap {
|
|
64
|
-
constructor(href) {
|
|
65
|
-
super(href);
|
|
66
|
-
console.warn(
|
|
67
|
-
"ExplorableSiteTree is deprecated. Please use ExplorableSiteMap instead.",
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export class FileTree extends FileMap {
|
|
73
|
-
constructor(...args) {
|
|
74
|
-
super(...args);
|
|
75
|
-
console.warn("FileTree is deprecated. Please use FileMap instead.");
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export class FunctionTree extends FunctionMap {
|
|
80
|
-
constructor(...args) {
|
|
81
|
-
super(...args);
|
|
82
|
-
console.warn("FunctionTree is deprecated. Please use FunctionMap instead.");
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export class SetTree extends SetMap {
|
|
87
|
-
constructor(set) {
|
|
88
|
-
super(set);
|
|
89
|
-
console.warn("SetTree is deprecated. Please use SetMap instead.");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export class SiteTree extends SiteMap {
|
|
94
|
-
constructor(...args) {
|
|
95
|
-
super(...args);
|
|
96
|
-
console.warn("SiteTree is deprecated. Please use SiteMap instead.");
|
|
97
|
-
}
|
|
98
|
-
}
|
package/src/Tree.js
CHANGED
|
@@ -8,6 +8,7 @@ export { default as cache } from "./operations/cache.js";
|
|
|
8
8
|
export { default as calendar } from "./operations/calendar.js";
|
|
9
9
|
export { default as child } from "./operations/child.js";
|
|
10
10
|
export { default as clear } from "./operations/clear.js";
|
|
11
|
+
export { default as combine } from "./operations/combine.js";
|
|
11
12
|
export { default as concat } from "./operations/concat.js";
|
|
12
13
|
export { default as constant } from "./operations/constant.js";
|
|
13
14
|
export { default as deepArrays } from "./operations/deepArrays.js";
|
|
@@ -29,23 +30,18 @@ export { default as flat } from "./operations/flat.js";
|
|
|
29
30
|
export { default as forEach } from "./operations/forEach.js";
|
|
30
31
|
export { default as from } from "./operations/from.js";
|
|
31
32
|
export { default as globKeys } from "./operations/globKeys.js";
|
|
32
|
-
export { default as group } from "./operations/group.js";
|
|
33
33
|
export { default as groupBy } from "./operations/groupBy.js";
|
|
34
34
|
export { default as has } from "./operations/has.js";
|
|
35
35
|
export { default as indent } from "./operations/indent.js";
|
|
36
36
|
export { default as inflatePaths } from "./operations/inflatePaths.js";
|
|
37
37
|
export { default as inners } from "./operations/inners.js";
|
|
38
38
|
export { default as invokeFunctions } from "./operations/invokeFunctions.js";
|
|
39
|
-
export { default as isAsyncMutableTree } from "./operations/isAsyncMutableTree.js";
|
|
40
|
-
export { default as isAsyncTree } from "./operations/isAsyncTree.js";
|
|
41
39
|
export { default as isMap } from "./operations/isMap.js";
|
|
42
40
|
export { default as isMaplike } from "./operations/isMaplike.js";
|
|
43
41
|
export { default as isReadOnlyMap } from "./operations/isReadOnlyMap.js";
|
|
44
42
|
export { default as isTraversable } from "./operations/isTraversable.js";
|
|
45
|
-
export { default as isTreelike } from "./operations/isTreelike.js";
|
|
46
43
|
export { default as json } from "./operations/json.js";
|
|
47
44
|
export { default as keys } from "./operations/keys.js";
|
|
48
|
-
export { default as length } from "./operations/length.js";
|
|
49
45
|
export { default as map } from "./operations/map.js";
|
|
50
46
|
export { default as mapExtension } from "./operations/mapExtension.js";
|
|
51
47
|
export { default as mapReduce } from "./operations/mapReduce.js";
|
package/src/drivers/ObjectMap.js
CHANGED
|
@@ -20,7 +20,7 @@ export default class ObjectMap extends SyncMap {
|
|
|
20
20
|
// objects such as Node's `Module` class for representing an ES module.
|
|
21
21
|
if (typeof object !== "object" || object === null) {
|
|
22
22
|
throw new TypeError(
|
|
23
|
-
`${this.constructor.name}: Expected an object or array
|
|
23
|
+
`${this.constructor.name}: Expected an object or array.`,
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
this.object = object;
|
|
@@ -38,6 +38,13 @@ export default class ObjectMap extends SyncMap {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
get(key) {
|
|
41
|
+
if (key == null) {
|
|
42
|
+
// Reject nullish key
|
|
43
|
+
throw new ReferenceError(
|
|
44
|
+
`${this.constructor.name}: Cannot get a null or undefined key.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
// Does the object have the key with or without a trailing slash?
|
|
42
49
|
const existingKey = findExistingKey(this.object, key);
|
|
43
50
|
if (existingKey === null) {
|
|
@@ -111,7 +118,8 @@ export default class ObjectMap extends SyncMap {
|
|
|
111
118
|
? name
|
|
112
119
|
: trailingSlash.toggle(
|
|
113
120
|
name,
|
|
114
|
-
descriptor.value !== undefined &&
|
|
121
|
+
descriptor.value !== undefined &&
|
|
122
|
+
this.isSubtree(descriptor.value),
|
|
115
123
|
);
|
|
116
124
|
result.add(key);
|
|
117
125
|
}
|
package/src/drivers/SetMap.js
CHANGED
|
@@ -13,4 +13,15 @@ export default class SetMap extends SyncMap {
|
|
|
13
13
|
const entries = Array.from(set).map((value, index) => [index, value]);
|
|
14
14
|
super(entries);
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
get(key) {
|
|
18
|
+
if (key == null) {
|
|
19
|
+
// Reject nullish key
|
|
20
|
+
throw new ReferenceError(
|
|
21
|
+
`${this.constructor.name}: Cannot get a null or undefined key.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return super.get(key);
|
|
26
|
+
}
|
|
16
27
|
}
|
package/src/drivers/SyncMap.js
CHANGED
|
@@ -128,7 +128,7 @@ export default class SyncMap extends Map {
|
|
|
128
128
|
*/
|
|
129
129
|
get(key) {
|
|
130
130
|
let value = super.get.call(this._self, key);
|
|
131
|
-
if (value === undefined) {
|
|
131
|
+
if (value === undefined && this.trailingSlashKeys) {
|
|
132
132
|
// Try alternate key with trailing slash added or removed
|
|
133
133
|
value = super.get.call(this._self, trailingSlash.toggle(key));
|
|
134
134
|
}
|
|
@@ -10,7 +10,7 @@ import keys from "./keys.js";
|
|
|
10
10
|
*/
|
|
11
11
|
export default async function addNextPrevious(maplike) {
|
|
12
12
|
const source = await args.map(maplike, "Tree.addNextPrevious");
|
|
13
|
-
|
|
13
|
+
const sourceKeys = await keys(source);
|
|
14
14
|
|
|
15
15
|
return Object.assign(new AsyncMap(), {
|
|
16
16
|
async get(key) {
|
|
@@ -29,7 +29,6 @@ export default async function addNextPrevious(maplike) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// Find the index of the current key
|
|
32
|
-
sourceKeys ??= await keys(source);
|
|
33
32
|
const index = sourceKeys.indexOf(key);
|
|
34
33
|
if (index >= 0) {
|
|
35
34
|
// Extend result with nextKey/previousKey properties.
|
|
@@ -47,7 +46,6 @@ export default async function addNextPrevious(maplike) {
|
|
|
47
46
|
},
|
|
48
47
|
|
|
49
48
|
async *keys() {
|
|
50
|
-
sourceKeys ??= await keys(source);
|
|
51
49
|
yield* sourceKeys;
|
|
52
50
|
},
|
|
53
51
|
|
package/src/operations/cache.js
CHANGED
|
@@ -67,6 +67,11 @@ export default async function treeCache(sourceMaplike, cacheMaplike) {
|
|
|
67
67
|
},
|
|
68
68
|
|
|
69
69
|
async *keys() {
|
|
70
|
+
// REVIEW: Saving our own copy of the source keys can create issues when
|
|
71
|
+
// this operation is applied in an Origami site. Because this keys() call
|
|
72
|
+
// happens outside of the language package's system cache, the system
|
|
73
|
+
// cache may not detect a dependency. If the underlying keys change, the
|
|
74
|
+
// keys obtained here won't be invalidated.
|
|
70
75
|
sourceKeys ??= await keys(source);
|
|
71
76
|
yield* sourceKeys;
|
|
72
77
|
},
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as args from "../utilities/args.js";
|
|
2
|
+
import isUnpackable from "../utilities/isUnpackable.js";
|
|
3
|
+
import isMap from "./isMap.js";
|
|
4
|
+
import keys from "./keys.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Does a pairwise invocation of `combineFn` for each value in the two trees. If
|
|
8
|
+
* one tree has a key that the other doesn't, the `combineFn` will be invoked
|
|
9
|
+
* with `undefined` for the missing value.
|
|
10
|
+
*
|
|
11
|
+
* This returns a new tree of all the results of the `combineFn` invocations
|
|
12
|
+
* that were not `undefined`. If all results were `undefined`, the overall
|
|
13
|
+
* result is itself `undefined`.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
16
|
+
*
|
|
17
|
+
* @param {Maplike} maplike1
|
|
18
|
+
* @param {Maplike} maplike2
|
|
19
|
+
* @param {function} combineFn
|
|
20
|
+
*/
|
|
21
|
+
export default async function combine(maplike1, maplike2, combineFn) {
|
|
22
|
+
const tree1 = await args.map(maplike1, "Tree.combine", {
|
|
23
|
+
deep: true,
|
|
24
|
+
position: 1,
|
|
25
|
+
});
|
|
26
|
+
const tree2 = await args.map(maplike2, "Tree.combine", {
|
|
27
|
+
deep: true,
|
|
28
|
+
position: 2,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (isUnpackable(combineFn)) {
|
|
32
|
+
combineFn = await combineFn.unpack();
|
|
33
|
+
}
|
|
34
|
+
const fn = args.fn(combineFn, "Tree.combine", {
|
|
35
|
+
position: 3,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const keys1 = await keys(tree1);
|
|
39
|
+
const keys2 = await keys(tree2);
|
|
40
|
+
const combinedKeys = new Set([...keys1, ...keys2]);
|
|
41
|
+
|
|
42
|
+
const result = {};
|
|
43
|
+
|
|
44
|
+
for (const key of combinedKeys) {
|
|
45
|
+
const value1 = await tree1.get(key);
|
|
46
|
+
const value2 = await tree2.get(key);
|
|
47
|
+
|
|
48
|
+
const combination =
|
|
49
|
+
isMap(value1) && isMap(value2)
|
|
50
|
+
? await combine(value1, value2, fn)
|
|
51
|
+
: await fn(value1, value2);
|
|
52
|
+
|
|
53
|
+
if (combination !== undefined) {
|
|
54
|
+
result[key] = combination;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
59
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as args from "../utilities/args.js";
|
|
|
2
2
|
import isUnpackable from "../utilities/isUnpackable.js";
|
|
3
3
|
import isMap from "./isMap.js";
|
|
4
4
|
import isMaplike from "./isMaplike.js";
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* Return an iterator that yields all entries in a tree, including nested trees.
|
|
7
8
|
*
|
package/src/operations/flat.js
CHANGED
|
@@ -17,7 +17,7 @@ import deepEntriesIterator from "./deepEntriesIterator.js";
|
|
|
17
17
|
* @param {number} [depth] The maximum depth to flatten
|
|
18
18
|
*/
|
|
19
19
|
export default async function flat(maplike, depth = 1) {
|
|
20
|
-
const map = await args.map(maplike, "Tree.flat"
|
|
20
|
+
const map = await args.map(maplike, "Tree.flat");
|
|
21
21
|
|
|
22
22
|
let index = 0;
|
|
23
23
|
let onlyNumericKeys = true;
|
|
@@ -7,7 +7,7 @@ export default async function invokeFunctions(maplike) {
|
|
|
7
7
|
deep: true,
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const result = Object.assign(new AsyncMap(), {
|
|
11
11
|
description: "invokeFunctions",
|
|
12
12
|
|
|
13
13
|
async get(key) {
|
|
@@ -30,4 +30,18 @@ export default async function invokeFunctions(maplike) {
|
|
|
30
30
|
|
|
31
31
|
trailingSlashKeys: /** @type {any} */ (source).trailingSlashKeys,
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
if (!(/** @type {any} */ (source).readOnly)) {
|
|
35
|
+
Object.assign(result, {
|
|
36
|
+
delete(key) {
|
|
37
|
+
return source.delete(key);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
set(key, value) {
|
|
41
|
+
return source.set(key, value);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result;
|
|
33
47
|
}
|
package/src/operations/map.js
CHANGED
|
@@ -5,10 +5,8 @@ import isPlainObject from "../utilities/isPlainObject.js";
|
|
|
5
5
|
import isUnpackable from "../utilities/isUnpackable.js";
|
|
6
6
|
import toFunction from "../utilities/toFunction.js";
|
|
7
7
|
import cachedKeyFunctions from "./cachedKeyFunctions.js";
|
|
8
|
-
import extensionKeyFunctions from "./extensionKeyFunctions.js";
|
|
9
8
|
import isMap from "./isMap.js";
|
|
10
9
|
import keys from "./keys.js";
|
|
11
|
-
import parseExtensions from "./parseExtensions.js";
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Transform the keys and/or values of a tree.
|
|
@@ -147,7 +145,6 @@ function validateOption(options, key) {
|
|
|
147
145
|
function validateOptions(options) {
|
|
148
146
|
let deep;
|
|
149
147
|
let description;
|
|
150
|
-
let extension;
|
|
151
148
|
let inverseKeyFn;
|
|
152
149
|
let keyFn;
|
|
153
150
|
let keyNeedsSourceValue;
|
|
@@ -162,7 +159,6 @@ function validateOptions(options) {
|
|
|
162
159
|
|
|
163
160
|
// Validate individual options
|
|
164
161
|
deep = validateOption(options, "deep");
|
|
165
|
-
extension = validateOption(options, "extension");
|
|
166
162
|
inverseKeyFn = validateOption(options, "inverseKey");
|
|
167
163
|
keyFn = validateOption(options, "key");
|
|
168
164
|
keyNeedsSourceValue = validateOption(options, "keyNeedsSourceValue");
|
|
@@ -196,49 +192,21 @@ function validateOptions(options) {
|
|
|
196
192
|
throw error;
|
|
197
193
|
}
|
|
198
194
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (extension && (keyFn || inverseKeyFn)) {
|
|
205
|
-
throw new TypeError(
|
|
206
|
-
`Tree.map: You can't specify extensions and also a key or inverseKey function`,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
if (extension && keyNeedsSourceValue === true) {
|
|
195
|
+
// If key or inverseKey weren't specified, look for sidecar functions
|
|
196
|
+
inverseKeyFn ??= valueFn?.inverseKey;
|
|
197
|
+
keyFn ??= valueFn?.key;
|
|
198
|
+
|
|
199
|
+
if (!keyFn && inverseKeyFn) {
|
|
210
200
|
throw new TypeError(
|
|
211
|
-
`Tree.map:
|
|
201
|
+
`Tree.map: You can't specify an inverseKey function without a key function`,
|
|
212
202
|
);
|
|
213
203
|
}
|
|
214
204
|
|
|
215
|
-
if (
|
|
216
|
-
//
|
|
217
|
-
const
|
|
218
|
-
const keyFns = extensionKeyFunctions(
|
|
219
|
-
parsed.sourceExtension,
|
|
220
|
-
parsed.resultExtension,
|
|
221
|
-
);
|
|
205
|
+
if (keyFn && !inverseKeyFn) {
|
|
206
|
+
// Only keyFn was provided, so we need to generate the inverseKeyFn
|
|
207
|
+
const keyFns = cachedKeyFunctions(keyFn, deep);
|
|
222
208
|
keyFn = keyFns.key;
|
|
223
209
|
inverseKeyFn = keyFns.inverseKey;
|
|
224
|
-
keyNeedsSourceValue = false;
|
|
225
|
-
} else {
|
|
226
|
-
// If key or inverseKey weren't specified, look for sidecar functions
|
|
227
|
-
inverseKeyFn ??= valueFn?.inverseKey;
|
|
228
|
-
keyFn ??= valueFn?.key;
|
|
229
|
-
|
|
230
|
-
if (!keyFn && inverseKeyFn) {
|
|
231
|
-
throw new TypeError(
|
|
232
|
-
`Tree.map: You can't specify an inverseKey function without a key function`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (keyFn && !inverseKeyFn) {
|
|
237
|
-
// Only keyFn was provided, so we need to generate the inverseKeyFn
|
|
238
|
-
const keyFns = cachedKeyFunctions(keyFn, deep);
|
|
239
|
-
keyFn = keyFns.key;
|
|
240
|
-
inverseKeyFn = keyFns.inverseKey;
|
|
241
|
-
}
|
|
242
210
|
}
|
|
243
211
|
|
|
244
212
|
if (!valueFn && !keyFn) {
|
|
@@ -250,7 +218,7 @@ function validateOptions(options) {
|
|
|
250
218
|
// Set defaults for options not specified. We don't set a default value for
|
|
251
219
|
// `deep` because a false value is a stronger signal than undefined.
|
|
252
220
|
description ??= "key/value map";
|
|
253
|
-
keyNeedsSourceValue ??= true;
|
|
221
|
+
keyNeedsSourceValue ??= keyFn?.needsSourceValue ?? true;
|
|
254
222
|
|
|
255
223
|
return {
|
|
256
224
|
deep,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import isPlainObject from "../utilities/isPlainObject.js";
|
|
2
2
|
import isUnpackable from "../utilities/isUnpackable.js";
|
|
3
|
+
import extensionKeyFunctions from "./extensionKeyFunctions.js";
|
|
3
4
|
import map from "./map.js";
|
|
5
|
+
import parseExtensions from "./parseExtensions.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* @typedef {import("../../index.ts").AsyncMap} AsyncMap
|
|
@@ -44,14 +46,17 @@ import map from "./map.js";
|
|
|
44
46
|
* @returns {Promise<AsyncMap>}
|
|
45
47
|
*/
|
|
46
48
|
export default async function mapExtension(maplike, arg2, arg3) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
let extension;
|
|
50
|
+
|
|
51
|
+
/** @type {import("../../index.ts").MapOptions} */
|
|
52
|
+
let options = { keyNeedsSourceValue: false };
|
|
53
|
+
let optionsArg;
|
|
50
54
|
if (arg3 === undefined) {
|
|
51
55
|
if (typeof arg2 === "string") {
|
|
52
|
-
|
|
56
|
+
extension = arg2;
|
|
53
57
|
} else if (isPlainObject(arg2)) {
|
|
54
|
-
|
|
58
|
+
extension = arg2.extension;
|
|
59
|
+
optionsArg = arg2;
|
|
55
60
|
} else {
|
|
56
61
|
throw new TypeError(
|
|
57
62
|
"Tree.mapExtension: Expected a string or options object for the second argument.",
|
|
@@ -63,14 +68,14 @@ export default async function mapExtension(maplike, arg2, arg3) {
|
|
|
63
68
|
"Tree.mapExtension: Expected a string for the second argument.",
|
|
64
69
|
);
|
|
65
70
|
}
|
|
66
|
-
|
|
71
|
+
extension = arg2;
|
|
67
72
|
if (isUnpackable(arg3)) {
|
|
68
73
|
arg3 = await arg3.unpack();
|
|
69
74
|
}
|
|
70
75
|
if (typeof arg3 === "function") {
|
|
71
76
|
options.value = arg3;
|
|
72
77
|
} else if (isPlainObject(arg3)) {
|
|
73
|
-
|
|
78
|
+
optionsArg = arg3;
|
|
74
79
|
} else {
|
|
75
80
|
throw new TypeError(
|
|
76
81
|
"Tree.mapExtension: Expected a function or options object for the third argument.",
|
|
@@ -78,9 +83,34 @@ export default async function mapExtension(maplike, arg2, arg3) {
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
if (!extension) {
|
|
87
|
+
throw new TypeError(
|
|
88
|
+
"Tree.mapExtension: An extension mapping string is required.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (optionsArg?.deep !== undefined) {
|
|
93
|
+
options.deep = optionsArg.deep;
|
|
94
|
+
}
|
|
95
|
+
if (optionsArg?.description !== undefined) {
|
|
96
|
+
options.description = optionsArg.description;
|
|
97
|
+
}
|
|
98
|
+
if (optionsArg?.value !== undefined) {
|
|
99
|
+
options.value = optionsArg.value;
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
if (!options.description) {
|
|
82
|
-
options.description = `mapExtension ${
|
|
103
|
+
options.description = `mapExtension ${extension}`;
|
|
83
104
|
}
|
|
84
105
|
|
|
106
|
+
// Use the extension mapping to generate key and inverseKey functions
|
|
107
|
+
const parsed = parseExtensions(extension);
|
|
108
|
+
const keyFns = extensionKeyFunctions(
|
|
109
|
+
parsed.sourceExtension,
|
|
110
|
+
parsed.resultExtension,
|
|
111
|
+
);
|
|
112
|
+
options.key = keyFns.key;
|
|
113
|
+
options.inverseKey = keyFns.inverseKey;
|
|
114
|
+
|
|
85
115
|
return map(maplike, options);
|
|
86
116
|
}
|
package/src/operations/merge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AsyncMap from "../drivers/AsyncMap.js";
|
|
2
2
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import assignPropertyDescriptors from "../utilities/assignPropertyDescriptors.js";
|
|
3
4
|
import isPlainObject from "../utilities/isPlainObject.js";
|
|
4
5
|
import isUnpackable from "../utilities/isUnpackable.js";
|
|
5
6
|
import from from "./from.js";
|
|
@@ -42,7 +43,7 @@ export default async function merge(...treelikes) {
|
|
|
42
43
|
|
|
43
44
|
// If all arguments are plain objects, return a plain object.
|
|
44
45
|
if (unpacked.every((source) => !isMap(source) && isPlainObject(source))) {
|
|
45
|
-
return
|
|
46
|
+
return assignPropertyDescriptors({}, ...unpacked);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const sources = unpacked.map((maplike) => from(maplike));
|
package/src/operations/root.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as symbols from "../symbols.js";
|
|
2
|
-
import
|
|
2
|
+
import from from "./from.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Walk up the `parent` chain to find the root of the tree.
|
|
@@ -8,9 +8,9 @@ import * as args from "../utilities/args.js";
|
|
|
8
8
|
*
|
|
9
9
|
* @param {Maplike} maplike
|
|
10
10
|
*/
|
|
11
|
-
export default
|
|
11
|
+
export default function root(maplike) {
|
|
12
12
|
/** @type {any} */
|
|
13
|
-
let current =
|
|
13
|
+
let current = from(maplike);
|
|
14
14
|
while (current.parent || current[symbols.parent]) {
|
|
15
15
|
current = current.parent || current[symbols.parent];
|
|
16
16
|
}
|
|
@@ -3,16 +3,22 @@ import * as args from "../utilities/args.js";
|
|
|
3
3
|
import keys from "./keys.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Return a new tree with the original's keys shuffled
|
|
6
|
+
* Return a new tree with the original's keys shuffled.
|
|
7
|
+
*
|
|
8
|
+
* The `randoms` option allows you to provide a function that either returns a
|
|
9
|
+
* random number between 0 and 1 (like `Math.random`) or a random integer. This
|
|
10
|
+
* can be used to create deterministic shuffling.
|
|
7
11
|
*
|
|
8
12
|
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
13
|
+
* @typedef {import("../../index.ts").Stringlike} Stringlike
|
|
9
14
|
*
|
|
10
15
|
* @param {Maplike} maplike
|
|
11
|
-
* @param {
|
|
16
|
+
* @param {{ randoms?: (() => number) }?} options
|
|
12
17
|
* @returns {Promise<AsyncMap>}
|
|
13
18
|
*/
|
|
14
|
-
export default async function shuffle(maplike,
|
|
19
|
+
export default async function shuffle(maplike, options = {}) {
|
|
15
20
|
const source = await args.map(maplike, "Tree.shuffle");
|
|
21
|
+
const randoms = options?.randoms ?? Math.random;
|
|
16
22
|
|
|
17
23
|
let mapKeys;
|
|
18
24
|
|
|
@@ -24,9 +30,9 @@ export default async function shuffle(maplike, reshuffle = false) {
|
|
|
24
30
|
},
|
|
25
31
|
|
|
26
32
|
async *keys() {
|
|
27
|
-
if (!mapKeys
|
|
33
|
+
if (!mapKeys) {
|
|
28
34
|
mapKeys = await keys(source);
|
|
29
|
-
shuffleArray(mapKeys);
|
|
35
|
+
shuffleArray(mapKeys, randoms);
|
|
30
36
|
}
|
|
31
37
|
yield* mapKeys;
|
|
32
38
|
},
|
|
@@ -42,10 +48,17 @@ export default async function shuffle(maplike, reshuffle = false) {
|
|
|
42
48
|
*
|
|
43
49
|
* Performs a Fisher-Yates shuffle. From http://sedition.com/perl/javascript-fy.html
|
|
44
50
|
*/
|
|
45
|
-
export function shuffleArray(array) {
|
|
51
|
+
export function shuffleArray(array, randoms) {
|
|
46
52
|
let i = array.length;
|
|
47
53
|
while (--i >= 0) {
|
|
48
|
-
const
|
|
54
|
+
const random = randoms();
|
|
55
|
+
|
|
56
|
+
const j =
|
|
57
|
+
random < 1
|
|
58
|
+
? // Like Math.random
|
|
59
|
+
Math.floor(random * (i + 1))
|
|
60
|
+
: // Random number
|
|
61
|
+
Math.floor(random) % (i + 1);
|
|
49
62
|
const temp = array[i];
|
|
50
63
|
array[i] = array[j];
|
|
51
64
|
array[j] = temp;
|
|
@@ -42,8 +42,12 @@ export default async function traverseOrThrow(maplike, ...keys) {
|
|
|
42
42
|
if (typeof (/** @type {any} */ (value).unpack) === "function") {
|
|
43
43
|
value = await value.unpack();
|
|
44
44
|
} else {
|
|
45
|
+
const type =
|
|
46
|
+
typeof value === "string" || value instanceof String
|
|
47
|
+
? "string"
|
|
48
|
+
: "binary";
|
|
45
49
|
throw new TraverseError(
|
|
46
|
-
|
|
50
|
+
`A path hit ${type} data that can't be unpacked.`,
|
|
47
51
|
{
|
|
48
52
|
head: maplike,
|
|
49
53
|
lastValue,
|
|
@@ -60,8 +64,9 @@ export default async function traverseOrThrow(maplike, ...keys) {
|
|
|
60
64
|
// We'll take as many keys as the function's length, but at least one.
|
|
61
65
|
let fnKeyCount = Math.max(fn.length, 1);
|
|
62
66
|
const args = remainingKeys.splice(0, fnKeyCount);
|
|
67
|
+
const normalized = args.map((key) => trailingSlash.remove(key));
|
|
63
68
|
key = null;
|
|
64
|
-
value = await fn(...
|
|
69
|
+
value = await fn(...normalized);
|
|
65
70
|
} else {
|
|
66
71
|
// Cast value to a map.
|
|
67
72
|
const map = from(value);
|
package/src/utilities/args.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function map(arg, operation, options = {}) {
|
|
|
69
69
|
} catch (/** @type {any} */ error) {
|
|
70
70
|
let message = error.message ?? error;
|
|
71
71
|
message = `${operation}: ${message}`;
|
|
72
|
-
const newError = new
|
|
72
|
+
const newError = new TypeError(message);
|
|
73
73
|
/** @type {any} */ (newError).position = position;
|
|
74
74
|
throw newError;
|
|
75
75
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is an analogue of Object.assign that destructively copies properties to
|
|
3
|
+
* a target object -- but avoids invoking property getters. Instead, it copies
|
|
4
|
+
* property descriptors over to the target object.
|
|
5
|
+
*
|
|
6
|
+
* @param {any} target
|
|
7
|
+
* @param {...any} sources
|
|
8
|
+
*/
|
|
9
|
+
export default function assignPropertyDescriptors(target, ...sources) {
|
|
10
|
+
for (const source of sources) {
|
|
11
|
+
if (!source) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const descriptors = Object.getOwnPropertyDescriptors(source);
|
|
15
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
16
|
+
if (descriptor.value !== undefined) {
|
|
17
|
+
// Simple value, copy it
|
|
18
|
+
target[key] = descriptor.value;
|
|
19
|
+
} else {
|
|
20
|
+
// Getter and/or setter, copy the descriptor
|
|
21
|
+
Object.defineProperty(target, key, descriptor);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return target;
|
|
26
|
+
}
|
|
@@ -41,7 +41,10 @@ export default function castArraylike(map, createFn = Object.fromEntries) {
|
|
|
41
41
|
// result. By default this will create a plain object from the entries.
|
|
42
42
|
const normalizedMap = new Map();
|
|
43
43
|
for (const [key, value] of map.entries()) {
|
|
44
|
-
|
|
44
|
+
// Normalize the key by stripping trailing slashes, but only if there
|
|
45
|
+
// aren't multiple keys that only differ by trailing slashes.
|
|
46
|
+
const normalize = !map.has(trailingSlash.toggle(key));
|
|
47
|
+
const normalized = normalize ? trailingSlash.remove(key) : key;
|
|
45
48
|
normalizedMap.set(normalized, value);
|
|
46
49
|
}
|
|
47
50
|
return createFn(normalizedMap);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import combine from "../../src/operations/combine.js";
|
|
4
|
+
|
|
5
|
+
describe("combine", () => {
|
|
6
|
+
test("combines two trees", async () => {
|
|
7
|
+
const oldTree = {
|
|
8
|
+
a: {
|
|
9
|
+
b: "old",
|
|
10
|
+
c: "old",
|
|
11
|
+
d: "old",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const newTree = {
|
|
15
|
+
a: {
|
|
16
|
+
b: "new",
|
|
17
|
+
c: "old",
|
|
18
|
+
},
|
|
19
|
+
e: "new",
|
|
20
|
+
};
|
|
21
|
+
const combination = await combine(oldTree, newTree, compareFn);
|
|
22
|
+
assert.deepEqual(combination, {
|
|
23
|
+
"a/": {
|
|
24
|
+
b: ["old", "new"],
|
|
25
|
+
c: ["old", "old"],
|
|
26
|
+
d: ["old", undefined],
|
|
27
|
+
},
|
|
28
|
+
e: [undefined, "new"],
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function compareFn(a, b) {
|
|
34
|
+
return [a, b];
|
|
35
|
+
}
|
|
@@ -16,4 +16,29 @@ describe("shuffle", () => {
|
|
|
16
16
|
const treeKeys = await keys(result);
|
|
17
17
|
assert.deepEqual(treeKeys.sort(), Object.keys(obj).sort());
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
test("accepts a randoms function for deterministic shuffling", async () => {
|
|
21
|
+
const obj = {
|
|
22
|
+
a: 1,
|
|
23
|
+
b: 2,
|
|
24
|
+
c: 3,
|
|
25
|
+
d: 4,
|
|
26
|
+
e: 5,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function count() {
|
|
30
|
+
let index = 0;
|
|
31
|
+
return function () {
|
|
32
|
+
return index++;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result1 = await shuffle(obj, { randoms: count() });
|
|
37
|
+
const keys1 = await keys(result1);
|
|
38
|
+
|
|
39
|
+
const result2 = await shuffle(obj, { randoms: count() });
|
|
40
|
+
const keys2 = await keys(result2);
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(keys1, keys2);
|
|
43
|
+
});
|
|
19
44
|
});
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
-
import
|
|
3
|
+
import size from "../../src/operations/size.js";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
5
|
+
describe("size", () => {
|
|
6
6
|
test("returns the number of keys in the tree", async () => {
|
|
7
7
|
const obj = {
|
|
8
8
|
a: 1,
|
|
9
9
|
b: 2,
|
|
10
10
|
c: 3,
|
|
11
11
|
};
|
|
12
|
-
const result = await
|
|
12
|
+
const result = await size(obj);
|
|
13
13
|
assert.equal(result, 3);
|
|
14
14
|
});
|
|
15
15
|
});
|
|
@@ -50,4 +50,36 @@ describe("castArraylike", () => {
|
|
|
50
50
|
3: "c",
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
test("strips trailing slashes if map only has one form of the key", () => {
|
|
55
|
+
const map = new /** @type {any} */ (Map)([
|
|
56
|
+
["a/", 1],
|
|
57
|
+
["b", 2],
|
|
58
|
+
["c/", 3],
|
|
59
|
+
]);
|
|
60
|
+
const result = castArraylike(map);
|
|
61
|
+
assert.deepEqual(result, {
|
|
62
|
+
a: 1,
|
|
63
|
+
b: 2,
|
|
64
|
+
c: 3,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test.only("preserves trailing slashes if map has both forms of the key", () => {
|
|
69
|
+
const map = new /** @type {any} */ (Map)([
|
|
70
|
+
["a/", 1],
|
|
71
|
+
["a", 2],
|
|
72
|
+
["b", 3],
|
|
73
|
+
["c/", 4],
|
|
74
|
+
["c", 5],
|
|
75
|
+
]);
|
|
76
|
+
const result = castArraylike(map);
|
|
77
|
+
assert.deepEqual(result, {
|
|
78
|
+
"a/": 1,
|
|
79
|
+
a: 2,
|
|
80
|
+
b: 3,
|
|
81
|
+
"c/": 4,
|
|
82
|
+
c: 5,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
53
85
|
});
|
package/src/operations/group.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import isReadOnlyMap from "./isReadOnlyMap.js";
|
|
2
|
-
|
|
3
|
-
export default function isAsyncMutableTree(treelike) {
|
|
4
|
-
console.warn(
|
|
5
|
-
"Tree.isAsyncMutableTree() is deprecated, use Tree.isReadOnlyMap() instead, which returns the inverse."
|
|
6
|
-
);
|
|
7
|
-
return !isReadOnlyMap(treelike);
|
|
8
|
-
}
|
package/src/operations/length.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import * as args from "../utilities/args.js";
|
|
2
|
-
import keys from "./keys.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Return the number of keys in the tree.
|
|
6
|
-
*
|
|
7
|
-
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
8
|
-
*
|
|
9
|
-
* @param {Maplike} maplike
|
|
10
|
-
*/
|
|
11
|
-
export default async function length(maplike) {
|
|
12
|
-
console.warn("Tree.length() is deprecated. Use Tree.size() instead.");
|
|
13
|
-
const tree = await args.map(maplike, "Tree.length");
|
|
14
|
-
const treeKeys = await keys(tree);
|
|
15
|
-
return treeKeys.length;
|
|
16
|
-
}
|