@weborigami/language 0.6.17 → 0.7.0-beta.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 (90) hide show
  1. package/index.ts +1 -0
  2. package/main.js +7 -1
  3. package/package.json +7 -6
  4. package/src/compiler/compile.js +10 -3
  5. package/src/compiler/optimize.js +71 -40
  6. package/src/compiler/parse.js +1 -1
  7. package/src/compiler/parserHelpers.js +5 -3
  8. package/src/handlers/addExtensionKeyFn.js +18 -0
  9. package/src/handlers/epub_handler.js +54 -0
  10. package/src/handlers/getSource.js +11 -0
  11. package/src/handlers/handlers.js +2 -0
  12. package/src/handlers/htm_handler.js +1 -1
  13. package/src/handlers/js_handler.js +13 -4
  14. package/src/handlers/mediaTypeExtensions.json +15 -0
  15. package/src/handlers/ori_handler.js +8 -7
  16. package/src/handlers/oridocument_handler.js +19 -28
  17. package/src/handlers/processOriExport.js +17 -0
  18. package/src/handlers/tsv_handler.js +1 -1
  19. package/src/handlers/txt_handler.js +4 -2
  20. package/src/handlers/xhtml_handler.js +1 -1
  21. package/src/handlers/yaml_handler.js +6 -3
  22. package/src/handlers/zip_handler.js +112 -0
  23. package/src/project/activeProjectRoot.js +9 -0
  24. package/src/project/getGlobalsForTree.js +5 -0
  25. package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
  26. package/src/project/jsGlobals.js +1 -0
  27. package/src/project/projectConfig.js +2 -2
  28. package/src/project/projectRootFromPath.js +2 -0
  29. package/src/protocols/constructHref.js +3 -3
  30. package/src/protocols/constructSiteTree.js +11 -2
  31. package/src/protocols/explore.js +1 -1
  32. package/src/protocols/explorehttp.js +1 -1
  33. package/src/protocols/fetchAndHandleExtension.js +23 -11
  34. package/src/protocols/files.js +1 -0
  35. package/src/protocols/http.js +4 -1
  36. package/src/protocols/https.js +4 -1
  37. package/src/protocols/httpstree.js +1 -1
  38. package/src/protocols/httptree.js +1 -1
  39. package/src/protocols/package.js +15 -3
  40. package/src/runtime/AsyncCacheTransform.d.ts +5 -0
  41. package/src/runtime/AsyncCacheTransform.js +134 -0
  42. package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
  43. package/src/runtime/HandleExtensionsTransform.js +18 -2
  44. package/src/runtime/OrigamiFileMap.d.ts +5 -2
  45. package/src/runtime/OrigamiFileMap.js +27 -4
  46. package/src/runtime/ScopeMap.js +72 -0
  47. package/src/runtime/SyncCacheTransform.d.ts +8 -0
  48. package/src/runtime/SyncCacheTransform.js +133 -0
  49. package/src/runtime/SystemCacheMap.js +259 -0
  50. package/src/runtime/WatchFilesMixin.js +52 -19
  51. package/src/runtime/enableValueCaching.js +192 -0
  52. package/src/runtime/execute.js +2 -2
  53. package/src/runtime/executionContext.js +7 -0
  54. package/src/runtime/explainReferenceError.js +7 -2
  55. package/src/runtime/expressionObject.js +54 -46
  56. package/src/runtime/handleExtension.js +65 -34
  57. package/src/runtime/interop.js +2 -2
  58. package/src/runtime/mergeTrees.js +1 -1
  59. package/src/runtime/ops.js +28 -33
  60. package/src/runtime/symbols.js +3 -0
  61. package/src/runtime/systemCache.js +3 -0
  62. package/src/runtime/volatile.js +14 -0
  63. package/test/compiler/codeHelpers.js +2 -1
  64. package/test/compiler/optimize.test.js +62 -54
  65. package/test/handlers/epub_handler.test.js +27 -0
  66. package/test/handlers/fixtures/test.zip +0 -0
  67. package/test/handlers/ori_handler.test.js +22 -3
  68. package/test/handlers/oridocument_handler.test.js +1 -1
  69. package/test/handlers/zip_handler.test.js +45 -0
  70. package/test/protocols/https.test.js +19 -0
  71. package/test/protocols/package.test.js +7 -2
  72. package/test/runtime/AsyncCacheTransform.test.js +91 -0
  73. package/test/runtime/OrigamiFileMap.test.js +26 -23
  74. package/test/runtime/ScopeMap.test.js +49 -0
  75. package/test/runtime/SyncCacheTransform.test.js +93 -0
  76. package/test/runtime/SystemCacheMap.test.js +239 -0
  77. package/test/runtime/asyncCalcs.js +28 -0
  78. package/test/runtime/enableValueCaching.test.js +55 -0
  79. package/test/runtime/errors.test.js +53 -30
  80. package/test/runtime/evaluate.test.js +9 -4
  81. package/test/runtime/execute.test.js +6 -1
  82. package/test/runtime/expressionObject.test.js +55 -15
  83. package/test/runtime/fetchAndHandleExtension.test.js +24 -0
  84. package/test/runtime/fixtures/unpack/hello.json +1 -0
  85. package/test/runtime/handleExtension.test.js +12 -1
  86. package/test/runtime/ops.test.js +70 -65
  87. package/test/runtime/syncCalcs.js +27 -0
  88. package/test/runtime/systemCache.test.js +66 -0
  89. package/src/runtime/assignPropertyDescriptors.js +0 -23
  90. package/src/runtime/asyncStorage.js +0 -7
