hono-preact 0.2.0 → 0.3.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 (58) 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 +7 -4
  13. package/dist/iso/index.js +5 -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/loader-fetch.js +65 -34
  22. package/dist/iso/internal/loader.d.ts +3 -3
  23. package/dist/iso/internal/route-boundary.d.ts +4 -4
  24. package/dist/iso/internal/safe-redirect.d.ts +7 -0
  25. package/dist/iso/internal/safe-redirect.js +27 -0
  26. package/dist/iso/internal/sse-decoder.d.ts +1 -1
  27. package/dist/iso/internal/sse-decoder.js +40 -26
  28. package/dist/iso/internal.d.ts +7 -1
  29. package/dist/iso/internal.js +8 -1
  30. package/dist/iso/optimistic-action.d.ts +10 -1
  31. package/dist/iso/optimistic-action.js +11 -3
  32. package/dist/iso/optimistic.d.ts +10 -1
  33. package/dist/iso/optimistic.js +45 -5
  34. package/dist/iso/outcomes.d.ts +14 -2
  35. package/dist/iso/outcomes.js +14 -3
  36. package/dist/iso/use-action-result.d.ts +25 -0
  37. package/dist/iso/use-action-result.js +39 -0
  38. package/dist/iso/use-form-status.d.ts +5 -0
  39. package/dist/iso/use-form-status.js +13 -0
  40. package/dist/server/actions-handler.d.ts +7 -0
  41. package/dist/server/actions-handler.js +42 -9
  42. package/dist/server/index.d.ts +2 -1
  43. package/dist/server/index.js +2 -1
  44. package/dist/server/loaders-handler.d.ts +8 -0
  45. package/dist/server/loaders-handler.js +37 -4
  46. package/dist/server/page-action-handler.d.ts +63 -0
  47. package/dist/server/page-action-handler.js +274 -0
  48. package/dist/server/page-action-resolvers.d.ts +28 -0
  49. package/dist/server/page-action-resolvers.js +147 -0
  50. package/dist/server/render.js +41 -3
  51. package/dist/server/route-server-modules.d.ts +7 -8
  52. package/dist/server/route-server-modules.js +7 -8
  53. package/dist/server/speculation-rules.d.ts +3 -0
  54. package/dist/server/speculation-rules.js +8 -0
  55. package/dist/server/sse.d.ts +43 -28
  56. package/dist/server/sse.js +113 -88
  57. package/dist/vite/server-entry.js +10 -2
  58. package/package.json +2 -2
