toiljs 0.0.67 → 0.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -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 +16 -2
- package/build/compiler/toil-docs.generated.js +2 -2
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/runtime.d.ts +13 -0
- package/build/devserver/daemon/runtime.js +29 -0
- 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/docs/cli.md +3 -1
- package/docs/getting-started.md +7 -7
- 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 +24 -5
- package/src/compiler/toil-docs.generated.ts +2 -2
- package/src/devserver/daemon/runtime.ts +48 -0
- 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/test/built-ssr.test.ts +98 -0
- 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/stream-emulation.test.ts +394 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { DbFunctionKind, persistDb, setDbCatalog } from '../db/index.js';
|
|
2
|
+
import { DbFunctionKind, derivesForWrites, parseDerives, persistDb, setDbCatalog, } from '../db/index.js';
|
|
3
3
|
import { parseRouteKinds, routeKindForRequest } from '../db/routeKinds.js';
|
|
4
4
|
import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from '../http/envelope.js';
|
|
5
5
|
import { buildHostImports, freshDispatchState } from './host.js';
|
|
@@ -87,6 +87,8 @@ export class WasmServerModule {
|
|
|
87
87
|
module = null;
|
|
88
88
|
loadedMtimeMs = -1;
|
|
89
89
|
routeKinds = [];
|
|
90
|
+
derives = [];
|
|
91
|
+
derivesDirty = false;
|
|
90
92
|
constructor(wasmPath) {
|
|
91
93
|
this.wasmPath = wasmPath;
|
|
92
94
|
}
|
|
@@ -101,6 +103,8 @@ export class WasmServerModule {
|
|
|
101
103
|
catch {
|
|
102
104
|
this.module = null;
|
|
103
105
|
this.routeKinds = [];
|
|
106
|
+
this.derives = [];
|
|
107
|
+
this.derivesDirty = false;
|
|
104
108
|
this.loadedMtimeMs = -1;
|
|
105
109
|
return false;
|
|
106
110
|
}
|
|
@@ -112,13 +116,16 @@ export class WasmServerModule {
|
|
|
112
116
|
this.assertExportSurface(module);
|
|
113
117
|
setDbCatalog(bytes);
|
|
114
118
|
this.routeKinds = parseRouteKinds(bytes);
|
|
119
|
+
this.derives = parseDerives(bytes);
|
|
115
120
|
this.module = module;
|
|
116
121
|
this.loadedMtimeMs = mtimeMs;
|
|
122
|
+
this.derivesDirty = this.derives.length > 0;
|
|
117
123
|
return true;
|
|
118
124
|
}
|
|
119
125
|
dispatch(req) {
|
|
120
126
|
if (this.module === null)
|
|
121
127
|
throw new Error(`server wasm not loaded (${this.wasmPath})`);
|
|
128
|
+
this.rebuildDerivedViewsIfStale();
|
|
122
129
|
const envelope = encodeRequestEnvelope(req);
|
|
123
130
|
const ref = { memory: null };
|
|
124
131
|
const state = freshDispatchState();
|
|
@@ -139,6 +146,7 @@ export class WasmServerModule {
|
|
|
139
146
|
const headers = [...resp.headers, ...state.headers];
|
|
140
147
|
const status = state.status ?? resp.status;
|
|
141
148
|
const unhandled = headers.some(([n]) => n.toLowerCase() === UNHANDLED_HEADER);
|
|
149
|
+
this.runAffectedDerives(state.db.writtenCollections);
|
|
142
150
|
persistDb();
|
|
143
151
|
return {
|
|
144
152
|
status,
|
|
@@ -171,6 +179,44 @@ export class WasmServerModule {
|
|
|
171
179
|
throw new Error(`guest returned out-of-bounds values: ptr=${String(ptr)} len=${String(len)}`);
|
|
172
180
|
return new Uint8Array(exports.memory.buffer, ptr, len).slice();
|
|
173
181
|
}
|
|
182
|
+
runAffectedDerives(written) {
|
|
183
|
+
if (this.module === null)
|
|
184
|
+
return;
|
|
185
|
+
for (const derive of derivesForWrites(this.derives, written)) {
|
|
186
|
+
try {
|
|
187
|
+
this.runDerive(derive.deriveId);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
console.error(`[toil] derive ${derive.dbName}#${derive.methodName} failed:`, err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
rebuildDerivedViewsIfStale() {
|
|
195
|
+
if (!this.derivesDirty)
|
|
196
|
+
return;
|
|
197
|
+
this.derivesDirty = false;
|
|
198
|
+
for (const derive of this.derives) {
|
|
199
|
+
try {
|
|
200
|
+
this.runDerive(derive.deriveId);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error(`[toil] derive ${derive.dbName}#${derive.methodName} failed on load:`, err);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
runDerive(deriveId) {
|
|
208
|
+
if (this.module === null)
|
|
209
|
+
return;
|
|
210
|
+
const ref = { memory: null };
|
|
211
|
+
const state = freshDispatchState();
|
|
212
|
+
state.db.functionKind = DbFunctionKind.Derive;
|
|
213
|
+
const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
|
|
214
|
+
const exports = instance.exports;
|
|
215
|
+
ref.memory = exports.memory;
|
|
216
|
+
if (typeof exports.derive_run !== 'function')
|
|
217
|
+
return;
|
|
218
|
+
exports.derive_run(deriveId);
|
|
219
|
+
}
|
|
174
220
|
assertImportSurface(module) {
|
|
175
221
|
const missing = WebAssembly.Module.imports(module)
|
|
176
222
|
.filter((i) => i.kind === 'function' && (i.module !== 'env' || !PROVIDED_IMPORTS.has(i.name)))
|
|
@@ -8,6 +8,7 @@ export interface DevServerOptions {
|
|
|
8
8
|
readonly host?: string;
|
|
9
9
|
readonly wasmFile: string;
|
|
10
10
|
readonly coldWasmFile?: string;
|
|
11
|
+
readonly streamWasmFile?: string;
|
|
11
12
|
readonly nodeMode?: string;
|
|
12
13
|
readonly daemon?: ResolvedDaemonConfig;
|
|
13
14
|
readonly vite: ViteTarget;
|
|
@@ -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
|
+
}
|