@withjoy/limiter 0.1.2 → 0.1.4-test

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 (38) hide show
  1. package/README.md +4 -0
  2. package/limitd-redis/LICENSE +21 -0
  3. package/limitd-redis/README.md +183 -0
  4. package/limitd-redis/docker-compose.yml +11 -0
  5. package/limitd-redis/index.js +2 -0
  6. package/limitd-redis/lib/cb.js +45 -0
  7. package/limitd-redis/lib/client.js +135 -0
  8. package/limitd-redis/lib/db.js +501 -0
  9. package/limitd-redis/lib/db_ping.js +106 -0
  10. package/limitd-redis/lib/put.lua +31 -0
  11. package/limitd-redis/lib/take.lua +48 -0
  12. package/limitd-redis/lib/utils.js +116 -0
  13. package/limitd-redis/lib/validation.js +64 -0
  14. package/limitd-redis/node_modules/lru-cache/LICENSE +15 -0
  15. package/limitd-redis/node_modules/lru-cache/README.md +158 -0
  16. package/limitd-redis/node_modules/lru-cache/index.js +468 -0
  17. package/limitd-redis/node_modules/lru-cache/package.json +74 -0
  18. package/limitd-redis/node_modules/ms/index.js +162 -0
  19. package/limitd-redis/node_modules/ms/license.md +21 -0
  20. package/limitd-redis/node_modules/ms/package.json +73 -0
  21. package/limitd-redis/node_modules/ms/readme.md +59 -0
  22. package/limitd-redis/node_modules/yallist/LICENSE +15 -0
  23. package/limitd-redis/node_modules/yallist/README.md +204 -0
  24. package/limitd-redis/node_modules/yallist/iterator.js +7 -0
  25. package/limitd-redis/node_modules/yallist/package.json +65 -0
  26. package/limitd-redis/node_modules/yallist/yallist.js +370 -0
  27. package/limitd-redis/opslevel.yml +6 -0
  28. package/limitd-redis/package-lock.json +3484 -0
  29. package/limitd-redis/package.json +31 -0
  30. package/limitd-redis/test/cb.tests.js +124 -0
  31. package/limitd-redis/test/client.tests.js +194 -0
  32. package/limitd-redis/test/db.tests.js +1318 -0
  33. package/limitd-redis/test/validation.tests.js +124 -0
  34. package/limiter.js +83 -19
  35. package/package.json +3 -2
  36. package/tests/limiter.test.js +27 -27
  37. package/tests/performTestWithTestPerMinute.js +1 -1
  38. package/tests/sanityCheck.js +33 -29
