@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.
- package/index.ts +1 -0
- package/main.js +7 -1
- package/package.json +7 -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/addExtensionKeyFn.js +18 -0
- package/src/handlers/epub_handler.js +54 -0
- package/src/handlers/getSource.js +11 -0
- package/src/handlers/handlers.js +2 -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 +19 -28
- 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/handlers/zip_handler.js +112 -0
- 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/epub_handler.test.js +27 -0
- package/test/handlers/fixtures/test.zip +0 -0
- package/test/handlers/ori_handler.test.js +22 -3
- package/test/handlers/oridocument_handler.test.js +1 -1
- package/test/handlers/zip_handler.test.js +45 -0
- 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
|
@@ -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
|
|
8
|
-
|
|
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,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
|
}
|