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.
- 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 +13 -4
- package/dist/iso/index.js +14 -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/history-shim.d.ts +7 -0
- package/dist/iso/internal/history-shim.js +79 -0
- package/dist/iso/internal/loader-fetch.js +65 -34
- package/dist/iso/internal/loader.d.ts +3 -3
- package/dist/iso/internal/merge-refs.d.ts +4 -0
- package/dist/iso/internal/merge-refs.js +14 -0
- package/dist/iso/internal/persist-registry.d.ts +10 -0
- package/dist/iso/internal/persist-registry.js +24 -0
- package/dist/iso/internal/route-boundary.d.ts +4 -4
- package/dist/iso/internal/route-change.d.ts +8 -2
- package/dist/iso/internal/route-change.js +107 -12
- 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/use-render.d.ts +11 -0
- package/dist/iso/internal/use-render.js +47 -0
- package/dist/iso/internal/view-transition-event.d.ts +23 -0
- package/dist/iso/internal/view-transition-event.js +25 -0
- package/dist/iso/internal.d.ts +12 -1
- package/dist/iso/internal.js +13 -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/persist.d.ts +14 -0
- package/dist/iso/persist.js +56 -0
- 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/iso/view-transition-lifecycle.d.ts +9 -0
- package/dist/iso/view-transition-lifecycle.js +18 -0
- package/dist/iso/view-transition-name.d.ts +17 -0
- package/dist/iso/view-transition-name.js +79 -0
- package/dist/iso/view-transition-types.d.ts +8 -0
- package/dist/iso/view-transition-types.js +21 -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 +136 -55
- 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/client-entry.js +12 -3
- package/dist/vite/server-entry.js +10 -2
- package/package.json +2 -2
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) {
|
|
@@ -2,14 +2,23 @@ import * as path from 'node:path';
|
|
|
2
2
|
export const VIRTUAL_CLIENT_ENTRY_ID = 'virtual:hono-preact/client';
|
|
3
3
|
const RESOLVED_ID = '\0' + VIRTUAL_CLIENT_ENTRY_ID;
|
|
4
4
|
export function generateClientEntrySource(opts) {
|
|
5
|
-
return (`import { h, hydrate } from 'preact';\n` +
|
|
5
|
+
return (`import { h, hydrate, render as renderPreact } from 'preact';\n` +
|
|
6
6
|
`import { LocationProvider } from 'preact-iso';\n` +
|
|
7
|
-
`import { Routes } from 'hono-preact';\n` +
|
|
8
|
-
`import { __dispatchRouteChange, installStreamRegistry } from 'hono-preact/internal';\n` +
|
|
7
|
+
`import { Routes, PersistHost } from 'hono-preact';\n` +
|
|
8
|
+
`import { __dispatchRouteChange, installStreamRegistry, installHistoryShim } from 'hono-preact/internal';\n` +
|
|
9
9
|
`import routes from '${opts.routesAbsPath}';\n` +
|
|
10
10
|
`\n` +
|
|
11
|
+
`installHistoryShim();\n` +
|
|
11
12
|
`installStreamRegistry();\n` +
|
|
12
13
|
`\n` +
|
|
14
|
+
`let persistHost = document.getElementById('__hp_persist_root');\n` +
|
|
15
|
+
`if (!persistHost) {\n` +
|
|
16
|
+
` persistHost = document.createElement('div');\n` +
|
|
17
|
+
` persistHost.id = '__hp_persist_root';\n` +
|
|
18
|
+
` document.body.appendChild(persistHost);\n` +
|
|
19
|
+
`}\n` +
|
|
20
|
+
`renderPreact(h(PersistHost, null), persistHost);\n` +
|
|
21
|
+
`\n` +
|
|
13
22
|
`let lastPath;\n` +
|
|
14
23
|
`function onRouteChange(path) {\n` +
|
|
15
24
|
` const from = lastPath;\n` +
|
|
@@ -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.4.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": {
|