Skip to content

Commit dc1276c

Browse files
committed
Delete earliest heartbeats only once there are 30 heartbeats
1 parent 2ec1c76 commit dc1276c

File tree

2 files changed

+91
-15
lines changed

2 files changed

+91
-15
lines changed

packages/app/src/heartbeatService.test.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ import '../test/setup';
2020
import {
2121
countBytes,
2222
HeartbeatServiceImpl,
23-
extractHeartbeatsForHeader
23+
extractHeartbeatsForHeader,
24+
getEarliestHeartbeatIdx
2425
} from './heartbeatService';
2526
import {
2627
Component,
2728
ComponentType,
2829
ComponentContainer
2930
} from '@firebase/component';
30-
import { PlatformLoggerService } from './types';
31+
import { PlatformLoggerService, SingleDateHeartbeat } from './types';
3132
import { FirebaseApp } from './public-types';
3233
import * as firebaseUtil from '@firebase/util';
3334
import { SinonStub, stub, useFakeTimers } from 'sinon';
@@ -173,7 +174,6 @@ describe('HeartbeatServiceImpl', () => {
173174
let writeStub: SinonStub;
174175
let userAgentString = USER_AGENT_STRING_1;
175176
const mockIndexedDBHeartbeats = [
176-
// Chosen so one will exceed 30 day limit and one will not.
177177
{
178178
agent: 'old-user-agent',
179179
date: '1969-12-01'
@@ -236,15 +236,14 @@ describe('HeartbeatServiceImpl', () => {
236236
});
237237
}
238238
});
239-
it(`triggerHeartbeat() writes new heartbeats and retains old ones newer than 30 days`, async () => {
239+
it(`triggerHeartbeat() writes new heartbeats and retains old ones`, async () => {
240240
userAgentString = USER_AGENT_STRING_2;
241241
clock.tick(3 * 24 * 60 * 60 * 1000);
242242
await heartbeatService.triggerHeartbeat();
243243
if (firebaseUtil.isIndexedDBAvailable()) {
244244
expect(writeStub).to.be.calledWith({
245245
heartbeats: [
246-
// The first entry exceeds the 30 day retention limit.
247-
mockIndexedDBHeartbeats[1],
246+
...mockIndexedDBHeartbeats,
248247
{ agent: USER_AGENT_STRING_2, date: '1970-01-04' }
249248
]
250249
});
@@ -260,6 +259,7 @@ describe('HeartbeatServiceImpl', () => {
260259
);
261260
if (firebaseUtil.isIndexedDBAvailable()) {
262261
expect(heartbeatHeaders).to.include('old-user-agent');
262+
expect(heartbeatHeaders).to.include('1969-12-01');
263263
expect(heartbeatHeaders).to.include('1969-12-31');
264264
}
265265
expect(heartbeatHeaders).to.include(USER_AGENT_STRING_2);
@@ -273,6 +273,36 @@ describe('HeartbeatServiceImpl', () => {
273273
const emptyHeaders = await heartbeatService.getHeartbeatsHeader();
274274
expect(emptyHeaders).to.equal('');
275275
});
276+
it('triggerHeartbeat() removes the earliest heartbeat once it exceeds the max number of heartbeats', async () => {
277+
// Trigger heartbeats until we reach the limit
278+
const numHeartbeats =
279+
heartbeatService._heartbeatsCache?.heartbeats.length!;
280+
for (let i = numHeartbeats; i <= 30; i++) {
281+
await heartbeatService.triggerHeartbeat();
282+
clock.tick(24 * 60 * 60 * 1000);
283+
}
284+
285+
expect(heartbeatService._heartbeatsCache?.heartbeats.length).to.equal(30);
286+
const earliestHeartbeatDate = getEarliestHeartbeatIdx(
287+
heartbeatService._heartbeatsCache?.heartbeats!
288+
);
289+
const earliestHeartbeat =
290+
heartbeatService._heartbeatsCache?.heartbeats[earliestHeartbeatDate]!;
291+
await heartbeatService.triggerHeartbeat();
292+
expect(heartbeatService._heartbeatsCache?.heartbeats.length).to.equal(30);
293+
expect(
294+
heartbeatService._heartbeatsCache?.heartbeats.indexOf(earliestHeartbeat)
295+
).to.equal(-1);
296+
});
297+
it('triggerHeartbeat() never exceeds 30 heartbeats', async () => {
298+
for (let i = 0; i <= 50; i++) {
299+
await heartbeatService.triggerHeartbeat();
300+
clock.tick(24 * 60 * 60 * 1000);
301+
expect(
302+
heartbeatService._heartbeatsCache?.heartbeats.length
303+
).to.be.lessThanOrEqual(30);
304+
}
305+
});
276306
});
277307

278308
describe('If IndexedDB records that a header was sent today', () => {
@@ -426,4 +456,22 @@ describe('HeartbeatServiceImpl', () => {
426456
);
427457
});
428458
});
459+
460+
describe('getEarliestHeartbeatIdx()', () => {
461+
it('returns -1 if the heartbeats array is empty', () => {
462+
const heartbeats: SingleDateHeartbeat[] = [];
463+
const idx = getEarliestHeartbeatIdx(heartbeats);
464+
expect(idx).to.equal(-1);
465+
});
466+
467+
it('returns the index of the earliest date', () => {
468+
const heartbeats = [
469+
{ agent: generateUserAgentString(2), date: '2022-01-02' },
470+
{ agent: generateUserAgentString(1), date: '2022-01-01' },
471+
{ agent: generateUserAgentString(3), date: '2022-01-03' }
472+
];
473+
const idx = getEarliestHeartbeatIdx(heartbeats);
474+
expect(idx).to.equal(1);
475+
});
476+
});
429477
});

packages/app/src/heartbeatService.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ import {
3636
import { logger } from './logger';
3737

3838
const MAX_HEADER_BYTES = 1024;
39-
// 30 days
40-
const STORED_HEARTBEAT_RETENTION_MAX_MILLIS = 30 * 24 * 60 * 60 * 1000;
39+
const MAX_NUM_STORED_HEARTBEATS = 30;
4140

4241
export class HeartbeatServiceImpl implements HeartbeatService {
4342
/**
@@ -109,14 +108,19 @@ export class HeartbeatServiceImpl implements HeartbeatService {
109108
} else {
110109
// There is no entry for this date. Create one.
111110
this._heartbeatsCache.heartbeats.push({ date, agent });
111+
112+
// If the number of stored heartbeats exceeds the maximum number of stored heartbeats, remove the heartbeat with the earliest date.
113+
// Since this is executed each time a heartbeat is pushed, the limit can only be exceeded by one, so only one needs to be removed.
114+
if (
115+
this._heartbeatsCache.heartbeats.length > MAX_NUM_STORED_HEARTBEATS
116+
) {
117+
const earliestHeartbeatIdx = getEarliestHeartbeatIdx(
118+
this._heartbeatsCache.heartbeats
119+
);
120+
this._heartbeatsCache.heartbeats.splice(earliestHeartbeatIdx, 1);
121+
}
112122
}
113-
// Remove entries older than 30 days.
114-
this._heartbeatsCache.heartbeats =
115-
this._heartbeatsCache.heartbeats.filter(singleDateHeartbeat => {
116-
const hbTimestamp = new Date(singleDateHeartbeat.date).valueOf();
117-
const now = Date.now();
118-
return now - hbTimestamp <= STORED_HEARTBEAT_RETENTION_MAX_MILLIS;
119-
});
123+
120124
return this._storage.overwrite(this._heartbeatsCache);
121125
} catch (e) {
122126
logger.warn(e);
@@ -303,3 +307,27 @@ export function countBytes(heartbeatsCache: HeartbeatsByUserAgent[]): number {
303307
JSON.stringify({ version: 2, heartbeats: heartbeatsCache })
304308
).length;
305309
}
310+
311+
/**
312+
* Returns the index of the heartbeat with the earliest date.
313+
* If the heartbeats array is empty, -1 is returned.
314+
*/
315+
export function getEarliestHeartbeatIdx(
316+
heartbeats: SingleDateHeartbeat[]
317+
): number {
318+
if (heartbeats.length === 0) {
319+
return -1;
320+
}
321+
322+
let earliestHeartbeatIdx = 0;
323+
let earliestHeartbeatDate = heartbeats[0].date;
324+
325+
for (let i = 1; i < heartbeats.length; i++) {
326+
if (heartbeats[i].date < earliestHeartbeatDate) {
327+
earliestHeartbeatDate = heartbeats[i].date;
328+
earliestHeartbeatIdx = i;
329+
}
330+
}
331+
332+
return earliestHeartbeatIdx;
333+
}

0 commit comments

Comments
 (0)