@flight-framework/core 0.0.2 → 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.
Files changed (44) hide show
  1. package/dist/{chunk-WOMWIW7D.js → chunk-54HPVE7N.js} +77 -6
  2. package/dist/chunk-54HPVE7N.js.map +1 -0
  3. package/dist/{chunk-ABNCAPQB.js → chunk-6WSPUG5L.js} +2 -2
  4. package/dist/chunk-6WSPUG5L.js.map +1 -0
  5. package/dist/chunk-I2B4WSHC.js +126 -0
  6. package/dist/chunk-I2B4WSHC.js.map +1 -0
  7. package/dist/chunk-MQQLYWZZ.js +288 -0
  8. package/dist/chunk-MQQLYWZZ.js.map +1 -0
  9. package/dist/chunk-OBNYNJB5.js +353 -0
  10. package/dist/chunk-OBNYNJB5.js.map +1 -0
  11. package/dist/chunk-WFAWAHJH.js +267 -0
  12. package/dist/chunk-WFAWAHJH.js.map +1 -0
  13. package/dist/chunk-XOIYNY4I.js +164 -0
  14. package/dist/chunk-XOIYNY4I.js.map +1 -0
  15. package/dist/chunk-YIOQC3DC.js +282 -0
  16. package/dist/chunk-YIOQC3DC.js.map +1 -0
  17. package/dist/file-router/index.d.ts +18 -1
  18. package/dist/file-router/index.js +1 -1
  19. package/dist/file-router/streaming-hints.d.ts +129 -0
  20. package/dist/file-router/streaming-hints.js +3 -0
  21. package/dist/file-router/streaming-hints.js.map +1 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +10 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/islands/index.d.ts +234 -0
  26. package/dist/islands/index.js +3 -0
  27. package/dist/islands/index.js.map +1 -0
  28. package/dist/streaming/adapters/index.d.ts +223 -0
  29. package/dist/streaming/adapters/index.js +3 -0
  30. package/dist/streaming/adapters/index.js.map +1 -0
  31. package/dist/streaming/conditional.d.ts +130 -0
  32. package/dist/streaming/conditional.js +3 -0
  33. package/dist/streaming/conditional.js.map +1 -0
  34. package/dist/streaming/index.d.ts +8 -0
  35. package/dist/streaming/index.js +1 -1
  36. package/dist/streaming/observability.d.ts +201 -0
  37. package/dist/streaming/observability.js +4 -0
  38. package/dist/streaming/observability.js.map +1 -0
  39. package/dist/streaming/priority.d.ts +103 -0
  40. package/dist/streaming/priority.js +3 -0
  41. package/dist/streaming/priority.js.map +1 -0
  42. package/package.json +25 -1
  43. package/dist/chunk-ABNCAPQB.js.map +0 -1
  44. package/dist/chunk-WOMWIW7D.js.map +0 -1
