effect-orpc 1.0.0-effect-v4.5 → 1.0.0-effect-v4.7
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 +240 -141
- package/dist/{chunk-I5EWBI42.js → chunk-SXPXGZUW.js} +6 -2
- package/dist/chunk-SXPXGZUW.js.map +1 -0
- package/dist/index.js +1050 -283
- package/dist/index.js.map +1 -1
- package/dist/node.js +3 -2
- package/dist/node.js.map +1 -1
- package/package.json +3 -3
- package/src/contract.ts +46 -11
- package/src/effect-builder.ts +341 -53
- package/src/effect-enhance-router.ts +3 -3
- package/src/effect-procedure.ts +232 -14
- package/src/effect-runtime.ts +980 -24
- package/src/extension/create-node-proxy.ts +17 -1
- package/src/extension/state.ts +16 -20
- package/src/index.ts +1 -0
- package/src/node.ts +1 -0
- package/src/runtime-source.ts +107 -0
- package/src/service-context-bridge.ts +11 -0
- package/src/tagged-error.ts +2 -11
- package/src/tests/contract.test.ts +21 -0
- package/src/tests/effect-builder.proxy.test.ts +15 -17
- package/src/tests/effect-builder.test.ts +843 -193
- package/src/tests/effect-callback-shapes.test.ts +411 -0
- package/src/tests/effect-error-map.test.ts +12 -14
- package/src/tests/effect-procedure.test.ts +53 -11
- package/src/tests/node-side-effect.test.ts +80 -0
- package/src/tests/parity-shared.ts +2 -2
- package/src/tests/parity.effect-builder.test.ts +10 -3
- package/src/tests/parity.effect-procedure.test.ts +24 -8
- package/src/tests/shared.ts +1 -25
- package/src/types/effect-builder-surface.ts +117 -1
- package/src/types/effect-procedure-surface.ts +98 -1
- package/src/types/index.ts +325 -8
- package/src/types/variants.ts +352 -19
- package/dist/chunk-I5EWBI42.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Inspired by [effect-trpc](https://github.com/mikearnaldi/effect-trpc).
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Effect-native procedures** - Write oRPC procedures using generators with `yield*` syntax
|
|
10
|
-
- **Type-safe service injection** -
|
|
10
|
+
- **Type-safe service injection** - Add base services with `.provide(layer)` or pass a `Layer` / `ManagedRuntime<R>` directly
|
|
11
11
|
- **Tagged errors** - Create Effect-native error classes with `ORPCTaggedError` that integrate with oRPC's error handling
|
|
12
12
|
- **Full oRPC compatibility** - Mix Effect procedures with standard oRPC procedures in the same router
|
|
13
13
|
- **Telemetry support with automatic tracing** - Procedures are automatically traced with OpenTelemetry-compatible spans. Customize span names with `.traced()`.
|
|
@@ -29,8 +29,8 @@ Runnable demos live in the repository's `examples/` directory.
|
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
31
|
import { os } from "@orpc/server";
|
|
32
|
-
import { Effect,
|
|
33
|
-
import { makeEffectORPC, ORPCTaggedError } from "effect-orpc";
|
|
32
|
+
import { Effect, ManagedRuntime } from "effect";
|
|
33
|
+
import { eos, makeEffectORPC, ORPCTaggedError } from "effect-orpc";
|
|
34
34
|
|
|
35
35
|
interface User {
|
|
36
36
|
id: number;
|
|
@@ -43,46 +43,41 @@ let users: User[] = [
|
|
|
43
43
|
{ id: 3, name: "James Dane" },
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
-
// Authenticated os with initial context & errors set
|
|
47
|
-
const authedOs = os
|
|
48
|
-
.errors({ UNAUTHORIZED: { status: 401 } })
|
|
49
|
-
.$context<{ userId?: number }>()
|
|
50
|
-
.use(({ context, errors, next }) => {
|
|
51
|
-
if (context.userId === undefined) throw errors.UNAUTHORIZED();
|
|
52
|
-
return next({ context: { ...context, userId: context.userId } });
|
|
53
|
-
});
|
|
54
|
-
|
|
55
46
|
// Define your services
|
|
56
|
-
class UsersRepo extends
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
readonly get: (id: number) => User | undefined;
|
|
60
|
-
}
|
|
61
|
-
>()("UsersRepo") {
|
|
62
|
-
static readonly layer = Layer.succeed(this, {
|
|
47
|
+
class UsersRepo extends Effect.Service<UsersRepo>()("UsersRepo", {
|
|
48
|
+
accessors: true,
|
|
49
|
+
sync: () => ({
|
|
63
50
|
get: (id: number) => users.find((u) => u.id === id),
|
|
64
|
-
})
|
|
65
|
-
}
|
|
51
|
+
}),
|
|
52
|
+
}) {}
|
|
66
53
|
|
|
67
54
|
// Special yieldable oRPC error class
|
|
68
55
|
class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
|
|
69
56
|
status: 404,
|
|
70
57
|
}) {}
|
|
71
58
|
|
|
72
|
-
// Create
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
UserNotFoundError
|
|
77
|
-
})
|
|
59
|
+
// Create an Effect-aware oRPC builder with your service layer, context, errors,
|
|
60
|
+
// and middleware.
|
|
61
|
+
const effectProcedure = eos
|
|
62
|
+
.provide(UsersRepo.Default)
|
|
63
|
+
.errors({ UNAUTHORIZED: { status: 401 }, UserNotFoundError })
|
|
64
|
+
.$context<{ userId?: number }>()
|
|
65
|
+
.use(({ context, errors, next }) => {
|
|
66
|
+
if (context.userId === undefined) throw errors.UNAUTHORIZED();
|
|
67
|
+
return next({ context: { ...context, userId: context.userId } });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Use ManagedRuntime only when scoped resources should be acquired once and
|
|
71
|
+
// released on shutdown, for example a shared cache, database pool, or telemetry SDK:
|
|
72
|
+
// const runtime = ManagedRuntime.make(UsersRepo.Default);
|
|
73
|
+
// const effectProcedure = makeEffectORPC(runtime).errors({ UserNotFoundError });
|
|
78
74
|
|
|
79
75
|
// Create the router with mixed procedures
|
|
80
76
|
export const router = {
|
|
81
77
|
health: os.handler(() => "ok"),
|
|
82
78
|
users: {
|
|
83
|
-
me:
|
|
84
|
-
const
|
|
85
|
-
const user = usersRepo.get(userId);
|
|
79
|
+
me: effectProcedure.effect(function* ({ context: { userId } }) {
|
|
80
|
+
const user = yield* UsersRepo.get(userId);
|
|
86
81
|
if (!user) {
|
|
87
82
|
return yield* new UserNotFoundError();
|
|
88
83
|
}
|
|
@@ -96,42 +91,36 @@ export type Router = typeof router;
|
|
|
96
91
|
|
|
97
92
|
## Type Safety
|
|
98
93
|
|
|
99
|
-
The wrapper enforces that Effect procedures only use services provided by
|
|
94
|
+
The wrapper enforces that Effect procedures only use services provided by `.provide(layer)`, request-scoped `.provide(tag, provider)` calls, or an initial `Layer` / `ManagedRuntime`. If you try to use a service that isn't available, you'll get a compile-time error:
|
|
100
95
|
|
|
101
96
|
```ts
|
|
102
|
-
import { Effect, Layer
|
|
103
|
-
import {
|
|
97
|
+
import { Context, Effect, Layer } from "effect";
|
|
98
|
+
import { eos } from "effect-orpc";
|
|
104
99
|
|
|
105
100
|
class ProvidedService extends Context.Service<
|
|
106
101
|
ProvidedService,
|
|
107
|
-
{
|
|
108
|
-
readonly doSomething: () => Effect.Effect<string>;
|
|
109
|
-
}
|
|
102
|
+
{ doSomething: () => Effect.Effect<string> }
|
|
110
103
|
>()("ProvidedService") {}
|
|
111
104
|
|
|
112
105
|
class MissingService extends Context.Service<
|
|
113
106
|
MissingService,
|
|
114
|
-
{
|
|
115
|
-
readonly doSomething: () => Effect.Effect<string>;
|
|
116
|
-
}
|
|
107
|
+
{ doSomething: () => Effect.Effect<string> }
|
|
117
108
|
>()("MissingService") {}
|
|
118
109
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}),
|
|
123
|
-
);
|
|
110
|
+
const AppLive = Layer.succeed(ProvidedService, {
|
|
111
|
+
doSomething: () => Effect.succeed("ok"),
|
|
112
|
+
});
|
|
124
113
|
|
|
125
|
-
const
|
|
114
|
+
const effectProcedure = eos.provide(AppLive);
|
|
126
115
|
|
|
127
|
-
// ✅ This compiles - ProvidedService is
|
|
128
|
-
const works =
|
|
116
|
+
// ✅ This compiles - ProvidedService is provided by AppLive
|
|
117
|
+
const works = effectProcedure.effect(function* () {
|
|
129
118
|
const service = yield* ProvidedService;
|
|
130
119
|
return yield* service.doSomething();
|
|
131
120
|
});
|
|
132
121
|
|
|
133
|
-
// ❌ This fails to compile - MissingService is not
|
|
134
|
-
const fails =
|
|
122
|
+
// ❌ This fails to compile - MissingService is not provided
|
|
123
|
+
const fails = effectProcedure.effect(function* () {
|
|
135
124
|
const service = yield* MissingService; // Type error!
|
|
136
125
|
return yield* service.doSomething();
|
|
137
126
|
});
|
|
@@ -148,7 +137,7 @@ const fails = effectOs.effect(function* () {
|
|
|
148
137
|
Make sure the tagged error class is passed to the effect `.errors()` to be able to yield the error class directly and make the client recognize it as defined.
|
|
149
138
|
|
|
150
139
|
```ts
|
|
151
|
-
const getUser =
|
|
140
|
+
const getUser = effectProcedure
|
|
152
141
|
// Mixed error maps
|
|
153
142
|
.errors({
|
|
154
143
|
// Regular oRPC error
|
|
@@ -217,19 +206,19 @@ All Effect procedures are automatically traced with `Effect.withSpan`. By defaul
|
|
|
217
206
|
const router = {
|
|
218
207
|
users: {
|
|
219
208
|
// Span name: "users.get"
|
|
220
|
-
get:
|
|
209
|
+
get: effectProcedure.input(z.object({ id: z.string() })).effect(function* ({
|
|
221
210
|
input,
|
|
222
211
|
}) {
|
|
223
212
|
const userService = yield* UserService;
|
|
224
213
|
return yield* userService.findById(input.id);
|
|
225
214
|
}),
|
|
226
215
|
// Span name: "users.create"
|
|
227
|
-
create:
|
|
228
|
-
input
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
216
|
+
create: effectProcedure
|
|
217
|
+
.input(z.object({ name: z.string() }))
|
|
218
|
+
.effect(function* ({ input }) {
|
|
219
|
+
const userService = yield* UserService;
|
|
220
|
+
return yield* userService.create(input.name);
|
|
221
|
+
}),
|
|
233
222
|
},
|
|
234
223
|
};
|
|
235
224
|
```
|
|
@@ -237,7 +226,7 @@ const router = {
|
|
|
237
226
|
Use `.traced()` to override the default span name:
|
|
238
227
|
|
|
239
228
|
```ts
|
|
240
|
-
const getUser =
|
|
229
|
+
const getUser = effectProcedure
|
|
241
230
|
.input(z.object({ id: z.string() }))
|
|
242
231
|
.traced("custom.span.name") // Override the default path-based name
|
|
243
232
|
.effect(function* ({ input }) {
|
|
@@ -248,7 +237,7 @@ const getUser = effectOs
|
|
|
248
237
|
|
|
249
238
|
### Enabling OpenTelemetry
|
|
250
239
|
|
|
251
|
-
To enable tracing, include the OpenTelemetry layer in your
|
|
240
|
+
To enable tracing, include the OpenTelemetry layer in your application layer:
|
|
252
241
|
|
|
253
242
|
```ts
|
|
254
243
|
import { NodeSdk } from "@effect/opentelemetry";
|
|
@@ -264,8 +253,7 @@ const TracingLive = NodeSdk.layer(
|
|
|
264
253
|
|
|
265
254
|
const AppLive = Layer.mergeAll(UserServiceLive, TracingLive);
|
|
266
255
|
|
|
267
|
-
const
|
|
268
|
-
const effectOs = makeEffectORPC(runtime);
|
|
256
|
+
const effectProcedure = eos.provide(AppLive);
|
|
269
257
|
```
|
|
270
258
|
|
|
271
259
|
### Error Stack Traces
|
|
@@ -278,24 +266,142 @@ MyCustomError: Something went wrong
|
|
|
278
266
|
at users.getById (/app/src/procedures.ts:41:35)
|
|
279
267
|
```
|
|
280
268
|
|
|
281
|
-
##
|
|
269
|
+
## Effect middleware
|
|
270
|
+
|
|
271
|
+
`.use(...)` accepts generator-based and Effect-returning middleware in addition
|
|
272
|
+
to native oRPC middleware. Two patterns are supported:
|
|
273
|
+
|
|
274
|
+
**Gate** — run auth or validation side effects, then let the pipeline continue
|
|
275
|
+
automatically (no need to call `next`):
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
effectProcedure.use(function* () {
|
|
279
|
+
const user = yield* CurrentUser;
|
|
280
|
+
yield* requireActiveUser(user);
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Wrap** — call downstream explicitly and return the result. When porting oRPC
|
|
285
|
+
middleware that uses `return next(...)`, use `return yield* next(...)`:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
effectProcedure.use(function* ({ next }) {
|
|
289
|
+
const user = yield* CurrentUser;
|
|
290
|
+
yield* requireActiveUser(user);
|
|
291
|
+
|
|
292
|
+
return yield* next({
|
|
293
|
+
context: { userId: user.id },
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
To transform the downstream output, capture `next()` and pass through `output`:
|
|
282
299
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
300
|
+
```ts
|
|
301
|
+
effectProcedure.use(function* ({ next }, _input, output) {
|
|
302
|
+
const result = yield* next();
|
|
303
|
+
return yield* output(`${result.output}-wrapped`);
|
|
304
|
+
});
|
|
305
|
+
```
|
|
286
306
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
`
|
|
307
|
+
Calling `yield* next()` without returning its result still runs the handler once, but prefer `return yield* next(...)` so the pipeline receives your middleware result explicitly.
|
|
308
|
+
|
|
309
|
+
Effect-returning middleware is also supported, including `Effect.fn(...)` and
|
|
310
|
+
`() => Effect.gen(...)`:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
effectProcedure.use(
|
|
314
|
+
Effect.fn("middleware.auth")(function* ({ next }) {
|
|
315
|
+
const user = yield* CurrentUser;
|
|
316
|
+
return yield* next({ context: { userId: user.id } });
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Request-scoped providers support the same generator or Effect-returning style:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
effectProcedure.provide(CurrentUser, function* ({ context }) {
|
|
325
|
+
yield* Effect.logDebug("resolving current user");
|
|
326
|
+
return context.user;
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Runtime boundaries and fiber context continuity
|
|
331
|
+
|
|
332
|
+
`effect-orpc` batches contiguous Effect-native steps into one runtime boundary. Effect-native steps are `.provide(...)`, `.provideOptional(...)`, generator `.use(function* ...)`, and `.effect(function* ...)`. Effect-returning handlers, providers, and middleware are supported too; named `Effect.fn(...)` callbacks keep their own spans rather than being wrapped as generators.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
eos
|
|
336
|
+
.provide(AppLive)
|
|
337
|
+
.provide(CurrentUser, function* ({ context }) {
|
|
338
|
+
return context.user;
|
|
339
|
+
})
|
|
340
|
+
.use(function* ({ next }) {
|
|
341
|
+
const user = yield* CurrentUser;
|
|
342
|
+
return yield* next({ context: { userId: user.id } });
|
|
343
|
+
})
|
|
344
|
+
.effect(function* ({ context }) {
|
|
345
|
+
const user = yield* CurrentUser;
|
|
346
|
+
return `${context.userId}:${user.id}`;
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The example above runs the provider, middleware, and handler inside a single Effect execution boundary.
|
|
351
|
+
|
|
352
|
+
A native oRPC middleware breaks the contiguous Effect pipeline. Pending Effect steps are flushed into one generated oRPC middleware before the native middleware:
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
eos
|
|
356
|
+
.provide(AppLive)
|
|
357
|
+
.provide(CurrentUser, getCurrentUser) // Effect group #1
|
|
358
|
+
.use(function* ({ next }) {
|
|
359
|
+
return yield* next();
|
|
360
|
+
})
|
|
361
|
+
.use(({ next }) => next()) // native oRPC middleware; flushes group #1
|
|
362
|
+
.use(function* ({ next }) {
|
|
363
|
+
return yield* next();
|
|
364
|
+
}) // Effect group #2
|
|
365
|
+
.effect(function* () {
|
|
366
|
+
return "ok";
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
That split still creates multiple runtime boundaries. If the Node bridge is installed, however, `effect-orpc` carries the current Effect service context through the native oRPC continuation and merges it into the next Effect boundary:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import "effect-orpc/node";
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Use the side-effect import when you only need continuity across internal `effect-orpc` boundaries, such as Effect group #1 → native oRPC middleware → Effect group #2.
|
|
377
|
+
|
|
378
|
+
Procedure-level `.provide*` after a native `.handler(...)` has no Effect handler boundary to attach to, so it is installed as an oRPC middleware that runs its provider Effect through the configured runtime source:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
eos
|
|
382
|
+
.provide(AppLive)
|
|
383
|
+
.handler(() => "ok") // native oRPC handler
|
|
384
|
+
.provide(CurrentUser, getCurrentUser); // fallback provider middleware
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
If you want `.provide*` and Effect middleware to batch with the handler, use `.effect(function* ...)` instead of `.handler(...)`.
|
|
388
|
+
|
|
389
|
+
## Request-Scoped Effect Context
|
|
390
|
+
|
|
391
|
+
The `/node` entrypoint installs a bridge backed by `AsyncLocalStorage`. It has two uses:
|
|
392
|
+
|
|
393
|
+
- `import "effect-orpc/node"` installs the bridge passively. This is enough for `effect-orpc` to propagate the current Effect service context across its own split runtime boundaries.
|
|
394
|
+
- `withFiberContext(() => next())` actively seeds the bridge from an external Effect scope, such as framework middleware wrapping an oRPC handler.
|
|
395
|
+
|
|
396
|
+
Use `withFiberContext` when request-local Effect context is created outside the oRPC pipeline and should be visible inside handlers:
|
|
290
397
|
|
|
291
398
|
```ts
|
|
292
399
|
import { Hono } from "hono";
|
|
293
|
-
import { Effect
|
|
294
|
-
import {
|
|
400
|
+
import { Effect } from "effect";
|
|
401
|
+
import { eos } from "effect-orpc";
|
|
295
402
|
import { withFiberContext } from "effect-orpc/node";
|
|
296
403
|
|
|
297
|
-
const
|
|
298
|
-
const effectOs = makeEffectORPC(runtime);
|
|
404
|
+
const effectProcedure = eos.provide(AppLive);
|
|
299
405
|
const app = new Hono();
|
|
300
406
|
|
|
301
407
|
app.use("*", async (c, next) => {
|
|
@@ -311,31 +417,19 @@ app.use("*", async (c, next) => {
|
|
|
311
417
|
});
|
|
312
418
|
```
|
|
313
419
|
|
|
314
|
-
|
|
315
|
-
service, `effect-orpc` prioritizes the captured context. The runtime is treated
|
|
316
|
-
as the application-wide base layer, while `withFiberContext` preserves the
|
|
317
|
-
more specific request-scoped values from outer middleware. This prevents
|
|
318
|
-
request-local references such as request IDs, logging annotations, tracing
|
|
319
|
-
context, or scoped overrides from being replaced by runtime defaults when the
|
|
320
|
-
handler crosses the runtime boundary.
|
|
420
|
+
Importing `withFiberContext` from `effect-orpc/node` also installs the bridge, so you do not need a separate side-effect import.
|
|
321
421
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
`FiberRef` state across framework async boundaries. The main package stays
|
|
325
|
-
runtime-agnostic.
|
|
422
|
+
When a captured fiber context and the application `Layer` / `ManagedRuntime` both provide the same service, `effect-orpc` prioritizes the captured context.
|
|
423
|
+
The application layer is treated as the base layer, while the bridge preserves more specific request-scoped values such as request IDs, logging annotations, tracing context, or scoped overrides when crossing runtime boundaries.
|
|
326
424
|
|
|
327
|
-
|
|
328
|
-
`/node` entrypoint at all.
|
|
425
|
+
The main package stays runtime-agnostic; `/node` is separate because the bridge relies on `AsyncLocalStorage` from `node:async_hooks`.
|
|
329
426
|
|
|
330
427
|
## Contract-First Usage
|
|
331
428
|
|
|
332
|
-
Use `implementEffect(contract,
|
|
333
|
-
and want to keep contract-first enforcement while adding Effect-native handlers.
|
|
334
|
-
Use `makeEffectORPC(runtime, builder?)` when you want to build procedures
|
|
335
|
-
directly from an oRPC builder.
|
|
429
|
+
Use `implementEffect(contract, layerOrRuntime)` when you already have an oRPC contract and want to keep contract-first enforcement while adding Effect-native handlers. Use `eos.provide(layer)` when you want to build procedures directly from the default Effect-aware builder.
|
|
336
430
|
|
|
337
431
|
```ts
|
|
338
|
-
import { Effect
|
|
432
|
+
import { Effect } from "effect";
|
|
339
433
|
import { eoc, implementEffect } from "effect-orpc";
|
|
340
434
|
import z from "zod";
|
|
341
435
|
|
|
@@ -355,8 +449,7 @@ const contract = {
|
|
|
355
449
|
},
|
|
356
450
|
};
|
|
357
451
|
|
|
358
|
-
const
|
|
359
|
-
const oe = implementEffect(contract, runtime);
|
|
452
|
+
const oe = implementEffect(contract, UsersRepo.Default);
|
|
360
453
|
|
|
361
454
|
export const router = oe.router({
|
|
362
455
|
users: {
|
|
@@ -367,45 +460,48 @@ export const router = oe.router({
|
|
|
367
460
|
});
|
|
368
461
|
```
|
|
369
462
|
|
|
370
|
-
Contract leaves keep the contract-defined input, output, and error surface.
|
|
371
|
-
They add `.effect(...)` alongside existing implementer methods such as
|
|
372
|
-
`.handler(...)` and `.use(...)`, but do not expose contract-changing builder
|
|
373
|
-
methods like `.input(...)` or `.output(...)`.
|
|
463
|
+
Contract leaves keep the contract-defined input, output, and error surface. They add `.effect(...)` alongside existing implementer methods such as `.handler(...)` and `.use(...)`, but do not expose contract-changing builder methods like `.input(...)` or `.output(...)`.
|
|
374
464
|
|
|
375
|
-
If your contract declares tagged Effect error classes, prefer `eoc.errors(...)`
|
|
376
|
-
instead of raw `oc.errors(...)` so the error schema and metadata are derived
|
|
377
|
-
directly from the `ORPCTaggedError` class.
|
|
465
|
+
If your contract declares tagged Effect error classes, prefer `eoc.errors(...)` instead of raw `oc.errors(...)` so the error schema and metadata are derived directly from the `ORPCTaggedError` class.
|
|
378
466
|
|
|
379
467
|
## API Reference
|
|
380
468
|
|
|
381
|
-
### `
|
|
469
|
+
### `eos`
|
|
382
470
|
|
|
383
|
-
|
|
471
|
+
The default Effect-aware procedure builder. Provide your application services with `.provide(layer)`:
|
|
384
472
|
|
|
385
|
-
|
|
386
|
-
|
|
473
|
+
```ts
|
|
474
|
+
const effectProcedure = eos.provide(AppLive);
|
|
475
|
+
```
|
|
387
476
|
|
|
388
|
-
|
|
477
|
+
Use `makeEffectORPC(runtime)` when a scoped `Layer` should be acquired once and released by your application shutdown path, such as a shared cache, database pool, HTTP client, or telemetry SDK:
|
|
389
478
|
|
|
390
479
|
```ts
|
|
391
|
-
|
|
392
|
-
|
|
480
|
+
const runtime = ManagedRuntime.make(AppLive);
|
|
481
|
+
|
|
482
|
+
const effectProcedure = makeEffectORPC(runtime);
|
|
483
|
+
|
|
484
|
+
// later, during app shutdown
|
|
485
|
+
await runtime.dispose();
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
`makeEffectORPC(builder)` is also available when you need to wrap an existing oRPC builder:
|
|
393
489
|
|
|
394
|
-
|
|
395
|
-
const effectAuthedOs = makeEffectORPC(
|
|
490
|
+
```ts
|
|
491
|
+
const effectAuthedOs = makeEffectORPC(authedBuilder).provide(AppLive);
|
|
396
492
|
```
|
|
397
493
|
|
|
398
|
-
### `implementEffect(contract,
|
|
494
|
+
### `implementEffect(contract, layerOrRuntime)`
|
|
399
495
|
|
|
400
496
|
Creates an Effect-aware contract implementer.
|
|
401
497
|
|
|
402
498
|
- `contract` - An oRPC contract router built with `oc`
|
|
403
|
-
- `
|
|
499
|
+
- `layerOrRuntime` - A `Layer<R, E, never>` provided per call, or a user-owned `ManagedRuntime<R, E>` when the application should control acquisition and release (for example, a shared cache, database pool, or telemetry SDK)
|
|
404
500
|
|
|
405
501
|
Returns a contract-shaped implementer tree whose leaves support `.effect(...)`.
|
|
406
502
|
|
|
407
503
|
```ts
|
|
408
|
-
const oe = implementEffect(contract,
|
|
504
|
+
const oe = implementEffect(contract, AppLive);
|
|
409
505
|
|
|
410
506
|
const router = oe.router({
|
|
411
507
|
users: {
|
|
@@ -420,8 +516,7 @@ const router = oe.router({
|
|
|
420
516
|
|
|
421
517
|
An Effect-aware wrapper around oRPC's `oc` contract builder.
|
|
422
518
|
|
|
423
|
-
Use it when you want contract definitions to accept `ORPCTaggedError` classes
|
|
424
|
-
directly in `.errors(...)` without duplicating the error schema.
|
|
519
|
+
Use it when you want contract definitions to accept `ORPCTaggedError` classes directly in `.errors(...)` without duplicating the error schema.
|
|
425
520
|
|
|
426
521
|
```ts
|
|
427
522
|
class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
|
|
@@ -445,39 +540,43 @@ const contract = {
|
|
|
445
540
|
|
|
446
541
|
Wraps an oRPC Builder with Effect support. Available methods:
|
|
447
542
|
|
|
448
|
-
| Method
|
|
449
|
-
|
|
|
450
|
-
| `.$config(config)`
|
|
451
|
-
| `.$context<U>()`
|
|
452
|
-
| `.$meta(meta)`
|
|
453
|
-
| `.$route(route)`
|
|
454
|
-
| `.$input(schema)`
|
|
455
|
-
| `.errors(map)`
|
|
456
|
-
| `.meta(meta)`
|
|
457
|
-
| `.route(route)`
|
|
458
|
-
| `.input(schema)`
|
|
459
|
-
| `.output(schema)`
|
|
460
|
-
| `.
|
|
461
|
-
| `.
|
|
462
|
-
| `.
|
|
463
|
-
| `.
|
|
464
|
-
| `.
|
|
465
|
-
| `.
|
|
466
|
-
| `.
|
|
467
|
-
| `.
|
|
543
|
+
| Method | Description |
|
|
544
|
+
| ------------------------- | ------------------------------------------------------------------------------------ |
|
|
545
|
+
| `.$config(config)` | Set or override the builder config |
|
|
546
|
+
| `.$context<U>()` | Set or override the initial context type |
|
|
547
|
+
| `.$meta(meta)` | Set or override the initial metadata |
|
|
548
|
+
| `.$route(route)` | Set or override the initial route configuration |
|
|
549
|
+
| `.$input(schema)` | Set or override the initial input schema |
|
|
550
|
+
| `.errors(map)` | Add type-safe custom errors |
|
|
551
|
+
| `.meta(meta)` | Set procedure metadata (merged with existing) |
|
|
552
|
+
| `.route(route)` | Configure OpenAPI route (merged with existing) |
|
|
553
|
+
| `.input(schema)` | Define input validation schema |
|
|
554
|
+
| `.output(schema)` | Define output validation schema |
|
|
555
|
+
| `.provide(layer)` | Provide a base Effect layer to downstream Effect middleware and handlers |
|
|
556
|
+
| `.provide(tag, provider)` | Provide a request-scoped Effect service to downstream Effect middleware and handlers |
|
|
557
|
+
| `.use(middleware)` | Add middleware |
|
|
558
|
+
| `.traced(name)` | Add a traceable span for telemetry (optional, defaults to the procedure's path) |
|
|
559
|
+
| `.handler(handler)` | Define a non-Effect handler (standard oRPC handler) |
|
|
560
|
+
| `.effect(handler)` | Define the Effect handler |
|
|
561
|
+
| `.prefix(prefix)` | Prefix all procedures in the router (for OpenAPI) |
|
|
562
|
+
| `.tag(...tags)` | Add tags to all procedures in the router (for OpenAPI) |
|
|
563
|
+
| `.router(router)` | Apply all options to a router |
|
|
564
|
+
| `.lazy(loader)` | Create and apply options to a lazy-loaded router |
|
|
468
565
|
|
|
469
566
|
### `EffectDecoratedProcedure`
|
|
470
567
|
|
|
471
568
|
The result of calling `.effect()`. Extends standard oRPC `DecoratedProcedure` with Effect type preservation.
|
|
472
569
|
|
|
473
|
-
| Method
|
|
474
|
-
|
|
|
475
|
-
| `.errors(map)`
|
|
476
|
-
| `.meta(meta)`
|
|
477
|
-
| `.route(route)`
|
|
478
|
-
| `.
|
|
479
|
-
| `.
|
|
480
|
-
| `.
|
|
570
|
+
| Method | Description |
|
|
571
|
+
| ------------------------- | --------------------------------------------- |
|
|
572
|
+
| `.errors(map)` | Add more custom errors |
|
|
573
|
+
| `.meta(meta)` | Update metadata (merged with existing) |
|
|
574
|
+
| `.route(route)` | Update route configuration (merged) |
|
|
575
|
+
| `.provide(layer)` | Provide a base Effect layer |
|
|
576
|
+
| `.provide(tag, provider)` | Provide a request-scoped Effect service |
|
|
577
|
+
| `.use(middleware)` | Add middleware |
|
|
578
|
+
| `.callable(options?)` | Make procedure directly invocable |
|
|
579
|
+
| `.actionable(options?)` | Make procedure compatible with server actions |
|
|
481
580
|
|
|
482
581
|
### `ORPCTaggedError(tag, options?)`
|
|
483
582
|
|
|
@@ -6,9 +6,13 @@ function installServiceContextBridge(nextBridge) {
|
|
|
6
6
|
function getCurrentServices() {
|
|
7
7
|
return bridge?.getCurrentServices();
|
|
8
8
|
}
|
|
9
|
+
function runWithServices(services, fn) {
|
|
10
|
+
return bridge?.runWithServices?.(services, fn) ?? fn();
|
|
11
|
+
}
|
|
9
12
|
|
|
10
13
|
export {
|
|
11
14
|
installServiceContextBridge,
|
|
12
|
-
getCurrentServices
|
|
15
|
+
getCurrentServices,
|
|
16
|
+
runWithServices
|
|
13
17
|
};
|
|
14
|
-
//# sourceMappingURL=chunk-
|
|
18
|
+
//# sourceMappingURL=chunk-SXPXGZUW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/service-context-bridge.ts"],"sourcesContent":["import type { Context } from \"effect\";\n\nexport interface ServiceContextBridge {\n readonly getCurrentServices: () => Context.Context<any> | undefined;\n readonly runWithServices?: <T>(\n services: Context.Context<any>,\n fn: () => Promise<T>,\n ) => Promise<T>;\n}\n\nlet bridge: ServiceContextBridge | undefined;\n\nexport function installServiceContextBridge(\n nextBridge: ServiceContextBridge | undefined,\n): void {\n bridge = nextBridge;\n}\n\nexport function getCurrentServices(): Context.Context<any> | undefined {\n return bridge?.getCurrentServices();\n}\n\nexport function runWithServices<T>(\n services: Context.Context<any>,\n fn: () => Promise<T>,\n): Promise<T> {\n return bridge?.runWithServices?.(services, fn) ?? fn();\n}\n"],"mappings":";AAUA,IAAI;AAEG,SAAS,4BACd,YACM;AACN,WAAS;AACX;AAEO,SAAS,qBAAuD;AACrE,SAAO,QAAQ,mBAAmB;AACpC;AAEO,SAAS,gBACd,UACA,IACY;AACZ,SAAO,QAAQ,kBAAkB,UAAU,EAAE,KAAK,GAAG;AACvD;","names":[]}
|