diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 745c128c10..20152d4c62 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -77,6 +77,16 @@ export default class Agent extends FrameworkMounter await this.mount(router); } + /** + * Stop the agent. + */ + override async stop(): Promise { + // Close anything related to ForestAdmin client + this.options.forestAdminClient.close(); + // Stop at framework level + await super.stop(); + } + /** * Restart the agent at runtime (remount routes). */ diff --git a/packages/agent/src/framework-mounter.ts b/packages/agent/src/framework-mounter.ts index 4ebb2a9b9b..403bc7d34b 100644 --- a/packages/agent/src/framework-mounter.ts +++ b/packages/agent/src/framework-mounter.ts @@ -34,7 +34,7 @@ export default class FrameworkMounter { for (const task of this.onEachStart) await task(router); // eslint-disable-line no-await-in-loop } - async stop(): Promise { + public async stop(): Promise { for (const task of this.onStop) await task(); // eslint-disable-line no-await-in-loop } diff --git a/packages/agent/test/__factories__/forest-admin-client.ts b/packages/agent/test/__factories__/forest-admin-client.ts index 4d23a0f269..1a03f0467e 100644 --- a/packages/agent/test/__factories__/forest-admin-client.ts +++ b/packages/agent/test/__factories__/forest-admin-client.ts @@ -38,6 +38,7 @@ const forestAdminClientFactory = ForestAdminClientFactory.define(() => ({ getConfiguration: jest.fn(), }, subscribeToServerEvents: jest.fn(), + close: jest.fn(), onRefreshCustomizations: jest.fn(), })); diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 3a5909969e..d53844844e 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -280,4 +280,15 @@ describe('Agent', () => { expect(mockPostSchema).not.toHaveBeenCalled(); }); }); + + describe('stop', () => { + test('stop should close the Forest Admin client', async () => { + const options = factories.forestAdminHttpDriverOptions.build(); + const agent = new Agent(options); + + await agent.stop(); + + expect(options.forestAdminClient.close).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/forestadmin-client/src/events-subscription/index.ts b/packages/forestadmin-client/src/events-subscription/index.ts index d797144a27..135fef585f 100644 --- a/packages/forestadmin-client/src/events-subscription/index.ts +++ b/packages/forestadmin-client/src/events-subscription/index.ts @@ -9,6 +9,8 @@ import { import { ForestAdminClientOptionsWithDefaults } from '../types'; export default class EventsSubscriptionService implements BaseEventsSubscriptionService { + private eventSource: EventSource; + constructor( private readonly options: ForestAdminClientOptionsWithDefaults, private readonly refreshEventsHandlerService: RefreshEventsHandlerService, @@ -35,30 +37,41 @@ export default class EventsSubscriptionService implements BaseEventsSubscription }; const url = new URL('/liana/v4/subscribe-to-events', this.options.forestServerUrl).toString(); - const source = new EventSource(url, eventSourceConfig); + const eventSource = new EventSource(url, eventSourceConfig); // Override reconnect interval to 5 seconds - source.reconnectInterval = 5000; + eventSource.reconnectInterval = 5000; - source.addEventListener('error', this.onEventError.bind(this)); + eventSource.addEventListener('error', this.onEventError.bind(this)); // Only listen after first open - source.once('open', () => source.addEventListener('open', () => this.onEventOpenAgain())); + eventSource.once('open', () => + eventSource.addEventListener('open', () => this.onEventOpenAgain()), + ); - source.addEventListener(ServerEventType.RefreshUsers, async () => + eventSource.addEventListener(ServerEventType.RefreshUsers, async () => this.refreshEventsHandlerService.refreshUsers(), ); - source.addEventListener(ServerEventType.RefreshRoles, async () => + eventSource.addEventListener(ServerEventType.RefreshRoles, async () => this.refreshEventsHandlerService.refreshRoles(), ); - source.addEventListener(ServerEventType.RefreshRenderings, async (event: ServerEvent) => + eventSource.addEventListener(ServerEventType.RefreshRenderings, async (event: ServerEvent) => this.handleSeverEventRefreshRenderings(event), ); - source.addEventListener(ServerEventType.RefreshCustomizations, async () => + eventSource.addEventListener(ServerEventType.RefreshCustomizations, async () => this.refreshEventsHandlerService.refreshCustomizations(), ); + + this.eventSource = eventSource; + } + + /** + * Close the current EventSource + */ + public close() { + this.eventSource?.close(); } private async handleSeverEventRefreshRenderings(event: ServerEvent) { diff --git a/packages/forestadmin-client/src/events-subscription/types.ts b/packages/forestadmin-client/src/events-subscription/types.ts index 960b2838dd..5cec9452c6 100644 --- a/packages/forestadmin-client/src/events-subscription/types.ts +++ b/packages/forestadmin-client/src/events-subscription/types.ts @@ -25,4 +25,5 @@ export interface RefreshEventsHandlerService { */ export interface BaseEventsSubscriptionService { subscribeEvents(): Promise; + close(): void; } diff --git a/packages/forestadmin-client/src/forest-admin-client-with-cache.ts b/packages/forestadmin-client/src/forest-admin-client-with-cache.ts index 0230ba4cd4..a94ce9ff56 100644 --- a/packages/forestadmin-client/src/forest-admin-client-with-cache.ts +++ b/packages/forestadmin-client/src/forest-admin-client-with-cache.ts @@ -82,6 +82,10 @@ export default class ForestAdminClientWithCache implements ForestAdminClient { await this.eventsSubscription.subscribeEvents(); } + public close() { + this.eventsSubscription.close(); + } + public onRefreshCustomizations(handler: () => void | Promise) { this.eventsHandler.onRefreshCustomizations(handler); } diff --git a/packages/forestadmin-client/src/types.ts b/packages/forestadmin-client/src/types.ts index fcc303c7f7..0f2c8fc4cf 100644 --- a/packages/forestadmin-client/src/types.ts +++ b/packages/forestadmin-client/src/types.ts @@ -55,6 +55,7 @@ export interface ForestAdminClient { markScopesAsUpdated(renderingId: number | string): void; subscribeToServerEvents(): Promise; + close(): void; onRefreshCustomizations(handler: () => void | Promise): void; } diff --git a/packages/forestadmin-client/test/__factories__/events-subscription/index.ts b/packages/forestadmin-client/test/__factories__/events-subscription/index.ts index 8c91c63326..ce4e517688 100644 --- a/packages/forestadmin-client/test/__factories__/events-subscription/index.ts +++ b/packages/forestadmin-client/test/__factories__/events-subscription/index.ts @@ -8,6 +8,7 @@ export class EventsSubscriptionServiceFactory extends Factory { service.subscribeEvents = jest.fn(); + service.close = jest.fn(); }); } } diff --git a/packages/forestadmin-client/test/events-subscription/index.test.ts b/packages/forestadmin-client/test/events-subscription/index.test.ts index 59e895b23c..1d2ecaffce 100644 --- a/packages/forestadmin-client/test/events-subscription/index.test.ts +++ b/packages/forestadmin-client/test/events-subscription/index.test.ts @@ -11,11 +11,14 @@ const once = jest.fn((event, callback) => { events[event] = callback; }); +const close = jest.fn(); + jest.mock('eventsource', () => ({ __esModule: true, default: jest.fn().mockImplementation(() => ({ addEventListener, once, + close, })), })); @@ -66,6 +69,40 @@ describe('EventsSubscriptionService', () => { }); }); + describe('close', () => { + test('should close current Event Source', async () => { + const eventsSubscriptionService = factories.eventsSubscription.build(); + eventsSubscriptionService.subscribeEvents(); + + eventsSubscriptionService.close(); + + expect(close).toHaveBeenCalled(); + }); + + describe('when server events are deactivated', () => { + test('should not do anything', async () => { + const eventsSubscriptionService = new EventsSubscriptionService( + { ...options, instantCacheRefresh: false }, + refreshEventsHandlerService, + ); + + eventsSubscriptionService.close(); + + expect(close).not.toHaveBeenCalled(); + }); + }); + + describe('when no event source instantiated yet', () => { + test('should not do anything', async () => { + const eventsSubscriptionService = factories.eventsSubscription.build(); + + eventsSubscriptionService.close(); + + expect(close).not.toHaveBeenCalled(); + }); + }); + }); + describe('handleSeverEvents', () => { describe('on RefreshUsers event', () => { test('should delegate to refreshEventsHandlerService', () => { diff --git a/packages/forestadmin-client/test/forest-admin-client-with-cache.test.ts b/packages/forestadmin-client/test/forest-admin-client-with-cache.test.ts index df557c1341..36ff48586c 100644 --- a/packages/forestadmin-client/test/forest-admin-client-with-cache.test.ts +++ b/packages/forestadmin-client/test/forest-admin-client-with-cache.test.ts @@ -232,7 +232,7 @@ describe('ForestAdminClientWithCache', () => { }); describe('subscribeToServerEvents', () => { - it('should subscribes to Server Events rendering service', async () => { + it('should subscribes to Server Events service', async () => { const eventsSubscriptionService = factories.eventsSubscription.mockAllMethods().build(); const forestAdminClient = new ForestAdminClient( factories.forestAdminClientOptions.build(), @@ -254,8 +254,31 @@ describe('ForestAdminClientWithCache', () => { }); }); + describe('close', () => { + it('should close', () => { + const eventsSubscriptionService = factories.eventsSubscription.mockAllMethods().build(); + const forestAdminClient = new ForestAdminClient( + factories.forestAdminClientOptions.build(), + factories.permission.build(), + factories.renderingPermission.build(), + factories.contextVariablesInstantiator.build(), + factories.chartHandler.build(), + factories.ipWhiteList.build(), + factories.schema.build(), + factories.auth.build(), + factories.modelCustomization.build(), + eventsSubscriptionService, + factories.eventsHandler.build(), + ); + + forestAdminClient.close(); + + expect(eventsSubscriptionService.close).toHaveBeenCalledWith(); + }); + }); + describe('onRefreshCustomizations', () => { - it('should subscribes to Server Events rendering service', async () => { + it('should subscribes the handler to the eventsHandler service', async () => { const eventsHandlerService = factories.eventsHandler.mockAllMethods().build(); const forestAdminClient = new ForestAdminClient( factories.forestAdminClientOptions.build(),