diff --git a/.changeset/fluffy-times-shave.md b/.changeset/fluffy-times-shave.md new file mode 100644 index 0000000..bad6a64 --- /dev/null +++ b/.changeset/fluffy-times-shave.md @@ -0,0 +1,5 @@ +--- +"effect-orpc": major +--- + +migrate to effect-v4 (effect-smol) diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..2ee4be3 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,12 @@ +{ + "mode": "pre", + "tag": "effect-v4", + "initialVersions": { + "effect-orpc": "0.1.2" + }, + "changesets": [ + "dull-spiders-smash", + "fluffy-times-shave", + "silly-doodles-cover" + ] +} diff --git a/.changeset/silly-doodles-cover.md b/.changeset/silly-doodles-cover.md new file mode 100644 index 0000000..16811e9 --- /dev/null +++ b/.changeset/silly-doodles-cover.md @@ -0,0 +1,5 @@ +--- +"effect-orpc": patch +--- + +docs: remove duplicate request-scoped context section diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f40f90e..122cef9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - effect-v4 concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/README.md b/README.md index 610690a..d68c551 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Runnable demos live in the repository's `examples/` directory. ```ts import { os } from "@orpc/server"; -import { Effect, ManagedRuntime } from "effect"; +import { Effect, Layer, ManagedRuntime, ServiceMap } from "effect"; import { makeEffectORPC, ORPCTaggedError } from "effect-orpc"; interface User { @@ -53,12 +53,16 @@ const authedOs = os }); // Define your services -class UsersRepo extends Effect.Service()("UsersRepo", { - accessors: true, - sync: () => ({ +class UsersRepo extends ServiceMap.Service< + UsersRepo, + { + readonly get: (id: number) => User | undefined; + } +>()("UsersRepo") { + static readonly layer = Layer.succeed(this, { get: (id: number) => users.find((u) => u.id === id), - }), -}) {} + }); +} // Special yieldable oRPC error class class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", { @@ -66,7 +70,7 @@ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", { }) {} // Create runtime with your services -const runtime = ManagedRuntime.make(UsersRepo.Default); +const runtime = ManagedRuntime.make(UsersRepo.layer); // Create Effect-aware oRPC builder from an other (optional) base oRPC builder and provide tagged errors const effectOs = makeEffectORPC(runtime, authedOs).errors({ UserNotFoundError, @@ -77,7 +81,8 @@ export const router = { health: os.handler(() => "ok"), users: { me: effectOs.effect(function* ({ context: { userId } }) { - const user = yield* UsersRepo.get(userId); + const usersRepo = yield* UsersRepo; + const user = usersRepo.get(userId); if (!user) { return yield* new UserNotFoundError(); } @@ -94,18 +99,22 @@ export type Router = typeof router; 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: ```ts -import { Context, Effect, Layer, ManagedRuntime } from "effect"; +import { Effect, Layer, ManagedRuntime, ServiceMap } from "effect"; import { makeEffectORPC } from "effect-orpc"; -class ProvidedService extends Context.Tag("ProvidedService")< +class ProvidedService extends ServiceMap.Service< ProvidedService, - { doSomething: () => Effect.Effect } ->() {} + { + readonly doSomething: () => Effect.Effect; + } +>()("ProvidedService") {} -class MissingService extends Context.Tag("MissingService")< +class MissingService extends ServiceMap.Service< MissingService, - { doSomething: () => Effect.Effect } ->() {} + { + readonly doSomething: () => Effect.Effect; + } +>()("MissingService") {} const runtime = ManagedRuntime.make( Layer.succeed(ProvidedService, { diff --git a/bun.lock b/bun.lock index 8d6f1be..dd8dc09 100644 --- a/bun.lock +++ b/bun.lock @@ -15,19 +15,19 @@ "examples/hono": { "name": "effect-orpc-example-hono", "dependencies": { - "@effect/opentelemetry": "^0.60.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.210.0", - "@opentelemetry/sdk-metrics": "^2.4.0", - "@opentelemetry/sdk-trace-base": "^2.4.0", - "@opentelemetry/sdk-trace-node": "^2.4.0", - "@opentelemetry/sdk-trace-web": "^2.4.0", + "@effect/opentelemetry": "^4.0.0-beta.27", + "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", + "@opentelemetry/sdk-metrics": "^2.5.0", + "@opentelemetry/sdk-trace-base": "^2.5.0", + "@opentelemetry/sdk-trace-node": "^2.5.0", + "@opentelemetry/sdk-trace-web": "^2.5.0", "@orpc/client": "^1.13.4", "@orpc/contract": "^1.13.4", "@orpc/openapi": "^1.13.4", "@orpc/server": "^1.13.4", "@orpc/zod": "^1.13.4", "@types/bun": "latest", - "effect": "^3.19.14", + "effect": "^4.0.0-beta.27", "effect-orpc": "workspace:*", "hono": "^4.11.4", "typescript": "^5.9.3", @@ -40,7 +40,7 @@ "version": "0.1.4", "devDependencies": { "@types/bun": "latest", - "effect": "^3.18.4", + "effect": "^4.0.0-beta.27", "oxfmt": "^0.21.0", "oxlint": "^1.36.0", "tsup": "^8.5.1", @@ -52,7 +52,7 @@ "@orpc/contract": ">=1.13.0", "@orpc/server": ">=1.13.0", "@orpc/shared": ">=1.13.0", - "effect": ">=3.18.0", + "effect": ">=4.0.0-beta.27", "typescript": "^5", }, }, @@ -101,9 +101,7 @@ "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], - "@effect/opentelemetry": ["@effect/opentelemetry@0.60.0", "", { "peerDependencies": { "@effect/platform": "^0.94.0", "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^3.19.13" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-YaKCKdtvzQ+efMCSg7f9583zxQU1TSyPSiTn4D8tnd/+okxuYrz2/RKbb+7qAUT1cajV6UIcdX/1lSLJi8uIew=="], - - "@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.28", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.28" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-n5EMb+JCrPx7mqOgfaAWvSXLFWLQopL43ZJnrNlYWJW5X+WJvHJoirwYly5h5i/fZkMkGd9Ztq/DhoiP7wAkwQ=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -191,21 +189,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.210.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q=="], - "@opentelemetry/core": ["@opentelemetry/core@2.4.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw=="], + "@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], - "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9JkyaCl70anEtuKZdoCQmjDuz1/paEixY/DWfsvHt7PGKq3t8/nQ/6/xwxHjG+SkPAUbo1Iq4h7STe7Pk2bc5A=="], + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-transformer": "0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uk78DcZoBNHIm26h0oXc8Pizh4KDJ/y04N5k/UaI9J7xR7mL8QcMcYPQG9xxN7m8qotXOMDRW6qTAyptav4+3w=="], + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="], - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-logs": "0.210.0", "@opentelemetry/sdk-metrics": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0", "protobufjs": "8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nkHBJVSJGOwkRZl+BFIr7gikA93/U8XkL2EWaiDbj3DVjmTEZQpegIKk0lT8oqQYfP8FC6zWNjuTfkaBVqa0ZQ=="], + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-logs": "0.211.0", "@opentelemetry/sdk-metrics": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0", "protobufjs": "8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], - "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ=="], + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA=="], "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw=="], @@ -485,7 +483,7 @@ "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - "effect": ["effect@3.19.14", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-3vwdq0zlvQOxXzXNKRIPKTqZNMyGCdaFUBfMPqpsyzZDre67kgC1EEHDV4EoQTovJ4w5fmJW756f86kkuz7WFA=="], + "effect": ["effect@4.0.0-beta.28", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-9T/LPIF/t54DN/z1gTnJ3Jwv4s6iWlN3w8Jvg7koDLI4whLXDEsnXWhMf05yQZUSNRJ0naVV1Ltwr2+cEj0HZA=="], "effect-orpc": ["effect-orpc@workspace:packages/effect-orpc"], @@ -507,7 +505,7 @@ "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], - "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-check": ["fast-check@4.5.3", "", { "dependencies": { "pure-rand": "^7.0.0" } }, "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -543,6 +541,8 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -565,6 +565,8 @@ "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -661,7 +663,7 @@ "protobufjs": ["protobufjs@8.0.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw=="], - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -737,6 +739,8 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], @@ -769,6 +773,8 @@ "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], @@ -779,6 +785,8 @@ "wildcard-match": ["wildcard-match@5.1.4", "", {}, "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], @@ -789,11 +797,11 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], - "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw=="], + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA=="], - "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw=="], + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg=="], diff --git a/examples/README.md b/examples/README.md index 14ce79b..2142f4f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,5 +8,5 @@ workspaces. ## Available Examples -- `hono-request-context`: Hono + oRPC + Effect with request-scoped `FiberRef` +- `hono-request-context`: Hono + oRPC + Effect v4 with request-scoped context propagation using `withFiberContext` from `effect-orpc/node`. diff --git a/examples/hono/README.md b/examples/hono/README.md index e318265..b0b8091 100644 --- a/examples/hono/README.md +++ b/examples/hono/README.md @@ -9,7 +9,8 @@ It demonstrates: - `makeEffectORPC` imported from `effect-orpc` - `eoc` + `implementEffect` contract routes imported from `effect-orpc` - `withFiberContext(() => next())` imported from `effect-orpc/node` -- nested Effect services preserving the same request-scoped annotations +- nested Effect services preserving the same request-scoped annotations and + references - a contract router with: - shared router-level errors, prefixes, and OpenAPI tags - per-procedure metadata, inputs, outputs, and route definitions diff --git a/examples/hono/orpc/router.ts b/examples/hono/orpc/router.ts index d3c2431..7943fa9 100644 --- a/examples/hono/orpc/router.ts +++ b/examples/hono/orpc/router.ts @@ -41,7 +41,8 @@ const directRouter = { .output(z.array(orderSchema)) .effect(function* () { yield* Effect.logInfo("Handler: GET /orders - listing all orders"); - const orders = yield* OrderService.listOrders(); + const service = yield* OrderService; + const orders = yield* service.listOrders(); return orders.map(toTypedOrder); }), test: directProcedureBuilder @@ -104,7 +105,10 @@ const contractRouter = contractImplementer.router({ }, orders: { list: ordersImplementer.list.effect(function* ({ context, input }) { - const orders = (yield* OrderService.listOrders()).map(toTypedOrder); + const service = yield* OrderService; + const orders = yield* service + .listOrders() + .pipe(Effect.map((o) => o.map(toTypedOrder))); const filtered = input.status ? orders.filter((order) => order.status === input.status) : orders; @@ -135,7 +139,8 @@ const contractRouter = contractImplementer.router({ ) .effect(function* ({ context, input, errors }) { const normalizedOrderId = input.orderId.trim().toUpperCase(); - const order = yield* OrderService.getOrder(normalizedOrderId); + const service = yield* OrderService; + const order = yield* service.getOrder(normalizedOrderId); if (order.status === "not found") { return yield* Effect.fail( @@ -215,9 +220,8 @@ const contractRouter = contractImplementer.router({ ); } - const order = yield* OrderService.getOrder( - input.orderId.trim().toUpperCase(), - ); + const service = yield* OrderService; + const order = yield* service.getOrder(input.orderId.trim().toUpperCase()); if (order.status === "not found") { return yield* Effect.fail( @@ -264,8 +268,9 @@ const contractRouter = contractImplementer.router({ ); } + const service = yield* OrderService; const orders = yield* Effect.forEach(input.orderIds, (orderId) => - OrderService.getOrder(orderId.trim().toUpperCase()), + service.getOrder(orderId.trim().toUpperCase()), ); return { @@ -293,8 +298,11 @@ const contractRouter = contractImplementer.router({ ); } - const orders = (yield* OrderService.listOrders()).map(toTypedOrder); - yield* Effect.forEach(orders, (order) => OrderService.getOrder(order.id)); + const service = yield* OrderService; + const orders = yield* service + .listOrders() + .pipe(Effect.map((o) => o.map(toTypedOrder))); + yield* Effect.forEach(orders, (order) => service.getOrder(order.id)); return { requestId: context.requestId, diff --git a/examples/hono/package.json b/examples/hono/package.json index 6c2bfb9..6b944c8 100644 --- a/examples/hono/package.json +++ b/examples/hono/package.json @@ -7,19 +7,19 @@ "test": "vitest *.test.ts" }, "dependencies": { - "@effect/opentelemetry": "^0.60.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.210.0", - "@opentelemetry/sdk-metrics": "^2.4.0", - "@opentelemetry/sdk-trace-base": "^2.4.0", - "@opentelemetry/sdk-trace-node": "^2.4.0", - "@opentelemetry/sdk-trace-web": "^2.4.0", + "@effect/opentelemetry": "^4.0.0-beta.27", + "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", + "@opentelemetry/sdk-metrics": "^2.5.0", + "@opentelemetry/sdk-trace-base": "^2.5.0", + "@opentelemetry/sdk-trace-node": "^2.5.0", + "@opentelemetry/sdk-trace-web": "^2.5.0", "@orpc/client": "^1.13.4", "@orpc/contract": "^1.13.4", "@orpc/openapi": "^1.13.4", "@orpc/server": "^1.13.4", "@orpc/zod": "^1.13.4", "@types/bun": "latest", - "effect": "^3.19.14", + "effect": "^4.0.0-beta.27", "effect-orpc": "workspace:*", "hono": "^4.11.4", "typescript": "^5.9.3", diff --git a/examples/hono/runtime.ts b/examples/hono/runtime.ts index b5c28e7..7322c17 100644 --- a/examples/hono/runtime.ts +++ b/examples/hono/runtime.ts @@ -14,8 +14,12 @@ const NodeSdkLive = NodeSdk.layer(() => ({ ), })); -export const AppLive = Layer.mergeAll(Logger.pretty, OrderService.Default).pipe( - Layer.provideMerge(NodeSdkLive), +const LoggerLive = Logger.layer([Logger.consolePretty()]); + +export const AppLive = Layer.mergeAll( + LoggerLive, + NodeSdkLive, + OrderService.layer, ); export const runtime = ManagedRuntime.make(AppLive); diff --git a/examples/hono/services/cache.ts b/examples/hono/services/cache.ts index 7e3bd8c..34135ca 100644 --- a/examples/hono/services/cache.ts +++ b/examples/hono/services/cache.ts @@ -1,46 +1,51 @@ -import { Effect } from "effect"; +import { Effect, Layer, ServiceMap } from "effect"; import { DatabaseService } from "./database"; -export class CacheService extends Effect.Service()( - "CacheService", +export class CacheService extends ServiceMap.Service< + CacheService, { - accessors: true, - dependencies: [DatabaseService.Default], - effect: Effect.gen(function* () { - const db = yield* DatabaseService; - const cache = new Map(); + readonly get: (key: string) => Effect.Effect; + readonly set: (key: string, value: unknown) => Effect.Effect; + } +>()("CacheService", { + make: Effect.gen(function* () { + const db = yield* DatabaseService; + const cache = new Map(); - return { - get: (key: string) => - Effect.gen(function* () { - yield* Effect.logInfo(`Cache lookup: ${key}`); + return { + get: (key: string) => + Effect.gen(function* () { + yield* Effect.logInfo(`Cache lookup: ${key}`); - if (cache.has(key)) { - yield* Effect.logInfo(`Cache HIT: ${key}`); - return cache.get(key); - } + if (cache.has(key)) { + yield* Effect.logInfo(`Cache HIT: ${key}`); + return cache.get(key); + } - yield* Effect.logInfo(`Cache MISS: ${key}, fetching from DB`); - const result = - yield* db.query`SELECT * FROM cache WHERE key = '${key}'`; - cache.set(key, result); - return result; - }).pipe( - Effect.annotateLogs("service", "CacheService"), - Effect.withSpan("CacheService.get"), - ), - set: (key: string, value: unknown) => - Effect.gen(function* () { - yield* Effect.logInfo(`Cache set: ${key}`); - cache.set(key, value); - yield* db.insert("cache", { key, value }); - return true; - }).pipe( - Effect.annotateLogs("service", "CacheService"), - Effect.withSpan("CacheService.set"), - ), - }; - }), - }, -) {} + yield* Effect.logInfo(`Cache MISS: ${key}, fetching from DB`); + const result = + yield* db.query`SELECT * FROM cache WHERE key = '${key}'`; + cache.set(key, result); + return result; + }).pipe( + Effect.annotateLogs("service", "CacheService"), + Effect.withSpan("CacheService.get"), + ), + set: (key: string, value: unknown) => + Effect.gen(function* () { + yield* Effect.logInfo(`Cache set: ${key}`); + cache.set(key, value); + yield* db.insert("cache", { key, value }); + return true; + }).pipe( + Effect.annotateLogs("service", "CacheService"), + Effect.withSpan("CacheService.set"), + ), + }; + }), +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(DatabaseService.layer), + ); +} diff --git a/examples/hono/services/database.ts b/examples/hono/services/database.ts index 74ccaaa..1abd127 100644 --- a/examples/hono/services/database.ts +++ b/examples/hono/services/database.ts @@ -1,27 +1,39 @@ -import { Effect } from "effect"; +import { Effect, Layer, ServiceMap } from "effect"; -export class DatabaseService extends Effect.Service()( - "DatabaseService", +export class DatabaseService extends ServiceMap.Service< + DatabaseService, { - sync: () => ({ - query: (sql: TemplateStringsArray, ..._args: unknown[]) => - Effect.gen(function* () { - yield* Effect.logInfo(`Executing SQL: ${sql}`); - yield* Effect.sleep("10 millis"); - return { rows: [{ id: 1, data: "result" }], rowCount: 1 }; - }).pipe( - Effect.annotateLogs("service", "DatabaseService"), - Effect.withSpan("DatabaseService.query"), - ), - insert: (table: string, _data: unknown) => - Effect.gen(function* () { - yield* Effect.logInfo(`Inserting into ${table}`); - yield* Effect.sleep("15 millis"); - return { id: Math.floor(Math.random() * 1000), success: true }; - }).pipe( - Effect.annotateLogs("service", "DatabaseService"), - Effect.withSpan("DatabaseService.insert"), - ), - }), - }, -) {} + readonly query: ( + sql: TemplateStringsArray, + ...args: unknown[] + ) => Effect.Effect<{ + rows: Array<{ id: number; data: string }>; + rowCount: number; + }>; + readonly insert: ( + table: string, + data: unknown, + ) => Effect.Effect<{ id: number; success: boolean }>; + } +>()("DatabaseService") { + static readonly layer = Layer.succeed(this, { + query: (sql: TemplateStringsArray, ..._args: unknown[]) => + Effect.gen(function* () { + yield* Effect.logInfo(`Executing SQL: ${sql}`); + yield* Effect.sleep("10 millis"); + return { rows: [{ id: 1, data: "result" }], rowCount: 1 }; + }).pipe( + Effect.annotateLogs("service", "DatabaseService"), + Effect.withSpan("DatabaseService.query"), + ), + insert: (table: string, _data: unknown) => + Effect.gen(function* () { + yield* Effect.logInfo(`Inserting into ${table}`); + yield* Effect.sleep("15 millis"); + return { id: Math.floor(Math.random() * 1000), success: true }; + }).pipe( + Effect.annotateLogs("service", "DatabaseService"), + Effect.withSpan("DatabaseService.insert"), + ), + }); +} diff --git a/examples/hono/services/order.ts b/examples/hono/services/order.ts index 50fb17f..0a04793 100644 --- a/examples/hono/services/order.ts +++ b/examples/hono/services/order.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect"; +import { Effect, Layer, ServiceMap } from "effect"; import { CacheService } from "./cache"; @@ -15,64 +15,73 @@ const HARDCODED_ORDERS: Record< "ORD-003": { id: "ORD-003", items: ["headphones"], status: "delivered" }, }; -export class OrderService extends Effect.Service()( - "OrderService", +export class OrderService extends ServiceMap.Service< + OrderService, { - accessors: true, - dependencies: [CacheService.Default], - effect: Effect.gen(function* () { - const cache = yield* CacheService; + readonly getOrder: ( + orderId: string, + ) => Effect.Effect<{ id: string; items: string[]; status: string }>; + readonly listOrders: () => Effect.Effect< + Array<{ id: string; items: string[]; status: string }> + >; + } +>()("OrderService", { + make: Effect.gen(function* () { + const cache = yield* CacheService; - return { - getOrder: (orderId: string) => - Effect.gen(function* () { - yield* Effect.logInfo(`Fetching order: ${orderId}`); + return { + getOrder: (orderId: string) => + Effect.gen(function* () { + yield* Effect.logInfo(`Fetching order: ${orderId}`); - const cached = yield* cache.get(`order:${orderId}`); - if (cached && typeof cached === "object" && "id" in cached) { - return cached as { id: string; items: string[]; status: string }; - } + const cached = yield* cache.get(`order:${orderId}`); + if (cached && typeof cached === "object" && "id" in cached) { + return cached as { id: string; items: string[]; status: string }; + } - const order = HARDCODED_ORDERS[orderId]; - if (order) { - yield* cache.set(`order:${orderId}`, order); - return order; - } + const order = HARDCODED_ORDERS[orderId]; + if (order) { + yield* cache.set(`order:${orderId}`, order); + return order; + } - return { id: orderId, items: [], status: "not found" }; - }).pipe( - Effect.annotateLogs("service", "OrderService"), - Effect.withSpan("OrderService.getOrder"), - ), - listOrders: () => - Effect.gen(function* () { - yield* Effect.logInfo("Listing all orders").pipe( - Effect.annotateLogs({ count: 3 }), - ); + return { id: orderId, items: [], status: "not found" }; + }).pipe( + Effect.annotateLogs("service", "OrderService"), + Effect.withSpan("OrderService.getOrder"), + ), + listOrders: () => + Effect.gen(function* () { + yield* Effect.logInfo("Listing all orders").pipe( + Effect.annotateLogs({ count: 3 }), + ); - const orders: Array<{ - id: string; - items: string[]; - status: string; - }> = []; + const orders: Array<{ + id: string; + items: string[]; + status: string; + }> = []; - for (const orderId of Object.keys(HARDCODED_ORDERS)) { - const cached = yield* cache.get(`order:${orderId}`); - if (cached && typeof cached === "object" && "id" in cached) { - orders.push( - cached as { id: string; items: string[]; status: string }, - ); - } else { - orders.push(HARDCODED_ORDERS[orderId]!); - } + for (const orderId of Object.keys(HARDCODED_ORDERS)) { + const cached = yield* cache.get(`order:${orderId}`); + if (cached && typeof cached === "object" && "id" in cached) { + orders.push( + cached as { id: string; items: string[]; status: string }, + ); + } else { + orders.push(HARDCODED_ORDERS[orderId]!); } + } - return orders; - }).pipe( - Effect.annotateLogs("app-service", "OrderService"), - Effect.withSpan("OrderService.listOrders"), - ), - }; - }), - }, -) {} + return orders; + }).pipe( + Effect.annotateLogs("app-service", "OrderService"), + Effect.withSpan("OrderService.listOrders"), + ), + }; + }), +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(CacheService.layer), + ); +} diff --git a/packages/effect-orpc/CHANGELOG.md b/packages/effect-orpc/CHANGELOG.md index a204879..ab057c4 100644 --- a/packages/effect-orpc/CHANGELOG.md +++ b/packages/effect-orpc/CHANGELOG.md @@ -1,5 +1,29 @@ # effect-orpc +## 1.0.0-effect-v4.3 + +### Patch Changes + +- b1d95d7: Add README + +## 1.0.0-effect-v4.2 + +### Patch Changes + +- ed5bc70: Sync readme from root to package so that it gets published on NPM + +## 1.0.0-effect-v4.1 + +### Patch Changes + +- ac41539: docs: remove duplicate request-scoped context section + +## 1.0.0-effect-v4.0 + +### Major Changes + +- 045df4a: migrate to effect-v4 (effect-smol) + ## 0.2.0 ### Minor Changes diff --git a/packages/effect-orpc/package.json b/packages/effect-orpc/package.json index 9e32599..88d5fd2 100644 --- a/packages/effect-orpc/package.json +++ b/packages/effect-orpc/package.json @@ -1,6 +1,6 @@ { "name": "effect-orpc", - "version": "0.2.0", + "version": "1.0.0-effect-v4.3", "keywords": [ "effect", "orpc", @@ -48,7 +48,7 @@ }, "devDependencies": { "@types/bun": "latest", - "effect": "^3.18.4", + "effect": "^4.0.0-beta.27", "oxfmt": "^0.21.0", "oxlint": "^1.36.0", "tsup": "^8.5.1", @@ -60,7 +60,7 @@ "@orpc/contract": ">=1.13.0", "@orpc/server": ">=1.13.0", "@orpc/shared": ">=1.13.0", - "effect": ">=3.18.0", + "effect": ">=4.0.0-beta.27", "typescript": "^5" } } diff --git a/packages/effect-orpc/src/effect-enhance-router.ts b/packages/effect-orpc/src/effect-enhance-router.ts index c5b6293..1c5a41e 100644 --- a/packages/effect-orpc/src/effect-enhance-router.ts +++ b/packages/effect-orpc/src/effect-enhance-router.ts @@ -16,7 +16,7 @@ import { type Context, type Lazyable, } from "@orpc/server"; -import type { ManagedRuntime } from "effect/ManagedRuntime"; +import type { ManagedRuntime } from "effect"; import { EffectProcedure } from "./effect-procedure"; import { effectErrorMapToErrorMap, type EffectErrorMap } from "./tagged-error"; @@ -30,7 +30,7 @@ interface EnhanceEffectRouterOptions< middlewares: readonly AnyMiddleware[]; errorMap: TEffectErrorMap; dedupeLeadingMiddlewares: boolean; - runtime: ManagedRuntime; + runtime: ManagedRuntime.ManagedRuntime; } export function enhanceEffectRouter< diff --git a/packages/effect-orpc/src/effect-runtime.ts b/packages/effect-orpc/src/effect-runtime.ts index 1427b11..a6fb8c8 100644 --- a/packages/effect-orpc/src/effect-runtime.ts +++ b/packages/effect-orpc/src/effect-runtime.ts @@ -5,9 +5,9 @@ import type { ProcedureHandlerOptions, } from "@orpc/server"; import type { ManagedRuntime } from "effect"; -import { Cause, Effect, Exit, FiberRefs } from "effect"; +import { Cause, Effect, Exit, Result, ServiceMap } from "effect"; -import { getCurrentFiberRefs } from "./fiber-context-bridge"; +import { getCurrentServices } from "./service-context-bridge"; import type { EffectErrorConstructorMap, EffectErrorMap } from "./tagged-error"; import { createEffectErrorConstructorMap, @@ -18,38 +18,50 @@ import type { EffectProcedureHandler, EffectSpanConfig } from "./types"; export function toORPCErrorFromCause( cause: Cause.Cause, ): ORPCError { - return Cause.match(cause, { - onDie(defect) { - return new ORPCError("INTERNAL_SERVER_ERROR", { - cause: defect, - }); - }, - onFail(error) { - if (isORPCTaggedError(error)) { - return error.toORPCError(); - } - if (error instanceof ORPCError) { - return error; - } + if (Cause.hasFails(cause)) { + const reason = Cause.findFail(cause); + if (Result.isFailure(reason)) { + return new ORPCError("INTERNAL_SERVER_ERROR"); + } + + const error = reason.success.error; + if (isORPCTaggedError(error)) { + return error.toORPCError(); + } + if (error instanceof ORPCError) { + return error; + } + + return new ORPCError("INTERNAL_SERVER_ERROR", { + cause: error, + }); + } + + if (Cause.hasDies(cause)) { + const reason = Cause.findDie(cause); + if (Result.isFailure(reason)) { return new ORPCError("INTERNAL_SERVER_ERROR", { - cause: error, + cause: new Error(`Died by unknown reason`), }); - }, - onInterrupt(fiberId) { + } + return new ORPCError("INTERNAL_SERVER_ERROR", { + cause: reason.success.defect, + }); + } + + if (Cause.hasInterrupts(cause)) { + const reason = Cause.findInterrupt(cause); + if (Result.isFailure(reason)) { return new ORPCError("INTERNAL_SERVER_ERROR", { - cause: new Error(`${fiberId} Interrupted`), + cause: new Error(`Unknown fiber got interrupted`), }); - }, - onSequential(left) { - return left; - }, - onEmpty: new ORPCError("INTERNAL_SERVER_ERROR", { - cause: new Error("Unknown error"), - }), - onParallel(left) { - return left; - }, - }); + } + return new ORPCError("INTERNAL_SERVER_ERROR", { + cause: new Error(`${reason.success.fiberId} got interrupted`), + }); + } + + return new ORPCError("INTERNAL_SERVER_ERROR"); } export function createEffectProcedureHandler< @@ -111,19 +123,17 @@ export function createEffectProcedureHandler< const tracedEffect = Effect.withSpan(resolver(effectOpts), spanName, { captureStackTrace, }); - const parentFiberRefs = getCurrentFiberRefs(); - const effectWithRefs = parentFiberRefs - ? Effect.fiberIdWith((fiberId) => - Effect.flatMap(Effect.getFiberRefs, (fiberRefs) => - Effect.setFiberRefs( - FiberRefs.joinAs(fiberRefs, fiberId, parentFiberRefs), - ).pipe(Effect.andThen(tracedEffect)), - ), - ) - : tracedEffect; - const exit = await runtime.runPromiseExit(effectWithRefs, { - signal: opts.signal, - }); + + const parentServices = getCurrentServices(); + const exit = parentServices + ? await Effect.runPromiseExitWith( + ServiceMap.merge(await runtime.services(), parentServices), + )(tracedEffect, { + signal: opts.signal, + }) + : await runtime.runPromiseExit(tracedEffect, { + signal: opts.signal, + }); if (Exit.isFailure(exit)) { throw toORPCErrorFromCause(exit.cause); diff --git a/packages/effect-orpc/src/fiber-context-bridge.ts b/packages/effect-orpc/src/fiber-context-bridge.ts deleted file mode 100644 index da70d8b..0000000 --- a/packages/effect-orpc/src/fiber-context-bridge.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { FiberRefs } from "effect"; - -export interface FiberContextBridge { - readonly getCurrentFiberRefs: () => FiberRefs.FiberRefs | undefined; -} - -let bridge: FiberContextBridge | undefined; - -export function installFiberContextBridge( - nextBridge: FiberContextBridge | undefined, -): void { - bridge = nextBridge; -} - -export function getCurrentFiberRefs(): FiberRefs.FiberRefs | undefined { - return bridge?.getCurrentFiberRefs(); -} diff --git a/packages/effect-orpc/src/node.ts b/packages/effect-orpc/src/node.ts index a0de53c..da535b0 100644 --- a/packages/effect-orpc/src/node.ts +++ b/packages/effect-orpc/src/node.ts @@ -1,23 +1,25 @@ import { AsyncLocalStorage } from "node:async_hooks"; -import type { FiberRefs } from "effect"; +import type { ServiceMap } from "effect"; import { Effect } from "effect"; import { - installFiberContextBridge, - type FiberContextBridge, -} from "./fiber-context-bridge"; + installServiceContextBridge, + type ServiceContextBridge, +} from "./service-context-bridge"; -const fiberRefsStorage = new AsyncLocalStorage(); +const servicesStorage = new AsyncLocalStorage>(); -const bridge: FiberContextBridge = { - getCurrentFiberRefs: () => fiberRefsStorage.getStore(), +const bridge: ServiceContextBridge = { + getCurrentServices: () => servicesStorage.getStore(), }; -installFiberContextBridge(bridge); +installServiceContextBridge(bridge); -export function withFiberContext(fn: () => Promise): Effect.Effect { - return Effect.flatMap(Effect.getFiberRefs, (fiberRefs) => - Effect.promise(() => fiberRefsStorage.run(fiberRefs, fn)), +export function withFiberContext( + fn: () => Promise, +): Effect.Effect { + return Effect.flatMap(Effect.services(), (services) => + Effect.promise(() => servicesStorage.run(services, fn)), ); } diff --git a/packages/effect-orpc/src/service-context-bridge.ts b/packages/effect-orpc/src/service-context-bridge.ts new file mode 100644 index 0000000..089c52d --- /dev/null +++ b/packages/effect-orpc/src/service-context-bridge.ts @@ -0,0 +1,17 @@ +import type { ServiceMap } from "effect"; + +export interface ServiceContextBridge { + readonly getCurrentServices: () => ServiceMap.ServiceMap | undefined; +} + +let bridge: ServiceContextBridge | undefined; + +export function installServiceContextBridge( + nextBridge: ServiceContextBridge | undefined, +): void { + bridge = nextBridge; +} + +export function getCurrentServices(): ServiceMap.ServiceMap | undefined { + return bridge?.getCurrentServices(); +} diff --git a/packages/effect-orpc/src/tagged-error.ts b/packages/effect-orpc/src/tagged-error.ts index 80edd07..2213b4b 100644 --- a/packages/effect-orpc/src/tagged-error.ts +++ b/packages/effect-orpc/src/tagged-error.ts @@ -315,7 +315,7 @@ export function ORPCTaggedError< return this[ORPCErrorSymbol]; } - override toJSON(): ORPCErrorJSON> & { + toJSON(): ORPCErrorJSON> & { _tag: TTag; } { return { diff --git a/packages/effect-orpc/src/tests/contract.test.ts b/packages/effect-orpc/src/tests/contract.test.ts index 76f48cf..e32be98 100644 --- a/packages/effect-orpc/src/tests/contract.test.ts +++ b/packages/effect-orpc/src/tests/contract.test.ts @@ -1,26 +1,28 @@ import { oc, type InferSchemaOutput } from "@orpc/contract"; import { call, ORPCError, type Router } from "@orpc/server"; -import { Effect, FiberRef, Layer, ManagedRuntime } from "effect"; +import { Effect, Layer, ManagedRuntime, ServiceMap } from "effect"; import { describe, expect, expectTypeOf, it } from "vitest"; import z from "zod"; import { eoc, implementEffect, ORPCTaggedError } from "../index"; import { withFiberContext } from "../node"; -class Counter extends Effect.Tag("Counter")< +class Counter extends ServiceMap.Service< Counter, { readonly increment: (n: number) => Effect.Effect; } ->() {} +>()("Counter") { + static readonly layer = Layer.succeed(this, { + increment: (n: number) => Effect.succeed(n + 1), + }); +} -const requestIdRef = FiberRef.unsafeMake("missing"); +const RequestId = ServiceMap.Reference("RequestId", { + defaultValue: () => "missing", +}); -const runtime = ManagedRuntime.make( - Layer.succeed(Counter, { - increment: (n: number) => Effect.succeed(n + 1), - }), -); +const runtime = ManagedRuntime.make(Counter.layer); const contract = { users: { @@ -41,7 +43,7 @@ describe("implementEffect", () => { const procedure = oe.users.list.effect(function* ({ input }) { const counter = yield* Counter; - const requestId = yield* FiberRef.get(requestIdRef); + const requestId = yield* RequestId; return { next: yield* counter.increment(input.amount), @@ -50,10 +52,11 @@ describe("implementEffect", () => { }); const result = await Effect.runPromise( - Effect.gen(function* () { - yield* FiberRef.set(requestIdRef, "req-123"); - return yield* withFiberContext(() => call(procedure, { amount: 2 })); - }), + Effect.provideService( + withFiberContext(() => call(procedure, { amount: 2 })), + RequestId, + "req-123", + ), ); expect(result).toEqual({ @@ -72,19 +75,18 @@ describe("implementEffect", () => { return { next: yield* counter.increment(input.amount), - requestId: yield* FiberRef.get(requestIdRef), + requestId: yield* RequestId, }; }), }, }); const result = await Effect.runPromise( - Effect.gen(function* () { - yield* FiberRef.set(requestIdRef, "req-456"); - return yield* withFiberContext(() => - call(router.users.list, { amount: 4 }), - ); - }), + Effect.provideService( + withFiberContext(() => call(router.users.list, { amount: 4 })), + RequestId, + "req-456", + ), ); expect(result).toEqual({ diff --git a/packages/effect-orpc/src/tests/effect-builder.test.ts b/packages/effect-orpc/src/tests/effect-builder.test.ts index b494a1d..3197540 100644 --- a/packages/effect-orpc/src/tests/effect-builder.test.ts +++ b/packages/effect-orpc/src/tests/effect-builder.test.ts @@ -1,7 +1,7 @@ import type { InferSchemaOutput } from "@orpc/contract"; import { isContractProcedure } from "@orpc/contract"; import { os } from "@orpc/server"; -import { Effect, FiberRef, Layer, ManagedRuntime } from "effect"; +import { Effect, Layer, ManagedRuntime, ServiceMap } from "effect"; import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest"; import z from "zod"; @@ -20,6 +20,9 @@ import { const mid = vi.fn(); const runtime = ManagedRuntime.make(Layer.empty); +const RequestId = ServiceMap.Reference("RequestId", { + defaultValue: () => "missing", +}); const def = { config: { @@ -160,16 +163,14 @@ describe("effectBuilder", () => { expect(effectFn).toHaveBeenCalledTimes(1); }); - it(".effect does not inherit parent FiberRefs by default", async () => { - const requestIdRef = FiberRef.unsafeMake("missing"); + it(".effect does not inherit parent request context by default", async () => { const applied = builder.effect(function* () { - return yield* FiberRef.get(requestIdRef); + return yield* RequestId; }); const result = await Effect.runPromise( - Effect.gen(function* () { - yield* FiberRef.set(requestIdRef, "req-123"); - return yield* Effect.promise(() => + Effect.provideService( + Effect.promise(() => applied["~effect"].handler({ context: {}, input: undefined, @@ -179,23 +180,23 @@ describe("effectBuilder", () => { lastEventId: undefined, errors: {}, }), - ); - }), + ), + RequestId, + "req-123", + ), ); expect(result).toBe("missing"); }); - it(".effect inherits parent FiberRefs with withFiberContext", async () => { - const requestIdRef = FiberRef.unsafeMake("missing"); + it(".effect inherits parent request context with withFiberContext", async () => { const applied = builder.effect(function* () { - return yield* FiberRef.get(requestIdRef); + return yield* RequestId; }); const result = await Effect.runPromise( - Effect.gen(function* () { - yield* FiberRef.set(requestIdRef, "req-123"); - return yield* withFiberContext(() => + Effect.provideService( + withFiberContext(() => applied["~effect"].handler({ context: {}, input: undefined, @@ -205,57 +206,95 @@ describe("effectBuilder", () => { lastEventId: undefined, errors: {}, }), - ); - }), + ), + RequestId, + "req-123", + ), ); expect(result).toBe("req-123"); }); - it(".effect merges context FiberRefs with runtime FiberRefs, prioritizing context FiberRefs", async () => { - const requestIdRef = FiberRef.unsafeMake("missing"); - - class Counter extends Effect.Tag("Counter")< + it("merges runtime services with captured request context", async () => { + class Counter extends ServiceMap.Service< Counter, - { increment: (n: number) => Effect.Effect } - >() {} + { + readonly increment: (n: number) => Effect.Effect; + } + >()("Counter") {} + + const serviceRuntime = ManagedRuntime.make( + Layer.succeed(Counter, { + increment: (n: number) => Effect.succeed(n + 1), + }), + ); + const serviceBuilder = makeEffectORPC(serviceRuntime); - const CounterLive = Layer.succeed(Counter, { - increment: (n: number) => Effect.succeed(n + 1), - }); - const serviceRuntime = ManagedRuntime.make(CounterLive); - const effectBuilder = makeEffectORPC(serviceRuntime); - const procedure = effectBuilder.input(z.number()).effect(function* ({ + const applied = serviceBuilder.input(z.number()).effect(function* ({ input, }) { - const requestId = yield* FiberRef.get(requestIdRef); - const value = yield* Counter.increment(input as number); + const counter = yield* Counter; + const requestId = yield* RequestId; - return { requestId, value }; + return { + next: yield* counter.increment(input), + requestId, + }; }); - try { - const result = await Effect.runPromise( - Effect.gen(function* () { - yield* FiberRef.set(requestIdRef, "req-123"); - return yield* withFiberContext(() => - procedure["~effect"].handler({ - context: {}, - input: 5, - path: ["test"], - procedure: procedure as any, - signal: undefined, - lastEventId: undefined, - errors: {}, - }), - ); - }), - ); + const result = await Effect.runPromise( + Effect.provideService( + withFiberContext(() => + applied["~effect"].handler({ + context: {}, + input: 2, + path: ["test"], + procedure: applied as any, + signal: undefined, + lastEventId: undefined, + errors: {}, + }), + ), + RequestId, + "req-ctx", + ), + ); - expect(result).toEqual({ requestId: "req-123", value: 6 }); - } finally { - await serviceRuntime.dispose(); - } + expect(result).toEqual({ next: 3, requestId: "req-ctx" }); + + await serviceRuntime.dispose(); + }); + + it("captured request context wins over runtime services", async () => { + const runtimeWithRequestId = ManagedRuntime.make( + Layer.succeed(RequestId, "runtime-id"), + ); + const serviceBuilder = makeEffectORPC(runtimeWithRequestId); + const applied = serviceBuilder.effect(function* () { + return yield* RequestId; + }); + + const result = await Effect.runPromise( + Effect.provideService( + withFiberContext(() => + applied["~effect"].handler({ + context: {}, + input: undefined, + path: ["test"], + procedure: applied as any, + signal: undefined, + lastEventId: undefined, + errors: {}, + }), + ), + RequestId, + "request-id", + ), + ); + + expect(result).toBe("request-id"); + + await runtimeWithRequestId.dispose(); }); }); @@ -366,10 +405,12 @@ describe("makeEffectORPC factory", () => { describe("effect with services", () => { it("can use services from runtime layer", async () => { // Define a simple service - class Counter extends Effect.Tag("Counter")< + class Counter extends ServiceMap.Service< Counter, - { increment: (n: number) => Effect.Effect } - >() {} + { + readonly increment: (n: number) => Effect.Effect; + } + >()("Counter") {} // Create a layer with the service const CounterLive = Layer.succeed(Counter, { @@ -562,10 +603,12 @@ describe("default tracing (without .traced())", () => { }); it("default tracing works with services from runtime", async () => { - class Greeter extends Effect.Tag("Greeter")< + class Greeter extends ServiceMap.Service< Greeter, - { greet: (name: string) => Effect.Effect } - >() {} + { + readonly greet: (name: string) => Effect.Effect; + } + >()("Greeter") {} const GreeterLive = Layer.succeed(Greeter, { greet: (name: string) => Effect.succeed(`Hello, ${name}!`), @@ -577,7 +620,8 @@ describe("default tracing (without .traced())", () => { const procedure = effectBuilder .input(z.object({ name: z.string() })) .effect(function* ({ input }) { - return yield* Greeter.greet(input.name); + const greeter = yield* Greeter; + return yield* greeter.greet(input.name); }); const result = await procedure["~effect"].handler({ diff --git a/packages/effect-orpc/src/tests/tagged-error.test.ts b/packages/effect-orpc/src/tests/tagged-error.test.ts index 0a19c7e..c5dd668 100644 --- a/packages/effect-orpc/src/tests/tagged-error.test.ts +++ b/packages/effect-orpc/src/tests/tagged-error.test.ts @@ -209,7 +209,8 @@ describe("class ORPCTaggedError", () => { expect(Exit.isFailure(result)).toBe(true); if (Exit.isFailure(result)) { - const error = result.cause._tag === "Fail" ? result.cause.error : null; + const reason = result.cause.reasons[0]; + const error = reason?._tag === "Fail" ? reason.error : null; expect(error).toBeInstanceOf(ValidationError); expect((error as ValidationError).data).toEqual({ fields: ["email"] }); } diff --git a/packages/effect-orpc/src/types/index.ts b/packages/effect-orpc/src/types/index.ts index f9ce192..b84c12b 100644 --- a/packages/effect-orpc/src/types/index.ts +++ b/packages/effect-orpc/src/types/index.ts @@ -23,7 +23,6 @@ import type { RouterBuilder, } from "@orpc/server"; import type { Effect, ManagedRuntime } from "effect"; -import type { YieldWrap } from "effect/Utils"; import type { EffectErrorConstructorMap, @@ -117,17 +116,10 @@ export type EffectProcedureHandler< EffectErrorConstructorMap, TMeta >, -) => Generator< - YieldWrap< - Effect.Effect< - any, - | EffectErrorMapToUnion - | ORPCError, - TRequirementsProvided - > - >, +) => Effect.fn.Return< THandlerOutput, - never + EffectErrorMapToUnion | ORPCError, + TRequirementsProvided >; export type EffectErrorMapToErrorMap = { diff --git a/packages/effect-orpc/src/types/variants.ts b/packages/effect-orpc/src/types/variants.ts index d902731..960df49 100644 --- a/packages/effect-orpc/src/types/variants.ts +++ b/packages/effect-orpc/src/types/variants.ts @@ -14,7 +14,6 @@ import type { BuilderDef, Context, EnhanceRouterOptions, - IntersectPick, Lazy, Lazyable, MapInputMiddleware, @@ -24,6 +23,7 @@ import type { ProcedureHandler, Router, } from "@orpc/server"; +import type { IntersectPick } from "@orpc/shared"; import type { EffectBuilderDef,