@withjoy/limiter 0.1.6 → 0.2.0
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/README.md +3 -3
- package/limiter.js +35 -3
- package/package.json +1 -1
- package/tests/limiter.test.js +103 -1
package/README.md
CHANGED
|
@@ -24,16 +24,16 @@ Your client can use any subset of these Buckets, but when it does, the Best Prac
|
|
|
24
24
|
However, for flexibility reasons, this is not strictly enforced.
|
|
25
25
|
|
|
26
26
|
## Why limitd-redis ?
|
|
27
|
-
Although this package is available on npm, it's no longer actively maintained. We've observed bugs in the existing published version, but these issues have already been resolved in the [limitd-redis](https://github.com/auth0/limitd-redis) gitHub repository. To address this, we've imported the latest stable version(v7.8.1) from there github repository and using it locally in the limiter
|
|
27
|
+
Although this package is available on npm, it's no longer actively maintained. We've observed bugs in the existing published version, but these issues have already been resolved in the [limitd-redis](https://github.com/auth0/limitd-redis) gitHub repository. To address this, we've imported the latest stable version(v7.8.1) from there github repository and using it locally in the `limiter`.
|
|
28
28
|
|
|
29
29
|
In case we need to update in the future, we can replace the "limitd-redis" directory with the latest version of [limitd-redis](https://github.com/auth0/limitd-redis) from their GitHub repository.
|
|
30
30
|
|
|
31
|
-
Note: We need to add dependency from limitd-redis to limiter package json before publish. We are using limitd-redis as directly not package inside limiter
|
|
31
|
+
Note: We need to add dependency from `limitd-redis` to `limiter` package json before publish. We are using `limitd-redis` as directly not package inside `limiter`.
|
|
32
32
|
|
|
33
33
|
## Testing:
|
|
34
34
|
|
|
35
35
|
`npm run test` will run offline tests using a mock.
|
|
36
|
-
`npm run sanity-check` will run against a limitd service configured to run with the test buckets.
|
|
36
|
+
`npm run sanity-check` will run against a `limitd` service configured to run with the test buckets.
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
## Quick Test
|
package/limiter.js
CHANGED
|
@@ -12,6 +12,7 @@ const defaultBuckets = {
|
|
|
12
12
|
eventsByIpRate: { size: 240, per_hour: 240 },
|
|
13
13
|
paymentAttempts: { size: 5000 },
|
|
14
14
|
paymentAttemptsByReceiverId: { size: 900 },
|
|
15
|
+
paymentAttemptsGiftCards: { size: 3000 },
|
|
15
16
|
// for development testing
|
|
16
17
|
testPerMinute: { size: 3, per_minute: 1 },
|
|
17
18
|
};
|
|
@@ -88,12 +89,36 @@ const limiterRetryConfig = z
|
|
|
88
89
|
.strict()
|
|
89
90
|
.default(LIMITER_RETRY_DEFAULTS);
|
|
90
91
|
|
|
92
|
+
// from https://github.com/auth0/limitd-redis#buckets
|
|
93
|
+
const limiterBucketConfig = z.object({
|
|
94
|
+
size: z.number(),
|
|
95
|
+
per_day: z.number().optional(),
|
|
96
|
+
per_hour: z.number().optional(),
|
|
97
|
+
per_minute: z.number().optional(),
|
|
98
|
+
per_second: z.number().optional(),
|
|
99
|
+
// and that's all we care about
|
|
100
|
+
// we need more? implement more of the `limitd-redis` schema
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const limiterConfig = z
|
|
104
|
+
.object({
|
|
105
|
+
limitdUrl: z.string(),
|
|
106
|
+
// the rest are optional / have defaults
|
|
107
|
+
buckets: z.record(z.string(), limiterBucketConfig).optional(),
|
|
108
|
+
keyPrefix: z.string().default(defaultRedisKeyPrefix),
|
|
109
|
+
circuitbreaker: limiterCircuitBreakerConfig,
|
|
110
|
+
retry: limiterRetryConfig,
|
|
111
|
+
commandTimeout: z.number().default(30),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
114
|
+
;
|
|
115
|
+
|
|
91
116
|
class Limiter {
|
|
92
117
|
// static defaultBuckets;
|
|
93
118
|
// static pickDefaultBuckets(keys) { ... };
|
|
94
119
|
|
|
95
120
|
constructor(config) {
|
|
96
|
-
this._config = config;
|
|
121
|
+
this._config = limiterConfig.parse(config);
|
|
97
122
|
this._limitdRedisConnectionPromise = this.connect();
|
|
98
123
|
}
|
|
99
124
|
|
|
@@ -104,12 +129,12 @@ class Limiter {
|
|
|
104
129
|
*/
|
|
105
130
|
connect() {
|
|
106
131
|
return new Promise((resolve, reject) => {
|
|
107
|
-
let buckets = this._config.buckets || {};
|
|
132
|
+
let buckets = this._config.buckets || {};
|
|
108
133
|
buckets = {
|
|
109
134
|
...defaultBuckets,
|
|
110
135
|
...buckets,
|
|
111
136
|
};
|
|
112
|
-
const keyPrefix = this._config.keyPrefix
|
|
137
|
+
const keyPrefix = this._config.keyPrefix;
|
|
113
138
|
if (!connection) {
|
|
114
139
|
connection = new LimitdRedis({
|
|
115
140
|
uri: this._config.limitdUrl,
|
|
@@ -136,6 +161,9 @@ let buckets = this._config.buckets || {};
|
|
|
136
161
|
close() {
|
|
137
162
|
return new Promise(async (resolve, reject) => {
|
|
138
163
|
const connectionPromise = await this._limitdRedisConnectionPromise;
|
|
164
|
+
if (!connectionPromise) {
|
|
165
|
+
return resolve(null);
|
|
166
|
+
}
|
|
139
167
|
connectionPromise.close((err, resp) => {
|
|
140
168
|
if (err) {
|
|
141
169
|
this._console.error(err);
|
|
@@ -253,7 +281,11 @@ Limiter.pickDefaultBuckets = function pickDefaultBuckets(keyArray) {
|
|
|
253
281
|
}
|
|
254
282
|
return buckets;
|
|
255
283
|
};
|
|
284
|
+
|
|
285
|
+
// schemas
|
|
256
286
|
Limiter.limiterCircuitBreakerConfig = limiterCircuitBreakerConfig;
|
|
257
287
|
Limiter.limiterRetryConfig = limiterRetryConfig;
|
|
288
|
+
Limiter.limiterBucketConfig = limiterBucketConfig;
|
|
289
|
+
Limiter.limiterConfig = limiterConfig;
|
|
258
290
|
|
|
259
291
|
module.exports = Limiter;
|
package/package.json
CHANGED
package/tests/limiter.test.js
CHANGED
|
@@ -54,7 +54,109 @@ const limiterWithMockService = async (succeeds = true, conforms = true) => {
|
|
|
54
54
|
return limiterProcessor;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
describe("Limiter", () => {
|
|
58
|
+
let limiter;
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
jest.spyOn(Limiter.prototype, "connect").mockImplementation(async () => Promise.resolve());
|
|
61
|
+
});
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
await limiter.close();
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("constructor", () => {
|
|
68
|
+
it('accepts maximal valid configuration', () => {
|
|
69
|
+
limiter = new Limiter({
|
|
70
|
+
limitdUrl: 'localhost:6379',
|
|
71
|
+
buckets: {
|
|
72
|
+
...TEST_BUCKETS,
|
|
73
|
+
perDay: { size: 11, per_day: 11 },
|
|
74
|
+
perMinute: { size: 21, per_minute: 21 },
|
|
75
|
+
perSecond: { size: 31, per_second: 31 },
|
|
76
|
+
},
|
|
77
|
+
keyPrefix: 'maximal:',
|
|
78
|
+
circuitbreaker: {
|
|
79
|
+
timeout: '41s',
|
|
80
|
+
maxFailures: 42,
|
|
81
|
+
cooldown: '43s',
|
|
82
|
+
maxCooldown: '44s',
|
|
83
|
+
name: 'maximal',
|
|
84
|
+
},
|
|
85
|
+
retry: {
|
|
86
|
+
retries: 51,
|
|
87
|
+
minTimeout: 52,
|
|
88
|
+
maxTimeout: 53,
|
|
89
|
+
},
|
|
90
|
+
commandTimeout: 61,
|
|
91
|
+
});
|
|
92
|
+
expect(limiter._config.limitdUrl).toBe('localhost:6379');
|
|
93
|
+
expect(limiter._config.buckets.perDay.size).toBe(11);
|
|
94
|
+
expect(limiter._config.keyPrefix).toBe('maximal:');
|
|
95
|
+
expect(limiter._config.circuitbreaker.timeout).toBe('41s');
|
|
96
|
+
expect(limiter._config.retry.retries).toBe(51);
|
|
97
|
+
expect(limiter._config.commandTimeout).toBe(61);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("accepts minimum valid configuration", async () => {
|
|
101
|
+
limiter = new Limiter({
|
|
102
|
+
limitdUrl: 'localhost:6379',
|
|
103
|
+
});
|
|
104
|
+
expect(limiter._config.limitdUrl).toBe('localhost:6379');
|
|
105
|
+
expect(limiter._config.buckets).toBeUndefined();
|
|
106
|
+
expect(limiter._config.keyPrefix).toBe('limitdRedis:');
|
|
107
|
+
expect(limiter._config.circuitbreaker.timeout).toBe('0.50s');
|
|
108
|
+
expect(limiter._config.retry.retries).toBe(3);
|
|
109
|
+
expect(limiter._config.commandTimeout).toBe(30);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("fails on invalid bucket configuration", () => {
|
|
113
|
+
expect(() => {
|
|
114
|
+
limiter = new Limiter({
|
|
115
|
+
limitdUrl: 'localhost:6379',
|
|
116
|
+
buckets: {
|
|
117
|
+
invalidBucket: { size: 'string' }, // Poisoning with an invalid size
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}).toThrow(/"invalidBucket"/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("cannot fail on invalid circuitbreaker configuration", () => {
|
|
124
|
+
expect(() => {
|
|
125
|
+
limiter = new Limiter({
|
|
126
|
+
limitdUrl: 'localhost:6379',
|
|
127
|
+
circuitbreaker: {
|
|
128
|
+
timeout: 'still-valid',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}).not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("fails on invalid retry configuration", () => {
|
|
135
|
+
expect(() => {
|
|
136
|
+
limiter = new Limiter({
|
|
137
|
+
limitdUrl: 'localhost:6379',
|
|
138
|
+
retry: {
|
|
139
|
+
retries: 'not-a-number', // Poisoning with an invalid number of retries
|
|
140
|
+
minTimeout: 52,
|
|
141
|
+
maxTimeout: 53,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}).toThrow(/"retries"/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("fails when limitdUrl is missing", () => {
|
|
148
|
+
expect(() => {
|
|
149
|
+
limiter = new Limiter({
|
|
150
|
+
buckets: {
|
|
151
|
+
emails: { size: 10 },
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}).toThrow(/"limitdUrl"/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("perform limitd-redis testing with test bucket", async (done) => {
|
|
58
160
|
const limiter = new Limiter({
|
|
59
161
|
limitdUrl: "localhost:6379",
|
|
60
162
|
buckets: {
|