@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
@@ -0,0 +1,239 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import SystemCacheMap from "../../src/runtime/SystemCacheMap.js";
4
+ import volatile from "../../src/runtime/volatile.js";
5
+
6
+ describe("SystemCacheMap", () => {
7
+ test("getOrInsertComputed tracks sync dependencies", async () => {
8
+ const cache = new SystemCacheMap();
9
+ let log;
10
+
11
+ // Create cache entries for
12
+ // { a = b + 1, b = 2 }
13
+ let getB = () =>
14
+ cache.getOrInsertComputed("b", () => {
15
+ log.push("b");
16
+ return 2;
17
+ });
18
+ let getA = () =>
19
+ cache.getOrInsertComputed("a", () => {
20
+ log.push("a");
21
+ const b = getB();
22
+ return b + 1;
23
+ });
24
+
25
+ log = [];
26
+ const a1 = getA();
27
+ assert.strictEqual(a1, 3);
28
+ assert.deepEqual(log, ["a", "b"]);
29
+ assert.deepEqual(cacheEntries(cache), [
30
+ ["a", { value: 3, upstreams: ["b"] }],
31
+ ["b", { value: 2, downstreams: ["a"] }],
32
+ ]);
33
+
34
+ log = [];
35
+ const a2 = getA();
36
+ assert.strictEqual(a2, 3);
37
+ assert.deepEqual(log, []); // no recalculation
38
+
39
+ // Delete a
40
+ cache.delete("a");
41
+ assert.deepEqual(cacheEntries(cache), [["b", { value: 2 }]]);
42
+ });
43
+
44
+ test("getOrInsertComputedAsync tracks async dependencies", async () => {
45
+ const cache = new SystemCacheMap();
46
+ let log;
47
+
48
+ // Create cache entries for
49
+ // { a = 2 * b, b = c + 1, c = 3 }
50
+ let getC = async () =>
51
+ await cache.getOrInsertComputedAsync("c", async () => {
52
+ log.push("c");
53
+ return 3;
54
+ });
55
+ let getB = async () =>
56
+ await cache.getOrInsertComputedAsync("b", async () => {
57
+ log.push("b");
58
+ const c = await getC();
59
+ return c + 1;
60
+ });
61
+ let getA = async () =>
62
+ await cache.getOrInsertComputedAsync("a", async () => {
63
+ log.push("a");
64
+ const b = await getB();
65
+ return 2 * b;
66
+ });
67
+
68
+ // Initial read of `a` populates cache
69
+ log = [];
70
+ const a1 = await getA();
71
+ assert.strictEqual(a1, 8);
72
+ assert.deepEqual(log, ["a", "b", "c"]);
73
+ assert.deepEqual(cacheEntries(cache), [
74
+ ["a", { value: 8, upstreams: ["b"] }],
75
+ ["b", { value: 4, downstreams: ["a"], upstreams: ["c"] }],
76
+ ["c", { value: 3, downstreams: ["b"] }],
77
+ ]);
78
+
79
+ // Subsequent read of `a` hits cache
80
+ log = [];
81
+ const a2 = await getA();
82
+ assert.strictEqual(a2, 8);
83
+ assert.deepEqual(log, []); // no recalculation
84
+
85
+ // Delete a
86
+ cache.delete("a");
87
+ assert.deepEqual(cacheEntries(cache), [
88
+ ["b", { value: 4, upstreams: ["c"] }],
89
+ ["c", { value: 3, downstreams: ["b"] }],
90
+ ]);
91
+
92
+ // Replace formula for a
93
+ // { a = 3 * b, b = c + 1, c = 3 }
94
+ getA = async () =>
95
+ await cache.getOrInsertComputedAsync("a", async () => {
96
+ log.push("a");
97
+ const b = await getB();
98
+ return 3 * b;
99
+ });
100
+ log = [];
101
+ const a3 = await getA();
102
+ assert.strictEqual(a3, 12);
103
+ assert.deepEqual(log, ["a"]); // recalc only a
104
+
105
+ // Delete b, confirm it's removed as both upstream and downstream
106
+ cache.delete("b");
107
+ assert.deepEqual(cacheEntries(cache), [["c", { value: 3 }]]);
108
+
109
+ // Replace formula for b
110
+ // { a = 3 * b, b = c + 10, c = 3 }
111
+ getB = async () =>
112
+ await cache.getOrInsertComputedAsync("b", async () => {
113
+ log.push("b");
114
+ const c = await getC();
115
+ return c + 10;
116
+ });
117
+ log = [];
118
+ const a4 = await getA();
119
+ assert.strictEqual(a4, 39);
120
+ assert.deepEqual(log, ["a", "b"]); // recalc a and b
121
+
122
+ // Replace formula for c
123
+ // { a = 3 * b, b = c + 10, c = 100 }
124
+ getC = async () =>
125
+ await cache.getOrInsertComputedAsync("c", async () => {
126
+ log.push("c");
127
+ return 100;
128
+ });
129
+ cache.delete("c");
130
+ log = [];
131
+ const a5 = await getA();
132
+ assert.strictEqual(a5, 330);
133
+ assert.deepEqual(log, ["a", "b", "c"]); // recalc all
134
+ });
135
+
136
+ test("async function can track sync dependency", async () => {
137
+ const cache = new SystemCacheMap();
138
+
139
+ // Create cache entries for
140
+ // { a = b + 1, b = 2 }
141
+ const getB = () =>
142
+ cache.getOrInsertComputed("b", () => {
143
+ return 2;
144
+ });
145
+ const getA = async () =>
146
+ cache.getOrInsertComputedAsync("a", async () => {
147
+ const b = getB();
148
+ return b + 1;
149
+ });
150
+
151
+ const a1 = await getA();
152
+ assert.strictEqual(a1, 3);
153
+ });
154
+
155
+ test("sync function can't have async dependency", async () => {
156
+ const cache = new SystemCacheMap();
157
+
158
+ // Create cache entries for
159
+ // { a = b, b = 1 }
160
+ const getB = async () =>
161
+ await cache.getOrInsertComputedAsync("b", async () => {
162
+ return 1;
163
+ });
164
+ const getA = () => cache.getOrInsertComputed("a", () => getB());
165
+
166
+ // a is sync but b is async
167
+ await assert.rejects(getA);
168
+ });
169
+
170
+ test("implicit dependency of child path on parent path", async () => {
171
+ const cache = new SystemCacheMap();
172
+
173
+ // Create cache entries
174
+ // parent = 1, parent/child = 2, other = 3
175
+ const getParent = () => cache.getOrInsertComputed("parent", () => 1);
176
+ cache.getOrInsertComputed("parent/child", () => {
177
+ // Force access of parent, no explicit dependency should be recorded
178
+ getParent();
179
+ return 2;
180
+ });
181
+ cache.getOrInsertComputed("other", () => 3);
182
+
183
+ assert.deepEqual(cacheEntries(cache), [
184
+ ["parent/child", { value: 2 }],
185
+ ["parent", { value: 1 }],
186
+ ["other", { value: 3 }],
187
+ ]);
188
+
189
+ // Deleting parent implicitly deletes all children
190
+ cache.delete("parent");
191
+ assert.deepEqual(cacheEntries(cache), [["other", { value: 3 }]]);
192
+ });
193
+
194
+ test("sync volatile values are not cached", async () => {
195
+ const cache = new SystemCacheMap();
196
+ let count = 0;
197
+ const getCount = () =>
198
+ cache.getOrInsertComputed("count", () => volatile(count++));
199
+
200
+ const first = getCount();
201
+ const second = getCount();
202
+ assert.strictEqual(Number(first), 0);
203
+ assert.strictEqual(Number(second), 1);
204
+ assert.strictEqual(count, 2); // computeFn ran twice, value not cached
205
+ });
206
+
207
+ test("async volatile values are not cached", async () => {
208
+ const cache = new SystemCacheMap();
209
+ let count = 0;
210
+ const getCount = () =>
211
+ cache.getOrInsertComputedAsync("count", async () => volatile(count++));
212
+
213
+ const first = await getCount();
214
+ const second = await getCount();
215
+ assert.strictEqual(Number(first), 0);
216
+ assert.strictEqual(Number(second), 1);
217
+ assert.strictEqual(count, 2); // computeFn ran twice, value not cached
218
+ });
219
+ });
220
+
221
+ export function cacheEntries(cache) {
222
+ const entries = [...cache.entries()];
223
+ return entries.map(([key, entry]) => [key, cacheEntry(entry)]);
224
+ }
225
+
226
+ export function cacheEntry(entry) {
227
+ const { downstreams, value } = entry;
228
+
229
+ const result = { value };
230
+ if (downstreams?.size > 0) {
231
+ result.downstreams = [...downstreams];
232
+ }
233
+
234
+ if (entry.upstreams?.size > 0) {
235
+ result.upstreams = [...entry.upstreams];
236
+ }
237
+
238
+ return result;
239
+ }
@@ -0,0 +1,28 @@
1
+ import { AsyncMap, SyncMap } from "@weborigami/async-tree";
2
+ import AsyncCacheTransform from "../../src/runtime/AsyncCacheTransform.js";
3
+ import SyncCacheTransform from "../../src/runtime/SyncCacheTransform.js";
4
+
5
+ export default function asyncCalcs(iterable) {
6
+ const data = new (SyncCacheTransform(SyncMap))(iterable);
7
+ const calcs = new (AsyncCacheTransform(AsyncResultsMap))(data);
8
+ return { calcs, data };
9
+ }
10
+
11
+ class AsyncResultsMap extends AsyncMap {
12
+ constructor(source) {
13
+ super();
14
+ this.source = source;
15
+ }
16
+
17
+ async get(key) {
18
+ let value = this.source.get(key);
19
+ if (typeof value === "function") {
20
+ value = await value();
21
+ }
22
+ return value;
23
+ }
24
+
25
+ async *keys() {
26
+ yield* this.source.keys();
27
+ }
28
+ }
@@ -0,0 +1,55 @@
1
+ import assert from "node:assert";
2
+ import { beforeEach, describe, test } from "node:test";
3
+ import enableValueCaching from "../../src/runtime/enableValueCaching.js";
4
+ import { cachePathSymbol } from "../../src/runtime/symbols.js";
5
+ import systemCache from "../../src/runtime/systemCache.js";
6
+
7
+ describe("enableValueCaching", () => {
8
+ beforeEach(() => {
9
+ systemCache.clear();
10
+ });
11
+
12
+ test("leaves primitive values as is", () => {
13
+ const value1 = enableValueCaching(1, "foo.ori/");
14
+ assert.equal(value1, 1);
15
+
16
+ const value2 = enableValueCaching("hello", "foo.ori/");
17
+ assert.equal(value2, "hello");
18
+ });
19
+
20
+ test("adds cachePath to plain object", () => {
21
+ const obj = { a: 1, b: 2 };
22
+ const value = enableValueCaching(obj, "foo.ori/");
23
+ assert.equal(value, obj);
24
+ assert.equal(value[cachePathSymbol], "foo.ori/");
25
+ });
26
+
27
+ test("adds cachePath to array", () => {
28
+ const arr = [1, 2, 3];
29
+ const value = enableValueCaching(arr, "foo.ori/");
30
+ assert.equal(value, arr);
31
+ assert.equal(value[cachePathSymbol], "foo.ori/");
32
+ });
33
+
34
+ test("applies cache transform to Map", () => {
35
+ const map = new Map([
36
+ ["a", 1],
37
+ ["b", 2],
38
+ ]);
39
+ const value = enableValueCaching(map, "foo.ori/");
40
+ assert(value instanceof Map);
41
+ assert.equal(value[cachePathSymbol], "foo.ori/");
42
+ assert.equal(value.get("a"), 1);
43
+ assert(systemCache.has("foo.ori/a"));
44
+ });
45
+
46
+ test("applies cache to function with string arguments", () => {
47
+ function fn(x, y) {
48
+ return `${x} ${y}`;
49
+ }
50
+ const value = enableValueCaching(fn, "foo.ori/");
51
+ assert(typeof value === "function");
52
+ assert.equal(value("Hello", "world"), "Hello world");
53
+ assert.equal(value[cachePathSymbol], "foo.ori/");
54
+ });
55
+ });
@@ -1,12 +1,18 @@
1
+ import { ObjectMap } from "@weborigami/async-tree";
1
2
  import assert from "node:assert";
