@atproto/xrpc-server 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +11 -4
  4. package/build.js +0 -8
  5. package/dist/auth.d.ts +1 -1
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.js +17210 -8937
  8. package/dist/index.js.map +4 -4
  9. package/dist/logger.d.ts +2 -1
  10. package/dist/rate-limiter.d.ts +29 -0
  11. package/dist/server.d.ts +5 -1
  12. package/dist/stream/logger.d.ts +2 -1
  13. package/dist/stream/stream.d.ts +5 -2
  14. package/dist/stream/subscription.d.ts +2 -1
  15. package/dist/stream/types.d.ts +6 -6
  16. package/dist/stream/websocket-keepalive.d.ts +23 -0
  17. package/dist/types.d.ts +67 -9
  18. package/dist/util.d.ts +15 -0
  19. package/package.json +19 -25
  20. package/src/auth.ts +2 -2
  21. package/src/index.ts +4 -0
  22. package/src/logger.ts +2 -1
  23. package/src/rate-limiter.ts +167 -0
  24. package/src/server.ts +117 -7
  25. package/src/stream/logger.ts +2 -1
  26. package/src/stream/stream.ts +24 -11
  27. package/src/stream/subscription.ts +21 -107
  28. package/src/stream/websocket-keepalive.ts +151 -0
  29. package/src/types.ts +83 -4
  30. package/src/util.ts +33 -0
  31. package/tests/bodies.test.ts +3 -3
  32. package/tests/procedures.test.ts +12 -12
  33. package/tests/queries.test.ts +19 -14
  34. package/tests/rate-limiter.test.ts +249 -0
  35. package/tests/responses.test.ts +77 -0
  36. package/tests/subscriptions.test.ts +71 -15
  37. package/tsconfig.build.json +1 -1
  38. package/tsconfig.json +3 -3
  39. package/dist/src/index.d.ts +0 -2
  40. package/dist/src/logger.d.ts +0 -2
  41. package/dist/src/server.d.ts +0 -19
  42. package/dist/src/types.d.ts +0 -115
  43. package/dist/src/util.d.ts +0 -10
  44. package/dist/tsconfig.build.tsbuildinfo +0 -1
  45. package/tsconfig.build.tsbuildinfo +0 -1
  46. package/update-pkg.js +0 -14
package/src/types.ts CHANGED
@@ -15,6 +15,11 @@ export type Options = {
15
15
  blobLimit?: number
16
16
  textLimit?: number
17
17
  }
18
+ rateLimits?: {
19
+ creator: RateLimiterCreator
20
+ global?: ServerRateLimitDescription[]
21
+ shared?: ServerRateLimitDescription[]
22
+ }
18
23
  }
19
24
 
20
25
  export type UndecodedParams = typeof express.request['query']
@@ -37,6 +42,7 @@ export type HandlerAuth = zod.infer<typeof handlerAuth>
37
42
  export const handlerSuccess = zod.object({
38
43
  encoding: zod.string(),
39
44
  body: zod.any(),
45
+ headers: zod.record(zod.string()).optional(),
40
46
  })
41
47
  export type HandlerSuccess = zod.infer<typeof handlerSuccess>
42
48
 
@@ -49,13 +55,17 @@ export type HandlerError = zod.infer<typeof handlerError>
49
55
 
50
56
  export type HandlerOutput = HandlerSuccess | HandlerError
51
57
 
52
- export type XRPCHandler = (ctx: {
58
+ export type XRPCReqContext = {
53
59
  auth: HandlerAuth | undefined
54
60
  params: Params
55
61
  input: HandlerInput | undefined
56
62
  req: express.Request
57
63
  res: express.Response
58
- }) => Promise<HandlerOutput> | HandlerOutput | undefined
64
+ }
65
+
66
+ export type XRPCHandler = (
67
+ ctx: XRPCReqContext,
68
+ ) => Promise<HandlerOutput> | HandlerOutput | undefined
59
69
 
