rolling-rate-limiter
Advanced tools
Comparing version
/// <reference types="node" /> | ||
export declare type Id = number | string; | ||
export declare type Seconds = number; | ||
export declare type Milliseconds = number; | ||
export declare type Microseconds = number; | ||
export declare type Seconds = number & { | ||
__brand: 'seconds'; | ||
}; | ||
export declare type Milliseconds = number & { | ||
__brand: 'milliseconds'; | ||
}; | ||
export declare type Microseconds = number & { | ||
__brand: 'microseconds'; | ||
}; | ||
/** | ||
@@ -30,5 +36,5 @@ * Generic options for constructing any rate limiter. | ||
export declare class RateLimiter { | ||
interval: number; | ||
interval: Microseconds; | ||
maxInInterval: number; | ||
minDifference: number; | ||
minDifference: Microseconds; | ||
constructor({ interval, maxInInterval, minDifference }: RateLimiterOptions); | ||
@@ -75,3 +81,3 @@ /** | ||
clear(id: Id): Promise<void>; | ||
protected getTimestamps(id: Id, addNewTimestamp: boolean): Promise<Array<Microseconds>>; | ||
protected getTimestamps(id: Id, addNewTimestamp: boolean): Promise<Microseconds[]>; | ||
} | ||
@@ -113,5 +119,6 @@ /** | ||
} | ||
export declare function getCurrentMicroseconds(): Microseconds; | ||
export declare function millisecondsToMicroseconds(milliseconds: Milliseconds): Microseconds; | ||
export declare function microsecondsToMilliseconds(microseconds: Microseconds): Milliseconds; | ||
export declare function microsecondsToTTLSeconds(microseconds: Microseconds): Seconds; | ||
export declare function microsecondsToSeconds(microseconds: Microseconds): Seconds; | ||
export {}; |
@@ -6,5 +6,4 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.microsecondsToTTLSeconds = exports.microsecondsToMilliseconds = exports.millisecondsToMicroseconds = exports.RedisRateLimiter = exports.InMemoryRateLimiter = exports.RateLimiter = void 0; | ||
exports.microsecondsToSeconds = exports.microsecondsToMilliseconds = exports.millisecondsToMicroseconds = exports.getCurrentMicroseconds = exports.RedisRateLimiter = exports.InMemoryRateLimiter = exports.RateLimiter = void 0; | ||
const assert_1 = __importDefault(require("assert")); | ||
const microtime_nodejs_1 = require("microtime-nodejs"); | ||
const uuid_1 = require("uuid"); | ||
@@ -16,5 +15,5 @@ /** | ||
constructor({ interval, maxInInterval, minDifference = 0 }) { | ||
assert_1.default(interval > 0, 'Must pass a positive integer for `options.interval`'); | ||
assert_1.default(maxInInterval > 0, 'Must pass a positive integer for `options.maxInInterval`'); | ||
assert_1.default(minDifference >= 0, '`options.minDifference` cannot be negative'); | ||
(0, assert_1.default)(interval > 0, 'Must pass a positive integer for `options.interval`'); | ||
(0, assert_1.default)(maxInInterval > 0, 'Must pass a positive integer for `options.maxInInterval`'); | ||
(0, assert_1.default)(minDifference >= 0, '`options.minDifference` cannot be negative'); | ||
this.interval = millisecondsToMicroseconds(interval); | ||
@@ -36,3 +35,3 @@ this.maxInInterval = maxInInterval; | ||
async wouldLimitWithInfo(id) { | ||
const currentTimestamp = microtime_nodejs_1.now(); | ||
const currentTimestamp = getCurrentMicroseconds(); | ||
const existingTimestamps = await this.getTimestamps(id, false); | ||
@@ -81,7 +80,8 @@ return this.calculateInfo([...existingTimestamps, currentTimestamp]); | ||
// until the interval is not full anymore. | ||
const microsecondsUntilAllowed = Math.max(this.minDifference, numTimestamps >= this.maxInInterval | ||
const microsecondsUntilUnblocked = numTimestamps >= this.maxInInterval | ||
? timestamps[Math.max(0, numTimestamps - this.maxInInterval)] - | ||
currentTimestamp + | ||
this.interval | ||
: 0); | ||
: 0; | ||
const microsecondsUntilAllowed = Math.max(this.minDifference, microsecondsUntilUnblocked); | ||
return { | ||
@@ -115,3 +115,3 @@ blocked, | ||
async getTimestamps(id, addNewTimestamp) { | ||
const currentTimestamp = microtime_nodejs_1.now(); | ||
const currentTimestamp = getCurrentMicroseconds(); | ||
// Update the stored timestamps, including filtering out old ones, and adding the new one. | ||
@@ -143,3 +143,3 @@ const clearBefore = currentTimestamp - this.interval; | ||
super(baseOptions); | ||
this.ttl = microsecondsToTTLSeconds(this.interval); | ||
this.ttl = microsecondsToSeconds(this.interval); | ||
this.client = client; | ||
@@ -156,3 +156,3 @@ this.namespace = namespace; | ||
async getTimestamps(id, addNewTimestamp) { | ||
const now = microtime_nodejs_1.now(); | ||
const now = getCurrentMicroseconds(); | ||
const key = this.makeKey(id); | ||
@@ -163,3 +163,3 @@ const clearBefore = now - this.interval; | ||
if (addNewTimestamp) { | ||
batch.zadd(key, String(now), uuid_1.v4()); | ||
batch.zadd(key, String(now), (0, uuid_1.v4)()); | ||
} | ||
@@ -196,4 +196,9 @@ batch.zrange(key, 0, -1, 'WITHSCORES'); | ||
exports.RedisRateLimiter = RedisRateLimiter; | ||
function getCurrentMicroseconds() { | ||
const hr = process.hrtime(); | ||
return (hr[0] * 1e6 + Math.ceil(hr[1] / 1000)); | ||
} | ||
exports.getCurrentMicroseconds = getCurrentMicroseconds; | ||
function millisecondsToMicroseconds(milliseconds) { | ||
return 1000 * milliseconds; | ||
return (1000 * milliseconds); | ||
} | ||
@@ -205,5 +210,5 @@ exports.millisecondsToMicroseconds = millisecondsToMicroseconds; | ||
exports.microsecondsToMilliseconds = microsecondsToMilliseconds; | ||
function microsecondsToTTLSeconds(microseconds) { | ||
function microsecondsToSeconds(microseconds) { | ||
return Math.ceil(microseconds / 1000 / 1000); | ||
} | ||
exports.microsecondsToTTLSeconds = microsecondsToTTLSeconds; | ||
exports.microsecondsToSeconds = microsecondsToSeconds; |
@@ -6,4 +6,4 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const process_1 = __importDefault(require("process")); | ||
const ioredis_1 = __importDefault(require("ioredis")); | ||
const microtime_nodejs_1 = __importDefault(require("microtime-nodejs")); | ||
const redis_1 = __importDefault(require("redis")); | ||
@@ -66,8 +66,11 @@ const _1 = require("."); | ||
let currentTime = 0; | ||
function setTime(newTime) { | ||
function setTime(timeInMilliseconds) { | ||
jest | ||
.spyOn(microtime_nodejs_1.default, 'now') | ||
.mockImplementation(() => _1.millisecondsToMicroseconds(newTime)); | ||
jest.advanceTimersByTime(Math.max(0, newTime - currentTime)); | ||
currentTime = newTime; | ||
.spyOn(process_1.default, 'hrtime') | ||
.mockImplementation(() => [ | ||
Math.floor(timeInMilliseconds / 1e3), | ||
(timeInMilliseconds % 1e3) * 1e6, | ||
]); | ||
jest.advanceTimersByTime(Math.max(0, timeInMilliseconds - currentTime)); | ||
currentTime = timeInMilliseconds; | ||
} | ||
@@ -74,0 +77,0 @@ function sharedExamples(_createLimiter) { |
{ | ||
"name": "rolling-rate-limiter", | ||
"version": "0.2.11", | ||
"version": "0.2.12", | ||
"description": "Rate limiter that supports a rolling window, either in-memory or backed by Redis", | ||
@@ -41,3 +41,2 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"microtime-nodejs": "~1.0.0", | ||
"uuid": "^8.3.0" | ||
@@ -51,3 +50,2 @@ }, | ||
"@types/uuid": "^8.3.0", | ||
"async": "~3.2.0", | ||
"eslint": "^7.25.0", | ||
@@ -57,3 +55,2 @@ "eslint-config-peterkhayes": "^4.0.0", | ||
"jest": "^26.6.3", | ||
"lodash": "^4.17.21", | ||
"redis": "^3.1.2", | ||
@@ -60,0 +57,0 @@ "ts-jest": "^26.5.5", |
# Rolling Rate Limiter | ||
 | ||
This is an implementation of a rate limiter in node.js that allows for rate limiting with a rolling window. It can use either in-memory storage or Redis as a backend. If Redis is used, multiple rate limiters can share one instance with different namespaces, and multiple processes can share rate limiter state safely. | ||
This is an implementation of a rate limiter in node.js that allows for rate limiting with a rolling window. It can use either in-memory storage or Redis as a backend. If Redis is used, multiple rate limiters can share one instance with different namespaces, and multiple processes can share rate limiter state safely. | ||
This means that if a user is allowed 5 actions per 60 seconds, any action will be blocked if 5 actions have already occured in the preceeding 60 seconds, without any set points at which this interval resets. This contrasts with some other rate limiter implementations, in which a user could make 5 requests at 0:59 and another 5 requests at 1:01. | ||
This means that if a user is allowed 5 actions per 60 seconds, any action will be blocked if 5 actions have already occured in the preceeding 60 seconds, without any set points at which this interval resets. This contrasts with some other rate limiter implementations, in which a user could make 5 requests at 0:59 and another 5 requests at 1:01. | ||
**Important Note**: | ||
As a consequence of the way the Redis algorithm works, if an action is blocked, it is still "counted". This means that if a user is continually attempting actions more quickly than the allowed rate, __all__ of their actions will be blocked until they pause or slow their requests. | ||
As a consequence of the way the Redis algorithm works, if an action is blocked, it is still "counted". This means that if a user is continually attempting actions more quickly than the allowed rate, **all** of their actions will be blocked until they pause or slow their requests. | ||
@@ -14,20 +15,22 @@ This behavior is somewhat counterintuitive, but it's the only way that I have found that uses an atomic `MULTI` set of commands for Redis. Without this, race conditions would be possible. [See more below.](#method-of-operation). | ||
## Upgrading from 0.1 | ||
Version 0.2 was released August 31 2020. The method of operation remains the same, but the API has changed. A short summary of the changes: | ||
* Library was rewritten in Typescript. | ||
* Rate limiters are now instances of a `RateLimiter` class. | ||
* Methods now use promises instead of callbacks. | ||
* A `wouldLimit` method is now available to see if an action would be blocked, without actually "counting" it as an action. | ||
* `limitWithInfo` and `wouldLimitWithInfo` methods are available to return more information about how and why an action was blocked or not blocked. | ||
* Tests were rewritten in Jest, and run on both `redis` and `ioredis` clients. | ||
- Library was rewritten in Typescript. | ||
- Rate limiters are now instances of a `RateLimiter` class. | ||
- Methods now use promises instead of callbacks. | ||
- A `wouldLimit` method is now available to see if an action would be blocked, without actually "counting" it as an action. | ||
- `limitWithInfo` and `wouldLimitWithInfo` methods are available to return more information about how and why an action was blocked or not blocked. | ||
- Tests were rewritten in Jest, and run on both `redis` and `ioredis` clients. | ||
## Quick start | ||
Basic use in an Express application. | ||
```javascript | ||
const { RedisRateLimiter } = require('rolling-rate-limiter'); | ||
const { RedisRateLimiter } = require("rolling-rate-limiter"); | ||
const limiter = new RedisRateLimiter({ | ||
client: redisClient, // client instance from `redis` or `ioredis` | ||
namespace: 'rate-limiter', // prefix for redis keys | ||
namespace: "rate-limiter", // prefix for redis keys | ||
interval: 60000, // milliseconds | ||
@@ -37,3 +40,3 @@ maxInInterval: 5, | ||
app.use(function(req, res, next) { | ||
app.use(function (req, res, next) { | ||
limiter.limit(req.ipAddress).then((wasBlocked) => { | ||
@@ -45,3 +48,3 @@ if (wasBlocked) { | ||
} | ||
}) | ||
}); | ||
}); | ||
@@ -51,40 +54,46 @@ ``` | ||
## Available limiters | ||
* `RedisRateLimiter` - Stores state in Redis. Can use `redis` or `ioredis` clients. | ||
* `InMemoryRateLimiter` - Stores state in memory. Useful in testing or outside of web servers. | ||
- `RedisRateLimiter` - Stores state in Redis. Can use `redis` or `ioredis` clients. | ||
- `InMemoryRateLimiter` - Stores state in memory. Useful in testing or outside of web servers. | ||
## Configuration options | ||
* `interval: number` - The length of the rate limiter's interval, in milliseconds. For example, if you want a user to be able to perform 5 actions per minute, this should be `60000`. | ||
* `maxInInterval: number` - The number of actions allowed in each interval. For example, in the scenario above, this would be `5` | ||
* `minDifference?: number` - Optional. The minimum time allowed between consecutive actions, in milliseconds. | ||
* `client: Client` (Redis only) - The Redis client to use. | ||
* `namespace: string` (Redis only) - A string to prepend to all keys to prevent conflicts with other code using Redis. | ||
- `interval: number` - The length of the rate limiter's interval, in milliseconds. For example, if you want a user to be able to perform 5 actions per minute, this should be `60000`. | ||
- `maxInInterval: number` - The number of actions allowed in each interval. For example, in the scenario above, this would be `5` | ||
- `minDifference?: number` - Optional. The minimum time allowed between consecutive actions, in milliseconds. | ||
- `client: Client` (Redis only) - The Redis client to use. | ||
- `namespace: string` (Redis only) - A string to prepend to all keys to prevent conflicts with other code using Redis. | ||
## Instance Methods | ||
All methods take an `Id`, which should be of type `number | string`. Commonly, this will be a user's id. | ||
* `limit(id: Id): Promise<boolean>` - Attempt to perform an action. Returns `false` if the action should be allowed, and `true` if the action should be blocked. | ||
* `wouldLimit(id: Id): Promise<boolean>` - Return what would happen if an action were attempted. Returns `false` if an action would not have been blocked, and `true` if an action would have been blocked. Does not "count" as an action. | ||
* `limitWithInfo(id: Id): Promise<RateLimitInfo>` - Attempt to perform an action. Returns whether the action should be blocked, as well as additional information about why it was blocked and how long the user must wait. | ||
* `wouldLimitWithInfo(id: Id): Promise<RateLimitInfo>` - Returns info about what would happened if an action were attempted and why. Does not "count" as an action. | ||
- `limit(id: Id): Promise<boolean>` - Attempt to perform an action. Returns `false` if the action should be allowed, and `true` if the action should be blocked. | ||
- `wouldLimit(id: Id): Promise<boolean>` - Return what would happen if an action were attempted. Returns `false` if an action would not have been blocked, and `true` if an action would have been blocked. Does not "count" as an action. | ||
- `limitWithInfo(id: Id): Promise<RateLimitInfo>` - Attempt to perform an action. Returns whether the action should be blocked, as well as additional information about why it was blocked and how long the user must wait. | ||
- `wouldLimitWithInfo(id: Id): Promise<RateLimitInfo>` - Returns info about what would happened if an action were attempted and why. Does not "count" as an action. | ||
`RateLimitInfo` contains the following properties: | ||
* `blocked: boolean` - Whether the action was blocked (or would have been blocked). | ||
* `blockedDueToCount: boolean` - Whether the action was blocked (or would have been blocked) because of the `interval` and `maxInInterval` properties. | ||
* `blockedDueToMinDifference: boolean` - Whether the action was blocked (or would have been blocked) because of the `minDistance` property. | ||
* `millisecondsUntilAllowed: number` - The number of milliseconds the user must wait until they can make another action. If another action would immediately be permitted, this is `0`. | ||
* `actionsRemaining: number` - The number of actions a user has left within the interval. Does not account for `minDifference`. | ||
- `blocked: boolean` - Whether the action was blocked (or would have been blocked). | ||
- `blockedDueToCount: boolean` - Whether the action was blocked (or would have been blocked) because of the `interval` and `maxInInterval` properties. | ||
- `blockedDueToMinDifference: boolean` - Whether the action was blocked (or would have been blocked) because of the `minDistance` property. | ||
- `millisecondsUntilAllowed: number` - The number of milliseconds the user must wait until they can make another action. If another action would immediately be permitted, this is `0`. | ||
- `actionsRemaining: number` - The number of actions a user has left within the interval. Does not account for `minDifference`. | ||
## Method of operation | ||
* Each identifier/user corresponds to a _sorted set_ data structure. The keys and values are both equal to the (microsecond) times at which actions were attempted, allowing easy manipulation of this list. | ||
* When a new action comes in for a user, all elements in the set that occurred earlier than (current time - interval) are dropped from the set. | ||
* If the number of elements in the set is still greater than the maximum, the current action is blocked. | ||
* If a minimum difference has been set and the most recent previous element is too close to the current time, the current action is blocked. | ||
* The current action is then added to the set. | ||
* _Note_: if an action is blocked, it is still added to the set. This means that if a user is continually attempting actions more quickly than the allowed rate, _all_ of their actions will be blocked until they pause or slow their requests. | ||
* If the limiter uses a redis instance, the keys are prefixed with namespace, allowing a single redis instance to support separate rate limiters. | ||
* All redis operations for a single rate-limit check/update are performed as an atomic transaction, allowing rate limiters running on separate processes or machines to share state safely. | ||
- Each identifier/user corresponds to a _sorted set_ data structure. The keys and values are both equal to the (microsecond) times at which actions were attempted, allowing easy manipulation of this list. | ||
- When a new action comes in for a user, all elements in the set that occurred earlier than (current time - interval) are dropped from the set. | ||
- If the number of elements in the set is still greater than the maximum, the current action is blocked. | ||
- If a minimum difference has been set and the most recent previous element is too close to the current time, the current action is blocked. | ||
- The current action is then added to the set. | ||
- _Note_: if an action is blocked, it is still added to the set. This means that if a user is continually attempting actions more quickly than the allowed rate, _all_ of their actions will be blocked until they pause or slow their requests. | ||
- If the limiter uses a redis instance, the keys are prefixed with namespace, allowing a single redis instance to support separate rate limiters. | ||
- All redis operations for a single rate-limit check/update are performed as an atomic transaction, allowing rate limiters running on separate processes or machines to share state safely. | ||
## Local development | ||
### Installation | ||
Install dependencies with `yarn`. | ||
@@ -95,6 +104,7 @@ | ||
### Testing | ||
* `yarn ci`: Runs the CI build, including linting, type checking, and tests. Requires [act](https://github.com/nektos/act) to run GitHub actions locally. | ||
* `yarn lint`: Runs ESLint. | ||
* `yarn test`: Runs Jest. | ||
* `yarn typecheck`: Runs TypeScript, without emitting output. | ||
* `yarn build`: Runs TypeScript and outputs to `./lib`. | ||
- `yarn ci`: Runs the CI build, including linting, type checking, and tests. Requires [act](https://github.com/nektos/act) to run GitHub actions locally. | ||
- `yarn lint`: Runs ESLint. | ||
- `yarn test`: Runs Jest. | ||
- `yarn typecheck`: Runs TypeScript, without emitting output. | ||
- `yarn build`: Runs TypeScript and outputs to `./lib`. |
@@ -0,3 +1,3 @@ | ||
import process from 'process'; | ||
import IORedis from 'ioredis'; | ||
import microtime from 'microtime-nodejs'; | ||
import redis from 'redis'; | ||
@@ -10,4 +10,2 @@ | ||
RedisRateLimiter, | ||
millisecondsToMicroseconds, | ||
Milliseconds, | ||
} from '.'; | ||
@@ -82,8 +80,11 @@ | ||
let currentTime = 0; | ||
function setTime(newTime: Milliseconds) { | ||
function setTime(timeInMilliseconds: number) { | ||
jest | ||
.spyOn(microtime, 'now') | ||
.mockImplementation(() => millisecondsToMicroseconds(newTime)); | ||
jest.advanceTimersByTime(Math.max(0, newTime - currentTime)); | ||
currentTime = newTime; | ||
.spyOn(process, 'hrtime') | ||
.mockImplementation(() => [ | ||
Math.floor(timeInMilliseconds / 1e3), | ||
(timeInMilliseconds % 1e3) * 1e6, | ||
]); | ||
jest.advanceTimersByTime(Math.max(0, timeInMilliseconds - currentTime)); | ||
currentTime = timeInMilliseconds; | ||
} | ||
@@ -90,0 +91,0 @@ |
import assert from 'assert'; | ||
import { now as getCurrentMicroseconds } from 'microtime-nodejs'; | ||
import { v4 as uuid } from 'uuid'; | ||
export type Id = number | string; | ||
export type Seconds = number; | ||
export type Milliseconds = number; | ||
export type Microseconds = number; | ||
export type Seconds = number & { __brand: 'seconds' }; | ||
export type Milliseconds = number & { __brand: 'milliseconds' }; | ||
export type Microseconds = number & { __brand: 'microseconds' }; | ||
@@ -36,5 +35,5 @@ /** | ||
export class RateLimiter { | ||
interval: number; | ||
interval: Microseconds; | ||
maxInInterval: number; | ||
minDifference: number; | ||
minDifference: Microseconds; | ||
@@ -46,5 +45,5 @@ constructor({ interval, maxInInterval, minDifference = 0 }: RateLimiterOptions) { | ||
this.interval = millisecondsToMicroseconds(interval); | ||
this.interval = millisecondsToMicroseconds(interval as Milliseconds); | ||
this.maxInInterval = maxInInterval; | ||
this.minDifference = millisecondsToMicroseconds(minDifference); | ||
this.minDifference = millisecondsToMicroseconds(minDifference as Milliseconds); | ||
} | ||
@@ -121,10 +120,13 @@ | ||
// until the interval is not full anymore. | ||
const microsecondsUntilUnblocked = | ||
numTimestamps >= this.maxInInterval | ||
? (timestamps[Math.max(0, numTimestamps - this.maxInInterval)] as number) - | ||
(currentTimestamp as number) + | ||
(this.interval as number) | ||
: 0; | ||
const microsecondsUntilAllowed = Math.max( | ||
this.minDifference, | ||
numTimestamps >= this.maxInInterval | ||
? timestamps[Math.max(0, numTimestamps - this.maxInInterval)] - | ||
currentTimestamp + | ||
this.interval | ||
: 0, | ||
); | ||
microsecondsUntilUnblocked, | ||
) as Microseconds; | ||
@@ -163,6 +165,3 @@ return { | ||
protected async getTimestamps( | ||
id: Id, | ||
addNewTimestamp: boolean, | ||
): Promise<Array<Microseconds>> { | ||
protected async getTimestamps(id: Id, addNewTimestamp: boolean) { | ||
const currentTimestamp = getCurrentMicroseconds(); | ||
@@ -187,3 +186,3 @@ // Update the stored timestamps, including filtering out old ones, and adding the new one. | ||
this.storage[id] = storedTimestamps; | ||
return storedTimestamps; | ||
return storedTimestamps as Array<Microseconds>; | ||
} | ||
@@ -226,3 +225,3 @@ } | ||
super(baseOptions); | ||
this.ttl = microsecondsToTTLSeconds(this.interval); | ||
this.ttl = microsecondsToSeconds(this.interval); | ||
this.client = client; | ||
@@ -279,19 +278,24 @@ this.namespace = namespace; | ||
private extractTimestampsFromZRangeResult(zRangeResult: Array<string>): Array<number> { | ||
private extractTimestampsFromZRangeResult(zRangeResult: Array<string>) { | ||
// We only want the stored timestamps, which are the values, or the odd indexes. | ||
// Map to numbers because by default all returned values are strings. | ||
return zRangeResult.filter((e, i) => i % 2).map(Number); | ||
return zRangeResult.filter((e, i) => i % 2).map(Number) as Array<Microseconds>; | ||
} | ||
} | ||
export function millisecondsToMicroseconds(milliseconds: Milliseconds): Microseconds { | ||
return 1000 * milliseconds; | ||
export function getCurrentMicroseconds() { | ||
const hr = process.hrtime(); | ||
return (hr[0] * 1e6 + Math.ceil(hr[1] / 1000)) as Microseconds; | ||
} | ||
export function microsecondsToMilliseconds(microseconds: Microseconds): Milliseconds { | ||
return Math.ceil(microseconds / 1000); | ||
export function millisecondsToMicroseconds(milliseconds: Milliseconds) { | ||
return (1000 * milliseconds) as Microseconds; | ||
} | ||
export function microsecondsToTTLSeconds(microseconds: Microseconds): Seconds { | ||
return Math.ceil(microseconds / 1000 / 1000); | ||
export function microsecondsToMilliseconds(microseconds: Microseconds) { | ||
return Math.ceil(microseconds / 1000) as Milliseconds; | ||
} | ||
export function microsecondsToSeconds(microseconds: Microseconds) { | ||
return Math.ceil(microseconds / 1000 / 1000) as Seconds; | ||
} |
Sorry, the diff of this file is not supported yet
1
-50%12
-14.29%1179
1.29%106
11.58%71462
-40.5%10
-9.09%- Removed
- Removed