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
@@ -1,101 +1,19 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { Server } from '@dacely/hyper-express';
4
3
  import pc from 'picocolors';
5
- import { DaemonHost, daemonEmulationEnabled } from './daemon/index.js';
4
+ import { startDaemonRuntime } from './daemon/runtime.js';
6
5
  import { configureDbPersistence } from './db/index.js';
7
6
  import { initEmailService } from './email/index.js';
8
- import { applyCacheRule, lookupCache } from './http/cache.js';
9
- import { METHOD_CODES } from './http/envelope.js';
10
7
  import { proxyToVite, wireWebsocketProxy } from './http/proxy.js';
8
+ import { assembleRouteSsr, dispatchWasmRequest, installRuntimeErrorHandler, isDispatchableMethod, runtimeServerOptions, sendSsr, } from './http/runtime.js';
11
9
  import { WasmServerModule } from './runtime/module.js';
12
- import { assembleSsr, buildSsrRoutes, pathnameOf, } from './ssr.js';
13
- const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
10
+ import { StreamRouter } from './stream/router.js';
11
+ import { streamEmulationEnabled, wireStreams } from './stream/wire.js';
12
+ import { buildSsrRoutes, pathnameOf } from './ssr.js';
14
13
  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
14
  function isViteInternal(url) {
34
15
  return VITE_PREFIXES.some((p) => url.startsWith(p));
35
16
  }
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
17
  export async function startDevServer(options) {
100
18
  const host = options.host ?? '127.0.0.1';
101
19
  const root = path.resolve(options.root);
@@ -132,74 +50,45 @@ export async function startDevServer(options) {
132
50
  }
133
51
  };
134
52
  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);
53
+ const app = new Server(runtimeServerOptions(options));
54
+ installRuntimeErrorHandler(app);
148
55
  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?.();
56
+ if (options.streamWasmFile !== undefined && streamEmulationEnabled(nodeMode)) {
57
+ wireStreams(app, options.vite, new StreamRouter(options.streamWasmFile));
58
+ }
59
+ else {
60
+ wireWebsocketProxy(app, options.vite);
164
61
  }
