stitchkit 0.60.1 → 0.61.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/application/kernel.d.ts +23 -0
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/server-resource.d.ts.map +1 -1
- package/dist/application.d.ts +1 -1
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +11 -6
- package/dist/browser/socket-io.d.ts +47 -0
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/browser/stream.d.ts +23 -0
- package/dist/browser/stream.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-t8xrqc9g.js → index-413xk7ga.js} +1 -1
- package/dist/{index-82e74yfx.js → index-eabpd4tb.js} +56 -7
- package/dist/{index-2cgbdckv.js → index-s1tywej8.js} +88 -19
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +213 -17
- package/dist/internal/optional-peer.d.ts +14 -0
- package/dist/internal/optional-peer.d.ts.map +1 -0
- package/dist/node.js +1 -1
- package/dist/realtime/contract.d.ts +7 -1
- package/dist/realtime/contract.d.ts.map +1 -1
- package/dist/realtime/index.d.ts +2 -1
- package/dist/realtime/index.d.ts.map +1 -1
- package/dist/realtime/rejected-frame.d.ts +86 -0
- package/dist/realtime/rejected-frame.d.ts.map +1 -0
- package/dist/realtime/request.d.ts +24 -0
- package/dist/realtime/request.d.ts.map +1 -1
- package/dist/realtime/socket.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 +196 -3
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/streaming-route.d.ts +130 -0
- package/dist/server/streaming-route.d.ts.map +1 -0
- package/dist/testing.js +1 -1
- package/llms-full.txt +364 -23
- package/package.json +2 -2
package/dist/server/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
parseMultipart,
|
|
8
8
|
socketIoLane,
|
|
9
9
|
webSocketLane
|
|
10
|
-
} from "../index-
|
|
10
|
+
} from "../index-s1tywej8.js";
|
|
11
11
|
import {
|
|
12
12
|
composeAuthHooks,
|
|
13
13
|
createAuthHook,
|
|
@@ -973,9 +973,11 @@ function streamSSE(generator) {
|
|
|
973
973
|
controller.close();
|
|
974
974
|
}
|
|
975
975
|
},
|
|
976
|
-
|
|
976
|
+
cancel() {
|
|
977
977
|
cancelled = true;
|
|
978
|
-
|
|
978
|
+
generator.return(undefined).catch(() => {
|
|
979
|
+
return;
|
|
980
|
+
});
|
|
979
981
|
}
|
|
980
982
|
});
|
|
981
983
|
return new Response(stream, {
|
|
@@ -1019,10 +1021,198 @@ async function* parseSSE(response, options) {
|
|
|
1019
1021
|
reader.releaseLock();
|
|
1020
1022
|
}
|
|
1021
1023
|
}
|
|
1024
|
+
// src/server/streaming-route.ts
|
|
1025
|
+
var DEFAULT_STREAM_HEARTBEAT_MS = 5000;
|
|
1026
|
+
var FRAMINGS = {
|
|
1027
|
+
ndjson: {
|
|
1028
|
+
contentType: "application/x-ndjson",
|
|
1029
|
+
keepAlive: `
|
|
1030
|
+
`,
|
|
1031
|
+
frame: (value) => `${JSON.stringify(value)}
|
|
1032
|
+
`
|
|
1033
|
+
},
|
|
1034
|
+
sse: {
|
|
1035
|
+
contentType: "text/event-stream",
|
|
1036
|
+
keepAlive: `: keep-alive
|
|
1037
|
+
|
|
1038
|
+
`,
|
|
1039
|
+
frame: (value) => `data: ${JSON.stringify(value)}
|
|
1040
|
+
|
|
1041
|
+
`,
|
|
1042
|
+
done: `data: [DONE]
|
|
1043
|
+
|
|
1044
|
+
`
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
function applyIdleTimeout(server, request, seconds) {
|
|
1048
|
+
if (typeof server !== "object" || server === null)
|
|
1049
|
+
return;
|
|
1050
|
+
const timeout = Reflect.get(server, "timeout");
|
|
1051
|
+
if (typeof timeout !== "function")
|
|
1052
|
+
return;
|
|
1053
|
+
try {
|
|
1054
|
+
Reflect.apply(timeout, server, [request, seconds]);
|
|
1055
|
+
} catch {}
|
|
1056
|
+
}
|
|
1057
|
+
function unrefTimer(timer) {
|
|
1058
|
+
if (typeof timer !== "object" || timer === null)
|
|
1059
|
+
return;
|
|
1060
|
+
const unref = Reflect.get(timer, "unref");
|
|
1061
|
+
if (typeof unref === "function")
|
|
1062
|
+
Reflect.apply(unref, timer, []);
|
|
1063
|
+
}
|
|
1064
|
+
var MAX_BUFFERED_FRAMES = 16;
|
|
1065
|
+
function responseHeaders(framing, extra) {
|
|
1066
|
+
const headers = new Headers(extra);
|
|
1067
|
+
headers.set("Content-Type", framing.contentType);
|
|
1068
|
+
headers.set("Cache-Control", "no-cache");
|
|
1069
|
+
headers.set("Connection", "keep-alive");
|
|
1070
|
+
headers.set("X-Accel-Buffering", "no");
|
|
1071
|
+
return headers;
|
|
1072
|
+
}
|
|
1073
|
+
function streamingRoute(options) {
|
|
1074
|
+
const heartbeatMs = options.heartbeatMs ?? DEFAULT_STREAM_HEARTBEAT_MS;
|
|
1075
|
+
if (!Number.isFinite(heartbeatMs) || heartbeatMs <= 0) {
|
|
1076
|
+
throw new TypeError("heartbeatMs must be a finite positive number of milliseconds");
|
|
1077
|
+
}
|
|
1078
|
+
const idleTimeoutSeconds = options.idleTimeoutSeconds ?? 0;
|
|
1079
|
+
if (!Number.isInteger(idleTimeoutSeconds) || idleTimeoutSeconds < 0) {
|
|
1080
|
+
throw new TypeError("idleTimeoutSeconds must be a non-negative integer");
|
|
1081
|
+
}
|
|
1082
|
+
const framing = FRAMINGS[options.format ?? "ndjson"];
|
|
1083
|
+
return {
|
|
1084
|
+
method: options.method ?? "GET",
|
|
1085
|
+
path: options.path,
|
|
1086
|
+
handler: async (request, context) => {
|
|
1087
|
+
applyIdleTimeout(context.server, request, idleTimeoutSeconds);
|
|
1088
|
+
const departed = new AbortController;
|
|
1089
|
+
const iterable = await options.source(request, { ...context, signal: departed.signal });
|
|
1090
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
1091
|
+
const encoder = new TextEncoder;
|
|
1092
|
+
let heartbeat = null;
|
|
1093
|
+
let closed = false;
|
|
1094
|
+
const stopHeartbeat = () => {
|
|
1095
|
+
if (heartbeat === null)
|
|
1096
|
+
return;
|
|
1097
|
+
clearInterval(heartbeat);
|
|
1098
|
+
heartbeat = null;
|
|
1099
|
+
};
|
|
1100
|
+
const release = () => {
|
|
1101
|
+
if (closed)
|
|
1102
|
+
return;
|
|
1103
|
+
closed = true;
|
|
1104
|
+
try {
|
|
1105
|
+
stopHeartbeat();
|
|
1106
|
+
signalDemand();
|
|
1107
|
+
departed.abort();
|
|
1108
|
+
Promise.resolve(iterator.return?.(undefined)).catch(() => {
|
|
1109
|
+
return;
|
|
1110
|
+
});
|
|
1111
|
+
} catch {}
|
|
1112
|
+
};
|
|
1113
|
+
let demand = null;
|
|
1114
|
+
const signalDemand = () => {
|
|
1115
|
+
const resolve2 = demand;
|
|
1116
|
+
demand = null;
|
|
1117
|
+
resolve2?.();
|
|
1118
|
+
};
|
|
1119
|
+
const stream = new ReadableStream({
|
|
1120
|
+
start(controller) {
|
|
1121
|
+
const send = (text) => {
|
|
1122
|
+
if (closed)
|
|
1123
|
+
return false;
|
|
1124
|
+
try {
|
|
1125
|
+
controller.enqueue(encoder.encode(text));
|
|
1126
|
+
return true;
|
|
1127
|
+
} catch {
|
|
1128
|
+
release();
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
const finish = () => {
|
|
1133
|
+
try {
|
|
1134
|
+
controller.close();
|
|
1135
|
+
} catch {}
|
|
1136
|
+
};
|
|
1137
|
+
if (request.signal.aborted) {
|
|
1138
|
+
release();
|
|
1139
|
+
finish();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
request.signal.addEventListener("abort", () => {
|
|
1143
|
+
release();
|
|
1144
|
+
finish();
|
|
1145
|
+
});
|
|
1146
|
+
send(framing.keepAlive);
|
|
1147
|
+
heartbeat = setInterval(() => {
|
|
1148
|
+
if ((controller.desiredSize ?? 1) > 0)
|
|
1149
|
+
send(framing.keepAlive);
|
|
1150
|
+
}, heartbeatMs);
|
|
1151
|
+
unrefTimer(heartbeat);
|
|
1152
|
+
const awaitDemand = async () => {
|
|
1153
|
+
while (!closed && (controller.desiredSize ?? 1) <= 0) {
|
|
1154
|
+
await new Promise((resolve2) => {
|
|
1155
|
+
demand = resolve2;
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
let sinceYield = 0;
|
|
1160
|
+
(async () => {
|
|
1161
|
+
try {
|
|
1162
|
+
for (;; ) {
|
|
1163
|
+
await awaitDemand();
|
|
1164
|
+
if (closed)
|
|
1165
|
+
return;
|
|
1166
|
+
const next = await iterator.next();
|
|
1167
|
+
if (closed)
|
|
1168
|
+
return;
|
|
1169
|
+
if (next.done)
|
|
1170
|
+
break;
|
|
1171
|
+
if (!send(framing.frame(next.value)))
|
|
1172
|
+
return;
|
|
1173
|
+
sinceYield += 1;
|
|
1174
|
+
if (sinceYield >= MAX_BUFFERED_FRAMES) {
|
|
1175
|
+
sinceYield = 0;
|
|
1176
|
+
await new Promise((resolve2) => {
|
|
1177
|
+
setTimeout(resolve2, 0);
|
|
1178
|
+
});
|
|
1179
|
+
if (closed)
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (framing.done)
|
|
1184
|
+
send(framing.done);
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
if (!closed)
|
|
1187
|
+
send(framing.frame(normalizeError(error).toJSON()));
|
|
1188
|
+
} finally {
|
|
1189
|
+
release();
|
|
1190
|
+
finish();
|
|
1191
|
+
}
|
|
1192
|
+
})();
|
|
1193
|
+
},
|
|
1194
|
+
pull() {
|
|
1195
|
+
signalDemand();
|
|
1196
|
+
},
|
|
1197
|
+
cancel() {
|
|
1198
|
+
release();
|
|
1199
|
+
}
|
|
1200
|
+
}, { highWaterMark: MAX_BUFFERED_FRAMES });
|
|
1201
|
+
return new Response(stream, { headers: responseHeaders(framing, options.headers) });
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function ndjsonRoute(options) {
|
|
1206
|
+
return streamingRoute({ ...options, format: "ndjson" });
|
|
1207
|
+
}
|
|
1208
|
+
function sseRoute(options) {
|
|
1209
|
+
return streamingRoute({ ...options, format: "sse" });
|
|
1210
|
+
}
|
|
1022
1211
|
export {
|
|
1023
1212
|
AppError,
|
|
1024
1213
|
DEFAULT_CORS_ALLOW_HEADERS,
|
|
1025
1214
|
DEFAULT_CORS_EXPOSE_HEADERS,
|
|
1215
|
+
DEFAULT_STREAM_HEARTBEAT_MS,
|
|
1026
1216
|
STITCH_ERROR_STATUS,
|
|
1027
1217
|
ShutdownOptionsSchema,
|
|
1028
1218
|
ShutdownResultSchema,
|
|
@@ -1069,6 +1259,7 @@ export {
|
|
|
1069
1259
|
implementRegistry,
|
|
1070
1260
|
isStitchErrorCode,
|
|
1071
1261
|
isWithinDir,
|
|
1262
|
+
ndjsonRoute,
|
|
1072
1263
|
normalizeError,
|
|
1073
1264
|
notFound,
|
|
1074
1265
|
openApiRoute,
|
|
@@ -1085,8 +1276,10 @@ export {
|
|
|
1085
1276
|
serveFile,
|
|
1086
1277
|
signJwt,
|
|
1087
1278
|
socketIoLane,
|
|
1279
|
+
sseRoute,
|
|
1088
1280
|
staticRoute,
|
|
1089
1281
|
streamSSE,
|
|
1282
|
+
streamingRoute,
|
|
1090
1283
|
unauthorized,
|
|
1091
1284
|
verifyJwt,
|
|
1092
1285
|
verifyPkce,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket-io.d.ts","sourceRoot":"","sources":["../../src/server/socket-io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EACV,MAAM,IAAI,SAAS,EAEpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EACV,gBAAgB,EAChB,MAAM,IAAI,cAAc,EAGzB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"socket-io.d.ts","sourceRoot":"","sources":["../../src/server/socket-io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EACV,MAAM,IAAI,SAAS,EAEpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EACV,gBAAgB,EAChB,MAAM,IAAI,cAAc,EAGzB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAA2B,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACxF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,KAAK,YAAY,EAAiB,MAAM,aAAa,CAAC;AAc/D,YAAY,EACV,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,EAAE,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACzD;;;;;OAKG;IACH,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC3B;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,6EAA6E;IAC7E,aAAa,IAAI,IAAI,CAAC;IACtB,8EAA8E;IAC9E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,sEAAsE;IACtE,WAAW,IAAI,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB,CACnC,aAAa,SAAS,cAAc,EACpC,aAAa,SAAS,cAAc,EACpC,KAAK,GAAG,GAAG,CACX,SAAQ,uBAAuB;IAC/B,+EAA+E;IAC/E,EAAE,EAAE,cAAc,CAAC,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,KAAK,CAAC,CAAC;CAC3E;AA8HD,wBAAsB,oBAAoB,CACxC,aAAa,SAAS,cAAc,EACpC,aAAa,SAAS,cAAc,EAKpC,OAAO,GAAG,GAAG,EACb,KAAK,GAAG,OAAO,EAEf,MAAM,EAAE,oBAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,GAC3C,OAAO,CAAC,oBAAoB,CAAC,aAAa,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAmKpE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,oBAAoB,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,WAAW,CAAC,GAC3E,YAAY,CAMd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/server/stream.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,QAAQ,
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/server/stream.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,QAAQ,CA2CtE;AAED,8BAA8B;AAC9B,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACpD;AAED;;;;GAIG;AACH,wBAAuB,QAAQ,CAAC,CAAC,EAC/B,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,eAAe,GACxB,cAAc,CAAC,CAAC,CAAC,CAmCnB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A long-lived response, with the three things everyone has to remember.
|
|
3
|
+
*
|
|
4
|
+
* `RawRoute` + `ctx.server` already gave every capability needed to serve a
|
|
5
|
+
* continuing NDJSON or SSE body. The problem was never capability — it was that
|
|
6
|
+
* the author of each such route had to independently remember three unrelated
|
|
7
|
+
* things, and forgetting any one of them breaks the stream **silently**:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Clear the generic HTTP idle timeout.** Without `server.timeout(req, 0)`
|
|
10
|
+
* Bun resets the connection after ten seconds. For a stream whose normal
|
|
11
|
+
* state is silence — a subscription to a rare event, the log of an idle
|
|
12
|
+
* process — that is a healthy connection being severed on a schedule.
|
|
13
|
+
* 2. **Send a heartbeat.** Even with the timeout cleared, intermediate proxies
|
|
14
|
+
* and client stacks are under no obligation to hold a connection carrying no
|
|
15
|
+
* bytes.
|
|
16
|
+
* 3. **Flush the headers at open.** A runtime does not send the response until
|
|
17
|
+
* the body produces something, so the consumer's `fetch` does not return
|
|
18
|
+
* until the first frame. On a quiet stream "subscribed and silent" becomes
|
|
19
|
+
* indistinguishable from "not answering" — and there is nothing to inspect,
|
|
20
|
+
* because there is no response yet.
|
|
21
|
+
*
|
|
22
|
+
* Each is obvious alone. Together they are a checklist that lived in the head
|
|
23
|
+
* of whoever wrote the route rather than in the types — and a route written
|
|
24
|
+
* later, doing the same job for another plane, got none of the three. Reviews
|
|
25
|
+
* did not catch it and neither did the tests, because every test published an
|
|
26
|
+
* event immediately and never lived long enough to reach the threshold. The
|
|
27
|
+
* shape of the defect is the point: not "done wrong", but "done incompletely,
|
|
28
|
+
* and the incompleteness is invisible".
|
|
29
|
+
*
|
|
30
|
+
* They are not independent, and the measurement is worth keeping: with a
|
|
31
|
+
* heartbeat under the threshold, point 1 does not change the outcome in
|
|
32
|
+
* process — either measure alone kept a connection alive through twelve
|
|
33
|
+
* seconds of silence, and only dropping both killed it. Point 1 earns its place
|
|
34
|
+
* against what a heartbeat cannot reach: a proxy or a client stack applying its
|
|
35
|
+
* own idle rule. Points 2 and 3 are each load-bearing on their own.
|
|
36
|
+
*
|
|
37
|
+
* A fourth thing is handled here that is easy to not even think of:
|
|
38
|
+
* **cancellation reaches the source.** When the consumer goes away, the async
|
|
39
|
+
* iterable is returned, so a departed subscriber does not leave live work
|
|
40
|
+
* running on the server.
|
|
41
|
+
*/
|
|
42
|
+
import type { HttpMethod } from '../contract/define';
|
|
43
|
+
import type { RawRoute, RawRouteContext } from './types';
|
|
44
|
+
/** How a value becomes bytes on the wire. The only thing the two formats differ in. */
|
|
45
|
+
export type StreamingFormat = 'ndjson' | 'sse';
|
|
46
|
+
/**
|
|
47
|
+
* Default heartbeat, deliberately well under Bun's ten-second idle threshold.
|
|
48
|
+
*
|
|
49
|
+
* A default at or near the threshold does not protect anything: the first
|
|
50
|
+
* pulse would arrive at about the moment the connection was already being
|
|
51
|
+
* dropped, so the option would look configured and do nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DEFAULT_STREAM_HEARTBEAT_MS = 5000;
|
|
54
|
+
export interface StreamingRouteOptions<TServer = unknown> {
|
|
55
|
+
path: string;
|
|
56
|
+
/** Default `GET` — a subscription is a read. */
|
|
57
|
+
method?: HttpMethod | 'ALL';
|
|
58
|
+
/** Wire framing. Default `ndjson`. */
|
|
59
|
+
format?: StreamingFormat;
|
|
60
|
+
/**
|
|
61
|
+
* Interval between keep-alive frames while the source is silent. Default
|
|
62
|
+
* `DEFAULT_STREAM_HEARTBEAT_MS`. Must be finite and positive.
|
|
63
|
+
*/
|
|
64
|
+
heartbeatMs?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Per-request idle timeout in **seconds**, applied through the runtime's own
|
|
67
|
+
* per-request control where it has one (Bun). Default `0` — no timeout.
|
|
68
|
+
*
|
|
69
|
+
* Zero by default and not by accident. The generic timeout exists to reap
|
|
70
|
+
* connections whose peer has gone quiet, and a stream whose normal state is
|
|
71
|
+
* silence is the one case where that inference is wrong — which is the reason
|
|
72
|
+
* this primitive is being reached for at all. It is still a field, because
|
|
73
|
+
* removing a general limit is a decision worth being able to see and to
|
|
74
|
+
* override.
|
|
75
|
+
*/
|
|
76
|
+
idleTimeoutSeconds?: number;
|
|
77
|
+
/**
|
|
78
|
+
* The frames. Returned as an async iterable so a generator is the natural
|
|
79
|
+
* way to write one.
|
|
80
|
+
*
|
|
81
|
+
* **A source that waits must honour `context.signal`.** See
|
|
82
|
+
* `StreamingSourceContext` — this is the one part of cancellation the
|
|
83
|
+
* primitive cannot do on the source's behalf.
|
|
84
|
+
*/
|
|
85
|
+
source: (request: Request, context: StreamingSourceContext<TServer>) => AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>;
|
|
86
|
+
/** Extra response headers. The framing headers below cannot be overridden. */
|
|
87
|
+
headers?: Record<string, string>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* What a source is given, and the reason the signal is not optional.
|
|
91
|
+
*
|
|
92
|
+
* Closing an async iterator is not enough to stop a source, and the mechanism
|
|
93
|
+
* is worth stating because the failure is silent. An async generator serialises
|
|
94
|
+
* its requests: `return()` issued while a `next()` is still in flight is
|
|
95
|
+
* QUEUED behind it. A subscription source's `next()` is in flight almost all
|
|
96
|
+
* the time — that is what a subscription is — so the return is queued behind a
|
|
97
|
+
* promise that will only settle when the next event arrives, which for a quiet
|
|
98
|
+
* plane may be never. `iterator.return()` is still called, because it does
|
|
99
|
+
* close a source suspended at a `yield`; it simply cannot interrupt one that is
|
|
100
|
+
* waiting.
|
|
101
|
+
*
|
|
102
|
+
* The signal is what can. It is aborted the moment the consumer goes away —
|
|
103
|
+
* through either route, a request abort or a stream cancel, which are not the
|
|
104
|
+
* same event — and a source that awaits on it stops immediately:
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* ndjsonRoute({
|
|
108
|
+
* path: '/events/subscribe',
|
|
109
|
+
* source: async function* (request, { signal }) {
|
|
110
|
+
* for await (const event of subscribe({ signal })) yield event
|
|
111
|
+
* },
|
|
112
|
+
* })
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export interface StreamingSourceContext<TServer = unknown> extends RawRouteContext<TServer> {
|
|
116
|
+
/** Aborted when the consumer goes away. A waiting source must honour it. */
|
|
117
|
+
signal: AbortSignal;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A `RawRoute` serving a continuing body.
|
|
121
|
+
*
|
|
122
|
+
* @see ndjsonRoute
|
|
123
|
+
* @see sseRoute
|
|
124
|
+
*/
|
|
125
|
+
export declare function streamingRoute<TServer = unknown>(options: StreamingRouteOptions<TServer>): RawRoute<TServer>;
|
|
126
|
+
/** A `streamingRoute` framed as newline-delimited JSON. Keep-alive is a blank line. */
|
|
127
|
+
export declare function ndjsonRoute<TServer = unknown>(options: Omit<StreamingRouteOptions<TServer>, 'format'>): RawRoute<TServer>;
|
|
128
|
+
/** A `streamingRoute` framed as Server-Sent Events — readable by `parseSSE`. */
|
|
129
|
+
export declare function sseRoute<TServer = unknown>(options: Omit<StreamingRouteOptions<TServer>, 'format'>): RawRoute<TServer>;
|
|
130
|
+
//# sourceMappingURL=streaming-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-route.d.ts","sourceRoot":"","sources":["../../src/server/streaming-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEzD,uFAAuF;AACvF,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AAEjD,MAAM,WAAW,qBAAqB,CAAC,OAAO,GAAG,OAAO;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,MAAM,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC;IAC5B,sCAAsC;IACtC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,MAAM,EAAE,CACN,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,sBAAsB,CAAC,OAAO,CAAC,KACrC,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9D,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,sBAAsB,CAAC,OAAO,GAAG,OAAO,CAAE,SAAQ,eAAe,CAAC,OAAO,CAAC;IACzF,4EAA4E;IAC5E,MAAM,EAAE,WAAW,CAAC;CACrB;AAiGD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAG,OAAO,EAC9C,OAAO,EAAE,qBAAqB,CAAC,OAAO,CAAC,GACtC,QAAQ,CAAC,OAAO,CAAC,CAgNnB;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,OAAO,GAAG,OAAO,EAC3C,OAAO,EAAE,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,GACtD,QAAQ,CAAC,OAAO,CAAC,CAEnB;AAED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,OAAO,GAAG,OAAO,EACxC,OAAO,EAAE,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,GACtD,QAAQ,CAAC,OAAO,CAAC,CAEnB"}
|