mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
2 Commits
ai-ux-impr
...
d41998ec1b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d41998ec1b | ||
![]() |
f917d1597e |
@@ -33,6 +33,49 @@ namespace gdjs {
|
||||
claimSecret?: string;
|
||||
};
|
||||
|
||||
// Rolling window rate limiting
|
||||
// Implements rate limiting to prevent abuse:
|
||||
// - Maximum 12 successful successful entries per minute across all leaderboards
|
||||
// - Maximum 6 successful successful entries per minute per individual leaderboard
|
||||
// - Works in addition to existing 500ms cooldown between entry tentatives
|
||||
let _successfulEntriesGlobal: number[] = []; // Timestamps of successful entries across all leaderboards
|
||||
|
||||
const GLOBAL_RATE_LIMIT_COUNT = 12;
|
||||
const PER_LEADERBOARD_RATE_LIMIT_COUNT = 6;
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute in milliseconds
|
||||
|
||||
/**
|
||||
* Clean old entries from the rolling window (older than 1 minute)
|
||||
*/
|
||||
const cleanOldEntries = (
|
||||
entries: number[],
|
||||
currentTime: number
|
||||
): number[] => {
|
||||
return entries.filter(
|
||||
(timestamp) => currentTime - timestamp < RATE_LIMIT_WINDOW_MS
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if adding a new entry would exceed global rate limits.
|
||||
*/
|
||||
const wouldExceedGlobalSuccessRateLimit = (): boolean => {
|
||||
const currentTime = Date.now();
|
||||
_successfulEntriesGlobal = cleanOldEntries(
|
||||
_successfulEntriesGlobal,
|
||||
currentTime
|
||||
);
|
||||
return _successfulEntriesGlobal.length >= GLOBAL_RATE_LIMIT_COUNT;
|
||||
};
|
||||
|
||||
/**
|
||||
* Record a successful entry for global rate limiting tracking.
|
||||
*/
|
||||
const recordGlobalSuccessfulEntry = (): void => {
|
||||
const currentTime = Date.now();
|
||||
_successfulEntriesGlobal.push(currentTime);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hold the state of the save of a score for a leaderboard.
|
||||
*/
|
||||
@@ -43,6 +86,9 @@ namespace gdjs {
|
||||
/** The promise that will be resolved when the score saving is done (successfully or not). */
|
||||
lastSavingPromise: Promise<void> | null = null;
|
||||
|
||||
/** Timestamps of successful entries for this leaderboard (for rate limiting) */
|
||||
private _successfulEntries: number[] = [];
|
||||
|
||||
// Score that is being saved:
|
||||
private _currentlySavingScore: number | null = null;
|
||||
private _currentlySavingPlayerName: string | null = null;
|
||||
@@ -107,13 +153,36 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
private _isTooSoonToSaveAnotherScore(): boolean {
|
||||
private _wouldExceedPerLeaderboardTentativeRateLimit(): boolean {
|
||||
// Prevent entries within 500ms of each other (per leaderboard)
|
||||
// as this would indicate surely a score saved every frame.
|
||||
//
|
||||
// Note that is on lastScoreSavingStartedAt, not lastScoreSavingSucceededAt,
|
||||
// which means we limit tentatives here (and not successes).
|
||||
return (
|
||||
!!this.lastScoreSavingStartedAt &&
|
||||
Date.now() - this.lastScoreSavingStartedAt < 500
|
||||
);
|
||||
}
|
||||
|
||||
private _wouldExceedPerLeaderboardSuccessRateLimit(): boolean {
|
||||
const currentTime = Date.now();
|
||||
this._successfulEntries = cleanOldEntries(
|
||||
this._successfulEntries,
|
||||
currentTime
|
||||
);
|
||||
return (
|
||||
this._successfulEntries.length >= PER_LEADERBOARD_RATE_LIMIT_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
private _recordPerLeaderboardAndGlobalSuccessfulEntry(): void {
|
||||
const currentTime = Date.now();
|
||||
this._successfulEntries.push(currentTime);
|
||||
|
||||
recordGlobalSuccessfulEntry();
|
||||
}
|
||||
|
||||
startSaving({
|
||||
playerName,
|
||||
playerId,
|
||||
@@ -141,7 +210,7 @@ namespace gdjs {
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
|
||||
if (this._isTooSoonToSaveAnotherScore()) {
|
||||
if (this._wouldExceedPerLeaderboardTentativeRateLimit()) {
|
||||
logger.warn(
|
||||
'Last entry was sent too little time ago. Ignoring this one.'
|
||||
);
|
||||
@@ -154,6 +223,24 @@ namespace gdjs {
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
|
||||
// Rolling window rate limiting check for successful entries.
|
||||
if (wouldExceedGlobalSuccessRateLimit()) {
|
||||
logger.warn(
|
||||
'Rate limit exceeded. Too many entries have been successfully sent recently across all leaderboards. Ignoring this one.'
|
||||
);
|
||||
this._setError('GLOBAL_RATE_LIMIT_EXCEEDED');
|
||||
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
if (this._wouldExceedPerLeaderboardSuccessRateLimit()) {
|
||||
logger.warn(
|
||||
'Rate limit exceeded. Too many entries have been successfully sent recently for this leaderboard. Ignoring this one.'
|
||||
);
|
||||
this._setError('LEADERBOARD_RATE_LIMIT_EXCEEDED');
|
||||
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
|
||||
let resolveSavingPromise: () => void;
|
||||
const savingPromise = new Promise<void>((resolve) => {
|
||||
resolveSavingPromise = resolve;
|
||||
@@ -169,6 +256,9 @@ namespace gdjs {
|
||||
|
||||
return {
|
||||
closeSaving: (leaderboardEntry) => {
|
||||
// Record successful entry for rolling window rate limiting.
|
||||
this._recordPerLeaderboardAndGlobalSuccessfulEntry();
|
||||
|
||||
if (savingPromise !== this.lastSavingPromise) {
|
||||
logger.info(
|
||||
'Score saving result received, but another save was launched in the meantime - ignoring the result of this one.'
|
||||
@@ -396,7 +486,10 @@ namespace gdjs {
|
||||
|
||||
try {
|
||||
const { closeSaving, closeSavingWithError } =
|
||||
scoreSavingState.startSaving({ playerName, score });
|
||||
scoreSavingState.startSaving({
|
||||
playerName,
|
||||
score,
|
||||
});
|
||||
|
||||
try {
|
||||
const leaderboardEntry = await saveScore({
|
||||
@@ -440,7 +533,10 @@ namespace gdjs {
|
||||
|
||||
try {
|
||||
const { closeSaving, closeSavingWithError } =
|
||||
scoreSavingState.startSaving({ playerId, score });
|
||||
scoreSavingState.startSaving({
|
||||
playerId,
|
||||
score,
|
||||
});
|
||||
|
||||
try {
|
||||
const leaderboardEntryId = await saveScore({
|
||||
|
55
newIDE/app/package-lock.json
generated
55
newIDE/app/package-lock.json
generated
@@ -33,7 +33,7 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"pixi-spine": "4.0.4",
|
||||
"pixi.js-legacy": "7.4.2",
|
||||
"posthog-js": "^1.57.2",
|
||||
"posthog-js": "1.275.2",
|
||||
"prop-types": "^15.5.10",
|
||||
"qr-creator": "^1.0.0",
|
||||
"react": "16.14.0",
|
||||
@@ -5770,6 +5770,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz",
|
||||
"integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw=="
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"license": "BSD-3-Clause"
|
||||
@@ -25646,16 +25651,43 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.57.2",
|
||||
"license": "MIT",
|
||||
"version": "1.275.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.275.2.tgz",
|
||||
"integrity": "sha512-g1fnV/GAcEdwwk4EVbJ1HMZhlhgKYxG1Z5KPGvr+q5re0ltyVq8jFA2PsF333jvOlI8R01LLdpYSIgU8sBiZfg==",
|
||||
"dependencies": {
|
||||
"fflate": "^0.4.1",
|
||||
"rrweb-snapshot": "^1.1.14"
|
||||
"@posthog/core": "1.3.0",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rrweb/types": "2.0.0-alpha.17",
|
||||
"rrweb-snapshot": "2.0.0-alpha.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rrweb/types": {
|
||||
"optional": true
|
||||
},
|
||||
"rrweb-snapshot": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js/node_modules/core-js": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
|
||||
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.13.1",
|
||||
"license": "MIT",
|
||||
"version": "10.27.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
|
||||
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
@@ -30091,10 +30123,6 @@
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-snapshot": {
|
||||
"version": "1.1.14",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rtl-css-js": {
|
||||
"version": "1.14.0",
|
||||
"license": "MIT",
|
||||
@@ -33106,6 +33134,11 @@
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
|
||||
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "6.1.0",
|
||||
"dev": true,
|
||||
|
@@ -69,7 +69,7 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"pixi-spine": "4.0.4",
|
||||
"pixi.js-legacy": "7.4.2",
|
||||
"posthog-js": "^1.57.2",
|
||||
"posthog-js": "1.275.2",
|
||||
"prop-types": "^15.5.10",
|
||||
"qr-creator": "^1.0.0",
|
||||
"react": "16.14.0",
|
||||
|
Reference in New Issue
Block a user