toiljs 0.0.59 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +311 -118
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -0
  32. package/build/devserver/db/catalog.js +80 -0
  33. package/build/devserver/db/database.d.ts +80 -0
  34. package/build/devserver/db/database.js +1032 -0
  35. package/build/devserver/db/index.d.ts +3 -0
  36. package/build/devserver/db/index.js +3 -0
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +121 -0
  40. package/build/devserver/db/types.js +52 -0
  41. package/build/devserver/email/index.js +1 -1
  42. package/build/devserver/index.d.ts +19 -24
  43. package/build/devserver/index.js +11 -165
  44. package/build/devserver/mstore/store.d.ts +18 -0
  45. package/build/devserver/mstore/store.js +82 -0
  46. package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
  47. package/build/devserver/{host.js → runtime/host.js} +51 -7
  48. package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
  49. package/build/devserver/{module.js → runtime/module.js} +34 -1
  50. package/build/devserver/server.d.ts +23 -0
  51. package/build/devserver/server.js +223 -0
  52. package/build/devserver/ssr.d.ts +25 -0
  53. package/build/devserver/ssr.js +114 -0
  54. package/build/devserver/wasm/sections.d.ts +2 -0
  55. package/build/devserver/wasm/sections.js +42 -0
  56. package/build/devserver/wasm/surface.d.ts +18 -0
  57. package/build/devserver/wasm/surface.js +41 -0
  58. package/docs/README.md +4 -4
  59. package/docs/auth-todo.md +6 -6
  60. package/docs/caching.md +5 -5
  61. package/docs/cli.md +15 -0
  62. package/docs/client.md +40 -0
  63. package/docs/crypto.md +4 -4
  64. package/docs/data.md +6 -6
  65. package/docs/email.md +28 -28
  66. package/docs/environment.md +10 -10
  67. package/docs/index.md +26 -0
  68. package/docs/ratelimit.md +10 -10
  69. package/docs/routing.md +2 -2
  70. package/docs/server.md +61 -0
  71. package/docs/ssr.md +561 -113
  72. package/docs/styling.md +22 -0
  73. package/docs/time.md +3 -3
  74. package/eslint.config.js +10 -1
  75. package/examples/basic/client/components/Header.tsx +3 -0
  76. package/examples/basic/client/routes/features/actions.tsx +0 -2
  77. package/examples/basic/client/routes/hello.tsx +89 -19
  78. package/examples/basic/client/styles/main.css +48 -0
  79. package/examples/basic/server/SsrHelloRender.ts +97 -0
  80. package/examples/basic/server/main.ts +5 -0
  81. package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
  82. package/examples/basic/server/streams/Echo.ts +49 -0
  83. package/package.json +12 -10
  84. package/scripts/gen-toil-docs.mjs +96 -0
  85. package/server/runtime/time.ts +3 -3
  86. package/src/cli/create.ts +40 -3
  87. package/src/cli/db.ts +158 -0
  88. package/src/cli/diagnostics.ts +19 -0
  89. package/src/cli/doctor.ts +20 -0
  90. package/src/cli/index.ts +10 -0
  91. package/src/cli/update.ts +58 -0
  92. package/src/client/index.ts +1 -1
  93. package/src/client/routing/mount.tsx +18 -2
  94. package/src/client/ssr/markers.tsx +22 -0
  95. package/src/compiler/config.ts +88 -2
  96. package/src/compiler/docs.ts +47 -308
  97. package/src/compiler/index.ts +236 -32
  98. package/src/compiler/ssr-codegen.ts +1 -1
  99. package/src/compiler/template-build.ts +247 -46
  100. package/src/compiler/toil-docs.generated.ts +26 -0
  101. package/src/devserver/daemon/catalog.ts +120 -0
  102. package/src/devserver/daemon/cron.ts +87 -0
  103. package/src/devserver/daemon/host.ts +224 -0
  104. package/src/devserver/daemon/index.ts +349 -0
  105. package/src/devserver/db/catalog.ts +108 -0
  106. package/src/devserver/db/database.ts +1633 -0
  107. package/src/devserver/db/index.ts +18 -0
  108. package/src/devserver/db/routeKinds.ts +147 -0
  109. package/src/devserver/db/types.ts +139 -0
  110. package/src/devserver/email/index.ts +1 -1
  111. package/src/devserver/index.ts +31 -287
  112. package/src/devserver/mstore/store.ts +121 -0
  113. package/src/devserver/{host.ts → runtime/host.ts} +98 -7
  114. package/src/devserver/{module.ts → runtime/module.ts} +47 -1
  115. package/src/devserver/server.ts +393 -0
  116. package/src/devserver/ssr.ts +166 -0
  117. package/src/devserver/wasm/sections.ts +59 -0
  118. package/src/devserver/wasm/surface.ts +88 -0
  119. package/test/daemon-build.test.ts +198 -0
  120. package/test/daemon-catalog.test.ts +265 -0
  121. package/test/daemon-emulation.test.ts +216 -0
  122. package/test/db.test.ts +0 -0
  123. package/test/devserver-database.test.ts +510 -14
  124. package/test/devserver-pqauth.test.ts +1 -1
  125. package/test/devserver-secrets.test.ts +5 -1
  126. package/test/doctor.test.ts +13 -0
  127. package/test/email-preview.test.ts +6 -1
  128. package/test/example-guestbook.test.ts +43 -1
  129. package/test/fixtures/daemon-app.ts +56 -0
  130. package/test/global-setup.ts +17 -0
  131. package/test/pqauth-e2e.test.ts +1 -1
  132. package/test/ssr-render.test.ts +94 -27
  133. package/test/ssr-template.test.tsx +44 -1
  134. package/vitest.config.ts +3 -0
  135. package/build/devserver/database.d.ts +0 -8
  136. package/build/devserver/database.js +0 -418
  137. package/src/devserver/database.ts +0 -618
  138. /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
  139. /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
  140. /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
  141. /package/build/devserver/{env.js → config/env.js} +0 -0
  142. /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
  143. /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
  144. /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
  145. /package/build/devserver/{cache.js → http/cache.js} +0 -0
  146. /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
  147. /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
  148. /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
  149. /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
  150. /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
  151. /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
  152. /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
  153. /package/src/devserver/{env.ts → config/env.ts} +0 -0
  154. /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
  155. /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
  156. /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
  157. /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
  158. /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
