@toa.io/extensions.exposition 1.0.0-alpha.269 → 1.0.0-alpha.270
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.
- package/CHANGELOG.md +31 -0
- package/components/exposition.stash/manifest.toa.yaml +13 -4
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/documentation/io.md +26 -23
- package/documentation/notes/throttling.md +39 -5
- package/features/etag.feature +55 -5
- package/features/io.throttle.feature +3 -5
- package/package.json +5 -5
- package/schemas/io/throttle.cos.yaml +2 -3
- package/source/Directive.test.ts +3 -0
- package/source/Directive.ts +25 -4
- package/source/Endpoint.ts +7 -11
- package/source/Factory.ts +16 -1
- package/source/HTTP/Context.ts +39 -8
- package/source/HTTP/Server.ts +16 -11
- package/source/HTTP/exceptions.ts +9 -3
- package/source/HTTP/messages.ts +31 -0
- package/source/RTD/Directives.ts +6 -0
- package/source/directives/auth/Authorization.ts +29 -3
- package/source/directives/io/Directive.ts +5 -4
- package/source/directives/io/IO.ts +15 -10
- package/source/directives/io/Input.ts +3 -3
- package/source/directives/io/Output.ts +12 -4
- package/source/directives/io/Throttle.ts +9 -12
- package/source/directives/io/lib/throttle/Configuration.test.ts +1 -2
- package/source/directives/io/lib/throttle/Configuration.ts +2 -4
- package/source/directives/io/lib/throttle/Quotas.test.ts +233 -148
- package/source/directives/io/lib/throttle/Quotas.ts +129 -53
- package/source/directives/io/lib/throttle/Sync.test.ts +153 -0
- package/source/directives/io/lib/throttle/Sync.ts +99 -0
- package/source/directives/io/lib/throttle/index.ts +1 -0
- package/source/directives/io/schemas.test.ts +7 -1
- package/transpiled/Directive.d.ts +2 -0
- package/transpiled/Directive.js +16 -4
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Endpoint.js +7 -8
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Factory.js +14 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +1 -1
- package/transpiled/HTTP/Context.js +30 -8
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/HTTP/Server.js +14 -10
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/exceptions.d.ts +5 -2
- package/transpiled/HTTP/exceptions.js +7 -3
- package/transpiled/HTTP/exceptions.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +2 -0
- package/transpiled/HTTP/messages.js +21 -0
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +5 -0
- package/transpiled/directives/auth/Authorization.d.ts +1 -0
- package/transpiled/directives/auth/Authorization.js +21 -2
- package/transpiled/directives/auth/Authorization.js.map +1 -1
- package/transpiled/directives/io/Directive.d.ts +4 -4
- package/transpiled/directives/io/IO.d.ts +9 -4
- package/transpiled/directives/io/IO.js +14 -9
- package/transpiled/directives/io/IO.js.map +1 -1
- package/transpiled/directives/io/Input.d.ts +1 -1
- package/transpiled/directives/io/Input.js +3 -3
- package/transpiled/directives/io/Input.js.map +1 -1
- package/transpiled/directives/io/Output.d.ts +2 -0
- package/transpiled/directives/io/Output.js +9 -3
- package/transpiled/directives/io/Output.js.map +1 -1
- package/transpiled/directives/io/Throttle.d.ts +3 -5
- package/transpiled/directives/io/Throttle.js +8 -10
- package/transpiled/directives/io/Throttle.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Configuration.d.ts +0 -1
- package/transpiled/directives/io/lib/throttle/Configuration.js +2 -3
- package/transpiled/directives/io/lib/throttle/Configuration.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Quotas.d.ts +46 -22
- package/transpiled/directives/io/lib/throttle/Quotas.js +107 -46
- package/transpiled/directives/io/lib/throttle/Quotas.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Sync.d.ts +30 -0
- package/transpiled/directives/io/lib/throttle/Sync.js +76 -0
- package/transpiled/directives/io/lib/throttle/Sync.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/index.d.ts +1 -0
- package/transpiled/directives/io/lib/throttle/index.js +3 -1
- package/transpiled/directives/io/lib/throttle/index.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/components/exposition.stash/operations/count.js +0 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setTimeout } from 'node:timers/promises'
|
|
2
2
|
import { Quotas } from './Quotas'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Batch } from './Sync'
|
|
4
4
|
import type { Configuration } from './Configuration'
|
|
5
5
|
import type { Input as Context, Output } from '../../../../io'
|
|
6
6
|
|
|
@@ -8,52 +8,77 @@ let quotas: Quotas
|
|
|
8
8
|
let configuration: Configuration
|
|
9
9
|
let context: Context
|
|
10
10
|
let output: Output
|
|
11
|
-
let counted: Request[]
|
|
12
11
|
|
|
13
12
|
beforeEach(() => {
|
|
14
13
|
output = { status: 200 }
|
|
15
|
-
counted = []
|
|
16
14
|
})
|
|
17
15
|
|
|
18
16
|
describe('common', () => {
|
|
19
17
|
beforeEach(() => {
|
|
20
18
|
context = createContext()
|
|
21
19
|
configuration = createConfiguration()
|
|
22
|
-
quotas = Quotas.create(configuration
|
|
20
|
+
quotas = Quotas.create(configuration)
|
|
23
21
|
})
|
|
24
22
|
|
|
25
|
-
it('should
|
|
26
|
-
expect(quotas.
|
|
23
|
+
it('should admit a burst of the whole budget', () => {
|
|
24
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
25
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should refuse past the budget', () => {
|
|
29
|
+
quotas.check(context, [])
|
|
30
|
+
quotas.check(context, [])
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
expect(quotas.check(context, [])).toBeGreaterThan(0)
|
|
29
33
|
})
|
|
30
34
|
|
|
31
|
-
it('should
|
|
32
|
-
|
|
35
|
+
it('should not charge what it refuses', async () => {
|
|
36
|
+
quotas.check(context, [])
|
|
37
|
+
quotas.check(context, [])
|
|
38
|
+
quotas.check(context, [])
|
|
39
|
+
quotas.check(context, [])
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
// a refused request does not push admission further out, so one emission is
|
|
42
|
+
// all it takes to be admitted again however hard the key is hammered
|
|
43
|
+
await timeout(configuration.interval / configuration.requests)
|
|
36
44
|
|
|
37
|
-
expect(quotas.
|
|
45
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
38
46
|
})
|
|
39
47
|
|
|
40
|
-
it('should
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
it('should earn the budget back over the interval', async () => {
|
|
49
|
+
quotas.check(context, [])
|
|
50
|
+
quotas.check(context, [])
|
|
43
51
|
|
|
44
|
-
expect(quotas.
|
|
52
|
+
expect(quotas.check(context, [])).toBeGreaterThan(0)
|
|
45
53
|
|
|
46
|
-
await timeout(configuration.
|
|
54
|
+
await timeout(configuration.interval)
|
|
47
55
|
|
|
48
|
-
expect(quotas.
|
|
56
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
57
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
49
58
|
})
|
|
59
|
+
})
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
describe('retry', () => {
|
|
62
|
+
it('should answer the seconds until the request would be admitted', () => {
|
|
63
|
+
// an emission is 5 seconds, which is what the budget takes to earn one back
|
|
64
|
+
quotas = Quotas.create(createConfiguration({ requests: 2, interval: 10_000 }))
|
|
65
|
+
context = createContext()
|
|
66
|
+
|
|
67
|
+
quotas.check(context, [])
|
|
68
|
+
quotas.check(context, [])
|
|
53
69
|
|
|
54
|
-
expect(
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
expect(quotas.check(context, [])).toBe(5)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should never answer zero when it refuses', () => {
|
|
74
|
+
quotas = Quotas.create(createConfiguration({ requests: 10, interval: 1000 }))
|
|
75
|
+
context = createContext()
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < 10; i++)
|
|
78
|
+
quotas.check(context, [])
|
|
79
|
+
|
|
80
|
+
// an emission here is a tenth of a second, and `Retry-After` counts in whole ones
|
|
81
|
+
expect(quotas.check(context, [])).toBe(1)
|
|
57
82
|
})
|
|
58
83
|
})
|
|
59
84
|
|
|
@@ -61,167 +86,224 @@ describe('group', () => {
|
|
|
61
86
|
beforeEach(() => {
|
|
62
87
|
context = createContext()
|
|
63
88
|
configuration = createConfiguration()
|
|
89
|
+
quotas = Quotas.create(configuration)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should report what it has charged', () => {
|
|
93
|
+
quotas.check(context, [])
|
|
94
|
+
|
|
95
|
+
const batch = flush()
|
|
96
|
+
|
|
97
|
+
expect(batch).toHaveLength(1)
|
|
98
|
+
expect(batch[0].delta).toBe(configuration.interval / configuration.requests)
|
|
99
|
+
expect(batch[0].quotas).toBe(quotas)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should refuse on what the group has spent, not on what this process has', () => {
|
|
103
|
+
quotas.check(context, [])
|
|
104
|
+
|
|
105
|
+
const batch = flush()
|
|
106
|
+
|
|
107
|
+
// one request here, but the group is already at capacity
|
|
108
|
+
quotas.settled(batch[0], configuration.interval, Date.now())
|
|
109
|
+
|
|
110
|
+
expect(quotas.check(context, [])).toBeGreaterThan(0)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should not refuse below the capacity', () => {
|
|
114
|
+
quotas.check(context, [])
|
|
115
|
+
|
|
116
|
+
const batch = flush()
|
|
117
|
+
|
|
118
|
+
quotas.settled(batch[0], 0, Date.now())
|
|
119
|
+
|
|
120
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should clear what was reported, and keep what was charged since', () => {
|
|
124
|
+
quotas.check(context, [])
|
|
125
|
+
|
|
126
|
+
const batch = flush()
|
|
127
|
+
|
|
128
|
+
quotas.check(context, [])
|
|
129
|
+
quotas.settled(batch[0], 0, Date.now())
|
|
130
|
+
|
|
131
|
+
expect(flush()[0].delta).toBe(configuration.interval / configuration.requests)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should keep what it could not report', () => {
|
|
135
|
+
quotas.check(context, [])
|
|
136
|
+
|
|
137
|
+
const first = flush()
|
|
138
|
+
|
|
139
|
+
// no `settled`, as a tick that cannot reach Redis leaves the debt to the next one
|
|
140
|
+
expect(flush()[0].delta).toBe(first[0].delta)
|
|
64
141
|
})
|
|
65
142
|
|
|
66
|
-
it('should
|
|
67
|
-
|
|
68
|
-
quotas = Quotas.create(configuration, createCounter(() => configuration.requests))
|
|
143
|
+
it('should take a debt for a key it has already dropped', () => {
|
|
144
|
+
quotas.check(context, [])
|
|
69
145
|
|
|
70
|
-
|
|
146
|
+
const batch = flush()
|
|
71
147
|
|
|
72
|
-
|
|
148
|
+
quotas.settled(batch[0], 0, Date.now())
|
|
149
|
+
flush(Date.now() + configuration.interval)
|
|
150
|
+
|
|
151
|
+
quotas.settled({ ...batch[0], delta: 0 }, configuration.interval, Date.now())
|
|
152
|
+
|
|
153
|
+
expect(quotas.check(context, [])).toBeGreaterThan(0)
|
|
73
154
|
})
|
|
74
155
|
|
|
75
|
-
it('should
|
|
76
|
-
|
|
156
|
+
it('should name keys so that quotas with different budgets never share one', () => {
|
|
157
|
+
const other = Quotas.create(createConfiguration({ requests: 3 }))
|
|
158
|
+
|
|
159
|
+
quotas.check(context, [])
|
|
160
|
+
other.check(context, [])
|
|
77
161
|
|
|
78
|
-
|
|
162
|
+
const batch = flush(Date.now(), quotas, other)
|
|
79
163
|
|
|
80
|
-
expect(
|
|
164
|
+
expect(batch[0].key).toBe(batch[1].key)
|
|
165
|
+
expect(quotas.name(batch[0].key)).not.toBe(other.name(batch[1].key))
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('sweeping', () => {
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
context = createContext()
|
|
172
|
+
configuration = createConfiguration()
|
|
173
|
+
quotas = Quotas.create(configuration)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should drop a key that is out of debt with nothing to report', () => {
|
|
177
|
+
quotas.check(context, [])
|
|
178
|
+
|
|
179
|
+
const batch = flush()
|
|
180
|
+
|
|
181
|
+
quotas.settled(batch[0], 0, Date.now())
|
|
182
|
+
|
|
183
|
+
// once the debt has drained, a key says no more than an absent one
|
|
184
|
+
expect(flush(Date.now() + configuration.interval)).toHaveLength(0)
|
|
185
|
+
expect(flush(Date.now() + configuration.interval)).toHaveLength(0)
|
|
186
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should keep a key that still owes something', () => {
|
|
190
|
+
quotas.check(context, [])
|
|
191
|
+
|
|
192
|
+
expect(flush(Date.now() + configuration.interval)).toHaveLength(1)
|
|
81
193
|
})
|
|
82
194
|
})
|
|
83
195
|
|
|
84
196
|
describe('path', () => {
|
|
85
197
|
beforeEach(() => {
|
|
86
198
|
configuration = createConfiguration()
|
|
87
|
-
quotas = Quotas.create(configuration
|
|
199
|
+
quotas = Quotas.create(configuration)
|
|
88
200
|
})
|
|
89
201
|
|
|
90
|
-
it('should have separate quotas',
|
|
202
|
+
it('should have separate quotas', () => {
|
|
91
203
|
const one = createContext({ url: new URL('http://localhost/one/') })
|
|
92
204
|
const two = createContext({ url: new URL('http://localhost/two/') })
|
|
93
205
|
|
|
94
|
-
|
|
95
|
-
|
|
206
|
+
quotas.check(one, [])
|
|
207
|
+
quotas.check(one, [])
|
|
96
208
|
|
|
97
|
-
expect(quotas.
|
|
98
|
-
expect(quotas.
|
|
99
|
-
|
|
100
|
-
await quotas.use(two, output)
|
|
101
|
-
await quotas.use(two, output)
|
|
102
|
-
|
|
103
|
-
expect(quotas.ok(two, [])).toBe(false)
|
|
209
|
+
expect(quotas.check(one, [])).toBeGreaterThan(0)
|
|
210
|
+
expect(quotas.check(two, [])).toBe(0)
|
|
104
211
|
})
|
|
105
212
|
})
|
|
106
213
|
|
|
107
214
|
describe('ip', () => {
|
|
108
215
|
beforeEach(() => {
|
|
109
216
|
configuration = createConfiguration({ key: [{ method: 'ip' }] })
|
|
110
|
-
quotas = Quotas.create(configuration
|
|
217
|
+
quotas = Quotas.create(configuration)
|
|
111
218
|
})
|
|
112
219
|
|
|
113
|
-
it('should have separate quotas',
|
|
220
|
+
it('should have separate quotas', () => {
|
|
114
221
|
const one = createContext({ request: { headers: { 'x-forwarded-for': '1.1.1.1' } } })
|
|
115
222
|
const two = createContext({ request: { headers: { 'x-forwarded-for': '2.2.2.2' } } })
|
|
116
223
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
expect(quotas.ok(one, [])).toBe(false)
|
|
121
|
-
expect(quotas.ok(two, [])).toBe(true)
|
|
224
|
+
quotas.check(one, [])
|
|
225
|
+
quotas.check(one, [])
|
|
122
226
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
expect(quotas.ok(two, [])).toBe(false)
|
|
227
|
+
expect(quotas.check(one, [])).toBeGreaterThan(0)
|
|
228
|
+
expect(quotas.check(two, [])).toBe(0)
|
|
127
229
|
})
|
|
128
230
|
})
|
|
129
231
|
|
|
130
232
|
describe('route', () => {
|
|
131
|
-
it('should key on the route, not on the path it came in on',
|
|
132
|
-
quotas = Quotas.create(createConfiguration({ key: [{ method: 'route' }] }),
|
|
133
|
-
createCounter(), '/users/:id')
|
|
233
|
+
it('should key on the route, not on the path it came in on', () => {
|
|
234
|
+
quotas = Quotas.create(createConfiguration({ key: [{ method: 'route' }] }), '/users/:id')
|
|
134
235
|
|
|
135
236
|
const one = createContext({ url: new URL('http://localhost/users/1/') })
|
|
136
237
|
const two = createContext({ url: new URL('http://localhost/users/2/') })
|
|
137
238
|
|
|
138
|
-
|
|
139
|
-
|
|
239
|
+
quotas.check(one, [])
|
|
240
|
+
quotas.check(two, [])
|
|
140
241
|
|
|
141
242
|
// two paths, one route, one quota — which is the whole difference from `path`
|
|
142
|
-
expect(
|
|
143
|
-
expect(quotas.
|
|
243
|
+
expect(flush()).toHaveLength(1)
|
|
244
|
+
expect(quotas.check(one, [])).toBeGreaterThan(0)
|
|
144
245
|
})
|
|
145
246
|
|
|
146
|
-
it('should have separate quotas per route',
|
|
147
|
-
const users = Quotas.create(createConfiguration({ key: [{ method: 'route' }] }),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const posts = Quotas.create(createConfiguration({ key: [{ method: 'route' }] }),
|
|
151
|
-
createCounter(), '/posts/:id')
|
|
247
|
+
it('should have separate quotas per route', () => {
|
|
248
|
+
const users = Quotas.create(createConfiguration({ key: [{ method: 'route' }] }), '/users/:id')
|
|
249
|
+
const posts = Quotas.create(createConfiguration({ key: [{ method: 'route' }] }), '/posts/:id')
|
|
152
250
|
|
|
153
251
|
context = createContext({ url: new URL('http://localhost/users/1/') })
|
|
154
252
|
|
|
155
|
-
|
|
156
|
-
|
|
253
|
+
users.check(context, [])
|
|
254
|
+
posts.check(context, [])
|
|
157
255
|
|
|
158
|
-
|
|
256
|
+
const batch = flush(Date.now(), users, posts)
|
|
257
|
+
|
|
258
|
+
expect(batch[0].key).not.toBe(batch[1].key)
|
|
159
259
|
})
|
|
160
260
|
})
|
|
161
261
|
|
|
162
262
|
describe('segment', () => {
|
|
163
263
|
beforeEach(() => {
|
|
164
264
|
configuration = createConfiguration({ key: [{ method: 'segment', options: 'id' }] })
|
|
165
|
-
quotas = Quotas.create(configuration
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('should have separate quotas per segment value', async () => {
|
|
265
|
+
quotas = Quotas.create(configuration)
|
|
169
266
|
context = createContext()
|
|
267
|
+
})
|
|
170
268
|
|
|
269
|
+
it('should have separate quotas per segment value', () => {
|
|
171
270
|
const one = [{ name: 'id', value: '1' }]
|
|
172
271
|
const two = [{ name: 'id', value: '2' }]
|
|
173
272
|
|
|
174
|
-
quotas.
|
|
175
|
-
|
|
176
|
-
quotas.ok(context, one)
|
|
177
|
-
await quotas.use(context, output)
|
|
178
|
-
|
|
179
|
-
expect(quotas.ok(context, one)).toBe(false)
|
|
180
|
-
expect(quotas.ok(context, two)).toBe(true)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('should count under the key preflight saw, which settle cannot recompute', async () => {
|
|
184
|
-
const one = createContext()
|
|
185
|
-
const two = createContext()
|
|
186
|
-
|
|
187
|
-
quotas.ok(one, [{ name: 'id', value: '1' }])
|
|
188
|
-
await quotas.use(one, output)
|
|
189
|
-
|
|
190
|
-
quotas.ok(two, [{ name: 'id', value: '2' }])
|
|
191
|
-
await quotas.use(two, output)
|
|
273
|
+
quotas.check(context, one)
|
|
274
|
+
quotas.check(context, one)
|
|
192
275
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(counted[0].name).not.toBe(counted[1].name)
|
|
276
|
+
expect(quotas.check(context, one)).toBeGreaterThan(0)
|
|
277
|
+
expect(quotas.check(context, two)).toBe(0)
|
|
196
278
|
})
|
|
197
279
|
})
|
|
198
280
|
|
|
199
281
|
describe('identity', () => {
|
|
200
282
|
beforeEach(() => {
|
|
201
283
|
configuration = createConfiguration({ key: [{ method: 'identity' }] })
|
|
202
|
-
quotas = Quotas.create(configuration
|
|
284
|
+
quotas = Quotas.create(configuration)
|
|
203
285
|
})
|
|
204
286
|
|
|
205
|
-
it('should have separate quotas per identity',
|
|
287
|
+
it('should have separate quotas per identity', () => {
|
|
206
288
|
const one = createContext({ identity: { id: 'one' } })
|
|
207
289
|
const two = createContext({ identity: { id: 'two' } })
|
|
208
290
|
|
|
209
|
-
|
|
210
|
-
|
|
291
|
+
quotas.check(one, [])
|
|
292
|
+
quotas.check(one, [])
|
|
211
293
|
|
|
212
|
-
expect(quotas.
|
|
213
|
-
expect(quotas.
|
|
294
|
+
expect(quotas.check(one, [])).toBeGreaterThan(0)
|
|
295
|
+
expect(quotas.check(two, [])).toBe(0)
|
|
214
296
|
})
|
|
215
297
|
|
|
216
|
-
it('should put every anonymous request in one quota',
|
|
298
|
+
it('should put every anonymous request in one quota', () => {
|
|
217
299
|
const one = createContext({ identity: null })
|
|
218
300
|
const two = createContext()
|
|
219
301
|
|
|
220
|
-
|
|
221
|
-
|
|
302
|
+
quotas.check(one, [])
|
|
303
|
+
quotas.check(two, [])
|
|
222
304
|
|
|
223
|
-
expect(quotas.
|
|
224
|
-
expect(quotas.
|
|
305
|
+
expect(quotas.check(one, [])).toBeGreaterThan(0)
|
|
306
|
+
expect(quotas.check(two, [])).toBeGreaterThan(0)
|
|
225
307
|
})
|
|
226
308
|
})
|
|
227
309
|
|
|
@@ -229,69 +311,72 @@ describe('status', () => {
|
|
|
229
311
|
beforeEach(() => {
|
|
230
312
|
context = createContext()
|
|
231
313
|
configuration = createConfiguration({ condition: [{ method: 'status', options: 404 }] })
|
|
232
|
-
quotas = Quotas.create(configuration
|
|
314
|
+
quotas = Quotas.create(configuration)
|
|
233
315
|
})
|
|
234
316
|
|
|
235
|
-
it('should not
|
|
236
|
-
|
|
317
|
+
it('should not charge before the response can be matched', () => {
|
|
318
|
+
quotas.check(context, [])
|
|
319
|
+
quotas.check(context, [])
|
|
320
|
+
quotas.check(context, [])
|
|
237
321
|
|
|
238
|
-
|
|
322
|
+
// only the response tells whether a request counts, so checking cannot charge
|
|
323
|
+
expect(flush()).toHaveLength(0)
|
|
239
324
|
})
|
|
240
325
|
|
|
241
|
-
it('should not
|
|
242
|
-
|
|
326
|
+
it('should not charge what the condition rejects', () => {
|
|
327
|
+
quotas.check(context, [])
|
|
328
|
+
quotas.use(context, output)
|
|
243
329
|
|
|
244
|
-
expect(
|
|
330
|
+
expect(flush()).toHaveLength(0)
|
|
331
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
245
332
|
})
|
|
246
333
|
|
|
247
|
-
it('should
|
|
248
|
-
|
|
249
|
-
|
|
334
|
+
it('should charge what the condition accepts', () => {
|
|
335
|
+
for (let i = 0; i < 2; i++) {
|
|
336
|
+
quotas.check(context, [])
|
|
337
|
+
quotas.use(context, { status: 404 })
|
|
338
|
+
}
|
|
250
339
|
|
|
251
|
-
expect(quotas.
|
|
340
|
+
expect(quotas.check(context, [])).toBeGreaterThan(0)
|
|
252
341
|
})
|
|
253
|
-
})
|
|
254
342
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
properties)
|
|
258
|
-
}
|
|
343
|
+
it('should charge the key checking saw, which settling cannot recompute', () => {
|
|
344
|
+
const keys = [{ method: 'segment' as const, options: 'id' }]
|
|
259
345
|
|
|
260
|
-
|
|
261
|
-
return Object.assign({ url: new URL('http://localhost/') }, properties) as unknown as Context
|
|
262
|
-
}
|
|
346
|
+
quotas = Quotas.create(createConfiguration({ key: keys, condition: configuration.condition }))
|
|
263
347
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
* here: what a counter reports is comcount's business, and this asserts what the
|
|
267
|
-
* quotas do with whatever number comes back.
|
|
268
|
-
*/
|
|
269
|
-
function createCounter (reply?: (count: number) => number): Promise<Remote> {
|
|
270
|
-
const counts: Record<string, number> = {}
|
|
348
|
+
const one = createContext()
|
|
349
|
+
const two = createContext()
|
|
271
350
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
expect(endpoint).toBe('count')
|
|
351
|
+
quotas.check(one, [{ name: 'id', value: '1' }])
|
|
352
|
+
quotas.use(one, { status: 404 })
|
|
275
353
|
|
|
276
|
-
|
|
354
|
+
quotas.check(two, [{ name: 'id', value: '2' }])
|
|
355
|
+
quotas.use(two, { status: 404 })
|
|
277
356
|
|
|
278
|
-
|
|
357
|
+
// settling is handed no parameters, so recomputing there would hash an empty
|
|
358
|
+
// segment both times and the two requests would charge one key
|
|
359
|
+
expect(flush()).toHaveLength(2)
|
|
360
|
+
})
|
|
361
|
+
})
|
|
279
362
|
|
|
280
|
-
|
|
363
|
+
function flush (now: number = Date.now(), ...of: Quotas[]): Batch[] {
|
|
364
|
+
const batch: Batch[] = []
|
|
281
365
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
366
|
+
for (const one of of.length === 0 ? [quotas] : of)
|
|
367
|
+
one.flush(now, batch)
|
|
285
368
|
|
|
286
|
-
return
|
|
369
|
+
return batch
|
|
287
370
|
}
|
|
288
371
|
|
|
289
|
-
|
|
290
|
-
|
|
372
|
+
function createConfiguration (properties?: Partial<Configuration>): Configuration {
|
|
373
|
+
return Object.assign({ key: [{ method: 'path' }], requests: 2, interval: 100 }, properties)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function createContext (properties?: any): Context {
|
|
377
|
+
return Object.assign({ url: new URL('http://localhost/') }, properties) as unknown as Context
|
|
291
378
|
}
|
|
292
379
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
interval: number
|
|
296
|
-
amount: number
|
|
380
|
+
async function timeout (ms: number): Promise<void> {
|
|
381
|
+
await setTimeout(ms * 1.2)
|
|
297
382
|
}
|