Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14,923 changes: 187 additions & 14,736 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
"package-lock.js"
],
"scripts": {
"build:cleanbefore": "rm -rf dist",
"build:cleanbefore": "shx rm -rf dist",
"build:lint": "tslint -c tslint.json -p tsconfig.lint.json",
"build:node": "tsc",
"build": "npm run build:cleanbefore && npm run build:node && npm run build:lint",
"prepare": "NODE_ENV=production npm run build",
"test": "find src -name '*.spec.test.ts' | TS_NODE_FILES=true TS_NODE_CACHE=false TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' xargs mocha -r ts-node/register --require source-map-support/register",
"do_verify": "find src -name '*.integration.test.ts' | TS_NODE_FILES=true TS_NODE_CACHE=false TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' xargs mocha -r ts-node/register --require source-map-support/register",
"prepare": "cross-env NODE_ENV=production npm run build",
"test": "find src -name '*.spec.test.ts' | cross-env TS_NODE_FILES=true TS_NODE_CACHE=false TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' xargs mocha -r ts-node/register --require source-map-support/register",
"do_verify": "find src -name '*.integration.test.ts' | cross-env TS_NODE_FILES=true TS_NODE_CACHE=false TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' xargs mocha -r ts-node/register --require source-map-support/register",
"verify": "bash scripts/run-integration-tests-with-firebase-emulator.sh",
"lint-fix": "tslint --fix -c tslint.json -p tsconfig.lint.json",
"checkall": "npm run lint && npm run build && npm run test && npm run verify",
Expand All @@ -45,11 +45,13 @@
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"codecov": "^3.8.3",
"cross-env": "^7.0.3",
"istanbul": "^0.4.5",
"lodash": "^4.17.21",
"mocha": "^10.0.0",
"nyc": "^15.1.0",
"semantic-release": "^17.4.7",
"shx": "^0.3.4",
"sinon": "^14.0.0",
"ts-node": "^10.8.1",
"tslint": "^6.1.3",
Expand Down Expand Up @@ -85,7 +87,8 @@
],
"author": "Jędrzej Lewandowski <jedrzejblew@gmail.com> (https://jedrzej.lewandowski.doctor/)",
"contributors": [
"Jędrzej Lewandowski <jedrzejblew@gmail.com> (https://jedrzej.lewandowski.doctor/)"
"Jędrzej Lewandowski <jedrzejblew@gmail.com> (https://jedrzej.lewandowski.doctor/)",
"Jean Costa <jean.adcc@gmail.com>"
],
"license": "MIT",
"bugs": {
Expand Down
12 changes: 5 additions & 7 deletions src/GenericRateLimiter.spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe("GenericRateLimiter", () => {

await genericRateLimiter.isQuotaExceededOrRecordCall(sampleQualifier);

expect(_.values(persistenceProviderMock.persistenceObject)[0].u).to.contain(sampleTimestamp);
expect(_.values(persistenceProviderMock.persistenceObject)[0].u).to.contain(sampleTimestamp * 1000);
});

it("does not put current timestamp when quota was exceeded", async () => {
Expand All @@ -89,9 +89,7 @@ describe("GenericRateLimiter", () => {
const quotaExceeded2 = await genericRateLimiter.isQuotaExceededOrRecordCall(sampleQualifier);
expect(quotaExceeded2).to.be.equal(true);

expect(_.values(persistenceProviderMock.persistenceObject)[0].u)
.to.be.an("array")
.with.length(1);
expect(_.values(persistenceProviderMock.persistenceObject)[0].u).to.be.an("array").with.length(1);
});

describe("threshold tests", () => {
Expand All @@ -112,7 +110,7 @@ describe("GenericRateLimiter", () => {

for (let i = 0; i < 6; i++) {
timestampProviderMock.setTimestampSeconds(timestamp);
savedTimestamps.push(timestamp);
savedTimestamps.push(timestamp * 1000);
await genericRateLimiter.isQuotaExceededOrRecordCall(sampleQualifier);
timestamp += periodSeconds / 3 + 0.1; // remember: never push floats to the edges ;)
}
Expand Down Expand Up @@ -158,7 +156,7 @@ describe("GenericRateLimiter", () => {
});
});

describe("check tests", function() {
describe("check tests", function () {
[
{
name: "isQuotaExceededOrRecordCall",
Expand All @@ -172,7 +170,7 @@ describe("GenericRateLimiter", () => {
return genericRateLimiter.isQuotaAlreadyExceededDoNotRecordCall.bind(genericRateLimiter);
},
},
].forEach(testedMethod =>
].forEach((testedMethod) =>
describe(`#${testedMethod.name}`, () => {
it("returns true if there are more calls than maxCalls", async () => {
const periodSeconds = 20;
Expand Down
47 changes: 24 additions & 23 deletions src/GenericRateLimiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,66 +36,67 @@ export class GenericRateLimiter {
const resultHolder = {
isQuotaExceeded: false,
};
await this.persistenceProvider.updateAndGet(this.configuration.name, qualifier, record => {
return this.runTransactionForAnswer(record, resultHolder);
});

await this.persistenceProvider.updateAndGet(this.configuration.name, qualifier, (record) =>
this.runTransactionForAnswer(record, resultHolder),
);

return resultHolder.isQuotaExceeded;
}

public async isQuotaAlreadyExceededDoNotRecordCall(qualifier: string): Promise<boolean> {
const timestampsSeconds = this.getTimestampsSeconds();
const timestampsMs = this.getTimestampsMs();
const record = await this.persistenceProvider.get(this.configuration.name, qualifier);
const recentUsages: number[] = this.selectRecentUsages(record.u, timestampsSeconds.threshold);
return this.isQuotaExceeded(recentUsages.length);
const recentUsagesMs: number[] = this.selectRecentUsages(record.u, timestampsMs.threshold);
return this.isQuotaExceeded(recentUsagesMs.length);
}

private runTransactionForAnswer(
input: PersistenceRecord,
resultHolder: { isQuotaExceeded: boolean },
): PersistenceRecord {
const timestampsSeconds = this.getTimestampsSeconds();
const timestampsMs = this.getTimestampsMs();

this.debugFn("Got record with usages " + input.u.length);

const recentUsages: number[] = this.selectRecentUsages(input.u, timestampsSeconds.threshold);
this.debugFn("Of these usages there are" + recentUsages.length + " usages that count into period");
const recentUsagesMs: number[] = this.selectRecentUsages(input.u, timestampsMs.threshold);
this.debugFn("Of these usages there are" + recentUsagesMs.length + " usages that count into period");

const result = this.isQuotaExceeded(recentUsages.length);
const result = this.isQuotaExceeded(recentUsagesMs.length);
resultHolder.isQuotaExceeded = result;
this.debugFn("The result is quotaExceeded=" + result);

if (!result) {
this.debugFn("Quota was not exceeded, so recording a usage at " + timestampsSeconds.current);
recentUsages.push(timestampsSeconds.current);
this.debugFn("Quota was not exceeded, so recording a usage at " + timestampsMs.current);
recentUsagesMs.push(timestampsMs.current);
}

const newRecord: PersistenceRecord = {
u: recentUsages,
u: recentUsagesMs,
};
return newRecord;
}

private selectRecentUsages(allUsages: number[], timestampThresholdSeconds: number): number[] {
const recentUsages: number[] = [];
private selectRecentUsages(allUsageTimestampsMs: number[], timestampThresholdMs: number): number[] {
const recentUsageTimestampsMs: number[] = [];

for (const usageTime of allUsages) {
if (usageTime > timestampThresholdSeconds) {
recentUsages.push(usageTime);
for (const timestampMs of allUsageTimestampsMs) {
if (timestampMs > timestampThresholdMs) {
recentUsageTimestampsMs.push(timestampMs);
}
}
return recentUsages;
return recentUsageTimestampsMs;
}

private isQuotaExceeded(numOfRecentUsages: number): boolean {
return numOfRecentUsages >= this.configuration.maxCalls;
}

private getTimestampsSeconds(): { current: number; threshold: number } {
const currentServerTimestampSeconds: number = this.timestampProvider.getTimestampSeconds();
private getTimestampsMs(): { current: number; threshold: number } {
const currentServerTimestampMs: number = this.timestampProvider.getTimestampMs();
return {
current: currentServerTimestampSeconds,
threshold: currentServerTimestampSeconds - this.configuration.periodSeconds,
current: currentServerTimestampMs,
threshold: currentServerTimestampMs - this.configuration.periodSeconds * 1000,
};
}
}
4 changes: 2 additions & 2 deletions src/timestamp/FirebaseTimestampProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as admin from "firebase-admin";
import { TimestampProvider } from "./TimestampProvider";

export class FirebaseTimestampProvider implements TimestampProvider {
public getTimestampSeconds(): number {
return admin.firestore.Timestamp.now().seconds;
public getTimestampMs(): number {
return admin.firestore.Timestamp.now().toMillis();
}
}
2 changes: 1 addition & 1 deletion src/timestamp/TimestampProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export interface TimestampProvider {
getTimestampSeconds(): number;
getTimestampMs(): number;
}
10 changes: 5 additions & 5 deletions src/timestamp/TimestampProviderMock.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { TimestampProvider } from "./TimestampProvider";

export class TimestampProviderMock implements TimestampProvider {
private timestampNowSeconds: number | undefined = undefined;
private timestampNowMs: number | undefined = undefined;

public getTimestampSeconds(): number {
if (!this.timestampNowSeconds) return Date.now() / 1000;
else return this.timestampNowSeconds;
public getTimestampMs(): number {
if (!this.timestampNowMs) return Date.now();
else return this.timestampNowMs;
}

public setTimestampSeconds(timestampSeconds: number) {
this.timestampNowSeconds = timestampSeconds;
this.timestampNowMs = timestampSeconds * 1000;
}
}