hono-preact 0.1.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/README.md +47 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/iso/action.d.ts +78 -0
- package/dist/iso/action.js +189 -0
- package/dist/iso/cache.d.ts +17 -0
- package/dist/iso/cache.js +122 -0
- package/dist/iso/client-script.d.ts +2 -0
- package/dist/iso/client-script.js +13 -0
- package/dist/iso/define-loader.d.ts +47 -0
- package/dist/iso/define-loader.js +118 -0
- package/dist/iso/define-page.d.ts +10 -0
- package/dist/iso/define-page.js +7 -0
- package/dist/iso/define-routes.d.ts +34 -0
- package/dist/iso/define-routes.js +251 -0
- package/dist/iso/form.d.ts +7 -0
- package/dist/iso/form.js +40 -0
- package/dist/iso/guard.d.ts +33 -0
- package/dist/iso/guard.js +32 -0
- package/dist/iso/head.d.ts +6 -0
- package/dist/iso/head.js +4 -0
- package/dist/iso/index.d.ts +30 -0
- package/dist/iso/index.js +29 -0
- package/dist/iso/internal/cache-key.d.ts +2 -0
- package/dist/iso/internal/cache-key.js +8 -0
- package/dist/iso/internal/contexts.d.ts +12 -0
- package/dist/iso/internal/contexts.js +7 -0
- package/dist/iso/internal/envelope.d.ts +8 -0
- package/dist/iso/internal/envelope.js +21 -0
- package/dist/iso/internal/guard-noop.d.ts +7 -0
- package/dist/iso/internal/guard-noop.js +6 -0
- package/dist/iso/internal/guards.d.ts +14 -0
- package/dist/iso/internal/guards.js +54 -0
- package/dist/iso/internal/loader-fetch.d.ts +20 -0
- package/dist/iso/internal/loader-fetch.js +123 -0
- package/dist/iso/internal/loader-runner.d.ts +15 -0
- package/dist/iso/internal/loader-runner.js +59 -0
- package/dist/iso/internal/loader-stub.d.ts +8 -0
- package/dist/iso/internal/loader-stub.js +19 -0
- package/dist/iso/internal/loader.d.ts +13 -0
- package/dist/iso/internal/loader.js +31 -0
- package/dist/iso/internal/optimistic-overlay.d.ts +10 -0
- package/dist/iso/internal/optimistic-overlay.js +11 -0
- package/dist/iso/internal/preload.d.ts +15 -0
- package/dist/iso/internal/preload.js +36 -0
- package/dist/iso/internal/route-boundary.d.ts +25 -0
- package/dist/iso/internal/route-boundary.js +24 -0
- package/dist/iso/internal/route-change.d.ts +4 -0
- package/dist/iso/internal/route-change.js +18 -0
- package/dist/iso/internal/route-locations.d.ts +11 -0
- package/dist/iso/internal/route-locations.js +15 -0
- package/dist/iso/internal/sse-decoder.d.ts +5 -0
- package/dist/iso/internal/sse-decoder.js +43 -0
- package/dist/iso/internal/stream-registry.d.ts +60 -0
- package/dist/iso/internal/stream-registry.js +98 -0
- package/dist/iso/internal/streaming-ssr.d.ts +17 -0
- package/dist/iso/internal/streaming-ssr.js +32 -0
- package/dist/iso/internal/use-loader-runner.d.ts +12 -0
- package/dist/iso/internal/use-loader-runner.js +185 -0
- package/dist/iso/internal/wrap-promise.d.ts +4 -0
- package/dist/iso/internal/wrap-promise.js +24 -0
- package/dist/iso/internal.d.ts +19 -0
- package/dist/iso/internal.js +49 -0
- package/dist/iso/is-browser.d.ts +4 -0
- package/dist/iso/is-browser.js +6 -0
- package/dist/iso/optimistic-action.d.ts +19 -0
- package/dist/iso/optimistic-action.js +25 -0
- package/dist/iso/optimistic.d.ts +5 -0
- package/dist/iso/optimistic.js +31 -0
- package/dist/iso/page.d.ts +16 -0
- package/dist/iso/page.js +10 -0
- package/dist/iso/prefetch.d.ts +22 -0
- package/dist/iso/prefetch.js +78 -0
- package/dist/iso/reload-context.d.ts +6 -0
- package/dist/iso/reload-context.js +9 -0
- package/dist/iso/route-change.d.ts +2 -0
- package/dist/iso/route-change.js +10 -0
- package/dist/iso/view-transitions.d.ts +1 -0
- package/dist/iso/view-transitions.js +6 -0
- package/dist/server/actions-handler.d.ts +33 -0
- package/dist/server/actions-handler.js +159 -0
- package/dist/server/context.d.ts +6 -0
- package/dist/server/context.js +6 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +5 -0
- package/dist/server/loaders-handler.d.ts +30 -0
- package/dist/server/loaders-handler.js +117 -0
- package/dist/server/middleware/location.d.ts +1 -0
- package/dist/server/middleware/location.js +10 -0
- package/dist/server/render.d.ts +5 -0
- package/dist/server/render.js +203 -0
- package/dist/server/route-server-modules.d.ts +12 -0
- package/dist/server/route-server-modules.js +13 -0
- package/dist/server/sse.d.ts +22 -0
- package/dist/server/sse.js +83 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1 -0
- package/dist/vite/client-entry.d.ts +10 -0
- package/dist/vite/client-entry.js +47 -0
- package/dist/vite/client-shim.d.ts +12 -0
- package/dist/vite/client-shim.js +62 -0
- package/dist/vite/guard-strip.d.ts +2 -0
- package/dist/vite/guard-strip.js +96 -0
- package/dist/vite/hono-preact.d.ts +12 -0
- package/dist/vite/hono-preact.js +111 -0
- package/dist/vite/index.d.ts +7 -0
- package/dist/vite/index.js +7 -0
- package/dist/vite/module-key-plugin.d.ts +12 -0
- package/dist/vite/module-key-plugin.js +114 -0
- package/dist/vite/module-key.d.ts +11 -0
- package/dist/vite/module-key.js +20 -0
- package/dist/vite/parser-options.d.ts +16 -0
- package/dist/vite/parser-options.js +22 -0
- package/dist/vite/server-entry.d.ts +26 -0
- package/dist/vite/server-entry.js +201 -0
- package/dist/vite/server-loader-validation.d.ts +2 -0
- package/dist/vite/server-loader-validation.js +73 -0
- package/dist/vite/server-loaders-parser.d.ts +22 -0
- package/dist/vite/server-loaders-parser.js +64 -0
- package/dist/vite/server-only.d.ts +3 -0
- package/dist/vite/server-only.js +244 -0
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readSSE } from './sse-decoder.js';
|
|
2
|
+
/**
|
|
3
|
+
* POST to /__loaders and consume the response.
|
|
4
|
+
*
|
|
5
|
+
* Static loaders return JSON; the parsed value resolves the returned promise.
|
|
6
|
+
* Streaming loaders return SSE; the first chunk resolves the promise, and
|
|
7
|
+
* subsequent chunks fire callbacks.onChunk. Stream errors after the first
|
|
8
|
+
* chunk fire callbacks.onError. Stream end fires callbacks.onEnd.
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchLoaderData(moduleKey, loaderName, location, signal, callbacks) {
|
|
11
|
+
const res = await fetch('/__loaders', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14
|
+
body: JSON.stringify({ module: moduleKey, loader: loaderName, location }),
|
|
15
|
+
signal,
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const body = (await res.json().catch(() => ({})));
|
|
19
|
+
throw new Error(body.error ?? `Loader failed with status ${res.status}`);
|
|
20
|
+
}
|
|
21
|
+
const contentType = res.headers.get('Content-Type') ?? '';
|
|
22
|
+
if (!contentType.includes('text/event-stream')) {
|
|
23
|
+
const json = (await res.json());
|
|
24
|
+
// Server-side `GuardRedirect` thrown from a loader (or a guard that runs
|
|
25
|
+
// inside it) comes back as a `{ __redirect }` envelope. Hand off to the
|
|
26
|
+
// browser via `location.assign` and return a promise that never settles:
|
|
27
|
+
// the current document is being replaced, no caller will see a value.
|
|
28
|
+
if (json !== null &&
|
|
29
|
+
typeof json === 'object' &&
|
|
30
|
+
'__redirect' in json &&
|
|
31
|
+
typeof json.__redirect === 'string') {
|
|
32
|
+
if (typeof window !== 'undefined') {
|
|
33
|
+
window.location.assign(json.__redirect);
|
|
34
|
+
}
|
|
35
|
+
return new Promise(() => {
|
|
36
|
+
/* never resolves; page is navigating */
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return json;
|
|
40
|
+
}
|
|
41
|
+
if (!res.body) {
|
|
42
|
+
throw new Error('Streaming loader response has no body');
|
|
43
|
+
}
|
|
44
|
+
// SSE: read the first message event synchronously (await first chunk),
|
|
45
|
+
// then kick off an async loop that pushes subsequent chunks to callbacks.
|
|
46
|
+
const iter = readSSE(res.body);
|
|
47
|
+
let firstChunk;
|
|
48
|
+
while (true) {
|
|
49
|
+
const step = await iter.next();
|
|
50
|
+
if (step.done) {
|
|
51
|
+
// Stream closed before any data event: error
|
|
52
|
+
throw new Error('Streaming loader closed before emitting any data');
|
|
53
|
+
}
|
|
54
|
+
const ev = step.value;
|
|
55
|
+
if (ev.event === 'message') {
|
|
56
|
+
try {
|
|
57
|
+
firstChunk = JSON.parse(ev.data);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error('Malformed first chunk in streaming loader');
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (ev.event === 'error') {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(ev.data);
|
|
67
|
+
const err = new Error(parsed.message ?? 'Streamed error');
|
|
68
|
+
if (parsed.name)
|
|
69
|
+
err.name = parsed.name;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e instanceof Error && e.message.startsWith('Malformed')) {
|
|
74
|
+
throw new Error('Malformed error event in streaming loader');
|
|
75
|
+
}
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Other events (result, etc.): ignore for loaders
|
|
80
|
+
}
|
|
81
|
+
// Continue consuming chunks in the background. Each subsequent message
|
|
82
|
+
// pushes a value via onChunk. Errors fire onError. End fires onEnd.
|
|
83
|
+
(async () => {
|
|
84
|
+
try {
|
|
85
|
+
while (true) {
|
|
86
|
+
const step = await iter.next();
|
|
87
|
+
if (step.done) {
|
|
88
|
+
callbacks.onEnd();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const ev = step.value;
|
|
92
|
+
if (ev.event === 'message') {
|
|
93
|
+
try {
|
|
94
|
+
callbacks.onChunk(JSON.parse(ev.data));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// malformed mid-stream chunk: skip
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (ev.event === 'error') {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(ev.data);
|
|
103
|
+
const err = new Error(parsed.message ?? 'Streamed error');
|
|
104
|
+
if (parsed.name)
|
|
105
|
+
err.name = parsed.name;
|
|
106
|
+
callbacks.onError(err);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
callbacks.onError(new Error('Streamed error'));
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Ignore other event types
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (signal.aborted)
|
|
118
|
+
return;
|
|
119
|
+
callbacks.onError(err instanceof Error ? err : new Error(String(err)));
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
return firstChunk;
|
|
123
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RouteHook } from 'preact-iso';
|
|
2
|
+
import type { LoaderRef } from '../define-loader.js';
|
|
3
|
+
export type LoaderRunCallbacks<T> = {
|
|
4
|
+
onChunk: (value: T) => void;
|
|
5
|
+
onError: (err: Error) => void;
|
|
6
|
+
onEnd: () => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Invoke a loader at runtime. Picks between client-side RPC fetch
|
|
10
|
+
* (when in browser + has __moduleKey) and direct fn invocation (SSR
|
|
11
|
+
* or test). Direct fn handles async-generator + ReadableStream by
|
|
12
|
+
* unwrapping the first chunk and registering continued chunks with
|
|
13
|
+
* the per-id streaming-SSR registry.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runLoader<T>(loaderRef: LoaderRef<T>, location: RouteHook, id: string, signal: AbortSignal, callbacks: LoaderRunCallbacks<T>): Promise<T>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isBrowser } from '../is-browser.js';
|
|
2
|
+
import { getRequestHonoContext } from '../cache.js';
|
|
3
|
+
import { fetchLoaderData } from './loader-fetch.js';
|
|
4
|
+
import { registerServerStreamingLoader } from './streaming-ssr.js';
|
|
5
|
+
function isAsyncGenerator(value) {
|
|
6
|
+
return (value != null &&
|
|
7
|
+
typeof value === 'object' &&
|
|
8
|
+
typeof value[Symbol.asyncIterator] === 'function' &&
|
|
9
|
+
typeof value.next === 'function');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Invoke a loader at runtime. Picks between client-side RPC fetch
|
|
13
|
+
* (when in browser + has __moduleKey) and direct fn invocation (SSR
|
|
14
|
+
* or test). Direct fn handles async-generator + ReadableStream by
|
|
15
|
+
* unwrapping the first chunk and registering continued chunks with
|
|
16
|
+
* the per-id streaming-SSR registry.
|
|
17
|
+
*/
|
|
18
|
+
export function runLoader(loaderRef, location, id, signal, callbacks) {
|
|
19
|
+
const useFetchPath = isBrowser() &&
|
|
20
|
+
typeof fetch === 'function' &&
|
|
21
|
+
loaderRef.__moduleKey !== undefined;
|
|
22
|
+
const loaderName = loaderRef.__loaderName ?? 'default';
|
|
23
|
+
if (useFetchPath) {
|
|
24
|
+
return fetchLoaderData(loaderRef.__moduleKey, loaderName, {
|
|
25
|
+
path: location.path,
|
|
26
|
+
pathParams: (location.pathParams ?? {}),
|
|
27
|
+
searchParams: (location.searchParams ?? {}),
|
|
28
|
+
}, signal, callbacks);
|
|
29
|
+
}
|
|
30
|
+
// Direct-fn path. Result may be a Promise<T>, a
|
|
31
|
+
// Promise<ReadableStream<T>>, or an AsyncGenerator<T>. For an async
|
|
32
|
+
// generator (server-side streaming loader), take the first chunk
|
|
33
|
+
// for the Suspense render and register the rest with the per-request
|
|
34
|
+
// streaming-ssr registry so renderPage can flush further chunks.
|
|
35
|
+
return (async () => {
|
|
36
|
+
const ctx = {
|
|
37
|
+
location,
|
|
38
|
+
signal,
|
|
39
|
+
get c() {
|
|
40
|
+
const c = getRequestHonoContext();
|
|
41
|
+
if (c === undefined) {
|
|
42
|
+
throw new Error('ctx.c is not available: this loader was invoked without an active server request scope. ' +
|
|
43
|
+
'Loaders that read ctx.c run inside loadersHandler (RPC) or renderPage (SSR); test/edge paths must avoid reading it.');
|
|
44
|
+
}
|
|
45
|
+
return c;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const result = await loaderRef.fn(ctx);
|
|
49
|
+
if (isAsyncGenerator(result)) {
|
|
50
|
+
const step = await result.next();
|
|
51
|
+
if (step.done) {
|
|
52
|
+
return undefined; // generator returned without yielding
|
|
53
|
+
}
|
|
54
|
+
registerServerStreamingLoader(id, result);
|
|
55
|
+
return step.value;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
})();
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineLoader } from '../define-loader.js';
|
|
2
|
+
import { fetchLoaderData } from './loader-fetch.js';
|
|
3
|
+
export function __$createLoaderStub_hpiso(opts) {
|
|
4
|
+
// LoaderHost's useLoaderRunner is the actual driver in the browser; this fn
|
|
5
|
+
// is the SSR / direct-fn fallback path only. The callbacks are intentionally
|
|
6
|
+
// no-ops: the consumer awaits the returned Promise<T> for the final value and
|
|
7
|
+
// has no use for intermediate chunk or error notifications.
|
|
8
|
+
const fn = async ({ location, signal, }) => fetchLoaderData(opts.__moduleKey, opts.__loaderName, {
|
|
9
|
+
path: location.path,
|
|
10
|
+
pathParams: location.pathParams,
|
|
11
|
+
searchParams: location.searchParams,
|
|
12
|
+
}, signal ?? new AbortController().signal, { onChunk: () => { }, onError: () => { }, onEnd: () => { } });
|
|
13
|
+
// defineLoader does the cache + symbol + useData/useError plumbing.
|
|
14
|
+
return defineLoader(fn, {
|
|
15
|
+
__moduleKey: opts.__moduleKey,
|
|
16
|
+
__loaderName: opts.__loaderName,
|
|
17
|
+
params: opts.params,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComponentChildren, JSX } from 'preact';
|
|
2
|
+
import type { RouteHook } from 'preact-iso';
|
|
3
|
+
import type { LoaderRef } from '../define-loader.js';
|
|
4
|
+
export { serializeLocationForCache } from './cache-key.js';
|
|
5
|
+
type LoaderHostProps<T> = {
|
|
6
|
+
loader: LoaderRef<T>;
|
|
7
|
+
location?: RouteHook;
|
|
8
|
+
fallback?: JSX.Element;
|
|
9
|
+
errorFallback?: ComponentChildren | ((err: Error, reset: () => void) => ComponentChildren);
|
|
10
|
+
children: ComponentChildren;
|
|
11
|
+
};
|
|
12
|
+
export declare function LoaderHost<T>({ loader: loaderRef, location: locationProp, fallback, errorFallback, children, }: LoaderHostProps<T>): JSX.Element;
|
|
13
|
+
export { LoaderHost as Loader };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { Suspense } from 'preact/compat';
|
|
3
|
+
import { useContext, useId } from 'preact/hooks';
|
|
4
|
+
import { ReloadContext } from '../reload-context.js';
|
|
5
|
+
import { ActiveLoaderIdContext, LoaderDataContext, LoaderErrorContext, LoaderIdContext, } from './contexts.js';
|
|
6
|
+
import { RouteLocationsContext } from './route-locations.js';
|
|
7
|
+
import { ErrorBoundary } from './route-boundary.js';
|
|
8
|
+
import { Envelope } from './envelope.js';
|
|
9
|
+
import { useLoaderRunner } from './use-loader-runner.js';
|
|
10
|
+
export { serializeLocationForCache } from './cache-key.js';
|
|
11
|
+
export function LoaderHost({ loader: loaderRef, location: locationProp, fallback, errorFallback, children, }) {
|
|
12
|
+
const id = useId();
|
|
13
|
+
const locMap = useContext(RouteLocationsContext);
|
|
14
|
+
const ctxLocation = loaderRef.__moduleKey
|
|
15
|
+
? locMap?.get(loaderRef.__moduleKey)
|
|
16
|
+
: undefined;
|
|
17
|
+
const location = (locationProp ?? ctxLocation);
|
|
18
|
+
if (!location) {
|
|
19
|
+
throw new Error(`Loader for module '${loaderRef.__moduleKey ?? '<unkeyed>'}' has no location: ` +
|
|
20
|
+
`wrap the page in a route that owns this server module, or pass location explicitly.`);
|
|
21
|
+
}
|
|
22
|
+
const { reader, overrideData, error, reload, reloading } = useLoaderRunner(loaderRef, location, id);
|
|
23
|
+
const suspenseContent = (_jsx(Suspense, { fallback: fallback, children: _jsx(DataReader, { reader: reader, overrideData: overrideData, children: _jsx(Envelope, { children: children }) }) }));
|
|
24
|
+
return (_jsx(LoaderIdContext.Provider, { value: id, children: _jsx(ActiveLoaderIdContext.Provider, { value: loaderRef.__id, children: _jsx(ReloadContext.Provider, { value: { reload, reloading }, children: _jsx(LoaderErrorContext.Provider, { value: error, children: errorFallback != null ? (_jsx(ErrorBoundary, { fallback: errorFallback, children: suspenseContent })) : (suspenseContent) }) }) }) }));
|
|
25
|
+
}
|
|
26
|
+
// Public name consumed by define-loader.ts and user code.
|
|
27
|
+
export { LoaderHost as Loader };
|
|
28
|
+
function DataReader({ reader, overrideData, children }) {
|
|
29
|
+
const data = overrideData !== undefined ? overrideData : reader.read();
|
|
30
|
+
return (_jsx(LoaderDataContext.Provider, { value: { data }, children: children }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentChildren } from 'preact';
|
|
2
|
+
import type { LoaderRef } from '../define-loader.js';
|
|
3
|
+
type OverlayProps<T, A> = {
|
|
4
|
+
loader: LoaderRef<T>;
|
|
5
|
+
reducer: (base: T, action: A) => T;
|
|
6
|
+
pending?: A[];
|
|
7
|
+
children: ComponentChildren;
|
|
8
|
+
};
|
|
9
|
+
export declare function OptimisticOverlay<T, A>({ reducer, pending, children, }: OverlayProps<T, A>): import("preact").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { useContext } from 'preact/hooks';
|
|
3
|
+
import { LoaderDataContext } from './contexts.js';
|
|
4
|
+
export function OptimisticOverlay({ reducer, pending = [], children, }) {
|
|
5
|
+
const ctx = useContext(LoaderDataContext);
|
|
6
|
+
if (!ctx)
|
|
7
|
+
throw new Error('<OptimisticOverlay> must be inside a route page that has a loader');
|
|
8
|
+
const base = ctx.data;
|
|
9
|
+
const projected = pending.reduce((acc, action) => reducer(acc, action), base);
|
|
10
|
+
return (_jsx(LoaderDataContext.Provider, { value: { data: projected }, children: children }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure read of the SSR'd preload payload for a loader. Safe to call during
|
|
3
|
+
* render: no DOM mutation, no side effects. The caller is responsible for
|
|
4
|
+
* scheduling `deletePreloadedData` in a useEffect after consuming the
|
|
5
|
+
* value so the attribute is cleared before a future re-mount could read
|
|
6
|
+
* a stale payload.
|
|
7
|
+
*
|
|
8
|
+
* (Previously this function deleted the attribute synchronously via a
|
|
9
|
+
* `finally` block during render. That was a real DOM mutation in the
|
|
10
|
+
* render phase, which Preact's reconciliation does not formally support
|
|
11
|
+
* and which broke determinism if the component re-rendered before the
|
|
12
|
+
* effect that consumed the value ran.)
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPreloadedData<T>(id: string): T | null;
|
|
15
|
+
export declare function deletePreloadedData(id: string): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isBrowser } from '../is-browser.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure read of the SSR'd preload payload for a loader. Safe to call during
|
|
4
|
+
* render: no DOM mutation, no side effects. The caller is responsible for
|
|
5
|
+
* scheduling `deletePreloadedData` in a useEffect after consuming the
|
|
6
|
+
* value so the attribute is cleared before a future re-mount could read
|
|
7
|
+
* a stale payload.
|
|
8
|
+
*
|
|
9
|
+
* (Previously this function deleted the attribute synchronously via a
|
|
10
|
+
* `finally` block during render. That was a real DOM mutation in the
|
|
11
|
+
* render phase, which Preact's reconciliation does not formally support
|
|
12
|
+
* and which broke determinism if the component re-rendered before the
|
|
13
|
+
* effect that consumed the value ran.)
|
|
14
|
+
*/
|
|
15
|
+
export function getPreloadedData(id) {
|
|
16
|
+
if (!isBrowser()) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const el = document.getElementById(id);
|
|
20
|
+
if (!el || !('loader' in el.dataset)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(el.dataset.loader ?? 'null');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function deletePreloadedData(id) {
|
|
31
|
+
const el = document.getElementById(id);
|
|
32
|
+
if (!el) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
delete el.dataset.loader;
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Component } from 'preact';
|
|
2
|
+
import type { ComponentChildren, FunctionComponent, JSX } from 'preact';
|
|
3
|
+
type ErrorFallback = JSX.Element | ((error: Error, reset: () => void) => JSX.Element);
|
|
4
|
+
type ErrorBoundaryProps = {
|
|
5
|
+
fallback?: ErrorFallback;
|
|
6
|
+
children: ComponentChildren;
|
|
7
|
+
};
|
|
8
|
+
export declare class ErrorBoundary extends Component<ErrorBoundaryProps, {
|
|
9
|
+
error: Error | null;
|
|
10
|
+
}> {
|
|
11
|
+
state: {
|
|
12
|
+
error: Error | null;
|
|
13
|
+
};
|
|
14
|
+
static getDerivedStateFromError(error: unknown): {
|
|
15
|
+
error: Error;
|
|
16
|
+
};
|
|
17
|
+
reset: () => void;
|
|
18
|
+
render(): ComponentChildren;
|
|
19
|
+
}
|
|
20
|
+
export declare const RouteBoundary: FunctionComponent<{
|
|
21
|
+
fallback?: JSX.Element;
|
|
22
|
+
errorFallback?: ErrorFallback;
|
|
23
|
+
children: ComponentChildren;
|
|
24
|
+
}>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { Component } from 'preact';
|
|
3
|
+
import { Suspense } from 'preact/compat';
|
|
4
|
+
export class ErrorBoundary extends Component {
|
|
5
|
+
state = { error: null };
|
|
6
|
+
static getDerivedStateFromError(error) {
|
|
7
|
+
return { error: error instanceof Error ? error : new Error(String(error)) };
|
|
8
|
+
}
|
|
9
|
+
reset = () => {
|
|
10
|
+
this.setState({ error: null });
|
|
11
|
+
};
|
|
12
|
+
render() {
|
|
13
|
+
const { error } = this.state;
|
|
14
|
+
if (!error)
|
|
15
|
+
return this.props.children;
|
|
16
|
+
const f = this.props.fallback;
|
|
17
|
+
if (typeof f === 'function')
|
|
18
|
+
return f(error, this.reset);
|
|
19
|
+
if (f)
|
|
20
|
+
return f;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const RouteBoundary = ({ fallback, errorFallback, children }) => (_jsx(ErrorBoundary, { fallback: errorFallback, children: _jsx(Suspense, { fallback: fallback, children: children }) }));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { flushSync } from 'preact/compat';
|
|
2
|
+
const subs = new Set();
|
|
3
|
+
export function __dispatchRouteChange(to, from) {
|
|
4
|
+
for (const cb of subs)
|
|
5
|
+
cb(to, from);
|
|
6
|
+
if (typeof document === 'undefined')
|
|
7
|
+
return;
|
|
8
|
+
const startViewTransition = document.startViewTransition;
|
|
9
|
+
if (typeof startViewTransition !== 'function')
|
|
10
|
+
return;
|
|
11
|
+
startViewTransition.call(document, () => flushSync(() => { }));
|
|
12
|
+
}
|
|
13
|
+
export function __subscribeRouteChange(sub) {
|
|
14
|
+
subs.add(sub);
|
|
15
|
+
return () => {
|
|
16
|
+
subs.delete(sub);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentChildren } from 'preact';
|
|
2
|
+
import type { RouteHook } from 'preact-iso';
|
|
3
|
+
export declare const RouteLocationsContext: import("preact").Context<ReadonlyMap<string, RouteHook>>;
|
|
4
|
+
export declare function RouteLocationsProvider({ moduleKey, location, children, }: {
|
|
5
|
+
moduleKey: string | undefined;
|
|
6
|
+
location: RouteHook;
|
|
7
|
+
children?: ComponentChildren;
|
|
8
|
+
}): import("preact").VNode<import("preact").Attributes & {
|
|
9
|
+
value: ReadonlyMap<string, RouteHook>;
|
|
10
|
+
children?: ComponentChildren;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, h } from 'preact';
|
|
2
|
+
import { useContext, useMemo } from 'preact/hooks';
|
|
3
|
+
const EMPTY_MAP = Object.freeze(new Map());
|
|
4
|
+
export const RouteLocationsContext = createContext(EMPTY_MAP);
|
|
5
|
+
export function RouteLocationsProvider({ moduleKey, location, children, }) {
|
|
6
|
+
const parent = useContext(RouteLocationsContext);
|
|
7
|
+
const next = useMemo(() => {
|
|
8
|
+
if (!moduleKey)
|
|
9
|
+
return parent;
|
|
10
|
+
const m = new Map(parent);
|
|
11
|
+
m.set(moduleKey, location);
|
|
12
|
+
return m;
|
|
13
|
+
}, [parent, moduleKey, location]);
|
|
14
|
+
return h(RouteLocationsContext.Provider, { value: next }, children);
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export async function* readSSE(stream) {
|
|
2
|
+
const reader = stream.getReader();
|
|
3
|
+
const decoder = new TextDecoder();
|
|
4
|
+
let buffer = '';
|
|
5
|
+
let event = 'message';
|
|
6
|
+
let dataLines = [];
|
|
7
|
+
try {
|
|
8
|
+
while (true) {
|
|
9
|
+
const { done, value } = await reader.read();
|
|
10
|
+
if (done) {
|
|
11
|
+
buffer += decoder.decode();
|
|
12
|
+
if (dataLines.length)
|
|
13
|
+
yield { event, data: dataLines.join('\n') };
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
buffer += decoder.decode(value, { stream: true });
|
|
17
|
+
let nl;
|
|
18
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
19
|
+
const line = buffer.slice(0, nl).replace(/\r$/, '');
|
|
20
|
+
buffer = buffer.slice(nl + 1);
|
|
21
|
+
if (line === '') {
|
|
22
|
+
if (dataLines.length) {
|
|
23
|
+
yield { event, data: dataLines.join('\n') };
|
|
24
|
+
}
|
|
25
|
+
event = 'message';
|
|
26
|
+
dataLines = [];
|
|
27
|
+
}
|
|
28
|
+
else if (line.startsWith(':')) {
|
|
29
|
+
// SSE comment / keepalive, ignore
|
|
30
|
+
}
|
|
31
|
+
else if (line.startsWith('event:')) {
|
|
32
|
+
event = line.slice(6).trim();
|
|
33
|
+
}
|
|
34
|
+
else if (line.startsWith('data:')) {
|
|
35
|
+
dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
reader.releaseLock();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
type StreamEvent = {
|
|
2
|
+
type: 'push';
|
|
3
|
+
loaderId: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
} | {
|
|
6
|
+
type: 'end';
|
|
7
|
+
loaderId: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'error';
|
|
10
|
+
loaderId: string;
|
|
11
|
+
error: {
|
|
12
|
+
message: string;
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type Subscriber = {
|
|
17
|
+
push: (value: unknown) => void;
|
|
18
|
+
end: () => void;
|
|
19
|
+
error: (err: Error) => void;
|
|
20
|
+
};
|
|
21
|
+
type StreamRegistry = {
|
|
22
|
+
push: (loaderId: string, value: unknown) => void;
|
|
23
|
+
end: (loaderId: string) => void;
|
|
24
|
+
error: (loaderId: string, error: {
|
|
25
|
+
message: string;
|
|
26
|
+
name: string;
|
|
27
|
+
}) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Pre-hydration buffer. The SSR inline bootstrap script populates this
|
|
30
|
+
* before the client bundle loads. After `installStreamRegistry()` runs,
|
|
31
|
+
* the field continues to back per-loader buffering of events whose
|
|
32
|
+
* subscriber hasn't mounted yet (the common case during streaming SSR).
|
|
33
|
+
*/
|
|
34
|
+
queue?: StreamEvent[];
|
|
35
|
+
};
|
|
36
|
+
declare global {
|
|
37
|
+
interface Window {
|
|
38
|
+
__HP_STREAM__?: StreamRegistry;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Subscribe a loader mount to events for its loader id. Returns an
|
|
43
|
+
* unsubscribe function. Drains any buffered events for this id via a
|
|
44
|
+
* microtask so it is safe to call during a render pass: the current
|
|
45
|
+
* render commits first, then the drain fires setState-y callbacks and
|
|
46
|
+
* Preact re-renders normally.
|
|
47
|
+
*/
|
|
48
|
+
export declare function subscribeToLoaderStream(loaderId: string, sub: Subscriber): () => void;
|
|
49
|
+
/**
|
|
50
|
+
* Install the live dispatcher on `window.__HP_STREAM__`. Any events that
|
|
51
|
+
* were buffered by the SSR inline bootstrap (in `window.__HP_STREAM__.queue`)
|
|
52
|
+
* are routed through `dispatchOrBuffer`, which either delivers them to an
|
|
53
|
+
* already-registered subscriber or holds them until one registers.
|
|
54
|
+
*/
|
|
55
|
+
export declare function installStreamRegistry(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Test-only: clear all buffers and subscribers. Not exposed via internal.ts.
|
|
58
|
+
*/
|
|
59
|
+
export declare function __resetStreamRegistryForTests(): void;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const subscribers = new Map();
|
|
2
|
+
const buffered = new Map();
|
|
3
|
+
function dispatchOrBuffer(ev) {
|
|
4
|
+
const sub = subscribers.get(ev.loaderId);
|
|
5
|
+
if (sub) {
|
|
6
|
+
dispatch(ev, sub);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
let bucket = buffered.get(ev.loaderId);
|
|
10
|
+
if (!bucket) {
|
|
11
|
+
bucket = [];
|
|
12
|
+
buffered.set(ev.loaderId, bucket);
|
|
13
|
+
}
|
|
14
|
+
bucket.push(ev);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function dispatch(ev, sub) {
|
|
18
|
+
if (ev.type === 'push')
|
|
19
|
+
sub.push(ev.value);
|
|
20
|
+
else if (ev.type === 'end')
|
|
21
|
+
sub.end();
|
|
22
|
+
else if (ev.type === 'error') {
|
|
23
|
+
const err = new Error(ev.error.message);
|
|
24
|
+
err.name = ev.error.name;
|
|
25
|
+
sub.error(err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Subscribe a loader mount to events for its loader id. Returns an
|
|
30
|
+
* unsubscribe function. Drains any buffered events for this id via a
|
|
31
|
+
* microtask so it is safe to call during a render pass: the current
|
|
32
|
+
* render commits first, then the drain fires setState-y callbacks and
|
|
33
|
+
* Preact re-renders normally.
|
|
34
|
+
*/
|
|
35
|
+
export function subscribeToLoaderStream(loaderId, sub) {
|
|
36
|
+
subscribers.set(loaderId, sub);
|
|
37
|
+
const bucket = buffered.get(loaderId);
|
|
38
|
+
if (bucket && bucket.length > 0) {
|
|
39
|
+
buffered.delete(loaderId);
|
|
40
|
+
// Defer to a microtask so this is safe to call during a render: the
|
|
41
|
+
// current render commits, then the drain fires setState-y callbacks
|
|
42
|
+
// and Preact re-renders normally.
|
|
43
|
+
queueMicrotask(() => {
|
|
44
|
+
// The subscriber may have unmounted between subscribe and the
|
|
45
|
+
// microtask firing; skip the drain in that case.
|
|
46
|
+
if (subscribers.get(loaderId) !== sub)
|
|
47
|
+
return;
|
|
48
|
+
for (const ev of bucket)
|
|
49
|
+
dispatch(ev, sub);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return () => {
|
|
53
|
+
if (subscribers.get(loaderId) === sub)
|
|
54
|
+
subscribers.delete(loaderId);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Install the live dispatcher on `window.__HP_STREAM__`. Any events that
|
|
59
|
+
* were buffered by the SSR inline bootstrap (in `window.__HP_STREAM__.queue`)
|
|
60
|
+
* are routed through `dispatchOrBuffer`, which either delivers them to an
|
|
61
|
+
* already-registered subscriber or holds them until one registers.
|
|
62
|
+
*/
|
|
63
|
+
export function installStreamRegistry() {
|
|
64
|
+
if (typeof window === 'undefined')
|
|
65
|
+
return;
|
|
66
|
+
const existing = window.__HP_STREAM__;
|
|
67
|
+
const initialQueue = existing?.queue ?? [];
|
|
68
|
+
const wasCapped = existing?.capped === true;
|
|
69
|
+
window.__HP_STREAM__ = {
|
|
70
|
+
push(loaderId, value) {
|
|
71
|
+
dispatchOrBuffer({ type: 'push', loaderId, value });
|
|
72
|
+
},
|
|
73
|
+
end(loaderId) {
|
|
74
|
+
dispatchOrBuffer({ type: 'end', loaderId });
|
|
75
|
+
},
|
|
76
|
+
error(loaderId, error) {
|
|
77
|
+
dispatchOrBuffer({ type: 'error', loaderId, error });
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
for (const ev of initialQueue)
|
|
81
|
+
dispatchOrBuffer(ev);
|
|
82
|
+
if (wasCapped) {
|
|
83
|
+
// The SSR-emitted bootstrap dropped events because the buffer hit its
|
|
84
|
+
// cap before this function ran. Surface to the dev console so a slow
|
|
85
|
+
// bundle-load / blocked CDN scenario is debuggable, not just silently
|
|
86
|
+
// lossy on the client.
|
|
87
|
+
console.warn('[hono-preact] streaming bootstrap buffer was capped before the client ' +
|
|
88
|
+
'bundle loaded — some streaming-loader events were dropped. Likely ' +
|
|
89
|
+
'cause: slow or blocked client bundle load.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Test-only: clear all buffers and subscribers. Not exposed via internal.ts.
|
|
94
|
+
*/
|
|
95
|
+
export function __resetStreamRegistryForTests() {
|
|
96
|
+
subscribers.clear();
|
|
97
|
+
buffered.clear();
|
|
98
|
+
}
|