62
+ const daemon = startDaemonRuntime({
63
+ coldWasmFile: options.coldWasmFile,
64
+ nodeMode,
65
+ daemon: options.daemon,
66
+ });
165
67
  app.any('/*', async (request, response) => {
166
68
  response.removeHeader('uWebSockets');
167
- const dispatchable = !isViteInternal(request.url) && METHOD_CODES[request.method] !== undefined;
69
+ const dispatchable = !isViteInternal(request.url) && isDispatchableMethod(request.method);
168
70
  if (dispatchable)
169
71
  refresh();
170
72
  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');
73
+ const dispatch = await dispatchWasmRequest({
74
+ module,
75
+ request,
76
+ response,
77
+ root,
78
+ cacheHost: request.headers.host ?? 'dev',
79
+ serverHeader: 'toil-dev',
80
+ errorPrefix: '✗',
81
+ });
82
+ if (dispatch.handled) {
191
83
  return;
192
84
  }
193
- if ((request.method === 'GET' || request.method === 'HEAD') &&
194
- ssrRoutes.length > 0) {
85
+ if ((request.method === 'GET' || request.method === 'HEAD') && ssrRoutes.length > 0) {
195
86
  const route = ssrRoutes.find((r) => r.test(pathnameOf(request.url)));
196
87
  if (route) {
197
88
  try {
198
- const out = route.entries.length === 0
199
- ? { status: 200, headers: [], html: route.tmpl }
200
- : assembleSsr(route, module.dispatchRender(envelopeReq));
89
+ const out = assembleRouteSsr(route, module, dispatch.envelopeReq);
201
90
  if (out !== null) {
202
- sendSsr(response, out, request.method === 'HEAD');
91
+ sendSsr(response, out, request.method === 'HEAD', 'toil-dev');
203
92
  return;
204
93
  }
205
94
  }
@@ -216,9 +105,7 @@ export async function startDevServer(options) {
216
105
  port: options.port,
217
106
  host,
218
107
  close: async () => {
219
- if (daemonTimer !== null)
220
- clearInterval(daemonTimer);
221
- daemonHost?.close();
108
+ daemon?.close();
222
109
  await app.shutdown();
223
110
  },
224
111
  };
@@ -6,6 +6,7 @@ export interface DevSsrTemplate {
6
6
  id: number;
7
7
  offset: number;
8
8
  }[];
9
+ hash?: Uint8Array;
9
10
  }
10
11
  export interface SsrRoute {
11
12
  test: (pathname: string) => boolean;
@@ -14,6 +15,7 @@ export interface SsrRoute {
14
15
  id: number;
15
16
  offset: number;
16
17
  }[];
18
+ hash?: Uint8Array;
17
19
  }
18
20
  export declare function pathnameOf(url: string): string;
19
21
  export declare function buildSsrRoutes(templates: readonly DevSsrTemplate[]): SsrRoute[];
@@ -33,7 +33,12 @@ function patternToTest(pattern) {
33
33
  return (pathname) => compiled.test(norm(pathname));
34
34
  }
35
35
  export function buildSsrRoutes(templates) {
36
- return templates.map((t) => ({ test: patternToTest(t.pattern), tmpl: t.tmpl, entries: t.entries }));
36
+ return templates.map((t) => ({
37
+ test: patternToTest(t.pattern),
38
+ tmpl: t.tmpl,
39
+ entries: t.entries,
40
+ hash: t.hash,
41
+ }));
37
42
  }
38
43
  function decodeValues(buf) {
39
44
  const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
@@ -44,6 +49,7 @@ function decodeValues(buf) {
44
49
  return null;
45
50
  const status = dv.getUint16(o, true);
46
51
  o += 2;
52
+ const hash = buf.subarray(o, o + 32);
47
53
  o += 32;
48
54
  const nHeaders = dv.getUint16(o, true);
49
55
  o += 2;
@@ -81,12 +87,21 @@ function decodeValues(buf) {
81
87
  values.set(id, buf.subarray(o, o + len));
82
88
  o += len;
83
89
  }
84
- return { status, headers, values };
90
+ return { status, hash, headers, values };
85
91
  }
86
92
  catch {
87
93
  return null;
88
94
  }
89
95
  }
96
+ function sameBytes(a, b) {
97
+ if (a.byteLength !== b.byteLength)
98
+ return false;
99
+ for (let i = 0; i < a.byteLength; i++) {
100
+ if (a[i] !== b[i])
101
+ return false;
102
+ }
103
+ return true;
104
+ }
90
105
  function splice(tmpl, inserts) {
91
106
  const parts = [];
92
107
  let prev = 0;
@@ -107,6 +122,8 @@ export function assembleSsr(route, envelope) {
107
122
  return null;
108
123
  if (decoded.status >= 500 || decoded.values.size === 0)
109
124
  return null;
125
+ if (route.hash !== undefined && !sameBytes(decoded.hash, route.hash))
126
+ return null;
110
127
  const inserts = route.entries
111
128
  .map((e) => ({ offset: e.offset, value: decoded.values.get(e.id) ?? new Uint8Array(0) }))
112
129
  .sort((a, b) => a.offset - b.offset);
@@ -0,0 +1,20 @@
1
+ export interface StreamDef {
2
+ readonly name: string;
3
+ readonly route: string;
4
+ readonly hooks: {
5
+ readonly connect: boolean;
6
+ readonly message: boolean;
7
+ readonly close: boolean;
8
+ readonly disconnect: boolean;
9
+ };
10
+ readonly scope: 'regional' | 'continental';
11
+ readonly messageMode: 'raw' | 'data';
12
+ readonly maxFrameBytes: number;
13
+ readonly ingressRingBytes: number;
14
+ readonly messageValueDataId: number;
15
+ readonly messageSchemaVersion: number;
16
+ readonly streamIndex: number;
17
+ }
18
+ export type StreamCatalog = Map<string, StreamDef>;
19
+ export declare function parseStreamCatalog(wasm: Buffer): StreamCatalog;
20
+ export declare function matchStreamRoute(catalog: StreamCatalog, path: string): StreamDef | null;
@@ -0,0 +1,54 @@
1
+ import { DataReader } from 'toiljs/io';
2
+ import { customSection } from '../wasm/sections.js';
3
+ export function parseStreamCatalog(wasm) {
4
+ const out = new Map();
5
+ let sec;
6
+ try {
7
+ sec = customSection(wasm, 'toilstream.catalog');
8
+ }
9
+ catch {
10
+ return out;
11
+ }
12
+ if (sec === null)
13
+ return out;
14
+ const r = new DataReader(sec);
15
+ r.readU16();
16
+ const n = r.readU16();
17
+ for (let i = 0; i < n && r.ok; i++) {
18
+ const name = r.readString();
19
+ const route = r.readString();
20
+ const bits = r.readU8();
21
+ const scope = r.readU8() === 1 ? 'continental' : 'regional';
22
+ const messageMode = r.readU8() === 1 ? 'data' : 'raw';
23
+ const maxFrameBytes = r.readU32();
24
+ const ingressRingBytes = r.readU32();
25
+ const messageValueDataId = r.readU32();
26
+ const messageSchemaVersion = r.readU32();
27
+ const streamIndex = r.readU16();
28
+ if (!r.ok)
29
+ break;
30
+ out.set(route, {
31
+ name,
32
+ route,
33
+ hooks: {
34
+ connect: (bits & 1) !== 0,
35
+ message: (bits & 2) !== 0,
36
+ close: (bits & 4) !== 0,
37
+ disconnect: (bits & 8) !== 0,
38
+ },
39
+ scope,
40
+ messageMode,
41
+ maxFrameBytes,
42
+ ingressRingBytes,
43
+ messageValueDataId,
44
+ messageSchemaVersion,
45
+ streamIndex,
46
+ });
47
+ }
48
+ return out;
49
+ }
50
+ export function matchStreamRoute(catalog, path) {
51
+ const q = path.indexOf('?');
52
+ const exact = q >= 0 ? path.slice(0, q) : path;
53
+ return catalog.get(exact) ?? null;
54
+ }
@@ -0,0 +1,9 @@
1
+ import { type DbDevState } from '../db/index.js';
2
+ import { type CryptoState } from '../runtime/crypto.js';
3
+ import { type MemoryRef } from '../runtime/host.js';
4
+ export interface StreamBoxState {
5
+ crypto: CryptoState;
6
+ db: DbDevState;
7
+ }
8
+ export declare function freshStreamBoxState(): StreamBoxState;
9
+ export declare function buildStreamImports(ref: MemoryRef, state: StreamBoxState): WebAssembly.Imports;
@@ -0,0 +1,15 @@
1
+ import { buildDatabaseImports, freshDbState } from '../db/index.js';
2
+ import { buildCryptoImports, freshCryptoState } from '../runtime/crypto.js';
3
+ import { buildEnvImports } from '../runtime/host.js';
4
+ export function freshStreamBoxState() {
5
+ return { crypto: freshCryptoState(), db: freshDbState() };
6
+ }
7
+ export function buildStreamImports(ref, state) {
8
+ return {
9
+ env: {
10
+ ...buildEnvImports(ref, state),
11
+ ...buildCryptoImports(ref, state.crypto),
12
+ ...buildDatabaseImports(ref, state.db),
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,37 @@
1
+ export declare function decodeRejectCode(rc: bigint): number;
2
+ export type StreamMessageOutcome = {
3
+ readonly kind: 'reply';
4
+ readonly frames: Buffer[];
5
+ } | {
6
+ readonly kind: 'reject';
7
+ readonly code: number;
8
+ };
9
+ export type StreamConnectOutcome = {
10
+ readonly kind: 'accept';
11
+ } | {
12
+ readonly kind: 'reject';
13
+ readonly code: number;
14
+ };
15
+ export declare class DevStreamBox {
16
+ private readonly exports;
17
+ private readonly _state;
18
+ private readonly rings;
19
+ private readonly streamInfo;
20
+ private constructor();
21
+ static load(wasm: Buffer): DevStreamBox;
22
+ get hasRings(): boolean;
23
+ get hasConnectBridge(): boolean;
24
+ onConnect(streamId: bigint, authority: string, path: string): StreamConnectOutcome;
25
+ onClose(streamId: bigint): bigint;
26
+ onDisconnect(streamId: bigint): bigint;
27
+ onMessage(streamId: bigint, inbound: Buffer): StreamMessageOutcome;
28
+ private dispatch;
29
+ private static resolveRings;
30
+ private static resolveStreamInfo;
31
+ private stampRings;
32
+ private stampOne;
33
+ private writeConnectInfo;
34
+ private resetEgressRing;
35
+ private ingressWrite;
36
+ private egressDrain;
37
+ }
@@ -0,0 +1,220 @@
1
+ import { parseSurface } from '../wasm/surface.js';
2
+ import { buildStreamImports, freshStreamBoxState } from './host.js';
3
+ const EVENT_CONNECT = 1;
4
+ const EVENT_MESSAGE = 2;
5
+ const EVENT_CLOSE = 3;
6
+ const EVENT_DISCONNECT = 4;
7
+ const RING_CTRL_BYTES = 32;
8
+ const RING_FRAME_HEADER = 12;
9
+ const RING_MAGIC = 0x3147_4e52;
10
+ const RING_VERSION = 1;
11
+ const RC_WRITE = 12;
12
+ const RC_READ = 16;
13
+ const FRAME_TYPE_DATA_RELIABLE = 1;
14
+ const MAX_STREAM_FRAME_LEN = 65536;
15
+ const SI_TRANSPORT = 8;
16
+ const SI_AUTH_LEN = 10;
17
+ const SI_PATH_LEN = 12;
18
+ const SI_RESERVED2 = 14;
19
+ const SI_BODY = 16;
20
+ const SI_TRANSPORT_WEBTRANSPORT = 1;
21
+ export function decodeRejectCode(rc) {
22
+ const raw = Number((-rc - 0x10000n) & 0xffffn);
23
+ return raw >= 0x0200 && raw <= 0x02ff ? raw : 0x0208;
24
+ }
25
+ export class DevStreamBox {
26
+ exports;
27
+ _state;
28
+ rings;
29
+ streamInfo;
30
+ constructor(exports, _state, rings, streamInfo) {
31
+ this.exports = exports;
32
+ this._state = _state;
33
+ this.rings = rings;
34
+ this.streamInfo = streamInfo;
35
+ }
36
+ static load(wasm) {
37
+ const surface = parseSurface(wasm);
38
+ if (surface === 'invalid' || surface.targetMode !== 'hot') {
39
+ throw new Error('stream box requires a hot artifact with a valid toil.surface');
40
+ }
41
+ const ref = { memory: null };
42
+ const state = freshStreamBoxState();
43
+ const module = new WebAssembly.Module(new Uint8Array(wasm));
44
+ const instance = new WebAssembly.Instance(module, buildStreamImports(ref, state));
45
+ const exports = instance.exports;
46
+ if (typeof exports.stream_dispatch !== 'function' ||
47
+ !(exports.memory instanceof WebAssembly.Memory)) {
48
+ throw new Error("stream artifact must export 'stream_dispatch' + 'memory'");
49
+ }
50
+ ref.memory = exports.memory;
51
+ const rings = DevStreamBox.resolveRings(exports);
52
+ const streamInfo = DevStreamBox.resolveStreamInfo(exports);
53
+ const box = new DevStreamBox(exports, state, rings, streamInfo);
54
+ if (rings)
55
+ box.stampRings();
56
+ return box;
57
+ }
58
+ get hasRings() {
59
+ return this.rings !== null;
60
+ }
61
+ get hasConnectBridge() {
62
+ return this.streamInfo !== null;
63
+ }
64
+ onConnect(streamId, authority, path) {
65
+ this.writeConnectInfo(streamId, authority, path);
66
+ const rc = this.dispatch(EVENT_CONNECT, streamId);
67
+ if (rc < 0n)
68
+ return { kind: 'reject', code: decodeRejectCode(rc) };
69
+ this.resetEgressRing();
70
+ return { kind: 'accept' };
71
+ }
72
+ onClose(streamId) {
73
+ return this.dispatch(EVENT_CLOSE, streamId);
74
+ }
75
+ onDisconnect(streamId) {
76
+ return this.dispatch(EVENT_DISCONNECT, streamId);
77
+ }
78
+ onMessage(streamId, inbound) {
79
+ if (!this.rings) {
80
+ throw new Error('stream box has no ring runtime; the message bridge is unavailable');
81
+ }
82
+ this.ingressWrite(inbound);
83
+ const ret = this.dispatch(EVENT_MESSAGE, streamId);
84
+ if (ret < 0n)
85
+ return { kind: 'reject', code: decodeRejectCode(ret) };
86
+ return { kind: 'reply', frames: this.egressDrain() };
87
+ }
88
+ dispatch(eventKind, streamId) {
89
+ const lo = Number(streamId & 0xffffffffn) | 0;
90
+ const hi = Number((streamId >> 32n) & 0xffffffffn) | 0;
91
+ return this.exports.stream_dispatch(eventKind, lo, hi);
92
+ }
93
+ static resolveRings(e) {
94
+ if (typeof e.stream_ring_offset !== 'function' ||
95
+ typeof e.stream_ring_capacity !== 'function' ||
96
+ typeof e.stream_egress_offset !== 'function' ||
97
+ typeof e.stream_egress_capacity !== 'function') {
98
+ return null;
99
+ }
100
+ return {
101
+ ingressOff: e.stream_ring_offset() >>> 0,
102
+ ingressCap: e.stream_ring_capacity() >>> 0,
103
+ egressOff: e.stream_egress_offset() >>> 0,
104
+ egressCap: e.stream_egress_capacity() >>> 0,
105
+ };
106
+ }
107
+ static resolveStreamInfo(e) {
108
+ if (typeof e.stream_info_offset !== 'function' || typeof e.stream_info_capacity !== 'function') {
109
+ return null;
110
+ }
111
+ return { offset: e.stream_info_offset() >>> 0, cap: e.stream_info_capacity() >>> 0 };
112
+ }
113
+ stampRings() {
114
+ const rings = this.rings;
115
+ if (!rings)
116
+ return;
117
+ this.stampOne(rings.ingressOff, rings.ingressCap);
118
+ this.stampOne(rings.egressOff, rings.egressCap);
119
+ }
120
+ stampOne(base, cap) {
121
+ const dv = new DataView(this.exports.memory.buffer);
122
+ dv.setUint32(base + 0, RING_MAGIC, true);
123
+ dv.setUint16(base + 4, RING_VERSION, true);
124
+ dv.setUint16(base + 6, 0, true);
125
+ dv.setUint32(base + 8, cap, true);
126
+ dv.setUint32(base + RC_WRITE, 0, true);
127
+ dv.setUint32(base + RC_READ, 0, true);
128
+ }
129
+ writeConnectInfo(streamId, authority, path) {
130
+ const info = this.streamInfo;
131
+ if (!info)
132
+ return;
133
+ const base = info.offset;
134
+ const body = Math.max(0, info.cap - SI_BODY);
135
+ const authBytes = Buffer.from(authority, 'utf8');
136
+ const authLen = Math.min(authBytes.length, 0xffff, body);
137
+ const pathBytes = Buffer.from(path, 'utf8');
138
+ const pathLen = Math.min(pathBytes.length, 0xffff, body - authLen);
139
+ const dv = new DataView(this.exports.memory.buffer);
140
+ dv.setBigUint64(base + 0, streamId, true);
141
+ dv.setUint8(base + SI_TRANSPORT, SI_TRANSPORT_WEBTRANSPORT);
142
+ dv.setUint8(base + SI_TRANSPORT + 1, 0);
143
+ dv.setUint16(base + SI_AUTH_LEN, authLen, true);
144
+ dv.setUint16(base + SI_PATH_LEN, pathLen, true);
145
+ dv.setUint16(base + SI_RESERVED2, 0, true);
146
+ const memU8 = new Uint8Array(this.exports.memory.buffer);
147
+ if (authLen > 0)
148
+ memU8.set(authBytes.subarray(0, authLen), base + SI_BODY);
149
+ if (pathLen > 0)
150
+ memU8.set(pathBytes.subarray(0, pathLen), base + SI_BODY + authLen);
151
+ }
152
+ resetEgressRing() {
153
+ const rings = this.rings;
154
+ if (!rings)
155
+ return;
156
+ const dv = new DataView(this.exports.memory.buffer);
157
+ dv.setUint32(rings.egressOff + RC_WRITE, 0, true);
158
+ dv.setUint32(rings.egressOff + RC_READ, 0, true);
159
+ }
160
+ ingressWrite(inbound) {
161
+ const rings = this.rings;
162
+ if (!rings)
163
+ throw new Error('ingressWrite: no ring runtime');
164
+ const { ingressOff: base, ingressCap: cap } = rings;
165
+ const dv = new DataView(this.exports.memory.buffer);
166
+ const n = inbound.length;
167
+ const frameLen = RING_FRAME_HEADER + n;
168
+ if (frameLen > cap) {
169
+ throw new Error(`inbound frame (${String(frameLen)} B) exceeds ingress capacity`);
170
+ }
171
+ const w0 = dv.getUint32(base + RC_WRITE, true);
172
+ const r0 = dv.getUint32(base + RC_READ, true);
173
+ let w;
174
+ if (r0 === w0) {
175
+ dv.setUint32(base + RC_WRITE, 0, true);
176
+ dv.setUint32(base + RC_READ, 0, true);
177
+ w = 0;
178
+ }
179
+ else {
180
+ w = w0;
181
+ }
182
+ if (w + frameLen > cap)
183
+ throw new Error('ingress frame would not fit (v1 is no-wrap)');
184
+ const f = base + RING_CTRL_BYTES + w;
185
+ dv.setUint8(f + 0, RING_VERSION);
186
+ dv.setUint8(f + 1, FRAME_TYPE_DATA_RELIABLE);
187
+ dv.setUint16(f + 2, 0, true);
188
+ dv.setUint32(f + 4, n, true);
189
+ dv.setUint32(f + 8, 0, true);
190
+ if (n > 0)
191
+ new Uint8Array(this.exports.memory.buffer, f + RING_FRAME_HEADER, n).set(inbound);
192
+ dv.setUint32(base + RC_WRITE, w + frameLen, true);
193
+ }
194
+ egressDrain() {
195
+ const rings = this.rings;
196
+ if (!rings)
197
+ return [];
198
+ const { egressOff: base, egressCap: cap } = rings;
199
+ const dv = new DataView(this.exports.memory.buffer);
200
+ const w = dv.getUint32(base + RC_WRITE, true);
201
+ let r = dv.getUint32(base + RC_READ, true);
202
+ const frames = [];
203
+ while (r < w) {
204
+ const f = base + RING_CTRL_BYTES + r;
205
+ if (r + RING_FRAME_HEADER > cap)
206
+ break;
207
+ const len = dv.getUint32(f + 4, true);
208
+ if (len > MAX_STREAM_FRAME_LEN)
209
+ break;
210
+ const span = RING_FRAME_HEADER + len;
211
+ if (r + span > cap)
212
+ break;
213
+ const payloadOff = f + RING_FRAME_HEADER;
214
+ frames.push(Buffer.from(new Uint8Array(this.exports.memory.buffer, payloadOff, len)));
215
+ r += span;
216
+ }
217
+ dv.setUint32(base + RC_READ, r, true);
218
+ return frames;
219
+ }
220
+ }
@@ -0,0 +1,34 @@
1
+ export declare const STREAM_REJECTED = 520;
2
+ export declare const STREAM_HOOK_TRAPPED = 512;
3
+ export type StreamUpgradeOutcome = {
4
+ readonly kind: 'accepted';
5
+ readonly streamId: bigint;
6
+ } | {
7
+ readonly kind: 'rejected';
8
+ readonly code: number;
9
+ };
10
+ export type StreamDispatchResult = {
11
+ readonly kind: 'reply';
12
+ readonly frames: Buffer[];
13
+ } | {
14
+ readonly kind: 'close';
15
+ readonly code: number;
16
+ } | {
17
+ readonly kind: 'noConnection';
18
+ };
19
+ export declare class StreamDevHost {
20
+ private readonly streamWasmPath;
21
+ private bytes;
22
+ private loadedMtimeMs;
23
+ private readonly conns;
24
+ private nextStreamId;
25
+ constructor(streamWasmPath: string);
26
+ get activeConnections(): number;
27
+ has(connId: string): boolean;
28
+ acceptUpgrade(connId: string, authority: string, path: string): StreamUpgradeOutcome;
29
+ dispatch(connId: string, inbound: Buffer): StreamDispatchResult;
30
+ close(connId: string): void;
31
+ disconnect(connId: string): void;
32
+ private refresh;
33
+ private allocStreamId;
34
+ }