Skip to content

Instantly share code, notes, and snippets.

@tavurth
Last active December 5, 2019 11:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tavurth/0e1d65b2cd9d5b45c95f26a03b02940d to your computer and use it in GitHub Desktop.
Save tavurth/0e1d65b2cd9d5b45c95f26a03b02940d to your computer and use it in GitHub Desktop.
class Forbidden extends Error {
constructor(message, status = 403) {
super(message);
this.status = status;
}
getStatus() {
return Number.parseInt(this.status);
}
getError() {
const error = this.toString();
return ~error.indexOf('Error:') ? error.slice(6).trim() : error;
}
toObject() {
// Remove the first 'Error' from the string
return {
error: this.getError(),
status: this.getStatus(),
};
}
}
class RateLimiter {
constructor({ ttl, threshold }) {
this.ttl = ttl;
this.threshold = threshold;
this.namespaces = new Map();
this.daemon = this.daemon.bind(this);
this.consumeSync = this.consumeSync.bind(this);
this.ensureNamespace = this.ensureNamespace.bind(this);
setInterval(this.daemon, this.ttl/2);
}
/**
* Runs every {ttl/2} milliseconds.
* If the namespace has timed out, refreshes it.
* If the namespace has not been touched, since the last daemon, deletes it.
*/
daemon() {
const nowDate = Date.now();
this.namespaces.forEach((value, key) => {
if (value.timeout < nowDate) {
if (value.remaining === this.threshold) {
return this.namespaces.delete(key);
}
value.remaining = this.threshold;
value.timeout = Date.now() + this.ttl;
}
});
}
/**
* Makes sure that a string namespace exists in this namespaces.
* If it does not exist, initializes it with the default values
* @param {string} namespace - For example, userId or user IP.
*/
ensureNamespace(namespace) {
if (this.namespaces.has(namespace)) {
return;
}
this.namespaces.set(namespace, {
remaining: this.threshold,
timeout: Date.now() + this.ttl,
});
}
/**
* Consumes one item from a namespace.
* When all uses are done it throws an error
* @param {string} namespace - For example, userId or user IP.
* @throws {Forbidden} When the user has no more uses for this namespace.
*/
consumeSync(namespace, error = 'rate.limit.exceeded') {
this.ensureNamespace(namespace);
const currentNs = this.namespaces.get(namespace);
if (currentNs.remaining <= 0) {
throw new Forbidden(error);
}
currentNs.remaining -= 1;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment