Skip to content

Commit 4f92be9

Browse files
committed
Add hook and sync controller interface.
1 parent 15e2d35 commit 4f92be9

File tree

7 files changed

+412
-24
lines changed

7 files changed

+412
-24
lines changed

index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export { useBackButton } from './react-native/hooks/useBackButton'
7474
export { useEventRefresh } from './react-native/hooks/useEventRefresh'
7575
export { useMeasure } from './react-native/hooks/useMeasure'
7676
export { useRefresh } from './react-native/hooks/useRefresh'
77+
export { useStartSyncWhenTop } from './react-native/hooks/useStartSyncWhenTop'
7778
export { useSyncFileCleanUpBlocker } from './react-native/hooks/useSyncFileCleanUpBlocker'
7879
export { useSyncInProgress } from './react-native/hooks/useSyncInProgress'
7980
export { UuidGenerator } from './react-native/services/UuidGenerator'
@@ -172,6 +173,7 @@ export type { SyncableStateCollectionItem } from './react-native/types/SyncableS
172173
export type { SyncableStateSingleton } from './react-native/types/SyncableStateSingleton'
173174
export type { SyncConfiguration } from './react-native/types/SyncConfiguration'
174175
export type { SyncConfigurationCollection } from './react-native/types/SyncConfigurationCollection'
176+
export type { SyncControllerInterface } from './react-native/types/SyncControllerInterface'
175177
export type { SyncInterface } from './react-native/types/SyncInterface'
176178
export type { SyncPullResponse } from './react-native/types/SyncPullResponse'
177179
export type { SyncState } from './react-native/types/SyncState'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from 'react'
2+
import type { SyncControllerInterface } from '../../types/SyncControllerInterface'
3+
4+
/**
5+
* Automatically starts a sync when this is the top card in a stack router.
6+
* @param syncController The sync controller which is to be signalled when this is the top card in a stack router.
7+
* @param top When true, this is the top card in the stack router.
8+
*/
9+
export function useStartSyncWhenTop (
10+
syncController: SyncControllerInterface,
11+
top: boolean
12+
): void {
13+
React.useEffect(() => {
14+
if (top) {
15+
syncController.resume()
16+
17+
void syncController.run()
18+
}
19+
}, [top])
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# `react-native-app-helpers/useStartSyncWhenTop`
2+
3+
A React hook which starts sync when the current card is at the top of the stack.
4+
5+
## Usage
6+
7+
```tsx
8+
import { useStartSyncWhenTop } from "react-native-app-helpers";
9+
import { syncController } from "./your-services";
10+
11+
const ExampleScreen = ({ top }) => {
12+
useStartSyncWhenTop(syncController, top);
13+
14+
return null;
15+
};
16+
```
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
import * as React from 'react'
2+
import * as TestRenderer from 'react-test-renderer'
3+
import {
4+
useStartSyncWhenTop,
5+
type SyncControllerInterface
6+
} from '../../..'
7+
8+
test('starts sync when mounting on top', async () => {
9+
const syncController: SyncControllerInterface = {
10+
resume: jest.fn(),
11+
requestCancel: jest.fn(),
12+
run: jest.fn().mockReturnValue(new Promise(() => {})),
13+
pause: jest.fn()
14+
}
15+
16+
const Component: React.FunctionComponent = () => {
17+
useStartSyncWhenTop(syncController, true)
18+
19+
return null
20+
}
21+
22+
const renderer = TestRenderer.create(<Component />)
23+
24+
await new Promise((resolve) => setTimeout(resolve, 10))
25+
26+
expect(syncController.resume).toHaveBeenCalledTimes(1)
27+
expect(syncController.requestCancel).not.toHaveBeenCalled()
28+
expect(syncController.run).toHaveBeenCalledTimes(1)
29+
expect(syncController.pause).not.toHaveBeenCalled()
30+
expect((syncController.resume as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan((syncController.run as jest.Mock).mock.invocationCallOrder[0] as number)
31+
32+
renderer.unmount()
33+
})
34+
35+
test('does nothing when run succeeds without changes', async () => {
36+
const syncController: SyncControllerInterface = {
37+
resume: jest.fn(),
38+
requestCancel: jest.fn(),
39+
run: jest.fn().mockResolvedValue('noChangesMade'),
40+
pause: jest.fn()
41+
}
42+
43+
const Component: React.FunctionComponent = () => {
44+
useStartSyncWhenTop(syncController, true)
45+
46+
return null
47+
}
48+
49+
const renderer = TestRenderer.create(<Component />)
50+
51+
await new Promise((resolve) => setTimeout(resolve, 10))
52+
53+
expect(syncController.resume).toHaveBeenCalledTimes(1)
54+
expect(syncController.requestCancel).not.toHaveBeenCalled()
55+
expect(syncController.run).toHaveBeenCalledTimes(1)
56+
expect(syncController.pause).not.toHaveBeenCalled()
57+
expect((syncController.resume as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan((syncController.run as jest.Mock).mock.invocationCallOrder[0] as number)
58+
59+
renderer.unmount()
60+
})
61+
62+
test('does nothing when run succeeds with changes', async () => {
63+
const syncController: SyncControllerInterface = {
64+
resume: jest.fn(),
65+
requestCancel: jest.fn(),
66+
run: jest.fn().mockResolvedValue('atLeastOneChangeMade'),
67+
pause: jest.fn()
68+
}
69+
70+
const Component: React.FunctionComponent = () => {
71+
useStartSyncWhenTop(syncController, true)
72+
73+
return null
74+
}
75+
76+
const renderer = TestRenderer.create(<Component />)
77+
78+
await new Promise((resolve) => setTimeout(resolve, 10))
79+
80+
expect(syncController.resume).toHaveBeenCalledTimes(1)
81+
expect(syncController.requestCancel).not.toHaveBeenCalled()
82+
expect(syncController.run).toHaveBeenCalledTimes(1)
83+
expect(syncController.pause).not.toHaveBeenCalled()
84+
expect((syncController.resume as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan((syncController.run as jest.Mock).mock.invocationCallOrder[0] as number)
85+
86+
renderer.unmount()
87+
})
88+
89+
test('does nothing when run fails with changes', async () => {
90+
const syncController: SyncControllerInterface = {
91+
resume: jest.fn(),
92+
requestCancel: jest.fn(),
93+
run: jest.fn().mockResolvedValue('failed'),
94+
pause: jest.fn()
95+
}
96+
97+
const Component: React.FunctionComponent = () => {
98+
useStartSyncWhenTop(syncController, true)
99+
100+
return null
101+
}
102+
103+
const renderer = TestRenderer.create(<Component />)
104+
105+
await new Promise((resolve) => setTimeout(resolve, 10))
106+
107+
expect(syncController.resume).toHaveBeenCalledTimes(1)
108+
expect(syncController.requestCancel).not.toHaveBeenCalled()
109+
expect(syncController.run).toHaveBeenCalledTimes(1)
110+
expect(syncController.pause).not.toHaveBeenCalled()
111+
expect((syncController.resume as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan((syncController.run as jest.Mock).mock.invocationCallOrder[0] as number)
112+
113+
renderer.unmount()
114+
})
115+
116+
test('does nothing when mounting below top', async () => {
117+
const syncController: SyncControllerInterface = {
118+
resume: jest.fn(),
119+
requestCancel: jest.fn(),
120+
run: jest.fn(),
121+
pause: jest.fn()
122+
}
123+
124+
const Component: React.FunctionComponent = () => {
125+
useStartSyncWhenTop(syncController, false)
126+
127+
return null
128+
}
129+
130+
const renderer = TestRenderer.create(<Component />)
131+
132+
await new Promise((resolve) => setTimeout(resolve, 10))
133+
134+
expect(syncController.resume).not.toHaveBeenCalled()
135+
expect(syncController.requestCancel).not.toHaveBeenCalled()
136+
expect(syncController.run).not.toHaveBeenCalled()
137+
expect(syncController.pause).not.toHaveBeenCalled()
138+
139+
renderer.unmount()
140+
})
141+
142+
test('does nothing when re-rendering on top', async () => {
143+
const syncController: SyncControllerInterface = {
144+
resume: jest.fn(),
145+
requestCancel: jest.fn(),
146+
run: jest.fn().mockResolvedValue('noChangesMade'),
147+
pause: jest.fn()
148+
}
149+
150+
const Component: React.FunctionComponent = () => {
151+
useStartSyncWhenTop(syncController, true)
152+
153+
return null
154+
}
155+
156+
const renderer = TestRenderer.create(<Component />)
157+
158+
await new Promise((resolve) => setTimeout(resolve, 10));
159+
160+
(syncController.run as jest.Mock).mockClear();
161+
(syncController.resume as jest.Mock).mockClear()
162+
163+
renderer.update(<Component />)
164+
165+
await new Promise((resolve) => setTimeout(resolve, 10))
166+
167+
expect(syncController.resume).not.toHaveBeenCalled()
168+
expect(syncController.requestCancel).not.toHaveBeenCalled()
169+
expect(syncController.run).not.toHaveBeenCalled()
170+
expect(syncController.pause).not.toHaveBeenCalled()
171+
172+
renderer.unmount()
173+
})
174+
175+
test('does nothing when re-rendering below top', async () => {
176+
const syncController: SyncControllerInterface = {
177+
resume: jest.fn(),
178+
requestCancel: jest.fn(),
179+
run: jest.fn(),
180+
pause: jest.fn()
181+
}
182+
183+
const Component: React.FunctionComponent = () => {
184+
useStartSyncWhenTop(syncController, false)
185+
186+
return null
187+
}
188+
189+
const renderer = TestRenderer.create(<Component />)
190+
191+
await new Promise((resolve) => setTimeout(resolve, 10))
192+
193+
renderer.update(<Component />)
194+
195+
await new Promise((resolve) => setTimeout(resolve, 10))
196+
197+
expect(syncController.resume).not.toHaveBeenCalled()
198+
expect(syncController.requestCancel).not.toHaveBeenCalled()
199+
expect(syncController.run).not.toHaveBeenCalled()
200+
expect(syncController.pause).not.toHaveBeenCalled()
201+
202+
renderer.unmount()
203+
})
204+
205+
test('does nothing when re-rendering from top to below', async () => {
206+
const syncController: SyncControllerInterface = {
207+
resume: jest.fn(),
208+
requestCancel: jest.fn(),
209+
run: jest.fn().mockResolvedValue('noChangesMade'),
210+
pause: jest.fn()
211+
}
212+
213+
const Component: React.FunctionComponent<{ readonly top: boolean }> = ({ top }) => {
214+
useStartSyncWhenTop(syncController, top)
215+
216+
return null
217+
}
218+
219+
const renderer = TestRenderer.create(<Component top={true} />)
220+
221+
await new Promise((resolve) => setTimeout(resolve, 10));
222+
223+
(syncController.run as jest.Mock).mockClear();
224+
(syncController.resume as jest.Mock).mockClear()
225+
226+
renderer.update(<Component top={false} />)
227+
228+
await new Promise((resolve) => setTimeout(resolve, 10))
229+
230+
expect(syncController.resume).not.toHaveBeenCalled()
231+
expect(syncController.requestCancel).not.toHaveBeenCalled()
232+
expect(syncController.run).not.toHaveBeenCalled()
233+
expect(syncController.pause).not.toHaveBeenCalled()
234+
235+
renderer.unmount()
236+
})
237+
238+
test('starts sync when re-rendering from below to top', async () => {
239+
const syncController: SyncControllerInterface = {
240+
resume: jest.fn(),
241+
requestCancel: jest.fn(),
242+
run: jest.fn().mockResolvedValue('noChangesMade'),
243+
pause: jest.fn()
244+
}
245+
246+
const Component: React.FunctionComponent<{ readonly top: boolean }> = ({ top }) => {
247+
useStartSyncWhenTop(syncController, top)
248+
249+
return null
250+
}
251+
252+
const renderer = TestRenderer.create(<Component top={false} />)
253+
254+
await new Promise((resolve) => setTimeout(resolve, 10))
255+
256+
renderer.update(<Component top={true} />)
257+
258+
await new Promise((resolve) => setTimeout(resolve, 10))
259+
260+
expect(syncController.resume).toHaveBeenCalledTimes(1)
261+
expect(syncController.requestCancel).not.toHaveBeenCalled()
262+
expect(syncController.run).toHaveBeenCalledTimes(1)
263+
expect(syncController.pause).not.toHaveBeenCalled()
264+
expect((syncController.resume as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan((syncController.run as jest.Mock).mock.invocationCallOrder[0] as number)
265+
266+
renderer.unmount()
267+
})
268+
269+
test('does nothing when unmounting from top', async () => {
270+
const syncController: SyncControllerInterface = {
271+
resume: jest.fn(),
272+
requestCancel: jest.fn(),
273+
run: jest.fn().mockResolvedValue('noChangesMade'),
274+
pause: jest.fn()
275+
}
276+
277+
const Component: React.FunctionComponent = () => {
278+
useStartSyncWhenTop(syncController, true)
279+
280+
return null
281+
}
282+
283+
const renderer = TestRenderer.create(<Component />)
284+
285+
await new Promise((resolve) => setTimeout(resolve, 10));
286+
287+
(syncController.run as jest.Mock).mockClear();
288+
(syncController.resume as jest.Mock).mockClear()
289+
290+
renderer.unmount()
291+
292+
await new Promise((resolve) => setTimeout(resolve, 10))
293+
294+
expect(syncController.resume).not.toHaveBeenCalled()
295+
expect(syncController.requestCancel).not.toHaveBeenCalled()
296+
expect(syncController.run).not.toHaveBeenCalled()
297+
expect(syncController.pause).not.toHaveBeenCalled()
298+
})
299+
300+
test('does nothing when unmounting from below top', async () => {
301+
const syncController: SyncControllerInterface = {
302+
resume: jest.fn(),
303+
requestCancel: jest.fn(),
304+
run: jest.fn(),
305+
pause: jest.fn()
306+
}
307+
308+
const Component: React.FunctionComponent = () => {
309+
useStartSyncWhenTop(syncController, false)
310+
311+
return null
312+
}
313+
314+
const renderer = TestRenderer.create(<Component />)
315+
316+
await new Promise((resolve) => setTimeout(resolve, 10))
317+
318+
renderer.unmount()
319+
320+
await new Promise((resolve) => setTimeout(resolve, 10))
321+
322+
expect(syncController.resume).not.toHaveBeenCalled()
323+
expect(syncController.requestCancel).not.toHaveBeenCalled()
324+
expect(syncController.run).not.toHaveBeenCalled()
325+
expect(syncController.pause).not.toHaveBeenCalled()
326+
})

0 commit comments

Comments
 (0)