@weborigami/async-tree 0.0.35

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 (57) hide show
  1. package/ReadMe.md +1 -0
  2. package/browser.js +13 -0
  3. package/index.ts +44 -0
  4. package/main.js +23 -0
  5. package/package.json +20 -0
  6. package/src/BrowserFileTree.js +137 -0
  7. package/src/DeferredTree.js +72 -0
  8. package/src/FileTree.js +193 -0
  9. package/src/FunctionTree.js +47 -0
  10. package/src/MapTree.js +51 -0
  11. package/src/ObjectTree.js +103 -0
  12. package/src/SetTree.js +40 -0
  13. package/src/SiteTree.js +124 -0
  14. package/src/Tree.d.ts +22 -0
  15. package/src/Tree.js +404 -0
  16. package/src/keysJson.d.ts +4 -0
  17. package/src/keysJson.js +56 -0
  18. package/src/operations/cache.js +75 -0
  19. package/src/operations/merge.js +60 -0
  20. package/src/operations/mergeDeep.js +47 -0
  21. package/src/transforms/cachedKeyMaps.js +86 -0
  22. package/src/transforms/groupBy.js +40 -0
  23. package/src/transforms/keyMapsForExtensions.js +64 -0
  24. package/src/transforms/map.js +106 -0
  25. package/src/transforms/regExpKeys.js +65 -0
  26. package/src/transforms/sort.js +22 -0
  27. package/src/transforms/sortBy.js +29 -0
  28. package/src/transforms/sortNatural.js +10 -0
  29. package/src/utilities.d.ts +10 -0
  30. package/src/utilities.js +124 -0
  31. package/test/BrowserFileTree.test.js +119 -0
  32. package/test/DeferredTree.test.js +23 -0
  33. package/test/FileTree.test.js +134 -0
  34. package/test/FunctionTree.test.js +51 -0
  35. package/test/MapTree.test.js +37 -0
  36. package/test/ObjectTree.test.js +159 -0
  37. package/test/SetTree.test.js +32 -0
  38. package/test/SiteTree.test.js +110 -0
  39. package/test/Tree.test.js +347 -0
  40. package/test/browser/assert.js +45 -0
  41. package/test/browser/index.html +35 -0
  42. package/test/browser/testRunner.js +51 -0
  43. package/test/fixtures/markdown/Alice.md +1 -0
  44. package/test/fixtures/markdown/Bob.md +1 -0
  45. package/test/fixtures/markdown/Carol.md +1 -0
  46. package/test/operations/cache.test.js +57 -0
  47. package/test/operations/merge.test.js +39 -0
  48. package/test/operations/mergeDeep.test.js +38 -0
  49. package/test/transforms/cachedKeyMaps.test.js +41 -0
  50. package/test/transforms/groupBy.test.js +29 -0
  51. package/test/transforms/keyMapsForExtensions.test.js +70 -0
  52. package/test/transforms/map.test.js +174 -0
  53. package/test/transforms/regExpKeys.test.js +25 -0
  54. package/test/transforms/sort.test.js +30 -0
  55. package/test/transforms/sortBy.test.js +22 -0
  56. package/test/transforms/sortNatural.test.js +21 -0
  57. package/test/utilities.test.js +24 -0
