diff --git a/.changeset/silly-nails-tickle.md b/.changeset/silly-nails-tickle.md new file mode 100644 index 00000000000..c7244d70b5d --- /dev/null +++ b/.changeset/silly-nails-tickle.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-node": minor +"@effect/rpc": minor +--- + +feat: add support for RPC middleware providing multiple services +`provides: [X, Y]` +return `Context.make(...)` diff --git a/packages/platform-node/test/fixtures/rpc-schemas.ts b/packages/platform-node/test/fixtures/rpc-schemas.ts index cc112c08771..8b872d5f24d 100644 --- a/packages/platform-node/test/fixtures/rpc-schemas.ts +++ b/packages/platform-node/test/fixtures/rpc-schemas.ts @@ -39,6 +39,18 @@ class TimingMiddleware extends RpcMiddleware.Tag()("TimingMidd wrap: true }) {} +export class Something extends Context.Tag("Something")() {} +export class SomethingElse extends Context.Tag("SomethingElse")() {} + +class SomethingMiddleware extends RpcMiddleware.Tag()("SomethingMiddleware", { + provides: [Something, SomethingElse] +}) {} + +class SomethingWrapMiddleware extends RpcMiddleware.Tag()("SomethingWrapMiddleware", { + provides: [Something, SomethingElse], + wrap: true +}) {} + class GetUser extends Rpc.make("GetUser", { success: User, payload: { id: Schema.String } @@ -66,6 +78,12 @@ export const UserRpcs = RpcGroup.make( }, success: Schema.Number }).middleware(TimingMiddleware), + Rpc.make("GetContext", { + success: Schema.Struct({ + Something: Schema.String, + SomethingElse: Schema.String + }) + }).middleware(SomethingMiddleware), Rpc.make("GetTimingMiddlewareMetrics", { success: Schema.Struct({ success: Schema.Number, @@ -73,7 +91,8 @@ export const UserRpcs = RpcGroup.make( count: Schema.Number }) }) -).middleware(AuthMiddleware) +) + .middleware(AuthMiddleware) const AuthLive = Layer.succeed( AuthMiddleware, @@ -98,6 +117,30 @@ const TimingLive = Layer.succeed( ) ) +const SomethingLive = Layer.succeed( + SomethingMiddleware, + SomethingMiddleware.of(() => + Effect.succeed( + Context.empty().pipe( + Context.add(Something, "something"), + Context.add(SomethingElse, "something-else") + ) + ) + ) +) + +Layer.succeed( + SomethingWrapMiddleware, + SomethingWrapMiddleware.of(({ next }) => + next.pipe(Effect.provide( + Context.empty().pipe( + Context.add(Something, "something"), + Context.add(SomethingElse, "something-else") + ) + )) + ) +) + const UsersLive = UserRpcs.toLayer(Effect.gen(function*() { let interrupts = 0 let emits = 0 @@ -135,6 +178,7 @@ const UsersLive = UserRpcs.toLayer(Effect.gen(function*() { Never: () => Effect.never.pipe(Effect.onInterrupt(() => Effect.sync(() => interrupts++))), "nested.test": () => Effect.void, TimedMethod: (_) => _.shouldFail ? Effect.die("boom") : Effect.succeed(1), + GetContext: () => Effect.all({ Something, SomethingElse }), GetTimingMiddlewareMetrics: () => Effect.all({ defect: Metric.value(rpcDefects).pipe(Effect.map((_) => _.count)), @@ -148,7 +192,8 @@ export const RpcLive = RpcServer.layer(UserRpcs).pipe( Layer.provide([ UsersLive, AuthLive, - TimingLive + TimingLive, + SomethingLive ]) ) @@ -166,6 +211,6 @@ export class UsersClient extends Context.Tag("UsersClient")< Layer.provide(AuthClient) ) static layerTest = Layer.scoped(UsersClient, RpcTest.makeClient(UserRpcs)).pipe( - Layer.provide([UsersLive, AuthLive, TimingLive, AuthClient]) + Layer.provide([UsersLive, AuthLive, TimingLive, SomethingLive, AuthClient]) ) } diff --git a/packages/platform-node/test/rpc-e2e.ts b/packages/platform-node/test/rpc-e2e.ts index df25cc805d6..6f83335c1b7 100644 --- a/packages/platform-node/test/rpc-e2e.ts +++ b/packages/platform-node/test/rpc-e2e.ts @@ -118,5 +118,12 @@ export const e2eSuite = ( assert.notEqual(defect, 0) assert.notEqual(success, 0) }).pipe(Effect.provide(layer))) + + it.effect("supports Context provide", () => + Effect.gen(function*() { + const client = yield* UsersClient + const context = yield* client.GetContext() + assert.deepStrictEqual(context, { Something: "something", SomethingElse: "something-else" }) + }).pipe(Effect.provide(layer))) }) } diff --git a/packages/rpc/src/Rpc.ts b/packages/rpc/src/Rpc.ts index 31b66a39575..e3439c8e9fd 100644 --- a/packages/rpc/src/Rpc.ts +++ b/packages/rpc/src/Rpc.ts @@ -2,6 +2,7 @@ * @since 1.0.0 */ import type { Headers } from "@effect/platform/Headers" +import type { NonEmptyReadonlyArray } from "effect/Array" import * as Context_ from "effect/Context" import type { Effect } from "effect/Effect" import type { Exit as Exit_ } from "effect/Exit" @@ -441,6 +442,9 @@ export type ExtractProvides = R extends Rpc ? _Middleware extends { readonly provides: Context_.Tag } ? _I : + _Middleware extends { + readonly provides: NonEmptyReadonlyArray> + } ? Context_.Tag.Identifier<_Middleware["provides"][number]> : never : never diff --git a/packages/rpc/src/RpcMiddleware.ts b/packages/rpc/src/RpcMiddleware.ts index 45cb9b45a36..961c3013ebb 100644 --- a/packages/rpc/src/RpcMiddleware.ts +++ b/packages/rpc/src/RpcMiddleware.ts @@ -2,6 +2,7 @@ * @since 1.0.0 */ import type { Headers } from "@effect/platform/Headers" +import type { NonEmptyReadonlyArray } from "effect/Array" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" @@ -128,6 +129,10 @@ export declare namespace TagClass { readonly provides: Context.Tag readonly optional?: false } ? Context.Tag.Identifier + : Options extends { + readonly provides: NonEmptyReadonlyArray> + readonly optional?: false + } ? Context.Tag.Identifier : never /** @@ -136,6 +141,8 @@ export declare namespace TagClass { */ export type Service = Options extends { readonly provides: Context.Tag } ? Context.Tag.Service + : Options extends { readonly provides: NonEmptyReadonlyArray> } ? + Context.Context> : void /** @@ -193,7 +200,8 @@ export declare namespace TagClass { readonly [TypeId]: TypeId readonly optional: Optional readonly failure: FailureSchema - readonly provides: Options extends { readonly provides: Context.Tag } ? Options["provides"] + readonly provides: Options extends + { readonly provides: Context.Tag | NonEmptyReadonlyArray> } ? Options["provides"] : undefined readonly requiredForClient: RequiredForClient readonly wrap: Wrap @@ -207,7 +215,7 @@ export declare namespace TagClass { export interface TagClassAny extends Context.Tag { readonly [TypeId]: TypeId readonly optional: boolean - readonly provides?: Context.Tag | undefined + readonly provides?: Context.Tag | NonEmptyReadonlyArray> | undefined readonly failure: Schema.Schema.All readonly requiredForClient: boolean readonly wrap: boolean @@ -220,7 +228,7 @@ export interface TagClassAny extends Context.Tag { export interface TagClassAnyWithProps extends Context.Tag | RpcMiddlewareWrap> { readonly [TypeId]: TypeId readonly optional: boolean - readonly provides?: Context.Tag + readonly provides?: Context.Tag | NonEmptyReadonlyArray> | undefined readonly failure: Schema.Schema.All readonly requiredForClient: boolean readonly wrap: boolean @@ -236,7 +244,7 @@ export const Tag = (): < readonly wrap?: boolean readonly optional?: boolean readonly failure?: Schema.Schema.All - readonly provides?: Context.Tag + readonly provides?: Context.Tag | NonEmptyReadonlyArray> readonly requiredForClient?: boolean } >( @@ -248,7 +256,7 @@ export const Tag = (): < options?: { readonly optional?: boolean readonly failure?: Schema.Schema.All - readonly provides?: Context.Tag + readonly provides?: Context.Tag | NonEmptyReadonlyArray> readonly requiredForClient?: boolean readonly wrap?: boolean } diff --git a/packages/rpc/src/RpcServer.ts b/packages/rpc/src/RpcServer.ts index 610ec3f055d..1b0ef7adc02 100644 --- a/packages/rpc/src/RpcServer.ts +++ b/packages/rpc/src/RpcServer.ts @@ -437,14 +437,20 @@ const applyMiddleware = ( handler = Effect.matchEffect(middleware(options), { onFailure: () => previous, onSuccess: tag.provides !== undefined - ? (value) => Effect.provideService(previous, tag.provides as any, value) + ? (value) => + Array.isArray(tag.provides) + ? Effect.provide(previous, value) as any + : Effect.provideService(previous, tag.provides as any, value) : (_) => previous }) } else { const middleware = Context.unsafeGet(context, tag) as RpcMiddleware + const previous = handler handler = tag.provides !== undefined - ? Effect.provideServiceEffect(handler, tag.provides as any, middleware(options)) - : Effect.zipRight(middleware(options), handler) + ? Array.isArray(tag.provides) + ? middleware(options).pipe(Effect.flatMap((value) => Effect.provide(previous, value))) as any + : Effect.provideServiceEffect(previous, tag.provides as any, middleware(options)) + : Effect.zipRight(middleware(options), previous) } }