Skip to content

Commit 1606253

Browse files
1cedrussinzii
andauthored
feat: optimize useWatchContractEvent (#64)
* tweak: optimize useWatchContractEvent * tweak: change the way handle listener * test: update unit tests * chore: add docstring & update import * tweak: use eventemitter to handle listeners instead * chore: add eventemitter3 * test: sync tests * refactor: use EventEmitter from @dedot/utils * refactor: better handler for whether emit or not system.events * refactor: ensure op stop when component unmount * refactor: standardize subscribing to internal events\ * fix: wrong import * refactor: add type suggestion for useWatchInternalEvent * refactoring & renaming * fix deps --------- Co-authored-by: Thang X. Vu <thang@coongcrafts.io>
1 parent 95246a9 commit 1606253

File tree

11 files changed

+272
-88
lines changed

11 files changed

+272
-88
lines changed

packages/typink/src/hooks/__tests__/test-utils.ts renamed to packages/typink/src/hooks/__tests__/test-utils.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { act } from '@testing-library/react';
2+
import { Props } from '../../types.js';
3+
import { TypinkEventsProvider } from '../../providers/index.js';
24

35
export const waitForNextUpdate = async (then?: () => Promise<void>) => {
46
await act(async () => {
@@ -10,3 +12,5 @@ export const waitForNextUpdate = async (then?: () => Promise<void>) => {
1012
export const sleep = (ms: number = 0) => {
1113
return new Promise((resolve) => setTimeout(resolve, ms));
1214
};
15+
16+
export const typinkEventsWrapper = ({ children }: Props) => <TypinkEventsProvider>{children}</TypinkEventsProvider>;

packages/typink/src/hooks/__tests__/usePSP22Balance.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useRawContract } from '../useRawContract.js';
55
import { useContractQuery } from '../useContractQuery.js';
66
import { useWatchContractEvent } from '../useWatchContractEvent.js';
77
import { renderHook, waitFor } from '@testing-library/react';
8-
import { waitForNextUpdate } from './test-utils';
8+
import { waitForNextUpdate } from './test-utils.js';
99

1010
vi.mock('../useTypink');
1111
vi.mock('../useWatchContractEvent');
Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,127 @@
1-
import { renderHook } from '@testing-library/react-hooks';
2-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { renderHook, waitFor } from '@testing-library/react';
32
import { useWatchContractEvent } from '../useWatchContractEvent.js';
43
import { useTypink } from '../useTypink.js';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
55
import { Contract } from 'dedot/contracts';
66
import { Psp22ContractApi } from '../psp22/contracts/psp22';
7-
import { waitForNextUpdate } from './test-utils.js';
7+
import { useTypinkEvents, useClient } from '../../providers/index.js';
8+
import { typinkEventsWrapper, waitForNextUpdate } from './test-utils.js';
9+
import type { ContractEvent } from 'dedot/contracts';
810

911
vi.mock('../useTypink', () => ({
1012
useTypink: vi.fn(),
1113
}));
1214

15+
vi.mock('../../providers/ClientProvider.js', () => ({
16+
useClient: vi.fn(),
17+
}));
18+
1319
describe('useWatchContractEvent', () => {
14-
const client = {
20+
const mockClient = {
1521
query: {
1622
system: {
1723
events: vi.fn(),
1824
},
1925
},
2026
};
2127

22-
const contract: Contract<Psp22ContractApi> = {
28+
const mockSub = vi.fn();
29+
30+
const mockContract = {
2331
events: {
2432
Transfer: {
25-
filter: vi.fn(),
33+
filter: (events: ContractEvent[]) => events.filter((event) => event.name === 'Transfer'),
2634
},
2735
},
28-
} as unknown as Contract<any>;
29-
30-
const mockUseTypink = {
31-
client,
32-
};
36+
} as any as Contract<Psp22ContractApi>;
3337

3438
beforeEach(() => {
35-
vi.mocked(useTypink).mockReturnValue(mockUseTypink as any);
39+
vi.mocked(useClient).mockReturnValue({
40+
client: mockClient,
41+
} as any);
42+
43+
vi.mocked(useTypink).mockReturnValue({
44+
client: mockClient,
45+
subscribeToEvent: mockSub,
46+
} as any);
3647
});
3748

3849
afterEach(() => {
3950
vi.clearAllMocks();
4051
});
4152

42-
it('should not call system.events if client is not defined', () => {
43-
vi.mocked(useTypink).mockReturnValue({ client: undefined } as any);
53+
it('should successfully subscribe to the system events when all parameters are valid', async () => {
54+
const { rerender } = renderHook(
55+
({ enabled }) => useWatchContractEvent(mockContract, 'Transfer', vi.fn(), enabled),
56+
{ initialProps: { enabled: true } },
57+
);
58+
59+
expect(mockSub).toHaveBeenCalledTimes(1);
4460

45-
renderHook(() => useWatchContractEvent(contract, 'Transfer', vi.fn()));
61+
rerender({ enabled: false });
62+
expect(mockSub).toHaveBeenCalledTimes(1);
4663

47-
expect(client.query.system.events).not.toHaveBeenCalled();
64+
rerender({ enabled: true });
65+
expect(mockSub).toHaveBeenCalledTimes(2);
4866
});
4967

50-
it('should not call system.events if contract is not defined', () => {
51-
renderHook(() =>
52-
// @ts-ignore
53-
useWatchContractEvent(undefined, 'Transfer', vi.fn()),
54-
);
68+
it('should call the callback function when new events are detected', async () => {
69+
const mockApprovalEvent = {
70+
name: 'Approval',
71+
data: {},
72+
};
73+
74+
const mockTransferEvent = {
75+
name: 'Transfer',
76+
data: {},
77+
};
78+
79+
mockClient.query.system.events.mockImplementation((callback) => {
80+
return new Promise((resolve) => {
81+
setTimeout(() => {
82+
callback([mockTransferEvent]);
83+
setTimeout(() => {
84+
callback([mockTransferEvent]);
85+
callback([mockApprovalEvent]);
86+
}, 50);
87+
}, 100);
88+
89+
resolve(() => {});
90+
});
91+
});
5592

56-
expect(client.query.system.events).not.toHaveBeenCalled();
57-
});
93+
const { result } = renderHook(() => useTypinkEvents(), { wrapper: typinkEventsWrapper });
5894

59-
it('should not call system.events if enabled is false', () => {
60-
renderHook(() => useWatchContractEvent(contract, 'Transfer', vi.fn(), false));
95+
await waitFor(() => {
96+
expect(result.current).toBeDefined();
97+
});
6198

62-
expect(client.query.system.events).not.toHaveBeenCalled();
63-
});
99+
mockSub.mockImplementation(result.current.subscribeToEvent);
64100

65-
it('should call system.events if all conditions are met', async () => {
66-
const onNewEvent = vi.fn();
67-
const events = [{ data: { from: 'address1', to: 'address2' } }];
101+
const mockCallback = vi.fn();
102+
renderHook(() => useWatchContractEvent(mockContract, 'Transfer', mockCallback));
68103

69-
// @ts-ignore
70-
vi.mocked(contract.events.Transfer.filter).mockReturnValue(events);
71-
vi.mocked(client.query.system.events).mockImplementation((callback) => {
72-
callback(events);
104+
await waitFor(() => {
105+
expect(mockCallback).toHaveBeenCalledTimes(1);
73106
});
74107

75-
renderHook(() => useWatchContractEvent(contract, 'Transfer', onNewEvent));
76-
77-
expect(client.query.system.events).toHaveBeenCalled();
78-
expect(contract.events.Transfer.filter).toHaveBeenCalledWith(events);
79-
expect(onNewEvent).toHaveBeenCalledWith(events);
108+
await waitFor(() => {
109+
expect(mockCallback).toHaveBeenCalledTimes(2);
110+
});
80111
});
81112

82113
it('should unsubscribe when component unmounts', async () => {
83-
const unsub = vi.fn();
114+
const mockUnsub = vi.fn();
115+
116+
mockSub.mockReturnValue(mockUnsub);
84117

85-
vi.mocked(client.query.system.events).mockResolvedValue(unsub);
86-
const { unmount } = renderHook(() => useWatchContractEvent(contract, 'Transfer', vi.fn()));
118+
const { unmount } = renderHook(() => useWatchContractEvent(mockContract, 'Transfer', vi.fn()));
87119

88120
// Wait for unsub to be set
89121
await waitForNextUpdate();
90122

91123
unmount();
92124

93-
expect(unsub).toHaveBeenCalled();
125+
expect(mockUnsub).toHaveBeenCalled();
94126
});
95127
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useState } from 'react';
2+
import { TypinkEvent } from '../../providers/index.js';
3+
4+
export const useListenerCounter = (event: TypinkEvent) => {
5+
const [counter, setCounter] = useState(0);
6+
7+
const tryIncrease = (eventToCheck: TypinkEvent) => {
8+
if (event !== eventToCheck) return;
9+
10+
setCounter((counter) => counter + 1);
11+
};
12+
13+
const tryDecrease = (eventToCheck: TypinkEvent) => {
14+
if (event !== eventToCheck) return;
15+
16+
setCounter((counter) => counter - 1);
17+
};
18+
19+
return {
20+
tryIncrease,
21+
tryDecrease,
22+
counter,
23+
hasAny: counter > 0,
24+
};
25+
};
Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { useEffect } from 'react';
2-
import { useTypink } from './useTypink.js';
31
import { OmitNever } from '../types.js';
42
import { Contract, GenericContractApi } from 'dedot/contracts';
5-
import { Unsub } from 'dedot/types';
6-
import { useDeepDeps } from './internal/index.js';
3+
import { useWatchTypinkEvent } from './useWatchTypinkEvent.js';
4+
import { useCallback } from 'react';
5+
import { useDeepDeps } from './internal/useDeepDeps.js';
6+
import { TypinkEvent } from '../providers/index.js';
77

8-
type UseContractEvent<A extends GenericContractApi = GenericContractApi> = OmitNever<{
8+
export type UseContractEvent<A extends GenericContractApi = GenericContractApi> = OmitNever<{
99
[K in keyof A['events']]: K extends string ? (K extends `${infer Literal}` ? Literal : never) : never;
1010
}>;
1111

@@ -30,37 +30,21 @@ export function useWatchContractEvent<
3030
onNewEvent: (events: ReturnType<T['events'][M]['filter']>) => void,
3131
enabled: boolean = true,
3232
): void {
33-
const { client } = useTypink();
34-
35-
useEffect(
36-
() => {
37-
if (!client || !contract || !enabled) return;
38-
39-
// handle unsubscribing when component unmounts
40-
let done = false;
41-
let unsub: Unsub | undefined;
42-
43-
(async () => {
44-
// TODO reuse this subscription more efficiently
45-
unsub = await client.query.system.events((events) => {
46-
if (done) {
47-
unsub && unsub();
48-
return;
49-
}
50-
51-
const contractEvents = contract.events[event].filter(events);
52-
if (contractEvents.length === 0) return;
53-
54-
// @ts-ignore
55-
onNewEvent(contractEvents);
56-
});
57-
})();
58-
59-
return () => {
60-
unsub && unsub();
61-
done = true;
62-
};
63-
},
64-
useDeepDeps([client, contract, onNewEvent, enabled]),
33+
useWatchTypinkEvent(
34+
TypinkEvent.SYSTEM_EVENTS,
35+
useCallback(
36+
(events) => {
37+
if (!contract || !enabled) return;
38+
39+
const contractEvents = contract.events[event].filter(events);
40+
41+
if (contractEvents.length <= 0) return;
42+
43+
// @ts-ignore
44+
onNewEvent(contractEvents);
45+
},
46+
useDeepDeps([contract, event, onNewEvent, enabled]),
47+
),
48+
enabled,
6549
);
6650
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useEffect } from 'react';
2+
import { useDeepDeps } from './internal/index.js';
3+
import { useTypink } from './useTypink.js';
4+
import { TypinkEvent, TypinkEventsRegistration } from '../providers/index.js';
5+
6+
/**
7+
* A React hook that watches for internal typink events.
8+
*
9+
* This hook sets up a subscription to system events and filters for the specified event.
10+
* When new events are detected, it calls the provided callback function.
11+
*
12+
* @param event - The name of the event to watch for.
13+
* @param callback - Callback function to be called when new events are detected.
14+
* @param enabled - Optional boolean to enable or disable the event watching. Defaults to true.
15+
*/
16+
export function useWatchTypinkEvent<T extends TypinkEvent>(
17+
event: T,
18+
callback: TypinkEventsRegistration[T],
19+
enabled: boolean = true,
20+
) {
21+
const { subscribeToEvent, client } = useTypink();
22+
23+
useEffect(
24+
() => {
25+
if (!client || !enabled) return;
26+
27+
let unmounted = false;
28+
29+
const unsub = subscribeToEvent(event, (events) => {
30+
if (unmounted) {
31+
unsub && unsub();
32+
return;
33+
}
34+
35+
callback(events);
36+
});
37+
38+
return () => {
39+
unsub && unsub();
40+
unmounted = true;
41+
};
42+
},
43+
useDeepDeps([client, callback, event, enabled]),
44+
);
45+
}

0 commit comments

Comments
 (0)