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.
Files changed (79) hide show
  1. package/dist/iso/action-result-context.d.ts +22 -0
  2. package/dist/iso/action-result-context.js +2 -0
  3. package/dist/iso/action.d.ts +52 -13
  4. package/dist/iso/action.js +204 -88
  5. package/dist/iso/cache.d.ts +9 -0
  6. package/dist/iso/cache.js +26 -0
  7. package/dist/iso/define-app.d.ts +7 -0
  8. package/dist/iso/define-loader.d.ts +12 -0
  9. package/dist/iso/define-loader.js +26 -16
  10. package/dist/iso/form.d.ts +13 -4
  11. package/dist/iso/form.js +115 -33
  12. package/dist/iso/index.d.ts +13 -4
  13. package/dist/iso/index.js +14 -2
  14. package/dist/iso/internal/action-envelope.d.ts +37 -0
  15. package/dist/iso/internal/action-envelope.js +47 -0
  16. package/dist/iso/internal/action-result-store.d.ts +28 -0
  17. package/dist/iso/internal/action-result-store.js +35 -0
  18. package/dist/iso/internal/envelope.js +1 -2
  19. package/dist/iso/internal/form-submit-store.d.ts +9 -0
  20. package/dist/iso/internal/form-submit-store.js +32 -0
  21. package/dist/iso/internal/history-shim.d.ts +7 -0
  22. package/dist/iso/internal/history-shim.js +79 -0
  23. package/dist/iso/internal/loader-fetch.js +65 -34
  24. package/dist/iso/internal/loader.d.ts +3 -3
  25. package/dist/iso/internal/merge-refs.d.ts +4 -0
  26. package/dist/iso/internal/merge-refs.js +14 -0
  27. package/dist/iso/internal/persist-registry.d.ts +10 -0
  28. package/dist/iso/internal/persist-registry.js +24 -0
  29. package/dist/iso/internal/route-boundary.d.ts +4 -4
  30. package/dist/iso/internal/route-change.d.ts +8 -2
  31. package/dist/iso/internal/route-change.js +107 -12
  32. package/dist/iso/internal/safe-redirect.d.ts +7 -0
  33. package/dist/iso/internal/safe-redirect.js +27 -0
  34. package/dist/iso/internal/sse-decoder.d.ts +1 -1
  35. package/dist/iso/internal/sse-decoder.js +40 -26
  36. package/dist/iso/internal/use-render.d.ts +11 -0
  37. package/dist/iso/internal/use-render.js +47 -0
  38. package/dist/iso/internal/view-transition-event.d.ts +23 -0
  39. package/dist/iso/internal/view-transition-event.js +25 -0
  40. package/dist/iso/internal.d.ts +12 -1
  41. package/dist/iso/internal.js +13 -1
  42. package/dist/iso/optimistic-action.d.ts +10 -1
  43. package/dist/iso/optimistic-action.js +11 -3
  44. package/dist/iso/optimistic.d.ts +10 -1
  45. package/dist/iso/optimistic.js +45 -5
  46. package/dist/iso/outcomes.d.ts +14 -2
  47. package/dist/iso/outcomes.js +14 -3
  48. package/dist/iso/persist.d.ts +14 -0
  49. package/dist/iso/persist.js +56 -0
  50. package/dist/iso/use-action-result.d.ts +25 -0
  51. package/dist/iso/use-action-result.js +39 -0
  52. package/dist/iso/use-form-status.d.ts +5 -0
  53. package/dist/iso/use-form-status.js +13 -0
  54. package/dist/iso/view-transition-lifecycle.d.ts +9 -0
  55. package/dist/iso/view-transition-lifecycle.js +18 -0
  56. package/dist/iso/view-transition-name.d.ts +17 -0
  57. package/dist/iso/view-transition-name.js +79 -0
  58. package/dist/iso/view-transition-types.d.ts +8 -0
  59. package/dist/iso/view-transition-types.js +21 -0
  60. package/dist/server/actions-handler.d.ts +7 -0
  61. package/dist/server/actions-handler.js +42 -9
  62. package/dist/server/index.d.ts +2 -1
  63. package/dist/server/index.js +2 -1
  64. package/dist/server/loaders-handler.d.ts +8 -0
  65. package/dist/server/loaders-handler.js +37 -4
  66. package/dist/server/page-action-handler.d.ts +63 -0
  67. package/dist/server/page-action-handler.js +274 -0
  68. package/dist/server/page-action-resolvers.d.ts +28 -0
  69. package/dist/server/page-action-resolvers.js +147 -0
  70. package/dist/server/render.js +136 -55
  71. package/dist/server/route-server-modules.d.ts +7 -8
  72. package/dist/server/route-server-modules.js +7 -8
  73. package/dist/server/speculation-rules.d.ts +3 -0
  74. package/dist/server/speculation-rules.js +8 -0
  75. package/dist/server/sse.d.ts +43 -28
  76. package/dist/server/sse.js +113 -88
  77. package/dist/vite/client-entry.js +12 -3
  78. package/dist/vite/server-entry.js +10 -2
  79. package/package.json +2 -2
