@weborigami/async-tree 0.5.4 → 0.5.6

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 (153) hide show
  1. package/index.ts +16 -6
  2. package/package.json +2 -2
  3. package/shared.js +20 -29
  4. package/src/Tree.js +59 -513
  5. package/src/constants.js +2 -0
  6. package/src/drivers/BrowserFileTree.js +9 -10
  7. package/src/drivers/DeepMapTree.js +3 -3
  8. package/src/drivers/DeepObjectTree.js +4 -5
  9. package/src/drivers/DeferredTree.js +2 -2
  10. package/src/drivers/FileTree.js +11 -33
  11. package/src/drivers/FunctionTree.js +3 -3
  12. package/src/drivers/MapTree.js +6 -6
  13. package/src/drivers/ObjectTree.js +6 -8
  14. package/src/drivers/SetTree.js +1 -1
  15. package/src/drivers/SiteTree.js +1 -1
  16. package/src/drivers/constantTree.js +1 -1
  17. package/src/extension.js +5 -3
  18. package/src/jsonKeys.js +5 -7
  19. package/src/operations/addNextPrevious.js +10 -9
  20. package/src/operations/assign.js +40 -0
  21. package/src/operations/cache.js +18 -12
  22. package/src/operations/cachedKeyFunctions.js +15 -4
  23. package/src/operations/clear.js +20 -0
  24. package/src/operations/deepMap.js +25 -0
  25. package/src/operations/deepMerge.js +11 -25
  26. package/src/operations/deepReverse.js +6 -7
  27. package/src/operations/deepTake.js +6 -7
  28. package/src/operations/deepText.js +4 -4
  29. package/src/operations/deepValuesIterator.js +8 -6
  30. package/src/operations/delete.js +20 -0
  31. package/src/operations/entries.js +16 -0
  32. package/src/operations/extensionKeyFunctions.js +1 -1
  33. package/src/operations/filter.js +7 -8
  34. package/src/operations/first.js +18 -0
  35. package/src/operations/forEach.js +20 -0
  36. package/src/operations/from.js +77 -0
  37. package/src/operations/globKeys.js +8 -8
  38. package/src/operations/group.js +3 -46
  39. package/src/operations/groupBy.js +51 -0
  40. package/src/operations/has.js +16 -0
  41. package/src/operations/indent.js +4 -2
  42. package/src/operations/inners.js +29 -0
  43. package/src/operations/invokeFunctions.js +5 -4
  44. package/src/operations/isAsyncMutableTree.js +15 -0
  45. package/src/operations/isAsyncTree.js +21 -0
  46. package/src/operations/isTraversable.js +15 -0
  47. package/src/operations/isTreelike.js +33 -0
  48. package/src/operations/json.js +4 -3
  49. package/src/operations/keys.js +14 -0
  50. package/src/operations/length.js +15 -0
  51. package/src/operations/map.js +156 -95
  52. package/src/operations/mapExtension.js +78 -0
  53. package/src/operations/mapReduce.js +44 -0
  54. package/src/operations/mask.js +18 -16
  55. package/src/operations/match.js +74 -0
  56. package/src/operations/merge.js +22 -20
  57. package/src/operations/paginate.js +3 -5
  58. package/src/operations/parent.js +13 -0
  59. package/src/operations/paths.js +51 -0
  60. package/src/operations/plain.js +34 -0
  61. package/src/operations/regExpKeys.js +4 -5
  62. package/src/operations/reverse.js +4 -6
  63. package/src/operations/root.js +17 -0
  64. package/src/operations/scope.js +4 -6
  65. package/src/operations/shuffle.js +46 -0
  66. package/src/operations/sort.js +19 -12
  67. package/src/operations/take.js +3 -5
  68. package/src/operations/text.js +3 -3
  69. package/src/operations/toFunction.js +14 -0
  70. package/src/operations/traverse.js +24 -0
  71. package/src/operations/traverseOrThrow.js +59 -0
  72. package/src/operations/traversePath.js +16 -0
  73. package/src/operations/values.js +15 -0
  74. package/src/operations/withKeys.js +33 -0
  75. package/src/utilities/TypedArray.js +2 -0
  76. package/src/utilities/box.js +20 -0
  77. package/src/utilities/castArraylike.js +38 -0
  78. package/src/utilities/getParent.js +33 -0
  79. package/src/utilities/getRealmObjectPrototype.js +19 -0
  80. package/src/utilities/getTreeArgument.js +43 -0
  81. package/src/utilities/isPacked.js +20 -0
  82. package/src/utilities/isPlainObject.js +29 -0
  83. package/src/utilities/isPrimitive.js +13 -0
  84. package/src/utilities/isStringlike.js +25 -0
  85. package/src/utilities/isUnpackable.js +13 -0
  86. package/src/utilities/keysFromPath.js +34 -0
  87. package/src/utilities/naturalOrder.js +9 -0
  88. package/src/utilities/pathFromKeys.js +18 -0
  89. package/src/utilities/setParent.js +38 -0
  90. package/src/utilities/toFunction.js +40 -0
  91. package/src/utilities/toPlainValue.js +95 -0
  92. package/src/utilities/toString.js +37 -0
  93. package/test/drivers/ExplorableSiteTree.test.js +1 -1
  94. package/test/drivers/FileTree.test.js +1 -1
  95. package/test/drivers/calendarTree.test.js +1 -1
  96. package/test/jsonKeys.test.js +1 -1
  97. package/test/operations/assign.test.js +54 -0
  98. package/test/operations/cache.test.js +1 -1
  99. package/test/operations/cachedKeyFunctions.test.js +16 -16
  100. package/test/operations/clear.test.js +34 -0
  101. package/test/operations/deepMerge.test.js +2 -6
  102. package/test/operations/deepReverse.test.js +1 -1
  103. package/test/operations/delete.test.js +20 -0
  104. package/test/operations/entries.test.js +18 -0
  105. package/test/operations/extensionKeyFunctions.test.js +10 -10
  106. package/test/operations/first.test.js +15 -0
  107. package/test/operations/fixtures/README.md +1 -0
  108. package/test/operations/forEach.test.js +22 -0
  109. package/test/operations/from.test.js +67 -0
  110. package/test/operations/globKeys.test.js +3 -3
  111. package/test/operations/{group.test.js → groupBy.test.js} +4 -4
  112. package/test/operations/has.test.js +15 -0
  113. package/test/operations/inners.test.js +30 -0
  114. package/test/operations/invokeFunctions.test.js +1 -1
  115. package/test/operations/isAsyncMutableTree.test.js +17 -0
  116. package/test/operations/isAsyncTree.test.js +26 -0
  117. package/test/operations/isTreelike.test.js +13 -0
  118. package/test/operations/keys.test.js +15 -0
  119. package/test/operations/length.test.js +15 -0
  120. package/test/operations/map.test.js +39 -70
  121. package/test/operations/mapExtension.test.js +53 -0
  122. package/test/operations/mapReduce.test.js +23 -0
  123. package/test/operations/mask.test.js +1 -1
  124. package/test/operations/match.test.js +33 -0
  125. package/test/operations/merge.test.js +23 -9
  126. package/test/operations/paginate.test.js +1 -1
  127. package/test/operations/parent.test.js +15 -0
  128. package/test/operations/paths.test.js +40 -0
  129. package/test/operations/plain.test.js +69 -0
  130. package/test/operations/reverse.test.js +1 -1
  131. package/test/operations/scope.test.js +1 -1
  132. package/test/operations/shuffle.test.js +18 -0
  133. package/test/operations/sort.test.js +3 -3
  134. package/test/operations/toFunction.test.js +16 -0
  135. package/test/operations/traverse.test.js +43 -0
  136. package/test/operations/traversePath.test.js +16 -0
  137. package/test/operations/values.test.js +18 -0
  138. package/test/operations/withKeys.test.js +21 -0
  139. package/test/utilities/box.test.js +26 -0
  140. package/test/utilities/getRealmObjectPrototype.test.js +11 -0
  141. package/test/utilities/isPlainObject.test.js +13 -0
  142. package/test/utilities/keysFromPath.test.js +14 -0
  143. package/test/utilities/naturalOrder.test.js +11 -0
  144. package/test/utilities/pathFromKeys.test.js +12 -0
  145. package/test/utilities/setParent.test.js +34 -0
  146. package/test/utilities/toFunction.test.js +34 -0
  147. package/test/utilities/toPlainValue.test.js +27 -0
  148. package/test/utilities/toString.test.js +22 -0
  149. package/src/Tree.d.ts +0 -24
  150. package/src/utilities.d.ts +0 -21
  151. package/src/utilities.js +0 -443
  152. package/test/Tree.test.js +0 -407
  153. package/test/utilities.test.js +0 -141
