toiljs 0.0.33 → 0.0.36

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 (130) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +1 -0
  3. package/as-pect.config.js +8 -2
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +124 -7
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/auth.d.ts +42 -0
  8. package/build/client/auth.js +179 -0
  9. package/build/client/index.d.ts +5 -1
  10. package/build/client/index.js +3 -1
  11. package/build/client/routing/loader.d.ts +1 -0
  12. package/build/client/routing/loader.js +37 -0
  13. package/build/client/routing/mount.js +32 -1
  14. package/build/client/ssr/markers.d.ts +34 -0
  15. package/build/client/ssr/markers.js +49 -0
  16. package/build/compiler/.tsbuildinfo +1 -1
  17. package/build/compiler/docs.js +88 -1
  18. package/build/compiler/generate.d.ts +2 -0
  19. package/build/compiler/generate.js +2 -2
  20. package/build/compiler/index.js +2 -0
  21. package/build/compiler/ssr-codegen.d.ts +2 -0
  22. package/build/compiler/ssr-codegen.js +36 -0
  23. package/build/compiler/template-build.d.ts +29 -0
  24. package/build/compiler/template-build.js +150 -0
  25. package/build/compiler/template.d.ts +22 -0
  26. package/build/compiler/template.js +169 -0
  27. package/build/devserver/.tsbuildinfo +1 -1
  28. package/build/devserver/cache.d.ts +8 -0
  29. package/build/devserver/cache.js +0 -0
  30. package/build/devserver/crypto.js +15 -0
  31. package/build/devserver/host.js +1 -0
  32. package/build/devserver/index.js +10 -1
  33. package/build/devserver/module.d.ts +1 -0
  34. package/build/devserver/module.js +23 -1
  35. package/docs/README.md +56 -0
  36. package/docs/auth.md +261 -0
  37. package/docs/caching.md +115 -0
  38. package/docs/cookies.md +457 -0
  39. package/docs/crypto.md +130 -0
  40. package/docs/data.md +131 -0
  41. package/docs/getting-started.md +128 -0
  42. package/docs/routing.md +259 -0
  43. package/docs/rpc.md +149 -0
  44. package/docs/ssr.md +184 -0
  45. package/docs/time.md +43 -0
  46. package/examples/basic/client/routes/auth.tsx +198 -0
  47. package/examples/basic/client/routes/cookies.tsx +199 -0
  48. package/examples/basic/client/routes/features/index.tsx +34 -10
  49. package/examples/basic/client/routes/hello.tsx +43 -0
  50. package/examples/basic/client/routes/pq.tsx +135 -0
  51. package/examples/basic/server/AuthTestHandler.ts +15 -0
  52. package/examples/basic/server/AuthVerifyHandler.ts +23 -0
  53. package/examples/basic/server/CacheHandler.ts +25 -0
  54. package/examples/basic/server/DecoCache.ts +18 -0
  55. package/examples/basic/server/FastTrapHandler.ts +8 -0
  56. package/examples/basic/server/README.md +19 -0
  57. package/examples/basic/server/SpinHandler.ts +18 -0
  58. package/examples/basic/server/SsrGreetingRender.ts +27 -0
  59. package/examples/basic/server/authexample-main.ts +8 -0
  60. package/examples/basic/server/authtest-main.ts +8 -0
  61. package/examples/basic/server/authverify-main.ts +8 -0
  62. package/examples/basic/server/cache-main.ts +8 -0
  63. package/examples/basic/server/core/AppHandler.ts +290 -0
  64. package/examples/basic/server/core/store.ts +31 -0
  65. package/examples/basic/server/deco-main.ts +18 -0
  66. package/examples/basic/server/main.ts +13 -2
  67. package/examples/basic/server/models/NewPlayer.ts +5 -0
  68. package/examples/basic/server/models/Player.ts +8 -0
  69. package/examples/basic/server/models/ScoreDelta.ts +5 -0
  70. package/examples/basic/server/models/Standings.ts +7 -0
  71. package/examples/basic/server/routes/Auth.ts +184 -0
  72. package/examples/basic/server/routes/Leaderboard.ts +20 -0
  73. package/examples/basic/server/routes/Players.ts +53 -0
  74. package/examples/basic/server/routes/PqDemo.ts +109 -0
  75. package/examples/basic/server/routes/Session.ts +73 -0
  76. package/examples/basic/server/scheduled/README.md +7 -0
  77. package/examples/basic/server/services/Stats.ts +11 -0
  78. package/examples/basic/server/services/remotes.ts +7 -0
  79. package/examples/basic/server/spin-main.ts +13 -0
  80. package/examples/basic/server/ssr/greeting.slots.ts +19 -0
  81. package/examples/basic/server/ssr-main.ts +18 -0
  82. package/examples/basic/server/toil-server-env.d.ts +94 -0
  83. package/examples/basic/server/trap-main.ts +8 -0
  84. package/package.json +5 -3
  85. package/server/globals/auth.ts +281 -0
  86. package/server/runtime/README.md +61 -0
  87. package/server/runtime/env/Server.ts +12 -0
  88. package/server/runtime/exports/index.ts +17 -0
  89. package/server/runtime/exports/render.ts +51 -0
  90. package/server/runtime/http/base64.ts +104 -0
  91. package/server/runtime/http/cookie.ts +416 -0
  92. package/server/runtime/http/cookies.ts +197 -0
  93. package/server/runtime/http/date.ts +72 -0
  94. package/server/runtime/http/percent.ts +76 -0
  95. package/server/runtime/http/securecookies.ts +224 -0
  96. package/server/runtime/index.ts +17 -0
  97. package/server/runtime/request.ts +24 -0
  98. package/server/runtime/response.ts +85 -0
  99. package/server/runtime/ssr/Ssr.ts +43 -0
  100. package/server/runtime/ssr/encode.ts +110 -0
  101. package/server/runtime/ssr/escape.ts +83 -0
  102. package/server/runtime/ssr/slots.ts +144 -0
  103. package/server/runtime/time.ts +29 -0
  104. package/src/cli/create.ts +159 -14
  105. package/src/client/auth.ts +322 -0
  106. package/src/client/index.ts +5 -1
  107. package/src/client/routing/loader.ts +56 -0
  108. package/src/client/routing/mount.tsx +37 -1
  109. package/src/client/ssr/markers.tsx +140 -0
  110. package/src/compiler/docs.ts +88 -1
  111. package/src/compiler/generate.ts +2 -2
  112. package/src/compiler/index.ts +5 -0
  113. package/src/compiler/ssr-codegen.ts +85 -0
  114. package/src/compiler/template-build.ts +275 -0
  115. package/src/compiler/template.ts +265 -0
  116. package/src/devserver/cache.ts +0 -0
  117. package/src/devserver/crypto.ts +23 -0
  118. package/src/devserver/host.ts +4 -0
  119. package/src/devserver/index.ts +21 -1
  120. package/src/devserver/module.ts +39 -1
  121. package/test/assembly/cookie.spec.ts +302 -0
  122. package/test/assembly/example.spec.ts +5 -1
  123. package/test/assembly/ssr.spec.ts +94 -0
  124. package/test/devserver.test.ts +48 -4
  125. package/test/fixtures/bignum-wire/spec.ts +27 -0
  126. package/test/rpc-bignum-wire.test.ts +164 -0
  127. package/test/ssr-render.test.ts +128 -0
  128. package/test/ssr-template.test.tsx +348 -0
  129. package/examples/basic/server/HelloHandler.ts +0 -42
  130. package/examples/basic/server/api.ts +0 -137
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Build-time edge-SSR template extraction.
3
+ *
4
+ * After a route component is rendered to HTML with the marker components in
5
+ * sentinel mode (`__setSsrBuild(true)`), the output carries PUA sentinel tokens
6
+ * at every hole. This module scans that HTML, strips the tokens, records their
7
+ * BYTE offsets, and emits:
8
+ *
9
+ * - `<route>.tmpl` — the stripped static scaffold (React's own bytes, holes
10
+ * removed); the edge mmaps this.
11
+ * - `<route>.slots` — the binary manifest the Rust host parses
12
+ * (`toil-backend/src/host/template/manifest.rs`).
13
+ *
14
+ * The repeat-region row sub-template + the holes' binding metadata are also
15
+ * captured (for the guest `render()` codegen), but those do NOT go in `.slots`
16
+ * (the host only needs insertion points; the guest pre-stamps repeat rows).
17
+ *
18
+ * The pure functions here (`extractFromHtml`, `encodeSlots`, `coherenceHash`,
19
+ * `reactEscapeHtml`, `spliceTemplate`) are deterministic and unit-tested; the
20
+ * `extractTemplates` orchestration drives a short-lived Vite SSR server, the
21
+ * same pattern as `ssg.ts`.
22
+ */
23
+
24
+ import { createHash } from 'node:crypto';
25
+
26
+ /** PUA sentinel framing, mirroring `src/client/ssr/markers.tsx`. */
27
+ const START = String.fromCharCode(0xe000);
28
+ const END = String.fromCharCode(0xe002);
29
+
30
+ export type SlotKind = 'text' | 'raw' | 'attr' | 'repeat';
31
+
32
+ /** Byte value of a kind on the wire / in `.slots`. Mirrors host `SlotKind`. */
33
+ export function kindByte(kind: SlotKind): number {
34
+ switch (kind) {
35
+ case 'text':
36
+ return 0;
37
+ case 'raw':
38
+ return 1;
39
+ case 'attr':
40
+ return 2;
41
+ case 'repeat':
42
+ return 3;
43
+ }
44
+ }
45
+
46
+ /** One extracted hole. `offset` is a byte offset into the enclosing template
47
+ * (the `.tmpl` for top-level slots, or the row sub-template for nested ones). */
48
+ export interface SlotRecord {
49
+ id: string;
50
+ kind: SlotKind;
51
+ offset: number;
52
+ /** Repeat only: the captured single-row scaffold (bytes) ... */
53
+ rowTemplate?: Buffer;
54
+ /** ... and the holes inside that row, offsets relative to `rowTemplate`. */
55
+ rowSlots?: SlotRecord[];
56
+ }
57
+
58
+ export interface Extracted {
59
+ /** Stripped static scaffold; the mmap'd `.tmpl`. */
60
+ tmpl: Buffer;
61
+ /** Top-level holes in document order, byte offsets into `tmpl`. */
62
+ slots: SlotRecord[];
63
+ }
64
+
65
+ interface ScanResult {
66
+ text: string;
67
+ byteLen: number;
68
+ slots: SlotRecord[];
69
+ }
70
+
71
+ /** Scan one HTML string, stripping sentinel tokens and recording hole offsets.
72
+ * Recurses into repeat regions to capture the row sub-template. */
73
+ function scan(html: string): ScanResult {
74
+ let out = '';
75
+ let byteLen = 0;
76
+ const slots: SlotRecord[] = [];
77
+ let i = 0;
78
+
79
+ const emit = (chunk: string): void => {
80
+ out += chunk;
81
+ byteLen += Buffer.byteLength(chunk, 'utf8');
82
+ };
83
+
84
+ while (i < html.length) {
85
+ const start = html.indexOf(START, i);
86
+ if (start === -1) {
87
+ emit(html.slice(i));
88
+ break;
89
+ }
90
+ if (start > i) emit(html.slice(i, start));
91
+
92
+ const kindChar = html[start + 1];
93
+ const tokEnd = html.indexOf(END, start + 2);
94
+ if (tokEnd === -1) throw new Error('toil ssr: unterminated sentinel token');
95
+ const id = html.slice(start + 2, tokEnd);
96
+ const afterTok = tokEnd + 1;
97
+
98
+ if (kindChar === 'R') {
99
+ // Repeat region: collect everything up to the matching close token.
100
+ const closeTok = START + 'r' + id + END;
101
+ const closeIdx = html.indexOf(closeTok, afterTok);
102
+ if (closeIdx === -1) {
103
+ throw new Error(`toil ssr: unterminated repeat region "${id}"`);
104
+ }
105
+ const innerHtml = html.slice(afterTok, closeIdx);
106
+ const inner = scan(innerHtml);
107
+ slots.push({
108
+ id,
109
+ kind: 'repeat',
110
+ offset: byteLen, // region collapses to a zero-width insertion point
111
+ rowTemplate: Buffer.from(inner.text, 'utf8'),
112
+ rowSlots: inner.slots,
113
+ });
114
+ i = closeIdx + closeTok.length;
115
+ continue;
116
+ }
117
+
118
+ const kind: SlotKind | null =
119
+ kindChar === 't' ? 'text' : kindChar === 'h' ? 'raw' : kindChar === 'a' ? 'attr' : null;
120
+ if (kind === null) {
121
+ throw new Error(`toil ssr: unknown sentinel kind "${kindChar ?? ''}"`);
122
+ }
123
+ slots.push({ id, kind, offset: byteLen });
124
+ i = afterTok;
125
+ }
126
+
127
+ return { text: out, byteLen, slots };
128
+ }
129
+
130
+ /** Strip sentinels from a rendered-with-markers HTML string into a `.tmpl` +
131
+ * ordered slot records. */
132
+ export function extractFromHtml(html: string): Extracted {
133
+ const r = scan(html);
134
+ return { tmpl: Buffer.from(r.text, 'utf8'), slots: r.slots };
135
+ }
136
+
137
+ /** Assign stable numeric slot ids to top-level holes in document order. These
138
+ * are the ids the host `.slots` and the guest `Slot` enum share. */
139
+ export function assignSlotIds(slots: SlotRecord[]): Map<string, number> {
140
+ const ids = new Map<string, number>();
141
+ let next = 0;
142
+ for (const s of slots) {
143
+ if (!ids.has(s.id)) ids.set(s.id, next++);
144
+ }
145
+ return ids;
146
+ }
147
+
148
+ /** Encode the `.slots` binary manifest the Rust host parses. Only top-level
149
+ * slots are emitted (the host's insertion points); repeat row data is for the
150
+ * guest codegen, not the host. */
151
+ export function encodeSlots(
152
+ tmplLen: number,
153
+ hash: Buffer,
154
+ slots: SlotRecord[],
155
+ slotIds: Map<string, number>,
156
+ ): Buffer {
157
+ if (hash.length !== 32) throw new Error('toil ssr: coherence hash must be 32 bytes');
158
+ const buf = Buffer.alloc(4 + 2 + 2 + 4 + 32 + 2 + slots.length * 8);
159
+ let o = 0;
160
+ buf.write('TSLT', o, 'ascii'); // magic, read as u32 LE on the host
161
+ o += 4;
162
+ buf.writeUInt16LE(1, o); // version
163
+ o += 2;
164
+ buf.writeUInt16LE(0, o); // flags
165
+ o += 2;
166
+ buf.writeUInt32LE(tmplLen, o);
167
+ o += 4;
168
+ hash.copy(buf, o);
169
+ o += 32;
170
+ buf.writeUInt16LE(slots.length, o);
171
+ o += 2;
172
+ for (const s of slots) {
173
+ const id = slotIds.get(s.id);
174
+ if (id === undefined) throw new Error(`toil ssr: no slot id for "${s.id}"`);
175
+ buf.writeUInt32LE(s.offset, o);
176
+ o += 4;
177
+ buf.writeUInt16LE(id, o);
178
+ o += 2;
179
+ buf.writeUInt8(kindByte(s.kind), o);
180
+ o += 1;
181
+ buf.writeUInt8(0, o); // reserved
182
+ o += 1;
183
+ }
184
+ return buf;
185
+ }
186
+
187
+ /** Canonical serialisation of the slot structure (recursive), used by the
188
+ * coherence hash so a change to any hole, kind, or nesting rotates it. */
189
+ function canonicalManifest(slots: SlotRecord[]): string {
190
+ return JSON.stringify(
191
+ slots.map((s) => ({
192
+ id: s.id,
193
+ kind: s.kind,
194
+ offset: s.offset,
195
+ row: s.rowSlots ? canonicalManifest(s.rowSlots) : undefined,
196
+ rowLen: s.rowTemplate ? s.rowTemplate.length : undefined,
197
+ })),
198
+ );
199
+ }
200
+
201
+ /** Coherence hash binding the `.tmpl` + slot structure. Stored in `.slots` and
202
+ * baked into the guest; the host 500s on a mismatch (deploy skew). */
203
+ export function coherenceHash(tmpl: Buffer, slots: SlotRecord[]): Buffer {
204
+ return createHash('sha256')
205
+ .update(tmpl)
206
+ .update('\0')
207
+ .update(canonicalManifest(slots), 'utf8')
208
+ .digest();
209
+ }
210
+
211
+ /**
212
+ * React-exact HTML escape (`react-dom/server` `escapeTextForBrowser`, regex
213
+ * `/["'&<>]/`). The SAME function React uses for text AND attributes. Must stay
214
+ * byte-identical to the guest's `server/runtime/ssr/escape.ts`, or hydration
215
+ * mismatches. Used by the golden byte-identity test to simulate the guest.
216
+ */
217
+ export function reactEscapeHtml(s: string): string {
218
+ let out = '';
219
+ let last = 0;
220
+ for (let i = 0; i < s.length; i++) {
221
+ let rep: string;
222
+ switch (s.charCodeAt(i)) {
223
+ case 34:
224
+ rep = '&quot;';
225
+ break;
226
+ case 38:
227
+ rep = '&amp;';
228
+ break;
229
+ case 39:
230
+ rep = '&#x27;';
231
+ break;
232
+ case 60:
233
+ rep = '&lt;';
234
+ break;
235
+ case 62:
236
+ rep = '&gt;';
237
+ break;
238
+ default:
239
+ continue;
240
+ }
241
+ out += s.slice(last, i) + rep;
242
+ last = i + 1;
243
+ }
244
+ return last === 0 ? s : out + s.slice(last);
245
+ }
246
+
247
+ /** Generic splice: interleave template slices with hole values at ascending
248
+ * byte offsets. Mirrors the Rust host `assemble`; used by the golden test (and
249
+ * any tooling that needs to materialise a full page from a template + values).
250
+ * `values` maps a byte offset to the bytes inserted there (offsets may repeat
251
+ * is not allowed; pass them in `slots` order). */
252
+ export function spliceTemplate(
253
+ tmpl: Buffer,
254
+ inserts: { offset: number; value: Buffer }[],
255
+ ): Buffer {
256
+ const parts: Buffer[] = [];
257
+ let prev = 0;
258
+ for (const ins of inserts) {
259
+ if (ins.offset > prev) parts.push(tmpl.subarray(prev, ins.offset));
260
+ if (ins.value.length > 0) parts.push(ins.value);
261
+ prev = ins.offset;
262
+ }
263
+ if (tmpl.length > prev) parts.push(tmpl.subarray(prev));
264
+ return Buffer.concat(parts);
265
+ }
Binary file
@@ -17,6 +17,8 @@
17
17
 
18
18
  import * as nodeCrypto from 'node:crypto';
19
19
 
20
+ import { ml_dsa44 } from '@btc-vision/post-quantum/ml-dsa.js';
21
+
20
22
  import type { MemoryRef } from './host.js';
21
23
 
22
24
  // --- ABI id tables (must match the std + Rust backend) ----------------------
@@ -211,6 +213,27 @@ export function buildCryptoImports(
211
213
 
212
214
  'crypto.derive_bits': (h: number, pp: number, pl: number, lengthBits: number): number =>
213
215
  deriveBitsOp(cs, ref, h, pp, pl, lengthBits),
216
+
217
+ // ML-DSA-44 (FIPS 204) verify for the auth primitive. Mirrors the edge
218
+ // host (`mldsa_verify_import.rs`): same size asserts, 1/0/neg result.
219
+ // Backed by the same noble lib the client signs with, so dev == prod.
220
+ 'crypto.mldsa_verify': (
221
+ pkPtr: number, pkLen: number,
222
+ msgPtr: number, msgLen: number,
223
+ sigPtr: number, sigLen: number,
224
+ ctxPtr: number, ctxLen: number,
225
+ ): number => {
226
+ if (pkLen !== 1312 || sigLen !== 2420 || ctxLen > 255) return -4;
227
+ try {
228
+ const pk = new Uint8Array(readBytes(ref, pkPtr, pkLen));
229
+ const msg = new Uint8Array(readBytes(ref, msgPtr, msgLen));
230
+ const sig = new Uint8Array(readBytes(ref, sigPtr, sigLen));
231
+ const ctx = new Uint8Array(readBytes(ref, ctxPtr, ctxLen));
232
+ return ml_dsa44.verify(sig, msg, pk, { context: ctx }) ? 1 : 0;
233
+ } catch {
234
+ return -1;
235
+ }
236
+ },
214
237
  };
