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
|
@@ -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
|
+
}
|
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 ');
|
|
@@ -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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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>;
|