@volter/world-core 2.0.0
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/LICENSE +202 -0
- package/README.md +29 -0
- package/app-route.cjs +154 -0
- package/app-route.d.cts +7 -0
- package/attach.cjs +80 -0
- package/dist/app-route.cjs +154 -0
- package/dist/app-route.d.cts +7 -0
- package/dist/attach.cjs +80 -0
- package/dist/generated/pack-facts.json +4306 -0
- package/dist/inject.cjs +1097 -0
- package/dist/network-policy.cjs +92 -0
- package/dist/network-policy.d.cts +10 -0
- package/dist/src/actions.d.ts +276 -0
- package/dist/src/actions.js +436 -0
- package/dist/src/ancestry.d.ts +22 -0
- package/dist/src/ancestry.js +238 -0
- package/dist/src/args.d.ts +3 -0
- package/dist/src/args.js +12 -0
- package/dist/src/blob-store.d.ts +55 -0
- package/dist/src/blob-store.js +186 -0
- package/dist/src/brand-tokens.d.ts +2 -0
- package/dist/src/brand-tokens.js +17 -0
- package/dist/src/changeset.d.ts +431 -0
- package/dist/src/changeset.js +0 -0
- package/dist/src/client-bundle.d.ts +1 -0
- package/dist/src/client-bundle.js +28 -0
- package/dist/src/credential.d.ts +38 -0
- package/dist/src/credential.js +114 -0
- package/dist/src/derived-core.d.ts +452 -0
- package/dist/src/derived-core.js +782 -0
- package/dist/src/derived.d.ts +84 -0
- package/dist/src/derived.js +122 -0
- package/dist/src/emit.d.ts +106 -0
- package/dist/src/emit.js +157 -0
- package/dist/src/executor.d.ts +120 -0
- package/dist/src/executor.js +387 -0
- package/dist/src/file-response.d.ts +3 -0
- package/dist/src/file-response.js +22 -0
- package/dist/src/fork.d.ts +26 -0
- package/dist/src/fork.js +68 -0
- package/dist/src/git/history.d.ts +36 -0
- package/dist/src/git/history.js +298 -0
- package/dist/src/git/index.d.ts +6 -0
- package/dist/src/git/index.js +6 -0
- package/dist/src/git/inflate.d.ts +11 -0
- package/dist/src/git/inflate.js +194 -0
- package/dist/src/git/objects.d.ts +64 -0
- package/dist/src/git/objects.js +161 -0
- package/dist/src/git/pack.d.ts +14 -0
- package/dist/src/git/pack.js +199 -0
- package/dist/src/git/refs.d.ts +19 -0
- package/dist/src/git/refs.js +35 -0
- package/dist/src/git/smart-http.d.ts +45 -0
- package/dist/src/git/smart-http.js +223 -0
- package/dist/src/hash.d.ts +38 -0
- package/dist/src/hash.js +48 -0
- package/dist/src/head.d.ts +140 -0
- package/dist/src/head.js +313 -0
- package/dist/src/history.d.ts +76 -0
- package/dist/src/history.js +322 -0
- package/dist/src/index.d.ts +73 -0
- package/dist/src/index.js +98 -0
- package/dist/src/lifecycle.d.ts +1 -0
- package/dist/src/lifecycle.js +8 -0
- package/dist/src/log.d.ts +254 -0
- package/dist/src/log.js +801 -0
- package/dist/src/mirror-shell.d.ts +2 -0
- package/dist/src/mirror-shell.js +13 -0
- package/dist/src/observe.d.ts +49 -0
- package/dist/src/observe.js +148 -0
- package/dist/src/pack-assets.d.ts +30 -0
- package/dist/src/pack-assets.js +88 -0
- package/dist/src/packRegistry.d.ts +374 -0
- package/dist/src/packRegistry.js +142 -0
- package/dist/src/placeholder-remote.d.ts +22 -0
- package/dist/src/placeholder-remote.js +86 -0
- package/dist/src/proxy.d.ts +25 -0
- package/dist/src/proxy.js +155 -0
- package/dist/src/rateBudget.d.ts +367 -0
- package/dist/src/rateBudget.js +925 -0
- package/dist/src/references.d.ts +18 -0
- package/dist/src/references.js +27 -0
- package/dist/src/remote-execute.d.ts +22 -0
- package/dist/src/remote-execute.js +1 -0
- package/dist/src/resource-blob.d.ts +10 -0
- package/dist/src/resource-blob.js +56 -0
- package/dist/src/scenario.d.ts +197 -0
- package/dist/src/scenario.js +425 -0
- package/dist/src/schemas.d.ts +78 -0
- package/dist/src/schemas.js +50 -0
- package/dist/src/serve-http.d.ts +48 -0
- package/dist/src/serve-http.js +340 -0
- package/dist/src/serve.d.ts +147 -0
- package/dist/src/serve.js +507 -0
- package/dist/src/shared-blob-index.d.ts +4 -0
- package/dist/src/shared-blob-index.js +126 -0
- package/dist/src/state-system.d.ts +70 -0
- package/dist/src/state-system.js +90 -0
- package/dist/src/storage.d.ts +101 -0
- package/dist/src/storage.js +337 -0
- package/dist/src/twin-fetch.d.ts +64 -0
- package/dist/src/twin-fetch.js +91 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +1 -0
- package/dist/src/v1-removed.d.ts +159 -0
- package/dist/src/v1-removed.js +124 -0
- package/dist/src/volter-home.d.ts +5 -0
- package/dist/src/volter-home.js +10 -0
- package/dist/src/world-clock.d.ts +4 -0
- package/dist/src/world-clock.js +32 -0
- package/dist/src/world-env.d.ts +3 -0
- package/dist/src/world-env.js +22 -0
- package/dist/src/world-store-sql.d.ts +27 -0
- package/dist/src/world-store-sql.js +86 -0
- package/dist/src/world-store.d.ts +168 -0
- package/dist/src/world-store.js +475 -0
- package/dist/src/worldConfig.d.ts +9 -0
- package/dist/src/worldConfig.js +17 -0
- package/dist/stream-bridge.cjs +80 -0
- package/dist/vendor-hosts.cjs +200 -0
- package/generated/pack-facts.json +4306 -0
- package/inject.cjs +1097 -0
- package/network-policy.cjs +92 -0
- package/network-policy.d.cts +10 -0
- package/package.json +103 -0
- package/src/actions.ts +564 -0
- package/src/ancestry.ts +213 -0
- package/src/args.ts +14 -0
- package/src/blob-store.ts +185 -0
- package/src/brand-tokens.ts +17 -0
- package/src/changeset.ts +1032 -0
- package/src/client-bundle.ts +29 -0
- package/src/credential.ts +140 -0
- package/src/derived-core.ts +1004 -0
- package/src/derived.ts +176 -0
- package/src/emit.ts +242 -0
- package/src/executor.ts +431 -0
- package/src/file-response.ts +22 -0
- package/src/fork.ts +89 -0
- package/src/git/history.ts +177 -0
- package/src/git/index.ts +6 -0
- package/src/git/inflate.ts +125 -0
- package/src/git/objects.ts +110 -0
- package/src/git/pack.ts +105 -0
- package/src/git/refs.ts +25 -0
- package/src/git/smart-http.ts +149 -0
- package/src/hash.ts +66 -0
- package/src/head.ts +318 -0
- package/src/history.ts +246 -0
- package/src/index.ts +323 -0
- package/src/lifecycle.ts +8 -0
- package/src/log.ts +793 -0
- package/src/mirror-shell.ts +15 -0
- package/src/observe.ts +130 -0
- package/src/pack-assets.ts +81 -0
- package/src/packRegistry.ts +408 -0
- package/src/placeholder-remote.ts +81 -0
- package/src/proxy.ts +183 -0
- package/src/rateBudget.ts +1115 -0
- package/src/references.ts +46 -0
- package/src/remote-execute.ts +26 -0
- package/src/resource-blob.ts +57 -0
- package/src/scenario.ts +479 -0
- package/src/schemas.ts +56 -0
- package/src/serve-http.ts +299 -0
- package/src/serve.ts +618 -0
- package/src/shared-blob-index.ts +108 -0
- package/src/state-system.ts +115 -0
- package/src/storage.ts +407 -0
- package/src/twin-fetch.ts +147 -0
- package/src/types.ts +50 -0
- package/src/v1-removed.ts +172 -0
- package/src/volter-home.ts +11 -0
- package/src/world-clock.ts +33 -0
- package/src/world-env.ts +18 -0
- package/src/world-store-sql.ts +118 -0
- package/src/world-store.ts +572 -0
- package/src/worldConfig.ts +27 -0
- package/stream-bridge.cjs +80 -0
- package/vendor-hosts.cjs +200 -0
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const WorldActorSchema = z.object({
|
|
4
|
+
id: z.string().min(1).optional(),
|
|
5
|
+
name: z.string().min(1).optional(),
|
|
6
|
+
kind: z.enum(['human', 'agent', 'bot', 'system']),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const WorldSubjectSchema = z.object({
|
|
10
|
+
type: z.string().min(1),
|
|
11
|
+
id: z.string().min(1),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const WorldExternalRefSchema = z.object({
|
|
15
|
+
provider: z.string().min(1),
|
|
16
|
+
id: z.string().min(1).optional(),
|
|
17
|
+
url: z.string().min(1).optional(),
|
|
18
|
+
cursor: z.string().min(1).optional(),
|
|
19
|
+
rawRef: z.string().min(1).optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const WorldServiceEventSchema = z.object({
|
|
23
|
+
id: z.string().min(1),
|
|
24
|
+
service: z.string().min(1),
|
|
25
|
+
type: z.string().min(1),
|
|
26
|
+
schemaVersion: z.number().int().positive(),
|
|
27
|
+
idempotencyKey: z.string().min(1),
|
|
28
|
+
occurredAt: z.string().min(1),
|
|
29
|
+
observedAt: z.string().min(1),
|
|
30
|
+
origin: z.enum(['virtual', 'external', 'agent', 'connector', 'replay', 'migration']),
|
|
31
|
+
actor: WorldActorSchema.optional(),
|
|
32
|
+
subject: WorldSubjectSchema,
|
|
33
|
+
causationId: z.string().min(1).optional(),
|
|
34
|
+
correlationId: z.string().min(1).optional(),
|
|
35
|
+
external: WorldExternalRefSchema.optional(),
|
|
36
|
+
data: z.record(z.string(), z.unknown()),
|
|
37
|
+
raw: z.record(z.string(), z.unknown()).optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// NOTE: WorldAnnotationSchema moved to @volter/tracker/world-annotations — annotating
|
|
41
|
+
// world events as sources/noise is a tracker (verification) concern, not the twin's.
|
|
42
|
+
|
|
43
|
+
export const GenericWorldStateSchema = z.object({
|
|
44
|
+
version: z.literal(1),
|
|
45
|
+
service: z.string().min(1),
|
|
46
|
+
rebuiltAt: z.string().min(1),
|
|
47
|
+
eventCount: z.number().int().nonnegative(),
|
|
48
|
+
latestEventId: z.string().min(1).optional(),
|
|
49
|
+
subjects: z.record(z.string(), z.object({
|
|
50
|
+
type: z.string().min(1),
|
|
51
|
+
id: z.string().min(1),
|
|
52
|
+
latestEventId: z.string().min(1),
|
|
53
|
+
latestType: z.string().min(1),
|
|
54
|
+
updatedAt: z.string().min(1),
|
|
55
|
+
})),
|
|
56
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// The HTTP server seam of the kernel (docs/contributing/architecture.md#the-serve-seam): a fetch handler on
|
|
2
|
+
// a port, in the shape every `Bun.serve` call already has, so the serve path meets the runtime in
|
|
3
|
+
// ONE place. Bun-backed where Bun is the runtime; `node:http`-backed otherwise, the request rebuilt
|
|
4
|
+
// as a standard Request and the handler's Response written back. Async, because binding a port is
|
|
5
|
+
// asynchronous on Node — Bun's synchronous bind is the special case.
|
|
6
|
+
//
|
|
7
|
+
// Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
|
|
8
|
+
// rather than a static import: this module rides into mirror-UI client bundles through the kernel's
|
|
9
|
+
// entrypoint, where a static `node:` import is the 2026-09-06 class of break.
|
|
10
|
+
|
|
11
|
+
export type HttpHandler = (request: Request) => Response | Promise<Response>;
|
|
12
|
+
export type WebSocketPeer = { send: (data: string | Uint8Array) => unknown; close: (code?: number, reason?: string) => unknown };
|
|
13
|
+
export type WebSocketUpgrade = {
|
|
14
|
+
accepts: (request: Request) => boolean;
|
|
15
|
+
open: (peer: WebSocketPeer, request: Request) => void;
|
|
16
|
+
message: (peer: WebSocketPeer, data: string | Uint8Array) => void | Promise<void>;
|
|
17
|
+
close: (peer: WebSocketPeer) => void;
|
|
18
|
+
};
|
|
19
|
+
export type ServeHttpOptions = {
|
|
20
|
+
/** Multiplexing infrastructure fronts leave vendor journaling to the downstream twins. */
|
|
21
|
+
twinRequestJournal?: boolean;
|
|
22
|
+
/** Optional persistent transport, with the same callbacks on Node and Bun. */
|
|
23
|
+
upgrade?: WebSocketUpgrade;
|
|
24
|
+
tls?: { key: string | Uint8Array; cert: string | Uint8Array };
|
|
25
|
+
/** the interface to listen on: loopback unless a caller names another. A twin is reached by its
|
|
26
|
+
* World over loopback; a front that is meant to be reached (a served world, a host) names its own. */
|
|
27
|
+
hostname?: string;
|
|
28
|
+
port?: number;
|
|
29
|
+
fetch: HttpHandler;
|
|
30
|
+
/** seconds a connection may idle (Bun honors it; Node's keep-alive timeout is set from it) */
|
|
31
|
+
idleTimeout?: number;
|
|
32
|
+
/** the largest request body accepted, in bytes (Bun's default is 128 MB; Node sets none) */
|
|
33
|
+
maxRequestBodySize?: number;
|
|
34
|
+
/** what a thrown handler answers with; absent, a 500 carrying the error (as Bun.serve) */
|
|
35
|
+
error?: (error: Error) => Response | Promise<Response>;
|
|
36
|
+
};
|
|
37
|
+
export type HttpServer = {
|
|
38
|
+
hostname: string;
|
|
39
|
+
port: number;
|
|
40
|
+
/** as Bun's: a URL, whose string form ends in `/` — packs build `${server.url}path` on that */
|
|
41
|
+
url: URL;
|
|
42
|
+
/** stop listening; `closeActive` (default true) drops in-flight connections as well */
|
|
43
|
+
stop: (closeActive?: boolean) => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** A decorator the request journal installs: every server made through the seam serves through it. */
|
|
47
|
+
let decorate: ((options: ServeHttpOptions) => HttpHandler) | undefined;
|
|
48
|
+
export function setServeDecorator(fn: ((options: ServeHttpOptions) => HttpHandler) | undefined): void { decorate = fn; }
|
|
49
|
+
|
|
50
|
+
type BunLike = { serve: (o: unknown) => { port?: number; hostname?: string; url?: URL; stop: (closeActive?: boolean) => unknown } };
|
|
51
|
+
const bunRuntime = (): BunLike | undefined => { const b = (globalThis as { Bun?: Partial<BunLike> }).Bun; return b && typeof b.serve === 'function' ? (b as BunLike) : undefined; };
|
|
52
|
+
|
|
53
|
+
/** The path a World asks a service it booted, when that service's port answers, which boot it belongs to. */
|
|
54
|
+
export const WORLD_BOOT_PATH = '/__volter/world-boot';
|
|
55
|
+
|
|
56
|
+
export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer> {
|
|
57
|
+
// Inside a World every server made through the seam answers which boot it belongs to, so the World can tell its own
|
|
58
|
+
// twin from a leftover World's that holds the same port. Outside a World nothing changes.
|
|
59
|
+
const worldBoot = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.VOLTER_WORLD_BOOT_ID;
|
|
60
|
+
if (worldBoot) {
|
|
61
|
+
const served = options.fetch;
|
|
62
|
+
options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Response.json({ boot: worldBoot }) : served(request)) };
|
|
63
|
+
}
|
|
64
|
+
// loopback by default: an unnamed interface is never every interface
|
|
65
|
+
const hostname = options.hostname ?? '127.0.0.1';
|
|
66
|
+
const urlHost = options.hostname ?? '127.0.0.1';
|
|
67
|
+
const bun = bunRuntime();
|
|
68
|
+
if (bun) {
|
|
69
|
+
// the global `Bun.serve` — the request journal's wrap of it (serve.ts) applies to packs and to
|
|
70
|
+
// this seam alike, so the seam never decorates twice on Bun
|
|
71
|
+
const upgrade = options.upgrade;
|
|
72
|
+
const server = bun.serve({ ...options, hostname, port: options.port ?? 0,
|
|
73
|
+
...(upgrade ? {
|
|
74
|
+
fetch: (request: Request, server: { upgrade: (request: Request, options: { data: { request: Request } }) => boolean }) => {
|
|
75
|
+
if (request.headers.get('upgrade')?.toLowerCase() === 'websocket' && upgrade.accepts(request) && server.upgrade(request, { data: { request } })) return undefined;
|
|
76
|
+
return options.fetch(request);
|
|
77
|
+
},
|
|
78
|
+
websocket: {
|
|
79
|
+
open: (peer: WebSocketPeer & { data: { request: Request } }) => upgrade.open(peer, peer.data.request),
|
|
80
|
+
message: (peer: WebSocketPeer, data: string | Uint8Array) => upgrade.message(peer, data),
|
|
81
|
+
close: upgrade.close,
|
|
82
|
+
},
|
|
83
|
+
} : {}),
|
|
84
|
+
});
|
|
85
|
+
const port = server.port ?? 0;
|
|
86
|
+
return { hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } };
|
|
87
|
+
}
|
|
88
|
+
return serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type NodeHttp = typeof import('node:http');
|
|
92
|
+
type NodeStream = typeof import('node:stream');
|
|
93
|
+
/** A Node builtin without a static import — `process.getBuiltinModule` (Node 22.3+, Bun): what a
|
|
94
|
+
* module that also rides into a browser bundle reaches `node:crypto` with. A bare `require()`
|
|
95
|
+
* is Bun's alone; Node's ESM has none (found 2026-09-07 by the pages running under node). */
|
|
96
|
+
export function nodeBuiltin<T = unknown>(name: string): T { return builtin<T>(name); }
|
|
97
|
+
function builtin<T>(name: string): T {
|
|
98
|
+
const get = (process as { getBuiltinModule?: (id: string) => unknown }).getBuiltinModule;
|
|
99
|
+
if (typeof get !== 'function') throw new Error(`serveHttp: this runtime is neither Bun nor a Node with process.getBuiltinModule (Node 22.3+)`);
|
|
100
|
+
return get(name) as T;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function serveOnNode(options: ServeHttpOptions & { hostname: string; urlHost: string }): Promise<HttpServer> {
|
|
104
|
+
const http = builtin<NodeHttp>('node:http');
|
|
105
|
+
const { Readable } = builtin<NodeStream>('node:stream');
|
|
106
|
+
if ('websocket' in options) throw new Error('serveHttp: use the portable upgrade callbacks for WebSocket support on Node');
|
|
107
|
+
const protocol = options.tls ? 'https' : 'http';
|
|
108
|
+
const factory = (handler: import('node:http').RequestListener) => options.tls
|
|
109
|
+
? builtin<typeof import('node:https')>('node:https').createServer({ key: typeof options.tls.key === 'string' ? options.tls.key : Buffer.from(options.tls.key), cert: typeof options.tls.cert === 'string' ? options.tls.cert : Buffer.from(options.tls.cert) }, handler)
|
|
110
|
+
: http.createServer(handler);
|
|
111
|
+
let cleanupReports = 0;
|
|
112
|
+
const reportCleanup = (stage: string, error: unknown) => {
|
|
113
|
+
// A disconnected peer cannot receive an error response. Keep bounded
|
|
114
|
+
// ownership evidence without printing request URLs, headers or bodies.
|
|
115
|
+
cleanupReports++;
|
|
116
|
+
if (cleanupReports > 32) {
|
|
117
|
+
if (cleanupReports === 33)
|
|
118
|
+
try { process.stderr.write('serveHttp further cleanup failures suppressed\n'); } catch { /* best-effort */ }
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let detail = 'unknown failure';
|
|
122
|
+
try {
|
|
123
|
+
detail = error instanceof Error
|
|
124
|
+
? `${error.name.slice(0, 64)}: ${error.message.slice(0, 512)}`
|
|
125
|
+
: String(error).slice(0, 512);
|
|
126
|
+
} catch { /* hostile thrown values are still reported by stage */ }
|
|
127
|
+
try { process.stderr.write(`serveHttp ${stage}: ${detail.slice(0, 512)}\n`); } catch { /* stderr is best-effort */ }
|
|
128
|
+
};
|
|
129
|
+
const active = new Set<AbortController>();
|
|
130
|
+
// Node's Request signal is dependent on the source signal. Neither a pending
|
|
131
|
+
// handler nor a streaming response necessarily keeps the Request alive, so
|
|
132
|
+
// retain it until the whole HTTP response path (including body cleanup) ends.
|
|
133
|
+
const activeRequests = new Set<Request>();
|
|
134
|
+
const server = factory((req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => {
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
active.add(controller);
|
|
137
|
+
const abort = () => { if (!controller.signal.aborted) controller.abort(); };
|
|
138
|
+
const onRequestClose = () => { if (!req.complete) abort(); };
|
|
139
|
+
const onResponseClose = () => { if (!res.writableFinished) abort(); };
|
|
140
|
+
const onSocketClose = () => { if (!res.writableFinished) abort(); };
|
|
141
|
+
req.on('aborted', abort);
|
|
142
|
+
req.on('close', onRequestClose);
|
|
143
|
+
req.on('error', abort);
|
|
144
|
+
req.socket.on('close', onSocketClose);
|
|
145
|
+
req.socket.on('error', abort);
|
|
146
|
+
res.on('close', onResponseClose);
|
|
147
|
+
res.on('error', abort);
|
|
148
|
+
const disconnected = () => controller.signal.aborted || res.destroyed;
|
|
149
|
+
const cancelBody = async (body: ReadableStream<Uint8Array> | null) => {
|
|
150
|
+
if (!body) return;
|
|
151
|
+
try { await body.cancel(controller.signal.reason); }
|
|
152
|
+
catch (error) { reportCleanup('late response body cancellation failed', error); }
|
|
153
|
+
};
|
|
154
|
+
const waitForDrain = () => new Promise<void>((resolve, reject) => {
|
|
155
|
+
if (disconnected()) { reject(new Error('HTTP client disconnected')); return; }
|
|
156
|
+
const retire = () => {
|
|
157
|
+
res.off('drain', drained);
|
|
158
|
+
res.off('close', closed);
|
|
159
|
+
res.off('error', errored);
|
|
160
|
+
controller.signal.removeEventListener('abort', closed);
|
|
161
|
+
};
|
|
162
|
+
const drained = () => { retire(); resolve(); };
|
|
163
|
+
const closed = () => { retire(); reject(new Error('HTTP client disconnected')); };
|
|
164
|
+
const errored = (error: Error) => { retire(); reject(error); };
|
|
165
|
+
res.once('drain', drained);
|
|
166
|
+
res.once('close', closed);
|
|
167
|
+
res.once('error', errored);
|
|
168
|
+
controller.signal.addEventListener('abort', closed, { once: true });
|
|
169
|
+
if (disconnected()) closed();
|
|
170
|
+
});
|
|
171
|
+
let request: Request | undefined;
|
|
172
|
+
const handle = async () => {
|
|
173
|
+
const method = req.method ?? 'GET';
|
|
174
|
+
let response: Response;
|
|
175
|
+
try {
|
|
176
|
+
const headers = new Headers();
|
|
177
|
+
for (const [k, v] of Object.entries(req.headers)) { if (v === undefined) continue; if (Array.isArray(v)) for (const one of v) headers.append(k, one); else headers.set(k, v); }
|
|
178
|
+
const url = `${protocol}://${req.headers.host ?? `${options.hostname}:${(server.address() as { port: number } | null)?.port ?? 0}`}${req.url ?? '/'}`;
|
|
179
|
+
const withBody = method !== 'GET' && method !== 'HEAD';
|
|
180
|
+
const init: RequestInit & { duplex?: 'half' } = { method, headers, signal: controller.signal, ...(withBody ? { body: Readable.toWeb(req) as unknown as ReadableStream, duplex: 'half' } : {}) };
|
|
181
|
+
request = new Request(url, init);
|
|
182
|
+
activeRequests.add(request);
|
|
183
|
+
response = await options.fetch(request);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (disconnected()) return;
|
|
186
|
+
// Preserve the existing Bun-shaped pre-header error hook and default 500.
|
|
187
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
188
|
+
try { response = options.error ? await options.error(err) : new Response(`${err.stack ?? err.message}\n`, { status: 500, headers: { 'content-type': 'text/plain' } }); }
|
|
189
|
+
catch (again) { response = new Response(`${again instanceof Error ? again.stack ?? again.message : String(again)}\n`, { status: 500, headers: { 'content-type': 'text/plain' } }); }
|
|
190
|
+
}
|
|
191
|
+
if (disconnected()) { await cancelBody(response.body); return; }
|
|
192
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
193
|
+
let cancelReader: Promise<void> | undefined;
|
|
194
|
+
const cancelOwnedReader = () => {
|
|
195
|
+
if (!reader) return Promise.resolve();
|
|
196
|
+
if (!cancelReader) {
|
|
197
|
+
cancelReader = Promise.resolve().then(() => reader!.cancel(controller.signal.reason)).catch((error) => {
|
|
198
|
+
reportCleanup('response reader cancellation failed', error);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return cancelReader;
|
|
202
|
+
};
|
|
203
|
+
const onAbort = () => { void cancelOwnedReader(); };
|
|
204
|
+
try {
|
|
205
|
+
if (method === 'HEAD') {
|
|
206
|
+
// A HEAD body is never sent, but its producer is still ours to stop.
|
|
207
|
+
// Failure here is not a successful response completion.
|
|
208
|
+
try { await response.body?.cancel(controller.signal.reason); }
|
|
209
|
+
catch (error) { reportCleanup('HEAD response body cancellation failed', error); throw error; }
|
|
210
|
+
if (!disconnected()) {
|
|
211
|
+
res.statusCode = response.status;
|
|
212
|
+
for (const [k, v] of response.headers) { if (k !== 'set-cookie') res.setHeader(k, v); }
|
|
213
|
+
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
214
|
+
if (cookies.length) res.setHeader('set-cookie', cookies);
|
|
215
|
+
res.end();
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (response.body) reader = response.body.getReader();
|
|
220
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
221
|
+
if (disconnected()) { await cancelOwnedReader(); return; }
|
|
222
|
+
res.statusCode = response.status;
|
|
223
|
+
for (const [k, v] of response.headers) { if (k !== 'set-cookie') res.setHeader(k, v); }
|
|
224
|
+
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
225
|
+
if (cookies.length) res.setHeader('set-cookie', cookies);
|
|
226
|
+
if (reader) {
|
|
227
|
+
while (true) {
|
|
228
|
+
if (disconnected()) throw new Error('HTTP client disconnected');
|
|
229
|
+
const next = await reader.read();
|
|
230
|
+
if (disconnected()) throw new Error('HTTP client disconnected');
|
|
231
|
+
if (next.done) break;
|
|
232
|
+
if (!res.write(next.value)) await waitForDrain();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!disconnected()) res.end();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// Once headers/body may have gone out, a second error response or clean EOF is false.
|
|
238
|
+
abort();
|
|
239
|
+
if (!res.destroyed) res.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
240
|
+
} finally {
|
|
241
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
242
|
+
if (reader) {
|
|
243
|
+
if (controller.signal.aborted) await cancelOwnedReader();
|
|
244
|
+
try { reader.releaseLock(); }
|
|
245
|
+
catch (error) { reportCleanup('response reader release failed', error); }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const responseSettled = new Promise<void>((resolve) => {
|
|
250
|
+
const settled = () => {
|
|
251
|
+
res.off('finish', settled);
|
|
252
|
+
res.off('close', settled);
|
|
253
|
+
resolve();
|
|
254
|
+
};
|
|
255
|
+
res.once('finish', settled);
|
|
256
|
+
res.once('close', settled);
|
|
257
|
+
if (res.writableFinished || res.destroyed) settled();
|
|
258
|
+
});
|
|
259
|
+
void handle().finally(() => {
|
|
260
|
+
if (request) activeRequests.delete(request);
|
|
261
|
+
}).catch((error) => {
|
|
262
|
+
abort();
|
|
263
|
+
if (!res.destroyed) res.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
264
|
+
}).then(async () => {
|
|
265
|
+
await responseSettled;
|
|
266
|
+
active.delete(controller);
|
|
267
|
+
req.off('aborted', abort);
|
|
268
|
+
req.off('close', onRequestClose);
|
|
269
|
+
req.off('error', abort);
|
|
270
|
+
req.socket.off('close', onSocketClose);
|
|
271
|
+
req.socket.off('error', abort);
|
|
272
|
+
res.off('close', onResponseClose);
|
|
273
|
+
res.off('error', abort);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
let socketServer: import('ws').WebSocketServer | undefined;
|
|
277
|
+
if (options.upgrade) {
|
|
278
|
+
const { WebSocketServer } = await import('ws');
|
|
279
|
+
socketServer = new WebSocketServer({ noServer: true });
|
|
280
|
+
const upgrade = options.upgrade;
|
|
281
|
+
server.on('upgrade', (req, socket, head) => {
|
|
282
|
+
const request = new Request(`${protocol}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, { headers: Object.fromEntries(Object.entries(req.headers).filter((entry): entry is [string, string] => typeof entry[1] === 'string')) });
|
|
283
|
+
if (!upgrade.accepts(request)) { socket.end('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n'); return; }
|
|
284
|
+
socketServer!.handleUpgrade(req, socket, head, (peer) => {
|
|
285
|
+
peer.on('message', (data, binary) => { const bytes = Array.isArray(data) ? Buffer.concat(data) : data instanceof ArrayBuffer ? new Uint8Array(data) : data; Promise.resolve(upgrade.message(peer, binary ? new Uint8Array(bytes) : Buffer.from(bytes).toString())).catch(() => peer.close(1011, 'handler failed')); });
|
|
286
|
+
peer.on('close', () => upgrade.close(peer));
|
|
287
|
+
peer.on('error', () => peer.close());
|
|
288
|
+
upgrade.open(peer, request);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (options.idleTimeout !== undefined) server.keepAliveTimeout = options.idleTimeout * 1000;
|
|
293
|
+
await new Promise<void>((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, options.hostname, () => { server.off('error', reject); resolve(); }); });
|
|
294
|
+
const port = (server.address() as { port: number }).port;
|
|
295
|
+
return {
|
|
296
|
+
hostname: options.hostname, port, url: new URL(`${protocol}://${options.urlHost}:${port}/`),
|
|
297
|
+
stop: (closeActive = true) => new Promise<void>((resolve) => { if (closeActive) { for (const controller of active) controller.abort(); for (const peer of socketServer?.clients ?? []) peer.terminate(); } socketServer?.close(); server.close(() => { if (closeActive && active.size) reportCleanup('forced stop left pending handler cleanup', `${active.size} request(s)`); resolve(); }); if (closeActive) server.closeAllConnections(); }),
|
|
298
|
+
};
|
|
299
|
+
}
|