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

Lines changed: 275 additions & 50 deletions

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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 4 additions & 7 deletions
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)
Lines changed: 37 additions & 0 deletions
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+
})
Lines changed: 21 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 1 addition & 9 deletions
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

Lines changed: 6 additions & 8 deletions
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
Lines changed: 1 addition & 0 deletions
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'
Lines changed: 9 additions & 0 deletions
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+
})

0 commit comments

Comments
 (0)