effect-orpc 1.0.0-effect-v4.3 → 1.0.0-effect-v4.4

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 CHANGED
@@ -29,7 +29,7 @@ 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, ServiceMap } from "effect";
32
+ import { Effect, Layer, ManagedRuntime, Context } from "effect";
33
33
  import { makeEffectORPC, ORPCTaggedError } from "effect-orpc";
34
34
 
35
35
  interface User {
@@ -53,7 +53,7 @@ const authedOs = os
53
53
  });
54
54
 
55
55
  // Define your services
56
- class UsersRepo extends ServiceMap.Service<
56
+ class UsersRepo extends Context.Service<
57
57
  UsersRepo,
58
58
  {
59
59
  readonly get: (id: number) => User | undefined;
@@ -99,17 +99,17 @@ export type Router = typeof router;
99
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:
100
100
 
101
101
  ```ts
102
- import { Effect, Layer, ManagedRuntime, ServiceMap } from "effect";
102
+ import { Effect, Layer, ManagedRuntime, Context } from "effect";
103
103
  import { makeEffectORPC } from "effect-orpc";
104
104
 
105
- class ProvidedService extends ServiceMap.Service<
105
+ class ProvidedService extends Context.Service<
106
106
  ProvidedService,
107
107
  {
108
108
  readonly doSomething: () => Effect.Effect<string>;
109
109
  }
110
110
  >()("ProvidedService") {}
111
111
 
112
- class MissingService extends ServiceMap.Service<
112
+ class MissingService extends Context.Service<
113
113
  MissingService,
114
114
  {
115
115
  readonly doSomething: () => Effect.Effect<string>;
@@ -327,6 +327,55 @@ runtime-agnostic.
327
327
  If you do not need framework-to-handler fiber propagation, you do not need the
328
328
  `/node` entrypoint at all.
329
329
 
330
+ ## Contract-First Usage
331
+
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.
336
+
337
+ ```ts
338
+ import { Effect, ManagedRuntime } from "effect";
339
+ import { eoc, implementEffect } from "effect-orpc";
340
+ import z from "zod";
341
+
342
+ class UsersRepo extends Effect.Service<UsersRepo>()("UsersRepo", {
343
+ accessors: true,
344
+ sync: () => ({
345
+ list: (amount: number) =>
346
+ Array.from({ length: amount }, (_, index) => `user-${index + 1}`),
347
+ }),
348
+ }) {}
349
+
350
+ const contract = {
351
+ users: {
352
+ list: eoc
353
+ .input(z.object({ amount: z.number().int().positive() }))
354
+ .output(z.array(z.string())),
355
+ },
356
+ };
357
+
358
+ const runtime = ManagedRuntime.make(UsersRepo.Default);
359
+ const oe = implementEffect(contract, runtime);
360
+
361
+ export const router = oe.router({
362
+ users: {
363
+ list: oe.users.list.effect(function* ({ input }) {
364
+ return yield* UsersRepo.list(input.amount);
365
+ }),
366
+ },
367
+ });
368
+ ```
369
+
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(...)`.
374
+
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.
378
+
330
379
  ## API Reference
331
380
 
332
381
  ### `makeEffectORPC(runtime, builder?)`
@@ -346,6 +395,52 @@ const effectOs = makeEffectORPC(runtime);
346
395
  const effectAuthedOs = makeEffectORPC(runtime, authedBuilder);
347
396
  ```
348
397
 
398
+ ### `implementEffect(contract, runtime)`
399
+
400
+ Creates an Effect-aware contract implementer.
401
+
402
+ - `contract` - An oRPC contract router built with `oc`
403
+ - `runtime` - A `ManagedRuntime<R, E>` instance that provides services for Effect procedures
404
+
405
+ Returns a contract-shaped implementer tree whose leaves support `.effect(...)`.
406
+
407
+ ```ts
408
+ const oe = implementEffect(contract, runtime);
409
+
410
+ const router = oe.router({
411
+ users: {
412
+ list: oe.users.list.effect(function* ({ input }) {
413
+ return yield* UsersRepo.list(input.amount);
414
+ }),
415
+ },
416
+ });
417
+ ```
418
+
419
+ ### `eoc`
420
+
421
+ An Effect-aware wrapper around oRPC's `oc` contract builder.
422
+
423
+ Use it when you want contract definitions to accept `ORPCTaggedError` classes
424
+ directly in `.errors(...)` without duplicating the error schema.
425
+
426
+ ```ts
427
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
428
+ code: "NOT_FOUND",
429
+ schema: z.object({ userId: z.string() }),
430
+ }) {}
431
+
432
+ const contract = {
433
+ users: {
434
+ find: eoc
435
+ .errors({
436
+ NOT_FOUND: UserNotFoundError,
437
+ })
438
+ .input(z.object({ userId: z.string() }))
439
+ .output(z.object({ userId: z.string() })),
440
+ },
441
+ };
442
+ ```
443
+
349
444
  ### `EffectBuilder`
350
445
 
351
446
  Wraps an oRPC Builder with Effect support. Available methods:
@@ -11,4 +11,4 @@ export {
11
11
  installServiceContextBridge,
12
12
  getCurrentServices
13
13
  };
14
- //# sourceMappingURL=chunk-E5YLLTJI.js.map
14
+ //# sourceMappingURL=chunk-I5EWBI42.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}\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"],"mappings":";AAMA,IAAI;AAEG,SAAS,4BACd,YACM;AACN,WAAS;AACX;AAEO,SAAS,qBAAuD;AACrE,SAAO,QAAQ,mBAAmB;AACpC;","names":[]}
package/dist/index.js CHANGED
@@ -1,15 +1,13 @@
1
1
  import {
2
2
  getCurrentServices
3
- } from "./chunk-E5YLLTJI.js";
3
+ } from "./chunk-I5EWBI42.js";
4
+
5
+ // src/contract.ts
6
+ import { isContractProcedure as isContractProcedure2 } from "@orpc/contract";
7
+ import { implement } from "@orpc/server";
4
8
 
5
9
  // src/effect-builder.ts
6
- import {
7
- mergeMeta as mergeMeta2,
8
- mergePrefix as mergePrefix2,
9
- mergeRoute as mergeRoute2,
10
- mergeTags,
11
- ORPCError as ORPCError2
12
- } from "@orpc/contract";
10
+ import { mergeMeta as mergeMeta2, mergePrefix as mergePrefix2, mergeRoute as mergeRoute2, mergeTags } from "@orpc/contract";
13
11
  import {
14
12
  addMiddleware as addMiddleware2,
15
13
  Builder,
@@ -17,13 +15,6 @@ import {
17
15
  fallbackConfig,
18
16
  lazy as lazy2
19
17
  } from "@orpc/server";
20
- import {
21
- Cause as Cause2,
22
- Effect,
23
- Exit,
24
- Result,
25
- ServiceMap
26
- } from "effect";
27
18
 
28
19
  // src/effect-enhance-router.ts
29
20
  import {
@@ -329,27 +320,9 @@ function enhanceEffectRouter(router, options) {
329
320
  return enhanced;
330
321
  }
331
322
 
332
- // src/effect-builder.ts
333
- function addSpanStackTrace() {
334
- const ErrorConstructor = Error;
335
- const limit = ErrorConstructor.stackTraceLimit;
336
- ErrorConstructor.stackTraceLimit = 3;
337
- const traceError = new Error();
338
- ErrorConstructor.stackTraceLimit = limit;
339
- let cache = false;
340
- return () => {
341
- if (cache !== false) {
342
- return cache;
343
- }
344
- if (traceError.stack !== void 0) {
345
- const stack = traceError.stack.split("\n");
346
- if (stack[3] !== void 0) {
347
- cache = stack[3].trim();
348
- return cache;
349
- }
350
- }
351
- };
352
- }
323
+ // src/effect-runtime.ts
324
+ import { ORPCError as ORPCError2 } from "@orpc/contract";
325
+ import { Cause as Cause2, Effect, Exit, Result, Context } from "effect";
353
326
  function toORPCErrorFromCause(cause) {
354
327
  if (Cause2.hasFails(cause)) {
355
328
  const reason = Cause2.findFail(cause);
@@ -391,6 +364,66 @@ function toORPCErrorFromCause(cause) {
391
364
  }
392
365
  return new ORPCError2("INTERNAL_SERVER_ERROR");
393
366
  }
367
+ function createEffectProcedureHandler(options) {
368
+ const {
369
+ runtime,
370
+ effectErrorMap,
371
+ effectFn,
372
+ spanConfig,
373
+ defaultCaptureStackTrace
374
+ } = options;
375
+ return async (opts) => {
376
+ const effectOpts = {
377
+ context: opts.context,
378
+ input: opts.input,
379
+ path: opts.path,
380
+ procedure: opts.procedure,
381
+ signal: opts.signal,
382
+ lastEventId: opts.lastEventId,
383
+ errors: createEffectErrorConstructorMap(effectErrorMap)
384
+ };
385
+ const spanName = spanConfig?.name ?? opts.path.join(".");
386
+ const captureStackTrace = spanConfig?.captureStackTrace ?? defaultCaptureStackTrace;
387
+ const resolver = Effect.fnUntraced(effectFn);
388
+ const tracedEffect = Effect.withSpan(resolver(effectOpts), spanName, {
389
+ captureStackTrace
390
+ });
391
+ const parentServices = getCurrentServices();
392
+ const exit = parentServices ? await Effect.runPromiseExitWith(
393
+ Context.merge(await runtime.context(), parentServices)
394
+ )(tracedEffect, {
395
+ signal: opts.signal
396
+ }) : await runtime.runPromiseExit(tracedEffect, {
397
+ signal: opts.signal
398
+ });
399
+ if (Exit.isFailure(exit)) {
400
+ throw toORPCErrorFromCause(exit.cause);
401
+ }
402
+ return exit.value;
403
+ };
404
+ }
405
+
406
+ // src/effect-builder.ts
407
+ function addSpanStackTrace() {
408
+ const ErrorConstructor = Error;
409
+ const limit = ErrorConstructor.stackTraceLimit;
410
+ ErrorConstructor.stackTraceLimit = 3;
411
+ const traceError = new Error();
412
+ ErrorConstructor.stackTraceLimit = limit;
413
+ let cache = false;
414
+ return () => {
415
+ if (cache !== false) {
416
+ return cache;
417
+ }
418
+ if (traceError.stack !== void 0) {
419
+ const stack = traceError.stack.split("\n");
420
+ if (stack[3] !== void 0) {
421
+ cache = stack[3].trim();
422
+ return cache;
423
+ }
424
+ }
425
+ };
426
+ }
394
427
  var EffectBuilder = class _EffectBuilder {
395
428
  constructor(def) {
396
429
  const { runtime, spanConfig, effectErrorMap, ...orpcDef } = def;
@@ -483,6 +516,14 @@ var EffectBuilder = class _EffectBuilder {
483
516
  inputSchema: initialInputSchema
484
517
  });
485
518
  }
519
+ /**
520
+ * Creates a middleware.
521
+ *
522
+ * @see {@link https://orpc.dev/docs/middleware Middleware Docs}
523
+ */
524
+ middleware(middleware) {
525
+ return decorateMiddleware2(middleware);
526
+ }
486
527
  /**
487
528
  * Adds type-safe custom errors.
488
529
  * Supports both traditional oRPC error definitions and ORPCTaggedError classes.
@@ -630,35 +671,13 @@ var EffectBuilder = class _EffectBuilder {
630
671
  return new EffectDecoratedProcedure({
631
672
  ...this["~effect"],
632
673
  handler: async (opts) => {
633
- const effectOpts = {
634
- context: opts.context,
635
- input: opts.input,
636
- path: opts.path,
637
- procedure: opts.procedure,
638
- signal: opts.signal,
639
- lastEventId: opts.lastEventId,
640
- errors: createEffectErrorConstructorMap(
641
- this["~effect"].effectErrorMap
642
- )
643
- };
644
- const spanName = spanConfig?.name ?? opts.path.join(".");
645
- const captureStackTrace = spanConfig?.captureStackTrace ?? defaultCaptureStackTrace;
646
- const resolver = Effect.fnUntraced(effectFn);
647
- const tracedEffect = Effect.withSpan(resolver(effectOpts), spanName, {
648
- captureStackTrace
649
- });
650
- const parentServices = getCurrentServices();
651
- const exit = parentServices ? await Effect.runPromiseExitWith(
652
- ServiceMap.merge(await runtime.services(), parentServices)
653
- )(tracedEffect, {
654
- signal: opts.signal
655
- }) : await runtime.runPromiseExit(tracedEffect, {
656
- signal: opts.signal
657
- });
658
- if (Exit.isFailure(exit)) {
659
- throw toORPCErrorFromCause(exit.cause);
660
- }
661
- return exit.value;
674
+ return createEffectProcedureHandler({
675
+ runtime,
676
+ effectErrorMap: this["~effect"].effectErrorMap,
677
+ effectFn,
678
+ spanConfig,
679
+ defaultCaptureStackTrace
680
+ })(opts);
662
681
  }
663
682
  });
664
683
  }
@@ -731,6 +750,256 @@ function emptyBuilder() {
731
750
  dedupeLeadingMiddlewares: true
732
751
  });
733
752
  }
753
+
754
+ // src/eoc.ts
755
+ import { isContractProcedure, oc } from "@orpc/contract";
756
+ var effectContractSymbol = /* @__PURE__ */ Symbol.for(
757
+ "@orpc/effect/contract"
758
+ );
759
+ function isWrappableContractBuilder(value) {
760
+ return typeof value === "object" && value !== null && "~orpc" in value;
761
+ }
762
+ function mergeEffectErrorMaps(left, right) {
763
+ if (!left) {
764
+ return right;
765
+ }
766
+ if (!right) {
767
+ return left;
768
+ }
769
+ return {
770
+ ...left,
771
+ ...right
772
+ };
773
+ }
774
+ function setEffectContractErrorMap(value, effectErrorMap) {
775
+ if (!effectErrorMap) {
776
+ return;
777
+ }
778
+ Object.defineProperty(value, effectContractSymbol, {
779
+ value: { errorMap: effectErrorMap },
780
+ enumerable: false,
781
+ configurable: true
782
+ });
783
+ }
784
+ function getEffectContractErrorMap(value) {
785
+ if (typeof value !== "object" || value === null) {
786
+ return void 0;
787
+ }
788
+ return value[effectContractSymbol]?.errorMap;
789
+ }
790
+ function applyEffectContractErrorMapToRouter(router, source, inheritedEffectErrorMap) {
791
+ const routerRecord = router;
792
+ const sourceRecord = source;
793
+ for (const key of Object.keys(routerRecord)) {
794
+ const routerValue = routerRecord[key];
795
+ const sourceValue = sourceRecord && typeof sourceRecord === "object" ? sourceRecord[key] : void 0;
796
+ if (!routerValue) {
797
+ continue;
798
+ }
799
+ if (isContractProcedure(routerValue)) {
800
+ const sourceEffectErrorMap = getEffectContractErrorMap(sourceValue);
801
+ setEffectContractErrorMap(
802
+ routerValue,
803
+ mergeEffectErrorMaps(inheritedEffectErrorMap, sourceEffectErrorMap)
804
+ );
805
+ continue;
806
+ }
807
+ if (typeof routerValue === "object") {
808
+ applyEffectContractErrorMapToRouter(
809
+ routerValue,
810
+ sourceValue,
811
+ inheritedEffectErrorMap
812
+ );
813
+ }
814
+ }
815
+ }
816
+ function wrapEffectContractBuilder(builder, inheritedEffectErrorMap) {
817
+ const currentEffectErrorMap = inheritedEffectErrorMap ?? getEffectContractErrorMap(builder);
818
+ if (typeof builder === "object" && builder !== null) {
819
+ setEffectContractErrorMap(builder, currentEffectErrorMap);
820
+ }
821
+ const proxy = new Proxy(builder, {
822
+ get(target, prop, receiver) {
823
+ if (prop === effectContractSymbol) {
824
+ return currentEffectErrorMap ? { errorMap: currentEffectErrorMap } : void 0;
825
+ }
826
+ if (prop === "errors") {
827
+ return (errors) => {
828
+ const nextEffectErrorMap = mergeEffectErrorMaps(
829
+ currentEffectErrorMap,
830
+ errors
831
+ );
832
+ return wrapEffectContractBuilder(
833
+ Reflect.apply(Reflect.get(target, prop, receiver), target, [
834
+ effectErrorMapToErrorMap(errors)
835
+ ]),
836
+ nextEffectErrorMap
837
+ );
838
+ };
839
+ }
840
+ if (prop === "router") {
841
+ return (router) => {
842
+ const result = Reflect.apply(
843
+ Reflect.get(target, prop, receiver),
844
+ target,
845
+ [router]
846
+ );
847
+ applyEffectContractErrorMapToRouter(
848
+ result,
849
+ router,
850
+ currentEffectErrorMap
851
+ );
852
+ return result;
853
+ };
854
+ }
855
+ const value = Reflect.get(target, prop, receiver);
856
+ if (typeof value !== "function") {
857
+ return value;
858
+ }
859
+ return (...args) => {
860
+ const result = Reflect.apply(value, target, args);
861
+ return isWrappableContractBuilder(result) ? wrapEffectContractBuilder(result, currentEffectErrorMap) : result;
862
+ };
863
+ }
864
+ });
865
+ setEffectContractErrorMap(proxy, currentEffectErrorMap);
866
+ return proxy;
867
+ }
868
+ var eoc = wrapEffectContractBuilder(
869
+ oc,
870
+ {}
871
+ );
872
+
873
+ // src/contract.ts
874
+ var CONTRACT_HIDDEN_METHODS = /* @__PURE__ */ new Set([
875
+ "$config",
876
+ "$context",
877
+ "$input",
878
+ "$meta",
879
+ "$route",
880
+ "errors",
881
+ "input",
882
+ "lazy",
883
+ "meta",
884
+ "middleware",
885
+ "output",
886
+ "prefix",
887
+ "route",
888
+ "router",
889
+ "tag"
890
+ ]);
891
+ function makeEnhanceOptions(runtime) {
892
+ return {
893
+ middlewares: [],
894
+ errorMap: {},
895
+ dedupeLeadingMiddlewares: true,
896
+ runtime
897
+ };
898
+ }
899
+ function wrapContractNode(contract, target, runtime) {
900
+ const cache = /* @__PURE__ */ new Map();
901
+ return new Proxy(target, {
902
+ get(currentTarget, prop, receiver) {
903
+ if (cache.has(prop)) {
904
+ return cache.get(prop);
905
+ }
906
+ if (isContractProcedure2(contract)) {
907
+ if (prop === "effect") {
908
+ const effect = (effectFn) => {
909
+ const effectErrorMap = getEffectContractErrorMap(contract) ?? currentTarget["~orpc"].errorMap;
910
+ return new EffectDecoratedProcedure({
911
+ ...currentTarget["~orpc"],
912
+ errorMap: effectErrorMapToErrorMap(effectErrorMap),
913
+ effectErrorMap,
914
+ runtime,
915
+ handler: createEffectProcedureHandler({
916
+ runtime,
917
+ effectErrorMap,
918
+ effectFn,
919
+ defaultCaptureStackTrace: addSpanStackTrace()
920
+ })
921
+ });
922
+ };
923
+ cache.set(prop, effect);
924
+ return effect;
925
+ }
926
+ if (prop === "use") {
927
+ const use = (...args) => wrapContractNode(
928
+ contract,
929
+ Reflect.apply(
930
+ Reflect.get(currentTarget, prop, currentTarget),
931
+ currentTarget,
932
+ args
933
+ ),
934
+ runtime
935
+ );
936
+ cache.set(prop, use);
937
+ return use;
938
+ }
939
+ if (CONTRACT_HIDDEN_METHODS.has(String(prop))) {
940
+ return void 0;
941
+ }
942
+ } else {
943
+ if (prop === "$context" || prop === "$config" || prop === "use") {
944
+ const wrappedMethod = (...args) => wrapContractNode(
945
+ contract,
946
+ Reflect.apply(
947
+ Reflect.get(currentTarget, prop, currentTarget),
948
+ currentTarget,
949
+ args
950
+ ),
951
+ runtime
952
+ );
953
+ cache.set(prop, wrappedMethod);
954
+ return wrappedMethod;
955
+ }
956
+ if (prop === "router" || prop === "lazy") {
957
+ const wrappedMethod = (...args) => enhanceEffectRouter(
958
+ Reflect.apply(
959
+ Reflect.get(currentTarget, prop, currentTarget),
960
+ currentTarget,
961
+ args
962
+ ),
963
+ makeEnhanceOptions(runtime)
964
+ );
965
+ cache.set(prop, wrappedMethod);
966
+ return wrappedMethod;
967
+ }
968
+ if (typeof prop === "string" && prop in contract) {
969
+ const child = wrapContractNode(
970
+ contract[prop],
971
+ Reflect.get(currentTarget, prop, receiver),
972
+ runtime
973
+ );
974
+ cache.set(prop, child);
975
+ return child;
976
+ }
977
+ }
978
+ const value = Reflect.get(currentTarget, prop, receiver);
979
+ return typeof value === "function" ? value.bind(currentTarget) : value;
980
+ },
981
+ has(currentTarget, prop) {
982
+ if (isContractProcedure2(contract)) {
983
+ if (prop === "effect") {
984
+ return true;
985
+ }
986
+ if (CONTRACT_HIDDEN_METHODS.has(String(prop))) {
987
+ return false;
988
+ }
989
+ } else if (typeof prop === "string" && prop in contract) {
990
+ return true;
991
+ }
992
+ return Reflect.has(currentTarget, prop);
993
+ }
994
+ });
995
+ }
996
+ function implementEffect(contract, runtime) {
997
+ return wrapContractNode(
998
+ contract,
999
+ implement(contract),
1000
+ runtime
1001
+ );
1002
+ }
734
1003
  export {
735
1004
  EffectBuilder,
736
1005
  EffectDecoratedProcedure,
@@ -739,6 +1008,8 @@ export {
739
1008
  addSpanStackTrace,
740
1009
  createEffectErrorConstructorMap,
741
1010
  effectErrorMapToErrorMap,
1011
+ eoc,
1012
+ implementEffect,
742
1013
  isORPCTaggedError,
743
1014
  isORPCTaggedErrorClass,
744
1015
  makeEffectORPC,