hono-preact 0.2.0 → 0.4.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 (79) hide show
  1. package/dist/iso/action-result-context.d.ts +22 -0
  2. package/dist/iso/action-result-context.js +2 -0
  3. package/dist/iso/action.d.ts +52 -13
  4. package/dist/iso/action.js +204 -88
  5. package/dist/iso/cache.d.ts +9 -0
  6. package/dist/iso/cache.js +26 -0
  7. package/dist/iso/define-app.d.ts +7 -0
  8. package/dist/iso/define-loader.d.ts +12 -0
  9. package/dist/iso/define-loader.js +26 -16
  10. package/dist/iso/form.d.ts +13 -4
  11. package/dist/iso/form.js +115 -33
  12. package/dist/iso/index.d.ts +13 -4
  13. package/dist/iso/index.js +14 -2
  14. package/dist/iso/internal/action-envelope.d.ts +37 -0
  15. package/dist/iso/internal/action-envelope.js +47 -0
  16. package/dist/iso/internal/action-result-store.d.ts +28 -0
  17. package/dist/iso/internal/action-result-store.js +35 -0
  18. package/dist/iso/internal/envelope.js +1 -2
  19. package/dist/iso/internal/form-submit-store.d.ts +9 -0
  20. package/dist/iso/internal/form-submit-store.js +32 -0
  21. package/dist/iso/internal/history-shim.d.ts +7 -0
  22. package/dist/iso/internal/history-shim.js +79 -0
  23. package/dist/iso/internal/loader-fetch.js +65 -34
  24. package/dist/iso/internal/loader.d.ts +3 -3
  25. package/dist/iso/internal/merge-refs.d.ts +4 -0
  26. package/dist/iso/internal/merge-refs.js +14 -0
  27. package/dist/iso/internal/persist-registry.d.ts +10 -0
  28. package/dist/iso/internal/persist-registry.js +24 -0
  29. package/dist/iso/internal/route-boundary.d.ts +4 -4
  30. package/dist/iso/internal/route-change.d.ts +8 -2
  31. package/dist/iso/internal/route-change.js +107 -12
  32. package/dist/iso/internal/safe-redirect.d.ts +7 -0
  33. package/dist/iso/internal/safe-redirect.js +27 -0
  34. package/dist/iso/internal/sse-decoder.d.ts +1 -1
  35. package/dist/iso/internal/sse-decoder.js +40 -26
  36. package/dist/iso/internal/use-render.d.ts +11 -0
  37. package/dist/iso/internal/use-render.js +47 -0
  38. package/dist/iso/internal/view-transition-event.d.ts +23 -0
  39. package/dist/iso/internal/view-transition-event.js +25 -0
  40. package/dist/iso/internal.d.ts +12 -1
  41. package/dist/iso/internal.js +13 -1
  42. package/dist/iso/optimistic-action.d.ts +10 -1
  43. package/dist/iso/optimistic-action.js +11 -3
  44. package/dist/iso/optimistic.d.ts +10 -1
  45. package/dist/iso/optimistic.js +45 -5
  46. package/dist/iso/outcomes.d.ts +14 -2
  47. package/dist/iso/outcomes.js +14 -3
  48. package/dist/iso/persist.d.ts +14 -0
  49. package/dist/iso/persist.js +56 -0
  50. package/dist/iso/use-action-result.d.ts +25 -0
  51. package/dist/iso/use-action-result.js +39 -0
  52. package/dist/iso/use-form-status.d.ts +5 -0
  53. package/dist/iso/use-form-status.js +13 -0
  54. package/dist/iso/view-transition-lifecycle.d.ts +9 -0
  55. package/dist/iso/view-transition-lifecycle.js +18 -0
  56. package/dist/iso/view-transition-name.d.ts +17 -0
  57. package/dist/iso/view-transition-name.js +79 -0
  58. package/dist/iso/view-transition-types.d.ts +8 -0
  59. package/dist/iso/view-transition-types.js +21 -0
  60. package/dist/server/actions-handler.d.ts +7 -0
  61. package/dist/server/actions-handler.js +42 -9
  62. package/dist/server/index.d.ts +2 -1
  63. package/dist/server/index.js +2 -1
  64. package/dist/server/loaders-handler.d.ts +8 -0
  65. package/dist/server/loaders-handler.js +37 -4
  66. package/dist/server/page-action-handler.d.ts +63 -0
  67. package/dist/server/page-action-handler.js +274 -0
  68. package/dist/server/page-action-resolvers.d.ts +28 -0
  69. package/dist/server/page-action-resolvers.js +147 -0
  70. package/dist/server/render.js +136 -55
  71. package/dist/server/route-server-modules.d.ts +7 -8
  72. package/dist/server/route-server-modules.js +7 -8
  73. package/dist/server/speculation-rules.d.ts +3 -0
  74. package/dist/server/speculation-rules.js +8 -0
  75. package/dist/server/sse.d.ts +43 -28
  76. package/dist/server/sse.js +113 -88
  77. package/dist/vite/client-entry.js +12 -3
  78. package/dist/vite/server-entry.js +10 -2
  79. package/package.json +2 -2
