toiljs 0.0.67 → 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 (99) hide show
  1. package/CHANGELOG.md +5 -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 +16 -2
  17. package/build/compiler/toil-docs.generated.js +2 -2
  18. package/build/devserver/.tsbuildinfo +1 -1
  19. package/build/devserver/daemon/runtime.d.ts +13 -0
  20. package/build/devserver/daemon/runtime.js +29 -0
  21. package/build/devserver/db/database.d.ts +1 -0
  22. package/build/devserver/db/database.js +10 -0
  23. package/build/devserver/db/derives.d.ts +7 -0
  24. package/build/devserver/db/derives.js +94 -0
  25. package/build/devserver/db/index.d.ts +1 -0
  26. package/build/devserver/db/index.js +1 -0
  27. package/build/devserver/db/types.d.ts +1 -0
  28. package/build/devserver/db/types.js +1 -0
  29. package/build/devserver/http/proxy.d.ts +5 -1
  30. package/build/devserver/http/proxy.js +39 -36
  31. package/build/devserver/http/runtime.d.ts +62 -0
  32. package/build/devserver/http/runtime.js +194 -0
  33. package/build/devserver/index.d.ts +2 -0
  34. package/build/devserver/index.js +1 -0
  35. package/build/devserver/production-ipc.d.ts +50 -0
  36. package/build/devserver/production-ipc.js +21 -0
  37. package/build/devserver/production-worker.d.ts +1 -0
  38. package/build/devserver/production-worker.js +73 -0
  39. package/build/devserver/production.d.ts +35 -0
  40. package/build/devserver/production.js +502 -0
  41. package/build/devserver/runtime/module.d.ts +5 -0
  42. package/build/devserver/runtime/module.js +47 -1
  43. package/build/devserver/server.d.ts +1 -0
  44. package/build/devserver/server.js +32 -145
  45. package/build/devserver/ssr.d.ts +2 -0
  46. package/build/devserver/ssr.js +19 -2
  47. package/build/devserver/stream/catalog.d.ts +20 -0
  48. package/build/devserver/stream/catalog.js +54 -0
  49. package/build/devserver/stream/host.d.ts +9 -0
  50. package/build/devserver/stream/host.js +15 -0
  51. package/build/devserver/stream/index.d.ts +37 -0
  52. package/build/devserver/stream/index.js +220 -0
  53. package/build/devserver/stream/manager.d.ts +34 -0
  54. package/build/devserver/stream/manager.js +103 -0
  55. package/build/devserver/stream/router.d.ts +25 -0
  56. package/build/devserver/stream/router.js +64 -0
  57. package/build/devserver/stream/wire.d.ts +5 -0
  58. package/build/devserver/stream/wire.js +33 -0
  59. package/build/devserver/stream/ws.d.ts +18 -0
  60. package/build/devserver/stream/ws.js +46 -0
  61. package/docs/cli.md +3 -1
  62. package/docs/getting-started.md +7 -7
  63. package/examples/basic/server/routes/Guestbook.ts +38 -13
  64. package/package.json +2 -2
  65. package/src/cli/index.ts +14 -1
  66. package/src/client/index.ts +2 -0
  67. package/src/client/rpc.ts +25 -1
  68. package/src/client/stream/client.ts +107 -0
  69. package/src/compiler/config.ts +15 -7
  70. package/src/compiler/index.ts +24 -5
  71. package/src/compiler/toil-docs.generated.ts +2 -2
  72. package/src/devserver/daemon/runtime.ts +48 -0
  73. package/src/devserver/db/database.ts +14 -0
  74. package/src/devserver/db/derives.ts +121 -0
  75. package/src/devserver/db/index.ts +1 -0
  76. package/src/devserver/db/types.ts +6 -0
  77. package/src/devserver/http/proxy.ts +53 -39
  78. package/src/devserver/http/runtime.ts +287 -0
  79. package/src/devserver/index.ts +2 -0
  80. package/src/devserver/production-ipc.ts +63 -0
  81. package/src/devserver/production-worker.ts +83 -0
  82. package/src/devserver/production.ts +706 -0
  83. package/src/devserver/runtime/module.ts +95 -1
  84. package/src/devserver/server.ts +52 -201
  85. package/src/devserver/ssr.ts +23 -3
  86. package/src/devserver/stream/catalog.ts +106 -0
  87. package/src/devserver/stream/host.ts +42 -0
  88. package/src/devserver/stream/index.ts +308 -0
  89. package/src/devserver/stream/manager.ts +163 -0
  90. package/src/devserver/stream/router.ts +101 -0
  91. package/src/devserver/stream/wire.ts +58 -0
  92. package/src/devserver/stream/ws.ts +76 -0
  93. package/test/built-ssr.test.ts +98 -0
  94. package/test/devserver.test.ts +20 -4
  95. package/test/example-guestbook.test.ts +8 -5
  96. package/test/fixtures/stream-echo.ts +26 -0
  97. package/test/fixtures/stream-gate.ts +24 -0
  98. package/test/fixtures/stream-trap.ts +18 -0
  99. package/test/stream-emulation.test.ts +394 -0
