@transclude/core 0.10.0 → 0.10.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/package.json +1 -1
- package/src/app.js +6 -1
- package/src/cache.js +52 -7
- package/src/compiler/shim.js +12 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transclude/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "An HTML-first server framework. A page is an .html file, the directory tree is the route table, and any fragment of a page is a URL of its own. Runs on Node, Bun, Deno and workerd, and ships no client JavaScript by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"html",
|
package/src/app.js
CHANGED
|
@@ -444,7 +444,12 @@ export function createApp({
|
|
|
444
444
|
return sendRendered(c, html, ctx, preload);
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
|
|
447
|
+
// The fourth argument is what holds the background rebuild. Without it
|
|
448
|
+
// workerd stops the rebuild when this response is sent, and the entry it
|
|
449
|
+
// leaves in the in-flight map answers every later request with a dead
|
|
450
|
+
// promise.
|
|
451
|
+
const after = afterFor(c, (error) => report(error, c));
|
|
452
|
+
const html = await cache.read(cacheKey(c.req.url), window, render, after);
|
|
448
453
|
|
|
449
454
|
// A miss rendered through the cache, and that render can answer with a
|
|
450
455
|
// `Response`. It was not stored, but it is still the answer.
|
package/src/cache.js
CHANGED
|
@@ -62,6 +62,35 @@ export function memoryStore({ max = 1000 } = {}) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* How long an unfinished render may hold a key.
|
|
67
|
+
*
|
|
68
|
+
* The map exists so one render happens per key. It assumed every promise it held
|
|
69
|
+
* would settle. On workerd one may not: the isolate is allowed to stop when the
|
|
70
|
+
* response is sent, and it stops the work with it. The `finally` never runs, the
|
|
71
|
+
* entry stays, and every later request for that key waits on a promise that is
|
|
72
|
+
* already dead. The page hangs for as long as the isolate lives.
|
|
73
|
+
*
|
|
74
|
+
* `after` is the fix, and this is the bound on it being wrong. A render slower
|
|
75
|
+
* than this loses its claim on the key rather than keeping it forever.
|
|
76
|
+
*/
|
|
77
|
+
const ABANDONED_MS = 30_000;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Keeps the background rebuild alive, and its failure off the unhandled path.
|
|
81
|
+
*
|
|
82
|
+
* `after` is `waitUntil` on workerd and a no-op elsewhere, and it attaches its
|
|
83
|
+
* own catch. Without one, the catch here is all there is: a rebuild that throws
|
|
84
|
+
* must not take down a process that was only serving a stale page.
|
|
85
|
+
*
|
|
86
|
+
* @param {Promise<unknown>} work
|
|
87
|
+
* @param {((work: Promise<unknown>) => void)|null} after
|
|
88
|
+
*/
|
|
89
|
+
function hold(work, after) {
|
|
90
|
+
if (after) after(work);
|
|
91
|
+
else work.catch(() => {});
|
|
92
|
+
}
|
|
93
|
+
|
|
65
94
|
/**
|
|
66
95
|
* One route's cache, wrapped around the render.
|
|
67
96
|
*
|
|
@@ -80,8 +109,11 @@ export function createCache(store = memoryStore(), { now = () => Date.now() } =
|
|
|
80
109
|
// and every request behind it each start their own.
|
|
81
110
|
const inFlight = new Map();
|
|
82
111
|
|
|
83
|
-
const refresh =
|
|
84
|
-
|
|
112
|
+
const refresh = (key, window, render) => {
|
|
113
|
+
const current = inFlight.get(key);
|
|
114
|
+
if (current && now() - current.at < ABANDONED_MS) return current.work;
|
|
115
|
+
|
|
116
|
+
const started = now();
|
|
85
117
|
|
|
86
118
|
const work = (async () => {
|
|
87
119
|
const result = await render();
|
|
@@ -97,15 +129,25 @@ export function createCache(store = memoryStore(), { now = () => Date.now() } =
|
|
|
97
129
|
store.delete(key);
|
|
98
130
|
}
|
|
99
131
|
return result;
|
|
100
|
-
})().finally(() =>
|
|
132
|
+
})().finally(() => {
|
|
133
|
+
// Only if this is still the entry made above. A render that ran past
|
|
134
|
+
// ABANDONED_MS was replaced, and it must not delete its replacement.
|
|
135
|
+
if (inFlight.get(key)?.at === started) inFlight.delete(key);
|
|
136
|
+
});
|
|
101
137
|
|
|
102
|
-
inFlight.set(key, work);
|
|
138
|
+
inFlight.set(key, { work, at: started });
|
|
103
139
|
return work;
|
|
104
140
|
};
|
|
105
141
|
|
|
106
142
|
return {
|
|
107
|
-
/**
|
|
108
|
-
|
|
143
|
+
/**
|
|
144
|
+
* `null` when the caller should just render, which is every uncached route.
|
|
145
|
+
*
|
|
146
|
+
* `after` is the request's `ctx.after`. Only the stale path uses it, and a
|
|
147
|
+
* caller that leaves it out gets a rebuild nothing holds, which is what this
|
|
148
|
+
* used to do everywhere.
|
|
149
|
+
*/
|
|
150
|
+
async read(key, window, render, after = null) {
|
|
109
151
|
if (!window) return null;
|
|
110
152
|
|
|
111
153
|
const hit = store.get(key);
|
|
@@ -116,7 +158,10 @@ export function createCache(store = memoryStore(), { now = () => Date.now() } =
|
|
|
116
158
|
// Stale. Answer with it now and rebuild behind the response. A failed
|
|
117
159
|
// rebuild leaves the stale entry in place rather than emptying the cache
|
|
118
160
|
// because one render threw.
|
|
119
|
-
|
|
161
|
+
//
|
|
162
|
+
// `after` is what keeps the rebuild alive. On workerd the isolate may stop
|
|
163
|
+
// the moment the response is sent, and work nothing holds stops with it.
|
|
164
|
+
hold(refresh(key, window, render), after);
|
|
120
165
|
return hit.html;
|
|
121
166
|
},
|
|
122
167
|
|
package/src/compiler/shim.js
CHANGED
|
@@ -18,6 +18,7 @@ import { ambientJsdoc } from './ambient.js';
|
|
|
18
18
|
import { parseEach } from './directives.js';
|
|
19
19
|
import { childrenOf } from './codegen.js';
|
|
20
20
|
import { splitInterpolations } from './interp.js';
|
|
21
|
+
import { GLOBALS as EXPRESSION_GLOBALS } from './expr.js';
|
|
21
22
|
import { splitBlocks } from './index.js';
|
|
22
23
|
import { planLift } from './script.js';
|
|
23
24
|
import { ACTION_METHODS } from '../document.js';
|
|
@@ -700,10 +701,17 @@ function collectRoots(node, scope, out) {
|
|
|
700
701
|
}
|
|
701
702
|
}
|
|
702
703
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
704
|
+
/**
|
|
705
|
+
* The names an expression resolves without a data lookup, plus the three
|
|
706
|
+
* literals acorn hands back as identifiers where jsep does not.
|
|
707
|
+
*
|
|
708
|
+
* Taken from the expression layer rather than written out again. The two lists
|
|
709
|
+
* were separate and drifted: `json` was in one and not the other, so the
|
|
710
|
+
* documented helper compiled, rendered, and then failed `npm run check` saying
|
|
711
|
+
* it was not a field of the page's data. Anything the compiler resolves has to
|
|
712
|
+
* resolve here too, or the shim checks code the compiler never emits.
|
|
713
|
+
*/
|
|
714
|
+
const GLOBALS = new Set([...EXPRESSION_GLOBALS, 'true', 'false', 'null']);
|
|
707
715
|
|
|
708
716
|
function collectUsedTags(nodes, components, found = new Set()) {
|
|
709
717
|
for (const node of nodes) {
|