From dc4fdf4672b0181e0e4c0dd1537685de3cac6aef Mon Sep 17 00:00:00 2001 From: "dogan.ay" <65234588+DayTF@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:05:29 +0100 Subject: [PATCH] fix(sse): detect buffering and warn user (#1256) --- .github/workflows/build.yml | 2 +- .../src/events-subscription/index.ts | 33 ++++++++++- .../test/events-subscription/index.test.ts | 58 ++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8334f5ff3..2c540e4760 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Build doc run: yarn docs - name: Archive documentation artifacts - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: api-reference diff --git a/packages/forestadmin-client/src/events-subscription/index.ts b/packages/forestadmin-client/src/events-subscription/index.ts index e8e1484c30..3fe6b83209 100644 --- a/packages/forestadmin-client/src/events-subscription/index.ts +++ b/packages/forestadmin-client/src/events-subscription/index.ts @@ -10,12 +10,28 @@ import { ForestAdminClientOptionsWithDefaults } from '../types'; export default class EventsSubscriptionService implements BaseEventsSubscriptionService { private eventSource: EventSource; + private heartBeatTimeout: NodeJS.Timeout; constructor( private readonly options: ForestAdminClientOptionsWithDefaults, private readonly refreshEventsHandlerService: RefreshEventsHandlerService, ) {} + private detectBuffering() { + clearTimeout(this.heartBeatTimeout); + + this.heartBeatTimeout = setTimeout(() => { + this.options.logger( + 'Error', + `Unable to detect ServerSentEvents Heartbeat. + Forest Admin uses ServerSentEvents to ensure that permission cache is up to date. + It seems that your agent does not receive events from our server, this may due to buffering of events from your networking infrastructure (reverse proxy). + https://docs.forestadmin.com/developer-guide-agents-nodejs/getting-started/install/troubleshooting#invalid-permissions + `, + ); + }, 45000); + } + async subscribeEvents(): Promise { if (!this.options.instantCacheRefresh) { this.options.logger( @@ -44,10 +60,22 @@ export default class EventsSubscriptionService implements BaseEventsSubscription eventSource.addEventListener('error', this.onEventError.bind(this)); // Only listen after first open - eventSource.once('open', () => - eventSource.addEventListener('open', () => this.onEventOpenAgain()), + eventSource.addEventListener( + 'open', + () => eventSource.addEventListener('open', () => this.onEventOpenAgain()), + { once: true }, ); + eventSource.addEventListener( + 'heartbeat', + () => { + clearTimeout(this.heartBeatTimeout); + }, + { once: true }, + ); + + this.detectBuffering(); + eventSource.addEventListener(ServerEventType.RefreshUsers, async () => this.refreshEventsHandlerService.refreshUsers(), ); @@ -71,6 +99,7 @@ export default class EventsSubscriptionService implements BaseEventsSubscription * Close the current EventSource */ public close() { + clearTimeout(this.heartBeatTimeout); this.eventSource?.close(); } diff --git a/packages/forestadmin-client/test/events-subscription/index.test.ts b/packages/forestadmin-client/test/events-subscription/index.test.ts index 1ca0ea51e3..2267d2bee7 100644 --- a/packages/forestadmin-client/test/events-subscription/index.test.ts +++ b/packages/forestadmin-client/test/events-subscription/index.test.ts @@ -35,7 +35,11 @@ describe('EventsSubscriptionService', () => { eventsSubscriptionService.subscribeEvents(); expect(addEventListener).toHaveBeenCalledWith('error', expect.any(Function)); - expect(once).toHaveBeenCalledWith('open', expect.any(Function)); + expect(addEventListener).toHaveBeenCalledWith('open', expect.any(Function), { once: true }); + + expect(addEventListener).toHaveBeenCalledWith('heartbeat', expect.any(Function), { + once: true, + }); expect(addEventListener).toHaveBeenCalledWith( ServerEventType.RefreshUsers, @@ -103,7 +107,59 @@ describe('EventsSubscriptionService', () => { }); }); + describe('detectBuffering', () => { + test('should log an error after the timeout', () => { + const spy = jest.spyOn(global, 'setTimeout'); + const eventsSubscriptionService = new EventsSubscriptionService( + options, + refreshEventsHandlerService, + ); + eventsSubscriptionService.subscribeEvents(); + + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith(expect.any(Function), 45000); + + const callback = spy.mock.calls[0][0]; + + callback(); + + expect(options.logger).toHaveBeenCalledWith( + 'Error', + `Unable to detect ServerSentEvents Heartbeat. + Forest Admin uses ServerSentEvents to ensure that permission cache is up to date. + It seems that your agent does not receive events from our server, this may due to buffering of events from your networking infrastructure (reverse proxy). + https://docs.forestadmin.com/developer-guide-agents-nodejs/getting-started/install/troubleshooting#invalid-permissions + `, + ); + + jest.clearAllMocks(); + }); + }); + describe('handleSeverEvents', () => { + describe('on Heartbeat', () => { + test('should clear heartbeat timeout', () => { + const eventsSubscriptionService = new EventsSubscriptionService( + options, + refreshEventsHandlerService, + ); + eventsSubscriptionService.subscribeEvents(); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line no-underscore-dangle + expect(eventsSubscriptionService.heartBeatTimeout._destroyed).toBeFalsy(); + + // eslint-disable-next-line @typescript-eslint/dot-notation + events['heartbeat']({}); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line no-underscore-dangle + expect(eventsSubscriptionService.heartBeatTimeout._destroyed).toBeTruthy(); + }); + }); + describe('on RefreshUsers event', () => { test('should delegate to refreshEventsHandlerService', () => { const eventsSubscriptionService = new EventsSubscriptionService(