@@ -0,0 +1,21 @@
1
+ export function encodeBody(body) {
2
+ return Buffer.from(body.buffer, body.byteOffset, body.length).toString('base64');
3
+ }
4
+ export function decodeBody(body) {
5
+ return Buffer.from(body, 'base64');
6
+ }
7
+ export function isWorkerToPrimaryMessage(value) {
8
+ if (typeof value !== 'object' || value === null)
9
+ return false;
10
+ const message = value;
11
+ return message.toil === 'ready' || message.toil === 'clientCount' || message.toil === 'request';
12
+ }
13
+ export function isPrimaryToWorkerMessage(value) {
14
+ if (typeof value !== 'object' || value === null)
15
+ return false;
16
+ const message = value;
17
+ return (message.toil === 'start' ||
18
+ message.toil === 'reply' ||
19
+ message.toil === 'broadcast' ||
20
+ message.toil === 'shutdown');
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import { startBuiltServerWorker } from './production.js';
2
+ import { isPrimaryToWorkerMessage, } from './production-ipc.js';
3
+ let running = null;
4
+ let workerId = 0;
5
+ const pending = new Map();
6
+ function send(message) {
7
+ try {
8
+ process.send?.(message);
9
+ }
10
+ catch {
11
+ }
12
+ }
13
+ function requestPrimary(request) {
14
+ const id = request.id;
15
+ return new Promise((resolve, reject) => {
16
+ const timeout = setTimeout(() => {
17
+ pending.delete(id);
18
+ reject(new Error('primary request timed out'));
19
+ }, 120_000);
20
+ timeout.unref();
21
+ pending.set(id, (reply) => {
22
+ clearTimeout(timeout);
23
+ resolve(reply);
24
+ });
25
+ send({ toil: 'request', request });
26
+ });
27
+ }
28
+ async function start(message) {
29
+ if (running !== null)
30
+ return;
31
+ workerId = message.workerId;
32
+ const options = message.options;
33
+ running = await startBuiltServerWorker(options, {
34
+ request: requestPrimary,
35
+ clientCount: (count) => send({ toil: 'clientCount', workerId, count }),
36
+ });
37
+ send({ toil: 'ready', workerId, port: running.port });
38
+ }
39
+ async function shutdown() {
40
+ const server = running;
41
+ running = null;
42
+ if (server !== null)
43
+ await server.close();
44
+ }
45
+ process.on('message', (value) => {
46
+ if (!isPrimaryToWorkerMessage(value))
47
+ return;
48
+ switch (value.toil) {
49
+ case 'start':
50
+ void start(value).catch((e) => {
51
+ process.stderr.write(`toiljs production worker failed: ${String(e)}\n`);
52
+ process.exit(1);
53
+ });
54
+ return;
55
+ case 'reply': {
56
+ const resolve = pending.get(value.id);
57
+ if (resolve === undefined)
58
+ return;
59
+ pending.delete(value.id);
60
+ resolve(value.reply);
61
+ return;
62
+ }
63
+ case 'broadcast':
64
+ running?.broadcast(value.message);
65
+ return;
66
+ case 'shutdown':
67
+ void shutdown().finally(() => process.exit(0));
68
+ return;
69
+ }
70
+ });
71
+ process.once('disconnect', () => {
72
+ void shutdown().finally(() => process.exit(0));
73
+ });
@@ -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
  }