@@ -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;
@@ -32,12 +32,21 @@ function mem(ref) {
32
32
  throw new Error('host import called before memory was bound');
33
33
  return Buffer.from(ref.memory.buffer);
34
34
  }
35
- function readBytes(ref, ptr, len) {
35
+ export function readBytes(ref, ptr, len) {
36
36
  const m = mem(ref);
37
37
  if (ptr < 0 || len < 0 || ptr + len > m.length)
38
38
  throw new Error(`host import read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
39
39
  return m.subarray(ptr, ptr + len);
40
40
  }
41
+ export function writeBytesOut(ref, bytes, outPtr, outCap) {
42
+ if (bytes.length > outCap)
43
+ return -1;
44
+ const m = mem(ref);
45
+ if (outPtr < 0 || outPtr + bytes.length > m.length)
46
+ throw new Error('host import write out of bounds');
47
+ bytes.copy(m, outPtr);
48
+ return bytes.length;
49
+ }
41
50
  function readGuestString(ref, ptr) {
42
51
  if (ptr === 0)
43
52
  return '';
@@ -80,6 +89,41 @@ function envLookup(ref, keyPtr, keyLen, outPtr, outCap, secure) {
80
89
  bytes.copy(m, outPtr);
81
90
  return bytes.length;
82
91
  }
92
+ export function buildEnvImports(ref, _state) {
93
+ return {
94
+ abort: (msgPtr, filePtr, line, col) => {
95
+ throw new WasmAbortError(readGuestString(ref, msgPtr), readGuestString(ref, filePtr), line, col);
96
+ },
97
+ env_get: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
98
+ env_get_secure: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
99
+ thread_spawn: (_startArg) => -1,
100
+ 'Date.now': () => BigInt(Date.now()),
101
+ email_send: (reqPtr, reqLen) => {
102
+ const raw = readBytes(ref, reqPtr, reqLen);
103
+ const svc = getEmailService();
104
+ if (svc === null) {
105
+ const to = parseEmailBlob(raw)?.to ?? '<unparsed>';
106
+ process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
107
+ return EmailStatus.Sent;
108
+ }
109
+ const { status, parsed } = svc.prepare(raw);
110
+ if (parsed === null) {
111
+ process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
112
+ return status;
113
+ }
114
+ void svc
115
+ .deliver(parsed)
116
+ .then((s) => {
117
+ const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
118
+ process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
119
+ })
120
+ .catch((e) => {
121
+ process.stdout.write(` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`);
122
+ });
123
+ return EmailStatus.Sent;
124
+ },
125
+ };
126
+ }
83
127
  export function buildHostImports(ref, state) {
84
128
  return {
85
129
  env: {
@@ -157,7 +201,7 @@ export function buildHostImports(ref, state) {
157
201
  env_get: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
158
202
  env_get_secure: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
159
203
  thread_spawn: (_startArg) => -1,
160
- 'Date.now': () => Date.now(),
204
+ 'Date.now': () => BigInt(Date.now()),
161
205
  ...buildCryptoImports(ref, state.crypto),
162
206
  ...buildDatabaseImports(ref, state.db),
163
207
  },
@@ -1,4 +1,4 @@
1
- import { type EnvelopeRequest } from './envelope.js';
1
+ import { type EnvelopeRequest } from '../http/envelope.js';
2
2
  export { WasmAbortError } from './host.js';
3
3
  export declare const UNHANDLED_HEADER = "x-toil-unhandled";
4
4
  export interface WasmDispatchResult {
@@ -12,6 +12,7 @@ export declare class WasmServerModule {
12
12
  private readonly wasmPath;
13
13
  private module;
14
14
  private loadedMtimeMs;
15
+ private routeKinds;
15
16
  constructor(wasmPath: string);
16
17
  get available(): boolean;
17
18
  refresh(): boolean;
@@ -1,9 +1,25 @@
1
1
  import fs from 'node:fs';
2
- import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from './envelope.js';
2
+ import { DbFunctionKind, persistDb, setDbCatalog } from '../db/index.js';
3
+ import { parseRouteKinds, routeKindForRequest } from '../db/routeKinds.js';
4
+ import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from '../http/envelope.js';
3
5
  import { buildHostImports, freshDispatchState } from './host.js';
4
6
  export { WasmAbortError } from './host.js';
5
7
  export const UNHANDLED_HEADER = 'x-toil-unhandled';
6
8
  const WASM_PAGE = 65536;
9
+ function dbKindForHttpMethod(method) {
10
+ switch (method.toUpperCase()) {
11
+ case 'GET':
12
+ case 'HEAD':
13
+ case 'OPTIONS':
14
+ return DbFunctionKind.Query;
15
+ case 'POST':
16
+ case 'PUT':
17
+ case 'PATCH':
18
+ case 'DELETE':
19
+ default:
20
+ return DbFunctionKind.Action;
21
+ }
22
+ }
7
23
  const PROVIDED_IMPORTS = new Set([
8
24
  'abort',
9
25
  'set_status',
@@ -50,6 +66,8 @@ const PROVIDED_IMPORTS = new Set([
50
66
  'data.counter_get',
51
67
  'data.counter_add',
52
68
  'data.append',
69
+ 'data.append_once',
70
+ 'data.enqueue',
53
71
  'data.latest',
54
72
  'data.capacity_set_total',
55
73
  'data.capacity_available',
@@ -57,11 +75,14 @@ const PROVIDED_IMPORTS = new Set([
57
75
  'data.capacity_confirm',
58
76
  'data.capacity_cancel',
59
77
  'data.take_result',
78
+ 'data.result_schema_version',
79
+ 'data.write_allowed',
60
80
  ]);
61
81
  export class WasmServerModule {
62
82
  wasmPath;
63
83
  module = null;
64
84
  loadedMtimeMs = -1;
85
+ routeKinds = [];
65
86
  constructor(wasmPath) {
66
87
  this.wasmPath = wasmPath;
67
88
  }
@@ -75,6 +96,7 @@ export class WasmServerModule {
75
96
  }
76
97
  catch {
77
98
  this.module = null;
99
+ this.routeKinds = [];
78
100
  this.loadedMtimeMs = -1;
79
101
  return false;
80
102
  }
@@ -84,6 +106,8 @@ export class WasmServerModule {
84
106
  const module = new WebAssembly.Module(bytes);
85
107
  this.assertImportSurface(module);
86
108
  this.assertExportSurface(module);
109
+ setDbCatalog(bytes);
110
+ this.routeKinds = parseRouteKinds(bytes);
87
111
  this.module = module;
88
112
  this.loadedMtimeMs = mtimeMs;
89
113
  return true;
@@ -95,6 +119,13 @@ export class WasmServerModule {
95
119
  const ref = { memory: null };
96
120
  const state = freshDispatchState();
97
121
  state.clientIp = req.clientIp ?? '';
122
+ const routeKind = routeKindForRequest(this.routeKinds, req.method, req.path);
123
+ state.db.functionKind =
124
+ this.routeKinds.length === 0
125
+ ? DbFunctionKind.Job
126
+ : routeKind === DbFunctionKind.Query
127
+ ? DbFunctionKind.Query
128
+ : dbKindForHttpMethod(req.method);
98
129
  const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
99
130
  const exports = instance.exports;
100
131
  ref.memory = exports.memory;
@@ -110,6 +141,7 @@ export class WasmServerModule {
110
141
  const headers = [...resp.headers, ...state.headers];
111
142
  const status = state.status ?? resp.status;
112
143
  const unhandled = headers.some(([n]) => n.toLowerCase() === UNHANDLED_HEADER);
144
+ persistDb();
113
145
  return {
114
146
  status,
115
147
  headers: headers.filter(([n]) => n.toLowerCase() !== UNHANDLED_HEADER),
@@ -125,6 +157,7 @@ export class WasmServerModule {
125
157
  const ref = { memory: null };
126
158
  const state = freshDispatchState();
127
159
  state.clientIp = req.clientIp ?? '';
160
+ state.db.functionKind = DbFunctionKind.Query;
128
161
  const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
129
162
  const exports = instance.exports;
130
163
  if (typeof exports.render !== 'function')
@@ -0,0 +1,23 @@
1
+ import type { EmailBackendConfig } from 'toiljs/shared';
2
+ import type { ResolvedDaemonConfig } from './daemon/host.js';
3
+ import { type ViteTarget } from './http/proxy.js';
4
+ import { type DevSsrTemplate } from './ssr.js';
5
+ export interface DevServerOptions {
6
+ readonly root: string;
7
+ readonly port: number;
8
+ readonly host?: string;
9
+ readonly wasmFile: string;
10
+ readonly coldWasmFile?: string;
11
+ readonly nodeMode?: string;
12
+ readonly daemon?: ResolvedDaemonConfig;
13
+ readonly vite: ViteTarget;
14
+ readonly maxBodyLength?: number;
15
+ readonly email?: EmailBackendConfig;
16
+ readonly ssrTemplates?: readonly DevSsrTemplate[];
17
+ }
18
+ export interface RunningDevServer {
19
+ readonly port: number;
20
+ readonly host: string;
21
+ close(): Promise<void>;
22
+ }
23
+ export declare function startDevServer(options: DevServerOptions): Promise<RunningDevServer>;
@@ -0,0 +1,223 @@
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 { DaemonHost, daemonEmulationEnabled } from './daemon/index.js';
6
+ import { configureDbPersistence } from './db/index.js';
7
+ import { initEmailService } from './email/index.js';
8
+ import { applyCacheRule, lookupCache } from './http/cache.js';
9
+ import { METHOD_CODES } from './http/envelope.js';
10
+ import { proxyToVite, wireWebsocketProxy } from './http/proxy.js';
11
+ import { WasmServerModule } from './runtime/module.js';
12
+ import { assembleSsr, buildSsrRoutes, pathnameOf, } from './ssr.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
+ function sendSsr(response, out, headOnly) {
83
+ response.status(out.status);
84
+ let hasContentType = false;
85
+ for (const [name, value] of out.headers) {
86
+ if (name.toLowerCase() === 'content-type')
87
+ hasContentType = true;
88
+ response.header(name, value);
89
+ }
90
+ if (!hasContentType)
91
+ response.header('content-type', 'text/html; charset=utf-8');
92
+ response.header('server', 'toil-dev');
93
+ if (headOnly) {
94
+ response.send('');
95
+ return;
96
+ }
97
+ response.send(Buffer.from(out.html.buffer, out.html.byteOffset, out.html.length));
98
+ }
99
+ export async function startDevServer(options) {
100
+ const host = options.host ?? '127.0.0.1';
101
+ const root = path.resolve(options.root);
102
+ const emailInit = initEmailService(root, options.email);
103
+ if (emailInit.service !== null) {
104
+ process.stdout.write(pc.dim(` ✉ email enabled: ${emailInit.note}`) + '\n');
105
+ }
106
+ else if (emailInit.note !== null) {
107
+ process.stdout.write(pc.yellow(' ! ') + pc.dim(`email off: ${emailInit.note}`) + '\n');
108
+ }
109
+ const module = new WasmServerModule(options.wasmFile);
110
+ const ssrRoutes = buildSsrRoutes(options.ssrTemplates ?? []);
111
+ if (ssrRoutes.length > 0) {
112
+ process.stdout.write(pc.dim(` edge SSR: ${String(ssrRoutes.length)} route(s) served server-side`) + '\n');
113
+ }
114
+ configureDbPersistence(path.join(root, '.toil', 'devdata.json'));
115
+ let warnedMissing = false;
116
+ let loadedOnce = false;
117
+ const refresh = () => {
118
+ try {
119
+ if (module.refresh() && loadedOnce) {
120
+ process.stdout.write(pc.green(' ✓ ') + pc.dim('server module reloaded') + '\n');
121
+ }
122
+ loadedOnce ||= module.available;
123
+ }
124
+ catch (e) {
125
+ process.stdout.write(pc.red(` ✗ server wasm failed to load: ${String(e)}`) + '\n');
126
+ }
127
+ if (!module.available && !warnedMissing) {
128
+ warnedMissing = true;
129
+ process.stdout.write(pc.yellow(' ! ') +
130
+ pc.dim(`server wasm not found at ${options.wasmFile}; serving client only`) +
131
+ '\n');
132
+ }
133
+ };
134
+ refresh();
135
+ const app = new Server({
136
+ max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
137
+ max_body_buffer: 1024 * 32,
138
+ fast_abort: true,
139
+ });
140
+ app.set_error_handler((_request, response, error) => {
141
+ if (response.completed)
142
+ return;
143
+ response.atomic(() => {
144
+ response.status(500).send(`internal error: ${error.message}\n`);
145
+ });
146
+ });
147
+ wireWebsocketProxy(app, options.vite);
148
+ const nodeMode = options.nodeMode ?? 'all';
149
+ let daemonHost = null;
150
+ let daemonTimer = null;
151
+ if (options.coldWasmFile !== undefined && daemonEmulationEnabled(nodeMode) && options.daemon) {
152
+ daemonHost = new DaemonHost(options.coldWasmFile, options.daemon, nodeMode);
153
+ const pollDaemon = () => {
154
+ try {
155
+ daemonHost?.refresh();
156
+ }
157
+ catch (e) {
158
+ process.stdout.write(pc.red(` ✗ daemon reload failed: ${String(e)}`) + '\n');
159
+ }
160
+ };
161
+ pollDaemon();
162
+ daemonTimer = setInterval(pollDaemon, 500);
163
+ daemonTimer.unref?.();
164
+ }
165
+ app.any('/*', async (request, response) => {
166
+ response.removeHeader('uWebSockets');
167
+ const dispatchable = !isViteInternal(request.url) && METHOD_CODES[request.method] !== undefined;
168
+ if (dispatchable)
169
+ refresh();
170
+ if (dispatchable && module.available) {
171
+ const envelopeReq = await toEnvelopeRequest(request);
172
+ const cacheHost = request.headers.host ?? 'dev';
173
+ const hasAuth = request.headers.cookie !== undefined || request.headers.authorization !== undefined;
174
+ const cached = lookupCache(cacheHost, request.method, request.url, envelopeReq.body);
175
+ if (cached !== null) {
176
+ sendWasmResponse(response, root, cached);
177
+ return;
178
+ }
179
+ try {
180
+ const result = module.dispatch(envelopeReq);
181
+ if (!result.unhandled) {
182
+ const finalized = applyCacheRule(cacheHost, request.method, request.url, envelopeReq.body, hasAuth, result);
183
+ sendWasmResponse(response, root, finalized);
184
+ return;
185
+ }
186
+ }
187
+ catch (e) {
188
+ process.stdout.write(pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
189
+ '\n');
190
+ response.status(500).send('internal error\n');
191
+ return;
192
+ }
193
+ if ((request.method === 'GET' || request.method === 'HEAD') &&
194
+ ssrRoutes.length > 0) {
195
+ const route = ssrRoutes.find((r) => r.test(pathnameOf(request.url)));
196
+ if (route) {
197
+ try {
198
+ const out = assembleSsr(route, module.dispatchRender(envelopeReq));
199
+ if (out !== null) {
200
+ sendSsr(response, out, request.method === 'HEAD');
201
+ return;
202
+ }
203
+ }
204
+ catch (e) {
205
+ process.stdout.write(pc.red(` ✗ SSR ${request.path}: ${String(e)}`) + '\n');
206
+ }
207
+ }
208
+ }
209
+ }
210
+ await proxyToVite(request, response, options.vite);
211
+ });
212
+ await app.listen(options.port, host);
213
+ return {
214
+ port: options.port,
215
+ host,
216
+ close: async () => {
217
+ if (daemonTimer !== null)
218
+ clearInterval(daemonTimer);
219
+ daemonHost?.close();
220
+ await app.shutdown();
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,25 @@
1
+ export interface DevSsrTemplate {
2
+ pattern: string;
3
+ name: string;
4
+ tmpl: Uint8Array;
5
+ entries: {
6
+ id: number;
7
+ offset: number;
8
+ }[];
9
+ }
10
+ export interface SsrRoute {
11
+ test: (pathname: string) => boolean;
12
+ tmpl: Uint8Array;
13
+ entries: {
14
+ id: number;
15
+ offset: number;
16
+ }[];
17
+ }
18
+ export declare function pathnameOf(url: string): string;
19
+ export declare function buildSsrRoutes(templates: readonly DevSsrTemplate[]): SsrRoute[];
20
+ export interface SsrResult {
21
+ status: number;
22
+ headers: [string, string][];
23
+ html: Uint8Array;
24
+ }
25
+ export declare function assembleSsr(route: SsrRoute, envelope: Uint8Array): SsrResult | null;
@@ -0,0 +1,114 @@
1
+ export function pathnameOf(url) {
2
+ const q = url.indexOf('?');
3
+ return q < 0 ? url : url.slice(0, q);
4
+ }
5
+ function patternToTest(pattern) {
6
+ const norm = (p) => (p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p);
7
+ let re = '';
8
+ let i = 0;
9
+ while (i < pattern.length) {
10
+ const ch = pattern[i];
11
+ if (ch === ':') {
12
+ i++;
13
+ while (i < pattern.length && /[A-Za-z0-9_]/.test(pattern[i]))
14
+ i++;
15
+ re += '[^/]+';
16
+ }
17
+ else if (ch === '*') {
18
+ re += '.*';
19
+ i++;
20
+ }
21
+ else if (ch === '[') {
22
+ const end = pattern.indexOf(']', i);
23
+ const inner = end < 0 ? '' : pattern.slice(i + 1, end);
24
+ re += inner.startsWith('...') ? '.*' : '[^/]+';
25
+ i = end < 0 ? pattern.length : end + 1;
26
+ }
27
+ else {
28
+ re += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ i++;
30
+ }
31
+ }
32
+ const compiled = new RegExp(`^${re}$`);
33
+ return (pathname) => compiled.test(norm(pathname));
34
+ }
35
+ export function buildSsrRoutes(templates) {
36
+ return templates.map((t) => ({ test: patternToTest(t.pattern), tmpl: t.tmpl, entries: t.entries }));
37
+ }
38
+ function decodeValues(buf) {
39
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
40
+ let o = 0;
41
+ const need = (n) => o + n <= buf.byteLength;
42
+ try {
43
+ if (!need(2 + 32 + 2))
44
+ return null;
45
+ const status = dv.getUint16(o, true);
46
+ o += 2;
47
+ o += 32;
48
+ const nHeaders = dv.getUint16(o, true);
49
+ o += 2;
50
+ const headers = [];
51
+ const dec = new TextDecoder();
52
+ for (let i = 0; i < nHeaders; i++) {
53
+ if (!need(4))
54
+ return null;
55
+ const nameLen = dv.getUint16(o, true);
56
+ const valLen = dv.getUint16(o + 2, true);
57
+ o += 4;
58
+ if (!need(nameLen + valLen))
59
+ return null;
60
+ const name = dec.decode(buf.subarray(o, o + nameLen));
61
+ o += nameLen;
62
+ const val = dec.decode(buf.subarray(o, o + valLen));
63
+ o += valLen;
64
+ headers.push([name, val]);
65
+ }
66
+ if (!need(2))
67
+ return null;
68
+ const nSlots = dv.getUint16(o, true);
69
+ o += 2;
70
+ const values = new Map();
71
+ for (let i = 0; i < nSlots; i++) {
72
+ if (!need(2 + 1 + 4))
73
+ return null;
74
+ const id = dv.getUint16(o, true);
75
+ o += 2;
76
+ o += 1;
77
+ const len = dv.getUint32(o, true);
78
+ o += 4;
79
+ if (!need(len))
80
+ return null;
81
+ values.set(id, buf.subarray(o, o + len));
82
+ o += len;
83
+ }
84
+ return { status, headers, values };
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ function splice(tmpl, inserts) {
91
+ const parts = [];
92
+ let prev = 0;
93
+ for (const ins of inserts) {
94
+ if (ins.offset > prev)
95
+ parts.push(tmpl.subarray(prev, ins.offset));
96
+ if (ins.value.length > 0)
97
+ parts.push(ins.value);
98
+ prev = ins.offset;
99
+ }
100
+ if (tmpl.length > prev)
101
+ parts.push(tmpl.subarray(prev));
102
+ return Buffer.concat(parts.map((p) => Buffer.from(p.buffer, p.byteOffset, p.byteLength)));
103
+ }
104
+ export function assembleSsr(route, envelope) {
105
+ const decoded = decodeValues(envelope);
106
+ if (decoded === null)
107
+ return null;
108
+ if (decoded.status >= 500 || decoded.values.size === 0)
109
+ return null;
110
+ const inserts = route.entries
111
+ .map((e) => ({ offset: e.offset, value: decoded.values.get(e.id) ?? new Uint8Array(0) }))
112
+ .sort((a, b) => a.offset - b.offset);
113
+ return { status: decoded.status, headers: decoded.headers, html: splice(route.tmpl, inserts) };
114
+ }
@@ -0,0 +1,2 @@
1
+ export declare function leb(buf: Buffer, pos: number): [number, number];
2
+ export declare function customSection(wasm: Buffer, want: string): Buffer | null;
@@ -0,0 +1,42 @@
1
+ export function leb(buf, pos) {
2
+ let result = 0;
3
+ let shift = 0;
4
+ let p = pos;
5
+ for (;;) {
6
+ if (p >= buf.length)
7
+ throw new RangeError('leb128 past end of buffer');
8
+ const b = buf[p++];
9
+ result |= (b & 0x7f) << shift;
10
+ if ((b & 0x80) === 0)
11
+ break;
12
+ shift += 7;
13
+ if (shift > 35)
14
+ throw new RangeError('leb128 too long');
15
+ }
16
+ return [result >>> 0, p];
17
+ }
18
+ export function customSection(wasm, want) {
19
+ if (wasm.length < 8 ||
20
+ wasm[0] !== 0x00 ||
21
+ wasm[1] !== 0x61 ||
22
+ wasm[2] !== 0x73 ||
23
+ wasm[3] !== 0x6d)
24
+ return null;
25
+ let pos = 8;
26
+ while (pos < wasm.length) {
27
+ const id = wasm[pos++];
28
+ let size;
29
+ [size, pos] = leb(wasm, pos);
30
+ const end = pos + size;
31
+ if (end > wasm.length || end < pos)
32
+ return null;
33
+ if (id === 0) {
34
+ const [nameLen, namePos] = leb(wasm, pos);
35
+ if (namePos + nameLen <= end &&
36
+ wasm.toString('latin1', namePos, namePos + nameLen) === want)
37
+ return wasm.subarray(namePos + nameLen, end);
38
+ }
39
+ pos = end;
40
+ }
41
+ return null;
42
+ }
@@ -0,0 +1,18 @@
1
+ export interface SurfaceFlags {
2
+ readonly rest: boolean;
3
+ readonly stream: boolean;
4
+ readonly daemon: boolean;
5
+ readonly scheduled: boolean;
6
+ readonly database: boolean;
7
+ readonly render: boolean;
8
+ }
9
+ export interface Surface {
10
+ readonly targetMode: 'hot' | 'cold';
11
+ readonly flags: SurfaceFlags;
12
+ readonly abiVersion: number;
13
+ readonly buildId: string;
14
+ readonly fingerprint: number;
15
+ readonly dataCoherenceHash: number;
16
+ readonly pairCoherenceHash: number;
17
+ }
18
+ export declare function parseSurface(wasm: Buffer): Surface | 'absent' | 'invalid';