@@ -0,0 +1,124 @@
1
+ const assert = require('chai').assert;
2
+
3
+ const { validateParams } = require('../lib/validation');
4
+
5
+ describe('validation', () => {
6
+ describe('validateParameters', () => {
7
+
8
+ const buckets = {
9
+ user: {
10
+ size: 10
11
+ }
12
+ };
13
+
14
+ describe('when providing invalid parameters', () => {
15
+ const invalidParameterSets = [
16
+ {
17
+ result: {
18
+ message: 'params are required',
19
+ code: 101
20
+ }
21
+ }, {
22
+ params: {},
23
+ result: {
24
+ message: 'type is required',
25
+ code: 102
26
+ }
27
+ }, {
28
+ params: {
29
+ type: 'ip'
30
+ },
31
+ result: {
32
+ message: 'undefined bucket type ip',
33
+ code: 103
34
+ }
35
+ }, {
36
+ params: {
37
+ type: 'user'
38
+ },
39
+ result: {
40
+ message: 'key is required',
41
+ code: 104
42
+ }
43
+ }, {
44
+ params: {
45
+ type: 'user',
46
+ key: 'tenant|username',
47
+ configOverride: 5
48
+ },
49
+ result: {
50
+ message: 'configuration overrides must be an object',
51
+ code: 105
52
+ }
53
+ }, {
54
+ params: {
55
+ type: 'user',
56
+ key: 'tenant|username',
57
+ configOverride: {}
58
+ },
59
+ result: {
60
+ message: 'configuration overrides must provide either a size or interval',
61
+ code: 106
62
+ }
63
+ }
64
+ ];
65
+
66
+ invalidParameterSets.forEach(testcase => {
67
+ it(`Should return a validation error, code ${testcase.result.code}`, () => {
68
+ const result = validateParams(testcase.params, buckets);
69
+ assert.strictEqual(result.name, 'LimitdRedisValidationError');
70
+ assert.strictEqual(result.message, testcase.result.message);
71
+ assert.deepEqual(result.extra, { code: testcase.result.code });
72
+ assert.exists(result.stack);
73
+ });
74
+ });
75
+ });
76
+
77
+ describe('when providing valid parameters', () => {
78
+ const validParameterSerts = [
79
+ {
80
+ params: {
81
+ type: 'user',
82
+ key: 'tenant|username',
83
+ },
84
+ name: 'type and key params'
85
+ }, {
86
+ params: {
87
+ type: 'user',
88
+ key: 'tenant|username',
89
+ configOverride: {
90
+ size: 77
91
+ }
92
+ },
93
+ name: 'configOverride with size'
94
+ }, {
95
+ params: {
96
+ type: 'user',
97
+ key: 'tenant|username',
98
+ configOverride: {
99
+ per_hour: 300
100
+ }
101
+ },
102
+ name: 'configOverride with interval'
103
+ }, {
104
+ params: {
105
+ type: 'user',
106
+ key: 'tenant|username',
107
+ configOverride: {
108
+ size: 30,
109
+ per_hour: 300
110
+ }
111
+ },
112
+ name: 'configOverride with size and interval'
113
+ },
114
+ ];
115
+
116
+ validParameterSerts.forEach(testcase => {
117
+ it(`Should not cause a validation error for ${testcase.name}`, () => {
118
+ const result = validateParams(testcase.params, buckets);
119
+ assert.isUndefined(result);
120
+ });
121
+ });
122
+ });
123
+ });
124
+ });
package/limiter.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  var LimitdRedis = require("limitd-redis"); // name to redis matter
4
+ var z = require('zod');
4
5
 
