toiljs 0.0.66 → 0.0.68

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 (111) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +63 -61
  3. package/build/backend/.tsbuildinfo +1 -1
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +13 -1
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/index.d.ts +2 -0
  8. package/build/client/index.js +1 -0
  9. package/build/client/rpc.js +21 -1
  10. package/build/client/stream/client.d.ts +11 -0
  11. package/build/client/stream/client.js +59 -0
  12. package/build/compiler/.tsbuildinfo +1 -1
  13. package/build/compiler/config.d.ts +2 -0
  14. package/build/compiler/config.js +9 -7
  15. package/build/compiler/index.d.ts +1 -0
  16. package/build/compiler/index.js +22 -6
  17. package/build/compiler/toil-docs.generated.js +3 -3
  18. package/build/devserver/.tsbuildinfo +1 -1
  19. package/build/devserver/daemon/index.js +4 -3
  20. package/build/devserver/daemon/runtime.d.ts +13 -0
  21. package/build/devserver/daemon/runtime.js +29 -0
  22. package/build/devserver/db/catalog.js +8 -12
  23. package/build/devserver/db/database.d.ts +1 -0
  24. package/build/devserver/db/database.js +10 -0
  25. package/build/devserver/db/derives.d.ts +7 -0
  26. package/build/devserver/db/derives.js +94 -0
  27. package/build/devserver/db/index.d.ts +1 -0
  28. package/build/devserver/db/index.js +1 -0
  29. package/build/devserver/db/types.d.ts +1 -0
  30. package/build/devserver/db/types.js +1 -0
  31. package/build/devserver/http/proxy.d.ts +5 -1
  32. package/build/devserver/http/proxy.js +39 -36
  33. package/build/devserver/http/runtime.d.ts +62 -0
  34. package/build/devserver/http/runtime.js +194 -0
  35. package/build/devserver/index.d.ts +2 -0
  36. package/build/devserver/index.js +1 -0
  37. package/build/devserver/production-ipc.d.ts +50 -0
  38. package/build/devserver/production-ipc.js +21 -0
  39. package/build/devserver/production-worker.d.ts +1 -0
  40. package/build/devserver/production-worker.js +73 -0
  41. package/build/devserver/production.d.ts +35 -0
  42. package/build/devserver/production.js +502 -0
  43. package/build/devserver/runtime/module.d.ts +5 -0
  44. package/build/devserver/runtime/module.js +47 -1
  45. package/build/devserver/server.d.ts +1 -0
  46. package/build/devserver/server.js +32 -145
  47. package/build/devserver/ssr.d.ts +2 -0
  48. package/build/devserver/ssr.js +19 -2
  49. package/build/devserver/stream/catalog.d.ts +20 -0
  50. package/build/devserver/stream/catalog.js +54 -0
  51. package/build/devserver/stream/host.d.ts +9 -0
  52. package/build/devserver/stream/host.js +15 -0
  53. package/build/devserver/stream/index.d.ts +37 -0
  54. package/build/devserver/stream/index.js +220 -0
  55. package/build/devserver/stream/manager.d.ts +34 -0
  56. package/build/devserver/stream/manager.js +103 -0
  57. package/build/devserver/stream/router.d.ts +25 -0
  58. package/build/devserver/stream/router.js +64 -0
  59. package/build/devserver/stream/wire.d.ts +5 -0
  60. package/build/devserver/stream/wire.js +33 -0
  61. package/build/devserver/stream/ws.d.ts +18 -0
  62. package/build/devserver/stream/ws.js +46 -0
  63. package/build/devserver/wasm/surface.d.ts +1 -1
  64. package/build/devserver/wasm/surface.js +1 -1
  65. package/docs/cli.md +3 -1
  66. package/docs/getting-started.md +7 -7
  67. package/docs/tiers.md +15 -9
  68. package/examples/basic/server/routes/Guestbook.ts +38 -13
  69. package/package.json +2 -2
  70. package/src/cli/index.ts +14 -1
  71. package/src/client/index.ts +2 -0
  72. package/src/client/rpc.ts +25 -1
  73. package/src/client/stream/client.ts +107 -0
  74. package/src/compiler/config.ts +15 -7
  75. package/src/compiler/index.ts +43 -18
  76. package/src/compiler/toil-docs.generated.ts +3 -3
  77. package/src/devserver/daemon/index.ts +7 -7
  78. package/src/devserver/daemon/runtime.ts +48 -0
  79. package/src/devserver/db/catalog.ts +9 -13
  80. package/src/devserver/db/database.ts +14 -0
  81. package/src/devserver/db/derives.ts +121 -0
  82. package/src/devserver/db/index.ts +1 -0
  83. package/src/devserver/db/types.ts +6 -0
  84. package/src/devserver/http/proxy.ts +53 -39
  85. package/src/devserver/http/runtime.ts +287 -0
  86. package/src/devserver/index.ts +2 -0
  87. package/src/devserver/production-ipc.ts +63 -0
  88. package/src/devserver/production-worker.ts +83 -0
  89. package/src/devserver/production.ts +706 -0
  90. package/src/devserver/runtime/module.ts +95 -1
  91. package/src/devserver/server.ts +52 -201
  92. package/src/devserver/ssr.ts +23 -3
  93. package/src/devserver/stream/catalog.ts +106 -0
  94. package/src/devserver/stream/host.ts +42 -0
  95. package/src/devserver/stream/index.ts +308 -0
  96. package/src/devserver/stream/manager.ts +163 -0
  97. package/src/devserver/stream/router.ts +101 -0
  98. package/src/devserver/stream/wire.ts +58 -0
  99. package/src/devserver/stream/ws.ts +76 -0
  100. package/src/devserver/wasm/surface.ts +5 -7
  101. package/test/built-ssr.test.ts +98 -0
  102. package/test/daemon-build.test.ts +15 -7
  103. package/test/daemon-catalog.test.ts +17 -8
  104. package/test/devserver-database.test.ts +8 -8
  105. package/test/devserver.test.ts +20 -4
  106. package/test/example-guestbook.test.ts +8 -5
  107. package/test/fixtures/stream-echo.ts +26 -0
  108. package/test/fixtures/stream-gate.ts +24 -0
  109. package/test/fixtures/stream-trap.ts +18 -0
  110. package/test/rpc-bignum-wire.test.ts +8 -8
  111. package/test/stream-emulation.test.ts +394 -0
