-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
### Summary Resolves #207 Sibling PR: gt-scheduler/firebase-conf#4 We want to be able to rate limit the `/fetchFriendSchedules` firebase cloud function endpoint to prevent DOS attacks and increased server costs. I implemented a client-side Leaky Bucket solution using a request count stored in the local storage. The counts are updated based on the last request time when a new request is made. The rate limiter does not restrict the background calls to the endpoint that syncs any change to friend schedules. Each second, the request count increases by 1 and is capped at 10. Note: This rate limiter does not sync request count across devices due to the usage of local storage. To do so, we might have to use an extra collection which might increase costs. ### Checklist - [x] /fetchFriendSchedules is rate limited. - [x] Throttled requests return an error and that error is displayed on the UI. ### How to Test - Change the rate limiter capacity and interval in `src\data\hooks\useRawFriendScheduleDataFromFirebaseFunction.ts` to something that can be reached by normal page reloads --------- Co-authored-by: nathangong <[email protected]> Co-authored-by: Nghi Ho <[email protected]> Co-authored-by: Hailey Ho <[email protected]>
- Loading branch information
1 parent
a225009
commit ef99fd6
Showing
2 changed files
with
139 additions
and
14 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
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,80 @@ | ||
import { useMemo, useCallback } from 'react'; | ||
import useLocalStorageState from 'use-local-storage-state'; | ||
|
||
interface RateLimiterBucket { | ||
remainingCount: number; | ||
lastRefreshTime: string | Date; | ||
} | ||
|
||
export default function useRateLimiter( | ||
bucketName: string, | ||
capacity: number, | ||
interval: number | ||
): { | ||
hasReachedLimit: boolean; | ||
refreshBucket: () => void; | ||
decrementBucketCount: () => void; | ||
} { | ||
const [bucket, setBucket] = useLocalStorageState<RateLimiterBucket>( | ||
bucketName, | ||
{ | ||
defaultValue: { | ||
remainingCount: capacity, | ||
lastRefreshTime: new Date(), | ||
}, | ||
storageSync: true, | ||
} | ||
); | ||
|
||
const intervalMs = useMemo(() => interval * 1000, [interval]); | ||
|
||
const hasReachedLimit = useMemo(() => { | ||
return bucket.remainingCount < 0; | ||
}, [bucket.remainingCount]); | ||
|
||
const refreshBucket = useCallback(() => { | ||
setBucket((currBucket) => { | ||
const oldDate = new Date(currBucket.lastRefreshTime); | ||
const newDate = new Date(); | ||
const isOldDateInvalid = Number.isNaN(oldDate.valueOf()); | ||
if (!isOldDateInvalid) { | ||
const bucketCountAdded = Math.floor( | ||
((newDate.valueOf() - oldDate.valueOf()) / intervalMs) * capacity | ||
); | ||
if (bucketCountAdded > 0) { | ||
return { | ||
remainingCount: Math.min( | ||
capacity, | ||
currBucket.remainingCount + | ||
bucketCountAdded + | ||
(currBucket.remainingCount < 0 ? 1 : 0) | ||
), | ||
lastRefreshTime: newDate, | ||
}; | ||
} | ||
} | ||
|
||
return { | ||
remainingCount: Math.min(capacity, currBucket.remainingCount), | ||
lastRefreshTime: isOldDateInvalid | ||
? new Date() | ||
: currBucket.lastRefreshTime, | ||
}; | ||
}); | ||
}, [capacity, intervalMs, setBucket]); | ||
|
||
const decrementBucketCount = useCallback(() => { | ||
setBucket((currBucket) => { | ||
return { | ||
...currBucket, | ||
remainingCount: currBucket.remainingCount - 1, | ||
}; | ||
}); | ||
}, [setBucket]); | ||
|
||
return { | ||
hasReachedLimit, | ||
refreshBucket, | ||
decrementBucketCount, | ||
}; | ||
} |