@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.
Files changed (83) hide show
  1. package/index.ts +1 -0
  2. package/main.js +7 -1
  3. package/package.json +6 -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/getSource.js +11 -0
  9. package/src/handlers/htm_handler.js +1 -1
  10. package/src/handlers/js_handler.js +13 -4
  11. package/src/handlers/mediaTypeExtensions.json +15 -0
  12. package/src/handlers/ori_handler.js +8 -7
  13. package/src/handlers/oridocument_handler.js +17 -10
  14. package/src/handlers/processOriExport.js +17 -0
  15. package/src/handlers/tsv_handler.js +1 -1
  16. package/src/handlers/txt_handler.js +4 -2
  17. package/src/handlers/xhtml_handler.js +1 -1
  18. package/src/handlers/yaml_handler.js +6 -3
  19. package/src/project/activeProjectRoot.js +9 -0
  20. package/src/project/getGlobalsForTree.js +5 -0
  21. package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
  22. package/src/project/jsGlobals.js +1 -0
  23. package/src/project/projectConfig.js +2 -2
  24. package/src/project/projectRootFromPath.js +2 -0
  25. package/src/protocols/constructHref.js +3 -3
  26. package/src/protocols/constructSiteTree.js +11 -2
  27. package/src/protocols/explore.js +1 -1
  28. package/src/protocols/explorehttp.js +1 -1
  29. package/src/protocols/fetchAndHandleExtension.js +23 -11
  30. package/src/protocols/files.js +1 -0
  31. package/src/protocols/http.js +4 -1
  32. package/src/protocols/https.js +4 -1
  33. package/src/protocols/httpstree.js +1 -1
  34. package/src/protocols/httptree.js +1 -1
  35. package/src/protocols/package.js +15 -3
  36. package/src/runtime/AsyncCacheTransform.d.ts +5 -0
  37. package/src/runtime/AsyncCacheTransform.js +134 -0
  38. package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
  39. package/src/runtime/HandleExtensionsTransform.js +18 -2
  40. package/src/runtime/OrigamiFileMap.d.ts +5 -2
  41. package/src/runtime/OrigamiFileMap.js +27 -4
  42. package/src/runtime/ScopeMap.js +72 -0
  43. package/src/runtime/SyncCacheTransform.d.ts +8 -0
  44. package/src/runtime/SyncCacheTransform.js +133 -0
  45. package/src/runtime/SystemCacheMap.js +259 -0
  46. package/src/runtime/WatchFilesMixin.js +52 -19
  47. package/src/runtime/enableValueCaching.js +192 -0
  48. package/src/runtime/execute.js +2 -2
  49. package/src/runtime/executionContext.js +7 -0
  50. package/src/runtime/explainReferenceError.js +7 -2
  51. package/src/runtime/expressionObject.js +54 -46
  52. package/src/runtime/handleExtension.js +65 -34
  53. package/src/runtime/interop.js +2 -2
  54. package/src/runtime/mergeTrees.js +1 -1
  55. package/src/runtime/ops.js +28 -33
  56. package/src/runtime/symbols.js +3 -0
  57. package/src/runtime/systemCache.js +3 -0
  58. package/src/runtime/volatile.js +14 -0
  59. package/test/compiler/codeHelpers.js +2 -1
  60. package/test/compiler/optimize.test.js +62 -54
  61. package/test/handlers/ori_handler.test.js +22 -3
  62. package/test/handlers/oridocument_handler.test.js +1 -1
  63. package/test/protocols/https.test.js +19 -0
  64. package/test/protocols/package.test.js +7 -2
  65. package/test/runtime/AsyncCacheTransform.test.js +91 -0
  66. package/test/runtime/OrigamiFileMap.test.js +26 -23
  67. package/test/runtime/ScopeMap.test.js +49 -0
  68. package/test/runtime/SyncCacheTransform.test.js +93 -0
  69. package/test/runtime/SystemCacheMap.test.js +239 -0
  70. package/test/runtime/asyncCalcs.js +28 -0
  71. package/test/runtime/enableValueCaching.test.js +55 -0
  72. package/test/runtime/errors.test.js +53 -30
  73. package/test/runtime/evaluate.test.js +9 -4
  74. package/test/runtime/execute.test.js +6 -1
  75. package/test/runtime/expressionObject.test.js +55 -15
  76. package/test/runtime/fetchAndHandleExtension.test.js +24 -0
  77. package/test/runtime/fixtures/unpack/hello.json +1 -0
  78. package/test/runtime/handleExtension.test.js +12 -1
  79. package/test/runtime/ops.test.js +70 -65
  80. package/test/runtime/syncCalcs.js +27 -0
  81. package/test/runtime/systemCache.test.js +66 -0
  82. package/src/runtime/assignPropertyDescriptors.js +0 -23
  83. package/src/runtime/asyncStorage.js +0 -7
