Implemented SSE subscription mechanism on the frontend (#17017)

This PR is a follow-up of https://github.com/twentyhq/twenty/pull/16966
and implements a new mechanism to handle SSE events.

It creates only one event stream per browser tab, then use mutations to
tell the backend which query to listen to, without re-mounting the event
stream connexion.

Then each event that comes from this unique subscription is then
dispatched in a new JavaScript CustomEvent, per queryId, on which
specific hooks add an event listener.

This PR introduces the generic tooling as well as the handling of update
events on table.
This commit is contained in:
Lucas Bordeau
2026-01-12 18:58:46 +01:00
committed by GitHub
parent d7638a9075
commit 04a370e043
38 changed files with 747 additions and 300 deletions
@@ -0,0 +1,92 @@
import { compareArraysOfObjectsByProperty } from '@/utils/array/compareArraysOfObjectsByProperty';
type TestObject = {
id: string;
name: string;
};
describe('compareArraysOfObjectsByProperty', () => {
it('should return false when both arrays are empty', () => {
expect(compareArraysOfObjectsByProperty([], [], 'id')).toBe(false);
});
it('should return false when arrays have same objects by property', () => {
const arrayA: TestObject[] = [
{ id: '1', name: 'Test 1' },
{ id: '2', name: 'Test 2' },
];
const arrayB: TestObject[] = [
{ id: '1', name: 'Different Name' },
{ id: '2', name: 'Another Name' },
];
expect(compareArraysOfObjectsByProperty(arrayA, arrayB, 'id')).toBe(false);
});
it('should return true when arrays have different lengths', () => {
const arrayA: TestObject[] = [
{ id: '1', name: 'Test 1' },
{ id: '2', name: 'Test 2' },
];
const arrayB: TestObject[] = [{ id: '1', name: 'Test 1' }];
expect(compareArraysOfObjectsByProperty(arrayA, arrayB, 'id')).toBe(true);
});
it('should return true when arrayA has items not in arrayB', () => {
const arrayA: TestObject[] = [
{ id: '1', name: 'Test 1' },
{ id: '2', name: 'Test 2' },
];
const arrayB: TestObject[] = [
{ id: '1', name: 'Test 1' },
{ id: '3', name: 'Test 3' },
];
expect(compareArraysOfObjectsByProperty(arrayA, arrayB, 'id')).toBe(true);
});
it('should return true when arrayB has items not in arrayA', () => {
const arrayA: TestObject[] = [
{ id: '1', name: 'Test 1' },
{ id: '3', name: 'Test 3' },
];
const arrayB: TestObject[] = [
{ id: '1', name: 'Test 1' },
{ id: '2', name: 'Test 2' },
];
expect(compareArraysOfObjectsByProperty(arrayA, arrayB, 'id')).toBe(true);
});
it('should return false when arrays have same items in different order', () => {
const arrayA: TestObject[] = [
{ id: '1', name: 'Test 1' },
{ id: '2', name: 'Test 2' },
{ id: '3', name: 'Test 3' },
];
const arrayB: TestObject[] = [
{ id: '3', name: 'Test 3' },
{ id: '1', name: 'Test 1' },
{ id: '2', name: 'Test 2' },
];
expect(compareArraysOfObjectsByProperty(arrayA, arrayB, 'id')).toBe(false);
});
it('should compare by the specified property', () => {
const arrayA: TestObject[] = [
{ id: '1', name: 'Alpha' },
{ id: '2', name: 'Beta' },
];
const arrayB: TestObject[] = [
{ id: '3', name: 'Alpha' },
{ id: '4', name: 'Beta' },
];
expect(compareArraysOfObjectsByProperty(arrayA, arrayB, 'name')).toBe(
false,
);
expect(compareArraysOfObjectsByProperty(arrayA, arrayB, 'id')).toBe(true);
});
});
@@ -0,0 +1,15 @@
export const compareArraysOfObjectsByProperty = <T, K extends keyof T>(
arrayA: T[],
arrayB: T[],
property: K,
) => {
return (
arrayA.length !== arrayB.length ||
arrayA.some(
(item) => !arrayB.some((itemB) => itemB[property] === item[property]),
) ||
arrayB.some(
(item) => !arrayA.some((itemA) => itemA[property] === item[property]),
)
);
};