@@ -42,7 +42,15 @@ function ViewRenderer({ loaderRef, props, render, }) {
42
42
  const reload = reloadCtx?.reload ?? (() => { });
43
43
  return render({ data, error, reload, ...props });
44
44
  }
45
+ function validateTimeoutMs(value, context) {
46
+ if (value === undefined || value === false)
47
+ return;
48
+ if (!Number.isFinite(value) || value < 0) {
49
+ throw new RangeError(`${context}: timeoutMs must be a non-negative finite number or false, got ${String(value)}`);
50
+ }
51
+ }
45
52
  export function defineLoader(fn, opts) {
53
+ validateTimeoutMs(opts?.timeoutMs, 'defineLoader');
46
54
  const idKey = opts?.__moduleKey
47
55
  ? opts.__loaderName
48
56
  ? `${opts.__moduleKey}::${opts.__loaderName}`
@@ -80,6 +88,7 @@ export function defineLoader(fn, opts) {
80
88
  fn,
81
89
  cache: cache,
82
90
  params: opts?.params ?? [],
91
+ timeoutMs: opts?.timeoutMs,
83
92
  // LoaderUse<T, boolean> structurally collapses to the same shape the
84
93
  // partitioner accepts; the cast hides only the generic narrowing on
85
94
  // StreamObserver's TChunk/TResult which is invariant. Identity-preserving.
@@ -97,26 +106,27 @@ export function defineLoader(fn, opts) {
97
106
  invalidate() {
98
107
  cache.invalidate();
99
108
  },
100
- Boundary: null,
101
- View: null,
102
- };
103
- const Boundary = ({ fallback, errorFallback, children, }) => {
104
- return h(LoaderHost, {
109
+ // `Boundary` and `View` close over `ref`. The captures are by reference
110
+ // and only deref at call time (component render), so the cycle is safe;
111
+ // both are fully initialized before any consumer can invoke them.
112
+ Boundary: ({ fallback, errorFallback, children }) => h((LoaderHost), {
105
113
  loader: ref,
106
114
  fallback,
107
115
  errorFallback,
108
116
  children,
109
- });
110
- };
111
- ref.Boundary = Boundary;
112
- const View = (render, viewOpts) => {
113
- const Wrapped = (props) => h(ref.Boundary, {
114
- fallback: viewOpts?.fallback,
115
- errorFallback: viewOpts?.errorFallback,
116
- children: h((ViewRenderer), { loaderRef: ref, props, render }),
117
- });
118
- return Wrapped;
117
+ }),
118
+ View: (render, viewOpts) => {
119
+ const Wrapped = (props) => h(ref.Boundary, {
120
+ fallback: viewOpts?.fallback,
121
+ errorFallback: viewOpts?.errorFallback,
122
+ children: h((ViewRenderer), {
123
+ loaderRef: ref,
124
+ props,
125
+ render,
126
+ }),
127
+ });
128
+ return Wrapped;
129
+ },
119
130
  };
120
- ref.View = View;
121
131
  return ref;
122
132
  }
@@ -1,7 +1,16 @@
1
1
  import type { JSX, ComponentChildren } from 'preact';
2
- export type FormProps<TPayload extends Record<string, unknown>> = Omit<JSX.HTMLAttributes<HTMLFormElement>, 'onSubmit'> & {
3
- mutate: (payload: TPayload) => Promise<unknown> | unknown;
4
- pending?: boolean;
2
+ import type { ActionStub } from './action.js';
3
+ import { type UseOptimisticActionResult } from './optimistic-action.js';
4
+ /**
5
+ * The `action` prop accepts either a plain action stub or the branded value
6
+ * returned by `useOptimisticAction`. The union lets `<Form>` discover the
7
+ * optimistic apply via `OPTIMISTIC_BRAND in action` narrowing without
8
+ * casting away the type.
9
+ */
10
+ type FormActionInput<TPayload, TResult> = ActionStub<TPayload, TResult, never> | UseOptimisticActionResult<TPayload, TResult, unknown>;
11
+ export type FormProps<TPayload, TResult> = Omit<JSX.HTMLAttributes<HTMLFormElement>, 'action' | 'method' | 'onSubmit' | 'enctype'> & {
12
+ action: FormActionInput<TPayload, TResult>;
5
13
  children?: ComponentChildren;
6
14
  };
7
- export declare function Form<TPayload extends Record<string, unknown>>({ mutate, pending, children, ...rest }: FormProps<TPayload>): JSX.Element;
15
+ export declare function Form<TPayload, TResult>({ action, children, ...rest }: FormProps<TPayload, TResult>): JSX.Element;
16
+ export {};
package/dist/iso/form.js CHANGED
@@ -1,40 +1,122 @@
1
- import { jsx as _jsx } from "preact/jsx-runtime";
2
- /**
3
- * Collect a FormData into a plain payload object.
4
- *
5
- * Repeated field names (checkboxes sharing a name, multi-select, multiple
6
- * `<input type="file" multiple>` entries) collect into an array. The old
7
- * `Object.fromEntries(fd)` produced the LAST value only, which was silent
8
- * data loss: a four-checkbox group would submit one value with no warning.
9
- *
10
- * Single-value fields stay scalar. Files survive as `File` instances.
11
- *
12
- * Consumers should type their `defineAction<TPayload, ...>` to match:
13
- * `tags: string[]`, `photos: File[]`, etc. for fields that may have multiple
14
- * values; scalar types for fields that won't.
15
- */
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useState, useCallback, useMemo } from 'preact/hooks';
3
+ import { OPTIMISTIC_BRAND, } from './optimistic-action.js';
4
+ import { beginSubmit, endSubmit } from './internal/form-submit-store.js';
5
+ import { setLastActionResult } from './internal/action-result-store.js';
6
+ import { assignSafeRedirect } from './internal/safe-redirect.js';
7
+ function hasOptimisticBrand(action) {
8
+ return OPTIMISTIC_BRAND in action;
9
+ }
16
10
  function collectFormData(fd) {
17
- const payload = {};
11
+ const out = {};
18
12
  for (const [key, value] of fd.entries()) {
19
- if (key in payload) {
20
- const existing = payload[key];
21
- payload[key] = Array.isArray(existing)
22
- ? [...existing, value]
23
- : [existing, value];
24
- }
25
- else {
26
- payload[key] = value;
27
- }
13
+ if (key === '__module' || key === '__action')
14
+ continue;
15
+ const existing = out[key];
16
+ out[key] =
17
+ existing === undefined
18
+ ? value
19
+ : Array.isArray(existing)
20
+ ? [...existing, value]
21
+ : [existing, value];
28
22
  }
29
- return payload;
23
+ return out;
30
24
  }
31
- export function Form({ mutate, pending, children, ...rest }) {
32
- const handleSubmit = (e) => {
25
+ export function Form({ action, children, ...rest }) {
26
+ const [pending, setPending] = useState(false);
27
+ const moduleKey = action.__module;
28
+ const actionName = action.__action;
29
+ const optimistic = useMemo(() => (hasOptimisticBrand(action) ? action[OPTIMISTIC_BRAND] : undefined), [action]);
30
+ const handleSubmit = useCallback(async (e) => {
33
31
  e.preventDefault();
34
32
  const formEl = e.currentTarget;
35
- const formData = new FormData(formEl);
36
- const payload = collectFormData(formData);
37
- void mutate(payload);
38
- };
39
- return (_jsx("form", { ...rest, onSubmit: handleSubmit, children: _jsx("fieldset", { disabled: pending, class: "hp-form-fieldset", children: children }) }));
33
+ const target = typeof window !== 'undefined'
34
+ ? window.location.pathname + window.location.search
35
+ : '/';
36
+ const fd = new FormData(formEl);
37
+ const payload = collectFormData(fd);
38
+ let handle;
39
+ if (optimistic)
40
+ handle = optimistic.addOptimistic(payload);
41
+ setPending(true);
42
+ beginSubmit(moduleKey, actionName);
43
+ try {
44
+ const res = await fetch(target, {
45
+ method: 'POST',
46
+ body: fd,
47
+ headers: { Accept: 'application/json' },
48
+ });
49
+ const env = (await res.json().catch(() => null));
50
+ if (!env) {
51
+ handle?.revert();
52
+ if (typeof window !== 'undefined')
53
+ window.location.reload();
54
+ return;
55
+ }
56
+ if (env.__outcome === 'redirect' && typeof env.to === 'string') {
57
+ const navigated = assignSafeRedirect(env.to);
58
+ if (navigated) {
59
+ handle?.settle();
60
+ return;
61
+ }
62
+ // Cross-origin: revert optimistic, surface as error result so useActionResult sees it.
63
+ handle?.revert();
64
+ setLastActionResult(moduleKey, actionName, {
65
+ kind: 'error',
66
+ message: `Refused cross-origin redirect to ${env.to}`,
67
+ submittedPayload: payload,
68
+ });
69
+ return;
70
+ }
71
+ if (env.__outcome === 'success') {
72
+ handle?.settle();
73
+ setLastActionResult(moduleKey, actionName, {
74
+ kind: 'success',
75
+ data: env.data,
76
+ submittedPayload: payload,
77
+ });
78
+ return;
79
+ }
80
+ if (env.__outcome === 'deny') {
81
+ handle?.revert();
82
+ setLastActionResult(moduleKey, actionName, {
83
+ kind: 'deny',
84
+ status: env.status ?? res.status,
85
+ message: env.message ?? `Request denied (${env.status ?? res.status})`,
86
+ data: env.data,
87
+ submittedPayload: payload,
88
+ });
89
+ return;
90
+ }
91
+ if (env.__outcome === 'error') {
92
+ handle?.revert();
93
+ setLastActionResult(moduleKey, actionName, {
94
+ kind: 'error',
95
+ message: env.message ?? 'Action failed',
96
+ submittedPayload: payload,
97
+ });
98
+ return;
99
+ }
100
+ // Unknown outcome (e.g. 'timeout'): treat as error.
101
+ handle?.revert();
102
+ setLastActionResult(moduleKey, actionName, {
103
+ kind: 'error',
104
+ message: env.message ?? `Unexpected outcome: ${env.__outcome ?? 'unknown'}`,
105
+ submittedPayload: payload,
106
+ });
107
+ }
108
+ catch (err) {
109
+ handle?.revert();
110
+ setLastActionResult(moduleKey, actionName, {
111
+ kind: 'error',
112
+ message: err instanceof Error ? err.message : String(err),
113
+ submittedPayload: payload,
114
+ });
115
+ }
116
+ finally {
117
+ setPending(false);
118
+ endSubmit(moduleKey, actionName);
119
+ }
120
+ }, [moduleKey, actionName, optimistic]);
121
+ return (_jsxs("form", { ...rest, method: "post", enctype: "multipart/form-data", onSubmit: handleSubmit, children: [_jsx("input", { type: "hidden", name: "__module", value: moduleKey }), _jsx("input", { type: "hidden", name: "__action", value: actionName }), _jsx("fieldset", { disabled: pending, class: "hp-form-fieldset", children: children })] }));
40
122
  }
@@ -7,15 +7,18 @@ export { defineRoutes, Routes } from './define-routes.js';
7
7
  export type { RouteDef, RoutesManifest, FlatRoute, ServerRoute, LayoutProps, ViewProps, } from './define-routes.js';
8
8
  export { defineLoader } from './define-loader.js';
9
9
  export type { LoaderRef, LoaderCtx, Loader as LoaderFn, } from './define-loader.js';
10
- export { defineAction, useAction } from './action.js';
10
+ export { defineAction, useAction, TimeoutError } from './action.js';
11
11
  export type { ActionStub, UseActionOptions, UseActionResult, MutateResult, } from './action.js';
12
12
  export type { ContentfulStatusCode } from 'hono/utils/http-status';
13
13
  export { useReload } from './reload-context.js';
14
14
  export { useOptimistic } from './optimistic.js';
15
- export type { OptimisticHandle } from './optimistic.js';
15
+ export type { OptimisticHandle, UseOptimisticOptions } from './optimistic.js';
16
16
  export { useOptimisticAction } from './optimistic-action.js';
17
17
  export type { UseOptimisticActionOptions, UseOptimisticActionResult, } from './optimistic-action.js';
18
18
  export { Form } from './form.js';
19
+ export { useActionResult, type ActionResult } from './use-action-result.js';
20
+ export { ActionResultContext, type ActionResultContextValue, } from './action-result-context.js';
21
+ export { useFormStatus, type FormStatus } from './use-form-status.js';
19
22
  export { createCache } from './cache.js';
20
23
  export type { LoaderCache } from './cache.js';
21
24
  export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
@@ -24,8 +27,8 @@ export { defineStreamObserver } from './define-stream-observer.js';
24
27
  export type { StreamObserver, ServerStreamCtx, } from './define-stream-observer.js';
25
28
  export { defineApp } from './define-app.js';
26
29
  export type { AppConfig, AppUseElement } from './define-app.js';
27
- export { redirect, deny, isOutcome, isRedirect, isDeny, isRender, } from './outcomes.js';
28
- export type { Outcome, RedirectOutcome, DenyOutcome, RenderOutcome, RedirectStatusCode, ErrorStatusCode, } from './outcomes.js';
30
+ export { redirect, deny, timeoutOutcome, isOutcome, isRedirect, isDeny, isRender, isTimeout, } from './outcomes.js';
31
+ export type { Outcome, RedirectOutcome, DenyOutcome, RenderOutcome, TimeoutOutcome, RedirectStatusCode, ErrorStatusCode, } from './outcomes.js';
29
32
  export { prefetch } from './prefetch.js';
30
33
  export { isBrowser, env } from './is-browser.js';
31
34
  export { useRouteChange } from './route-change.js';
@@ -33,3 +36,9 @@ export type { RouteChangeHandler } from './route-change.js';
33
36
  export { Head } from './head.js';
34
37
  export type { HeadProps } from './head.js';
35
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, 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
@@ -8,20 +8,23 @@ export { Route, Router, lazy, useLocation, useRoute } from 'preact-iso';
8
8
  export { defineRoutes, Routes } from './define-routes.js';
9
9
  // Server bindings.
10
10
  export { defineLoader } from './define-loader.js';
11
- export { defineAction, useAction } from './action.js';
11
+ export { defineAction, useAction, TimeoutError } from './action.js';
12
12
  // Hooks.
13
13
  export { useReload } from './reload-context.js';
14
14
  export { useOptimistic } from './optimistic.js';
15
15
  export { useOptimisticAction } from './optimistic-action.js';
16
16
  // Forms.
17
17
  export { Form } from './form.js';
18
+ export { useActionResult } from './use-action-result.js';
19
+ export { ActionResultContext, } from './action-result-context.js';
20
+ export { useFormStatus } from './use-form-status.js';
18
21
  // Cache + invalidation.
19
22
  export { createCache } from './cache.js';
20
23
  // Middleware + outcomes (the new system).
21
24
  export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
22
25
  export { defineStreamObserver } from './define-stream-observer.js';
23
26
  export { defineApp } from './define-app.js';
24
- export { redirect, deny, isOutcome, isRedirect, isDeny, isRender, } from './outcomes.js';
27
+ export { redirect, deny, timeoutOutcome, isOutcome, isRedirect, isDeny, isRender, isTimeout, } from './outcomes.js';
25
28
  // Utilities.
26
29
  export { prefetch } from './prefetch.js';
27
30
  export { isBrowser, env } from './is-browser.js';
@@ -29,3 +32,12 @@ export { isBrowser, env } from './is-browser.js';
29
32
  export { useRouteChange } from './route-change.js';
30
33
  export { Head } from './head.js';
31
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, } 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,37 @@
1
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
2
+ import type { Outcome } from '../outcomes.js';
3
+ export type ActionEnvelope = {
4
+ __outcome: 'success';
5
+ data: unknown;
6
+ } | {
7
+ __outcome: 'redirect';
8
+ to: string;
9
+ status: number;
10
+ } | {
11
+ __outcome: 'deny';
12
+ status: number;
13
+ message: string;
14
+ data?: unknown;
15
+ } | {
16
+ __outcome: 'error';
17
+ message: string;
18
+ } | {
19
+ __outcome: 'timeout';
20
+ timeoutMs: number;
21
+ };
22
+ export type ActionResolution = {
23
+ kind: 'success';
24
+ data: unknown;
25
+ } | {
26
+ kind: 'outcome';
27
+ outcome: Outcome;
28
+ } | {
29
+ kind: 'error';
30
+ message: string;
31
+ };
32
+ export type SerializedEnvelope = {
33
+ body: ActionEnvelope;
34
+ status: ContentfulStatusCode;
35
+ headers: Record<string, string> | undefined;
36
+ };
37
+ export declare function serializeActionOutcome(resolution: ActionResolution): SerializedEnvelope;
@@ -0,0 +1,47 @@
1
+ export function serializeActionOutcome(resolution) {
2
+ if (resolution.kind === 'success') {
3
+ return {
4
+ body: { __outcome: 'success', data: resolution.data },
5
+ status: 200,
6
+ headers: undefined,
7
+ };
8
+ }
9
+ if (resolution.kind === 'error') {
10
+ return {
11
+ body: { __outcome: 'error', message: resolution.message },
12
+ status: 500,
13
+ headers: undefined,
14
+ };
15
+ }
16
+ const { outcome } = resolution;
17
+ if (outcome.__outcome === 'redirect') {
18
+ return {
19
+ body: { __outcome: 'redirect', to: outcome.to, status: outcome.status },
20
+ status: 200,
21
+ headers: outcome.headers,
22
+ };
23
+ }
24
+ if (outcome.__outcome === 'deny') {
25
+ const body = {
26
+ __outcome: 'deny',
27
+ status: outcome.status,
28
+ message: outcome.message,
29
+ };
30
+ if (outcome.data !== undefined)
31
+ body.data = outcome.data;
32
+ return { body, status: outcome.status, headers: outcome.headers };
33
+ }
34
+ if (outcome.__outcome === 'timeout') {
35
+ return {
36
+ body: { __outcome: 'timeout', timeoutMs: outcome.timeoutMs },
37
+ status: 504,
38
+ headers: undefined,
39
+ };
40
+ }
41
+ // 'render' outcome is page-scope only; should never reach an action.
42
+ return {
43
+ body: { __outcome: 'error', message: 'render outcome is page-scope only' },
44
+ status: 500,
45
+ headers: undefined,
46
+ };
47
+ }
@@ -0,0 +1,28 @@
1
+ type Listener = () => void;
2
+ export type StoredActionResult = {
3
+ kind: 'success';
4
+ data: unknown;
5
+ submittedPayload: unknown;
6
+ } | {
7
+ kind: 'deny';
8
+ status: number;
9
+ message: string;
10
+ data?: unknown;
11
+ submittedPayload: unknown;
12
+ } | {
13
+ kind: 'error';
14
+ message: string;
15
+ submittedPayload: unknown | null;
16
+ };
17
+ type Entry = StoredActionResult & {
18
+ module: string;
19
+ action: string;
20
+ };
21
+ export declare function setLastActionResult(module: string, action: string, result: StoredActionResult): void;
22
+ export declare function clearLastActionResult(module: string, action: string): void;
23
+ export declare function getLastActionResult(stub?: {
24
+ __module: string;
25
+ __action: string;
26
+ }): Entry | null;
27
+ export declare function subscribeActionResults(listener: Listener): () => void;
28
+ export {};
@@ -0,0 +1,35 @@
1
+ const results = new Map();
2
+ const listeners = new Set();
3
+ function key(module, action) {
4
+ return `${module}::${action}`;
5
+ }
6
+ export function setLastActionResult(module, action, result) {
7
+ const k = key(module, action);
8
+ // Delete-then-set to bump to most-recent position in Map iteration order,
9
+ // so no-stub readers see the latest action result, not the earliest.
10
+ results.delete(k);
11
+ results.set(k, { ...result, module, action });
12
+ for (const l of listeners)
13
+ l();
14
+ }
15
+ export function clearLastActionResult(module, action) {
16
+ if (results.delete(key(module, action))) {
17
+ for (const l of listeners)
18
+ l();
19
+ }
20
+ }
21
+ export function getLastActionResult(stub) {
22
+ if (stub)
23
+ return results.get(key(stub.__module, stub.__action)) ?? null;
24
+ // No stub: return the most recently written entry.
25
+ let last = null;
26
+ for (const entry of results.values())
27
+ last = entry;
28
+ return last;
29
+ }
30
+ export function subscribeActionResults(listener) {
31
+ listeners.add(listener);
32
+ return () => {
33
+ listeners.delete(listener);
34
+ };
35
+ }
@@ -13,8 +13,7 @@ export const Envelope = ({ as = 'section', children, }) => {
13
13
  const dataLoader = isBrowser() ? 'null' : JSON.stringify(ctx.data ?? null);
14
14
  if (typeof as === 'string') {
15
15
  const Tag = as;
16
- const props = { id, 'data-loader': dataLoader };
17
- return (_jsx(Tag, { ...props, children: children }));
16
+ return (_jsx(Tag, { id: id, "data-loader": dataLoader, children: children }));
18
17
  }
19
18
  const Wrapper = as;
20
19
  return (_jsx(Wrapper, { id: id, "data-loader": dataLoader, children: children }));
@@ -0,0 +1,9 @@
1
+ type Listener = () => void;
2
+ export declare function beginSubmit(module: string, action: string): void;
3
+ export declare function endSubmit(module: string, action: string): void;
4
+ export declare function isPending(stub?: {
5
+ __module: string;
6
+ __action: string;
7
+ }): boolean;
8
+ export declare function subscribe(listener: Listener): () => void;
9
+ export {};
@@ -0,0 +1,32 @@
1
+ const counts = new Map();
2
+ const listeners = new Set();
3
+ function key(module, action) {
4
+ return `${module}::${action}`;
5
+ }
6
+ export function beginSubmit(module, action) {
7
+ const k = key(module, action);
8
+ counts.set(k, (counts.get(k) ?? 0) + 1);
9
+ for (const l of listeners)
10
+ l();
11
+ }
12
+ export function endSubmit(module, action) {
13
+ const k = key(module, action);
14
+ const n = (counts.get(k) ?? 0) - 1;
15
+ if (n <= 0)
16
+ counts.delete(k);
17
+ else
18
+ counts.set(k, n);
19
+ for (const l of listeners)
20
+ l();
21
+ }
22
+ export function isPending(stub) {
23
+ if (stub)
24
+ return (counts.get(key(stub.__module, stub.__action)) ?? 0) > 0;
25
+ return counts.size > 0;
26
+ }
27
+ export function subscribe(listener) {
28
+ listeners.add(listener);
29
+ return () => {
30
+ listeners.delete(listener);
31
+ };
32
+ }
@@ -0,0 +1,7 @@
1
+ import type { NavDirection } from './view-transition-event.js';
2
+ export declare function installHistoryShim(): void;
3
+ export declare function getNavDirection(): NavDirection;
4
+ /** Test-only reset. Do not call from production code. */
5
+ export declare function resetHistoryShimForTesting(): void;
6
+ /** Test-only direction setter. Do not call from production code. */
7
+ export declare function setNavDirectionForTesting(dir: NavDirection): void;
@@ -0,0 +1,79 @@
1
+ let installed = false;
2
+ let counter = 0;
3
+ let lastDirection = 'initial';
4
+ let originalPush = null;
5
+ let originalReplace = null;
6
+ let popstateListener = null;
7
+ function readCounterFromState() {
8
+ if (typeof history === 'undefined')
9
+ return 0;
10
+ const state = history.state;
11
+ return state?.__hpVtIdx ?? 0;
12
+ }
13
+ export function installHistoryShim() {
14
+ if (installed)
15
+ return;
16
+ if (typeof history === 'undefined' || typeof window === 'undefined')
17
+ return;
18
+ installed = true;
19
+ counter = readCounterFromState();
20
+ lastDirection = 'initial';
21
+ originalPush = history.pushState.bind(history);
22
+ originalReplace = history.replaceState.bind(history);
23
+ history.pushState = function patchedPush(state, title, url) {
24
+ counter += 1;
25
+ const merged = {
26
+ ...(state ?? {}),
27
+ __hpVtIdx: counter,
28
+ };
29
+ originalPush(merged, title, url);
30
+ lastDirection = 'push';
31
+ };
32
+ history.replaceState = function patchedReplace(state, title, url) {
33
+ const merged = {
34
+ ...(state ?? {}),
35
+ __hpVtIdx: counter,
36
+ };
37
+ originalReplace(merged, title, url);
38
+ lastDirection = 'replace';
39
+ };
40
+ popstateListener = (e) => {
41
+ const incoming = e.state?.__hpVtIdx ?? 0;
42
+ lastDirection =
43
+ incoming < counter ? 'back' : incoming > counter ? 'forward' : 'replace';
44
+ counter = incoming;
45
+ };
46
+ window.addEventListener('popstate', popstateListener, { capture: true });
47
+ // Stamp the current entry so subsequent diffs are well-defined.
48
+ if (history.state?.__hpVtIdx === undefined) {
49
+ originalReplace({ ...(history.state ?? {}), __hpVtIdx: counter }, '');
50
+ }
51
+ }
52
+ export function getNavDirection() {
53
+ return lastDirection;
54
+ }
55
+ /** Test-only reset. Do not call from production code. */
56
+ export function resetHistoryShimForTesting() {
57
+ if (installed &&
58
+ typeof history !== 'undefined' &&
59
+ originalPush &&
60
+ originalReplace) {
61
+ history.pushState = originalPush;
62
+ history.replaceState = originalReplace;
63
+ }
64
+ if (typeof window !== 'undefined' && popstateListener) {
65
+ window.removeEventListener('popstate', popstateListener, {
66
+ capture: true,
67
+ });
68
+ }
69
+ installed = false;
70
+ counter = 0;
71
+ lastDirection = 'initial';
72
+ originalPush = null;
73
+ originalReplace = null;
74
+ popstateListener = null;
75
+ }
76
+ /** Test-only direction setter. Do not call from production code. */
77
+ export function setNavDirectionForTesting(dir) {
78
+ lastDirection = dir;
79
+ }