effect-orpc 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -318,6 +318,55 @@ runtime-agnostic.
318
318
  If you do not need framework-to-handler fiber propagation, you do not need the
319
319
  `/node` entrypoint at all.
320
320
 
321
+ ## Contract-First Usage
322
+
323
+ Use `implementEffect(contract, runtime)` when you already have an oRPC contract
324
+ and want to keep contract-first enforcement while adding Effect-native handlers.
325
+ Use `makeEffectORPC(runtime, builder?)` when you want to build procedures
326
+ directly from an oRPC builder.
327
+
328
+ ```ts
329
+ import { Effect, ManagedRuntime } from "effect";
330
+ import { eoc, implementEffect } from "effect-orpc";
331
+ import z from "zod";
332
+
333
+ class UsersRepo extends Effect.Service<UsersRepo>()("UsersRepo", {
334
+ accessors: true,
335
+ sync: () => ({
336
+ list: (amount: number) =>
337
+ Array.from({ length: amount }, (_, index) => `user-${index + 1}`),
338
+ }),
339
+ }) {}
340
+
341
+ const contract = {
342
+ users: {
343
+ list: eoc
344
+ .input(z.object({ amount: z.number().int().positive() }))
345
+ .output(z.array(z.string())),
346
+ },
347
+ };
348
+
349
+ const runtime = ManagedRuntime.make(UsersRepo.Default);
350
+ const oe = implementEffect(contract, runtime);
351
+
352
+ export const router = oe.router({
353
+ users: {
354
+ list: oe.users.list.effect(function* ({ input }) {
355
+ return yield* UsersRepo.list(input.amount);
356
+ }),
357
+ },
358
+ });
359
+ ```
360
+
361
+ Contract leaves keep the contract-defined input, output, and error surface.
362
+ They add `.effect(...)` alongside existing implementer methods such as
363
+ `.handler(...)` and `.use(...)`, but do not expose contract-changing builder
364
+ methods like `.input(...)` or `.output(...)`.
365
+
366
+ If your contract declares tagged Effect error classes, prefer `eoc.errors(...)`
367
+ instead of raw `oc.errors(...)` so the error schema and metadata are derived
368
+ directly from the `ORPCTaggedError` class.
369
+
321
370
  ## API Reference
322
371
 
323
372
  ### `makeEffectORPC(runtime, builder?)`
@@ -337,6 +386,52 @@ const effectOs = makeEffectORPC(runtime);
337
386
  const effectAuthedOs = makeEffectORPC(runtime, authedBuilder);
