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,58 @@
|
|
|
1
|
+
export interface DbDevState {
|
|
2
|
+
handles: string[];
|
|
3
|
+
lastResult: Buffer | null;
|
|
4
|
+
lastResultVersion: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function freshDbState(): DbDevState;
|
|
7
|
+
export interface Reservation {
|
|
8
|
+
amount: bigint;
|
|
9
|
+
expiresMs: number;
|
|
10
|
+
confirmed: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface CapLedger {
|
|
13
|
+
total: bigint;
|
|
14
|
+
reservations: Map<bigint, Reservation>;
|
|
15
|
+
nextId: bigint;
|
|
16
|
+
}
|
|
17
|
+
export interface DbSnapshot {
|
|
18
|
+
store: Record<string, {
|
|
19
|
+
v: string;
|
|
20
|
+
sv: number;
|
|
21
|
+
}>;
|
|
22
|
+
views: Record<string, {
|
|
23
|
+
v: string;
|
|
24
|
+
sv: number;
|
|
25
|
+
}>;
|
|
26
|
+
members: Record<string, Record<string, {
|
|
27
|
+
v: string;
|
|
28
|
+
sv: number;
|
|
29
|
+
}>>;
|
|
30
|
+
counters: Record<string, string>;
|
|
31
|
+
events: Record<string, {
|
|
32
|
+
v: string;
|
|
33
|
+
sv: number;
|
|
34
|
+
}[]>;
|
|
35
|
+
eventDedup: Record<string, string[]>;
|
|
36
|
+
capacity: Record<string, {
|
|
37
|
+
total: string;
|
|
38
|
+
nextId: string;
|
|
39
|
+
reservations: [string, {
|
|
40
|
+
amount: string;
|
|
41
|
+
expiresMs: number;
|
|
42
|
+
confirmed: boolean;
|
|
43
|
+
}][];
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export declare const MAX_RESERVATIONS = 4096;
|
|
47
|
+
export declare const MAX_RESERVATION_TTL_MS = 86400000;
|
|
48
|
+
export declare const MAX_NAME = 512;
|
|
49
|
+
export declare const MAX_KEY = 4096;
|
|
50
|
+
export declare const MAX_VALUE: number;
|
|
51
|
+
export declare function satI64(v: bigint): bigint;
|
|
52
|
+
export declare const ABSENT = -2;
|
|
53
|
+
export declare const TOO_SMALL = -1;
|
|
54
|
+
export declare const INVALID_HANDLE = -1001;
|
|
55
|
+
export declare const ALREADY_EXISTS = -1003;
|
|
56
|
+
export declare const CONFLICT = -1004;
|
|
57
|
+
export declare const CODEC_ERR = -1006;
|
|
58
|
+
export declare const TOO_MANY_KEYS = -1020;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function freshDbState() {
|
|
2
|
+
return { handles: [], lastResult: null, lastResultVersion: -1 };
|
|
3
|
+
}
|
|
4
|
+
export const MAX_RESERVATIONS = 4096;
|
|
5
|
+
export const MAX_RESERVATION_TTL_MS = 86_400_000;
|
|
6
|
+
export const MAX_NAME = 512;
|
|
7
|
+
export const MAX_KEY = 4096;
|
|
8
|
+
export const MAX_VALUE = 256 * 1024;
|
|
9
|
+
const I64_MIN = -(2n ** 63n);
|
|
10
|
+
const I64_MAX = 2n ** 63n - 1n;
|
|
11
|
+
export function satI64(v) {
|
|
12
|
+
return v < I64_MIN ? I64_MIN : v > I64_MAX ? I64_MAX : v;
|
|
13
|
+
}
|
|
14
|
+
export const ABSENT = -2;
|
|
15
|
+
export const TOO_SMALL = -1;
|
|
16
|
+
export const INVALID_HANDLE = -1001;
|
|
17
|
+
export const ALREADY_EXISTS = -1003;
|
|
18
|
+
export const CONFLICT = -1004;
|
|
19
|
+
export const CODEC_ERR = -1006;
|
|
20
|
+
export const TOO_MANY_KEYS = -1020;
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './envelope.js';
|
|
4
|
-
export type { EnvelopeRequest, EnvelopeResponse } from './envelope.js';
|
|
5
|
-
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './module.js';
|
|
6
|
-
export type { WasmDispatchResult } from './module.js';
|
|
7
|
-
export { buildHostImports, freshDispatchState } from './host.js';
|
|
8
|
-
export type { DispatchState, MemoryRef } from './host.js';
|
|
9
|
-
export type { ViteTarget } from './proxy.js';
|
|
10
|
-
export interface DevServerOptions {
|
|
11
|
-
readonly root: string;
|
|
12
|
-
readonly port: number;
|
|
13
|
-
readonly host?: string;
|
|
14
|
-
readonly wasmFile: string;
|
|
15
|
-
readonly vite: ViteTarget;
|
|
16
|
-
readonly maxBodyLength?: number;
|
|
17
|
-
readonly email?: EmailBackendConfig;
|
|
18
|
-
}
|
|
19
|
-
export interface RunningDevServer {
|
|
20
|
-
readonly port: number;
|
|
21
|
-
readonly host: string;
|
|
22
|
-
close(): Promise<void>;
|
|
23
|
-
}
|
|
24
|
-
export declare function startDevServer(options: DevServerOptions): Promise<RunningDevServer>;
|
|
1
|
+
export { startDevServer } from './server.js';
|
|
2
|
+
export type { DevServerOptions, RunningDevServer } from './server.js';
|
|
3
|
+
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './http/envelope.js';
|
|
4
|
+
export type { EnvelopeRequest, EnvelopeResponse } from './http/envelope.js';
|
|
5
|
+
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './runtime/module.js';
|
|
6
|
+
export type { WasmDispatchResult } from './runtime/module.js';
|
|
7
|
+
export { buildHostImports, freshDispatchState } from './runtime/host.js';
|
|
8
|
+
export type { DispatchState, MemoryRef } from './runtime/host.js';
|
|
9
|
+
export type { ViteTarget } from './http/proxy.js';
|
package/build/devserver/index.js
CHANGED
|
@@ -1,165 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { applyCacheRule, lookupCache } from './cache.js';
|
|
6
|
-
import { initEmailService } from './email/index.js';
|
|
7
|
-
import { METHOD_CODES } from './envelope.js';
|
|
8
|
-
import { WasmServerModule } from './module.js';
|
|
9
|
-
import { proxyToVite, wireWebsocketProxy } from './proxy.js';
|
|
10
|
-
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './envelope.js';
|
|
11
|
-
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './module.js';
|
|
12
|
-
export { buildHostImports, freshDispatchState } from './host.js';
|
|
13
|
-
const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
14
|
-
const VITE_PREFIXES = ['/@', '/node_modules/', '/__toil/'];
|
|
15
|
-
const MIME = {
|
|
16
|
-
'.html': 'text/html; charset=utf-8',
|
|
17
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
18
|
-
'.mjs': 'text/javascript; charset=utf-8',
|
|
19
|
-
'.css': 'text/css; charset=utf-8',
|
|
20
|
-
'.json': 'application/json; charset=utf-8',
|
|
21
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
22
|
-
'.svg': 'image/svg+xml',
|
|
23
|
-
'.png': 'image/png',
|
|
24
|
-
'.jpg': 'image/jpeg',
|
|
25
|
-
'.jpeg': 'image/jpeg',
|
|
26
|
-
'.webp': 'image/webp',
|
|
27
|
-
'.avif': 'image/avif',
|
|
28
|
-
'.gif': 'image/gif',
|
|
29
|
-
'.ico': 'image/x-icon',
|
|
30
|
-
'.wasm': 'application/wasm',
|
|
31
|
-
'.woff2': 'font/woff2',
|
|
32
|
-
};
|
|
33
|
-
function isViteInternal(url) {
|
|
34
|
-
return VITE_PREFIXES.some((p) => url.startsWith(p));
|
|
35
|
-
}
|
|
36
|
-
function resolveSendfile(root, file) {
|
|
37
|
-
const resolved = path.resolve(root, file);
|
|
38
|
-
if (resolved !== root && !resolved.startsWith(root + path.sep))
|
|
39
|
-
return null;
|
|
40
|
-
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile())
|
|
41
|
-
return null;
|
|
42
|
-
return resolved;
|
|
43
|
-
}
|
|
44
|
-
async function toEnvelopeRequest(request) {
|
|
45
|
-
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
46
|
-
const body = hasBody ? new Uint8Array(await request.buffer()) : new Uint8Array(0);
|
|
47
|
-
const xff = request.headers['x-forwarded-for'];
|
|
48
|
-
const clientIp = typeof xff === 'string' && xff.length > 0 ? xff.split(',')[0].trim() : '127.0.0.1';
|
|
49
|
-
return {
|
|
50
|
-
method: request.method,
|
|
51
|
-
path: request.url,
|
|
52
|
-
headers: Object.entries(request.headers),
|
|
53
|
-
body,
|
|
54
|
-
clientIp,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function sendWasmResponse(response, root, result) {
|
|
58
|
-
response.status(result.status);
|
|
59
|
-
let hasContentType = false;
|
|
60
|
-
for (const [name, value] of result.headers) {
|
|
61
|
-
if (name.toLowerCase() === 'content-type')
|
|
62
|
-
hasContentType = true;
|
|
63
|
-
response.header(name, value);
|
|
64
|
-
}
|
|
65
|
-
response.header('server', 'toil-dev');
|
|
66
|
-
if (result.sendfile !== null) {
|
|
67
|
-
const file = resolveSendfile(root, result.sendfile);
|
|
68
|
-
if (file === null) {
|
|
69
|
-
response.status(404).send('not found\n');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (!hasContentType) {
|
|
73
|
-
response.header('content-type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream');
|
|
74
|
-
}
|
|
75
|
-
response.sendFile(file);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (!hasContentType)
|
|
79
|
-
response.header('content-type', 'text/plain; charset=utf-8');
|
|
80
|
-
response.send(Buffer.from(result.body.buffer, result.body.byteOffset, result.body.length));
|
|
81
|
-
}
|
|
82
|
-
export async function startDevServer(options) {
|
|
83
|
-
const host = options.host ?? '127.0.0.1';
|
|
84
|
-
const root = path.resolve(options.root);
|
|
85
|
-
const emailInit = initEmailService(root, options.email);
|
|
86
|
-
if (emailInit.service !== null) {
|
|
87
|
-
process.stdout.write(pc.dim(` ✉ email enabled: ${emailInit.note}`) + '\n');
|
|
88
|
-
}
|
|
89
|
-
else if (emailInit.note !== null) {
|
|
90
|
-
process.stdout.write(pc.yellow(' ! ') + pc.dim(`email off: ${emailInit.note}`) + '\n');
|
|
91
|
-
}
|
|
92
|
-
const module = new WasmServerModule(options.wasmFile);
|
|
93
|
-
let warnedMissing = false;
|
|
94
|
-
let loadedOnce = false;
|
|
95
|
-
const refresh = () => {
|
|
96
|
-
try {
|
|
97
|
-
if (module.refresh() && loadedOnce) {
|
|
98
|
-
process.stdout.write(pc.green(' ✓ ') + pc.dim('server module reloaded') + '\n');
|
|
99
|
-
}
|
|
100
|
-
loadedOnce ||= module.available;
|
|
101
|
-
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
process.stdout.write(pc.red(` ✗ server wasm failed to load: ${String(e)}`) + '\n');
|
|
104
|
-
}
|
|
105
|
-
if (!module.available && !warnedMissing) {
|
|
106
|
-
warnedMissing = true;
|
|
107
|
-
process.stdout.write(pc.yellow(' ! ') +
|
|
108
|
-
pc.dim(`server wasm not found at ${options.wasmFile}; serving client only`) +
|
|
109
|
-
'\n');
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
refresh();
|
|
113
|
-
const app = new Server({
|
|
114
|
-
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
115
|
-
max_body_buffer: 1024 * 32,
|
|
116
|
-
fast_abort: true,
|
|
117
|
-
});
|
|
118
|
-
app.set_error_handler((_request, response, error) => {
|
|
119
|
-
if (response.completed)
|
|
120
|
-
return;
|
|
121
|
-
response.atomic(() => {
|
|
122
|
-
response.status(500).send(`internal error: ${error.message}\n`);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
wireWebsocketProxy(app, options.vite);
|
|
126
|
-
app.any('/*', async (request, response) => {
|
|
127
|
-
response.removeHeader('uWebSockets');
|
|
128
|
-
const dispatchable = !isViteInternal(request.url) && METHOD_CODES[request.method] !== undefined;
|
|
129
|
-
if (dispatchable)
|
|
130
|
-
refresh();
|
|
131
|
-
if (dispatchable && module.available) {
|
|
132
|
-
const envelopeReq = await toEnvelopeRequest(request);
|
|
133
|
-
const cacheHost = request.headers.host ?? 'dev';
|
|
134
|
-
const hasAuth = request.headers.cookie !== undefined || request.headers.authorization !== undefined;
|
|
135
|
-
const cached = lookupCache(cacheHost, request.method, request.url, envelopeReq.body);
|
|
136
|
-
if (cached !== null) {
|
|
137
|
-
sendWasmResponse(response, root, cached);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const result = module.dispatch(envelopeReq);
|
|
142
|
-
if (!result.unhandled) {
|
|
143
|
-
const finalized = applyCacheRule(cacheHost, request.method, request.url, envelopeReq.body, hasAuth, result);
|
|
144
|
-
sendWasmResponse(response, root, finalized);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch (e) {
|
|
149
|
-
process.stdout.write(pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
|
|
150
|
-
'\n');
|
|
151
|
-
response.status(500).send('internal error\n');
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
await proxyToVite(request, response, options.vite);
|
|
156
|
-
});
|
|
157
|
-
await app.listen(options.port, host);
|
|
158
|
-
return {
|
|
159
|
-
port: options.port,
|
|
160
|
-
host,
|
|
161
|
-
close: async () => {
|
|
162
|
-
await app.shutdown();
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
}
|
|
1
|
+
export { startDevServer } from './server.js';
|
|
2
|
+
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './http/envelope.js';
|
|
3
|
+
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './runtime/module.js';
|
|
4
|
+
export { buildHostImports, freshDispatchState } from './runtime/host.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { type DbDevState } from '../db/index.js';
|
|
1
2
|
import { type CryptoState } from './crypto.js';
|
|
2
|
-
import { type DbDevState } from './database.js';
|
|
3
3
|
export declare class WasmAbortError extends Error {
|
|
4
4
|
constructor(message: string, fileName: string, line: number, column: number);
|
|
5
5
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { devEnvGet, devEnvGetSecure } from '../config/env.js';
|
|
2
|
+
import { ratelimitCheck } from '../config/ratelimit.js';
|
|
3
|
+
import { buildDatabaseImports, freshDbState } from '../db/index.js';
|
|
4
|
+
import { EmailStatus, getEmailService } from '../email/index.js';
|
|
5
|
+
import { parseEmailBlob } from '../email/wire.js';
|
|
1
6
|
import { buildCryptoImports, freshCryptoState } from './crypto.js';
|
|
2
|
-
import { buildDatabaseImports, freshDbState } from './database.js';
|
|
3
|
-
import { EmailStatus, getEmailService } from './email/index.js';
|
|
4
|
-
import { parseEmailBlob } from './email/wire.js';
|
|
5
|
-
import { devEnvGet, devEnvGetSecure } from './env.js';
|
|
6
|
-
import { ratelimitCheck } from './ratelimit.js';
|
|
7
7
|
const MAX_TOTAL_HEADERS_BYTES = 64 * 1024;
|
|
8
8
|
const MAX_HEADER_NAME_LEN = 256;
|
|
9
9
|
const MAX_HEADER_VALUE_LEN = 8192;
|
|
@@ -157,7 +157,7 @@ export function buildHostImports(ref, state) {
|
|
|
157
157
|
env_get: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
|
|
158
158
|
env_get_secure: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
|
|
159
159
|
thread_spawn: (_startArg) => -1,
|
|
160
|
-
'Date.now': () => Date.now(),
|
|
160
|
+
'Date.now': () => BigInt(Date.now()),
|
|
161
161
|
...buildCryptoImports(ref, state.crypto),
|
|
162
162
|
...buildDatabaseImports(ref, state.db),
|
|
163
163
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { persistDb, setDbCatalog } from '../db/index.js';
|
|
3
|
+
import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from '../http/envelope.js';
|
|
3
4
|
import { buildHostImports, freshDispatchState } from './host.js';
|
|
4
5
|
export { WasmAbortError } from './host.js';
|
|
5
6
|
export const UNHANDLED_HEADER = 'x-toil-unhandled';
|
|
@@ -50,6 +51,8 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
50
51
|
'data.counter_get',
|
|
51
52
|
'data.counter_add',
|
|
52
53
|
'data.append',
|
|
54
|
+
'data.append_once',
|
|
55
|
+
'data.enqueue',
|
|
53
56
|
'data.latest',
|
|
54
57
|
'data.capacity_set_total',
|
|
55
58
|
'data.capacity_available',
|
|
@@ -57,6 +60,8 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
57
60
|
'data.capacity_confirm',
|
|
58
61
|
'data.capacity_cancel',
|
|
59
62
|
'data.take_result',
|
|
63
|
+
'data.result_schema_version',
|
|
64
|
+
'data.write_allowed',
|
|
60
65
|
]);
|
|
61
66
|
export class WasmServerModule {
|
|
62
67
|
wasmPath;
|
|
@@ -84,6 +89,7 @@ export class WasmServerModule {
|
|
|
84
89
|
const module = new WebAssembly.Module(bytes);
|
|
85
90
|
this.assertImportSurface(module);
|
|
86
91
|
this.assertExportSurface(module);
|
|
92
|
+
setDbCatalog(bytes);
|
|
87
93
|
this.module = module;
|
|
88
94
|
this.loadedMtimeMs = mtimeMs;
|
|
89
95
|
return true;
|
|
@@ -110,6 +116,7 @@ export class WasmServerModule {
|
|
|
110
116
|
const headers = [...resp.headers, ...state.headers];
|
|
111
117
|
const status = state.status ?? resp.status;
|
|
112
118
|
const unhandled = headers.some(([n]) => n.toLowerCase() === UNHANDLED_HEADER);
|
|
119
|
+
persistDb();
|
|
113
120
|
return {
|
|
114
121
|
status,
|
|
115
122
|
headers: headers.filter(([n]) => n.toLowerCase() !== UNHANDLED_HEADER),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EmailBackendConfig } from 'toiljs/shared';
|
|
2
|
+
import { type ViteTarget } from './http/proxy.js';
|
|
3
|
+
export interface DevServerOptions {
|
|
4
|
+
readonly root: string;
|
|
5
|
+
readonly port: number;
|
|
6
|
+
readonly host?: string;
|
|
7
|
+
readonly wasmFile: string;
|
|
8
|
+
readonly vite: ViteTarget;
|
|
9
|
+
readonly maxBodyLength?: number;
|
|
10
|
+
readonly email?: EmailBackendConfig;
|
|
11
|
+
}
|
|
12
|
+
export interface RunningDevServer {
|
|
13
|
+
readonly port: number;
|
|
14
|
+
readonly host: string;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function startDevServer(options: DevServerOptions): Promise<RunningDevServer>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Server } from '@dacely/hyper-express';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { configureDbPersistence } from './db/index.js';
|
|
6
|
+
import { initEmailService } from './email/index.js';
|
|
7
|
+
import { applyCacheRule, lookupCache } from './http/cache.js';
|
|
8
|
+
import { METHOD_CODES } from './http/envelope.js';
|
|
9
|
+
import { proxyToVite, wireWebsocketProxy } from './http/proxy.js';
|
|
10
|
+
import { WasmServerModule } from './runtime/module.js';
|
|
11
|
+
const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
12
|
+
const VITE_PREFIXES = ['/@', '/node_modules/', '/__toil/'];
|
|
13
|
+
const MIME = {
|
|
14
|
+
'.html': 'text/html; charset=utf-8',
|
|
15
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
16
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
17
|
+
'.css': 'text/css; charset=utf-8',
|
|
18
|
+
'.json': 'application/json; charset=utf-8',
|
|
19
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
20
|
+
'.svg': 'image/svg+xml',
|
|
21
|
+
'.png': 'image/png',
|
|
22
|
+
'.jpg': 'image/jpeg',
|
|
23
|
+
'.jpeg': 'image/jpeg',
|
|
24
|
+
'.webp': 'image/webp',
|
|
25
|
+
'.avif': 'image/avif',
|
|
26
|
+
'.gif': 'image/gif',
|
|
27
|
+
'.ico': 'image/x-icon',
|
|
28
|
+
'.wasm': 'application/wasm',
|
|
29
|
+
'.woff2': 'font/woff2',
|
|
30
|
+
};
|
|
31
|
+
function isViteInternal(url) {
|
|
32
|
+
return VITE_PREFIXES.some((p) => url.startsWith(p));
|
|
33
|
+
}
|
|
34
|
+
function resolveSendfile(root, file) {
|
|
35
|
+
const resolved = path.resolve(root, file);
|
|
36
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep))
|
|
37
|
+
return null;
|
|
38
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile())
|
|
39
|
+
return null;
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
async function toEnvelopeRequest(request) {
|
|
43
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
44
|
+
const body = hasBody ? new Uint8Array(await request.buffer()) : new Uint8Array(0);
|
|
45
|
+
const xff = request.headers['x-forwarded-for'];
|
|
46
|
+
const clientIp = typeof xff === 'string' && xff.length > 0 ? xff.split(',')[0].trim() : '127.0.0.1';
|
|
47
|
+
return {
|
|
48
|
+
method: request.method,
|
|
49
|
+
path: request.url,
|
|
50
|
+
headers: Object.entries(request.headers),
|
|
51
|
+
body,
|
|
52
|
+
clientIp,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function sendWasmResponse(response, root, result) {
|
|
56
|
+
response.status(result.status);
|
|
57
|
+
let hasContentType = false;
|
|
58
|
+
for (const [name, value] of result.headers) {
|
|
59
|
+
if (name.toLowerCase() === 'content-type')
|
|
60
|
+
hasContentType = true;
|
|
61
|
+
response.header(name, value);
|
|
62
|
+
}
|
|
63
|
+
response.header('server', 'toil-dev');
|
|
64
|
+
if (result.sendfile !== null) {
|
|
65
|
+
const file = resolveSendfile(root, result.sendfile);
|
|
66
|
+
if (file === null) {
|
|
67
|
+
response.status(404).send('not found\n');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!hasContentType) {
|
|
71
|
+
response.header('content-type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream');
|
|
72
|
+
}
|
|
73
|
+
response.sendFile(file);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!hasContentType)
|
|
77
|
+
response.header('content-type', 'text/plain; charset=utf-8');
|
|
78
|
+
response.send(Buffer.from(result.body.buffer, result.body.byteOffset, result.body.length));
|
|
79
|
+
}
|
|
80
|
+
export async function startDevServer(options) {
|
|
81
|
+
const host = options.host ?? '127.0.0.1';
|
|
82
|
+
const root = path.resolve(options.root);
|
|
83
|
+
const emailInit = initEmailService(root, options.email);
|
|
84
|
+
if (emailInit.service !== null) {
|
|
85
|
+
process.stdout.write(pc.dim(` ✉ email enabled: ${emailInit.note}`) + '\n');
|
|
86
|
+
}
|
|
87
|
+
else if (emailInit.note !== null) {
|
|
88
|
+
process.stdout.write(pc.yellow(' ! ') + pc.dim(`email off: ${emailInit.note}`) + '\n');
|
|
89
|
+
}
|
|
90
|
+
const module = new WasmServerModule(options.wasmFile);
|
|
91
|
+
configureDbPersistence(path.join(root, '.toil', 'devdata.json'));
|
|
92
|
+
let warnedMissing = false;
|
|
93
|
+
let loadedOnce = false;
|
|
94
|
+
const refresh = () => {
|
|
95
|
+
try {
|
|
96
|
+
if (module.refresh() && loadedOnce) {
|
|
97
|
+
process.stdout.write(pc.green(' ✓ ') + pc.dim('server module reloaded') + '\n');
|
|
98
|
+
}
|
|
99
|
+
loadedOnce ||= module.available;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
process.stdout.write(pc.red(` ✗ server wasm failed to load: ${String(e)}`) + '\n');
|
|
103
|
+
}
|
|
104
|
+
if (!module.available && !warnedMissing) {
|
|
105
|
+
warnedMissing = true;
|
|
106
|
+
process.stdout.write(pc.yellow(' ! ') +
|
|
107
|
+
pc.dim(`server wasm not found at ${options.wasmFile}; serving client only`) +
|
|
108
|
+
'\n');
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
refresh();
|
|
112
|
+
const app = new Server({
|
|
113
|
+
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
114
|
+
max_body_buffer: 1024 * 32,
|
|
115
|
+
fast_abort: true,
|
|
116
|
+
});
|
|
117
|
+
app.set_error_handler((_request, response, error) => {
|
|
118
|
+
if (response.completed)
|
|
119
|
+
return;
|
|
120
|
+
response.atomic(() => {
|
|
121
|
+
response.status(500).send(`internal error: ${error.message}\n`);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
wireWebsocketProxy(app, options.vite);
|
|
125
|
+
app.any('/*', async (request, response) => {
|
|
126
|
+
response.removeHeader('uWebSockets');
|
|
127
|
+
const dispatchable = !isViteInternal(request.url) && METHOD_CODES[request.method] !== undefined;
|
|
128
|
+
if (dispatchable)
|
|
129
|
+
refresh();
|
|
130
|
+
if (dispatchable && module.available) {
|
|
131
|
+
const envelopeReq = await toEnvelopeRequest(request);
|
|
132
|
+
const cacheHost = request.headers.host ?? 'dev';
|
|
133
|
+
const hasAuth = request.headers.cookie !== undefined || request.headers.authorization !== undefined;
|
|
134
|
+
const cached = lookupCache(cacheHost, request.method, request.url, envelopeReq.body);
|
|
135
|
+
if (cached !== null) {
|
|
136
|
+
sendWasmResponse(response, root, cached);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const result = module.dispatch(envelopeReq);
|
|
141
|
+
if (!result.unhandled) {
|
|
142
|
+
const finalized = applyCacheRule(cacheHost, request.method, request.url, envelopeReq.body, hasAuth, result);
|
|
143
|
+
sendWasmResponse(response, root, finalized);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
process.stdout.write(pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
|
|
149
|
+
'\n');
|
|
150
|
+
response.status(500).send('internal error\n');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
await proxyToVite(request, response, options.vite);
|
|
155
|
+
});
|
|
156
|
+
await app.listen(options.port, host);
|
|
157
|
+
return {
|
|
158
|
+
port: options.port,
|
|
159
|
+
host,
|
|
160
|
+
close: async () => {
|
|
161
|
+
await app.shutdown();
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
package/docs/time.md
CHANGED
|
@@ -11,7 +11,7 @@ from `toiljs/server/runtime`.
|
|
|
11
11
|
```ts
|
|
12
12
|
import { Time } from 'toiljs/server/runtime'; // optional; Time is also a global
|
|
13
13
|
|
|
14
|
-
const ms = Time.nowMillis(); //
|
|
14
|
+
const ms = Time.nowMillis(); // u64 milliseconds since the Unix epoch
|
|
15
15
|
const s = Time.nowSeconds(); // u64 whole seconds since the Unix epoch
|
|
16
16
|
```
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ const s = Time.nowSeconds(); // u64 whole seconds since the Unix epoch
|
|
|
19
19
|
|
|
20
20
|
| Member | Signature | Description |
|
|
21
21
|
| --- | --- | --- |
|
|
22
|
-
| `Time.nowMillis()` | `static nowMillis():
|
|
22
|
+
| `Time.nowMillis()` | `static nowMillis(): u64` | Milliseconds since the Unix epoch (the raw host `Date.now()` value). |
|
|
23
23
|
| `Time.nowSeconds()` | `static nowSeconds(): u64` | Whole seconds since the epoch (`nowMillis() / 1000`). The unit used by sessions and login challenges. |
|
|
24
24
|
|
|
25
25
|
## Semantics
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ToilDB data MIGRATION for the guestbook's `GuestEntry` (see ../models/GuestEntry).
|
|
3
|
+
*
|
|
4
|
+
* THE CONVENTION: every `@migrate` lives in a `*.migration.ts` file under a
|
|
5
|
+
* `migrations/` folder (enforced at compile time). The build auto-discovers this
|
|
6
|
+
* file - nothing imports it - and weaves its transform into `GuestEntry`'s decoder.
|
|
7
|
+
*
|
|
8
|
+
* THE STORY: `GuestEntry` started life with just `author` + `message`. Later we
|
|
9
|
+
* added an `at` timestamp. Without a migration, every entry already written would
|
|
10
|
+
* fail to decode (its bytes have no `at`). With this file, an OLD entry is decoded
|
|
11
|
+
* as its original shape and upgraded on READ - lazily, per row, only when touched.
|
|
12
|
+
* No backfill, no downtime: rows written under the old layout keep working, and a
|
|
13
|
+
* read rewrites the converged value back so it is paid for at most once.
|
|
14
|
+
*
|
|
15
|
+
* Try it under `toiljs dev`: sign the guestbook, then add a field to `GuestEntry`
|
|
16
|
+
* + extend this transform + rebuild. The entries already on disk (in `.toil/`)
|
|
17
|
+
* surface their OLD schema_version, so this `@migrate` runs when you `list()` them.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { GuestEntry } from '../models/GuestEntry';
|
|
21
|
+
|
|
22
|
+
/** The ORIGINAL `GuestEntry` layout (v1): no `at` timestamp. Kept so entries on
|
|
23
|
+
* disk written under it still decode. One kept shape per past layout. */
|
|
24
|
+
@data
|
|
25
|
+
export class GuestEntryV1 {
|
|
26
|
+
author: string = '';
|
|
27
|
+
message: string = '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Upgrade a v1 entry to the current `GuestEntry`. The DELTA form `(old, into)`
|
|
32
|
+
* auto-copies the fields the two layouts SHARE (`author`, `message`); we only fill
|
|
33
|
+
* the field that is new. A migration is a PURE transform - it may not touch the
|
|
34
|
+
* database (that is a compile error).
|
|
35
|
+
*/
|
|
36
|
+
@migrate
|
|
37
|
+
export function up(old: GuestEntryV1, into: GuestEntry): void {
|
|
38
|
+
into.at = 0; // unknown for pre-timestamp entries
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toiljs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.60",
|
|
5
5
|
"author": "Dacely",
|
|
6
6
|
"description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
|
|
7
7
|
"repository": {
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"nodemailer": "^9.0.1",
|
|
132
132
|
"picocolors": "^1.1.1",
|
|
133
133
|
"sharp": "^0.35.2",
|
|
134
|
-
"toilscript": "^0.1.
|
|
134
|
+
"toilscript": "^0.1.37",
|
|
135
135
|
"typescript-eslint": "^8.61.1",
|
|
136
136
|
"vite": "^8.0.16",
|
|
137
137
|
"vite-imagetools": "^10.0.1",
|
|
@@ -144,6 +144,9 @@
|
|
|
144
144
|
"react-dom": ">=18.0.0",
|
|
145
145
|
"typescript": ">=6.0.0"
|
|
146
146
|
},
|
|
147
|
+
"overrides": {
|
|
148
|
+
"elliptic": "file:./stubs/elliptic"
|
|
149
|
+
},
|
|
147
150
|
"devDependencies": {
|
|
148
151
|
"@babel/core": "^8.0.1",
|
|
149
152
|
"@babel/preset-env": "^8.0.2",
|
package/server/runtime/time.ts
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
@global
|
|
18
18
|
export class Time {
|
|
19
19
|
/** Milliseconds since the Unix epoch (the host `Date.now()` value). */
|
|
20
|
-
static nowMillis():
|
|
21
|
-
return
|
|
20
|
+
static nowMillis(): u64 {
|
|
21
|
+
return Date.now();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Whole seconds since the Unix epoch (`nowMillis() / 1000`), the unit used
|
|
25
25
|
* for session and challenge timestamps. */
|
|
26
26
|
static nowSeconds(): u64 {
|
|
27
|
-
return
|
|
27
|
+
return Date.now() / 1000;
|
|
28
28
|
}
|
|
29
29
|
}
|