Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions packages/core/src/ClientSocketManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,157 @@ describe("ClientSocketManager: unit tests", () => {
expect(socketManager.connect).not.toHaveBeenCalled();
expect(socketManager.disconnect).not.toHaveBeenCalled();
});

describe("with visibility change configuration", () => {
it("should not connect when connectOnVisible is false", () => {
const customSocketManager = new ClientSocketManager("fakeUrl", {
visibilityChange: {
connectOnVisible: false,
},
eventHandlers: {
onVisiblePage: vi.fn(),
},
});

// Mock connect/disconnect to avoid real socket calls
customSocketManager.connect = vi.fn() as any;
customSocketManager.disconnect = vi.fn() as any;

Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "visible",
});

Object.defineProperty(customSocketManager, "connected", {
configurable: true,
value: false,
});

// @ts-expect-error private method access for testing
customSocketManager._handleVisibilityChange();

const { onVisiblePage } = customSocketManager["_inputListeners"];

expect(onVisiblePage).toHaveBeenCalledTimes(1);
expect(customSocketManager.connect).not.toHaveBeenCalled();
expect(customSocketManager.disconnect).not.toHaveBeenCalled();
});

it("should not disconnect when disconnectOnHidden is false", () => {
const customSocketManager = new ClientSocketManager("fakeUrl", {
visibilityChange: {
disconnectOnHidden: false,
},
eventHandlers: {
onHiddenPage: vi.fn(),
},
});

// Mock connect/disconnect to avoid real socket calls
customSocketManager.connect = vi.fn() as any;
customSocketManager.disconnect = vi.fn() as any;

Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "hidden",
});

// @ts-expect-error private method access for testing
customSocketManager._handleVisibilityChange();

const { onHiddenPage } = customSocketManager["_inputListeners"];

expect(onHiddenPage).toHaveBeenCalledTimes(1);
expect(customSocketManager.disconnect).not.toHaveBeenCalled();
expect(customSocketManager.connect).not.toHaveBeenCalled();
});

it("should use default behavior when visibilityChange is not provided", () => {
const defaultSocketManager = new ClientSocketManager("fakeUrl", {
eventHandlers: {
onVisiblePage: vi.fn(),
onHiddenPage: vi.fn(),
},
});

// Mock connect/disconnect to avoid real socket calls
defaultSocketManager.connect = vi.fn() as any;
defaultSocketManager.disconnect = vi.fn() as any;

// Test visible page behavior (should connect by default)
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "visible",
});

Object.defineProperty(defaultSocketManager, "connected", {
configurable: true,
value: false,
});

// @ts-expect-error private method access for testing
defaultSocketManager._handleVisibilityChange();

expect(defaultSocketManager.connect).toHaveBeenCalledTimes(1);

// Reset and test hidden page behavior (should disconnect by default)
const connectMock = defaultSocketManager.connect as any;
connectMock.mockClear();
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "hidden",
});

// @ts-expect-error private method access for testing
defaultSocketManager._handleVisibilityChange();

expect(defaultSocketManager.disconnect).toHaveBeenCalledTimes(1);
});

it("should work with both options set to false", () => {
const customSocketManager = new ClientSocketManager("fakeUrl", {
visibilityChange: {
connectOnVisible: false,
disconnectOnHidden: false,
},
eventHandlers: {
onVisiblePage: vi.fn(),
onHiddenPage: vi.fn(),
},
});

// Mock connect/disconnect to avoid real socket calls
customSocketManager.connect = vi.fn() as any;
customSocketManager.disconnect = vi.fn() as any;

// Test visible page
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "visible",
});

Object.defineProperty(customSocketManager, "connected", {
configurable: true,
value: false,
});

// @ts-expect-error private method access for testing
customSocketManager._handleVisibilityChange();

expect(customSocketManager.connect).not.toHaveBeenCalled();

// Test hidden page
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "hidden",
});

// @ts-expect-error private method access for testing
customSocketManager._handleVisibilityChange();

expect(customSocketManager.disconnect).not.toHaveBeenCalled();
});
});
});

it("should call warnDisposedClient and return correct recovered state", async () => {
Expand Down
20 changes: 18 additions & 2 deletions packages/core/src/ClientSocketManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,31 @@ class ClientSocketManager<

private _inputListeners: ClientSocketManagerListenerOptions = {};

private _connectOnVisible: boolean;
private _disconnectOnHidden: boolean;

constructor(uri: string, options?: ClientSocketManagerOptions) {
const {
path = "/socket.io",
reconnectionDelay = 500,
reconnectionDelayMax = 2000,
eventHandlers,
devtool: devtoolOpt,
visibilityChange: visibilityChangeOpt,
...restOptions
} = options ?? {};

const { enabled: devtoolEnabled = false, zIndex: devtoolZIndex = 999999 } =
devtoolOpt ?? {};

const {
connectOnVisible = true,
disconnectOnHidden = true,
} = visibilityChangeOpt ?? {};

this._connectOnVisible = connectOnVisible;
this._disconnectOnHidden = disconnectOnHidden;

try {
this._socket = io(uri, {
...restOptions,
Expand Down Expand Up @@ -206,11 +218,15 @@ class ClientSocketManager<
if (isPageVisible) {
this._inputListeners.onVisiblePage?.call(this);

if (!this.connected) this.connect();
if (this._connectOnVisible && !this.connected) {
this.connect();
}
} else {
this._inputListeners.onHiddenPage?.call(this);

this.disconnect();
if (this._disconnectOnHidden) {
this.disconnect();
}
}
}

Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,24 @@ export type ClientSocketManagerOptions = OverrideMembers<
*/
zIndex?: number;
};
/**
* Page visibility change behavior options.
*
* These options control how the socket connection behaves when the page
* visibility changes (e.g., when switching tabs or minimizing the browser).
*/
visibilityChange?: {
/**
* Whether to automatically connect when the page becomes visible.
*
* @default true
*/
connectOnVisible?: boolean;
/**
* Whether to automatically disconnect when the page becomes hidden.
*
* @default true
*/
disconnectOnHidden?: boolean;
};
};