Skip to content

Commit

Permalink
Implemented @Throttle.
Browse files Browse the repository at this point in the history
  • Loading branch information
dimadeveatii committed Jan 24, 2019
1 parent 30549d0 commit 1dbb750
Show file tree
Hide file tree
Showing 14 changed files with 525 additions and 25 deletions.
18 changes: 18 additions & 0 deletions examples/throttle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { throttle } from '../lib';

class Service {
@throttle(3, { interval: 'second' })
get() {
return 42;
}
}

const service = new Service();

service.get();
service.get();
service.get();

// Only 3 executions per second are allowed.
try { service.get(); }
catch (err) { console.log(err.message); }
6 changes: 2 additions & 4 deletions examples/timeout.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { timeout } from '../lib';

class Test {
class Service {
@timeout(10)
do(): Promise<number> {
return new Promise((res, rej) => setTimeout(res, 1000));
}
}

console.log('Hello world.');

const t = new Test().do().catch(err => console.log('failed'));
const t = new Service().do().catch(err => console.log(err.message));
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export { cache } from './cache';
export { circuit } from './circuit';
export { concurrency } from './concurrency';
export { retry } from './retry';
export { throttle } from './throttle';
export { throttle, ThrottleOptions } from './throttle';
export { timeout } from './timeout';
7 changes: 0 additions & 7 deletions lib/throttle.ts

This file was deleted.

35 changes: 35 additions & 0 deletions lib/throttle/ThrottleOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type ThrottleOptions = {

/**
* Time interval of throttling limit.
* When a custom number is provided is used as milliseconds.
* Defaults to `second`.
*/
interval?: 'second' | 'minute' | number;

/**
* The scope of method throttling.
* The `args-hash` (default) scope defines as a scope the arguments list
* (for all class instances). See `object-hash` package for details about
* calculating the hash of arguments.
* The `class` scope defines a single scope for all class instances.
* The `instance` scope defines as a scope the method within current instance
* (regardless of arguments list).
*/
scope?: 'args-hash' | 'class' | 'instance',

/**
* Sets the behavior of handling throttle limit.
* When `throw` (default) then in case of reached limit throws immediately with an error.
* When `reject` then returns a rejected promise with an error.
* When `ignore` then doesn't throw any error and immediately
* terminates execution (returns undefined).
* When `ignoreAsync` then doesn't throw any error and immediately
* returns a resolved promise.
*/
behavior?: 'throw' | 'reject' | 'ignore' | 'ignoreAsync',
};

export const DEFAULT_INTERVAL = 1000;
export const DEFAULT_SCOPE = 'args-hash';
export const DEFAULT_BEHAVIOR = 'throw';
14 changes: 14 additions & 0 deletions lib/throttle/Throttler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class Throttler {
private count: number = 0;

constructor(private limit: number, private interval: number) {
}

pass(): boolean {
if (this.count >= this.limit) return false;

this.count += 1;
setTimeout(() => this.count -= 1, this.interval);
return true;
}
}
65 changes: 65 additions & 0 deletions lib/throttle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ThrottleOptions, DEFAULT_INTERVAL, DEFAULT_SCOPE } from './ThrottleOptions';
import { createScope } from './scopes';

export { ThrottleOptions };

/**
* Limits the number of executions of a method within a given interval of time.
* When the limit is exceeded the method is immediately
* rejected with `Throttle limit exceeded.` error (configurable, see `options.behavior`).
* @param limit the max number of executions within an interval of time (see `options.interval`).
* @param options (optional) additional options,
* defaults to {interval: 'second', scope: 'args-hash', error: 'throw'}
*/
export function throttle(
limit: number,
options?: ThrottleOptions) {

return function (target: any, propertyKey: any, descriptor: PropertyDescriptor) {
const method = descriptor.value;
const raise = raiseStrategy(options);
const scope = createScope(
options && options.scope || DEFAULT_SCOPE,
limit,
calculateInterval(options));

descriptor.value = function () {
const throttler = scope.throttler(this, Array.from(arguments));
if (!throttler.pass()) {
return raise(new Error('Throttle limit exceeded.'));
}

return method.apply(this, arguments);
};
};
}

