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.
- package/dist/iso/action-result-context.d.ts +22 -0
- package/dist/iso/action-result-context.js +2 -0
- package/dist/iso/action.d.ts +52 -13
- package/dist/iso/action.js +204 -88
- package/dist/iso/cache.d.ts +9 -0
- package/dist/iso/cache.js +26 -0
- package/dist/iso/define-app.d.ts +7 -0
- package/dist/iso/define-loader.d.ts +12 -0
- package/dist/iso/define-loader.js +26 -16
- package/dist/iso/form.d.ts +13 -4
- package/dist/iso/form.js +115 -33
- package/dist/iso/index.d.ts +7 -4
- package/dist/iso/index.js +5 -2
- package/dist/iso/internal/action-envelope.d.ts +37 -0
- package/dist/iso/internal/action-envelope.js +47 -0
- package/dist/iso/internal/action-result-store.d.ts +28 -0
- package/dist/iso/internal/action-result-store.js +35 -0
- package/dist/iso/internal/envelope.js +1 -2
- package/dist/iso/internal/form-submit-store.d.ts +9 -0
- package/dist/iso/internal/form-submit-store.js +32 -0
- package/dist/iso/internal/loader-fetch.js +65 -34
- package/dist/iso/internal/loader.d.ts +3 -3
- package/dist/iso/internal/route-boundary.d.ts +4 -4
- package/dist/iso/internal/safe-redirect.d.ts +7 -0
- package/dist/iso/internal/safe-redirect.js +27 -0
- package/dist/iso/internal/sse-decoder.d.ts +1 -1
- package/dist/iso/internal/sse-decoder.js +40 -26
- package/dist/iso/internal.d.ts +7 -1
- package/dist/iso/internal.js +8 -1
- package/dist/iso/optimistic-action.d.ts +10 -1
- package/dist/iso/optimistic-action.js +11 -3
- package/dist/iso/optimistic.d.ts +10 -1
- package/dist/iso/optimistic.js +45 -5
- package/dist/iso/outcomes.d.ts +14 -2
- package/dist/iso/outcomes.js +14 -3
- package/dist/iso/use-action-result.d.ts +25 -0
- package/dist/iso/use-action-result.js +39 -0
- package/dist/iso/use-form-status.d.ts +5 -0
- package/dist/iso/use-form-status.js +13 -0
- package/dist/server/actions-handler.d.ts +7 -0
- package/dist/server/actions-handler.js +42 -9
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/loaders-handler.d.ts +8 -0
- package/dist/server/loaders-handler.js +37 -4
- package/dist/server/page-action-handler.d.ts +63 -0
- package/dist/server/page-action-handler.js +274 -0
- package/dist/server/page-action-resolvers.d.ts +28 -0
- package/dist/server/page-action-resolvers.js +147 -0
- package/dist/server/render.js +41 -3
- package/dist/server/route-server-modules.d.ts +7 -8
- package/dist/server/route-server-modules.js +7 -8
- package/dist/server/speculation-rules.d.ts +3 -0
- package/dist/server/speculation-rules.js +8 -0
- package/dist/server/sse.d.ts +43 -28
- package/dist/server/sse.js +113 -88
- package/dist/vite/server-entry.js +10 -2
- package/package.json +2 -2
package/dist/server/render.js
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
+
}
|
package/dist/server/sse.d.ts
CHANGED
|
@@ -1,45 +1,60 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
2
|
import type { StreamObserver, ServerStreamCtx } from '../iso/index';
|
|
3
|
-
|
|
4
|
-
|
|
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).
|
|
9
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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(
|
|
51
|
+
export declare function sseGeneratorResponse(_c: Context, gen: AsyncGenerator<unknown, unknown, unknown>, options?: SseResponseOptions): Response;
|
|
33
52
|
/**
|
|
34
|
-
* Wrap a ReadableStream<T
|
|
35
|
-
* Each enqueued chunk is JSON-encoded and written as a `data:`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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(
|
|
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>;
|
package/dist/server/sse.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 (
|
|
34
|
-
const step = await
|
|
67
|
+
while (true) {
|
|
68
|
+
const step = await source.next();
|
|
35
69
|
if (step.done) {
|
|
36
70
|
if (emitResult && step.value !== undefined) {
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
70
|
-
event: '
|
|
71
|
-
|
|
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
|
|
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
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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('
|
|
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.
|
|
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": {
|