@@ -0,0 +1,35 @@
1
+ import type { EmailBackendConfig } from 'toiljs/shared';
2
+ import type { ResolvedDaemonConfig } from './daemon/host.js';
3
+ import { type ThreadedReply, type ThreadedRequest } from './production-ipc.js';
4
+ import { type DevSsrTemplate } from './ssr.js';
5
+ export interface BuiltServerOptions {
6
+ readonly root: string;
7
+ readonly staticRoot: string;
8
+ readonly wasmFile?: string;
9
+ readonly coldWasmFile?: string;
10
+ readonly nodeMode?: string;
11
+ readonly daemon?: ResolvedDaemonConfig;
12
+ readonly port?: number;
13
+ readonly host?: string;
14
+ readonly allowedOrigins?: readonly string[];
15
+ readonly wsPath?: string;
16
+ readonly cors?: boolean;
17
+ readonly maxBodyLength?: number;
18
+ readonly email?: EmailBackendConfig;
19
+ readonly threads?: number | 'auto';
20
+ }
21
+ export interface RunningBuiltServer {
22
+ readonly port: number;
23
+ readonly host: string;
24
+ readonly wsPath: string;
25
+ broadcast(message: string): void;
26
+ clientCount(): number;
27
+ close(): Promise<void>;
28
+ }
29
+ export declare function loadBuiltSsrTemplates(staticRoot: string): DevSsrTemplate[];
30
+ export interface BuiltServerWorkerController {
31
+ request(request: ThreadedRequest): Promise<ThreadedReply>;
32
+ clientCount(count: number): void;
33
+ }
34
+ export declare function startBuiltServerWorker(options: BuiltServerOptions, controller: BuiltServerWorkerController): Promise<RunningBuiltServer>;
35
+ export declare function startBuiltServer(options: BuiltServerOptions): Promise<RunningBuiltServer>;
@@ -0,0 +1,502 @@
1
+ import { fork } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { availableParallelism } from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Server, } from '@dacely/hyper-express';
7
+ import pc from 'picocolors';
8
+ import { startDaemonRuntime } from './daemon/runtime.js';
9
+ import { configureDbPersistence } from './db/index.js';
10
+ import { initEmailService } from './email/index.js';
11
+ import { assembleRouteSsr, dispatchEnvelopeRequest, installRuntimeErrorHandler, isDispatchableMethod, prepareSsrResponse, prepareWasmResponse, resolveStaticFile, runtimeServerOptions, toEnvelopeRequest, } from './http/runtime.js';
12
+ import { decodeBody, encodeBody, isWorkerToPrimaryMessage, } from './production-ipc.js';
13
+ import { WasmServerModule } from './runtime/module.js';
14
+ import { buildSsrRoutes, pathnameOf } from './ssr.js';
15
+ const WS_MAX_PAYLOAD_LENGTH = 1024 * 1024;
16
+ const WS_IDLE_TIMEOUT = 120;
17
+ const WS_MAX_BACKPRESSURE = 1024 * 1024 * 2;
18
+ const CORS_METHODS = 'GET, POST, OPTIONS, PUT, PATCH, DELETE';
19
+ const CORS_HEADERS = 'X-Requested-With, content-type';
20
+ function resolveBuiltServerPaths(options) {
21
+ const port = options.port ?? 3000;
22
+ const host = options.host ?? '127.0.0.1';
23
+ const wsPath = options.wsPath ?? '/_toil';
24
+ const staticRoot = path.resolve(options.staticRoot);
25
+ const indexHtml = path.join(staticRoot, 'index.html');
26
+ if (!fs.existsSync(indexHtml)) {
27
+ throw new Error(`No build found in ${staticRoot}. Run \`toiljs build\` first.`);
28
+ }
29
+ return {
30
+ port,
31
+ host,
32
+ wsPath,
33
+ cors: options.cors ?? true,
34
+ projectRoot: path.resolve(options.root),
35
+ staticRoot,
36
+ indexHtml,
37
+ };
38
+ }
39
+ function resolveThreadCount(threads) {
40
+ const raw = process.env.TOILJS_THREADS ?? threads;
41
+ if (raw === undefined || raw === 'auto')
42
+ return Math.max(1, availableParallelism());
43
+ const n = typeof raw === 'number' ? raw : Number.parseInt(raw, 10);
44
+ if (!Number.isFinite(n))
45
+ return Math.max(1, availableParallelism());
46
+ return Math.max(1, Math.min(128, Math.floor(n)));
47
+ }
48
+ function headerValue(headers, name) {
49
+ const lower = name.toLowerCase();
50
+ return headers.find(([k]) => k.toLowerCase() === lower)?.[1];
51
+ }
52
+ function textReply(status, body) {
53
+ return {
54
+ kind: 'response',
55
+ status,
56
+ headers: [['content-type', 'text/plain; charset=utf-8']],
57
+ body: encodeBody(new TextEncoder().encode(body)),
58
+ sendfile: null,
59
+ };
60
+ }
61
+ function preparedReply(out) {
62
+ return {
63
+ kind: 'response',
64
+ status: out.status,
65
+ headers: out.headers,
66
+ body: encodeBody(out.body),
67
+ sendfile: out.sendfile,
68
+ };
69
+ }
70
+ function sendThreadedReply(response, reply) {
71
+ if (reply.kind === 'fallback')
72
+ return false;
73
+ response.status(reply.status);
74
+ for (const [name, value] of reply.headers)
75
+ response.header(name, value);
76
+ if (reply.sendfile !== null) {
77
+ response.sendFile(reply.sendfile);
78
+ return true;
79
+ }
80
+ const body = decodeBody(reply.body);
81
+ response.send(Buffer.from(body.buffer, body.byteOffset, body.length));
82
+ return true;
83
+ }
84
+ function isWsOriginAllowed(origin, hostHeader, allowed) {
85
+ if (!origin)
86
+ return true;
87
+ if (allowed?.includes(origin))
88
+ return true;
89
+ try {
90
+ return new URL(origin).host === hostHeader;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ function parseSlotsManifest(slotsFile) {
97
+ const bin = fs.readFileSync(slotsFile);
98
+ if (bin.length < 46)
99
+ return null;
100
+ if (bin.subarray(0, 4).toString('ascii') !== 'TSLT')
101
+ return null;
102
+ const n = bin.readUInt16LE(44);
103
+ if (bin.length < 46 + n * 8)
104
+ return null;
105
+ const entries = [];
106
+ let o = 46;
107
+ for (let i = 0; i < n; i++) {
108
+ entries.push({ offset: bin.readUInt32LE(o), id: bin.readUInt16LE(o + 4) });
109
+ o += 8;
110
+ }
111
+ return { entries, hash: Buffer.from(bin.subarray(12, 44)) };
112
+ }
113
+ function isTemplateIndexEntry(value) {
114
+ if (typeof value !== 'object' || value === null)
115
+ return false;
116
+ const v = value;
117
+ return typeof v.route === 'string' && typeof v.name === 'string';
118
+ }
119
+ export function loadBuiltSsrTemplates(staticRoot) {
120
+ const ssrDir = path.join(staticRoot, '_ssr');
121
+ const indexFile = path.join(ssrDir, 'templates.json');
122
+ if (!fs.existsSync(indexFile))
123
+ return [];
124
+ let index;
125
+ try {
126
+ index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ if (!Array.isArray(index))
132
+ return [];
133
+ const out = [];
134
+ for (const item of index) {
135
+ if (!isTemplateIndexEntry(item))
136
+ continue;
137
+ if (!/^[A-Za-z0-9_]+$/.test(item.name))
138
+ continue;
139
+ const tmplFile = path.join(ssrDir, `${item.name}.tmpl`);
140
+ const slotsFile = path.join(ssrDir, `${item.name}.slots`);
141
+ if (!fs.existsSync(tmplFile) || !fs.existsSync(slotsFile))
142
+ continue;
143
+ const parsed = parseSlotsManifest(slotsFile);
144
+ if (parsed === null)
145
+ continue;
146
+ out.push({
147
+ pattern: item.route,
148
+ name: item.name,
149
+ tmpl: fs.readFileSync(tmplFile),
150
+ entries: parsed.entries,
151
+ hash: parsed.hash,
152
+ });
153
+ }
154
+ return out;
155
+ }
156
+ function createBuiltRuntime(options, paths) {
157
+ const emailInit = initEmailService(paths.projectRoot, options.email);
158
+ if (emailInit.service !== null) {
159
+ process.stdout.write(pc.dim(` email enabled: ${emailInit.note}`) + '\n');
160
+ }
161
+ else if (emailInit.note !== null) {
162
+ process.stdout.write(pc.yellow(' ! ') + pc.dim(`email off: ${emailInit.note}`) + '\n');
163
+ }
164
+ const module = options.wasmFile !== undefined && fs.existsSync(options.wasmFile)
165
+ ? new WasmServerModule(options.wasmFile)
166
+ : null;
167
+ if (module !== null) {
168
+ try {
169
+ module.refresh();
170
+ }
171
+ catch (e) {
172
+ process.stdout.write(pc.red(` x server wasm failed to load: ${String(e)}`) + '\n');
173
+ }
174
+ }
175
+ const ssrRoutes = buildSsrRoutes(loadBuiltSsrTemplates(paths.staticRoot));
176
+ if (ssrRoutes.length > 0) {
177
+ process.stdout.write(pc.dim(` edge SSR: ${String(ssrRoutes.length)} route(s) served server-side`) + '\n');
178
+ }
179
+ configureDbPersistence(path.join(paths.projectRoot, '.toil', 'devdata.json'));
180
+ return { projectRoot: paths.projectRoot, module, ssrRoutes };
181
+ }
182
+ function startBuiltDaemon(options, paths) {
183
+ configureDbPersistence(path.join(paths.projectRoot, '.toil', 'devdata.json'));
184
+ return startDaemonRuntime({
185
+ coldWasmFile: options.coldWasmFile,
186
+ nodeMode: options.nodeMode,
187
+ daemon: options.daemon,
188
+ });
189
+ }
190
+ async function handleBuiltRuntimeRequest(runtime, request) {
191
+ const envelopeReq = {
192
+ method: request.method,
193
+ path: request.url,
194
+ headers: request.headers,
195
+ body: decodeBody(request.body),
196
+ clientIp: request.clientIp,
197
+ };
198
+ const dispatchable = isDispatchableMethod(request.method);
199
+ if (dispatchable && runtime.module !== null && runtime.module.available) {
200
+ const hasAuth = headerValue(request.headers, 'cookie') !== undefined ||
201
+ headerValue(request.headers, 'authorization') !== undefined;
202
+ try {
203
+ const dispatch = dispatchEnvelopeRequest({
204
+ module: runtime.module,
205
+ envelopeReq,
206
+ method: request.method,
207
+ url: request.url,
208
+ cacheHost: headerValue(request.headers, 'host') ?? 'self-host',
209
+ hasAuth,
210
+ });
211
+ if (dispatch.result !== null) {
212
+ return preparedReply(prepareWasmResponse(runtime.projectRoot, dispatch.result, 'toil'));
213
+ }
214
+ }
215
+ catch (e) {
216
+ process.stdout.write(pc.red(` x ${request.method} ${request.path} server error: ${String(e)}`) + '\n');
217
+ return textReply(500, 'internal error\n');
218
+ }
219
+ }
220
+ if (request.method === 'GET' || request.method === 'HEAD') {
221
+ const route = runtime.ssrRoutes.find((r) => r.test(pathnameOf(request.url)));
222
+ if (route !== undefined) {
223
+ try {
224
+ const out = assembleRouteSsr(route, runtime.module, envelopeReq);
225
+ if (out !== null) {
226
+ return preparedReply(prepareSsrResponse(out, request.method === 'HEAD', 'toil'));
227
+ }
228
+ }
229
+ catch (e) {
230
+ process.stdout.write(pc.red(` x SSR ${request.path}: ${String(e)}`) + '\n');
231
+ return textReply(500, 'internal error\n');
232
+ }
233
+ }
234
+ return { kind: 'fallback' };
235
+ }
236
+ return textReply(404, 'not found\n');
237
+ }
238
+ async function startBuiltHttpServer(options, paths, dynamicHandler, runtimeOptions = {}) {
239
+ const cors = paths.cors;
240
+ const app = new Server(runtimeServerOptions(options));
241
+ const clients = new Set();
242
+ installRuntimeErrorHandler(app);
243
+ if (cors) {
244
+ app.use((request, response, next) => {
245
+ if (request.method !== 'OPTIONS') {
246
+ response.setHeader('Access-Control-Allow-Origin', '*');
247
+ response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
248
+ response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
249
+ }
250
+ response.removeHeader('uWebSockets');
251
+ next();
252
+ });
253
+ app.options('/*', (_request, response) => {
254
+ response.setHeader('Access-Control-Allow-Origin', '*');
255
+ response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
256
+ response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
257
+ response.setHeader('Access-Control-Max-Age', '86400');
258
+ response.status(204).send();
259
+ });
260
+ }
261
+ app.ws(paths.wsPath, {
262
+ message_type: 'String',
263
+ max_payload_length: WS_MAX_PAYLOAD_LENGTH,
264
+ idle_timeout: WS_IDLE_TIMEOUT,
265
+ max_backpressure: WS_MAX_BACKPRESSURE,
266
+ }, (ws) => {
267
+ clients.add(ws);
268
+ runtimeOptions.onClientCount?.(clients.size);
269
+ ws.send(JSON.stringify({ type: 'connected', clients: clients.size }));
270
+ ws.on('message', (message) => {
271
+ for (const client of clients)
272
+ client.send(message);
273
+ });
274
+ ws.on('drain', () => { });
275
+ ws.on('close', () => {
276
+ clients.delete(ws);
277
+ runtimeOptions.onClientCount?.(clients.size);
278
+ });
279
+ });
280
+ app.upgrade(paths.wsPath, (request, response) => {
281
+ if (!isWsOriginAllowed(request.headers.origin, request.headers.host, options.allowedOrigins)) {
282
+ response.status(403).send();
283
+ return;
284
+ }
285
+ response.upgrade({});
286
+ });
287
+ app.any('/*', async (request, response) => {
288
+ response.removeHeader('uWebSockets');
289
+ if (request.method === 'GET' || request.method === 'HEAD') {
290
+ const file = resolveStaticFile(paths.staticRoot, request.path);
291
+ if (file !== null) {
292
+ response.sendFile(file);
293
+ return;
294
+ }
295
+ }
296
+ if (await dynamicHandler(request, response))
297
+ return;
298
+ if (request.method === 'GET' || request.method === 'HEAD') {
299
+ response.sendFile(paths.indexHtml);
300
+ return;
301
+ }
302
+ response.status(404).send('not found\n');
303
+ });
304
+ await app.listen(paths.port, paths.host);
305
+ return {
306
+ port: paths.port,
307
+ host: paths.host,
308
+ wsPath: paths.wsPath,
309
+ broadcast: (message) => {
310
+ for (const client of clients)
311
+ client.send(message);
312
+ },
313
+ clientCount: () => clients.size,
314
+ close: async () => {
315
+ await app.shutdown();
316
+ },
317
+ };
318
+ }
319
+ async function toThreadedRequest(request, id) {
320
+ const envelopeReq = await toEnvelopeRequest(request);
321
+ return {
322
+ id,
323
+ method: request.method,
324
+ url: request.url,
325
+ path: request.path,
326
+ headers: envelopeReq.headers,
327
+ body: encodeBody(envelopeReq.body),
328
+ clientIp: envelopeReq.clientIp ?? '127.0.0.1',
329
+ };
330
+ }
331
+ async function startBuiltServerSingle(options) {
332
+ const paths = resolveBuiltServerPaths(options);
333
+ const runtime = createBuiltRuntime(options, paths);
334
+ const daemon = startBuiltDaemon(options, paths);
335
+ try {
336
+ const server = await startBuiltHttpServer(options, paths, async (request, response) => {
337
+ const reply = await handleBuiltRuntimeRequest(runtime, await toThreadedRequest(request, 0));
338
+ return sendThreadedReply(response, reply);
339
+ });
340
+ return {
341
+ ...server,
342
+ close: async () => {
343
+ daemon?.close();
344
+ await server.close();
345
+ },
346
+ };
347
+ }
348
+ catch (e) {
349
+ daemon?.close();
350
+ throw e;
351
+ }
352
+ }
353
+ export async function startBuiltServerWorker(options, controller) {
354
+ const paths = resolveBuiltServerPaths(options);
355
+ let nextRequestId = 1;
356
+ return startBuiltHttpServer({ ...options, threads: 1 }, paths, async (request, response) => {
357
+ const reply = await controller.request(await toThreadedRequest(request, nextRequestId++));
358
+ return sendThreadedReply(response, reply);
359
+ }, { onClientCount: (count) => controller.clientCount(count) });
360
+ }
361
+ function sendToWorker(worker, message) {
362
+ try {
363
+ worker.send?.(message);
364
+ }
365
+ catch {
366
+ }
367
+ }
368
+ function stopWorker(worker) {
369
+ return new Promise((resolve) => {
370
+ if (worker.exitCode !== null || worker.signalCode !== null) {
371
+ resolve();
372
+ return;
373
+ }
374
+ const hard = setTimeout(() => {
375
+ try {
376
+ worker.kill('SIGTERM');
377
+ }
378
+ catch {
379
+ }
380
+ resolve();
381
+ }, 1500);
382
+ hard.unref();
383
+ worker.once('exit', () => {
384
+ clearTimeout(hard);
385
+ resolve();
386
+ });
387
+ sendToWorker(worker, { toil: 'shutdown' });
388
+ });
389
+ }
390
+ async function startThreadedBuiltServer(options, threads) {
391
+ const paths = resolveBuiltServerPaths(options);
392
+ const runtime = createBuiltRuntime(options, paths);
393
+ const daemon = startBuiltDaemon(options, paths);
394
+ const workerScript = fileURLToPath(new URL('./production-worker.js', import.meta.url));
395
+ const workers = new Map();
396
+ const clientCounts = new Map();
397
+ let closing = false;
398
+ const handleMessage = (worker, message) => {
399
+ switch (message.toil) {
400
+ case 'clientCount':
401
+ clientCounts.set(message.workerId, message.count);
402
+ return;
403
+ case 'request':
404
+ void handleBuiltRuntimeRequest(runtime, message.request)
405
+ .then((reply) => sendToWorker(worker, {
406
+ toil: 'reply',
407
+ id: message.request.id,
408
+ reply,
409
+ }))
410
+ .catch((e) => sendToWorker(worker, {
411
+ toil: 'reply',
412
+ id: message.request.id,
413
+ reply: textReply(500, `internal error: ${String(e)}\n`),
414
+ }));
415
+ return;
416
+ case 'ready':
417
+ return;
418
+ }
419
+ };
420
+ const spawnWorker = (workerId, initial) => new Promise((resolve, reject) => {
421
+ const worker = fork(workerScript, [], {
422
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
423
+ env: process.env,
424
+ });
425
+ workers.set(workerId, worker);
426
+ clientCounts.set(workerId, 0);
427
+ let ready = false;
428
+ const failInitial = (error) => {
429
+ if (!initial || ready)
430
+ return;
431
+ reject(error);
432
+ };
433
+ worker.on('message', (value) => {
434
+ if (!isWorkerToPrimaryMessage(value))
435
+ return;
436
+ if (value.toil === 'ready') {
437
+ ready = true;
438
+ resolve();
439
+ return;
440
+ }
441
+ handleMessage(worker, value);
442
+ });
443
+ worker.once('error', (error) => {
444
+ failInitial(error);
445
+ });
446
+ worker.once('exit', (code, signal) => {
447
+ workers.delete(workerId);
448
+ clientCounts.delete(workerId);
449
+ if (closing)
450
+ return;
451
+ if (!ready) {
452
+ failInitial(new Error(`production worker ${String(workerId)} exited before listening (code ${String(code)}, signal ${String(signal)})`));
453
+ return;
454
+ }
455
+ void spawnWorker(workerId, false).catch((e) => {
456
+ process.stdout.write(pc.red(` x production worker ${String(workerId)} restart failed: ${String(e)}`) + '\n');
457
+ });
458
+ });
459
+ sendToWorker(worker, {
460
+ toil: 'start',
461
+ workerId,
462
+ options: { ...options, threads: 1 },
463
+ });
464
+ });
465
+ try {
466
+ await Promise.all(Array.from({ length: threads }, (_, i) => spawnWorker(i + 1, true)));
467
+ }
468
+ catch (e) {
469
+ closing = true;
470
+ daemon?.close();
471
+ await Promise.all([...workers.values()].map((worker) => stopWorker(worker)));
472
+ throw e;
473
+ }
474
+ process.stdout.write(pc.dim(` production threads: ${String(threads)} HTTP workers`) + '\n');
475
+ return {
476
+ port: paths.port,
477
+ host: paths.host,
478
+ wsPath: paths.wsPath,
479
+ broadcast: (message) => {
480
+ for (const worker of workers.values()) {
481
+ sendToWorker(worker, { toil: 'broadcast', message });
482
+ }
483
+ },
484
+ clientCount: () => {
485
+ let total = 0;
486
+ for (const count of clientCounts.values())
487
+ total += count;
488
+ return total;
489
+ },
490
+ close: async () => {
491
+ closing = true;
492
+ daemon?.close();
493
+ await Promise.all([...workers.values()].map((worker) => stopWorker(worker)));
494
+ },
495
+ };
496
+ }
497
+ export async function startBuiltServer(options) {
498
+ const threads = resolveThreadCount(options.threads);
499
+ if (threads <= 1)
500
+ return startBuiltServerSingle(options);
501
+ return startThreadedBuiltServer(options, threads);
502
+ }
@@ -16,11 +16,16 @@ export declare class WasmServerModule {
16
16
  private module;
17
17
  private loadedMtimeMs;
18
18
  private routeKinds;
19
+ private derives;
20
+ private derivesDirty;
19
21
  constructor(wasmPath: string);
20
22
  get available(): boolean;
21
23
  refresh(): boolean;
22
24
  dispatch(req: EnvelopeRequest): WasmDispatchResult;
23
25
  dispatchRender(req: EnvelopeRequest): Uint8Array;
26
+ private runAffectedDerives;
27
+ private rebuildDerivedViewsIfStale;
28
+ private runDerive;
24
29
  private assertImportSurface;
25
30
  private assertExportSurface;
26
31
  }
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- import { DbFunctionKind, persistDb, setDbCatalog } from '../db/index.js';
2
+ import { DbFunctionKind, derivesForWrites, parseDerives, persistDb, setDbCatalog, } from '../db/index.js';
3
3
  import { parseRouteKinds, routeKindForRequest } from '../db/routeKinds.js';
4
4
  import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from '../http/envelope.js';
5
5
  import { buildHostImports, freshDispatchState } from './host.js';
@@ -87,6 +87,8 @@ export class WasmServerModule {
87
87
  module = null;
88
88
  loadedMtimeMs = -1;
89
89
  routeKinds = [];
90
+ derives = [];
91
+ derivesDirty = false;
90
92
  constructor(wasmPath) {
91
93
  this.wasmPath = wasmPath;
92
94
  }
@@ -101,6 +103,8 @@ export class WasmServerModule {
101
103
  catch {
102
104
  this.module = null;
103
105
  this.routeKinds = [];
106
+ this.derives = [];
107
+ this.derivesDirty = false;
104
108
  this.loadedMtimeMs = -1;
105
109
  return false;
106
110
  }
@@ -112,13 +116,16 @@ export class WasmServerModule {
112
116
  this.assertExportSurface(module);
113
117
  setDbCatalog(bytes);
114
118
  this.routeKinds = parseRouteKinds(bytes);
119
+ this.derives = parseDerives(bytes);
115
120
  this.module = module;
116
121
  this.loadedMtimeMs = mtimeMs;
122
+ this.derivesDirty = this.derives.length > 0;
117
123
  return true;
118
124
  }
119
125
  dispatch(req) {
120
126
  if (this.module === null)
121
127
  throw new Error(`server wasm not loaded (${this.wasmPath})`);
128
+ this.rebuildDerivedViewsIfStale();
122
129
  const envelope = encodeRequestEnvelope(req);
123
130
  const ref = { memory: null };
124
131
  const state = freshDispatchState();
@@ -139,6 +146,7 @@ export class WasmServerModule {
139
146
  const headers = [...resp.headers, ...state.headers];
140
147
  const status = state.status ?? resp.status;
141
148
  const unhandled = headers.some(([n]) => n.toLowerCase() === UNHANDLED_HEADER);
149
+ this.runAffectedDerives(state.db.writtenCollections);
142
150
  persistDb();
143
151
  return {
144
152
  status,
@@ -171,6 +179,44 @@ export class WasmServerModule {
171
179
  throw new Error(`guest returned out-of-bounds values: ptr=${String(ptr)} len=${String(len)}`);
172
180
  return new Uint8Array(exports.memory.buffer, ptr, len).slice();
173
181
  }
182
+ runAffectedDerives(written) {
183
+ if (this.module === null)
184
+ return;
185
+ for (const derive of derivesForWrites(this.derives, written)) {
186
+ try {
187
+ this.runDerive(derive.deriveId);
188
+ }
189
+ catch (err) {
190
+ console.error(`[toil] derive ${derive.dbName}#${derive.methodName} failed:`, err);
191
+ }
192
+ }
193
+ }
194
+ rebuildDerivedViewsIfStale() {
195
+ if (!this.derivesDirty)
196
+ return;
197
+ this.derivesDirty = false;
198
+ for (const derive of this.derives) {
199
+ try {
200
+ this.runDerive(derive.deriveId);
201
+ }
202
+ catch (err) {
203
+ console.error(`[toil] derive ${derive.dbName}#${derive.methodName} failed on load:`, err);
204
+ }
205
+ }
206
+ }
207
+ runDerive(deriveId) {
208
+ if (this.module === null)
209
+ return;
210
+ const ref = { memory: null };
211
+ const state = freshDispatchState();
212
+ state.db.functionKind = DbFunctionKind.Derive;
213
+ const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
214
+ const exports = instance.exports;
215
+ ref.memory = exports.memory;
216
+ if (typeof exports.derive_run !== 'function')
217
+ return;
218
+ exports.derive_run(deriveId);
219
+ }
174
220
  assertImportSurface(module) {
175
221
  const missing = WebAssembly.Module.imports(module)
176
222
  .filter((i) => i.kind === 'function' && (i.module !== 'env' || !PROVIDED_IMPORTS.has(i.name)))
@@ -8,6 +8,7 @@ export interface DevServerOptions {
8
8
  readonly host?: string;
9
9
  readonly wasmFile: string;
10
10
  readonly coldWasmFile?: string;
11
+ readonly streamWasmFile?: string;
11
12
  readonly nodeMode?: string;
12
13
  readonly daemon?: ResolvedDaemonConfig;
13
14
  readonly vite: ViteTarget;