stitchkit 0.66.1 → 0.68.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/dist/agent-runtime.js +5 -6
- package/dist/application/admission.d.ts +131 -0
- package/dist/application/admission.d.ts.map +1 -0
- package/dist/application/channel.d.ts +106 -0
- package/dist/application/channel.d.ts.map +1 -0
- package/dist/application/graph.d.ts +1 -1
- package/dist/application/graph.d.ts.map +1 -1
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/latest-sink.d.ts.map +1 -1
- package/dist/application/resource.d.ts +72 -1
- package/dist/application/resource.d.ts.map +1 -1
- package/dist/application/server-resource.d.ts +26 -5
- package/dist/application/server-resource.d.ts.map +1 -1
- package/dist/application-grammy.js +2 -2
- package/dist/application-opentelemetry.js +1 -1
- package/dist/application.d.ts +4 -2
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +408 -24
- package/dist/browser/client.d.ts +2 -3
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/contract-stream.d.ts +5 -0
- package/dist/browser/contract-stream.d.ts.map +1 -0
- package/dist/browser/http.d.ts +3 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/stream.d.ts +4 -1
- package/dist/browser/stream.d.ts.map +1 -1
- package/dist/browser/transport.d.ts +3 -0
- package/dist/browser/transport.d.ts.map +1 -0
- package/dist/cli.js +6 -7
- package/dist/contract/define.d.ts +34 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors.d.ts +4 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.d.ts +2 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +5 -1
- package/dist/contract/stream.d.ts +19 -0
- package/dist/contract/stream.d.ts.map +1 -0
- package/dist/{index-xxye8j3k.js → index-0w9abg87.js} +4 -0
- package/dist/index-2k4yrqkc.js +9 -0
- package/dist/index-69m2278y.js +115 -0
- package/dist/{index-eabpd4tb.js → index-6wzr93cg.js} +21 -2
- package/dist/{index-1pgeyyee.js → index-7pn02bgv.js} +4 -6
- package/dist/index-9ky9hhg3.js +484 -0
- package/dist/{index-wrhf06ak.js → index-avjd67h6.js} +8 -8
- package/dist/{index-413xk7ga.js → index-cq9q73nf.js} +50 -1
- package/dist/{index-6taryy00.js → index-dafax5md.js} +1 -1
- package/dist/{index-s1tywej8.js → index-nymn7wbg.js} +1103 -20
- package/dist/{index-y91zd0ch.js → index-pr0qsmjy.js} +3 -2
- package/dist/index-v5bayx1z.js +138 -0
- package/dist/{index-mfw1pec7.js → index-wqyrvhzz.js} +3 -3
- package/dist/{index-svqhcrrj.js → index-zd4v31v2.js} +156 -10
- package/dist/{index-x8v8b7rc.js → index-zpyj7hsv.js} +330 -58
- package/dist/index.js +270 -105
- package/dist/internal/bounded-lines.d.ts +4 -0
- package/dist/internal/bounded-lines.d.ts.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +11 -8
- package/dist/observability/index.js +6 -7
- package/dist/remote.js +5 -6
- package/dist/server/contract-stream.d.ts +5 -0
- package/dist/server/contract-stream.d.ts.map +1 -0
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +41 -283
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/process-signals.d.ts.map +1 -1
- package/dist/server/stream.d.ts +3 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/streaming-route.d.ts +2 -0
- package/dist/server/streaming-route.d.ts.map +1 -1
- package/dist/server/types.d.ts +13 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/unix-client-body.d.ts +4 -0
- package/dist/server/unix-client-body.d.ts.map +1 -0
- package/dist/server/unix-client-bun.d.ts +13 -0
- package/dist/server/unix-client-bun.d.ts.map +1 -0
- package/dist/server/unix-client-error.d.ts +9 -0
- package/dist/server/unix-client-error.d.ts.map +1 -0
- package/dist/server/unix-client.d.ts +25 -0
- package/dist/server/unix-client.d.ts.map +1 -0
- package/dist/testing.js +7 -5
- package/dist/tools.js +14 -16
- package/llms-full.txt +594 -29
- package/package.json +1 -1
- package/dist/index-48ffdxgk.js +0 -6
- package/dist/index-pzyt11ch.js +0 -127
- package/dist/index-tvwcrx2d.js +0 -224
- package/dist/index-wd8g8z6e.js +0 -206
- package/dist/index-yr276yz0.js +0 -6
|
@@ -7,35 +7,34 @@ import {
|
|
|
7
7
|
defaultSignalSource,
|
|
8
8
|
guardSignalCallback,
|
|
9
9
|
reportSignalError
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-pr0qsmjy.js";
|
|
11
11
|
import {
|
|
12
12
|
ShutdownOptionsSchema
|
|
13
13
|
} from "./index-dk6e56g0.js";
|
|
14
14
|
import {
|
|
15
|
-
errorCode,
|
|
16
15
|
extractIp,
|
|
17
16
|
getClientInfo,
|
|
18
17
|
getRequestContext,
|
|
19
|
-
normalizeError,
|
|
20
18
|
parseQueryParams,
|
|
21
|
-
recordedErrorMessage,
|
|
22
19
|
resolveSocketIp,
|
|
23
20
|
resolveTraceId,
|
|
24
21
|
runWithRequestContext,
|
|
25
22
|
setRequestEndpoint,
|
|
26
|
-
setRequestError
|
|
27
|
-
|
|
28
|
-
zodIssues
|
|
29
|
-
} from "./index-tvwcrx2d.js";
|
|
23
|
+
setRequestError
|
|
24
|
+
} from "./index-69m2278y.js";
|
|
30
25
|
import {
|
|
26
|
+
DEFAULT_CONTRACT_STREAM_FRAME_BYTES,
|
|
27
|
+
errorCode,
|
|
31
28
|
isUnsafeKey,
|
|
32
|
-
safeJsonParse
|
|
33
|
-
} from "./index-wd8g8z6e.js";
|
|
34
|
-
import {
|
|
35
29
|
joinRoutePath,
|
|
30
|
+
normalizeError,
|
|
36
31
|
parseTrailingWildcard,
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
recordedErrorMessage,
|
|
33
|
+
resolveTraceContext,
|
|
34
|
+
safeJsonParse,
|
|
35
|
+
validateDeclaredOutput,
|
|
36
|
+
zodIssues
|
|
37
|
+
} from "./index-9ky9hhg3.js";
|
|
39
38
|
import {
|
|
40
39
|
isRecord,
|
|
41
40
|
transportResult
|
|
@@ -43,7 +42,7 @@ import {
|
|
|
43
42
|
import {
|
|
44
43
|
AppError,
|
|
45
44
|
badRequest
|
|
46
|
-
} from "./index-
|
|
45
|
+
} from "./index-0w9abg87.js";
|
|
47
46
|
|
|
48
47
|
// src/server/multipart.ts
|
|
49
48
|
var DEFAULT_MAX_REQUEST_BYTES = 25 * 1024 * 1024;
|
|
@@ -470,6 +469,195 @@ async function parseMultipart(req, descriptor, fieldsSchema, receivers) {
|
|
|
470
469
|
}
|
|
471
470
|
}
|
|
472
471
|
|
|
472
|
+
// src/server/streaming-route.ts
|
|
473
|
+
var DEFAULT_STREAM_HEARTBEAT_MS = 5000;
|
|
474
|
+
var FRAMINGS = {
|
|
475
|
+
ndjson: {
|
|
476
|
+
contentType: "application/x-ndjson",
|
|
477
|
+
keepAlive: `
|
|
478
|
+
`,
|
|
479
|
+
frame: (value) => `${JSON.stringify(value)}
|
|
480
|
+
`
|
|
481
|
+
},
|
|
482
|
+
sse: {
|
|
483
|
+
contentType: "text/event-stream",
|
|
484
|
+
keepAlive: `: keep-alive
|
|
485
|
+
|
|
486
|
+
`,
|
|
487
|
+
frame: (value) => `data: ${JSON.stringify(value)}
|
|
488
|
+
|
|
489
|
+
`,
|
|
490
|
+
done: `data: [DONE]
|
|
491
|
+
|
|
492
|
+
`
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
function applyIdleTimeout(server, request, seconds) {
|
|
496
|
+
if (typeof server !== "object" || server === null)
|
|
497
|
+
return;
|
|
498
|
+
const timeout = Reflect.get(server, "timeout");
|
|
499
|
+
if (typeof timeout !== "function")
|
|
500
|
+
return;
|
|
501
|
+
try {
|
|
502
|
+
Reflect.apply(timeout, server, [request, seconds]);
|
|
503
|
+
} catch {}
|
|
504
|
+
}
|
|
505
|
+
function unrefTimer(timer) {
|
|
506
|
+
if (typeof timer !== "object" || timer === null)
|
|
507
|
+
return;
|
|
508
|
+
const unref = Reflect.get(timer, "unref");
|
|
509
|
+
if (typeof unref === "function")
|
|
510
|
+
Reflect.apply(unref, timer, []);
|
|
511
|
+
}
|
|
512
|
+
var MAX_BUFFERED_FRAMES = 16;
|
|
513
|
+
function responseHeaders(framing, extra) {
|
|
514
|
+
const headers = new Headers(extra);
|
|
515
|
+
headers.set("Content-Type", framing.contentType);
|
|
516
|
+
headers.set("Cache-Control", "no-cache");
|
|
517
|
+
headers.set("Connection", "keep-alive");
|
|
518
|
+
headers.set("X-Accel-Buffering", "no");
|
|
519
|
+
return headers;
|
|
520
|
+
}
|
|
521
|
+
function streamingRoute(options) {
|
|
522
|
+
const heartbeatMs = options.heartbeatMs ?? DEFAULT_STREAM_HEARTBEAT_MS;
|
|
523
|
+
if (!Number.isFinite(heartbeatMs) || heartbeatMs <= 0) {
|
|
524
|
+
throw new TypeError("heartbeatMs must be a finite positive number of milliseconds");
|
|
525
|
+
}
|
|
526
|
+
const idleTimeoutSeconds = options.idleTimeoutSeconds ?? 0;
|
|
527
|
+
if (!Number.isInteger(idleTimeoutSeconds) || idleTimeoutSeconds < 0) {
|
|
528
|
+
throw new TypeError("idleTimeoutSeconds must be a non-negative integer");
|
|
529
|
+
}
|
|
530
|
+
const framing = FRAMINGS[options.format ?? "ndjson"];
|
|
531
|
+
return {
|
|
532
|
+
method: options.method ?? "GET",
|
|
533
|
+
path: options.path,
|
|
534
|
+
handler: async (request, context) => {
|
|
535
|
+
applyIdleTimeout(context.server, request, idleTimeoutSeconds);
|
|
536
|
+
const departed = new AbortController;
|
|
537
|
+
const iterable = await options.source(request, { ...context, signal: departed.signal });
|
|
538
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
539
|
+
const encoder2 = new TextEncoder;
|
|
540
|
+
let heartbeat = null;
|
|
541
|
+
let closed = false;
|
|
542
|
+
const stopHeartbeat = () => {
|
|
543
|
+
if (heartbeat === null)
|
|
544
|
+
return;
|
|
545
|
+
clearInterval(heartbeat);
|
|
546
|
+
heartbeat = null;
|
|
547
|
+
};
|
|
548
|
+
const release = () => {
|
|
549
|
+
if (closed)
|
|
550
|
+
return;
|
|
551
|
+
closed = true;
|
|
552
|
+
try {
|
|
553
|
+
options.onClose?.();
|
|
554
|
+
stopHeartbeat();
|
|
555
|
+
signalDemand();
|
|
556
|
+
departed.abort();
|
|
557
|
+
Promise.resolve(iterator.return?.(undefined)).catch(() => {
|
|
558
|
+
return;
|
|
559
|
+
});
|
|
560
|
+
} catch {}
|
|
561
|
+
};
|
|
562
|
+
let demand = null;
|
|
563
|
+
const signalDemand = () => {
|
|
564
|
+
const resolve = demand;
|
|
565
|
+
demand = null;
|
|
566
|
+
resolve?.();
|
|
567
|
+
};
|
|
568
|
+
const stream = new ReadableStream({
|
|
569
|
+
start(controller) {
|
|
570
|
+
const send = (text) => {
|
|
571
|
+
if (closed)
|
|
572
|
+
return false;
|
|
573
|
+
try {
|
|
574
|
+
controller.enqueue(encoder2.encode(text));
|
|
575
|
+
return true;
|
|
576
|
+
} catch {
|
|
577
|
+
release();
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const finish = () => {
|
|
582
|
+
try {
|
|
583
|
+
controller.close();
|
|
584
|
+
} catch {}
|
|
585
|
+
};
|
|
586
|
+
if (request.signal.aborted) {
|
|
587
|
+
release();
|
|
588
|
+
finish();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
request.signal.addEventListener("abort", () => {
|
|
592
|
+
release();
|
|
593
|
+
finish();
|
|
594
|
+
});
|
|
595
|
+
send(framing.keepAlive);
|
|
596
|
+
heartbeat = setInterval(() => {
|
|
597
|
+
if ((controller.desiredSize ?? 1) > 0)
|
|
598
|
+
send(framing.keepAlive);
|
|
599
|
+
}, heartbeatMs);
|
|
600
|
+
unrefTimer(heartbeat);
|
|
601
|
+
const awaitDemand = async () => {
|
|
602
|
+
while (!closed && (controller.desiredSize ?? 1) <= 0) {
|
|
603
|
+
await new Promise((resolve) => {
|
|
604
|
+
demand = resolve;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
let sinceYield = 0;
|
|
609
|
+
(async () => {
|
|
610
|
+
try {
|
|
611
|
+
for (;; ) {
|
|
612
|
+
await awaitDemand();
|
|
613
|
+
if (closed)
|
|
614
|
+
return;
|
|
615
|
+
const next = await iterator.next();
|
|
616
|
+
if (closed)
|
|
617
|
+
return;
|
|
618
|
+
if (next.done)
|
|
619
|
+
break;
|
|
620
|
+
if (!send(framing.frame(next.value)))
|
|
621
|
+
return;
|
|
622
|
+
sinceYield += 1;
|
|
623
|
+
if (sinceYield >= MAX_BUFFERED_FRAMES) {
|
|
624
|
+
sinceYield = 0;
|
|
625
|
+
await new Promise((resolve) => {
|
|
626
|
+
setTimeout(resolve, 0);
|
|
627
|
+
});
|
|
628
|
+
if (closed)
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (framing.done)
|
|
633
|
+
send(framing.done);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (!closed)
|
|
636
|
+
send(framing.frame(normalizeError(error).toJSON()));
|
|
637
|
+
} finally {
|
|
638
|
+
release();
|
|
639
|
+
finish();
|
|
640
|
+
}
|
|
641
|
+
})();
|
|
642
|
+
},
|
|
643
|
+
pull() {
|
|
644
|
+
signalDemand();
|
|
645
|
+
},
|
|
646
|
+
cancel() {
|
|
647
|
+
release();
|
|
648
|
+
}
|
|
649
|
+
}, { highWaterMark: MAX_BUFFERED_FRAMES });
|
|
650
|
+
return new Response(stream, { headers: responseHeaders(framing, options.headers) });
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function ndjsonRoute(options) {
|
|
655
|
+
return streamingRoute({ ...options, format: "ndjson" });
|
|
656
|
+
}
|
|
657
|
+
function sseRoute(options) {
|
|
658
|
+
return streamingRoute({ ...options, format: "sse" });
|
|
659
|
+
}
|
|
660
|
+
|
|
473
661
|
// src/server/request-body.ts
|
|
474
662
|
function requestAbortReason(req) {
|
|
475
663
|
return req.signal.reason ?? new DOMException("The connection was closed", "AbortError");
|
|
@@ -615,6 +803,97 @@ function buildErrorContext(req, url, traceId, clientIp) {
|
|
|
615
803
|
};
|
|
616
804
|
}
|
|
617
805
|
|
|
806
|
+
// src/server/contract-stream.ts
|
|
807
|
+
function encodedBytes(value, format) {
|
|
808
|
+
const json = JSON.stringify(value);
|
|
809
|
+
if (json === undefined)
|
|
810
|
+
throw new TypeError("Streaming item is not JSON serializable");
|
|
811
|
+
const framing = format === "sse" ? `data: ${json}
|
|
812
|
+
|
|
813
|
+
` : `${json}
|
|
814
|
+
`;
|
|
815
|
+
return new TextEncoder().encode(framing).byteLength;
|
|
816
|
+
}
|
|
817
|
+
function waitForAbort(signal) {
|
|
818
|
+
return new Promise((_resolve, reject) => {
|
|
819
|
+
const rejectAborted = () => reject(signal.reason ?? new Error("Stream aborted"));
|
|
820
|
+
if (signal.aborted)
|
|
821
|
+
rejectAborted();
|
|
822
|
+
else
|
|
823
|
+
signal.addEventListener("abort", rejectAborted, { once: true });
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function contractStreamResponse(request, context, source, descriptor, operationAbort) {
|
|
827
|
+
const format = descriptor.format ?? "ndjson";
|
|
828
|
+
const maxFrameBytes = descriptor.maxFrameBytes ?? DEFAULT_CONTRACT_STREAM_FRAME_BYTES;
|
|
829
|
+
let lifetime;
|
|
830
|
+
let lifetimeExpired = false;
|
|
831
|
+
if (descriptor.lifetimeMs !== undefined) {
|
|
832
|
+
lifetime = setTimeout(() => {
|
|
833
|
+
lifetimeExpired = true;
|
|
834
|
+
operationAbort.abort(new Error("Stream lifetime expired"));
|
|
835
|
+
}, descriptor.lifetimeMs);
|
|
836
|
+
lifetime.unref?.();
|
|
837
|
+
}
|
|
838
|
+
const frames = async function* () {
|
|
839
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
840
|
+
const aborted = waitForAbort(operationAbort.signal);
|
|
841
|
+
let terminalSeen = descriptor.terminal === undefined;
|
|
842
|
+
try {
|
|
843
|
+
for (;; ) {
|
|
844
|
+
const next = await Promise.race([iterator.next(), aborted]);
|
|
845
|
+
if (next.done)
|
|
846
|
+
break;
|
|
847
|
+
const parsed = descriptor.item.safeParse(next.value);
|
|
848
|
+
if (!parsed.success) {
|
|
849
|
+
console.error("[stitchkit] contract stream item failed validation:", parsed.error);
|
|
850
|
+
throw new AppError("STREAM_ITEM_INVALID", "Stream item did not match its contract", 500);
|
|
851
|
+
}
|
|
852
|
+
const item = parsed.data;
|
|
853
|
+
if (descriptor.terminal?.safeParse(item).success)
|
|
854
|
+
terminalSeen = true;
|
|
855
|
+
const frame = { type: "data", data: item };
|
|
856
|
+
if (encodedBytes(frame, format) > maxFrameBytes) {
|
|
857
|
+
console.error(`[stitchkit] contract stream frame exceeded its ${maxFrameBytes} byte limit`);
|
|
858
|
+
throw new AppError("STREAM_FRAME_TOO_LARGE", "Stream item exceeded its declared frame limit", 500);
|
|
859
|
+
}
|
|
860
|
+
yield frame;
|
|
861
|
+
}
|
|
862
|
+
if (!terminalSeen) {
|
|
863
|
+
throw new AppError("STREAM_TERMINAL_MISSING", "Stream completed before its declared terminal item", 500);
|
|
864
|
+
}
|
|
865
|
+
yield { type: "end" };
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (operationAbort.signal.aborted && !lifetimeExpired)
|
|
868
|
+
return;
|
|
869
|
+
if (lifetimeExpired) {
|
|
870
|
+
yield {
|
|
871
|
+
type: "error",
|
|
872
|
+
error: new AppError("STREAM_LIFETIME_EXCEEDED", "Stream exceeded its declared lifetime", 408).toJSON().error
|
|
873
|
+
};
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
yield { type: "error", error: normalizeError(error).toJSON().error };
|
|
877
|
+
} finally {
|
|
878
|
+
if (lifetime !== undefined)
|
|
879
|
+
clearTimeout(lifetime);
|
|
880
|
+
operationAbort.abort();
|
|
881
|
+
Promise.resolve(iterator.return?.(undefined)).catch(() => {
|
|
882
|
+
return;
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
const route = streamingRoute({
|
|
887
|
+
path: "/",
|
|
888
|
+
format,
|
|
889
|
+
heartbeatMs: descriptor.heartbeatMs,
|
|
890
|
+
idleTimeoutSeconds: descriptor.idleTimeoutSeconds,
|
|
891
|
+
source: () => frames(),
|
|
892
|
+
onClose: () => operationAbort.abort()
|
|
893
|
+
});
|
|
894
|
+
return Promise.resolve(route.handler(request, context));
|
|
895
|
+
}
|
|
896
|
+
|
|
618
897
|
// src/server/logger.ts
|
|
619
898
|
var c = {
|
|
620
899
|
reset: "\x1B[0m",
|
|
@@ -1089,6 +1368,12 @@ function isClientClosedRequest(req, error) {
|
|
|
1089
1368
|
return true;
|
|
1090
1369
|
return req.signal.reason !== undefined && containsRequestAbortReason(error, req.signal.reason);
|
|
1091
1370
|
}
|
|
1371
|
+
function isAsyncIterable(value) {
|
|
1372
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
return typeof Reflect.get(value, Symbol.asyncIterator) === "function";
|
|
1376
|
+
}
|
|
1092
1377
|
function createHandler(config) {
|
|
1093
1378
|
const { cors, hooks, logging = false, observability, trustProxy = false } = config;
|
|
1094
1379
|
if (cors)
|
|
@@ -1309,6 +1594,17 @@ function createHandler(config) {
|
|
|
1309
1594
|
const responseMetadata = method.responseMeta ? createResponseMetadata() : undefined;
|
|
1310
1595
|
if (responseMetadata)
|
|
1311
1596
|
ctx.response = responseMetadata;
|
|
1597
|
+
const streamAbort = method.stream ? new AbortController : undefined;
|
|
1598
|
+
if (streamAbort) {
|
|
1599
|
+
if (req.signal.aborted)
|
|
1600
|
+
streamAbort.abort(req.signal.reason);
|
|
1601
|
+
else {
|
|
1602
|
+
req.signal.addEventListener("abort", () => streamAbort.abort(req.signal.reason), {
|
|
1603
|
+
once: true
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
ctx.signal = streamAbort.signal;
|
|
1607
|
+
}
|
|
1312
1608
|
let result = await method.handler(ctx);
|
|
1313
1609
|
if (method.rawResponse) {
|
|
1314
1610
|
if (!(result instanceof Response)) {
|
|
@@ -1323,6 +1619,16 @@ function createHandler(config) {
|
|
|
1323
1619
|
complete(rawRes.status);
|
|
1324
1620
|
return rawRes;
|
|
1325
1621
|
}
|
|
1622
|
+
if (method.stream) {
|
|
1623
|
+
if (!streamAbort || !isAsyncIterable(result)) {
|
|
1624
|
+
streamAbort?.abort();
|
|
1625
|
+
throw new AppError("INTERNAL_SERVER_ERROR", `Streaming endpoint ${method.serviceName}.${method.key} must return an AsyncIterable`, 500);
|
|
1626
|
+
}
|
|
1627
|
+
const response = await contractStreamResponse(req, { params: pathParams, server, ipAddress: clientIp.socketIp }, result, method.stream, streamAbort);
|
|
1628
|
+
const withCors = applyCors(response, cors, req);
|
|
1629
|
+
complete(withCors.status);
|
|
1630
|
+
return withCors;
|
|
1631
|
+
}
|
|
1326
1632
|
if (groupHooks?.afterHandle) {
|
|
1327
1633
|
const transformed = await groupHooks.afterHandle(ctx, result, method);
|
|
1328
1634
|
if (transformed !== undefined)
|
|
@@ -1347,14 +1653,14 @@ function createHandler(config) {
|
|
|
1347
1653
|
if (method.outputSchema && (responseStatus === 204 || responseStatus === 205)) {
|
|
1348
1654
|
throw new AppError("INTERNAL_SERVER_ERROR", `${method.serviceName}.${method.key} cannot combine output with bodyless status ${responseStatus}`, 500);
|
|
1349
1655
|
}
|
|
1350
|
-
const
|
|
1351
|
-
applyResponseMetadata(
|
|
1656
|
+
const responseHeaders2 = new Headers(corsHeaders2(cors, req));
|
|
1657
|
+
applyResponseMetadata(responseHeaders2, responseMetadata, `${method.serviceName}.${method.key}`);
|
|
1352
1658
|
if (!method.outputSchema) {
|
|
1353
|
-
const empty = new Response(null, { status: responseStatus, headers:
|
|
1659
|
+
const empty = new Response(null, { status: responseStatus, headers: responseHeaders2 });
|
|
1354
1660
|
complete(responseStatus);
|
|
1355
1661
|
return empty;
|
|
1356
1662
|
}
|
|
1357
|
-
const body = Response.json(result, { status: responseStatus, headers:
|
|
1663
|
+
const body = Response.json(result, { status: responseStatus, headers: responseHeaders2 });
|
|
1358
1664
|
complete(responseStatus);
|
|
1359
1665
|
return body;
|
|
1360
1666
|
} catch (err) {
|
|
@@ -1465,7 +1771,17 @@ function bindProcessSignals(handle, options = {}) {
|
|
|
1465
1771
|
bound.add(handle);
|
|
1466
1772
|
const signals = [...new Set(options.signals ?? DEFAULT_PROCESS_SIGNALS)];
|
|
1467
1773
|
const source = options.signalSource ?? defaultSignalSource;
|
|
1468
|
-
const
|
|
1774
|
+
const requested = options.shutdown ?? {};
|
|
1775
|
+
const validated = ShutdownOptionsSchema.omit({ signal: true }).parse(requested);
|
|
1776
|
+
const budgets = {
|
|
1777
|
+
...requested.gracePeriodMs !== undefined && { gracePeriodMs: validated.gracePeriodMs },
|
|
1778
|
+
...requested.forceTimeoutMs !== undefined && {
|
|
1779
|
+
forceTimeoutMs: validated.forceTimeoutMs
|
|
1780
|
+
},
|
|
1781
|
+
...requested.retryAfterSeconds !== undefined && {
|
|
1782
|
+
retryAfterSeconds: validated.retryAfterSeconds
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1469
1785
|
const controller = new AbortController;
|
|
1470
1786
|
const handlers = [];
|
|
1471
1787
|
let started = false;
|
|
@@ -2096,4 +2412,771 @@ function socketIoLane(websocket) {
|
|
|
2096
2412
|
});
|
|
2097
2413
|
}
|
|
2098
2414
|
|
|
2099
|
-
|
|
2415
|
+
// src/server/unix-client-error.ts
|
|
2416
|
+
class UnixClientTransportError extends Error {
|
|
2417
|
+
code;
|
|
2418
|
+
delivery;
|
|
2419
|
+
constructor(code, message, delivery = "possibly-dispatched", options) {
|
|
2420
|
+
super(message, options);
|
|
2421
|
+
this.code = code;
|
|
2422
|
+
this.delivery = delivery;
|
|
2423
|
+
this.name = "UnixClientTransportError";
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/server/unix-client.ts
|
|
2428
|
+
import {
|
|
2429
|
+
Agent,
|
|
2430
|
+
request as httpRequest
|
|
2431
|
+
} from "node:http";
|
|
2432
|
+
|
|
2433
|
+
// src/server/unix-client-body.ts
|
|
2434
|
+
async function readBoundedRequestBody(request, maxBytes) {
|
|
2435
|
+
const body = request.body;
|
|
2436
|
+
if (!body)
|
|
2437
|
+
return;
|
|
2438
|
+
const declared = request.headers.get("content-length");
|
|
2439
|
+
if (declared !== null && Number(declared) > maxBytes) {
|
|
2440
|
+
throw new UnixClientTransportError("UNIX_REQUEST_TOO_LARGE", `Unix request body exceeds the ${maxBytes} byte limit`, "not-dispatched");
|
|
2441
|
+
}
|
|
2442
|
+
const reader = body.getReader();
|
|
2443
|
+
const chunks = [];
|
|
2444
|
+
let total = 0;
|
|
2445
|
+
try {
|
|
2446
|
+
for (;; ) {
|
|
2447
|
+
if (request.signal.aborted)
|
|
2448
|
+
throw request.signal.reason;
|
|
2449
|
+
const chunk = await reader.read();
|
|
2450
|
+
if (chunk.done)
|
|
2451
|
+
break;
|
|
2452
|
+
total += chunk.value.byteLength;
|
|
2453
|
+
if (total > maxBytes) {
|
|
2454
|
+
throw new UnixClientTransportError("UNIX_REQUEST_TOO_LARGE", `Unix request body exceeds the ${maxBytes} byte limit`, "not-dispatched");
|
|
2455
|
+
}
|
|
2456
|
+
chunks.push(chunk.value);
|
|
2457
|
+
}
|
|
2458
|
+
} finally {
|
|
2459
|
+
reader.releaseLock();
|
|
2460
|
+
}
|
|
2461
|
+
const result = new Uint8Array(total);
|
|
2462
|
+
let offset = 0;
|
|
2463
|
+
for (const chunk of chunks) {
|
|
2464
|
+
result.set(chunk, offset);
|
|
2465
|
+
offset += chunk.byteLength;
|
|
2466
|
+
}
|
|
2467
|
+
return result;
|
|
2468
|
+
}
|
|
2469
|
+
function boundedIncomingBody(response, maxBytes, onDone) {
|
|
2470
|
+
let finished = false;
|
|
2471
|
+
let received = 0;
|
|
2472
|
+
const finish = () => {
|
|
2473
|
+
if (finished)
|
|
2474
|
+
return;
|
|
2475
|
+
finished = true;
|
|
2476
|
+
onDone();
|
|
2477
|
+
};
|
|
2478
|
+
return new ReadableStream({
|
|
2479
|
+
start(controller) {
|
|
2480
|
+
response.pause();
|
|
2481
|
+
response.on("data", (chunk) => {
|
|
2482
|
+
if (finished)
|
|
2483
|
+
return;
|
|
2484
|
+
received += chunk.byteLength;
|
|
2485
|
+
if (received > maxBytes) {
|
|
2486
|
+
controller.error(new UnixClientTransportError("UNIX_RESPONSE_TOO_LARGE", `Unix response body exceeds the ${maxBytes} byte limit`, "response-received"));
|
|
2487
|
+
response.destroy();
|
|
2488
|
+
finish();
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
2492
|
+
response.pause();
|
|
2493
|
+
});
|
|
2494
|
+
response.once("end", () => {
|
|
2495
|
+
if (finished)
|
|
2496
|
+
return;
|
|
2497
|
+
controller.close();
|
|
2498
|
+
finish();
|
|
2499
|
+
});
|
|
2500
|
+
response.once("aborted", () => {
|
|
2501
|
+
if (finished)
|
|
2502
|
+
return;
|
|
2503
|
+
controller.error(new UnixClientTransportError("UNIX_RESPONSE_ABORTED", "Unix response was aborted before completion", "response-received"));
|
|
2504
|
+
finish();
|
|
2505
|
+
});
|
|
2506
|
+
response.once("error", (error) => {
|
|
2507
|
+
if (finished)
|
|
2508
|
+
return;
|
|
2509
|
+
controller.error(new UnixClientTransportError("UNIX_RESPONSE_ABORTED", "Unix response failed before completion", "response-received", { cause: error }));
|
|
2510
|
+
finish();
|
|
2511
|
+
});
|
|
2512
|
+
},
|
|
2513
|
+
pull() {
|
|
2514
|
+
response.resume();
|
|
2515
|
+
},
|
|
2516
|
+
cancel() {
|
|
2517
|
+
response.destroy();
|
|
2518
|
+
finish();
|
|
2519
|
+
}
|
|
2520
|
+
}, { highWaterMark: 1 });
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// src/server/unix-client-bun.ts
|
|
2524
|
+
var MAX_BODY_CHUNK_BYTES = 64 * 1024;
|
|
2525
|
+
function isBunUnixRuntime(candidate) {
|
|
2526
|
+
if (typeof candidate !== "object" || candidate === null)
|
|
2527
|
+
return false;
|
|
2528
|
+
return typeof Reflect.get(candidate, "connect") === "function";
|
|
2529
|
+
}
|
|
2530
|
+
function runtimeBun() {
|
|
2531
|
+
const candidate = Reflect.get(globalThis, "Bun");
|
|
2532
|
+
return isBunUnixRuntime(candidate) ? candidate : undefined;
|
|
2533
|
+
}
|
|
2534
|
+
function hasBunUnixRuntime() {
|
|
2535
|
+
return runtimeBun() !== undefined;
|
|
2536
|
+
}
|
|
2537
|
+
function concatBytes2(left, right) {
|
|
2538
|
+
if (left.byteLength === 0)
|
|
2539
|
+
return right.slice();
|
|
2540
|
+
const combined = new Uint8Array(left.byteLength + right.byteLength);
|
|
2541
|
+
combined.set(left);
|
|
2542
|
+
combined.set(right, left.byteLength);
|
|
2543
|
+
return combined;
|
|
2544
|
+
}
|
|
2545
|
+
function indexOfSequence(bytes, sequence) {
|
|
2546
|
+
outer:
|
|
2547
|
+
for (let index = 0;index <= bytes.byteLength - sequence.length; index += 1) {
|
|
2548
|
+
for (let offset = 0;offset < sequence.length; offset += 1) {
|
|
2549
|
+
if (bytes[index + offset] !== sequence[offset])
|
|
2550
|
+
continue outer;
|
|
2551
|
+
}
|
|
2552
|
+
return index;
|
|
2553
|
+
}
|
|
2554
|
+
return -1;
|
|
2555
|
+
}
|
|
2556
|
+
function requestBytes(request, body) {
|
|
2557
|
+
const url = new URL(request.url);
|
|
2558
|
+
const headers = new Headers(request.headers);
|
|
2559
|
+
headers.set("host", url.host);
|
|
2560
|
+
headers.set("connection", "close");
|
|
2561
|
+
headers.set("accept-encoding", "identity");
|
|
2562
|
+
headers.delete("transfer-encoding");
|
|
2563
|
+
headers.delete("content-length");
|
|
2564
|
+
if (body !== undefined)
|
|
2565
|
+
headers.set("content-length", String(body.byteLength));
|
|
2566
|
+
const lines = [`${request.method} ${url.pathname}${url.search} HTTP/1.1`];
|
|
2567
|
+
for (const [name, value] of headers)
|
|
2568
|
+
lines.push(`${name}: ${value}`);
|
|
2569
|
+
const head = new TextEncoder().encode(`${lines.join(`\r
|
|
2570
|
+
`)}\r
|
|
2571
|
+
\r
|
|
2572
|
+
`);
|
|
2573
|
+
return body === undefined ? head : concatBytes2(head, body);
|
|
2574
|
+
}
|
|
2575
|
+
function parseContentLength(headers) {
|
|
2576
|
+
const raw = headers.get("content-length");
|
|
2577
|
+
if (raw === null)
|
|
2578
|
+
return;
|
|
2579
|
+
if (!/^\d+$/.test(raw))
|
|
2580
|
+
throw new Error("Unix response has an invalid Content-Length");
|
|
2581
|
+
const value = Number(raw);
|
|
2582
|
+
if (!Number.isSafeInteger(value)) {
|
|
2583
|
+
throw new Error("Unix response Content-Length is not a safe integer");
|
|
2584
|
+
}
|
|
2585
|
+
return value;
|
|
2586
|
+
}
|
|
2587
|
+
function bunUnixRequest(options) {
|
|
2588
|
+
const outgoing = requestBytes(options.request, options.body);
|
|
2589
|
+
const bun = runtimeBun();
|
|
2590
|
+
if (!bun)
|
|
2591
|
+
throw new Error("Bun Unix runtime is unavailable");
|
|
2592
|
+
let socket;
|
|
2593
|
+
let controller;
|
|
2594
|
+
let wire = new Uint8Array;
|
|
2595
|
+
let responseResolved = false;
|
|
2596
|
+
let headersReceived = false;
|
|
2597
|
+
let settled = false;
|
|
2598
|
+
let bodyComplete = false;
|
|
2599
|
+
let peerEnded = false;
|
|
2600
|
+
let chunked = false;
|
|
2601
|
+
let chunkRemaining;
|
|
2602
|
+
let expectChunkCrlf = false;
|
|
2603
|
+
let trailers = false;
|
|
2604
|
+
let remainingLength;
|
|
2605
|
+
let received = 0;
|
|
2606
|
+
let writeOffset = 0;
|
|
2607
|
+
let resolveResponse = () => {
|
|
2608
|
+
return;
|
|
2609
|
+
};
|
|
2610
|
+
let rejectResponse = () => {
|
|
2611
|
+
return;
|
|
2612
|
+
};
|
|
2613
|
+
const response = new Promise((resolve, reject) => {
|
|
2614
|
+
resolveResponse = resolve;
|
|
2615
|
+
rejectResponse = reject;
|
|
2616
|
+
});
|
|
2617
|
+
const transportFailure = (error) => {
|
|
2618
|
+
if (error instanceof UnixClientTransportError)
|
|
2619
|
+
return error;
|
|
2620
|
+
if (headersReceived) {
|
|
2621
|
+
return new UnixClientTransportError("UNIX_RESPONSE_ABORTED", "Unix response failed before completion", "response-received", { cause: error });
|
|
2622
|
+
}
|
|
2623
|
+
const dispatched = writeOffset > 0;
|
|
2624
|
+
return new UnixClientTransportError(dispatched ? "UNIX_DELIVERY_UNCERTAIN" : "UNIX_CONNECT_FAILED", dispatched ? "Unix request transport failed after dispatch may have begun" : "Unix socket connection failed before request dispatch", dispatched ? "possibly-dispatched" : "not-dispatched", { cause: error });
|
|
2625
|
+
};
|
|
2626
|
+
const clearRegistration = options.registerAbort(() => {
|
|
2627
|
+
fail(new UnixClientTransportError("UNIX_CLIENT_CLOSED", "Unix client transport is closed"));
|
|
2628
|
+
});
|
|
2629
|
+
const timer = setTimeout(() => {
|
|
2630
|
+
fail(new UnixClientTransportError("UNIX_HEADERS_TIMEOUT", `Unix response headers did not arrive within ${options.headersTimeoutMs}ms`));
|
|
2631
|
+
}, options.headersTimeoutMs);
|
|
2632
|
+
timer.unref();
|
|
2633
|
+
const removeRequestAbort = () => {
|
|
2634
|
+
options.request.signal.removeEventListener("abort", abortRequest);
|
|
2635
|
+
};
|
|
2636
|
+
const finish = (terminate = true) => {
|
|
2637
|
+
if (settled)
|
|
2638
|
+
return;
|
|
2639
|
+
settled = true;
|
|
2640
|
+
clearTimeout(timer);
|
|
2641
|
+
clearRegistration();
|
|
2642
|
+
removeRequestAbort();
|
|
2643
|
+
if (terminate)
|
|
2644
|
+
socket?.terminate();
|
|
2645
|
+
};
|
|
2646
|
+
function fail(error) {
|
|
2647
|
+
if (settled)
|
|
2648
|
+
return;
|
|
2649
|
+
if (responseResolved)
|
|
2650
|
+
controller?.error(error);
|
|
2651
|
+
else
|
|
2652
|
+
rejectResponse(error);
|
|
2653
|
+
finish();
|
|
2654
|
+
}
|
|
2655
|
+
function abortRequest() {
|
|
2656
|
+
fail(options.request.signal.reason ?? new DOMException("Request aborted", "AbortError"));
|
|
2657
|
+
}
|
|
2658
|
+
const takeWire = (bytes) => {
|
|
2659
|
+
const value = wire.slice(0, bytes);
|
|
2660
|
+
wire = wire.slice(bytes);
|
|
2661
|
+
return value;
|
|
2662
|
+
};
|
|
2663
|
+
const enqueue = (value) => {
|
|
2664
|
+
received += value.byteLength;
|
|
2665
|
+
if (received > options.maxResponseBytes) {
|
|
2666
|
+
fail(new UnixClientTransportError("UNIX_RESPONSE_TOO_LARGE", `Unix response body exceeds the ${options.maxResponseBytes} byte limit`, "response-received"));
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
controller?.enqueue(value);
|
|
2670
|
+
};
|
|
2671
|
+
const completeBody = () => {
|
|
2672
|
+
if (bodyComplete || settled)
|
|
2673
|
+
return;
|
|
2674
|
+
bodyComplete = true;
|
|
2675
|
+
controller?.close();
|
|
2676
|
+
finish();
|
|
2677
|
+
};
|
|
2678
|
+
const resumeForData = () => {
|
|
2679
|
+
if (settled || bodyComplete || peerEnded)
|
|
2680
|
+
return;
|
|
2681
|
+
socket?.resume();
|
|
2682
|
+
};
|
|
2683
|
+
const pumpChunked = () => {
|
|
2684
|
+
if (!controller || settled || bodyComplete)
|
|
2685
|
+
return;
|
|
2686
|
+
for (;; ) {
|
|
2687
|
+
if ((controller.desiredSize ?? 1) <= 0)
|
|
2688
|
+
return;
|
|
2689
|
+
if (trailers) {
|
|
2690
|
+
if (wire.byteLength >= 2 && wire[0] === 13 && wire[1] === 10) {
|
|
2691
|
+
takeWire(2);
|
|
2692
|
+
completeBody();
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
const trailerEnd = indexOfSequence(wire, [13, 10, 13, 10]);
|
|
2696
|
+
if (trailerEnd < 0) {
|
|
2697
|
+
if (wire.byteLength > options.maxHeaderBytes) {
|
|
2698
|
+
fail(new UnixClientTransportError("UNIX_HEADERS_TOO_LARGE", `Unix response trailers exceed the ${options.maxHeaderBytes} byte limit`, "response-received"));
|
|
2699
|
+
} else if (peerEnded) {
|
|
2700
|
+
fail(transportFailure(new Error("Unix chunked response ended inside trailers")));
|
|
2701
|
+
} else {
|
|
2702
|
+
resumeForData();
|
|
2703
|
+
}
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
takeWire(trailerEnd + 4);
|
|
2707
|
+
completeBody();
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
if (expectChunkCrlf) {
|
|
2711
|
+
if (wire.byteLength < 2) {
|
|
2712
|
+
if (peerEnded) {
|
|
2713
|
+
fail(transportFailure(new Error("Unix chunked response ended before chunk delimiter")));
|
|
2714
|
+
} else
|
|
2715
|
+
resumeForData();
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
if (wire[0] !== 13 || wire[1] !== 10) {
|
|
2719
|
+
fail(transportFailure(new Error("Unix chunked response has an invalid chunk delimiter")));
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
takeWire(2);
|
|
2723
|
+
expectChunkCrlf = false;
|
|
2724
|
+
}
|
|
2725
|
+
if (chunkRemaining === undefined) {
|
|
2726
|
+
const lineEnd = indexOfSequence(wire, [13, 10]);
|
|
2727
|
+
if (lineEnd < 0) {
|
|
2728
|
+
if (wire.byteLength > 1024) {
|
|
2729
|
+
fail(transportFailure(new Error("Unix chunk header is too large")));
|
|
2730
|
+
} else if (peerEnded) {
|
|
2731
|
+
fail(transportFailure(new Error("Unix chunked response ended inside chunk header")));
|
|
2732
|
+
} else
|
|
2733
|
+
resumeForData();
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
const line = new TextDecoder("ascii", { fatal: true }).decode(takeWire(lineEnd));
|
|
2737
|
+
takeWire(2);
|
|
2738
|
+
const sizeText = line.split(";", 1)[0]?.trim() ?? "";
|
|
2739
|
+
if (!/^[0-9a-f]+$/i.test(sizeText)) {
|
|
2740
|
+
fail(transportFailure(new Error("Unix chunked response has an invalid chunk size")));
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
chunkRemaining = Number.parseInt(sizeText, 16);
|
|
2744
|
+
if (!Number.isSafeInteger(chunkRemaining)) {
|
|
2745
|
+
fail(transportFailure(new Error("Unix chunk size is not a safe integer")));
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
if (chunkRemaining === 0) {
|
|
2749
|
+
chunkRemaining = undefined;
|
|
2750
|
+
trailers = true;
|
|
2751
|
+
continue;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (wire.byteLength === 0) {
|
|
2755
|
+
if (peerEnded) {
|
|
2756
|
+
fail(transportFailure(new Error("Unix chunked response ended inside a chunk")));
|
|
2757
|
+
} else
|
|
2758
|
+
resumeForData();
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
const bytes = Math.min(chunkRemaining, wire.byteLength, MAX_BODY_CHUNK_BYTES);
|
|
2762
|
+
enqueue(takeWire(bytes));
|
|
2763
|
+
if (settled)
|
|
2764
|
+
return;
|
|
2765
|
+
chunkRemaining -= bytes;
|
|
2766
|
+
if (chunkRemaining === 0) {
|
|
2767
|
+
chunkRemaining = undefined;
|
|
2768
|
+
expectChunkCrlf = true;
|
|
2769
|
+
}
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
const pumpPlain = () => {
|
|
2774
|
+
if (!controller || settled || bodyComplete)
|
|
2775
|
+
return;
|
|
2776
|
+
if ((controller.desiredSize ?? 1) <= 0)
|
|
2777
|
+
return;
|
|
2778
|
+
if (wire.byteLength > 0) {
|
|
2779
|
+
const permitted = remainingLength ?? wire.byteLength;
|
|
2780
|
+
const bytes = Math.min(permitted, wire.byteLength, MAX_BODY_CHUNK_BYTES);
|
|
2781
|
+
enqueue(takeWire(bytes));
|
|
2782
|
+
if (settled)
|
|
2783
|
+
return;
|
|
2784
|
+
if (remainingLength !== undefined) {
|
|
2785
|
+
remainingLength -= bytes;
|
|
2786
|
+
if (remainingLength === 0) {
|
|
2787
|
+
completeBody();
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (peerEnded) {
|
|
2794
|
+
if (remainingLength !== undefined && remainingLength > 0) {
|
|
2795
|
+
fail(transportFailure(new Error("Unix response ended before Content-Length bytes arrived")));
|
|
2796
|
+
} else {
|
|
2797
|
+
completeBody();
|
|
2798
|
+
}
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
resumeForData();
|
|
2802
|
+
};
|
|
2803
|
+
const pump = () => {
|
|
2804
|
+
if (chunked)
|
|
2805
|
+
pumpChunked();
|
|
2806
|
+
else
|
|
2807
|
+
pumpPlain();
|
|
2808
|
+
};
|
|
2809
|
+
const parseHeaders = () => {
|
|
2810
|
+
const headerEnd = indexOfSequence(wire, [13, 10, 13, 10]);
|
|
2811
|
+
if (headerEnd < 0) {
|
|
2812
|
+
if (wire.byteLength > options.maxHeaderBytes) {
|
|
2813
|
+
fail(new UnixClientTransportError("UNIX_HEADERS_TOO_LARGE", `Unix response headers exceed the ${options.maxHeaderBytes} byte limit`, "response-received"));
|
|
2814
|
+
} else {
|
|
2815
|
+
socket?.resume();
|
|
2816
|
+
}
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
if (headerEnd > options.maxHeaderBytes) {
|
|
2820
|
+
fail(new UnixClientTransportError("UNIX_HEADERS_TOO_LARGE", `Unix response headers exceed the ${options.maxHeaderBytes} byte limit`, "response-received"));
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
headersReceived = true;
|
|
2824
|
+
let text;
|
|
2825
|
+
try {
|
|
2826
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(takeWire(headerEnd));
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
fail(transportFailure(error));
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
takeWire(4);
|
|
2832
|
+
const lines = text.split(`\r
|
|
2833
|
+
`);
|
|
2834
|
+
const statusLine = lines.shift() ?? "";
|
|
2835
|
+
const matched = /^HTTP\/1\.[01] (\d{3})(?: (.*))?$/.exec(statusLine);
|
|
2836
|
+
if (!matched) {
|
|
2837
|
+
fail(transportFailure(new Error("Unix response has an invalid HTTP status line")));
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
const status = Number(matched[1]);
|
|
2841
|
+
const statusText = matched[2] ?? "";
|
|
2842
|
+
const headers = new Headers;
|
|
2843
|
+
try {
|
|
2844
|
+
for (const line of lines) {
|
|
2845
|
+
const separator = line.indexOf(":");
|
|
2846
|
+
if (separator <= 0)
|
|
2847
|
+
throw new Error("Unix response has an invalid header line");
|
|
2848
|
+
headers.append(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
|
2849
|
+
}
|
|
2850
|
+
remainingLength = parseContentLength(headers);
|
|
2851
|
+
} catch (error) {
|
|
2852
|
+
fail(transportFailure(error));
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
if (remainingLength !== undefined && remainingLength > options.maxResponseBytes) {
|
|
2856
|
+
fail(new UnixClientTransportError("UNIX_RESPONSE_TOO_LARGE", `Unix response body exceeds the ${options.maxResponseBytes} byte limit`, "response-received"));
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
chunked = headers.get("transfer-encoding")?.toLowerCase().includes("chunked") ?? false;
|
|
2860
|
+
const bodyless = options.request.method === "HEAD" || status === 204 || status === 205 || status === 304;
|
|
2861
|
+
clearTimeout(timer);
|
|
2862
|
+
responseResolved = true;
|
|
2863
|
+
if (bodyless || remainingLength === 0) {
|
|
2864
|
+
resolveResponse(new Response(null, { status, statusText, headers }));
|
|
2865
|
+
finish();
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
const body = new ReadableStream({
|
|
2869
|
+
start(value) {
|
|
2870
|
+
controller = value;
|
|
2871
|
+
pump();
|
|
2872
|
+
},
|
|
2873
|
+
pull() {
|
|
2874
|
+
pump();
|
|
2875
|
+
},
|
|
2876
|
+
cancel() {
|
|
2877
|
+
finish();
|
|
2878
|
+
}
|
|
2879
|
+
}, { highWaterMark: 1 });
|
|
2880
|
+
resolveResponse(new Response(body, { status, statusText, headers }));
|
|
2881
|
+
};
|
|
2882
|
+
const flushRequest = (active) => {
|
|
2883
|
+
while (writeOffset < outgoing.byteLength) {
|
|
2884
|
+
const written = active.write(outgoing, writeOffset, outgoing.byteLength - writeOffset);
|
|
2885
|
+
if (written < 0) {
|
|
2886
|
+
fail(transportFailure(new Error("Unix request socket closed while writing")));
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
if (written === 0)
|
|
2890
|
+
return;
|
|
2891
|
+
writeOffset += written;
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
if (options.request.signal.aborted) {
|
|
2895
|
+
fail(options.request.signal.reason ?? new DOMException("Request aborted", "AbortError"));
|
|
2896
|
+
return response;
|
|
2897
|
+
}
|
|
2898
|
+
options.request.signal.addEventListener("abort", abortRequest, { once: true });
|
|
2899
|
+
bun.connect({
|
|
2900
|
+
unix: options.socketPath,
|
|
2901
|
+
socket: {
|
|
2902
|
+
binaryType: "uint8array",
|
|
2903
|
+
open(active) {
|
|
2904
|
+
socket = active;
|
|
2905
|
+
if (settled) {
|
|
2906
|
+
active.terminate();
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
flushRequest(active);
|
|
2910
|
+
},
|
|
2911
|
+
drain(active) {
|
|
2912
|
+
flushRequest(active);
|
|
2913
|
+
},
|
|
2914
|
+
data(active, data) {
|
|
2915
|
+
active.pause();
|
|
2916
|
+
wire = concatBytes2(wire, data);
|
|
2917
|
+
if (!responseResolved)
|
|
2918
|
+
parseHeaders();
|
|
2919
|
+
else
|
|
2920
|
+
pump();
|
|
2921
|
+
},
|
|
2922
|
+
end() {
|
|
2923
|
+
peerEnded = true;
|
|
2924
|
+
if (!responseResolved) {
|
|
2925
|
+
fail(transportFailure(new Error("Unix connection ended before response headers")));
|
|
2926
|
+
} else
|
|
2927
|
+
pump();
|
|
2928
|
+
},
|
|
2929
|
+
close(_active, error) {
|
|
2930
|
+
peerEnded = true;
|
|
2931
|
+
if (settled)
|
|
2932
|
+
return;
|
|
2933
|
+
if (error)
|
|
2934
|
+
fail(transportFailure(error));
|
|
2935
|
+
else if (!responseResolved) {
|
|
2936
|
+
fail(transportFailure(new Error("Unix connection closed before response headers")));
|
|
2937
|
+
} else
|
|
2938
|
+
pump();
|
|
2939
|
+
},
|
|
2940
|
+
error(_active, error) {
|
|
2941
|
+
fail(transportFailure(error));
|
|
2942
|
+
},
|
|
2943
|
+
connectError(_active, error) {
|
|
2944
|
+
fail(transportFailure(error));
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}).catch((error) => fail(transportFailure(error)));
|
|
2948
|
+
return response;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
// src/server/unix-client.ts
|
|
2952
|
+
var DEFAULT_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
2953
|
+
var DEFAULT_HEADERS_TIMEOUT_MS = 30000;
|
|
2954
|
+
var DEFAULT_MAX_CONNECTIONS = 8;
|
|
2955
|
+
var DEFAULT_MAX_REDIRECTS = 5;
|
|
2956
|
+
var DEFAULT_MAX_HEADER_BYTES2 = 64 * 1024;
|
|
2957
|
+
function positiveInteger(value, fallback, name) {
|
|
2958
|
+
const resolved = value ?? fallback;
|
|
2959
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
2960
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
2961
|
+
}
|
|
2962
|
+
return resolved;
|
|
2963
|
+
}
|
|
2964
|
+
function nonNegativeInteger(value, fallback, name) {
|
|
2965
|
+
const resolved = value ?? fallback;
|
|
2966
|
+
if (!Number.isSafeInteger(resolved) || resolved < 0) {
|
|
2967
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
2968
|
+
}
|
|
2969
|
+
return resolved;
|
|
2970
|
+
}
|
|
2971
|
+
function isRedirect(status) {
|
|
2972
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
2973
|
+
}
|
|
2974
|
+
function requestBody(body) {
|
|
2975
|
+
if (body === undefined)
|
|
2976
|
+
return;
|
|
2977
|
+
const copy = new ArrayBuffer(body.byteLength);
|
|
2978
|
+
new Uint8Array(copy).set(body);
|
|
2979
|
+
return copy;
|
|
2980
|
+
}
|
|
2981
|
+
function responseHeaders2(response) {
|
|
2982
|
+
const headers = new Headers;
|
|
2983
|
+
for (let index = 0;index < response.rawHeaders.length; index += 2) {
|
|
2984
|
+
const name = response.rawHeaders[index];
|
|
2985
|
+
const value = response.rawHeaders[index + 1];
|
|
2986
|
+
if (name !== undefined && value !== undefined)
|
|
2987
|
+
headers.append(name, value);
|
|
2988
|
+
}
|
|
2989
|
+
return headers;
|
|
2990
|
+
}
|
|
2991
|
+
function redirectedRequest(request, location, status, body) {
|
|
2992
|
+
const url = new URL(location, request.url);
|
|
2993
|
+
const changesToGet = status === 303 || (status === 301 || status === 302) && request.method === "POST";
|
|
2994
|
+
const headers = new Headers(request.headers);
|
|
2995
|
+
if (changesToGet) {
|
|
2996
|
+
headers.delete("content-length");
|
|
2997
|
+
headers.delete("content-type");
|
|
2998
|
+
}
|
|
2999
|
+
return new Request(url, {
|
|
3000
|
+
method: changesToGet ? "GET" : request.method,
|
|
3001
|
+
headers,
|
|
3002
|
+
...!changesToGet && body !== undefined && { body: requestBody(body) },
|
|
3003
|
+
redirect: request.redirect,
|
|
3004
|
+
signal: request.signal
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
function createUnixClientTransport(config) {
|
|
3008
|
+
if (!config.socketPath.startsWith("/") || config.socketPath.includes("\x00")) {
|
|
3009
|
+
throw new TypeError("socketPath must be an absolute Unix socket path");
|
|
3010
|
+
}
|
|
3011
|
+
const maxRequestBytes = positiveInteger(config.maxRequestBytes, DEFAULT_MAX_BODY_BYTES, "maxRequestBytes");
|
|
3012
|
+
const maxResponseBytes = positiveInteger(config.maxResponseBytes, DEFAULT_MAX_BODY_BYTES, "maxResponseBytes");
|
|
3013
|
+
const headersTimeoutMs = positiveInteger(config.headersTimeoutMs, DEFAULT_HEADERS_TIMEOUT_MS, "headersTimeoutMs");
|
|
3014
|
+
const maxConnections = positiveInteger(config.maxConnections, DEFAULT_MAX_CONNECTIONS, "maxConnections");
|
|
3015
|
+
const maxHeaderBytes = positiveInteger(config.maxHeaderBytes, DEFAULT_MAX_HEADER_BYTES2, "maxHeaderBytes");
|
|
3016
|
+
const maxRedirects = nonNegativeInteger(config.maxRedirects, DEFAULT_MAX_REDIRECTS, "maxRedirects");
|
|
3017
|
+
const agent = new Agent({ keepAlive: true, maxSockets: maxConnections });
|
|
3018
|
+
const activeRequests = new Set;
|
|
3019
|
+
const activeResponses = new Set;
|
|
3020
|
+
const bunAborters = new Set;
|
|
3021
|
+
let closed = false;
|
|
3022
|
+
const dispatch = async (request, redirectCount) => {
|
|
3023
|
+
if (closed) {
|
|
3024
|
+
throw new UnixClientTransportError("UNIX_CLIENT_CLOSED", "Unix client transport is closed", "not-dispatched");
|
|
3025
|
+
}
|
|
3026
|
+
if (request.signal.aborted)
|
|
3027
|
+
throw request.signal.reason;
|
|
3028
|
+
const url = new URL(request.url);
|
|
3029
|
+
if (url.protocol !== "http:") {
|
|
3030
|
+
throw new TypeError("Unix client transport accepts only http: request URLs");
|
|
3031
|
+
}
|
|
3032
|
+
const body = await readBoundedRequestBody(request, maxRequestBytes);
|
|
3033
|
+
if (hasBunUnixRuntime()) {
|
|
3034
|
+
if (bunAborters.size >= maxConnections) {
|
|
3035
|
+
throw new UnixClientTransportError("UNIX_CONNECTION_LIMIT", `Unix client connection limit ${maxConnections} reached`, "not-dispatched");
|
|
3036
|
+
}
|
|
3037
|
+
const response = await bunUnixRequest({
|
|
3038
|
+
socketPath: config.socketPath,
|
|
3039
|
+
request,
|
|
3040
|
+
body,
|
|
3041
|
+
maxResponseBytes,
|
|
3042
|
+
maxHeaderBytes,
|
|
3043
|
+
headersTimeoutMs,
|
|
3044
|
+
registerAbort(abort) {
|
|
3045
|
+
bunAborters.add(abort);
|
|
3046
|
+
return () => void bunAborters.delete(abort);
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
const location2 = response.headers.get("location");
|
|
3050
|
+
if (location2 && isRedirect(response.status)) {
|
|
3051
|
+
if (request.redirect === "error" || redirectCount >= maxRedirects) {
|
|
3052
|
+
await response.body?.cancel();
|
|
3053
|
+
throw new UnixClientTransportError("UNIX_REDIRECT_REFUSED", redirectCount >= maxRedirects ? `Unix redirect limit ${maxRedirects} exceeded` : "Unix request redirect mode is error", "response-received");
|
|
3054
|
+
}
|
|
3055
|
+
if (request.redirect === "follow") {
|
|
3056
|
+
await response.body?.cancel();
|
|
3057
|
+
return dispatch(redirectedRequest(request, location2, response.status, body), redirectCount + 1);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return response;
|
|
3061
|
+
}
|
|
3062
|
+
const headers = Object.fromEntries(request.headers.entries());
|
|
3063
|
+
if (!Object.keys(headers).some((name) => name.toLowerCase() === "host")) {
|
|
3064
|
+
headers.host = url.host;
|
|
3065
|
+
}
|
|
3066
|
+
const incoming = await new Promise((resolve, reject) => {
|
|
3067
|
+
let settled = false;
|
|
3068
|
+
const resolveOnce = (response) => {
|
|
3069
|
+
if (settled)
|
|
3070
|
+
return;
|
|
3071
|
+
settled = true;
|
|
3072
|
+
resolve(response);
|
|
3073
|
+
};
|
|
3074
|
+
const rejectOnce = (error) => {
|
|
3075
|
+
if (settled)
|
|
3076
|
+
return;
|
|
3077
|
+
settled = true;
|
|
3078
|
+
reject(error);
|
|
3079
|
+
};
|
|
3080
|
+
const outgoing = httpRequest({
|
|
3081
|
+
socketPath: config.socketPath,
|
|
3082
|
+
path: `${url.pathname}${url.search}`,
|
|
3083
|
+
method: request.method,
|
|
3084
|
+
headers,
|
|
3085
|
+
agent,
|
|
3086
|
+
signal: request.signal
|
|
3087
|
+
}, resolveOnce);
|
|
3088
|
+
let connected = false;
|
|
3089
|
+
let requestStartBytes = 0;
|
|
3090
|
+
outgoing.once("socket", (socket) => {
|
|
3091
|
+
requestStartBytes = socket.bytesWritten;
|
|
3092
|
+
if (!socket.connecting && !socket.destroyed)
|
|
3093
|
+
connected = true;
|
|
3094
|
+
else {
|
|
3095
|
+
socket.once("connect", () => {
|
|
3096
|
+
connected = true;
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
activeRequests.add(outgoing);
|
|
3101
|
+
const timer = setTimeout(() => {
|
|
3102
|
+
const error = new UnixClientTransportError("UNIX_HEADERS_TIMEOUT", `Unix response headers did not arrive within ${headersTimeoutMs}ms`);
|
|
3103
|
+
rejectOnce(error);
|
|
3104
|
+
outgoing.destroy(error);
|
|
3105
|
+
}, headersTimeoutMs);
|
|
3106
|
+
timer.unref();
|
|
3107
|
+
outgoing.once("response", () => clearTimeout(timer));
|
|
3108
|
+
outgoing.once("error", (error) => {
|
|
3109
|
+
const dispatched = connected && (outgoing.socket?.bytesWritten ?? requestStartBytes) > requestStartBytes;
|
|
3110
|
+
rejectOnce(new UnixClientTransportError(dispatched ? "UNIX_DELIVERY_UNCERTAIN" : "UNIX_CONNECT_FAILED", dispatched ? "Unix request transport failed after dispatch may have begun" : "Unix socket connection failed before request dispatch", dispatched ? "possibly-dispatched" : "not-dispatched", { cause: error }));
|
|
3111
|
+
});
|
|
3112
|
+
outgoing.once("close", () => {
|
|
3113
|
+
clearTimeout(timer);
|
|
3114
|
+
activeRequests.delete(outgoing);
|
|
3115
|
+
});
|
|
3116
|
+
outgoing.end(body);
|
|
3117
|
+
});
|
|
3118
|
+
activeResponses.add(incoming);
|
|
3119
|
+
const releaseResponse = () => void activeResponses.delete(incoming);
|
|
3120
|
+
const status = incoming.statusCode ?? 0;
|
|
3121
|
+
const responseHeadersValue = responseHeaders2(incoming);
|
|
3122
|
+
const declaredResponseBytes = responseHeadersValue.get("content-length");
|
|
3123
|
+
if (declaredResponseBytes !== null && /^\d+$/.test(declaredResponseBytes) && Number(declaredResponseBytes) > maxResponseBytes) {
|
|
3124
|
+
incoming.destroy();
|
|
3125
|
+
releaseResponse();
|
|
3126
|
+
throw new UnixClientTransportError("UNIX_RESPONSE_TOO_LARGE", `Unix response body exceeds the ${maxResponseBytes} byte limit`, "response-received");
|
|
3127
|
+
}
|
|
3128
|
+
const location = responseHeadersValue.get("location");
|
|
3129
|
+
if (location && isRedirect(status)) {
|
|
3130
|
+
if (request.redirect === "error" || redirectCount >= maxRedirects) {
|
|
3131
|
+
incoming.destroy();
|
|
3132
|
+
releaseResponse();
|
|
3133
|
+
throw new UnixClientTransportError("UNIX_REDIRECT_REFUSED", redirectCount >= maxRedirects ? `Unix redirect limit ${maxRedirects} exceeded` : "Unix request redirect mode is error", "response-received");
|
|
3134
|
+
}
|
|
3135
|
+
if (request.redirect === "follow") {
|
|
3136
|
+
incoming.destroy();
|
|
3137
|
+
releaseResponse();
|
|
3138
|
+
return dispatch(redirectedRequest(request, location, status, body), redirectCount + 1);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
const bodyless = request.method === "HEAD" || status === 204 || status === 205 || status === 304;
|
|
3142
|
+
if (bodyless) {
|
|
3143
|
+
incoming.resume();
|
|
3144
|
+
incoming.once("end", releaseResponse);
|
|
3145
|
+
return new Response(null, {
|
|
3146
|
+
status,
|
|
3147
|
+
statusText: incoming.statusMessage,
|
|
3148
|
+
headers: responseHeadersValue
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
return new Response(boundedIncomingBody(incoming, maxResponseBytes, releaseResponse), {
|
|
3152
|
+
status,
|
|
3153
|
+
statusText: incoming.statusMessage,
|
|
3154
|
+
headers: responseHeadersValue
|
|
3155
|
+
});
|
|
3156
|
+
};
|
|
3157
|
+
return {
|
|
3158
|
+
fetch(input, init) {
|
|
3159
|
+
return dispatch(new Request(input, init), 0);
|
|
3160
|
+
},
|
|
3161
|
+
get closed() {
|
|
3162
|
+
return closed;
|
|
3163
|
+
},
|
|
3164
|
+
async close() {
|
|
3165
|
+
if (closed)
|
|
3166
|
+
return;
|
|
3167
|
+
closed = true;
|
|
3168
|
+
for (const request of activeRequests)
|
|
3169
|
+
request.destroy();
|
|
3170
|
+
for (const response of activeResponses)
|
|
3171
|
+
response.destroy();
|
|
3172
|
+
for (const abort of [...bunAborters])
|
|
3173
|
+
abort();
|
|
3174
|
+
activeRequests.clear();
|
|
3175
|
+
activeResponses.clear();
|
|
3176
|
+
bunAborters.clear();
|
|
3177
|
+
agent.destroy();
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
export { parseMultipart, DEFAULT_STREAM_HEARTBEAT_MS, streamingRoute, ndjsonRoute, sseRoute, createHandler, bindProcessSignals, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane, UnixClientTransportError, createUnixClientTransport };
|