@weborigami/language 0.6.17 → 0.7.0-beta.1
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/index.ts +1 -0
- package/main.js +7 -1
- package/package.json +6 -6
- package/src/compiler/compile.js +10 -3
- package/src/compiler/optimize.js +71 -40
- package/src/compiler/parse.js +1 -1
- package/src/compiler/parserHelpers.js +5 -3
- package/src/handlers/getSource.js +11 -0
- package/src/handlers/htm_handler.js +1 -1
- package/src/handlers/js_handler.js +13 -4
- package/src/handlers/mediaTypeExtensions.json +15 -0
- package/src/handlers/ori_handler.js +8 -7
- package/src/handlers/oridocument_handler.js +17 -10
- package/src/handlers/processOriExport.js +17 -0
- package/src/handlers/tsv_handler.js +1 -1
- package/src/handlers/txt_handler.js +4 -2
- package/src/handlers/xhtml_handler.js +1 -1
- package/src/handlers/yaml_handler.js +6 -3
- package/src/project/activeProjectRoot.js +9 -0
- package/src/project/getGlobalsForTree.js +5 -0
- package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
- package/src/project/jsGlobals.js +1 -0
- package/src/project/projectConfig.js +2 -2
- package/src/project/projectRootFromPath.js +2 -0
- package/src/protocols/constructHref.js +3 -3
- package/src/protocols/constructSiteTree.js +11 -2
- package/src/protocols/explore.js +1 -1
- package/src/protocols/explorehttp.js +1 -1
- package/src/protocols/fetchAndHandleExtension.js +23 -11
- package/src/protocols/files.js +1 -0
- package/src/protocols/http.js +4 -1
- package/src/protocols/https.js +4 -1
- package/src/protocols/httpstree.js +1 -1
- package/src/protocols/httptree.js +1 -1
- package/src/protocols/package.js +15 -3
- package/src/runtime/AsyncCacheTransform.d.ts +5 -0
- package/src/runtime/AsyncCacheTransform.js +134 -0
- package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
- package/src/runtime/HandleExtensionsTransform.js +18 -2
- package/src/runtime/OrigamiFileMap.d.ts +5 -2
- package/src/runtime/OrigamiFileMap.js +27 -4
- package/src/runtime/ScopeMap.js +72 -0
- package/src/runtime/SyncCacheTransform.d.ts +8 -0
- package/src/runtime/SyncCacheTransform.js +133 -0
- package/src/runtime/SystemCacheMap.js +259 -0
- package/src/runtime/WatchFilesMixin.js +52 -19
- package/src/runtime/enableValueCaching.js +192 -0
- package/src/runtime/execute.js +2 -2
- package/src/runtime/executionContext.js +7 -0
- package/src/runtime/explainReferenceError.js +7 -2
- package/src/runtime/expressionObject.js +54 -46
- package/src/runtime/handleExtension.js +65 -34
- package/src/runtime/interop.js +2 -2
- package/src/runtime/mergeTrees.js +1 -1
- package/src/runtime/ops.js +28 -33
- package/src/runtime/symbols.js +3 -0
- package/src/runtime/systemCache.js +3 -0
- package/src/runtime/volatile.js +14 -0
- package/test/compiler/codeHelpers.js +2 -1
- package/test/compiler/optimize.test.js +62 -54
- package/test/handlers/ori_handler.test.js +22 -3
- package/test/handlers/oridocument_handler.test.js +1 -1
- package/test/protocols/https.test.js +19 -0
- package/test/protocols/package.test.js +7 -2
- package/test/runtime/AsyncCacheTransform.test.js +91 -0
- package/test/runtime/OrigamiFileMap.test.js +26 -23
- package/test/runtime/ScopeMap.test.js +49 -0
- package/test/runtime/SyncCacheTransform.test.js +93 -0
- package/test/runtime/SystemCacheMap.test.js +239 -0
- package/test/runtime/asyncCalcs.js +28 -0
- package/test/runtime/enableValueCaching.test.js +55 -0
- package/test/runtime/errors.test.js +53 -30
- package/test/runtime/evaluate.test.js +9 -4
- package/test/runtime/execute.test.js +6 -1
- package/test/runtime/expressionObject.test.js +55 -15
- package/test/runtime/fetchAndHandleExtension.test.js +24 -0
- package/test/runtime/fixtures/unpack/hello.json +1 -0
- package/test/runtime/handleExtension.test.js +12 -1
- package/test/runtime/ops.test.js +70 -65
- package/test/runtime/syncCalcs.js +27 -0
- package/test/runtime/systemCache.test.js +66 -0
- package/src/runtime/assignPropertyDescriptors.js +0 -23
- package/src/runtime/asyncStorage.js +0 -7
|
@@ -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 =
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { AsyncMap, isPlainObject, SyncMap, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import AsyncCacheTransform from "./AsyncCacheTransform.js";
|
|
3
|
+
import { cachePathSymbol } from "./symbols.js";
|
|
4
|
+
import SyncCacheTransform from "./SyncCacheTransform.js";
|
|
5
|
+
import systemCache from "./systemCache.js";
|
|
6
|
+
import SystemCacheMap from "./SystemCacheMap.js";
|
|
7
|
+
|
|
8
|
+
// For detecting async functions
|
|
9
|
+
const AsyncFunction = async function () {}.constructor;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Given a maplike object whose values can be cached, enable caching. This may
|
|
13
|
+
* apply a caching transform, and sets a cache path on the object so that it can
|
|
14
|
+
* use as the prefix for cache paths.
|
|
15
|
+
*
|
|
16
|
+
* Note: this typically destructively modifies the given value.
|
|
17
|
+
*
|
|
18
|
+
* @param {any} value
|
|
19
|
+
* @param {string} cachePath
|
|
20
|
+
*/
|
|
21
|
+
export default function enableValueCaching(value, cachePath) {
|
|
22
|
+
if (!(typeof value === "object" || typeof value === "function")) {
|
|
23
|
+
// Don't need to apply caching to primitive value
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cacheable =
|
|
28
|
+
value[cachePathSymbol] === undefined && Tree.isMaplike(value);
|
|
29
|
+
if (cacheable) {
|
|
30
|
+
if (isPlainObject(value)) {
|
|
31
|
+
// Expression objects do their own caching
|
|
32
|
+
// TODO: What if it's some other kind of plain object?
|
|
33
|
+
markCacheable(value, cachePath);
|
|
34
|
+
} else if (Array.isArray(value)) {
|
|
35
|
+
// Cache arrays
|
|
36
|
+
markCacheable(value, cachePath);
|
|
37
|
+
} else if (value instanceof Function) {
|
|
38
|
+
// Cache a function
|
|
39
|
+
value = cacheFunction(value, cachePath);
|
|
40
|
+
} else if (
|
|
41
|
+
isTransformApplied(SyncCacheTransform, value) ||
|
|
42
|
+
isTransformApplied(AsyncCacheTransform, value)
|
|
43
|
+
) {
|
|
44
|
+
// Already has caching transform applied; just mark cacheable
|
|
45
|
+
markCacheable(value, cachePath);
|
|
46
|
+
} else {
|
|
47
|
+
// Other maplike; convert to a Map/AsyncMap
|
|
48
|
+
value = Tree.from(value);
|
|
49
|
+
if (value instanceof Map) {
|
|
50
|
+
if (!(value instanceof SyncMap)) {
|
|
51
|
+
// Convert regular Map to SyncMap so we can extend it
|
|
52
|
+
value = new (SyncCacheTransform(SyncMap))(value);
|
|
53
|
+
} else {
|
|
54
|
+
// Cache a SyncMap
|
|
55
|
+
value = transformObject(SyncCacheTransform, value);
|
|
56
|
+
}
|
|
57
|
+
} else if (value instanceof AsyncMap) {
|
|
58
|
+
// Cache an AsyncMap
|
|
59
|
+
value = transformObject(AsyncCacheTransform, value);
|
|
60
|
+
}
|
|
61
|
+
markCacheable(value, cachePath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cache a function with arity 1 or greater that takes string arguments
|
|
70
|
+
*
|
|
71
|
+
* @param {Function} fn
|
|
72
|
+
* @param {string} cachePath
|
|
73
|
+
*/
|
|
74
|
+
export function cacheFunction(fn, cachePath) {
|
|
75
|
+
if (fn.length === 0) {
|
|
76
|
+
// Return as is
|
|
77
|
+
return fn;
|
|
78
|
+
}
|
|
79
|
+
let result;
|
|
80
|
+
if (fn instanceof AsyncFunction) {
|
|
81
|
+
// Return an async function that caches results for a unary argument
|
|
82
|
+
result = async (...args) => {
|
|
83
|
+
if (!allStringArguments(args)) {
|
|
84
|
+
// Run function in context of this cache path, but don't cache result
|
|
85
|
+
return systemCache.runInContextAsync(cachePath, () => fn(...args));
|
|
86
|
+
}
|
|
87
|
+
const keyCachePath = SystemCacheMap.joinPath(cachePath, args.join("/"));
|
|
88
|
+
let result = systemCache.getOrInsertComputedAsync(
|
|
89
|
+
keyCachePath,
|
|
90
|
+
async () => fn(...args),
|
|
91
|
+
);
|
|
92
|
+
result = enableValueCaching(result, keyCachePath);
|
|
93
|
+
return result;
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
// Return a sync function that caches results for a unary argument
|
|
97
|
+
result = (...args) => {
|
|
98
|
+
if (!allStringArguments(args)) {
|
|
99
|
+
// Run function in context of this cache path, but don't cache result
|
|
100
|
+
return systemCache.runInContext(cachePath, () => fn(...args));
|
|
101
|
+
}
|
|
102
|
+
const keyCachePath = SystemCacheMap.joinPath(cachePath, args.join("/"));
|
|
103
|
+
let result = systemCache.getOrInsertComputed(keyCachePath, () =>
|
|
104
|
+
fn(...args),
|
|
105
|
+
);
|
|
106
|
+
result = enableValueCaching(result, keyCachePath);
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
Object.defineProperty(result, "length", {
|
|
111
|
+
value: fn.length,
|
|
112
|
+
configurable: true,
|
|
113
|
+
});
|
|
114
|
+
markCacheable(result, cachePath);
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Non-string keys and non-empty strings can't be cached
|
|
119
|
+
function allStringArguments(args) {
|
|
120
|
+
return (
|
|
121
|
+
args.length > 0 &&
|
|
122
|
+
args.every((arg) => typeof arg === "string" && arg.length > 0)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isTransformApplied(Transform, obj) {
|
|
127
|
+
let transformName = Transform.name;
|
|
128
|
+
if (!transformName) {
|
|
129
|
+
throw `isTransformApplied was called on an unnamed transform function, but a name is required.`;
|
|
130
|
+
}
|
|
131
|
+
if (transformName.endsWith("Transform")) {
|
|
132
|
+
transformName = transformName.slice(0, -9);
|
|
133
|
+
}
|
|
134
|
+
// Walk up prototype chain looking for a constructor with the same name as the
|
|
135
|
+
// transform. This is not a great test.
|
|
136
|
+
for (let proto = obj; proto; proto = Object.getPrototypeOf(proto)) {
|
|
137
|
+
if (proto.constructor.name === transformName) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function markCacheable(object, cachePath) {
|
|
145
|
+
Object.defineProperty(object, cachePathSymbol, {
|
|
146
|
+
configurable: true,
|
|
147
|
+
enumerable: false,
|
|
148
|
+
value: cachePath,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Apply a functional class mixin to an individual object instance.
|
|
153
|
+
*
|
|
154
|
+
* This works by create an intermediate class, creating an instance of that, and
|
|
155
|
+
* then setting the intermediate class's prototype to the given individual
|
|
156
|
+
* object. The resulting, extended object is then returned.
|
|
157
|
+
*
|
|
158
|
+
* This manipulation of the prototype chain is generally sound in JavaScript,
|
|
159
|
+
* with some caveats. In particular, the original object class cannot make
|
|
160
|
+
* direct use of private members; JavaScript will complain if the extended
|
|
161
|
+
* object does anything that requires access to those private members.
|
|
162
|
+
*
|
|
163
|
+
* @param {Function} Transform
|
|
164
|
+
* @param {any} obj
|
|
165
|
+
*/
|
|
166
|
+
export function transformObject(Transform, obj) {
|
|
167
|
+
// Apply the mixin to Object and instantiate that. The Object base class here
|
|
168
|
+
// is going to be cut out of the prototype chain in a moment; we just use
|
|
169
|
+
// Object as a convenience because its constructor takes no arguments.
|
|
170
|
+
const mixed = new (Transform(Object))();
|
|
171
|
+
|
|
172
|
+
// Find the highest prototype in the chain that was added by the class mixin.
|
|
173
|
+
// The mixin may have added multiple prototypes to the chain. Walk up the
|
|
174
|
+
// prototype chain until we hit Object.
|
|
175
|
+
let mixinProto = Object.getPrototypeOf(mixed);
|
|
176
|
+
while (Object.getPrototypeOf(mixinProto) !== Object.prototype) {
|
|
177
|
+
mixinProto = Object.getPrototypeOf(mixinProto);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Redirect the prototype chain above the mixin to point to the original
|
|
181
|
+
// object. The mixed object now extends the original object with the mixin.
|
|
182
|
+
Object.setPrototypeOf(mixinProto, obj);
|
|
183
|
+
|
|
184
|
+
// Create a new constructor for this mixed object that reflects its prototype
|
|
185
|
+
// chain. Because we've already got the instance we want, we won't use this
|
|
186
|
+
// constructor now, but this can be used later to instantiate other objects
|
|
187
|
+
// that look like the mixed one.
|
|
188
|
+
mixed.constructor = Transform(obj.constructor);
|
|
189
|
+
|
|
190
|
+
// Return the mixed object.
|
|
191
|
+
return mixed;
|
|
192
|
+
}
|