@weborigami/language 0.7.0-beta.2 → 0.7.0-beta.3

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 (36) hide show
  1. package/main.js +1 -0
  2. package/package.json +2 -2
  3. package/src/compiler/compile.js +0 -3
  4. package/src/compiler/optimize.js +56 -23
  5. package/src/compiler/parserHelpers.js +11 -8
  6. package/src/handlers/getSource.js +1 -0
  7. package/src/handlers/ori_handler.js +2 -2
  8. package/src/handlers/oridocument_handler.js +2 -2
  9. package/src/handlers/{processOriExport.js → processOrigamiExport.js} +1 -1
  10. package/src/runtime/AsyncCacheTransform.d.ts +5 -1
  11. package/src/runtime/AsyncCacheTransform.js +22 -25
  12. package/src/runtime/OrigamiFileMap.js +3 -3
  13. package/src/runtime/SyncCacheTransform.d.ts +2 -1
  14. package/src/runtime/SyncCacheTransform.js +31 -34
  15. package/src/runtime/SystemCacheMap.js +28 -17
  16. package/src/runtime/counters.js +3 -0
  17. package/src/runtime/enableValueCaching.js +8 -7
  18. package/src/runtime/errors.js +33 -1
  19. package/src/runtime/execute.js +3 -0
  20. package/src/runtime/explainReferenceError.js +11 -24
  21. package/src/runtime/explainTraverseError.js +15 -6
  22. package/src/runtime/expressionObject.js +53 -34
  23. package/src/runtime/handleExtension.js +18 -9
  24. package/src/runtime/ops.js +4 -3
  25. package/test/compiler/compile.test.js +0 -1
  26. package/test/compiler/optimize.test.js +60 -15
  27. package/test/compiler/parse.test.js +38 -9
  28. package/test/runtime/AsyncCacheTransform.test.js +9 -4
  29. package/test/runtime/SyncCacheTransform.test.js +4 -0
  30. package/test/runtime/asyncCalcs.js +26 -4
  31. package/test/runtime/errors.test.js +4 -4
  32. package/test/runtime/expressionObject.test.js +23 -12
  33. package/test/runtime/handleExtension.test.js +0 -2
  34. package/test/runtime/ops.test.js +5 -3
  35. package/test/runtime/syncCalcs.js +26 -3
  36. package/test/runtime/systemCache.test.js +4 -0
package/main.js CHANGED
@@ -15,6 +15,7 @@ export { default as projectRoot } from "./src/project/projectRoot.js";
15
15
  export { default as projectRootFromPath } from "./src/project/projectRootFromPath.js";
16
16
  export { default as fetchAndHandleExtension } from "./src/protocols/fetchAndHandleExtension.js";
17
17
  export * as Protocols from "./src/protocols/protocols.js";
18
+ export { default as counters } from "./src/runtime/counters.js";
18
19
  export { formatError, highlightError, lineInfo } from "./src/runtime/errors.js";
19
20
  export { default as evaluate } from "./src/runtime/evaluate.js";
20
21
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.7.0-beta.2",
3
+ "version": "0.7.0-beta.3",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,7 +11,7 @@
11
11
  "typescript": "6.0.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.7.0-beta.2",
14
+ "@weborigami/async-tree": "0.7.0-beta.3",
15
15
  "adm-zip": "0.5.17",
16
16
  "exif-parser": "0.1.12",
17
17
  "watcher": "2.3.1",
@@ -1,4 +1,3 @@
1
- import { trailingSlash } from "@weborigami/async-tree";
2
1
  import { createExpressionFunction } from "../runtime/expressionFunction.js";
3
2
  import optimize from "./optimize.js";
4
3
  import { parse } from "./parse.js";
