@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.
Files changed (178) hide show
  1. package/browser.js +1 -1
  2. package/index.ts +31 -35
  3. package/main.js +1 -2
  4. package/package.json +4 -7
  5. package/shared.js +77 -12
  6. package/src/Tree.js +8 -2
  7. package/src/drivers/AsyncMap.js +210 -0
  8. package/src/drivers/{BrowserFileTree.js → BrowserFileMap.js} +36 -27
  9. package/src/drivers/{calendarTree.js → CalendarMap.js} +81 -62
  10. package/src/drivers/ConstantMap.js +30 -0
  11. package/src/drivers/DeepObjectMap.js +27 -0
  12. package/src/drivers/{ExplorableSiteTree.js → ExplorableSiteMap.js} +7 -7
  13. package/src/drivers/FileMap.js +245 -0
  14. package/src/drivers/{FunctionTree.js → FunctionMap.js} +19 -22
  15. package/src/drivers/ObjectMap.js +139 -0
  16. package/src/drivers/SetMap.js +13 -0
  17. package/src/drivers/{SiteTree.js → SiteMap.js} +16 -17
  18. package/src/drivers/SyncMap.js +245 -0
  19. package/src/jsonKeys.d.ts +2 -2
  20. package/src/jsonKeys.js +6 -5
  21. package/src/operations/addNextPrevious.js +35 -36
  22. package/src/operations/assign.js +30 -21
  23. package/src/operations/cache.js +29 -35
  24. package/src/operations/cachedKeyFunctions.js +1 -1
  25. package/src/operations/calendar.js +5 -0
  26. package/src/operations/clear.js +13 -12
  27. package/src/operations/constant.js +5 -0
  28. package/src/operations/deepEntries.js +23 -0
  29. package/src/operations/deepMap.js +9 -9
  30. package/src/operations/deepMerge.js +36 -25
  31. package/src/operations/deepReverse.js +23 -16
  32. package/src/operations/deepTake.js +7 -7
  33. package/src/operations/deepText.js +4 -4
  34. package/src/operations/deepValues.js +3 -6
  35. package/src/operations/deepValuesIterator.js +11 -11
  36. package/src/operations/delete.js +8 -12
  37. package/src/operations/entries.js +17 -10
  38. package/src/operations/filter.js +9 -7
  39. package/src/operations/first.js +12 -10
  40. package/src/operations/forEach.js +10 -13
  41. package/src/operations/from.js +31 -39
  42. package/src/operations/globKeys.js +22 -17
  43. package/src/operations/group.js +2 -2
  44. package/src/operations/groupBy.js +24 -22
  45. package/src/operations/has.js +7 -9
  46. package/src/operations/indent.js +2 -2
  47. package/src/operations/inners.js +19 -15
  48. package/src/operations/invokeFunctions.js +22 -10
  49. package/src/operations/isAsyncMutableTree.js +5 -12
  50. package/src/operations/isAsyncTree.js +5 -20
  51. package/src/operations/isMap.js +39 -0
  52. package/src/operations/isMaplike.js +34 -0
  53. package/src/operations/isReadOnlyMap.js +14 -0
  54. package/src/operations/isTraversable.js +3 -3
  55. package/src/operations/isTreelike.js +5 -30
  56. package/src/operations/json.js +4 -12
  57. package/src/operations/keys.js +17 -8
  58. package/src/operations/length.js +9 -8
  59. package/src/operations/map.js +27 -30
  60. package/src/operations/mapExtension.js +20 -16
  61. package/src/operations/mapReduce.js +22 -17
  62. package/src/operations/mask.js +31 -22
  63. package/src/operations/match.js +13 -9
  64. package/src/operations/merge.js +43 -35
  65. package/src/operations/paginate.js +26 -18
  66. package/src/operations/parent.js +7 -7
  67. package/src/operations/paths.js +8 -8
  68. package/src/operations/plain.js +6 -6
  69. package/src/operations/regExpKeys.js +21 -12
  70. package/src/operations/reverse.js +21 -15
  71. package/src/operations/root.js +6 -5
  72. package/src/operations/scope.js +31 -26
  73. package/src/operations/shuffle.js +23 -16
  74. package/src/operations/size.js +13 -0
  75. package/src/operations/sort.js +55 -40
  76. package/src/operations/sync.js +21 -0
  77. package/src/operations/take.js +23 -11
  78. package/src/operations/text.js +4 -4
  79. package/src/operations/toFunction.js +7 -7
  80. package/src/operations/traverse.js +4 -4
  81. package/src/operations/traverseOrThrow.js +13 -9
  82. package/src/operations/traversePath.js +2 -2
  83. package/src/operations/values.js +18 -9
  84. package/src/operations/withKeys.js +22 -16
  85. package/src/symbols.js +1 -0
  86. package/src/utilities/castArraylike.js +10 -2
  87. package/src/utilities/getMapArgument.js +38 -0
  88. package/src/utilities/getParent.js +2 -2
  89. package/src/utilities/isStringlike.js +7 -5
  90. package/src/utilities/setParent.js +7 -7
  91. package/src/utilities/toFunction.js +2 -2
  92. package/src/utilities/toPlainValue.js +22 -18
  93. package/test/SampleAsyncMap.js +34 -0
  94. package/test/browser/assert.js +20 -0
  95. package/test/browser/index.html +54 -21
  96. package/test/drivers/AsyncMap.test.js +119 -0
  97. package/test/drivers/{BrowserFileTree.test.js → BrowserFileMap.test.js} +42 -23
  98. package/test/drivers/{calendarTree.test.js → CalendarMap.test.js} +17 -19
  99. package/test/drivers/ConstantMap.test.js +15 -0
  100. package/test/drivers/DeepObjectMap.test.js +36 -0
  101. package/test/drivers/{ExplorableSiteTree.test.js → ExplorableSiteMap.test.js} +29 -14
  102. package/test/drivers/FileMap.test.js +185 -0
  103. package/test/drivers/FunctionMap.test.js +56 -0
  104. package/test/drivers/ObjectMap.test.js +166 -0
  105. package/test/drivers/SetMap.test.js +35 -0
  106. package/test/drivers/{SiteTree.test.js → SiteMap.test.js} +14 -10
  107. package/test/drivers/SyncMap.test.js +321 -0
  108. package/test/jsonKeys.test.js +2 -2
  109. package/test/operations/addNextPrevious.test.js +3 -2
  110. package/test/operations/assign.test.js +30 -35
  111. package/test/operations/cache.test.js +8 -6
  112. package/test/operations/cachedKeyFunctions.test.js +6 -5
  113. package/test/operations/clear.test.js +6 -27
  114. package/test/operations/deepEntries.test.js +32 -0
  115. package/test/operations/deepMerge.test.js +6 -5
  116. package/test/operations/deepReverse.test.js +2 -2
  117. package/test/operations/deepTake.test.js +2 -2
  118. package/test/operations/deepText.test.js +4 -4
  119. package/test/operations/deepValuesIterator.test.js +2 -2
  120. package/test/operations/delete.test.js +2 -2
  121. package/test/operations/extensionKeyFunctions.test.js +6 -5
  122. package/test/operations/filter.test.js +3 -3
  123. package/test/operations/from.test.js +23 -31
  124. package/test/operations/globKeys.test.js +9 -9
  125. package/test/operations/groupBy.test.js +6 -5
  126. package/test/operations/inners.test.js +4 -4
  127. package/test/operations/invokeFunctions.test.js +2 -2
  128. package/test/operations/isMap.test.js +15 -0
  129. package/test/operations/isMaplike.test.js +15 -0
  130. package/test/operations/json.test.js +2 -2
  131. package/test/operations/keys.test.js +16 -3
  132. package/test/operations/map.test.js +20 -18
  133. package/test/operations/mapExtension.test.js +6 -6
  134. package/test/operations/mapReduce.test.js +2 -2
  135. package/test/operations/mask.test.js +4 -3
  136. package/test/operations/match.test.js +2 -2
  137. package/test/operations/merge.test.js +15 -11
  138. package/test/operations/paginate.test.js +5 -5
  139. package/test/operations/parent.test.js +3 -3
  140. package/test/operations/paths.test.js +6 -6
  141. package/test/operations/plain.test.js +8 -8
  142. package/test/operations/regExpKeys.test.js +12 -11
  143. package/test/operations/reverse.test.js +4 -3
  144. package/test/operations/scope.test.js +6 -5
  145. package/test/operations/shuffle.test.js +3 -2
  146. package/test/operations/sort.test.js +7 -10
  147. package/test/operations/sync.test.js +43 -0
  148. package/test/operations/take.test.js +2 -2
  149. package/test/operations/toFunction.test.js +2 -2
  150. package/test/operations/traverse.test.js +4 -5
  151. package/test/operations/withKeys.test.js +2 -2
  152. package/test/utilities/setParent.test.js +6 -6
  153. package/test/utilities/toFunction.test.js +2 -2
  154. package/test/utilities/toPlainValue.test.js +51 -12
  155. package/src/drivers/DeepMapTree.js +0 -23
  156. package/src/drivers/DeepObjectTree.js +0 -18
  157. package/src/drivers/DeferredTree.js +0 -81
  158. package/src/drivers/FileTree.js +0 -276
  159. package/src/drivers/MapTree.js +0 -70
  160. package/src/drivers/ObjectTree.js +0 -158
  161. package/src/drivers/SetTree.js +0 -34
  162. package/src/drivers/constantTree.js +0 -19
  163. package/src/drivers/limitConcurrency.js +0 -63
  164. package/src/internal.js +0 -16
  165. package/src/utilities/getTreeArgument.js +0 -43
  166. package/test/drivers/DeepMapTree.test.js +0 -17
  167. package/test/drivers/DeepObjectTree.test.js +0 -35
  168. package/test/drivers/DeferredTree.test.js +0 -22
  169. package/test/drivers/FileTree.test.js +0 -192
  170. package/test/drivers/FunctionTree.test.js +0 -46
  171. package/test/drivers/MapTree.test.js +0 -59
  172. package/test/drivers/ObjectTree.test.js +0 -163
  173. package/test/drivers/SetTree.test.js +0 -44
  174. package/test/drivers/constantTree.test.js +0 -13
  175. package/test/drivers/limitConcurrency.test.js +0 -41
  176. package/test/operations/isAsyncMutableTree.test.js +0 -17
  177. package/test/operations/isAsyncTree.test.js +0 -26
  178. package/test/operations/isTreelike.test.js +0 -13
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ import SyncMap from "./SyncMap.js";
2
+
3
+ /**
4
+ * A map of Set objects.
5
+ */
6
+ export default class SetMap extends SyncMap {
7
+ /**
8
+ * @param {Set} set
9
+ */
10
+ constructor(set) {
11
+ super(set.entries());
12
+ }
13
+ }
@@ -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 SiteTree {
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<any>} */
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 `SiteTree` that can return the keys for a site route,
69
- * see [ExplorableSiteTree](ExplorableSiteTree.html).
62
+ * For a variation of `SiteMap` that can return the keys for a site route,
63
+ * see [ExplorableSiteMap](ExplorableSiteMap.html).
70
64
  *