@@ -46,15 +46,7 @@ describe("optimize", () => {
46
46
  });
47
47
 
48
48
  test("resolve deeper context references", () => {
49
- // Compilation of `{ a: 1, more: { a } }`
50
- const code = createCode([
51
- ops.object,
52
- ["a", [ops.literal, 1]],
53
- [
54
- "more",
55
- [ops.object, ["a", [markers.traverse, [markers.reference, "a"]]]],
56
- ],
57
- ]);
49
+ const expression = `{ a: 1, more: { a } }`;
58
50
  const expected = [
59
51
  ops.object,
60
52
  ["a", 1],
@@ -72,7 +64,7 @@ describe("optimize", () => {
72
64
  ],
73
65
  ],
74
66
  ];
75
- assertCodeEqual(optimize(code), expected);
67
+ assertCompile(expression, expected);
76
68
  });
77
69
 
78
70
  test("when defining a property, avoid recursive references", () => {
@@ -103,7 +95,7 @@ describe("optimize", () => {
103
95
  assertCompile(expression, expected, "shell");
104
96
  });
105
97
 
106
- describe("resolve reference", () => {
98
+ describe.only("resolve reference", () => {
107
99
  test("external reference", () => {
108
100
  // Compilation of `folder` where folder isn't a variable
109
101
  const code = createCode([
@@ -113,35 +105,36 @@ describe("optimize", () => {
113
105
  const parent = {};
114
106
  const expected = [
115
107
  ops.cache,
116
- {},
117
- "folder",
118
- [
119
- [ops.scope, parent],
120
- [ops.literal, "folder"],
121
- ],
108
+ "test.ori/_refs/folder",
109
+ [[ops.scope], [ops.literal, "folder"]],
122
110
  ];
123
111
  const globals = {};
124
- assertCodeEqual(optimize(code, { globals, parent }), expected);
112
+ assertCodeEqual(
113
+ optimize(code, { cachePath: "test.ori", globals, parent }),
114
+ expected,
115
+ );
125
116
  });
126
117
 
127
- test("external reference", () => {
128
- // Compilation of `index.html` where `index` isn't a variable
118
+ test("external reference with implied unpack", () => {
119
+ // Compilation of `greet.ori("world")`
129
120
  const code = createCode([
130
- markers.traverse,
131
- [markers.reference, "index.html"],
121
+ [markers.traverse, [markers.reference, "greet.ori"]],
122
+ [ops.literal, "world"],
132
123
  ]);
133
124
  const parent = {};
134
125
  const expected = [
135
- ops.cache,
136
- {},
137
- "index.html",
138
126
  [
139
- [ops.scope, parent],
140
- [ops.literal, "index.html"],
127
+ ops.cache,
128
+ "test.ori/_refs/greet.ori/",
129
+ [ops.unpack, [[ops.scope], [ops.literal, "greet.ori"]]],
141
130
  ],
131
+ "world",
142
132
  ];
143
133
  const globals = {};
144
- assertCodeEqual(optimize(code, { globals, parent }), expected);
134
+ assertCodeEqual(
135
+ optimize(code, { cachePath: "test.ori", globals, parent }),
136
+ expected,
137
+ );
145
138
  });
146
139
 
147
140
  test("external reference inside object with matching key", () => {
@@ -162,18 +155,17 @@ describe("optimize", () => {
162
155
  ops.getter,
163
156
  [
164
157
  ops.cache,
165
- {},
166
- "posts.txt",
167
- [
168
- [ops.scope, parent],
169
- [ops.literal, "posts.txt"],
170
- ],
158
+ "test.ori/_refs/posts.txt",
159
+ [[ops.scope], [ops.literal, "posts.txt"]],
171
160
  ],
172
161
  ],
173
162
  ],
174
163
  ];
175
164
  const globals = {};
176
- assertCodeEqual(optimize(code, { globals, parent }), expected);
165
+ assertCodeEqual(
166
+ optimize(code, { cachePath: "test.ori", globals, parent }),
167
+ expected,
168
+ );
177
169
  });
178
170
 
179
171
  test("global reference", () => {
@@ -233,17 +225,26 @@ describe("optimize", () => {
233
225
  });
234
226
 
235
227
  test("root directory", () => {
236
- // Compilation of `</>`
237
- const code = createCode([markers.traverse, [markers.external, "/"]]);
238
- const expected = [ops.cache, {}, "/", [ops.rootDirectory]];
239
- assertCodeEqual(optimize(code), expected);
228
+ // Compilation of `</etc/passwd>`
229
+ const code = createCode([
230
+ markers.traverse,
231
+ [markers.external, "/"],
232
+ [ops.literal, "etc/"],
233
+ [ops.literal, "passwd"],
234
+ ]);
235
+ const expected = [
236
+ ops.cache,
237
+ "test.ori/_refs//etc/passwd",
238
+ [[ops.rootDirectory], [ops.literal, "etc/"], [ops.literal, "passwd"]],
239
+ ];
240
+ assertCodeEqual(optimize(code, { cachePath: "test.ori" }), expected);
240
241
  });
241
242
 
242
243
  test("home directory", () => {
243
244
  // Compilation of `<~>`
244
245
  const code = createCode([markers.traverse, [ops.homeDirectory]]);
245
- const expected = [ops.cache, {}, "~", [ops.homeDirectory]];
246
- assertCodeEqual(optimize(code), expected);
246
+ const expected = [ops.cache, "test.ori/_refs/~", [ops.homeDirectory]];
247
+ assertCodeEqual(optimize(code, { cachePath: "test.ori" }), expected);
247
248
  });
248
249
  });
249
250
 
@@ -259,16 +260,18 @@ describe("optimize", () => {
259
260
  const parent = {};
260
261
  const expected = [
261
262
  ops.cache,
262
- {},
263
- "path/to/file",
263
+ "test.ori/_refs/path/to/file",
264
264
  [
265
- [ops.scope, parent],
265
+ [ops.scope],
266
266
  [ops.literal, "path/"],
267
267
  [ops.literal, "to/"],
268
268
  [ops.literal, "file"],
269
269
  ],
270
270
  ];
271
- assertCodeEqual(optimize(code, { parent }), expected);
271
+ assertCodeEqual(
272
+ optimize(code, { cachePath: "test.ori", parent }),
273
+ expected,
274
+ );
272
275
  });
273
276
 
274
277
  test("implicit external path", () => {
@@ -282,15 +285,13 @@ describe("optimize", () => {
282
285
  const parent = {};
283
286
  const expected = [
284
287
  ops.cache,
285
- {},
286
- "package.json/name",
287
- [
288
- [ops.scope, parent],
289
- [ops.literal, "package.json/"],
290
- [ops.literal, "name"],
291
- ],
288
+ "test.ori/_refs/package.json/name",
289
+ [[ops.scope], [ops.literal, "package.json/"], [ops.literal, "name"]],
292
290
  ];
293
- assertCodeEqual(optimize(code, { globals, parent }), expected);
291
+ assertCodeEqual(
292
+ optimize(code, { cachePath: "test.ori", globals, parent }),
293
+ expected,
294
+ );
294
295
  });
295
296
 
296
297
  test("local path", () => {
@@ -314,9 +315,16 @@ describe("optimize", () => {
314
315
  });
315
316
 
316
317
  function assertCompile(expression, expected, mode = "shell") {
318
+ const source =
319
+ typeof expression !== "string"
320
+ ? expression
321
+ : {
322
+ text: expression,
323
+ relativePath: "test.ori",
324
+ };
317
325
  const parent = new SyncMap();
318
326
  const globals = {};
319
- const fn = compile.expression(expression, { globals, mode, parent });
327
+ const fn = compile.expression(source, { globals, mode, parent });
320
328
  const actual = fn.code;
321
329
  assertCodeLocations(actual);
322
330
  assertCodeEqual(actual, expected);
@@ -3,9 +3,11 @@ import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
  import ori_handler from "../../src/handlers/ori_handler.js";
5
5
  import OrigamiFileMap from "../../src/runtime/OrigamiFileMap.js";
6
+ import { cachePathSymbol } from "../../src/runtime/symbols.js";
6
7
 
7
8
  const fixturesUrl = new URL("fixtures", import.meta.url);
8
9
  const fixtures = new OrigamiFileMap(fixturesUrl);
10
+ await fixtures.initializeGlobals();
9
11
 
10
12
  describe(".ori handler", async () => {
11
13
  test("loads a string expression", async () => {
@@ -38,16 +40,18 @@ describe(".ori handler", async () => {
38
40
  const tree = await ori_handler.unpack(source);
39
41
  assert.deepEqual(
40
42
  await Tree.traverse(tree, "public", "message"),
41
- "Hello, world!"
43
+ "Hello, world!",
42
44
  );
43
45
  });
44
46
 
45
47
  test("loads an object containing an object shorthand", async () => {
46
- const assets = new ObjectMap({});
48
+ const assets = new ObjectMap({ a: 1, b: 2 });
47
49
  const parent = new ObjectMap({ assets });
48
50
  const source = `{ assets }`;
49
51
  const object = await ori_handler.unpack(source, { parent });
50
- assert.equal(object.assets, assets);
52
+ const expectedValues = Array.from(assets.values());
53
+ const actualValues = Array.from(object.assets.values());
54
+ assert.deepEqual(actualValues, expectedValues);
51
55
  });
52
56
 
53
57
  test("loads a template literal", async () => {
@@ -84,4 +88,19 @@ describe(".ori handler", async () => {
84
88
  const indexHtml = await tree["index.html"];
85
89
  assert.equal(indexHtml, "Hello, world!");
86
90
  });
91
+
92
+ test("enables caching for top-level object", async () => {
93
+ const parent = new ObjectMap({});
94
+ const source = `{
95
+ message = "Hello"
96
+ }`;
97
+ const object = await ori_handler.unpack(source, {
98
+ key: "test.ori",
99
+ parent,
100
+ });
101
+ assert.equal(object[cachePathSymbol], "test.ori/");
102
+ assert.deepEqual(await Tree.plain(object), {
103
+ message: "Hello",
104
+ });
105
+ });
87
106
  });
@@ -22,7 +22,7 @@ describe("Origami document handler", () => {
22
22
 
23
23
  test("Argument to template document available as underscore", async () => {
24
24
  const text = "<h1>${ _ }</h1>";
25
- const fn = await oridocument_handler.unpack(text);
25
+ const fn = await oridocument_handler.unpack(text, { key: "test.ori.html" });
26
26
  const result = await fn("Home");
27
27
  assert.equal(result, "<h1>Home</h1>");
28
28
  });
@@ -0,0 +1,19 @@
1
+ import assert from "node:assert";
2
+ import { beforeEach, describe, test } from "node:test";
3
+ import https from "../../src/protocols/https.js";
4
+ import systemCache from "../../src/runtime/systemCache.js";
5
+
6
+ describe("https", () => {
7
+ beforeEach(() => {
8
+ systemCache.clear();
9
+ });
10
+
11
+ test("caches fetched resources", async () => {
12
+ const result1 = await https("example.com", {});
13
+ assert(systemCache.has("https://example.com"));
14
+ const text = new TextDecoder().decode(result1);
15
+ assert(text.includes("Example Domain"));
16
+ const result2 = await https("example.com", {});
17
+ assert.strictEqual(result1, result2);
18
+ });
19
+ });
@@ -1,16 +1,21 @@
1
1
  import assert from "node:assert";
2
2
  import process from "node:process";
3
3
  import { describe, test } from "node:test";
4
+ import coreGlobals from "../../src/project/coreGlobals.js";
4
5
  import projectRootFromPath from "../../src/project/projectRootFromPath.js";
5
6
  import packageProtocol from "../../src/protocols/package.js";
6
7
 
7
8
  describe("package: protocol", () => {
8
9
  test("returns a package's main export(s)", async () => {
9
10
  // Reproduce the type of evaluation context object the runtime would create
10
- const projectRoot = await projectRootFromPath(process.cwd());
11
- const state = { parent: projectRoot };
11
+ const parent = await projectRootFromPath(process.cwd());
12
+ const globals = await coreGlobals();
13
+ /** @type {any} */ (parent).globals = globals;
14
+ const state = { globals, parent };
12
15
 
13
16
  const result = await packageProtocol("@weborigami", "async-tree", state);
17
+
18
+ // Try a method from the package
14
19
  const { toString } = result;
15
20
  assert.equal(toString(123), "123");
16
21
  });
@@ -0,0 +1,91 @@
1
+ import assert from "node:assert";
2
+ import { beforeEach, describe, test } from "node:test";
3
+ import systemCache from "../../src/runtime/systemCache.js";
4
+
5
+ // We test AsyncCacheTransform via asyncCalcs since it's a fairly small
6
+ // application of the transform.
7
+ import { Tree } from "@weborigami/async-tree";
8
+ import asyncCalcs from "./asyncCalcs.js";
9
+
10
+ describe("AsyncCacheTransform", () => {
11
+ beforeEach(() => {
12
+ systemCache.clear();
13
+ });
14
+
15
+ test("caches values and records dependencies", async () => {
16
+ // { a = 2 * b, b = c + 1, c = 3 }
17
+ let log = [];
18
+ const { calcs, data } = asyncCalcs([
19
+ [
20
+ "a",
21
+ async () => {
22
+ log.push("a");
23
+ const b = await calcs.get("b");
24
+ return 2 * b;
25
+ },
26
+ ],
27
+ [
28
+ "b",
29
+ async () => {
30
+ log.push("b");
31
+ const c = await calcs.get("c");
32
+ return c + 1;
33
+ },
34
+ ],
35
+ [
36
+ "c",
37
+ async () => {
38
+ log.push("c");
39
+ return 3;
40
+ },
41
+ ],
42
+ ]);
43
+
44
+ assert.deepEqual(await Tree.entries(calcs), [
45
+ ["a", 8],
46
+ ["b", 4],
47
+ ["c", 3],
48
+ ]);
49
+ assert.deepEqual(log, ["a", "b", "c"]);
50
+
51
+ log = [];
52
+ const a1 = await calcs.get("a");
53
+ assert.strictEqual(a1, 8);
54
+ assert.deepEqual(log, []); // a is cached, no new calcs
55
+
56
+ // Replace formula for a
57
+ // { a = 3 * b, b = c + 1, c = 3 }
58
+ data.set("a", async () => {
59
+ log.push("a");
60
+ const b = await calcs.get("b");
61
+ return 3 * b;
62
+ });
63
+ log = [];
64
+ const a2 = await calcs.get("a");
65
+ assert.strictEqual(a2, 12);
66
+ assert.deepEqual(log, ["a"]); // recalc only a
67
+
68
+ // Replace formula for b
69
+ // { a = 3 * b, b = c + 10, c = 3 }
70
+ data.set("b", async () => {
71
+ log.push("b");
72
+ const c = await calcs.get("c");
73
+ return c + 10;
74
+ });
75
+ log = [];
76
+ const a3 = await calcs.get("a");
77
+ assert.strictEqual(a3, 39);
78
+ assert.deepEqual(log, ["a", "b"]); // recalc a and b
79
+
80
+ // Replace value of c
81
+ // { a = 3 * b, b = c + 10, c = 100 }
82
+ data.set("c", async () => {
83
+ log.push("c");
84
+ return 100;
85
+ });
86
+ log = [];
87
+ const a4 = await calcs.get("a");
88
+ assert.strictEqual(a4, 330);
89
+ assert.deepEqual(log, ["a", "b", "c"]); // recalc all
90
+ });
91
+ });
@@ -1,40 +1,43 @@
1
1
  import assert from "node:assert";
2
2
  import * as fs from "node:fs";
3
3
  import path from "node:path";
4
- import { describe, test } from "node:test";
4
+ import { afterEach, beforeEach, describe, test } from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import OrigamiFileMap from "../../src/runtime/OrigamiFileMap.js";
7
7
 
8
8
  const dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const tempDirectory = path.join(dirname, "fixtures/temp/OrigamiFileMap");
9
+ const tempDirectory = path.join(dirname, "fixtures/temp");
10
10
 
11
11
  describe("OrigamiFileMap", () => {
12
- test.skip("can watch its folder for changes", { timeout: 2000 }, async () => {
13
- createTempDirectory();
12
+ let tempFiles;
14
13
 
15
- const tempFiles = new OrigamiFileMap(tempDirectory);
16
- const changedFileName = await new Promise(async (resolve) => {
14
+ beforeEach(() => {
15
+ // Remove any existing files or directories inside the temp directory so
16
+ // tests start from a clean slate. Use force so this is safe if the
17
+ // directory doesn't exist.
18
+ fs.rmSync(tempDirectory, { force: true, recursive: true });
19
+ fs.mkdirSync(tempDirectory, { recursive: true });
20
+ tempFiles = new OrigamiFileMap(tempDirectory);
21
+ });
22
+
23
+ afterEach(() => {
24
+ // HACK: should removeEventListener, which should stop watching
25
+ tempFiles.unwatch();
26
+ fs.rmSync(tempDirectory, { force: true, recursive: true });
27
+ });
28
+
29
+ test("can watch its folder for changes", { timeout: 2000 }, async () => {
30
+ const changedFilePath = await new Promise(async (resolve) => {
17
31
  tempFiles.addEventListener("change", (event) => {
18
- resolve(/** @type {any} */ (event).options.key);
32
+ resolve(/** @type {any} */ (event).options.filePath);
19
33
  });
34
+ await tempFiles.watch();
20
35
  tempFiles.set(
21
- "foo.txt",
22
- "This file is left over from testing and can be removed."
36
+ "temp.txt",
37
+ "This file is left over from testing and can be removed.",
23
38
  );
24
39
  });
25
- removeTempDirectory();
26
- assert.equal(changedFileName, "foo.txt");
40
+
41
+ assert.equal(path.basename(changedFilePath), "temp.txt");
27
42
  });
28
43
  });
29
-
30
- function createTempDirectory() {
31
- // Remove any existing files or directories inside the temp directory so
32
- // tests start from a clean slate. Use force so this is safe if the
33
- // directory doesn't exist.
34
- fs.rmSync(tempDirectory, { force: true, recursive: true });
35
- fs.mkdirSync(tempDirectory, { recursive: true });
36
- }
37
-
38
- function removeTempDirectory() {
39
- fs.rmSync(tempDirectory, { force: true, recursive: true });
40
- }
@@ -0,0 +1,49 @@
1
+ import { ObjectMap } from "@weborigami/async-tree";
2
+ import assert from "node:assert";
3
+ import { describe, test } from "node:test";
4
+ import ScopeMap from "../../src/runtime/ScopeMap.js";
5
+ import { cachePathSymbol } from "../../src/runtime/symbols.js";
6
+ import SyncCacheTransform from "../../src/runtime/SyncCacheTransform.js";
7
+ import systemCache from "../../src/runtime/systemCache.js";
8
+
9
+ describe("scope", () => {
10
+ test("gets the first defined value from the scope trees", async () => {
11
+ class SyncCacheObjectMap extends SyncCacheTransform(ObjectMap) {}
12
+
13
+ /** @type {any} */
14
+ const outer = new SyncCacheObjectMap({
15
+ a: 1,
16
+ b: 2,
17
+ });
18
+ outer[cachePathSymbol] = "outer";
19
+
20
+ /** @type {any} */
21
+ const inner = new SyncCacheObjectMap({
22
+ a: 3,
23
+ });
24
+ inner[cachePathSymbol] = "outer/inner";
25
+
26
+ inner.parent = outer;
27
+ const innerScope = new ScopeMap(inner);
28
+
29
+ // Inner tree has precedence
30
+ const a = systemCache.getOrInsertComputed("test/a", () =>
31
+ innerScope.get("a"),
32
+ );
33
+ assert.equal(a, 3);
34
+
35
+ // If tree doesn't have value, finds value from parent
36
+ const b = systemCache.getOrInsertComputed("test/b", () =>
37
+ innerScope.get("b"),
38
+ );
39
+ assert.equal(b, 2);
40
+
41
+ const c = systemCache.getOrInsertComputed("test/c", () =>
42
+ innerScope.get("c"),
43
+ );
44
+ assert.equal(c, undefined);
45
+
46
+ assert.deepEqual([...innerScope.keys()], ["a", "b"]);
47
+ assert.deepEqual(/** @type {any} */ (innerScope).trees, [inner, outer]);
48
+ });
49
+ });
@@ -0,0 +1,93 @@
1
+ import assert from "node:assert";
2
+ import { beforeEach, describe, test } from "node:test";
3
+ import systemCache from "../../src/runtime/systemCache.js";
4
+
5
+ // We test SyncCacheTransform via syncCalcs since it's a fairly small
6
+ // application of the transform.
7
+ import syncCalcs from "./syncCalcs.js";
8
+
9
+ describe("SyncCacheTransform", () => {
10
+ beforeEach(() => {
11
+ systemCache.clear();
12
+ });
13
+
14
+ test("caches values and records dependencies", () => {
15
+ // { a = 2 * b, b = c + 1, c = 3 }
16
+ let log = [];
17
+ const { calcs, data } = syncCalcs([
18
+ [
19
+ "a",
20
+ () => {
21
+ log.push("a");
22
+ const b = calcs.get("b");
23
+ return 2 * b;
24
+ },
25
+ ],
26
+ [
27
+ "b",
28
+ () => {
29
+ log.push("b");
30
+ const c = calcs.get("c");
31
+ return c + 1;
32
+ },
33
+ ],
34
+ [
35
+ "c",
36
+ () => {
37
+ log.push("c");
38
+ return 3;
39
+ },
40
+ ],
41
+ ]);
42
+
43
+ assert.deepEqual(
44
+ [...calcs.entries()],
45
+ [
46
+ ["a", 8],
47
+ ["b", 4],
48
+ ["c", 3],
49
+ ],
50
+ );
51
+ assert.deepEqual(log, ["a", "b", "c"]);
52
+
53
+ log = [];
54
+ const a1 = calcs.get("a");
55
+ assert.strictEqual(a1, 8);
56
+ assert.deepEqual(log, []); // a is cached, no new calcs
57
+
58
+ // Replace formula for a
59
+ // { a = 3 * b, b = c + 1, c = 3 }
60
+ data.set("a", () => {
61
+ log.push("a");
62
+ const b = calcs.get("b");
63
+ return 3 * b;
64
+ });
65
+ log = [];
66
+ const a2 = calcs.get("a");
67
+ assert.strictEqual(a2, 12);
68
+ assert.deepEqual(log, ["a"]); // recalc only a
69
+
70
+ // Replace formula for b
71
+ // { a = 3 * b, b = c + 10, c = 3 }
72
+ data.set("b", () => {
73
+ log.push("b");
74
+ const c = calcs.get("c");
75
+ return c + 10;
76
+ });
77
+ log = [];
78
+ const a3 = calcs.get("a");
79
+ assert.strictEqual(a3, 39);
80
+ assert.deepEqual(log, ["a", "b"]); // recalc a and b
81
+
82
+ // Replace value of c
83
+ // { a = 3 * b, b = c + 10, c = 100 }
84
+ data.set("c", () => {
85
+ log.push("c");
86
+ return 100;
87
+ });
88
+ log = [];
89
+ const a4 = calcs.get("a");
90
+ assert.strictEqual(a4, 330);
91
+ assert.deepEqual(log, ["a", "b", "c"]); // recalc all
92
+ });
93
+ });