@@ -33,13 +32,11 @@ function compile(source, options) {
33
32
 
34
33
  // Select a path the code will use for caching
35
34
  const cachePath = source.cachePath ?? `_compile${count++}`;
36
- const objectCachePath = cachePath ? trailingSlash.add(cachePath) : null;
37
35
 
38
36
  // Optimize the code
39
37
  const optimized = optimize(code, {
40
38
  cachePath,
41
39
  globals,
42
- objectCachePath,
43
40
  });
44
41
 
45
42
  // Create a function that executes the optimized code.
@@ -1,4 +1,4 @@
1
- import { pathFromKeys, trailingSlash } from "@weborigami/async-tree";
1
+ import { trailingSlash } from "@weborigami/async-tree";
2
2
  import jsGlobals from "../project/jsGlobals.js";
3
3
  import {
4
4
  KEY_TYPE,
@@ -31,9 +31,13 @@ export const REFERENCE_EXTERNAL = 4;
31
31
  * @returns {AnnotatedCode}
32
32
  */
33
33
  export default function optimize(code, options = {}) {
34
- // Cache path for this source file
35
- const cachePath = options.cachePath ?? null;
36
34
  const globals = options.globals ?? jsGlobals;
35
+ const cachePath = options.cachePath; // top-level cache path for this code
36
+ // Cache path for attached objects
37
+ const objectCachePath =
38
+ options.objectCachePath ??
39
+ (cachePath ? trailingSlash.add(cachePath) : null);
40
+ const attached = options.attached ?? true;
37
41
 
38
42
  // The locals is an array, one item for each function or object context that
39
43
  // has been entered. The array grows to the right. Array items are objects
@@ -66,7 +70,8 @@ export default function optimize(code, options = {}) {
66
70
  return inlineLiteral(code);
67
71
 
68
72
  case ops.object:
69
- const propertyNames = getPropertyNames(args);
73
+ const entries = args.slice(1);
74
+ const propertyNames = getPropertyNames(entries);
70
75
  locals.push({
71
76
  type: REFERENCE_INHERITED,
72
77
  names: propertyNames,
@@ -80,14 +85,24 @@ export default function optimize(code, options = {}) {
80
85
  if (op === ops.object) {
81
86
  if (index === 0) {
82
87
  return child; // return op as is
88
+ } else if (index === 1) {
89
+ // Replace null cache path with actual value
90
+ return objectCachePath;
83
91
  } else {
84
92
  // Object entry
85
93
  const [key, value] = child;
86
94
  const adjustedLocals = avoidLocalRecursion(locals, key);
87
95
  const isStringKey = typeof key === "string";
96
+ const cachePathKey = isStringKey
97
+ ? baseKeyName(key)
98
+ : `_arg${index - 2}`;
99
+ const childObjectCachePath = attached
100
+ ? SystemCacheMap.joinPath(objectCachePath, cachePathKey + "/")
101
+ : objectCachePath;
88
102
  const childOptions = {
89
103
  ...options,
90
104
  locals: adjustedLocals,
105
+ objectCachePath: childObjectCachePath,
91
106
  };
92
107
  const optimizedKey = isStringKey
93
108
  ? key
@@ -98,12 +113,30 @@ export default function optimize(code, options = {}) {
98
113
  );
99
114
  return annotate([optimizedKey, optimizedValue], child.location);
100
115
  }
116
+ } else if (op === ops.getter) {
117
+ if (index === 0) {
118
+ return child; // return op as is
119
+ } else {
120
+ // Already set up to optimize the (single) child
121
+ return optimize(/** @type {AnnotatedCode} */ (child), options);
122
+ }
101
123
  } else if (Array.isArray(child) && "location" in child) {
102
124
  // Review: Aside from ops.object (above), what non-instruction arrays
103
125
  // does this descend into?
104
- const childOptions = { ...options, locals };
105
- // Objects below here are detached from top-level object; not cached
106
- delete childOptions.objectCachePath;
126
+ let childObjectCachePath = objectCachePath;
127
+ if (attached) {
128
+ // Objects below this point are detached, use `_objects` cache path
129
+ childObjectCachePath = SystemCacheMap.joinPath(
130
+ objectCachePath,
131
+ `_objects/`,
132
+ );
133
+ }
134
+ const childOptions = {
135
+ ...options,
136
+ attached: false,
137
+ locals,
138
+ objectCachePath: childObjectCachePath,
139
+ };
107
140
  return optimize(/** @type {AnnotatedCode} */ (child), childOptions);
108
141
  } else {
109
142
  return child;
@@ -124,23 +157,20 @@ export default function optimize(code, options = {}) {
124
157
  // remove any local variable with that name from the stack of locals to avoid a
125
158
  // recursive reference.
126
159
  function avoidLocalRecursion(locals, key) {
127
- if (key[0] === "(" && key[key.length - 1] === ")") {
128
- // Non-enumerable property, remove parentheses
129
- key = key.slice(1, -1);
130
- }
131
-
132
160
  const currentFrameIndex = locals.length - 1;
133
161
  if (locals[currentFrameIndex]?.type !== REFERENCE_INHERITED) {
134
162
  // Not an inherited context, nothing to do
135
163
  return locals;
136
164
  }
137
165
 
166
+ const baseKey = baseKeyName(key);
167
+
138
168
  // See if the key matches any of the local variable names in the current
139
169
  // context (ignoring trailing slashes)
140
170
  const matchingKeyIndex = locals[currentFrameIndex].names.findIndex(
141
171
  (name) =>
142
172
  // Ignore trailing slashes when comparing keys
143
- trailingSlash.remove(name) === trailingSlash.remove(key),
173
+ trailingSlash.remove(name) === trailingSlash.remove(baseKey),
144
174
  );
145
175
 
146
176
  if (matchingKeyIndex >= 0) {
@@ -158,11 +188,12 @@ function avoidLocalRecursion(locals, key) {
158
188
  }
159
189
  }
160
190
 
161
- function cacheExternalPath(code, cachePath) {
162
- const keys = code.map(keyFromCode).filter((key) => key !== null);
163
- const keysPath = pathFromKeys(keys);
164
- const refCachePath = SystemCacheMap.joinPath(cachePath, "_refs", keysPath);
165
- return annotate([ops.cache, refCachePath, code], code.location);
191
+ function baseKeyName(key) {
192
+ if (key[0] === "(" && key[key.length - 1] === ")") {
193
+ // Non-enumerable property, remove parentheses
194
+ key = key.slice(1, -1);
195
+ }
196
+ return key;
166
197
  }
167
198
 
168
199
  // A reference with periods like x.y.z
@@ -366,6 +397,13 @@ function resolvePath(code, globals, locals, cachePath) {
366
397
 
367
398
  let { type, result } = reference(head, globals, locals);
368
399
 
400
+ if (type === REFERENCE_EXTERNAL && cachePath) {
401
+ // External references should be cached
402
+ const headKey = keyFromCode(head);
403
+ const refCachePath = SystemCacheMap.joinPath(cachePath, "_refs", headKey);
404
+ result = annotate([ops.cache, refCachePath, result], head.location);
405
+ }
406
+
369
407
  if (tail.length > 0) {
370
408
  // If the result is a traversal, we can safely extend it
371
409
  const extendResult =
@@ -383,10 +421,5 @@ function resolvePath(code, globals, locals, cachePath) {
383
421
  }
384
422
  }
385
423
 
386
- if (type === REFERENCE_EXTERNAL && cachePath) {
387
- // External references should be cached
388
- return cacheExternalPath(result, cachePath);
389
- }
390
-
391
424
  return result;
392
425
  }
@@ -272,7 +272,7 @@ export function makeDocument(front, body, location) {
272
272
  entries.push(annotate(["_body", body], location));
273
273
 
274
274
  // Return the code for the document object
275
- return annotate([ops.object, ...entries], location);
275
+ return annotate([ops.object, null, ...entries], location);
276
276
  }
277
277
 
278
278
  /**
@@ -341,7 +341,7 @@ function makeMerge(spreads, location) {
341
341
 
342
342
  for (const spread of spreads) {
343
343
  if (spread[0] === ops.object) {
344
- const spreadEntries = spread.slice(1);
344
+ const spreadEntries = spread.slice(2);
345
345
  topEntries.push(...spreadEntries);
346
346
  // Also add an object to the result with indirect references
347
347
  const indirectEntries = spreadEntries.map((entry) => {
@@ -352,7 +352,7 @@ function makeMerge(spreads, location) {
352
352
  return annotate([key, getter], entry.location);
353
353
  });
354
354
  const indirectObject = annotate(
355
- [ops.object, ...indirectEntries],
355
+ [ops.object, null, ...indirectEntries],
356
356
  location,
357
357
  );
358
358
  resultEntries.push(indirectObject);
@@ -368,7 +368,7 @@ function makeMerge(spreads, location) {
368
368
  topEntries.push(annotate(["_result", result], location));
369
369
 
370
370
  // Construct the top-level object
371
- const topObject = annotate([ops.object, ...topEntries], location);
371
+ const topObject = annotate([ops.object, null, ...topEntries], location);
372
372
 
373
373
  // Get the _result property
374
374
  const code = annotate([topObject, "_result"], location);
@@ -390,14 +390,17 @@ export function makeObject(entries, location) {
390
390
  if (key === markers.spread) {
391
391
  if (value[0] === ops.object) {
392
392
  // Spread of an object; fold into current object
393
- const spreadEntries = value.slice(1);
393
+ const spreadEntries = value.slice(2);
394
394
  currentEntries.push(...spreadEntries);
395
395
  } else {
396
396
  // Spread of a tree; accumulate
397
397
  if (currentEntries.length > 0) {
398
398
  const location = { ...currentEntries[0].location };
399
399
  location.end = currentEntries[currentEntries.length - 1].location.end;
400
- const spread = annotate([ops.object, ...currentEntries], location);
400
+ const spread = annotate(
401
+ [ops.object, null, ...currentEntries],
402
+ location,
403
+ );
401
404
  spreads.push(spread);
402
405
  currentEntries = [];
403
406
  }
@@ -424,7 +427,7 @@ export function makeObject(entries, location) {
424
427
  if (currentEntries.length > 0) {
425
428
  const location = { ...currentEntries[0].location };
426
429
  location.end = currentEntries[currentEntries.length - 1].location.end;
427
- const spread = annotate([ops.object, ...currentEntries], location);
430
+ const spread = annotate([ops.object, null, ...currentEntries], location);
428
431
  spreads.push(spread);
429
432
  currentEntries = [];
430
433
  }
@@ -442,7 +445,7 @@ export function makeObject(entries, location) {
442
445
  }
443
446
  } else {
444
447
  // Empty object
445
- code = [ops.object];
448
+ code = [ops.object, null];
446
449
  }
447
450
 
448
451
  return annotate(code, location);
@@ -30,6 +30,7 @@ export default function getSource(packed, options = {}) {
30
30
  const parentPath = /** @type {any} */ (parent).path;
31
31
  cachePath = SystemCacheMap.joinPath(parentPath, sourceName);
32
32
  } else {
33
+ // TODO: Avoid cache path collisions in this case
33
34
  cachePath = sourceName;
34
35
  }
35
36
  }
@@ -3,7 +3,7 @@ import * as compile from "../compiler/compile.js";
3
3
  import coreGlobals from "../project/coreGlobals.js";
4
4
  import getGlobalsForTree from "../project/getGlobalsForTree.js";
5
5
  import getSource from "./getSource.js";
6
- import processOriExport from "./processOriExport.js";
6
+ import processOrigamiExport from "./processOrigamiExport.js";
7
7
 
8
8
  /**
9
9
  * An Origami expression file
@@ -31,7 +31,7 @@ export default {
31
31
  // Evaluate the program
32
32
  let result = await fn();
33
33
 
34
- result = processOriExport(result, source, parent);
34
+ result = processOrigamiExport(result, source, parent);
35
35
 
36
36
  return result;
37
37
  },
@@ -4,7 +4,7 @@ import coreGlobals from "../project/coreGlobals.js";
4
4
  import getGlobalsForTree from "../project/getGlobalsForTree.js";
5
5
  import addExtensionKeyFn from "./addExtensionKeyFn.js";
6
6
  import getSource from "./getSource.js";
7
- import processOriExport from "./processOriExport.js";
7
+ import processOrigamiExport from "./processOrigamiExport.js";
8
8
 
9
9
  /**
10
10
  * An Origami template document: a plain text file that contains Origami
@@ -31,7 +31,7 @@ export default {
31
31
  // Invoke the definition to get back the template function or object
32
32
  let result = await fn();
33
33
 
34
- result = processOriExport(result, source, parent);
34
+ result = processOrigamiExport(result, source, parent);
35
35
 
36
36
  if (result instanceof Function) {
37
37
  const key = options.key;
@@ -5,7 +5,7 @@ import enableValueCaching from "../runtime/enableValueCaching.js";
5
5
  * Given an object that's the top-level result of an Origami file, perform any
6
6
  * necessary processing.
7
7
  */
8
- export default function processOriExport(value, source, parent) {
8
+ export default function processOrigamiExport(value, source, parent) {
9
9
  setParent(value, parent);
10
10
 
11
11
  if (source.cachePath) {
@@ -1,5 +1,9 @@
1
1
  import { Mixin } from "../../index.ts";
2
2
 
3
- declare const AsyncCacheTransform: Mixin<{}>
3
+ declare const AsyncCacheTransform: Mixin<{
4
+ cachePathForKey(key: string): string;
5
+ onKeysChange(key: string): void;
6
+ onValueChange(key: string): void;
7
+ }>
4
8
 
5
9
  export default AsyncCacheTransform;
@@ -45,24 +45,21 @@ export default function AsyncCacheTransform(Base) {
45
45
  this.cache = systemCache;
46
46
  }
47
47
 
48
- get cachePath() {
49
- // @ts-ignore
50
- return this[cachePathSymbol];
51
- }
52
-
53
48
  cachePathForKey(key) {
54
49
  return key === "."
55
- ? this.cachePath
56
- : SystemCacheMap.joinPath(this.cachePath, key);
50
+ ? // @ts-ignore
51
+ this[cachePathSymbol]
52
+ : // @ts-ignore
53
+ SystemCacheMap.joinPath(this[cachePathSymbol], key);
57
54
  }
58
55
 
59
- async delete(key) {
60
- const deleted = await super.delete(key);
61
- if (typeof key === "string") {
62
- systemCache.delete(this.cachePathForKey(key));
63
- }
64
- return deleted;
65
- }
56
+ // async delete(key) {
57
+ // const deleted = await super.delete(key);
58
+ // if (typeof key === "string") {
59
+ // systemCache.delete(this.cachePathForKey(key));
60
+ // }
61
+ // return deleted;
62
+ // }
66
63
 
67
64
  async get(key) {
68
65
  if (typeof key !== "string" || key.length === 0) {
@@ -119,16 +116,16 @@ export default function AsyncCacheTransform(Base) {
119
116
  systemCache.delete(this.cachePathForKey(key));
120
117
  }
121
118
 
122
- async set(key, value) {
123
- if (typeof key !== "string") {
124
- return super.set(key, value);
125
- }
126
- systemCache.delete(this.cachePathForKey(key));
127
- if (!this.has(key)) {
128
- // Adding a new key, need to invalidate cached keys
129
- this.invalidateKeys();
130
- }
131
- super.set(key, value);
132
- }
119
+ // async set(key, value) {
120
+ // if (typeof key !== "string") {
121
+ // return super.set(key, value);
122
+ // }
123
+ // systemCache.delete(this.cachePathForKey(key));
124
+ // if (!this.has(key)) {
125
+ // // Adding a new key, need to invalidate cached keys
126
+ // this.invalidateKeys();
127
+ // }
128
+ // super.set(key, value);
129
+ // }
133
130
  };
134
131
  }
@@ -4,15 +4,15 @@ import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
4
4
  import ImportModulesMixin from "./ImportModulesMixin.js";
5
5
  import SyncCacheTransform from "./SyncCacheTransform.js";
6
6
  import WatchFilesMixin from "./WatchFilesMixin.js";
7
- import { noCacheSymbol } from "./symbols.js";
7
+ import { cachePathSymbol, noCacheSymbol } from "./symbols.js";
8
8
 
9
9
  export default class OrigamiFileMap extends SyncCacheTransform(
10
10
  HandleExtensionsTransform(
11
11
  ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap))),
12
12
  ),
13
13
  ) {
14
- get cachePath() {
15
- return super.cachePath ?? this.path;
14
+ get [cachePathSymbol]() {
15
+ return this.path;
16
16
  }
17
17
 
18
18
  // Workaround to register file paths in the system cache without trailing
@@ -1,8 +1,9 @@
1
1
  import { Mixin } from "../../index.ts";
2
2
 
3
3
  declare const SyncCacheTransform: Mixin<{
4
- get cachePath(): string;
5
4
  cachePathForKey(key: string): string;
5
+ onKeysChange(key: string): void;
6
+ onValueChange(key: string): void;
6
7
  }>
7
8
 
8
9
  export default SyncCacheTransform;
@@ -45,28 +45,25 @@ export default function SyncCacheTransform(Base) {
45
45
  this.cache = systemCache;
46
46
  }
47
47
 
48
- get cachePath() {
49
- // @ts-ignore
50
- return this[cachePathSymbol];
51
- }
52
-
53
48
  cachePathForKey(key) {
54
49
  return key === "."
55
- ? this.cachePath
56
- : SystemCacheMap.joinPath(this.cachePath, key);
50
+ ? // @ts-ignore
51
+ this[cachePathSymbol]
52
+ : // @ts-ignore
53
+ SystemCacheMap.joinPath(this[cachePathSymbol], key);
57
54
  }
58
55
 
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
- }
56
+ // delete(key) {
57
+ // const deleted = super.delete(key);
58
+ // if (typeof key === "string") {
59
+ // systemCache.delete(this.cachePathForKey(key));
60
+ // if (deleted) {
61
+ // // Deleted an existing key, need to invalidate cached keys
62
+ // this.invalidateKeys();
63
+ // }
64
+ // }
65
+ // return deleted;
66
+ // }
70
67
 
71
68
  get(key) {
72
69
  if (typeof key !== "string" || key.length === 0) {
@@ -113,21 +110,21 @@ export default function SyncCacheTransform(Base) {
113
110
  systemCache.delete(this.cachePathForKey(key));
114
111
  }
115
112
 
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
- }
113
+ // set(key, value) {
114
+ // if (!this._self) {
115
+ // // Initializing in constructor
116
+ // super.set(key, value);
117
+ // return;
118
+ // }
119
+ // if (typeof key !== "string") {
120
+ // return super.set(key, value);
121
+ // }
122
+ // systemCache.delete(this.cachePathForKey(key));
123
+ // if (!this.has(key)) {
124
+ // // Adding a new key, need to invalidate cached keys
125
+ // this.invalidateKeys();
126
+ // }
127
+ // super.set(key, value);
128
+ // }
132
129
  };
133
130
  }
@@ -28,15 +28,16 @@ let nextPathId = 0;
28
28
  export default class SystemCacheMap extends SyncMap {
29
29
  delete(path) {
30
30
  // Find all entries that depend on this path directly or indirectly
31
- const toDelete = this.dependentEntries(path);
31
+ const dependentEntries = new Map();
32
+ this.visitDependentEntries(path, dependentEntries);
32
33
 
33
34
  // Delete those dependent entries
34
- for (const deletePath of toDelete.keys()) {
35
+ for (const deletePath of dependentEntries.keys()) {
35
36
  super.delete(deletePath);
36
37
  }
37
38
 
38
39
  // Remove deleted entries as being downstream from still-existing entries
39
- for (const [deletePath, deleteEntry] of toDelete.entries()) {
40
+ for (const [deletePath, deleteEntry] of dependentEntries.entries()) {
40
41
  for (const upstreamPath of deleteEntry.upstreams ?? []) {
41
42
  const upstreamEntry = this.get(upstreamPath);
42
43
  if (upstreamEntry?.downstreams) {
@@ -44,6 +45,14 @@ export default class SystemCacheMap extends SyncMap {
44
45
  if (upstreamEntry.downstreams.size === 0) {
45
46
  // No more downstream dependencies, clean up entry
46
47
  delete upstreamEntry.downstreams;
48
+ if (upstreamEntry.upstreams?.size === 0) {
49
+ // No more upstream dependencies either, clean up entry
50
+ delete upstreamEntry.upstreams;
51
+ if (!("value" in upstreamEntry)) {
52
+ // No more data in this entry, delete it
53
+ super.delete(upstreamPath);
54
+ }
55
+ }
47
56
  }
48
57
  }
49
58
  }
@@ -53,36 +62,38 @@ export default class SystemCacheMap extends SyncMap {
53
62
  }
54
63
 
55
64
  // Return all entries that directly or indirectly depend on the given path
56
- dependentEntries(path) {
57
- const result = new Map();
65
+ visitDependentEntries(path, dependentEntries) {
66
+ // The entries that will be added for the given path
67
+ const pathAdditions = new Map();
58
68
 
59
69
  const entry = this.get(path);
60
70
  if (entry) {
61
71
  // Path itself has an entry
62
- result.set(path, entry);
72
+ pathAdditions.set(path, entry);
63
73
  }
64
74
 
65
75
  // Add all entries with child paths that implicitly depend on this entry
76
+ // REVIEW: This is expensive: it iterates through the entire cache, and is
77
+ // done for each node visit.
66
78
  for (const [otherPath, otherEntry] of this.entries()) {
67
79
  if (this.isChildPath(path, otherPath)) {
68
- result.set(otherPath, otherEntry);
80
+ pathAdditions.set(otherPath, otherEntry);
69
81
  }
70
82
  }
71
83
 
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
- }
84
+ // Add all the direct dependencies
85
+ for (const [dependentPath, dependentEntry] of pathAdditions.entries()) {
86
+ if (!dependentEntries.has(dependentPath)) {
87
+ // Didn't already have this path in the result set
88
+ dependentEntries.set(dependentPath, dependentEntry);
89
+ // Visit all entries downstream of this one
90
+ for (const downstreamPath of dependentEntry.downstreams ?? []) {
91
+ if (!dependentEntries.has(downstreamPath)) {
92
+ this.visitDependentEntries(downstreamPath, dependentEntries);
80
93
  }
81
94
  }
82
95
  }
83
96
  }
84
-
85
- return result;
86
97
  }
87
98
 
88
99
  // REVIEW: This doesn't have the correct signature for getOrInsertComputed,
@@ -0,0 +1,3 @@
1
+ export default {
2
+ executions: 0,
3
+ };
@@ -19,14 +19,15 @@ const AsyncFunction = async function () {}.constructor;
19
19
  * @param {string} cachePath
20
20
  */
21
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
- }
22
+ // Value must be a defined object or function, and not have already have a
23
+ // cache path, and be maplike.
24
+ const cachable =
25
+ value &&
26
+ (typeof value === "object" || typeof value === "function") &&
27
+ value[cachePathSymbol] === undefined &&
28
+ Tree.isMaplike(value);
26
29
 
27
- const cacheable =
28
- value[cachePathSymbol] === undefined && Tree.isMaplike(value);
29
- if (cacheable) {
30
+ if (cachable) {
30
31
  if (isPlainObject(value)) {
31
32
  // Expression objects do their own caching
32
33
  // TODO: What if it's some other kind of plain object?