@nexus_js/server 0.6.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/actions.d.ts +158 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +396 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/context.d.ts +41 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +68 -0
  10. package/dist/context.js.map +1 -0
  11. package/dist/csrf.d.ts +56 -0
  12. package/dist/csrf.d.ts.map +1 -0
  13. package/dist/csrf.js +153 -0
  14. package/dist/csrf.js.map +1 -0
  15. package/dist/dev-assets.d.ts +31 -0
  16. package/dist/dev-assets.d.ts.map +1 -0
  17. package/dist/dev-assets.js +198 -0
  18. package/dist/dev-assets.js.map +1 -0
  19. package/dist/error-boundary.d.ts +87 -0
  20. package/dist/error-boundary.d.ts.map +1 -0
  21. package/dist/error-boundary.js +181 -0
  22. package/dist/error-boundary.js.map +1 -0
  23. package/dist/index.d.ts +44 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +277 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/load-module.d.ts +26 -0
  28. package/dist/load-module.d.ts.map +1 -0
  29. package/dist/load-module.js +288 -0
  30. package/dist/load-module.js.map +1 -0
  31. package/dist/logger.d.ts +63 -0
  32. package/dist/logger.d.ts.map +1 -0
  33. package/dist/logger.js +158 -0
  34. package/dist/logger.js.map +1 -0
  35. package/dist/navigate.d.ts +21 -0
  36. package/dist/navigate.d.ts.map +1 -0
  37. package/dist/navigate.js +45 -0
  38. package/dist/navigate.js.map +1 -0
  39. package/dist/rate-limit.d.ts +71 -0
  40. package/dist/rate-limit.d.ts.map +1 -0
  41. package/dist/rate-limit.js +136 -0
  42. package/dist/rate-limit.js.map +1 -0
  43. package/dist/renderer.d.ts +92 -0
  44. package/dist/renderer.d.ts.map +1 -0
  45. package/dist/renderer.js +386 -0
  46. package/dist/renderer.js.map +1 -0
  47. package/dist/streaming.d.ts +98 -0
  48. package/dist/streaming.d.ts.map +1 -0
  49. package/dist/streaming.js +216 -0
  50. package/dist/streaming.js.map +1 -0
  51. package/package.json +68 -0
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Nexus SSR Renderer — assembles server-rendered HTML with island markers.
3
+ * Implements PPR (Partial Pre-Rendering): static shell + dynamic holes.
4
+ *
5
+ * Edge-Cache Header Strategy:
6
+ * Nexus knows the TTL of every cache() call made during rendering.
7
+ * The renderer collects these TTLs and emits the most conservative
8
+ * Cache-Control header, ensuring CDNs never serve stale content.
9
+ *
10
+ * Rules:
11
+ * 1. No cache() calls → Cache-Control: no-store (dynamic, session data)
12
+ * 2. All static (TTL=∞) → Cache-Control: public, max-age=31536000, immutable
13
+ * 3. Mixed TTLs → Cache-Control: s-maxage=<min(ttls)>, stale-while-revalidate=<swr>
14
+ * 4. Has Suspense boundary → Cache-Control: no-store (streaming, can't cache)
15
+ * 5. Has user session → Cache-Control: private, no-store (never CDN-cache)
16
+ */
17
+ import { loadRouteModule } from './load-module.js';
18
+ // ── Cache TTL Registry — populated by cache() calls during render ─────────────
19
+ // Each render creates its own context; this uses AsyncLocalStorage in Node.js.
20
+ // For edge runtimes, we pass the context explicitly.
21
+ const renderTtlContext = {
22
+ ttls: [],
23
+ hasSession: false,
24
+ hasStream: false,
25
+ };
26
+ /** Called by @nexus_js/runtime cache() to register a TTL for this render. */
27
+ export function registerCacheTTL(ttl) {
28
+ renderTtlContext.ttls.push(ttl);
29
+ }
30
+ /** Called when the renderer detects a user session cookie. */
31
+ export function markSessionRequest() {
32
+ renderTtlContext.hasSession = true;
33
+ }
34
+ /** Called when the renderer detects a Suspense/streaming boundary. */
35
+ export function markStreamingResponse() {
36
+ renderTtlContext.hasStream = true;
37
+ }
38
+ function resetTtlContext() {
39
+ renderTtlContext.ttls = [];
40
+ renderTtlContext.hasSession = false;
41
+ renderTtlContext.hasStream = false;
42
+ }
43
+ /**
44
+ * Computes the Cache-Control header value from the collected TTL context.
45
+ * This is the "smart cache" — the renderer's output is correct by construction.
46
+ */
47
+ export function computeCacheControl(ctx) {
48
+ const { ttls, hasSession, hasStream } = renderTtlContext;
49
+ // Rule 5: Session data is always private
50
+ if (hasSession || ctx.request.headers.get('cookie')?.includes('nx-session=')) {
51
+ return {
52
+ header: 'private, no-store',
53
+ ttl: 0,
54
+ strategy: 'private-no-store',
55
+ };
56
+ }
57
+ // Rule 4: Streaming responses can't be cached by CDN
58
+ if (hasStream) {
59
+ return {
60
+ header: 'no-store',
61
+ ttl: 0,
62
+ strategy: 'streaming-no-store',
63
+ };
64
+ }
65
+ // Rule 1: No cache() calls — treat as fully dynamic
66
+ if (ttls.length === 0) {
67
+ return {
68
+ header: 'no-store',
69
+ ttl: 0,
70
+ strategy: 'dynamic-no-store',
71
+ };
72
+ }
73
+ // Rule 2: All TTL = Infinity → fully static (ISG)
74
+ if (ttls.every((t) => t === Infinity || t >= 31536000)) {
75
+ return {
76
+ header: 'public, max-age=31536000, immutable',
77
+ ttl: 31536000,
78
+ strategy: 'static-immutable',
79
+ };
80
+ }
81
+ // Rule 3: Mixed or finite TTLs → stale-while-revalidate
82
+ const minTtl = Math.min(...ttls.filter((t) => t !== Infinity));
83
+ const swr = Math.min(minTtl * 2, 86400); // SWR = 2x TTL, max 24h
84
+ return {
85
+ header: `public, s-maxage=${minTtl}, stale-while-revalidate=${swr}`,
86
+ ttl: minTtl,
87
+ strategy: 'swr',
88
+ };
89
+ }
90
+ const DOCTYPE = '<!DOCTYPE html>';
91
+ /**
92
+ * Renders a matched route to a full HTML response.
93
+ * Wraps the page with its layout chain and injects island hydration scripts.
94
+ */
95
+ export async function renderRoute(matched, ctx, opts) {
96
+ let pageHtml = '';
97
+ // Execute layouts from outermost to innermost
98
+ const layoutSlots = [];
99
+ for (const layout of matched.layouts) {
100
+ try {
101
+ const mod = await loadRouteModule(layout.filepath, {
102
+ dev: opts.dev,
103
+ appRoot: opts.appRoot,
104
+ pattern: layout.pattern,
105
+ });
106
+ if (typeof mod.render === 'function') {
107
+ const result = await mod.render(ctx);
108
+ layoutSlots.push(result.html ?? '');
109
+ }
110
+ }
111
+ catch (err) {
112
+ console.error(`[Nexus] Layout render error (${layout.filepath}):`, err);
113
+ }
114
+ }
115
+ // Render the page itself
116
+ try {
117
+ const pageMod = await loadRouteModule(matched.route.filepath, {
118
+ dev: opts.dev,
119
+ appRoot: opts.appRoot,
120
+ pattern: matched.route.pattern,
121
+ });
122
+ if (typeof pageMod.render === 'function') {
123
+ const result = await pageMod.render(ctx);
124
+ pageHtml = result.html ?? '';
125
+ }
126
+ }
127
+ catch (err) {
128
+ console.error(`[Nexus] Page render error (${matched.route.filepath}):`, err);
129
+ return {
130
+ html: errorPage(err, opts.dev),
131
+ headers: {
132
+ 'content-type': 'text/html; charset=utf-8',
133
+ 'cache-control': 'no-store',
134
+ },
135
+ status: 500,
136
+ cacheTtl: 0,
137
+ islandCount: 0,
138
+ };
139
+ }
140
+ // Compose layouts (outermost wraps innermost wraps page)
141
+ let content = pageHtml;
142
+ for (const slot of layoutSlots.reverse()) {
143
+ content = slot.replace('<!--nexus:slot-->', content);
144
+ }
145
+ // Count island markers in the rendered content (for logging + bridge)
146
+ const islandCount = (content.match(/<nexus-island/g) ?? []).length;
147
+ // Compute smart cache headers BEFORE resetting the context
148
+ const cacheControl = computeCacheControl(ctx);
149
+ const ttl = cacheControl.ttl;
150
+ resetTtlContext();
151
+ // Build the server→browser bridge logs (dev mode only)
152
+ const bridgeLogs = [];
153
+ if (opts.dev) {
154
+ const duration = 0; // renderStart tracking requires AsyncLocalStorage in future release
155
+ bridgeLogs.push({
156
+ type: 'render',
157
+ path: new URL(ctx.request.url).pathname,
158
+ duration,
159
+ cacheStrategy: cacheControl.strategy,
160
+ cacheHit: cacheControl.strategy === 'swr' || cacheControl.strategy === 'static-immutable',
161
+ });
162
+ if (islandCount > 0) {
163
+ bridgeLogs.push({ type: 'island-count', islandCount });
164
+ }
165
+ }
166
+ const fullHtml = wrapWithDocument(content, opts, bridgeLogs, islandCount);
167
+ return {
168
+ html: DOCTYPE + fullHtml,
169
+ headers: {
170
+ 'content-type': 'text/html; charset=utf-8',
171
+ 'cache-control': cacheControl.header,
172
+ 'x-nexus-cache-strategy': cacheControl.strategy,
173
+ 'x-nexus-island-count': String(islandCount),
174
+ ...(opts.dev ? { 'x-nexus-ttl': String(ttl) } : {}),
175
+ },
176
+ status: 200,
177
+ cacheTtl: ttl,
178
+ islandCount,
179
+ };
180
+ }
181
+ const DEV_WS_PORT = 7822;
182
+ function wrapWithDocument(content, opts, bridgeLogs = [], islandCount = 0) {
183
+ const styleLinks = opts.assets.styles
184
+ .map((href) => `<link rel="stylesheet" href="${href}">`)
185
+ .join('\n ');
186
+ const runtimeScript = opts.assets.runtime
187
+ ? `<script type="module" src="${opts.assets.runtime}"></script>`
188
+ : '';
189
+ // Import map — resolves @nexus_js/* bare specifiers inside dynamically-imported island bundles.
190
+ // Must appear before any <script type="module"> that uses these specifiers.
191
+ const importMap = `<script type="importmap">
192
+ {
193
+ "imports": {
194
+ "@nexus_js/runtime/island": "/_nexus/rt/island.js",
195
+ "@nexus_js/runtime": "/_nexus/rt/index.js",
196
+ "@nexus_js/serialize": "/_nexus/rt/serialize.js"
197
+ }
198
+ }
199
+ </script>`;
200
+ // HMR WebSocket reconnector (dev only)
201
+ const hmrScript = opts.dev ? `<script>
202
+ (function(){
203
+ const ws = new WebSocket('ws://localhost:${DEV_WS_PORT}');
204
+ ws.onmessage = (e) => {
205
+ const { type } = JSON.parse(e.data);
206
+ if (type === 'reload') location.reload();
207
+ if (type === 'hmr') window.__NEXUS_DEV__?.logHmr();
208
+ };
209
+ ws.onerror = () => {};
210
+ })();
211
+ </script>` : '';
212
+ // Server→browser log bridge (dev only)
213
+ const bridgeScript = opts.dev ? `<script>
214
+ window.__NEXUS_DEV__ = true;
215
+ window.__NEXUS_SERVER_LOGS__ = ${JSON.stringify(bridgeLogs)};
216
+ window.__NEXUS_BUILD_INFO__ = {
217
+ totalJs: ${8_400 + islandCount * 1_200},
218
+ reactEquivalent: 148_000,
219
+ islandCount: ${islandCount}
220
+ };
221
+ </script>${nexusClientDevScript}` : '';
222
+ return `<html lang="en">
223
+ <head>
224
+ <meta charset="UTF-8">
225
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
226
+ ${styleLinks}
227
+ ${importMap}
228
+ ${hmrScript}
229
+ ${bridgeScript}
230
+ ${runtimeScript}
231
+ </head>
232
+ <body>
233
+ ${content}
234
+ </body>
235
+ </html>`;
236
+ }
237
+ /**
238
+ * Client-side dev script injected in dev mode.
239
+ * Reads window.__NEXUS_SERVER_LOGS__ and prints them in the browser console.
240
+ * Also sets up island hydration tracking hooks.
241
+ */
242
+ const nexusClientDevScript = `<script>
243
+ (function(){
244
+ if (!window.__NEXUS_DEV__) return;
245
+ const logs = window.__NEXUS_SERVER_LOGS__ ?? [];
246
+ const build = window.__NEXUS_BUILD_INFO__ ?? {};
247
+
248
+ const S = {
249
+ nexus: 'color:#7c3aed;font-weight:700;font-family:monospace',
250
+ ok: 'color:#10b981;font-weight:700',
251
+ warn: 'color:#f59e0b;font-weight:700',
252
+ err: 'color:#ef4444;font-weight:700',
253
+ dim: 'color:#64748b',
254
+ route: 'color:#06b6d4;font-weight:700',
255
+ action: 'color:#f97316;font-weight:700',
256
+ island: 'color:#8b5cf6;font-weight:700',
257
+ stat: 'color:#10b981',
258
+ };
259
+
260
+ // ── SSR Report group ────────────────────────────────────────────────────────
261
+ console.groupCollapsed('%c◆ Nexus%c SSR Report', S.nexus, S.dim);
262
+
263
+ for (const log of logs) {
264
+ if (log.type === 'render') {
265
+ const cTag = log.cacheHit
266
+ ? ['%c⚡ Cache HIT', S.ok]
267
+ : ['%c🌐 Cache MISS', S.warn];
268
+ const strat = log.cacheStrategy ? ' · ' + log.cacheStrategy : '';
269
+ console.log(
270
+ '%c🚀 Route%c ' + log.path + ' %c' + log.duration + 'ms%c ' + cTag[0] + strat,
271
+ S.ok, S.route, S.dim, '', cTag[1]
272
+ );
273
+ }
274
+ if (log.type === 'island-count' && log.islandCount) {
275
+ console.log('%c📦 Islands%c ' + log.islandCount + ' to hydrate', S.ok, S.dim);
276
+ }
277
+ }
278
+
279
+ if (build.totalJs) {
280
+ const kb = (build.totalJs / 1024).toFixed(1);
281
+ const saved = ((build.reactEquivalent - build.totalJs) / 1024).toFixed(0);
282
+ console.log('%c💎 JS: ' + kb + 'KB%c — saved ' + saved + 'KB vs React', S.nexus, S.dim);
283
+ }
284
+
285
+ console.groupEnd();
286
+
287
+ // ── Island hydration hook (called by @nexus_js/runtime island loader) ──────────
288
+ window.__NEXUS_LOG_ISLAND__ = function(name, strategy, ms) {
289
+ console.log(
290
+ '%c[Nexus] 🏝️ Island%c <' + name + ' />%c hydrated ' +
291
+ '%c(' + strategy + ')%c — ' + ms.toFixed(1) + 'ms',
292
+ S.nexus, S.island, '', S.dim, ''
293
+ );
294
+ };
295
+
296
+ // ── $state change hook (called by Runes proxy in dev mode) ─────────────────
297
+ window.__NEXUS_LOG_STATE__ = function(key, prev, next, source) {
298
+ console.log(
299
+ '%c[Nexus] ✨ $state%c "' + key + '" %c' + JSON.stringify(prev) +
300
+ ' → ' + JSON.stringify(next) + (source ? '%c ↳ ' + source : '%c'),
301
+ S.nexus, S.warn, S.dim, S.dim
302
+ );
303
+ };
304
+
305
+ // ── $optimistic hook ───────────────────────────────────────────────────────
306
+ window.__NEXUS_LOG_OPTIMISTIC__ = function(key, value) {
307
+ console.log(
308
+ '%c[Nexus] 🔄 $optimistic%c "' + key + '" → %c' + JSON.stringify(value),
309
+ S.nexus, S.warn, S.stat
310
+ );
311
+ };
312
+
313
+ // ── SPA Navigation hook ────────────────────────────────────────────────────
314
+ window.__NEXUS_LOG_NAV__ = function(to, morphKey) {
315
+ console.log('%c[Nexus] 🗺️ Navigating to%c ' + to, S.nexus, S.route);
316
+ if (morphKey) {
317
+ console.log('%c[Nexus] 🪄 Morphing%c [' + morphKey + ']', S.nexus, S.dim);
318
+ }
319
+ };
320
+
321
+ // ── Action lifecycle hooks ─────────────────────────────────────────────────
322
+ window.__NEXUS_LOG_ACTION__ = function(name, phase, data) {
323
+ if (phase === 'call') {
324
+ console.log('%c[Nexus] ▲ Action%c ' + name + '() called', S.nexus, S.action);
325
+ } else if (phase === 'optimistic') {
326
+ window.__NEXUS_LOG_OPTIMISTIC__?.(name, data);
327
+ } else if (phase === 'success') {
328
+ console.log('%c[Nexus] ✅ Action%c ' + name + '() synced', S.nexus, S.ok);
329
+ } else if (phase === 'error') {
330
+ console.error('%c[Nexus] ✖ Action%c ' + name + '() failed', S.err, '');
331
+ } else if (phase === 'cancelled') {
332
+ console.warn('%c[Nexus] ↩ Action%c ' + name + '() cancelled (race)', S.warn, '');
333
+ }
334
+ };
335
+
336
+ // ── A11y checker — runs 1s after mount ─────────────────────────────────────
337
+ setTimeout(function() {
338
+ const issues = [];
339
+ document.querySelectorAll('img:not([alt])').forEach(function(el) {
340
+ issues.push('<img> missing alt → ' + (el.getAttribute('src') || '').split('/').pop());
341
+ });
342
+ document.querySelectorAll('button, [role="button"]').forEach(function(el) {
343
+ if (!el.textContent?.trim() && !el.getAttribute('aria-label')) {
344
+ issues.push('<button> missing accessible label');
345
+ }
346
+ });
347
+ document.querySelectorAll('a:not([aria-label]):not([href])').forEach(function() {
348
+ issues.push('<a> missing href or aria-label');
349
+ });
350
+ if (issues.length) {
351
+ console.groupCollapsed('%c[Nexus] ⚠️ A11y — ' + issues.length + ' issue' + (issues.length !== 1 ? 's' : '') + ' found', 'color:#f59e0b;font-weight:700');
352
+ issues.forEach(function(i) { console.warn(' •', i); });
353
+ console.groupEnd();
354
+ }
355
+ }, 1200);
356
+ })();
357
+ </script>`;
358
+ /**
359
+ * Serializes island props for client-side hydration.
360
+ * Uses base64 to safely embed arbitrary JSON in HTML attributes.
361
+ */
362
+ export function serializeIslandProps(props) {
363
+ return btoa(JSON.stringify(props));
364
+ }
365
+ /**
366
+ * Wraps a component's HTML output with island metadata for client hydration.
367
+ */
368
+ export function wrapIsland(html, opts) {
369
+ const propsAttr = opts.props ? ` data-nexus-props="${serializeIslandProps(opts.props)}"` : '';
370
+ const mediaAttr = opts.mediaQuery ? ` data-nexus-media="${opts.mediaQuery}"` : '';
371
+ return `<nexus-island
372
+ data-nexus-island="${opts.id}"
373
+ data-nexus-component="${opts.componentPath}"
374
+ data-nexus-strategy="${opts.strategy}"${propsAttr}${mediaAttr}
375
+ >${html}</nexus-island>`;
376
+ }
377
+ function errorPage(err, dev) {
378
+ const message = err instanceof Error ? err.message : String(err);
379
+ const stack = dev && err instanceof Error ? err.stack ?? '' : '';
380
+ return `${DOCTYPE}<html><body style="font-family:monospace;padding:2rem;background:#0a0a0f;color:#ff6b6b">
381
+ <h1 style="color:#ff3e00">Nexus — Server Error</h1>
382
+ <pre>${message}</pre>
383
+ ${dev ? `<pre style="color:#6b6b80;font-size:0.8rem">${stack}</pre>` : ''}
384
+ </body></html>`;
385
+ }
386
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA6CnD,iFAAiF;AACjF,+EAA+E;AAC/E,qDAAqD;AAErD,MAAM,gBAAgB,GAAgE;IACpF,IAAI,EAAE,EAAE;IACR,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,KAAK;CACjB,CAAC;AAEF,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,kBAAkB;IAChC,gBAAgB,CAAC,UAAU,GAAG,IAAI,CAAC;AACrC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,qBAAqB;IACnC,gBAAgB,CAAC,SAAS,GAAG,IAAI,CAAC;AACpC,CAAC;AAED,SAAS,eAAe;IACtB,gBAAgB,CAAC,IAAI,GAAG,EAAE,CAAC;IAC3B,gBAAgB,CAAC,UAAU,GAAG,KAAK,CAAC;IACpC,gBAAgB,CAAC,SAAS,GAAG,KAAK,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAiB;IAKnD,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,gBAAgB,CAAC;IAEzD,yCAAyC;IACzC,IAAI,UAAU,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QAC7E,OAAO;YACL,MAAM,EAAE,mBAAmB;YAC3B,GAAG,EAAE,CAAC;YACN,QAAQ,EAAE,kBAAkB;SAC7B,CAAC;IACJ,CAAC;IAED,qDAAqD;IACrD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,MAAM,EAAE,UAAU;YAClB,GAAG,EAAE,CAAC;YACN,QAAQ,EAAE,oBAAoB;SAC/B,CAAC;IACJ,CAAC;IAED,oDAAoD;IACpD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,MAAM,EAAE,UAAU;YAClB,GAAG,EAAE,CAAC;YACN,QAAQ,EAAE,kBAAkB;SAC7B,CAAC;IACJ,CAAC;IAED,kDAAkD;IAClD,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,IAAI,CAAC,IAAI,QAAQ,CAAC,EAAE,CAAC;QACvD,OAAO;YACL,MAAM,EAAE,qCAAqC;YAC7C,GAAG,EAAE,QAAQ;YACb,QAAQ,EAAE,kBAAkB;SAC7B,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC;IAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,wBAAwB;IAEjE,OAAO;QACL,MAAM,EAAE,oBAAoB,MAAM,4BAA4B,GAAG,EAAE;QACnE,GAAG,EAAE,MAAM;QACX,QAAQ,EAAE,KAAK;KAChB,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAElC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAqB,EACrB,GAAiB,EACjB,IAAmB;IAEnB,IAAI,QAAQ,GAAG,EAAE,CAAC;IAElB,8CAA8C;IAC9C,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,QAAQ,EAAE;gBACjD,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,OAAO,EAAE,MAAM,CAAC,OAAO;aACxB,CAAC,CAAC;YACH,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBACrC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACrC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,gCAAgC,MAAM,CAAC,QAAQ,IAAI,EAAE,GAAG,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE;YAC5D,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO;SAC/B,CAAC,CAAC;QACH,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzC,QAAQ,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QAC/B,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,8BAA8B,OAAO,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,GAAG,CAAC,CAAC;QAC7E,OAAO;YACL,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;YAC9B,OAAO,EAAE;gBACP,cAAc,EAAE,0BAA0B;gBAC1C,eAAe,EAAE,UAAU;aAC5B;YACD,MAAM,EAAE,GAAG;YACX,QAAQ,EAAE,CAAC;YACX,WAAW,EAAE,CAAC;SACf,CAAC;IACJ,CAAC;IAED,yDAAyD;IACzD,IAAI,OAAO,GAAG,QAAQ,CAAC;IACvB,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,CAAC;QACzC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;IACvD,CAAC;IAED,sEAAsE;IACtE,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;IAEnE,2DAA2D;IAC3D,MAAM,YAAY,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC;IAC7B,eAAe,EAAE,CAAC;IAElB,uDAAuD;IACvD,MAAM,UAAU,GAAsB,EAAE,CAAC;IACzC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,oEAAoE;QACxF,UAAU,CAAC,IAAI,CAAC;YACd,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ;YACvC,QAAQ;YACR,aAAa,EAAE,YAAY,CAAC,QAAQ;YACpC,QAAQ,EAAE,YAAY,CAAC,QAAQ,KAAK,KAAK,IAAI,YAAY,CAAC,QAAQ,KAAK,kBAAkB;SAC1F,CAAC,CAAC;QACH,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACpB,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IAE1E,OAAO;QACL,IAAI,EAAE,OAAO,GAAG,QAAQ;QACxB,OAAO,EAAE;YACP,cAAc,EAAE,0BAA0B;YAC1C,eAAe,EAAE,YAAY,CAAC,MAAM;YACpC,wBAAwB,EAAE,YAAY,CAAC,QAAQ;YAC/C,sBAAsB,EAAE,MAAM,CAAC,WAAW,CAAC;YAC3C,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpD;QACD,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,GAAG;QACb,WAAW;KACZ,CAAC;AACJ,CAAC;AAED,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB,SAAS,gBAAgB,CACvB,OAAe,EACf,IAAmB,EACnB,aAAgC,EAAE,EAClC,WAAW,GAAG,CAAC;IAEf,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM;SAClC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gCAAgC,IAAI,IAAI,CAAC;SACvD,IAAI,CAAC,QAAQ,CAAC,CAAC;IAElB,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO;QACvC,CAAC,CAAC,8BAA8B,IAAI,CAAC,MAAM,CAAC,OAAO,aAAa;QAChE,CAAC,CAAC,EAAE,CAAC;IAEP,gGAAgG;IAChG,4EAA4E;IAC5E,MAAM,SAAS,GAAG;;;;;;;;UAQV,CAAC;IAET,uCAAuC;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;;6CAEc,WAAW;;;;;;;;UAQ9C,CAAC,CAAC,CAAC,EAAE,CAAC;IAEd,uCAAuC;IACvC,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;;iCAED,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;;aAE9C,KAAK,GAAG,WAAW,GAAG,KAAK;;iBAEvB,WAAW;;WAEjB,oBAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAErC,OAAO;;;;MAIH,UAAU;MACV,SAAS;MACT,SAAS;MACT,YAAY;MACZ,aAAa;;;MAGb,OAAO;;QAEL,CAAC;AACT,CAAC;AAED;;;;GAIG;AACH,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmHnB,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA8B;IACjE,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,IAMC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,sBAAsB,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9F,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,sBAAsB,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAElF,OAAO;yBACgB,IAAI,CAAC,EAAE;4BACJ,IAAI,CAAC,aAAa;2BACnB,IAAI,CAAC,QAAQ,IAAI,SAAS,GAAG,SAAS;KAC5D,IAAI,iBAAiB,CAAC;AAC3B,CAAC;AAED,SAAS,SAAS,CAAC,GAAY,EAAE,GAAY;IAC3C,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,GAAG,IAAI,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAEjE,OAAO,GAAG,OAAO;;WAER,OAAO;MACZ,GAAG,CAAC,CAAC,CAAC,+CAA+C,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE;iBAC5D,CAAC;AAClB,CAAC"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Nexus Streaming SSR — Out-of-order HTML flushing with Suspense boundaries.
3
+ *
4
+ * How it works:
5
+ *
6
+ * 1. The route render function returns immediately with the static shell.
7
+ * Any Promise in the template becomes a placeholder:
8
+ * <template id="nx-hole-3a9f"></template>
9
+ *
10
+ * 2. The HTTP connection stays OPEN (Transfer-Encoding: chunked).
11
+ * The browser receives and paints the static shell instantly.
12
+ *
13
+ * 3. When each Promise resolves, the server writes two chunks:
14
+ * a) The resolved HTML wrapped in a <template id="nx-fill-3a9f">
15
+ * b) A tiny inline script that moves the template content
16
+ * to the hole's position and removes both elements.
17
+ *
18
+ * 4. Error boundary: if a Promise rejects, the server writes the
19
+ * error.nx fallback HTML instead.
20
+ *
21
+ * Result: Users see content as it becomes available, not all-at-once.
22
+ * The Time-to-First-Byte is the server's fastest possible response.
23
+ *
24
+ * Wire format (chunks sent over HTTP):
25
+ * Chunk 1: Full HTML with placeholders
26
+ * <div>Static content</div>
27
+ * <template id="nx-hole-3a9f" data-nx-fallback="<p>Loading...</p>"></template>
28
+ * <template id="nx-hole-7b2c"></template>
29
+ *
30
+ * Chunk 2: Resolved content (arrives async)
31
+ * <template id="nx-fill-3a9f"><article>...</article></template>
32
+ * <script>__nx_fill("3a9f")</script>
33
+ *
34
+ * Chunk 3: Another resolution or error boundary
35
+ * <template id="nx-fill-7b2c"><div class="error">...</div></template>
36
+ * <script>__nx_fill("7b2c")</script>
37
+ *
38
+ * Final chunk (closes stream):
39
+ * <script>__nx_stream_complete()</script>
40
+ */
41
+ export interface StreamingBoundary {
42
+ id: string;
43
+ /** The promise that will resolve to HTML */
44
+ promise: Promise<string>;
45
+ /** Fallback HTML shown while loading */
46
+ fallback?: string;
47
+ /** Error boundary HTML (overrides error.nx at component level) */
48
+ errorFallback?: string;
49
+ }
50
+ export interface StreamController {
51
+ /** Write the initial HTML shell (immediately) */
52
+ writeShell: (html: string) => void;
53
+ /** Register a deferred content promise */
54
+ defer: (boundary: StreamingBoundary) => void;
55
+ /** Signal stream completion */
56
+ close: () => void;
57
+ }
58
+ /**
59
+ * Creates a streaming SSR response.
60
+ * Returns a Web-standard `ReadableStream` for use in any edge runtime.
61
+ */
62
+ export declare function createStreamingResponse(renderFn: (ctrl: StreamController) => void | Promise<void>, opts?: {
63
+ headers?: HeadersInit;
64
+ onError?: (err: unknown) => string;
65
+ }): Response;
66
+ /**
67
+ * Template tag for deferred content in .nx templates.
68
+ *
69
+ * Usage in template:
70
+ * {#await fetchPosts()}
71
+ * <p>Loading posts...</p>
72
+ * {:then posts}
73
+ * {#each posts as p}<article>{p.title}</article>{/each}
74
+ * {:catch error}
75
+ * <p class="error">{error.message}</p>
76
+ * {/await}
77
+ *
78
+ * Compiled output:
79
+ * createSuspenseBoundary(fetchPosts(), {
80
+ * fallback: '<p>Loading posts...</p>',
81
+ * render: (posts) => posts.map(...).join(''),
82
+ * })
83
+ */
84
+ export declare function createSuspenseBoundary<T>(promise: Promise<T>, opts: {
85
+ fallback?: string;
86
+ render: (value: T) => string;
87
+ errorFallback?: string | ((err: unknown) => string);
88
+ }): {
89
+ id: string;
90
+ placeholder: string;
91
+ boundary: StreamingBoundary;
92
+ };
93
+ /**
94
+ * Pipes a streaming response to a Node.js `ServerResponse`.
95
+ * Used by the Node.js server adapter.
96
+ */
97
+ export declare function pipeToNodeResponse(webResponse: Response, nodeRes: import('node:http').ServerResponse): Promise<void>;
98
+ //# sourceMappingURL=streaming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,0CAA0C;IAC1C,KAAK,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,+BAA+B;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA0BD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC1D,IAAI,GAAE;IACJ,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;CAC/B,GACL,QAAQ,CAqEV;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,IAAI,EAAE;IACJ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;CACrD,GACA;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAoBlE;AAID;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,QAAQ,EACrB,OAAO,EAAE,OAAO,WAAW,EAAE,cAAc,GAC1C,OAAO,CAAC,IAAI,CAAC,CAmBf"}