Skip to content

Commit 7134cf9

Browse files
authored
feat(client)!: update ClientRetryPlugin options (#280)
- rename `eventIteratorLastRetry` ⇾ `lastEventRetry` - rename `eventIteratorLastEventId` ⇾ `lastEventId` - add some missing tests <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **Documentation** - Updated plugin documentation to reflect the revised default retry configuration. - **New Features** - Enhanced the retry mechanism with a customizable decision process and improved error context during retry attempts. - **Refactor** - Streamlined internal retry logic by standardizing parameter naming for improved clarity and consistency. - Updated default retry delay configuration for better functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 1ea8333 commit 7134cf9

3 files changed

Lines changed: 65 additions & 30 deletions

File tree

apps/content/docs/plugins/client-retry.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const planets = await client.planet.list({ limit: 10 }, {
5555
By default, retries are disabled unless a `retry` count is explicitly set.
5656

5757
- **retry:** Maximum retry attempts before throwing an error (default: `0`).
58-
- **retryDelay:** Delay between retries (default: `(o) => o.eventIteratorLastRetry ?? 2000`).
58+
- **retryDelay:** Delay between retries (default: `(o) => o.lastEventRetry ?? 2000`).
5959
- **shouldRetry:** Function that determines whether to retry (default: `true`).
6060
:::
6161

packages/client/src/plugins/retry.test.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -221,14 +221,16 @@ describe('clientRetryPlugin', () => {
221221
expect(handlerFn).toHaveBeenCalledTimes(1)
222222
})
223223

224-
it('should retry with id in data', async () => {
224+
it('should retry with meta data', async () => {
225225
handlerFn.mockImplementation(async function* () {
226226
yield 1
227-
yield withEventMeta({ order: 2 }, { id: '5' })
227+
yield withEventMeta({ order: 2 }, { id: '5', retry: 5678 })
228228
throw new Error('fail')
229229
})
230230

231-
const iterator = await client('hello', { context: { retry: 3, retryDelay: 0 }, lastEventId: '1' })
231+
const shouldRetry = vi.fn(() => true)
232+
233+
const iterator = await client('hello', { context: { retry: 3, retryDelay: 0, shouldRetry }, lastEventId: '1' })
232234

233235
expect(await iterator.next()).toSatisfy(({ done, value }) => {
234236
expect(done).toEqual(false)
@@ -239,7 +241,7 @@ describe('clientRetryPlugin', () => {
239241
expect(await iterator.next()).toSatisfy(({ done, value }) => {
240242
expect(done).toEqual(false)
241243
expect(value).toEqual({ order: 2 })
242-
expect(getEventMeta(value)).toMatchObject({ id: '5' })
244+
expect(getEventMeta(value)).toMatchObject({ id: '5', retry: 5678 })
243245
return true
244246
})
245247

@@ -252,16 +254,27 @@ describe('clientRetryPlugin', () => {
252254
expect(handlerFn).toHaveBeenCalledTimes(2)
253255
expect(handlerFn).toHaveBeenNthCalledWith(1, expect.objectContaining({ input: 'hello', lastEventId: '1' }))
254256
expect(handlerFn).toHaveBeenNthCalledWith(2, expect.objectContaining({ input: 'hello', lastEventId: '5' }))
257+
258+
expect(shouldRetry).toHaveBeenCalledTimes(1)
259+
expect(shouldRetry).toHaveBeenNthCalledWith(
260+
1,
261+
expect.objectContaining({ error: expect.any(Error), lastEventId: '5', lastEventRetry: 5678 }),
262+
expect.objectContaining({ lastEventId: '5' }),
263+
[],
264+
'hello',
265+
)
255266
})
256267

257-
it('should retry with id in error', async () => {
268+
it('should retry with meta in error', async () => {
258269
handlerFn.mockImplementation(async function* () {
259270
yield 1
260271
yield { order: 2 }
261-
throw withEventMeta(new Error('fail'), { id: '10' })
272+
throw withEventMeta(new Error('fail'), { id: '10', retry: 1234 })
262273
})
263274

264-
const iterator = await client('hello', { context: { retry: 3, retryDelay: 0 }, lastEventId: '1' })
275+
const shouldRetry = vi.fn(() => true)
276+
277+
const iterator = await client('hello', { context: { retry: 1, retryDelay: 0, shouldRetry }, lastEventId: '1' })
265278

266279
expect(await iterator.next()).toSatisfy(({ done, value }) => {
267280
expect(done).toEqual(false)
@@ -281,9 +294,30 @@ describe('clientRetryPlugin', () => {
281294
return true
282295
})
283296

297+
expect(await iterator.next()).toSatisfy(({ done, value }) => {
298+
expect(done).toEqual(false)
299+
expect(value).toEqual({ order: 2 })
300+
return true
301+
})
302+
303+
await expect(iterator.next()).rejects.toSatisfy((error) => {
304+
expect(error).toBeInstanceOf(ORPCError)
305+
expect(getEventMeta(error)).toMatchObject({ id: '10', retry: 1234 })
306+
return true
307+
})
308+
284309
expect(handlerFn).toHaveBeenCalledTimes(2)
285310
expect(handlerFn).toHaveBeenNthCalledWith(1, expect.objectContaining({ input: 'hello', lastEventId: '1' }))
286311
expect(handlerFn).toHaveBeenNthCalledWith(2, expect.objectContaining({ input: 'hello', lastEventId: '10' }))
312+
313+
expect(shouldRetry).toHaveBeenCalledTimes(1)
314+
expect(shouldRetry).toHaveBeenNthCalledWith(
315+
1,
316+
expect.objectContaining({ error: expect.any(Error), lastEventId: '10', lastEventRetry: 1234 }),
317+
expect.objectContaining({ lastEventId: '10' }),
318+
[],
319+
'hello',
320+
)
287321
})
288322

289323
it('should retry with delay', { retry: 5 }, async () => {
@@ -338,8 +372,9 @@ describe('clientRetryPlugin', () => {
338372
})
339373

340374
it('onRetry', async () => {
375+
let time = 0
341376
handlerFn.mockImplementation(async function* () {
342-
throw new Error('fail')
377+
throw withEventMeta(new Error('fail'), { id: `${time++}` })
343378
})
344379

345380
const clean = vi.fn()
@@ -354,21 +389,21 @@ describe('clientRetryPlugin', () => {
354389
expect(onRetry).toHaveBeenCalledTimes(3)
355390
expect(onRetry).toHaveBeenNthCalledWith(
356391
1,
357-
{ attemptIndex: 0, error: expect.any(ORPCError) },
392+
{ attemptIndex: 0, error: expect.any(ORPCError), lastEventId: '0' },
358393
expect.objectContaining({ context: { retry: 3, retryDelay: 0, onRetry } }),
359394
[],
360395
'hello',
361396
)
362397
expect(onRetry).toHaveBeenNthCalledWith(
363398
2,
364-
{ attemptIndex: 1, error: expect.any(ORPCError) },
399+
{ attemptIndex: 1, error: expect.any(ORPCError), lastEventId: '1' },
365400
expect.objectContaining({ context: { retry: 3, retryDelay: 0, onRetry } }),
366401
[],
367402
'hello',
368403
)
369404
expect(onRetry).toHaveBeenNthCalledWith(
370405
3,
371-
{ attemptIndex: 2, error: expect.any(ORPCError) },
406+
{ attemptIndex: 2, error: expect.any(ORPCError), lastEventId: '2' },
372407
expect.objectContaining({ context: { retry: 3, retryDelay: 0, onRetry } }),
373408
[],
374409
'hello',

packages/client/src/plugins/retry.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { isAsyncIteratorObject, value } from '@orpc/shared'
66
import { getEventMeta } from '@orpc/standard-server'
77

88
export interface ClientRetryPluginAttemptOptions {
9-
eventIteratorLastRetry: number | undefined
10-
eventIteratorLastEventId: string | undefined
9+
lastEventRetry: number | undefined
10+
lastEventId: string | undefined
1111
attemptIndex: number
1212
error: unknown
1313
}
@@ -24,7 +24,7 @@ export interface ClientRetryPluginContext {
2424
/**
2525
* Delay (in ms) before retrying.
2626
*
27-
* @default (o) => o.eventIteratorLastRetry ?? 2000
27+
* @default (o) => o.lastEventRetry ?? 2000
2828
*/
2929
retryDelay?: Value<number, [
3030
attemptOptions: ClientRetryPluginAttemptOptions,
@@ -70,7 +70,7 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
7070

7171
constructor(options: ClientRetryPluginOptions = {}) {
7272
this.defaultRetry = options.default?.retry ?? 0
73-
this.defaultRetryDelay = options.default?.retryDelay ?? (o => o.eventIteratorLastRetry ?? 2000)
73+
this.defaultRetryDelay = options.default?.retryDelay ?? (o => o.lastEventRetry ?? 2000)
7474
this.defaultShouldRetry = options.default?.shouldRetry ?? true
7575
this.defaultOnRetry = options.default?.onRetry
7676
}
@@ -88,15 +88,17 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
8888
return interceptorOptions.next()
8989
}
9090

91-
let eventIteratorLastEventId = interceptorOptions.options.lastEventId
92-
let eventIteratorLastRetry: undefined | number
91+
let lastEventId = interceptorOptions.options.lastEventId
92+
let lastEventRetry: undefined | number
9393
let unsubscribe: void | (() => void)
9494
let attemptIndex = 0
9595

9696
const next = async (initial?: { error: unknown }) => {
9797
let current = initial
9898

9999
while (true) {
100+
const newClientOptions = { ...interceptorOptions.options, lastEventId }
101+
100102
if (current) {
101103
if (attemptIndex >= maxAttempts) {
102104
throw current.error
@@ -105,14 +107,14 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
105107
const attemptOptions: ClientRetryPluginAttemptOptions = {
106108
attemptIndex,
107109
error: current.error,
108-
eventIteratorLastEventId,
109-
eventIteratorLastRetry,
110+
lastEventId,
111+
lastEventRetry,
110112
}
111113

112114
const shouldRetryBool = await value(
113115
shouldRetry,
114116
attemptOptions,
115-
interceptorOptions.options,
117+
newClientOptions,
116118
interceptorOptions.path,
117119
interceptorOptions.input,
118120
)
@@ -123,15 +125,15 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
123125

124126
unsubscribe = onRetry?.(
125127
attemptOptions,
126-
interceptorOptions.options,
128+
newClientOptions,
127129
interceptorOptions.path,
128130
interceptorOptions.input,
129131
)
130132

131133
const retryDelayMs = await value(
132134
retryDelay,
133135
attemptOptions,
134-
interceptorOptions.options,
136+
newClientOptions,
135137
interceptorOptions.path,
136138
interceptorOptions.input,
137139
)
@@ -142,8 +144,6 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
142144
}
143145

144146
try {
145-
const newClientOptions = { ...interceptorOptions.options, lastEventId: eventIteratorLastEventId }
146-
147147
const output = await interceptorOptions.next({
148148
...interceptorOptions,
149149
options: newClientOptions,
@@ -152,7 +152,7 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
152152
return output
153153
}
154154
catch (error) {
155-
if (interceptorOptions.options.signal?.aborted === true) {
155+
if (newClientOptions.signal?.aborted === true) {
156156
throw error
157157
}
158158

@@ -180,8 +180,8 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
180180
const item = await current.next()
181181

182182
const meta = getEventMeta(item.value)
183-
eventIteratorLastEventId = meta?.id ?? eventIteratorLastEventId
184-
eventIteratorLastRetry = meta?.retry ?? eventIteratorLastRetry
183+
lastEventId = meta?.id ?? lastEventId
184+
lastEventRetry = meta?.retry ?? lastEventRetry
185185

186186
if (item.done) {
187187
return item.value
@@ -191,8 +191,8 @@ export class ClientRetryPlugin<T extends ClientContext & ClientRetryPluginContex
191191
}
192192
catch (error) {
193193
const meta = getEventMeta(error)
194-
eventIteratorLastEventId = meta?.id ?? eventIteratorLastEventId
195-
eventIteratorLastRetry = meta?.retry ?? eventIteratorLastRetry
194+
lastEventId = meta?.id ?? lastEventId
195+
lastEventRetry = meta?.retry ?? lastEventRetry
196196

197197
const maybeEventIterator = await next({ error })
198198

0 commit comments

Comments
 (0)