chore: global sync per minute safety limit (#2765)
This commit is contained in:
38
packages/snjs/lib/Services/Sync/SyncFrequencyGuard.spec.ts
Normal file
38
packages/snjs/lib/Services/Sync/SyncFrequencyGuard.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { SyncFrequencyGuard } from './SyncFrequencyGuard'
|
||||
|
||||
describe('SyncFrequencyGuard', () => {
|
||||
const createUseCase = () => new SyncFrequencyGuard(3)
|
||||
|
||||
it('should return false when sync calls threshold is not reached', () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
expect(useCase.isSyncCallsThresholdReachedThisMinute()).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when sync calls threshold is reached', () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
useCase.incrementCallsPerMinute()
|
||||
useCase.incrementCallsPerMinute()
|
||||
useCase.incrementCallsPerMinute()
|
||||
|
||||
expect(useCase.isSyncCallsThresholdReachedThisMinute()).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when sync calls threshold is reached but a new minute has started', () => {
|
||||
const spyOnGetCallsPerMinuteKey = jest.spyOn(SyncFrequencyGuard.prototype as any, 'getCallsPerMinuteKey')
|
||||
spyOnGetCallsPerMinuteKey.mockReturnValueOnce('2020-1-1T1:1')
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
useCase.incrementCallsPerMinute()
|
||||
useCase.incrementCallsPerMinute()
|
||||
useCase.incrementCallsPerMinute()
|
||||
|
||||
spyOnGetCallsPerMinuteKey.mockReturnValueOnce('2020-1-1T1:2')
|
||||
|
||||
expect(useCase.isSyncCallsThresholdReachedThisMinute()).toBe(false)
|
||||
|
||||
spyOnGetCallsPerMinuteKey.mockRestore()
|
||||
})
|
||||
})
|
||||
40
packages/snjs/lib/Services/Sync/SyncFrequencyGuard.ts
Normal file
40
packages/snjs/lib/Services/Sync/SyncFrequencyGuard.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { SyncFrequencyGuardInterface } from './SyncFrequencyGuardInterface'
|
||||
|
||||
export class SyncFrequencyGuard implements SyncFrequencyGuardInterface {
|
||||
private callsPerMinuteMap: Map<string, number>
|
||||
|
||||
constructor(private syncCallsThresholdPerMinute: number) {
|
||||
this.callsPerMinuteMap = new Map<string, number>()
|
||||
}
|
||||
|
||||
isSyncCallsThresholdReachedThisMinute(): boolean {
|
||||
const stringDateToTheMinute = this.getCallsPerMinuteKey()
|
||||
const persistedCallsCount = this.callsPerMinuteMap.get(stringDateToTheMinute) || 0
|
||||
|
||||
return persistedCallsCount >= this.syncCallsThresholdPerMinute
|
||||
}
|
||||
|
||||
incrementCallsPerMinute(): void {
|
||||
const stringDateToTheMinute = this.getCallsPerMinuteKey()
|
||||
const persistedCallsCount = this.callsPerMinuteMap.get(stringDateToTheMinute)
|
||||
const newMinuteStarted = persistedCallsCount === undefined
|
||||
|
||||
if (newMinuteStarted) {
|
||||
this.clear()
|
||||
|
||||
this.callsPerMinuteMap.set(stringDateToTheMinute, 1)
|
||||
} else {
|
||||
this.callsPerMinuteMap.set(stringDateToTheMinute, persistedCallsCount + 1)
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.callsPerMinuteMap.clear()
|
||||
}
|
||||
|
||||
private getCallsPerMinuteKey(): string {
|
||||
const now = new Date()
|
||||
|
||||
return `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}T${now.getHours()}:${now.getMinutes()}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SyncFrequencyGuardInterface {
|
||||
incrementCallsPerMinute(): void
|
||||
isSyncCallsThresholdReachedThisMinute(): boolean
|
||||
clear(): void
|
||||
}
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
import { CreatePayloadFromRawServerItem } from './Account/Utilities'
|
||||
import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { SyncFrequencyGuardInterface } from './SyncFrequencyGuardInterface'
|
||||
|
||||
const DEFAULT_MAJOR_CHANGE_THRESHOLD = 15
|
||||
const INVALID_SESSION_RESPONSE_STATUS = 401
|
||||
@@ -169,6 +170,7 @@ export class SyncService
|
||||
private readonly options: ApplicationSyncOptions,
|
||||
private logger: LoggerInterface,
|
||||
private sockets: WebSocketsService,
|
||||
private syncFrequencyGuard: SyncFrequencyGuardInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -643,7 +645,8 @@ export class SyncService
|
||||
const syncInProgress = this.opStatus.syncInProgress
|
||||
const databaseLoaded = this.databaseLoaded
|
||||
const canExecuteSync = !this.syncLock
|
||||
const shouldExecuteSync = canExecuteSync && databaseLoaded && !syncInProgress
|
||||
const syncLimitReached = this.syncFrequencyGuard.isSyncCallsThresholdReachedThisMinute()
|
||||
const shouldExecuteSync = canExecuteSync && databaseLoaded && !syncInProgress && !syncLimitReached
|
||||
|
||||
if (shouldExecuteSync) {
|
||||
this.syncLock = true
|
||||
@@ -1296,6 +1299,8 @@ export class SyncService
|
||||
|
||||
this.lastSyncDate = new Date()
|
||||
|
||||
this.syncFrequencyGuard.incrementCallsPerMinute()
|
||||
|
||||
if (operation instanceof AccountSyncOperation && operation.numberOfItemsInvolved >= this.majorChangeThreshold) {
|
||||
void this.notifyEvent(SyncEvent.MajorDataChange)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user