60
70
  export type XRPCStreamHandler = (ctx: {
61
71
  auth: HandlerAuth | undefined
@@ -75,7 +85,66 @@ export type StreamAuthVerifier = (ctx: {
75
85
  req: IncomingMessage
76
86
  }) => Promise<AuthOutput> | AuthOutput
77
87
 
88
+ export type CalcKeyFn = (ctx: XRPCReqContext) => string
89
+ export type CalcPointsFn = (ctx: XRPCReqContext) => number
90
+
91
+ export interface RateLimiterI {
92
+ consume: RateLimiterConsume
93
+ }
94
+
95
+ export type RateLimiterConsume = (
96
+ ctx: XRPCReqContext,
97
+ opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn },
98
+ ) => Promise<RateLimiterStatus | RateLimitExceededError | null>
99
+
100
+ export type RateLimiterCreator = (opts: {
101
+ keyPrefix: string
102
+ durationMs: number
103
+ points: number
104
+ calcKey?: (ctx: XRPCReqContext) => string
105
+ calcPoints?: (ctx: XRPCReqContext) => number
106
+ }) => RateLimiterI
107
+
108
+ export type ServerRateLimitDescription = {
109
+ name: string
110
+ durationMs: number
111
+ points: number
112
+ calcKey?: (ctx: XRPCReqContext) => string
113
+ calcPoints?: (ctx: XRPCReqContext) => number
114
+ }
115
+
116
+ export type SharedRateLimitOpts = {
117
+ name: string
118
+ calcKey?: (ctx: XRPCReqContext) => string
119
+ calcPoints?: (ctx: XRPCReqContext) => number
120
+ }
121
+
122
+ export type RouteRateLimitOpts = {
123
+ durationMs: number
124
+ points: number
125
+ calcKey?: (ctx: XRPCReqContext) => string
126
+ calcPoints?: (ctx: XRPCReqContext) => number
127
+ }
128
+
129
+ export type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts
130
+
131
+ export const isShared = (
132
+ opts: HandlerRateLimitOpts,
133
+ ): opts is SharedRateLimitOpts => {
134
+ return typeof opts['name'] === 'string'
135
+ }
136
+
137
+ export type RateLimiterStatus = {
138
+ limit: number
139
+ duration: number
140
+ remainingPoints: number
141
+ msBeforeNext: number
142
+ consumedPoints: number
143
+ isFirstInDuration: boolean
144
+ }
145
+
78
146
  export type XRPCHandlerConfig = {
147
+ rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[]
79
148
  auth?: AuthVerifier
80
149
  handler: XRPCHandler
81
150
  }
@@ -153,6 +222,16 @@ export class ForbiddenError extends XRPCError {
153
222
  }
154
223
  }
155
224
 