package/ReadMe.md ADDED
@@ -0,0 +1 @@
1
+ This library contains definitions for asynchronous trees backed by standard JavaScript classes like `Object` and `Map` and standard browser APIs such as the [Origin Private File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system). The library also includes collections of helpers for common tree operations.
package/browser.js ADDED
@@ -0,0 +1,13 @@
1
+ // Exports for browser
2
+
3
+ export { default as DeferredTree } from "./src/DeferredTree.js";
4
+ // Skip FileTree.js, which is Node.js only.
5
+ export { default as BrowserFileTree } from "./src/BrowserFileTree.js";
6
+ export { default as FunctionTree } from "./src/FunctionTree.js";
7
+ export { default as MapTree } from "./src/MapTree.js";
8
+ export { default as ObjectTree } from "./src/ObjectTree.js";
9
+ export { default as SetTree } from "./src/SetTree.js";
10
+ export { default as SiteTree } from "./src/SiteTree.js";
11
+ export * as Tree from "./src/Tree.js";
12
+ export * as keysJson from "./src/keysJson.js";
13
+ export * from "./src/utilities.js";
package/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { AsyncTree } from "@weborigami/types";
2
+
3
+ export * from "./main.js";
4
+
5
+ export type KeyFn = (key: any, innerTree: AsyncTree) => any;
6
+
7
+ /**
8
+ * An object with a non-trivial `toString` method.
9
+ *
10
+ * TODO: We want to deliberately exclude the base `Object` class because its
11
+ * `toString` method return non-useful strings like `[object Object]`. How can
12
+ * we declare that in TypeScript?
13
+ */
14
+ export type HasString = {
15
+ toString(): string;
16
+ };
17
+
18
+ export type PlainObject = {
19
+ [key: string]: any;
20
+ };
21
+
22
+ export type ReduceFn = (values: any[], keys: any[]) => Promise<any>;
23
+
24
+ export type StringLike = string | HasString;
25
+
26
+ type NativeTreelike =
27
+ any[] |
28
+ AsyncTree |
29
+ Function |
30
+ Map<any, any> |
31
+ PlainObject |
32
+ Set<any>;
33
+
34
+ export type Treelike =
35
+ NativeTreelike |
36
+ Unpackable<NativeTreelike>;
37
+
38
+ export type TreeTransform = (tree: AsyncTree) => AsyncTree;
39
+
40
+ export type Unpackable<T> = {
41
+ unpack(): Promise<T>
42
+ };
43
+
44
+ export type ValueKeyFn = (value: any, key: any, innerTree: AsyncTree) => any;
package/main.js ADDED
@@ -0,0 +1,23 @@
1
+ // Exports for Node.js
2
+
3
+ export { default as DeferredTree } from "./src/DeferredTree.js";
4
+ export { default as FileTree } from "./src/FileTree.js";
5
+ export { default as FunctionTree } from "./src/FunctionTree.js";
6
+ export { default as MapTree } from "./src/MapTree.js";
7
+ export { default as ObjectTree } from "./src/ObjectTree.js";
8
+ // Skip BrowserFileTree.js, which is browser-only.
9
+ export { default as SetTree } from "./src/SetTree.js";
10
+ export { default as SiteTree } from "./src/SiteTree.js";
11
+ export * as Tree from "./src/Tree.js";
12
+ export * as keysJson from "./src/keysJson.js";
13
+ export { default as cache } from "./src/operations/cache.js";
14
+ export { default as merge } from "./src/operations/merge.js";
15
+ export { default as mergeDeep } from "./src/operations/mergeDeep.js";
16
+ export { default as cachedKeyMaps } from "./src/transforms/cachedKeyMaps.js";
17
+ export { default as groupBy } from "./src/transforms/groupBy.js";
18
+ export { default as keyMapsForExtensions } from "./src/transforms/keyMapsForExtensions.js";
19
+ export { default as map } from "./src/transforms/map.js";
20
+ export { default as sort } from "./src/transforms/sort.js";
21
+ export { default as sortBy } from "./src/transforms/sortBy.js";
22
+ export { default as sortNatural } from "./src/transforms/sortNatural.js";
23
+ export * from "./src/utilities.js";
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@weborigami/async-tree",
3
+ "version": "0.0.35",
4
+ "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
+ "type": "module",
6
+ "main": "./main.js",
7
+ "browser": "./browser.js",
8
+ "types": "./index.ts",
9
+ "devDependencies": {
10
+ "@types/node": "20.8.10",
11
+ "typescript": "5.2.2"
12
+ },
13
+ "dependencies": {
14
+ "@weborigami/types": "https://gitpkg.now.sh/GraphOrigami/origami/types?main"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test",
18
+ "typecheck": "tsc"
19
+ }
20
+ }
@@ -0,0 +1,137 @@
1
+ import * as Tree from "./Tree.js";
2
+ import { hiddenFileNames, isStringLike, sortNatural } from "./utilities.js";
3
+
4
+ const TypedArray = Object.getPrototypeOf(Uint8Array);
5
+
6
+ /**
7
+ * A tree of files backed by a browser-hosted file system such as the standard
8
+ * Origin Private File System or the (as of October 2023) experimental File
9
+ * System Access API.
10
+ *
11
+ * @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
12
+ * @implements {AsyncMutableTree}
13
+ */
14
+ export default class BrowserFileTree {
15
+ /**
16
+ * Construct a tree of files backed by a browser-hosted file system.
17
+ *
18
+ * The directory handle can be obtained via any of the [methods that return a
19
+ * FileSystemDirectoryHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle).
20
+ * If no directory is supplied, the tree is rooted at the Origin Private File
21
+ * System for the current site.
22
+ *
23
+ * @param {FileSystemDirectoryHandle} [directoryHandle]
24
+ */
25
+ constructor(directoryHandle) {
26
+ /** @type {FileSystemDirectoryHandle}
27
+ * @ts-ignore */
28
+ this.directory = directoryHandle;
29
+ }
30
+
31
+ async get(key) {
32
+ const directory = await this.getDirectory();
33
+
34
+ // Try the key as a subfolder name.
35
+ try {
36
+ const subfolderHandle = await directory.getDirectoryHandle(key);
37
+ return Reflect.construct(this.constructor, [subfolderHandle]);
38
+ } catch (error) {
39
+ if (
40
+ !(
41
+ error instanceof DOMException &&
42
+ (error.name === "NotFoundError" || error.name === "TypeMismatchError")
43
+ )
44
+ ) {
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ // Try the key as a file name.
50
+ try {
51
+ const fileHandle = await directory.getFileHandle(key);
52
+ const file = await fileHandle.getFile();
53
+ return file.arrayBuffer();
54
+ } catch (error) {
55
+ if (!(error instanceof DOMException && error.name === "NotFoundError")) {
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ return undefined;
61
+ }
62
+
63
+ async getDirectory() {
64
+ this.directory ??= await navigator.storage.getDirectory();
65
+ return this.directory;
66
+ }
67
+
68
+ async keys() {
69
+ const directory = await this.getDirectory();
70
+ let keys = [];
71
+ // @ts-ignore
72
+ for await (const key of directory.keys()) {
73
+ keys.push(key);
74
+ }
75
+ // Filter out unhelpful file names.
76
+ keys = keys.filter((key) => !hiddenFileNames.includes(key));
77
+ sortNatural(keys);
78
+ return keys;
79
+ }
80
+
81
+ async set(key, value) {
82
+ const directory = await this.getDirectory();
83
+
84
+ if (value === undefined) {
85
+ // Delete file.
86
+ try {
87
+ await directory.removeEntry(key);
88
+ } catch (error) {
89
+ // If the file didn't exist, ignore the error.
90
+ if (
91
+ !(error instanceof DOMException && error.name === "NotFoundError")
92
+ ) {
93
+ throw error;
94
+ }
95
+ }
96
+ return this;
97
+ }
98
+
99
+ // Treat null value as empty string; will create an empty file.
100
+ if (value === null) {
101
+ value = "";
102
+ }
103
+
104
+ // True if fs.writeFile can directly write the value to a file.
105
+ let isWriteable =
106
+ value instanceof ArrayBuffer ||
107
+ value instanceof TypedArray ||
108
+ value instanceof DataView ||
109
+ value instanceof Blob;
110
+
111
+ if (!isWriteable && isStringLike(value)) {
112
+ // Value has a meaningful `toString` method, use that.
113
+ value = String(value);
114
+ isWriteable = true;
115
+ }
116
+
117
+ if (isWriteable) {
118
+ // Write file.
119
+ const fileHandle = await directory.getFileHandle(key, { create: true });
120
+ const writable = await fileHandle.createWritable();
121
+ await writable.write(value);
122
+ await writable.close();
123
+ } else if (Tree.isTreelike(value)) {
124
+ // Treat value as a tree and write it out as a subdirectory.
125
+ const subdirectory = await directory.getDirectoryHandle(key, {
126
+ create: true,
127
+ });
128
+ const destTree = Reflect.construct(this.constructor, [subdirectory]);
129
+ await Tree.assign(destTree, value);
130
+ } else {
131
+ const typeName = value?.constructor?.name ?? "unknown";
132
+ throw new TypeError(`Cannot write a value of type ${typeName} as ${key}`);
133
+ }
134
+
135
+ return this;
136
+ }
137
+ }
@@ -0,0 +1,72 @@
1
+ import * as Tree from "./Tree.js";
2
+
3
+ /**
4
+ * A tree that is loaded lazily.
5
+ *
6
+ * This is useful in situations that must return a tree synchronously. If
7
+ * constructing the tree requires an asynchronous operation, this class can be
8
+ * used as a wrapper that can be returned immediately. The tree will be loaded
9
+ * the first time the keys() or get() functions are called.
10
+ *
11
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
12
+ * @implements {AsyncTree}
13
+ */
14
+ export default class DeferredTree {
15
+ /**
16
+ * @param {Function|Promise<any>} loader
17
+ */
18
+ constructor(loader) {
19
+ this.loader = loader;
20
+ this.treePromise = null;
21
+ this._tree = null;
22
+ this._parent = null;
23
+ }
24
+
25
+ async get(key) {
26
+ const tree = await this.tree();
27
+ return tree.get(key);
28
+ }
29
+
30
+ async loadResult() {
31
+ if (!(this.loader instanceof Promise)) {
32
+ this.loader = this.loader();
33
+ }
34
+ return this.loader;
35
+ }
36
+
37
+ async keys() {
38
+ const tree = await this.tree();
39
+ return tree.keys();
40
+ }
41
+
42
+ get parent() {
43
+ return this._tree?.parent ?? this._parent;
44
+ }
45
+ set parent(parent) {
46
+ if (this._tree && !this._tree.parent) {
47
+ this._tree.parent = parent;
48
+ } else {
49
+ this._parent = parent;
50
+ }
51
+ }
52
+
53
+ async tree() {
54
+ if (this._tree) {
55
+ return this._tree;
56
+ }
57
+
58
+ // Use a promise to ensure the treelike is only converted to a tree once.
59
+ this.treePromise ??= this.loadResult().then((treelike) => {
60
+ this._tree = Tree.from(treelike);
61
+ if (this._parent) {
62
+ if (!this._tree.parent) {
63
+ this._tree.parent = this._parent;
64
+ }
65
+ this._parent = null;
66
+ }
67
+ return this._tree;
68
+ });
69
+
70
+ return this.treePromise;
71
+ }
72
+ }
@@ -0,0 +1,193 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import * as Tree from "./Tree.js";
5
+ import {
6
+ getRealmObjectPrototype,
7
+ hiddenFileNames,
8
+ sortNatural,
9
+ } from "./utilities.js";
10
+
11
+ const TypedArray = Object.getPrototypeOf(Uint8Array);
12
+
13
+ /**
14
+ * A file system tree via the Node file system API.
15
+ *
16
+ * @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
17
+ * @implements {AsyncMutableTree}
18
+ */
19
+ export default class FileTree {
20
+ /**
21
+ * @param {string} location
22
+ */
23
+ constructor(location) {
24
+ this.dirname = location.startsWith("file://")
25
+ ? path.dirname(fileURLToPath(location))
26
+ : path.resolve(process.cwd(), location);
27
+ this.parent = null;
28
+ }
29
+
30
+ async get(key) {
31
+ const filePath = path.resolve(this.dirname, key);
32
+
33
+ let stats;
34
+ try {
35
+ stats = await fs.stat(filePath);
36
+ } catch (/** @type {any} */ error) {
37
+ if (error.code === "ENOENT" /* File not found */) {
38
+ return undefined;
39
+ }
40
+ throw error;
41
+ }
42
+
43
+ if (stats.isDirectory()) {
44
+ // Return subdirectory as a tree
45
+ const subtree = Reflect.construct(this.constructor, [filePath]);
46
+ subtree.parent = this;
47
+ return subtree;
48
+ } else {
49
+ // Return file contents
50
+ return fs.readFile(filePath);
51
+ }
52
+ }
53
+
54
+ async isKeyForSubtree(key) {
55
+ const filePath = path.join(this.dirname, key);
56
+ const stats = await stat(filePath);
57
+ return stats ? stats.isDirectory() : false;
58
+ }
59
+
60
+ /**
61
+ * Enumerate the names of the files/subdirectories in this directory.
62
+ */
63
+ async keys() {
64
+ let entries;
65
+ try {
66
+ entries = await fs.readdir(this.dirname, { withFileTypes: true });
67
+ } catch (/** @type {any} */ error) {
68
+ if (error.code !== "ENOENT") {
69
+ throw error;
70
+ }
71
+ entries = [];
72
+ }
73
+
74
+ let names = entries.map((entry) => entry.name);
75
+
76
+ // Filter out unhelpful file names.
77
+ names = names.filter((name) => !hiddenFileNames.includes(name));
78
+
79
+ // Node fs.readdir sort order appears to be unreliable; see, e.g.,
80
+ // https://github.com/nodejs/node/issues/3232.
81
+ sortNatural(names);
82
+ return names;
83
+ }
84
+
85
+ get path() {
86
+ return this.dirname;
87
+ }
88
+
89
+ async set(key, value) {
90
+ // Where are we going to write this value?
91
+ const stringKey = key ? String(key) : "";
92
+ const destPath = path.resolve(this.dirname, stringKey);
93
+
94
+ if (value === undefined) {
95
+ // Delete the file or directory.
96
+ let stats;
97
+ try {
98
+ stats = await stat(destPath);
99
+ } catch (/** @type {any} */ error) {
100
+ if (error.code === "ENOENT" /* File not found */) {
101
+ return this;
102
+ }
103
+ throw error;
104
+ }
105
+
106
+ if (stats?.isDirectory()) {
107
+ // Delete directory.
108
+ await fs.rm(destPath, { recursive: true });
109
+ } else if (stats) {
110
+ // Delete file.
111
+ await fs.unlink(destPath);
112
+ }
113
+
114
+ return this;
115
+ }
116
+
117
+ // Treat null value as empty string; will create an empty file.
118
+ if (value === null) {
119
+ value = "";
120
+ }
121
+
122
+ if (value instanceof ArrayBuffer) {
123
+ // Convert ArrayBuffer to Uint8Array, which Node.js can write directly.
124
+ value = new Uint8Array(value);
125
+ }
126
+
127
+ // True if fs.writeFile can directly write the value to a file.
128
+ let isWriteable =
129
+ value instanceof TypedArray ||
130
+ value instanceof DataView ||
131
+ (globalThis.ReadableStream && value instanceof ReadableStream);
132
+
133
+ if (!isWriteable && isStringLike(value)) {
134
+ // Value has a meaningful `toString` method, use that.
135
+ value = String(value);
136
+ isWriteable = true;
137
+ }
138
+
139
+ if (isWriteable) {
140
+ // Ensure this directory exists.
141
+ await fs.mkdir(this.dirname, { recursive: true });
142
+ // Write out the value as the contents of a file.
143
+ await fs.writeFile(destPath, value);
144
+ } else if (Tree.isTreelike(value)) {
145
+ // Treat value as a tree and write it out as a subdirectory.
146
+ const destTree = Reflect.construct(this.constructor, [destPath]);
147
+ await Tree.assign(destTree, value);
148
+ } else {
149
+ const typeName = value?.constructor?.name ?? "unknown";
150
+ throw new TypeError(
151
+ `Cannot write a value of type ${typeName} as ${stringKey}`
152
+ );
153
+ }
154
+
155
+ return this;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Return true if the object is a string or object with a non-trival `toString`
161
+ * method.
162
+ *
163
+ * @param {any} obj
164
+ */
165
+ function isStringLike(obj) {
166
+ if (typeof obj === "string") {
167
+ return true;
168
+ } else if (obj?.toString === undefined) {
169
+ return false;
170
+ } else if (obj.toString === getRealmObjectPrototype(obj).toString) {
171
+ // The stupid Object.prototype.toString implementation always returns
172
+ // "[object Object]", so if that's the only toString method the object has,
173
+ // we return false.
174
+ return false;
175
+ } else {
176
+ return true;
177
+ }
178
+ }
179
+
180
+ // Return the file information for the file/folder at the given path.
181
+ // If it does not exist, return undefined.
182
+ async function stat(filePath) {
183
+ try {
184
+ // Await the result here so that, if the file doesn't exist, the catch block
185
+ // below will catch the exception.
186
+ return await fs.stat(filePath);
187
+ } catch (/** @type {any} */ error) {
188
+ if (error.code === "ENOENT" /* File not found */) {
189
+ return undefined;
190
+ }
191
+ throw error;
192
+ }
193
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A tree defined by a function and an optional domain.
3
+ *
4
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
5
+ * @implements {AsyncTree}
6
+ */
7
+ export default class FunctionTree {
8
+ /**
9
+ * @param {function} fn the key->value function
10
+ * @param {Iterable<any>} [domain] optional domain of the function
11
+ */
12
+ constructor(fn, domain = []) {
13
+ this.fn = fn;
14
+ this.domain = domain;
15
+ this.parent = null;
16
+ }
17
+
18
+ /**
19
+ * Return the application of the function to the given key.
20
+ *
21
+ * @param {any} key
22
+ */
23
+ async get(key) {
24
+ const value =
25
+ this.fn.length <= 1
26
+ ? // Function takes no arguments or only one argument: invoke
27
+ await this.fn.call(null, key)
28
+ : // Bind the key to the first parameter. Subsequent get calls will
29
+ // eventually bind all parameters until only one remains. At that point,
30
+ // the above condition will apply and the function will be invoked.
31
+ Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
32
+
33
+ return value;
34
+ }
35
+
36
+ /**
37
+ * Enumerates the function's domain (if defined) as the tree's keys. If no domain
38
+ * was defined, this returns an empty iterator.
39
+ */
40
+ async keys() {
41
+ return this.domain;
42
+ }
43
+
44
+ async unpack() {
45
+ return this.fn;
46
+ }
47
+ }
package/src/MapTree.js ADDED
@@ -0,0 +1,51 @@
1
+ import * as Tree from "./Tree.js";
2
+
3
+ /**
4
+ * A tree backed by a JavaScript `Map` object.
5
+ *
6
+ * Note: By design, the standard `Map` class already complies with the
7
+ * `AsyncTree` interface. This class adds some additional tree behavior, such as
8
+ * constructing subtree instances and setting their `parent` property. While
9
+ * we'd like to construct this by subclassing `Map`, that class appears
10
+ * puzzingly and deliberately implemented to break subclasses.
11
+ *
12
+ * @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
13
+ * @implements {AsyncMutableTree}
14
+ */
15
+ export default class MapTree {
16
+ /**
17
+ * @param {Iterable} [iterable]
18
+ */
19
+ constructor(iterable = []) {
20
+ this.map = new Map(iterable);
21
+ this.parent = null;
22
+ }
23
+
24
+ async get(key) {
25
+ let value = this.map.get(key);
26
+
27
+ if (value instanceof Map) {
28
+ value = Reflect.construct(this.constructor, [value]);
29
+ }
30
+
31
+ if (Tree.isAsyncTree(value) && !value.parent) {
32
+ value.parent = this;
33
+ }
34
+
35
+ return value;
36
+ }
37
+
38
+ async isKeyForSubtree(key) {
39
+ const value = this.map.get(key);
40
+ return value instanceof Map || Tree.isAsyncTree(value);
41
+ }
42
+
43
+ async keys() {
44
+ return this.map.keys();
45
+ }
46
+
47
+ async set(key, value) {
48
+ this.map.set(key, value);
49
+ return this;
50
+ }
51
+ }