@weborigami/async-tree 0.0.54 → 0.0.56

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/main.js CHANGED
@@ -23,8 +23,11 @@ export { default as sort } from "./src/operations/sort.js";
23
23
  export { default as take } from "./src/operations/take.js";
24
24
  export * as symbols from "./src/symbols.js";
25
25
  export { default as cachedKeyFunctions } from "./src/transforms/cachedKeyFunctions.js";
26
+ export { default as deepReverse } from "./src/transforms/deepReverse.js";
27
+ export { default as invokeFunctions } from "./src/transforms/invokeFunctions.js";
26
28
  export { default as keyFunctionsForExtensions } from "./src/transforms/keyFunctionsForExtensions.js";
27
29
  export { default as mapFn } from "./src/transforms/mapFn.js";
30
+ export { default as reverse } from "./src/transforms/reverse.js";
28
31
  export { default as sortFn } from "./src/transforms/sortFn.js";
29
32
  export { default as takeFn } from "./src/transforms/takeFn.js";
30
33
  export * from "./src/utilities.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,7 +11,7 @@
11
11
  "typescript": "5.4.5"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.0.54"
14
+ "@weborigami/types": "0.0.56"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
package/src/FileTree.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  getRealmObjectPrototype,
7
7
  hiddenFileNames,
8
8
  isPacked,
9
+ isPlainObject,
9
10
  naturalOrder,
10
11
  } from "./utilities.js";
11
12
 
