@rc312/tss-effect-serverfn 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +126 -2
- package/dist/bootstrap.server.d.ts +12 -0
- package/dist/bootstrap.server.d.ts.map +1 -0
- package/dist/bootstrap.server.js +17 -0
- package/dist/bootstrap.server.js.map +1 -0
- package/dist/boundary.server.d.ts +26 -0
- package/dist/boundary.server.d.ts.map +1 -0
- package/dist/boundary.server.js +117 -0
- package/dist/boundary.server.js.map +1 -0
- package/dist/compiler-runtime.d.ts +40 -0
- package/dist/compiler-runtime.d.ts.map +1 -0
- package/dist/compiler-runtime.js +54 -0
- package/dist/compiler-runtime.js.map +1 -0
- package/dist/create-effect-server-fn.d.ts +84 -0
- package/dist/create-effect-server-fn.d.ts.map +1 -0
- package/dist/create-effect-server-fn.js +27 -0
- package/dist/create-effect-server-fn.js.map +1 -0
- package/dist/effect-middleware.d.ts +116 -0
- package/dist/effect-middleware.d.ts.map +1 -0
- package/dist/effect-middleware.js +46 -0
- package/dist/effect-middleware.js.map +1 -0
- package/dist/execution.d.ts +35 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +7 -0
- package/dist/execution.js.map +1 -0
- package/dist/failure.server.d.ts +44 -0
- package/dist/failure.server.d.ts.map +1 -0
- package/dist/failure.server.js +161 -0
- package/dist/failure.server.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/provenance.server.d.ts +54 -0
- package/dist/provenance.server.d.ts.map +1 -0
- package/dist/provenance.server.js +87 -0
- package/dist/provenance.server.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -0
- package/dist/vite.d.ts +24 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +296 -0
- package/dist/vite.js.map +1 -0
- package/docs/composition.md +188 -0
- package/docs/usage.md +223 -0
- package/package.json +67 -4
- package/src/bootstrap.server.ts +22 -0
- package/src/boundary.server.ts +198 -0
- package/src/compiler-runtime.ts +75 -0
- package/src/create-effect-server-fn.ts +197 -0
- package/src/effect-middleware.ts +171 -0
- package/src/execution.ts +39 -0
- package/src/failure.server.ts +257 -0
- package/src/index.ts +12 -0
- package/src/provenance.server.ts +125 -0
- package/src/server.ts +7 -0
- package/src/vite.ts +462 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
3
|
+
import type {
|
|
4
|
+
Fetcher,
|
|
5
|
+
FunctionMiddlewareWithTypes,
|
|
6
|
+
Method,
|
|
7
|
+
Register,
|
|
8
|
+
ServerFnBuilder,
|
|
9
|
+
ServerFnCtx,
|
|
10
|
+
ServerFnOptions,
|
|
11
|
+
ServerFnStrict,
|
|
12
|
+
ServerFnStrictOutput,
|
|
13
|
+
} from "@tanstack/react-start";
|
|
14
|
+
import type { ValidateSerializableInput } from "@tanstack/router-core";
|
|
15
|
+
import type { Effect, Schema } from "effect";
|
|
16
|
+
|
|
17
|
+
import type { ExecutionCapability, RequestServices } from "./execution.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type-only bridge from an Effect Schema to Start's validator inference.
|
|
21
|
+
* Clients supply encoded input; handlers receive decoded data. Runtime decoding
|
|
22
|
+
* is supplied by the compiler bridge, not a Standard Schema adapter.
|
|
23
|
+
*/
|
|
24
|
+
type NativeValidator<Input> = Input extends Schema.Constraint
|
|
25
|
+
? StandardSchemaV1<Input["Encoded"], Input["Type"]>
|
|
26
|
+
: undefined;
|
|
27
|
+
|
|
28
|
+
type HandlerContext<MethodType, Middlewares, Input> = ServerFnCtx<
|
|
29
|
+
Register,
|
|
30
|
+
MethodType,
|
|
31
|
+
Middlewares,
|
|
32
|
+
NativeValidator<Input>
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
type DecodingServices<Input> = Input extends Schema.Constraint
|
|
36
|
+
? Input["DecodingServices"]
|
|
37
|
+
: never;
|
|
38
|
+
|
|
39
|
+
type AppendMiddlewares<
|
|
40
|
+
Current,
|
|
41
|
+
Next extends NativeMiddlewares,
|
|
42
|
+
> = Current extends readonly unknown[] ? readonly [...Current, ...Next] : Next;
|
|
43
|
+
|
|
44
|
+
type SerializableOutput<A, Strict extends ServerFnStrict> =
|
|
45
|
+
ServerFnStrictOutput<Strict> extends false
|
|
46
|
+
? A
|
|
47
|
+
: A extends Response
|
|
48
|
+
? A
|
|
49
|
+
: ValidateSerializableInput<Register, A>;
|
|
50
|
+
|
|
51
|
+
type NativeBuilder<
|
|
52
|
+
M extends Method,
|
|
53
|
+
Strict extends ServerFnStrict,
|
|
54
|
+
> = ServerFnBuilder<Register, M, Strict>;
|
|
55
|
+
|
|
56
|
+
type NativeMiddlewares = Parameters<
|
|
57
|
+
NativeBuilder<Method, ServerFnStrict>["middleware"]
|
|
58
|
+
>[0];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Enable handlers only when middleware supplies a runtime and all decoding
|
|
62
|
+
* services. The returned Effect must also satisfy service and wire-output checks.
|
|
63
|
+
*/
|
|
64
|
+
type Handler<
|
|
65
|
+
M extends Method,
|
|
66
|
+
Middlewares,
|
|
67
|
+
Input,
|
|
68
|
+
Strict extends ServerFnStrict,
|
|
69
|
+
> =
|
|
70
|
+
HandlerContext<M, Middlewares, Input>["context"] extends ExecutionCapability<
|
|
71
|
+
infer Services
|
|
72
|
+
>
|
|
73
|
+
? [Exclude<DecodingServices<Input>, Services | RequestServices>] extends [
|
|
74
|
+
never,
|
|
75
|
+
]
|
|
76
|
+
? <A, E>(
|
|
77
|
+
handler: (
|
|
78
|
+
context: HandlerContext<M, Middlewares, Input>
|
|
79
|
+
) => Effect.Effect<A, E, Services | RequestServices> &
|
|
80
|
+
Effect.Effect<SerializableOutput<A, Strict>, unknown, unknown>
|
|
81
|
+
) => Fetcher<Middlewares, NativeValidator<Input>, A>
|
|
82
|
+
: never
|
|
83
|
+
: never;
|
|
84
|
+
|
|
85
|
+
type SerializableInput<
|
|
86
|
+
Input extends Schema.Constraint,
|
|
87
|
+
M extends Method,
|
|
88
|
+
Strict extends ServerFnStrict,
|
|
89
|
+
> = Strict extends false | { input: false }
|
|
90
|
+
? unknown
|
|
91
|
+
: [M, Input["Encoded"]] extends ["POST", FormData]
|
|
92
|
+
? unknown
|
|
93
|
+
: Schema.ConstraintCodec<
|
|
94
|
+
unknown,
|
|
95
|
+
ValidateSerializableInput<Register, Input["Encoded"]>,
|
|
96
|
+
unknown,
|
|
97
|
+
unknown
|
|
98
|
+
>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Start's server-function builder with raw Effect Schemas and Effect handlers.
|
|
102
|
+
*
|
|
103
|
+
* Native method, middleware-context, and serialization inference are preserved.
|
|
104
|
+
* Configured builders remain callable until a validator is attached, following
|
|
105
|
+
* Start's builder contract.
|
|
106
|
+
*
|
|
107
|
+
* @template M - HTTP method used by the server function.
|
|
108
|
+
* @template Middlewares - Accumulated native middleware and its context types.
|
|
109
|
+
* @template Input - The attached Effect Schema, or `undefined` before validation.
|
|
110
|
+
* @template Strict - Start's input/output serialization-check policy.
|
|
111
|
+
* @template Callable - Whether this builder can configure another endpoint.
|
|
112
|
+
*/
|
|
113
|
+
export type EffectServerFnBuilder<
|
|
114
|
+
M extends Method = "GET",
|
|
115
|
+
Middlewares = undefined,
|
|
116
|
+
Input = undefined,
|
|
117
|
+
Strict extends ServerFnStrict = true,
|
|
118
|
+
Callable extends boolean = true,
|
|
119
|
+
> = Omit<
|
|
120
|
+
NativeBuilder<M, Strict>,
|
|
121
|
+
"handler" | "validator" | "middleware" | "~types"
|
|
122
|
+
> & {
|
|
123
|
+
readonly "~types": FunctionMiddlewareWithTypes<
|
|
124
|
+
Register,
|
|
125
|
+
Middlewares,
|
|
126
|
+
NativeValidator<Input>,
|
|
127
|
+
undefined,
|
|
128
|
+
undefined,
|
|
129
|
+
undefined,
|
|
130
|
+
undefined
|
|
131
|
+
>["~types"] & {
|
|
132
|
+
readonly method: M;
|
|
133
|
+
readonly strict: Strict;
|
|
134
|
+
readonly response: undefined;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Return an Effect using decoded data and native middleware context.
|
|
138
|
+
* The generated client function returns a Promise; remote rejections do not
|
|
139
|
+
* carry the Effect error channel as a TypeScript rejection type.
|
|
140
|
+
*/
|
|
141
|
+
readonly handler: Handler<M, Middlewares, Input, Strict>;
|
|
142
|
+
/** Decode raw input in the handler's application runtime and request Scope. */
|
|
143
|
+
readonly validator: <NextInput extends Schema.Constraint>(
|
|
144
|
+
schema: NextInput & SerializableInput<NextInput, M, Strict>
|
|
145
|
+
) => EffectServerFnBuilder<M, Middlewares, NextInput, Strict, false>;
|
|
146
|
+
/** Append native middleware, retaining its inferred context and execution services. */
|
|
147
|
+
readonly middleware: <const Next extends NativeMiddlewares>(
|
|
148
|
+
middlewares: Next
|
|
149
|
+
) => EffectServerFnBuilder<
|
|
150
|
+
M,
|
|
151
|
+
AppendMiddlewares<Middlewares, Next>,
|
|
152
|
+
Input,
|
|
153
|
+
Strict,
|
|
154
|
+
Callable
|
|
155
|
+
>;
|
|
156
|
+
} & (Callable extends true
|
|
157
|
+
? <
|
|
158
|
+
NextMethod extends Method = M,
|
|
159
|
+
NextStrict extends ServerFnStrict = Strict,
|
|
160
|
+
>(
|
|
161
|
+
options?: ServerFnOptions<NextMethod, NextStrict>
|
|
162
|
+
) => EffectServerFnBuilder<NextMethod, Middlewares, Input, NextStrict>
|
|
163
|
+
: unknown);
|
|
164
|
+
|
|
165
|
+
/** Factory signature for an Effect-aware native Start server-function builder. */
|
|
166
|
+
export type CreateEffectServerFn = <
|
|
167
|
+
M extends Method = "GET",
|
|
168
|
+
Strict extends ServerFnStrict = true,
|
|
169
|
+
>(
|
|
170
|
+
options?: ServerFnOptions<M, Strict>
|
|
171
|
+
) => EffectServerFnBuilder<M, undefined, undefined, Strict>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Define a Start server function using an Effect Schema and an Effect handler.
|
|
175
|
+
*
|
|
176
|
+
* Requires `effectServerFns()` from `@rc312/tss-effect-serverfn/vite`, installed before
|
|
177
|
+
* `tanstackStart()`, and execution middleware from `createEffectMiddleware`.
|
|
178
|
+
* This value retains the native `createServerFn` identity; the plugin adapts
|
|
179
|
+
* validators and handlers before Start performs extraction and RPC generation.
|
|
180
|
+
*
|
|
181
|
+
* @param options - Native Start options, including HTTP method and strictness.
|
|
182
|
+
* @returns A builder whose completed handler exposes Start's Promise client.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* Using the shared factory configured with execution middleware:
|
|
186
|
+
* ```ts
|
|
187
|
+
* import { Effect, Schema } from "effect";
|
|
188
|
+
* import { createEffectServerFn } from "./effect-server-fn";
|
|
189
|
+
*
|
|
190
|
+
* export const greet = createEffectServerFn({ method: "GET" })
|
|
191
|
+
* .validator(Schema.Struct({ name: Schema.NonEmptyString }))
|
|
192
|
+
* .handler(({ data }) => Effect.succeed(`Hello, ${data.name}!`));
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export const createEffectServerFn: CreateEffectServerFn =
|
|
196
|
+
// SAFETY: The compiler lowers raw schemas and Effect callbacks before TanStack consumes them.
|
|
197
|
+
createServerFn as typeof createServerFn & CreateEffectServerFn;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createMiddleware, createServerOnlyFn } from "@tanstack/react-start";
|
|
2
|
+
import type { ServerFnMeta } from "@tanstack/react-start";
|
|
3
|
+
import type { ValidateSerializableInput } from "@tanstack/router-core";
|
|
4
|
+
import type { Cause, Duration, Effect, Exit, Schema } from "effect";
|
|
5
|
+
import type { HttpServerRequest } from "effect/http";
|
|
6
|
+
|
|
7
|
+
import { bootstrapFailure } from "./bootstrap.server.ts";
|
|
8
|
+
import type { ExecutionCapability } from "./execution.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Server-local diagnostics passed to a configured failure reporter after request
|
|
12
|
+
* cleanup. Causes can contain private input and application details; only the
|
|
13
|
+
* projected rejection is eligible for transport to the client.
|
|
14
|
+
*/
|
|
15
|
+
export interface FailureReport {
|
|
16
|
+
/** Original operation Cause, including any request-scope finalizer failures. */
|
|
17
|
+
readonly cause: Cause.Cause<unknown>;
|
|
18
|
+
/** The phase active when the operation failed. */
|
|
19
|
+
readonly phase: "input" | "execution";
|
|
20
|
+
/** Proposed client rejection: the approved encoded error or trusted fallback. */
|
|
21
|
+
readonly failure: unknown;
|
|
22
|
+
/** An additional fault from inspecting or encoding the original failure. */
|
|
23
|
+
readonly processingCause?: Cause.Cause<unknown> | undefined;
|
|
24
|
+
/** TanStack's identity and source metadata for the current server function. */
|
|
25
|
+
readonly serverFnMeta: ServerFnMeta;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Application-owned execution and failure policy resolved for a handler call.
|
|
30
|
+
*
|
|
31
|
+
* The adapter owns the request Scope; the application owns the runtime and its
|
|
32
|
+
* disposal. Encoding and reporting run after request cleanup, with application
|
|
33
|
+
* services and HTTP request metadata still available.
|
|
34
|
+
*
|
|
35
|
+
* @template R - Services supplied by the application's runner.
|
|
36
|
+
* @template RuntimeError - Failures from acquiring the application's runtime.
|
|
37
|
+
* @template P - Public rejection record produced by the error encoder.
|
|
38
|
+
*/
|
|
39
|
+
export interface EffectConfiguration<R, RuntimeError, P extends object> {
|
|
40
|
+
/**
|
|
41
|
+
* Execute in the application environment and return the complete Exit.
|
|
42
|
+
* Use `Effect.runPromiseExit` for service-free work or a shared
|
|
43
|
+
* `ManagedRuntime`'s `runPromiseExit`. Custom runners must honor the supplied
|
|
44
|
+
* abort signal and await finalization before settling.
|
|
45
|
+
*/
|
|
46
|
+
readonly runPromiseExit: <A, E>(
|
|
47
|
+
program: Effect.Effect<A, E, R>,
|
|
48
|
+
options?: Effect.RunOptions
|
|
49
|
+
) => Promise<Exit.Exit<A, E | RuntimeError>>;
|
|
50
|
+
/**
|
|
51
|
+
* Approve and encode a single typed business failure for the client.
|
|
52
|
+
* Undeclared properties are stripped. Unapproved failures, defects, mixed
|
|
53
|
+
* Causes, and encoding failures use `fallbackFailure` instead.
|
|
54
|
+
*/
|
|
55
|
+
readonly publicError?: Schema.ConstraintEncoder<
|
|
56
|
+
// Built-in wire values avoid a recursive Register.config dependency for global middleware.
|
|
57
|
+
ValidateSerializableInput<unknown, P>,
|
|
58
|
+
NoInfer<R> | HttpServerRequest.HttpServerRequest
|
|
59
|
+
>;
|
|
60
|
+
/**
|
|
61
|
+
* Replace native `ErrorReporter.report` with an awaited Effect.
|
|
62
|
+
* Control flow, ordinary invalid input, and pure interruption skip reporting.
|
|
63
|
+
* Faults or timeouts in this callback leave the business rejection intact and
|
|
64
|
+
* allow outer framework reporting. Compose native reporting here if desired.
|
|
65
|
+
*/
|
|
66
|
+
readonly reportFailure?: (
|
|
67
|
+
report: FailureReport
|
|
68
|
+
) => Effect.Effect<
|
|
69
|
+
void,
|
|
70
|
+
unknown,
|
|
71
|
+
NoInfer<R> | HttpServerRequest.HttpServerRequest
|
|
72
|
+
>;
|
|
73
|
+
/**
|
|
74
|
+
* Timeout for the `reportFailure` override only.
|
|
75
|
+
*
|
|
76
|
+
* @default "5 seconds"
|
|
77
|
+
*/
|
|
78
|
+
readonly reportTimeout?: Duration.Input;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Configure the eager safe fallback and lazy application execution policy.
|
|
83
|
+
*
|
|
84
|
+
* @template R - Services supplied by the resolved runner.
|
|
85
|
+
* @template RuntimeError - Failures from acquiring the application's runtime.
|
|
86
|
+
* @template P - Public rejection record produced by the error encoder.
|
|
87
|
+
* @template F - Trusted fallback record available before application loading.
|
|
88
|
+
*/
|
|
89
|
+
export interface EffectMiddlewareOptions<
|
|
90
|
+
R,
|
|
91
|
+
RuntimeError,
|
|
92
|
+
P extends object,
|
|
93
|
+
F extends object,
|
|
94
|
+
> {
|
|
95
|
+
/**
|
|
96
|
+
* Trusted, serializable rejection for failures that cannot be exposed.
|
|
97
|
+
* Keep it independent of lazy application loading and free of private details.
|
|
98
|
+
* A fresh copy receives server-local diagnostics for each rejection.
|
|
99
|
+
*/
|
|
100
|
+
readonly fallbackFailure: F & ValidateSerializableInput<unknown, F>;
|
|
101
|
+
/**
|
|
102
|
+
* Resolve configuration when the generated handler requests execution.
|
|
103
|
+
* Native middleware that rejects or replaces the handler does not invoke this
|
|
104
|
+
* callback. Reuse an application runtime rather than creating one per request.
|
|
105
|
+
*/
|
|
106
|
+
readonly resolve: () =>
|
|
107
|
+
| EffectConfiguration<R, RuntimeError, P>
|
|
108
|
+
| PromiseLike<EffectConfiguration<R, RuntimeError, P>>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const loadBoundary = createServerOnlyFn(() => import("./boundary.server.ts"));
|
|
112
|
+
|
|
113
|
+
const makeBootstrapFailure = createServerOnlyFn(bootstrapFailure);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create native Start middleware that supplies Effect execution to server functions.
|
|
117
|
+
*
|
|
118
|
+
* Install one owner before authorization and validation middleware. The Vite
|
|
119
|
+
* plugin's generated handler uses it to decode input and execute business work
|
|
120
|
+
* in one request Scope, with cancellation tied to the actual HTTP request.
|
|
121
|
+
* Cleanup completes before native post-handler middleware resumes.
|
|
122
|
+
*
|
|
123
|
+
* @param options - Trusted fallback and lazy application configuration.
|
|
124
|
+
* @returns Function middleware for a configured `createEffectServerFn` builder
|
|
125
|
+
* or Start's global function-middleware registration.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { createServerOnlyFn } from "@tanstack/react-start";
|
|
130
|
+
* import { Effect } from "effect";
|
|
131
|
+
* import {
|
|
132
|
+
* createEffectMiddleware,
|
|
133
|
+
* createEffectServerFn as base,
|
|
134
|
+
* } from "@rc312/tss-effect-serverfn";
|
|
135
|
+
*
|
|
136
|
+
* export const createEffectServerFn = base().middleware([
|
|
137
|
+
* createEffectMiddleware({
|
|
138
|
+
* fallbackFailure: { code: "INTERNAL", message: "Something went wrong" },
|
|
139
|
+
* resolve: createServerOnlyFn(() => ({
|
|
140
|
+
* runPromiseExit: Effect.runPromiseExit,
|
|
141
|
+
* })),
|
|
142
|
+
* }),
|
|
143
|
+
* ]);
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export const createEffectMiddleware = <
|
|
147
|
+
R,
|
|
148
|
+
RuntimeError,
|
|
149
|
+
P extends object,
|
|
150
|
+
F extends object,
|
|
151
|
+
>(
|
|
152
|
+
options: EffectMiddlewareOptions<R, RuntimeError, P, F>
|
|
153
|
+
) =>
|
|
154
|
+
createMiddleware({ type: "function" }).server<ExecutionCapability<R>>(
|
|
155
|
+
async ({ next, context, method, serverFnMeta }) => {
|
|
156
|
+
let boundary: Awaited<ReturnType<typeof loadBoundary>>;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
boundary = await loadBoundary();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw makeBootstrapFailure<F>(options.fallbackFailure, error);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return boundary.execute(
|
|
165
|
+
options,
|
|
166
|
+
{ method, serverFnMeta },
|
|
167
|
+
context,
|
|
168
|
+
(execution) => next({ context: execution })
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
);
|
package/src/execution.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Effect, Scope } from "effect";
|
|
2
|
+
import type { HttpServerRequest } from "effect/http";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Native middleware-context key shared by the compiler bridge and runtime owner.
|
|
6
|
+
* This server-local capability must never be forwarded through `sendContext`.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const executionKey = "@rc312/tss-effect-serverfn/execution";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Services supplied by the adapter in addition to the application's services.
|
|
13
|
+
* The Scope covers decoding and business work. The HTTP request provides
|
|
14
|
+
* metadata; Start has already consumed the RPC body.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export type RequestServices = Scope.Scope | HttpServerRequest.HttpServerRequest;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Request-local execution contract used by generated handlers.
|
|
21
|
+
*
|
|
22
|
+
* Phase notifications occur inside the running Effect immediately before each
|
|
23
|
+
* stage. Calls to `run` are rejected after the enclosing native middleware ends.
|
|
24
|
+
*
|
|
25
|
+
* @template R - Services supplied by the application runtime.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export interface ExecutionCapability<R> {
|
|
29
|
+
readonly [executionKey]: {
|
|
30
|
+
/** Run scoped work and await cleanup before resolving or rejecting the Promise. */
|
|
31
|
+
readonly run: <A, E>(
|
|
32
|
+
program: Effect.Effect<A, E, R | RequestServices>
|
|
33
|
+
) => Promise<A>;
|
|
34
|
+
/** Classify subsequent failures as input-decoding failures. */
|
|
35
|
+
readonly beginInput: () => void;
|
|
36
|
+
/** Classify subsequent failures as handler-execution failures. */
|
|
37
|
+
readonly beginExecution: () => void;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/* oxlint-disable anti-slop/no-unknown-parameters -- This boundary classifies arbitrary thrown values; parsing them first would lose native control flow and error identity. */
|
|
2
|
+
import { setResponseStatus } from "@tanstack/react-start/server";
|
|
3
|
+
import { isNotFound, isRedirect } from "@tanstack/router-core";
|
|
4
|
+
import {
|
|
5
|
+
Cause,
|
|
6
|
+
Effect,
|
|
7
|
+
ErrorReporter,
|
|
8
|
+
Exit,
|
|
9
|
+
Option,
|
|
10
|
+
Predicate,
|
|
11
|
+
Schema,
|
|
12
|
+
} from "effect";
|
|
13
|
+
import type { HttpServerRequest } from "effect/http";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
EffectConfiguration,
|
|
17
|
+
FailureReport,
|
|
18
|
+
} from "./effect-middleware.ts";
|
|
19
|
+
import {
|
|
20
|
+
getServerFnFailureDisposition,
|
|
21
|
+
ownFailure,
|
|
22
|
+
} from "./provenance.server.ts";
|
|
23
|
+
|
|
24
|
+
const isControlFlow = (value: unknown) =>
|
|
25
|
+
value instanceof Response || isRedirect(value) || isNotFound(value);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract a typed failure only when it is the Cause's sole reason.
|
|
29
|
+
* Mixed Causes, defects, and interruption cannot qualify as a public business
|
|
30
|
+
* error. `Some(undefined)` still represents a real typed failure.
|
|
31
|
+
*/
|
|
32
|
+
const getSingleFailure = <E>(cause: Cause.Cause<E>): Option.Option<E> => {
|
|
33
|
+
const reason = cause.reasons.length === 1 ? cause.reasons[0] : undefined;
|
|
34
|
+
|
|
35
|
+
return reason && Cause.isFailReason(reason)
|
|
36
|
+
? Option.some(reason.error)
|
|
37
|
+
: Option.none();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isOwnedFailure = (value: unknown) =>
|
|
41
|
+
getServerFnFailureDisposition(value) !== undefined;
|
|
42
|
+
|
|
43
|
+
const suppressInterruption = <E>(cause: Cause.Cause<E>) => {
|
|
44
|
+
const rejection = Cause.squash(cause);
|
|
45
|
+
|
|
46
|
+
return Predicate.isObject(rejection)
|
|
47
|
+
? ownFailure(rejection, "suppress", cause)
|
|
48
|
+
: rejection;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reject a configuration or runtime failure without invoking application policy.
|
|
53
|
+
* Runtime faults are never approved by the business-error schema, even when
|
|
54
|
+
* they resemble a public error or native redirect.
|
|
55
|
+
*
|
|
56
|
+
* @param fallback - Trusted rejection record to copy for an unexpected failure.
|
|
57
|
+
* @param cause - Original failure retained in the server-local registry.
|
|
58
|
+
* @throws {unknown} The squashed interruption, or a fresh fallback with native reporting ownership.
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export const rejectRuntimeFailure = <F extends object, E>(
|
|
62
|
+
fallback: F,
|
|
63
|
+
cause: Cause.Cause<E>
|
|
64
|
+
): never => {
|
|
65
|
+
if (Cause.hasInterruptsOnly(cause)) {
|
|
66
|
+
throw suppressInterruption(cause);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw ownFailure({ ...fallback }, "native", cause);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle a rejection escaping the native middleware chain.
|
|
74
|
+
* Recognized control flow and already-owned rejections retain their identity.
|
|
75
|
+
* Other values, including objects whose inspection throws, receive the trusted
|
|
76
|
+
* fallback without loading the application runtime.
|
|
77
|
+
*
|
|
78
|
+
* @param fallback - Trusted rejection record for unrecognized failures.
|
|
79
|
+
* @param error - Value thrown by native middleware or the generated handler.
|
|
80
|
+
* @throws {unknown} The recognized rejection or a registered fallback record.
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export const rejectMiddlewareFailure = <F extends object>(
|
|
84
|
+
fallback: F,
|
|
85
|
+
error: unknown
|
|
86
|
+
): never => {
|
|
87
|
+
let passthrough = false;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
passthrough = isControlFlow(error) || isOwnedFailure(error);
|
|
91
|
+
} catch {
|
|
92
|
+
// Inspection faults remain private.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (passthrough) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return rejectRuntimeFailure(fallback, Cause.fail(error));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Await the selected reporter and determine who owns further reporting.
|
|
104
|
+
* An explicit override replaces native reporting and its ignore policy. The
|
|
105
|
+
* override is interruptible and timeout-bounded even during masked rejection
|
|
106
|
+
* processing. Reporter failures leave ownership with the outer framework.
|
|
107
|
+
*
|
|
108
|
+
* @returns An Effect yielding `boundary` after handling, `suppress` for a native
|
|
109
|
+
* ignore annotation, or `native` when outer framework reporting is still needed.
|
|
110
|
+
*/
|
|
111
|
+
const reportFailure = Effect.fnUntraced(function* reportFailure<
|
|
112
|
+
R,
|
|
113
|
+
RuntimeError,
|
|
114
|
+
P extends object,
|
|
115
|
+
>(
|
|
116
|
+
report: FailureReport,
|
|
117
|
+
configuration: EffectConfiguration<R, RuntimeError, P>,
|
|
118
|
+
ignoredByNativeReporter: boolean
|
|
119
|
+
): Effect.fn.Return<
|
|
120
|
+
"native" | "boundary" | "suppress",
|
|
121
|
+
never,
|
|
122
|
+
R | HttpServerRequest.HttpServerRequest
|
|
123
|
+
> {
|
|
124
|
+
const { reportFailure: override, reportTimeout } = configuration;
|
|
125
|
+
|
|
126
|
+
if (override) {
|
|
127
|
+
const reportingExit = yield* Effect.exit(
|
|
128
|
+
Effect.suspend(() => override(report)).pipe(
|
|
129
|
+
Effect.interruptible,
|
|
130
|
+
Effect.timeout(reportTimeout ?? "5 seconds")
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return Exit.isSuccess(reportingExit) ? "boundary" : "native";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const reporters = yield* ErrorReporter.CurrentErrorReporters;
|
|
138
|
+
const reportingExit = yield* Effect.exit(ErrorReporter.report(report.cause));
|
|
139
|
+
|
|
140
|
+
if (ignoredByNativeReporter) {
|
|
141
|
+
return "suppress";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Exit.isSuccess(reportingExit) && reporters.size > 0
|
|
145
|
+
? "boundary"
|
|
146
|
+
: "native";
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Project a completed operation's Cause into a client rejection and report it.
|
|
151
|
+
*
|
|
152
|
+
* Pure interruption and native control flow pass through without reporting.
|
|
153
|
+
* Ordinary decoder failures become `INVALID_INPUT` with HTTP 400. Only a single
|
|
154
|
+
* execution failure can be approved by the public-error encoder; other failures
|
|
155
|
+
* use the trusted fallback. Diagnostics stay attached by server-local identity.
|
|
156
|
+
*
|
|
157
|
+
* The caller must close the operation Scope before running this Effect and
|
|
158
|
+
* contain any defect in projection itself. Encoder/reporter services come from
|
|
159
|
+
* the application runtime and HTTP request, not the closed operation Scope.
|
|
160
|
+
*
|
|
161
|
+
* @returns An Effect yielding the rejection value for the Promise boundary to
|
|
162
|
+
* throw. The original Cause is preserved separately from the client payload.
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
export const projectFailure = Effect.fnUntraced(function* projectFailure<
|
|
166
|
+
R,
|
|
167
|
+
RuntimeError,
|
|
168
|
+
P extends object,
|
|
169
|
+
F extends object,
|
|
170
|
+
>(
|
|
171
|
+
cause: Cause.Cause<unknown>,
|
|
172
|
+
phase: FailureReport["phase"],
|
|
173
|
+
configuration: EffectConfiguration<R, RuntimeError, P>,
|
|
174
|
+
fallback: F,
|
|
175
|
+
serverFnMeta: FailureReport["serverFnMeta"]
|
|
176
|
+
) {
|
|
177
|
+
if (Cause.hasInterruptsOnly(cause)) {
|
|
178
|
+
return suppressInterruption(cause);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let failure: FailureReport["failure"] = { ...fallback };
|
|
182
|
+
let processingCause: Cause.Cause<unknown> | undefined;
|
|
183
|
+
|
|
184
|
+
const inspectionExit = yield* Effect.exit(
|
|
185
|
+
Effect.sync(() => {
|
|
186
|
+
const single = getSingleFailure(cause);
|
|
187
|
+
|
|
188
|
+
return Option.isSome(single)
|
|
189
|
+
? {
|
|
190
|
+
ignored: ErrorReporter.isIgnored(single.value),
|
|
191
|
+
invalidInput:
|
|
192
|
+
phase === "input" && Schema.isSchemaError(single.value),
|
|
193
|
+
passthrough:
|
|
194
|
+
isControlFlow(single.value) || isOwnedFailure(single.value),
|
|
195
|
+
value: single.value,
|
|
196
|
+
}
|
|
197
|
+
: undefined;
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (Exit.isFailure(inspectionExit)) {
|
|
202
|
+
processingCause = inspectionExit.cause;
|
|
203
|
+
} else if (inspectionExit.value) {
|
|
204
|
+
const single = inspectionExit.value;
|
|
205
|
+
|
|
206
|
+
if (single.passthrough) {
|
|
207
|
+
return single.value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (single.invalidInput) {
|
|
211
|
+
yield* Effect.sync(() => setResponseStatus(400));
|
|
212
|
+
|
|
213
|
+
return ownFailure(
|
|
214
|
+
{ code: "INVALID_INPUT", message: "Invalid input" },
|
|
215
|
+
"suppress",
|
|
216
|
+
cause
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { publicError } = configuration;
|
|
221
|
+
|
|
222
|
+
if (phase === "execution" && publicError) {
|
|
223
|
+
const encodingExit = yield* Effect.exit(
|
|
224
|
+
Effect.suspend(() =>
|
|
225
|
+
Schema.encodeUnknownEffect(publicError, {
|
|
226
|
+
onExcessProperty: "ignore",
|
|
227
|
+
})(single.value)
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (Exit.isSuccess(encodingExit)) {
|
|
232
|
+
failure = encodingExit.value;
|
|
233
|
+
} else {
|
|
234
|
+
processingCause = encodingExit.cause;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const report: FailureReport = {
|
|
240
|
+
cause,
|
|
241
|
+
failure,
|
|
242
|
+
phase,
|
|
243
|
+
processingCause,
|
|
244
|
+
serverFnMeta,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const ignored =
|
|
248
|
+
Exit.isSuccess(inspectionExit) && inspectionExit.value?.ignored === true;
|
|
249
|
+
|
|
250
|
+
const disposition = yield* reportFailure(report, configuration, ignored);
|
|
251
|
+
|
|
252
|
+
if (Predicate.isObject(failure)) {
|
|
253
|
+
return ownFailure({ ...failure }, disposition, cause);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return ownFailure({ ...fallback }, "native", cause);
|
|
257
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type CreateEffectServerFn,
|
|
3
|
+
createEffectServerFn,
|
|
4
|
+
type EffectServerFnBuilder,
|
|
5
|
+
} from "./create-effect-server-fn.ts";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
type EffectConfiguration,
|
|
9
|
+
type EffectMiddlewareOptions,
|
|
10
|
+
createEffectMiddleware,
|
|
11
|
+
type FailureReport,
|
|
12
|
+
} from "./effect-middleware.ts";
|