@withjoy/limiter 0.1.7 → 0.2.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.
- package/README.md +3 -3
- package/limiter.js +50 -4
- 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
|
@@ -89,12 +89,36 @@ const limiterRetryConfig = z
|
|
|
89
89
|
.strict()
|
|
90
90
|
.default(LIMITER_RETRY_DEFAULTS);
|
|
91
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
|
+
|
|
92
116
|
class Limiter {
|
|
93
117
|
// static defaultBuckets;
|
|
94
118
|
// static pickDefaultBuckets(keys) { ... };
|
|
95
119
|
|
|
96
120
|
constructor(config) {
|
|
97
|
-
this._config = config;
|
|
121
|
+
this._config = limiterConfig.parse(config);
|
|
98
122
|
this._limitdRedisConnectionPromise = this.connect();
|
|
99
123
|
}
|
|
100
124
|
|
|
@@ -105,12 +129,12 @@ class Limiter {
|
|
|
105
129
|
*/
|
|
106
130
|
connect() {
|
|
107
131
|
return new Promise((resolve, reject) => {
|
|
108
|
-
let buckets = this._config.buckets || {};
|
|
132
|
+
let buckets = this._config.buckets || {};
|
|
109
133
|
buckets = {
|
|
110
134
|
...defaultBuckets,
|
|
111
135
|
...buckets,
|
|
112
136
|
};
|
|
113
|
-
const keyPrefix = this._config.keyPrefix
|
|
137
|
+
const keyPrefix = this._config.keyPrefix;
|
|
114
138
|
if (!connection) {
|
|
115
139
|
connection = new LimitdRedis({
|
|
116
140
|
uri: this._config.limitdUrl,
|
|
@@ -137,6 +161,9 @@ let buckets = this._config.buckets || {};
|
|
|
137
161
|
close() {
|
|
138
162
|
return new Promise(async (resolve, reject) => {
|
|
139
163
|
const connectionPromise = await this._limitdRedisConnectionPromise;
|
|
164
|
+
if (!connectionPromise) {
|
|
165
|
+
return resolve(null);
|
|
166
|
+
}
|
|
140
167
|
connectionPromise.close((err, resp) => {
|
|
141
168
|
if (err) {
|
|
142
169
|
this._console.error(err);
|
|
@@ -152,11 +179,26 @@ let buckets = this._config.buckets || {};
|
|
|
152
179
|
class LimiterProcessor {
|
|
153
180
|
|
|
154
181
|
constructor(console) {
|
|
155
|
-
this._console = console;
|
|
182
|
+
this._console = this._normalizeConsole(console);
|
|
156
183
|
this._operationList = [];
|
|
157
184
|
this._limitdRedis = connection;
|
|
158
185
|
}
|
|
159
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Normalize logger interface to ensure .info method exists
|
|
189
|
+
* Maps .log or .verbose to .info if .info doesn't exist
|
|
190
|
+
*
|
|
191
|
+
* TODO: Once we migrate to TypeScript and define a formal ILimiterLogger interface,
|
|
192
|
+
* this normalization should no longer be needed. The interface will enforce the
|
|
193
|
+
* required .info() and .error() methods at compile time.
|
|
194
|
+
*/
|
|
195
|
+
_normalizeConsole(console) {
|
|
196
|
+
if (!console.info) {
|
|
197
|
+
console.info = (console.log || console.verbose || (() => {})).bind(console);
|
|
198
|
+
}
|
|
199
|
+
return console;
|
|
200
|
+
}
|
|
201
|
+
|
|
160
202
|
/*
|
|
161
203
|
Go through the log of things we did and do the opposite
|
|
162
204
|
*/
|
|
@@ -254,7 +296,11 @@ Limiter.pickDefaultBuckets = function pickDefaultBuckets(keyArray) {
|
|
|
254
296
|
}
|
|
255
297
|
return buckets;
|
|
256
298
|
};
|
|
299
|
+
|
|
300
|
+
// schemas
|
|
257
301
|
Limiter.limiterCircuitBreakerConfig = limiterCircuitBreakerConfig;
|
|
258
302
|
Limiter.limiterRetryConfig = limiterRetryConfig;
|
|
303
|
+
Limiter.limiterBucketConfig = limiterBucketConfig;
|
|
304
|
+
Limiter.limiterConfig = limiterConfig;
|
|
259
305
|
|
|
260
306
|
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: {
|