215
238
  }
216
239
 
@@ -137,6 +137,10 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
137
137
 
138
138
  thread_spawn: (_startArg: number): number => -1,
139
139
 
140
+ // `Date.now()` -> wall-clock milliseconds, matching the edge host.
141
+ // The guest divides by 1000 for Unix seconds (sessions, challenges).
142
+ 'Date.now': (): number => Date.now(),
143
+
140
144
  // Web Crypto host functions (`env.crypto.*`), backed by Node's
141
145
  // `crypto`. The dev server skips metering, so these charge nothing.
142
146
  ...buildCryptoImports(ref, state.crypto),
@@ -25,6 +25,7 @@ import path from 'node:path';
25
25
  import { Server, type Request, type Response } from '@dacely/hyper-express';
26
26
  import pc from 'picocolors';
27
27
 
28
+ import { applyCacheRule, lookupCache } from './cache.js';
28
29
  import { METHOD_CODES, type EnvelopeRequest } from './envelope.js';
29
30
  import { WasmServerModule } from './module.js';
30
31
  import { proxyToVite, wireWebsocketProxy, type ViteTarget } from './proxy.js';
@@ -210,10 +211,29 @@ export async function startDevServer(options: DevServerOptions): Promise<Running
210
211
 
211
212
  if (dispatchable && module.available) {
212
213
  const envelopeReq = await toEnvelopeRequest(request);
214
+ // Honor the tenant cache directive locally, same rules as the
215
+ // edge: serve an identical request from the per-process cache,
216
+ // else dispatch and apply/strip the directive on the response.
217
+ const cacheHost = request.headers.host ?? 'dev';
218
+ const hasAuth =
219
+ request.headers.cookie !== undefined || request.headers.authorization !== undefined;
220
+ const cached = lookupCache(cacheHost, request.method, request.url, envelopeReq.body);
221
+ if (cached !== null) {
222
+ sendWasmResponse(response, root, cached);
223
+ return;
224
+ }
213
225
  try {
214
226
  const result = module.dispatch(envelopeReq);
215
227
  if (!result.unhandled) {
216
- sendWasmResponse(response, root, result);
228
+ const finalized = applyCacheRule(
229
+ cacheHost,
230
+ request.method,
231
+ request.url,
232
+ envelopeReq.body,
233
+ hasAuth,
234
+ result,
235
+ );
236
+ sendWasmResponse(response, root, finalized);
217
237
  return;
218
238
  }
219
239
  } catch (e) {
@@ -52,11 +52,12 @@ interface HandleExports {
52
52
 
53
53
  /** Host functions the dev server provides under `env` (see `host.ts`). */
54
54
  const PROVIDED_IMPORTS = new Set([
55
- 'abort', 'set_status', 'set_header', 'respond_file', 'thread_spawn',
55
+ 'abort', 'set_status', 'set_header', 'respond_file', 'thread_spawn', 'Date.now',
56
56
  // Web Crypto host functions (see ./crypto.ts).
57
57
  'crypto.fill_random', 'crypto.random_uuid', 'crypto.take_result', 'crypto.digest',
58
58
  'crypto.import_key', 'crypto.export_key', 'crypto.encrypt', 'crypto.decrypt',
59
59
  'crypto.sign', 'crypto.verify', 'crypto.derive_bits',
60
+ 'crypto.mldsa_verify',
60
61
  ]);
61
62
 
62
63
  export class WasmServerModule {
@@ -151,6 +152,43 @@ export class WasmServerModule {
151
152
  };
152
153
  }
153
154
 
155
+ /**
156
+ * Run one request through the guest `render` entrypoint (edge SSR). Returns
157
+ * the raw values-envelope bytes (status + template_hash + headers + slot
158
+ * values) that the edge splices against the template manifest. Throws if
159
+ * the module has no `render` export. Mirrors {@link dispatch}'s fresh-
160
+ * instance + grow-and-write-envelope contract.
161
+ */
162
+ dispatchRender(req: EnvelopeRequest): Uint8Array {
163
+ if (this.module === null) throw new Error(`server wasm not loaded (${this.wasmPath})`);
164
+
165
+ const envelope = encodeRequestEnvelope(req);
166
+ const ref: MemoryRef = { memory: null };
167
+ const state = freshDispatchState();
168
+ const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
169
+ const exports = instance.exports as unknown as HandleExports & {
170
+ render?: (reqOfs: number, reqLen: number) => bigint;
171
+ };
172
+ if (typeof exports.render !== 'function')
173
+ throw new Error(`guest wasm has no 'render' export (${this.wasmPath})`);
174
+ ref.memory = exports.memory;
175
+
176
+ const reqOfs = exports.memory.buffer.byteLength;
177
+ exports.memory.grow(Math.ceil(envelope.length / WASM_PAGE) || 1);
178
+ Buffer.from(exports.memory.buffer).set(envelope, reqOfs);
179
+
180
+ const packed = exports.render(reqOfs, envelope.length);
181
+ const { ptr, len } = unpackHandleResult(packed);
182
+
183
+ const memSize = exports.memory.buffer.byteLength;
184
+ if (len > memSize || ptr + len > memSize)
185
+ throw new Error(
186
+ `guest returned out-of-bounds values: ptr=${String(ptr)} len=${String(len)}`,
187
+ );
188
+ // Copy out of the (about-to-be-dropped) instance's memory.
189
+ return new Uint8Array(exports.memory.buffer, ptr, len).slice();
190
+ }
191
+
154
192
  /** Fail instantiation up front, with names, when the guest needs imports we do not provide. */
155
193
  private assertImportSurface(module: WebAssembly.Module): void {
156
194
  const missing = WebAssembly.Module.imports(module)