@alpaca-headless/dynamic-renderings-engine 1.0.4694
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/LICENSE +8 -0
- package/README.md +43 -0
- package/THIRD_PARTY_LICENSES +385 -0
- package/dist/assets/esbuild.wasm +0 -0
- package/dist/assets/quickjs.wasm +0 -0
- package/dist/assets/tailwindcss/theme.css +510 -0
- package/dist/assets/tailwindcss/utilities.css +1 -0
- package/dist/compile-thread.mjs +19561 -0
- package/dist/css-policy/css-policy.d.mts +13 -0
- package/dist/css-policy/css-policy.mjs +396 -0
- package/dist/engine.mjs +463 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.js +2 -0
- package/dist/protocol/protocol.d.ts +46 -0
- package/dist/protocol/protocol.js +59 -0
- package/dist/protocol/types.d.ts +113 -0
- package/dist/protocol/types.js +2 -0
- package/dist/render-thread.mjs +1915 -0
- package/dist/runtime.js +14052 -0
- package/dist/runtime.meta.json +4 -0
- package/index.d.mts +107 -0
- package/package.json +56 -0
package/dist/engine.mjs
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { createRequire as __parheliaCreateRequire } from "node:module"; const require = __parheliaCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// dist/protocol/protocol.js
|
|
4
|
+
var protocol = {
|
|
5
|
+
/**
|
|
6
|
+
* The compiler generation. Bump it when compiled output changes in a way a cached bundle or a
|
|
7
|
+
* CM check must notice. 3: client code (`./client`), browser bundles and first paints.
|
|
8
|
+
*/
|
|
9
|
+
compiledFormat: 3,
|
|
10
|
+
limits: {
|
|
11
|
+
sourceBytes: 204800,
|
|
12
|
+
cssBytes: 65536,
|
|
13
|
+
treeBytes: 524288,
|
|
14
|
+
treeDepth: 64,
|
|
15
|
+
treeNodes: 2e4,
|
|
16
|
+
/** Per component: QuickJS's interrupt deadline. */
|
|
17
|
+
componentMs: 2e3,
|
|
18
|
+
/** Per page: compiles still missing and all renders. */
|
|
19
|
+
pageMs: 5e3,
|
|
20
|
+
/** Per compile, in a compile thread. */
|
|
21
|
+
buildMs: 2e4,
|
|
22
|
+
/** V8 heap of a render thread. */
|
|
23
|
+
threadHeapMb: 128,
|
|
24
|
+
/** QuickJS memory of one tenant's runtime. */
|
|
25
|
+
runtimeMemoryMb: 64,
|
|
26
|
+
/** Render threads (one per active tenant). */
|
|
27
|
+
maxThreads: 4,
|
|
28
|
+
/** Compile threads. */
|
|
29
|
+
maxBuilds: 2
|
|
30
|
+
},
|
|
31
|
+
nodeTypes: ["el", "field", "slot", "icon", "translate", "editorFields", "island", "islandChildren", "fragment"],
|
|
32
|
+
fieldKinds: ["text", "richtext", "picture", "link"],
|
|
33
|
+
errorCodes: ["module-missing", "timeout", "render-error", "oom", "too-large", "rate-limited", "preparing"],
|
|
34
|
+
/** `./client` resolves to the rendering's own client code. */
|
|
35
|
+
serverImports: ["react", "react/jsx-runtime", "@alpaca-headless/alpaca-headless", "./client"],
|
|
36
|
+
clientImports: ["react", "react/jsx-runtime", "react-dom"],
|
|
37
|
+
/**
|
|
38
|
+
* The names client code may import. In the browser they resolve to the head's React, so every
|
|
39
|
+
* name must exist in the minimum React the host package declares (19.2.0); importing any other
|
|
40
|
+
* name fails the compile.
|
|
41
|
+
*/
|
|
42
|
+
browserExports: {
|
|
43
|
+
react: [
|
|
44
|
+
"Activity",
|
|
45
|
+
"Children",
|
|
46
|
+
"Component",
|
|
47
|
+
"Fragment",
|
|
48
|
+
"Profiler",
|
|
49
|
+
"PureComponent",
|
|
50
|
+
"StrictMode",
|
|
51
|
+
"Suspense",
|
|
52
|
+
"cloneElement",
|
|
53
|
+
"createContext",
|
|
54
|
+
"createElement",
|
|
55
|
+
"createRef",
|
|
56
|
+
"forwardRef",
|
|
57
|
+
"isValidElement",
|
|
58
|
+
"lazy",
|
|
59
|
+
"memo",
|
|
60
|
+
"startTransition",
|
|
61
|
+
"use",
|
|
62
|
+
"useActionState",
|
|
63
|
+
"useCallback",
|
|
64
|
+
"useContext",
|
|
65
|
+
"useDebugValue",
|
|
66
|
+
"useDeferredValue",
|
|
67
|
+
"useEffect",
|
|
68
|
+
"useEffectEvent",
|
|
69
|
+
"useId",
|
|
70
|
+
"useImperativeHandle",
|
|
71
|
+
"useInsertionEffect",
|
|
72
|
+
"useLayoutEffect",
|
|
73
|
+
"useMemo",
|
|
74
|
+
"useOptimistic",
|
|
75
|
+
"useReducer",
|
|
76
|
+
"useRef",
|
|
77
|
+
"useState",
|
|
78
|
+
"useSyncExternalStore",
|
|
79
|
+
"useTransition",
|
|
80
|
+
"version"
|
|
81
|
+
],
|
|
82
|
+
"react-dom": [
|
|
83
|
+
"createPortal",
|
|
84
|
+
"flushSync",
|
|
85
|
+
"preconnect",
|
|
86
|
+
"prefetchDNS",
|
|
87
|
+
"preinit",
|
|
88
|
+
"preinitModule",
|
|
89
|
+
"preload",
|
|
90
|
+
"preloadModule",
|
|
91
|
+
"requestFormReset",
|
|
92
|
+
"useFormStatus",
|
|
93
|
+
"version"
|
|
94
|
+
],
|
|
95
|
+
"react/jsx-runtime": ["jsx", "jsxs", "Fragment"]
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/compile-pool.mjs
|
|
100
|
+
import { Worker } from "node:worker_threads";
|
|
101
|
+
var threadUrl = new URL("./compile-thread.mjs", import.meta.url);
|
|
102
|
+
function transient(message) {
|
|
103
|
+
return { ok: false, transient: true, diagnostics: [{ severity: "error", file: "build", message }] };
|
|
104
|
+
}
|
|
105
|
+
var CompilePool = class {
|
|
106
|
+
constructor({ size = 2, compileMs, queueLimit = 20, heapMb = 256 }) {
|
|
107
|
+
this.size = size;
|
|
108
|
+
this.compileMs = compileMs;
|
|
109
|
+
this.queueLimit = queueLimit;
|
|
110
|
+
this.heapMb = heapMb;
|
|
111
|
+
this.idle = [];
|
|
112
|
+
this.all = /* @__PURE__ */ new Set();
|
|
113
|
+
this.queue = [];
|
|
114
|
+
this.nextJob = 1;
|
|
115
|
+
}
|
|
116
|
+
stats() {
|
|
117
|
+
return { threads: this.all.size, idle: this.idle.length, queued: this.queue.length };
|
|
118
|
+
}
|
|
119
|
+
spawn() {
|
|
120
|
+
const worker = new Worker(threadUrl, {
|
|
121
|
+
resourceLimits: { maxOldGenerationSizeMb: this.heapMb },
|
|
122
|
+
env: {},
|
|
123
|
+
stdout: false,
|
|
124
|
+
stderr: false
|
|
125
|
+
});
|
|
126
|
+
const state = { worker, job: null };
|
|
127
|
+
state.ready = new Promise((resolve) => worker.once("message", () => resolve()));
|
|
128
|
+
worker.on("message", (m) => {
|
|
129
|
+
if (m.type === "built" && state.job && state.job.id === m.jobId) {
|
|
130
|
+
const job = state.job;
|
|
131
|
+
state.job = null;
|
|
132
|
+
clearTimeout(job.timer);
|
|
133
|
+
job.resolve(m.result);
|
|
134
|
+
this.release(state);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const fail = () => {
|
|
138
|
+
this.all.delete(state);
|
|
139
|
+
this.idle = this.idle.filter((s) => s !== state);
|
|
140
|
+
if (state.job) {
|
|
141
|
+
const job = state.job;
|
|
142
|
+
state.job = null;
|
|
143
|
+
clearTimeout(job.timer);
|
|
144
|
+
job.resolve(transient("The compile thread stopped."));
|
|
145
|
+
}
|
|
146
|
+
this.pump();
|
|
147
|
+
};
|
|
148
|
+
worker.on("error", fail);
|
|
149
|
+
worker.on("exit", fail);
|
|
150
|
+
this.all.add(state);
|
|
151
|
+
return state;
|
|
152
|
+
}
|
|
153
|
+
release(state) {
|
|
154
|
+
state.worker.unref();
|
|
155
|
+
this.idle.push(state);
|
|
156
|
+
this.pump();
|
|
157
|
+
}
|
|
158
|
+
pump() {
|
|
159
|
+
while (this.queue.length) {
|
|
160
|
+
let state = this.idle.pop();
|
|
161
|
+
if (!state) {
|
|
162
|
+
if (this.all.size >= this.size) return;
|
|
163
|
+
state = this.spawn();
|
|
164
|
+
}
|
|
165
|
+
const job = this.queue.shift();
|
|
166
|
+
this.start(state, job);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async start(state, job) {
|
|
170
|
+
state.worker.ref();
|
|
171
|
+
await state.ready;
|
|
172
|
+
state.job = job;
|
|
173
|
+
const remaining = Math.max(1e3, job.deadline - Date.now());
|
|
174
|
+
job.timer = setTimeout(() => {
|
|
175
|
+
state.job = null;
|
|
176
|
+
job.resolve(transient(`The compile took longer than ${this.compileMs} ms.`));
|
|
177
|
+
state.worker.terminate().catch(() => {
|
|
178
|
+
});
|
|
179
|
+
}, remaining);
|
|
180
|
+
state.worker.postMessage({ type: "build", jobId: job.id, request: job.request });
|
|
181
|
+
}
|
|
182
|
+
/** Compiles one rendering. Never rejects; a failure is { ok: false, diagnostics }. */
|
|
183
|
+
compile(request) {
|
|
184
|
+
if (this.queue.length >= this.queueLimit) {
|
|
185
|
+
return Promise.resolve(transient("Too many compiles are queued. Try again shortly."));
|
|
186
|
+
}
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
this.queue.push({ id: this.nextJob++, request, resolve, deadline: Date.now() + this.compileMs });
|
|
189
|
+
this.pump();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async close() {
|
|
193
|
+
this.queue.length = 0;
|
|
194
|
+
await Promise.all([...this.all].map((s) => s.worker.terminate().catch(() => {
|
|
195
|
+
})));
|
|
196
|
+
this.all.clear();
|
|
197
|
+
this.idle = [];
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/render-pool.mjs
|
|
202
|
+
import { Worker as Worker2 } from "node:worker_threads";
|
|
203
|
+
var threadUrl2 = new URL("./render-thread.mjs", import.meta.url);
|
|
204
|
+
var RenderPool = class {
|
|
205
|
+
constructor({ limits, maxThreads = limits.maxThreads, idleMs = 5 * 6e4, rate = { perSecond: 40, burst: 200 } }) {
|
|
206
|
+
this.limits = limits;
|
|
207
|
+
this.maxThreads = maxThreads;
|
|
208
|
+
this.idleMs = idleMs;
|
|
209
|
+
this.rate = rate;
|
|
210
|
+
this.threads = /* @__PURE__ */ new Map();
|
|
211
|
+
this.waiting = [];
|
|
212
|
+
this.modules = /* @__PURE__ */ new Map();
|
|
213
|
+
this.moduleBytes = 0;
|
|
214
|
+
this.buckets = /* @__PURE__ */ new Map();
|
|
215
|
+
this.nextJob = 1;
|
|
216
|
+
this.sweeper = setInterval(() => this.sweep(), 3e4);
|
|
217
|
+
this.sweeper.unref();
|
|
218
|
+
}
|
|
219
|
+
stats() {
|
|
220
|
+
return {
|
|
221
|
+
threads: [...this.threads.values()].map((t) => ({ tenant: t.tenant, busy: t.busy, lastUsed: t.lastUsed })),
|
|
222
|
+
waiting: this.waiting.length,
|
|
223
|
+
modules: this.modules.size,
|
|
224
|
+
moduleBytes: this.moduleBytes
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
rememberModules(sources) {
|
|
228
|
+
const size = (m) => m.server.length + (m.client?.length ?? 0);
|
|
229
|
+
for (const [buildId, source] of Object.entries(sources || {})) {
|
|
230
|
+
if (!source || typeof source.server !== "string") continue;
|
|
231
|
+
const previous = this.modules.get(buildId);
|
|
232
|
+
if (previous) {
|
|
233
|
+
this.modules.delete(buildId);
|
|
234
|
+
this.moduleBytes -= size(previous);
|
|
235
|
+
}
|
|
236
|
+
const module = { server: source.server };
|
|
237
|
+
if (typeof source.client === "string") {
|
|
238
|
+
module.client = source.client;
|
|
239
|
+
module.clientExports = Array.isArray(source.clientExports) ? source.clientExports.filter((n) => typeof n === "string") : [];
|
|
240
|
+
}
|
|
241
|
+
this.modules.set(buildId, module);
|
|
242
|
+
this.moduleBytes += size(module);
|
|
243
|
+
}
|
|
244
|
+
while (this.moduleBytes > 64 * 1024 * 1024 && this.modules.size > 0) {
|
|
245
|
+
const [oldest, entry] = this.modules.entries().next().value;
|
|
246
|
+
this.modules.delete(oldest);
|
|
247
|
+
this.moduleBytes -= size(entry);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
takeToken(tenant, count) {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
const bucket = this.buckets.get(tenant) || { tokens: this.rate.burst, at: now };
|
|
253
|
+
bucket.tokens = Math.min(this.rate.burst, bucket.tokens + (now - bucket.at) / 1e3 * this.rate.perSecond);
|
|
254
|
+
bucket.at = now;
|
|
255
|
+
this.buckets.set(tenant, bucket);
|
|
256
|
+
if (bucket.tokens < count) return false;
|
|
257
|
+
bucket.tokens -= count;
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
spawn(tenant) {
|
|
261
|
+
const worker = new Worker2(threadUrl2, {
|
|
262
|
+
workerData: { limits: this.limits },
|
|
263
|
+
resourceLimits: { maxOldGenerationSizeMb: this.limits.threadHeapMb, maxYoungGenerationSizeMb: 16 },
|
|
264
|
+
env: {},
|
|
265
|
+
stdout: false,
|
|
266
|
+
stderr: false
|
|
267
|
+
});
|
|
268
|
+
const state = { tenant, worker, busy: false, ready: false, lastUsed: Date.now(), job: null };
|
|
269
|
+
state.readyPromise = new Promise((resolve) => {
|
|
270
|
+
worker.once("message", (m) => {
|
|
271
|
+
if (m.type === "ready") {
|
|
272
|
+
state.ready = true;
|
|
273
|
+
resolve();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
worker.on("message", (m) => this.onMessage(state, m));
|
|
278
|
+
worker.on("error", (err) => this.onDeath(state, err && err.code === "ERR_WORKER_OUT_OF_MEMORY" ? "oom" : "render-error", err));
|
|
279
|
+
worker.on("exit", () => this.onDeath(state, "render-error"));
|
|
280
|
+
this.threads.set(tenant, state);
|
|
281
|
+
return state;
|
|
282
|
+
}
|
|
283
|
+
async acquire(tenant) {
|
|
284
|
+
for (; ; ) {
|
|
285
|
+
let state = this.threads.get(tenant);
|
|
286
|
+
if (state && !state.busy) {
|
|
287
|
+
state.busy = true;
|
|
288
|
+
return state;
|
|
289
|
+
}
|
|
290
|
+
if (!state && this.threads.size < this.maxThreads) {
|
|
291
|
+
state = this.spawn(tenant);
|
|
292
|
+
state.busy = true;
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
if (!state) {
|
|
296
|
+
const idle = [...this.threads.values()].filter((t) => !t.busy).sort((a, b) => a.lastUsed - b.lastUsed)[0];
|
|
297
|
+
if (idle) {
|
|
298
|
+
this.retire(idle);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
await new Promise((resolve) => this.waiting.push(resolve));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
release(state) {
|
|
306
|
+
state.worker.unref();
|
|
307
|
+
state.busy = false;
|
|
308
|
+
state.lastUsed = Date.now();
|
|
309
|
+
const next = this.waiting.shift();
|
|
310
|
+
if (next) next();
|
|
311
|
+
}
|
|
312
|
+
retire(state) {
|
|
313
|
+
this.threads.delete(state.tenant);
|
|
314
|
+
state.retired = true;
|
|
315
|
+
state.worker.terminate().catch(() => {
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
sweep() {
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
for (const state of [...this.threads.values()]) {
|
|
321
|
+
if (!state.busy && now - state.lastUsed > this.idleMs) this.retire(state);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
onMessage(state, message) {
|
|
325
|
+
const job = state.job;
|
|
326
|
+
if (!job || message.jobId !== job.id) return;
|
|
327
|
+
if (message.type === "start") {
|
|
328
|
+
job.current = message.key;
|
|
329
|
+
clearTimeout(job.watchdog);
|
|
330
|
+
job.watchdog = setTimeout(() => this.onDeath(state, "timeout"), this.limits.componentMs + 1500);
|
|
331
|
+
} else if (message.type === "result") {
|
|
332
|
+
clearTimeout(job.watchdog);
|
|
333
|
+
job.current = null;
|
|
334
|
+
job.results.set(message.key, message);
|
|
335
|
+
} else if (message.type === "done") {
|
|
336
|
+
clearTimeout(job.watchdog);
|
|
337
|
+
state.job = null;
|
|
338
|
+
job.resolve();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
onDeath(state, code, err) {
|
|
342
|
+
if (state.dead) return;
|
|
343
|
+
state.dead = true;
|
|
344
|
+
if (this.threads.get(state.tenant) === state) this.threads.delete(state.tenant);
|
|
345
|
+
state.worker.terminate().catch(() => {
|
|
346
|
+
});
|
|
347
|
+
const job = state.job;
|
|
348
|
+
state.job = null;
|
|
349
|
+
if (job) {
|
|
350
|
+
clearTimeout(job.watchdog);
|
|
351
|
+
if (job.current) {
|
|
352
|
+
const message = code === "oom" ? `The rendering exceeded the ${this.limits.threadHeapMb} MB memory limit.` : code === "timeout" ? `The rendering ran longer than ${this.limits.componentMs} ms.` : "The rendering thread stopped unexpectedly.";
|
|
353
|
+
job.results.set(job.current, { key: job.current, ok: false, code, message });
|
|
354
|
+
}
|
|
355
|
+
job.resolve({ died: true });
|
|
356
|
+
}
|
|
357
|
+
if (!state.retired) this.release(state);
|
|
358
|
+
}
|
|
359
|
+
async runOnThread(tenant, components, deadline, fresh) {
|
|
360
|
+
const state = await this.acquire(tenant);
|
|
361
|
+
state.worker.ref();
|
|
362
|
+
try {
|
|
363
|
+
if (!state.ready) await state.readyPromise;
|
|
364
|
+
const job = { id: this.nextJob++, results: /* @__PURE__ */ new Map(), current: null, watchdog: null };
|
|
365
|
+
const done = new Promise((resolve) => {
|
|
366
|
+
job.resolve = resolve;
|
|
367
|
+
});
|
|
368
|
+
state.job = job;
|
|
369
|
+
const modules = {};
|
|
370
|
+
for (const c of components) {
|
|
371
|
+
const m = this.modules.get(c.buildId);
|
|
372
|
+
if (m) modules[c.buildId] = m;
|
|
373
|
+
}
|
|
374
|
+
state.worker.postMessage({ type: "render", jobId: job.id, components, modules, fresh: Boolean(fresh) });
|
|
375
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
376
|
+
let pageTimer;
|
|
377
|
+
const pageTimeout = new Promise((resolve) => {
|
|
378
|
+
pageTimer = setTimeout(() => resolve({ pageTimeout: true }), remaining);
|
|
379
|
+
});
|
|
380
|
+
const outcome = await Promise.race([done, pageTimeout]);
|
|
381
|
+
clearTimeout(pageTimer);
|
|
382
|
+
if (outcome && outcome.pageTimeout) {
|
|
383
|
+
this.onDeath(state, "timeout");
|
|
384
|
+
}
|
|
385
|
+
return job.results;
|
|
386
|
+
} finally {
|
|
387
|
+
if (!state.dead && !state.retired) this.release(state);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Renders every component of one tenant for one page. Returns results in request order.
|
|
392
|
+
* `fresh` runs the job in a context of its own that is discarded afterwards.
|
|
393
|
+
*/
|
|
394
|
+
async renderPage({ tenant, components, sources, deadlineMs, fresh = false }) {
|
|
395
|
+
if (!this.takeToken(tenant, components.length)) {
|
|
396
|
+
return components.map((c) => ({ key: c.key, ok: false, code: "rate-limited", message: "Too many renders for this tenant." }));
|
|
397
|
+
}
|
|
398
|
+
this.rememberModules(sources);
|
|
399
|
+
const deadline = Date.now() + Math.min(deadlineMs || this.limits.pageMs, this.limits.pageMs);
|
|
400
|
+
const results = /* @__PURE__ */ new Map();
|
|
401
|
+
let pending = components.filter((c) => {
|
|
402
|
+
if (this.modules.has(c.buildId)) return true;
|
|
403
|
+
results.set(c.key, { key: c.key, ok: false, code: "module-missing", message: "Module source not provided." });
|
|
404
|
+
return false;
|
|
405
|
+
});
|
|
406
|
+
while (pending.length && Date.now() < deadline) {
|
|
407
|
+
const batch = await this.runOnThread(tenant, pending, deadline, fresh);
|
|
408
|
+
for (const [key, value] of batch) results.set(key, value);
|
|
409
|
+
pending = pending.filter((c) => !results.has(c.key));
|
|
410
|
+
}
|
|
411
|
+
for (const c of pending) {
|
|
412
|
+
results.set(c.key, { key: c.key, ok: false, code: "timeout", message: `The page's rendering budget of ${this.limits.pageMs} ms ran out.` });
|
|
413
|
+
}
|
|
414
|
+
return components.map((c) => {
|
|
415
|
+
const raw = results.get(c.key);
|
|
416
|
+
if (raw && typeof raw.json === "string") {
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(raw.json);
|
|
419
|
+
if (parsed.ok) return { key: c.key, ok: true, tree: parsed.tree, ms: raw.ms };
|
|
420
|
+
return { key: c.key, ok: false, code: parsed.code || "render-error", message: String(parsed.message || ""), logs: parsed.logs, ms: raw.ms };
|
|
421
|
+
} catch {
|
|
422
|
+
return { key: c.key, ok: false, code: "render-error", message: "The render result was not valid JSON." };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { key: c.key, ok: false, code: raw?.code || "render-error", message: raw?.message || "No result.", ms: raw?.ms };
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async close() {
|
|
429
|
+
clearInterval(this.sweeper);
|
|
430
|
+
await Promise.all([...this.threads.values()].map((t) => t.worker.terminate().catch(() => {
|
|
431
|
+
})));
|
|
432
|
+
this.threads.clear();
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/index.mjs
|
|
437
|
+
function createEngine(options = {}) {
|
|
438
|
+
const limits = { ...protocol.limits, ...options.limits || {} };
|
|
439
|
+
const compiler = new CompilePool({
|
|
440
|
+
size: options.compileThreads ?? limits.maxBuilds,
|
|
441
|
+
compileMs: limits.buildMs,
|
|
442
|
+
queueLimit: options.compileQueue ?? 20
|
|
443
|
+
});
|
|
444
|
+
const renderer = new RenderPool({
|
|
445
|
+
limits,
|
|
446
|
+
maxThreads: options.renderThreads ?? limits.maxThreads,
|
|
447
|
+
idleMs: options.idleMs,
|
|
448
|
+
rate: options.rate
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
limits,
|
|
452
|
+
compile: (request) => compiler.compile(request),
|
|
453
|
+
renderPage: (request) => renderer.renderPage(request),
|
|
454
|
+
stats: () => ({ compile: compiler.stats(), render: renderer.stats() }),
|
|
455
|
+
close: () => Promise.all([compiler.close(), renderer.close()]).then(() => void 0)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
export {
|
|
459
|
+
CompilePool,
|
|
460
|
+
RenderPool,
|
|
461
|
+
createEngine,
|
|
462
|
+
protocol
|
|
463
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const protocol: {
|
|
2
|
+
/**
|
|
3
|
+
* The compiler generation. Bump it when compiled output changes in a way a cached bundle or a
|
|
4
|
+
* CM check must notice. 3: client code (`./client`), browser bundles and first paints.
|
|
5
|
+
*/
|
|
6
|
+
readonly compiledFormat: 3;
|
|
7
|
+
readonly limits: {
|
|
8
|
+
readonly sourceBytes: 204800;
|
|
9
|
+
readonly cssBytes: 65536;
|
|
10
|
+
readonly treeBytes: 524288;
|
|
11
|
+
readonly treeDepth: 64;
|
|
12
|
+
readonly treeNodes: 20000;
|
|
13
|
+
/** Per component: QuickJS's interrupt deadline. */
|
|
14
|
+
readonly componentMs: 2000;
|
|
15
|
+
/** Per page: compiles still missing and all renders. */
|
|
16
|
+
readonly pageMs: 5000;
|
|
17
|
+
/** Per compile, in a compile thread. */
|
|
18
|
+
readonly buildMs: 20000;
|
|
19
|
+
/** V8 heap of a render thread. */
|
|
20
|
+
readonly threadHeapMb: 128;
|
|
21
|
+
/** QuickJS memory of one tenant's runtime. */
|
|
22
|
+
readonly runtimeMemoryMb: 64;
|
|
23
|
+
/** Render threads (one per active tenant). */
|
|
24
|
+
readonly maxThreads: 4;
|
|
25
|
+
/** Compile threads. */
|
|
26
|
+
readonly maxBuilds: 2;
|
|
27
|
+
};
|
|
28
|
+
readonly nodeTypes: readonly ["el", "field", "slot", "icon", "translate", "editorFields", "island", "islandChildren", "fragment"];
|
|
29
|
+
readonly fieldKinds: readonly ["text", "richtext", "picture", "link"];
|
|
30
|
+
readonly errorCodes: readonly ["module-missing", "timeout", "render-error", "oom", "too-large", "rate-limited", "preparing"];
|
|
31
|
+
/** `./client` resolves to the rendering's own client code. */
|
|
32
|
+
readonly serverImports: readonly ["react", "react/jsx-runtime", "@alpaca-headless/alpaca-headless", "./client"];
|
|
33
|
+
readonly clientImports: readonly ["react", "react/jsx-runtime", "react-dom"];
|
|
34
|
+
/**
|
|
35
|
+
* The names client code may import. In the browser they resolve to the head's React, so every
|
|
36
|
+
* name must exist in the minimum React the host package declares (19.2.0); importing any other
|
|
37
|
+
* name fails the compile.
|
|
38
|
+
*/
|
|
39
|
+
readonly browserExports: {
|
|
40
|
+
readonly react: readonly ["Activity", "Children", "Component", "Fragment", "Profiler", "PureComponent", "StrictMode", "Suspense", "cloneElement", "createContext", "createElement", "createRef", "forwardRef", "isValidElement", "lazy", "memo", "startTransition", "use", "useActionState", "useCallback", "useContext", "useDebugValue", "useDeferredValue", "useEffect", "useEffectEvent", "useId", "useImperativeHandle", "useInsertionEffect", "useLayoutEffect", "useMemo", "useOptimistic", "useReducer", "useRef", "useState", "useSyncExternalStore", "useTransition", "version"];
|
|
41
|
+
readonly "react-dom": readonly ["createPortal", "flushSync", "preconnect", "prefetchDNS", "preinit", "preinitModule", "preload", "preloadModule", "requestFormReset", "useFormStatus", "version"];
|
|
42
|
+
readonly "react/jsx-runtime": readonly ["jsx", "jsxs", "Fragment"];
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export type Protocol = typeof protocol;
|
|
46
|
+
export type ProtocolLimits = Protocol["limits"];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What a rendering may use and what it produces: the render-tree node types, the imports a
|
|
2
|
+
// rendering may make, and the limits. The engine (@alpaca-headless/dynamic-renderings-engine) bundles
|
|
3
|
+
// this module when it is built. The host compiles and runs renderings itself, so both sides
|
|
4
|
+
// always come from the same release; compiledFormat only tells the CM (and bundle URLs) which
|
|
5
|
+
// compiler a head runs.
|
|
6
|
+
export const protocol = {
|
|
7
|
+
/**
|
|
8
|
+
* The compiler generation. Bump it when compiled output changes in a way a cached bundle or a
|
|
9
|
+
* CM check must notice. 3: client code (`./client`), browser bundles and first paints.
|
|
10
|
+
*/
|
|
11
|
+
compiledFormat: 3,
|
|
12
|
+
limits: {
|
|
13
|
+
sourceBytes: 204800,
|
|
14
|
+
cssBytes: 65536,
|
|
15
|
+
treeBytes: 524288,
|
|
16
|
+
treeDepth: 64,
|
|
17
|
+
treeNodes: 20000,
|
|
18
|
+
/** Per component: QuickJS's interrupt deadline. */
|
|
19
|
+
componentMs: 2000,
|
|
20
|
+
/** Per page: compiles still missing and all renders. */
|
|
21
|
+
pageMs: 5000,
|
|
22
|
+
/** Per compile, in a compile thread. */
|
|
23
|
+
buildMs: 20000,
|
|
24
|
+
/** V8 heap of a render thread. */
|
|
25
|
+
threadHeapMb: 128,
|
|
26
|
+
/** QuickJS memory of one tenant's runtime. */
|
|
27
|
+
runtimeMemoryMb: 64,
|
|
28
|
+
/** Render threads (one per active tenant). */
|
|
29
|
+
maxThreads: 4,
|
|
30
|
+
/** Compile threads. */
|
|
31
|
+
maxBuilds: 2,
|
|
32
|
+
},
|
|
33
|
+
nodeTypes: ["el", "field", "slot", "icon", "translate", "editorFields", "island", "islandChildren", "fragment"],
|
|
34
|
+
fieldKinds: ["text", "richtext", "picture", "link"],
|
|
35
|
+
errorCodes: ["module-missing", "timeout", "render-error", "oom", "too-large", "rate-limited", "preparing"],
|
|
36
|
+
/** `./client` resolves to the rendering's own client code. */
|
|
37
|
+
serverImports: ["react", "react/jsx-runtime", "@alpaca-headless/alpaca-headless", "./client"],
|
|
38
|
+
clientImports: ["react", "react/jsx-runtime", "react-dom"],
|
|
39
|
+
/**
|
|
40
|
+
* The names client code may import. In the browser they resolve to the head's React, so every
|
|
41
|
+
* name must exist in the minimum React the host package declares (19.2.0); importing any other
|
|
42
|
+
* name fails the compile.
|
|
43
|
+
*/
|
|
44
|
+
browserExports: {
|
|
45
|
+
react: [
|
|
46
|
+
"Activity", "Children", "Component", "Fragment", "Profiler", "PureComponent", "StrictMode", "Suspense",
|
|
47
|
+
"cloneElement", "createContext", "createElement", "createRef", "forwardRef", "isValidElement", "lazy", "memo",
|
|
48
|
+
"startTransition", "use", "useActionState", "useCallback", "useContext", "useDebugValue", "useDeferredValue",
|
|
49
|
+
"useEffect", "useEffectEvent", "useId", "useImperativeHandle", "useInsertionEffect", "useLayoutEffect",
|
|
50
|
+
"useMemo", "useOptimistic", "useReducer", "useRef", "useState", "useSyncExternalStore", "useTransition",
|
|
51
|
+
"version",
|
|
52
|
+
],
|
|
53
|
+
"react-dom": [
|
|
54
|
+
"createPortal", "flushSync", "preconnect", "prefetchDNS", "preinit", "preinitModule", "preload",
|
|
55
|
+
"preloadModule", "requestFormReset", "useFormStatus", "version",
|
|
56
|
+
],
|
|
57
|
+
"react/jsx-runtime": ["jsx", "jsxs", "Fragment"],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export type ElementNode = {
|
|
2
|
+
t: "el";
|
|
3
|
+
tag: string;
|
|
4
|
+
props: Record<string, unknown>;
|
|
5
|
+
children: RenderNode[];
|
|
6
|
+
};
|
|
7
|
+
export type FieldNode = {
|
|
8
|
+
t: "field";
|
|
9
|
+
kind: "text" | "richtext" | "picture" | "link";
|
|
10
|
+
field: string;
|
|
11
|
+
tag?: string;
|
|
12
|
+
props: Record<string, unknown>;
|
|
13
|
+
children?: RenderNode[];
|
|
14
|
+
};
|
|
15
|
+
export type SlotNode = {
|
|
16
|
+
t: "slot";
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
export type IconNode = {
|
|
20
|
+
t: "icon";
|
|
21
|
+
name: string;
|
|
22
|
+
props: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
export type TranslateNode = {
|
|
25
|
+
t: "translate";
|
|
26
|
+
key: string;
|
|
27
|
+
};
|
|
28
|
+
export type EditorFieldsNode = {
|
|
29
|
+
t: "editorFields";
|
|
30
|
+
group: string;
|
|
31
|
+
add: string[];
|
|
32
|
+
remove: string[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A component of the rendering's client code (`./client`), named by its export. `firstPaint` is
|
|
36
|
+
* that component rendered once in the engine (initial state, no effects); its `islandChildren`
|
|
37
|
+
* node marks where `children`, rendered by the server code, go.
|
|
38
|
+
*/
|
|
39
|
+
export type IslandNode = {
|
|
40
|
+
t: "island";
|
|
41
|
+
name: string;
|
|
42
|
+
props: Record<string, unknown>;
|
|
43
|
+
children?: RenderNode[];
|
|
44
|
+
firstPaint: RenderNode[];
|
|
45
|
+
};
|
|
46
|
+
export type IslandChildrenNode = {
|
|
47
|
+
t: "islandChildren";
|
|
48
|
+
};
|
|
49
|
+
export type FragmentNode = {
|
|
50
|
+
t: "fragment";
|
|
51
|
+
children: RenderNode[];
|
|
52
|
+
};
|
|
53
|
+
export type RenderNode = string | ElementNode | FieldNode | SlotNode | IconNode | TranslateNode | EditorFieldsNode | IslandNode | IslandChildrenNode | FragmentNode;
|
|
54
|
+
export type ComponentDto = {
|
|
55
|
+
id: string;
|
|
56
|
+
templateId?: string;
|
|
57
|
+
language?: string;
|
|
58
|
+
editing: boolean;
|
|
59
|
+
fields: Record<string, {
|
|
60
|
+
name: string;
|
|
61
|
+
type?: string;
|
|
62
|
+
value: unknown;
|
|
63
|
+
}>;
|
|
64
|
+
placeholders: string[];
|
|
65
|
+
};
|
|
66
|
+
export type RenderResult = {
|
|
67
|
+
key: string;
|
|
68
|
+
ok: true;
|
|
69
|
+
tree: RenderNode[];
|
|
70
|
+
ms?: number;
|
|
71
|
+
} | {
|
|
72
|
+
key: string;
|
|
73
|
+
ok: false;
|
|
74
|
+
code: string;
|
|
75
|
+
message: string;
|
|
76
|
+
logs?: string[];
|
|
77
|
+
ms?: number;
|
|
78
|
+
};
|
|
79
|
+
/** One rendering in the CM's /alpaca/headless/renderings/sources answer. */
|
|
80
|
+
export type RenderingSource = {
|
|
81
|
+
id: string;
|
|
82
|
+
name?: string;
|
|
83
|
+
kind: "component";
|
|
84
|
+
tenant: string;
|
|
85
|
+
code: string;
|
|
86
|
+
clientCode?: string | null;
|
|
87
|
+
/** An administrator approved exactly this code. Publishing targets return approved renderings only. */
|
|
88
|
+
approved: boolean;
|
|
89
|
+
};
|
|
90
|
+
/** The whole /alpaca/headless/renderings/sources answer: the page's renderings and its tenant's theme. */
|
|
91
|
+
export type SourcesResponse = {
|
|
92
|
+
renderings: RenderingSource[];
|
|
93
|
+
theme?: {
|
|
94
|
+
id: string;
|
|
95
|
+
tokens: string;
|
|
96
|
+
approved: boolean;
|
|
97
|
+
} | null;
|
|
98
|
+
};
|
|
99
|
+
/** What a dynamic component needs at render time, computed once per page. */
|
|
100
|
+
export type PreparedComponent = {
|
|
101
|
+
renderingId: string;
|
|
102
|
+
buildId?: string;
|
|
103
|
+
result?: RenderResult;
|
|
104
|
+
css?: string | null;
|
|
105
|
+
cssRejected?: string;
|
|
106
|
+
/** Its compile did not finish within the page budget; it continues in the background. */
|
|
107
|
+
preparing?: boolean;
|
|
108
|
+
error?: string;
|
|
109
|
+
/** First 32 hex digits of the client code's hash, when the rendering has client code. */
|
|
110
|
+
clientHash?: string;
|
|
111
|
+
/** The CM reported the rendering as approved. */
|
|
112
|
+
approved?: boolean;
|
|
113
|
+
};
|