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.
Files changed (36) hide show
  1. package/README.md +240 -141
  2. package/dist/{chunk-I5EWBI42.js → chunk-SXPXGZUW.js} +6 -2
  3. package/dist/chunk-SXPXGZUW.js.map +1 -0
  4. package/dist/index.js +1050 -283
  5. package/dist/index.js.map +1 -1
  6. package/dist/node.js +3 -2
  7. package/dist/node.js.map +1 -1
  8. package/package.json +3 -3
  9. package/src/contract.ts +46 -11
  10. package/src/effect-builder.ts +341 -53
  11. package/src/effect-enhance-router.ts +3 -3
  12. package/src/effect-procedure.ts +232 -14
  13. package/src/effect-runtime.ts +980 -24
  14. package/src/extension/create-node-proxy.ts +17 -1
  15. package/src/extension/state.ts +16 -20
  16. package/src/index.ts +1 -0
  17. package/src/node.ts +1 -0
  18. package/src/runtime-source.ts +107 -0
  19. package/src/service-context-bridge.ts +11 -0
  20. package/src/tagged-error.ts +2 -11
  21. package/src/tests/contract.test.ts +21 -0
  22. package/src/tests/effect-builder.proxy.test.ts +15 -17
  23. package/src/tests/effect-builder.test.ts +843 -193
  24. package/src/tests/effect-callback-shapes.test.ts +411 -0
  25. package/src/tests/effect-error-map.test.ts +12 -14
  26. package/src/tests/effect-procedure.test.ts +53 -11
  27. package/src/tests/node-side-effect.test.ts +80 -0
  28. package/src/tests/parity-shared.ts +2 -2
  29. package/src/tests/parity.effect-builder.test.ts +10 -3
  30. package/src/tests/parity.effect-procedure.test.ts +24 -8
  31. package/src/tests/shared.ts +1 -25
  32. package/src/types/effect-builder-surface.ts +117 -1
  33. package/src/types/effect-procedure-surface.ts +98 -1
  34. package/src/types/index.ts +325 -8
  35. package/src/types/variants.ts +352 -19
  36. 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** - Use `ManagedRuntime<R>` to provide services to procedures with compile-time safety
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, Layer, ManagedRuntime, Context } from "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 Context.Service<
57
- UsersRepo,
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 runtime with your services
73
- const runtime = ManagedRuntime.make(UsersRepo.layer);
74
- // Create Effect-aware oRPC builder from an other (optional) base oRPC builder and provide tagged errors
75
- const effectOs = makeEffectORPC(runtime, authedOs).errors({
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: effectOs.effect(function* ({ context: { userId } }) {
84
- const usersRepo = yield* UsersRepo;
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 the `ManagedRuntime`. If you try to use a service that isn't in the runtime, you'll get a compile-time error:
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, ManagedRuntime, Context } from "effect";
103
- import { makeEffectORPC } from "effect-orpc";
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 runtime = ManagedRuntime.make(
120
- Layer.succeed(ProvidedService, {
121
- doSomething: () => Effect.succeed("ok"),
122
- }),
123
- );
110
+ const AppLive = Layer.succeed(ProvidedService, {
111
+ doSomething: () => Effect.succeed("ok"),
112
+ });
124
113
 
125
- const effectOs = makeEffectORPC(runtime);
114
+ const effectProcedure = eos.provide(AppLive);
126
115
 
127
- // ✅ This compiles - ProvidedService is in the runtime
128
- const works = effectOs.effect(function* () {
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 in the runtime
134
- const fails = effectOs.effect(function* () {
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 = effectOs
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: effectOs.input(z.object({ id: z.string() })).effect(function* ({
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: effectOs.input(z.object({ name: z.string() })).effect(function* ({
228
- input,
229
- }) {
230
- const userService = yield* UserService;
231
- return yield* userService.create(input.name);
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 = effectOs
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 runtime:
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 runtime = ManagedRuntime.make(AppLive);
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
- ## Request-Scoped Fiber Context
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
- If you run `effect-orpc` inside a framework such as Hono, the handler executes
284
- through the runtime boundary and will not automatically inherit request-local
285
- `FiberRef` state from outer middleware.
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
- To preserve request-scoped logs, tracing annotations, and
288
- other fiber-local state, wrap the framework continuation with `withFiberContext` from
289
- `effect-orpc/node`.
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, ManagedRuntime } from "effect";
294
- import { makeEffectORPC } from "effect-orpc";
400
+ import { Effect } from "effect";
401
+ import { eos } from "effect-orpc";
295
402
  import { withFiberContext } from "effect-orpc/node";
296
403
 
297
- const runtime = ManagedRuntime.make(AppLive);
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
- When a captured fiber context and the `ManagedRuntime` both provide the same
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
- The reason for the separate `/node` entrypoint is that `withFiberContext` relies
323
- on Node/Bun's `AsyncLocalStorage` from `node:async_hooks` to carry Effect
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
- If you do not need framework-to-handler fiber propagation, you do not need the
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, runtime)` when you already have an oRPC 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, ManagedRuntime } from "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 runtime = ManagedRuntime.make(UsersRepo.Default);
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
- ### `makeEffectORPC(runtime, builder?)`
469
+ ### `eos`
382
470
 
383
- Creates an Effect-aware procedure builder.
471
+ The default Effect-aware procedure builder. Provide your application services with `.provide(layer)`:
384
472
 
385
- - `runtime` - A `ManagedRuntime<R, E>` instance that provides services for Effect procedures
386
- - `builder` (optional) - An oRPC Builder instance to wrap. Defaults to `os` from `@orpc/server`
473
+ ```ts
474
+ const effectProcedure = eos.provide(AppLive);
475
+ ```
387
476
 
388
- Returns an `EffectBuilder` instance.
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
- // With default builder
392
- const effectOs = makeEffectORPC(runtime);
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
- // With customized builder
395
- const effectAuthedOs = makeEffectORPC(runtime, authedBuilder);
490
+ ```ts
491
+ const effectAuthedOs = makeEffectORPC(authedBuilder).provide(AppLive);
396
492
  ```
397
493
 
398
- ### `implementEffect(contract, runtime)`
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
- - `runtime` - A `ManagedRuntime<R, E>` instance that provides services for Effect procedures
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, runtime);
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 | Description |
449
- | ------------------- | ------------------------------------------------------------------------------- |
450
- | `.$config(config)` | Set or override the builder config |
451
- | `.$context<U>()` | Set or override the initial context type |
452
- | `.$meta(meta)` | Set or override the initial metadata |
453
- | `.$route(route)` | Set or override the initial route configuration |
454
- | `.$input(schema)` | Set or override the initial input schema |
455
- | `.errors(map)` | Add type-safe custom errors |
456
- | `.meta(meta)` | Set procedure metadata (merged with existing) |
457
- | `.route(route)` | Configure OpenAPI route (merged with existing) |
458
- | `.input(schema)` | Define input validation schema |
459
- | `.output(schema)` | Define output validation schema |
460
- | `.use(middleware)` | Add middleware |
461
- | `.traced(name)` | Add a traceable span for telemetry (optional, defaults to the procedure's path) |
462
- | `.handler(handler)` | Define a non-Effect handler (standard oRPC handler) |
463
- | `.effect(handler)` | Define the Effect handler |
464
- | `.prefix(prefix)` | Prefix all procedures in the router (for OpenAPI) |
465
- | `.tag(...tags)` | Add tags to all procedures in the router (for OpenAPI) |
466
- | `.router(router)` | Apply all options to a router |
467
- | `.lazy(loader)` | Create and apply options to a lazy-loaded router |
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 | Description |
474
- | ----------------------- | --------------------------------------------- |
475
- | `.errors(map)` | Add more custom errors |
476
- | `.meta(meta)` | Update metadata (merged with existing) |
477
- | `.route(route)` | Update route configuration (merged) |
478
- | `.use(middleware)` | Add middleware |
479
- | `.callable(options?)` | Make procedure directly invocable |
480
- | `.actionable(options?)` | Make procedure compatible with server actions |
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-I5EWBI42.js.map
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":[]}