Skip to content

Commit 059e2dc

Browse files
committed
fix(core): Fix client hook edge cases around multiple callbacks
1 parent 3c76c5d commit 059e2dc

File tree

2 files changed

+75
-14
lines changed

2 files changed

+75
-14
lines changed

packages/core/src/client.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
137137
private _outcomes: { [key: string]: number };
138138

139139
// eslint-disable-next-line @typescript-eslint/ban-types
140-
private _hooks: Record<string, Function[]>;
140+
private _hooks: Record<string, Set<Function>>;
141141

142142
/**
143143
* Initializes this client instance.
@@ -685,21 +685,23 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
685685
* Register a hook on this client.
686686
*/
687687
public on(hook: string, callback: unknown): () => void {
688-
const hooks = (this._hooks[hook] = this._hooks[hook] || []);
688+
const hookCallbacks = (this._hooks[hook] = this._hooks[hook] || new Set());
689689

690-
// @ts-expect-error We assume the types are correct
691-
hooks.push(callback);
690+
// Wrap the callback in a function so that registering the same callback instance multiple
691+
// times results in the callback being called multiple times.
692+
// @ts-expect-error - The `callback` type is correct and must be a function due to the
693+
// individual, specific overloads of this function.
694+
// eslint-disable-next-line @typescript-eslint/ban-types
695+
const uniqueCallback: Function = (...args: unknown[]) => callback(...args);
696+
697+
hookCallbacks.add(uniqueCallback);
692698

693699
// This function returns a callback execution handler that, when invoked,
694700
// deregisters a callback. This is crucial for managing instances where callbacks
695701
// need to be unregistered to prevent self-referencing in callback closures,
696702
// ensuring proper garbage collection.
697703
return () => {
698-
// @ts-expect-error We assume the types are correct
699-
const cbIndex = hooks.indexOf(callback);
700-
if (cbIndex > -1) {
701-
hooks.splice(cbIndex, 1);
702-
}
704+
hookCallbacks.delete(uniqueCallback);
703705
};
704706
}
705707

packages/core/test/lib/client.test.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,10 +2397,8 @@ describe('Client', () => {
23972397

23982398
client.emit('beforeEnvelope', mockEnvelope);
23992399
});
2400-
});
24012400

2402-
describe('hook removal with `on`', () => {
2403-
it('should return a cleanup function that, when executed, unregisters a hook', async () => {
2401+
it('returns a cleanup function that, when executed, unregisters a hook', async () => {
24042402
vi.useFakeTimers();
24052403
expect.assertions(8);
24062404

@@ -2420,7 +2418,7 @@ describe('Client', () => {
24202418
const callback = vi.fn();
24212419
const removeAfterSendEventListenerFn = client.on('afterSendEvent', callback);
24222420

2423-
expect(client['_hooks']['afterSendEvent']).toEqual([callback]);
2421+
expect(client['_hooks']['afterSendEvent']!.size).toBe(1);
24242422

24252423
client.sendEvent(errorEvent);
24262424
vi.runAllTimers();
@@ -2435,7 +2433,7 @@ describe('Client', () => {
24352433

24362434
// Should unregister `afterSendEvent` callback.
24372435
removeAfterSendEventListenerFn();
2438-
expect(client['_hooks']['afterSendEvent']).toEqual([]);
2436+
expect(client['_hooks']['afterSendEvent']!.size).toBe(0);
24392437

24402438
client.sendEvent(errorEvent);
24412439
vi.runAllTimers();
@@ -2450,6 +2448,67 @@ describe('Client', () => {
24502448
expect(callback).toBeCalledTimes(1);
24512449
expect(callback).toBeCalledWith(errorEvent, { statusCode: 200 });
24522450
});
2451+
2452+
it('allows synchronously unregistering multiple callbacks from within the callback', () => {
2453+
const client = new TestClient(
2454+
getDefaultTestClientOptions({
2455+
dsn: PUBLIC_DSN,
2456+
enableSend: true,
2457+
}),
2458+
);
2459+
2460+
const callback1 = vi.fn();
2461+
const callback2 = vi.fn();
2462+
2463+
const removeCallback1 = client.on('close', () => {
2464+
callback1();
2465+
removeCallback1();
2466+
});
2467+
const removeCallback2 = client.on('close', () => {
2468+
callback2();
2469+
removeCallback2();
2470+
});
2471+
2472+
client.emit('close');
2473+
2474+
expect(callback1).toHaveBeenCalledTimes(1);
2475+
expect(callback2).toHaveBeenCalledTimes(1);
2476+
2477+
callback1.mockReset();
2478+
callback2.mockReset();
2479+
2480+
client.emit('close');
2481+
2482+
expect(callback1).toHaveBeenCalledTimes(0);
2483+
expect(callback2).toHaveBeenCalledTimes(0);
2484+
});
2485+
2486+
it('allows registering and unregistering the same callback multiple times', () => {
2487+
const client = new TestClient(options);
2488+
const callback = vi.fn();
2489+
2490+
const unregister1 = client.on('close', callback);
2491+
const unregister2 = client.on('close', callback);
2492+
2493+
client.emit('close');
2494+
2495+
expect(callback).toHaveBeenCalledTimes(2);
2496+
2497+
unregister1();
2498+
2499+
callback.mockReset();
2500+
2501+
client.emit('close');
2502+
2503+
expect(callback).toHaveBeenCalledTimes(1);
2504+
2505+
unregister2();
2506+
2507+
callback.mockReset();
2508+
client.emit('close');
2509+
2510+
expect(callback).toHaveBeenCalledTimes(0);
2511+
});
24532512
});
24542513

24552514
describe('withMonitor', () => {

0 commit comments

Comments
 (0)