@weborigami/async-tree 0.6.0 → 0.6.2

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