Skip to content

Commit

Permalink
fix(sse): detect buffering and warn user (#1256)
Browse files Browse the repository at this point in the history
  • Loading branch information
DayTF authored Jan 31, 2025
1 parent 80c8c8a commit dc4fdf4
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 31 additions & 2 deletions packages/forestadmin-client/src/events-subscription/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
if (!this.options.instantCacheRefresh) {
this.options.logger(
Expand Down Expand Up @@ -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(),
);
Expand All @@ -71,6 +99,7 @@ export default class EventsSubscriptionService implements BaseEventsSubscription
* Close the current EventSource
*/
public close() {
clearTimeout(this.heartBeatTimeout);
this.eventSource?.close();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit dc4fdf4

Please sign in to comment.