toiljs 0.0.59 → 0.0.61

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 (158) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +311 -118
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -0
  32. package/build/devserver/db/catalog.js +80 -0
  33. package/build/devserver/db/database.d.ts +80 -0
  34. package/build/devserver/db/database.js +1032 -0
  35. package/build/devserver/db/index.d.ts +3 -0
  36. package/build/devserver/db/index.js +3 -0
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +121 -0
  40. package/build/devserver/db/types.js +52 -0
  41. package/build/devserver/email/index.js +1 -1
  42. package/build/devserver/index.d.ts +19 -24
  43. package/build/devserver/index.js +11 -165
  44. package/build/devserver/mstore/store.d.ts +18 -0
  45. package/build/devserver/mstore/store.js +82 -0
  46. package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
  47. package/build/devserver/{host.js → runtime/host.js} +51 -7
  48. package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
  49. package/build/devserver/{module.js → runtime/module.js} +34 -1
  50. package/build/devserver/server.d.ts +23 -0
  51. package/build/devserver/server.js +223 -0
  52. package/build/devserver/ssr.d.ts +25 -0
  53. package/build/devserver/ssr.js +114 -0
  54. package/build/devserver/wasm/sections.d.ts +2 -0
  55. package/build/devserver/wasm/sections.js +42 -0
  56. package/build/devserver/wasm/surface.d.ts +18 -0
  57. package/build/devserver/wasm/surface.js +41 -0
  58. package/docs/README.md +4 -4
  59. package/docs/auth-todo.md +6 -6
  60. package/docs/caching.md +5 -5
  61. package/docs/cli.md +15 -0
  62. package/docs/client.md +40 -0
  63. package/docs/crypto.md +4 -4
  64. package/docs/data.md +6 -6
  65. package/docs/email.md +28 -28
  66. package/docs/environment.md +10 -10
  67. package/docs/index.md +26 -0
  68. package/docs/ratelimit.md +10 -10
  69. package/docs/routing.md +2 -2
  70. package/docs/server.md +61 -0
  71. package/docs/ssr.md +561 -113
  72. package/docs/styling.md +22 -0
  73. package/docs/time.md +3 -3
  74. package/eslint.config.js +10 -1
  75. package/examples/basic/client/components/Header.tsx +3 -0
  76. package/examples/basic/client/routes/features/actions.tsx +0 -2
  77. package/examples/basic/client/routes/hello.tsx +89 -19
  78. package/examples/basic/client/styles/main.css +48 -0
  79. package/examples/basic/server/SsrHelloRender.ts +97 -0
  80. package/examples/basic/server/main.ts +5 -0
  81. package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
  82. package/examples/basic/server/streams/Echo.ts +49 -0
  83. package/package.json +12 -10
  84. package/scripts/gen-toil-docs.mjs +96 -0
  85. package/server/runtime/time.ts +3 -3
  86. package/src/cli/create.ts +40 -3
  87. package/src/cli/db.ts +158 -0
  88. package/src/cli/diagnostics.ts +19 -0
  89. package/src/cli/doctor.ts +20 -0
  90. package/src/cli/index.ts +10 -0
  91. package/src/cli/update.ts +58 -0
  92. package/src/client/index.ts +1 -1
  93. package/src/client/routing/mount.tsx +18 -2
  94. package/src/client/ssr/markers.tsx +22 -0
  95. package/src/compiler/config.ts +88 -2
  96. package/src/compiler/docs.ts +47 -308
  97. package/src/compiler/index.ts +236 -32
  98. package/src/compiler/ssr-codegen.ts +1 -1
  99. package/src/compiler/template-build.ts +247 -46
  100. package/src/compiler/toil-docs.generated.ts +26 -0
  101. package/src/devserver/daemon/catalog.ts +120 -0
  102. package/src/devserver/daemon/cron.ts +87 -0
  103. package/src/devserver/daemon/host.ts +224 -0
  104. package/src/devserver/daemon/index.ts +349 -0
  105. package/src/devserver/db/catalog.ts +108 -0
  106. package/src/devserver/db/database.ts +1633 -0
  107. package/src/devserver/db/index.ts +18 -0
  108. package/src/devserver/db/routeKinds.ts +147 -0
  109. package/src/devserver/db/types.ts +139 -0
  110. package/src/devserver/email/index.ts +1 -1
  111. package/src/devserver/index.ts +31 -287
  112. package/src/devserver/mstore/store.ts +121 -0
  113. package/src/devserver/{host.ts → runtime/host.ts} +98 -7
  114. package/src/devserver/{module.ts → runtime/module.ts} +47 -1
  115. package/src/devserver/server.ts +393 -0
  116. package/src/devserver/ssr.ts +166 -0
  117. package/src/devserver/wasm/sections.ts +59 -0
  118. package/src/devserver/wasm/surface.ts +88 -0
  119. package/test/daemon-build.test.ts +198 -0
  120. package/test/daemon-catalog.test.ts +265 -0
  121. package/test/daemon-emulation.test.ts +216 -0
  122. package/test/db.test.ts +0 -0
  123. package/test/devserver-database.test.ts +510 -14
  124. package/test/devserver-pqauth.test.ts +1 -1
  125. package/test/devserver-secrets.test.ts +5 -1
  126. package/test/doctor.test.ts +13 -0
  127. package/test/email-preview.test.ts +6 -1
  128. package/test/example-guestbook.test.ts +43 -1
  129. package/test/fixtures/daemon-app.ts +56 -0
  130. package/test/global-setup.ts +17 -0
  131. package/test/pqauth-e2e.test.ts +1 -1
  132. package/test/ssr-render.test.ts +94 -27
  133. package/test/ssr-template.test.tsx +44 -1
  134. package/vitest.config.ts +3 -0
  135. package/build/devserver/database.d.ts +0 -8
  136. package/build/devserver/database.js +0 -418
  137. package/src/devserver/database.ts +0 -618
  138. /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
  139. /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
  140. /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
  141. /package/build/devserver/{env.js → config/env.js} +0 -0
  142. /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
  143. /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
  144. /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
  145. /package/build/devserver/{cache.js → http/cache.js} +0 -0
  146. /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
  147. /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
  148. /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
  149. /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
  150. /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
  151. /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
  152. /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
  153. /package/src/devserver/{env.ts → config/env.ts} +0 -0
  154. /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
  155. /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
  156. /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
  157. /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
  158. /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Dev MemoryStore: a single in-memory `Map` with per-entry TTL, one per process,