@@ -165,6 +166,9 @@ export default class FileTree {
165
166
  await fs.mkdir(this.dirname, { recursive: true });
166
167
  // Write out the value as the contents of a file.
167
168
  await fs.writeFile(destPath, value);
169
+ } else if (isPlainObject(value) && Object.keys(value).length === 0) {
170
+ // Special case: empty object means create an empty directory.
171
+ await fs.mkdir(destPath, { recursive: true });
168
172
  } else if (Tree.isTreelike(value)) {
169
173
  // Treat value as a tree and write it out as a subdirectory.
170
174
  const destTree = Reflect.construct(this.constructor, [destPath]);
@@ -1,8 +1,10 @@
1
1
  import { DeepObjectTree, Tree } from "../internal.js";
2
2
 
3
3
  /**
4
- * Caches values from a source tree in a second cache tree. If no second tree is
5
- * supplied, an in-memory cache is used.
4
+ * Caches values from a source tree in a second cache tree. Cache source tree
5
+ * keys in memory.
6
+ *
7
+ * If no second tree is supplied, an in-memory value cache is used.
6
8
  *
7
9
  * An optional third filter tree can be supplied. If a filter tree is supplied,
8
10
  * only values for keys that match the filter will be cached.
@@ -22,6 +24,7 @@ export default function treeCache(sourceTreelike, cacheTree, filterTreelike) {
22
24
 
23
25
  /** @type {AsyncMutableTree} */
24
26
  const cache = cacheTree ?? new DeepObjectTree({});
27
+ let keys;
25
28
  return {
26
29
  description: "cache",
27
30
 
@@ -65,14 +68,7 @@ export default function treeCache(sourceTreelike, cacheTree, filterTreelike) {
65
68
  },
66
69
 
67
70
  async keys() {
68
- const keys = new Set(await source.keys());
69
-
70
- // We also add the cache's keys in case the keys provided by the source
71
- // tree have changed since the cache was updated.
72
- for (const key of await cache.keys()) {
73
- keys.add(key);
74
- }
75
-
71
+ keys ??= source.keys();
76
72
  return keys;
77
73
  },
78
74
  };
@@ -0,0 +1,29 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ /**
4
+ * Reverse the order of keys at all levels of the tree.
5
+ *
6
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
+ * @typedef {import("../../index.ts").Treelike} Treelike
8
+ *
9
+ * @param {Treelike} treelike
10
+ * @returns {AsyncTree}
11
+ */
12
+ export default function deepReverse(treelike) {
13
+ const tree = Tree.from(treelike);
14
+ return {
15
+ async get(key) {
16
+ let value = await tree.get(key);
17
+ if (Tree.isAsyncTree(value)) {
18
+ value = deepReverse(value);
19
+ }
20
+ return value;
21
+ },
22
+
23
+ async keys() {
24
+ const keys = Array.from(await tree.keys());
25
+ keys.reverse();
26
+ return keys;
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,18 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ export default function invokeFunctions(treelike) {
4
+ const tree = Tree.from(treelike);
5
+ return {
6
+ async get(key) {
7
+ let value = await tree.get(key);
8
+ if (typeof value === "function") {
9
+ value = value();
10
+ }
11
+ return value;
12
+ },
13
+
14
+ async keys() {
15
+ return tree.keys();
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,25 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ /**
4
+ * Reverse the order of the top-level keys in the tree.
5
+ *
6
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
+ * @typedef {import("../../index.ts").Treelike} Treelike
8
+ *
9
+ * @param {Treelike} treelike
10
+ * @returns {AsyncTree}
11
+ */
12
+ export default function reverse(treelike) {
13
+ const tree = Tree.from(treelike);
14
+ return {
15
+ async get(key) {
16
+ return tree.get(key);
17
+ },
18
+
19
+ async keys() {
20
+ const keys = Array.from(await tree.keys());
21
+ keys.reverse();
22
+ return keys;
23
+ },
24
+ };
25
+ }
@@ -6,6 +6,8 @@ export const hiddenFileNames: string[];
6
6
  export function isPacked(object: any): object is Packed;
7
7
  export function isPlainObject(object: any): object is PlainObject;
8
8
  export function isUnpackable(object): object is { unpack: () => any };
9
- export function isStringLike(obj: any): obj is StringLike;
9
+ export function isStringLike(object: any): object is StringLike;
10
10
  export function keysFromPath(path: string): string[];
11
11
  export const naturalOrder: (a: string, b: string) => number;
12
+ export function pipeline(start: any, ...functions: Function[]): Promise<any>;
13
+ export function toString(object: any): string;
package/src/utilities.js CHANGED
@@ -1,3 +1,4 @@
1
+ const textDecoder = new TextDecoder();
1
2
  const TypedArray = Object.getPrototypeOf(Uint8Array);
2
3
 
3
4
  /**
@@ -142,3 +143,42 @@ export function keysFromPath(pathname) {
142
143
  export const naturalOrder = new Intl.Collator(undefined, {
143
144
  numeric: true,
144
145
  }).compare;
146
+
147
+ /**
148
+ * Apply a series of functions to a value, passing the result of each function
149
+ * to the next one.
150
+ *
151
+ * @param {any} start
152
+ * @param {...Function} fns
153
+ */
154
+ export async function pipeline(start, ...fns) {
155
+ return fns.reduce(async (acc, fn) => fn(await acc), start);
156
+ }
157
+
158
+ /**
159
+ * Return a string form of the object, handling cases not generally handled by
160
+ * the standard JavaScript `toString()` method:
161
+ *
162
+ * 1. If the object is an ArrayBuffer or TypedArray, decode the array as UTF-8.
163
+ * 2. If the object is otherwise a plain JavaScript object with the useless
164
+ * default toString() method, return null instead of "[object Object]". In
165
+ * practice, it's generally more useful to have this method fail than to
166
+ * return a useless string.
167
+ *
168
+ * @param {any} object
169
+ * @returns {string|null}
170
+ */
171
+ export function toString(object) {
172
+ if (object instanceof ArrayBuffer || object instanceof TypedArray) {
173
+ // Treat the buffer as UTF-8 text.
174
+ const decoded = textDecoder.decode(object);
175
+ // If the result appears to contain non-printable characters, it's probably not a string.
176
+ // https://stackoverflow.com/a/1677660/76472
177
+ const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
178
+ return hasNonPrintableCharacters ? null : decoded;
179
+ } else if (isStringLike(object)) {
180
+ return String(object);
181
+ } else {
182
+ return null;
183
+ }
184
+ }
@@ -9,7 +9,7 @@ import { Tree } from "../src/internal.js";
9
9
  const dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const tempDirectory = path.join(dirname, "fixtures/temp");
11
11
 
12
- describe("FileTree", async () => {
12
+ describe.only("FileTree", async () => {
13
13
  test("can get the keys of the tree", async () => {
14
14
  const fixture = createFixture("fixtures/markdown");
15
15
  assert.deepEqual(Array.from(await fixture.keys()), [
@@ -61,6 +61,23 @@ describe("FileTree", async () => {
61
61
  await removeTempDirectory();
62
62
  });
63
63
 
64
+ test.only("can create empty subfolder via set()", async () => {
65
+ await createTempDirectory();
66
+
67
+ // Write out new, empty folder called "empty".
68
+ const tempFiles = new FileTree(tempDirectory);
69
+ await tempFiles.set("empty", {});
70
+
71
+ // Verify folder exists and has no contents.
72
+ const folderPath = path.join(tempDirectory, "empty");
73
+ const stats = await fs.stat(folderPath);
74
+ assert(stats.isDirectory());
75
+ const files = await fs.readdir(folderPath);
76
+ assert.deepEqual(files, []);
77
+
78
+ await removeTempDirectory();
79
+ });
80
+
64
81
  test("can write out subfolder via set()", async () => {
65
82
  await createTempDirectory();
66
83
 
@@ -0,0 +1,24 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import assert from "node:assert";
3
+ import { describe, test } from "node:test";
4
+ import deepReverse from "../../src/transforms/deepReverse.js";
5
+
6
+ describe("deepReverse", () => {
7
+ test("reverses keys at all levels of a tree", async () => {
8
+ const tree = {
9
+ a: 1,
10
+ b: {
11
+ c: 2,
12
+ d: 3,
13
+ },
14
+ };
15
+ const reversed = deepReverse.call(null, tree);
16
+ assert.deepEqual(await Tree.plain(reversed), {
17
+ b: {
18
+ d: 3,
19
+ c: 2,
20
+ },
21
+ a: 1,
22
+ });
23
+ });
24
+ });
@@ -0,0 +1,17 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import { Tree } from "../../src/internal.js";
4
+ import invokeFunctions from "../../src/transforms/invokeFunctions.js";
5
+
6
+ describe("invokeFunctions", () => {
7
+ test("invokes function values, leaves other values as is", async () => {
8
+ const fixture = invokeFunctions({
9
+ a: 1,
10
+ b: () => 2,
11
+ });
12
+ assert.deepEqual(await Tree.plain(fixture), {
13
+ a: 1,
14
+ b: 2,
15
+ });
16
+ });
17
+ });
@@ -0,0 +1,23 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import assert from "node:assert";
3
+ import { describe, test } from "node:test";
4
+ import reverse from "../../src/transforms/reverse.js";
5
+
6
+ describe("reverse", () => {
7
+ test("reverses a tree's top-level keys", async () => {
8
+ const tree = {
9
+ a: "A",
10
+ b: "B",
11
+ c: "C",
12
+ };
13
+ const reversed = reverse.call(null, tree);
14
+ // @ts-ignore
15
+ assert.deepEqual(Array.from(await reversed.keys()), ["c", "b", "a"]);
16
+ // @ts-ignore
17
+ assert.deepEqual(await Tree.plain(reversed), {
18
+ c: "C",
19
+ b: "B",
20
+ a: "A",
21
+ });
22
+ });
23
+ });
@@ -27,4 +27,29 @@ describe("utilities", () => {
27
27
  strings.sort(utilities.naturalOrder);
28
28
  assert.deepEqual(strings, ["file1", "file9", "file10"]);
29
29
  });
30
+
31
+ test("pipeline applies a series of functions to a value", async () => {
32
+ const addOne = (n) => n + 1;
33
+ const double = (n) => n * 2;
34
+ const square = (n) => n * n;
35
+ const result = await utilities.pipeline(1, addOne, double, square);
36
+ assert.equal(result, 16);
37
+ });
38
+
39
+ test("toString returns the value of an object's `toString` method", () => {
40
+ const object = {
41
+ toString: () => "text",
42
+ };
43
+ assert.equal(utilities.toString(object), "text");
44
+ });
45
+
46
+ test("toString returns null for an object with no useful `toString`", () => {
47
+ const object = {};
48
+ assert.equal(utilities.toString(object), null);
49
+ });
50
+
51
+ test("toString decodes an ArrayBuffer as UTF-8", () => {
52
+ const buffer = Buffer.from("text", "utf8");
53
+ assert.equal(utilities.toString(buffer), "text");
54
+ });
30
55
  });