@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
package/docs/usage.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Usage reference
|
|
2
|
+
|
|
3
|
+
Start with the [quick start](../README.md#quick-start). The options below build on its shared `src/effect-server-fn.ts` factory.
|
|
4
|
+
|
|
5
|
+
For `pipe`, `flow`, `Effect.fn`, do notation, service composition, and typed recovery, see the [composition examples](composition.md).
|
|
6
|
+
|
|
7
|
+
## Application services
|
|
8
|
+
|
|
9
|
+
Build one `ManagedRuntime` from your application's Layer. Here is a complete small example:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// src/runtime.server.ts
|
|
13
|
+
import { Context, Effect, Layer, ManagedRuntime } from "effect";
|
|
14
|
+
|
|
15
|
+
export class Messages extends Context.Service<
|
|
16
|
+
Messages,
|
|
17
|
+
{ readonly greet: (name: string) => Effect.Effect<string> }
|
|
18
|
+
>()("app/Messages") {}
|
|
19
|
+
|
|
20
|
+
export const runtime = ManagedRuntime.make(
|
|
21
|
+
Layer.succeed(Messages, {
|
|
22
|
+
greet: (name) => Effect.succeed(`Hello, ${name}!`),
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Replace the quick start's `resolve` option with:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
resolve: createServerOnlyFn(async () => {
|
|
31
|
+
const { runtime } = await import("./runtime.server");
|
|
32
|
+
return { runPromiseExit: runtime.runPromiseExit };
|
|
33
|
+
}),
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Your endpoints can now use those services directly:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// src/greeting.functions.ts
|
|
40
|
+
import { Effect, Schema } from "effect";
|
|
41
|
+
|
|
42
|
+
import { createEffectServerFn } from "./effect-server-fn";
|
|
43
|
+
import { Messages } from "./runtime.server";
|
|
44
|
+
|
|
45
|
+
export const getGreeting = createEffectServerFn({ method: "GET" })
|
|
46
|
+
.validator(Schema.Struct({ name: Schema.NonEmptyString }))
|
|
47
|
+
.handler(({ data }) =>
|
|
48
|
+
Effect.gen(function* greet() {
|
|
49
|
+
const messages = yield* Messages;
|
|
50
|
+
return { message: yield* messages.greet(data.name) };
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The compiler removes the handler and its private imports from browser output. Missing handler, decoder, encoder, or reporter services produce type errors.
|
|
56
|
+
|
|
57
|
+
Reuse the runtime across requests. Your application owns `runtime.dispose()` at shutdown and when replacing the runtime during HMR. The adapter owns each request's Scope.
|
|
58
|
+
|
|
59
|
+
For Vite HMR, add this to `runtime.server.ts`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
if (import.meta.hot) {
|
|
63
|
+
import.meta.hot.dispose(() => runtime.dispose());
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In production, await `runtime.dispose()` in your server host's shutdown hook after draining active requests. The shutdown hook depends on your deployment adapter.
|
|
68
|
+
|
|
69
|
+
## Start function utilities
|
|
70
|
+
|
|
71
|
+
All four of Start's public `create…Fn` utilities work with this integration. Import `createServerOnlyFn`, `createClientOnlyFn`, and `createIsomorphicFn` directly from `@tanstack/react-start`. Their generic signatures already preserve Effect success, error, and service types.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// src/environment.ts
|
|
75
|
+
import {
|
|
76
|
+
createClientOnlyFn,
|
|
77
|
+
createIsomorphicFn,
|
|
78
|
+
createServerOnlyFn,
|
|
79
|
+
} from "@tanstack/react-start";
|
|
80
|
+
import { Config, Effect } from "effect";
|
|
81
|
+
|
|
82
|
+
export const readRegion = createServerOnlyFn(() => Config.string("APP_REGION"));
|
|
83
|
+
|
|
84
|
+
export const readBrowserLanguage = createClientOnlyFn(() =>
|
|
85
|
+
Effect.sync(() => navigator.language)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const readLanguage = createIsomorphicFn()
|
|
89
|
+
.server(() => Effect.succeed("en"))
|
|
90
|
+
.client(readBrowserLanguage);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Set `APP_REGION` in your server environment, then compose the utilities in an endpoint using the configured factory:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/details.functions.ts
|
|
97
|
+
import { Effect } from "effect";
|
|
98
|
+
|
|
99
|
+
import { createEffectServerFn } from "./effect-server-fn";
|
|
100
|
+
import { readLanguage, readRegion } from "./environment";
|
|
101
|
+
|
|
102
|
+
export const getDetails = createEffectServerFn().handler(() =>
|
|
103
|
+
Effect.gen(function* details() {
|
|
104
|
+
return {
|
|
105
|
+
region: yield* readRegion(),
|
|
106
|
+
language: yield* readLanguage(),
|
|
107
|
+
};
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Start selects the implementation at build time and removes the other environment's implementation and unused imports. Both `.server(...).client(...)` and `.client(...).server(...)` work.
|
|
113
|
+
|
|
114
|
+
### Execution rules
|
|
115
|
+
|
|
116
|
+
- **Effect RPC handlers:** `createEffectServerFn` runs the returned Effect in the configured runtime and request Scope. Utilities composed with `yield*` share that execution, cancellation, and cleanup.
|
|
117
|
+
- **Environment utilities:** calling a helper creates an Effect; it does not run it. Compose it with other Effects, or execute it with your application runtime at a loader/event-handler boundary. These helpers do not perform RPC or create a request Scope.
|
|
118
|
+
- **Wrong environment:** `createServerOnlyFn` and `createClientOnlyFn` throw synchronously when called on the wrong side, before returning an Effect. Calling them inside `Effect.gen` turns that throw into a defect.
|
|
119
|
+
- **Missing isomorphic branch:** Start returns `undefined` in that environment. Supply both branches when callers will `yield*` the result; use `() => Effect.void` for an intentionally empty branch.
|
|
120
|
+
- **Types:** isomorphic implementations share argument types and retain a union of their return types, including Effect errors and required services. Supply the inferred services when executing the result.
|
|
121
|
+
- **Ordinary RPC handlers:** native `createServerFn` retains Start's normal value/Promise API. Use the configured `createEffectServerFn` when returning an Effect from an RPC handler.
|
|
122
|
+
|
|
123
|
+
## Middleware and request services
|
|
124
|
+
|
|
125
|
+
Add native middleware to the configured factory or individual endpoints:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
createEffectServerFn({ method: "POST" })
|
|
129
|
+
.middleware([authMiddleware])
|
|
130
|
+
.validator(Input)
|
|
131
|
+
.handler(({ data, context }) => program(data, context.user));
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Here `authMiddleware`, `Input`, and `program` are application code. Install `createEffectMiddleware` before auth and native validators so it can sanitize their unexpected failures. Provision request-specific Effect services explicitly with `Effect.provideService` in the handler.
|
|
135
|
+
|
|
136
|
+
Native global function middleware via `createStart` is also supported; register its config with TanStack's `Register` interface for context inference.
|
|
137
|
+
|
|
138
|
+
The adapter supplies:
|
|
139
|
+
|
|
140
|
+
- `Scope.Scope` for input decoding and business execution.
|
|
141
|
+
- `HttpServerRequest.HttpServerRequest` from `effect/http`, backed by the actual Start request. Use it for metadata; Start has already consumed the RPC body.
|
|
142
|
+
|
|
143
|
+
## Execution and cancellation
|
|
144
|
+
|
|
145
|
+
1. Native middleware runs in its usual order.
|
|
146
|
+
2. The endpoint Schema decodes once with `Schema.decodeUnknownEffect` in the application runtime.
|
|
147
|
+
3. The handler's Effect executes in that same request Scope.
|
|
148
|
+
4. Finalizers finish before native middleware unwinds. Post-handler middleware sees decoded data.
|
|
149
|
+
|
|
150
|
+
Async and serviceful schemas retain Effect's defect and interruption semantics. Native middleware validator exceptions receive the safe fallback; ordinary endpoint schema failures receive the fixed `INVALID_INPUT` response.
|
|
151
|
+
|
|
152
|
+
Request abort interrupts Effect work and waits for finalizers. Pass `Effect.tryPromise`'s signal to cancellation-aware foreign APIs. Arbitrary preparation/configuration Promises cannot be forcibly cancelled; an aborted request does not start subsequent Effect work.
|
|
153
|
+
|
|
154
|
+
Scope closes before the adapted handler Promise settles. Self-contained Responses work, but resources acquired in the handler cannot escape in a streaming body or deferred value. The adapter does not dispose your application runtime.
|
|
155
|
+
|
|
156
|
+
Native `method`, `strict`, FormData POST constraints, and success serialization apply. Use `Schema.encodeEffect` inside the handler when success values need transformation.
|
|
157
|
+
|
|
158
|
+
## Public errors
|
|
159
|
+
|
|
160
|
+
Without a `publicError` schema, business failures use `fallbackFailure`. To expose selected failures, define an ordinary native error:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// src/errors.ts
|
|
164
|
+
import { Schema } from "effect";
|
|
165
|
+
|
|
166
|
+
export class NameUnavailable extends Schema.TaggedError<NameUnavailable>()(
|
|
167
|
+
"NameUnavailable",
|
|
168
|
+
{ message: Schema.String }
|
|
169
|
+
) {}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Configure the shared factory's resolver:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
resolve: createServerOnlyFn(async () => {
|
|
176
|
+
const { runtime } = await import("./runtime.server");
|
|
177
|
+
const { NameUnavailable } = await import("./errors");
|
|
178
|
+
return {
|
|
179
|
+
runPromiseExit: runtime.runPromiseExit,
|
|
180
|
+
publicError: NameUnavailable,
|
|
181
|
+
};
|
|
182
|
+
}),
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Fail in application code with `yield* new NameUnavailable({ message: "Choose another name" })`. The Promise client rejects with the encoded record, containing `_tag` and `message`. For several error types, configure a native `Schema.Union`.
|
|
186
|
+
|
|
187
|
+
Only a single typed business failure accepted by the public schema is exposed. Encoding uses `Schema.encodeUnknownEffect`, supports application services, and strips undeclared fields. Every declared field is approved for disclosure. Keep `fallbackFailure` a trusted serializable record available before application loading.
|
|
188
|
+
|
|
189
|
+
Defects, mixed Causes, runtime/configuration failures, encoder faults, and cleanup defects use the fallback. Original Causes remain server-local. Public records use Start's built-in serializable types; custom registered serializers remain available for successful results.
|
|
190
|
+
|
|
191
|
+
A record's `statusCode` field does not set HTTP status. Use Start's native response utilities for that policy. For native control flow, return a successful `Response` or use `Effect.fail(redirect(...))`, `Effect.fail(notFound())`, or `Effect.fail(response)`. Defects containing these values remain defects.
|
|
192
|
+
|
|
193
|
+
## Reporting and observability
|
|
194
|
+
|
|
195
|
+
Use native log/span annotations, `Effect.fn`, and `Effect.withSpan`. The adapter creates one operation span that ends with the original scoped outcome. Cause annotations preserve trace information; annotations are not copied into a custom attribute accumulator.
|
|
196
|
+
|
|
197
|
+
The default reporting action is `ErrorReporter.report` in the application environment. Install reporters with native `ErrorReporter.layer`. Native identity deduplication, severity, attributes, and `ErrorReporter.ignore` apply. Public visibility does not imply reporting suppression. Control flow, ordinary invalid input, and pure interruption are excluded.
|
|
198
|
+
|
|
199
|
+
Optional fields returned by `resolve`:
|
|
200
|
+
|
|
201
|
+
| Option | Meaning |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `publicError` | Schema approving and encoding public business failures |
|
|
204
|
+
| `reportFailure(report)` | Effect-valued replacement for the default reporting action |
|
|
205
|
+
| `reportTimeout` | Native `Duration.Input`; defaults to `"5 seconds"` for the override |
|
|
206
|
+
|
|
207
|
+
An override is awaited after request-scope cleanup, with application services and request metadata. Its faults/timeouts cannot replace the business rejection. Use `Effect.tryPromise` and its signal for foreign reporters. Compose `ErrorReporter.report` explicitly if the override should also invoke native reporters. ErrorReporter and the HTTP request APIs are unstable in Effect 4.0.5.
|
|
208
|
+
|
|
209
|
+
For outer framework reporting, `@rc312/tss-effect-serverfn/server` exports:
|
|
210
|
+
|
|
211
|
+
- `getServerFnFailureCause(error)` — original server-local diagnostic Cause.
|
|
212
|
+
- `getServerFnFailureDisposition(error)` — `{ capture, handled }`, or `undefined` for unrelated errors. Outer capture hooks should skip handled errors. Ownership does not imply guaranteed delivery.
|
|
213
|
+
- `acceptServerFnErrorEvent(error)` — suppresses ignored failures and repeated event acceptance by rejection identity. Use in an event delivery hook, including delivery initiated by the boundary.
|
|
214
|
+
|
|
215
|
+
If no reporter is installed or reporting fails, ownership stays with the native framework. Unrelated errors on the same request remain reportable.
|
|
216
|
+
|
|
217
|
+
## Compiler support
|
|
218
|
+
|
|
219
|
+
The Vite plugin adapts Schemas and Effects; TanStack owns extraction, RPC IDs, manifests, and transport. Supported declarations use static method chains, const aliases, namespace imports, and transparent configured-factory exports/re-exports.
|
|
220
|
+
|
|
221
|
+
Export configured builders directly. Dynamic wrapper factories, computed method calls, and mutable factory bindings are unsupported. Put `.validator(schema)` on endpoints rather than using a validated builder as middleware. `/compiler-runtime` is an internal generated-code entry point.
|
|
222
|
+
|
|
223
|
+
Clients keep Start's native Promise API. Use `useServerFn` for component redirect/not-found handling. Remote rejections are not typed as the handler's Effect error channel.
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rc312/tss-effect-serverfn",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed Effect execution and safe failure boundaries for TanStack Start server functions.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/RyanCallahan312/tss-effect-serverfn.git",
|
|
9
|
+
"directory": "packages/tss-effect-serverfn"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"docs",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"./src/server.ts",
|
|
19
|
+
"./dist/server.js"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js",
|
|
23
|
+
"./compiler-runtime": "./dist/compiler-runtime.js",
|
|
24
|
+
"./server": "./dist/server.js",
|
|
25
|
+
"./vite": "./dist/vite.js",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org/"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -b tsconfig.json",
|
|
34
|
+
"dev": "tsc -b tsconfig.json --watch",
|
|
35
|
+
"typecheck": "tsc --noEmit -p tsconfig.json --tsBuildInfoFile .cache/typecheck.tsbuildinfo && tsc -p tsconfig.test.json && tsc -p tsconfig.fixtures.json",
|
|
36
|
+
"test": "vitest run && node --test test/*.test.mjs",
|
|
37
|
+
"prepack": "bun run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@babel/core": "^7.29.7",
|
|
41
|
+
"@babel/types": "^7.29.8",
|
|
42
|
+
"@standard-schema/spec": "^1.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@effect/vitest": "^4.0.5",
|
|
46
|
+
"@tanstack/react-router": "1.170.44",
|
|
47
|
+
"@tanstack/start-storage-context": "^1.167.39",
|
|
48
|
+
"@types/babel__core": "^7.20.5",
|
|
49
|
+
"@types/node": "^26.6.5",
|
|
50
|
+
"react": "19.3.1",
|
|
51
|
+
"react-dom": "19.3.1",
|
|
52
|
+
"vite": "^8.3.4",
|
|
53
|
+
"vitest": "^5.0.4"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@tanstack/react-start": "^1.169.2",
|
|
57
|
+
"@tanstack/router-core": "^1.171.37",
|
|
58
|
+
"effect": "^4.0.5",
|
|
59
|
+
"vite": "^8.3.4"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"vite": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22.12.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Cause } from "effect";
|
|
2
|
+
|
|
3
|
+
import { ownFailure } from "./provenance.server.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a safe rejection when the lazy execution boundary cannot be loaded.
|
|
7
|
+
* This eager path depends only on the provenance registry and never initializes
|
|
8
|
+
* application services or invokes an application reporter.
|
|
9
|
+
*
|
|
10
|
+
* @param template - Trusted fallback available before loading application code.
|
|
11
|
+
* @param cause - Original module-loading rejection, retained only on the server.
|
|
12
|
+
* @returns A fresh fallback record eligible for native framework reporting.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export const bootstrapFailure = <P extends object>(
|
|
16
|
+
template: Readonly<P>,
|
|
17
|
+
cause: unknown
|
|
18
|
+
): P => {
|
|
19
|
+
const failure = { ...template };
|
|
20
|
+
|
|
21
|
+
return ownFailure(failure, "native", Cause.fail(cause));
|
|
22
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { Method } from "@tanstack/react-start";
|
|
2
|
+
import { getRequest } from "@tanstack/react-start/server";
|
|
3
|
+
import { Cause, Effect, Exit, Predicate } from "effect";
|
|
4
|
+
import { HttpServerRequest } from "effect/http";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
EffectConfiguration,
|
|
8
|
+
EffectMiddlewareOptions,
|
|
9
|
+
FailureReport,
|
|
10
|
+
} from "./effect-middleware.ts";
|
|
11
|
+
import { executionKey } from "./execution.ts";
|
|
12
|
+
import type { ExecutionCapability } from "./execution.ts";
|
|
13
|
+
import {
|
|
14
|
+
projectFailure,
|
|
15
|
+
rejectMiddlewareFailure,
|
|
16
|
+
rejectRuntimeFailure,
|
|
17
|
+
} from "./failure.server.ts";
|
|
18
|
+
import { ownFailure } from "./provenance.server.ts";
|
|
19
|
+
|
|
20
|
+
interface Operation {
|
|
21
|
+
readonly method: Method;
|
|
22
|
+
readonly serverFnMeta: FailureReport["serverFnMeta"];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lend a request-local execution capability to the native middleware chain.
|
|
27
|
+
*
|
|
28
|
+
* Runtime configuration is resolved only when the generated handler calls `run`.
|
|
29
|
+
* Decoding and business work share a Scope and operation span; both close before
|
|
30
|
+
* failure projection and reporting. Request cancellation interrupts that work,
|
|
31
|
+
* while cleanup and rejection processing are awaited before returning to Start.
|
|
32
|
+
* The application's runtime remains application-owned.
|
|
33
|
+
*
|
|
34
|
+
* @param options - Execution configuration and trusted fallback rejection.
|
|
35
|
+
* @param operation - Native function metadata used for tracing and reporting.
|
|
36
|
+
* @param context - Incoming native context, checked for conflicting ownership.
|
|
37
|
+
* @param continueMiddleware - Continue the native chain with the new capability.
|
|
38
|
+
* @returns The native middleware result, or a rejection selected by the boundary.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export const execute = async <
|
|
42
|
+
R,
|
|
43
|
+
RuntimeError,
|
|
44
|
+
P extends object,
|
|
45
|
+
F extends object,
|
|
46
|
+
Output,
|
|
47
|
+
>(
|
|
48
|
+
options: EffectMiddlewareOptions<R, RuntimeError, P, F>,
|
|
49
|
+
operation: Operation,
|
|
50
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- This native framework context is inspected only for conflicting execution ownership.
|
|
51
|
+
context: unknown,
|
|
52
|
+
continueMiddleware: (context: ExecutionCapability<R>) => Promise<Output>
|
|
53
|
+
): Promise<Output> => {
|
|
54
|
+
const { fallbackFailure } = options;
|
|
55
|
+
let request: Request;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
request = getRequest();
|
|
59
|
+
|
|
60
|
+
if (Predicate.hasProperty(context, executionKey)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Multiple Effect middleware owners cannot execute one server function"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return rejectRuntimeFailure(fallbackFailure, Cause.fail(error));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let phase: FailureReport["phase"] = "execution";
|
|
70
|
+
let active = true;
|
|
71
|
+
|
|
72
|
+
const capability: ExecutionCapability<R> = {
|
|
73
|
+
[executionKey]: {
|
|
74
|
+
beginExecution: () => {
|
|
75
|
+
phase = "execution";
|
|
76
|
+
},
|
|
77
|
+
beginInput: () => {
|
|
78
|
+
phase = "input";
|
|
79
|
+
},
|
|
80
|
+
async run(program) {
|
|
81
|
+
if (!active) {
|
|
82
|
+
return rejectRuntimeFailure(
|
|
83
|
+
fallbackFailure,
|
|
84
|
+
Cause.die(new Error("Effect execution outlived its native handler"))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (request.signal.aborted) {
|
|
89
|
+
return rejectRuntimeFailure(fallbackFailure, Cause.interrupt());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Resolve lazily: native middleware can reject or replace the handler without acquiring the runtime.
|
|
93
|
+
let configuration: EffectConfiguration<R, RuntimeError, P>;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
configuration = await options.resolve();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return rejectRuntimeFailure(fallbackFailure, Cause.fail(error));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const scopedOperation = Effect.scoped(program).pipe(
|
|
102
|
+
Effect.withSpan(operation.serverFnMeta.name, {
|
|
103
|
+
attributes: {
|
|
104
|
+
"rpc.boundary": "server-function",
|
|
105
|
+
"rpc.method": operation.method,
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const supervisedOperation = Effect.uninterruptibleMask((restore) =>
|
|
111
|
+
Effect.gen(function* superviseOperation() {
|
|
112
|
+
// Cancellation interrupts the operation; cleanup and failure projection are awaited.
|
|
113
|
+
const operationExit = yield* Effect.exit(restore(scopedOperation));
|
|
114
|
+
|
|
115
|
+
if (Exit.isSuccess(operationExit)) {
|
|
116
|
+
return operationExit;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const projectionExit = yield* Effect.exit(
|
|
120
|
+
projectFailure(
|
|
121
|
+
operationExit.cause,
|
|
122
|
+
phase,
|
|
123
|
+
configuration,
|
|
124
|
+
fallbackFailure,
|
|
125
|
+
operation.serverFnMeta
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const rejection = Exit.isSuccess(projectionExit)
|
|
130
|
+
? projectionExit.value
|
|
131
|
+
: ownFailure(
|
|
132
|
+
{ ...fallbackFailure },
|
|
133
|
+
"native",
|
|
134
|
+
operationExit.cause
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return Exit.fail(rejection);
|
|
138
|
+
})
|
|
139
|
+
).pipe(
|
|
140
|
+
Effect.provideService(
|
|
141
|
+
HttpServerRequest.HttpServerRequest,
|
|
142
|
+
HttpServerRequest.fromWeb(request)
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// The outer Exit represents runtime acquisition/execution faults; the inner Exit is the projected operation result.
|
|
147
|
+
let runtimeExit: Exit.Exit<
|
|
148
|
+
Effect.Success<typeof supervisedOperation>,
|
|
149
|
+
RuntimeError
|
|
150
|
+
>;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
runtimeExit = await configuration.runPromiseExit(
|
|
154
|
+
supervisedOperation,
|
|
155
|
+
{ signal: request.signal }
|
|
156
|
+
);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return rejectRuntimeFailure(fallbackFailure, Cause.die(error));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Exit.isFailure(runtimeExit)) {
|
|
162
|
+
return rejectRuntimeFailure(fallbackFailure, runtimeExit.cause);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const operationExit = runtimeExit.value;
|
|
166
|
+
|
|
167
|
+
if (Exit.isFailure(operationExit)) {
|
|
168
|
+
throw Cause.squash(operationExit.cause);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return operationExit.value;
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
if (request.signal.aborted) {
|
|
178
|
+
return rejectRuntimeFailure(fallbackFailure, Cause.interrupt());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const output = await continueMiddleware(capability);
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
Predicate.hasProperty(output, "result") &&
|
|
185
|
+
Effect.isEffect(output.result)
|
|
186
|
+
) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
"Missing effectServerFns() Vite plugin: the handler returned an unexecuted Effect"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return output;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return rejectMiddlewareFailure(fallbackFailure, error);
|
|
195
|
+
} finally {
|
|
196
|
+
active = false;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Effect, Predicate, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import { executionKey } from "./execution.ts";
|
|
4
|
+
import type { ExecutionCapability } from "./execution.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Nominal token carrying input decoding across native middleware validation.
|
|
8
|
+
* The class identity prevents arbitrary user data from impersonating a decoder.
|
|
9
|
+
*/
|
|
10
|
+
class PendingInput {
|
|
11
|
+
readonly decode: Effect.Effect<unknown, Schema.SchemaError, unknown>;
|
|
12
|
+
|
|
13
|
+
constructor(decode: Effect.Effect<unknown, Schema.SchemaError, unknown>) {
|
|
14
|
+
this.decode = decode;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Adapt a raw Effect Schema into a deferred validator for generated Start code.
|
|
20
|
+
*
|
|
21
|
+
* Constructing the validator or applying it to input does not run decoding.
|
|
22
|
+
* {@link effectHandler} executes the stored decoder in the same runtime and
|
|
23
|
+
* Scope as business work, so service requirements and finalizers are preserved.
|
|
24
|
+
*
|
|
25
|
+
* @param schema - Endpoint Schema evaluated at the original declaration site.
|
|
26
|
+
* @returns A native validator that wraps raw input in a pending decoder.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export const prepareInput =
|
|
30
|
+
<S extends Schema.Constraint>(schema: S) =>
|
|
31
|
+
<A>(value: A) =>
|
|
32
|
+
new PendingInput(
|
|
33
|
+
Effect.suspend(() => Schema.decodeUnknownEffect(schema)(value))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adapt an Effect-returning callback into Start's Promise-based provider handler.
|
|
38
|
+
*
|
|
39
|
+
* Pending input is decoded first and written back to the native context. Phase
|
|
40
|
+
* transitions let the boundary distinguish input errors from business failures.
|
|
41
|
+
* Callback construction is suspended so synchronous throws become defects.
|
|
42
|
+
*
|
|
43
|
+
* @param handler - The original application handler retained by compiler lowering.
|
|
44
|
+
* @returns A provider callback that delegates execution to the request capability.
|
|
45
|
+
* @throws {Error} If execution middleware has not supplied a valid capability.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export const effectHandler =
|
|
49
|
+
<C extends { data: unknown; context: ExecutionCapability<never> }, A, E, R>(
|
|
50
|
+
handler: (context: C) => Effect.Effect<A, E, R>
|
|
51
|
+
) =>
|
|
52
|
+
(context: C): Promise<A> => {
|
|
53
|
+
const execution = context.context[executionKey];
|
|
54
|
+
|
|
55
|
+
if (!execution || !Predicate.isFunction(execution.run)) {
|
|
56
|
+
throw new Error("Effect server functions require createEffectMiddleware");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const program = Effect.gen(function* executeProgram() {
|
|
60
|
+
if (context.data instanceof PendingInput) {
|
|
61
|
+
execution.beginInput();
|
|
62
|
+
// Update the native context so post-handler middleware also sees decoded data.
|
|
63
|
+
context.data = yield* context.data.decode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
execution.beginExecution();
|
|
67
|
+
|
|
68
|
+
return yield* Effect.suspend(() => handler(context));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// SAFETY: The configured builder checks handler and decoder services; lowering erases that shared environment at this single execution edge.
|
|
72
|
+
return execution.run(
|
|
73
|
+
program as Effect.Effect<A, E | Schema.SchemaError, never>
|
|
74
|
+
);
|
|
75
|
+
};
|