338
387
  ```
339
388
 
389
+ ### `implementEffect(contract, runtime)`
390
+
391
+ Creates an Effect-aware contract implementer.
392
+
393
+ - `contract` - An oRPC contract router built with `oc`
394
+ - `runtime` - A `ManagedRuntime<R, E>` instance that provides services for Effect procedures
395
+
396
+ Returns a contract-shaped implementer tree whose leaves support `.effect(...)`.
397
+
398
+ ```ts
399
+ const oe = implementEffect(contract, runtime);
400
+
401
+ const router = oe.router({
402
+ users: {
403
+ list: oe.users.list.effect(function* ({ input }) {
404
+ return yield* UsersRepo.list(input.amount);
405
+ }),
406
+ },
407
+ });
408
+ ```
409
+
410
+ ### `eoc`
411
+
412
+ An Effect-aware wrapper around oRPC's `oc` contract builder.
413
+
414
+ Use it when you want contract definitions to accept `ORPCTaggedError` classes
415
+ directly in `.errors(...)` without duplicating the error schema.
416
+
417
+ ```ts
418
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
419
+ code: "NOT_FOUND",
420
+ schema: z.object({ userId: z.string() }),
421
+ }) {}
422
+
423
+ const contract = {
424
+ users: {
425
+ find: eoc
426
+ .errors({
427
+ NOT_FOUND: UserNotFoundError,
428
+ })
429
+ .input(z.object({ userId: z.string() }))
430
+ .output(z.object({ userId: z.string() })),
431
+ },
432
+ };
433
+ ```
434
+
340
435
  ### `EffectBuilder`
341
436
 
342
437
  Wraps an oRPC Builder with Effect support. Available methods:
package/dist/index.js CHANGED
@@ -2,14 +2,12 @@ import {
2
2
  getCurrentFiberRefs
3
3
  } from "./chunk-VOWRLWZZ.js";
4
4
 
5
+ // src/contract.ts
6
+ import { isContractProcedure as isContractProcedure2 } from "@orpc/contract";
7
+ import { implement } from "@orpc/server";
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,7 +15,6 @@ import {
17
15
  fallbackConfig,
18
16
  lazy as lazy2
19
17
  } from "@orpc/server";
20
- import { Cause as Cause2, Effect, Exit, FiberRefs } from "effect";
21
18
 
22
19
  // src/effect-enhance-router.ts
23
20
  import {
@@ -323,6 +320,86 @@ function enhanceEffectRouter(router, options) {
323
320
  return enhanced;
324
321
  }
325
322
 
323
+ // src/effect-runtime.ts
324
+ import { ORPCError as ORPCError2 } from "@orpc/contract";
325
+ import { Cause as Cause2, Effect, Exit, FiberRefs } from "effect";
326
+ function toORPCErrorFromCause(cause) {
327
+ return Cause2.match(cause, {
328
+ onDie(defect) {
329
+ return new ORPCError2("INTERNAL_SERVER_ERROR", {
330
+ cause: defect
331
+ });
332
+ },
333
+ onFail(error) {
334
+ if (isORPCTaggedError(error)) {
335
+ return error.toORPCError();
336
+ }
337
+ if (error instanceof ORPCError2) {
338
+ return error;
339
+ }
340
+ return new ORPCError2("INTERNAL_SERVER_ERROR", {
341
+ cause: error
342
+ });
343
+ },
344
+ onInterrupt(fiberId) {
345
+ return new ORPCError2("INTERNAL_SERVER_ERROR", {
346
+ cause: new Error(`${fiberId} Interrupted`)
347
+ });
348
+ },
349
+ onSequential(left) {
350
+ return left;
351
+ },
352
+ onEmpty: new ORPCError2("INTERNAL_SERVER_ERROR", {
353
+ cause: new Error("Unknown error")
354
+ }),
355
+ onParallel(left) {
356
+ return left;
357
+ }
358
+ });
359
+ }
360
+ function createEffectProcedureHandler(options) {
361
+ const {
362
+ runtime,
363
+ effectErrorMap,
364
+ effectFn,
365
+ spanConfig,
366
+ defaultCaptureStackTrace
367
+ } = options;
368
+ return async (opts) => {
369
+ const effectOpts = {
370
+ context: opts.context,
371
+ input: opts.input,
372
+ path: opts.path,
373
+ procedure: opts.procedure,
374
+ signal: opts.signal,
375
+ lastEventId: opts.lastEventId,
376
+ errors: createEffectErrorConstructorMap(effectErrorMap)
377
+ };
378
+ const spanName = spanConfig?.name ?? opts.path.join(".");
379
+ const captureStackTrace = spanConfig?.captureStackTrace ?? defaultCaptureStackTrace;
380
+ const resolver = Effect.fnUntraced(effectFn);
381
+ const tracedEffect = Effect.withSpan(resolver(effectOpts), spanName, {
382
+ captureStackTrace
383
+ });
384
+ const parentFiberRefs = getCurrentFiberRefs();
385
+ const effectWithRefs = parentFiberRefs ? Effect.fiberIdWith(
386
+ (fiberId) => Effect.flatMap(
387
+ Effect.getFiberRefs,
388
+ (fiberRefs) => Effect.setFiberRefs(
389
+ FiberRefs.joinAs(fiberRefs, fiberId, parentFiberRefs)
390
+ ).pipe(Effect.andThen(tracedEffect))
391
+ )
392
+ ) : tracedEffect;
393
+ const exit = await runtime.runPromiseExit(effectWithRefs, {
394
+ signal: opts.signal
395
+ });
396
+ if (Exit.isFailure(exit)) {
397
+ throw toORPCErrorFromCause(exit.cause);
398
+ }
399
+ return exit.value;
400
+ };
401
+ }
402
+
326
403
  // src/effect-builder.ts
327
404
  function addSpanStackTrace() {
328
405
  const ErrorConstructor = Error;
@@ -436,6 +513,14 @@ var EffectBuilder = class _EffectBuilder {
436
513
  inputSchema: initialInputSchema
437
514
  });
438
515
  }
516
+ /**
517
+ * Creates a middleware.
518
+ *
519
+ * @see {@link https://orpc.dev/docs/middleware Middleware Docs}
520
+ */
521
+ middleware(middleware) {
522
+ return decorateMiddleware2(middleware);
523
+ }
439
524
  /**
440
525
  * Adds type-safe custom errors.
441
526
  * Supports both traditional oRPC error definitions and ORPCTaggedError classes.
@@ -583,70 +668,13 @@ var EffectBuilder = class _EffectBuilder {
583
668
  return new EffectDecoratedProcedure({
584
669
  ...this["~effect"],
585
670
  handler: async (opts) => {
586
- const effectOpts = {
587
- context: opts.context,
588
- input: opts.input,
589
- path: opts.path,
590
- procedure: opts.procedure,
591
- signal: opts.signal,
592
- lastEventId: opts.lastEventId,
593
- errors: createEffectErrorConstructorMap(
594
- this["~effect"].effectErrorMap
595
- )
596
- };
597
- const spanName = spanConfig?.name ?? opts.path.join(".");
598
- const captureStackTrace = spanConfig?.captureStackTrace ?? defaultCaptureStackTrace;
599
- const resolver = Effect.fnUntraced(effectFn);
600
- const tracedEffect = Effect.withSpan(resolver(effectOpts), spanName, {
601
- captureStackTrace
602
- });
603
- const parentFiberRefs = getCurrentFiberRefs();
604
- const effectWithRefs = parentFiberRefs ? Effect.fiberIdWith(
605
- (fiberId) => Effect.flatMap(
606
- Effect.getFiberRefs,
607
- (fiberRefs) => Effect.setFiberRefs(
608
- FiberRefs.joinAs(fiberRefs, fiberId, parentFiberRefs)
609
- ).pipe(Effect.andThen(tracedEffect))
610
- )
611
- ) : tracedEffect;
612
- const exit = await runtime.runPromiseExit(effectWithRefs, {
613
- signal: opts.signal
614
- });
615
- if (Exit.isFailure(exit)) {
616
- throw Cause2.match(exit.cause, {
617
- onDie(defect) {
618
- return new ORPCError2("INTERNAL_SERVER_ERROR", {
619
- cause: defect
620
- });
621
- },
622
- onFail(error) {
623
- if (isORPCTaggedError(error)) {
624
- return error.toORPCError();
625
- }
626
- if (error instanceof ORPCError2) {
627
- return error;
628
- }
629
- return new ORPCError2("INTERNAL_SERVER_ERROR", {
630
- cause: error
631
- });
632
- },
633
- onInterrupt(fiberId) {
634
- return new ORPCError2("INTERNAL_SERVER_ERROR", {
635
- cause: new Error(`${fiberId} Interrupted`)
636
- });
637
- },
638
- onSequential(left) {
639
- return left;
640
- },
641
- onEmpty: new ORPCError2("INTERNAL_SERVER_ERROR", {
642
- cause: new Error("Unknown error")
643
- }),
644
- onParallel(left) {
645
- return left;
646
- }
647
- });
648
- }
649
- return exit.value;
671
+ return createEffectProcedureHandler({
672
+ runtime,
673
+ effectErrorMap: this["~effect"].effectErrorMap,
674
+ effectFn,
675
+ spanConfig,
676
+ defaultCaptureStackTrace
677
+ })(opts);
650
678
  }
651
679
  });
652
680
  }
@@ -719,6 +747,256 @@ function emptyBuilder() {
719
747
  dedupeLeadingMiddlewares: true
720
748
  });
721
749
  }
750
+
751
+ // src/eoc.ts
752
+ import { isContractProcedure, oc } from "@orpc/contract";
753
+ var effectContractSymbol = /* @__PURE__ */ Symbol.for(
754
+ "@orpc/effect/contract"
755
+ );
756
+ function isWrappableContractBuilder(value) {
757
+ return typeof value === "object" && value !== null && "~orpc" in value;
758
+ }
759
+ function mergeEffectErrorMaps(left, right) {
760
+ if (!left) {
761
+ return right;
762
+ }
763
+ if (!right) {
764
+ return left;
765
+ }
766
+ return {
767
+ ...left,
768
+ ...right
769
+ };
770
+ }
771
+ function setEffectContractErrorMap(value, effectErrorMap) {
772
+ if (!effectErrorMap) {
773
+ return;
774
+ }
775
+ Object.defineProperty(value, effectContractSymbol, {
776
+ value: { errorMap: effectErrorMap },
777
+ enumerable: false,
778
+ configurable: true
779
+ });
780
+ }
781
+ function getEffectContractErrorMap(value) {
782
+ if (typeof value !== "object" || value === null) {
783
+ return void 0;
784
+ }
785
+ return value[effectContractSymbol]?.errorMap;
786
+ }
787
+ function applyEffectContractErrorMapToRouter(router, source, inheritedEffectErrorMap) {
788
+ const routerRecord = router;
789
+ const sourceRecord = source;
790
+ for (const key of Object.keys(routerRecord)) {
791
+ const routerValue = routerRecord[key];
792
+ const sourceValue = sourceRecord && typeof sourceRecord === "object" ? sourceRecord[key] : void 0;
793
+ if (!routerValue) {
794
+ continue;
795
+ }
796
+ if (isContractProcedure(routerValue)) {
797
+ const sourceEffectErrorMap = getEffectContractErrorMap(sourceValue);
798
+ setEffectContractErrorMap(
799
+ routerValue,
800
+ mergeEffectErrorMaps(inheritedEffectErrorMap, sourceEffectErrorMap)
801
+ );
802
+ continue;
803
+ }
804
+ if (typeof routerValue === "object") {
805
+ applyEffectContractErrorMapToRouter(
806
+ routerValue,
807
+ sourceValue,
808
+ inheritedEffectErrorMap
809
+ );
810
+ }
811
+ }
812
+ }
813
+ function wrapEffectContractBuilder(builder, inheritedEffectErrorMap) {
814
+ const currentEffectErrorMap = inheritedEffectErrorMap ?? getEffectContractErrorMap(builder);
815
+ if (typeof builder === "object" && builder !== null) {
816
+ setEffectContractErrorMap(builder, currentEffectErrorMap);
817
+ }
818
+ const proxy = new Proxy(builder, {
819
+ get(target, prop, receiver) {
820
+ if (prop === effectContractSymbol) {
821
+ return currentEffectErrorMap ? { errorMap: currentEffectErrorMap } : void 0;
822
+ }
823
+ if (prop === "errors") {
824
+ return (errors) => {
825
+ const nextEffectErrorMap = mergeEffectErrorMaps(
826
+ currentEffectErrorMap,
827
+ errors
828
+ );
829
+ return wrapEffectContractBuilder(
830
+ Reflect.apply(Reflect.get(target, prop, receiver), target, [
831
+ effectErrorMapToErrorMap(errors)
832
+ ]),
833
+ nextEffectErrorMap
834
+ );
835
+ };
836
+ }
837
+ if (prop === "router") {
838
+ return (router) => {
839
+ const result = Reflect.apply(
840
+ Reflect.get(target, prop, receiver),
841
+ target,
842
+ [router]
843
+ );
844
+ applyEffectContractErrorMapToRouter(
845
+ result,
846
+ router,
847
+ currentEffectErrorMap
848
+ );
849
+ return result;
850
+ };
851
+ }
852
+ const value = Reflect.get(target, prop, receiver);
853
+ if (typeof value !== "function") {
854
+ return value;
855
+ }
856
+ return (...args) => {
857
+ const result = Reflect.apply(value, target, args);
858
+ return isWrappableContractBuilder(result) ? wrapEffectContractBuilder(result, currentEffectErrorMap) : result;
859
+ };
860
+ }
861
+ });
862
+ setEffectContractErrorMap(proxy, currentEffectErrorMap);
863
+ return proxy;
864
+ }
865
+ var eoc = wrapEffectContractBuilder(
866
+ oc,
867
+ {}
868
+ );
869
+
870
+ // src/contract.ts
871
+ var CONTRACT_HIDDEN_METHODS = /* @__PURE__ */ new Set([
872
+ "$config",
873
+ "$context",
874
+ "$input",
875
+ "$meta",
876
+ "$route",
877
+ "errors",
878
+ "input",
879
+ "lazy",
880
+ "meta",
881
+ "middleware",
882
+ "output",
883
+ "prefix",
884
+ "route",
885
+ "router",
886
+ "tag"
887
+ ]);
888
+ function makeEnhanceOptions(runtime) {
889
+ return {
890
+ middlewares: [],
891
+ errorMap: {},
892
+ dedupeLeadingMiddlewares: true,
893
+ runtime
894
+ };
895
+ }
896
+ function wrapContractNode(contract, target, runtime) {
897
+ const cache = /* @__PURE__ */ new Map();
898
+ return new Proxy(target, {
899
+ get(currentTarget, prop, receiver) {
900
+ if (cache.has(prop)) {
901
+ return cache.get(prop);
902
+ }
903
+ if (isContractProcedure2(contract)) {
904
+ if (prop === "effect") {
905
+ const effect = (effectFn) => {
906
+ const effectErrorMap = getEffectContractErrorMap(contract) ?? currentTarget["~orpc"].errorMap;
907
+ return new EffectDecoratedProcedure({
908
+ ...currentTarget["~orpc"],
909
+ errorMap: effectErrorMapToErrorMap(effectErrorMap),
910
+ effectErrorMap,
911
+ runtime,
912
+ handler: createEffectProcedureHandler({
913
+ runtime,
914
+ effectErrorMap,
915
+ effectFn,
916
+ defaultCaptureStackTrace: addSpanStackTrace()
917
+ })
918
+ });
919
+ };
920
+ cache.set(prop, effect);
921
+ return effect;
922
+ }
923
+ if (prop === "use") {
924
+ const use = (...args) => wrapContractNode(
925
+ contract,
926
+ Reflect.apply(
927
+ Reflect.get(currentTarget, prop, currentTarget),
928
+ currentTarget,
929
+ args
930
+ ),
931
+ runtime
932
+ );
933
+ cache.set(prop, use);
934
+ return use;
935
+ }
936
+ if (CONTRACT_HIDDEN_METHODS.has(String(prop))) {
937
+ return void 0;
938
+ }
939
+ } else {
940
+ if (prop === "$context" || prop === "$config" || prop === "use") {
941
+ const wrappedMethod = (...args) => wrapContractNode(
942
+ contract,
943
+ Reflect.apply(
944
+ Reflect.get(currentTarget, prop, currentTarget),
945
+ currentTarget,
946
+ args
947
+ ),
948
+ runtime
949
+ );
950
+ cache.set(prop, wrappedMethod);
951
+ return wrappedMethod;
952
+ }
953
+ if (prop === "router" || prop === "lazy") {
954
+ const wrappedMethod = (...args) => enhanceEffectRouter(
955
+ Reflect.apply(
956
+ Reflect.get(currentTarget, prop, currentTarget),
957
+ currentTarget,
958
+ args
959
+ ),
960
+ makeEnhanceOptions(runtime)
961
+ );
962
+ cache.set(prop, wrappedMethod);
963
+ return wrappedMethod;
964
+ }
965
+ if (typeof prop === "string" && prop in contract) {
966
+ const child = wrapContractNode(
967
+ contract[prop],
968
+ Reflect.get(currentTarget, prop, receiver),
969
+ runtime
970
+ );
971
+ cache.set(prop, child);
972
+ return child;
973
+ }
974
+ }
975
+ const value = Reflect.get(currentTarget, prop, receiver);
976
+ return typeof value === "function" ? value.bind(currentTarget) : value;
977
+ },
978
+ has(currentTarget, prop) {
979
+ if (isContractProcedure2(contract)) {
980
+ if (prop === "effect") {
981
+ return true;
982
+ }
983
+ if (CONTRACT_HIDDEN_METHODS.has(String(prop))) {
984
+ return false;
985
+ }
986
+ } else if (typeof prop === "string" && prop in contract) {
987
+ return true;
988
+ }
989
+ return Reflect.has(currentTarget, prop);
990
+ }
991
+ });
992
+ }
993
+ function implementEffect(contract, runtime) {
994
+ return wrapContractNode(
995
+ contract,
996
+ implement(contract),
997
+ runtime
998
+ );
999
+ }
722
1000
  export {
723
1001
  EffectBuilder,
724
1002
  EffectDecoratedProcedure,
@@ -727,6 +1005,8 @@ export {
727
1005
  addSpanStackTrace,
728
1006
  createEffectErrorConstructorMap,
729
1007
  effectErrorMapToErrorMap,
1008
+ eoc,
1009
+ implementEffect,
730
1010
  isORPCTaggedError,
731
1011
  isORPCTaggedErrorClass,
732
1012
  makeEffectORPC,