225
+ export class RateLimitExceededError extends XRPCError {
226
+ constructor(
227
+ public status: RateLimiterStatus,
228
+ errorMessage?: string,
229
+ customErrorName?: string,
230
+ ) {
231
+ super(ResponseType.RateLimitExceeded, errorMessage, customErrorName)
232
+ }
233
+ }
234
+
156
235
  export class InternalServerError extends XRPCError {
157
236
  constructor(errorMessage?: string, customErrorName?: string) {
158
237
  super(ResponseType.InternalServerError, errorMessage, customErrorName)
@@ -165,9 +244,9 @@ export class UpstreamFailureError extends XRPCError {
165
244
  }
166
245
  }
167
246
 
168
- export class NotEnoughResoucesError extends XRPCError {
247
+ export class NotEnoughResourcesError extends XRPCError {
169
248
  constructor(errorMessage?: string, customErrorName?: string) {
170
- super(ResponseType.NotEnoughResouces, errorMessage, customErrorName)
249
+ super(ResponseType.NotEnoughResources, errorMessage, customErrorName)
171
250
  }
172
251
  }
173
252
 
package/src/util.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import assert from 'assert'
1
2
  import { Readable, Transform } from 'stream'
2
3
  import { createDeflate, createGunzip } from 'zlib'
3
4
  import express from 'express'
@@ -266,3 +267,35 @@ function decodeBodyStream(
266
267
 
267
268
  return stream
268
269
  }
270
+
271
+ export function serverTimingHeader(timings: ServerTiming[]) {
272
+ return timings
273
+ .map((timing) => {
274
+ let header = timing.name
275
+ if (timing.duration) header += `;dur=${timing.duration}`
276
+ if (timing.description) header += `;desc="${timing.description}"`
277
+ return header
278
+ })
279
+ .join(', ')
280
+ }
281
+
282
+ export class ServerTimer implements ServerTiming {
283
+ public duration?: number
284
+ private startMs?: number
285
+ constructor(public name: string, public description?: string) {}
286
+ start() {
287
+ this.startMs = Date.now()
288
+ return this
289
+ }
290
+ stop() {
291
+ assert(this.startMs, "timer hasn't been started")
292
+ this.duration = Date.now() - this.startMs
293
+ return this
294
+ }
295
+ }
296
+
297
+ export interface ServerTiming {
298
+ name: string
299
+ duration?: number
300
+ description?: string
301
+ }
@@ -43,7 +43,7 @@ const LEXICONS = [
43
43
  },
44
44
  {
45
45
  lexicon: 1,
46
- id: 'io.example.validationTest2',
46
+ id: 'io.example.validationTestTwo',
47
47
  defs: {
48
48
  main: {
49
49
  type: 'query',
@@ -101,7 +101,7 @@ describe('Bodies', () => {
101
101
  body: ctx.input?.body,
102
102
  }),
103
103
  )
104
- server.method('io.example.validationTest2', () => ({
104
+ server.method('io.example.validationTestTwo', () => ({
105
105
  encoding: 'json',
106
106
  body: { wrong: 'data' },
107
107
  }))
@@ -175,7 +175,7 @@ describe('Bodies', () => {
175
175
  return logger.error(obj, ...args)
176
176
  }
177
177
 
178
- await expect(client.call('io.example.validationTest2')).rejects.toThrow(
178
+ await expect(client.call('io.example.validationTestTwo')).rejects.toThrow(
179
179
  'Internal Server Error',
180
180
  )
181
181
  expect(error).toEqual(`Output must have the property "foo"`)
@@ -8,7 +8,7 @@ import * as xrpcServer from '../src'
8
8
  const LEXICONS = [
9
9
  {
10
10
  lexicon: 1,
11
- id: 'io.example.ping1',
11
+ id: 'io.example.pingOne',
12
12
  defs: {
13
13
  main: {
14
14
  type: 'procedure',
@@ -26,7 +26,7 @@ const LEXICONS = [
26
26
  },
27
27
  {
28
28
  lexicon: 1,
29
- id: 'io.example.ping2',
29
+ id: 'io.example.pingTwo',
30
30
  defs: {
31
31
  main: {
32
32
  type: 'procedure',
@@ -41,7 +41,7 @@ const LEXICONS = [
41
41
  },
42
42
  {
43
43
  lexicon: 1,
44
- id: 'io.example.ping3',
44
+ id: 'io.example.pingThree',
45
45
  defs: {
46
46
  main: {
47
47
  type: 'procedure',
@@ -56,7 +56,7 @@ const LEXICONS = [
56
56
  },
57
57
  {
58
58
  lexicon: 1,
59
- id: 'io.example.ping4',
59
+ id: 'io.example.pingFour',
60
60
  defs: {
61
61
  main: {
62
62
  type: 'procedure',
@@ -84,17 +84,17 @@ const LEXICONS = [
84
84
  describe('Procedures', () => {
85
85
  let s: http.Server
86
86
  const server = xrpcServer.createServer(LEXICONS)
87
- server.method('io.example.ping1', (ctx: { params: xrpcServer.Params }) => {
87
+ server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
88
88
  return { encoding: 'text/plain', body: ctx.params.message }
89
89
  })
90
90
  server.method(
91
- 'io.example.ping2',
91
+ 'io.example.pingTwo',
92
92
  (ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
93
93
  return { encoding: 'text/plain', body: ctx.input?.body }
94
94
  },
95
95
  )
96
96
  server.method(
97
- 'io.example.ping3',
97
+ 'io.example.pingThree',
98
98
  async (ctx: {
99
99
  params: xrpcServer.Params
100
100
  input?: xrpcServer.HandlerInput
@@ -112,7 +112,7 @@ describe('Procedures', () => {
112
112
  },
113
113
  )
114
114
  server.method(
115
- 'io.example.ping4',
115
+ 'io.example.pingFour',
116
116
  (ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
117
117
  return {
118
118
  encoding: 'application/json',
@@ -133,14 +133,14 @@ describe('Procedures', () => {
133
133
  })
134
134
 
135
135
  it('serves requests', async () => {
136
- const res1 = await client.call('io.example.ping1', {
136
+ const res1 = await client.call('io.example.pingOne', {
137
137
  message: 'hello world',
138
138
  })
139
139
  expect(res1.success).toBeTruthy()
140
140
  expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
141
141
  expect(res1.data).toBe('hello world')
142
142
 
143
- const res2 = await client.call('io.example.ping2', {}, 'hello world', {
143
+ const res2 = await client.call('io.example.pingTwo', {}, 'hello world', {
144
144
  encoding: 'text/plain',
145
145
  })
146
146
  expect(res2.success).toBeTruthy()
@@ -148,7 +148,7 @@ describe('Procedures', () => {
148
148
  expect(res2.data).toBe('hello world')
149
149
 
150
150
  const res3 = await client.call(
151
- 'io.example.ping3',
151
+ 'io.example.pingThree',
152
152
  {},
153
153
  new TextEncoder().encode('hello world'),
154
154
  { encoding: 'application/octet-stream' },
@@ -158,7 +158,7 @@ describe('Procedures', () => {
158
158
  expect(new TextDecoder().decode(res3.data)).toBe('hello world')
159
159
 
160
160
  const res4 = await client.call(
161
- 'io.example.ping4',
161
+ 'io.example.pingFour',
162
162
  {},
163
163
  { message: 'hello world' },
164
164
  )
@@ -7,7 +7,7 @@ import * as xrpcServer from '../src'
7
7
  const LEXICONS = [
8
8
  {
9
9
  lexicon: 1,
10
- id: 'io.example.ping1',
10
+ id: 'io.example.pingOne',
11
11
  defs: {
12
12
  main: {
13
13
  type: 'query',
@@ -25,7 +25,7 @@ const LEXICONS = [
25
25
  },
26
26
  {
27
27
  lexicon: 1,
28
- id: 'io.example.ping2',
28
+ id: 'io.example.pingTwo',
29
29
  defs: {
30
30
  main: {
31
31
  type: 'query',
@@ -43,7 +43,7 @@ const LEXICONS = [
43
43
  },
44
44
  {
45
45
  lexicon: 1,
46
- id: 'io.example.ping3',
46
+ id: 'io.example.pingThree',
47
47
  defs: {
48
48
  main: {
49
49
  type: 'query',
@@ -69,21 +69,25 @@ const LEXICONS = [
69
69
  describe('Queries', () => {
70
70
  let s: http.Server
71
71
  const server = xrpcServer.createServer(LEXICONS)
72
- server.method('io.example.ping1', (ctx: { params: xrpcServer.Params }) => {
72
+ server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
73
73
  return { encoding: 'text/plain', body: ctx.params.message }
74
74
  })
75
- server.method('io.example.ping2', (ctx: { params: xrpcServer.Params }) => {
75
+ server.method('io.example.pingTwo', (ctx: { params: xrpcServer.Params }) => {
76
76
  return {
77
77
  encoding: 'application/octet-stream',
78
78
  body: new TextEncoder().encode(String(ctx.params.message)),
79
79
  }
80
80
  })
81
- server.method('io.example.ping3', (ctx: { params: xrpcServer.Params }) => {
82
- return {
83
- encoding: 'application/json',
84
- body: { message: ctx.params.message },
85
- }
86
- })
81
+ server.method(
82
+ 'io.example.pingThree',
83
+ (ctx: { params: xrpcServer.Params }) => {
84
+ return {
85
+ encoding: 'application/json',
86
+ body: { message: ctx.params.message },
87
+ headers: { 'x-test-header-name': 'test-value' },
88
+ }
89
+ },
90
+ )
87
91
  xrpc.addLexicons(LEXICONS)
88
92
 
89
93
  let client: ServiceClient
@@ -97,25 +101,26 @@ describe('Queries', () => {
97
101
  })
98
102
 
99
103
  it('serves requests', async () => {
100
- const res1 = await client.call('io.example.ping1', {
104
+ const res1 = await client.call('io.example.pingOne', {
101
105
  message: 'hello world',
102
106
  })
103
107
  expect(res1.success).toBeTruthy()
104
108
  expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
105
109
  expect(res1.data).toBe('hello world')
106
110
 
107
- const res2 = await client.call('io.example.ping2', {
111
+ const res2 = await client.call('io.example.pingTwo', {
108
112
  message: 'hello world',
109
113
  })
110
114
  expect(res2.success).toBeTruthy()
111
115
  expect(res2.headers['content-type']).toBe('application/octet-stream')
112
116
  expect(new TextDecoder().decode(res2.data)).toBe('hello world')
113
117
 
114
- const res3 = await client.call('io.example.ping3', {
118
+ const res3 = await client.call('io.example.pingThree', {
115
119
  message: 'hello world',
116
120
  })
117
121
  expect(res3.success).toBeTruthy()
118
122
  expect(res3.headers['content-type']).toBe('application/json; charset=utf-8')
119
123
  expect(res3.data?.message).toBe('hello world')
124
+ expect(res3.headers['x-test-header-name']).toEqual('test-value')
120
125
  })
121
126
  })
@@ -0,0 +1,249 @@
1
+ import * as http from 'http'
2
+ import getPort from 'get-port'
3
+ import xrpc, { ServiceClient } from '@atproto/xrpc'
4
+ import { createServer, closeServer } from './_util'
5
+ import * as xrpcServer from '../src'
6
+ import { RateLimiter } from '../src'
7
+ import { MINUTE } from '@atproto/common'
8
+
9
+ const LEXICONS = [
10
+ {
11
+ lexicon: 1,
12
+ id: 'io.example.routeLimit',
13
+ defs: {
14
+ main: {
15
+ type: 'query',
16
+ parameters: {
17
+ type: 'params',
18
+ required: ['str'],
19
+ properties: {
20
+ str: { type: 'string' },
21
+ },
22
+ },
23
+ output: {
24
+ encoding: 'application/json',
25
+ },
26
+ },
27
+ },
28
+ },
29
+ {
30
+ lexicon: 1,
31
+ id: 'io.example.sharedLimitOne',
32
+ defs: {
33
+ main: {
34
+ type: 'query',
35
+ parameters: {
36
+ type: 'params',
37
+ required: ['points'],
38
+ properties: {
39
+ points: { type: 'integer' },
40
+ },
41
+ },
42
+ output: {
43
+ encoding: 'application/json',
44
+ },
45
+ },
46
+ },
47
+ },
48
+ {
49
+ lexicon: 1,
50
+ id: 'io.example.sharedLimitTwo',
51
+ defs: {
52
+ main: {
53
+ type: 'query',
54
+ parameters: {
55
+ type: 'params',
56
+ required: ['points'],
57
+ properties: {
58
+ points: { type: 'integer' },
59
+ },
60
+ },
61
+ output: {
62
+ encoding: 'application/json',
63
+ },
64
+ },
65
+ },
66
+ },
67
+ {
68
+ lexicon: 1,
69
+ id: 'io.example.toggleLimit',
70
+ defs: {
71
+ main: {
72
+ type: 'query',
73
+ parameters: {
74
+ type: 'params',
75
+ properties: {
76
+ shouldCount: { type: 'boolean' },
77
+ },
78
+ },
79
+ output: {
80
+ encoding: 'application/json',
81
+ },
82
+ },
83
+ },
84
+ },
85
+ {
86
+ lexicon: 1,
87
+ id: 'io.example.noLimit',
88
+ defs: {
89
+ main: {
90
+ type: 'query',
91
+ output: {
92
+ encoding: 'application/json',
93
+ },
94
+ },
95
+ },
96
+ },
97
+ ]
98
+
99
+ describe('Parameters', () => {
100
+ let s: http.Server
101
+ const server = xrpcServer.createServer(LEXICONS, {
102
+ rateLimits: {
103
+ creator: (opts: xrpcServer.RateLimiterOpts) =>
104
+ RateLimiter.memory({
105
+ bypassSecret: 'bypass',
106
+ ...opts,
107
+ }),
108
+ shared: [
109
+ {
110
+ name: 'shared-limit',
111
+ durationMs: 5 * MINUTE,
112
+ points: 6,
113
+ },
114
+ ],
115
+ global: [
116
+ {
117
+ name: 'global-ip',
118
+ durationMs: 5 * MINUTE,
119
+ points: 100,
120
+ },
121
+ ],
122
+ },
123
+ })
124
+ server.method('io.example.routeLimit', {
125
+ rateLimit: {
126
+ durationMs: 5 * MINUTE,
127
+ points: 5,
128
+ calcKey: ({ params }) => params.str as string,
129
+ },
130
+ handler: (ctx: { params: xrpcServer.Params }) => ({
131
+ encoding: 'json',
132
+ body: ctx.params,
133
+ }),
134
+ })
135
+
136
+ server.method('io.example.sharedLimitOne', {
137
+ rateLimit: {
138
+ name: 'shared-limit',
139
+ calcPoints: ({ params }) => params.points as number,
140
+ },
141
+ handler: (ctx: { params: xrpcServer.Params }) => ({
142
+ encoding: 'json',
143
+ body: ctx.params,
144
+ }),
145
+ })
146
+ server.method('io.example.sharedLimitTwo', {
147
+ rateLimit: {
148
+ name: 'shared-limit',
149
+ calcPoints: ({ params }) => params.points as number,
150
+ },
151
+ handler: (ctx: { params: xrpcServer.Params }) => ({
152
+ encoding: 'json',
153
+ body: ctx.params,
154
+ }),
155
+ })
156
+ server.method('io.example.toggleLimit', {
157
+ rateLimit: [
158
+ {
159
+ durationMs: 5 * MINUTE,
160
+ points: 5,
161
+ calcPoints: ({ params }) => (params.shouldCount ? 1 : 0),
162
+ },
163
+ {
164
+ durationMs: 5 * MINUTE,
165
+ points: 10,
166
+ },
167
+ ],
168
+ handler: (ctx: { params: xrpcServer.Params }) => ({
169
+ encoding: 'json',
170
+ body: ctx.params,
171
+ }),
172
+ })
173
+ server.method('io.example.noLimit', {
174
+ handler: () => ({
175
+ encoding: 'json',
176
+ body: {},
177
+ }),
178
+ })
179
+
180
+ xrpc.addLexicons(LEXICONS)
181
+
182
+ let client: ServiceClient
183
+ beforeAll(async () => {
184
+ const port = await getPort()
185
+ s = await createServer(port, server)
186
+ client = xrpc.service(`http://localhost:${port}`)
187
+ })
188
+ afterAll(async () => {
189
+ await closeServer(s)
190
+ })
191
+
192
+ it('rate limits a given route', async () => {
193
+ const makeCall = () => client.call('io.example.routeLimit', { str: 'test' })
194
+ for (let i = 0; i < 5; i++) {
195
+ await makeCall()
196
+ }
197
+ await expect(makeCall).rejects.toThrow('Rate Limit Exceeded')
198
+ })
199
+
200
+ it('rate limits on a shared route', async () => {
201
+ await client.call('io.example.sharedLimitOne', { points: 1 })
202
+ await client.call('io.example.sharedLimitTwo', { points: 1 })
203
+ await client.call('io.example.sharedLimitOne', { points: 2 })
204
+ await client.call('io.example.sharedLimitTwo', { points: 2 })
205
+ await expect(
206
+ client.call('io.example.sharedLimitOne', { points: 1 }),
207
+ ).rejects.toThrow('Rate Limit Exceeded')
208
+ await expect(
209
+ client.call('io.example.sharedLimitTwo', { points: 1 }),
210
+ ).rejects.toThrow('Rate Limit Exceeded')
211
+ })
212
+
213
+ it('applies multiple rate-limits', async () => {
214
+ const makeCall = (shouldCount: boolean) =>
215
+ client.call('io.example.toggleLimit', { shouldCount })
216
+ for (let i = 0; i < 5; i++) {
217
+ await makeCall(true)
218
+ }
219
+ await expect(() => makeCall(true)).rejects.toThrow('Rate Limit Exceeded')
220
+ for (let i = 0; i < 4; i++) {
221
+ await makeCall(false)
222
+ }
223
+ await expect(() => makeCall(false)).rejects.toThrow('Rate Limit Exceeded')
224
+ })
225
+
226
+ it('applies global limits', async () => {
227
+ const makeCall = () => client.call('io.example.noLimit')
228
+ const calls: Promise<unknown>[] = []
229
+ for (let i = 0; i < 110; i++) {
230
+ calls.push(makeCall())
231
+ }
232
+ await expect(Promise.all(calls)).rejects.toThrow('Rate Limit Exceeded')
233
+ })
234
+
235
+ it('can bypass rate limits', async () => {
236
+ const makeCall = () =>
237
+ client.call(
238
+ 'io.example.noLimit',
239
+ {},
240
+ {},
241
+ { headers: { 'X-RateLimit-Bypass': 'bypass' } },
242
+ )
243
+ const calls: Promise<unknown>[] = []
244
+ for (let i = 0; i < 110; i++) {
245
+ calls.push(makeCall())
246
+ }
247
+ await Promise.all(calls)
248
+ })
249
+ })