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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { customSection } from '../wasm/sections.js';
|
|
2
|
+
const SECTION = 'toildb.derives';
|
|
3
|
+
const VERSION = 1;
|
|
4
|
+
const MAX_SECTION_BYTES = 128 * 1024;
|
|
5
|
+
const MAX_DERIVES = 1024;
|
|
6
|
+
const MAX_NAME_BYTES = 1024;
|
|
7
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
8
|
+
export function parseDerives(wasm) {
|
|
9
|
+
let section;
|
|
10
|
+
try {
|
|
11
|
+
section = customSection(wasm, SECTION);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
if (section === null)
|
|
17
|
+
return [];
|
|
18
|
+
if (section.length > MAX_SECTION_BYTES)
|
|
19
|
+
return [];
|
|
20
|
+
const r = new Reader(section);
|
|
21
|
+
const version = r.u16();
|
|
22
|
+
if (!r.ok || version !== VERSION)
|
|
23
|
+
return [];
|
|
24
|
+
const count = r.u16();
|
|
25
|
+
if (!r.ok || count > MAX_DERIVES)
|
|
26
|
+
return [];
|
|
27
|
+
const derives = [];
|
|
28
|
+
for (let i = 0; i < count && r.ok; i++) {
|
|
29
|
+
const deriveId = r.u16();
|
|
30
|
+
const dbName = r.string();
|
|
31
|
+
const methodName = r.string();
|
|
32
|
+
if (!r.ok || dbName.length === 0)
|
|
33
|
+
return [];
|
|
34
|
+
derives.push({ deriveId, dbName, methodName });
|
|
35
|
+
}
|
|
36
|
+
if (!r.ok || r.remaining() !== 0)
|
|
37
|
+
return [];
|
|
38
|
+
return derives;
|
|
39
|
+
}
|
|
40
|
+
export function derivesForWrites(derives, written) {
|
|
41
|
+
if (derives.length === 0 || written.size === 0)
|
|
42
|
+
return [];
|
|
43
|
+
const dbs = new Set();
|
|
44
|
+
for (const key of written) {
|
|
45
|
+
const slash = key.indexOf('/');
|
|
46
|
+
dbs.add(slash >= 0 ? key.slice(0, slash) : key);
|
|
47
|
+
}
|
|
48
|
+
return derives.filter((d) => dbs.has(d.dbName));
|
|
49
|
+
}
|
|
50
|
+
class Reader {
|
|
51
|
+
bytes;
|
|
52
|
+
pos = 0;
|
|
53
|
+
ok = true;
|
|
54
|
+
constructor(bytes) {
|
|
55
|
+
this.bytes = bytes;
|
|
56
|
+
}
|
|
57
|
+
remaining() {
|
|
58
|
+
return this.bytes.length - this.pos;
|
|
59
|
+
}
|
|
60
|
+
u16() {
|
|
61
|
+
if (!this.ok || this.pos + 2 > this.bytes.length) {
|
|
62
|
+
this.ok = false;
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
const out = this.bytes.readUInt16LE(this.pos);
|
|
66
|
+
this.pos += 2;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
u32() {
|
|
70
|
+
if (!this.ok || this.pos + 4 > this.bytes.length) {
|
|
71
|
+
this.ok = false;
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const out = this.bytes.readUInt32LE(this.pos);
|
|
75
|
+
this.pos += 4;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
string() {
|
|
79
|
+
const len = this.u32();
|
|
80
|
+
if (!this.ok || len > MAX_NAME_BYTES || this.pos + len > this.bytes.length) {
|
|
81
|
+
this.ok = false;
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const out = UTF8_DECODER.decode(this.bytes.subarray(this.pos, this.pos + len));
|
|
86
|
+
this.pos += len;
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
this.ok = false;
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { __resetDbForTests, __setDbCatalogForTests, buildDatabaseImports, configureDbPersistence, DevDatabase, devDb, persistDb, setDbCatalog, } from './database.js';
|
|
2
2
|
export { parseCatalog } from './catalog.js';
|
|
3
|
+
export { type DeriveEntry, derivesForWrites, parseDerives } from './derives.js';
|
|
3
4
|
export { CollectionFamily, DbFunctionKind, type DbDevState, freshDbState } from './types.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { __resetDbForTests, __setDbCatalogForTests, buildDatabaseImports, configureDbPersistence, DevDatabase, devDb, persistDb, setDbCatalog, } from './database.js';
|
|
2
2
|
export { parseCatalog } from './catalog.js';
|
|
3
|
+
export { derivesForWrites, parseDerives } from './derives.js';
|
|
3
4
|
export { CollectionFamily, DbFunctionKind, freshDbState } from './types.js';
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { type Request, type Response, type Server } from '@dacely/hyper-express';
|
|
1
|
+
import { type Request, type Response, type Server, type Websocket } from '@dacely/hyper-express';
|
|
2
2
|
export interface ViteTarget {
|
|
3
3
|
readonly host: string;
|
|
4
4
|
readonly port: number;
|
|
5
5
|
}
|
|
6
6
|
export declare function proxyToVite(request: Request, response: Response, target: ViteTarget): Promise<void>;
|
|
7
7
|
export declare function wireWebsocketProxy(app: Server, target: ViteTarget): void;
|
|
8
|
+
export declare function pipeToVite(ws: Websocket, target: ViteTarget, ctx: {
|
|
9
|
+
url: string;
|
|
10
|
+
protocol: string;
|
|
11
|
+
}): void;
|
|
@@ -59,41 +59,44 @@ export function wireWebsocketProxy(app, target) {
|
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
61
|
app.ws('/*', { message_type: 'Buffer', idle_timeout: 120, max_payload_length: 16 * 1024 * 1024 }, (ws) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
62
|
+
pipeToVite(ws, target, ws.context);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function pipeToVite(ws, target, ctx) {
|
|
66
|
+
const { url, protocol } = ctx;
|
|
67
|
+
const upstream = new WebSocket(`ws://${target.host}:${String(target.port)}${url}`, protocol ? protocol.split(',').map((p) => p.trim()) : []);
|
|
68
|
+
upstream.binaryType = 'arraybuffer';
|
|
69
|
+
const pending = [];
|
|
70
|
+
let open = false;
|
|
71
|
+
upstream.onopen = () => {
|
|
72
|
+
open = true;
|
|
73
|
+
for (const m of pending)
|
|
74
|
+
upstream.send(m);
|
|
75
|
+
pending.length = 0;
|
|
76
|
+
};
|
|
77
|
+
upstream.onmessage = (event) => {
|
|
78
|
+
if (typeof event.data === 'string')
|
|
79
|
+
ws.send(event.data);
|
|
80
|
+
else
|
|
81
|
+
ws.send(Buffer.from(event.data), true);
|
|
82
|
+
};
|
|
83
|
+
upstream.onclose = (event) => {
|
|
84
|
+
ws.close(event.code, event.reason);
|
|
85
|
+
};
|
|
86
|
+
upstream.onerror = () => {
|
|
87
|
+
ws.close();
|
|
88
|
+
};
|
|
89
|
+
ws.on('message', (message, isBinary) => {
|
|
90
|
+
const m = toUpstreamMessage(message, isBinary);
|
|
91
|
+
if (open)
|
|
92
|
+
upstream.send(m);
|
|
93
|
+
else
|
|
94
|
+
pending.push(m);
|
|
95
|
+
});
|
|
96
|
+
ws.on('close', () => {
|
|
97
|
+
if (upstream.readyState === WebSocket.OPEN ||
|
|
98
|
+
upstream.readyState === WebSocket.CONNECTING) {
|
|
99
|
+
upstream.close();
|
|
100
|
+
}
|
|
98
101
|
});
|
|
99
102
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Request, type Response, type Server } from '@dacely/hyper-express';
|
|
2
|
+
import { type EnvelopeRequest } from './envelope.js';
|
|
3
|
+
import { type CacheableResult } from './cache.js';
|
|
4
|
+
import { WasmServerModule } from '../runtime/module.js';
|
|
5
|
+
import { type SsrResult, type SsrRoute } from '../ssr.js';
|
|
6
|
+
export declare const DEFAULT_MAX_BODY_LENGTH: number;
|
|
7
|
+
export declare const MAX_BODY_BUFFER: number;
|
|
8
|
+
export declare const HTTP_IDLE_TIMEOUT = 60;
|
|
9
|
+
export declare const HTTP_RESPONSE_TIMEOUT = 120;
|
|
10
|
+
export declare const MIME: Readonly<Record<string, string>>;
|
|
11
|
+
export interface RuntimeServerOptions {
|
|
12
|
+
readonly maxBodyLength?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function runtimeServerOptions(options: RuntimeServerOptions): {
|
|
15
|
+
max_body_length: number;
|
|
16
|
+
max_body_buffer: number;
|
|
17
|
+
fast_abort: true;
|
|
18
|
+
idle_timeout: number;
|
|
19
|
+
response_timeout: number;
|
|
20
|
+
};
|
|
21
|
+
export declare function installRuntimeErrorHandler(app: Server): void;
|
|
22
|
+
export declare function isDispatchableMethod(method: string): boolean;
|
|
23
|
+
export declare function resolveFileInside(root: string, file: string): string | null;
|
|
24
|
+
export declare function resolveStaticFile(root: string, requestPath: string): string | null;
|
|
25
|
+
export interface PreparedHttpResponse {
|
|
26
|
+
readonly status: number;
|
|
27
|
+
readonly headers: readonly (readonly [string, string])[];
|
|
28
|
+
readonly body: Uint8Array;
|
|
29
|
+
readonly sendfile: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function toEnvelopeRequest(request: Request): Promise<EnvelopeRequest>;
|
|
32
|
+
export declare function prepareWasmResponse(root: string, result: Pick<CacheableResult, 'status' | 'headers' | 'body' | 'sendfile'>, serverHeader: string): PreparedHttpResponse;
|
|
33
|
+
export declare function sendPreparedResponse(response: Response, out: PreparedHttpResponse): void;
|
|
34
|
+
export declare function sendWasmResponse(response: Response, root: string, result: Pick<CacheableResult, 'status' | 'headers' | 'body' | 'sendfile'>, serverHeader: string): void;
|
|
35
|
+
export declare function prepareSsrResponse(out: SsrResult, headOnly: boolean, serverHeader: string): PreparedHttpResponse;
|
|
36
|
+
export declare function sendSsr(response: Response, out: SsrResult, headOnly: boolean, serverHeader: string): void;
|
|
37
|
+
export interface WasmDispatchOutcome {
|
|
38
|
+
readonly envelopeReq: EnvelopeRequest;
|
|
39
|
+
readonly handled: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface EnvelopeDispatchOutcome {
|
|
42
|
+
readonly result: CacheableResult | null;
|
|
43
|
+
readonly handled: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function dispatchEnvelopeRequest(options: {
|
|
46
|
+
readonly module: WasmServerModule;
|
|
47
|
+
readonly envelopeReq: EnvelopeRequest;
|
|
48
|
+
readonly method: string;
|
|
49
|
+
readonly url: string;
|
|
50
|
+
readonly cacheHost: string;
|
|
51
|
+
readonly hasAuth: boolean;
|
|
52
|
+
}): EnvelopeDispatchOutcome;
|
|
53
|
+
export declare function dispatchWasmRequest(options: {
|
|
54
|
+
readonly module: WasmServerModule;
|
|
55
|
+
readonly request: Request;
|
|
56
|
+
readonly response: Response;
|
|
57
|
+
readonly root: string;
|
|
58
|
+
readonly cacheHost: string;
|
|
59
|
+
readonly serverHeader: string;
|
|
60
|
+
readonly errorPrefix: string;
|
|
61
|
+
}): Promise<WasmDispatchOutcome>;
|
|
62
|
+
export declare function assembleRouteSsr(route: SsrRoute, module: WasmServerModule | null, envelopeReq: EnvelopeRequest): SsrResult | null;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { METHOD_CODES } from './envelope.js';
|
|
5
|
+
import { applyCacheRule, lookupCache } from './cache.js';
|
|
6
|
+
import { assembleSsr } from '../ssr.js';
|
|
7
|
+
export const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
8
|
+
export const MAX_BODY_BUFFER = 1024 * 32;
|
|
9
|
+
export const HTTP_IDLE_TIMEOUT = 60;
|
|
10
|
+
export const HTTP_RESPONSE_TIMEOUT = 120;
|
|
11
|
+
export const MIME = {
|
|
12
|
+
'.html': 'text/html; charset=utf-8',
|
|
13
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
14
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
15
|
+
'.css': 'text/css; charset=utf-8',
|
|
16
|
+
'.json': 'application/json; charset=utf-8',
|
|
17
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
18
|
+
'.svg': 'image/svg+xml',
|
|
19
|
+
'.png': 'image/png',
|
|
20
|
+
'.jpg': 'image/jpeg',
|
|
21
|
+
'.jpeg': 'image/jpeg',
|
|
22
|
+
'.webp': 'image/webp',
|
|
23
|
+
'.avif': 'image/avif',
|
|
24
|
+
'.gif': 'image/gif',
|
|
25
|
+
'.ico': 'image/x-icon',
|
|
26
|
+
'.wasm': 'application/wasm',
|
|
27
|
+
'.woff2': 'font/woff2',
|
|
28
|
+
};
|
|
29
|
+
export function runtimeServerOptions(options) {
|
|
30
|
+
return {
|
|
31
|
+
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
32
|
+
max_body_buffer: MAX_BODY_BUFFER,
|
|
33
|
+
fast_abort: true,
|
|
34
|
+
idle_timeout: HTTP_IDLE_TIMEOUT,
|
|
35
|
+
response_timeout: HTTP_RESPONSE_TIMEOUT,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function installRuntimeErrorHandler(app) {
|
|
39
|
+
app.set_error_handler((_request, response, error) => {
|
|
40
|
+
if (response.completed)
|
|
41
|
+
return;
|
|
42
|
+
response.atomic(() => {
|
|
43
|
+
response.status(500).send(`internal error: ${error.message}\n`);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function isDispatchableMethod(method) {
|
|
48
|
+
return METHOD_CODES[method] !== undefined;
|
|
49
|
+
}
|
|
50
|
+
export function resolveFileInside(root, file) {
|
|
51
|
+
const resolved = path.resolve(root, file);
|
|
52
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep))
|
|
53
|
+
return null;
|
|
54
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile())
|
|
55
|
+
return null;
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
export function resolveStaticFile(root, requestPath) {
|
|
59
|
+
let decoded;
|
|
60
|
+
try {
|
|
61
|
+
decoded = decodeURIComponent(requestPath);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (decoded === '/' || decoded === '')
|
|
67
|
+
return null;
|
|
68
|
+
return resolveFileInside(root, decoded.replace(/^\/+/, ''));
|
|
69
|
+
}
|
|
70
|
+
export async function toEnvelopeRequest(request) {
|
|
71
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
72
|
+
const body = hasBody ? new Uint8Array(await request.buffer()) : new Uint8Array(0);
|
|
73
|
+
const xff = request.headers['x-forwarded-for'];
|
|
74
|
+
const clientIp = typeof xff === 'string' && xff.length > 0 ? xff.split(',')[0].trim() : '127.0.0.1';
|
|
75
|
+
return {
|
|
76
|
+
method: request.method,
|
|
77
|
+
path: request.url,
|
|
78
|
+
headers: Object.entries(request.headers),
|
|
79
|
+
body,
|
|
80
|
+
clientIp,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function prepareWasmResponse(root, result, serverHeader) {
|
|
84
|
+
const headers = [];
|
|
85
|
+
let hasContentType = false;
|
|
86
|
+
for (const [name, value] of result.headers) {
|
|
87
|
+
if (name.toLowerCase() === 'content-type')
|
|
88
|
+
hasContentType = true;
|
|
89
|
+
headers.push([name, value]);
|
|
90
|
+
}
|
|
91
|
+
headers.push(['server', serverHeader]);
|
|
92
|
+
if (result.sendfile !== null) {
|
|
93
|
+
const file = resolveFileInside(root, result.sendfile);
|
|
94
|
+
if (file === null) {
|
|
95
|
+
return {
|
|
96
|
+
status: 404,
|
|
97
|
+
headers: [
|
|
98
|
+
['server', serverHeader],
|
|
99
|
+
['content-type', 'text/plain; charset=utf-8'],
|
|
100
|
+
],
|
|
101
|
+
body: new TextEncoder().encode('not found\n'),
|
|
102
|
+
sendfile: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!hasContentType) {
|
|
106
|
+
headers.push([
|
|
107
|
+
'content-type',
|
|
108
|
+
MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
return { status: result.status, headers, body: new Uint8Array(0), sendfile: file };
|
|
112
|
+
}
|
|
113
|
+
if (!hasContentType)
|
|
114
|
+
headers.push(['content-type', 'text/plain; charset=utf-8']);
|
|
115
|
+
return { status: result.status, headers, body: result.body, sendfile: null };
|
|
116
|
+
}
|
|
117
|
+
export function sendPreparedResponse(response, out) {
|
|
118
|
+
response.status(out.status);
|
|
119
|
+
for (const [name, value] of out.headers)
|
|
120
|
+
response.header(name, value);
|
|
121
|
+
if (out.sendfile !== null) {
|
|
122
|
+
response.sendFile(out.sendfile);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
response.send(Buffer.from(out.body.buffer, out.body.byteOffset, out.body.length));
|
|
126
|
+
}
|
|
127
|
+
export function sendWasmResponse(response, root, result, serverHeader) {
|
|
128
|
+
sendPreparedResponse(response, prepareWasmResponse(root, result, serverHeader));
|
|
129
|
+
}
|
|
130
|
+
export function prepareSsrResponse(out, headOnly, serverHeader) {
|
|
131
|
+
const headers = [];
|
|
132
|
+
let hasContentType = false;
|
|
133
|
+
for (const [name, value] of out.headers) {
|
|
134
|
+
if (name.toLowerCase() === 'content-type')
|
|
135
|
+
hasContentType = true;
|
|
136
|
+
headers.push([name, value]);
|
|
137
|
+
}
|
|
138
|
+
if (!hasContentType)
|
|
139
|
+
headers.push(['content-type', 'text/html; charset=utf-8']);
|
|
140
|
+
headers.push(['server', serverHeader]);
|
|
141
|
+
return {
|
|
142
|
+
status: out.status,
|
|
143
|
+
headers,
|
|
144
|
+
body: headOnly ? new Uint8Array(0) : out.html,
|
|
145
|
+
sendfile: null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function sendSsr(response, out, headOnly, serverHeader) {
|
|
149
|
+
sendPreparedResponse(response, prepareSsrResponse(out, headOnly, serverHeader));
|
|
150
|
+
}
|
|
151
|
+
export function dispatchEnvelopeRequest(options) {
|
|
152
|
+
const cached = lookupCache(options.cacheHost, options.method, options.url, options.envelopeReq.body);
|
|
153
|
+
if (cached !== null)
|
|
154
|
+
return { result: cached, handled: true };
|
|
155
|
+
const result = options.module.dispatch(options.envelopeReq);
|
|
156
|
+
if (result.unhandled)
|
|
157
|
+
return { result: null, handled: false };
|
|
158
|
+
return {
|
|
159
|
+
result: applyCacheRule(options.cacheHost, options.method, options.url, options.envelopeReq.body, options.hasAuth, result),
|
|
160
|
+
handled: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export async function dispatchWasmRequest(options) {
|
|
164
|
+
const envelopeReq = await toEnvelopeRequest(options.request);
|
|
165
|
+
const hasAuth = options.request.headers.cookie !== undefined ||
|
|
166
|
+
options.request.headers.authorization !== undefined;
|
|
167
|
+
try {
|
|
168
|
+
const dispatch = dispatchEnvelopeRequest({
|
|
169
|
+
module: options.module,
|
|
170
|
+
envelopeReq,
|
|
171
|
+
method: options.request.method,
|
|
172
|
+
url: options.request.url,
|
|
173
|
+
cacheHost: options.cacheHost,
|
|
174
|
+
hasAuth,
|
|
175
|
+
});
|
|
176
|
+
if (dispatch.result !== null) {
|
|
177
|
+
sendWasmResponse(options.response, options.root, dispatch.result, options.serverHeader);
|
|
178
|
+
return { envelopeReq, handled: true };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
process.stdout.write(pc.red(` ${options.errorPrefix} ${options.request.method} ${options.request.path} server error: ${String(e)}`) + '\n');
|
|
183
|
+
options.response.status(500).send('internal error\n');
|
|
184
|
+
return { envelopeReq, handled: true };
|
|
185
|
+
}
|
|
186
|
+
return { envelopeReq, handled: false };
|
|
187
|
+
}
|
|
188
|
+
export function assembleRouteSsr(route, module, envelopeReq) {
|
|
189
|
+
if (route.entries.length === 0)
|
|
190
|
+
return { status: 200, headers: [], html: route.tmpl };
|
|
191
|
+
if (module === null || !module.available)
|
|
192
|
+
return null;
|
|
193
|
+
return assembleSsr(route, module.dispatchRender(envelopeReq));
|
|
194
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { startDevServer } from './server.js';
|
|
2
2
|
export type { DevServerOptions, RunningDevServer } from './server.js';
|
|
3
|
+
export { loadBuiltSsrTemplates, startBuiltServer } from './production.js';
|
|
4
|
+
export type { BuiltServerOptions, RunningBuiltServer } from './production.js';
|
|
3
5
|
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './http/envelope.js';
|
|
4
6
|
export type { EnvelopeRequest, EnvelopeResponse } from './http/envelope.js';
|
|
5
7
|
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './runtime/module.js';
|
package/build/devserver/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { startDevServer } from './server.js';
|
|
2
|
+
export { loadBuiltSsrTemplates, startBuiltServer } from './production.js';
|
|
2
3
|
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './http/envelope.js';
|
|
3
4
|
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './runtime/module.js';
|
|
4
5
|
export { buildHostImports, freshDispatchState } from './runtime/host.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface ThreadedRequest {
|
|
2
|
+
readonly id: number;
|
|
3
|
+
readonly method: string;
|
|
4
|
+
readonly url: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly headers: readonly (readonly [string, string])[];
|
|
7
|
+
readonly body: string;
|
|
8
|
+
readonly clientIp: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ThreadedHttpResponse {
|
|
11
|
+
readonly kind: 'response';
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly headers: readonly (readonly [string, string])[];
|
|
14
|
+
readonly body: string;
|
|
15
|
+
readonly sendfile: string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface ThreadedFallbackResponse {
|
|
18
|
+
readonly kind: 'fallback';
|
|
19
|
+
}
|
|
20
|
+
export type ThreadedReply = ThreadedHttpResponse | ThreadedFallbackResponse;
|
|
21
|
+
export type WorkerToPrimaryMessage = {
|
|
22
|
+
readonly toil: 'ready';
|
|
23
|
+
readonly port: number;
|
|
24
|
+
readonly workerId: number;
|
|
25
|
+
} | {
|
|
26
|
+
readonly toil: 'clientCount';
|
|
27
|
+
readonly count: number;
|
|
28
|
+
readonly workerId: number;
|
|
29
|
+
} | {
|
|
30
|
+
readonly toil: 'request';
|
|
31
|
+
readonly request: ThreadedRequest;
|
|
32
|
+
};
|
|
33
|
+
export type PrimaryToWorkerMessage = {
|
|
34
|
+
readonly toil: 'start';
|
|
35
|
+
readonly workerId: number;
|
|
36
|
+
readonly options: unknown;
|
|
37
|
+
} | {
|
|
38
|
+
readonly toil: 'reply';
|
|
39
|
+
readonly id: number;
|
|
40
|
+
readonly reply: ThreadedReply;
|
|
41
|
+
} | {
|
|
42
|
+
readonly toil: 'broadcast';
|
|
43
|
+
readonly message: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly toil: 'shutdown';
|
|
46
|
+
};
|
|
47
|
+
export declare function encodeBody(body: Uint8Array): string;
|
|
48
|
+
export declare function decodeBody(body: string): Uint8Array;
|
|
49
|
+
export declare function isWorkerToPrimaryMessage(value: unknown): value is WorkerToPrimaryMessage;
|
|
50
|
+
export declare function isPrimaryToWorkerMessage(value: unknown): value is PrimaryToWorkerMessage;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function encodeBody(body) {
|
|
2
|
+
return Buffer.from(body.buffer, body.byteOffset, body.length).toString('base64');
|
|
3
|
+
}
|
|
4
|
+
export function decodeBody(body) {
|
|
5
|
+
return Buffer.from(body, 'base64');
|
|
6
|
+
}
|
|
7
|
+
export function isWorkerToPrimaryMessage(value) {
|
|
8
|
+
if (typeof value !== 'object' || value === null)
|
|
9
|
+
return false;
|
|
10
|
+
const message = value;
|
|
11
|
+
return message.toil === 'ready' || message.toil === 'clientCount' || message.toil === 'request';
|
|
12
|
+
}
|
|
13
|
+
export function isPrimaryToWorkerMessage(value) {
|
|
14
|
+
if (typeof value !== 'object' || value === null)
|
|
15
|
+
return false;
|
|
16
|
+
const message = value;
|
|
17
|
+
return (message.toil === 'start' ||
|
|
18
|
+
message.toil === 'reply' ||
|
|
19
|
+
message.toil === 'broadcast' ||
|
|
20
|
+
message.toil === 'shutdown');
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { startBuiltServerWorker } from './production.js';
|
|
2
|
+
import { isPrimaryToWorkerMessage, } from './production-ipc.js';
|
|
3
|
+
let running = null;
|
|
4
|
+
let workerId = 0;
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
function send(message) {
|
|
7
|
+
try {
|
|
8
|
+
process.send?.(message);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function requestPrimary(request) {
|
|
14
|
+
const id = request.id;
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const timeout = setTimeout(() => {
|
|
17
|
+
pending.delete(id);
|
|
18
|
+
reject(new Error('primary request timed out'));
|
|
19
|
+
}, 120_000);
|
|
20
|
+
timeout.unref();
|
|
21
|
+
pending.set(id, (reply) => {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
resolve(reply);
|
|
24
|
+
});
|
|
25
|
+
send({ toil: 'request', request });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function start(message) {
|
|
29
|
+
if (running !== null)
|
|
30
|
+
return;
|
|
31
|
+
workerId = message.workerId;
|
|
32
|
+
const options = message.options;
|
|
33
|
+
running = await startBuiltServerWorker(options, {
|
|
34
|
+
request: requestPrimary,
|
|
35
|
+
clientCount: (count) => send({ toil: 'clientCount', workerId, count }),
|
|
36
|
+
});
|
|
37
|
+
send({ toil: 'ready', workerId, port: running.port });
|
|
38
|
+
}
|
|
39
|
+
async function shutdown() {
|
|
40
|
+
const server = running;
|
|
41
|
+
running = null;
|
|
42
|
+
if (server !== null)
|
|
43
|
+
await server.close();
|
|
44
|
+
}
|
|
45
|
+
process.on('message', (value) => {
|
|
46
|
+
if (!isPrimaryToWorkerMessage(value))
|
|
47
|
+
return;
|
|
48
|
+
switch (value.toil) {
|
|
49
|
+
case 'start':
|
|
50
|
+
void start(value).catch((e) => {
|
|
51
|
+
process.stderr.write(`toiljs production worker failed: ${String(e)}\n`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
case 'reply': {
|
|
56
|
+
const resolve = pending.get(value.id);
|
|
57
|
+
if (resolve === undefined)
|
|
58
|
+
return;
|
|
59
|
+
pending.delete(value.id);
|
|
60
|
+
resolve(value.reply);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
case 'broadcast':
|
|
64
|
+
running?.broadcast(value.message);
|
|
65
|
+
return;
|
|
66
|
+
case 'shutdown':
|
|
67
|
+
void shutdown().finally(() => process.exit(0));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
process.once('disconnect', () => {
|
|
72
|
+
void shutdown().finally(() => process.exit(0));
|
|
73
|
+
});
|