Skip to content

Commit d17ef5e

Browse files
authored
feat(client, server)!: Adapter-Level Plugin Interception (#292)
Plugins now have the ability to intercept and modify requests and responses directly at the adapter level ### `Event Iterator Keep Alive` We move configs for `Event Iterator Keep Alive` from `.handle` to `handler options` (define when creating handler) ### Rename internal APIs - `HandlerPlugin` -> `StandardHandlerPlugin` - `ClientOptions` -> `FriendlyClientOptions` - `ClientOptionsOut` -> `ClientOptions` - `ClientPlugin` -> `StandardLinkPlugin` - `StandardHandleOptions` -> `FriendlyStandardHandleOptions` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Documentation** - Clarified API descriptions, including keep-alive behavior and headers for improved clarity. - **Refactor** - Streamlined internal handler implementations and standardized plugin and configuration interfaces across client and server components. - **Tests** - Expanded test coverage for options resolution and plugin initialization, while simplifying scenarios to focus on core functionality. - **Chores** - Removed deprecated dependencies, updated package versions, and consolidated module exports for a leaner codebase. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 62795ca commit d17ef5e

61 files changed

Lines changed: 743 additions & 869 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/content/docs/openapi/openapi-handler.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ To keep [Event Iterator](/docs/event-iterator) connections alive, `OpenAPIHandle
9999

100100
- `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings.
101101
- `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds).
102-
- `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping messages.
102+
- `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping comments.
103103

104104
```ts
105-
const result = await handler.handle(request, {
105+
const handler = new OpenAPIHandler(router, {
106106
eventIteratorKeepAliveEnabled: true,
107107
eventIteratorKeepAliveInterval: 5000, // 5 seconds
108108
eventIteratorKeepAliveComment: '',

apps/content/docs/rpc-handler.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default async function fetch(request: Request) {
7272
}
7373
```
7474

75-
## Event-Iterator Keep Alive
75+
## Event Iterator Keep Alive
7676

7777
To keep [Event Iterator](/docs/event-iterator) connections alive, `RPCHandler` periodically sends a ping comment to the client. You can configure this behavior using the following options:
7878

@@ -81,7 +81,7 @@ To keep [Event Iterator](/docs/event-iterator) connections alive, `RPCHandler` p
8181
- `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping comments.
8282

8383
```ts
84-
const result = await handler.handle(request, {
84+
const handler = new RPCHandler(router, {
8585
eventIteratorKeepAliveEnabled: true,
8686
eventIteratorKeepAliveInterval: 5000, // 5 seconds
8787
eventIteratorKeepAliveComment: '',

packages/client/src/adapters/fetch/link-fetch-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { StandardLazyResponse, StandardRequest } from '@orpc/standard-server'
22
import type { ToFetchRequestOptions } from '@orpc/standard-server-fetch'
3-
import type { ClientContext, ClientOptionsOut } from '../../types'
3+
import type { ClientContext, ClientOptions } from '../../types'
44
import type { StandardLinkClient } from '../standard'
55
import { toFetchRequest, toStandardLazyResponse } from '@orpc/standard-server-fetch'
66

77
export interface LinkFetchClientOptions<T extends ClientContext> extends ToFetchRequestOptions {
88
fetch?: (
99
request: Request,
1010
init: Record<never, never>,
11-
options: ClientOptionsOut<T>,
11+
options: ClientOptions<T>,
1212
path: readonly string[],
1313
input: unknown
1414
) => Promise<Response>
@@ -23,7 +23,7 @@ export class LinkFetchClient<T extends ClientContext> implements StandardLinkCli
2323
this.toFetchRequestOptions = options
2424
}
2525

26-
async call(request: StandardRequest, options: ClientOptionsOut<T>, path: readonly string[], input: unknown): Promise<StandardLazyResponse> {
26+
async call(request: StandardRequest, options: ClientOptions<T>, path: readonly string[], input: unknown): Promise<StandardLazyResponse> {
2727
const fetchRequest = toFetchRequest(request, this.toFetchRequestOptions)
2828

2929
const fetchResponse = await this.fetch(fetchRequest, {}, options, path, input)

packages/client/src/adapters/fetch/rpc-link.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ClientContext, ClientLink, ClientOptionsOut } from '../../types'
1+
import type { ClientContext, ClientLink, ClientOptions } from '../../types'
22
import type { StandardRPCLinkOptions } from '../standard'
33
import type { LinkFetchClientOptions } from './link-fetch-client'
44
import { StandardLink, StandardRPCJsonSerializer, StandardRPCLinkCodec, StandardRPCSerializer } from '../standard'
@@ -19,7 +19,7 @@ export class RPCLink<T extends ClientContext> implements ClientLink<T> {
1919
this.standardLink = new StandardLink(linkCodec, linkClient, options)
2020
}
2121

22-
async call(path: readonly string[], input: unknown, options: ClientOptionsOut<T>): Promise<unknown> {
22+
async call(path: readonly string[], input: unknown, options: ClientOptions<T>): Promise<unknown> {
2323
return this.standardLink.call(path, input, options)
2424
}
2525
}

packages/client/src/adapters/fetch/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { ClientContext, ClientOptionsOut } from '../../types'
1+
import type { ClientContext, ClientOptions } from '../../types'
22

33
export interface FetchWithContext<TClientContext extends ClientContext> {
44
(
55
url: URL,
66
init: RequestInit,
7-
options: ClientOptionsOut<TClientContext>,
7+
options: ClientOptions<TClientContext>,
88
path: readonly string[],
99
input: unknown,
1010
): Promise<Response>

packages/client/src/adapters/standard/link.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,20 @@ describe('standardLink', () => {
5252
request: '__standard_request__',
5353
})
5454
})
55+
56+
it('plugins', () => {
57+
const init = vi.fn()
58+
59+
const options = {
60+
plugins: [
61+
{ init },
62+
],
63+
interceptors: [vi.fn()],
64+
clientInterceptors: [vi.fn()],
65+
}
66+
const link = new StandardLink(codec, client, options)
67+
68+
expect(init).toHaveBeenCalledOnce()
69+
expect(init).toHaveBeenCalledWith(options)
70+
})
5571
})

packages/client/src/adapters/standard/link.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import type { Interceptor } from '@orpc/shared'
22
import type { StandardLazyResponse, StandardRequest } from '@orpc/standard-server'
3-
import type { ClientContext, ClientLink, ClientOptionsOut } from '../../types'
3+
import type { ClientContext, ClientLink, ClientOptions } from '../../types'
44
import type { StandardLinkClient, StandardLinkCodec } from './types'
5-
import { intercept } from '@orpc/shared'
6-
import { type ClientPlugin, CompositeClientPlugin } from '../../plugins'
5+
import { intercept, toArray } from '@orpc/shared'
76

87
export class InvalidEventIteratorRetryResponse extends Error { }
98

9+
export interface StandardLinkPlugin<T extends ClientContext> {
10+
init?(options: StandardLinkOptions<T>): void
11+
}
12+
1013
export interface StandardLinkOptions<T extends ClientContext> {
11-
interceptors?: Interceptor<{ path: readonly string[], input: unknown, options: ClientOptionsOut<T> }, unknown, unknown>[]
14+
interceptors?: Interceptor<{ path: readonly string[], input: unknown, options: ClientOptions<T> }, unknown, unknown>[]
1215
clientInterceptors?: Interceptor<{ request: StandardRequest }, StandardLazyResponse, unknown>[]
13-
plugins?: ClientPlugin<T>[]
16+
plugins?: StandardLinkPlugin<T>[]
1417
}
1518

1619
export class StandardLink<T extends ClientContext> implements ClientLink<T> {
@@ -22,23 +25,23 @@ export class StandardLink<T extends ClientContext> implements ClientLink<T> {
2225
public readonly sender: StandardLinkClient<T>,
2326
options: StandardLinkOptions<T> = {},
2427
) {
25-
const plugin = new CompositeClientPlugin(options.plugins)
26-
27-
plugin.init(options)
28+
for (const plugin of toArray(options.plugins)) {
29+
plugin.init?.(options)
30+
}
2831

29-
this.interceptors = options.interceptors ?? []
30-
this.clientInterceptors = options.clientInterceptors ?? []
32+
this.interceptors = toArray(options.interceptors)
33+
this.clientInterceptors = toArray(options.clientInterceptors)
3134
}
3235

33-
call(path: readonly string[], input: unknown, options: ClientOptionsOut<T>): Promise<unknown> {
36+
call(path: readonly string[], input: unknown, options: ClientOptions<T>): Promise<unknown> {
3437
return intercept(this.interceptors, { path, input, options }, async ({ path, input, options }) => {
3538
const output = await this.#call(path, input, options)
3639

3740
return output
3841
})
3942
}
4043

41-
async #call(path: readonly string[], input: unknown, options: ClientOptionsOut<T>): Promise<unknown> {
44+
async #call(path: readonly string[], input: unknown, options: ClientOptions<T>): Promise<unknown> {
4245
const request = await this.codec.encode(path, input, options)
4346

4447
const response = await intercept(

packages/client/src/adapters/standard/rpc-link-codec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { StandardHeaders, StandardLazyResponse, StandardRequest } from '@orpc/standard-server'
2-
import type { ClientContext, ClientOptionsOut } from '../../types'
2+
import type { ClientContext, ClientOptions } from '../../types'
33
import type { StandardRPCSerializer } from './rpc-serializer'
44
import type { StandardLinkCodec } from './types'
55
import { isAsyncIteratorObject, stringifyJSON, trim, value, type Value } from '@orpc/shared'
@@ -12,7 +12,7 @@ export interface StandardRPCLinkCodecOptions<T extends ClientContext> {
1212
* Base url for all requests.
1313
*/
1414
url: Value<string | URL, [
15-
options: ClientOptionsOut<T>,
15+
options: ClientOptions<T>,
1616
path: readonly string[],
1717
input: unknown,
1818
]>
@@ -23,7 +23,7 @@ export interface StandardRPCLinkCodecOptions<T extends ClientContext> {
2323
* @default 2083
2424
*/
2525
maxUrlLength?: Value<number, [
26-
options: ClientOptionsOut<T>,
26+
options: ClientOptions<T>,
2727
path: readonly string[],
2828
input: unknown,
2929
]>
@@ -34,7 +34,7 @@ export interface StandardRPCLinkCodecOptions<T extends ClientContext> {
3434
* @default 'POST'
3535
*/
3636
method?: Value<HTTPMethod, [
37-
options: ClientOptionsOut<T>,
37+
options: ClientOptions<T>,
3838
path: readonly string[],
3939
input: unknown,
4040
]>
@@ -51,7 +51,7 @@ export interface StandardRPCLinkCodecOptions<T extends ClientContext> {
5151
* Inject headers to the request.
5252
*/
5353
headers?: Value<StandardHeaders, [
54-
options: ClientOptionsOut<T>,
54+
options: ClientOptions<T>,
5555
path: readonly string[],
5656
input: unknown,
5757
]>
@@ -75,7 +75,7 @@ export class StandardRPCLinkCodec<T extends ClientContext> implements StandardLi
7575
this.headers = options.headers ?? {}
7676
}
7777

78-
async encode(path: readonly string[], input: unknown, options: ClientOptionsOut<any>): Promise<StandardRequest> {
78+
async encode(path: readonly string[], input: unknown, options: ClientOptions<T>): Promise<StandardRequest> {
7979
const expectedMethod = await value(this.expectedMethod, options, path, input)
8080
const headers = { ...await value(this.headers, options, path, input) }
8181
const baseUrl = await value(this.baseUrl, options, path, input)
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { StandardLazyResponse, StandardRequest } from '@orpc/standard-server'
2-
import type { ClientContext, ClientOptionsOut } from '../../types'
2+
import type { ClientContext, ClientOptions } from '../../types'
33

44
export interface StandardLinkCodec<T extends ClientContext> {
5-
encode(path: readonly string[], input: unknown, options: ClientOptionsOut<any>): Promise<StandardRequest>
6-
decode(response: StandardLazyResponse, options: ClientOptionsOut<T>, path: readonly string[], input: unknown): Promise<unknown>
5+
encode(path: readonly string[], input: unknown, options: ClientOptions<T>): Promise<StandardRequest>
6+
decode(response: StandardLazyResponse, options: ClientOptions<T>, path: readonly string[], input: unknown): Promise<unknown>
77
}
88

99
export interface StandardLinkClient<T extends ClientContext> {
10-
call(request: StandardRequest, options: ClientOptionsOut<T>, path: readonly string[], input: unknown): Promise<StandardLazyResponse>
10+
call(request: StandardRequest, options: ClientOptions<T>, path: readonly string[], input: unknown): Promise<StandardLazyResponse>
1111
}

packages/client/src/client.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Client, ClientLink, InferClientContext, NestedClient } from './types'
1+
import type { Client, ClientLink, FriendlyClientOptions, InferClientContext, NestedClient } from './types'
2+
import { resolveFriendlyClientOptions } from './utils'
23

34
export interface createORPCClientOptions {
45
/**
@@ -13,13 +14,10 @@ export function createORPCClient<T extends NestedClient<any>>(
1314
): T {
1415
const path = options?.path ?? []
1516

16-
const procedureClient: Client<InferClientContext<T>, unknown, unknown, Error> = async (...[input, options]) => {
17-
const optionsOut = {
18-
...options,
19-
context: options?.context ?? {} as InferClientContext<T>, // options.context can be undefined when all field is optional
20-
}
21-
22-
return await link.call(path, input, optionsOut)
17+
const procedureClient: Client<InferClientContext<T>, unknown, unknown, Error> = async (
18+
...[input, options = {} as FriendlyClientOptions<InferClientContext<T>>]
19+
) => {
20+
return await link.call(path, input, resolveFriendlyClientOptions(options))
2321
}
2422

2523
const recursive = new Proxy(procedureClient, {

0 commit comments

Comments
 (0)