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.
- package/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +15 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +311 -118
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +21 -1
- package/build/compiler/template-build.js +110 -26
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +80 -0
- package/build/devserver/db/database.js +1032 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +121 -0
- package/build/devserver/db/types.js +52 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +19 -24
- package/build/devserver/index.js +11 -165
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
- package/build/devserver/{host.js → runtime/host.js} +51 -7
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
- package/build/devserver/{module.js → runtime/module.js} +34 -1
- package/build/devserver/server.d.ts +23 -0
- package/build/devserver/server.js +223 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +3 -3
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +40 -3
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +247 -46
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +108 -0
- package/src/devserver/db/database.ts +1633 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +139 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +31 -287
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/{host.ts → runtime/host.ts} +98 -7
- package/src/devserver/{module.ts → runtime/module.ts} +47 -1
- package/src/devserver/server.ts +393 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +510 -14
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/email-preview.test.ts +6 -1
- package/test/example-guestbook.test.ts +43 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/pqauth-e2e.test.ts +1 -1
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /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': ():
|
|
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 '
|
|
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;
|