-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
30549d0
commit 1dbb750
Showing
14 changed files
with
525 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.