toiljs 0.0.58 → 0.0.60
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/CHANGELOG.md +19 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +309 -116
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/catalog.d.ts +1 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +64 -0
- package/build/devserver/db/database.js +662 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/types.d.ts +58 -0
- package/build/devserver/db/types.js +20 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +9 -24
- package/build/devserver/index.js +4 -165
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +1 -1
- package/build/devserver/{host.js → runtime/host.js} +6 -6
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +1 -1
- package/build/devserver/{module.js → runtime/module.js} +8 -1
- package/build/devserver/server.d.ts +17 -0
- package/build/devserver/server.js +164 -0
- package/docs/time.md +2 -2
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/package.json +5 -2
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +38 -1
- 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/devserver/db/catalog.ts +100 -0
- package/src/devserver/db/database.ts +1169 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/types.ts +76 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +19 -287
- package/src/devserver/{host.ts → runtime/host.ts} +6 -6
- package/src/devserver/{module.ts → runtime/module.ts} +13 -1
- package/src/devserver/server.ts +292 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +114 -9
- 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/example-guestbook.test.ts +43 -1
- package/test/pqauth-e2e.test.ts +1 -1
- 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,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev ToilDB subsystem: the {@link DevDatabase} class + its process
|
|
3
|
+
* singleton, the per-request state, the catalog parser, and the `data.*` host
|
|
4
|
+
* imports. The production edge backs the SAME imports with ScyllaDB.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
__resetDbForTests,
|
|
9
|
+
__setDbCatalogForTests,
|
|
10
|
+
buildDatabaseImports,
|
|
11
|
+
configureDbPersistence,
|
|
12
|
+
DevDatabase,
|
|
13
|
+
devDb,
|
|
14
|
+
persistDb,
|
|
15
|
+
setDbCatalog,
|
|
16
|
+
} from './database.js';
|
|
17
|
+
export { parseCatalog } from './catalog.js';
|
|
18
|
+
export { type DbDevState, freshDbState } from './types.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types, limits, and ABI return codes for the dev ToilDB emulation
|
|
3
|
+
* (see `./database.ts`). The wire ABI mirrors the production edge byte-for-byte
|
|
4
|
+
* (toildb/ABI.md): every byte region is a (ptr,len) into guest memory; returns
|
|
5
|
+
* are `>= 0` success (a length/handle/flag), `-1` too-small, `-2` absent,
|
|
6
|
+
* `<= -1000` a typed error (`-(1000+TDLnnn)`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Per-request data state: resolved handles + the last variable-length result +
|
|
10
|
+
* the schema_version of that result's row (surfaced via result_schema_version). */
|
|
11
|
+
export interface DbDevState {
|
|
12
|
+
handles: string[];
|
|
13
|
+
lastResult: Buffer | null;
|
|
14
|
+
lastResultVersion: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function freshDbState(): DbDevState {
|
|
18
|
+
return { handles: [], lastResult: null, lastResultVersion: -1 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A finite-resource escrow: a ceiling + a set of reservations, each held (in
|
|
22
|
+
* flight, TTL'd) or confirmed (a permanent consume). Both count against available;
|
|
23
|
+
* a confirmed reservation never expires. Mirrors `toildb::capacity::Escrow`. */
|
|
24
|
+
export interface Reservation {
|
|
25
|
+
amount: bigint;
|
|
26
|
+
expiresMs: number;
|
|
27
|
+
confirmed: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface CapLedger {
|
|
30
|
+
total: bigint;
|
|
31
|
+
reservations: Map<bigint, Reservation>;
|
|
32
|
+
nextId: bigint;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The on-disk snapshot shape: dev data + its versions, JSON with base64 buffers. */
|
|
36
|
+
export interface DbSnapshot {
|
|
37
|
+
store: Record<string, { v: string; sv: number }>; // records + unique owners (+ schema_version)
|
|
38
|
+
views: Record<string, { v: string; sv: number }>;
|
|
39
|
+
members: Record<string, Record<string, { v: string; sv: number }>>;
|
|
40
|
+
counters: Record<string, string>;
|
|
41
|
+
events: Record<string, { v: string; sv: number }[]>;
|
|
42
|
+
eventDedup: Record<string, string[]>;
|
|
43
|
+
capacity: Record<
|
|
44
|
+
string,
|
|
45
|
+
{
|
|
46
|
+
total: string;
|
|
47
|
+
nextId: string;
|
|
48
|
+
reservations: [string, { amount: string; expiresMs: number; confirmed: boolean }][];
|
|
49
|
+
}
|
|
50
|
+
>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Edge caps (toildb::capacity::escrow): bound the reservation count + the hold TTL. */
|
|
54
|
+
export const MAX_RESERVATIONS = 4096;
|
|
55
|
+
export const MAX_RESERVATION_TTL_MS = 86_400_000; // 24h
|
|
56
|
+
|
|
57
|
+
export const MAX_NAME = 512;
|
|
58
|
+
export const MAX_KEY = 4096;
|
|
59
|
+
export const MAX_VALUE = 256 * 1024;
|
|
60
|
+
|
|
61
|
+
// i64 saturation bounds (the edge `MemEngine`/`ScyllaEngine` counters are i64).
|
|
62
|
+
const I64_MIN = -(2n ** 63n);
|
|
63
|
+
const I64_MAX = 2n ** 63n - 1n;
|
|
64
|
+
export function satI64(v: bigint): bigint {
|
|
65
|
+
return v < I64_MIN ? I64_MIN : v > I64_MAX ? I64_MAX : v;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Return codes, mirroring the edge ABI (`toildb::observe::diagnostics`): a typed
|
|
69
|
+
// error is `-(1000 + TDLnnn)`; a plain absence is ABSENT (-2), not a typed error.
|
|
70
|
+
export const ABSENT = -2; // NotFound / absent
|
|
71
|
+
export const TOO_SMALL = -1;
|
|
72
|
+
export const INVALID_HANDLE = -1001; // TDL001
|
|
73
|
+
export const ALREADY_EXISTS = -1003; // TDL003 (create on an existing key)
|
|
74
|
+
export const CONFLICT = -1004; // TDL004 (e.g. unique release by a non-owner)
|
|
75
|
+
export const CODEC_ERR = -1006; // TDL006 (e.g. a non-positive reserve amount)
|
|
76
|
+
export const TOO_MANY_KEYS = -1020; // TDL020 (get_many over the per-call cap)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* optimistically; an async-capable host (a future self-host) can `await deliver()`
|
|
10
10
|
* for the true status.
|
|
11
11
|
*/
|
|
12
|
-
import { loadEnvFiles } from '../dotenv.js';
|
|
12
|
+
import { loadEnvFiles } from '../config/dotenv.js';
|
|
13
13
|
import { EmailCaps } from './caps.js';
|
|
14
14
|
import { type ResolvedEmailConfig, resolveEmailConfig } from './config.js';
|
|
15
15
|
import { type OutboundMessage, sendVia } from './providers.js';
|
package/src/devserver/index.ts
CHANGED
|
@@ -1,298 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* the same stack as `toiljs/backend`) that dispatches HTTP requests into the
|
|
4
|
-
* ToilScript-compiled server wasm exactly like the production edge does, and
|
|
5
|
-
* proxies everything the server does not claim to an internal Vite dev server,
|
|
6
|
-
* so dev keeps 100% of Vite's behavior (HMR, transforms, toolbar endpoints,
|
|
7
|
-
* public assets, SPA fallback).
|
|
2
|
+
* Public barrel for the toiljs WASM dev server. The folder is organized into:
|
|
8
3
|
*
|
|
9
|
-
*
|
|
4
|
+
* - `server.ts` the uWebSockets.js front + `startDevServer` entrypoint
|
|
5
|
+
* - `runtime/` the wasm module loader, host import surface, and crypto shims
|
|
6
|
+
* - `db/` the in-process ToilDB emulation (the {@link DevDatabase} class)
|
|
7
|
+
* - `http/` the envelope codec, response cache, and Vite proxy
|
|
8
|
+
* - `config/` the dotenv loader, `Environment.get` source, and rate limiter
|
|
9
|
+
* - `email/` the dev / self-host email pipeline
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* ▼ │
|
|
15
|
-
* Vite dev server (loopback) ◄──────────────┘
|
|
16
|
-
*
|
|
17
|
-
* Dev intentionally skips the edge's metering, gas, pooling and snapshot-reset
|
|
18
|
-
* machinery; the ABI (envelope layout, `handle(ofs, len) -> i64`, host import
|
|
19
|
-
* surface, trap isolation) is identical so a server that runs here runs there.
|
|
11
|
+
* The production edge backs the SAME wasm ABI (envelope layout, `handle(ofs,
|
|
12
|
+
* len) -> i64`, host import surface, trap isolation), so a server that runs here
|
|
13
|
+
* runs there. See `server.ts` for the request flow.
|
|
20
14
|
*/
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import { type Request, type Response, Server } from '@dacely/hyper-express';
|
|
26
|
-
import pc from 'picocolors';
|
|
27
|
-
|
|
28
|
-
import type { EmailBackendConfig } from 'toiljs/shared';
|
|
29
|
-
|
|
30
|
-
import { applyCacheRule, lookupCache } from './cache.js';
|
|
31
|
-
import { initEmailService } from './email/index.js';
|
|
32
|
-
import { type EnvelopeRequest, METHOD_CODES } from './envelope.js';
|
|
33
|
-
import { WasmServerModule } from './module.js';
|
|
34
|
-
import { proxyToVite, type ViteTarget, wireWebsocketProxy } from './proxy.js';
|
|
16
|
+
export { startDevServer } from './server.js';
|
|
17
|
+
export type { DevServerOptions, RunningDevServer } from './server.js';
|
|
35
18
|
|
|
36
19
|
export {
|
|
37
20
|
METHOD_CODES,
|
|
38
21
|
encodeRequestEnvelope,
|
|
39
22
|
decodeResponseEnvelope,
|
|
40
23
|
unpackHandleResult,
|
|
41
|
-
} from './envelope.js';
|
|
42
|
-
export type { EnvelopeRequest, EnvelopeResponse } from './envelope.js';
|
|
43
|
-
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './module.js';
|
|
44
|
-
export type { WasmDispatchResult } from './module.js';
|
|
45
|
-
export { buildHostImports, freshDispatchState } from './host.js';
|
|
46
|
-
export type { DispatchState, MemoryRef } from './host.js';
|
|
47
|
-
export type { ViteTarget } from './proxy.js';
|
|
48
|
-
|
|
49
|
-
const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Paths that are Vite's own by construction; skipping the wasm round-trip for
|
|
53
|
-
* them keeps the hot path of module serving untouched. Everything else is
|
|
54
|
-
* offered to the server first (it answers or yields via the unhandled marker).
|
|
55
|
-
*/
|
|
56
|
-
const VITE_PREFIXES = ['/@', '/node_modules/', '/__toil/'];
|
|
57
|
-
|
|
58
|
-
/** Minimal type map for `respond_file` bodies when the guest set no content-type. */
|
|
59
|
-
const MIME: Readonly<Record<string, string>> = {
|
|
60
|
-
'.html': 'text/html; charset=utf-8',
|
|
61
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
62
|
-
'.mjs': 'text/javascript; charset=utf-8',
|
|
63
|
-
'.css': 'text/css; charset=utf-8',
|
|
64
|
-
'.json': 'application/json; charset=utf-8',
|
|
65
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
66
|
-
'.svg': 'image/svg+xml',
|
|
67
|
-
'.png': 'image/png',
|
|
68
|
-
'.jpg': 'image/jpeg',
|
|
69
|
-
'.jpeg': 'image/jpeg',
|
|
70
|
-
'.webp': 'image/webp',
|
|
71
|
-
'.avif': 'image/avif',
|
|
72
|
-
'.gif': 'image/gif',
|
|
73
|
-
'.ico': 'image/x-icon',
|
|
74
|
-
'.wasm': 'application/wasm',
|
|
75
|
-
'.woff2': 'font/woff2',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/** Options for {@link startDevServer}. */
|
|
79
|
-
export interface DevServerOptions {
|
|
80
|
-
/** Project root; `respond_file` paths resolve against it (and may not escape it). */
|
|
81
|
-
readonly root: string;
|
|
82
|
-
/** Public listening port (the one the browser opens). */
|
|
83
|
-
readonly port: number;
|
|
84
|
-
/** Bind host. Default `127.0.0.1`. */
|
|
85
|
-
readonly host?: string;
|
|
86
|
-
/** Absolute path to the ToilScript server wasm (toilconfig `targets.release.outFile`). */
|
|
87
|
-
readonly wasmFile: string;
|
|
88
|
-
/** The internal Vite dev server to proxy unclaimed traffic to. */
|
|
89
|
-
readonly vite: ViteTarget;
|
|
90
|
-
/** Max request body bytes. Default 8 MB. */
|
|
91
|
-
readonly maxBodyLength?: number;
|
|
92
|
-
/**
|
|
93
|
-
* The `toil.config.ts` `server.email` section (non-secret). When set (and the
|
|
94
|
-
* API key is in `.env.secrets`), `EmailService.send` really sends in dev;
|
|
95
|
-
* otherwise it stays a log-only mock. See `./email`.
|
|
96
|
-
*/
|
|
97
|
-
readonly email?: EmailBackendConfig;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** A running dev server. */
|
|
101
|
-
export interface RunningDevServer {
|
|
102
|
-
readonly port: number;
|
|
103
|
-
readonly host: string;
|
|
104
|
-
/** Gracefully shuts the front server down (the Vite server is owned by the caller). */
|
|
105
|
-
close(): Promise<void>;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** True for requests that belong to Vite by construction (never offered to the wasm). */
|
|
109
|
-
function isViteInternal(url: string): boolean {
|
|
110
|
-
return VITE_PREFIXES.some((p) => url.startsWith(p));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Resolves a guest `respond_file` path inside `root`, refusing traversal outside it. */
|
|
114
|
-
function resolveSendfile(root: string, file: string): string | null {
|
|
115
|
-
const resolved = path.resolve(root, file);
|
|
116
|
-
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
|
|
117
|
-
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) return null;
|
|
118
|
-
return resolved;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Builds the envelope request for one incoming HTTP request. */
|
|
122
|
-
async function toEnvelopeRequest(request: Request): Promise<EnvelopeRequest> {
|
|
123
|
-
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
124
|
-
const body = hasBody ? new Uint8Array(await request.buffer()) : new Uint8Array(0);
|
|
125
|
-
// Dev parity for `client_ip`: the edge keys on the unspoofable socket peer,
|
|
126
|
-
// but the dev server has no DPDK socket, so best-effort from a proxy's
|
|
127
|
-
// `x-forwarded-for`, else localhost, so `ctx.clientIp()` returns a value.
|
|
128
|
-
const xff = request.headers['x-forwarded-for'];
|
|
129
|
-
const clientIp =
|
|
130
|
-
typeof xff === 'string' && xff.length > 0 ? xff.split(',')[0]!.trim() : '127.0.0.1';
|
|
131
|
-
return {
|
|
132
|
-
method: request.method,
|
|
133
|
-
// `url` keeps the query string; the guest's RouteContext parses it off the path.
|
|
134
|
-
path: request.url,
|
|
135
|
-
headers: Object.entries(request.headers),
|
|
136
|
-
body,
|
|
137
|
-
clientIp,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Sends a shaped wasm response, mirroring the edge's response defaults. */
|
|
142
|
-
function sendWasmResponse(
|
|
143
|
-
response: Response,
|
|
144
|
-
root: string,
|
|
145
|
-
result: {
|
|
146
|
-
status: number;
|
|
147
|
-
headers: readonly (readonly [string, string])[];
|
|
148
|
-
body: Uint8Array;
|
|
149
|
-
sendfile: string | null;
|
|
150
|
-
},
|
|
151
|
-
): void {
|
|
152
|
-
response.status(result.status);
|
|
153
|
-
let hasContentType = false;
|
|
154
|
-
for (const [name, value] of result.headers) {
|
|
155
|
-
if (name.toLowerCase() === 'content-type') hasContentType = true;
|
|
156
|
-
response.header(name, value);
|
|
157
|
-
}
|
|
158
|
-
response.header('server', 'toil-dev');
|
|
159
|
-
|
|
160
|
-
if (result.sendfile !== null) {
|
|
161
|
-
const file = resolveSendfile(root, result.sendfile);
|
|
162
|
-
if (file === null) {
|
|
163
|
-
response.status(404).send('not found\n');
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (!hasContentType) {
|
|
167
|
-
// The edge defaults file bodies to application/octet-stream; in dev we
|
|
168
|
-
// guess from the extension so a guest-served asset renders in the browser.
|
|
169
|
-
response.header(
|
|
170
|
-
'content-type',
|
|
171
|
-
MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
response.sendFile(file);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!hasContentType) response.header('content-type', 'text/plain; charset=utf-8');
|
|
179
|
-
response.send(Buffer.from(result.body.buffer, result.body.byteOffset, result.body.length));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Starts the front server. The caller owns the Vite dev server (start it on a
|
|
184
|
-
* loopback port first) and the toilscript rebuild watcher; this watches only
|
|
185
|
-
* the wasm artifact and hot-swaps the compiled module when it changes.
|
|
186
|
-
*/
|
|
187
|
-
export async function startDevServer(options: DevServerOptions): Promise<RunningDevServer> {
|
|
188
|
-
const host = options.host ?? '127.0.0.1';
|
|
189
|
-
const root = path.resolve(options.root);
|
|
190
|
-
|
|
191
|
-
// Wire the email service from toil.config `server.email` + `.env.secrets`
|
|
192
|
-
// (TOIL_EMAIL_*). Configured -> real sends; otherwise the import stays a
|
|
193
|
-
// log-only mock. A partial-but-invalid config logs why it stayed off.
|
|
194
|
-
const emailInit = initEmailService(root, options.email);
|
|
195
|
-
if (emailInit.service !== null) {
|
|
196
|
-
process.stdout.write(pc.dim(` ✉ email enabled: ${emailInit.note}`) + '\n');
|
|
197
|
-
} else if (emailInit.note !== null) {
|
|
198
|
-
process.stdout.write(pc.yellow(' ! ') + pc.dim(`email off: ${emailInit.note}`) + '\n');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const module = new WasmServerModule(options.wasmFile);
|
|
202
|
-
|
|
203
|
-
let warnedMissing = false;
|
|
204
|
-
let loadedOnce = false;
|
|
205
|
-
const refresh = (): void => {
|
|
206
|
-
try {
|
|
207
|
-
if (module.refresh() && loadedOnce) {
|
|
208
|
-
process.stdout.write(pc.green(' ✓ ') + pc.dim('server module reloaded') + '\n');
|
|
209
|
-
}
|
|
210
|
-
loadedOnce ||= module.available;
|
|
211
|
-
} catch (e) {
|
|
212
|
-
process.stdout.write(pc.red(` ✗ server wasm failed to load: ${String(e)}`) + '\n');
|
|
213
|
-
}
|
|
214
|
-
if (!module.available && !warnedMissing) {
|
|
215
|
-
warnedMissing = true;
|
|
216
|
-
process.stdout.write(
|
|
217
|
-
pc.yellow(' ! ') +
|
|
218
|
-
pc.dim(`server wasm not found at ${options.wasmFile}; serving client only`) +
|
|
219
|
-
'\n',
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
refresh();
|
|
224
|
-
|
|
225
|
-
const app = new Server({
|
|
226
|
-
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
227
|
-
max_body_buffer: 1024 * 32,
|
|
228
|
-
fast_abort: true,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
app.set_error_handler((_request: Request, response: Response, error: Error) => {
|
|
232
|
-
if (response.completed) return;
|
|
233
|
-
response.atomic(() => {
|
|
234
|
-
response.status(500).send(`internal error: ${error.message}\n`);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
wireWebsocketProxy(app, options.vite);
|
|
239
|
-
|
|
240
|
-
app.any('/*', async (request: Request, response: Response) => {
|
|
241
|
-
response.removeHeader('uWebSockets');
|
|
242
|
-
|
|
243
|
-
const dispatchable =
|
|
244
|
-
!isViteInternal(request.url) && METHOD_CODES[request.method] !== undefined;
|
|
245
|
-
if (dispatchable) refresh();
|
|
246
|
-
|
|
247
|
-
if (dispatchable && module.available) {
|
|
248
|
-
const envelopeReq = await toEnvelopeRequest(request);
|
|
249
|
-
// Honor the tenant cache directive locally, same rules as the
|
|
250
|
-
// edge: serve an identical request from the per-process cache,
|
|
251
|
-
// else dispatch and apply/strip the directive on the response.
|
|
252
|
-
const cacheHost = request.headers.host ?? 'dev';
|
|
253
|
-
const hasAuth =
|
|
254
|
-
request.headers.cookie !== undefined || request.headers.authorization !== undefined;
|
|
255
|
-
const cached = lookupCache(cacheHost, request.method, request.url, envelopeReq.body);
|
|
256
|
-
if (cached !== null) {
|
|
257
|
-
sendWasmResponse(response, root, cached);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
try {
|
|
261
|
-
const result = module.dispatch(envelopeReq);
|
|
262
|
-
if (!result.unhandled) {
|
|
263
|
-
const finalized = applyCacheRule(
|
|
264
|
-
cacheHost,
|
|
265
|
-
request.method,
|
|
266
|
-
request.url,
|
|
267
|
-
envelopeReq.body,
|
|
268
|
-
hasAuth,
|
|
269
|
-
result,
|
|
270
|
-
);
|
|
271
|
-
sendWasmResponse(response, root, finalized);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
} catch (e) {
|
|
275
|
-
// A trap (ToilScript abort, OOB, malformed envelope) is isolated to
|
|
276
|
-
// this request, exactly like the edge poisoning one instance.
|
|
277
|
-
process.stdout.write(
|
|
278
|
-
pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
|
|
279
|
-
'\n',
|
|
280
|
-
);
|
|
281
|
-
response.status(500).send('internal error\n');
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
await proxyToVite(request, response, options.vite);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
await app.listen(options.port, host);
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
port: options.port,
|
|
293
|
-
host,
|
|
294
|
-
close: async (): Promise<void> => {
|
|
295
|
-
await app.shutdown();
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
}
|
|
24
|
+
} from './http/envelope.js';
|
|
25
|
+
export type { EnvelopeRequest, EnvelopeResponse } from './http/envelope.js';
|
|
26
|
+
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './runtime/module.js';
|
|
27
|
+
export type { WasmDispatchResult } from './runtime/module.js';
|
|
28
|
+
export { buildHostImports, freshDispatchState } from './runtime/host.js';
|
|
29
|
+
export type { DispatchState, MemoryRef } from './runtime/host.js';
|
|
30
|
+
export type { ViteTarget } from './http/proxy.js';
|
|
@@ -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;
|
|
@@ -307,7 +307,7 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
|
|
|
307
307
|
|
|
308
308
|
// `Date.now()` -> wall-clock milliseconds, matching the edge host.
|
|
309
309
|
// The guest divides by 1000 for Unix seconds (sessions, challenges).
|
|
310
|
-
'Date.now': ():
|
|
310
|
+
'Date.now': (): bigint => BigInt(Date.now()),
|
|
311
311
|
|
|
312
312
|
// Web Crypto host functions (`env.crypto.*`), backed by Node's
|
|
313
313
|
// `crypto`. The dev server skips metering, so these charge nothing.
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
|
|
16
|
+
import { persistDb, setDbCatalog } from '../db/index.js';
|
|
16
17
|
import {
|
|
17
18
|
decodeResponseEnvelope,
|
|
18
19
|
encodeRequestEnvelope,
|
|
19
20
|
type EnvelopeRequest,
|
|
20
21
|
unpackHandleResult,
|
|
21
|
-
} from '
|
|
22
|
+
} from '../http/envelope.js';
|
|
22
23
|
import { buildHostImports, freshDispatchState, type MemoryRef } from './host.js';
|
|
23
24
|
|
|
24
25
|
export { WasmAbortError } from './host.js';
|
|
@@ -100,6 +101,8 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
100
101
|
'data.counter_get',
|
|
101
102
|
'data.counter_add',
|
|
102
103
|
'data.append',
|
|
104
|
+
'data.append_once',
|
|
105
|
+
'data.enqueue',
|
|
103
106
|
'data.latest',
|
|
104
107
|
'data.capacity_set_total',
|
|
105
108
|
'data.capacity_available',
|
|
@@ -107,6 +110,8 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
107
110
|
'data.capacity_confirm',
|
|
108
111
|
'data.capacity_cancel',
|
|
109
112
|
'data.take_result',
|
|
113
|
+
'data.result_schema_version',
|
|
114
|
+
'data.write_allowed',
|
|
110
115
|
]);
|
|
111
116
|
|
|
112
117
|
export class WasmServerModule {
|
|
@@ -141,6 +146,9 @@ export class WasmServerModule {
|
|
|
141
146
|
const module = new WebAssembly.Module(bytes);
|
|
142
147
|
this.assertImportSurface(module);
|
|
143
148
|
this.assertExportSurface(module);
|
|
149
|
+
// Refresh collection -> current schema_version so writes stamp the live layout;
|
|
150
|
+
// after a @data type evolves + rebuild, old on-disk rows now look out of date.
|
|
151
|
+
setDbCatalog(bytes);
|
|
144
152
|
this.module = module;
|
|
145
153
|
this.loadedMtimeMs = mtimeMs;
|
|
146
154
|
return true;
|
|
@@ -193,6 +201,10 @@ export class WasmServerModule {
|
|
|
193
201
|
|
|
194
202
|
const unhandled = headers.some(([n]) => n.toLowerCase() === UNHANDLED_HEADER);
|
|
195
203
|
|
|
204
|
+
// Flush any DB writes this request made to disk, so dev data survives a
|
|
205
|
+
// restart (and a crash never loses an already-served write).
|
|
206
|
+
persistDb();
|
|
207
|
+
|
|
196
208
|
return {
|
|
197
209
|
status,
|
|
198
210
|
headers: headers.filter(([n]) => n.toLowerCase() !== UNHANDLED_HEADER),
|