2
- import { describe, test } from "node:test";
3
+ import { beforeEach, describe, test } from "node:test";
3
4
  import coreGlobals from "../../src/project/coreGlobals.js";
4
5
  import { formatError } from "../../src/runtime/errors.js";
5
6
  import evaluate from "../../src/runtime/evaluate.js";
7
+ import systemCache from "../../src/runtime/systemCache.js";
6
8
 
7
9
  const globals = await coreGlobals();
8
10
 
9
11
  describe("formatError", () => {
12
+ beforeEach(() => {
13
+ systemCache.clear();
14
+ });
15
+
10
16
  describe("ReferenceError", () => {
11
17
  test("identifies an undefined function", async () => {
12
18
  await assertError(
@@ -17,7 +23,7 @@ evaluating: \x1b[31mdoesntExist\x1b[0m`,
17
23
  );
18
24
  });
19
25
 
20
- test("identifies the argument that produced an error", async () => {
26
+ test.skip("identifies the argument that produced an error", async () => {
21
27
  await assertError(
22
28
  `Tree.map(foo, (_) => _)`,
23
29
  `ReferenceError: Tree.map: The map argument wasn't defined.
@@ -73,9 +79,9 @@ evaluating: \x1B[31mdatu.toString\x1B[0m
73
79
  });
74
80
 
75
81
  test("proposes typos using scope keys", async () => {
76
- const parent = {
82
+ const parent = new ObjectMap({
77
83
  "index.ori": "Index page",
78
- };
84
+ });
79
85
  await assertError(
80
86
  `index.orj(1, 2, 3)`,
81
87
  `ReferenceError: Couldn't find the function or map to execute.
@@ -170,11 +176,14 @@ evaluating: \x1B[31mposts.md\x1B[0m
170
176
  });
171
177
 
172
178
  test("handles a traversal failure inside a reference error", async () => {
173
- const parent = {
174
- post1: {
175
- title: "First post",
179
+ const parent = new ObjectMap(
180
+ {
181
+ post1: {
182
+ title: "First post",
183
+ },
176
184
  },
177
- };
185
+ { deep: true },
186
+ );
178
187
  await assertError(
179
188
  `(post1/totle).toUpperCase()`,
180
189
  `ReferenceError: Tried to get a property of something that doesn't exist.
@@ -197,16 +206,19 @@ evaluating: \x1B[31mrepeat\x1B[0m`,
197
206
 
198
207
  describe("TraverseError", () => {
199
208
  test("suggests typos for failed path", async () => {
200
- const parent = {
201
- a: {
202
- b: {
203
- sub: {
204
- // need 2+ characters for typos
205
- c: 1,
209
+ const parent = new ObjectMap(
210
+ {
211
+ a: {
212
+ b: {
213
+ sub: {
214
+ // need 2+ characters for typos
215
+ c: 1,
216
+ },
206
217
  },
207
218
  },
208
219
  },
209
- };
220
+ { deep: true },
221
+ );
210
222
  await assertError(
211
223
  `a/b/sup/c`,
212
224
  `TraverseError: A path hit a null or undefined value.
@@ -219,9 +231,7 @@ evaluating: \x1B[31msup/\x1B[0m`,
219
231
  });
220
232
 
221
233
  test("identifies when a numeric key failed", async () => {
222
- const parent = {
223
- map: new Map([[1, new Map([["a", true]])]]),
224
- };
234
+ const parent = new Map([["map", new Map([[1, new Map([["a", true]])]])]]);
225
235
  await assertError(
226
236
  `map/1/a`,
227
237
  `TraverseError: A path hit a null or undefined value.
@@ -235,12 +245,12 @@ evaluating: \x1B[31m1/\x1B[0m`,
235
245
  });
236
246
 
237
247
  test("identifies when a value couldn't be unpacked due to a missing extension handler", async () => {
238
- const parent = {
248
+ const parent = new ObjectMap({
239
249
  "file.foo": Uint8Array.from("hello"),
240
- };
250
+ });
241
251
  await assertError(
242
252
  `file.foo/bar`,
243
- `TraverseError: A path hit binary file data that can't be unpacked.
253
+ `TraverseError: A path hit binary data that can't be unpacked.
244
254
  Tried to traverse path: file.foo/bar
245
255
  Stopped unexpectedly at: file.foo/
246
256
  The value couldn't be unpacked because no file extension handler is registered for ".foo".
@@ -250,11 +260,14 @@ evaluating: \x1B[31mfile.foo/\x1B[0m`,
250
260
  });
251
261
 
252
262
  test("identifies when data was already unpacked", async () => {
253
- const parent = {
254
- a: {
255
- "b.json": 1,
263
+ const parent = new ObjectMap(
264
+ {
265
+ a: {
266
+ "b.json": 1,
267
+ },
256
268
  },
257
- };
269
+ { deep: true },
270
+ );
258
271
  await assertError(
259
272
  `a/b.json/`,
260
273
  `TraverseError: A path tried to unpack data that's already unpacked.
@@ -267,9 +280,12 @@ evaluating: \x1B[31mb.json/\x1B[0m`,
267
280
  });
268
281
 
269
282
  test("identifies when a value didn't exist to be unpacked", async () => {
270
- const parent = {
271
- a: {},
272
- };
283
+ const parent = new ObjectMap(
284
+ {
285
+ a: {},
286
+ },
287
+ { deep: true },
288
+ );
273
289
  await assertError(
274
290
  `a/b/`,
275
291
  `TraverseError: A path tried to unpack a value that doesn't exist.
@@ -282,12 +298,19 @@ evaluating: \x1B[31mb/\x1B[0m`,
282
298
  });
283
299
  });
284
300
 
285
- async function assertError(source, expectedMessage, options) {
301
+ async function assertError(expression, expectedMessage, options) {
286
302
  try {
303
+ const source =
304
+ typeof expression !== "string"
305
+ ? expression
306
+ : {
307
+ text: expression,
308
+ relativePath: "test.ori",
309
+ };
287
310
  await evaluate(source, {
288
311
  globals,
289
312
  object: null,
290
- parent: {},
313
+ parent: null,
291
314
  stack: [],
292
315
  ...options,
293
316
  });
@@ -1,7 +1,8 @@
1
- import { Tree } from "@weborigami/async-tree";
1
+ import { ObjectMap, Tree } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
- import { describe, test } from "node:test";
3
+ import { beforeEach, describe, test } from "node:test";
4
4
  import evaluate from "../../src/runtime/evaluate.js";
5
+ import systemCache from "../../src/runtime/systemCache.js";
5
6
 
6
7
  const globals = {
7
8
  concat: (...args) => args.join(""),
@@ -10,6 +11,10 @@ const globals = {
10
11
  };
11
12
 
12
13
  describe("evaluate", () => {
14
+ beforeEach(() => {
15
+ systemCache.clear();
16
+ });
17
+
13
18
  test("array", async () => {
14
19
  await assertEvaluation("[]", []);
15
20
  await assertEvaluation("[ 1, 2, 3, ]", [1, 2, 3]);
@@ -31,9 +36,9 @@ describe("evaluate", () => {
31
36
 
32
37
  test("angle bracket path", async () => {
33
38
  await assertEvaluation("<data>", "Bob", {
34
- parent: {
39
+ parent: new ObjectMap({
35
40
  data: "Bob",
36
- },
41
+ }),
37
42
  });
38
43
  });
39
44
 
@@ -1,11 +1,16 @@
1
1
  import assert from "node:assert";
2
- import { describe, test } from "node:test";
2
+ import { beforeEach, describe, test } from "node:test";
3
3
 
4
4
  import { SyncMap } from "@weborigami/async-tree";
5
5
  import execute from "../../src/runtime/execute.js";
6
+ import systemCache from "../../src/runtime/systemCache.js";
6
7
  import { createCode } from "../compiler/codeHelpers.js";
7
8
 
8
9
  describe("execute", () => {
10
+ beforeEach(() => {
11
+ systemCache.clear();
12
+ });
13
+
9
14
  test("if object in function position isn't a function, can unpack it", async () => {
10
15
  const fn = (...args) => args.join(",");
11
16
  const packed = new String();
@@ -1,35 +1,59 @@
1
1
  import { ObjectMap, symbols, SyncMap, Tree } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
- import { describe, test } from "node:test";
3
+ import { beforeEach, describe, test } from "node:test";
4
4
 
5
5
  import expressionObject from "../../src/runtime/expressionObject.js";
6
6
  import { ops } from "../../src/runtime/internal.js";
7
+ import { cachePathSymbol } from "../../src/runtime/symbols.js";
8
+ import systemCache from "../../src/runtime/systemCache.js";
7
9
 
8
10
  describe("expressionObject", () => {
11
+ beforeEach(() => {
12
+ systemCache.clear();
13
+ });
14
+
9
15
  test("can instantiate an object", async () => {
10
- const container = new ObjectMap({
16
+ const parent = new ObjectMap({
11
17
  upper: (s) => s.toUpperCase(),
12
18
  });
13
19
 
14
20
  const entries = [
15
- ["hello", [[[ops.scope, container], "upper"], "hello"]],
16
- ["world", [[[ops.scope, container], "upper"], "world"]],
21
+ ["hello", [[[ops.scope], "upper"], "hello"]],
22
+ ["world", [[[ops.scope], "upper"], "world"]],
17
23
  ];
18
24
  const context = new SyncMap();
19
25
 
20
- const object = await expressionObject(entries, { object: context });
26
+ const object = await expressionObject(entries, {
27
+ object: context,
28
+ parent,
29
+ });
21
30
  assert.equal(await object.hello, "HELLO");
22
31
  assert.equal(await object.world, "WORLD");
23
32
  assert.equal(object[symbols.parent], context);
24
33
  });
25
34
 
26
- test("can define a property getter", async () => {
35
+ test("static property", async () => {
36
+ let count = 0;
37
+ const increment = () => count++;
38
+ const entries = [["count", [increment]]];
39
+ const object = await expressionObject(entries);
40
+ const propertyDescriptor = Object.getOwnPropertyDescriptor(object, "count");
41
+ assert.equal(propertyDescriptor?.value, 0);
42
+ assert.equal(object.count, 0);
43
+ assert.equal(count, 1); // getter should have been called
44
+ });
45
+
46
+ test("with caching on, property getter uses system cache", async () => {
27
47
  let count = 0;
28
48
  const increment = () => count++;
29
49
  const entries = [["count", [ops.getter, [increment]]]];
30
50
  const object = await expressionObject(entries);
51
+ object[cachePathSymbol] = "foo.ori/"; // enable caching on this object tree
31
52
  assert.equal(await object.count, 0);
32
- assert.equal(await object.count, 1);
53
+ const propertyDescriptor = Object.getOwnPropertyDescriptor(object, "count");
54
+ assert(propertyDescriptor?.get); // should still be a getter
55
+ assert.equal(count, 1); // getter should have been called
56
+ assert.equal(await object.count, 0); // getter result should be cached
33
57
  });
34
58
 
35
59
  test("treats a getter for a primitive value as a regular property", async () => {
@@ -75,10 +99,16 @@ describe("expressionObject", () => {
75
99
  });
76
100
  });
77
101
 
78
- test.only("returned object values can be unpacked", async () => {
102
+ test("returned object values can be unpacked", async () => {
79
103
  const entries = [["data.json", `{ "a": 1 }`]];
80
104
  const context = new SyncMap();
81
- const result = await expressionObject(entries, { object: context });
105
+ const globals = {
106
+ json_handler: { unpack: (data) => JSON.parse(data) },
107
+ };
108
+ const result = await expressionObject(entries, {
109
+ object: context,
110
+ globals,
111
+ });
82
112
  const dataJson = await result["data.json"];
83
113
  const json = await dataJson.unpack();
84
114
  assert.deepEqual(json, { a: 1 });
@@ -115,11 +145,21 @@ describe("expressionObject", () => {
115
145
  ]);
116
146
  });
117
147
 
118
- // test("sets symbols.async on objects with getters", async () => {
119
- // const noGetter = await expressionObject([["eager", 1]]);
120
- // assert.equal(noGetter[symbols.async], undefined);
148
+ test("tracks dependencies on upstream values", async () => {
149
+ systemCache.clear();
121
150
 
122
- // const hasGetter = await expressionObject([["lazy", [ops.getter, [2]]]]);
123
- // assert.equal(hasGetter[symbols.async], true);
124
- // });
151
+ // The `number` property gets a value from the cache
152
+ const getNumber = () =>
153
+ systemCache.getOrInsertComputed("dependency", () => 1);
154
+ const entries = [["number", [ops.getter, [getNumber]]]];
155
+ const object = await expressionObject(entries);
156
+ object[cachePathSymbol] = "src/test.ori/"; // enable caching on this object tree
157
+ const number = await object.number;
158
+ assert.equal(number, 1);
159
+
160
+ const dependencyEntry = systemCache.get("dependency");
161
+ const objectEntry = systemCache.get("src/test.ori/number");
162
+ assert(dependencyEntry.downstreams.has("src/test.ori/number"));
163
+ assert(objectEntry.upstreams.has("dependency"));
164
+ });
125
165
  });
@@ -0,0 +1,24 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import json_handler from "../../src/handlers/json_handler.js";
4
+ import fetchAndHandleExtension from "../../src/protocols/fetchAndHandleExtension.js";
5
+
6
+ describe("fetchAndHandleExtension", () => {
7
+ test("can unpack based on MIME content type", async () => {
8
+ /** @type {any} */
9
+ const parent = new Map();
10
+ parent.globals = {
11
+ json_handler,
12
+ };
13
+ const buffer = await fetchAndHandleExtension(
14
+ "https://weborigami.org/samples/help/pet.json",
15
+ null,
16
+ {
17
+ parent,
18
+ },
19
+ );
20
+ // @ts-ignore
21
+ const data = await buffer.unpack();
22
+ assert.strictEqual(data.name, "Fluffy");
23
+ });
24
+ });
@@ -0,0 +1 @@
1
+ "Hello"