71
- * @returns {Promise<Iterable<string>>}
65
+ * @returns {AsyncIterableIterator<string>}
72
66
  */
73
- async keys() {
74
- return [];
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 (SiteTree.mediaTypeIsText(mediaType)) {
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
@@ -1,4 +1,4 @@
1
- import { Treelike } from "../index.ts";
1
+ import { Maplike } from "../index.ts";
2
2
 
3
3
  export function parse(json: string): any;
4
- export function stringify(treelike: Treelike): Promise<string>;
4
+ export function stringify(maplike: Maplike): Promise<string>;
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(treelike) {
15
- const tree = from(treelike);
16
- let keys = Array.from(await tree.keys());
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
- keys = keys.filter((key) => key !== ".keys.json");
19
- const json = JSON.stringify(keys);
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 getTreeArgument from "../utilities/getTreeArgument.js";
2
- import entries from "./entries.js";
3
- import isTreelike from "./isTreelike.js";
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
- * Add nextKey/previousKey properties to values.
6
+ * Return a map that adds nextKey/previousKey properties to values.
8
7
  *
9
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
10
- * @typedef {import("../../index.ts").PlainObject} PlainObject
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(treelike) {
16
- const tree = await getTreeArgument(treelike, "addNextPrevious");
11
+ export default async function addNextPrevious(maplike) {
12
+ const source = await getMapArgument(maplike, "addNextPrevious");
13
+ let sourceKeys;
17
14
 
18
- const treeEntries = [...(await entries(tree))];
19
- const keys = treeEntries.map(([key]) => key);
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
- // Map to an array of [key, result] pairs, where the result includes
22
- // nextKey/previousKey properties.
23
- const mappedEntries = await Promise.all(
24
- treeEntries.map(async ([key, value], index) => {
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 = { value };
28
+ resultValue.value = sourceValue;
36
29
  }
37
30
 
38
- if (resultValue) {
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 = keys[index + 1];
36
+ const nextKey = sourceKeys[index + 1];
41
37
  if (nextKey) {
42
38
  resultValue.nextKey = nextKey;
43
39
  }
44
- const previousKey = keys[index - 1];
40
+ const previousKey = sourceKeys[index - 1];
45
41
  if (previousKey) {
46
42
  resultValue.previousKey = previousKey;
47
43
  }
48
44
  }
49
45
 
50
- return [key, resultValue];
51
- })
52
- );
46
+ return resultValue;
47
+ },
48
+
49
+ async *keys() {
50
+ sourceKeys ??= await keys(source);
51
+ yield* sourceKeys;
52
+ },
53
53
 
54
- return treelike instanceof Array
55
- ? mappedEntries.map(([_, value]) => value)
56
- : Object.fromEntries(mappedEntries);
54
+ trailingSlashKeys: /** @type {any} */ (source).trailingSlashKeys,
55
+ });
57
56
  }