@@ -0,0 +1,267 @@
1
+ // src/islands/index.ts
2
+ var instanceCounter = 0;
3
+ function generateInstanceId(islandId) {
4
+ return `${islandId}:${++instanceCounter}:${Math.random().toString(36).slice(2, 7)}`;
5
+ }
6
+ function defineIsland(config) {
7
+ return (props) => ({
8
+ config,
9
+ props: { ...config.defaultProps, ...props },
10
+ instanceId: generateInstanceId(config.id)
11
+ });
12
+ }
13
+ function serializeProps(props) {
14
+ try {
15
+ return JSON.stringify(props);
16
+ } catch {
17
+ console.warn("[Flight Islands] Props are not serializable, using empty object");
18
+ return "{}";
19
+ }
20
+ }
21
+ function getHydrateAttr(trigger) {
22
+ if (typeof trigger === "object" && trigger.type === "custom") {
23
+ return `custom:${trigger.name}`;
24
+ }
25
+ return trigger;
26
+ }
27
+ var globalAdapter = null;
28
+ function setIslandAdapter(adapter) {
29
+ globalAdapter = adapter;
30
+ }
31
+ async function renderIsland(island, adapter) {
32
+ const renderAdapter = adapter || globalAdapter;
33
+ if (!renderAdapter) {
34
+ throw new Error(
35
+ "[Flight Islands] No render adapter set. Call setIslandAdapter() or pass adapter to renderIsland()."
36
+ );
37
+ }
38
+ const { config, props, instanceId } = island;
39
+ const componentHtml = await renderAdapter.renderToString(config.component, props);
40
+ const css = renderAdapter.getCSS?.(config.component);
41
+ const hydrateAttr = getHydrateAttr(config.hydrate);
42
+ const propsJson = serializeProps(props);
43
+ const html = `
44
+ <flight-island
45
+ data-island="${config.id}"
46
+ data-instance="${instanceId}"
47
+ data-hydrate="${hydrateAttr}"
48
+ ${config.mediaQuery ? `data-media="${config.mediaQuery}"` : ""}
49
+ ${config.clientEntry ? `data-entry="${config.clientEntry}"` : ""}
50
+ ${config.priority ? `data-priority="${config.priority}"` : ""}
51
+ >
52
+ ${componentHtml}
53
+ </flight-island>`;
54
+ const propsScript = `<script type="application/json" data-island-props="${instanceId}">${propsJson}</script>`;
55
+ return {
56
+ html,
57
+ id: config.id,
58
+ instanceId,
59
+ propsScript,
60
+ css
61
+ };
62
+ }
63
+ async function renderIslands(islands, adapter) {
64
+ const rendered = await Promise.all(
65
+ islands.map((island) => renderIsland(island, adapter))
66
+ );
67
+ return {
68
+ html: rendered.map((r) => r.html).join("\n"),
69
+ propsScripts: rendered.map((r) => r.propsScript).join("\n"),
70
+ css: rendered.filter((r) => r.css).map((r) => r.css).join("\n")
71
+ };
72
+ }
73
+ function createIslandRegistry() {
74
+ const loaders = /* @__PURE__ */ new Map();
75
+ return {
76
+ register(id, loader) {
77
+ loaders.set(id, loader);
78
+ },
79
+ get(id) {
80
+ return loaders.get(id);
81
+ },
82
+ has(id) {
83
+ return loaders.has(id);
84
+ }
85
+ };
86
+ }
87
+ var globalRegistry = createIslandRegistry();
88
+ function registerIsland(id, loader) {
89
+ globalRegistry.register(id, loader);
90
+ }
91
+ function hydrateIslands(options) {
92
+ const {
93
+ root = typeof document !== "undefined" ? document.body : null,
94
+ registry = globalRegistry,
95
+ onHydrate,
96
+ onError
97
+ } = options || {};
98
+ if (!root || typeof document === "undefined") {
99
+ console.warn("[Flight Islands] hydrateIslands called in non-browser environment");
100
+ return;
101
+ }
102
+ const islands = root.querySelectorAll("flight-island[data-island]");
103
+ islands.forEach((element) => {
104
+ const islandId = element.dataset.island;
105
+ const instanceId = element.dataset.instance;
106
+ const hydrateTrigger = element.dataset.hydrate;
107
+ const mediaQuery = element.dataset.media;
108
+ if (!registry.has(islandId)) {
109
+ console.warn(`[Flight Islands] No component registered for island: ${islandId}`);
110
+ return;
111
+ }
112
+ setupHydration(
113
+ element,
114
+ islandId,
115
+ instanceId,
116
+ hydrateTrigger,
117
+ mediaQuery,
118
+ registry,
119
+ onHydrate,
120
+ onError
121
+ );
122
+ });
123
+ }
124
+ function setupHydration(element, islandId, instanceId, trigger, mediaQuery, registry, onHydrate, onError) {
125
+ const hydrate = async () => {
126
+ const startTime = performance.now();
127
+ try {
128
+ const loader = registry.get(islandId);
129
+ const module = await loader();
130
+ const Component = module.default || module;
131
+ const propsScript = document.querySelector(`[data-island-props="${instanceId}"]`);
132
+ const props = propsScript ? JSON.parse(propsScript.textContent || "{}") : {};
133
+ element.dispatchEvent(new CustomEvent("flight:hydrate", {
134
+ detail: { Component, props, islandId, instanceId },
135
+ bubbles: true
136
+ }));
137
+ element.setAttribute("data-hydrated", "true");
138
+ const duration = performance.now() - startTime;
139
+ onHydrate?.(islandId, instanceId, duration);
140
+ } catch (error) {
141
+ console.error(`[Flight Islands] Failed to hydrate ${islandId}:`, error);
142
+ element.setAttribute("data-hydrate-error", "true");
143
+ onError?.(islandId, error);
144
+ }
145
+ };
146
+ if (element.hasAttribute("data-hydrated")) return;
147
+ switch (trigger) {
148
+ case "load":
149
+ hydrate();
150
+ break;
151
+ case "idle":
152
+ if ("requestIdleCallback" in window) {
153
+ window.requestIdleCallback(() => hydrate(), { timeout: 2e3 });
154
+ } else {
155
+ setTimeout(hydrate, 200);
156
+ }
157
+ break;
158
+ case "visible": {
159
+ const observer = new IntersectionObserver(
160
+ (entries) => {
161
+ entries.forEach((entry) => {
162
+ if (entry.isIntersecting) {
163
+ observer.disconnect();
164
+ hydrate();
165
+ }
166
+ });
167
+ },
168
+ { rootMargin: "50px" }
169
+ );
170
+ observer.observe(element);
171
+ break;
172
+ }
173
+ case "interaction": {
174
+ const events = ["click", "focus", "touchstart", "mouseenter"];
175
+ const onInteraction = () => {
176
+ events.forEach((e) => element.removeEventListener(e, onInteraction));
177
+ hydrate();
178
+ };
179
+ events.forEach((e) => element.addEventListener(e, onInteraction, { once: true, passive: true }));
180
+ break;
181
+ }
182
+ case "media":
183
+ if (mediaQuery) {
184
+ const mql = window.matchMedia(mediaQuery);
185
+ const check = () => {
186
+ if (mql.matches) {
187
+ mql.removeEventListener("change", check);
188
+ hydrate();
189
+ }
190
+ };
191
+ if (mql.matches) {
192
+ hydrate();
193
+ } else {
194
+ mql.addEventListener("change", check);
195
+ }
196
+ }
197
+ break;
198
+ case "never":
199
+ break;
200
+ default:
201
+ if (trigger.startsWith("custom:")) {
202
+ element.dispatchEvent(new CustomEvent("flight:custom-hydrate", {
203
+ detail: { hydrate, trigger: trigger.replace("custom:", "") },
204
+ bubbles: true
205
+ }));
206
+ }
207
+ }
208
+ }
209
+ function createReactIslandAdapter(deps) {
210
+ return {
211
+ name: "react",
212
+ async renderToString(component, props) {
213
+ const element = deps.createElement(component, props);
214
+ return deps.renderToString(element);
215
+ }
216
+ };
217
+ }
218
+ function createPreactIslandAdapter(deps) {
219
+ return {
220
+ name: "preact",
221
+ async renderToString(component, props) {
222
+ const element = deps.h(component, props);
223
+ return deps.renderToString(element);
224
+ }
225
+ };
226
+ }
227
+ function createVueIslandAdapter(deps) {
228
+ return {
229
+ name: "vue",
230
+ async renderToString(component, props) {
231
+ const app = deps.createSSRApp(component, props);
232
+ return deps.renderToString(app);
233
+ }
234
+ };
235
+ }
236
+ function createSolidIslandAdapter(deps) {
237
+ return {
238
+ name: "solid",
239
+ async renderToString(component, props) {
240
+ return deps.renderToString(() => component(props));
241
+ }
242
+ };
243
+ }
244
+ function registerFlightIslandElement() {
245
+ if (typeof customElements === "undefined") return;
246
+ if (customElements.get("flight-island")) return;
247
+ class FlightIsland extends HTMLElement {
248
+ static get observedAttributes() {
249
+ return ["data-hydrated", "data-hydrate-error"];
250
+ }
251
+ connectedCallback() {
252
+ }
253
+ attributeChangedCallback(name, _oldValue, newValue) {
254
+ if (name === "data-hydrated" && newValue === "true") {
255
+ this.classList.add("hydrated");
256
+ }
257
+ if (name === "data-hydrate-error" && newValue === "true") {
258
+ this.classList.add("hydrate-error");
259
+ }
260
+ }
261
+ }
262
+ customElements.define("flight-island", FlightIsland);
263
+ }
264
+
265
+ export { createIslandRegistry, createPreactIslandAdapter, createReactIslandAdapter, createSolidIslandAdapter, createVueIslandAdapter, defineIsland, hydrateIslands, registerFlightIslandElement, registerIsland, renderIsland, renderIslands, setIslandAdapter };
266
+ //# sourceMappingURL=chunk-WFAWAHJH.js.map
267
+ //# sourceMappingURL=chunk-WFAWAHJH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/islands/index.ts"],"names":[],"mappings":"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","file":"chunk-WFAWAHJH.js","sourcesContent":["/**\r\n * @flight-framework/core - Islands Architecture\r\n * \r\n * Selective hydration with fine-grained control over when and how\r\n * components become interactive. Framework-agnostic islands primitives.\r\n * \r\n * Best Practices 2026:\r\n * - Minimal JavaScript: Only hydrate what needs interactivity\r\n * - Lazy hydration: Load JS when actually needed\r\n * - Progressive enhancement: Works without JS\r\n * - Performance: Reduce main thread work\r\n * \r\n * @example\r\n * ```typescript\r\n * import { defineIsland, renderIsland, hydrateIslands } from '@flight-framework/core/islands';\r\n * \r\n * // Server: Define an interactive island\r\n * const Counter = defineIsland({\r\n * id: 'counter',\r\n * component: CounterComponent,\r\n * hydrate: 'visible', // Only hydrate when scrolled into view\r\n * });\r\n * \r\n * // Server: Render to HTML with island markers\r\n * const html = await renderIsland(Counter, { initial: 0 });\r\n * \r\n * // Client: Hydrate all islands on the page\r\n * hydrateIslands();\r\n * ```\r\n */\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\n/**\r\n * When to hydrate the island\r\n */\r\nexport type HydrationTrigger =\r\n | 'load' // Immediate: hydrate as soon as JS loads\r\n | 'idle' // When browser is idle (requestIdleCallback)\r\n | 'visible' // When island enters viewport (IntersectionObserver)\r\n | 'interaction' // On first user interaction (click, focus, hover)\r\n | 'media' // When media query matches\r\n | 'never' // Never hydrate (static HTML only)\r\n | CustomHydrationTrigger;\r\n\r\n/**\r\n * Custom hydration trigger function\r\n */\r\nexport interface CustomHydrationTrigger {\r\n type: 'custom';\r\n /** Name for debugging */\r\n name: string;\r\n /** Function that returns a promise resolving when hydration should occur */\r\n when: (element: HTMLElement) => Promise<void>;\r\n}\r\n\r\n/**\r\n * Island configuration\r\n */\r\nexport interface IslandConfig<P = Record<string, unknown>> {\r\n /** Unique identifier for this island type */\r\n id: string;\r\n /** The component to render (framework-specific) */\r\n component: unknown;\r\n /** When to hydrate on client */\r\n hydrate: HydrationTrigger;\r\n /** Default props */\r\n defaultProps?: P;\r\n /** Media query for 'media' trigger */\r\n mediaQuery?: string;\r\n /** Path to client-side component bundle */\r\n clientEntry?: string;\r\n /** Fallback HTML while not hydrated (optional override) */\r\n fallback?: string;\r\n /** Loading priority hint */\r\n priority?: 'high' | 'low';\r\n}\r\n\r\n/**\r\n * Island instance with props\r\n */\r\nexport interface Island<P = Record<string, unknown>> {\r\n /** Island configuration */\r\n config: IslandConfig<P>;\r\n /** Instance props */\r\n props: P;\r\n /** Unique instance ID (for multiple instances of same island type) */\r\n instanceId: string;\r\n}\r\n\r\n/**\r\n * Rendered island HTML with metadata\r\n */\r\nexport interface RenderedIsland {\r\n /** The HTML string */\r\n html: string;\r\n /** Island ID */\r\n id: string;\r\n /** Instance ID */\r\n instanceId: string;\r\n /** Serialized props for hydration */\r\n propsScript: string;\r\n /** CSS if any */\r\n css?: string;\r\n}\r\n\r\n/**\r\n * Island registry for client-side hydration\r\n */\r\nexport interface IslandRegistry {\r\n /** Register a component for an island ID */\r\n register(id: string, loader: () => Promise<unknown>): void;\r\n /** Get registered loader */\r\n get(id: string): (() => Promise<unknown>) | undefined;\r\n /** Check if island is registered */\r\n has(id: string): boolean;\r\n}\r\n\r\n/**\r\n * Client-side hydration options\r\n */\r\nexport interface HydrateOptions {\r\n /** Root element to scan for islands */\r\n root?: HTMLElement;\r\n /** Custom island registry */\r\n registry?: IslandRegistry;\r\n /** Callback when an island is hydrated */\r\n onHydrate?: (id: string, instanceId: string, duration: number) => void;\r\n /** Callback on hydration error */\r\n onError?: (id: string, error: Error) => void;\r\n}\r\n\r\n// ============================================================================\r\n// Server-Side: Island Definition\r\n// ============================================================================\r\n\r\nlet instanceCounter = 0;\r\n\r\n/**\r\n * Generate a unique instance ID\r\n */\r\nfunction generateInstanceId(islandId: string): string {\r\n return `${islandId}:${++instanceCounter}:${Math.random().toString(36).slice(2, 7)}`;\r\n}\r\n\r\n/**\r\n * Define an island component\r\n */\r\nexport function defineIsland<P = Record<string, unknown>>(\r\n config: IslandConfig<P>\r\n): (props?: P) => Island<P> {\r\n return (props?: P) => ({\r\n config,\r\n props: { ...config.defaultProps, ...props } as P,\r\n instanceId: generateInstanceId(config.id),\r\n });\r\n}\r\n\r\n/**\r\n * Serialize props for client-side hydration\r\n */\r\nfunction serializeProps(props: Record<string, unknown>): string {\r\n try {\r\n return JSON.stringify(props);\r\n } catch {\r\n console.warn('[Flight Islands] Props are not serializable, using empty object');\r\n return '{}';\r\n }\r\n}\r\n\r\n/**\r\n * Get hydration trigger attribute value\r\n */\r\nfunction getHydrateAttr(trigger: HydrationTrigger): string {\r\n if (typeof trigger === 'object' && trigger.type === 'custom') {\r\n return `custom:${trigger.name}`;\r\n }\r\n return trigger as string;\r\n}\r\n\r\n// ============================================================================\r\n// Server-Side: Island Rendering\r\n// ============================================================================\r\n\r\n/**\r\n * UI framework adapter for island rendering\r\n */\r\nexport interface IslandRenderAdapter {\r\n /** Framework name */\r\n name: string;\r\n /** Render component to HTML */\r\n renderToString(component: unknown, props: Record<string, unknown>): Promise<string>;\r\n /** Get CSS if any */\r\n getCSS?(component: unknown): string;\r\n}\r\n\r\nlet globalAdapter: IslandRenderAdapter | null = null;\r\n\r\n/**\r\n * Set the global island render adapter\r\n */\r\nexport function setIslandAdapter(adapter: IslandRenderAdapter): void {\r\n globalAdapter = adapter;\r\n}\r\n\r\n/**\r\n * Render an island to HTML with hydration markers\r\n */\r\nexport async function renderIsland<P extends Record<string, unknown>>(\r\n island: Island<P>,\r\n adapter?: IslandRenderAdapter\r\n): Promise<RenderedIsland> {\r\n const renderAdapter = adapter || globalAdapter;\r\n\r\n if (!renderAdapter) {\r\n throw new Error(\r\n '[Flight Islands] No render adapter set. Call setIslandAdapter() or pass adapter to renderIsland().'\r\n );\r\n }\r\n\r\n const { config, props, instanceId } = island;\r\n\r\n // Render component to HTML\r\n const componentHtml = await renderAdapter.renderToString(config.component, props);\r\n const css = renderAdapter.getCSS?.(config.component);\r\n\r\n // Build island wrapper with data attributes\r\n const hydrateAttr = getHydrateAttr(config.hydrate);\r\n const propsJson = serializeProps(props);\r\n\r\n const html = `\r\n<flight-island \r\n data-island=\"${config.id}\"\r\n data-instance=\"${instanceId}\"\r\n data-hydrate=\"${hydrateAttr}\"\r\n ${config.mediaQuery ? `data-media=\"${config.mediaQuery}\"` : ''}\r\n ${config.clientEntry ? `data-entry=\"${config.clientEntry}\"` : ''}\r\n ${config.priority ? `data-priority=\"${config.priority}\"` : ''}\r\n>\r\n ${componentHtml}\r\n</flight-island>`;\r\n\r\n // Props script (inline for hydration)\r\n const propsScript = `<script type=\"application/json\" data-island-props=\"${instanceId}\">${propsJson}</script>`;\r\n\r\n return {\r\n html,\r\n id: config.id,\r\n instanceId,\r\n propsScript,\r\n css,\r\n };\r\n}\r\n\r\n/**\r\n * Render multiple islands and collect their outputs\r\n */\r\nexport async function renderIslands<P extends Record<string, unknown>>(\r\n islands: Island<P>[],\r\n adapter?: IslandRenderAdapter\r\n): Promise<{\r\n html: string;\r\n propsScripts: string;\r\n css: string;\r\n}> {\r\n const rendered = await Promise.all(\r\n islands.map(island => renderIsland(island, adapter))\r\n );\r\n\r\n return {\r\n html: rendered.map(r => r.html).join('\\n'),\r\n propsScripts: rendered.map(r => r.propsScript).join('\\n'),\r\n css: rendered.filter(r => r.css).map(r => r.css).join('\\n'),\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Client-Side: Island Hydration\r\n// ============================================================================\r\n\r\n/**\r\n * Create an island registry for client-side hydration\r\n */\r\nexport function createIslandRegistry(): IslandRegistry {\r\n const loaders = new Map<string, () => Promise<unknown>>();\r\n\r\n return {\r\n register(id: string, loader: () => Promise<unknown>) {\r\n loaders.set(id, loader);\r\n },\r\n get(id: string) {\r\n return loaders.get(id);\r\n },\r\n has(id: string) {\r\n return loaders.has(id);\r\n },\r\n };\r\n}\r\n\r\n// Global registry for convenience\r\nconst globalRegistry = createIslandRegistry();\r\n\r\n/**\r\n * Register an island component for client-side hydration\r\n */\r\nexport function registerIsland(id: string, loader: () => Promise<unknown>): void {\r\n globalRegistry.register(id, loader);\r\n}\r\n\r\n/**\r\n * Client-side hydration bootstrapper\r\n * Call this in your client entry point\r\n */\r\nexport function hydrateIslands(options?: HydrateOptions): void {\r\n const {\r\n root = typeof document !== 'undefined' ? document.body : null,\r\n registry = globalRegistry,\r\n onHydrate,\r\n onError,\r\n } = options || {};\r\n\r\n if (!root || typeof document === 'undefined') {\r\n console.warn('[Flight Islands] hydrateIslands called in non-browser environment');\r\n return;\r\n }\r\n\r\n // Find all island elements\r\n const islands = root.querySelectorAll<HTMLElement>('flight-island[data-island]');\r\n\r\n islands.forEach((element) => {\r\n const islandId = element.dataset.island!;\r\n const instanceId = element.dataset.instance!;\r\n const hydrateTrigger = element.dataset.hydrate as string;\r\n const mediaQuery = element.dataset.media;\r\n\r\n // Check if component is registered\r\n if (!registry.has(islandId)) {\r\n console.warn(`[Flight Islands] No component registered for island: ${islandId}`);\r\n return;\r\n }\r\n\r\n // Setup hydration based on trigger\r\n setupHydration(\r\n element,\r\n islandId,\r\n instanceId,\r\n hydrateTrigger,\r\n mediaQuery,\r\n registry,\r\n onHydrate,\r\n onError\r\n );\r\n });\r\n}\r\n\r\n/**\r\n * Setup hydration trigger for an island element\r\n */\r\nfunction setupHydration(\r\n element: HTMLElement,\r\n islandId: string,\r\n instanceId: string,\r\n trigger: string,\r\n mediaQuery: string | undefined,\r\n registry: IslandRegistry,\r\n onHydrate?: (id: string, instanceId: string, duration: number) => void,\r\n onError?: (id: string, error: Error) => void\r\n): void {\r\n const hydrate = async () => {\r\n const startTime = performance.now();\r\n\r\n try {\r\n // Load component\r\n const loader = registry.get(islandId)!;\r\n const module = await loader() as { default?: unknown };\r\n const Component = module.default || module;\r\n\r\n // Get props\r\n const propsScript = document.querySelector(`[data-island-props=\"${instanceId}\"]`);\r\n const props = propsScript ? JSON.parse(propsScript.textContent || '{}') : {};\r\n\r\n // Hydrate (framework-specific, emit event for now)\r\n element.dispatchEvent(new CustomEvent('flight:hydrate', {\r\n detail: { Component, props, islandId, instanceId },\r\n bubbles: true,\r\n }));\r\n\r\n // Mark as hydrated\r\n element.setAttribute('data-hydrated', 'true');\r\n\r\n const duration = performance.now() - startTime;\r\n onHydrate?.(islandId, instanceId, duration);\r\n\r\n } catch (error) {\r\n console.error(`[Flight Islands] Failed to hydrate ${islandId}:`, error);\r\n element.setAttribute('data-hydrate-error', 'true');\r\n onError?.(islandId, error as Error);\r\n }\r\n };\r\n\r\n // Skip if already hydrated\r\n if (element.hasAttribute('data-hydrated')) return;\r\n\r\n switch (trigger) {\r\n case 'load':\r\n // Immediate hydration\r\n hydrate();\r\n break;\r\n\r\n case 'idle':\r\n // When browser is idle\r\n if ('requestIdleCallback' in window) {\r\n (window as unknown as { requestIdleCallback: (cb: () => void, opts?: { timeout: number }) => void })\r\n .requestIdleCallback(() => hydrate(), { timeout: 2000 });\r\n } else {\r\n setTimeout(hydrate, 200);\r\n }\r\n break;\r\n\r\n case 'visible': {\r\n // When element enters viewport\r\n const observer = new IntersectionObserver(\r\n (entries) => {\r\n entries.forEach((entry) => {\r\n if (entry.isIntersecting) {\r\n observer.disconnect();\r\n hydrate();\r\n }\r\n });\r\n },\r\n { rootMargin: '50px' }\r\n );\r\n observer.observe(element);\r\n break;\r\n }\r\n\r\n case 'interaction': {\r\n // On first user interaction\r\n const events = ['click', 'focus', 'touchstart', 'mouseenter'];\r\n const onInteraction = () => {\r\n events.forEach(e => element.removeEventListener(e, onInteraction));\r\n hydrate();\r\n };\r\n events.forEach(e => element.addEventListener(e, onInteraction, { once: true, passive: true }));\r\n break;\r\n }\r\n\r\n case 'media':\r\n // When media query matches\r\n if (mediaQuery) {\r\n const mql = window.matchMedia(mediaQuery);\r\n const check = () => {\r\n if (mql.matches) {\r\n mql.removeEventListener('change', check);\r\n hydrate();\r\n }\r\n };\r\n if (mql.matches) {\r\n hydrate();\r\n } else {\r\n mql.addEventListener('change', check);\r\n }\r\n }\r\n break;\r\n\r\n case 'never':\r\n // Never hydrate\r\n break;\r\n\r\n default:\r\n // Custom trigger\r\n if (trigger.startsWith('custom:')) {\r\n // Custom triggers need manual setup\r\n element.dispatchEvent(new CustomEvent('flight:custom-hydrate', {\r\n detail: { hydrate, trigger: trigger.replace('custom:', '') },\r\n bubbles: true,\r\n }));\r\n }\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Utility: Framework-Specific Adapter Factories\r\n// These return adapters that users configure with their own framework imports\r\n// ============================================================================\r\n\r\n/**\r\n * Create a React island render adapter\r\n * \r\n * @example\r\n * ```typescript\r\n * import { renderToString } from 'react-dom/server';\r\n * import { createElement } from 'react';\r\n * \r\n * const reactAdapter = createReactIslandAdapter({\r\n * renderToString,\r\n * createElement,\r\n * });\r\n * ```\r\n */\r\nexport function createReactIslandAdapter(deps: {\r\n renderToString: (element: unknown) => string;\r\n createElement: (type: unknown, props?: unknown) => unknown;\r\n}): IslandRenderAdapter {\r\n return {\r\n name: 'react',\r\n async renderToString(component, props) {\r\n const element = deps.createElement(component, props);\r\n return deps.renderToString(element);\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create a Preact island render adapter\r\n * \r\n * @example\r\n * ```typescript\r\n * import { renderToString } from 'preact-render-to-string';\r\n * import { h } from 'preact';\r\n * \r\n * const preactAdapter = createPreactIslandAdapter({\r\n * renderToString,\r\n * h,\r\n * });\r\n * ```\r\n */\r\nexport function createPreactIslandAdapter(deps: {\r\n renderToString: (element: unknown) => string;\r\n h: (type: unknown, props?: unknown) => unknown;\r\n}): IslandRenderAdapter {\r\n return {\r\n name: 'preact',\r\n async renderToString(component, props) {\r\n const element = deps.h(component, props);\r\n return deps.renderToString(element);\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create a Vue island render adapter\r\n * \r\n * @example\r\n * ```typescript\r\n * import { renderToString, createSSRApp } from 'vue/server-renderer';\r\n * \r\n * const vueAdapter = createVueIslandAdapter({\r\n * renderToString,\r\n * createSSRApp,\r\n * });\r\n * ```\r\n */\r\nexport function createVueIslandAdapter(deps: {\r\n renderToString: (app: unknown) => Promise<string>;\r\n createSSRApp: (component: unknown, props?: unknown) => unknown;\r\n}): IslandRenderAdapter {\r\n return {\r\n name: 'vue',\r\n async renderToString(component, props) {\r\n const app = deps.createSSRApp(component, props);\r\n return deps.renderToString(app);\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create a Solid island render adapter\r\n * \r\n * @example\r\n * ```typescript\r\n * import { renderToString } from 'solid-js/web';\r\n * \r\n * const solidAdapter = createSolidIslandAdapter({\r\n * renderToString,\r\n * });\r\n * ```\r\n */\r\nexport function createSolidIslandAdapter(deps: {\r\n renderToString: (fn: () => unknown) => string;\r\n}): IslandRenderAdapter {\r\n return {\r\n name: 'solid',\r\n async renderToString(component, props) {\r\n // Solid uses a function component pattern\r\n return deps.renderToString(() => (component as (p: unknown) => unknown)(props));\r\n },\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Custom Element Registration (optional)\r\n// ============================================================================\r\n\r\n/**\r\n * Register the flight-island custom element\r\n * This provides additional functionality like slot support\r\n */\r\nexport function registerFlightIslandElement(): void {\r\n if (typeof customElements === 'undefined') return;\r\n if (customElements.get('flight-island')) return;\r\n\r\n class FlightIsland extends HTMLElement {\r\n static get observedAttributes() {\r\n return ['data-hydrated', 'data-hydrate-error'];\r\n }\r\n\r\n connectedCallback() {\r\n // Could add loading indicator logic here\r\n }\r\n\r\n attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\r\n if (name === 'data-hydrated' && newValue === 'true') {\r\n this.classList.add('hydrated');\r\n }\r\n if (name === 'data-hydrate-error' && newValue === 'true') {\r\n this.classList.add('hydrate-error');\r\n }\r\n }\r\n }\r\n\r\n customElements.define('flight-island', FlightIsland);\r\n}\r\n"]}
@@ -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-XOIYNY4I.js.map
164
+ //# sourceMappingURL=chunk-XOIYNY4I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/streaming/conditional.ts"],"names":["result"],"mappings":";AAkFO,IAAM,oBAAA,GAAiC;AAAA,EAC1C,YAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA;AAAA,EACA,cAAA;AAAA,EACA,cAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA;AAAA,EACA,aAAA;AAAA,EACA,cAAA;AAAA,EACA,WAAA;AAAA,EACA,cAAA;AAAA,EACA,aAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,WAAA;AAAA;AAAA,EACA,aAAA;AAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA;AACJ;AAKO,SAAS,KAAA,CAAM,SAAkB,cAAA,EAAoC;AACxE,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,IAAK,EAAA;AACvD,EAAA,MAAM,WAAW,cAAA,IAAkB,oBAAA;AACnC,EAAA,OAAO,SAAS,IAAA,CAAK,CAAA,OAAA,KAAW,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAC,CAAA;AAC3D;AAKO,SAAS,gBAAgB,OAAA,EAA2B;AAEvD,EAAA,IAAI,QAAQ,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA,KAAM,QAAQ,OAAO,IAAA;AAC1D,EAAA,IAAI,QAAQ,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA,KAAM,QAAQ,OAAO,IAAA;AAG9D,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA,IAAK,EAAA;AAChD,EAAA,IAAI,MAAA,CAAO,QAAA,CAAS,4BAA4B,CAAA,EAAG,OAAO,IAAA;AAE1D,EAAA,OAAO,KAAA;AACX;AAKO,SAAS,iBAAiB,OAAA,EAA2B;AAExD,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,KAAK,CAAA;AACrC,EAAA,IAAI,GAAA,IAAO,CAAC,SAAA,EAAW,IAAI,EAAE,QAAA,CAAS,GAAG,GAAG,OAAO,IAAA;AAGnD,EAAA,IAAI,QAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,KAAM,MAAM,OAAO,IAAA;AAGtD,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA;AAC/C,EAAA,IAAI,QAAA,IAAY,UAAA,CAAW,QAAQ,CAAA,GAAI,GAAG,OAAO,IAAA;AAEjD,EAAA,OAAO,KAAA;AACX;AAKO,SAAS,kBAAkB,OAAA,EAA2B;AACzD,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,IAAK,EAAA;AAGvD,EAAA,IAAI,eAAA,CAAgB,IAAA,CAAK,SAAS,CAAA,EAAG,OAAO,KAAA;AAG5C,EAAA,IAAI,aAAA,CAAc,IAAA,CAAK,SAAS,CAAA,EAAG,OAAO,KAAA;AAG1C,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA;AAC/C,EAAA,IAAI,OAAA,EAAS;AAET,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,OAAO,IAAA;AACX;AAMA,SAAS,mBAAA,CACL,WACA,WAAA,EACkB;AAClB,EAAA,QAAQ,SAAA;AAAW,IACf,KAAK,eAAA;AACD,MAAA,OAAO,MAAM,IAAA;AAAA,IACjB,KAAK,cAAA;AACD,MAAA,OAAO,MAAM,KAAA;AAAA,IACjB,KAAK,SAAA;AACD,MAAA,OAAO,CAAC,QAAQ,CAAC,KAAA,CAAM,KAAK,WAAW,CAAA,IAAK,CAAC,eAAA,CAAgB,GAAG,CAAA;AAAA,IACpE,KAAK,cAAA;AACD,MAAA,OAAO,CAAC,QAAQ,CAAC,gBAAA,CAAiB,GAAG,CAAA,IAAK,CAAC,KAAA,CAAM,GAAA,EAAK,WAAW,CAAA;AAAA,IACrE,KAAK,gBAAA;AACD,MAAA,OAAO,CAAC,QAAQ,iBAAA,CAAkB,GAAG,KAAK,CAAC,KAAA,CAAM,KAAK,WAAW,CAAA;AAAA;AAE7E;AASA,eAAsB,SAClB,MAAA,EAC0D;AAC1D,EAAA,MAAM;AAAA,IACF,OAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA;AAAA,IACA,MAAA,EAAQ,QAAA;AAAA,IACR,WAAA;AAAA,IACA,gBAAA,GAAmB,UAAA;AAAA,IACnB,gBAAA,GAAmB;AAAA,GACvB,GAAI,MAAA;AAGJ,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,gBAAgB,CAAA,EAAG;AACxC,IAAA,MAAMA,OAAAA,GAAS,MAAM,MAAA,EAAO;AAC5B,IAAA,OAAO,EAAE,MAAA,EAAAA,OAAAA,EAAQ,SAAA,EAAW,IAAA,EAAM,QAAQ,wBAAA,EAAyB;AAAA,EACvE;AACA,EAAA,IAAI,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,gBAAgB,CAAA,EAAG;AACxC,IAAA,MAAMA,OAAAA,GAAS,MAAM,QAAA,EAAS;AAC9B,IAAA,OAAO,EAAE,MAAA,EAAAA,OAAAA,EAAQ,SAAA,EAAW,KAAA,EAAO,QAAQ,wBAAA,EAAyB;AAAA,EACxE;AAGA,EAAA,MAAM,cAAc,OAAO,SAAA,KAAc,aACnC,SAAA,GACA,mBAAA,CAAoB,WAAW,WAAW,CAAA;AAGhD,EAAA,IAAI,YAAA;AACJ,EAAA,IAAI,MAAA;AAEJ,EAAA,IAAI;AACA,IAAA,YAAA,GAAe,MAAM,YAAY,OAAO,CAAA;AACxC,IAAA,MAAA,GAAS,eAAe,kBAAA,GAAqB,kBAAA;AAAA,EACjD,SAAS,KAAA,EAAO;AAEZ,IAAA,YAAA,GAAe,KAAA;AACf,IAAA,MAAA,GAAS,CAAA,iBAAA,EAAoB,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,UAAU,SAAS,CAAA,CAAA;AAAA,EACnF;AAGA,EAAA,IAAI,OAAO,cAAc,QAAA,EAAU;AAC/B,IAAA,IAAI,CAAC,YAAA,IAAgB,SAAA,KAAc,SAAA,EAAW;AAC1C,MAAA,MAAA,GAAS,KAAA,CAAM,OAAA,EAAS,WAAW,CAAA,GAC7B,cAAA,GACA,2BAAA;AAAA,IACV,CAAA,MAAA,IAAW,CAAC,YAAA,IAAgB,SAAA,KAAc,cAAA,EAAgB;AACtD,MAAA,MAAA,GAAS,gBAAA,CAAiB,OAAO,CAAA,GAC3B,0BAAA,GACA,cAAA;AAAA,IACV;AAAA,EACJ;AAGA,EAAA,MAAM,SAAS,YAAA,GAAe,MAAM,MAAA,EAAO,GAAI,MAAM,QAAA,EAAS;AAC9D,EAAA,OAAO,EAAE,MAAA,EAAQ,SAAA,EAAW,YAAA,EAAc,MAAA,EAAO;AACrD;AAKO,SAAS,yBAAA,CACZ,kBACA,OAAA,EAKF;AACE,EAAA,OAAO,OAAO,MAAA,KAKR;AACF,IAAA,OAAO,QAAA,CAAS;AAAA,MACZ,SAAS,MAAA,CAAO,OAAA;AAAA,MAChB,SAAA,EAAW,OAAO,SAAA,IAAa,gBAAA;AAAA,MAC/B,QAAQ,MAAA,CAAO,MAAA;AAAA,MACf,QAAQ,MAAA,CAAO,MAAA;AAAA,MACf,GAAG;AAAA,KACN,CAAA;AAAA,EACL,CAAA;AACJ;AASO,SAAS,mBAAA,CACZ,UACA,QAAA,EACQ;AACR,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,QAAA,CAAS,OAAO,CAAA;AAC5C,EAAA,OAAA,CAAQ,GAAA,CAAI,oBAAA,EAAsB,QAAA,CAAS,SAAA,GAAY,SAAS,OAAO,CAAA;AACvE,EAAA,OAAA,CAAQ,GAAA,CAAI,2BAAA,EAA6B,QAAA,CAAS,MAAM,CAAA;AAExD,EAAA,OAAO,IAAI,QAAA,CAAS,QAAA,CAAS,IAAA,EAAM;AAAA,IAC/B,QAAQ,QAAA,CAAS,MAAA;AAAA,IACjB,YAAY,QAAA,CAAS,UAAA;AAAA,IACrB;AAAA,GACH,CAAA;AACL;AAKO,SAAS,uBAAA,CACZ,QACA,OAAA,EAKQ;AACR,EAAA,MAAM,EAAE,MAAA,GAAS,GAAA,EAAK,OAAA,GAAU,IAAI,WAAA,GAAc,IAAA,EAAK,GAAI,OAAA,IAAW,EAAC;AAEvE,EAAA,OAAO,IAAI,SAAS,MAAA,EAAQ;AAAA,IACxB,MAAA;AAAA,IACA,OAAA,EAAS;AAAA,MACL,cAAA,EAAgB,0BAAA;AAAA,MAChB,mBAAA,EAAqB,cAAc,SAAA,GAAY,MAAA;AAAA,MAC/C,wBAAA,EAA0B,SAAA;AAAA,MAC1B,oBAAA,EAAsB,cAAc,MAAA,GAAS,OAAA;AAAA,MAC7C,GAAG;AAAA;AACP,GACH,CAAA;AACL;AAKO,SAAS,oBAAA,CACZ,MACA,OAAA,EAIQ;AACR,EAAA,MAAM,EAAE,SAAS,GAAA,EAAK,OAAA,GAAU,EAAC,EAAE,GAAI,WAAW,EAAC;AAEnD,EAAA,OAAO,IAAI,SAAS,IAAA,EAAM;AAAA,IACtB,MAAA;AAAA,IACA,OAAA,EAAS;AAAA,MACL,cAAA,EAAgB,0BAAA;AAAA,MAChB,oBAAA,EAAsB,OAAA;AAAA,MACtB,GAAG;AAAA;AACP,GACH,CAAA;AACL","file":"chunk-XOIYNY4I.js","sourcesContent":["/**\r\n * @flight-framework/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 '@flight-framework/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"]}