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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DaemonHost } from './index.js';
|
|
2
|
+
import type { ResolvedDaemonConfig } from './host.js';
|
|
3
|
+
export interface DaemonRuntimeOptions {
|
|
4
|
+
readonly coldWasmFile?: string;
|
|
5
|
+
readonly nodeMode?: string;
|
|
6
|
+
readonly daemon?: ResolvedDaemonConfig;
|
|
7
|
+
readonly pollMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RunningDaemonRuntime {
|
|
10
|
+
readonly host: DaemonHost;
|
|
11
|
+
close(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function startDaemonRuntime(options: DaemonRuntimeOptions): RunningDaemonRuntime | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { DaemonHost, daemonEmulationEnabled } from './index.js';
|
|
3
|
+
export function startDaemonRuntime(options) {
|
|
4
|
+
const nodeMode = options.nodeMode ?? 'all';
|
|
5
|
+
if (options.coldWasmFile === undefined ||
|
|
6
|
+
!daemonEmulationEnabled(nodeMode) ||
|
|
7
|
+
options.daemon === undefined) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const host = new DaemonHost(options.coldWasmFile, options.daemon, nodeMode);
|
|
11
|
+
const pollDaemon = () => {
|
|
12
|
+
try {
|
|
13
|
+
host.refresh();
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
process.stdout.write(pc.red(` x daemon reload failed: ${String(e)}`) + '\n');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
pollDaemon();
|
|
20
|
+
const timer = setInterval(pollDaemon, options.pollMs ?? 500);
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
return {
|
|
23
|
+
host,
|
|
24
|
+
close: () => {
|
|
25
|
+
clearInterval(timer);
|
|
26
|
+
host.close();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -55,6 +55,7 @@ export declare class DevDatabase {
|
|
|
55
55
|
viewGet(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
|
|
56
56
|
viewPublish(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number): number;
|
|
57
57
|
counterGet(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
|
|
58
|
+
private recordWrite;
|
|
58
59
|
counterAdd(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, delta: number | bigint, idemPtr: number): number;
|
|
59
60
|
append(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, evPtr: number, evLen: number, idemPtr: number): number;
|
|
60
61
|
appendOnce(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, evidPtr: number, evidLen: number, evPtr: number, evLen: number): number;
|
|
@@ -509,6 +509,7 @@ export class DevDatabase {
|
|
|
509
509
|
if (outcome.kind === 'unit') {
|
|
510
510
|
this.store.set(sk, value);
|
|
511
511
|
this.stampVersion(coll, sk);
|
|
512
|
+
this.recordWrite(db, coll);
|
|
512
513
|
}
|
|
513
514
|
this.recordIdemFinish(coll, key, 'C', idem, requestHash, outcome);
|
|
514
515
|
return this.replayRecordOutcome(db, outcome);
|
|
@@ -533,6 +534,7 @@ export class DevDatabase {
|
|
|
533
534
|
if (outcome.kind === 'value') {
|
|
534
535
|
this.store.set(sk, v);
|
|
535
536
|
this.stampVersion(coll, sk);
|
|
537
|
+
this.recordWrite(db, coll);
|
|
536
538
|
}
|
|
537
539
|
this.recordIdemFinish(coll, key, 'P', idem, requestHash, outcome);
|
|
538
540
|
return this.replayRecordOutcome(db, outcome);
|
|
@@ -721,6 +723,11 @@ export class DevDatabase {
|
|
|
721
723
|
db.lastResult = out;
|
|
722
724
|
return out.length;
|
|
723
725
|
}
|
|
726
|
+
recordWrite(db, coll) {
|
|
727
|
+
if (db.functionKind === DbFunctionKind.Derive)
|
|
728
|
+
return;
|
|
729
|
+
db.writtenCollections.add(coll.name);
|
|
730
|
+
}
|
|
724
731
|
counterAdd(ref, db, handle, keyPtr, keyLen, delta, idemPtr) {
|
|
725
732
|
const coll = collForOp(db, handle, DbOp.CounterAdd, CollectionFamily.Counter);
|
|
726
733
|
if (typeof coll === 'number')
|
|
@@ -737,6 +744,7 @@ export class DevDatabase {
|
|
|
737
744
|
}
|
|
738
745
|
const sk = storeKey(coll.name, key);
|
|
739
746
|
this.counters.set(sk, satI64((this.counters.get(sk) ?? 0n) + d));
|
|
747
|
+
this.recordWrite(db, coll);
|
|
740
748
|
return 0;
|
|
741
749
|
}
|
|
742
750
|
append(ref, db, handle, keyPtr, keyLen, evPtr, evLen, idemPtr) {
|
|
@@ -770,6 +778,7 @@ export class DevDatabase {
|
|
|
770
778
|
log.push(ev);
|
|
771
779
|
(this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)).push(sv);
|
|
772
780
|
}
|
|
781
|
+
this.recordWrite(db, coll);
|
|
773
782
|
return 0;
|
|
774
783
|
}
|
|
775
784
|
appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen) {
|
|
@@ -799,6 +808,7 @@ export class DevDatabase {
|
|
|
799
808
|
(this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)).push(sv);
|
|
800
809
|
}
|
|
801
810
|
seen.add(evid);
|
|
811
|
+
this.recordWrite(db, coll);
|
|
802
812
|
return 1;
|
|
803
813
|
}
|
|
804
814
|
enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface DeriveEntry {
|
|
2
|
+
readonly deriveId: number;
|
|
3
|
+
readonly dbName: string;
|
|
4
|
+
readonly methodName: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseDerives(wasm: Buffer): readonly DeriveEntry[];
|
|
7
|
+
export declare function derivesForWrites(derives: readonly DeriveEntry[], written: ReadonlySet<string>): readonly DeriveEntry[];
|
|
@@ -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;
|