@flightdev/core 0.6.7
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 +21 -0
- package/README.md +541 -0
- package/dist/actions/index.d.ts +743 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/adapters/index.d.ts +502 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/cache/index.d.ts +191 -0
- package/dist/cache/index.js +3 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/chunk-62HISNA3.js +354 -0
- package/dist/chunk-62HISNA3.js.map +1 -0
- package/dist/chunk-63LWTEDQ.js +341 -0
- package/dist/chunk-63LWTEDQ.js.map +1 -0
- package/dist/chunk-63SCEXD7.js +3 -0
- package/dist/chunk-63SCEXD7.js.map +1 -0
- package/dist/chunk-72MYOTUB.js +667 -0
- package/dist/chunk-72MYOTUB.js.map +1 -0
- package/dist/chunk-7CNW24MQ.js +257 -0
- package/dist/chunk-7CNW24MQ.js.map +1 -0
- package/dist/chunk-7WIEAUJT.js +300 -0
- package/dist/chunk-7WIEAUJT.js.map +1 -0
- package/dist/chunk-7ZZF4ULK.js +259 -0
- package/dist/chunk-7ZZF4ULK.js.map +1 -0
- package/dist/chunk-AE3JTS73.js +222 -0
- package/dist/chunk-AE3JTS73.js.map +1 -0
- package/dist/chunk-AP5NLUSB.js +258 -0
- package/dist/chunk-AP5NLUSB.js.map +1 -0
- package/dist/chunk-C37YQQI7.js +221 -0
- package/dist/chunk-C37YQQI7.js.map +1 -0
- package/dist/chunk-DCLVXFVH.js +225 -0
- package/dist/chunk-DCLVXFVH.js.map +1 -0
- package/dist/chunk-DZMWWDFD.js +223 -0
- package/dist/chunk-DZMWWDFD.js.map +1 -0
- package/dist/chunk-GCQZ4FHI.js +245 -0
- package/dist/chunk-GCQZ4FHI.js.map +1 -0
- package/dist/chunk-IPP44XY6.js +47 -0
- package/dist/chunk-IPP44XY6.js.map +1 -0
- package/dist/chunk-IW7FTQQX.js +267 -0
- package/dist/chunk-IW7FTQQX.js.map +1 -0
- package/dist/chunk-JX4YSCBH.js +428 -0
- package/dist/chunk-JX4YSCBH.js.map +1 -0
- package/dist/chunk-KX6UYWWR.js +229 -0
- package/dist/chunk-KX6UYWWR.js.map +1 -0
- package/dist/chunk-LWVETFJV.js +46 -0
- package/dist/chunk-LWVETFJV.js.map +1 -0
- package/dist/chunk-MCL2MCA2.js +285 -0
- package/dist/chunk-MCL2MCA2.js.map +1 -0
- package/dist/chunk-MZXCF35B.js +205 -0
- package/dist/chunk-MZXCF35B.js.map +1 -0
- package/dist/chunk-NCGPUFWV.js +96 -0
- package/dist/chunk-NCGPUFWV.js.map +1 -0
- package/dist/chunk-OEJMIE2Q.js +351 -0
- package/dist/chunk-OEJMIE2Q.js.map +1 -0
- package/dist/chunk-OYF2OAKS.js +394 -0
- package/dist/chunk-OYF2OAKS.js.map +1 -0
- package/dist/chunk-P6S43FYZ.js +316 -0
- package/dist/chunk-P6S43FYZ.js.map +1 -0
- package/dist/chunk-PL37KFRJ.js +3 -0
- package/dist/chunk-PL37KFRJ.js.map +1 -0
- package/dist/chunk-Q7BS5QC5.js +197 -0
- package/dist/chunk-Q7BS5QC5.js.map +1 -0
- package/dist/chunk-SDYPG3JD.js +288 -0
- package/dist/chunk-SDYPG3JD.js.map +1 -0
- package/dist/chunk-SUG56SZO.js +256 -0
- package/dist/chunk-SUG56SZO.js.map +1 -0
- package/dist/chunk-UVH5XJRP.js +164 -0
- package/dist/chunk-UVH5XJRP.js.map +1 -0
- package/dist/chunk-WZIJKCL3.js +282 -0
- package/dist/chunk-WZIJKCL3.js.map +1 -0
- package/dist/chunk-Y22AMGTM.js +3 -0
- package/dist/chunk-Y22AMGTM.js.map +1 -0
- package/dist/chunk-Z7G23XWU.js +200 -0
- package/dist/chunk-Z7G23XWU.js.map +1 -0
- package/dist/chunk-ZJU5M4IB.js +125 -0
- package/dist/chunk-ZJU5M4IB.js.map +1 -0
- package/dist/chunk-ZVC3ZWLM.js +52 -0
- package/dist/chunk-ZVC3ZWLM.js.map +1 -0
- package/dist/chunk-ZZZML7Y3.js +310 -0
- package/dist/chunk-ZZZML7Y3.js.map +1 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +16 -0
- package/dist/client.js.map +1 -0
- package/dist/config/index.d.ts +170 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/errors/index.d.ts +267 -0
- package/dist/errors/index.js +4 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/file-router/index.d.ts +184 -0
- package/dist/file-router/index.js +3 -0
- package/dist/file-router/index.js.map +1 -0
- package/dist/file-router/streaming-hints.d.ts +129 -0
- package/dist/file-router/streaming-hints.js +3 -0
- package/dist/file-router/streaming-hints.js.map +1 -0
- package/dist/handlers/index.d.ts +59 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/index.d.ts +588 -0
- package/dist/index.js +886 -0
- package/dist/index.js.map +1 -0
- package/dist/islands/index.d.ts +234 -0
- package/dist/islands/index.js +3 -0
- package/dist/islands/index.js.map +1 -0
- package/dist/middleware/index.d.ts +305 -0
- package/dist/middleware/index.js +3 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/react/index.d.ts +73 -0
- package/dist/react/index.js +52 -0
- package/dist/react/index.js.map +1 -0
- package/dist/render/index.d.ts +131 -0
- package/dist/render/index.js +3 -0
- package/dist/render/index.js.map +1 -0
- package/dist/router/index.d.ts +65 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/rsc/adapters/index.d.ts +8 -0
- package/dist/rsc/adapters/index.js +7 -0
- package/dist/rsc/adapters/index.js.map +1 -0
- package/dist/rsc/adapters/preact.d.ts +97 -0
- package/dist/rsc/adapters/preact.js +3 -0
- package/dist/rsc/adapters/preact.js.map +1 -0
- package/dist/rsc/adapters/react.d.ts +82 -0
- package/dist/rsc/adapters/react.js +3 -0
- package/dist/rsc/adapters/react.js.map +1 -0
- package/dist/rsc/adapters/solid.d.ts +84 -0
- package/dist/rsc/adapters/solid.js +3 -0
- package/dist/rsc/adapters/solid.js.map +1 -0
- package/dist/rsc/adapters/vue.d.ts +80 -0
- package/dist/rsc/adapters/vue.js +3 -0
- package/dist/rsc/adapters/vue.js.map +1 -0
- package/dist/rsc/boundaries.d.ts +182 -0
- package/dist/rsc/boundaries.js +3 -0
- package/dist/rsc/boundaries.js.map +1 -0
- package/dist/rsc/context.d.ts +201 -0
- package/dist/rsc/context.js +3 -0
- package/dist/rsc/context.js.map +1 -0
- package/dist/rsc/index.d.ts +232 -0
- package/dist/rsc/index.js +15 -0
- package/dist/rsc/index.js.map +1 -0
- package/dist/rsc/legacy.d.ts +155 -0
- package/dist/rsc/legacy.js +3 -0
- package/dist/rsc/legacy.js.map +1 -0
- package/dist/rsc/payload.d.ts +262 -0
- package/dist/rsc/payload.js +3 -0
- package/dist/rsc/payload.js.map +1 -0
- package/dist/rsc/plugins/esbuild.d.ts +124 -0
- package/dist/rsc/plugins/esbuild.js +4 -0
- package/dist/rsc/plugins/esbuild.js.map +1 -0
- package/dist/rsc/plugins/index.d.ts +4 -0
- package/dist/rsc/plugins/index.js +6 -0
- package/dist/rsc/plugins/index.js.map +1 -0
- package/dist/rsc/plugins/rollup.d.ts +103 -0
- package/dist/rsc/plugins/rollup.js +4 -0
- package/dist/rsc/plugins/rollup.js.map +1 -0
- package/dist/rsc/renderer.d.ts +162 -0
- package/dist/rsc/renderer.js +5 -0
- package/dist/rsc/renderer.js.map +1 -0
- package/dist/rsc/stream.d.ts +129 -0
- package/dist/rsc/stream.js +3 -0
- package/dist/rsc/stream.js.map +1 -0
- package/dist/rsc/vite-plugin.d.ts +78 -0
- package/dist/rsc/vite-plugin.js +4 -0
- package/dist/rsc/vite-plugin.js.map +1 -0
- package/dist/server/index.d.ts +135 -0
- package/dist/server/index.js +6 -0
- package/dist/server/index.js.map +1 -0
- package/dist/streaming/adapters/index.d.ts +223 -0
- package/dist/streaming/adapters/index.js +3 -0
- package/dist/streaming/adapters/index.js.map +1 -0
- package/dist/streaming/conditional.d.ts +130 -0
- package/dist/streaming/conditional.js +3 -0
- package/dist/streaming/conditional.js.map +1 -0
- package/dist/streaming/index.d.ts +177 -0
- package/dist/streaming/index.js +3 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/streaming/observability.d.ts +201 -0
- package/dist/streaming/observability.js +4 -0
- package/dist/streaming/observability.js.map +1 -0
- package/dist/streaming/priority.d.ts +103 -0
- package/dist/streaming/priority.js +3 -0
- package/dist/streaming/priority.js.map +1 -0
- package/dist/utils/index.d.ts +42 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +228 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/streaming/conditional.ts
|
|
2
|
+
var DEFAULT_BOT_PATTERNS = [
|
|
3
|
+
/googlebot/i,
|
|
4
|
+
/bingbot/i,
|
|
5
|
+
/slurp/i,
|
|
6
|
+
// Yahoo
|
|
7
|
+
/duckduckbot/i,
|
|
8
|
+
/baiduspider/i,
|
|
9
|
+
/yandexbot/i,
|
|
10
|
+
/sogou/i,
|
|
11
|
+
/facebot/i,
|
|
12
|
+
// Facebook
|
|
13
|
+
/twitterbot/i,
|
|
14
|
+
/linkedinbot/i,
|
|
15
|
+
/whatsapp/i,
|
|
16
|
+
/telegrambot/i,
|
|
17
|
+
/discordbot/i,
|
|
18
|
+
/slackbot/i,
|
|
19
|
+
/applebot/i,
|
|
20
|
+
/crawler/i,
|
|
21
|
+
/spider/i,
|
|
22
|
+
/bot\//i,
|
|
23
|
+
/bot;/i,
|
|
24
|
+
/headless/i,
|
|
25
|
+
// Headless browsers
|
|
26
|
+
/lighthouse/i,
|
|
27
|
+
// Google Lighthouse
|
|
28
|
+
/pagespeed/i,
|
|
29
|
+
/gtmetrix/i,
|
|
30
|
+
/pingdom/i,
|
|
31
|
+
/uptimerobot/i
|
|
32
|
+
];
|
|
33
|
+
function isBot(request, customPatterns) {
|
|
34
|
+
const userAgent = request.headers.get("user-agent") || "";
|
|
35
|
+
const patterns = customPatterns || DEFAULT_BOT_PATTERNS;
|
|
36
|
+
return patterns.some((pattern) => pattern.test(userAgent));
|
|
37
|
+
}
|
|
38
|
+
function prefersNoStream(request) {
|
|
39
|
+
if (request.headers.get("x-no-stream") === "true") return true;
|
|
40
|
+
if (request.headers.get("x-prefer-static") === "true") return true;
|
|
41
|
+
const accept = request.headers.get("accept") || "";
|
|
42
|
+
if (accept.includes("text/html; streaming=false")) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function isSlowConnection(request) {
|
|
46
|
+
const ect = request.headers.get("ect");
|
|
47
|
+
if (ect && ["slow-2g", "2g"].includes(ect)) return true;
|
|
48
|
+
if (request.headers.get("save-data") === "on") return true;
|
|
49
|
+
const downlink = request.headers.get("downlink");
|
|
50
|
+
if (downlink && parseFloat(downlink) < 1) return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function supportsStreaming(request) {
|
|
54
|
+
const userAgent = request.headers.get("user-agent") || "";
|
|
55
|
+
if (/MSIE|Trident/i.test(userAgent)) return false;
|
|
56
|
+
if (/Opera Mini/i.test(userAgent)) return false;
|
|
57
|
+
const secChUa = request.headers.get("sec-ch-ua");
|
|
58
|
+
if (secChUa) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function getBuiltInCondition(condition, botPatterns) {
|
|
64
|
+
switch (condition) {
|
|
65
|
+
case "always-stream":
|
|
66
|
+
return () => true;
|
|
67
|
+
case "never-stream":
|
|
68
|
+
return () => false;
|
|
69
|
+
case "no-bots":
|
|
70
|
+
return (req) => !isBot(req, botPatterns) && !prefersNoStream(req);
|
|
71
|
+
case "fast-network":
|
|
72
|
+
return (req) => !isSlowConnection(req) && !isBot(req, botPatterns);
|
|
73
|
+
case "modern-browser":
|
|
74
|
+
return (req) => supportsStreaming(req) && !isBot(req, botPatterns);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function streamIf(config) {
|
|
78
|
+
const {
|
|
79
|
+
request,
|
|
80
|
+
condition,
|
|
81
|
+
stream,
|
|
82
|
+
static: staticFn,
|
|
83
|
+
botPatterns,
|
|
84
|
+
forceStreamParam = "__stream",
|
|
85
|
+
forceStaticParam = "__static"
|
|
86
|
+
} = config;
|
|
87
|
+
const url = new URL(request.url);
|
|
88
|
+
if (url.searchParams.has(forceStreamParam)) {
|
|
89
|
+
const result2 = await stream();
|
|
90
|
+
return { result: result2, streaming: true, reason: "forced via query param" };
|
|
91
|
+
}
|
|
92
|
+
if (url.searchParams.has(forceStaticParam)) {
|
|
93
|
+
const result2 = await staticFn();
|
|
94
|
+
return { result: result2, streaming: false, reason: "forced via query param" };
|
|
95
|
+
}
|
|
96
|
+
const conditionFn = typeof condition === "function" ? condition : getBuiltInCondition(condition, botPatterns);
|
|
97
|
+
let shouldStream;
|
|
98
|
+
let reason;
|
|
99
|
+
try {
|
|
100
|
+
shouldStream = await conditionFn(request);
|
|
101
|
+
reason = shouldStream ? "condition passed" : "condition failed";
|
|
102
|
+
} catch (error) {
|
|
103
|
+
shouldStream = false;
|
|
104
|
+
reason = `condition error: ${error instanceof Error ? error.message : "unknown"}`;
|
|
105
|
+
}
|
|
106
|
+
if (typeof condition === "string") {
|
|
107
|
+
if (!shouldStream && condition === "no-bots") {
|
|
108
|
+
reason = isBot(request, botPatterns) ? "bot detected" : "user prefers no streaming";
|
|
109
|
+
} else if (!shouldStream && condition === "fast-network") {
|
|
110
|
+
reason = isSlowConnection(request) ? "slow connection detected" : "bot detected";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const result = shouldStream ? await stream() : await staticFn();
|
|
114
|
+
return { result, streaming: shouldStream, reason };
|
|
115
|
+
}
|
|
116
|
+
function createConditionalStreamer(defaultCondition, options) {
|
|
117
|
+
return async (config) => {
|
|
118
|
+
return streamIf({
|
|
119
|
+
request: config.request,
|
|
120
|
+
condition: config.condition || defaultCondition,
|
|
121
|
+
stream: config.stream,
|
|
122
|
+
static: config.static,
|
|
123
|
+
...options
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function addStreamingHeaders(response, decision) {
|
|
128
|
+
const headers = new Headers(response.headers);
|
|
129
|
+
headers.set("X-Flight-Streaming", decision.streaming ? "true" : "false");
|
|
130
|
+
headers.set("X-Flight-Streaming-Reason", decision.reason);
|
|
131
|
+
return new Response(response.body, {
|
|
132
|
+
status: response.status,
|
|
133
|
+
statusText: response.statusText,
|
|
134
|
+
headers
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function createStreamingResponse(stream, options) {
|
|
138
|
+
const { status = 200, headers = {}, isStreaming = true } = options || {};
|
|
139
|
+
return new Response(stream, {
|
|
140
|
+
status,
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
143
|
+
"Transfer-Encoding": isStreaming ? "chunked" : void 0,
|
|
144
|
+
"X-Content-Type-Options": "nosniff",
|
|
145
|
+
"X-Flight-Streaming": isStreaming ? "true" : "false",
|
|
146
|
+
...headers
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function createStaticResponse(html, options) {
|
|
151
|
+
const { status = 200, headers = {} } = options || {};
|
|
152
|
+
return new Response(html, {
|
|
153
|
+
status,
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
156
|
+
"X-Flight-Streaming": "false",
|
|
157
|
+
...headers
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { DEFAULT_BOT_PATTERNS, addStreamingHeaders, createConditionalStreamer, createStaticResponse, createStreamingResponse, isBot, isSlowConnection, prefersNoStream, streamIf, supportsStreaming };
|
|
163
|
+
//# sourceMappingURL=chunk-UVH5XJRP.js.map
|
|
164
|
+
//# sourceMappingURL=chunk-UVH5XJRP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/streaming/conditional.ts"],"names":["result"],"mappings":"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","file":"chunk-UVH5XJRP.js","sourcesContent":["/**\r\n * @flightdev/core - Conditional Streaming\r\n * \r\n * Smart streaming decisions based on request context.\r\n * Bots get full HTML, users get streaming - YOU control the logic.\r\n * \r\n * Best Practices 2026:\r\n * - SEO-friendly: Crawlers receive complete HTML\r\n * - Performance: Users get progressive streaming\r\n * - Flexibility: Custom conditions for any use case\r\n * \r\n * @example\r\n * ```typescript\r\n * import { streamIf } from '@flightdev/core/streaming';\r\n * \r\n * const response = await streamIf({\r\n * request,\r\n * condition: (req) => !isBot(req), // YOUR logic\r\n * stream: () => createStreamingSSR({ ... }),\r\n * static: () => renderToString(<App />),\r\n * });\r\n * ```\r\n */\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\n/**\r\n * Condition function to determine if streaming should be used\r\n */\r\nexport type StreamingCondition = (request: Request) => boolean | Promise<boolean>;\r\n\r\n/**\r\n * Built-in condition types\r\n */\r\nexport type BuiltInCondition =\r\n | 'always-stream' // Always use streaming\r\n | 'never-stream' // Always use static\r\n | 'no-bots' // Stream for users, static for bots\r\n | 'fast-network' // Stream only on fast connections\r\n | 'modern-browser'; // Stream only on modern browsers\r\n\r\n/**\r\n * Configuration for conditional streaming\r\n */\r\nexport interface StreamIfConfig<T = Response> {\r\n /** The incoming request */\r\n request: Request;\r\n /** Condition to check (function or built-in) */\r\n condition: StreamingCondition | BuiltInCondition;\r\n /** Factory for streaming response */\r\n stream: () => Promise<T> | T;\r\n /** Factory for static response */\r\n static: () => Promise<T> | T;\r\n /** Optional: Custom bot patterns */\r\n botPatterns?: RegExp[];\r\n /** Optional: Force streaming via query param (for testing) */\r\n forceStreamParam?: string;\r\n /** Optional: Force static via query param (for testing) */\r\n forceStaticParam?: string;\r\n}\r\n\r\n/**\r\n * Result of condition evaluation\r\n */\r\nexport interface StreamingDecision {\r\n /** Whether streaming was used */\r\n streaming: boolean;\r\n /** Reason for the decision */\r\n reason: string;\r\n /** The response */\r\n response: Response;\r\n}\r\n\r\n// ============================================================================\r\n// Bot Detection\r\n// ============================================================================\r\n\r\n/**\r\n * Default bot patterns for detection\r\n */\r\nexport const DEFAULT_BOT_PATTERNS: RegExp[] = [\r\n /googlebot/i,\r\n /bingbot/i,\r\n /slurp/i, // Yahoo\r\n /duckduckbot/i,\r\n /baiduspider/i,\r\n /yandexbot/i,\r\n /sogou/i,\r\n /facebot/i, // Facebook\r\n /twitterbot/i,\r\n /linkedinbot/i,\r\n /whatsapp/i,\r\n /telegrambot/i,\r\n /discordbot/i,\r\n /slackbot/i,\r\n /applebot/i,\r\n /crawler/i,\r\n /spider/i,\r\n /bot\\//i,\r\n /bot;/i,\r\n /headless/i, // Headless browsers\r\n /lighthouse/i, // Google Lighthouse\r\n /pagespeed/i,\r\n /gtmetrix/i,\r\n /pingdom/i,\r\n /uptimerobot/i,\r\n];\r\n\r\n/**\r\n * Check if a request is from a bot/crawler\r\n */\r\nexport function isBot(request: Request, customPatterns?: RegExp[]): boolean {\r\n const userAgent = request.headers.get('user-agent') || '';\r\n const patterns = customPatterns || DEFAULT_BOT_PATTERNS;\r\n return patterns.some(pattern => pattern.test(userAgent));\r\n}\r\n\r\n/**\r\n * Check if request explicitly asks for no streaming\r\n */\r\nexport function prefersNoStream(request: Request): boolean {\r\n // Check for custom header\r\n if (request.headers.get('x-no-stream') === 'true') return true;\r\n if (request.headers.get('x-prefer-static') === 'true') return true;\r\n\r\n // Check Accept header for preference\r\n const accept = request.headers.get('accept') || '';\r\n if (accept.includes('text/html; streaming=false')) return true;\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Check if a connection is likely slow (based on headers)\r\n */\r\nexport function isSlowConnection(request: Request): boolean {\r\n // ECT (Effective Connection Type) header\r\n const ect = request.headers.get('ect');\r\n if (ect && ['slow-2g', '2g'].includes(ect)) return true;\r\n\r\n // Save-Data header\r\n if (request.headers.get('save-data') === 'on') return true;\r\n\r\n // Downlink header (Mbps)\r\n const downlink = request.headers.get('downlink');\r\n if (downlink && parseFloat(downlink) < 1) return true;\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Check if browser supports streaming well\r\n */\r\nexport function supportsStreaming(request: Request): boolean {\r\n const userAgent = request.headers.get('user-agent') || '';\r\n\r\n // IE doesn't support streaming well\r\n if (/MSIE|Trident/i.test(userAgent)) return false;\r\n\r\n // Very old browsers\r\n if (/Opera Mini/i.test(userAgent)) return false;\r\n\r\n // Check for modern browser features via client hints\r\n const secChUa = request.headers.get('sec-ch-ua');\r\n if (secChUa) {\r\n // Has client hints = modern browser\r\n return true;\r\n }\r\n\r\n return true; // Default to supporting streaming\r\n}\r\n\r\n// ============================================================================\r\n// Built-in Conditions\r\n// ============================================================================\r\n\r\nfunction getBuiltInCondition(\r\n condition: BuiltInCondition,\r\n botPatterns?: RegExp[]\r\n): StreamingCondition {\r\n switch (condition) {\r\n case 'always-stream':\r\n return () => true;\r\n case 'never-stream':\r\n return () => false;\r\n case 'no-bots':\r\n return (req) => !isBot(req, botPatterns) && !prefersNoStream(req);\r\n case 'fast-network':\r\n return (req) => !isSlowConnection(req) && !isBot(req, botPatterns);\r\n case 'modern-browser':\r\n return (req) => supportsStreaming(req) && !isBot(req, botPatterns);\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Main Function\r\n// ============================================================================\r\n\r\n/**\r\n * Conditionally use streaming or static rendering based on request context\r\n */\r\nexport async function streamIf<T = Response>(\r\n config: StreamIfConfig<T>\r\n): Promise<{ result: T; streaming: boolean; reason: string }> {\r\n const {\r\n request,\r\n condition,\r\n stream,\r\n static: staticFn,\r\n botPatterns,\r\n forceStreamParam = '__stream',\r\n forceStaticParam = '__static',\r\n } = config;\r\n\r\n // Check for forced mode via query params (useful for testing)\r\n const url = new URL(request.url);\r\n if (url.searchParams.has(forceStreamParam)) {\r\n const result = await stream();\r\n return { result, streaming: true, reason: 'forced via query param' };\r\n }\r\n if (url.searchParams.has(forceStaticParam)) {\r\n const result = await staticFn();\r\n return { result, streaming: false, reason: 'forced via query param' };\r\n }\r\n\r\n // Resolve condition\r\n const conditionFn = typeof condition === 'function'\r\n ? condition\r\n : getBuiltInCondition(condition, botPatterns);\r\n\r\n // Evaluate condition\r\n let shouldStream: boolean;\r\n let reason: string;\r\n\r\n try {\r\n shouldStream = await conditionFn(request);\r\n reason = shouldStream ? 'condition passed' : 'condition failed';\r\n } catch (error) {\r\n // On error, default to static (safer for SEO)\r\n shouldStream = false;\r\n reason = `condition error: ${error instanceof Error ? error.message : 'unknown'}`;\r\n }\r\n\r\n // Add specific reason for built-in conditions\r\n if (typeof condition === 'string') {\r\n if (!shouldStream && condition === 'no-bots') {\r\n reason = isBot(request, botPatterns)\r\n ? 'bot detected'\r\n : 'user prefers no streaming';\r\n } else if (!shouldStream && condition === 'fast-network') {\r\n reason = isSlowConnection(request)\r\n ? 'slow connection detected'\r\n : 'bot detected';\r\n }\r\n }\r\n\r\n // Execute appropriate renderer\r\n const result = shouldStream ? await stream() : await staticFn();\r\n return { result, streaming: shouldStream, reason };\r\n}\r\n\r\n/**\r\n * Create a middleware-style conditional streamer\r\n */\r\nexport function createConditionalStreamer<T = Response>(\r\n defaultCondition: StreamingCondition | BuiltInCondition,\r\n options?: {\r\n botPatterns?: RegExp[];\r\n forceStreamParam?: string;\r\n forceStaticParam?: string;\r\n }\r\n) {\r\n return async (config: {\r\n request: Request;\r\n stream: () => Promise<T> | T;\r\n static: () => Promise<T> | T;\r\n condition?: StreamingCondition | BuiltInCondition;\r\n }) => {\r\n return streamIf({\r\n request: config.request,\r\n condition: config.condition || defaultCondition,\r\n stream: config.stream,\r\n static: config.static,\r\n ...options,\r\n });\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Response Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Add streaming decision headers to response (for debugging)\r\n */\r\nexport function addStreamingHeaders(\r\n response: Response,\r\n decision: { streaming: boolean; reason: string }\r\n): Response {\r\n const headers = new Headers(response.headers);\r\n headers.set('X-Flight-Streaming', decision.streaming ? 'true' : 'false');\r\n headers.set('X-Flight-Streaming-Reason', decision.reason);\r\n\r\n return new Response(response.body, {\r\n status: response.status,\r\n statusText: response.statusText,\r\n headers,\r\n });\r\n}\r\n\r\n/**\r\n * Create a streaming-aware Response with appropriate headers\r\n */\r\nexport function createStreamingResponse(\r\n stream: ReadableStream<Uint8Array>,\r\n options?: {\r\n status?: number;\r\n headers?: Record<string, string>;\r\n isStreaming?: boolean;\r\n }\r\n): Response {\r\n const { status = 200, headers = {}, isStreaming = true } = options || {};\r\n\r\n return new Response(stream, {\r\n status,\r\n headers: {\r\n 'Content-Type': 'text/html; charset=utf-8',\r\n 'Transfer-Encoding': isStreaming ? 'chunked' : undefined,\r\n 'X-Content-Type-Options': 'nosniff',\r\n 'X-Flight-Streaming': isStreaming ? 'true' : 'false',\r\n ...headers,\r\n } as HeadersInit,\r\n });\r\n}\r\n\r\n/**\r\n * Create a static HTML Response\r\n */\r\nexport function createStaticResponse(\r\n html: string,\r\n options?: {\r\n status?: number;\r\n headers?: Record<string, string>;\r\n }\r\n): Response {\r\n const { status = 200, headers = {} } = options || {};\r\n\r\n return new Response(html, {\r\n status,\r\n headers: {\r\n 'Content-Type': 'text/html; charset=utf-8',\r\n 'X-Flight-Streaming': 'false',\r\n ...headers,\r\n },\r\n });\r\n}\r\n"]}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createStreamingSSR } from './chunk-C37YQQI7.js';
|
|
2
|
+
|
|
3
|
+
// src/streaming/observability.ts
|
|
4
|
+
function generateStreamId() {
|
|
5
|
+
return `stream_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
6
|
+
}
|
|
7
|
+
async function createInstrumentedStream(config) {
|
|
8
|
+
const {
|
|
9
|
+
shell,
|
|
10
|
+
shellEnd,
|
|
11
|
+
suspenseBoundaries,
|
|
12
|
+
options = {},
|
|
13
|
+
observer,
|
|
14
|
+
onMetrics,
|
|
15
|
+
meta
|
|
16
|
+
} = config;
|
|
17
|
+
const streamId = generateStreamId();
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
const boundaryTimings = [];
|
|
20
|
+
const boundaryStarts = /* @__PURE__ */ new Map();
|
|
21
|
+
let shellTime = 0;
|
|
22
|
+
let totalBytes = 0;
|
|
23
|
+
let resolveMetrics;
|
|
24
|
+
const metricsPromise = new Promise((r) => {
|
|
25
|
+
resolveMetrics = r;
|
|
26
|
+
});
|
|
27
|
+
observer?.onStreamStart?.(streamId);
|
|
28
|
+
const instrumentedBoundaries = suspenseBoundaries.map((boundary) => {
|
|
29
|
+
const boundaryStart = Date.now();
|
|
30
|
+
boundaryStarts.set(boundary.id, boundaryStart);
|
|
31
|
+
observer?.onBoundaryStart?.({
|
|
32
|
+
streamId,
|
|
33
|
+
boundaryId: boundary.id,
|
|
34
|
+
startTime: boundaryStart - startTime
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
...boundary,
|
|
38
|
+
contentPromise: boundary.contentPromise.then(
|
|
39
|
+
(content) => {
|
|
40
|
+
const endTime = Date.now();
|
|
41
|
+
const timing = {
|
|
42
|
+
id: boundary.id,
|
|
43
|
+
startTime: boundaryStarts.get(boundary.id) - startTime,
|
|
44
|
+
endTime: endTime - startTime,
|
|
45
|
+
duration: endTime - boundaryStarts.get(boundary.id),
|
|
46
|
+
success: true,
|
|
47
|
+
contentSize: new TextEncoder().encode(content).length
|
|
48
|
+
};
|
|
49
|
+
boundaryTimings.push(timing);
|
|
50
|
+
observer?.onBoundaryEnd?.({ ...timing, streamId });
|
|
51
|
+
return content;
|
|
52
|
+
},
|
|
53
|
+
(error) => {
|
|
54
|
+
const endTime = Date.now();
|
|
55
|
+
const timing = {
|
|
56
|
+
id: boundary.id,
|
|
57
|
+
startTime: boundaryStarts.get(boundary.id) - startTime,
|
|
58
|
+
endTime: endTime - startTime,
|
|
59
|
+
duration: endTime - boundaryStarts.get(boundary.id),
|
|
60
|
+
success: false,
|
|
61
|
+
error: error instanceof Error ? error.message : String(error)
|
|
62
|
+
};
|
|
63
|
+
boundaryTimings.push(timing);
|
|
64
|
+
observer?.onBoundaryEnd?.({ ...timing, streamId });
|
|
65
|
+
observer?.onError?.({ streamId, boundaryId: boundary.id, error });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const result = await createStreamingSSR({
|
|
72
|
+
shell,
|
|
73
|
+
shellEnd,
|
|
74
|
+
suspenseBoundaries: instrumentedBoundaries,
|
|
75
|
+
options: {
|
|
76
|
+
...options,
|
|
77
|
+
onShellReady: () => {
|
|
78
|
+
shellTime = Date.now() - startTime;
|
|
79
|
+
observer?.onShellReady?.({ streamId, duration: shellTime });
|
|
80
|
+
options.onShellReady?.();
|
|
81
|
+
},
|
|
82
|
+
onAllReady: () => {
|
|
83
|
+
const totalStreamTime = Date.now() - startTime;
|
|
84
|
+
const metrics = {
|
|
85
|
+
streamId,
|
|
86
|
+
startTime,
|
|
87
|
+
shellTime,
|
|
88
|
+
totalStreamTime,
|
|
89
|
+
boundaries: boundaryTimings,
|
|
90
|
+
successCount: boundaryTimings.filter((b) => b.success).length,
|
|
91
|
+
errorCount: boundaryTimings.filter((b) => !b.success).length,
|
|
92
|
+
totalBytes,
|
|
93
|
+
meta
|
|
94
|
+
};
|
|
95
|
+
observer?.onStreamEnd?.(metrics);
|
|
96
|
+
onMetrics?.(metrics);
|
|
97
|
+
resolveMetrics(metrics);
|
|
98
|
+
options.onAllReady?.();
|
|
99
|
+
},
|
|
100
|
+
onError: (error) => {
|
|
101
|
+
observer?.onError?.({ streamId, error });
|
|
102
|
+
options.onError?.(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const countingStream = result.stream.pipeThrough(
|
|
107
|
+
new TransformStream({
|
|
108
|
+
transform(chunk, controller) {
|
|
109
|
+
totalBytes += chunk.length;
|
|
110
|
+
controller.enqueue(chunk);
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
return {
|
|
115
|
+
stream: countingStream,
|
|
116
|
+
abort: result.abort,
|
|
117
|
+
shellReady: result.shellReady,
|
|
118
|
+
allReady: result.allReady,
|
|
119
|
+
metrics: metricsPromise,
|
|
120
|
+
streamId
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
var MetricsAggregator = class {
|
|
124
|
+
metrics = [];
|
|
125
|
+
maxSamples;
|
|
126
|
+
constructor(maxSamples = 1e3) {
|
|
127
|
+
this.maxSamples = maxSamples;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Add metrics from a stream
|
|
131
|
+
*/
|
|
132
|
+
add(metrics) {
|
|
133
|
+
this.metrics.push(metrics);
|
|
134
|
+
if (this.metrics.length > this.maxSamples) {
|
|
135
|
+
this.metrics.shift();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get average shell time
|
|
140
|
+
*/
|
|
141
|
+
getAverageShellTime() {
|
|
142
|
+
if (this.metrics.length === 0) return 0;
|
|
143
|
+
return this.metrics.reduce((sum, m) => sum + m.shellTime, 0) / this.metrics.length;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get average total stream time
|
|
147
|
+
*/
|
|
148
|
+
getAverageTotalTime() {
|
|
149
|
+
if (this.metrics.length === 0) return 0;
|
|
150
|
+
return this.metrics.reduce((sum, m) => sum + m.totalStreamTime, 0) / this.metrics.length;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get slowest boundaries (by average duration)
|
|
154
|
+
*/
|
|
155
|
+
getSlowestBoundaries(limit = 5) {
|
|
156
|
+
const boundaryStats = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const m of this.metrics) {
|
|
158
|
+
for (const b of m.boundaries) {
|
|
159
|
+
const stats = boundaryStats.get(b.id) || { totalDuration: 0, count: 0, errors: 0 };
|
|
160
|
+
stats.totalDuration += b.duration;
|
|
161
|
+
stats.count++;
|
|
162
|
+
if (!b.success) stats.errors++;
|
|
163
|
+
boundaryStats.set(b.id, stats);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return [...boundaryStats.entries()].map(([id, stats]) => ({
|
|
167
|
+
id,
|
|
168
|
+
avgDuration: stats.totalDuration / stats.count,
|
|
169
|
+
errorRate: stats.errors / stats.count
|
|
170
|
+
})).sort((a, b) => b.avgDuration - a.avgDuration).slice(0, limit);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get overall error rate
|
|
174
|
+
*/
|
|
175
|
+
getErrorRate() {
|
|
176
|
+
const total = this.metrics.reduce((sum, m) => sum + m.boundaries.length, 0);
|
|
177
|
+
const errors = this.metrics.reduce((sum, m) => sum + m.errorCount, 0);
|
|
178
|
+
return total > 0 ? errors / total : 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get percentiles for shell time
|
|
182
|
+
*/
|
|
183
|
+
getShellTimePercentiles() {
|
|
184
|
+
const sorted = [...this.metrics].map((m) => m.shellTime).sort((a, b) => a - b);
|
|
185
|
+
const len = sorted.length;
|
|
186
|
+
if (len === 0) return { p50: 0, p90: 0, p99: 0 };
|
|
187
|
+
return {
|
|
188
|
+
p50: sorted[Math.floor(len * 0.5)] ?? 0,
|
|
189
|
+
p90: sorted[Math.floor(len * 0.9)] ?? 0,
|
|
190
|
+
p99: sorted[Math.floor(len * 0.99)] ?? 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Export all metrics for external analysis
|
|
195
|
+
*/
|
|
196
|
+
export() {
|
|
197
|
+
return [...this.metrics];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Clear all metrics
|
|
201
|
+
*/
|
|
202
|
+
clear() {
|
|
203
|
+
this.metrics = [];
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
function createLoggerObserver(log) {
|
|
207
|
+
return {
|
|
208
|
+
onStreamStart: (streamId) => {
|
|
209
|
+
log("[Flight] Stream started", { streamId });
|
|
210
|
+
},
|
|
211
|
+
onShellReady: ({ streamId, duration }) => {
|
|
212
|
+
log("[Flight] Shell ready", { streamId, duration: `${duration}ms` });
|
|
213
|
+
},
|
|
214
|
+
onBoundaryStart: ({ streamId, boundaryId, startTime }) => {
|
|
215
|
+
log("[Flight] Boundary started", { streamId, boundaryId, startTime: `${startTime}ms` });
|
|
216
|
+
},
|
|
217
|
+
onBoundaryEnd: (timing) => {
|
|
218
|
+
log("[Flight] Boundary completed", {
|
|
219
|
+
streamId: timing.streamId,
|
|
220
|
+
boundaryId: timing.id,
|
|
221
|
+
duration: `${timing.duration}ms`,
|
|
222
|
+
success: timing.success,
|
|
223
|
+
size: timing.contentSize ? `${timing.contentSize}b` : void 0
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
onStreamEnd: (metrics) => {
|
|
227
|
+
log("[Flight] Stream completed", {
|
|
228
|
+
streamId: metrics.streamId,
|
|
229
|
+
totalTime: `${metrics.totalStreamTime}ms`,
|
|
230
|
+
shellTime: `${metrics.shellTime}ms`,
|
|
231
|
+
boundaries: metrics.boundaries.length,
|
|
232
|
+
errors: metrics.errorCount
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
onError: ({ streamId, boundaryId, error }) => {
|
|
236
|
+
log("[Flight] Stream error", { streamId, boundaryId, error: error.message });
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function createHttpObserver(endpoint, options) {
|
|
241
|
+
const { headers = {}, batchSize = 10, flushInterval = 5e3 } = options || {};
|
|
242
|
+
const buffer = [];
|
|
243
|
+
let flushTimer = null;
|
|
244
|
+
const flush = async () => {
|
|
245
|
+
if (buffer.length === 0) return;
|
|
246
|
+
const batch = buffer.splice(0, buffer.length);
|
|
247
|
+
try {
|
|
248
|
+
await fetch(endpoint, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: {
|
|
251
|
+
"Content-Type": "application/json",
|
|
252
|
+
...headers
|
|
253
|
+
},
|
|
254
|
+
body: JSON.stringify({ metrics: batch, timestamp: Date.now() })
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error("[Flight] Failed to send metrics:", error);
|
|
258
|
+
buffer.unshift(...batch);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const scheduleFlush = () => {
|
|
262
|
+
if (flushTimer) return;
|
|
263
|
+
flushTimer = setTimeout(() => {
|
|
264
|
+
flushTimer = null;
|
|
265
|
+
flush();
|
|
266
|
+
}, flushInterval);
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
onStreamEnd: (metrics) => {
|
|
270
|
+
buffer.push(metrics);
|
|
271
|
+
if (buffer.length >= batchSize) {
|
|
272
|
+
flush();
|
|
273
|
+
} else {
|
|
274
|
+
scheduleFlush();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { MetricsAggregator, createHttpObserver, createInstrumentedStream, createLoggerObserver };
|
|
281
|
+
//# sourceMappingURL=chunk-WZIJKCL3.js.map
|
|
282
|
+
//# sourceMappingURL=chunk-WZIJKCL3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/streaming/observability.ts"],"names":[],"mappings":";;;AA2IA,SAAS,gBAAA,GAA2B;AAChC,EAAA,OAAO,CAAA,OAAA,EAAU,IAAA,CAAK,GAAA,EAAK,IAAI,IAAA,CAAK,MAAA,EAAO,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AACzE;AAKA,eAAsB,yBAClB,MAAA,EACiC;AACjC,EAAA,MAAM;AAAA,IACF,KAAA;AAAA,IACA,QAAA;AAAA,IACA,kBAAA;AAAA,IACA,UAAU,EAAC;AAAA,IACX,QAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACJ,GAAI,MAAA;AAEJ,EAAA,MAAM,WAAW,gBAAA,EAAiB;AAClC,EAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,EAAA,MAAM,kBAAoC,EAAC;AAC3C,EAAA,MAAM,cAAA,uBAAqB,GAAA,EAAoB;AAC/C,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,IAAI,UAAA,GAAa,CAAA;AAGjB,EAAA,IAAI,cAAA;AACJ,EAAA,MAAM,cAAA,GAAiB,IAAI,OAAA,CAA0B,CAAC,CAAA,KAAM;AAAE,IAAA,cAAA,GAAiB,CAAA;AAAA,EAAG,CAAC,CAAA;AAGnF,EAAA,QAAA,EAAU,gBAAgB,QAAQ,CAAA;AAGlC,EAAA,MAAM,sBAAA,GAAmD,kBAAA,CAAmB,GAAA,CAAI,CAAA,QAAA,KAAY;AACxF,IAAA,MAAM,aAAA,GAAgB,KAAK,GAAA,EAAI;AAC/B,IAAA,cAAA,CAAe,GAAA,CAAI,QAAA,CAAS,EAAA,EAAI,aAAa,CAAA;AAE7C,IAAA,QAAA,EAAU,eAAA,GAAkB;AAAA,MACxB,QAAA;AAAA,MACA,YAAY,QAAA,CAAS,EAAA;AAAA,MACrB,WAAW,aAAA,GAAgB;AAAA,KAC9B,CAAA;AAED,IAAA,OAAO;AAAA,MACH,GAAG,QAAA;AAAA,MACH,cAAA,EAAgB,SAAS,cAAA,CAAe,IAAA;AAAA,QACpC,CAAC,OAAA,KAAY;AACT,UAAA,MAAM,OAAA,GAAU,KAAK,GAAA,EAAI;AACzB,UAAA,MAAM,MAAA,GAAyB;AAAA,YAC3B,IAAI,QAAA,CAAS,EAAA;AAAA,YACb,SAAA,EAAW,cAAA,CAAe,GAAA,CAAI,QAAA,CAAS,EAAE,CAAA,GAAK,SAAA;AAAA,YAC9C,SAAS,OAAA,GAAU,SAAA;AAAA,YACnB,QAAA,EAAU,OAAA,GAAU,cAAA,CAAe,GAAA,CAAI,SAAS,EAAE,CAAA;AAAA,YAClD,OAAA,EAAS,IAAA;AAAA,YACT,aAAa,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE;AAAA,WACnD;AACA,UAAA,eAAA,CAAgB,KAAK,MAAM,CAAA;AAC3B,UAAA,QAAA,EAAU,aAAA,GAAgB,EAAE,GAAG,MAAA,EAAQ,UAAU,CAAA;AACjD,UAAA,OAAO,OAAA;AAAA,QACX,CAAA;AAAA,QACA,CAAC,KAAA,KAAU;AACP,UAAA,MAAM,OAAA,GAAU,KAAK,GAAA,EAAI;AACzB,UAAA,MAAM,MAAA,GAAyB;AAAA,YAC3B,IAAI,QAAA,CAAS,EAAA;AAAA,YACb,SAAA,EAAW,cAAA,CAAe,GAAA,CAAI,QAAA,CAAS,EAAE,CAAA,GAAK,SAAA;AAAA,YAC9C,SAAS,OAAA,GAAU,SAAA;AAAA,YACnB,QAAA,EAAU,OAAA,GAAU,cAAA,CAAe,GAAA,CAAI,SAAS,EAAE,CAAA;AAAA,YAClD,OAAA,EAAS,KAAA;AAAA,YACT,OAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK;AAAA,WAChE;AACA,UAAA,eAAA,CAAgB,KAAK,MAAM,CAAA;AAC3B,UAAA,QAAA,EAAU,aAAA,GAAgB,EAAE,GAAG,MAAA,EAAQ,UAAU,CAAA;AACjD,UAAA,QAAA,EAAU,UAAU,EAAE,QAAA,EAAU,YAAY,QAAA,CAAS,EAAA,EAAI,OAAuB,CAAA;AAChF,UAAA,MAAM,KAAA;AAAA,QACV;AAAA;AACJ,KACJ;AAAA,EACJ,CAAC,CAAA;AAGD,EAAA,MAAM,MAAA,GAAS,MAAM,kBAAA,CAAmB;AAAA,IACpC,KAAA;AAAA,IACA,QAAA;AAAA,IACA,kBAAA,EAAoB,sBAAA;AAAA,IACpB,OAAA,EAAS;AAAA,MACL,GAAG,OAAA;AAAA,MACH,cAAc,MAAM;AAChB,QAAA,SAAA,GAAY,IAAA,CAAK,KAAI,GAAI,SAAA;AACzB,QAAA,QAAA,EAAU,YAAA,GAAe,EAAE,QAAA,EAAU,QAAA,EAAU,WAAW,CAAA;AAC1D,QAAA,OAAA,CAAQ,YAAA,IAAe;AAAA,MAC3B,CAAA;AAAA,MACA,YAAY,MAAM;AACd,QAAA,MAAM,eAAA,GAAkB,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAErC,QAAA,MAAM,OAAA,GAA4B;AAAA,UAC9B,QAAA;AAAA,UACA,SAAA;AAAA,UACA,SAAA;AAAA,UACA,eAAA;AAAA,UACA,UAAA,EAAY,eAAA;AAAA,UACZ,cAAc,eAAA,CAAgB,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,CAAA,CAAE,MAAA;AAAA,UACrD,YAAY,eAAA,CAAgB,MAAA,CAAO,OAAK,CAAC,CAAA,CAAE,OAAO,CAAA,CAAE,MAAA;AAAA,UACpD,UAAA;AAAA,UACA;AAAA,SACJ;AAEA,QAAA,QAAA,EAAU,cAAc,OAAO,CAAA;AAC/B,QAAA,SAAA,GAAY,OAAO,CAAA;AACnB,QAAA,cAAA,CAAgB,OAAO,CAAA;AACvB,QAAA,OAAA,CAAQ,UAAA,IAAa;AAAA,MACzB,CAAA;AAAA,MACA,OAAA,EAAS,CAAC,KAAA,KAAU;AAChB,QAAA,QAAA,EAAU,OAAA,GAAU,EAAE,QAAA,EAAU,KAAA,EAAO,CAAA;AACvC,QAAA,OAAA,CAAQ,UAAU,KAAK,CAAA;AAAA,MAC3B;AAAA;AACJ,GACH,CAAA;AAGD,EAAA,MAAM,cAAA,GAAiB,OAAO,MAAA,CAAO,WAAA;AAAA,IACjC,IAAI,eAAA,CAAwC;AAAA,MACxC,SAAA,CAAU,OAAO,UAAA,EAAY;AACzB,QAAA,UAAA,IAAc,KAAA,CAAM,MAAA;AACpB,QAAA,UAAA,CAAW,QAAQ,KAAK,CAAA;AAAA,MAC5B;AAAA,KACH;AAAA,GACL;AAEA,EAAA,OAAO;AAAA,IACH,MAAA,EAAQ,cAAA;AAAA,IACR,OAAO,MAAA,CAAO,KAAA;AAAA,IACd,YAAY,MAAA,CAAO,UAAA;AAAA,IACnB,UAAU,MAAA,CAAO,QAAA;AAAA,IACjB,OAAA,EAAS,cAAA;AAAA,IACT;AAAA,GACJ;AACJ;AASO,IAAM,oBAAN,MAAwB;AAAA,EACnB,UAA8B,EAAC;AAAA,EAC/B,UAAA;AAAA,EAER,WAAA,CAAY,aAAa,GAAA,EAAM;AAC3B,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,OAAA,EAAiC;AACjC,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,OAAO,CAAA;AACzB,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,GAAS,IAAA,CAAK,UAAA,EAAY;AACvC,MAAA,IAAA,CAAK,QAAQ,KAAA,EAAM;AAAA,IACvB;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAA,GAA8B;AAC1B,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AACtC,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,CAAC,GAAA,EAAK,CAAA,KAAM,GAAA,GAAM,CAAA,CAAE,SAAA,EAAW,CAAC,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,MAAA;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAA,GAA8B;AAC1B,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AACtC,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,CAAC,GAAA,EAAK,CAAA,KAAM,GAAA,GAAM,CAAA,CAAE,eAAA,EAAiB,CAAC,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,MAAA;AAAA,EACtF;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAA,CAAqB,QAAQ,CAAA,EAA6D;AACtF,IAAA,MAAM,aAAA,uBAAoB,GAAA,EAAsE;AAEhG,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,OAAA,EAAS;AAC1B,MAAA,KAAA,MAAW,CAAA,IAAK,EAAE,UAAA,EAAY;AAC1B,QAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,GAAA,CAAI,CAAA,CAAE,EAAE,CAAA,IAAK,EAAE,aAAA,EAAe,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,CAAA,EAAE;AACjF,QAAA,KAAA,CAAM,iBAAiB,CAAA,CAAE,QAAA;AACzB,QAAA,KAAA,CAAM,KAAA,EAAA;AACN,QAAA,IAAI,CAAC,CAAA,CAAE,OAAA,EAAS,KAAA,CAAM,MAAA,EAAA;AACtB,QAAA,aAAA,CAAc,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,KAAK,CAAA;AAAA,MACjC;AAAA,IACJ;AAEA,IAAA,OAAO,CAAC,GAAG,aAAA,CAAc,OAAA,EAAS,CAAA,CAC7B,GAAA,CAAI,CAAC,CAAC,EAAA,EAAI,KAAK,CAAA,MAAO;AAAA,MACnB,EAAA;AAAA,MACA,WAAA,EAAa,KAAA,CAAM,aAAA,GAAgB,KAAA,CAAM,KAAA;AAAA,MACzC,SAAA,EAAW,KAAA,CAAM,MAAA,GAAS,KAAA,CAAM;AAAA,KACpC,CAAE,CAAA,CACD,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,WAAA,GAAc,CAAA,CAAE,WAAW,CAAA,CAC5C,KAAA,CAAM,GAAG,KAAK,CAAA;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,YAAA,GAAuB;AACnB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,CAAC,GAAA,EAAK,CAAA,KAAM,GAAA,GAAM,CAAA,CAAE,UAAA,CAAW,MAAA,EAAQ,CAAC,CAAA;AAC1E,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,CAAC,KAAK,CAAA,KAAM,GAAA,GAAM,CAAA,CAAE,UAAA,EAAY,CAAC,CAAA;AACpE,IAAA,OAAO,KAAA,GAAQ,CAAA,GAAI,MAAA,GAAS,KAAA,GAAQ,CAAA;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,uBAAA,GAAqE;AACjE,IAAA,MAAM,SAAS,CAAC,GAAG,IAAA,CAAK,OAAO,EAAE,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,EAAE,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,IAAI,CAAC,CAAA;AAC3E,IAAA,MAAM,MAAM,MAAA,CAAO,MAAA;AACnB,IAAA,IAAI,GAAA,KAAQ,GAAG,OAAO,EAAE,KAAK,CAAA,EAAG,GAAA,EAAK,CAAA,EAAG,GAAA,EAAK,CAAA,EAAE;AAE/C,IAAA,OAAO;AAAA,MACH,KAAK,MAAA,CAAO,IAAA,CAAK,MAAM,GAAA,GAAM,GAAG,CAAC,CAAA,IAAK,CAAA;AAAA,MACtC,KAAK,MAAA,CAAO,IAAA,CAAK,MAAM,GAAA,GAAM,GAAG,CAAC,CAAA,IAAK,CAAA;AAAA,MACtC,KAAK,MAAA,CAAO,IAAA,CAAK,MAAM,GAAA,GAAM,IAAI,CAAC,CAAA,IAAK;AAAA,KAC3C;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAA,GAA6B;AACzB,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,OAAO,CAAA;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,KAAA,GAAc;AACV,IAAA,IAAA,CAAK,UAAU,EAAC;AAAA,EACpB;AACJ;AASO,SAAS,qBACZ,GAAA,EACiB;AACjB,EAAA,OAAO;AAAA,IACH,aAAA,EAAe,CAAC,QAAA,KAAa;AACzB,MAAA,GAAA,CAAI,yBAAA,EAA2B,EAAE,QAAA,EAAU,CAAA;AAAA,IAC/C,CAAA;AAAA,IACA,YAAA,EAAc,CAAC,EAAE,QAAA,EAAU,UAAS,KAAM;AACtC,MAAA,GAAA,CAAI,wBAAwB,EAAE,QAAA,EAAU,UAAU,CAAA,EAAG,QAAQ,MAAM,CAAA;AAAA,IACvE,CAAA;AAAA,IACA,iBAAiB,CAAC,EAAE,QAAA,EAAU,UAAA,EAAY,WAAU,KAAM;AACtD,MAAA,GAAA,CAAI,2BAAA,EAA6B,EAAE,QAAA,EAAU,UAAA,EAAY,WAAW,CAAA,EAAG,SAAS,MAAM,CAAA;AAAA,IAC1F,CAAA;AAAA,IACA,aAAA,EAAe,CAAC,MAAA,KAAW;AACvB,MAAA,GAAA,CAAI,6BAAA,EAA+B;AAAA,QAC/B,UAAW,MAAA,CAAe,QAAA;AAAA,QAC1B,YAAY,MAAA,CAAO,EAAA;AAAA,QACnB,QAAA,EAAU,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAA,CAAA;AAAA,QAC5B,SAAS,MAAA,CAAO,OAAA;AAAA,QAChB,MAAM,MAAA,CAAO,WAAA,GAAc,CAAA,EAAG,MAAA,CAAO,WAAW,CAAA,CAAA,CAAA,GAAM;AAAA,OACzD,CAAA;AAAA,IACL,CAAA;AAAA,IACA,WAAA,EAAa,CAAC,OAAA,KAAY;AACtB,MAAA,GAAA,CAAI,2BAAA,EAA6B;AAAA,QAC7B,UAAU,OAAA,CAAQ,QAAA;AAAA,QAClB,SAAA,EAAW,CAAA,EAAG,OAAA,CAAQ,eAAe,CAAA,EAAA,CAAA;AAAA,QACrC,SAAA,EAAW,CAAA,EAAG,OAAA,CAAQ,SAAS,CAAA,EAAA,CAAA;AAAA,QAC/B,UAAA,EAAY,QAAQ,UAAA,CAAW,MAAA;AAAA,QAC/B,QAAQ,OAAA,CAAQ;AAAA,OACnB,CAAA;AAAA,IACL,CAAA;AAAA,IACA,SAAS,CAAC,EAAE,QAAA,EAAU,UAAA,EAAY,OAAM,KAAM;AAC1C,MAAA,GAAA,CAAI,yBAAyB,EAAE,QAAA,EAAU,YAAY,KAAA,EAAO,KAAA,CAAM,SAAS,CAAA;AAAA,IAC/E;AAAA,GACJ;AACJ;AAKO,SAAS,kBAAA,CAAmB,UAAkB,OAAA,EAI/B;AAClB,EAAA,MAAM,EAAE,OAAA,GAAU,EAAC,EAAG,SAAA,GAAY,IAAI,aAAA,GAAgB,GAAA,EAAK,GAAI,OAAA,IAAW,EAAC;AAC3E,EAAA,MAAM,SAA6B,EAAC;AACpC,EAAA,IAAI,UAAA,GAAmD,IAAA;AAEvD,EAAA,MAAM,QAAQ,YAAY;AACtB,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACzB,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,OAAO,MAAM,CAAA;AAE5C,IAAA,IAAI;AACA,MAAA,MAAM,MAAM,QAAA,EAAU;AAAA,QAClB,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACL,cAAA,EAAgB,kBAAA;AAAA,UAChB,GAAG;AAAA,SACP;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,OAAA,EAAS,OAAO,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,EAAG;AAAA,OACjE,CAAA;AAAA,IACL,SAAS,KAAA,EAAO;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,oCAAoC,KAAK,CAAA;AAEvD,MAAA,MAAA,CAAO,OAAA,CAAQ,GAAG,KAAK,CAAA;AAAA,IAC3B;AAAA,EACJ,CAAA;AAEA,EAAA,MAAM,gBAAgB,MAAM;AACxB,IAAA,IAAI,UAAA,EAAY;AAChB,IAAA,UAAA,GAAa,WAAW,MAAM;AAC1B,MAAA,UAAA,GAAa,IAAA;AACb,MAAA,KAAA,EAAM;AAAA,IACV,GAAG,aAAa,CAAA;AAAA,EACpB,CAAA;AAEA,EAAA,OAAO;AAAA,IACH,WAAA,EAAa,CAAC,OAAA,KAAY;AACtB,MAAA,MAAA,CAAO,KAAK,OAAO,CAAA;AACnB,MAAA,IAAI,MAAA,CAAO,UAAU,SAAA,EAAW;AAC5B,QAAA,KAAA,EAAM;AAAA,MACV,CAAA,MAAO;AACH,QAAA,aAAA,EAAc;AAAA,MAClB;AAAA,IACJ;AAAA,GACJ;AACJ","file":"chunk-WZIJKCL3.js","sourcesContent":["/**\r\n * @flightdev/core - Streaming Observability\r\n * \r\n * Zero-telemetry metrics and instrumentation for streaming SSR.\r\n * All data stays on YOUR infrastructure - never sent anywhere.\r\n * \r\n * Best Practices 2026:\r\n * - Comprehensive timing metrics for each boundary\r\n * - Hook-based observability for custom integrations\r\n * - Performance insights without vendor lock-in\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createInstrumentedStream } from '@flightdev/core/streaming';\r\n * \r\n * const { stream, metrics } = await createInstrumentedStream({\r\n * shell: '<html>...',\r\n * boundaries: [...],\r\n * onMetrics: (m) => myAnalytics.track(m), // YOUR system\r\n * });\r\n * ```\r\n */\r\n\r\nimport type { StreamingRenderOptions, SuspenseBoundaryConfig } from './index.js';\r\nimport { createStreamingSSR } from './index.js';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\n/**\r\n * Timing information for a single boundary\r\n */\r\nexport interface BoundaryTiming {\r\n /** Boundary identifier */\r\n id: string;\r\n /** When the boundary started resolving (ms since stream start) */\r\n startTime: number;\r\n /** When the boundary finished resolving */\r\n endTime: number;\r\n /** Total duration in milliseconds */\r\n duration: number;\r\n /** Whether it resolved successfully */\r\n success: boolean;\r\n /** Size of content in bytes (if successful) */\r\n contentSize?: number;\r\n /** Error message if failed */\r\n error?: string;\r\n}\r\n\r\n/**\r\n * Overall streaming metrics\r\n */\r\nexport interface StreamingMetrics {\r\n /** Unique ID for this stream session */\r\n streamId: string;\r\n /** When streaming started */\r\n startTime: number;\r\n /** Time to shell ready (TTFB improvement) */\r\n shellTime: number;\r\n /** Time until all boundaries resolved */\r\n totalStreamTime: number;\r\n /** Individual boundary timings */\r\n boundaries: BoundaryTiming[];\r\n /** Number of boundaries that resolved successfully */\r\n successCount: number;\r\n /** Number of boundaries that failed */\r\n errorCount: number;\r\n /** Total bytes streamed */\r\n totalBytes: number;\r\n /** Strategy used (if priority streaming) */\r\n strategy?: string;\r\n /** Custom metadata you can add */\r\n meta?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Observability hooks for streaming events\r\n */\r\nexport interface StreamingObserver {\r\n /** Called when streaming starts */\r\n onStreamStart?: (streamId: string) => void;\r\n /** Called when shell is sent */\r\n onShellReady?: (timing: { streamId: string; duration: number }) => void;\r\n /** Called when a boundary starts resolving */\r\n onBoundaryStart?: (info: { streamId: string; boundaryId: string; startTime: number }) => void;\r\n /** Called when a boundary finishes */\r\n onBoundaryEnd?: (timing: BoundaryTiming & { streamId: string }) => void;\r\n /** Called when streaming completes */\r\n onStreamEnd?: (metrics: StreamingMetrics) => void;\r\n /** Called on any error */\r\n onError?: (error: { streamId: string; boundaryId?: string; error: Error }) => void;\r\n}\r\n\r\n/**\r\n * Configuration for instrumented streaming\r\n */\r\nexport interface InstrumentedStreamConfig {\r\n /** Initial HTML shell */\r\n shell: string;\r\n /** Closing HTML */\r\n shellEnd: string;\r\n /** Suspense boundaries */\r\n suspenseBoundaries: SuspenseBoundaryConfig[];\r\n /** Streaming options */\r\n options?: StreamingRenderOptions;\r\n /** Observer hooks */\r\n observer?: StreamingObserver;\r\n /** Callback when all metrics are ready */\r\n onMetrics?: (metrics: StreamingMetrics) => void;\r\n /** Custom metadata to include in metrics */\r\n meta?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Result of instrumented streaming\r\n */\r\nexport interface InstrumentedStreamResult {\r\n /** The readable stream */\r\n stream: ReadableStream<Uint8Array>;\r\n /** Abort the stream */\r\n abort: () => void;\r\n /** Shell ready promise */\r\n shellReady: Promise<void>;\r\n /** All content ready promise */\r\n allReady: Promise<void>;\r\n /** Final metrics (resolves when stream completes) */\r\n metrics: Promise<StreamingMetrics>;\r\n /** Stream ID for correlation */\r\n streamId: string;\r\n}\r\n\r\n// ============================================================================\r\n// Implementation\r\n// ============================================================================\r\n\r\n/**\r\n * Generate a unique stream ID\r\n */\r\nfunction generateStreamId(): string {\r\n return `stream_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\r\n}\r\n\r\n/**\r\n * Create an instrumented streaming SSR response with full observability\r\n */\r\nexport async function createInstrumentedStream(\r\n config: InstrumentedStreamConfig\r\n): Promise<InstrumentedStreamResult> {\r\n const {\r\n shell,\r\n shellEnd,\r\n suspenseBoundaries,\r\n options = {},\r\n observer,\r\n onMetrics,\r\n meta,\r\n } = config;\r\n\r\n const streamId = generateStreamId();\r\n const startTime = Date.now();\r\n const boundaryTimings: BoundaryTiming[] = [];\r\n const boundaryStarts = new Map<string, number>();\r\n let shellTime = 0;\r\n let totalBytes = 0;\r\n\r\n // Metrics promise\r\n let resolveMetrics: (m: StreamingMetrics) => void;\r\n const metricsPromise = new Promise<StreamingMetrics>((r) => { resolveMetrics = r; });\r\n\r\n // Notify stream start\r\n observer?.onStreamStart?.(streamId);\r\n\r\n // Wrap boundaries with instrumentation\r\n const instrumentedBoundaries: SuspenseBoundaryConfig[] = suspenseBoundaries.map(boundary => {\r\n const boundaryStart = Date.now();\r\n boundaryStarts.set(boundary.id, boundaryStart);\r\n\r\n observer?.onBoundaryStart?.({\r\n streamId,\r\n boundaryId: boundary.id,\r\n startTime: boundaryStart - startTime,\r\n });\r\n\r\n return {\r\n ...boundary,\r\n contentPromise: boundary.contentPromise.then(\r\n (content) => {\r\n const endTime = Date.now();\r\n const timing: BoundaryTiming = {\r\n id: boundary.id,\r\n startTime: boundaryStarts.get(boundary.id)! - startTime,\r\n endTime: endTime - startTime,\r\n duration: endTime - boundaryStarts.get(boundary.id)!,\r\n success: true,\r\n contentSize: new TextEncoder().encode(content).length,\r\n };\r\n boundaryTimings.push(timing);\r\n observer?.onBoundaryEnd?.({ ...timing, streamId });\r\n return content;\r\n },\r\n (error) => {\r\n const endTime = Date.now();\r\n const timing: BoundaryTiming = {\r\n id: boundary.id,\r\n startTime: boundaryStarts.get(boundary.id)! - startTime,\r\n endTime: endTime - startTime,\r\n duration: endTime - boundaryStarts.get(boundary.id)!,\r\n success: false,\r\n error: error instanceof Error ? error.message : String(error),\r\n };\r\n boundaryTimings.push(timing);\r\n observer?.onBoundaryEnd?.({ ...timing, streamId });\r\n observer?.onError?.({ streamId, boundaryId: boundary.id, error: error as Error });\r\n throw error;\r\n }\r\n ),\r\n };\r\n });\r\n\r\n // Create the underlying stream with instrumented callbacks\r\n const result = await createStreamingSSR({\r\n shell,\r\n shellEnd,\r\n suspenseBoundaries: instrumentedBoundaries,\r\n options: {\r\n ...options,\r\n onShellReady: () => {\r\n shellTime = Date.now() - startTime;\r\n observer?.onShellReady?.({ streamId, duration: shellTime });\r\n options.onShellReady?.();\r\n },\r\n onAllReady: () => {\r\n const totalStreamTime = Date.now() - startTime;\r\n\r\n const metrics: StreamingMetrics = {\r\n streamId,\r\n startTime,\r\n shellTime,\r\n totalStreamTime,\r\n boundaries: boundaryTimings,\r\n successCount: boundaryTimings.filter(b => b.success).length,\r\n errorCount: boundaryTimings.filter(b => !b.success).length,\r\n totalBytes,\r\n meta,\r\n };\r\n\r\n observer?.onStreamEnd?.(metrics);\r\n onMetrics?.(metrics);\r\n resolveMetrics!(metrics);\r\n options.onAllReady?.();\r\n },\r\n onError: (error) => {\r\n observer?.onError?.({ streamId, error });\r\n options.onError?.(error);\r\n },\r\n },\r\n });\r\n\r\n // Wrap stream to count bytes\r\n const countingStream = result.stream.pipeThrough(\r\n new TransformStream<Uint8Array, Uint8Array>({\r\n transform(chunk, controller) {\r\n totalBytes += chunk.length;\r\n controller.enqueue(chunk);\r\n },\r\n })\r\n );\r\n\r\n return {\r\n stream: countingStream,\r\n abort: result.abort,\r\n shellReady: result.shellReady,\r\n allReady: result.allReady,\r\n metrics: metricsPromise,\r\n streamId,\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Utility: Metrics Aggregator\r\n// ============================================================================\r\n\r\n/**\r\n * Aggregate metrics from multiple streams for analysis\r\n */\r\nexport class MetricsAggregator {\r\n private metrics: StreamingMetrics[] = [];\r\n private maxSamples: number;\r\n\r\n constructor(maxSamples = 1000) {\r\n this.maxSamples = maxSamples;\r\n }\r\n\r\n /**\r\n * Add metrics from a stream\r\n */\r\n add(metrics: StreamingMetrics): void {\r\n this.metrics.push(metrics);\r\n if (this.metrics.length > this.maxSamples) {\r\n this.metrics.shift();\r\n }\r\n }\r\n\r\n /**\r\n * Get average shell time\r\n */\r\n getAverageShellTime(): number {\r\n if (this.metrics.length === 0) return 0;\r\n return this.metrics.reduce((sum, m) => sum + m.shellTime, 0) / this.metrics.length;\r\n }\r\n\r\n /**\r\n * Get average total stream time\r\n */\r\n getAverageTotalTime(): number {\r\n if (this.metrics.length === 0) return 0;\r\n return this.metrics.reduce((sum, m) => sum + m.totalStreamTime, 0) / this.metrics.length;\r\n }\r\n\r\n /**\r\n * Get slowest boundaries (by average duration)\r\n */\r\n getSlowestBoundaries(limit = 5): { id: string; avgDuration: number; errorRate: number }[] {\r\n const boundaryStats = new Map<string, { totalDuration: number; count: number; errors: number }>();\r\n\r\n for (const m of this.metrics) {\r\n for (const b of m.boundaries) {\r\n const stats = boundaryStats.get(b.id) || { totalDuration: 0, count: 0, errors: 0 };\r\n stats.totalDuration += b.duration;\r\n stats.count++;\r\n if (!b.success) stats.errors++;\r\n boundaryStats.set(b.id, stats);\r\n }\r\n }\r\n\r\n return [...boundaryStats.entries()]\r\n .map(([id, stats]) => ({\r\n id,\r\n avgDuration: stats.totalDuration / stats.count,\r\n errorRate: stats.errors / stats.count,\r\n }))\r\n .sort((a, b) => b.avgDuration - a.avgDuration)\r\n .slice(0, limit);\r\n }\r\n\r\n /**\r\n * Get overall error rate\r\n */\r\n getErrorRate(): number {\r\n const total = this.metrics.reduce((sum, m) => sum + m.boundaries.length, 0);\r\n const errors = this.metrics.reduce((sum, m) => sum + m.errorCount, 0);\r\n return total > 0 ? errors / total : 0;\r\n }\r\n\r\n /**\r\n * Get percentiles for shell time\r\n */\r\n getShellTimePercentiles(): { p50: number; p90: number; p99: number } {\r\n const sorted = [...this.metrics].map(m => m.shellTime).sort((a, b) => a - b);\r\n const len = sorted.length;\r\n if (len === 0) return { p50: 0, p90: 0, p99: 0 };\r\n\r\n return {\r\n p50: sorted[Math.floor(len * 0.5)] ?? 0,\r\n p90: sorted[Math.floor(len * 0.9)] ?? 0,\r\n p99: sorted[Math.floor(len * 0.99)] ?? 0,\r\n };\r\n }\r\n\r\n /**\r\n * Export all metrics for external analysis\r\n */\r\n export(): StreamingMetrics[] {\r\n return [...this.metrics];\r\n }\r\n\r\n /**\r\n * Clear all metrics\r\n */\r\n clear(): void {\r\n this.metrics = [];\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Utility: Create Observer from Logger\r\n// ============================================================================\r\n\r\n/**\r\n * Create a streaming observer from a simple logger function\r\n */\r\nexport function createLoggerObserver(\r\n log: (message: string, data?: Record<string, unknown>) => void\r\n): StreamingObserver {\r\n return {\r\n onStreamStart: (streamId) => {\r\n log('[Flight] Stream started', { streamId });\r\n },\r\n onShellReady: ({ streamId, duration }) => {\r\n log('[Flight] Shell ready', { streamId, duration: `${duration}ms` });\r\n },\r\n onBoundaryStart: ({ streamId, boundaryId, startTime }) => {\r\n log('[Flight] Boundary started', { streamId, boundaryId, startTime: `${startTime}ms` });\r\n },\r\n onBoundaryEnd: (timing) => {\r\n log('[Flight] Boundary completed', {\r\n streamId: (timing as any).streamId,\r\n boundaryId: timing.id,\r\n duration: `${timing.duration}ms`,\r\n success: timing.success,\r\n size: timing.contentSize ? `${timing.contentSize}b` : undefined,\r\n });\r\n },\r\n onStreamEnd: (metrics) => {\r\n log('[Flight] Stream completed', {\r\n streamId: metrics.streamId,\r\n totalTime: `${metrics.totalStreamTime}ms`,\r\n shellTime: `${metrics.shellTime}ms`,\r\n boundaries: metrics.boundaries.length,\r\n errors: metrics.errorCount,\r\n });\r\n },\r\n onError: ({ streamId, boundaryId, error }) => {\r\n log('[Flight] Stream error', { streamId, boundaryId, error: error.message });\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create observer that sends metrics to a custom endpoint (YOUR infrastructure)\r\n */\r\nexport function createHttpObserver(endpoint: string, options?: {\r\n headers?: Record<string, string>;\r\n batchSize?: number;\r\n flushInterval?: number;\r\n}): StreamingObserver {\r\n const { headers = {}, batchSize = 10, flushInterval = 5000 } = options || {};\r\n const buffer: StreamingMetrics[] = [];\r\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n const flush = async () => {\r\n if (buffer.length === 0) return;\r\n const batch = buffer.splice(0, buffer.length);\r\n\r\n try {\r\n await fetch(endpoint, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...headers,\r\n },\r\n body: JSON.stringify({ metrics: batch, timestamp: Date.now() }),\r\n });\r\n } catch (error) {\r\n console.error('[Flight] Failed to send metrics:', error);\r\n // Re-add to buffer for retry\r\n buffer.unshift(...batch);\r\n }\r\n };\r\n\r\n const scheduleFlush = () => {\r\n if (flushTimer) return;\r\n flushTimer = setTimeout(() => {\r\n flushTimer = null;\r\n flush();\r\n }, flushInterval);\r\n };\r\n\r\n return {\r\n onStreamEnd: (metrics) => {\r\n buffer.push(metrics);\r\n if (buffer.length >= batchSize) {\r\n flush();\r\n } else {\r\n scheduleFlush();\r\n }\r\n },\r\n };\r\n}\r\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-Y22AMGTM.js"}
|