From d2617441d8d5a5e1c4719171a1c2cad35cb08702 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Wed, 11 Dec 2024 10:17:17 +0100 Subject: [PATCH 01/11] feat: delete entries from the DB of the signaling server as soon as the channel is closed Signed-off-by: nidhal-labidi --- flottform/forms/src/flottform-channel-host.ts | 38 ++++++++++++++----- .../forms/src/flottform-text-input-host.ts | 1 + 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/flottform/forms/src/flottform-channel-host.ts b/flottform/forms/src/flottform-channel-host.ts index ca75481..1640107 100644 --- a/flottform/forms/src/flottform-channel-host.ts +++ b/flottform/forms/src/flottform-channel-host.ts @@ -11,6 +11,9 @@ import { export class FlottformChannelHost extends EventEmitter { private flottformApi: string | URL; + private baseApi: string; + private endpointId: string = ''; + private hostKey: string = ''; private createClientUrl: (params: { endpointId: string }) => Promise; private rtcConfiguration: RTCConfiguration; private pollTimeForIceInMs: number; @@ -35,6 +38,11 @@ export class FlottformChannelHost extends EventEmitter { }) { super(); this.flottformApi = flottformApi; + this.baseApi = ( + this.flottformApi instanceof URL ? this.flottformApi : new URL(this.flottformApi) + ) + .toString() + .replace(/\/$/, ''); this.createClientUrl = createClientUrl; this.rtcConfiguration = DEFAULT_WEBRTC_CONFIG; this.pollTimeForIceInMs = pollTimeForIceInMs; @@ -55,14 +63,9 @@ export class FlottformChannelHost extends EventEmitter { if (this.openPeerConnection) { this.close(); } - const baseApi = ( - this.flottformApi instanceof URL ? this.flottformApi : new URL(this.flottformApi) - ) - .toString() - .replace(/\/$/, ''); try { - this.rtcConfiguration.iceServers = await this.fetchIceServers(baseApi); + this.rtcConfiguration.iceServers = await this.fetchIceServers(this.baseApi); } catch (error) { // Use the default configuration as a fallback this.logger.error(error); @@ -74,11 +77,13 @@ export class FlottformChannelHost extends EventEmitter { const session = await this.openPeerConnection.createOffer(); await this.openPeerConnection.setLocalDescription(session); - const { endpointId, hostKey } = await this.createEndpoint(baseApi, session); + const { endpointId, hostKey } = await this.createEndpoint(this.baseApi, session); + this.hostKey = hostKey; + this.endpointId = endpointId; this.logger.log('Created endpoint', { endpointId, hostKey }); - const getEndpointInfoUrl = `${baseApi}/${endpointId}`; - const putHostInfoUrl = `${baseApi}/${endpointId}/host`; + const getEndpointInfoUrl = `${this.baseApi}/${endpointId}`; + const putHostInfoUrl = `${this.baseApi}/${endpointId}/host`; const hostIceCandidates = new Set(); await this.putHostInfo(putHostInfoUrl, hostKey, hostIceCandidates, session); @@ -103,6 +108,8 @@ export class FlottformChannelHost extends EventEmitter { this.openPeerConnection = null; } this.changeState('disconnected'); + // Cleanup old entries. + this.deleteEndpoint(this.baseApi, this.endpointId, this.hostKey); }; private setupDataChannelListener = () => { @@ -223,6 +230,19 @@ export class FlottformChannelHost extends EventEmitter { return response.json(); }; + private deleteEndpoint = async (baseApi: string, endpointId: string, hostKey: string) => { + const response = await fetch(`${baseApi}/${endpointId}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ hostKey }) + }); + + return response.json(); + }; + private fetchIceServers = async (baseApi: string) => { const response = await fetch(`${baseApi}/ice-server-credentials`, { method: 'GET', diff --git a/flottform/forms/src/flottform-text-input-host.ts b/flottform/forms/src/flottform-text-input-host.ts index 3217647..04a25c4 100644 --- a/flottform/forms/src/flottform-text-input-host.ts +++ b/flottform/forms/src/flottform-text-input-host.ts @@ -67,6 +67,7 @@ export class FlottformTextInputHost extends BaseInputHost { this.emit('receive'); // We suppose that the data received is small enough to be all included in 1 message this.emit('done', e.data); + this.close(); }; private registerListeners = () => { From 12d007aee45f15865b471bf30b40ba4a7b4c080b Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Wed, 11 Dec 2024 14:55:08 +0100 Subject: [PATCH 02/11] feat: add automated cleanup mechanism for inactive database entries Signed-off-by: nidhal-labidi --- flottform/server/src/database.ts | 45 +++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index 271a410..46a6da9 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -13,6 +13,7 @@ type EndpointInfo = { session: RTCSessionDescriptionInit; iceCandidates: RTCIceCandidateInit[]; }; + lastUpdate: number; }; type SafeEndpointInfo = Omit; @@ -25,8 +26,40 @@ function createRandomEndpointId(): string { class FlottformDatabase { private map = new Map(); + private cleanupIntervalId: NodeJS.Timeout | null = null; + private cleanupPeriod = 30 * 60 * 1000; // (30 minutes) + private entryTTL = 25 * 60 * 1000; // Time-to-Live for each entry (25 minutes) - constructor() {} + constructor() { + this.startCleanup(); + } + + private startCleanup() { + this.cleanupIntervalId = setInterval(() => { + // Loop over all entries and delete the stale ones. + const now = Date.now(); + for (const [endpointId, endpointInfo] of this.map) { + const lastUpdated = endpointInfo.lastUpdate; + if (now - lastUpdated > this.entryTTL) { + this.map.delete(endpointId); + console.log(`Cleaned up stale entry: ${endpointId}`); + } + } + }, this.cleanupPeriod); + } + + private stopCleanup() { + // Clear the interval to stop cleanup + if (this.cleanupIntervalId) { + clearInterval(this.cleanupIntervalId); + this.cleanupIntervalId = null; + } + } + + // Stop the cleanup when the database is no longer needed + destroy() { + this.stopCleanup(); + } async createEndpoint({ session }: { session: RTCSessionDescriptionInit }): Promise { const entry = { @@ -35,7 +68,8 @@ class FlottformDatabase { hostInfo: { session, iceCandidates: [] - } + }, + lastUpdate: Date.now() }; this.map.set(entry.endpointId, entry); return entry; @@ -46,6 +80,7 @@ class FlottformDatabase { if (!entry) { throw Error('Endpoint not found'); } + entry.lastUpdate = Date.now(); const { hostKey: _ignore1, clientKey: _ignore2, ...endpoint } = entry; return endpoint; @@ -72,7 +107,8 @@ class FlottformDatabase { const newInfo = { ...existingSession, - hostInfo: { ...existingSession.hostInfo, session, iceCandidates } + hostInfo: { ...existingSession.hostInfo, session, iceCandidates }, + lastUpdate: Date.now() }; this.map.set(endpointId, newInfo); @@ -105,7 +141,8 @@ class FlottformDatabase { const newInfo = { ...existingSession, clientKey, - clientInfo: { session, iceCandidates } + clientInfo: { session, iceCandidates }, + lastUpdate: Date.now() }; this.map.set(endpointId, newInfo); From 481b54b79274731f0320c91e2216ec3726a87253 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Mon, 16 Dec 2024 16:57:53 +0100 Subject: [PATCH 03/11] wip: update old test to match the new error message Signed-off-by: nidhal-labidi --- flottform/server/src/database.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flottform/server/src/database.spec.ts b/flottform/server/src/database.spec.ts index 381ab72..9aa076c 100644 --- a/flottform/server/src/database.spec.ts +++ b/flottform/server/src/database.spec.ts @@ -168,7 +168,9 @@ describe('Flottform database', () => { session: answer, iceCandidates: [] }) - ).rejects.toThrow(/peerkey/i); + ).rejects.toThrow( + /clientKey is wrong: Another peer is already connected and you cannot change this info without the correct key anymore. If you lost your key, initiate a new Flottform connection./i + ); const infoAfter = await db.getEndpoint({ endpointId }); expect(infoBefore).toStrictEqual(infoAfter); }); From 2edce18caf295ab9c77b4db00da5ea864c2a08b6 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Mon, 16 Dec 2024 17:12:14 +0100 Subject: [PATCH 04/11] add optional parameters to the FlottformDatabase constructor Signed-off-by: nidhal-labidi --- flottform/server/src/database.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index 46a6da9..2c20adc 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -27,10 +27,12 @@ function createRandomEndpointId(): string { class FlottformDatabase { private map = new Map(); private cleanupIntervalId: NodeJS.Timeout | null = null; - private cleanupPeriod = 30 * 60 * 1000; // (30 minutes) - private entryTTL = 25 * 60 * 1000; // Time-to-Live for each entry (25 minutes) + private cleanupPeriod; + private entryTTL; // Time-to-Live for each entry - constructor() { + constructor(cleanupPeriod = 30 * 60 * 1000, entryTTL = 25 * 60 * 1000) { + this.cleanupPeriod = cleanupPeriod; + this.entryTTL = entryTTL; this.startCleanup(); } From fce4f551c33abb14f179d3e40e18e7b5a4374acb Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Tue, 17 Dec 2024 11:02:59 +0100 Subject: [PATCH 05/11] wip: add optional parameters for the constructor and methods of FlottformDatabase to allow testing Signed-off-by: nidhal-labidi --- flottform/server/src/database.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index 2c20adc..c244c3f 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -92,12 +92,14 @@ class FlottformDatabase { endpointId, hostKey, session, - iceCandidates + iceCandidates, + lastUpdate = Date.now() }: { endpointId: EndpointId; hostKey: HostKey; session: RTCSessionDescriptionInit; iceCandidates: RTCIceCandidateInit[]; + lastUpdate?: number; }): Promise { const existingSession = this.map.get(endpointId); if (!existingSession) { @@ -110,7 +112,7 @@ class FlottformDatabase { const newInfo = { ...existingSession, hostInfo: { ...existingSession.hostInfo, session, iceCandidates }, - lastUpdate: Date.now() + lastUpdate }; this.map.set(endpointId, newInfo); @@ -123,12 +125,14 @@ class FlottformDatabase { endpointId, clientKey, session, - iceCandidates + iceCandidates, + lastUpdate = Date.now() }: { endpointId: EndpointId; clientKey: ClientKey; session: RTCSessionDescriptionInit; iceCandidates: RTCIceCandidateInit[]; + lastUpdate?: number; }): Promise> { const existingSession = this.map.get(endpointId); if (!existingSession) { @@ -144,7 +148,7 @@ class FlottformDatabase { ...existingSession, clientKey, clientInfo: { session, iceCandidates }, - lastUpdate: Date.now() + lastUpdate }; this.map.set(endpointId, newInfo); @@ -169,8 +173,11 @@ class FlottformDatabase { } } -export async function createFlottformDatabase(): Promise { - return new FlottformDatabase(); +export async function createFlottformDatabase( + cleanupPeriod = 30 * 60 * 1000, + entryTTL = 25 * 60 * 1000 +): Promise { + return new FlottformDatabase(cleanupPeriod, entryTTL); } export type { FlottformDatabase }; From 89195be0deb5bae3e4ef584fe1c2c77599bf1de8 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Tue, 17 Dec 2024 11:03:43 +0100 Subject: [PATCH 06/11] feat: add test for the cleanup mechanism of FlottformDatabase Signed-off-by: nidhal-labidi --- flottform/server/src/database.spec.ts | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/flottform/server/src/database.spec.ts b/flottform/server/src/database.spec.ts index 9aa076c..5988079 100644 --- a/flottform/server/src/database.spec.ts +++ b/flottform/server/src/database.spec.ts @@ -175,4 +175,61 @@ describe('Flottform database', () => { expect(infoBefore).toStrictEqual(infoAfter); }); }); + + describe('startCleanup()', () => { + function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + it('Should clean up stale entries after entryTTL', async () => { + const db = await createFlottformDatabase(100, 50); + const conn = new RTCPeerConnection(); + const offer = await conn.createOffer(); + const { endpointId } = await db.createEndpoint({ session: offer }); + + const connPeer = new RTCPeerConnection(); + await connPeer.setRemoteDescription(offer); + const answer = await connPeer.createAnswer(); + const clientKey = 'random-key'; + + await db.putClientInfo({ + endpointId, + clientKey, + session: answer, + iceCandidates: [], + lastUpdate: Date.now() + }); + + // Sleep for enough time to trigger the first cleanup + await sleep(110); + + // The endpoint should be cleaned by now + expect(async () => await db.getEndpoint({ endpointId })).rejects.toThrow(/endpoint/i); + }); + + it("Shouldn't clean up entries before entryTTL is expired", async () => { + const db = await createFlottformDatabase(100, 50); + const conn = new RTCPeerConnection(); + const offer = await conn.createOffer(); + const { endpointId } = await db.createEndpoint({ session: offer }); + + const connPeer = new RTCPeerConnection(); + await connPeer.setRemoteDescription(offer); + const answer = await connPeer.createAnswer(); + const clientKey = 'random-key'; + + await db.putClientInfo({ + endpointId, + clientKey, + session: answer, + iceCandidates: [], + lastUpdate: Date.now() + }); + + // The endpoint shouldn't be cleaned by now + const retrievedInfo = await db.getEndpoint({ endpointId }); + expect(retrievedInfo).toBeDefined(); + expect(retrievedInfo?.hostInfo.session).toStrictEqual(offer); + }); + }); }); From 6511b0a409f6781b2f99e8d5b101554e78040aa6 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Tue, 17 Dec 2024 12:11:59 +0100 Subject: [PATCH 07/11] feat: use setTimeout instead of setInterval for the cleanup mechanism Signed-off-by: nidhal-labidi --- flottform/server/src/database.spec.ts | 6 ++--- flottform/server/src/database.ts | 36 +++++++++++++++------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/flottform/server/src/database.spec.ts b/flottform/server/src/database.spec.ts index 5988079..1222e81 100644 --- a/flottform/server/src/database.spec.ts +++ b/flottform/server/src/database.spec.ts @@ -182,7 +182,7 @@ describe('Flottform database', () => { } it('Should clean up stale entries after entryTTL', async () => { - const db = await createFlottformDatabase(100, 50); + const db = await createFlottformDatabase(1000, 500); const conn = new RTCPeerConnection(); const offer = await conn.createOffer(); const { endpointId } = await db.createEndpoint({ session: offer }); @@ -201,14 +201,14 @@ describe('Flottform database', () => { }); // Sleep for enough time to trigger the first cleanup - await sleep(110); + await sleep(1100); // The endpoint should be cleaned by now expect(async () => await db.getEndpoint({ endpointId })).rejects.toThrow(/endpoint/i); }); it("Shouldn't clean up entries before entryTTL is expired", async () => { - const db = await createFlottformDatabase(100, 50); + const db = await createFlottformDatabase(1000, 500); const conn = new RTCPeerConnection(); const offer = await conn.createOffer(); const { endpointId } = await db.createEndpoint({ session: offer }); diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index c244c3f..b1d33fc 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -26,9 +26,9 @@ function createRandomEndpointId(): string { class FlottformDatabase { private map = new Map(); - private cleanupIntervalId: NodeJS.Timeout | null = null; - private cleanupPeriod; - private entryTTL; // Time-to-Live for each entry + private cleanupTimeoutId: NodeJS.Timeout | null = null; + private cleanupPeriod: number; + private entryTTL: number; // Time-to-Live for each entry constructor(cleanupPeriod = 30 * 60 * 1000, entryTTL = 25 * 60 * 1000) { this.cleanupPeriod = cleanupPeriod; @@ -37,24 +37,28 @@ class FlottformDatabase { } private startCleanup() { - this.cleanupIntervalId = setInterval(() => { - // Loop over all entries and delete the stale ones. - const now = Date.now(); - for (const [endpointId, endpointInfo] of this.map) { - const lastUpdated = endpointInfo.lastUpdate; - if (now - lastUpdated > this.entryTTL) { - this.map.delete(endpointId); - console.log(`Cleaned up stale entry: ${endpointId}`); - } + this.cleanupTimeoutId = setTimeout(this.cleanupFn.bind(this), this.cleanupPeriod); + } + + private cleanupFn() { + if (!this.map || this.map.size === 0) return; + const now = Date.now(); + // Loop over all entries and delete the stale ones. + for (const [endpointId, endpointInfo] of this.map) { + const lastUpdated = endpointInfo.lastUpdate; + if (now - lastUpdated > this.entryTTL) { + this.map.delete(endpointId); + console.log(`Cleaned up stale entry: ${endpointId}`); } - }, this.cleanupPeriod); + } + this.cleanupTimeoutId = setTimeout(this.startCleanup.bind(this), this.cleanupPeriod); } private stopCleanup() { // Clear the interval to stop cleanup - if (this.cleanupIntervalId) { - clearInterval(this.cleanupIntervalId); - this.cleanupIntervalId = null; + if (this.cleanupTimeoutId) { + clearTimeout(this.cleanupTimeoutId); + this.cleanupTimeoutId = null; } } From 9c9b49e97d62fa875034ccec8153920102ae7db7 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Wed, 18 Dec 2024 11:10:59 +0100 Subject: [PATCH 08/11] update the properties of FlottformDatabase, update tests to use fake timers Signed-off-by: nidhal-labidi --- flottform/server/src/database.spec.ts | 13 +++++++----- flottform/server/src/database.ts | 30 +++++++++++++++------------ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/flottform/server/src/database.spec.ts b/flottform/server/src/database.spec.ts index 1222e81..051491d 100644 --- a/flottform/server/src/database.spec.ts +++ b/flottform/server/src/database.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; import { createFlottformDatabase } from './database'; describe('Flottform database', () => { @@ -177,9 +177,12 @@ describe('Flottform database', () => { }); describe('startCleanup()', () => { - function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); it('Should clean up stale entries after entryTTL', async () => { const db = await createFlottformDatabase(1000, 500); @@ -201,7 +204,7 @@ describe('Flottform database', () => { }); // Sleep for enough time to trigger the first cleanup - await sleep(1100); + vi.advanceTimersByTime(1100); // The endpoint should be cleaned by now expect(async () => await db.getEndpoint({ endpointId })).rejects.toThrow(/endpoint/i); diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index b1d33fc..54aea22 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -17,6 +17,9 @@ type EndpointInfo = { }; type SafeEndpointInfo = Omit; +const DEFAULT_CLEANUP_PERIOD = 30 * 60 * 1000; +const DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS = 25 * 60 * 1000; + function createRandomHostKey(): string { return crypto.randomUUID(); } @@ -28,11 +31,11 @@ class FlottformDatabase { private map = new Map(); private cleanupTimeoutId: NodeJS.Timeout | null = null; private cleanupPeriod: number; - private entryTTL: number; // Time-to-Live for each entry + private entryTimeToLive: number; - constructor(cleanupPeriod = 30 * 60 * 1000, entryTTL = 25 * 60 * 1000) { + constructor(cleanupPeriod = DEFAULT_CLEANUP_PERIOD, entryTTL = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS) { this.cleanupPeriod = cleanupPeriod; - this.entryTTL = entryTTL; + this.entryTimeToLive = entryTTL; this.startCleanup(); } @@ -41,14 +44,15 @@ class FlottformDatabase { } private cleanupFn() { - if (!this.map || this.map.size === 0) return; - const now = Date.now(); - // Loop over all entries and delete the stale ones. - for (const [endpointId, endpointInfo] of this.map) { - const lastUpdated = endpointInfo.lastUpdate; - if (now - lastUpdated > this.entryTTL) { - this.map.delete(endpointId); - console.log(`Cleaned up stale entry: ${endpointId}`); + if (this.map && this.map.size !== 0) { + const now = Date.now(); + // Loop over all entries and delete the stale ones. + for (const [endpointId, endpointInfo] of this.map) { + const lastUpdated = endpointInfo.lastUpdate; + if (now - lastUpdated > this.entryTimeToLive) { + this.map.delete(endpointId); + console.log(`Cleaned up stale entry: ${endpointId}`); + } } } this.cleanupTimeoutId = setTimeout(this.startCleanup.bind(this), this.cleanupPeriod); @@ -178,8 +182,8 @@ class FlottformDatabase { } export async function createFlottformDatabase( - cleanupPeriod = 30 * 60 * 1000, - entryTTL = 25 * 60 * 1000 + cleanupPeriod = DEFAULT_CLEANUP_PERIOD, + entryTTL = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS ): Promise { return new FlottformDatabase(cleanupPeriod, entryTTL); } From 742aa3d0b5fca4b3b64bd02ebd164f3c7280c73f Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Wed, 18 Dec 2024 13:41:02 +0100 Subject: [PATCH 09/11] update the constructor of FlottformDatabase and tests Signed-off-by: nidhal-labidi --- flottform/server/src/database.spec.ts | 10 ++++++++-- flottform/server/src/database.ts | 22 +++++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/flottform/server/src/database.spec.ts b/flottform/server/src/database.spec.ts index 051491d..1d96846 100644 --- a/flottform/server/src/database.spec.ts +++ b/flottform/server/src/database.spec.ts @@ -185,7 +185,10 @@ describe('Flottform database', () => { }); it('Should clean up stale entries after entryTTL', async () => { - const db = await createFlottformDatabase(1000, 500); + const db = await createFlottformDatabase({ + cleanupPeriod: 1000, + entryTimeToLive: 500 + }); const conn = new RTCPeerConnection(); const offer = await conn.createOffer(); const { endpointId } = await db.createEndpoint({ session: offer }); @@ -211,7 +214,10 @@ describe('Flottform database', () => { }); it("Shouldn't clean up entries before entryTTL is expired", async () => { - const db = await createFlottformDatabase(1000, 500); + const db = await createFlottformDatabase({ + cleanupPeriod: 1000, + entryTimeToLive: 500 + }); const conn = new RTCPeerConnection(); const offer = await conn.createOffer(); const { endpointId } = await db.createEndpoint({ session: offer }); diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index 54aea22..67c090a 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -33,9 +33,15 @@ class FlottformDatabase { private cleanupPeriod: number; private entryTimeToLive: number; - constructor(cleanupPeriod = DEFAULT_CLEANUP_PERIOD, entryTTL = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS) { + constructor({ + cleanupPeriod = DEFAULT_CLEANUP_PERIOD, + entryTimeToLive = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS + }: { + cleanupPeriod?: number; + entryTimeToLive?: number; + } = {}) { this.cleanupPeriod = cleanupPeriod; - this.entryTimeToLive = entryTTL; + this.entryTimeToLive = entryTimeToLive; this.startCleanup(); } @@ -51,7 +57,6 @@ class FlottformDatabase { const lastUpdated = endpointInfo.lastUpdate; if (now - lastUpdated > this.entryTimeToLive) { this.map.delete(endpointId); - console.log(`Cleaned up stale entry: ${endpointId}`); } } } @@ -181,11 +186,14 @@ class FlottformDatabase { } } -export async function createFlottformDatabase( +export async function createFlottformDatabase({ cleanupPeriod = DEFAULT_CLEANUP_PERIOD, - entryTTL = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS -): Promise { - return new FlottformDatabase(cleanupPeriod, entryTTL); + entryTimeToLive = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS +}: { + cleanupPeriod?: number; + entryTimeToLive?: number; +} = {}): Promise { + return new FlottformDatabase({ cleanupPeriod, entryTimeToLive }); } export type { FlottformDatabase }; From 338636cbc017bdd74035190a67298a411cb0cdf0 Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Tue, 18 Feb 2025 11:13:04 +0100 Subject: [PATCH 10/11] fix: the attribute lastUpdate is being taken care of internally only Signed-off-by: nidhal-labidi --- flottform/server/src/database.spec.ts | 9 ++++---- flottform/server/src/database.ts | 30 ++++++++++++++++----------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/flottform/server/src/database.spec.ts b/flottform/server/src/database.spec.ts index 1d96846..cf37b90 100644 --- a/flottform/server/src/database.spec.ts +++ b/flottform/server/src/database.spec.ts @@ -33,7 +33,8 @@ describe('Flottform database', () => { await db.putHostInfo({ endpointId, hostKey: 'clearly-wrong', - iceCandidates: [] + iceCandidates: [], + session: offer }) ).rejects.toThrow(/hostkey/i); }); @@ -202,8 +203,7 @@ describe('Flottform database', () => { endpointId, clientKey, session: answer, - iceCandidates: [], - lastUpdate: Date.now() + iceCandidates: [] }); // Sleep for enough time to trigger the first cleanup @@ -231,8 +231,7 @@ describe('Flottform database', () => { endpointId, clientKey, session: answer, - iceCandidates: [], - lastUpdate: Date.now() + iceCandidates: [] }); // The endpoint shouldn't be cleaned by now diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index 67c090a..b3ccd21 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -15,7 +15,7 @@ type EndpointInfo = { }; lastUpdate: number; }; -type SafeEndpointInfo = Omit; +type SafeEndpointInfo = Omit; const DEFAULT_CLEANUP_PERIOD = 30 * 60 * 1000; const DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS = 25 * 60 * 1000; @@ -96,7 +96,7 @@ class FlottformDatabase { throw Error('Endpoint not found'); } entry.lastUpdate = Date.now(); - const { hostKey: _ignore1, clientKey: _ignore2, ...endpoint } = entry; + const { hostKey: _ignore1, clientKey: _ignore2, lastUpdate: _ignore3, ...endpoint } = entry; return endpoint; } @@ -105,14 +105,12 @@ class FlottformDatabase { endpointId, hostKey, session, - iceCandidates, - lastUpdate = Date.now() + iceCandidates }: { endpointId: EndpointId; hostKey: HostKey; session: RTCSessionDescriptionInit; iceCandidates: RTCIceCandidateInit[]; - lastUpdate?: number; }): Promise { const existingSession = this.map.get(endpointId); if (!existingSession) { @@ -125,11 +123,16 @@ class FlottformDatabase { const newInfo = { ...existingSession, hostInfo: { ...existingSession.hostInfo, session, iceCandidates }, - lastUpdate + lastUpdate: Date.now() }; this.map.set(endpointId, newInfo); - const { hostKey: _ignore1, clientKey: _ignore2, ...newEndpoint } = newInfo; + const { + hostKey: _ignore1, + clientKey: _ignore2, + lastUpdate: _ignore3, + ...newEndpoint + } = newInfo; return newEndpoint; } @@ -138,14 +141,12 @@ class FlottformDatabase { endpointId, clientKey, session, - iceCandidates, - lastUpdate = Date.now() + iceCandidates }: { endpointId: EndpointId; clientKey: ClientKey; session: RTCSessionDescriptionInit; iceCandidates: RTCIceCandidateInit[]; - lastUpdate?: number; }): Promise> { const existingSession = this.map.get(endpointId); if (!existingSession) { @@ -161,11 +162,16 @@ class FlottformDatabase { ...existingSession, clientKey, clientInfo: { session, iceCandidates }, - lastUpdate + lastUpdate: Date.now() }; this.map.set(endpointId, newInfo); - const { hostKey: _ignore1, clientKey: _ignore2, ...newEndpoint } = newInfo; + const { + hostKey: _ignore1, + clientKey: _ignore2, + lastUpdate: _ignore3, + ...newEndpoint + } = newInfo; return newEndpoint; } From 8852cc54d0e03accbc7d32620a4427635de659dd Mon Sep 17 00:00:00 2001 From: nidhal-labidi Date: Tue, 18 Feb 2025 13:00:44 +0100 Subject: [PATCH 11/11] feat: add a heartbeat function in the host to keep the connection alive Signed-off-by: nidhal-labidi --- flottform/forms/src/flottform-channel-host.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/flottform/forms/src/flottform-channel-host.ts b/flottform/forms/src/flottform-channel-host.ts index 1640107..a510045 100644 --- a/flottform/forms/src/flottform-channel-host.ts +++ b/flottform/forms/src/flottform-channel-host.ts @@ -19,6 +19,7 @@ export class FlottformChannelHost extends EventEmitter { private pollTimeForIceInMs: number; private logger: Logger; + private keepConnectionAliveIntervalId: NodeJS.Timeout | undefined; private state: FlottformState | 'disconnected' = 'new'; private channelNumber: number = 0; private openPeerConnection: RTCPeerConnection | null = null; @@ -65,7 +66,7 @@ export class FlottformChannelHost extends EventEmitter { } try { - this.rtcConfiguration.iceServers = await this.fetchIceServers(this.baseApi); + this.rtcConfiguration.iceServers = await this.fetchIceServers(); } catch (error) { // Use the default configuration as a fallback this.logger.error(error); @@ -108,10 +109,22 @@ export class FlottformChannelHost extends EventEmitter { this.openPeerConnection = null; } this.changeState('disconnected'); + // Stop heartbeat function. + clearInterval(this.keepConnectionAliveIntervalId); // Cleanup old entries. this.deleteEndpoint(this.baseApi, this.endpointId, this.hostKey); }; + private keepConnectionAlive = async (getEndpointInfoUrl: string) => { + this.keepConnectionAliveIntervalId = setInterval( + async () => { + // Make a GET request to refresh the connection + await retrieveEndpointInfo(getEndpointInfoUrl); + }, + 5 * 60 * 1000 + ); + }; + private setupDataChannelListener = () => { if (this.dataChannel == null) { this.changeState( @@ -177,12 +190,18 @@ export class FlottformChannelHost extends EventEmitter { this.logger.info(`onconnectionstatechange - ${this.openPeerConnection!.connectionState}`); if (this.openPeerConnection!.connectionState === 'connected') { this.stopPollingForConnection(); + // Start the heartbeat process + this.keepConnectionAlive(getEndpointInfoUrl); } if (this.openPeerConnection!.connectionState === 'disconnected') { this.startPollingForConnection(getEndpointInfoUrl); + // Stop the hearbeat process + clearInterval(this.keepConnectionAliveIntervalId); } if (this.openPeerConnection!.connectionState === 'failed') { this.stopPollingForConnection(); + // Stop the hearbeat process + clearInterval(this.keepConnectionAliveIntervalId); this.changeState('error', { message: 'connection-failed' }); } }; @@ -243,8 +262,8 @@ export class FlottformChannelHost extends EventEmitter { return response.json(); }; - private fetchIceServers = async (baseApi: string) => { - const response = await fetch(`${baseApi}/ice-server-credentials`, { + private fetchIceServers = async () => { + const response = await fetch(`${this.baseApi}/ice-server-credentials`, { method: 'GET', headers: { Accept: 'application/json'