function raiseStrategy(options: ThrottleOptions) {
const value = options && options.behavior || 'throw';

switch (value) {
case 'reject':
return err => Promise.reject(err);
case 'throw':
return (err) => { throw err; };
case 'ignore':
return () => { };
case 'ignoreAsync':
return () => Promise.resolve();
default:
throw new Error(`Option ${value} is not supported for 'behavior'.`);
}
}

function calculateInterval(options: ThrottleOptions) {
const value = options && options.interval || DEFAULT_INTERVAL;

switch (value) {
case 'minute':
return 60000;
case 'second':
return 1000;
default:
<number>value;
}
}
41 changes: 41 additions & 0 deletions lib/throttle/scopes/ArgsHashScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as hash from 'object-hash';
import { Throttler } from '../Throttler';

export class ArgsHashScope {
private readonly map: Map<string, [Throttler, any]> = new Map();

constructor(
private readonly limit: number,
private readonly interval: number) {
}

throttler(instance: any, args: any) {
const key = hash(args);
const value = this.map.get(key);

return value ? this.update(key, value) : this.create(key);
}

private update(key: string, value: [Throttler, any]) {
// Clear previous timeout and create a new one
// with extended expiration.
clearTimeout(value[1]);
this.map.set(key, [value[0], this.remember(key)]);

return value[0];
}

private create(key: string) {
const throttle = new Throttler(this.limit, this.interval);

// Keep the throttler in map only for `interval` period
// to avoid memory leaks.
this.map.set(key, [throttle, this.remember(key)]);

return throttle;
}

private remember(key: string) {
return setTimeout(() => this.map.delete(key), this.interval + 100);
}
}
13 changes: 13 additions & 0 deletions lib/throttle/scopes/ClassScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Throttler } from '../Throttler';

export class ClassScope {
private readonly throttle;

constructor(limit: number, interval: number) {
this.throttle = new Throttler(limit, interval);
}

throttler() {
return this.throttle;
}
}
23 changes: 23 additions & 0 deletions lib/throttle/scopes/InstanceScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Throttler } from '../Throttler';

export class InstanceScope {
// Use a weak map so that original instances can be garbage collected.
// Once an instance is garbage collected, the reference to throttle is removed.
private readonly map: WeakMap<any, Throttler> = new WeakMap();

constructor(
private readonly limit: number,
private readonly interval: number) {
}

throttler(instance: any) {
return this.map.get(instance) || this.create(instance);
}

private create(instance: any) {
const throttle = new Throttler(this.limit, this.interval);
this.map.set(instance, throttle);

return throttle;
}
}
19 changes: 19 additions & 0 deletions lib/throttle/scopes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { InstanceScope } from './InstanceScope';
import { ClassScope } from './ClassScope';
import { ArgsHashScope } from './ArgsHashScope';
import { Throttler } from '../Throttler';

type ScopeType = { throttler(instance: any, args: any): Throttler };

export function createScope(scope: string, limit: number, interval: number): ScopeType {
switch (scope) {
case 'instance':
return new InstanceScope(limit, interval);
case 'class':
return new ClassScope(limit, interval);
case 'args-hash':
return new ArgsHashScope(limit, interval);
default:
throw new Error(`Scope '${scope}' is not supported.`);
}
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"url": "https://labs42.io"
},
"contributors": [],
"dependencies": {},
"dependencies": {
"object-hash": "1.3.1"
},
"devDependencies": {
"@types/chai": "4.1.7",
"@types/chai-as-promised": "7.1.0",
Expand Down Expand Up @@ -61,4 +63,4 @@
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0"
}
}
}
Loading

0 comments on commit 1dbb750

Please sign in to comment.