@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 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 || defaultRedisKeyPrefix;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withjoy/limiter",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "description": "Api Rate limiter",
5
5
  "main": "limiter.js",
6
6
  "scripts": {
@@ -54,7 +54,109 @@ const limiterWithMockService = async (succeeds = true, conforms = true) => {
54
54
  return limiterProcessor;
55
55
  };
56
56
 
57
- test("perfrom limitd-redis testing with test bucket", async (done) => {
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: {