@@ -2,8 +2,11 @@ import { FileMap } from "@weborigami/async-tree";
2
2
  import EventTargetMixin from "./EventTargetMixin.js";
3
3
  import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
4
4
  import ImportModulesMixin from "./ImportModulesMixin.js";
5
+ import SyncCacheTransform from "./SyncCacheTransform.js";
5
6
  import WatchFilesMixin from "./WatchFilesMixin.js";
6
7
 
7
- export default class OrigamiFileMap extends HandleExtensionsTransform(
8
+ export default class OrigamiFileMap extends SyncCacheTransform(HandleExtensionsTransform(
8
9
  ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap)))
9
- ) {}
10
+ )) {
11
+ globals: any;
12
+ }
@@ -1,9 +1,32 @@
1
- import { FileMap } from "@weborigami/async-tree";
1
+ import { FileMap, trailingSlash } from "@weborigami/async-tree";
2
2
  import EventTargetMixin from "./EventTargetMixin.js";
3
3
  import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
4
4
  import ImportModulesMixin from "./ImportModulesMixin.js";
5
+ import SyncCacheTransform from "./SyncCacheTransform.js";
5
6
  import WatchFilesMixin from "./WatchFilesMixin.js";
7
+ import { noCacheSymbol } from "./symbols.js";
6
8
 
