Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/silly-nails-tickle.md
Original file line number Diff line number Diff line change
@@ -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(...)`
51 changes: 48 additions & 3 deletions packages/platform-node/test/fixtures/rpc-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ class TimingMiddleware extends RpcMiddleware.Tag<TimingMiddleware>()("TimingMidd
wrap: true
}) {}

export class Something extends Context.Tag("Something")<Something, "something">() {}
export class SomethingElse extends Context.Tag("SomethingElse")<SomethingElse, "something-else">() {}

class SomethingMiddleware extends RpcMiddleware.Tag<SomethingMiddleware>()("SomethingMiddleware", {
provides: [Something, SomethingElse]
}) {}

class SomethingWrapMiddleware extends RpcMiddleware.Tag<SomethingWrapMiddleware>()("SomethingWrapMiddleware", {
provides: [Something, SomethingElse],
wrap: true
}) {}

class GetUser extends Rpc.make("GetUser", {
success: User,
payload: { id: Schema.String }
Expand Down Expand Up @@ -66,14 +78,21 @@ 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,
defect: Schema.Number,
count: Schema.Number
})
})
).middleware(AuthMiddleware)
)
.middleware(AuthMiddleware)

const AuthLive = Layer.succeed(
AuthMiddleware,
Expand All @@ -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
Expand Down Expand Up @@ -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)),
Expand All @@ -148,7 +192,8 @@ export const RpcLive = RpcServer.layer(UserRpcs).pipe(
Layer.provide([
UsersLive,
AuthLive,
TimingLive
TimingLive,
SomethingLive
])
)

Expand All @@ -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])
)
}
7 changes: 7 additions & 0 deletions packages/platform-node/test/rpc-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,12 @@ export const e2eSuite = <E>(
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)))
})
}
4 changes: 4 additions & 0 deletions packages/rpc/src/Rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -441,6 +442,9 @@ export type ExtractProvides<R extends Any, Tag extends string> = R extends
Rpc<Tag, infer _Payload, infer _Success, infer _Error, infer _Middleware> ? _Middleware extends {
readonly provides: Context_.Tag<infer _I, infer _S>
} ? _I :
_Middleware extends {
readonly provides: NonEmptyReadonlyArray<Context_.Tag<any, any>>
} ? Context_.Tag.Identifier<_Middleware["provides"][number]> :
never :
never

Expand Down
18 changes: 13 additions & 5 deletions packages/rpc/src/RpcMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -128,6 +129,10 @@ export declare namespace TagClass {
readonly provides: Context.Tag<any, any>
readonly optional?: false
} ? Context.Tag.Identifier<Options["provides"]>
: Options extends {
readonly provides: NonEmptyReadonlyArray<Context.Tag<any, any>>
readonly optional?: false
} ? Context.Tag.Identifier<Options["provides"][number]>
: never

/**
Expand All @@ -136,6 +141,8 @@ export declare namespace TagClass {
*/
export type Service<Options> = Options extends { readonly provides: Context.Tag<any, any> }
? Context.Tag.Service<Options["provides"]>
: Options extends { readonly provides: NonEmptyReadonlyArray<Context.Tag<any, any>> } ?
Context.Context<Context.Tag.Identifier<Options["provides"][number]>>
: void

/**
Expand Down Expand Up @@ -193,7 +200,8 @@ export declare namespace TagClass {
readonly [TypeId]: TypeId
readonly optional: Optional<Options>
readonly failure: FailureSchema<Options>
readonly provides: Options extends { readonly provides: Context.Tag<any, any> } ? Options["provides"]
readonly provides: Options extends
{ readonly provides: Context.Tag<any, any> | NonEmptyReadonlyArray<Context.Tag<any, any>> } ? Options["provides"]
: undefined
readonly requiredForClient: RequiredForClient<Options>
readonly wrap: Wrap<Options>
Expand All @@ -207,7 +215,7 @@ export declare namespace TagClass {
export interface TagClassAny extends Context.Tag<any, any> {
readonly [TypeId]: TypeId
readonly optional: boolean
readonly provides?: Context.Tag<any, any> | undefined
readonly provides?: Context.Tag<any, any> | NonEmptyReadonlyArray<Context.Tag<any, any>> | undefined
readonly failure: Schema.Schema.All
readonly requiredForClient: boolean
readonly wrap: boolean
Expand All @@ -220,7 +228,7 @@ export interface TagClassAny extends Context.Tag<any, any> {
export interface TagClassAnyWithProps extends Context.Tag<any, RpcMiddleware<any, any> | RpcMiddlewareWrap<any, any>> {
readonly [TypeId]: TypeId
readonly optional: boolean
readonly provides?: Context.Tag<any, any>
readonly provides?: Context.Tag<any, any> | NonEmptyReadonlyArray<Context.Tag<any, any>> | undefined
readonly failure: Schema.Schema.All
readonly requiredForClient: boolean
readonly wrap: boolean
Expand All @@ -236,7 +244,7 @@ export const Tag = <Self>(): <
readonly wrap?: boolean
readonly optional?: boolean
readonly failure?: Schema.Schema.All
readonly provides?: Context.Tag<any, any>
readonly provides?: Context.Tag<any, any> | NonEmptyReadonlyArray<Context.Tag<any, any>>
readonly requiredForClient?: boolean
}
>(
Expand All @@ -248,7 +256,7 @@ export const Tag = <Self>(): <
options?: {
readonly optional?: boolean
readonly failure?: Schema.Schema.All
readonly provides?: Context.Tag<any, any>
readonly provides?: Context.Tag<any, any> | NonEmptyReadonlyArray<Context.Tag<any, any>>
readonly requiredForClient?: boolean
readonly wrap?: boolean
}
Expand Down
12 changes: 9 additions & 3 deletions packages/rpc/src/RpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,14 +437,20 @@ const applyMiddleware = <A, E, R>(
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<any, any>
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)
}
}

Expand Down