@@ -1,6 +1,5 @@
1
- import from from "./from.js";
2
- import isAsyncMutableTree from "./isAsyncMutableTree.js";
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").Treelike} Treelike
11
+ * @typedef {import("../../index.ts").Maplike} Maplike
13
12
  *
14
- * @param {Treelike} target
15
- * @param {Treelike} source
13
+ * @param {Maplike} target
14
+ * @param {Maplike} source
16
15
  */
17
16
  export default async function assign(target, source) {
18
- const targetTree = from(target);
19
- const sourceTree = from(source);
20
- if (!isAsyncMutableTree(targetTree)) {
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 keys = Array.from(await sourceTree.keys());
25
- const promises = keys.map(async (key) => {
26
- const sourceValue = await sourceTree.get(key);
27
- if (isAsyncTree(sourceValue)) {
28
- const targetValue = await targetTree.get(key);
29
- if (isAsyncMutableTree(targetValue)) {
30
- // Both source and target are trees; recurse.
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
- return;
40
+ } else {
41
+ // Copy the value from the source to the target.
42
+ await targetTree.set(key, sourceValue);
33
43
  }
34
- }
35
- // Copy the value from the source to the target.
36
- await /** @type {any} */ (targetTree).set(key, sourceValue);
37
- });
44
+ })();
45
+ promises.push(promise);
46
+ }
38
47
  await Promise.all(promises);
39
48
  return targetTree;
40
49
  }