7
- export default class OrigamiFileMap extends HandleExtensionsTransform(
8
- ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap)))
9
- ) {}
9
+ export default class OrigamiFileMap extends SyncCacheTransform(
10
+ HandleExtensionsTransform(
11
+ ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap))),
12
+ ),
13
+ ) {
14
+ get cachePath() {
15
+ return super.cachePath ?? this.path;
16
+ }
17
+
18
+ // Workaround to register file paths in the system cache without trailing
19
+ // slahes. This is so that if someone calls `get("site.ori/")`, the cache path
20
+ // will be "site.ori". It's not clear whether this is the best solution, but
21
+ // hopefully suffices for now.
22
+ cachePathForKey(key) {
23
+ const normalized = trailingSlash.remove(key);
24
+ return super.cachePathForKey(normalized);
25
+ }
26
+
27
+ globals = null;
28
+
29
+ // Don't cache files, just record their dependencies. The OS already caches
30
+ // files, so caching them just consumes memory and may slow things down.
31
+ [noCacheSymbol] = true;
32
+ }
@@ -0,0 +1,72 @@
1
+ import { SyncMap } from "@weborigami/async-tree";
2
+ import path from "node:path";
3
+ import { cachePathSymbol } from "./symbols.js";
4
+ import systemCache from "./systemCache.js";
5
+
6
+ /**
7
+ * Return a sync map of all values in scope for the given sync source map.
8
+ *
9
+ * @param {SyncMap} source
10
+ */
11
+ export default class ScopeMap extends SyncMap {
12
+ constructor(source) {
13
+ super();
14
+ this.source = source;
15
+ this.trailingSlashKeys = source.trailingSlashKeys;
16
+ }
17
+
18
+ get(key) {
19
+ let value;
20
+
21
+ // Starting with this map, search up its parent hierarchy
22
+ let current = this.source;
23
+ while (current) {
24
+ // If the keys of this folder change, the scope request for this key could
25
+ // return a different value, so a change in keys needs to invalidate the
26
+ // value. Whether or not the get() request below succeeds, track the keys
27
+ // of this folder as an upstream dependency of the value being requested.
28
+ if (current[cachePathSymbol]) {
29
+ const folderKeysPath = path.join(current[cachePathSymbol], "_keys");
30
+ systemCache.trackCurrentDependency(folderKeysPath);
31
+ }
32
+
33
+ value = current.get(key);
34
+ if (value !== undefined) {
35
+ break;
36
+ }
37
+ current = current.parent;
38
+ }
39
+
40
+ return value;
41
+ }
42
+
43
+ // Collect all keys for this tree and all parents
44
+ keys() {
45
+ const scopeKeys = new Set();
46
+ let current = this.source;
47
+ while (current) {
48
+ for (const key of current.keys()) {
49
+ scopeKeys.add(key);
50
+ }
51
+ current = current.parent;
52
+ }
53
+ return scopeKeys[Symbol.iterator]();
54
+ }
55
+
56
+ // Collect all keys for this tree and all parents.
57
+ //
58
+ // This method exists for debugging purposes, as it's helpful to be able to
59
+ // quickly flatten and view the entire scope chain.
60
+ get trees() {
61
+ const result = [];
62
+
63
+ /** @type {SyncMap|null} */
64
+ let current = this.source;
65
+ while (current) {
66
+ result.push(current);
67
+ current = "parent" in current ? current.parent : null;
68
+ }
69
+
70
+ return result;
71
+ }
72
+ }
@@ -0,0 +1,8 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const SyncCacheTransform: Mixin<{
4
+ get cachePath(): string;
5
+ cachePathForKey(key: string): string;
6
+ }>
7
+
8
+ export default SyncCacheTransform;
@@ -0,0 +1,133 @@
1
+ import enableValueCaching from "./enableValueCaching.js";
2
+ import { cachePathSymbol, noCacheSymbol } from "./symbols.js";
3
+ import systemCache from "./systemCache.js";
4
+ import SystemCacheMap from "./SystemCacheMap.js";
5
+
6
+ /**
7
+ * General-purpose mixin for Origami maps with dependency tracking, used for:
8
+ * files, site resources, and scope references in Origami files
9
+ *
10
+ * This wraps a map's get() and keys() methods to add caching and dependency tracking.
11
+ * It tracks which cached values are downstream of other cached values so that if
12
+ * an upstream value changes, all dependent downstream cached values can be
13
+ * invalidated efficiently.
14
+ *
15
+ * Cache entries look like:
16
+ *
17
+ * key -> {
18
+ * downstreams: Set(path),
19
+ * value
20
+ * }
21
+ *
22
+ * This allows for efficiently evicting all a value and all its downstream
23
+ * dependent cached values.
24
+ *
25
+ * Example project:
26
+ *
27
+ * site.ori loads a.ori and b.ori
28
+ * a.ori loads c.ori
29
+ * b.ori loads c.ori
30
+ * c.ori doesn't load anything
31
+ *
32
+ * Resulting cache:
33
+ *
34
+ * site.ori -> { value: ... }
35
+ * a.ori -> { downstreams: Set(site.ori), value: ... }
36
+ * b.ori -> { downstreams: Set(site.ori), value: ... }
37
+ * c.ori -> { downstreams: Set(a.ori, b.ori), value: ... }
38
+ */
39
+ export default function SyncCacheTransform(Base) {
40
+ return class SyncCache extends Base {
41
+ constructor(...args) {
42
+ super(...args);
43
+
44
+ // Expose cache for debugging
45
+ this.cache = systemCache;
46
+ }
47
+
48
+ get cachePath() {
49
+ // @ts-ignore
50
+ return this[cachePathSymbol];
51
+ }
52
+
53
+ cachePathForKey(key) {
54
+ return key === "."
55
+ ? this.cachePath
56
+ : SystemCacheMap.joinPath(this.cachePath, key);
57
+ }
58
+
59
+ delete(key) {
60
+ const deleted = super.delete(key);
61
+ if (typeof key === "string") {
62
+ systemCache.delete(this.cachePathForKey(key));
63
+ if (deleted) {
64
+ // Deleted an existing key, need to invalidate cached keys
65
+ this.invalidateKeys();
66
+ }
67
+ }
68
+ return deleted;
69
+ }
70
+
71
+ get(key) {
72
+ if (typeof key !== "string" || key.length === 0) {
73
+ // Non-string keys and non-empty strings can't be cached
74
+ return super.get(key);
75
+ }
76
+ const cachePath = this.cachePathForKey(key);
77
+ const value = systemCache.getOrInsertComputed(cachePath, () => {
78
+ let result = super.get(key);
79
+ if (result !== undefined) {
80
+ // @ts-ignore
81
+ if (this[noCacheSymbol]) {
82
+ result[noCacheSymbol] = true;
83
+ } else {
84
+ result = enableValueCaching(result, cachePath);
85
+ }
86
+ }
87
+ return result;
88
+ });
89
+ return value;
90
+ }
91
+
92
+ invalidateKeys() {
93
+ const keysPath = this.cachePathForKey("_keys");
94
+ systemCache.delete(keysPath);
95
+ }
96
+
97
+ *keys() {
98
+ const keysPath = this.cachePathForKey("_keys");
99
+ const keys = systemCache.getOrInsertComputed(keysPath, () =>
100
+ // We can't cache an iterator; convert to array
101
+ Array.from(super.keys()),
102
+ );
103
+ yield* keys;
104
+ }
105
+
106
+ onKeysChange(key) {
107
+ super.onKeysChange?.(key);
108
+ this.invalidateKeys();
109
+ }
110
+
111
+ onValueChange(key) {
112
+ super.onValueChange?.(key);
113
+ systemCache.delete(this.cachePathForKey(key));
114
+ }
115
+
116
+ set(key, value) {
117
+ if (!this._self) {
118
+ // Initializing in constructor
119
+ super.set(key, value);
120
+ return;
121
+ }
122
+ if (typeof key !== "string") {
123
+ return super.set(key, value);
124
+ }
125
+ systemCache.delete(this.cachePathForKey(key));
126
+ if (!this.has(key)) {
127
+ // Adding a new key, need to invalidate cached keys
128
+ this.invalidateKeys();
129
+ }
130
+ super.set(key, value);
131
+ }
132
+ };
133
+ }
@@ -0,0 +1,259 @@
1
+ import { SyncMap, trailingSlash } from "@weborigami/async-tree";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { noCacheSymbol, volatileSymbol } from "./symbols.js";
4
+
5
+ // Async storage for tracking dependencies encountered during function evaluation
6
+ const asyncStorage = new AsyncLocalStorage();
7
+
8
+ // Sync analogue to AsyncLocalStorage for tracking dependencies in sync functions
9
+ const syncStorage = {
10
+ getStore() {
11
+ return this.stack.at(-1);
12
+ },
13
+
14
+ run(context, fn) {
15
+ this.stack.push(context);
16
+ const value = fn();
17
+ this.stack.pop();
18
+ return value;
19
+ },
20
+
21
+ /** @type {any[]} */
22
+ stack: [],
23
+ };
24
+
25
+ // For choosing a quasi-unique path for maps without a `cachePath` property
26
+ let nextPathId = 0;
27
+
28
+ export default class SystemCacheMap extends SyncMap {
29
+ delete(path) {
30
+ // Find all entries that depend on this path directly or indirectly
31
+ const toDelete = this.dependentEntries(path);
32
+
33
+ // Delete those dependent entries
34
+ for (const deletePath of toDelete.keys()) {
35
+ super.delete(deletePath);
36
+ }
37
+
38
+ // Remove deleted entries as being downstream from still-existing entries
39
+ for (const [deletePath, deleteEntry] of toDelete.entries()) {
40
+ for (const upstreamPath of deleteEntry.upstreams ?? []) {
41
+ const upstreamEntry = this.get(upstreamPath);
42
+ if (upstreamEntry?.downstreams) {
43
+ upstreamEntry.downstreams.delete(deletePath);
44
+ if (upstreamEntry.downstreams.size === 0) {
45
+ // No more downstream dependencies, clean up entry
46
+ delete upstreamEntry.downstreams;
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ return true;
53
+ }
54
+
55
+ // Return all entries that directly or indirectly depend on the given path
56
+ dependentEntries(path) {
57
+ const result = new Map();
58
+
59
+ const entry = this.get(path);
60
+ if (entry) {
61
+ // Path itself has an entry
62
+ result.set(path, entry);
63
+ }
64
+
65
+ // Add all entries with child paths that implicitly depend on this entry
66
+ for (const [otherPath, otherEntry] of this.entries()) {
67
+ if (this.isChildPath(path, otherPath)) {
68
+ result.set(otherPath, otherEntry);
69
+ }
70
+ }
71
+
72
+ // For each entry, add all entries downstream of it
73
+ for (const entry of result.values()) {
74
+ for (const downstreamPath of entry.downstreams ?? []) {
75
+ if (!result.has(downstreamPath)) {
76
+ for (const [key, value] of this.dependentEntries(downstreamPath)) {
77
+ if (!result.has(key)) {
78
+ result.set(key, value);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ // REVIEW: This doesn't have the correct signature for getOrInsertComputed,
89
+ // because it returns the entry `value` property, not the actual entry.
90
+ getOrInsertComputed(path, computeFn) {
91
+ let entry = this.get(path);
92
+
93
+ if (entry && "value" in entry) {
94
+ // Cache hit, value already computed
95
+ this.trackCurrentDependency(path, entry);
96
+ return entry.value;
97
+ }
98
+
99
+ // Cache miss, or entry has no value yet
100
+ let value;
101
+
102
+ if (!entry) {
103
+ // Create empty entry for this path
104
+ entry = {};
105
+ this.set(path, entry);
106
+ }
107
+
108
+ // Create new sync context to track entries downstream of this value
109
+ const context = { downstream: path };
110
+
111
+ // Get value in sync context
112
+ value = syncStorage.run(context, computeFn);
113
+ if (value?.[noCacheSymbol] || value?.[volatileSymbol]) {
114
+ // Don't cache value
115
+ delete entry.value;
116
+ } else {
117
+ // Add resolved value to cache
118
+ entry.value = value;
119
+ }
120
+
121
+ this.trackCurrentDependency(path, entry);
122
+
123
+ return value;
124
+ }
125
+
126
+ async getOrInsertComputedAsync(path, computeFn) {
127
+ let entry = this.get(path);
128
+
129
+ if (entry && "value" in entry) {
130
+ // Cache hit, value already computed
131
+ this.trackCurrentDependency(path, entry);
132
+ return entry.value;
133
+ }
134
+
135
+ // Cache miss, or entry has no value yet
136
+ if (syncStorage.getStore()) {
137
+ // A function that was supposed to be sync called an async function
138
+ throw new Error("Cannot track async dependencies in a sync context");
139
+ }
140
+
141
+ if (!entry) {
142
+ // Create empty entry for this path
143
+ entry = {};
144
+ this.set(path, entry);
145
+ }
146
+
147
+ // Create new async context to track entries downstream of this value
148
+ const context = { downstream: path };
149
+
150
+ // Get value in async context, don't await the result yet. Add promise to
151
+ // cache so concurrent requests get the same promise.
152
+ entry.value = asyncStorage.run(context, async () => {
153
+ const value = await computeFn();
154
+ if (value?.[noCacheSymbol] || value?.[volatileSymbol]) {
155
+ // Don't cache value
156
+ delete entry.value;
157
+ } else {
158
+ // Add resolved value to cache
159
+ entry.value = value;
160
+ }
161
+ return value;
162
+ });
163
+
164
+ this.trackCurrentDependency(path, entry);
165
+
166
+ return entry.value;
167
+ }
168
+
169
+ /**
170
+ * Like standard path.join(), but without special handling for absolute or
171
+ * relative paths: adding "/", ".", or ".." adds those strings to the path.
172
+ * This also avoids the behavior in path.join() where consecutive separators
173
+ * are collapsed. We want the cache path `a//b` to be distinct from `a/b`.
174
+ *
175
+ * @param {string[]} segments
176
+ */
177
+ static joinPath(...segments) {
178
+ let result = segments.shift() ?? "";
179
+ while (segments.length > 0) {
180
+ if (!result.endsWith("/")) {
181
+ result += "/";
182
+ }
183
+ let segment = segments.shift() ?? "";
184
+ result += segment;
185
+ }
186
+ return result;
187
+ }
188
+
189
+ // A path is considered a child path if the parent path (including a trailing
190
+ // slash) is a prefix of the child path.
191
+ isChildPath(parentPath, childPath) {
192
+ const normalized = trailingSlash.add(parentPath);
193
+ return childPath.startsWith(normalized);
194
+ }
195
+
196
+ static nextDefaultCachePath() {
197
+ const cachePath = `_object${nextPathId}`;
198
+ nextPathId++;
199
+ return cachePath;
200
+ }
201
+
202
+ runInContext(cachePath, fn) {
203
+ const context = { downstream: cachePath };
204
+ return syncStorage.run(context, fn);
205
+ }
206
+
207
+ runInContextAsync(cachePath, fn) {
208
+ const context = { downstream: cachePath };
209
+ return asyncStorage.run(context, fn);
210
+ }
211
+
212
+ /**
213
+ * Given a path for an upstream dependency, and optionally the entry for that
214
+ * path if it has already been retrieved, track the dependency between the
215
+ * upstream entry and the currently running downstream path.
216
+ *
217
+ * @param {string} upstreamPath
218
+ * @param {any} [upstreamEntry]
219
+ */
220
+ trackCurrentDependency(upstreamPath, upstreamEntry = null) {
221
+ if (!upstreamEntry) {
222
+ upstreamEntry = this.get(upstreamPath);
223
+ if (!upstreamEntry) {
224
+ // Create empty entry for this path, so that dependencies can be tracked
225
+ // for values that aren't cached.
226
+ upstreamEntry = {};
227
+ this.set(upstreamPath, upstreamEntry);
228
+ }
229
+ }
230
+
231
+ // Is this call happening downstream of another cached value?
232
+ const { downstream } =
233
+ syncStorage.getStore() ?? asyncStorage.getStore() ?? {};
234
+ if (downstream) {
235
+ if (this.isChildPath(upstreamPath, downstream)) {
236
+ // Downstream path is a child of the upstream path, no need to record
237
+ // explicit dependency
238
+ return;
239
+ }
240
+
241
+ let downstreamEntry = this.get(downstream);
242
+ if (!downstreamEntry) {
243
+ // The downstream entry has been deleted from the cache. It seems that
244
+ // Node can resurrect an asyncStorage for a run that has already
245
+ // finished. To cope, we reconstruct an entry.
246
+ downstreamEntry = {};
247
+ this.set(downstream, downstreamEntry);
248
+ }
249
+
250
+ // Add the downstream entry to the upstream entry's downstreams
251
+ upstreamEntry.downstreams ??= new Set();
252
+ upstreamEntry.downstreams.add(downstream);
253
+
254
+ // Add the upstream entry to the downstream entry's upstreams
255
+ downstreamEntry.upstreams ??= new Set();
256
+ downstreamEntry.upstreams.add(upstreamPath);
257
+ }
258
+ }
259
+ }
@@ -1,24 +1,16 @@
1
+ import { keysFromPath, Tree } from "@weborigami/async-tree";
1
2
  import * as fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import Watcher from "watcher";
4
5
  import TreeEvent from "./TreeEvent.js";
5
6
 
6
- // Map of paths to trees used by watcher
7
- const pathTreeMap = new Map();
8
-
9
7
  export default function WatchFilesMixin(Base) {
10
8
  return class WatchFiles extends Base {
11
9
  addEventListener(type, listener) {
12
10
  super.addEventListener(type, listener);
13
- if (type === "change") {
14
- this.watch();
15
- }
16
11
  }
17
12
 
18
13
  onChange(filePath) {
19
- // Reset cached values.
20
- this.subfoldersMap = new Map();
21
-
22
14
  // Special case: ignore events in .git folder
23
15
  if (filePath.includes(`${path.sep}.git${path.sep}`)) {
24
16
  return;
@@ -27,38 +19,79 @@ export default function WatchFilesMixin(Base) {
27
19
  this.dispatchEvent(new TreeEvent("change", { filePath }));
28
20
  }
29
21
 
22
+ onKeysChange(key) {
23
+ super.onKeysChange?.(key);
24
+ // this.dispatchEvent(new TreeEvent("keyschange", { action, key }));
25
+ }
26
+
27
+ onValueChange(key) {
28
+ super.onValueChange?.(key);
29
+ // this.dispatchEvent(new TreeEvent("valuechange", { key }));
30
+ }
31
+
30
32
  unwatch() {
31
33
  if (!this.watching) {
32
34
  return;
33
35
  }
34
36
 
35
37
  this.watcher?.close();
36
- this.watching = false;
38
+ this.watching = null;
37
39
  }
38
40
 
39
- // Turn on watching for the directory.
41
+ // Turn on watching for the directory; resolves when the watcher is ready.
40
42
  watch() {
41
43
  if (this.watching) {
42
- return;
44
+ return this.watching;
43
45
  }
44
- this.watching = true;
45
46
 
46
47
  // Ensure the directory exists.
47
48
  fs.mkdirSync(this.dirname, { recursive: true });
48
49
 
49
50
  this.watcher = new Watcher(this.dirname, {
50
51
  ignoreInitial: true,
51
- persistent: false,
52
52
  recursive: true,
53
53
  });
54
- this.watcher.on("all", (event, filePath) => {
54
+ this.watching = new Promise((resolve) => {
55
+ this.watcher?.on("ready", resolve);
56
+ });
57
+ this.watcher.on("all", async (event, filePath) => {
55
58
  this.onChange(filePath);
59
+
60
+ const relativePath = path.relative(this.dirname, filePath);
61
+ if (relativePath.startsWith(".git")) {
62
+ return; // Ignore noisy events in .git folder
63
+ }
64
+ if (relativePath.startsWith("..")) {
65
+ // Event outside the watched directory, shouldn't happen but ignore
66
+ return;
67
+ }
68
+
69
+ const keys = keysFromPath(relativePath);
70
+ const key = keys.pop();
71
+
72
+ const target = await Tree.traverse(this, ...keys);
73
+ if (target) {
74
+ switch (event) {
75
+ case "add":
76
+ case "addDir":
77
+ target.onKeysChange(key);
78
+ break;
79
+
80
+ case "change":
81
+ target.onValueChange(key);
82
+ break;
83
+
84
+ case "unlink":
85
+ case "unlinkDir":
86
+ // Removing file/folder invalidates both its value and the keys
87
+ target.onValueChange(key);
88
+ target.onKeysChange(key);
89
+ break;
90
+ }
91
+ }
56
92
  });
57
93
 
58
- // Add to the list of FileTree instances watching this directory.
59
- const treeRefs = pathTreeMap.get(this.dirname) ?? [];
60
- treeRefs.push(new WeakRef(this));
61
- pathTreeMap.set(this.dirname, treeRefs);
94
+ return this.watching;
62
95
  }
63
96
  };
64
97
  }