@@ -0,0 +1,29 @@
1
+ import getRealmObjectPrototype from "./getRealmObjectPrototype.js";
2
+
3
+ /**
4
+ * Return true if the object is a plain JavaScript object created by `{}`,
5
+ * `new Object()`, or `Object.create(null)`.
6
+ *
7
+ * This function also considers object-like things with no prototype (like a
8
+ * `Module`) as plain objects.
9
+ *
10
+ * @typedef {import("../../index.ts").PlainObject} PlainObject
11
+ *
12
+ * @param {any} obj
13
+ * @returns {obj is PlainObject}
14
+ */
15
+ export default function isPlainObject(obj) {
16
+ // From https://stackoverflow.com/q/51722354/76472
17
+ if (typeof obj !== "object" || obj === null) {
18
+ return false;
19
+ }
20
+
21
+ // We treat object-like things with no prototype (like a Module) as plain
22
+ // objects.
23
+ if (Object.getPrototypeOf(obj) === null) {
24
+ return true;
25
+ }
26
+
27
+ // Do we inherit directly from Object in this realm?
28
+ return Object.getPrototypeOf(obj) === getRealmObjectPrototype(obj);
29
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Return true if the value is a primitive JavaScript value.
3
+ *
4
+ * @param {any} value
5
+ */
6
+ export default function isPrimitive(value) {
7
+ // Check for null first, since typeof null === "object".
8
+ if (value === null) {
9
+ return true;
10
+ }
11
+ const type = typeof value;
12
+ return type !== "object" && type !== "function";
13
+ }
@@ -0,0 +1,25 @@
1
+ import getRealmObjectPrototype from "./getRealmObjectPrototype.js";
2
+
3
+ /**
4
+ * Return true if the object is a string or object with a non-trival `toString`
5
+ * method.
6
+ *
7
+ * @typedef {import("../../index.ts").Stringlike} Stringlike
8
+ *
9
+ * @param {any} obj
10
+ * @returns {obj is Stringlike}
11
+ */
12
+ export default function isStringlike(obj) {
13
+ if (typeof obj === "string") {
14
+ return true;
15
+ } else if (obj?.toString === undefined) {
16
+ return false;
17
+ } else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
18
+ // The stupid Object.prototype.toString implementation always returns
19
+ // "[object Object]", so if that's the only toString method the object has,
20
+ // we return false.
21
+ return false;
22
+ } else {
23
+ return true;
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ import isPacked from "./isPacked.js";
2
+
3
+ /**
4
+ * @typedef {import("../../index.ts").Unpackable} Unpackable
5
+ *
6
+ * @param {any} obj
7
+ * @returns {obj is Unpackable}
8
+ */
9
+ export default function isUnpackable(obj) {
10
+ return (
11
+ isPacked(obj) && typeof (/** @type {any} */ (obj).unpack) === "function"
12
+ );
13
+ }
@@ -0,0 +1,34 @@
1
+ import * as trailingSlash from "../trailingSlash.js";
2
+
3
+ /**
4
+ * Given a path like "/foo/bar/baz", return an array of keys like ["foo/",
5
+ * "bar/", "baz"].
6
+ *
7
+ * Leading slashes are ignored. Consecutive slashes will be ignored. Trailing
8
+ * slashes are preserved.
9
+ *
10
+ * @param {string} pathname
11
+ */
12
+ export default function keysFromPath(pathname) {
13
+ // Split the path at each slash
14
+ let keys = pathname.split("/");
15
+ if (keys[0] === "") {
16
+ // The path begins with a slash; drop that part.
17
+ keys.shift();
18
+ }
19
+ if (keys.at(-1) === "") {
20
+ // The path ends with a slash; drop that part.
21
+ keys.pop();
22
+ }
23
+ // Drop any empty keys
24
+ keys = keys.filter((key) => key !== "");
25
+ // Add the trailing slash back to all keys but the last
26
+ for (let i = 0; i < keys.length - 1; i++) {
27
+ keys[i] += "/";
28
+ }
29
+ // Add trailing slash to last key if path ended with a slash
30
+ if (keys.length > 0 && trailingSlash.has(pathname)) {
31
+ keys[keys.length - 1] += "/";
32
+ }
33
+ return keys;
34
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compare two strings using [natural sort
3
+ * order](https://en.wikipedia.org/wiki/Natural_sort_order).
4
+ */
5
+ const naturalOrder = new Intl.Collator(undefined, {
6
+ numeric: true,
7
+ }).compare;
8
+
9
+ export default naturalOrder;
@@ -0,0 +1,18 @@
1
+ import * as trailingSlash from "../trailingSlash.js";
2
+
3
+ /**
4
+ * Return a slash-separated path for the given keys.
5
+ *
6
+ * This takes care to avoid adding consecutive slashes if they keys themselves
7
+ * already have trailing slashes.
8
+ *
9
+ * @param {string[]} keys
10
+ */
11
+ export default function pathFromKeys(keys) {
12
+ // Ensure there's a slash between all keys. If the last key has a trailing
13
+ // slash, leave it there.
14
+ const normalized = keys.map((key, index) =>
15
+ index < keys.length - 1 ? trailingSlash.add(key) : key
16
+ );
17
+ return normalized.join("");
18
+ }
@@ -0,0 +1,38 @@
1
+ import isAsyncTree from "../operations/isAsyncTree.js";
2
+ import * as symbols from "../symbols.js";
3
+
4
+ /**
5
+ * If the child object doesn't have a parent yet, set it to the indicated
6
+ * parent. If the child is an AsyncTree, set the `parent` property. Otherwise,
7
+ * set the `symbols.parent` property.
8
+ *
9
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
10
+ *
11
+ * @param {*} child
12
+ * @param {AsyncTree|null} parent
13
+ */
14
+ export default function setParent(child, parent) {
15
+ if (isAsyncTree(child)) {
16
+ // Value is a subtree; set its parent to this tree.
17
+ if (!child.parent) {
18
+ child.parent = parent;
19
+ }
20
+ } else if (Object.isExtensible(child) && !child[symbols.parent]) {
21
+ try {
22
+ // Add parent reference as a symbol to avoid polluting the object. This
23
+ // reference will be used if the object is later used as a tree. We set
24
+ // `enumerable` to false even thought this makes no practical difference
25
+ // (symbols are never enumerated) because it can provide a hint in the
26
+ // debugger that the property is for internal use.
27
+ Object.defineProperty(child, symbols.parent, {
28
+ configurable: true,
29
+ enumerable: false,
30
+ value: parent,
31
+ writable: true,
32
+ });
33
+ } catch (error) {
34
+ // Ignore exceptions. Some esoteric objects don't allow adding properties.
35
+ // We can still treat them as trees, but they won't have a parent.
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,40 @@
1
+ import from from "../operations/from.js";
2
+ import isTreelike from "../operations/isTreelike.js";
3
+ import isUnpackable from "./isUnpackable.js";
4
+
5
+ /**
6
+ * Convert the given object to a function.
7
+ *
8
+ * @typedef {import("../../index.ts").Invocable} Invocable
9
+ *
10
+ * @param {Invocable} obj
11
+ * @returns {Function|null}
12
+ */
13
+ export default function toFunction(obj) {
14
+ if (typeof obj === "function") {
15
+ // Return a function as is.
16
+ return obj;
17
+ } else if (isUnpackable(obj)) {
18
+ // Extract the contents of the object and convert that to a function.
19
+ let fnPromise;
20
+ return async function (...args) {
21
+ if (!fnPromise) {
22
+ // unpack() may return a function or a promise for a function; normalize
23
+ // to a promise for a function
24
+ const unpackPromise = Promise.resolve(
25
+ /** @type {any} */ (obj).unpack()
26
+ );
27
+ fnPromise = unpackPromise.then((content) => toFunction(content));
28
+ }
29
+ const fn = await fnPromise;
30
+ return fn(...args);
31
+ };
32
+ } else if (isTreelike(obj)) {
33
+ // Return a function that invokes the tree's getter.
34
+ const tree = from(obj);
35
+ return tree.get.bind(tree);
36
+ } else {
37
+ // Not a function
38
+ return null;
39
+ }
40
+ }
@@ -0,0 +1,95 @@
1
+ import ObjectTree from "../drivers/ObjectTree.js";
2
+ import isTreelike from "../operations/isTreelike.js";
3
+ import mapReduce from "../operations/mapReduce.js";
4
+ import * as trailingSlash from "../trailingSlash.js";
5
+ import castArraylike from "./castArraylike.js";
6
+ import isPrimitive from "./isPrimitive.js";
7
+ import isStringlike from "./isStringlike.js";
8
+ import toString from "./toString.js";
9
+ import TypedArray from "./TypedArray.js";
10
+
11
+ /**
12
+ * Convert the given input to the plainest possible JavaScript value. This
13
+ * helper is intended for functions that want to accept an argument from the ori
14
+ * CLI, which could a string, a stream of data, or some other kind of JavaScript
15
+ * object.
16
+ *
17
+ * If the input is a function, it will be invoked and its result will be
18
+ * processed.
19
+ *
20
+ * If the input is a promise, it will be resolved and its result will be
21
+ * processed.
22
+ *
23
+ * If the input is treelike, it will be converted to a plain JavaScript object,
24
+ * recursively traversing the tree and converting all values to plain types.
25
+ *
26
+ * If the input is stringlike, its text will be returned.
27
+ *
28
+ * If the input is a ArrayBuffer or typed array, it will be interpreted as UTF-8
29
+ * text if it does not contain unprintable characters. If it does, it will be
30
+ * returned as a base64-encoded string.
31
+ *
32
+ * If the input has a custom class instance, its public properties will be
33
+ * returned as a plain object.
34
+ *
35
+ * @param {any} input
36
+ * @returns {Promise<any>}
37
+ */
38
+ export default async function toPlainValue(input) {
39
+ if (input instanceof Function) {
40
+ // Invoke function
41
+ input = input();
42
+ }
43
+ if (input instanceof Promise) {
44
+ // Resolve promise
45
+ input = await input;
46
+ }
47
+
48
+ if (isPrimitive(input) || input instanceof Date) {
49
+ return input;
50
+ } else if (isTreelike(input)) {
51
+ // Recursively convert tree to plain object.
52
+ return mapReduce(input, toPlainValue, (values, keys, tree) => {
53
+ // Special case for an empty tree: if based on array, return array.
54
+ if (tree instanceof ObjectTree && keys.length === 0) {
55
+ return /** @type {any} */ (tree).object instanceof Array ? [] : {};
56
+ }
57
+ // Normalize slashes in keys.
58
+ keys = keys.map(trailingSlash.remove);
59
+ return castArraylike(keys, values);
60
+ });
61
+ } else if (input instanceof ArrayBuffer || input instanceof TypedArray) {
62
+ // Try to interpret the buffer as UTF-8 text, otherwise use base64.
63
+ const text = toString(input);
64
+ if (text !== null) {
65
+ return text;
66
+ } else {
67
+ return toBase64(input);
68
+ }
69
+ } else if (isStringlike(input)) {
70
+ return toString(input);
71
+ } else {
72
+ // Some other kind of class instance; return its public properties.
73
+ const plain = {};
74
+ for (const [key, value] of Object.entries(input)) {
75
+ plain[key] = await toPlainValue(value);
76
+ }
77
+ return plain;
78
+ }
79
+ }
80
+
81
+ function toBase64(object) {
82
+ if (typeof Buffer !== "undefined") {
83
+ // Node.js environment
84
+ return Buffer.from(object).toString("base64");
85
+ } else {
86
+ // Browser environment
87
+ let binary = "";
88
+ const bytes = new Uint8Array(object);
89
+ const len = bytes.byteLength;
90
+ for (let i = 0; i < len; i++) {
91
+ binary += String.fromCharCode(bytes[i]);
92
+ }
93
+ return btoa(binary);
94
+ }
95
+ }
@@ -0,0 +1,37 @@
1
+ import isPrimitive from "./isPrimitive.js";
2
+ import isStringlike from "./isStringlike.js";
3
+ import TypedArray from "./TypedArray.js";
4
+
5
+ const textDecoder = new TextDecoder();
6
+
7
+ /**
8
+ * Return a string form of the object, handling cases not generally handled by
9
+ * the standard JavaScript `toString()` method:
10
+ *
11
+ * 1. If the object is an ArrayBuffer or TypedArray, decode the array as UTF-8.
12
+ * 2. If the object is otherwise a plain JavaScript object with the useless
13
+ * default toString() method, return null instead of "[object Object]". In
14
+ * practice, it's generally more useful to have this method fail than to
15
+ * return a useless string.
16
+ * 3. If the object is a defined primitive value, return the result of
17
+ * String(object).
18
+ *
19
+ * Otherwise return null.
20
+ *
21
+ * @param {any} object
22
+ * @returns {string|null}
23
+ */
24
+ export default function toString(object) {
25
+ if (object instanceof ArrayBuffer || object instanceof TypedArray) {
26
+ // Treat the buffer as UTF-8 text.
27
+ const decoded = textDecoder.decode(object);
28
+ // If the result appears to contain non-printable characters, it's probably not a string.
29
+ // https://stackoverflow.com/a/1677660/76472
30
+ const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
31
+ return hasNonPrintableCharacters ? null : decoded;
32
+ } else if (isStringlike(object) || (object !== null && isPrimitive(object))) {
33
+ return String(object);
34
+ } else {
35
+ return null;
36
+ }
37
+ }
@@ -77,7 +77,7 @@ describe("ExplorableSiteTree", () => {
77
77
  test("can convert a site to a plain object", async () => {
78
78
  const fixture = new ExplorableSiteTree(mockHost);
79
79
  // Convert buffers to strings.
80
- const strings = Tree.map(fixture, {
80
+ const strings = await Tree.map(fixture, {
81
81
  deep: true,
82
82
  value: (value) => textDecoder.decode(value),
83
83
  });
@@ -132,7 +132,7 @@ describe("FileTree", async () => {
132
132
 
133
133
  // Read them back in.
134
134
  const actualFiles = await tempFiles.get("folder");
135
- const strings = Tree.map(actualFiles, {
135
+ const strings = await Tree.map(actualFiles, {
136
136
  deep: true,
137
137
  value: (buffer) => textDecoder.decode(buffer),
138
138
  });
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import calendar from "../../src/drivers/calendarTree.js";
4
- import { toPlainValue } from "../../src/utilities.js";
4
+ import toPlainValue from "../../src/utilities/toPlainValue.js";
5
5
 
6
6
  describe("calendarTree", () => {
7
7
  test("without a start or end, returns a tree for today", async () => {
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
- import { DeepObjectTree } from "../src/internal.js";
3
+ import DeepObjectTree from "../src/drivers/DeepObjectTree.js";
4
4
  import * as jsonKeys from "../src/jsonKeys.js";
5
5
 
6
6
  describe("jsonKeys", () => {
@@ -0,0 +1,54 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import DeepObjectTree from "../../src/drivers/DeepObjectTree.js";
4
+ import ObjectTree from "../../src/drivers/ObjectTree.js";
5
+ import assign from "../../src/operations/assign.js";
6
+ import plain from "../../src/operations/plain.js";
7
+
8
+ describe("assign", () => {
9
+ test("assign applies one tree to another", async () => {
10
+ const target = new DeepObjectTree({
11
+ a: 1,
12
+ b: 2,
13
+ more: {
14
+ d: 3,
15
+ },
16
+ });
17
+
18
+ const source = new DeepObjectTree({
19
+ a: 4, // Overwrite existing value
20
+ b: undefined, // Delete
21
+ c: 5, // Add
22
+ more: {
23
+ // Should leave existing `more` keys alone.
24
+ e: 6, // Add
25
+ },
26
+ // Add new subtree
27
+ extra: {
28
+ f: 7,
29
+ },
30
+ });
31
+
32
+ // Apply changes.
33
+ const result = await assign(target, source);
34
+
35
+ assert.equal(result, target);
36
+ assert.deepEqual(await plain(target), {
37
+ a: 4,
38
+ c: 5,
39
+ more: {
40
+ d: 3,
41
+ e: 6,
42
+ },
43
+ extra: {
44
+ f: 7,
45
+ },
46
+ });
47
+ });
48
+
49
+ test("assign() can apply updates to an array", async () => {
50
+ const target = new ObjectTree(["a", "b", "c"]);
51
+ await assign(target, ["d", "e"]);
52
+ assert.deepEqual(await plain(target), ["d", "e", "c"]);
53
+ });
54
+ });
@@ -6,7 +6,7 @@ import cache from "../../src/operations/cache.js";
6
6
  describe("cache", () => {
7
7
  test("caches reads of values from one tree into another", async () => {
8
8
  const objectCache = new ObjectTree({});
9
- const fixture = cache(
9
+ const fixture = await cache(
10
10
  new DeepObjectTree({
11
11
  a: 1,
12
12
  b: 2,
@@ -12,7 +12,7 @@ describe("cachedKeyFunctions", () => {
12
12
  });
13
13
 
14
14
  let callCount = 0;
15
- const addUnderscore = async (sourceKey, tree) => {
15
+ const addUnderscore = async (sourceValue, sourceKey, tree) => {
16
16
  callCount++;
17
17
  return `_${sourceKey}`;
18
18
  };
@@ -26,15 +26,15 @@ describe("cachedKeyFunctions", () => {
26
26
  assert.equal(await inverseKey("_b", tree), "b"); // Cache miss
27
27
  assert.equal(callCount, 2);
28
28
 
29
- assert.equal(await key("a", tree), "_a");
30
- assert.equal(await key("a", tree), "_a");
31
- assert.equal(await key("b", tree), "_b");
29
+ assert.equal(await key(null, "a", tree), "_a");
30
+ assert.equal(await key(null, "a", tree), "_a");
31
+ assert.equal(await key(null, "b", tree), "_b");
32
32
  assert.equal(callCount, 2);
33
33
 
34
34
  // `c` isn't in tree, so we should get undefined.
35
35
  assert.equal(await inverseKey("_c", tree), undefined);
36
36
  // But key mapping is still possible.
37
- assert.equal(await key("c", tree), "_c");
37
+ assert.equal(await key(null, "c", tree), "_c");
38
38
  // And now we have a cache hit.
39
39
  assert.equal(await inverseKey("_c", tree), "c");
40
40
  assert.equal(callCount, 3);
@@ -49,7 +49,7 @@ describe("cachedKeyFunctions", () => {
49
49
  });
50
50
 
51
51
  let callCount = 0;
52
- const addUnderscore = async (sourceKey, tree) => {
52
+ const addUnderscore = async (sourceValue, sourceKey, tree) => {
53
53
  callCount++;
54
54
  return `_${sourceKey}`;
55
55
  };
@@ -66,12 +66,12 @@ describe("cachedKeyFunctions", () => {
66
66
  assert.equal(await inverseKey("b/", tree), "b/");
67
67
  assert.equal(callCount, 1);
68
68
 
69
- assert.equal(await key("a", tree), "_a");
70
- assert.equal(await key("a", tree), "_a");
69
+ assert.equal(await key(null, "a", tree), "_a");
70
+ assert.equal(await key(null, "a", tree), "_a");
71
71
  assert.equal(callCount, 1);
72
72
 
73
- assert.equal(await key("b/", tree), "b/");
74
- assert.equal(await key("b", tree), "b");
73
+ assert.equal(await key(null, "b/", tree), "b/");
74
+ assert.equal(await key(null, "b", tree), "b");
75
75
  assert.equal(callCount, 1);
76
76
  });
77
77
 
@@ -79,11 +79,11 @@ describe("cachedKeyFunctions", () => {
79
79
  const tree = new ObjectTree({
80
80
  a: "letter a",
81
81
  });
82
- const addUnderscore = async (sourceKey) => `_${sourceKey}`;
82
+ const addUnderscore = async (sourceValue, sourceKey) => `_${sourceKey}`;
83
83
  const { inverseKey, key } = cachedKeyFunctions(addUnderscore);
84
84
 
85
- assert.equal(await key("a/", tree), "_a/");
86
- assert.equal(await key("a", tree), "_a");
85
+ assert.equal(await key(null, "a/", tree), "_a/");
86
+ assert.equal(await key(null, "a", tree), "_a");
87
87
 
88
88
  assert.equal(await inverseKey("_a/", tree), "a/");
89
89
  assert.equal(await inverseKey("_a", tree), "a");
@@ -93,14 +93,14 @@ describe("cachedKeyFunctions", () => {
93
93
  const tree = new ObjectTree({
94
94
  a: "letter a",
95
95
  });
96
- const addUnderscoreAndSlash = async (sourceKey) =>
96
+ const addUnderscoreAndSlash = async (sourceValue, sourceKey) =>
97
97
  `_${trailingSlash.remove(sourceKey)}/`;
98
98
  const { inverseKey, key } = cachedKeyFunctions(addUnderscoreAndSlash);
99
99
 
100
100
  assert.equal(await inverseKey("_a/", tree), "a");
101
101
  assert.equal(await inverseKey("_a", tree), "a");
102
102
 
103
- assert.equal(await key("a", tree), "_a/");
104
- assert.equal(await key("a/", tree), "_a/");
103
+ assert.equal(await key(null, "a", tree), "_a/");
104
+ assert.equal(await key(null, "a/", tree), "_a/");
105
105
  });
106
106
  });
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert";
2
+ import * as fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { describe, test } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import FileTree from "../../src/drivers/FileTree.js";
7
+ import { ObjectTree, Tree } from "../../src/internal.js";
8
+
9
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const tempDirectory = path.join(dirname, "fixtures/temp");
11
+
12
+ describe("clear", () => {
13
+ test("unsets all public keys in an object tree", async () => {
14
+ const tree = new ObjectTree({ a: 1, b: 2, c: 3 });
15
+ await Tree.clear(tree);
16
+ assert.deepEqual(await Tree.plain(tree), {});
17
+ });
18
+
19
+ test("unsets all public keys in a file tree", async () => {
20
+ // Create a temp directory with some files
21
+ await fs.mkdir(tempDirectory, { recursive: true });
22
+ await fs.writeFile(path.join(tempDirectory, "a"), "1");
23
+ await fs.writeFile(path.join(tempDirectory, "b"), "2");
24
+
25
+ const tree = new FileTree(tempDirectory);
26
+ await Tree.clear(tree);
27
+
28
+ const files = await fs.readdir(tempDirectory);
29
+ assert.deepEqual(files, []);
30
+
31
+ // Remove temp directory
32
+ await fs.rm(tempDirectory, { recursive: true });
33
+ });
34
+ });
@@ -1,11 +1,11 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { DeepObjectTree, Tree } from "../../src/internal.js";
4
- import mergeDeep from "../../src/operations/deepMerge.js";
4
+ import deepMerge from "../../src/operations/deepMerge.js";
5
5
 
6
6
  describe("mergeDeep", () => {
7
7
  test("can merge deep", async () => {
8
- const fixture = mergeDeep(
8
+ const fixture = deepMerge(
9
9
  new DeepObjectTree({
10
10
  a: {
11
11
  b: 0, // Will be obscured by `b` below
@@ -34,9 +34,5 @@ describe("mergeDeep", () => {
34
34
  f: 4,
35
35
  },
36
36
  });
37
-
38
- // Parent of a subvalue is the merged tree
39
- const a = await fixture.get("a");
40
- assert.equal(a.parent, fixture);
41
37
  });
42
38
  });
@@ -12,7 +12,7 @@ describe("deepReverse", () => {
12
12
  d: 3,
13
13
  },
14
14
  };
15
- const reversed = deepReverse.call(null, tree);
15
+ const reversed = await deepReverse(tree);
16
16
  assert.deepEqual(await Tree.plain(reversed), {
17
17
  b: {
18
18
  d: 3,
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import ObjectTree from "../../src/drivers/ObjectTree.js";
4
+ import { default as del } from "../../src/operations/delete.js";
5
+ import plain from "../../src/operations/plain.js";
6
+
7
+ describe("delete", () => {
8
+ test("removes a value", async () => {
9
+ const fixture = new ObjectTree({
10
+ "Alice.md": "Hello, **Alice**.",
11
+ "Bob.md": "Hello, **Bob**.",
12
+ "Carol.md": "Hello, **Carol**.",
13
+ });
14
+ await del(fixture, "Alice.md");
15
+ assert.deepEqual(await plain(fixture), {
16
+ "Bob.md": "Hello, **Bob**.",
17
+ "Carol.md": "Hello, **Carol**.",
18
+ });
19
+ });
20
+ });
@@ -0,0 +1,18 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import entries from "../../src/operations/entries.js";
4
+
5
+ describe("entries", () => {
6
+ test("entries() returns the [key, value] pairs", async () => {
7
+ const fixture = {
8
+ "Alice.md": "Hello, **Alice**.",
9
+ "Bob.md": "Hello, **Bob**.",
10
+ "Carol.md": "Hello, **Carol**.",
11
+ };
12
+ assert.deepEqual(Array.from(await entries(fixture)), [
13
+ ["Alice.md", "Hello, **Alice**."],
14
+ ["Bob.md", "Hello, **Bob**."],
15
+ ["Carol.md", "Hello, **Carol**."],
16
+ ]);
17
+ });
18
+ });