@@ -0,0 +1,147 @@
1
+ function extractActions(mod) {
2
+ const moduleKey = mod.__moduleKey;
3
+ if (typeof moduleKey !== 'string' || !mod.serverActions)
4
+ return [];
5
+ const out = [];
6
+ for (const [name, val] of Object.entries(mod.serverActions)) {
7
+ if (typeof val !== 'function')
8
+ continue;
9
+ // `defineAction` attaches `use` and `timeoutMs` as non-enumerable
10
+ // properties on the function (see packages/iso/src/action.ts). Read
11
+ // them here as the single deserialization boundary; the handler reads
12
+ // `entry.fn`, `entry.use`, `entry.timeoutMs` through the typed
13
+ // ActionEntry shape from this point on.
14
+ const metadata = val;
15
+ out.push({
16
+ name,
17
+ entry: {
18
+ fn: val,
19
+ use: metadata.use ?? [],
20
+ timeoutMs: metadata.timeoutMs,
21
+ moduleKey,
22
+ },
23
+ });
24
+ }
25
+ return out;
26
+ }
27
+ function segmentsOf(p) {
28
+ return p.split('/').filter((s) => s !== '');
29
+ }
30
+ function urlPathMatchesPattern(urlPath, pattern) {
31
+ const ps = segmentsOf(pattern);
32
+ const us = segmentsOf(urlPath);
33
+ for (let i = 0; i < ps.length; i++) {
34
+ const p = ps[i];
35
+ if (p === '*')
36
+ return true;
37
+ if (i >= us.length)
38
+ return false;
39
+ if (p.startsWith(':'))
40
+ continue;
41
+ if (p !== us[i])
42
+ return false;
43
+ }
44
+ return ps.length === us.length;
45
+ }
46
+ function patternScore(pattern) {
47
+ let score = 0;
48
+ for (const seg of segmentsOf(pattern)) {
49
+ if (seg === '*')
50
+ score += 0;
51
+ else if (seg.startsWith(':'))
52
+ score += 1;
53
+ else
54
+ score += 2;
55
+ }
56
+ return score;
57
+ }
58
+ /**
59
+ * Build action resolvers keyed by route path and by module key. Each
60
+ * ServerRoute contributes its own serverActions and its ancestors' serverActions
61
+ * to the merged map for that path. Ancestor entries are written first so that
62
+ * a page-level action shadows a same-named layout action when names collide.
63
+ *
64
+ * Lazy semantics: the first call triggers loading all server modules. The result
65
+ * is cached for the process lifetime (unless dev=true, which rebuilds on every
66
+ * call so edits take effect without restarting the server).
67
+ *
68
+ * NOTE: framework-private. Intended consumer is the generated server entry and
69
+ * pageActionHandler.
70
+ */
71
+ export function makePageActionResolvers(serverRoutes, options = {}) {
72
+ const dev = options.dev ?? false;
73
+ let buildPromise = null;
74
+ const build = async () => {
75
+ // Load each distinct server thunk once; a thunk may appear as `server`
76
+ // on one route and as an `ancestor` on its children.
77
+ const thunkCache = new Map();
78
+ const load = (thunk) => {
79
+ let p = thunkCache.get(thunk);
80
+ if (!p) {
81
+ p = thunk().then((m) => m);
82
+ thunkCache.set(thunk, p);
83
+ }
84
+ return p;
85
+ };
86
+ const byPathMap = new Map();
87
+ const byModuleKeyMap = new Map();
88
+ await Promise.all(serverRoutes.map(async (route) => {
89
+ const ancestorMods = await Promise.all(route.ancestors.map(load));
90
+ const selfMod = await load(route.server);
91
+ const merged = new Map();
92
+ // Write ancestors first (outer -> inner), then self. Later writes
93
+ // shadow earlier ones, so a page-level action wins over a layout
94
+ // action of the same name.
95
+ for (const mod of [...ancestorMods, selfMod]) {
96
+ for (const { name, entry } of extractActions(mod)) {
97
+ merged.set(name, entry);
98
+ let m = byModuleKeyMap.get(entry.moduleKey);
99
+ if (!m) {
100
+ m = new Map();
101
+ byModuleKeyMap.set(entry.moduleKey, m);
102
+ }
103
+ m.set(name, entry);
104
+ }
105
+ }
106
+ byPathMap.set(route.path, merged);
107
+ }));
108
+ return { byPathMap, byModuleKeyMap };
109
+ };
110
+ const get = () => {
111
+ if (dev)
112
+ return build();
113
+ if (buildPromise)
114
+ return buildPromise;
115
+ buildPromise = build().catch((err) => {
116
+ buildPromise = null;
117
+ return Promise.reject(err);
118
+ });
119
+ return buildPromise;
120
+ };
121
+ return {
122
+ async byPath(path) {
123
+ const { byPathMap } = await get();
124
+ let bestPattern = null;
125
+ let bestScore = -1;
126
+ let bestDepth = -1;
127
+ for (const pattern of byPathMap.keys()) {
128
+ if (!urlPathMatchesPattern(path, pattern))
129
+ continue;
130
+ const score = patternScore(pattern);
131
+ const depth = segmentsOf(pattern).length;
132
+ if (score > bestScore || (score === bestScore && depth > bestDepth)) {
133
+ bestPattern = pattern;
134
+ bestScore = score;
135
+ bestDepth = depth;
136
+ }
137
+ }
138
+ return bestPattern
139
+ ? (byPathMap.get(bestPattern) ?? new Map())
140
+ : new Map();
141
+ },
142
+ async byModuleKey(moduleKey, actionName) {
143
+ const { byModuleKeyMap } = await get();
144
+ return byModuleKeyMap.get(moduleKey)?.get(actionName);
145
+ },
146
+ };
147
+ }
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { createDispatcher, HoofdProvider } from 'hoofd/preact';
3
3
  import { prerender, locationStub } from 'preact-iso/prerender';