5
6
  const defaultBuckets = {
6
7
  emailsByEventId: { size: 3000 },
@@ -37,13 +38,63 @@ class Operation {
37
38
  // global singleton
38
39
  let connection = null;
39
40
 
41
+ const LIMITER_CIRCUIT_BREAKER_DEFAULTS = {
42
+ timeout: "0.50s",
43
+ maxFailures: 50,
44
+ cooldown: "1s",
45
+ maxCooldown: "3s",
46
+ name: "limitr",
47
+ };
48
+
49
+ const LIMITER_RETRY_DEFAULTS = {
50
+ retries: 3,
51
+ minTimeout: 10,
52
+ maxTimeout: 30,
53
+ };
54
+
55
+ /**
56
+ * Circuit Breaker configuration for limiter. derived from limitd-redis package
57
+ * https://github.com/auth0/limitd-redis/blob/03ba193fe436c5b887ff410d4f2623512ee5c9e2/lib/client.js#L11C1-L12C1
58
+ */
59
+ const limiterCircuitBreakerConfig = z
60
+ .object({
61
+ timeout: z.coerce
62
+ .string()
63
+ .default(LIMITER_CIRCUIT_BREAKER_DEFAULTS.timeout),
64
+ maxFailures: z.coerce
65
+ .number()
66
+ .default(LIMITER_CIRCUIT_BREAKER_DEFAULTS.maxFailures),
67
+ cooldown: z.coerce
68
+ .string()
69
+ .default(LIMITER_CIRCUIT_BREAKER_DEFAULTS.cooldown),
70
+ maxCooldown: z.coerce
71
+ .string()
72
+ .default(LIMITER_CIRCUIT_BREAKER_DEFAULTS.maxCooldown),
73
+ name: z.coerce.string().default(LIMITER_CIRCUIT_BREAKER_DEFAULTS.name),
74
+ })
75
+ .strict()
76
+ .default(LIMITER_CIRCUIT_BREAKER_DEFAULTS);
77
+
78
+ /**
79
+ * Limiter Retry configuration for limiter. derived from limitd-redis package
80
+ * https://github.com/auth0/limitd-redis/blob/03ba193fe436c5b887ff410d4f2623512ee5c9e2/lib/client.js#L22
81
+ */
82
+ const limiterRetryConfig = z
83
+ .object({
84
+ retries: z.coerce.number().default(LIMITER_RETRY_DEFAULTS.retries),
85
+ minTimeout: z.coerce.number().default(LIMITER_RETRY_DEFAULTS.minTimeout),
86
+ maxTimeout: z.coerce.number().default(LIMITER_RETRY_DEFAULTS.maxTimeout),
87
+ })
88
+ .strict()
89
+ .default(LIMITER_RETRY_DEFAULTS);
90
+
40
91
  class Limiter {
41
92
  // static defaultBuckets;
42
93
  // static pickDefaultBuckets(keys) { ... };
43
94
 
44
95
  constructor(config) {
45
96
  this._config = config;
46
- this._limitdRedisConnection = this.connect();
97
+ this._limitdRedisConnectionPromise = this.connect();
47
98
  }
48
99
 
49
100
  /*
@@ -52,29 +103,40 @@ class Limiter {
52
103
  stub it to prevent a real connect from happening under Test Suite Conditions
53
104
  */
54
105
  connect() {
55
- let buckets = this._config.buckets || {};
56
- buckets = {
57
- ...defaultBuckets,
58
- ...buckets,
59
- };
60
- const keyPrefix = this._config.keyPrefix || defaultRedisKeyPrefix;
61
- if (!connection) {
62
- connection = new LimitdRedis({
63
- uri: this._config.limitdUrl,
64
- buckets,
65
- keyPrefix,
66
- });
67
- }
68
- return connection;
106
+ return new Promise((resolve, reject) => {
107
+ let buckets = this._config.buckets || {};
108
+ buckets = {
109
+ ...defaultBuckets,
110
+ ...buckets,
111
+ };
112
+ const keyPrefix = this._config.keyPrefix || defaultRedisKeyPrefix;
113
+ if (!connection) {
114
+ connection = new LimitdRedis({
115
+ uri: this._config.limitdUrl,
116
+ buckets,
117
+ keyPrefix,
118
+ circuitbreaker: this._config.circuitbreaker,
119
+ commandTimeout: this._config.commandTimeout,
120
+ retry: this._config.retry,
121
+ });
122
+ connection.db.on('ready', () => {
123
+ return resolve(connection);
124
+ });
125
+ } else {
126
+ return resolve(connection);
127
+ }
128
+ });
69
129
  }
70
130
 
71
- getLimiterProcessor(console) {
131
+ async getLimiterProcessor(console) {
132
+ await this._limitdRedisConnectionPromise;
72
133
  return new LimiterProcessor(console);
73
134
  }
74
135
 
75
136
  close() {
76
- return new Promise((resolve, reject) => {
77
- this._limitdRedisConnection.close((err, resp) => {
137
+ return new Promise(async (resolve, reject) => {
138
+ const connectionPromise = await this._limitdRedisConnectionPromise;
139
+ connectionPromise.close((err, resp) => {
78
140
  if (err) {
79
141
  this._console.error(err);
80
142
  return reject(err);
@@ -87,7 +149,7 @@ class Limiter {
87
149
  }
88
150
 
89
151
  class LimiterProcessor {
90
-
152
+
91
153
  constructor(console) {
92
154
  this._console = console;
93
155
  this._operationList = [];
@@ -191,5 +253,7 @@ Limiter.pickDefaultBuckets = function pickDefaultBuckets(keyArray) {
191
253
  }
192
254
  return buckets;
193
255
  };
256
+ Limiter.limiterCircuitBreakerConfig = limiterCircuitBreakerConfig;
257
+ Limiter.limiterRetryConfig = limiterRetryConfig;
194
258
 
195
259
  module.exports = Limiter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withjoy/limiter",
3
- "version": "0.1.2",
3
+ "version": "0.1.4-test",
4
4
  "description": "Api Rate limiter",
5
5
  "main": "limiter.js",
6
6
  "scripts": {
@@ -10,7 +10,8 @@
10
10
  "author": "services@withjoy.com",
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
- "limitd-redis": "^5.1.1"
13
+ "limitd-redis": "file:./limitd-redis",
14
+ "zod": "^3.22.4"
14
15
  },
15
16
  "devDependencies": {
16
17
  "jest": "^24.9.0"
@@ -16,13 +16,13 @@ const TEST_BUCKETS = {
16
16
  The limitd service is essentially a token bank
17
17
  that we can withdraw from (take) or deposit to (put)
18
18
  */
19
- const limiterWithMockService = (succeeds = true, conforms = true) => {
19
+ const limiterWithMockService = async (succeeds = true, conforms = true) => {
20
20
  let token_pile = 0;
21
21
  const limiter = new Limiter({
22
22
  limitdUrl: "localhost:6379",
23
23
  buckets: TEST_BUCKETS,
24
24
  });
25
- const limiterProcessor = limiter.getLimiterProcessor(console);
25
+ const limiterProcessor = await limiter.getLimiterProcessor(console);
26
26
  limiterProcessor._limitdRedis = new (class MockLimitd {
27
27
  constructor(should_succeed, should_conform) {
28
28
  this.should_succeed = should_succeed;
@@ -61,7 +61,7 @@ test("perfrom limitd-redis testing with test bucket", async (done) => {
61
61
  test: { size: 3, per_second: 2 },
62
62
  },
63
63
  })
64
- const limiterProcessor = limiter.getLimiterProcessor(console);
64
+ const limiterProcessor = await limiter.getLimiterProcessor(console);
65
65
  // take 2 # Conformant - Remaining: 1
66
66
  const result1 = await limiterProcessor.transfer("test", "test-key", 2);
67
67
  expect(result1).toMatchObject({
@@ -93,8 +93,8 @@ test("perfrom limitd-redis testing with test bucket", async (done) => {
93
93
  done();
94
94
  });
95
95
 
96
- test("Resolves to null if we aren't doing a real transfer", (done) => {
97
- let limiterProcessor = limiterWithMockService();
96
+ test("Resolves to null if we aren't doing a real transfer", async (done) => {
97
+ let limiterProcessor = await limiterWithMockService();
98
98
  limiterProcessor.transfer("a", "b", 0, null).then((result) => {
99
99
  expect(result).toBe(null);
100
100
  expect(limiterProcessor._limitdRedis._num_tokens()).toBe(0);
@@ -102,8 +102,8 @@ test("Resolves to null if we aren't doing a real transfer", (done) => {
102
102
  });
103
103
  });
104
104
 
105
- test("Adds tokens if provided a negative token amount", (done) => {
106
- let limiterProcessor = limiterWithMockService();
105
+ test("Adds tokens if provided a negative token amount", async (done) => {
106
+ let limiterProcessor = await limiterWithMockService();
107
107
  limiterProcessor.transfer("a", "b", -5).then((result) => {
108
108
  expect(result).toMatchObject({});
109
109
  expect(limiterProcessor._limitdRedis._num_tokens()).toBe(5);
@@ -111,8 +111,8 @@ test("Adds tokens if provided a negative token amount", (done) => {
111
111
  });
112
112
  });
113
113
 
114
- test("Removes tokens if provided a positive token amount", (done) => {
115
- let limiterProcessor = limiterWithMockService();
114
+ test("Removes tokens if provided a positive token amount", async (done) => {
115
+ let limiterProcessor = await limiterWithMockService();
116
116
  limiterProcessor.transfer("a", "b", 5).then((result) => {
117
117
  expect(result).toMatchObject({ conformant: true });
118
118
  expect(limiterProcessor._limitdRedis._num_tokens()).toBe(-5);
@@ -120,8 +120,8 @@ test("Removes tokens if provided a positive token amount", (done) => {
120
120
  });
121
121
  });
122
122
 
123
- test("Reverts all taken tokens when revert is called", (done) => {
124
- let limiterProcessor = limiterWithMockService();
123
+ test("Reverts all taken tokens when revert is called", async (done) => {
124
+ let limiterProcessor = await limiterWithMockService();
125
125
  limiterProcessor
126
126
  .transfer("a", "b", 5)
127
127
  .then((result) => {
@@ -135,8 +135,8 @@ test("Reverts all taken tokens when revert is called", (done) => {
135
135
  });
136
136
  });
137
137
 
138
- test("Reverts all given tokens when revert is called", (done) => {
139
- let limiterProcessor = limiterWithMockService();
138
+ test("Reverts all given tokens when revert is called", async (done) => {
139
+ let limiterProcessor = await limiterWithMockService();
140
140
  limiterProcessor
141
141
  .transfer("a", "b", -5)
142
142
  .then((result) => {
@@ -150,8 +150,8 @@ test("Reverts all given tokens when revert is called", (done) => {
150
150
  });
151
151
  });
152
152
 
153
- test("Reverts a series of actions when revert is called", (done) => {
154
- let limiterProcessor = limiterWithMockService();
153
+ test("Reverts a series of actions when revert is called", async (done) => {
154
+ let limiterProcessor = await limiterWithMockService();
155
155
  limiterProcessor
156
156
  .transfer("a", "b", -5)
157
157
  .then((result) => {
@@ -170,8 +170,8 @@ test("Reverts a series of actions when revert is called", (done) => {
170
170
  });
171
171
  });
172
172
 
173
- test("Properly executes a transferAll()", (done) => {
174
- let limiterProcessor = limiterWithMockService();
173
+ test("Properly executes a transferAll()", async (done) => {
174
+ let limiterProcessor = await limiterWithMockService();
175
175
  limiterProcessor
176
176
  .transferAll([
177
177
  ["a", "b", -5],
@@ -188,26 +188,26 @@ test("Properly executes a transferAll()", (done) => {
188
188
  });
189
189
  });
190
190
 
191
- test("Errors from the underlying service are propagated (put)", () => {
192
- let limiterProcessor = limiterWithMockService((succeeds = false));
191
+ test("Errors from the underlying service are propagated (put)", async () => {
192
+ let limiterProcessor = await limiterWithMockService((succeeds = false));
193
193
  let endsInFailure = limiterProcessor.transfer("a", "b", -5);
194
194
  return expect(endsInFailure).rejects.toThrow(PUT_FAILED);
195
195
  });
196
196
 
197
- test("Errors from the underlying service are propagated (take)", () => {
198
- let limiterProcessor = limiterWithMockService((succeeds = false));
197
+ test("Errors from the underlying service are propagated (take)", async () => {
198
+ let limiterProcessor = await limiterWithMockService((succeeds = false));
199
199
  let endsInFailure = limiterProcessor.transfer("a", "b", 5);
200
200
  return expect(endsInFailure).rejects.toThrow(TAKE_FAILED);
201
201
  });
202
202
 
203
- test("Non-conforming response results in an error", () => {
204
- let limiterProcessor = limiterWithMockService((succeeds = true), (conforms = false));
203
+ test("Non-conforming response results in an error", async () => {
204
+ let limiterProcessor = await limiterWithMockService((succeeds = true), (conforms = false));
205
205
  let endsInFailure = limiterProcessor.transfer("a", "b", 5);
206
206
  return expect(endsInFailure).rejects.toThrow("Non Conformant");
207
207
  });
208
208
 
209
- test("An operation that fails is not logged", (done) => {
210
- let limiterProcessor = limiterWithMockService();
209
+ test("An operation that fails is not logged", async (done) => {
210
+ let limiterProcessor = await limiterWithMockService();
211
211
  limiterProcessor
212
212
  .transfer("emailRate", "b", -5)
213
213
  .then((result) => {
@@ -229,8 +229,8 @@ test("An operation that fails is not logged", (done) => {
229
229
  });
230
230
  });
231
231
 
232
- test("Properly handles spamming revert() calls", (done) => {
233
- let limiterProcessor = limiterWithMockService();
232
+ test("Properly handles spamming revert() calls", async (done) => {
233
+ let limiterProcessor = await limiterWithMockService();
234
234
  limiterProcessor
235
235
  .transfer("a", "b", -5)
236
236
  .then((result) => {
@@ -6,7 +6,7 @@ async function performTestWithTestPerMinute() {
6
6
  limitdUrl: "localhost:6379",
7
7
  console: console,
8
8
  });
9
- const limiterProcessor = limiter.getLimiterProcessor(console);
9
+ const limiterProcessor = await limiter.getLimiterProcessor(console);
10
10
  // take 2 # Conformant - Remaining: 1
11
11
  const result1 = await limiterProcessor.transfer("testPerMinute", "key", 2);
12
12
  console.log("First Turn Take Result", result1);
@@ -2,32 +2,36 @@
2
2
 
3
3
  var Limiter = require("../limiter");
4
4
 
5
- const buckets = {
6
- emails: { size: 10 },
7
- emailRate: { size: 10, per_hour: 10 },
8
- };
9
-
10
- var limiter = new Limiter({
11
- limitdUrl: "localhost:6379",
12
- buckets,
13
- });
14
-
15
- var limiterProcessor = limiter.getLimiterProcessor(console);
16
-
17
- var callback = function (err) {
18
- if (err) {
19
- console.log(err);
20
- process.exit(1);
21
- }
22
-
23
- process.exit(0);
24
- };
25
-
26
- var withdrawls = [
27
- ["emails", process.argv[2], -parseInt(process.argv[3])],
28
- ["emailRate", process.argv[2], -parseInt(process.argv[3])],
29
- ["emails", process.argv[2], parseInt(process.argv[3])],
30
- ["emailRate", process.argv[2], parseInt(process.argv[3])],
31
- ];
32
-
33
- limiterProcessor.transferAll(withdrawls).then(() => callback(), callback);
5
+ async function sanityCheck(){
6
+ const buckets = {
7
+ emails: { size: 10 },
8
+ emailRate: { size: 10, per_hour: 10 },
9
+ };
10
+
11
+ var limiter = new Limiter({
12
+ limitdUrl: "localhost:6379",
13
+ buckets,
14
+ });
15
+
16
+ const limiterProcessor = await limiter.getLimiterProcessor(console);
17
+
18
+ var callback = function (err) {
19
+ if (err) {
20
+ console.log(err);
21
+ process.exit(1);
22
+ }
23
+
24
+ process.exit(0);
25
+ };
26
+
27
+ var withdrawls = [
28
+ ["emails", process.argv[2], -parseInt(process.argv[3])],
29
+ ["emailRate", process.argv[2], -parseInt(process.argv[3])],
30
+ ["emails", process.argv[2], parseInt(process.argv[3])],
31
+ ["emailRate", process.argv[2], parseInt(process.argv[3])],
32
+ ];
33
+
34
+ limiterProcessor.transferAll(withdrawls).then(() => callback(), callback);
35
+ }
36
+
37
+ sanityCheck();