@weborigami/async-tree 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as symbols from "../symbols.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import setParent from "../utilities/setParent.js";
|
|
4
|
+
import SyncMap from "./SyncMap.js";
|
|
5
|
+
|
|
6
|
+
export default class ObjectMap extends SyncMap {
|
|
7
|
+
constructor(object = {}) {
|
|
8
|
+
super();
|
|
9
|
+
// Note: we use `typeof` here instead of `instanceof Object` to allow for
|
|
10
|
+
// objects such as Node's `Module` class for representing an ES module.
|
|
11
|
+
if (typeof object !== "object" || object === null) {
|
|
12
|
+
throw new TypeError(
|
|
13
|
+
`${this.constructor.name}: Expected an object or array.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
this.object = object;
|
|
17
|
+
this.parent = object[symbols.parent] ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
delete(key) {
|
|
21
|
+
const existingKey = findExistingKey(this.object, key);
|
|
22
|
+
if (existingKey === null) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
delete this.object[existingKey];
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(key) {
|
|
30
|
+
// Does the object have the key with or without a trailing slash?
|
|
31
|
+
const existingKey = findExistingKey(this.object, key);
|
|
32
|
+
if (existingKey === null) {
|
|
33
|
+
// Key doesn't exist
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let value = this.object[existingKey];
|
|
38
|
+
|
|
39
|
+
if (value === undefined) {
|
|
40
|
+
// Key exists but value is undefined
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setParent(value, this);
|
|
45
|
+
|
|
46
|
+
// Is value an instance method?
|
|
47
|
+
const isInstanceMethod =
|
|
48
|
+
value instanceof Function && !Object.hasOwn(this.object, key);
|
|
49
|
+
if (isInstanceMethod) {
|
|
50
|
+
// Bind it to the object
|
|
51
|
+
value = value.bind(this.object);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @returns {boolean} */
|
|
58
|
+
isSubtree(value) {
|
|
59
|
+
return value instanceof Map;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
keys() {
|
|
63
|
+
// Defer to symbols.keys if defined
|
|
64
|
+
if (typeof this.object[symbols.keys] === "function") {
|
|
65
|
+
return this.object[symbols.keys]();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = new Set();
|
|
69
|
+
|
|
70
|
+
// Walk up the prototype chain
|
|
71
|
+
for (
|
|
72
|
+
let current = this.object;
|
|
73
|
+
current !== null;
|
|
74
|
+
current = Object.getPrototypeOf(current)
|
|
75
|
+
) {
|
|
76
|
+
// Look at all the properties at this level of the prototype chain
|
|
77
|
+
const descriptors = Object.getOwnPropertyDescriptors(current);
|
|
78
|
+
for (const [name, descriptor] of Object.entries(descriptors)) {
|
|
79
|
+
if (name === "constructor" || name === "__proto__") {
|
|
80
|
+
continue; // Uninteresting property
|
|
81
|
+
}
|
|
82
|
+
// Skip non-enumerable properties unless they have get/set
|
|
83
|
+
if (
|
|
84
|
+
!descriptor.enumerable &&
|
|
85
|
+
descriptor.get === undefined &&
|
|
86
|
+
descriptor.set === undefined
|
|
87
|
+
) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Preserve existing slash; add slash for subtrees
|
|
91
|
+
const key = trailingSlash.has(name)
|
|
92
|
+
? name
|
|
93
|
+
: trailingSlash.toggle(
|
|
94
|
+
name,
|
|
95
|
+
descriptor.value !== undefined && this.isSubtree(descriptor.value)
|
|
96
|
+
);
|
|
97
|
+
result.add(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result[Symbol.iterator]();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set(key, value) {
|
|
105
|
+
const existingKey = findExistingKey(this.object, key);
|
|
106
|
+
|
|
107
|
+
// If the key exists under a different form, delete the existing key.
|
|
108
|
+
if (existingKey !== null && existingKey !== key) {
|
|
109
|
+
delete this.object[existingKey];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (value === /** @type {any} */ (this.constructor).EMPTY) {
|
|
113
|
+
// Create empty subtree
|
|
114
|
+
value = Reflect.construct(this.constructor, []);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Set the value for the key.
|
|
118
|
+
this.object[key] = value;
|
|
119
|
+
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get trailingSlashKeys() {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findExistingKey(object, key) {
|
|
129
|
+
// First try key as is
|
|
130
|
+
if (key in object) {
|
|
131
|
+
return key;
|
|
132
|
+
}
|
|
133
|
+
// Try alternate form
|
|
134
|
+
const alternateKey = trailingSlash.toggle(key);
|
|
135
|
+
if (alternateKey in object) {
|
|
136
|
+
return alternateKey;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import * as trailingSlash from "../trailingSlash.js";
|
|
2
2
|
import setParent from "../utilities/setParent.js";
|
|
3
|
+
import AsyncMap from "./AsyncMap.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A tree of values obtained via HTTP/HTTPS calls. These values will be strings
|
|
6
7
|
* for HTTP responses with a MIME text type; otherwise they will be ArrayBuffer
|
|
7
8
|
* instances.
|
|
8
|
-
*
|
|
9
|
-
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
10
|
-
* @implements {AsyncTree}
|
|
11
9
|
*/
|
|
12
|
-
export default class
|
|
10
|
+
export default class SiteMap extends AsyncMap {
|
|
13
11
|
/**
|
|
14
12
|
* @param {string} href
|
|
15
13
|
*/
|
|
16
14
|
constructor(href = globalThis?.location.href) {
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
17
|
if (href?.startsWith(".") && globalThis?.location !== undefined) {
|
|
18
18
|
// URL represents a relative path; concatenate with current location.
|
|
19
19
|
href = new URL(href, globalThis.location.href).href;
|
|
@@ -23,10 +23,9 @@ export default class SiteTree {
|
|
|
23
23
|
href = trailingSlash.add(href);
|
|
24
24
|
|
|
25
25
|
this.href = href;
|
|
26
|
-
this.parent = null;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
/** @returns {Promise<
|
|
28
|
+
/** @returns {Promise<ArrayBuffer|string|undefined>} */
|
|
30
29
|
async get(key) {
|
|
31
30
|
if (key == null) {
|
|
32
31
|
// Reject nullish key.
|
|
@@ -44,11 +43,6 @@ export default class SiteTree {
|
|
|
44
43
|
return value;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
// HACK: For now we don't allow lookup of Origami extension handlers.
|
|
48
|
-
if (key.endsWith(".handler")) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
46
|
const href = new URL(key, this.href).href;
|
|
53
47
|
|
|
54
48
|
// Fetch the data at the given route.
|
|
@@ -65,13 +59,13 @@ export default class SiteTree {
|
|
|
65
59
|
/**
|
|
66
60
|
* Returns an empty set of keys.
|
|
67
61
|
*
|
|
68
|
-
* For a variation of `
|
|
69
|
-
* see [
|
|
62
|
+
* For a variation of `SiteMap` that can return the keys for a site route,
|
|
63
|
+
* see [ExplorableSiteMap](ExplorableSiteMap.html).
|
|
70
64
|
*
|
|
71
|
-
* @returns {
|
|
65
|
+
* @returns {AsyncIterableIterator<string>}
|
|
72
66
|
*/
|
|
73
|
-
async keys() {
|
|
74
|
-
|
|
67
|
+
async *keys() {
|
|
68
|
+
yield* [];
|
|
75
69
|
}
|
|
76
70
|
|
|
77
71
|
// Return true if the given media type is a standard text type.
|
|
@@ -102,13 +96,14 @@ export default class SiteTree {
|
|
|
102
96
|
return this.href;
|
|
103
97
|
}
|
|
104
98
|
|
|
99
|
+
/** @param {Response} response */
|
|
105
100
|
processResponse(response) {
|
|
106
101
|
if (!response.ok) {
|
|
107
102
|
return undefined;
|
|
108
103
|
}
|
|
109
104
|
|
|
110
105
|
const mediaType = response.headers?.get("Content-Type");
|
|
111
|
-
if (
|
|
106
|
+
if (SiteMap.mediaTypeIsText(mediaType)) {
|
|
112
107
|
return response.text();
|
|
113
108
|
} else {
|
|
114
109
|
const buffer = response.arrayBuffer();
|
|
@@ -117,6 +112,10 @@ export default class SiteTree {
|
|
|
117
112
|
}
|
|
118
113
|
}
|
|
119
114
|
|
|
115
|
+
get trailingSlashKeys() {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
120
119
|
get url() {
|
|
121
120
|
return new URL(this.href);
|
|
122
121
|
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
|
+
import setParent from "../utilities/setParent.js";
|
|
3
|
+
|
|
4
|
+
const previewSymbol = Symbol("preview");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A base class for creating custom Map subclasses for use in trees.
|
|
8
|
+
*
|
|
9
|
+
* Instances of SyncMap (and its subclasses) pass `instanceof Map`, and all Map
|
|
10
|
+
* methods have compatible signatures.
|
|
11
|
+
*
|
|
12
|
+
* Subclasses may be read-only or read-write. A read-only subclass overrides
|
|
13
|
+
* get() but not set() or delete(). A read-write subclass overrides all three
|
|
14
|
+
* methods.
|
|
15
|
+
*
|
|
16
|
+
* For use in trees, SyncMap instances may indicate a `parent` node. They can
|
|
17
|
+
* also indicate children subtrees using the trailing slash convention: a key
|
|
18
|
+
* for a subtree may optionally end with a slash. The get() and has() methods
|
|
19
|
+
* support optional trailing slashes on keys.
|
|
20
|
+
*/
|
|
21
|
+
export default class SyncMap extends Map {
|
|
22
|
+
_initialized = false;
|
|
23
|
+
|
|
24
|
+
constructor(iterable) {
|
|
25
|
+
super(iterable);
|
|
26
|
+
|
|
27
|
+
/** @type {SyncMap|null} */
|
|
28
|
+
this._parent = null;
|
|
29
|
+
|
|
30
|
+
// Record self-reference for use in Map method calls that insist on the
|
|
31
|
+
// receiver being a Map instance. This allows method calls to work even when
|
|
32
|
+
// the prototype chain is extended via Object.create().
|
|
33
|
+
//
|
|
34
|
+
// We separately use this member to determine whether the constructor has
|
|
35
|
+
// been called to initialize the instance. See set().
|
|
36
|
+
this._self = this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Removes all key/value entries from the map.
|
|
41
|
+
*
|
|
42
|
+
* Unlike the standard `Map.prototype.clear()`, this method invokes an
|
|
43
|
+
* overridden `keys()` and `delete()` to ensure proper behavior in subclasses.
|
|
44
|
+
*
|
|
45
|
+
* If the `readOnly` property is true, calling this method throws a
|
|
46
|
+
* `TypeError`.
|
|
47
|
+
*/
|
|
48
|
+
clear() {
|
|
49
|
+
if (this.readOnly) {
|
|
50
|
+
throw new TypeError("clear() can't be called on a read-only map");
|
|
51
|
+
}
|
|
52
|
+
for (const key of this.keys()) {
|
|
53
|
+
this.delete(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes the entry for the given key, return true if an entry was removed
|
|
59
|
+
* and false if there was no entry for the key.
|
|
60
|
+
*
|
|
61
|
+
* If the `readOnly` property is true, calling this method throws a
|
|
62
|
+
* `TypeError`.
|
|
63
|
+
*/
|
|
64
|
+
delete(key) {
|
|
65
|
+
if (this.readOnly) {
|
|
66
|
+
throw new TypeError("delete() can't be called on a read-only map");
|
|
67
|
+
}
|
|
68
|
+
return super.delete.call(this._self, key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static EMPTY = Symbol("EMPTY");
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns a new `Iterator` object that contains a two-member array of [key,
|
|
75
|
+
* value] for each element in the map in insertion order.
|
|
76
|
+
*
|
|
77
|
+
* Unlike the standard `Map.prototype.clear()`, this method invokes an
|
|
78
|
+
* overridden `keys()` and `get()` to ensure proper behavior in subclasses.
|
|
79
|
+
*/
|
|
80
|
+
entries() {
|
|
81
|
+
// We'd like to just define entries() as a generator but TypeScript
|
|
82
|
+
// complains that it doesn't match the Map interface. We define the
|
|
83
|
+
// generator internally and then cast it to the expected type.
|
|
84
|
+
const self = this;
|
|
85
|
+
function* gen() {
|
|
86
|
+
for (const key of self.keys()) {
|
|
87
|
+
yield [key, self.get(key)];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return /** @type {MapIterator<[any, any]>} */ (gen());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Calls `callback` once for each key/value pair in the map, in insertion order.
|
|
95
|
+
*
|
|
96
|
+
* Unlike the standard `Map.prototype.forEach()`, this method invokes an
|
|
97
|
+
* overridden `entries()` to ensure proper behavior in subclasses.
|
|
98
|
+
*
|
|
99
|
+
* @param {(value: any, key: any, thisArg: any) => void} callback
|
|
100
|
+
* @param {any?} thisArg
|
|
101
|
+
*/
|
|
102
|
+
forEach(callback, thisArg = this) {
|
|
103
|
+
for (const [key, value] of this.entries()) {
|
|
104
|
+
callback(value, key, thisArg);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns the value associated with the key, or undefined if there is none.
|
|
110
|
+
*/
|
|
111
|
+
get(key) {
|
|
112
|
+
let value = super.get.call(this._self, key);
|
|
113
|
+
if (value === undefined) {
|
|
114
|
+
// Try alternate key with trailing slash added or removed
|
|
115
|
+
value = super.get.call(this._self, trailingSlash.toggle(key));
|
|
116
|
+
}
|
|
117
|
+
if (value === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
setParent(value, this);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns true if the given key appears in the set returned by keys().
|
|
126
|
+
*
|
|
127
|
+
* It doesn't matter whether the value returned by get() is defined or not.
|
|
128
|
+
*
|
|
129
|
+
* If the requested key has a trailing slash but has no associated value, but
|
|
130
|
+
* the alternate form with a slash does appear, this returns true.
|
|
131
|
+
*
|
|
132
|
+
* @param {any} key
|
|
133
|
+
*/
|
|
134
|
+
has(key) {
|
|
135
|
+
const keys = Array.from(this.keys());
|
|
136
|
+
return (
|
|
137
|
+
keys.includes(key) ||
|
|
138
|
+
(!trailingSlash.has(key) && keys.includes(trailingSlash.add(key)))
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Returns a new `Iterator` object that contains the keys for each element in
|
|
144
|
+
* the map in insertion order.
|
|
145
|
+
*
|
|
146
|
+
* @returns {MapIterator<any>}
|
|
147
|
+
*/
|
|
148
|
+
keys() {
|
|
149
|
+
return super.keys.call(this._self);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The parent of this node in a tree.
|
|
154
|
+
*/
|
|
155
|
+
get parent() {
|
|
156
|
+
return this._parent;
|
|
157
|
+
}
|
|
158
|
+
set parent(parent) {
|
|
159
|
+
this._parent = parent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* True if the object is read-only. This will be true if the `get()` method has
|
|
164
|
+
* been overridden but `set()` and `delete()` have not.
|
|
165
|
+
*/
|
|
166
|
+
get readOnly() {
|
|
167
|
+
return (
|
|
168
|
+
this.get !== SyncMap.prototype.get &&
|
|
169
|
+
(this.set === SyncMap.prototype.set ||
|
|
170
|
+
this.delete === SyncMap.prototype.delete)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Adds a new entry with a specified key and value to this Map, or updates an
|
|
176
|
+
* existing entry if the key already exists.
|
|
177
|
+
*
|
|
178
|
+
* If the `readOnly` property is true, calling this method throws a `TypeError`.
|
|
179
|
+
*/
|
|
180
|
+
set(key, value) {
|
|
181
|
+
// The Map constructor takes an optional `iterable` argument. If specified,
|
|
182
|
+
// then set() will be called during construction. We want to allow this to
|
|
183
|
+
// work even for read-only subclasses, so we allow set() to be called during
|
|
184
|
+
// initialization. Once the `_self` member is set, we know initialization is
|
|
185
|
+
// complete; after that point, calling set() on a read-only subclass will
|
|
186
|
+
// throw.
|
|
187
|
+
if (this._self !== undefined && this.readOnly) {
|
|
188
|
+
throw new TypeError("set() can't be called on a read-only map");
|
|
189
|
+
}
|
|
190
|
+
// If _self is not set, use the current instance as the receiver. This is
|
|
191
|
+
// necessary to let the constructor call `super()`.
|
|
192
|
+
const target = this._self ?? this;
|
|
193
|
+
|
|
194
|
+
// Support EMPTY symbol to create empty subtrees
|
|
195
|
+
if (value === /** @type {any} */ (this.constructor).EMPTY) {
|
|
196
|
+
value = Reflect.construct(this.constructor, []);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return super.set.call(target, key, value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns the number of keys in the map.
|
|
204
|
+
*
|
|
205
|
+
* The `size` property invokes an overridden `keys()` to ensure proper
|
|
206
|
+
* behavior in subclasses. Because a subclass may not enforce a direct
|
|
207
|
+
* correspondence between `keys()` and `get()`, the size may not reflect the
|
|
208
|
+
* number of values that can be retrieved.
|
|
209
|
+
*/
|
|
210
|
+
get size() {
|
|
211
|
+
const keys = Array.from(this.keys());
|
|
212
|
+
return keys.length;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Returns the map's `entries()`.
|
|
217
|
+
*/
|
|
218
|
+
[Symbol.iterator]() {
|
|
219
|
+
return this.entries();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Returns a new `Iterator` object that contains the values for each element
|
|
224
|
+
* in the map in insertion order.
|
|
225
|
+
*/
|
|
226
|
+
values() {
|
|
227
|
+
// See notes at entries()
|
|
228
|
+
const self = this;
|
|
229
|
+
function* gen() {
|
|
230
|
+
for (const key of self.keys()) {
|
|
231
|
+
yield self.get(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return /** @type {MapIterator<[any]>} */ (gen());
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// For debugging we make entries() available as a gettable property.
|
|
239
|
+
Object.defineProperty(SyncMap.prototype, previewSymbol, {
|
|
240
|
+
configurable: true,
|
|
241
|
+
enumerable: false,
|
|
242
|
+
get: function () {
|
|
243
|
+
return Array.from(this.entries());
|
|
244
|
+
},
|
|
245
|
+
});
|
package/src/jsonKeys.d.ts
CHANGED
package/src/jsonKeys.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import from from "./operations/from.js";
|
|
2
|
+
import keys from "./operations/keys.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Given a tree node, return a JSON string that can be written to a .keys.json
|
|
@@ -11,11 +12,11 @@ import from from "./operations/from.js";
|
|
|
11
12
|
* "index.html" for a specific resource available at the node, or a string with
|
|
12
13
|
* a trailing slash like "about/" for a subtree of that node.
|
|
13
14
|
*/
|
|
14
|
-
export async function stringify(
|
|
15
|
-
const tree = from(
|
|
16
|
-
let
|
|
15
|
+
export async function stringify(maplike) {
|
|
16
|
+
const tree = from(maplike);
|
|
17
|
+
let treeKeys = await keys(tree);
|
|
17
18
|
// Skip the key `.keys.json` if present.
|
|
18
|
-
|
|
19
|
-
const json = JSON.stringify(
|
|
19
|
+
treeKeys = treeKeys.filter((key) => key !== ".keys.json");
|
|
20
|
+
const json = JSON.stringify(treeKeys);
|
|
20
21
|
return json;
|
|
21
22
|
}
|
|
@@ -1,57 +1,56 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import plain from "./plain.js";
|
|
1
|
+
import AsyncMap from "../drivers/AsyncMap.js";
|
|
2
|
+
import getMapArgument from "../utilities/getMapArgument.js";
|
|
3
|
+
import keys from "./keys.js";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
6
|
+
* Return a map that adds nextKey/previousKey properties to values.
|
|
8
7
|
*
|
|
9
|
-
* @
|
|
10
|
-
* @
|
|
11
|
-
*
|
|
12
|
-
* @param {import("../../index.ts").Treelike} treelike
|
|
13
|
-
* @returns {Promise<PlainObject|Array>}
|
|
8
|
+
* @param {import("../../index.ts").Maplike} maplike
|
|
9
|
+
* @returns {Promise<AsyncMap>}
|
|
14
10
|
*/
|
|
15
|
-
export default async function addNextPrevious(
|
|
16
|
-
const
|
|
11
|
+
export default async function addNextPrevious(maplike) {
|
|
12
|
+
const source = await getMapArgument(maplike, "addNextPrevious");
|
|
13
|
+
let sourceKeys;
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
return Object.assign(new AsyncMap(), {
|
|
16
|
+
async get(key) {
|
|
17
|
+
const sourceValue = await source.get(key);
|
|
18
|
+
if (sourceValue === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
let resultValue;
|
|
26
|
-
if (value === undefined) {
|
|
27
|
-
resultValue = undefined;
|
|
28
|
-
} else if (isTreelike(value)) {
|
|
29
|
-
resultValue = await plain(value);
|
|
30
|
-
} else if (typeof value === "object") {
|
|
31
|
-
// Clone value to avoid modifying the original object
|
|
32
|
-
resultValue = { ...value };
|
|
22
|
+
const resultValue = {};
|
|
23
|
+
if (typeof sourceValue === "object") {
|
|
24
|
+
// Copy to avoid modifying the original object
|
|
25
|
+
Object.assign(resultValue, sourceValue);
|
|
33
26
|
} else {
|
|
34
27
|
// Take the object as the `value` property
|
|
35
|
-
resultValue =
|
|
28
|
+
resultValue.value = sourceValue;
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
|
|
31
|
+
// Find the index of the current key
|
|
32
|
+
sourceKeys ??= await keys(source);
|
|
33
|
+
const index = sourceKeys.indexOf(key);
|
|
34
|
+
if (index >= 0) {
|
|
39
35
|
// Extend result with nextKey/previousKey properties.
|
|
40
|
-
const nextKey =
|
|
36
|
+
const nextKey = sourceKeys[index + 1];
|
|
41
37
|
if (nextKey) {
|
|
42
38
|
resultValue.nextKey = nextKey;
|
|
43
39
|
}
|
|
44
|
-
const previousKey =
|
|
40
|
+
const previousKey = sourceKeys[index - 1];
|
|
45
41
|
if (previousKey) {
|
|
46
42
|
resultValue.previousKey = previousKey;
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
46
|
+
return resultValue;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async *keys() {
|
|
50
|
+
sourceKeys ??= await keys(source);
|
|
51
|
+
yield* sourceKeys;
|
|
52
|
+
},
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: Object.fromEntries(mappedEntries);
|
|
54
|
+
trailingSlashKeys: /** @type {any} */ (source).trailingSlashKeys,
|
|
55
|
+
});
|
|
57
56
|
}
|
package/src/operations/assign.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import isAsyncTree from "./isAsyncTree.js";
|
|
1
|
+
import getMapArgument from "../utilities/getMapArgument.js";
|
|
2
|
+
import isMaplike from "./isMaplike.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Apply the key/values pairs from the source tree to the target tree.
|
|
@@ -9,32 +8,42 @@ import isAsyncTree from "./isAsyncTree.js";
|
|
|
9
8
|
* subtrees, then the subtrees will be merged recursively. Otherwise, the
|
|
10
9
|
* value from the source tree will overwrite the value in the target tree.
|
|
11
10
|
*
|
|
12
|
-
* @typedef {import("../../index.ts").
|
|
11
|
+
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
13
12
|
*
|
|
14
|
-
* @param {
|
|
15
|
-
* @param {
|
|
13
|
+
* @param {Maplike} target
|
|
14
|
+
* @param {Maplike} source
|
|
16
15
|
*/
|
|
17
16
|
export default async function assign(target, source) {
|
|
18
|
-
const targetTree =
|
|
19
|
-
const sourceTree =
|
|
20
|
-
if (
|
|
17
|
+
const targetTree = await getMapArgument(target, "assign", { position: 0 });
|
|
18
|
+
const sourceTree = await getMapArgument(source, "assign", { position: 1 });
|
|
19
|
+
if ("readOnly" in targetTree && targetTree.readOnly) {
|
|
21
20
|
throw new TypeError("Target must be a mutable asynchronous tree");
|
|
22
21
|
}
|
|
23
22
|
// Fire off requests to update all keys, then wait for all of them to finish.
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
23
|
+
const promises = [];
|
|
24
|
+
for await (const key of sourceTree.keys()) {
|
|
25
|
+
const promise = (async () => {
|
|
26
|
+
const sourceValue = await sourceTree.get(key);
|
|
27
|
+
|
|
28
|
+
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
|
+
// Recurse to copy subtree
|
|
31
39
|
await assign(targetValue, sourceValue);
|
|
32
|
-
|
|
40
|
+
} else {
|
|
41
|
+
// Copy the value from the source to the target.
|
|
42
|
+
await targetTree.set(key, sourceValue);
|
|
33
43
|
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
44
|
+
})();
|
|
45
|
+
promises.push(promise);
|
|
46
|
+
}
|
|
38
47
|
await Promise.all(promises);
|
|
39
48
|
return targetTree;
|
|
40
49
|
}
|