@rc312/tss-effect-serverfn 0.0.0-stage → 0.2.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -2
  3. package/dist/bootstrap.server.d.ts +12 -0
  4. package/dist/bootstrap.server.d.ts.map +1 -0
  5. package/dist/bootstrap.server.js +17 -0
  6. package/dist/bootstrap.server.js.map +1 -0
  7. package/dist/boundary.server.d.ts +26 -0
  8. package/dist/boundary.server.d.ts.map +1 -0
  9. package/dist/boundary.server.js +117 -0
  10. package/dist/boundary.server.js.map +1 -0
  11. package/dist/compiler-runtime.d.ts +40 -0
  12. package/dist/compiler-runtime.d.ts.map +1 -0
  13. package/dist/compiler-runtime.js +54 -0
  14. package/dist/compiler-runtime.js.map +1 -0
  15. package/dist/create-effect-server-fn.d.ts +84 -0
  16. package/dist/create-effect-server-fn.d.ts.map +1 -0
  17. package/dist/create-effect-server-fn.js +27 -0
  18. package/dist/create-effect-server-fn.js.map +1 -0
  19. package/dist/effect-middleware.d.ts +116 -0
  20. package/dist/effect-middleware.d.ts.map +1 -0
  21. package/dist/effect-middleware.js +46 -0
  22. package/dist/effect-middleware.js.map +1 -0
  23. package/dist/execution.d.ts +35 -0
  24. package/dist/execution.d.ts.map +1 -0
  25. package/dist/execution.js +7 -0
  26. package/dist/execution.js.map +1 -0
  27. package/dist/failure.server.d.ts +44 -0
  28. package/dist/failure.server.d.ts.map +1 -0
  29. package/dist/failure.server.js +161 -0
  30. package/dist/failure.server.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/provenance.server.d.ts +54 -0
  36. package/dist/provenance.server.d.ts.map +1 -0
  37. package/dist/provenance.server.js +87 -0
  38. package/dist/provenance.server.js.map +1 -0
  39. package/dist/server.d.ts +3 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +3 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/vite.d.ts +24 -0
  44. package/dist/vite.d.ts.map +1 -0
  45. package/dist/vite.js +296 -0
  46. package/dist/vite.js.map +1 -0
  47. package/docs/composition.md +188 -0
  48. package/docs/development.md +48 -0
  49. package/docs/usage.md +223 -0
  50. package/package.json +69 -4
  51. package/src/bootstrap.server.ts +22 -0
  52. package/src/boundary.server.ts +198 -0
  53. package/src/compiler-runtime.ts +75 -0
  54. package/src/create-effect-server-fn.ts +197 -0
  55. package/src/effect-middleware.ts +171 -0
  56. package/src/execution.ts +39 -0
  57. package/src/failure.server.ts +257 -0
  58. package/src/index.ts +12 -0
  59. package/src/provenance.server.ts +125 -0
  60. package/src/server.ts +7 -0
  61. 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
+ );
@@ -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";