hono-preact 0.3.0 → 0.5.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.
@@ -51,7 +51,6 @@ export type RoutesManifest = {
51
51
  export declare function defineRoutes(tree: RouteDef[]): RoutesManifest;
52
52
  export type RoutesProps = {
53
53
  routes: RoutesManifest;
54
- onRouteChange?: (url: string) => void;
55
54
  };
56
55
  export declare const Routes: ComponentType<RoutesProps>;
57
56
  export {};
@@ -1,6 +1,7 @@
1
1
  import { h } from 'preact';
2
2
  import { lazy, Route, Router, useLocation } from 'preact-iso';
3
3
  import { RouteLocationsProvider } from './internal/route-locations.js';
4
+ import { __noteLoadEnd, __noteLoadStart } from './internal/route-change.js';
4
5
  function wrapWithRouteLocations(serverMod, location, node) {
5
6
  const moduleKey = serverMod
6
7
  ?.__moduleKey;
@@ -158,7 +159,10 @@ function makeLayoutGroupComponent(layoutImport, server, layoutPathPattern, child
158
159
  const inner = buildInnerRoutes(children, viewCache);
159
160
  const Wrapper = (location) => {
160
161
  const layoutLocation = deriveLayoutLocation(location, layoutPathPattern);
161
- const layoutNode = h(Layout, null, h(Router, null, ...inner));
162
+ const layoutNode = h(Layout, null, h(asRouteComponent(Router), {
163
+ onLoadStart: __noteLoadStart,
164
+ onLoadEnd: __noteLoadEnd,
165
+ }, ...inner));
162
166
  return wrapWithRouteLocations(serverMod, layoutLocation, layoutNode);
163
167
  };
164
168
  return { default: Wrapper };
@@ -276,8 +280,11 @@ export function defineRoutes(tree) {
276
280
  serverRoutes: collectServerRoutes(tree),
277
281
  };
278
282
  }
279
- export const Routes = ({ routes, onRouteChange, }) => {
280
- return h(asRouteComponent(Router), onRouteChange ? { onRouteChange } : null, ...routes.flat.map((r) => h(Route, {
283
+ export const Routes = ({ routes }) => {
284
+ return h(asRouteComponent(Router), {
285
+ onLoadStart: __noteLoadStart,
286
+ onLoadEnd: __noteLoadEnd,
287
+ }, ...routes.flat.map((r) => h(Route, {
281
288
  key: r.key,
282
289
  path: r.path,
283
290
  component: asRouteComponent(r.component),
package/dist/iso/form.js CHANGED
@@ -34,6 +34,12 @@ export function Form({ action, children, ...rest }) {
34
34
  ? window.location.pathname + window.location.search
35
35
  : '/';
36
36
  const fd = new FormData(formEl);
37
+ // Source the action identity from props, not the DOM hidden inputs. On an
38
+ // initial SSR page those inputs render empty (server-side defineAction
39
+ // carries no name metadata) and Preact's hydrate() does not patch their
40
+ // values, so reading them back would post __module/__action='' and 404.
41
+ fd.set('__module', moduleKey);
42
+ fd.set('__action', actionName);
37
43
  const payload = collectFormData(fd);
38
44
  let handle;
39
45
  if (optimistic)
@@ -36,3 +36,9 @@ export type { RouteChangeHandler } from './route-change.js';
36
36
  export { Head } from './head.js';
37
37
  export type { HeadProps } from './head.js';
38
38
  export { ClientScript } from './client-script.js';
39
+ export { useViewTransitionLifecycle, type ViewTransitionLifecycle, type ViewTransitionPhaseCallback, } from './view-transition-lifecycle.js';
40
+ export type { ViewTransitionEvent, NavDirection, ViewTransitionReason, } from './internal/view-transition-event.js';
41
+ export { useViewTransitionTypes, subscribeViewTransitionTypes, type ViewTransitionTypesInput, type ViewTransitionTypesNav, } from './view-transition-types.js';
42
+ export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
43
+ export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, type ViewTransitionNameProps, type ViewTransitionGroupProps, } from './view-transition-name.js';
44
+ export { Persist, PersistHost, type PersistProps } from './persist.js';
package/dist/iso/index.js CHANGED
@@ -32,3 +32,12 @@ export { isBrowser, env } from './is-browser.js';
32
32
  export { useRouteChange } from './route-change.js';
33
33
  export { Head } from './head.js';
34
34
  export { ClientScript } from './client-script.js';
35
+ // View transition lifecycle hook.
36
+ export { useViewTransitionLifecycle, } from './view-transition-lifecycle.js';
37
+ // View transitions types.
38
+ export { useViewTransitionTypes, subscribeViewTransitionTypes, } from './view-transition-types.js';
39
+ export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
40
+ // View transition name + group hooks and components.
41
+ export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, } from './view-transition-name.js';
42
+ // Persist components.
43
+ export { Persist, PersistHost } from './persist.js';
@@ -0,0 +1,20 @@
1
+ import type { NavDirection } from './view-transition-event.js';
2
+ export declare function onNavigation(listener: () => void): () => void;
3
+ export declare function installHistoryShim(): void;
4
+ export declare function getNavDirection(): NavDirection;
5
+ /**
6
+ * True once any client-side navigation (push, replace, or popstate) has
7
+ * occurred since the page loaded. Before the first navigation the document is
8
+ * still showing its server-rendered, freshly hydrated route. PageMiddlewareHost
9
+ * uses this to pick its render strategy: on the initial load it renders the
10
+ * server children during hydration and applies the client middleware outcome
11
+ * afterwards (so a Suspense boundary never resolves to non-SSR content
12
+ * mid-hydration, which would orphan the server route DOM); after a navigation
13
+ * it suspends on the chain normally. `lastDirection` only ever moves away from
14
+ * 'initial' (never back), so this is a monotonic "have we navigated yet" flag.
15
+ */
16
+ export declare function hasClientNavigated(): boolean;
17
+ /** Test-only reset. Do not call from production code. */
18
+ export declare function resetHistoryShimForTesting(): void;
19
+ /** Test-only direction setter. Do not call from production code. */
20
+ export declare function setNavDirectionForTesting(dir: NavDirection): void;
@@ -0,0 +1,110 @@
1
+ let installed = false;
2
+ let counter = 0;
3
+ let lastDirection = 'initial';
4
+ // Fires synchronously whenever a navigation occurs (pushState / replaceState /
5
+ // popstate), BEFORE the resulting re-render. Lets the view-transition scheduler
6
+ // observe a navigation at navigation time rather than only at render time (which
7
+ // Preact's render batching can coalesce away). See route-change.ts.
8
+ const navListeners = new Set();
9
+ export function onNavigation(listener) {
10
+ navListeners.add(listener);
11
+ return () => navListeners.delete(listener);
12
+ }
13
+ function notifyNavigation() {
14
+ for (const listener of navListeners)
15
+ listener();
16
+ }
17
+ let originalPush = null;
18
+ let originalReplace = null;
19
+ let popstateListener = null;
20
+ function readCounterFromState() {
21
+ if (typeof history === 'undefined')
22
+ return 0;
23
+ const state = history.state;
24
+ return state?.__hpVtIdx ?? 0;
25
+ }
26
+ export function installHistoryShim() {
27
+ if (installed)
28
+ return;
29
+ if (typeof history === 'undefined' || typeof window === 'undefined')
30
+ return;
31
+ installed = true;
32
+ counter = readCounterFromState();
33
+ lastDirection = 'initial';
34
+ originalPush = history.pushState.bind(history);
35
+ originalReplace = history.replaceState.bind(history);
36
+ history.pushState = function patchedPush(state, title, url) {
37
+ counter += 1;
38
+ const merged = {
39
+ ...(state ?? {}),
40
+ __hpVtIdx: counter,
41
+ };
42
+ originalPush(merged, title, url);
43
+ lastDirection = 'push';
44
+ notifyNavigation();
45
+ };
46
+ history.replaceState = function patchedReplace(state, title, url) {
47
+ const merged = {
48
+ ...(state ?? {}),
49
+ __hpVtIdx: counter,
50
+ };
51
+ originalReplace(merged, title, url);
52
+ lastDirection = 'replace';
53
+ notifyNavigation();
54
+ };
55
+ popstateListener = (e) => {
56
+ const incoming = e.state?.__hpVtIdx ?? 0;
57
+ lastDirection =
58
+ incoming < counter ? 'back' : incoming > counter ? 'forward' : 'replace';
59
+ counter = incoming;
60
+ notifyNavigation();
61
+ };
62
+ window.addEventListener('popstate', popstateListener, { capture: true });
63
+ // Stamp the current entry so subsequent diffs are well-defined.
64
+ if (history.state?.__hpVtIdx === undefined) {
65
+ originalReplace({ ...(history.state ?? {}), __hpVtIdx: counter }, '');
66
+ }
67
+ }
68
+ export function getNavDirection() {
69
+ return lastDirection;
70
+ }
71
+ /**
72
+ * True once any client-side navigation (push, replace, or popstate) has
73
+ * occurred since the page loaded. Before the first navigation the document is
74
+ * still showing its server-rendered, freshly hydrated route. PageMiddlewareHost
75
+ * uses this to pick its render strategy: on the initial load it renders the
76
+ * server children during hydration and applies the client middleware outcome
77
+ * afterwards (so a Suspense boundary never resolves to non-SSR content
78
+ * mid-hydration, which would orphan the server route DOM); after a navigation
79
+ * it suspends on the chain normally. `lastDirection` only ever moves away from
80
+ * 'initial' (never back), so this is a monotonic "have we navigated yet" flag.
81
+ */
82
+ export function hasClientNavigated() {
83
+ return lastDirection !== 'initial';
84
+ }
85
+ /** Test-only reset. Do not call from production code. */
86
+ export function resetHistoryShimForTesting() {
87
+ if (installed &&
88
+ typeof history !== 'undefined' &&
89
+ originalPush &&
90
+ originalReplace) {
91
+ history.pushState = originalPush;
92
+ history.replaceState = originalReplace;
93
+ }
94
+ if (typeof window !== 'undefined' && popstateListener) {
95
+ window.removeEventListener('popstate', popstateListener, {
96
+ capture: true,
97
+ });
98
+ }
99
+ installed = false;
100
+ counter = 0;
101
+ lastDirection = 'initial';
102
+ originalPush = null;
103
+ originalReplace = null;
104
+ popstateListener = null;
105
+ navListeners.clear();
106
+ }
107
+ /** Test-only direction setter. Do not call from production code. */
108
+ export function setNavDirectionForTesting(dir) {
109
+ lastDirection = dir;
110
+ }
@@ -0,0 +1,4 @@
1
+ import type { Ref } from 'preact';
2
+ type AnyRef<T> = Ref<T> | null | undefined;
3
+ export declare function mergeRefs<T>(...refs: AnyRef<T>[]): (node: T | null) => void;
4
+ export {};
@@ -0,0 +1,14 @@
1
+ export function mergeRefs(...refs) {
2
+ return (node) => {
3
+ for (const ref of refs) {
4
+ if (ref == null)
5
+ continue;
6
+ if (typeof ref === 'function') {
7
+ ref(node);
8
+ }
9
+ else {
10
+ ref.current = node;
11
+ }
12
+ }
13
+ };
14
+ }
@@ -1,12 +1,13 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { useLocation } from 'preact-iso';
3
3
  import { Suspense } from 'preact/compat';
4
- import { useContext, useEffect, useRef } from 'preact/hooks';
4
+ import { useContext, useEffect, useRef, useState } from 'preact/hooks';
5
5
  import { isBrowser } from '../is-browser.js';
6
6
  import { isRedirect, isRender } from '../outcomes.js';
7
7
  import { dispatchServer, dispatchClient } from './middleware-runner.js';
8
8
  import { partitionUse } from './use-partitioner.js';
9
9
  import wrapPromise from './wrap-promise.js';
10
+ import { hasClientNavigated } from './history-shim.js';
10
11
  import { HonoRequestContext } from './contexts.js';
11
12
  function startChain(use, location, honoCtx) {
12
13
  const { middleware } = partitionUse(use);
@@ -44,68 +45,147 @@ function startChain(use, location, honoCtx) {
44
45
  inner: async () => undefined,
45
46
  }).then((r) => r.kind === 'outcome' ? { outcome: r.outcome } : { outcome: undefined });
46
47
  }
47
- function HostConsumer({ resultRef, children, }) {
48
- // resultRef.current is populated by the parent before this consumer
49
- // renders; the null branch is just a type-narrow guard.
50
- const wrapped = resultRef.current;
51
- const { outcome } = wrapped ? wrapped.read() : { outcome: undefined };
52
- const { route } = useLocation();
53
- // Client-side redirect: navigate in an effect rather than during render.
54
- // Render-time side effects are forbidden by Suspense semantics; doing
55
- // route() in render would also fire on every Preact re-entry during
56
- // suspension resume. Keyed on the resolved target so a fresh outcome
57
- // for the same path doesn't refire (the outcome is cached per chain,
58
- // so this only changes when the path itself changes and a new chain
59
- // produces a redirect to a different target).
60
- const redirectTo = isRedirect(outcome) && isBrowser() ? outcome.to : null;
61
- useEffect(() => {
62
- if (redirectTo !== null)
63
- route(redirectTo);
64
- // `route` is intentionally omitted from deps: it comes from
65
- // useLocation() which is stable per LocationProvider mount, and
66
- // referencing it here would re-fire the effect on every render the
67
- // provider produces.
68
- }, [redirectTo]);
48
+ /**
49
+ * Applies a settled host outcome to the rendered tree. Shared rendering logic
50
+ * for both host strategies (Suspense and Deferred).
51
+ */
52
+ function renderOutcome(outcome, children) {
69
53
  if (outcome === undefined) {
70
54
  return _jsx(_Fragment, { children: children });
71
55
  }
72
56
  if (isRedirect(outcome)) {
73
57
  if (isBrowser()) {
74
- // Effect above will schedule the navigation; render nothing in the
75
- // meantime so the old tree doesn't briefly flash.
58
+ // The navigation is scheduled in an effect; render nothing meanwhile so
59
+ // the old tree doesn't briefly flash.
76
60
  return null;
77
61
  }
78
- // Server: rethrow so renderPage's outer handler can translate to HTTP redirect.
62
+ // Server: rethrow so renderPage's outer handler can translate to an HTTP redirect.
79
63
  throw outcome;
80
64
  }
81
65
  if (isRender(outcome)) {
82
66
  const Alt = outcome.Component;
83
- // Equality-by-reference semantics: each `render(Component)` call
84
- // returns a fresh outcome object, but the wrapped chain caches its
85
- // result for the lifetime of a path. Within the same path render
86
- // outcomes are stable. Across paths, the `resultRef` is rewrapped
87
- // (see PageMiddlewareHost below), so a fresh chain produces a fresh
88
- // outcome and Preact remounts naturally when `Alt` differs. If a
89
- // middleware returns the SAME component reference across paths,
90
- // Preact treats it as the same element and preserves state. That's
91
- // the documented semantic; if callers need a forced remount, they
92
- // can wrap the returned component or vary props.
67
+ // Equality-by-reference semantics: each `render(Component)` call returns a
68
+ // fresh outcome object, but the wrapped chain caches its result for the
69
+ // lifetime of a path. Within the same path render outcomes are stable.
70
+ // Across paths the chain is re-dispatched, so a fresh chain produces a
71
+ // fresh outcome and Preact remounts naturally when `Alt` differs. If a
72
+ // middleware returns the SAME component reference across paths, Preact
73
+ // treats it as the same element and preserves state. That's the documented
74
+ // semantic; callers needing a forced remount can wrap the component or vary
75
+ // props.
93
76
  return _jsx(Alt, {});
94
77
  }
95
78
  // Deny on the page-render path: rethrow so the outer error boundary or
96
79
  // handler can translate to the right response.
97
80
  throw outcome;
98
81
  }
99
- export const PageMiddlewareHost = ({ use = [], location, fallback, children }) => {
100
- const honoCtx = useContext(HonoRequestContext).context;
82
+ /**
83
+ * Suspense strategy: suspend on the middleware chain and render its outcome.
84
+ * Used for SSR (prerender awaits the suspension) and for post-navigation client
85
+ * renders (no hydration to mismatch, a fallback is fine while the chain runs).
86
+ */
87
+ function HostConsumer({ resultRef, children, }) {
88
+ // resultRef.current is populated by the parent before this consumer
89
+ // renders; the null branch is just a type-narrow guard.
90
+ const wrapped = resultRef.current;
91
+ const { outcome } = wrapped ? wrapped.read() : { outcome: undefined };
92
+ const { route } = useLocation();
93
+ // Client-side redirect: navigate in an effect rather than during render
94
+ // (render-time side effects are forbidden by Suspense semantics, and route()
95
+ // in render would also re-fire on every Preact re-entry during suspension
96
+ // resume). Keyed on the resolved target so a fresh outcome for the same path
97
+ // doesn't refire.
98
+ const redirectTo = isRedirect(outcome) && isBrowser() ? outcome.to : null;
99
+ useEffect(() => {
100
+ if (redirectTo === null)
101
+ return;
102
+ // A plain SPA route() is correct here. This consumer only runs on the
103
+ // server (where the redirect outcome is thrown above, not routed) and for
104
+ // post-navigation client renders; initial-load redirects are handled by
105
+ // DeferredHost, which renders the SSR content during hydration so there is
106
+ // no server-committed tree for the Router to orphan. (The
107
+ // orphan-on-hydration-mismatch is expected Preact behavior, see
108
+ // preactjs/preact#4442.)
109
+ route(redirectTo);
110
+ // `route` is intentionally omitted from deps: it comes from useLocation()
111
+ // which is stable per LocationProvider mount, and referencing it here would
112
+ // re-fire the effect on every render the provider produces.
113
+ }, [redirectTo]);
114
+ return renderOutcome(outcome, children);
115
+ }
116
+ /**
117
+ * Deferred strategy: used on the INITIAL document load (browser, before any
118
+ * client navigation). Renders the server-rendered children during hydration so
119
+ * the hydrated DOM matches SSR, then runs the client chain post-hydration and
120
+ * applies its outcome as a normal update. This avoids a Suspense boundary
121
+ * resolving to non-SSR content mid-hydration, which orphans the server route
122
+ * DOM (expected Preact behavior, preactjs/preact#4442) and stacks the redirect
123
+ * target on top (the "client redirect double-mount").
124
+ *
125
+ * Timing contract: on the initial load the client middleware runs AFTER
126
+ * hydration (in the effect), not before first paint. The server already
127
+ * authorized and rendered this content, so a client guard is advisory here and
128
+ * applies once hydrated; anything it would redirect away from is already on
129
+ * screen from SSR, so deferring exposes nothing new. Post-navigation renders
130
+ * take SuspenseHost, where the chain blocks the render as before.
131
+ *
132
+ * Known limitation: this matches the COMMON case where the server passed and
133
+ * rendered `children`. If a SERVER middleware instead rendered an alternative
134
+ * (so SSR markup != children) and the client produces no matching outcome, the
135
+ * client still renders `children` and the SSR/client mismatch is on the user -
136
+ * the framework does not transmit the server outcome to the client. This is
137
+ * pre-existing (it predates the deferred strategy) and out of scope here.
138
+ */
139
+ function DeferredHost({ use, location, honoCtx, children, }) {
140
+ const { route } = useLocation();
141
+ // null = chain not yet settled, or settled to a redirect/pass: keep rendering
142
+ // the server children. A settled render()/deny outcome is stored so it can be
143
+ // swapped in via a normal post-hydration update.
144
+ const [applied, setApplied] = useState(null);
145
+ useEffect(() => {
146
+ let cancelled = false;
147
+ // Reset to the server children before re-dispatching, so a stale render()
148
+ // outcome from a previous path cannot linger if this host is ever reused
149
+ // across a path change (in practice it only handles the initial load, but
150
+ // a persisted layout host could in principle see location.path change).
151
+ setApplied(null);
152
+ startChain(use, location, honoCtx).then((result) => {
153
+ if (cancelled)
154
+ return;
155
+ if (isRedirect(result.outcome)) {
156
+ // SPA navigate from the fully hydrated tree. Because the server's
157
+ // content (not null) was rendered during hydration, there is no
158
+ // orphaned route DOM for the Router to stack the target on top of.
159
+ route(result.outcome.to);
160
+ }
161
+ else if (result.outcome !== undefined) {
162
+ // render() / deny: surface after hydration via a normal update.
163
+ setApplied(result);
164
+ }
165
+ // undefined (chain passed): keep the already-rendered children.
166
+ });
167
+ return () => {
168
+ cancelled = true;
169
+ };
170
+ // Re-dispatch if the path changes (mirrors SuspenseHost's per-path dispatch).
171
+ }, [location.path]);
172
+ if (applied === null)
173
+ return _jsx(_Fragment, { children: children });
174
+ return renderOutcome(applied.outcome, children);
175
+ }
176
+ /**
177
+ * Suspense strategy wrapper. Lazily dispatches the chain once per path (see the
178
+ * lazy-ref note below) and renders the outcome through HostConsumer.
179
+ */
180
+ function SuspenseHost({ use, location, honoCtx, fallback, children, }) {
101
181
  // Lazy ref pattern. `useRef(null)` serves a dual purpose: it's the
102
- // "not-yet-computed" sentinel AND the persistent slot for the wrapped
103
- // chain result. We compute on first render and on subsequent renders
104
- // ONLY when the path changed. `useRef(wrapPromise(startChain(...)))`
105
- // would evaluate `startChain` every render before useRef decided whether
106
- // to keep it, which synchronously fires `dispatchServer`/`dispatchClient`
107
- // every render. That's O(renders) middleware invocations instead of
108
- // O(navigations); auth checks, analytics, redirects would all repeat.
182
+ // "not-yet-computed" sentinel AND the persistent slot for the wrapped chain
183
+ // result. We compute on first render and on subsequent renders ONLY when the
184
+ // path changed. `useRef(wrapPromise(startChain(...)))` would evaluate
185
+ // `startChain` every render before useRef decided whether to keep it, which
186
+ // synchronously fires `dispatchServer`/`dispatchClient` every render. That's
187
+ // O(renders) middleware invocations instead of O(navigations); auth checks,
188
+ // analytics, redirects would all repeat.
109
189
  const resultRef = useRef(null);
110
190
  const prevPath = useRef(location.path);
111
191
  if (resultRef.current === null) {
@@ -116,4 +196,27 @@ export const PageMiddlewareHost = ({ use = [], location, fallback, children }) =
116
196
  resultRef.current = wrapPromise(startChain(use, location, honoCtx));
117
197
  }
118
198
  return (_jsx(Suspense, { fallback: fallback, children: _jsx(HostConsumer, { resultRef: resultRef, children: children }) }));
199
+ }
200
+ export const PageMiddlewareHost = ({ use = [], location, fallback, children }) => {
201
+ const honoCtx = useContext(HonoRequestContext).context;
202
+ // Choose the render strategy ONCE per mount so the hook order stays stable
203
+ // across renders (hasClientNavigated() flips to true after the first
204
+ // navigation, which would otherwise change which child - and its hooks - we
205
+ // render).
206
+ //
207
+ // Initial document load (browser, no navigation yet) -> DeferredHost: a
208
+ // Suspense boundary that suspends during hydration and resolves to non-SSR
209
+ // content (e.g. null for a redirect) orphans the server-rendered route DOM
210
+ // (expected Preact behavior, preactjs/preact#4442), which the Router then
211
+ // stacks the redirect target on top of. Rendering the server children during
212
+ // hydration and applying the client outcome afterwards removes that mismatch.
213
+ //
214
+ // Server render and post-navigation client renders have no hydration to
215
+ // mismatch, so the Suspense path (suspend on the chain, render the outcome)
216
+ // is correct there.
217
+ const deferRef = useRef(isBrowser() && !hasClientNavigated());
218
+ if (deferRef.current) {
219
+ return (_jsx(DeferredHost, { use: use, location: location, honoCtx: honoCtx, children: children }));
220
+ }
221
+ return (_jsx(SuspenseHost, { use: use, location: location, honoCtx: honoCtx, fallback: fallback, children: children }));
119
222
  };
@@ -0,0 +1,10 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ export interface PersistEntry {
3
+ children: ComponentChildren;
4
+ viewTransitionName: string | undefined;
5
+ }
6
+ export declare function __persistRegistryWrite(id: string, entry: PersistEntry): void;
7
+ export declare function __persistRegistryRead(): ReadonlyMap<string, PersistEntry>;
8
+ export declare function __persistRegistrySubscribe(sub: () => void): () => void;
9
+ /** Test-only reset. Do not call from production code. */
10
+ export declare function __persistRegistryResetForTesting(): void;
@@ -0,0 +1,24 @@
1
+ let map = new Map();
2
+ const subs = new Set();
3
+ export function __persistRegistryWrite(id, entry) {
4
+ // Replace the map reference so consumers using identity-checks can detect.
5
+ const next = new Map(map);
6
+ next.set(id, entry);
7
+ map = next;
8
+ for (const sub of subs)
9
+ sub();
10
+ }
11
+ export function __persistRegistryRead() {
12
+ return map;
13
+ }
14
+ export function __persistRegistrySubscribe(sub) {
15
+ subs.add(sub);
16
+ return () => {
17
+ subs.delete(sub);
18
+ };
19
+ }
20
+ /** Test-only reset. Do not call from production code. */
21
+ export function __persistRegistryResetForTesting() {
22
+ map = new Map();
23
+ subs.clear();
24
+ }
@@ -1,4 +1,27 @@
1
- type Sub = (to: string, from: string | undefined) => void;
1
+ import { ViewTransitionEvent } from './view-transition-event.js';
2
+ export type PhaseName = 'beforeTransition' | 'beforeSwap' | 'afterSwap' | 'afterTransition';
3
+ type PhaseSub = (event: ViewTransitionEvent) => void | Promise<void>;
4
+ type LegacySub = (to: string, from: string | undefined) => void;
5
+ export declare function __subscribePhase(phase: PhaseName, sub: PhaseSub): () => void;
6
+ export declare function __subscribeRouteChange(sub: LegacySub): () => void;
7
+ export declare function __noteLoadStart(): void;
8
+ export declare function __noteLoadEnd(): void;
9
+ /** @internal Test-only reset for coordinator state. */
10
+ export declare function __resetTransitionStateForTesting(): void;
11
+ /**
12
+ * @internal Install the view-transition render scheduler (client only).
13
+ *
14
+ * Takes ownership of `options.debounceRendering`: it captures the previous value
15
+ * as `prevDebounce` (delegated to for every non-navigation flush) and installs
16
+ * `scheduleRender` in its place. This assumes nothing else permanently overrides
17
+ * `options.debounceRendering` afterward. `preact/compat`'s `flushSync` swaps it
18
+ * temporarily and restores it, so that composes fine; a second permanent
19
+ * override would shadow this scheduler (the install is idempotent, so calling it
20
+ * twice is a no-op, not a double-install). Reversed by
21
+ * `__resetTransitionStateForTesting`.
22
+ */
23
+ export declare function installNavTransitionScheduler(): void;
2
24
  export declare function __dispatchRouteChange(to: string, from: string | undefined): void;
3
- export declare function __subscribeRouteChange(sub: Sub): () => void;
25
+ /** @internal Test-only reset for default-types installer. */
26
+ export declare function resetDefaultTypesForTesting(): void;
4
27
  export {};