3
+ * persisted nowhere (ephemeral by definition). Backs the `mstore.*` host imports
4
+ * (RECONCILIATION Part 4, F2: HANDLELESS, ttl in SECONDS, inline drain). Keys are
5
+ * auto-scoped to host+region; the dev process is one host/region, so the key is
6
+ * used verbatim. Shared by streams (Phase 4) AND the daemon (both reference the
7
+ * same `devMemoryStore` singleton), matching doc 06's "shared across
8
+ * streams/handlers on the same region".
9
+ *
10
+ * TTL is enforced LAZILY on read (no background sweep), mirroring the dev DB's
11
+ * no-background-thread design. The error space is RECONCILIATION Part 3's 0x03xx
12
+ * registry; the host-import layer (daemon/host.ts) maps these results onto the
13
+ * Part 3 negative-return bridge.
14
+ */
15
+
16
+ interface MStoreEntry {
17
+ value: Buffer;
18
+ /** `0` means no TTL; otherwise the epoch-ms the entry expires at. */
19
+ expiresAtMs: number;
20
+ }
21
+
22
+ export class DevMemoryStore {
23
+ private readonly map = new Map<string, MStoreEntry>();
24
+
25
+ private now(): number {
26
+ return Date.now();
27
+ }
28
+
29
+ /** The live entry for `key`, collecting it lazily if its TTL has passed. */
30
+ private live(key: string): MStoreEntry | null {
31
+ const e = this.map.get(key);
32
+ if (e === undefined) return null;
33
+ if (e.expiresAtMs !== 0 && e.expiresAtMs <= this.now()) {
34
+ this.map.delete(key);
35
+ return null;
36
+ }
37
+ return e;
38
+ }
39
+
40
+ private exp(ttlSecs: number): number {
41
+ return ttlSecs > 0 ? this.now() + ttlSecs * 1000 : 0;
42
+ }
43
+
44
+ /** The value, or `null` (=> 0x0301 MSTORE_NOT_FOUND). */
45
+ get(key: string): Buffer | null {
46
+ const e = this.live(key);
47
+ return e ? e.value : null;
48
+ }
49
+
50
+ set(key: string, value: Buffer, ttlSecs: number): void {
51
+ this.map.set(key, { value: Buffer.from(value), expiresAtMs: this.exp(ttlSecs) });
52
+ }
53
+
54
+ delete(key: string): boolean {
55
+ return this.map.delete(key);
56
+ }
57
+
58
+ /** Add `delta` to the i64 stored at `key`; `null` => 0x0306 MSTORE_NOT_A_NUMBER. */
59
+ incr(key: string, delta: bigint, ttlSecs: number): bigint | null {
60
+ const e = this.live(key);
61
+ let cur = 0n;
62
+ if (e !== null) {
63
+ const s = e.value.toString('utf8').trim();
64
+ if (!/^-?\d+$/.test(s)) return null;
65
+ try {
66
+ cur = BigInt(s);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ const next = BigInt.asIntN(64, cur + delta);
72
+ this.map.set(key, {
73
+ value: Buffer.from(next.toString(), 'utf8'),
74
+ // An incr on an existing key keeps its TTL unless a new one is given.
75
+ expiresAtMs: ttlSecs > 0 ? this.exp(ttlSecs) : (e?.expiresAtMs ?? 0),
76
+ });
77
+ return next;
78
+ }
79
+
80
+ /** `expect === null` means expect-absent (the dev mapping of `expect_len == 0`).
81
+ * Returns `false` => 0x0304 MSTORE_CONFLICT. */
82
+ cas(key: string, expect: Buffer | null, next: Buffer, ttlSecs: number): boolean {
83
+ const e = this.live(key);
84
+ if (expect === null) {
85
+ if (e !== null) return false; // expect-absent, but present
86
+ } else if (e === null || !e.value.equals(expect)) {
87
+ return false; // expect-match failed
88
+ }
89
+ this.map.set(key, { value: Buffer.from(next), expiresAtMs: this.exp(ttlSecs) });
90
+ return true;
91
+ }
92
+
93
+ /** Re-arm the TTL of a live key; `false` => key absent (0x0301). */
94
+ expire(key: string, ttlSecs: number): boolean {
95
+ const e = this.live(key);
96
+ if (!e) return false;
97
+ e.expiresAtMs = this.exp(ttlSecs);
98
+ return true;
99
+ }
100
+
101
+ /**
102
+ * Prefix walk. `cursor` is an opaque resume index; a stale cursor (one that
103
+ * points past the current live key set after deletions) returns `null`
104
+ * (=> 0x0307 MSTORE_SCAN_BUSY). Returns the next cursor + the matched keys.
105
+ */
106
+ scan(prefix: string, cursor: bigint): { next: bigint; keys: string[] } | null {
107
+ const live = [...this.map.keys()].filter((k) => this.live(k) !== null && k.startsWith(prefix));
108
+ live.sort();
109
+ const start = Number(cursor);
110
+ if (start < 0 || start > live.length) return null; // stale cursor
111
+ const batch = live.slice(start);
112
+ return { next: BigInt(live.length), keys: batch };
113
+ }
114
+
115
+ /** Test-only: drop all entries. */
116
+ __reset(): void {
117
+ this.map.clear();
118
+ }
119
+ }
120
+
121
+ export const devMemoryStore = new DevMemoryStore();
@@ -17,12 +17,12 @@
17
17
  * `WebAssembly.Instance`, so offering the full surface costs nothing.
18
18
  */
19
19
 
20
+ import { devEnvGet, devEnvGetSecure } from '../config/env.js';
21
+ import { ratelimitCheck } from '../config/ratelimit.js';
22
+ import { buildDatabaseImports, type DbDevState, freshDbState } from '../db/index.js';
23
+ import { EmailStatus, getEmailService } from '../email/index.js';
24
+ import { parseEmailBlob } from '../email/wire.js';
20
25
  import { buildCryptoImports, type CryptoState, freshCryptoState } from './crypto.js';
21
- import { buildDatabaseImports, type DbDevState, freshDbState } from './database.js';
22
- import { EmailStatus, getEmailService } from './email/index.js';
23
- import { parseEmailBlob } from './email/wire.js';
24
- import { devEnvGet, devEnvGetSecure } from './env.js';
25
- import { ratelimitCheck } from './ratelimit.js';
26
26
 
27
27
  /** Limits identical to the edge's `set_header` / `respond_file` bounds. */
28
28
  const MAX_TOTAL_HEADERS_BYTES = 64 * 1024;
@@ -91,13 +91,33 @@ function mem(ref: MemoryRef): Buffer {
91
91
  }
92
92
 
93
93
  /** Bounds-checked byte read out of guest linear memory. */
94
- function readBytes(ref: MemoryRef, ptr: number, len: number): Buffer {
94
+ export function readBytes(ref: MemoryRef, ptr: number, len: number): Buffer {
95
95
  const m = mem(ref);
96
96
  if (ptr < 0 || len < 0 || ptr + len > m.length)
97
97
  throw new Error(`host import read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
98
98
  return m.subarray(ptr, ptr + len);
99
99
  }
100
100
 
101
+ /**
102
+ * Bounds-checked write of a variable-length result into a guest out-buffer, with
103
+ * the edge's inline-drain return protocol: the byte length on success, or `-1`
104
+ * (STATUS_TOO_SMALL) when `outCap` is too small (the guest retries with a bigger
105
+ * buffer). Used by the handleless `mstore.*` imports (RECONCILIATION Part 4 F2).
106
+ */
107
+ export function writeBytesOut(
108
+ ref: MemoryRef,
109
+ bytes: Buffer,
110
+ outPtr: number,
111
+ outCap: number,
112
+ ): number {
113
+ if (bytes.length > outCap) return -1; // TOO_SMALL
114
+ const m = mem(ref);
115
+ if (outPtr < 0 || outPtr + bytes.length > m.length)
116
+ throw new Error('host import write out of bounds');
117
+ bytes.copy(m, outPtr);
118
+ return bytes.length;
119
+ }
120
+
101
121
  /**
102
122
  * Read a ToilScript string (UTF-16LE payload, byte length in the u32 at
103
123
  * `ptr - 4`). Used by `abort`, whose pointers reference string objects rather
@@ -169,6 +189,77 @@ function envLookup(
169
189
  return bytes.length;
170
190
  }
171
191
 
192
+ /**
193
+ * The portion of the `env.*` request surface that is SHARED by the daemon (cold)
194
+ * box: panic hook, `Environment.get`/`getSecure`, `email_send`, `thread_spawn`,
195
+ * and `Date.now`. It deliberately EXCLUDES the response/stream functions a cold
196
+ * box must not have (`set_status`/`set_header`/`respond_file`/`client_ip`/
197
+ * `ratelimit_check`), which stay in {@link buildHostImports}. None of these read
198
+ * the per-dispatch response state, so they need only `ref`. The crypto and DB
199
+ * namespaces are spread on top by each box's loader (they carry their own state).
200
+ */
201
+ export function buildEnvImports(
202
+ ref: MemoryRef,
203
+ _state: { crypto: CryptoState; db: DbDevState },
204
+ ): Record<string, (...a: never[]) => unknown> {
205
+ return {
206
+ abort: (msgPtr: number, filePtr: number, line: number, col: number): void => {
207
+ throw new WasmAbortError(
208
+ readGuestString(ref, msgPtr),
209
+ readGuestString(ref, filePtr),
210
+ line,
211
+ col,
212
+ );
213
+ },
214
+
215
+ // `Environment.get` / `getSecure`: copy one tenant env value into the
216
+ // guest buffer. Returns the byte length (0 = present-but-empty), -1 if
217
+ // the buffer is too small (the guest retries bigger), -2 if absent.
218
+ env_get: (keyPtr: number, keyLen: number, outPtr: number, outCap: number): number =>
219
+ envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
220
+ env_get_secure: (
221
+ keyPtr: number,
222
+ keyLen: number,
223
+ outPtr: number,
224
+ outCap: number,
225
+ ): number => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
226
+
227
+ thread_spawn: (_startArg: number): number => -1,
228
+
229
+ // `Date.now()` -> wall-clock milliseconds, matching the edge host.
230
+ 'Date.now': (): bigint => BigInt(Date.now()),
231
+
232
+ // `env::email_send`: the FULL email pipeline in dev. A daemon may send
233
+ // mail, so this stays in the shared subset (00 B2 / doc 08 AN-8).
234
+ email_send: (reqPtr: number, reqLen: number): number => {
235
+ const raw = readBytes(ref, reqPtr, reqLen);
236
+ const svc = getEmailService();
237
+ if (svc === null) {
238
+ const to = parseEmailBlob(raw)?.to ?? '<unparsed>';
239
+ process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
240
+ return EmailStatus.Sent;
241
+ }
242
+ const { status, parsed } = svc.prepare(raw);
243
+ if (parsed === null) {
244
+ process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
245
+ return status;
246
+ }
247
+ void svc
248
+ .deliver(parsed)
249
+ .then((s) => {
250
+ const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
251
+ process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
252
+ })
253
+ .catch((e: unknown) => {
254
+ process.stdout.write(
255
+ ` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`,
256
+ );
257
+ });
258
+ return EmailStatus.Sent; // optimistic; sync wasm can't await the send
259
+ },
260
+ };
261
+ }
262
+
172
263
  /**
173
264
  * Build the `env` import object for one instance. `state` collects what the
174
265
  * imperative imports produce during a dispatch; bind a fresh state per request.
@@ -307,7 +398,7 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
307
398
 
308
399
  // `Date.now()` -> wall-clock milliseconds, matching the edge host.
309
400
  // The guest divides by 1000 for Unix seconds (sessions, challenges).
310
- 'Date.now': (): number => Date.now(),
401
+ 'Date.now': (): bigint => BigInt(Date.now()),
311
402
 
312
403
  // Web Crypto host functions (`env.crypto.*`), backed by Node's
313
404
  // `crypto`. The dev server skips metering, so these charge nothing.
@@ -13,12 +13,14 @@
13
13
 
14
14
  import fs from 'node:fs';
15
15
 
16
+ import { DbFunctionKind, persistDb, setDbCatalog } from '../db/index.js';
17
+ import { parseRouteKinds, routeKindForRequest, type RouteKindEntry } from '../db/routeKinds.js';
16
18
  import {
17
19
  decodeResponseEnvelope,
18
20
  encodeRequestEnvelope,
19
21
  type EnvelopeRequest,
20
22
  unpackHandleResult,
21
- } from './envelope.js';
23
+ } from '../http/envelope.js';
22
24
  import { buildHostImports, freshDispatchState, type MemoryRef } from './host.js';
23
25
 
24
26
  export { WasmAbortError } from './host.js';
@@ -34,6 +36,21 @@ export const UNHANDLED_HEADER = 'x-toil-unhandled';
34
36
 
35
37
  const WASM_PAGE = 65536;
36
38
 
39
+ function dbKindForHttpMethod(method: string): DbFunctionKind {
40
+ switch (method.toUpperCase()) {
41
+ case 'GET':
42
+ case 'HEAD':
43
+ case 'OPTIONS':
44
+ return DbFunctionKind.Query;
45
+ case 'POST':
46
+ case 'PUT':
47
+ case 'PATCH':
48
+ case 'DELETE':
49
+ default:
50
+ return DbFunctionKind.Action;
51
+ }
52
+ }
53
+
37
54
  /** The shaped outcome of one guest dispatch. */
38
55
  export interface WasmDispatchResult {
39
56
  readonly status: number;
@@ -100,6 +117,8 @@ const PROVIDED_IMPORTS = new Set([
100
117
  'data.counter_get',
101
118
  'data.counter_add',
102
119
  'data.append',
120
+ 'data.append_once',
121
+ 'data.enqueue',
103
122
  'data.latest',
104
123
  'data.capacity_set_total',
105
124
  'data.capacity_available',
@@ -107,11 +126,14 @@ const PROVIDED_IMPORTS = new Set([
107
126
  'data.capacity_confirm',
108
127
  'data.capacity_cancel',
109
128
  'data.take_result',
129
+ 'data.result_schema_version',
130
+ 'data.write_allowed',
110
131
  ]);
111
132
 
112
133
  export class WasmServerModule {
113
134
  private module: WebAssembly.Module | null = null;
114
135
  private loadedMtimeMs = -1;
136
+ private routeKinds: readonly RouteKindEntry[] = [];
115
137
 
116
138
  constructor(private readonly wasmPath: string) {}
117
139
 
@@ -132,6 +154,7 @@ export class WasmServerModule {
132
154
  mtimeMs = fs.statSync(this.wasmPath).mtimeMs;
133
155
  } catch {
134
156
  this.module = null;
157
+ this.routeKinds = [];
135
158
  this.loadedMtimeMs = -1;
136
159
  return false;
137
160
  }
@@ -141,6 +164,10 @@ export class WasmServerModule {
141
164
  const module = new WebAssembly.Module(bytes);
142
165
  this.assertImportSurface(module);
143
166
  this.assertExportSurface(module);
167
+ // Refresh collection -> current schema_version so writes stamp the live layout;
168
+ // after a @data type evolves + rebuild, old on-disk rows now look out of date.
169
+ setDbCatalog(bytes);
170
+ this.routeKinds = parseRouteKinds(bytes);
144
171
  this.module = module;
145
172
  this.loadedMtimeMs = mtimeMs;
146
173
  return true;
@@ -159,6 +186,20 @@ export class WasmServerModule {
159
186
  const ref: MemoryRef = { memory: null };
160
187
  const state = freshDispatchState();
161
188
  state.clientIp = req.clientIp ?? '';
189
+ // Enforce per-route DB-kind gating ONLY when the guest declares its route
190
+ // kinds (the `toildb.route_kinds` custom section). A guest built with a
191
+ // toolchain that does not emit that section leaves `routeKinds` empty;
192
+ // inferring a kind from the HTTP method and enforcing it would wrongly
193
+ // reject legitimate bounded reads (e.g. a GET that reads `events.latest`,
194
+ // a scan-class op denied in `Query`). With no declarations we keep the
195
+ // unenforced default (`Job`, see `freshDbState`).
196
+ const routeKind = routeKindForRequest(this.routeKinds, req.method, req.path);
197
+ state.db.functionKind =
198
+ this.routeKinds.length === 0
199
+ ? DbFunctionKind.Job
200
+ : routeKind === DbFunctionKind.Query
201
+ ? DbFunctionKind.Query
202
+ : dbKindForHttpMethod(req.method);
162
203
  const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
163
204
  const exports = instance.exports as unknown as HandleExports;
164
205
  ref.memory = exports.memory;
@@ -193,6 +234,10 @@ export class WasmServerModule {
193
234
 
194
235
  const unhandled = headers.some(([n]) => n.toLowerCase() === UNHANDLED_HEADER);
195
236
 
237
+ // Flush any DB writes this request made to disk, so dev data survives a
238
+ // restart (and a crash never loses an already-served write).
239
+ persistDb();
240
+
196
241
  return {
197
242
  status,
198
243
  headers: headers.filter(([n]) => n.toLowerCase() !== UNHANDLED_HEADER),
@@ -216,6 +261,7 @@ export class WasmServerModule {
216
261
  const ref: MemoryRef = { memory: null };
217
262
  const state = freshDispatchState();
218
263
  state.clientIp = req.clientIp ?? '';
264
+ state.db.functionKind = DbFunctionKind.Query;
219
265
  const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
220
266
  const exports = instance.exports as unknown as HandleExports & {
221
267
  render?: (reqOfs: number, reqLen: number) => bigint;