hono-preact 0.1.0 → 0.3.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 +2 -1
- package/dist/adapter-cloudflare.d.ts +1 -0
- package/dist/adapter-cloudflare.d.ts.map +1 -0
- package/dist/adapter-cloudflare.js +2 -0
- package/dist/adapter-node.d.ts +1 -0
- package/dist/adapter-node.d.ts.map +1 -0
- package/dist/adapter-node.js +2 -0
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- 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 +60 -25
- package/dist/iso/action.js +210 -58
- package/dist/iso/cache.d.ts +9 -0
- package/dist/iso/cache.js +26 -0
- package/dist/iso/define-app.d.ts +14 -0
- package/dist/iso/define-app.js +3 -0
- package/dist/iso/define-loader.d.ts +31 -0
- package/dist/iso/define-loader.js +30 -16
- package/dist/iso/define-middleware.d.ts +43 -0
- package/dist/iso/define-middleware.js +6 -0
- package/dist/iso/define-page.d.ts +7 -2
- package/dist/iso/define-page.js +1 -1
- package/dist/iso/define-routes.d.ts +24 -1
- package/dist/iso/define-routes.js +34 -0
- package/dist/iso/define-stream-observer.d.ts +20 -0
- package/dist/iso/define-stream-observer.js +3 -0
- package/dist/iso/form.d.ts +13 -4
- package/dist/iso/form.js +115 -33
- package/dist/iso/index.d.ts +15 -7
- package/dist/iso/index.js +9 -4
- 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/contexts.d.ts +0 -2
- package/dist/iso/internal/contexts.js +0 -1
- 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/loader-fetch.js +102 -41
- package/dist/iso/internal/loader-runner.js +105 -8
- package/dist/iso/internal/loader.d.ts +3 -3
- package/dist/iso/internal/middleware-runner.d.ts +22 -0
- package/dist/iso/internal/middleware-runner.js +79 -0
- package/dist/iso/internal/page-middleware-host.d.ts +13 -0
- package/dist/iso/internal/page-middleware-host.js +119 -0
- package/dist/iso/internal/route-boundary.d.ts +5 -4
- package/dist/iso/internal/route-boundary.js +16 -0
- 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/stream-observer-runner.d.ts +13 -0
- package/dist/iso/internal/stream-observer-runner.js +48 -0
- package/dist/iso/internal/use-partitioner.d.ts +9 -0
- package/dist/iso/internal/use-partitioner.js +11 -0
- package/dist/iso/internal/use-types.d.ts +7 -0
- package/dist/iso/internal/use-types.js +1 -0
- package/dist/iso/internal.d.ts +12 -5
- package/dist/iso/internal.js +16 -7
- 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 +50 -0
- package/dist/iso/outcomes.js +67 -0
- package/dist/iso/page-only.d.ts +5 -0
- package/dist/iso/page-only.js +20 -0
- package/dist/iso/page.d.ts +3 -3
- package/dist/iso/page.js +3 -3
- 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/page.d.ts +1 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +8 -0
- package/dist/server/actions-handler.d.ts +27 -6
- package/dist/server/actions-handler.js +121 -52
- package/dist/server/context.js +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/server/loaders-handler.d.ts +24 -0
- package/dist/server/loaders-handler.js +128 -18
- 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.d.ts +2 -0
- package/dist/server/render.js +142 -33
- package/dist/server/route-server-modules.d.ts +48 -8
- package/dist/server/route-server-modules.js +190 -7
- package/dist/server/speculation-rules.d.ts +3 -0
- package/dist/server/speculation-rules.js +8 -0
- package/dist/server/sse.d.ts +50 -12
- package/dist/server/sse.js +130 -53
- package/dist/vite/adapter-cloudflare.d.ts +2 -0
- package/dist/vite/adapter-cloudflare.js +25 -0
- package/dist/vite/adapter-node.d.ts +2 -0
- package/dist/vite/adapter-node.js +49 -0
- package/dist/vite/adapter.d.ts +29 -0
- package/dist/vite/adapter.js +1 -0
- package/dist/vite/client-shim.js +5 -4
- package/dist/vite/guard-strip.js +52 -27
- package/dist/vite/hono-preact.d.ts +6 -6
- package/dist/vite/hono-preact.js +48 -77
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +1 -1
- package/dist/vite/node-dev-server.d.ts +4 -0
- package/dist/vite/node-dev-server.js +121 -0
- package/dist/vite/server-entry.d.ts +30 -7
- package/dist/vite/server-entry.js +170 -79
- package/dist/vite/server-exports-contract.d.ts +6 -0
- package/dist/vite/server-exports-contract.js +43 -0
- package/dist/vite/server-loader-validation.js +36 -9
- package/dist/vite/server-loaders-parser.d.ts +17 -1
- package/dist/vite/server-loaders-parser.js +41 -0
- package/dist/vite/server-only.js +20 -2
- package/package.json +33 -5
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ Full walkthrough: https://framework.sbesh.com/docs/quick-start
|
|
|
37
37
|
|
|
38
38
|
## Subpaths
|
|
39
39
|
|
|
40
|
-
- `hono-preact`: iso runtime exports (routes, pages, loaders, actions, forms,
|
|
40
|
+
- `hono-preact`: iso runtime exports (routes, pages, loaders, actions, forms, middleware, outcomes).
|
|
41
|
+
- `hono-preact/page`: page-scope outcome kitchen sink (`redirect`, `deny`, `render`, predicates).
|
|
41
42
|
- `hono-preact/server`: server entry, `renderPage`, SSR streaming helpers.
|
|
42
43
|
- `hono-preact/vite`: `honoPreact()` plugin for Vite.
|
|
43
44
|
- `hono-preact/internal`: advanced exports for tooling authors. No stability guarantee.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vite/adapter-cloudflare';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-cloudflare.d.ts","sourceRoot":"","sources":["../src/adapter-cloudflare.ts"],"names":[],"mappings":"AACA,cAAc,sCAAsC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vite/adapter-node';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-node.d.ts","sourceRoot":"","sources":["../src/adapter-node.ts"],"names":[],"mappings":"AACA,cAAc,gCAAgC,CAAC"}
|
package/dist/internal.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './iso/internal
|
|
1
|
+
export * from './iso/internal';
|
package/dist/internal.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './iso/internal
|
|
1
|
+
export * from './iso/internal.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type ActionResultContextValue = {
|
|
2
|
+
module: string;
|
|
3
|
+
action: string;
|
|
4
|
+
kind: 'success';
|
|
5
|
+
data: unknown;
|
|
6
|
+
submittedPayload: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
module: string;
|
|
9
|
+
action: string;
|
|
10
|
+
kind: 'deny';
|
|
11
|
+
status: number;
|
|
12
|
+
message: string;
|
|
13
|
+
data?: unknown;
|
|
14
|
+
submittedPayload: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
module: string;
|
|
17
|
+
action: string;
|
|
18
|
+
kind: 'error';
|
|
19
|
+
message: string;
|
|
20
|
+
submittedPayload: unknown;
|
|
21
|
+
} | null;
|
|
22
|
+
export declare const ActionResultContext: import("preact").Context<ActionResultContextValue>;
|
package/dist/iso/action.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
|
-
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
2
|
import type { LoaderRef } from './define-loader.js';
|
|
3
|
+
import type { ActionUse } from './internal/use-types.js';
|
|
4
4
|
export type ActionStub<TPayload, TResult, TChunk = never> = {
|
|
5
5
|
readonly __module: string;
|
|
6
6
|
readonly __action: string;
|
|
@@ -12,8 +12,37 @@ export type ActionCtx = {
|
|
|
12
12
|
signal: AbortSignal;
|
|
13
13
|
};
|
|
14
14
|
export type ActionFn<TPayload, TResult, TChunk = never> = ((ctx: ActionCtx, payload: TPayload) => Promise<TResult>) | ((ctx: ActionCtx, payload: TPayload) => Promise<ReadableStream<TChunk>>) | ((ctx: ActionCtx, payload: TPayload) => AsyncGenerator<TChunk, TResult, unknown>);
|
|
15
|
-
export
|
|
16
|
-
|
|
15
|
+
export type DefineActionOpts<TChunk = never, TResult = unknown> = {
|
|
16
|
+
/**
|
|
17
|
+
* Per-action middleware and (for streaming actions) stream observers.
|
|
18
|
+
* Attached to the function as a non-enumerable property; the
|
|
19
|
+
* page-action-handler reads it through the typed `ActionEntry` map built at
|
|
20
|
+
* module-load time (`packages/server/src/page-action-handler.ts`).
|
|
21
|
+
*/
|
|
22
|
+
use?: ActionUse<TChunk, TResult, boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Per-action timeout in milliseconds. When omitted, the handler applies
|
|
25
|
+
* its configured default (30s). Pass `false` to disable the timeout for
|
|
26
|
+
* this action.
|
|
27
|
+
*/
|
|
28
|
+
timeoutMs?: number | false;
|
|
29
|
+
/**
|
|
30
|
+
* The module key the client-side `useAction` hook will reference in its
|
|
31
|
+
* RPC envelope. Production wires this through the Vite plugin's
|
|
32
|
+
* client-stub emission; test code can pass it directly to construct a
|
|
33
|
+
* properly-shaped stub without bypassing the type system.
|
|
34
|
+
*/
|
|
35
|
+
__module?: string;
|
|
36
|
+
/** The action name the client-side `useAction` hook will reference. */
|
|
37
|
+
__action?: string;
|
|
38
|
+
};
|
|
39
|
+
export declare class TimeoutError extends Error {
|
|
40
|
+
readonly kind: "timeout";
|
|
41
|
+
readonly timeoutMs: number;
|
|
42
|
+
constructor(timeoutMs: number);
|
|
43
|
+
}
|
|
44
|
+
export declare function defineAction<TPayload, TResult, TChunk = never>(fn: ActionFn<TPayload, TResult, TChunk>, opts?: DefineActionOpts<TChunk, TResult>): ActionStub<TPayload, TResult, TChunk>;
|
|
45
|
+
type UseActionOptionsCommon<TChunk = never> = {
|
|
17
46
|
/**
|
|
18
47
|
* How to update loader caches after the action commits. Three modes:
|
|
19
48
|
*
|
|
@@ -29,30 +58,47 @@ export type UseActionOptions<TPayload, TResult, TChunk = never, TSnapshot = unkn
|
|
|
29
58
|
* See `/docs/reloading` for the full mental model.
|
|
30
59
|
*/
|
|
31
60
|
invalidate?: 'auto' | false | ReadonlyArray<LoaderRef<unknown>>;
|
|
32
|
-
onMutate?: (payload: TPayload) => TSnapshot;
|
|
33
61
|
onChunk?: (chunk: TChunk) => void;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Options when `onMutate` is provided. `onSuccess` / `onError` receive the
|
|
65
|
+
* value `onMutate` returned for this specific mutation as the second
|
|
66
|
+
* parameter, so concurrent calls can be paired with their own snapshot.
|
|
67
|
+
*/
|
|
68
|
+
type UseActionWithMutate<TPayload, TResult, TChunk, TSnapshot> = UseActionOptionsCommon<TChunk> & {
|
|
69
|
+
onMutate: (payload: TPayload) => TSnapshot;
|
|
34
70
|
onError?: (err: Error, snapshot: TSnapshot) => void;
|
|
35
71
|
onSuccess?: (data: TResult, snapshot: TSnapshot) => void;
|
|
36
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Options when `onMutate` is not provided. `onSuccess` / `onError` take
|
|
75
|
+
* only the result / error — there is no snapshot to thread through.
|
|
76
|
+
*/
|
|
77
|
+
type UseActionWithoutMutate<TResult, TChunk> = UseActionOptionsCommon<TChunk> & {
|
|
78
|
+
onMutate?: undefined;
|
|
79
|
+
onError?: (err: Error) => void;
|
|
80
|
+
onSuccess?: (data: TResult) => void;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Discriminated by `onMutate`. Providing `onMutate` requires the
|
|
84
|
+
* `onSuccess` / `onError` callbacks to accept the snapshot; omitting
|
|
85
|
+
* `onMutate` types those callbacks as single-argument.
|
|
86
|
+
*/
|
|
87
|
+
export type UseActionOptions<TPayload, TResult, TChunk = never, TSnapshot = unknown> = UseActionWithMutate<TPayload, TResult, TChunk, TSnapshot> | UseActionWithoutMutate<TResult, TChunk>;
|
|
37
88
|
/**
|
|
38
89
|
* The value `mutate` resolves to. A discriminated union so callers can
|
|
39
90
|
* chain on success without awaiting then probing the hook's `data`/`error`
|
|
40
91
|
* state, and without leaking unhandled rejections in fire-and-forget callers.
|
|
41
92
|
*
|
|
42
|
-
* - Success: `{ ok: true, data }`.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
93
|
+
* - Success: `{ ok: true, data }`. `data` is `undefined` for streaming
|
|
94
|
+
* actions that close without emitting a `result` SSE event (the type
|
|
95
|
+
* reflects this honestly: callers must narrow before using `data`).
|
|
45
96
|
* - Failure: `{ ok: false, error }`. The same `Error` instance is also
|
|
46
97
|
* written to the hook's `error` state and passed to `onError`.
|
|
47
|
-
*
|
|
48
|
-
* Returning a union (rather than throwing) keeps `mutate(...)` ergonomic
|
|
49
|
-
* for non-awaiting call sites — the existing `error` state field is the
|
|
50
|
-
* idiomatic way to render an error UI — while still letting awaiting
|
|
51
|
-
* callers do `if (result.ok) navigate(...)`.
|
|
52
98
|
*/
|
|
53
99
|
export type MutateResult<TResult> = {
|
|
54
100
|
ok: true;
|
|
55
|
-
data: TResult;
|
|
101
|
+
data: TResult | undefined;
|
|
56
102
|
} | {
|
|
57
103
|
ok: false;
|
|
58
104
|
error: Error;
|
|
@@ -64,15 +110,4 @@ export type UseActionResult<TPayload, TResult> = {
|
|
|
64
110
|
data: TResult | null;
|
|
65
111
|
};
|
|
66
112
|
export declare function useAction<TPayload, TResult, TChunk = never, TSnapshot = unknown>(stub: ActionStub<TPayload, TResult, TChunk>, options?: UseActionOptions<TPayload, TResult, TChunk, TSnapshot>): UseActionResult<TPayload, TResult>;
|
|
67
|
-
export
|
|
68
|
-
c: Context;
|
|
69
|
-
module: string;
|
|
70
|
-
action: string;
|
|
71
|
-
payload: unknown;
|
|
72
|
-
};
|
|
73
|
-
export type ActionGuardFn = (ctx: ActionGuardContext, next: () => Promise<void>) => Promise<void>;
|
|
74
|
-
export declare class ActionGuardError extends Error {
|
|
75
|
-
readonly status: ContentfulStatusCode;
|
|
76
|
-
constructor(message: string, status?: ContentfulStatusCode);
|
|
77
|
-
}
|
|
78
|
-
export declare const defineActionGuard: (fn: ActionGuardFn) => ActionGuardFn;
|
|
113
|
+
export {};
|
package/dist/iso/action.js
CHANGED
|
@@ -1,11 +1,62 @@
|
|
|
1
1
|
import { useCallback, useContext, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { ReloadContext } from './reload-context.js';
|
|
3
3
|
import { ActiveLoaderIdContext } from './internal/contexts.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { beginSubmit, endSubmit } from './internal/form-submit-store.js';
|
|
5
|
+
import { assignSafeRedirect } from './internal/safe-redirect.js';
|
|
6
|
+
import { setLastActionResult, } from './internal/action-result-store.js';
|
|
7
|
+
export class TimeoutError extends Error {
|
|
8
|
+
kind = 'timeout';
|
|
9
|
+
timeoutMs;
|
|
10
|
+
constructor(timeoutMs) {
|
|
11
|
+
super(`Request timed out after ${timeoutMs}ms`);
|
|
12
|
+
this.name = 'TimeoutError';
|
|
13
|
+
this.timeoutMs = timeoutMs;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function validateTimeoutMs(value, context) {
|
|
17
|
+
if (value === undefined || value === false)
|
|
18
|
+
return;
|
|
19
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
20
|
+
throw new RangeError(`${context}: timeoutMs must be a non-negative finite number or false, got ${String(value)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function defineAction(fn, opts) {
|
|
24
|
+
validateTimeoutMs(opts?.timeoutMs, 'defineAction');
|
|
25
|
+
// SHAPE NOTE: `ActionStub` describes the CLIENT-side shape produced by the
|
|
26
|
+
// Vite plugin (`packages/vite/src/server-only.ts`) — an object with
|
|
27
|
+
// `__module`, `__action`, and a `useAction` method. `defineAction` runs on
|
|
28
|
+
// the SERVER side and returns the raw function with metadata attached via
|
|
29
|
+
// `Object.defineProperty`. These are two different runtime shapes unified
|
|
30
|
+
// under one type so consumers can import a server action and use it
|
|
31
|
+
// identically on both sides; the plugin handles the substitution at the
|
|
32
|
+
// value level. The `as unknown as` cast at the return is the single
|
|
33
|
+
// bounded acknowledgement of this dual-shape contract. A future cleanup
|
|
34
|
+
// could split into `ServerActionImpl` / `ActionStub` types if the lie
|
|
35
|
+
// starts to bite, but for now it's localized and documented.
|
|
36
|
+
//
|
|
37
|
+
// `Object.defineProperty` is used instead of direct assignment so a frozen
|
|
38
|
+
// module export (strict ESM, HMR-frozen modules) does not throw.
|
|
39
|
+
const attach = (key, value) => {
|
|
40
|
+
Object.defineProperty(fn, key, {
|
|
41
|
+
value,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
enumerable: false,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
if (opts?.use)
|
|
48
|
+
attach('use', opts.use);
|
|
49
|
+
if (opts?.timeoutMs !== undefined)
|
|
50
|
+
attach('timeoutMs', opts.timeoutMs);
|
|
51
|
+
if (opts?.__module !== undefined)
|
|
52
|
+
attach('__module', opts.__module);
|
|
53
|
+
if (opts?.__action !== undefined)
|
|
54
|
+
attach('__action', opts.__action);
|
|
7
55
|
return fn;
|
|
8
56
|
}
|
|
57
|
+
function recordOutcome(module, action, result) {
|
|
58
|
+
setLastActionResult(module, action, result);
|
|
59
|
+
}
|
|
9
60
|
function hasFileValues(payload) {
|
|
10
61
|
if (typeof File === 'undefined')
|
|
11
62
|
return false;
|
|
@@ -28,18 +79,49 @@ export function useAction(stub, options) {
|
|
|
28
79
|
setError(null);
|
|
29
80
|
const currentStub = stubRef.current;
|
|
30
81
|
const currentOptions = optionsRef.current;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
82
|
+
const callbacks = currentOptions?.onMutate
|
|
83
|
+
? {
|
|
84
|
+
kind: 'with-mutate',
|
|
85
|
+
snapshot: currentOptions.onMutate(payload),
|
|
86
|
+
onSuccess: currentOptions.onSuccess,
|
|
87
|
+
onError: currentOptions.onError,
|
|
88
|
+
}
|
|
89
|
+
: {
|
|
90
|
+
kind: 'without-mutate',
|
|
91
|
+
onSuccess: currentOptions?.onSuccess,
|
|
92
|
+
onError: currentOptions?.onError,
|
|
93
|
+
};
|
|
94
|
+
const invokeSuccess = (data) => {
|
|
95
|
+
if (callbacks.kind === 'with-mutate') {
|
|
96
|
+
callbacks.onSuccess?.(data, callbacks.snapshot);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
callbacks.onSuccess?.(data);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const invokeError = (err) => {
|
|
103
|
+
if (callbacks.kind === 'with-mutate') {
|
|
104
|
+
callbacks.onError?.(err, callbacks.snapshot);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
callbacks.onError?.(err);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const target = typeof window !== 'undefined'
|
|
111
|
+
? window.location.pathname + window.location.search
|
|
112
|
+
: '/';
|
|
35
113
|
let finalResult;
|
|
114
|
+
// Tracks whether a branch has already written to the action-result store.
|
|
115
|
+
// The outer catch writes for unclassified errors (network failures, parse
|
|
116
|
+
// errors) only when no branch has already recorded the outcome.
|
|
117
|
+
let outcomeRecorded = false;
|
|
118
|
+
beginSubmit(currentStub.__module, currentStub.__action);
|
|
36
119
|
try {
|
|
37
|
-
const stub = currentStub;
|
|
38
120
|
let response;
|
|
39
121
|
if (hasFileValues(payload)) {
|
|
40
122
|
const fd = new FormData();
|
|
41
|
-
fd.append('__module',
|
|
42
|
-
fd.append('__action',
|
|
123
|
+
fd.append('__module', currentStub.__module);
|
|
124
|
+
fd.append('__action', currentStub.__action);
|
|
43
125
|
for (const [key, value] of Object.entries(payload)) {
|
|
44
126
|
if (key === '__module' || key === '__action')
|
|
45
127
|
continue;
|
|
@@ -53,44 +135,27 @@ export function useAction(stub, options) {
|
|
|
53
135
|
fd.append(key, JSON.stringify(value));
|
|
54
136
|
}
|
|
55
137
|
}
|
|
56
|
-
response = await fetch(
|
|
138
|
+
response = await fetch(target, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { Accept: 'application/json, text/event-stream;q=0.9' },
|
|
141
|
+
body: fd,
|
|
142
|
+
});
|
|
57
143
|
}
|
|
58
144
|
else {
|
|
59
|
-
response = await fetch(
|
|
145
|
+
response = await fetch(target, {
|
|
60
146
|
method: 'POST',
|
|
61
|
-
headers: {
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
Accept: 'application/json, text/event-stream;q=0.9',
|
|
150
|
+
},
|
|
62
151
|
body: JSON.stringify({
|
|
63
|
-
module:
|
|
64
|
-
action:
|
|
152
|
+
module: currentStub.__module,
|
|
153
|
+
action: currentStub.__action,
|
|
65
154
|
payload,
|
|
66
155
|
}),
|
|
67
156
|
});
|
|
68
157
|
}
|
|
69
|
-
if (!response.ok) {
|
|
70
|
-
const body = (await response.json());
|
|
71
|
-
throw new Error(body.error ?? `Action failed with status ${response.status}`);
|
|
72
|
-
}
|
|
73
158
|
const contentType = response.headers.get('Content-Type') ?? '';
|
|
74
|
-
// Server-side `GuardRedirect` thrown from an action (or its guards) comes
|
|
75
|
-
// back as `{ __redirect }`. Hand off to the browser; the rest of this
|
|
76
|
-
// promise will never settle, but the page is navigating away anyway.
|
|
77
|
-
if (!contentType.includes('text/event-stream')) {
|
|
78
|
-
const peek = (await response
|
|
79
|
-
.clone()
|
|
80
|
-
.json()
|
|
81
|
-
.catch(() => undefined));
|
|
82
|
-
if (peek !== null &&
|
|
83
|
-
typeof peek === 'object' &&
|
|
84
|
-
peek !== undefined &&
|
|
85
|
-
'__redirect' in peek &&
|
|
86
|
-
typeof peek.__redirect === 'string') {
|
|
87
|
-
if (typeof window !== 'undefined') {
|
|
88
|
-
window.location.assign(peek.__redirect);
|
|
89
|
-
}
|
|
90
|
-
// Cast through `as` because TS can't see this promise never settles.
|
|
91
|
-
return await new Promise(() => { });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
159
|
if (contentType.includes('text/event-stream') && response.body) {
|
|
95
160
|
const { readSSE } = await import('./internal/sse-decoder.js');
|
|
96
161
|
let resultValue;
|
|
@@ -112,6 +177,15 @@ export function useAction(stub, options) {
|
|
|
112
177
|
streamError = new Error(`Malformed result event in stream: ${e instanceof Error ? e.message : String(e)}`);
|
|
113
178
|
}
|
|
114
179
|
}
|
|
180
|
+
else if (ev.event === 'timeout') {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(ev.data);
|
|
183
|
+
streamError = new TimeoutError(parsed.timeoutMs ?? 0);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
streamError = new Error(`Malformed timeout event in stream: ${e instanceof Error ? e.message : String(e)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
115
189
|
else if (ev.event === 'error') {
|
|
116
190
|
try {
|
|
117
191
|
const parsed = JSON.parse(ev.data);
|
|
@@ -125,26 +199,101 @@ export function useAction(stub, options) {
|
|
|
125
199
|
}
|
|
126
200
|
}
|
|
127
201
|
if (streamError) {
|
|
202
|
+
recordOutcome(currentStub.__module, currentStub.__action, {
|
|
203
|
+
kind: 'error',
|
|
204
|
+
message: streamError.message,
|
|
205
|
+
submittedPayload: payload,
|
|
206
|
+
});
|
|
207
|
+
outcomeRecorded = true;
|
|
128
208
|
throw streamError;
|
|
129
209
|
}
|
|
130
210
|
if (resultValue !== undefined) {
|
|
131
211
|
setData(resultValue);
|
|
132
|
-
|
|
212
|
+
invokeSuccess(resultValue);
|
|
133
213
|
finalResult = resultValue;
|
|
214
|
+
recordOutcome(currentStub.__module, currentStub.__action, {
|
|
215
|
+
kind: 'success',
|
|
216
|
+
data: resultValue,
|
|
217
|
+
submittedPayload: payload,
|
|
218
|
+
});
|
|
219
|
+
outcomeRecorded = true;
|
|
134
220
|
}
|
|
135
221
|
else {
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
222
|
+
// Streaming action closed without emitting a `result` event;
|
|
223
|
+
// resolve with `data: undefined`. `onSuccess` is not called
|
|
224
|
+
// in this branch since there is no result value to deliver
|
|
225
|
+
// (matches the static-action path where onSuccess only fires
|
|
226
|
+
// with a real value).
|
|
140
227
|
finalResult = undefined;
|
|
141
228
|
}
|
|
142
229
|
}
|
|
143
230
|
else {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
231
|
+
// Uniform envelope path. All non-streaming responses carry a JSON
|
|
232
|
+
// body shaped as { __outcome, ... } regardless of HTTP status.
|
|
233
|
+
let env;
|
|
234
|
+
try {
|
|
235
|
+
env = (await response.json());
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new Error(`Malformed envelope (HTTP ${response.status})`);
|
|
239
|
+
}
|
|
240
|
+
if (env.__outcome === 'success') {
|
|
241
|
+
const data = env.data;
|
|
242
|
+
setData(data);
|
|
243
|
+
invokeSuccess(data);
|
|
244
|
+
finalResult = data;
|
|
245
|
+
recordOutcome(currentStub.__module, currentStub.__action, {
|
|
246
|
+
kind: 'success',
|
|
247
|
+
data,
|
|
248
|
+
submittedPayload: payload,
|
|
249
|
+
});
|
|
250
|
+
outcomeRecorded = true;
|
|
251
|
+
}
|
|
252
|
+
else if (env.__outcome === 'redirect' &&
|
|
253
|
+
typeof env.to === 'string') {
|
|
254
|
+
if (assignSafeRedirect(env.to)) {
|
|
255
|
+
// Navigation issued; this promise never settles.
|
|
256
|
+
return await new Promise(() => { });
|
|
257
|
+
}
|
|
258
|
+
// Cross-origin: surface as an error so the caller can handle it.
|
|
259
|
+
throw new Error(`Refused cross-origin redirect to ${env.to}`);
|
|
260
|
+
}
|
|
261
|
+
else if (env.__outcome === 'deny') {
|
|
262
|
+
const msg = env.message ??
|
|
263
|
+
`Request denied (${env.status ?? response.status})`;
|
|
264
|
+
recordOutcome(currentStub.__module, currentStub.__action, {
|
|
265
|
+
kind: 'deny',
|
|
266
|
+
status: env.status ?? response.status,
|
|
267
|
+
message: msg,
|
|
268
|
+
data: env.data,
|
|
269
|
+
submittedPayload: payload,
|
|
270
|
+
});
|
|
271
|
+
outcomeRecorded = true;
|
|
272
|
+
throw new Error(msg);
|
|
273
|
+
}
|
|
274
|
+
else if (env.__outcome === 'timeout' &&
|
|
275
|
+
typeof env.timeoutMs === 'number') {
|
|
276
|
+
recordOutcome(currentStub.__module, currentStub.__action, {
|
|
277
|
+
kind: 'error',
|
|
278
|
+
message: `Request timed out after ${env.timeoutMs}ms`,
|
|
279
|
+
submittedPayload: payload,
|
|
280
|
+
});
|
|
281
|
+
outcomeRecorded = true;
|
|
282
|
+
throw new TimeoutError(env.timeoutMs);
|
|
283
|
+
}
|
|
284
|
+
else if (env.__outcome === 'error') {
|
|
285
|
+
const msg = env.message ?? 'Action failed';
|
|
286
|
+
recordOutcome(currentStub.__module, currentStub.__action, {
|
|
287
|
+
kind: 'error',
|
|
288
|
+
message: msg,
|
|
289
|
+
submittedPayload: payload,
|
|
290
|
+
});
|
|
291
|
+
outcomeRecorded = true;
|
|
292
|
+
throw new Error(msg);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
throw new Error(`Unknown action outcome: ${env.__outcome}`);
|
|
296
|
+
}
|
|
148
297
|
}
|
|
149
298
|
if (currentOptions?.invalidate === 'auto') {
|
|
150
299
|
reloadCtx?.reload();
|
|
@@ -168,22 +317,25 @@ export function useAction(stub, options) {
|
|
|
168
317
|
}
|
|
169
318
|
catch (err) {
|
|
170
319
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
320
|
+
// Write to the store only for unclassified errors (network failures,
|
|
321
|
+
// parse errors). Per-branch errors set outcomeRecorded before throwing.
|
|
322
|
+
if (!outcomeRecorded) {
|
|
323
|
+
recordOutcome(currentStub.__module, currentStub.__action, {
|
|
324
|
+
kind: 'error',
|
|
325
|
+
message: e.message,
|
|
326
|
+
submittedPayload: payload,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
171
329
|
setError(e);
|
|
172
|
-
|
|
330
|
+
invokeError(e);
|
|
173
331
|
setPending(false);
|
|
174
332
|
return { ok: false, error: e };
|
|
175
333
|
}
|
|
334
|
+
finally {
|
|
335
|
+
endSubmit(currentStub.__module, currentStub.__action);
|
|
336
|
+
}
|
|
176
337
|
setPending(false);
|
|
177
338
|
return { ok: true, data: finalResult };
|
|
178
339
|
}, []);
|
|
179
340
|
return { mutate, pending, error, data };
|
|
180
341
|
}
|
|
181
|
-
export class ActionGuardError extends Error {
|
|
182
|
-
status;
|
|
183
|
-
constructor(message, status = 403) {
|
|
184
|
-
super(message);
|
|
185
|
-
this.status = status;
|
|
186
|
-
this.name = 'ActionGuardError';
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
export const defineActionGuard = (fn) => fn;
|
package/dist/iso/cache.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Loader } from './define-loader.js';
|
|
2
|
+
import type { ActionResolution } from './internal/action-envelope.js';
|
|
2
3
|
export interface LoaderCache<T> {
|
|
3
4
|
get(locKey?: string): T | null;
|
|
4
5
|
set(value: T, locKey?: string): void;
|
|
@@ -9,6 +10,14 @@ export interface LoaderCache<T> {
|
|
|
9
10
|
type RequestStore = Map<symbol, unknown>;
|
|
10
11
|
export declare function getRequestStore(): RequestStore | undefined;
|
|
11
12
|
export declare function getRequestHonoContext<T = unknown>(): T | undefined;
|
|
13
|
+
export type ActionResultSlot = {
|
|
14
|
+
module: string;
|
|
15
|
+
action: string;
|
|
16
|
+
resolution: ActionResolution;
|
|
17
|
+
submittedPayload: unknown;
|
|
18
|
+
};
|
|
19
|
+
export declare function getActionResultSlot(): ActionResultSlot | null;
|
|
20
|
+
export declare function setActionResultSlot(slot: ActionResultSlot): void;
|
|
12
21
|
export declare function runRequestScope<R>(fn: () => R | Promise<R>, initial?: {
|
|
13
22
|
honoContext?: unknown;
|
|
14
23
|
}): R | Promise<R>;
|
package/dist/iso/cache.js
CHANGED
|
@@ -18,6 +18,7 @@ if (!looksLikeBrowser) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
const HONO_CONTEXT_KEY = Symbol('@hono-preact/iso/honoContext');
|
|
21
|
+
const ACTION_RESULT_KEY = Symbol('@hono-preact/iso/actionResult');
|
|
21
22
|
export function getRequestStore() {
|
|
22
23
|
return alsInstance?.getStore();
|
|
23
24
|
}
|
|
@@ -36,9 +37,34 @@ export function getRequestHonoContext() {
|
|
|
36
37
|
}
|
|
37
38
|
return ctx;
|
|
38
39
|
}
|
|
40
|
+
export function getActionResultSlot() {
|
|
41
|
+
const store = getRequestStore();
|
|
42
|
+
if (!store)
|
|
43
|
+
return null;
|
|
44
|
+
const slot = store.get(ACTION_RESULT_KEY);
|
|
45
|
+
return (slot ?? null);
|
|
46
|
+
}
|
|
47
|
+
export function setActionResultSlot(slot) {
|
|
48
|
+
const store = getRequestStore();
|
|
49
|
+
if (!store) {
|
|
50
|
+
throw new Error('setActionResultSlot must be called inside runRequestScope');
|
|
51
|
+
}
|
|
52
|
+
store.set(ACTION_RESULT_KEY, slot);
|
|
53
|
+
}
|
|
39
54
|
export function runRequestScope(fn, initial) {
|
|
40
55
|
if (!alsInstance)
|
|
41
56
|
return fn();
|
|
57
|
+
const existing = alsInstance.getStore();
|
|
58
|
+
if (existing) {
|
|
59
|
+
// Nested call: inherit the parent store. Seeded values are written
|
|
60
|
+
// additively so the inner caller's overrides take effect without
|
|
61
|
+
// wiping the parent's per-request state (e.g. the action-result
|
|
62
|
+
// slot set by pageActionHandler before it invokes renderPage).
|
|
63
|
+
if (initial?.honoContext !== undefined) {
|
|
64
|
+
existing.set(HONO_CONTEXT_KEY, initial.honoContext);
|
|
65
|
+
}
|
|
66
|
+
return fn();
|
|
67
|
+
}
|
|
42
68
|
const store = new Map();
|
|
43
69
|
if (initial?.honoContext !== undefined) {
|
|
44
70
|
store.set(HONO_CONTEXT_KEY, initial.honoContext);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ServerMiddleware, ClientMiddleware } from './define-middleware.js';
|
|
2
|
+
import type { StreamObserver } from './define-stream-observer.js';
|
|
3
|
+
export type AppUseElement = ServerMiddleware<'page'> | ClientMiddleware | StreamObserver<unknown, never>;
|
|
4
|
+
export type AppConfig = {
|
|
5
|
+
use?: ReadonlyArray<AppUseElement>;
|
|
6
|
+
/**
|
|
7
|
+
* When `true`, the server emits a `<script type="speculationrules">` tag
|
|
8
|
+
* into `<head>` that instructs supporting browsers to prefetch same-origin
|
|
9
|
+
* `<a href>` links on moderate eagerness. Defaults to `false`. Individual
|
|
10
|
+
* links opt out with `data-no-prefetch`.
|
|
11
|
+
*/
|
|
12
|
+
speculation?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function defineApp(config: AppConfig): AppConfig;
|