@@ -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, '&')
@@ -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 ');
@@ -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>;
@@ -1,130 +1,155 @@
1
- import { streamSSE } from 'hono/streaming';
2
1
  import { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from '../iso/internal.js';
2
+ function sseEncodeTransform() {
3
+ const encoder = new TextEncoder();
4
+ return new TransformStream({
5
+ transform(frame, controller) {
6
+ const lines = [];
7
+ if (frame.event)
8
+ lines.push(`event: ${frame.event}`);
9
+ if (frame.id)
10
+ lines.push(`id: ${frame.id}`);
11
+ lines.push(`data: ${frame.data}`);
12
+ controller.enqueue(encoder.encode(lines.join('\n') + '\n\n'));
13
+ },
14
+ });
15
+ }
16
+ function isTimeoutAbort(signal) {
17
+ return Boolean(signal?.aborted &&
18
+ signal.reason instanceof DOMException &&
19
+ signal.reason.name === 'TimeoutError');
20
+ }
3
21
  function encodeErrorPayload(err) {
4
22
  const message = err instanceof Error ? err.message : String(err);
5
23
  const name = err instanceof Error ? err.name : 'Error';
6
24
  return JSON.stringify({ message, name });
7
25
  }
8
26
  /**
9
- * Wrap an async generator as an SSE response.
10
- *
11
- * Each yield is JSON-encoded and written as a `data:` event.
12
- * If `emitResult` is true and the generator's return value is defined,
13
- * it is written as `event: result\ndata: <json>` before the stream closes.
14
- * If the generator throws, an `event: error\ndata: {"message","name"}` frame
15
- * is written and the stream closes cleanly (Hono's default error handler is
16
- * never invoked because we catch inside the callback).
27
+ * Adapt a `ReadableStream<T>` as an async generator (with no return value).
28
+ * The reader is released in `finally`, which fires either when the consumer
29
+ * stops iterating or when the source is exhausted.
30
+ */
31
+ async function* iterReadable(stream) {
32
+ const reader = stream.getReader();
33
+ try {
34
+ while (true) {
35
+ const { done, value } = await reader.read();
36
+ if (done)
37
+ return;
38
+ yield value;
39
+ }
40
+ }
41
+ finally {
42
+ reader.cancel().catch(() => undefined);
43
+ }
44
+ }
45
+ /**
46
+ * The shared pump implementation. Iterates `source` (a generator that may
47
+ * return a final value), encodes each yielded value as a JSON `data:` frame,
48
+ * runs the observer lifecycle, and translates errors into `event: error` or
49
+ * `event: timeout` frames.
17
50
  *
18
- * When `observers` is provided, the pump fires the corresponding lifecycle
19
- * hooks (`onStart` / `onChunk` / `onEnd` / `onError` / `onAbort`) so
20
- * users can attach instrumentation via `defineStreamObserver(...)`.
51
+ * Observer state (`chunks`, `started`, `finished`) lives in the outer
52
+ * function scope so the outer ReadableStream's `cancel()` callback can fire
53
+ * `onAbort` when the consumer cancels mid-stream.
21
54
  */
22
- export function sseGeneratorResponse(c, gen, options = {}) {
23
- const { emitResult = false, observers, observerCtx } = options;
55
+ function buildSseResponse(source, options) {
56
+ const { emitResult = false, observers, observerCtx, signal, timeoutMs, } = options;
24
57
  const obs = observers ?? [];
25
- return streamSSE(c, async (stream) => {
26
- let chunks = 0;
27
- let started = false;
58
+ let chunks = 0;
59
+ let started = false;
60
+ let finished = false;
61
+ async function* framePump() {
28
62
  if (obs.length > 0 && observerCtx) {
29
63
  fanStart(obs, observerCtx);
30
64
  started = true;
31
65
  }
32
66
  try {
33
- while (!stream.aborted) {
34
- const step = await gen.next();
67
+ while (true) {
68
+ const step = await source.next();
35
69
  if (step.done) {
36
70
  if (emitResult && step.value !== undefined) {
37
- await stream.writeSSE({
38
- event: 'result',
39
- data: JSON.stringify(step.value),
40
- });
71
+ yield { event: 'result', data: JSON.stringify(step.value) };
41
72
  }
42
73
  if (started && observerCtx) {
43
74
  fanEnd(obs, observerCtx, { chunks, result: step.value });
44
75
  }
76
+ finished = true;
45
77
  return;
46
78
  }
47
- await stream.writeSSE({ data: JSON.stringify(step.value) });
79
+ yield { data: JSON.stringify(step.value) };
48
80
  if (started && observerCtx) {
49
81
  fanChunk(obs, observerCtx, step.value, chunks);
50
82
  }
51
83
  chunks += 1;
52
84
  }
53
- // Loop exited because the response stream was aborted (typically a
54
- // client disconnect). Release the generator and notify observers.
55
- await gen.return(undefined).catch(() => {
56
- /* swallow */
57
- });
58
- if (started && observerCtx) {
59
- fanAbort(obs, observerCtx, { chunks });
60
- }
61
85
  }
62
86
  catch (err) {
63
- await gen.return(undefined).catch(() => {
64
- /* swallow */
65
- });
87
+ await source.return(undefined).catch(() => undefined);
66
88
  if (started && observerCtx) {
67
89
  fanError(obs, observerCtx, err, { chunks });
68
90
  }
69
- await stream.writeSSE({
70
- event: 'error',
71
- data: encodeErrorPayload(err),
72
- });
91
+ if (isTimeoutAbort(signal) && typeof timeoutMs === 'number') {
92
+ yield { event: 'timeout', data: JSON.stringify({ timeoutMs }) };
93
+ }
94
+ else {
95
+ yield { event: 'error', data: encodeErrorPayload(err) };
96
+ }
97
+ finished = true;
73
98
  }
99
+ }
100
+ const pump = framePump();
101
+ const body = new ReadableStream({
102
+ async pull(controller) {
103
+ const { value, done } = await pump.next();
104
+ if (done) {
105
+ controller.close();
106
+ }
107
+ else {
108
+ controller.enqueue(value);
109
+ }
110
+ },
111
+ cancel() {
112
+ // Consumer cancelled before the pump completed. Notify observers via
113
+ // `onAbort` exactly when we've genuinely been aborted mid-stream
114
+ // (i.e. the source started but didn't finish naturally).
115
+ if (!finished && started && observerCtx) {
116
+ fanAbort(obs, observerCtx, { chunks });
117
+ }
118
+ pump.return(undefined).catch(() => undefined);
119
+ source.return(undefined).catch(() => undefined);
120
+ },
121
+ }).pipeThrough(sseEncodeTransform());
122
+ return new Response(body, {
123
+ headers: {
124
+ 'content-type': 'text/event-stream',
125
+ 'cache-control': 'no-cache',
126
+ },
74
127
  });
75
128
  }
76
129
  /**
77
- * Wrap a ReadableStream<T> (with T a JSON-encodable value) as an SSE response.
78
- * Each enqueued chunk is JSON-encoded and written as a `data:` event.
130
+ * Wrap an async generator as an SSE response.
79
131
  *
80
- * Observer fanout mirrors `sseGeneratorResponse`: `onStart` fires before the
81
- * first read, `onChunk` per chunk, `onEnd` on normal completion, `onError` on
82
- * throw, `onAbort` when the response stream is aborted.
132
+ * Each yield is JSON-encoded and written as a `data:` event. If `emitResult`
133
+ * is true and the generator's return value is defined, it is written as
134
+ * `event: result\ndata: <json>` before the stream closes. If the generator
135
+ * throws, an `event: error` or `event: timeout` frame is written and the
136
+ * stream closes cleanly. Observer lifecycle hooks (`onStart` / `onChunk` /
137
+ * `onEnd` / `onError` / `onAbort`) fire from inside the pump.
83
138
  */
84
- export function sseReadableStreamResponse(c, source, options = {}) {
85
- const { observers, observerCtx } = options;
86
- const obs = observers ?? [];
87
- return streamSSE(c, async (stream) => {
88
- const reader = source.getReader();
89
- let chunks = 0;
90
- let started = false;
91
- if (obs.length > 0 && observerCtx) {
92
- fanStart(obs, observerCtx);
93
- started = true;
94
- }
95
- try {
96
- while (!stream.aborted) {
97
- const { done, value } = await reader.read();
98
- if (done) {
99
- if (started && observerCtx) {
100
- fanEnd(obs, observerCtx, { chunks, result: undefined });
101
- }
102
- return;
103
- }
104
- await stream.writeSSE({ data: JSON.stringify(value) });
105
- if (started && observerCtx) {
106
- fanChunk(obs, observerCtx, value, chunks);
107
- }
108
- chunks += 1;
109
- }
110
- if (started && observerCtx) {
111
- fanAbort(obs, observerCtx, { chunks });
112
- }
113
- }
114
- catch (err) {
115
- if (started && observerCtx) {
116
- fanError(obs, observerCtx, err, { chunks });
117
- }
118
- await stream.writeSSE({
119
- event: 'error',
120
- data: encodeErrorPayload(err),
121
- });
122
- }
123
- finally {
124
- reader.cancel().catch(() => {
125
- /* swallow */
126
- });
127
- }
139
+ export function sseGeneratorResponse(_c, gen, options = {}) {
140
+ return buildSseResponse(gen, options);
141
+ }
142
+ /**
143
+ * Wrap a `ReadableStream<T>` (with `T` a JSON-encodable value) as an SSE
144
+ * response. Each enqueued chunk is JSON-encoded and written as a `data:`
145
+ * event. Observer lifecycle hooks fire identically to `sseGeneratorResponse`;
146
+ * `emitResult` is not meaningful here (streams have no return value) and is
147
+ * ignored.
148
+ */
149
+ export function sseReadableStreamResponse(_c, source, options = {}) {
150
+ return buildSseResponse(iterReadable(source), {
151
+ ...options,
152
+ emitResult: false,
128
153
  });
129
154
  }
130
155
  export function isAsyncGenerator(value) {
@@ -22,9 +22,10 @@ export function generateCoreAppModule(opts) {
22
22
  `import { LocationProvider } from 'preact-iso';\n` +
23
23
  `import { Routes, env } from 'hono-preact';\n` +
24
24
  `import {\n` +
25
- ` actionsHandler,\n` +
26
25
  ` loadersHandler,\n` +
26
+ ` makePageActionResolvers,\n` +
27
27
  ` makePageUseResolvers,\n` +
28
+ ` pageActionHandler,\n` +
28
29
  ` renderPage,\n` +
29
30
  ` routeServerModules,\n` +
30
31
  `} from 'hono-preact/server';\n` +
@@ -37,11 +38,18 @@ export function generateCoreAppModule(opts) {
37
38
  `const dev = import.meta.env.DEV;\n` +
38
39
  `const serverModules = routeServerModules(routes);\n` +
39
40
  `const pageUseResolvers = makePageUseResolvers(routes.serverRoutes, { dev });\n` +
41
+ `const pageActionResolvers = makePageActionResolvers(routes.serverRoutes, { dev });\n` +
40
42
  `\n` +
41
43
  `export const app = new Hono()\n` +
42
44
  apiMount +
43
45
  ` .post('/__loaders', loadersHandler(serverModules, { dev, appConfig, resolvePageUse: pageUseResolvers.byPath }))\n` +
44
- ` .post('/__actions', actionsHandler(serverModules, { dev, appConfig, resolvePageUse: pageUseResolvers.byModuleKey }))\n` +
46
+ ` .post('*', pageActionHandler({\n` +
47
+ ` resolverByPath: pageActionResolvers.byPath,\n` +
48
+ ` resolvePageUseByPath: pageUseResolvers.byPath,\n` +
49
+ ` renderPage,\n` +
50
+ ` resolvePageNode: () => h(Layout, null, h(LocationProvider, null, h(Routes, { routes }))),\n` +
51
+ ` appConfig,\n` +
52
+ ` }))\n` +
45
53
  ` .get('*', (c) => renderPage(c, h(Layout, null, h(LocationProvider, null, h(Routes, { routes }))), { appConfig }));\n` +
46
54
  `\n` +
47
55
  `export default app;\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-preact",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Hono on the edge, Preact in the browser, manifest driven routes, typed RPC, streaming everywhere.",
5
5
  "keywords": [
6
6
  "hono",
@@ -95,8 +95,8 @@
95
95
  },
96
96
  "devDependencies": {
97
97
  "typescript": "*",
98
- "@hono-preact/iso": "0.1.0",
99
98
  "@hono-preact/server": "0.1.0",
99
+ "@hono-preact/iso": "0.1.0",
100
100
  "@hono-preact/vite": "0.1.0"
101
101
  },
102
102
  "scripts": {