4
- import { env, isOutcome, } from '../iso/index.js';
5
- import { HonoRequestContext, runRequestScope, captureRequestScope, takeServerStreamingLoaders, dispatchServer, partitionUse, } from '../iso/internal.js';
4
+ import { env, isOutcome, ActionResultContext, } from '../iso/index.js';
5
+ import { HonoRequestContext, runRequestScope, captureRequestScope, takeServerStreamingLoaders, dispatchServer, partitionUse, getActionResultSlot, } from '../iso/internal.js';
6
+ import { speculationRulesTag } from './speculation-rules.js';
6
7
  function escapeHtml(str) {
7
8
  return str
8
9
  .replace(/&/g, '&amp;')
@@ -45,6 +46,42 @@ function translateRootOutcome(c, outcome) {
45
46
  }
46
47
  return c.text('render outcome is page-scope only and cannot be issued by root middleware', 500);
47
48
  }
49
+ function buildActionResultContext() {
50
+ const slot = getActionResultSlot();
51
+ if (!slot)
52
+ return null;
53
+ if (slot.resolution.kind === 'success') {
54
+ return {
55
+ module: slot.module,
56
+ action: slot.action,
57
+ kind: 'success',
58
+ data: slot.resolution.data,
59
+ submittedPayload: slot.submittedPayload,
60
+ };
61
+ }
62
+ if (slot.resolution.kind === 'error') {
63
+ return {
64
+ module: slot.module,
65
+ action: slot.action,
66
+ kind: 'error',
67
+ message: slot.resolution.message,
68
+ submittedPayload: slot.submittedPayload,
69
+ };
70
+ }
71
+ const { outcome } = slot.resolution;
72
+ if (outcome.__outcome === 'deny') {
73
+ return {
74
+ module: slot.module,
75
+ action: slot.action,
76
+ kind: 'deny',
77
+ status: outcome.status,
78
+ message: outcome.message,
79
+ data: outcome.data,
80
+ submittedPayload: slot.submittedPayload,
81
+ };
82
+ }
83
+ return null;
84
+ }
48
85
  export async function renderPage(c, node, options) {
49
86
  const dispatcher = createDispatcher();
50
87
  const previousEnv = env.current;
@@ -92,7 +129,7 @@ export async function renderPage(c, node, options) {
92
129
  // while we await suspended children.
93
130
  locationStub(reqUrl.pathname + reqUrl.search);
94
131
  bindRequestScope = captureRequestScope();
95
- const rendered = await prerender(_jsx(HonoRequestContext.Provider, { value: { context: c }, children: _jsx(HoofdProvider, { value: dispatcher, children: node }) }));
132
+ const rendered = await prerender(_jsx(ActionResultContext.Provider, { value: buildActionResultContext(), children: _jsx(HonoRequestContext.Provider, { value: { context: c }, children: _jsx(HoofdProvider, { value: dispatcher, children: node }) }) }));
96
133
  const loaders = takeServerStreamingLoaders();
97
134
  return {
98
135
  kind: 'value',
@@ -130,6 +167,7 @@ export async function renderPage(c, node, options) {
130
167
  titleSource != null ? `<title>${escapeHtml(titleSource)}</title>` : '',
131
168
  ...metas.map((m) => `<meta ${toAttrs(m)} />`),
132
169
  ...links.map((l) => `<link ${toAttrs(l)} />`),
170
+ speculationRulesTag(options?.appConfig ?? {}),
133
171
  ]
134
172
  .filter(Boolean)
135
173
  .join('\n ');
@@ -183,65 +221,105 @@ export async function renderPage(c, node, options) {
183
221
  // throw and, for the per-loader catch, get logged as a synthetic error
184
222
  // chunk that nobody can read anyway).
185
223
  let aborted = false;
186
- const responseStream = new ReadableStream({
187
- start(controller) {
188
- // Re-enter the captured request scope so generator continuations and
189
- // anything they touch (e.g. `getRequestHonoContext`, per-request loader
190
- // caches) see the same per-request store the initial prerender saw.
191
- return bindRequestScope(async () => {
224
+ // Multi-producer backpressure via TransformStream. Each loader pump writes
225
+ // to a shared `writer`, awaiting `writer.ready` before each write so that
226
+ // the per-loader iteration is paced by the consumer's read rate (not by
227
+ // however fast the generator yields). Without this, a tight-loop streaming
228
+ // loader would buffer chunks into the ReadableStream queue unbounded; see
229
+ // render-stream.test.tsx "pauses production when the HTML consumer is
230
+ // slow (backpressure)".
231
+ const { writable, readable: responseStream } = new TransformStream();
232
+ const writer = writable.getWriter();
233
+ // When the consumer cancels the readable side (e.g. Hono drops the response,
234
+ // or the runtime tears down the request), the writable side transitions to
235
+ // an errored state and `writer.closed` rejects. Propagate to the loader
236
+ // generators symmetrically with the request-signal abort path below. The
237
+ // `aborted` guard makes the self-triggered case (`writer.abort()` in our
238
+ // own finally) a no-op.
239
+ writer.closed.catch(() => {
240
+ if (aborted)
241
+ return;
242
+ aborted = true;
243
+ for (const { gen } of streamingLoaders) {
244
+ gen.return(undefined).catch(() => {
245
+ /* swallow */
246
+ });
247
+ }
248
+ });
249
+ // Re-enter the captured request scope so generator continuations and
250
+ // anything they touch (e.g. `getRequestHonoContext`, per-request loader
251
+ // caches) see the same per-request store the initial prerender saw.
252
+ void bindRequestScope(async () => {
253
+ // Yield one microtask before doing anything else. `renderPage` is still
254
+ // on the synchronous frame that constructs this response (TransformStream
255
+ // is created, then `c.body(...)` runs and commits the headers). Resuming
256
+ // a generator can call `setCookie(ctx.c, ...)`, which mutates Hono's
257
+ // prepared headers; deferring the pump guarantees the response is built
258
+ // first, so post-first-yield header writes are consistently excluded
259
+ // rather than racing construction. Cookies must be set before the
260
+ // loader's first yield to reach the streamed response.
261
+ await Promise.resolve();
262
+ try {
263
+ if (aborted)
264
+ return;
265
+ await writer.ready;
266
+ if (aborted)
267
+ return;
268
+ await writer.write(encoder.encode(`<!doctype html>${beforeBody}\n${bootstrap}\n`));
269
+ // Drive each pending generator in parallel; emit script tags per chunk.
270
+ await Promise.all(streamingLoaders.map(async ({ loaderId, gen }) => {
192
271
  try {
193
- if (aborted)
194
- return;
195
- controller.enqueue(encoder.encode(`<!doctype html>${beforeBody}\n${bootstrap}\n`));
196
- // Yield one microtask before advancing any loader generator past
197
- // its first yield. `renderPage` is still on the synchronous frame
198
- // that constructs this response (`new ReadableStream(...)` returns,
199
- // then `c.body(...)` runs and commits the headers). Resuming a
200
- // generator can call `setCookie(ctx.c, ...)`, which mutates Hono's
201
- // prepared headers; deferring the pump guarantees the response is
202
- // built first, so post-first-yield header writes are consistently
203
- // excluded rather than racing construction. Cookies must be set
204
- // before the loader's first yield to reach the streamed response.
205
- await Promise.resolve();
206
- // Drive each pending generator in parallel; emit script tags per chunk.
207
- await Promise.all(streamingLoaders.map(async ({ loaderId, gen }) => {
208
- try {
209
- while (!aborted) {
210
- const step = await gen.next();
211
- if (aborted)
212
- return;
213
- if (step.done) {
214
- controller.enqueue(encoder.encode(`<script>window.__HP_STREAM__.end(${jsonForScript(loaderId)});document.currentScript.remove()</script>\n`));
215
- return;
216
- }
217
- controller.enqueue(encoder.encode(`<script>window.__HP_STREAM__.push(${jsonForScript(loaderId)},${jsonForScript(step.value)});document.currentScript.remove()</script>\n`));
218
- }
219
- }
220
- catch (err) {
221
- if (aborted)
222
- return;
223
- const message = err instanceof Error ? err.message : String(err);
224
- const name = err instanceof Error ? err.name : 'Error';
225
- controller.enqueue(encoder.encode(`<script>window.__HP_STREAM__.error(${jsonForScript(loaderId)},${jsonForScript({ message, name })});document.currentScript.remove()</script>\n`));
272
+ while (!aborted) {
273
+ const step = await gen.next();
274
+ if (aborted)
275
+ return;
276
+ await writer.ready;
277
+ if (aborted)
278
+ return;
279
+ if (step.done) {
280
+ await writer.write(encoder.encode(`<script>window.__HP_STREAM__.end(${jsonForScript(loaderId)});document.currentScript.remove()</script>\n`));
281
+ return;
226
282
  }
227
- }));
228
- if (!aborted)
229
- controller.enqueue(encoder.encode(afterBody));
283
+ await writer.write(encoder.encode(`<script>window.__HP_STREAM__.push(${jsonForScript(loaderId)},${jsonForScript(step.value)});document.currentScript.remove()</script>\n`));
284
+ }
230
285
  }
231
- finally {
232
- if (!aborted)
233
- controller.close();
286
+ catch (err) {
287
+ if (aborted)
288
+ return;
289
+ const message = err instanceof Error ? err.message : String(err);
290
+ const name = err instanceof Error ? err.name : 'Error';
291
+ try {
292
+ await writer.ready;
293
+ if (aborted)
294
+ return;
295
+ await writer.write(encoder.encode(`<script>window.__HP_STREAM__.error(${jsonForScript(loaderId)},${jsonForScript({ message, name })});document.currentScript.remove()</script>\n`));
296
+ }
297
+ catch {
298
+ /* swallow: writable side closed/errored */
299
+ }
234
300
  }
235
- });
236
- },
237
- cancel() {
238
- aborted = true;
239
- for (const { gen } of streamingLoaders) {
240
- gen.return(undefined).catch(() => {
301
+ }));
302
+ if (!aborted) {
303
+ await writer.ready;
304
+ if (!aborted)
305
+ await writer.write(encoder.encode(afterBody));
306
+ }
307
+ }
308
+ catch {
309
+ /* swallow: writable side closed/errored mid-pump */
310
+ }
311
+ finally {
312
+ if (aborted) {
313
+ writer.abort().catch(() => {
314
+ /* swallow */
315
+ });
316
+ }
317
+ else {
318
+ writer.close().catch(() => {
241
319
  /* swallow */
242
320
  });
243
321
  }
244
- },
322
+ }
245
323
  });
246
324
  requestSignal.addEventListener('abort', () => {
247
325
  aborted = true;
@@ -250,6 +328,9 @@ export async function renderPage(c, node, options) {
250
328
  /* swallow */
251
329
  });
252
330
  }
331
+ writer.abort().catch(() => {
332
+ /* swallow */
333
+ });
253
334
  });
254
335
  // Route through `c.body()` rather than `new Response(...)` so Hono merges
255
336
  // its prepared headers into the streamed response. A streaming loader's
@@ -1,18 +1,17 @@
1
1
  import type { RoutesManifest, ServerRoute } from '../iso/index';
2
2
  /**
3
3
  * Convert a RoutesManifest into the array of lazy server-module loaders
4
- * that loadersHandler / actionsHandler accept. Previously returned a record
5
- * keyed by stringified integers; those keys were unused at the call site
6
- * (handlers iterate values only), so the array form is just the same data
7
- * without dead surface. Vite-style globs (`Record<string, ...>`) are still
8
- * accepted by the handlers directly; this helper is for the
9
- * routes-manifest-driven path used by the framework's generated server
10
- * entry.
4
+ * that loadersHandler accepts. Previously returned a record keyed by
5
+ * stringified integers; those keys were unused at the call site (handlers
6
+ * iterate values only), so the array form is just the same data without dead
7
+ * surface. Vite-style globs (`Record<string, ...>`) are still accepted by
8
+ * loadersHandler directly; this helper is for the routes-manifest-driven
9
+ * path used by the framework's generated server entry.
11
10
  */
12
11
  export declare function routeServerModules(manifest: RoutesManifest): ReadonlyArray<() => Promise<unknown>>;
13
12
  /**
14
13
  * Build the two page-layer `use` resolvers wired into loadersHandler and
15
- * actionsHandler. The loader handler matches by the location's URL path;
14
+ * pageActionHandler. The loader handler matches by the location's URL path;
16
15
  * the action handler matches by the action's owning module key. Both
17
16
  * lookups share one underlying composed map populated by loading every
18
17
  * routed `.server.*` module exactly once (then caching the result).
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Convert a RoutesManifest into the array of lazy server-module loaders
3
- * that loadersHandler / actionsHandler accept. Previously returned a record
4
- * keyed by stringified integers; those keys were unused at the call site
5
- * (handlers iterate values only), so the array form is just the same data
6
- * without dead surface. Vite-style globs (`Record<string, ...>`) are still
7
- * accepted by the handlers directly; this helper is for the
8
- * routes-manifest-driven path used by the framework's generated server
9
- * entry.
3
+ * that loadersHandler accepts. Previously returned a record keyed by
4
+ * stringified integers; those keys were unused at the call site (handlers
5
+ * iterate values only), so the array form is just the same data without dead
6
+ * surface. Vite-style globs (`Record<string, ...>`) are still accepted by
7
+ * loadersHandler directly; this helper is for the routes-manifest-driven
8
+ * path used by the framework's generated server entry.
10
9
  */
11
10
  export function routeServerModules(manifest) {
12
11
  return manifest.serverImports;
@@ -74,7 +73,7 @@ function pageUseFromMod(mod, patternPath) {
74
73
  }
75
74
  /**
76
75
  * Build the two page-layer `use` resolvers wired into loadersHandler and
77
- * actionsHandler. The loader handler matches by the location's URL path;
76
+ * pageActionHandler. The loader handler matches by the location's URL path;
78
77
  * the action handler matches by the action's owning module key. Both
79
78
  * lookups share one underlying composed map populated by loading every
80
79
  * routed `.server.*` module exactly once (then caching the result).
@@ -0,0 +1,3 @@
1
+ import type { AppConfig } from '../iso/index';
2
+ export declare const SPECULATION_RULES_TAG = "<script type=\"speculationrules\">{\"prefetch\":[{\"where\":{\"and\":[{\"href_matches\":\"/*\"},{\"not\":{\"selector_matches\":\"[data-no-prefetch]\"}}]},\"eagerness\":\"moderate\"}]}</script>";
3
+ export declare function speculationRulesTag(config: AppConfig): string;
@@ -0,0 +1,8 @@
1
+ const SPECULATION_RULES_JSON = '{"prefetch":[{"where":{"and":[' +
2
+ '{"href_matches":"/*"},' +
3
+ '{"not":{"selector_matches":"[data-no-prefetch]"}}' +
4
+ ']},"eagerness":"moderate"}]}';
5
+ export const SPECULATION_RULES_TAG = `<script type="speculationrules">${SPECULATION_RULES_JSON}</script>`;
6
+ export function speculationRulesTag(config) {
7
+ return config.speculation === true ? SPECULATION_RULES_TAG : '';
8
+ }
@@ -1,45 +1,60 @@
1
1
  import type { Context } from 'hono';
2
2
  import type { StreamObserver, ServerStreamCtx } from '../iso/index';
3
- export type SseGeneratorOptions = {
4
- /** When true, the generator's return value is emitted as `event: result`. */
3
+ /**
4
+ * Options shared by both SSE response helpers. Encodes the lifecycle the SSE
5
+ * pump runs through:
6
+ *
7
+ * - Observer fanout: `onStart` fires before the first chunk, `onChunk` per
8
+ * value yielded by the source, `onEnd` on normal completion, `onError` on
9
+ * a thrown error, `onAbort` when the consumer cancels the response stream
10
+ * before the source finishes.
11
+ * - Timeout discrimination: when `signal.aborted` and `signal.reason` is a
12
+ * `TimeoutError` `DOMException`, the catch path emits `event: timeout`
13
+ * with `{ timeoutMs }` instead of the generic `event: error` frame.
14
+ */
15
+ export type SseResponseOptions = {
16
+ /**
17
+ * When true, the generator's return value (if defined) is emitted as
18
+ * `event: result` before the stream closes. Only meaningful for
19
+ * generator-sourced responses; ignored for `ReadableStream` sources.
20
+ */
5
21
  emitResult?: boolean;
6
22
  /**
7
23
  * Stream observers harvested from the loader/action's `use` array (the
8
- * non-middleware partition). The SSE pump fires `onStart` before the
9
- * first chunk, `onChunk` per yielded value, `onEnd` on clean completion,
10
- * `onError` on throw, and `onAbort` when the response stream is aborted
11
- * (typically because the client disconnected). Hooks are isolated: a
12
- * throwing observer never corrupts the stream.
24
+ * non-middleware partition). Hooks are isolated: a throwing observer
25
+ * never corrupts the stream.
13
26
  */
14
27
  observers?: ReadonlyArray<StreamObserver<unknown, never>>;
15
28
  /** Server-stream ctx threaded to each observer hook. */
16
29
  observerCtx?: ServerStreamCtx;
30
+ /**
31
+ * The handler's timeout signal (from `AbortSignal.timeout(timeoutMs)`),
32
+ * inspected in the catch path to distinguish a deadline-driven abort
33
+ * from a generic throw.
34
+ */
35
+ signal?: AbortSignal;
36
+ /** Used only with `signal`; the timeout value reported in the frame. */
37
+ timeoutMs?: number;
17
38
  };
39
+ /** Alias retained for source compatibility with earlier code. */
40
+ export type SseGeneratorOptions = SseResponseOptions;
18
41
  /**
19
42
  * Wrap an async generator as an SSE response.
20
43
  *
21
- * Each yield is JSON-encoded and written as a `data:` event.
22
- * If `emitResult` is true and the generator's return value is defined,
23
- * it is written as `event: result\ndata: <json>` before the stream closes.
24
- * If the generator throws, an `event: error\ndata: {"message","name"}` frame
25
- * is written and the stream closes cleanly (Hono's default error handler is
26
- * never invoked because we catch inside the callback).
27
- *
28
- * When `observers` is provided, the pump fires the corresponding lifecycle
29
- * hooks (`onStart` / `onChunk` / `onEnd` / `onError` / `onAbort`) so
30
- * users can attach instrumentation via `defineStreamObserver(...)`.
44
+ * Each yield is JSON-encoded and written as a `data:` event. If `emitResult`
45
+ * is true and the generator's return value is defined, it is written as
46
+ * `event: result\ndata: <json>` before the stream closes. If the generator
47
+ * throws, an `event: error` or `event: timeout` frame is written and the
48
+ * stream closes cleanly. Observer lifecycle hooks (`onStart` / `onChunk` /
49
+ * `onEnd` / `onError` / `onAbort`) fire from inside the pump.
31
50
  */
32
- export declare function sseGeneratorResponse(c: Context, gen: AsyncGenerator<unknown, unknown, unknown>, options?: SseGeneratorOptions): Response;
51
+ export declare function sseGeneratorResponse(_c: Context, gen: AsyncGenerator<unknown, unknown, unknown>, options?: SseResponseOptions): Response;
33
52
  /**
34
- * Wrap a ReadableStream<T> (with T a JSON-encodable value) as an SSE response.
35
- * Each enqueued chunk is JSON-encoded and written as a `data:` event.
36
- *
37
- * Observer fanout mirrors `sseGeneratorResponse`: `onStart` fires before the
38
- * first read, `onChunk` per chunk, `onEnd` on normal completion, `onError` on
39
- * throw, `onAbort` when the response stream is aborted.
53
+ * Wrap a `ReadableStream<T>` (with `T` a JSON-encodable value) as an SSE
54
+ * response. Each enqueued chunk is JSON-encoded and written as a `data:`
55
+ * event. Observer lifecycle hooks fire identically to `sseGeneratorResponse`;
56
+ * `emitResult` is not meaningful here (streams have no return value) and is
57
+ * ignored.
40
58
  */
41
- export declare function sseReadableStreamResponse(c: Context, source: ReadableStream<unknown>, options?: {
42
- observers?: ReadonlyArray<StreamObserver<unknown, never>>;
43
- observerCtx?: ServerStreamCtx;
44
- }): Response;
59
+ export declare function sseReadableStreamResponse(_c: Context, source: ReadableStream<unknown>, options?: SseResponseOptions): Response;
45
60
  export declare function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown>;