Skip to content

Commit 350a165

Browse files
authored
feat(client, server): make plugin execution order configurable (#344)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Expanded public APIs to expose new composite plugin handlers. - Introduced configurable ordering properties to manage plugin execution priority. - **Bug Fixes** - Removed outdated interfaces to simplify plugin handling. - **Refactor** - Streamlined plugin initialization by consolidating individual initializations into unified composite handlers. - **Tests** - Added automated tests to validate the initialization process and execution order of the composite plugins. - Introduced backward compatibility tests for plugin interfaces. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 96f9dd3 commit 350a165

25 files changed

+275
-50
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './link'
2+
export * from './plugin'
23
export * from './rpc-json-serializer'
34
export * from './rpc-link'
45
export * from './rpc-link-codec'

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

+4-7
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@ import type { StandardLazyResponse, StandardRequest } from '@orpc/standard-serve
33
import type { ClientContext, ClientLink, ClientOptions } from '../../types'
44
import type { StandardLinkClient, StandardLinkCodec } from './types'
55
import { intercept, toArray } from '@orpc/shared'
6+
import { CompositeStandardLinkPlugin, type StandardLinkPlugin } from './plugin'
67

78
export class InvalidEventIteratorRetryResponse extends Error { }
89

9-
export interface StandardLinkPlugin<T extends ClientContext> {
10-
init?(options: StandardLinkOptions<T>): void
11-
}
12-
1310
export interface StandardLinkInterceptorOptions<T extends ClientContext> extends ClientOptions<T> {
1411
path: readonly string[]
1512
input: unknown
@@ -34,9 +31,9 @@ export class StandardLink<T extends ClientContext> implements ClientLink<T> {
3431
public readonly sender: StandardLinkClient<T>,
3532
options: StandardLinkOptions<T> = {},
3633
) {
37-
for (const plugin of toArray(options.plugins)) {
38-
plugin.init?.(options)
39-
}
34+
const plugin = new CompositeStandardLinkPlugin(options.plugins)
35+
36+
plugin.init(options)
4037

4138
this.interceptors = toArray(options.interceptors)
4239
this.clientInterceptors = toArray(options.clientInterceptors)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { StandardLinkPlugin } from './plugin'
2+
import { CompositeStandardLinkPlugin } from './plugin'
3+
4+
describe('compositeStandardLinkPlugin', () => {
5+
it('forward init and sort plugins', () => {
6+
const plugin1 = {
7+
init: vi.fn(),
8+
order: 1,
9+
} satisfies StandardLinkPlugin<any>
10+
const plugin2 = {
11+
init: vi.fn(),
12+
} satisfies StandardLinkPlugin<any>
13+
const plugin3 = {
14+
init: vi.fn(),
15+
order: -1,
16+
} satisfies StandardLinkPlugin<any>
17+
18+
const compositePlugin = new CompositeStandardLinkPlugin([plugin1, plugin2, plugin3])
19+
20+
const interceptor = vi.fn()
21+
22+
const options = { interceptors: [interceptor] }
23+
24+
compositePlugin.init(options)
25+
26+
expect(plugin1.init).toHaveBeenCalledOnce()
27+
expect(plugin2.init).toHaveBeenCalledOnce()
28+
expect(plugin3.init).toHaveBeenCalledOnce()
29+
30+
expect(plugin1.init.mock.calls[0]![0]).toBe(options)
31+
expect(plugin2.init.mock.calls[0]![0]).toBe(options)
32+
expect(plugin3.init.mock.calls[0]![0]).toBe(options)
33+
34+
expect(plugin3.init).toHaveBeenCalledBefore(plugin2.init)
35+
expect(plugin2.init).toHaveBeenCalledBefore(plugin1.init)
36+
})
37+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { ClientContext } from '../../types'
2+
import type { StandardLinkOptions } from './link'
3+
4+
export interface StandardLinkPlugin<T extends ClientContext> {
5+
order?: number
6+
init?(options: StandardLinkOptions<T>): void
7+
}
8+
9+
export class CompositeStandardLinkPlugin<T extends ClientContext, TPlugin extends StandardLinkPlugin<T>> implements StandardLinkPlugin<T> {
10+
protected readonly plugins: TPlugin[]
11+
12+
constructor(plugins: readonly TPlugin[] = []) {
13+
this.plugins = [...plugins].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
14+
}
15+
16+
init(options: StandardLinkOptions<T>): void {
17+
for (const plugin of this.plugins) {
18+
plugin.init?.(options)
19+
}
20+
}
21+
}

packages/client/src/plugins/batch.ts

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export class BatchLinkPlugin<T extends ClientContext> implements StandardLinkPlu
7676
][]
7777
>
7878

79+
order = 5_000_000
80+
7981
constructor(options: BatchLinkPluginOptions<T>) {
8082
this.groups = options.groups
8183
this.pending = new Map()

packages/server/src/adapters/fetch/body-limit-plugin.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Context } from '../../context'
2-
import type { FetchHandlerOptions, FetchHandlerPlugin } from './handler'
2+
import type { FetchHandlerOptions } from './handler'
3+
import type { FetchHandlerPlugin } from './plugin'
34
import { ORPCError } from '@orpc/client'
45

56
export interface BodyLimitPluginOptions {

packages/server/src/adapters/fetch/handler.test-d.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import type { StandardHandlerPlugin } from '../standard'
2-
import type { FetchHandler, FetchHandlerPlugin } from './handler'
3-
4-
describe('FetchHandlerPlugin', () => {
5-
it('backward compatibility', () => {
6-
expectTypeOf<FetchHandlerPlugin<{ a: string }>>().toMatchTypeOf<StandardHandlerPlugin<{ a: string }>>()
7-
expectTypeOf<StandardHandlerPlugin<{ a: string }>>().toMatchTypeOf<FetchHandlerPlugin<{ a: string }>>()
8-
})
9-
})
1+
import type { FetchHandler } from './handler'
102

113
describe('FetchHandler', () => {
124
it('optional context when all context is optional', () => {

packages/server/src/adapters/fetch/handler.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import type { Interceptor, MaybeOptionalOptions, ThrowableError } from '@orpc/shared'
22
import type { Context } from '../../context'
3-
import type { StandardHandleOptions, StandardHandler, StandardHandlerPlugin } from '../standard'
3+
import type { StandardHandleOptions, StandardHandler } from '../standard'
44
import type { FriendlyStandardHandleOptions } from '../standard/utils'
5+
import type { FetchHandlerPlugin } from './plugin'
56
import { intercept, resolveMaybeOptionalOptions, toArray } from '@orpc/shared'
67
import { toFetchResponse, type ToFetchResponseOptions, toStandardLazyRequest } from '@orpc/standard-server-fetch'
78
import { resolveFriendlyStandardHandleOptions } from '../standard/utils'
9+
import { CompositeFetchHandlerPlugin } from './plugin'
810

911
export type FetchHandleResult = { matched: true, response: Response } | { matched: false, response: undefined }
1012

11-
export interface FetchHandlerPlugin<T extends Context> extends StandardHandlerPlugin<T> {
12-
initRuntimeAdapter?(options: FetchHandlerOptions<T>): void
13-
}
14-
1513
export interface FetchHandlerInterceptorOptions<T extends Context> extends StandardHandleOptions<T> {
1614
request: Request
1715
toFetchResponseOptions: ToFetchResponseOptions
@@ -31,9 +29,9 @@ export class FetchHandler<T extends Context> {
3129
private readonly standardHandler: StandardHandler<T>,
3230
options: NoInfer<FetchHandlerOptions<T>> = {},
3331
) {
34-
for (const plugin of toArray(options.plugins)) {
35-
plugin.initRuntimeAdapter?.(options)
36-
}
32+
const plugin = new CompositeFetchHandlerPlugin(options.plugins)
33+
34+
plugin.initRuntimeAdapter(options)
3735

3836
this.adapterInterceptors = toArray(options.adapterInterceptors)
3937
this.toFetchResponseOptions = options
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './body-limit-plugin'
22
export * from './handler'
3+
export * from './plugin'
34
export * from './rpc-handler'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { StandardHandlerPlugin } from '../standard'
2+
import type { FetchHandlerPlugin } from './plugin'
3+
4+
describe('FetchHandlerPlugin', () => {
5+
it('backward compatibility', () => {
6+
expectTypeOf<FetchHandlerPlugin<{ a: string }>>().toMatchTypeOf<StandardHandlerPlugin<{ a: string }>>()
7+
expectTypeOf<StandardHandlerPlugin<{ a: string }>>().toMatchTypeOf<FetchHandlerPlugin<{ a: string }>>()
8+
})
9+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { FetchHandlerPlugin } from './plugin'
2+
import { CompositeFetchHandlerPlugin } from './plugin'
3+
4+
describe('compositeFetchHandlerPlugin', () => {
5+
it('forward initRuntimeAdapter and sort plugins', () => {
6+
const plugin1 = {
7+
initRuntimeAdapter: vi.fn(),
8+
order: 1,
9+
} satisfies FetchHandlerPlugin<any>
10+
const plugin2 = {
11+
initRuntimeAdapter: vi.fn(),
12+
} satisfies FetchHandlerPlugin<any>
13+
const plugin3 = {
14+
initRuntimeAdapter: vi.fn(),
15+
order: -1,
16+
} satisfies FetchHandlerPlugin<any>
17+
18+
const compositePlugin = new CompositeFetchHandlerPlugin([plugin1, plugin2, plugin3])
19+
20+
const interceptor = vi.fn()
21+
22+
const options = { adapterInterceptors: [interceptor] }
23+
24+
compositePlugin.initRuntimeAdapter(options)
25+
26+
expect(plugin1.initRuntimeAdapter).toHaveBeenCalledOnce()
27+
expect(plugin2.initRuntimeAdapter).toHaveBeenCalledOnce()
28+
expect(plugin3.initRuntimeAdapter).toHaveBeenCalledOnce()
29+
30+
expect(plugin1.initRuntimeAdapter.mock.calls[0]![0]).toBe(options)
31+
expect(plugin2.initRuntimeAdapter.mock.calls[0]![0]).toBe(options)
32+
expect(plugin3.initRuntimeAdapter.mock.calls[0]![0]).toBe(options)
33+
34+
expect(plugin3.initRuntimeAdapter).toHaveBeenCalledBefore(plugin2.initRuntimeAdapter)
35+
expect(plugin2.initRuntimeAdapter).toHaveBeenCalledBefore(plugin1.initRuntimeAdapter)
36+
})
37+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Context } from '../../context'
2+
import type { FetchHandlerOptions } from './handler'
3+
import { CompositeStandardHandlerPlugin, type StandardHandlerPlugin } from '../standard'
4+
5+
export interface FetchHandlerPlugin<T extends Context> extends StandardHandlerPlugin<T> {
6+
initRuntimeAdapter?(options: FetchHandlerOptions<T>): void
7+
}
8+
9+
export class CompositeFetchHandlerPlugin<T extends Context, TPlugin extends FetchHandlerPlugin<T>>
10+
extends CompositeStandardHandlerPlugin<T, TPlugin> implements FetchHandlerPlugin<T> {
11+
initRuntimeAdapter(options: FetchHandlerOptions<T>): void {
12+
for (const plugin of this.plugins) {
13+
plugin.initRuntimeAdapter?.(options)
14+
}
15+
}
16+
}

packages/server/src/adapters/node/body-limit-plugin.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Context } from '../../context'
2-
import type { NodeHttpHandlerOptions, NodeHttpHandlerPlugin } from './handler'
2+
import type { NodeHttpHandlerOptions } from './handler'
3+
import type { NodeHttpHandlerPlugin } from './plugin'
34
import { ORPCError } from '@orpc/client'
45

56
export interface BodyLimitPluginOptions {

packages/server/src/adapters/node/handler.test-d.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import type { IncomingMessage, ServerResponse } from 'node:http'
2-
import type { StandardHandlerPlugin } from '../standard'
3-
import type { NodeHttpHandler, NodeHttpHandlerPlugin } from './handler'
4-
5-
describe('NodeHttpHandlerPlugin', () => {
6-
it('backward compatibility', () => {
7-
expectTypeOf<NodeHttpHandlerPlugin<{ a: string }>>().toMatchTypeOf<StandardHandlerPlugin<{ a: string }>>()
8-
expectTypeOf<StandardHandlerPlugin<{ a: string }>>().toMatchTypeOf<NodeHttpHandlerPlugin<{ a: string }>>()
9-
})
10-
})
2+
import type { NodeHttpHandler } from './handler'
113

124
describe('NodeHttpHandler', () => {
135
it('optional context when all context is optional', () => {

packages/server/src/adapters/node/handler.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import type { Interceptor, MaybeOptionalOptions, ThrowableError } from '@orpc/shared'
22
import type { NodeHttpRequest, NodeHttpResponse, SendStandardResponseOptions } from '@orpc/standard-server-node'
33
import type { Context } from '../../context'
4-
import type { StandardHandleOptions, StandardHandler, StandardHandlerPlugin } from '../standard'
4+
import type { StandardHandleOptions, StandardHandler } from '../standard'
55
import { intercept, resolveMaybeOptionalOptions, toArray } from '@orpc/shared'
66
import { sendStandardResponse, toStandardLazyRequest } from '@orpc/standard-server-node'
77
import { type FriendlyStandardHandleOptions, resolveFriendlyStandardHandleOptions } from '../standard/utils'
8+
import { CompositeNodeHttpHandlerPlugin, type NodeHttpHandlerPlugin } from './plugin'
89

910
export type NodeHttpHandleResult = { matched: true } | { matched: false }
1011

11-
export interface NodeHttpHandlerPlugin<T extends Context> extends StandardHandlerPlugin<T> {
12-
initRuntimeAdapter?(options: NodeHttpHandlerOptions<T>): void
13-
}
14-
1512
export interface NodeHttpHandlerInterceptorOptions<T extends Context> extends StandardHandleOptions<T> {
1613
request: NodeHttpRequest
1714
response: NodeHttpResponse
@@ -32,9 +29,9 @@ export class NodeHttpHandler<T extends Context> implements NodeHttpHandler<T> {
3229
private readonly standardHandler: StandardHandler<T>,
3330
options: NoInfer<NodeHttpHandlerOptions<T>> = {},
3431
) {
35-
for (const plugin of toArray(options.plugins)) {
36-
plugin.initRuntimeAdapter?.(options)
37-
}
32+
const plugin = new CompositeNodeHttpHandlerPlugin(options.plugins)
33+
34+
plugin.initRuntimeAdapter(options)
3835

3936
this.adapterInterceptors = toArray(options.adapterInterceptors)
4037
this.sendStandardResponseOptions = options
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './body-limit-plugin'
22
export * from './handler'
3+
export * from './plugin'
34
export * from './rpc-handler'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { StandardHandlerPlugin } from '../standard'
2+
import type { NodeHttpHandlerPlugin } from './plugin'
3+
4+
describe('NodeHttpHandlerPlugin', () => {
5+
it('backward compatibility', () => {
6+
expectTypeOf<NodeHttpHandlerPlugin<{ a: string }>>().toMatchTypeOf<StandardHandlerPlugin<{ a: string }>>()
7+
expectTypeOf<StandardHandlerPlugin<{ a: string }>>().toMatchTypeOf<NodeHttpHandlerPlugin<{ a: string }>>()
8+
})
9+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { NodeHttpHandlerPlugin } from './plugin'
2+
import { CompositeNodeHttpHandlerPlugin } from './plugin'
3+
4+
describe('compositeNodeHttpHandlerPlugin', () => {
5+
it('forward initRuntimeAdapter and sort plugins', () => {
6+
const plugin1 = {
7+
initRuntimeAdapter: vi.fn(),
8+
order: 1,
9+
} satisfies NodeHttpHandlerPlugin<any>
10+
const plugin2 = {
11+
initRuntimeAdapter: vi.fn(),
12+
} satisfies NodeHttpHandlerPlugin<any>
13+
const plugin3 = {
14+
initRuntimeAdapter: vi.fn(),
15+
order: -1,
16+
} satisfies NodeHttpHandlerPlugin<any>
17+
18+
const compositePlugin = new CompositeNodeHttpHandlerPlugin([plugin1, plugin2, plugin3])
19+
20+
const interceptor = vi.fn()
21+
22+
const options = { adapterInterceptors: [interceptor] }
23+
24+
compositePlugin.initRuntimeAdapter(options)
25+
26+
expect(plugin1.initRuntimeAdapter).toHaveBeenCalledOnce()
27+
expect(plugin2.initRuntimeAdapter).toHaveBeenCalledOnce()
28+
expect(plugin3.initRuntimeAdapter).toHaveBeenCalledOnce()
29+
30+
expect(plugin1.initRuntimeAdapter.mock.calls[0]![0]).toBe(options)
31+
expect(plugin2.initRuntimeAdapter.mock.calls[0]![0]).toBe(options)
32+
expect(plugin3.initRuntimeAdapter.mock.calls[0]![0]).toBe(options)
33+
34+
expect(plugin3.initRuntimeAdapter).toHaveBeenCalledBefore(plugin2.initRuntimeAdapter)
35+
expect(plugin2.initRuntimeAdapter).toHaveBeenCalledBefore(plugin1.initRuntimeAdapter)
36+
})
37+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Context } from '../../context'
2+
import type { StandardHandlerPlugin } from '../standard'
3+
import type { NodeHttpHandlerOptions } from './handler'
4+
import { CompositeStandardHandlerPlugin } from '../standard'
5+
6+
export interface NodeHttpHandlerPlugin<T extends Context> extends StandardHandlerPlugin<T> {
7+
initRuntimeAdapter?(options: NodeHttpHandlerOptions<T>): void
8+
}
9+
10+
export class CompositeNodeHttpHandlerPlugin<T extends Context, TPlugin extends NodeHttpHandlerPlugin<T>>
11+
extends CompositeStandardHandlerPlugin<T, TPlugin> implements NodeHttpHandlerPlugin<T> {
12+
initRuntimeAdapter(options: NodeHttpHandlerOptions<T>): void {
13+
for (const plugin of this.plugins) {
14+
plugin.initRuntimeAdapter?.(options)
15+
}
16+
}
17+
}

packages/server/src/adapters/standard/handler.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { StandardCodec, StandardMatcher } from './types'
99
import { ORPCError, toORPCError } from '@orpc/client'
1010
import { intercept, toArray } from '@orpc/shared'
1111
import { createProcedureClient } from '../../procedure-client'
12+
import { CompositeStandardHandlerPlugin, type StandardHandlerPlugin } from './plugin'
1213

1314
export interface StandardHandleOptions<T extends Context> {
1415
prefix?: HTTPPath
@@ -17,10 +18,6 @@ export interface StandardHandleOptions<T extends Context> {
1718

1819
export type StandardHandleResult = { matched: true, response: StandardResponse } | { matched: false, response: undefined }
1920

20-
export interface StandardHandlerPlugin<TContext extends Context> {
21-
init?(options: StandardHandlerOptions<TContext>): void
22-
}
23-
2421
export interface StandardHandlerInterceptorOptions<T extends Context> extends StandardHandleOptions<T> {
2522
request: StandardLazyRequest
2623
}
@@ -60,9 +57,9 @@ export class StandardHandler<T extends Context> {
6057
private readonly codec: StandardCodec,
6158
options: NoInfer<StandardHandlerOptions<T>>,
6259
) {
63-
for (const plugin of toArray(options.plugins)) {
64-
plugin.init?.(options)
65-
}
60+
const plugins = new CompositeStandardHandlerPlugin(options.plugins)
61+
62+
plugins.init(options)
6663

6764
this.interceptors = toArray(options.interceptors)
6865
this.clientInterceptors = toArray(options.clientInterceptors)

packages/server/src/adapters/standard/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './handler'
2+
export * from './plugin'
23
export * from './rpc-codec'
34
export * from './rpc-handler'
45
export * from './rpc-matcher'

0 commit comments

Comments
 (0)