@flight-framework/core 0.0.3 → 0.1.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/{chunk-CLMFEKYM.js → chunk-54HPVE7N.js} +67 -2
- package/dist/chunk-54HPVE7N.js.map +1 -0
- package/dist/{chunk-ABNCAPQB.js → chunk-6WSPUG5L.js} +2 -2
- package/dist/chunk-6WSPUG5L.js.map +1 -0
- package/dist/chunk-I2B4WSHC.js +126 -0
- package/dist/chunk-I2B4WSHC.js.map +1 -0
- package/dist/chunk-MQQLYWZZ.js +288 -0
- package/dist/chunk-MQQLYWZZ.js.map +1 -0
- package/dist/chunk-OBNYNJB5.js +353 -0
- package/dist/chunk-OBNYNJB5.js.map +1 -0
- package/dist/chunk-WFAWAHJH.js +267 -0
- package/dist/chunk-WFAWAHJH.js.map +1 -0
- package/dist/chunk-XOIYNY4I.js +164 -0
- package/dist/chunk-XOIYNY4I.js.map +1 -0
- package/dist/chunk-YIOQC3DC.js +282 -0
- package/dist/chunk-YIOQC3DC.js.map +1 -0
- package/dist/file-router/index.d.ts +9 -0
- package/dist/file-router/index.js +1 -1
- 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/index.d.ts +6 -0
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- 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/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 +8 -0
- package/dist/streaming/index.js +1 -1
- 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/package.json +25 -1
- package/dist/chunk-ABNCAPQB.js.map +0 -1
- package/dist/chunk-CLMFEKYM.js.map +0 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createStreamingSSR } from './chunk-6WSPUG5L.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-YIOQC3DC.js.map
|
|
282
|
+
//# sourceMappingURL=chunk-YIOQC3DC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/streaming/observability.ts"],"names":[],"mappings":"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","file":"chunk-YIOQC3DC.js","sourcesContent":["/**\r\n * @flight-framework/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 '@flight-framework/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"]}
|
|
@@ -26,6 +26,15 @@ interface FileRoute {
|
|
|
26
26
|
meta?: Record<string, unknown>;
|
|
27
27
|
/** Parallel route slot name (for @folder convention) */
|
|
28
28
|
slot?: string;
|
|
29
|
+
/** Intercepting route info (for (.) (..) (...) convention) */
|
|
30
|
+
interceptInfo?: {
|
|
31
|
+
/** Number of levels to intercept: 1 = same, 2 = parent, 3+ = root */
|
|
32
|
+
level: number;
|
|
33
|
+
/** Target route segment to intercept */
|
|
34
|
+
target: string;
|
|
35
|
+
/** Original path that triggers interception */
|
|
36
|
+
interceptPath: string;
|
|
37
|
+
};
|
|
29
38
|
}
|
|
30
39
|
interface FileRouterOptions {
|
|
31
40
|
/** Root directory to scan (default: src/routes) */
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flight-framework/core - File Router Streaming Hints
|
|
3
|
+
*
|
|
4
|
+
* Per-route streaming configuration through exports.
|
|
5
|
+
* The user defines streaming behavior at the route level.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // src/routes/products/[id].page.tsx
|
|
10
|
+
*
|
|
11
|
+
* // Static export for streaming hints
|
|
12
|
+
* export const streaming = {
|
|
13
|
+
* enabled: true,
|
|
14
|
+
* timeout: 5000,
|
|
15
|
+
* priority: 'high',
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* // Or dynamic function based on params
|
|
19
|
+
* export function getStreamingConfig(params: { id: string }) {
|
|
20
|
+
* return {
|
|
21
|
+
* enabled: params.id !== 'preview',
|
|
22
|
+
* timeout: 3000,
|
|
23
|
+
* };
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* export default function ProductPage({ params }) {
|
|
27
|
+
* return <Product id={params.id} />;
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Static streaming configuration export
|
|
33
|
+
*/
|
|
34
|
+
interface StreamingHints {
|
|
35
|
+
/** Whether streaming is enabled for this route (default: true) */
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
/** Timeout before aborting streaming (ms) */
|
|
38
|
+
timeout?: number;
|
|
39
|
+
/** Priority hint for streaming scheduler */
|
|
40
|
+
priority?: 'high' | 'normal' | 'low';
|
|
41
|
+
/** Expected suspense boundary IDs */
|
|
42
|
+
boundaries?: string[];
|
|
43
|
+
/** Force static rendering (no streaming) */
|
|
44
|
+
forceStatic?: boolean;
|
|
45
|
+
/** Cache the static version */
|
|
46
|
+
cache?: {
|
|
47
|
+
/** Time to cache in seconds */
|
|
48
|
+
ttl?: number;
|
|
49
|
+
/** Stale-while-revalidate time in seconds */
|
|
50
|
+
swr?: number;
|
|
51
|
+
/** Cache tags for invalidation */
|
|
52
|
+
tags?: string[];
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Dynamic streaming configuration function
|
|
57
|
+
*/
|
|
58
|
+
type GetStreamingConfig<TParams = Record<string, string>> = (params: TParams, request?: Request) => StreamingHints | Promise<StreamingHints>;
|
|
59
|
+
/**
|
|
60
|
+
* Route module with streaming exports
|
|
61
|
+
*/
|
|
62
|
+
interface StreamingRouteModule {
|
|
63
|
+
/** Default component/handler */
|
|
64
|
+
default: unknown;
|
|
65
|
+
/** Static streaming configuration */
|
|
66
|
+
streaming?: StreamingHints;
|
|
67
|
+
/** Dynamic streaming configuration function */
|
|
68
|
+
getStreamingConfig?: GetStreamingConfig;
|
|
69
|
+
/** Other route exports (metadata, generateStaticParams, etc.) */
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolved streaming configuration for a request
|
|
74
|
+
*/
|
|
75
|
+
interface ResolvedStreamingConfig extends StreamingHints {
|
|
76
|
+
/** Source of the configuration */
|
|
77
|
+
source: 'static' | 'dynamic' | 'default';
|
|
78
|
+
/** Route path */
|
|
79
|
+
routePath: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Default streaming hints when none are specified
|
|
83
|
+
*/
|
|
84
|
+
declare const DEFAULT_STREAMING_HINTS: Required<StreamingHints>;
|
|
85
|
+
/**
|
|
86
|
+
* Resolve streaming configuration for a route request
|
|
87
|
+
*/
|
|
88
|
+
declare function resolveStreamingConfig(module: StreamingRouteModule, params: Record<string, string>, request?: Request, routePath?: string): Promise<ResolvedStreamingConfig>;
|
|
89
|
+
/**
|
|
90
|
+
* Load route module and extract streaming configuration
|
|
91
|
+
*/
|
|
92
|
+
declare function loadRouteWithStreaming(filePath: string, moduleLoader?: (path: string) => Promise<StreamingRouteModule>): Promise<{
|
|
93
|
+
module: StreamingRouteModule;
|
|
94
|
+
hasStreamingConfig: boolean;
|
|
95
|
+
hasGetStreamingConfig: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Determine if streaming should be used based on config and request
|
|
99
|
+
*/
|
|
100
|
+
declare function shouldStream(config: ResolvedStreamingConfig, request?: Request): {
|
|
101
|
+
stream: boolean;
|
|
102
|
+
reason: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Create an abort controller with timeout
|
|
106
|
+
*/
|
|
107
|
+
declare function createStreamingController(timeout: number): {
|
|
108
|
+
controller: AbortController;
|
|
109
|
+
signal: AbortSignal;
|
|
110
|
+
cleanup: () => void;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Generate cache key for a streaming route
|
|
114
|
+
*/
|
|
115
|
+
declare function generateCacheKey(routePath: string, params: Record<string, string>, config: StreamingHints): string;
|
|
116
|
+
/**
|
|
117
|
+
* Cache headers for static streaming fallback
|
|
118
|
+
*/
|
|
119
|
+
declare function getStreamingCacheHeaders(config: StreamingHints): Record<string, string>;
|
|
120
|
+
/**
|
|
121
|
+
* Check if a module has streaming configuration
|
|
122
|
+
*/
|
|
123
|
+
declare function hasStreamingConfig(module: unknown): module is StreamingRouteModule;
|
|
124
|
+
/**
|
|
125
|
+
* Validate streaming hints object
|
|
126
|
+
*/
|
|
127
|
+
declare function isValidStreamingHints(obj: unknown): obj is StreamingHints;
|
|
128
|
+
|
|
129
|
+
export { DEFAULT_STREAMING_HINTS, type GetStreamingConfig, type ResolvedStreamingConfig, type StreamingHints, type StreamingRouteModule, createStreamingController, generateCacheKey, getStreamingCacheHeaders, hasStreamingConfig, isValidStreamingHints, loadRouteWithStreaming, resolveStreamingConfig, shouldStream };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { DEFAULT_STREAMING_HINTS, createStreamingController, generateCacheKey, getStreamingCacheHeaders, hasStreamingConfig, isValidStreamingHints, loadRouteWithStreaming, resolveStreamingConfig, shouldStream } from '../chunk-I2B4WSHC.js';
|
|
2
|
+
//# sourceMappingURL=streaming-hints.js.map
|
|
3
|
+
//# sourceMappingURL=streaming-hints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"streaming-hints.js"}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export { FileRoute, FileRouter, FileRouterOptions, ScanResult, createFileRouter,
|
|
|
10
10
|
export { HttpMethod as FileHttpMethod, RouteHandler as FileRouteHandler, RouteHandlerContext as FileRouteHandlerContext, createRouteContext, error, json, parseBody, redirect } from './handlers/index.js';
|
|
11
11
|
export { ActionResult, CookieOptions, RedirectError, ServerAction, redirect as actionRedirect, cookies, executeAction, executeFormAction, getAction, handleActionRequest, isRedirectError, parseFormData, registerAction } from './actions/index.js';
|
|
12
12
|
export { StreamingRenderOptions, StreamingRenderResult, SuspenseBoundaryConfig, createLazyContent, createStreamingResponse, createStreamingSSR, renderWithStreaming, streamParallel, streamSequential } from './streaming/index.js';
|
|
13
|
+
export { BoundaryResolution, PriorityBoundary, PriorityStreamConfig, PriorityStreamResult, StreamingStrategy, streamWithPriority, validateDependencies } from './streaming/priority.js';
|
|
14
|
+
export { BoundaryTiming, InstrumentedStreamConfig, InstrumentedStreamResult, MetricsAggregator, StreamingMetrics, StreamingObserver, createHttpObserver, createInstrumentedStream, createLoggerObserver } from './streaming/observability.js';
|
|
15
|
+
export { BuiltInCondition, DEFAULT_BOT_PATTERNS, StreamIfConfig, StreamingCondition, addStreamingHeaders, createConditionalStreamer, createStreamingResponse as createConditionalStreamingResponse, createStaticResponse, isBot, isSlowConnection, prefersNoStream, streamIf, supportsStreaming } from './streaming/conditional.js';
|
|
16
|
+
export { AdapterOptions, FrameworkType, HTMXAdapterOptions, ReactAdapterOptions, SolidAdapterOptions, StreamingAdapter, SvelteAdapterOptions, VueAdapterOptions, createHTMXStreamAdapter, createReactStreamAdapter, createSolidStreamAdapter, createStreamAdapter, createSvelteStreamAdapter, createVueStreamAdapter } from './streaming/adapters/index.js';
|
|
17
|
+
export { CustomHydrationTrigger, HydrateOptions, HydrationTrigger, Island, IslandConfig, IslandRegistry, IslandRenderAdapter, RenderedIsland, createIslandRegistry, createPreactIslandAdapter, createReactIslandAdapter, createSolidIslandAdapter, createVueIslandAdapter, defineIsland, hydrateIslands, registerFlightIslandElement, registerIsland, renderIsland, renderIslands, setIslandAdapter } from './islands/index.js';
|
|
18
|
+
export { DEFAULT_STREAMING_HINTS, GetStreamingConfig, ResolvedStreamingConfig, StreamingHints, StreamingRouteModule, createStreamingController, generateCacheKey, getStreamingCacheHeaders, hasStreamingConfig, isValidStreamingHints, loadRouteWithStreaming, resolveStreamingConfig, shouldStream } from './file-router/streaming-hints.js';
|
|
13
19
|
export { ClientComponent, ComponentType as RSCComponentType, RenderContext as RSCRenderContext, ServerComponent, composeComponents, createAsyncComponent, createClientBoundary, createRenderContext, deserializeProps, detectComponentType, executeServerComponent, hasUseClientDirective, hasUseServerDirective, isNotFoundError, isRedirectError as isRscRedirectError, notFound, redirect as rscRedirect, revalidatePath as rscRevalidatePath, revalidateTag as rscRevalidateTag, serializeProps, serverFetch, withErrorBoundary } from './rsc/index.js';
|
|
14
20
|
|
|
15
21
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createHTMXStreamAdapter, createReactStreamAdapter, createSolidStreamAdapter, createStreamAdapter, createSvelteStreamAdapter, createVueStreamAdapter } from './chunk-MQQLYWZZ.js';
|
|
2
|
+
export { createIslandRegistry, createPreactIslandAdapter, createReactIslandAdapter, createSolidIslandAdapter, createVueIslandAdapter, defineIsland, hydrateIslands, registerFlightIslandElement, registerIsland, renderIsland, renderIslands, setIslandAdapter } from './chunk-WFAWAHJH.js';
|
|
3
|
+
export { DEFAULT_STREAMING_HINTS, createStreamingController, generateCacheKey, getStreamingCacheHeaders, hasStreamingConfig, isValidStreamingHints, loadRouteWithStreaming, resolveStreamingConfig, shouldStream } from './chunk-I2B4WSHC.js';
|
|
4
|
+
export { createFileRouter, loadRoutes, scanRoutes } from './chunk-54HPVE7N.js';
|
|
2
5
|
export { RedirectError, redirect as actionRedirect, cookies, executeAction, executeFormAction, getAction, handleActionRequest, isRedirectError, parseFormData, registerAction } from './chunk-JIW55ZVD.js';
|
|
3
6
|
export { createRouteContext, error, json, parseBody, redirect } from './chunk-W6D62JCI.js';
|
|
4
|
-
export { createLazyContent, createStreamingResponse, createStreamingSSR, renderWithStreaming, streamParallel, streamSequential } from './chunk-ABNCAPQB.js';
|
|
5
7
|
export { composeComponents, createAsyncComponent, createClientBoundary, createRenderContext, deserializeProps, detectComponentType, executeServerComponent, hasUseClientDirective, hasUseServerDirective, isNotFoundError, isRedirectError as isRscRedirectError, notFound, redirect as rscRedirect, revalidatePath as rscRevalidatePath, revalidateTag as rscRevalidateTag, serializeProps, serverFetch, withErrorBoundary } from './chunk-5KF3QQWZ.js';
|
|
8
|
+
export { streamWithPriority, validateDependencies } from './chunk-OBNYNJB5.js';
|
|
9
|
+
export { MetricsAggregator, createHttpObserver, createInstrumentedStream, createLoggerObserver } from './chunk-YIOQC3DC.js';
|
|
10
|
+
export { createLazyContent, createStreamingResponse, createStreamingSSR, renderWithStreaming, streamParallel, streamSequential } from './chunk-6WSPUG5L.js';
|
|
11
|
+
export { DEFAULT_BOT_PATTERNS, addStreamingHeaders, createConditionalStreamer, createStreamingResponse as createConditionalStreamingResponse, createStaticResponse, isBot, isSlowConnection, prefersNoStream, streamIf, supportsStreaming } from './chunk-XOIYNY4I.js';
|
|
6
12
|
import './chunk-ZVC3ZWLM.js';
|
|
7
13
|
export { createCache } from './chunk-3AIQVGTM.js';
|
|
8
14
|
import './chunk-SUILH4ID.js';
|
|
@@ -527,7 +533,7 @@ function shouldDynamicallyRender(module, hasParams) {
|
|
|
527
533
|
}
|
|
528
534
|
return false;
|
|
529
535
|
}
|
|
530
|
-
function
|
|
536
|
+
function createStreamingResponse3(stream) {
|
|
531
537
|
return new Response(stream, {
|
|
532
538
|
status: 200,
|
|
533
539
|
headers: {
|
|
@@ -860,6 +866,6 @@ function createDevToolsData(startTime, options = {}) {
|
|
|
860
866
|
// src/index.ts
|
|
861
867
|
var VERSION = "0.0.1";
|
|
862
868
|
|
|
863
|
-
export { VERSION, buildCacheControlHeader, createDefaultRouteRules, createDevToolsData, createHtmlStream, createISRCacheFromFlightCache, createMemoryISRCache, createRevalidateHandler,
|
|
869
|
+
export { VERSION, buildCacheControlHeader, createDefaultRouteRules, createDevToolsData, createHtmlStream, createISRCacheFromFlightCache, createMemoryISRCache, createRevalidateHandler, createStreamingResponse3 as createStaticStreamingResponse, expandDynamicRoutes, extractLinks, generateAllStaticPaths, getCacheOptionsFromRule, getISRCache, getOrGeneratePage, getRenderModeFromRule, getRevalidateTime, injectDevTools, isDevToolsEnabled, matchRouteRule, mergeMetadata, mergeRouteRules, prerenderRoutes, purgeAllPages, purgePage, renderMetadataToHead, resolveMetadata, revalidatePath2 as revalidatePath, revalidatePaths, revalidateTag2 as revalidateTag, setISRCache, shouldDynamicallyRender };
|
|
864
870
|
//# sourceMappingURL=index.js.map
|
|
865
871
|
//# sourceMappingURL=index.js.map
|