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.
- package/CHANGELOG.md +10 -0
- package/README.md +63 -61
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +13 -1
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +2 -0
- package/build/client/index.js +1 -0
- package/build/client/rpc.js +21 -1
- package/build/client/stream/client.d.ts +11 -0
- package/build/client/stream/client.js +59 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +9 -7
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +22 -6
- package/build/compiler/toil-docs.generated.js +3 -3
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/index.js +4 -3
- package/build/devserver/daemon/runtime.d.ts +13 -0
- package/build/devserver/daemon/runtime.js +29 -0
- package/build/devserver/db/catalog.js +8 -12
- package/build/devserver/db/database.d.ts +1 -0
- package/build/devserver/db/database.js +10 -0
- package/build/devserver/db/derives.d.ts +7 -0
- package/build/devserver/db/derives.js +94 -0
- package/build/devserver/db/index.d.ts +1 -0
- package/build/devserver/db/index.js +1 -0
- package/build/devserver/db/types.d.ts +1 -0
- package/build/devserver/db/types.js +1 -0
- package/build/devserver/http/proxy.d.ts +5 -1
- package/build/devserver/http/proxy.js +39 -36
- package/build/devserver/http/runtime.d.ts +62 -0
- package/build/devserver/http/runtime.js +194 -0
- package/build/devserver/index.d.ts +2 -0
- package/build/devserver/index.js +1 -0
- package/build/devserver/production-ipc.d.ts +50 -0
- package/build/devserver/production-ipc.js +21 -0
- package/build/devserver/production-worker.d.ts +1 -0
- package/build/devserver/production-worker.js +73 -0
- package/build/devserver/production.d.ts +35 -0
- package/build/devserver/production.js +502 -0
- package/build/devserver/runtime/module.d.ts +5 -0
- package/build/devserver/runtime/module.js +47 -1
- package/build/devserver/server.d.ts +1 -0
- package/build/devserver/server.js +32 -145
- package/build/devserver/ssr.d.ts +2 -0
- package/build/devserver/ssr.js +19 -2
- package/build/devserver/stream/catalog.d.ts +20 -0
- package/build/devserver/stream/catalog.js +54 -0
- package/build/devserver/stream/host.d.ts +9 -0
- package/build/devserver/stream/host.js +15 -0
- package/build/devserver/stream/index.d.ts +37 -0
- package/build/devserver/stream/index.js +220 -0
- package/build/devserver/stream/manager.d.ts +34 -0
- package/build/devserver/stream/manager.js +103 -0
- package/build/devserver/stream/router.d.ts +25 -0
- package/build/devserver/stream/router.js +64 -0
- package/build/devserver/stream/wire.d.ts +5 -0
- package/build/devserver/stream/wire.js +33 -0
- package/build/devserver/stream/ws.d.ts +18 -0
- package/build/devserver/stream/ws.js +46 -0
- package/build/devserver/wasm/surface.d.ts +1 -1
- package/build/devserver/wasm/surface.js +1 -1
- package/docs/cli.md +3 -1
- package/docs/getting-started.md +7 -7
- package/docs/tiers.md +15 -9
- package/examples/basic/server/routes/Guestbook.ts +38 -13
- package/package.json +2 -2
- package/src/cli/index.ts +14 -1
- package/src/client/index.ts +2 -0
- package/src/client/rpc.ts +25 -1
- package/src/client/stream/client.ts +107 -0
- package/src/compiler/config.ts +15 -7
- package/src/compiler/index.ts +43 -18
- package/src/compiler/toil-docs.generated.ts +3 -3
- package/src/devserver/daemon/index.ts +7 -7
- package/src/devserver/daemon/runtime.ts +48 -0
- package/src/devserver/db/catalog.ts +9 -13
- package/src/devserver/db/database.ts +14 -0
- package/src/devserver/db/derives.ts +121 -0
- package/src/devserver/db/index.ts +1 -0
- package/src/devserver/db/types.ts +6 -0
- package/src/devserver/http/proxy.ts +53 -39
- package/src/devserver/http/runtime.ts +287 -0
- package/src/devserver/index.ts +2 -0
- package/src/devserver/production-ipc.ts +63 -0
- package/src/devserver/production-worker.ts +83 -0
- package/src/devserver/production.ts +706 -0
- package/src/devserver/runtime/module.ts +95 -1
- package/src/devserver/server.ts +52 -201
- package/src/devserver/ssr.ts +23 -3
- package/src/devserver/stream/catalog.ts +106 -0
- package/src/devserver/stream/host.ts +42 -0
- package/src/devserver/stream/index.ts +308 -0
- package/src/devserver/stream/manager.ts +163 -0
- package/src/devserver/stream/router.ts +101 -0
- package/src/devserver/stream/wire.ts +58 -0
- package/src/devserver/stream/ws.ts +76 -0
- package/src/devserver/wasm/surface.ts +5 -7
- package/test/built-ssr.test.ts +98 -0
- package/test/daemon-build.test.ts +15 -7
- package/test/daemon-catalog.test.ts +17 -8
- package/test/devserver-database.test.ts +8 -8
- package/test/devserver.test.ts +20 -4
- package/test/example-guestbook.test.ts +8 -5
- package/test/fixtures/stream-echo.ts +26 -0
- package/test/fixtures/stream-gate.ts +24 -0
- package/test/fixtures/stream-trap.ts +18 -0
- package/test/rpc-bignum-wire.test.ts +8 -8
- 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 {
|
|
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 {
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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) &&
|
|
69
|
+
const dispatchable = !isViteInternal(request.url) && isDispatchableMethod(request.method);
|
|
168
70
|
if (dispatchable)
|
|
169
71
|
refresh();
|
|
170
72
|
if (dispatchable && module.available) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
clearInterval(daemonTimer);
|
|
221
|
-
daemonHost?.close();
|
|
108
|
+
daemon?.close();
|
|
222
109
|
await app.shutdown();
|
|
223
110
|
},
|
|
224
111
|
};
|
package/build/devserver/ssr.d.ts
CHANGED
|
@@ -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[];
|
package/build/devserver/ssr.js
CHANGED
|
@@ -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) => ({
|
|
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
|
+
}
|