@dhis2/app-service-offline 3.11.3 → 3.12.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/build/cjs/__tests__/integration.test.js +51 -82
  2. package/build/cjs/index.js +0 -7
  3. package/build/cjs/lib/__tests__/cacheable-section-state.test.js +7 -14
  4. package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +17 -20
  5. package/build/cjs/lib/__tests__/network-status.test.js +135 -148
  6. package/build/cjs/lib/__tests__/offline-provider.test.js +12 -22
  7. package/build/cjs/lib/__tests__/use-cacheable-section.test.js +87 -98
  8. package/build/cjs/lib/__tests__/use-online-status-message.test.js +7 -14
  9. package/build/cjs/lib/cacheable-section-state.js +27 -38
  10. package/build/cjs/lib/cacheable-section.js +26 -27
  11. package/build/cjs/lib/clear-sensitive-caches.js +14 -24
  12. package/build/cjs/lib/dhis2-connection-status/dev-debug-log.js +1 -3
  13. package/build/cjs/lib/dhis2-connection-status/dhis2-connection-status.js +27 -58
  14. package/build/cjs/lib/dhis2-connection-status/dhis2-connection-status.test.js +287 -230
  15. package/build/cjs/lib/dhis2-connection-status/index.js +0 -1
  16. package/build/cjs/lib/dhis2-connection-status/is-ping-available.js +0 -6
  17. package/build/cjs/lib/dhis2-connection-status/is-ping-available.test.js +0 -1
  18. package/build/cjs/lib/dhis2-connection-status/smart-interval.js +35 -49
  19. package/build/cjs/lib/dhis2-connection-status/use-ping-query.js +4 -5
  20. package/build/cjs/lib/global-state-service.js +9 -27
  21. package/build/cjs/lib/network-status.js +10 -13
  22. package/build/cjs/lib/offline-interface.js +3 -14
  23. package/build/cjs/lib/offline-provider.js +1 -12
  24. package/build/cjs/lib/online-status-message.js +5 -17
  25. package/build/cjs/setupRTL.js +1 -1
  26. package/build/cjs/utils/__tests__/render-counter.test.js +3 -12
  27. package/build/cjs/utils/render-counter.js +2 -10
  28. package/build/cjs/utils/test-mocks.js +13 -18
  29. package/build/es/__tests__/integration.test.js +51 -74
  30. package/build/es/index.js +2 -2
  31. package/build/es/lib/__tests__/cacheable-section-state.test.js +2 -4
  32. package/build/es/lib/__tests__/clear-sensitive-caches.test.js +19 -16
  33. package/build/es/lib/__tests__/network-status.test.js +105 -114
  34. package/build/es/lib/__tests__/offline-provider.test.js +13 -15
  35. package/build/es/lib/__tests__/use-cacheable-section.test.js +69 -73
  36. package/build/es/lib/__tests__/use-online-status-message.test.js +2 -3
  37. package/build/es/lib/cacheable-section-state.js +25 -26
  38. package/build/es/lib/cacheable-section.js +23 -15
  39. package/build/es/lib/clear-sensitive-caches.js +13 -21
  40. package/build/es/lib/dhis2-connection-status/dev-debug-log.js +1 -3
  41. package/build/es/lib/dhis2-connection-status/dhis2-connection-status.js +26 -37
  42. package/build/es/lib/dhis2-connection-status/dhis2-connection-status.test.js +223 -159
  43. package/build/es/lib/dhis2-connection-status/is-ping-available.js +0 -5
  44. package/build/es/lib/dhis2-connection-status/smart-interval.js +34 -42
  45. package/build/es/lib/dhis2-connection-status/use-ping-query.js +6 -3
  46. package/build/es/lib/global-state-service.js +6 -12
  47. package/build/es/lib/network-status.js +10 -9
  48. package/build/es/lib/offline-interface.js +0 -3
  49. package/build/es/lib/offline-provider.js +0 -3
  50. package/build/es/lib/online-status-message.js +3 -2
  51. package/build/es/setupRTL.js +1 -1
  52. package/build/es/utils/__tests__/render-counter.test.js +2 -4
  53. package/build/es/utils/render-counter.js +1 -3
  54. package/build/es/utils/test-mocks.js +8 -9
  55. package/build/types/lib/cacheable-section.d.ts +1 -1
  56. package/build/types/lib/dhis2-connection-status/dhis2-connection-status.d.ts +1 -1
  57. package/build/types/lib/network-status.d.ts +1 -1
  58. package/build/types/lib/online-status-message.d.ts +1 -1
  59. package/build/types/types.d.ts +1 -1
  60. package/package.json +4 -4
@@ -1,32 +1,31 @@
1
- import { renderHook, act } from '@testing-library/react-hooks';
1
+ import { renderHook, act, waitFor } from '@testing-library/react';
2
2
  import React from 'react';
3
3
  import { errorRecordingMock, failedMessageRecordingMock, mockOfflineInterface } from '../../utils/test-mocks';
4
4
  import { useCacheableSection } from '../cacheable-section';
5
- import { OfflineProvider } from '../offline-provider'; // Suppress 'act' warning for these tests
5
+ import { OfflineProvider } from '../offline-provider';
6
6
 
7
+ // Suppress 'act' warning for these tests
7
8
  const originalError = console.error;
8
9
  beforeEach(() => {
9
10
  jest.spyOn(console, 'error').mockImplementation(function () {
10
11
  const pattern = /Warning: An update to .* inside a test was not wrapped in act/;
11
-
12
12
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
13
13
  args[_key] = arguments[_key];
14
14
  }
15
-
16
15
  if (typeof args[0] === 'string' && pattern.test(args[0])) {
17
16
  return;
18
17
  }
19
-
20
18
  return originalError.call(console, ...args);
21
19
  });
22
20
  });
23
21
  afterEach(() => {
24
- jest.clearAllMocks() // This syntax appeases typescript:
22
+ jest.clearAllMocks()
23
+ // This syntax appeases typescript:
25
24
  ;
26
25
  console.error.mockRestore();
27
26
  });
28
27
  it('renders in the default state initially', () => {
29
- const wrapper = (_ref) => {
28
+ const wrapper = _ref => {
30
29
  let {
31
30
  children
32
31
  } = _ref;
@@ -34,7 +33,6 @@ it('renders in the default state initially', () => {
34
33
  offlineInterface: mockOfflineInterface
35
34
  }, children);
36
35
  };
37
-
38
36
  const {
39
37
  result
40
38
  } = renderHook(() => useCacheableSection('one'), {
@@ -45,7 +43,7 @@ it('renders in the default state initially', () => {
45
43
  expect(result.current.lastUpdated).toBeUndefined();
46
44
  });
47
45
  it('has stable references', () => {
48
- const wrapper = (_ref2) => {
46
+ const wrapper = _ref2 => {
49
47
  let {
50
48
  children
51
49
  } = _ref2;
@@ -53,7 +51,6 @@ it('has stable references', () => {
53
51
  offlineInterface: mockOfflineInterface
54
52
  }, children);
55
53
  };
56
-
57
54
  const {
58
55
  result,
59
56
  rerender
@@ -74,61 +71,67 @@ it('has stable references', () => {
74
71
  });
75
72
  it('handles a successful recording', async done => {
76
73
  const [sectionId, timeoutDelay] = ['one', 1234];
77
- const testOfflineInterface = { ...mockOfflineInterface,
74
+ const recordingSuccessOfflineInterface = {
75
+ ...mockOfflineInterface,
78
76
  getCachedSections: jest.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([{
79
77
  sectionId: sectionId,
80
78
  lastUpdated: new Date()
81
79
  }])
82
80
  };
83
-
84
- const wrapper = (_ref3) => {
81
+ const wrapper = _ref3 => {
85
82
  let {
86
83
  children
87
84
  } = _ref3;
88
85
  return /*#__PURE__*/React.createElement(OfflineProvider, {
89
- offlineInterface: testOfflineInterface
86
+ offlineInterface: recordingSuccessOfflineInterface
90
87
  }, children);
91
88
  };
92
-
93
89
  const {
94
- result,
95
- waitFor
90
+ result
96
91
  } = renderHook(() => useCacheableSection(sectionId), {
97
92
  wrapper
98
93
  });
99
-
100
94
  const assertRecordingStarted = () => {
101
95
  expect(result.current.recordingState).toBe('recording');
102
96
  };
103
-
104
97
  const assertRecordingCompleted = async () => {
105
- expect(result.current.recordingState).toBe('default'); // Test that 'isCached' gets updated
106
-
107
- expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2);
98
+ expect(result.current.recordingState).toBe('default');
99
+
100
+ // Test that 'isCached' gets updated
101
+ expect(recordingSuccessOfflineInterface.getCachedSections).toBeCalledTimes(2);
102
+ // Recording states are updated synchronously, but getting isCached
103
+ // state is asynchronous -- need to wait for that here.
104
+ // An assertion is not used as the waitFor condition because it may skew
105
+ // the total number assertions in this test if it needs to retry. Number
106
+ // of assertions is checked at the bottom of this test to make sure both
107
+ // of these callbacks are called.
108
108
  await waitFor(() => result.current.isCached === true);
109
109
  expect(result.current.isCached).toBe(true);
110
- expect(result.current.lastUpdated).toBeInstanceOf(Date); // If this cb is not called, test should time out and fail
110
+ expect(result.current.lastUpdated).toBeInstanceOf(Date);
111
111
 
112
+ // If this cb is not called, test should time out and fail
112
113
  done();
113
114
  };
114
-
115
115
  await act(async () => {
116
116
  await result.current.startRecording({
117
117
  onStarted: assertRecordingStarted,
118
118
  onCompleted: assertRecordingCompleted,
119
119
  recordingTimeoutDelay: timeoutDelay
120
120
  });
121
- }); // At this stage, recording should be 'pending'
121
+ });
122
122
 
123
- expect(result.current.recordingState).toBe('pending'); // Check correct options sent to offline interface
123
+ // At this stage, recording should be 'pending'
124
+ expect(result.current.recordingState).toBe('pending');
124
125
 
126
+ // Check correct options sent to offline interface
125
127
  const options = mockOfflineInterface.startRecording.mock.calls[0][0];
126
128
  expect(options.sectionId).toBe(sectionId);
127
129
  expect(options.recordingTimeoutDelay).toBe(timeoutDelay);
128
130
  expect(typeof options.onStarted).toBe('function');
129
131
  expect(typeof options.onCompleted).toBe('function');
130
- expect(typeof options.onError).toBe('function'); // Make sure all async assertions are called
132
+ expect(typeof options.onError).toBe('function');
131
133
 
134
+ // Make sure all async assertions are called
132
135
  expect.assertions(11);
133
136
  });
134
137
  it('handles a recording that encounters an error', async done => {
@@ -136,78 +139,72 @@ it('handles a recording that encounters an error', async done => {
136
139
  jest.spyOn(console, 'error').mockImplementation(function () {
137
140
  const actPattern = /Warning: An update to .* inside a test was not wrapped in act/;
138
141
  const errPattern = /Error during recording/;
139
-
140
142
  for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
141
143
  args[_key2] = arguments[_key2];
142
144
  }
143
-
144
145
  const matchesPattern = actPattern.test(args[0]) || errPattern.test(args[0]);
145
-
146
146
  if (typeof args[0] === 'string' && matchesPattern) {
147
147
  return;
148
148
  }
149
-
150
149
  return originalError.call(console, ...args);
151
150
  });
152
- const testOfflineInterface = { ...mockOfflineInterface,
151
+ const recordingErrorOfflineInterface = {
152
+ ...mockOfflineInterface,
153
153
  startRecording: errorRecordingMock
154
154
  };
155
-
156
- const wrapper = (_ref4) => {
155
+ const wrapper = _ref4 => {
157
156
  let {
158
157
  children
159
158
  } = _ref4;
160
159
  return /*#__PURE__*/React.createElement(OfflineProvider, {
161
- offlineInterface: testOfflineInterface
160
+ offlineInterface: recordingErrorOfflineInterface
162
161
  }, children);
163
162
  };
164
-
165
163
  const {
166
164
  result
167
165
  } = renderHook(() => useCacheableSection('one'), {
168
166
  wrapper
169
167
  });
170
-
171
168
  const assertRecordingStarted = () => {
172
169
  expect(result.current.recordingState).toBe('recording');
173
170
  };
174
-
175
171
  const assertRecordingError = error => {
176
172
  expect(result.current.recordingState).toBe('error');
177
173
  expect(error.message).toMatch(/test err/); // see errorRecordingMock
174
+ expect(console.error).toHaveBeenCalledWith('Error during recording:', error);
178
175
 
179
- expect(console.error).toHaveBeenCalledWith('Error during recording:', error); // Expect only one call, from initialization:
180
-
181
- expect(mockOfflineInterface.getCachedSections).toBeCalledTimes(1); // If this cb is not called, test should time out and fail
176
+ // Expect only one call, from initialization:
177
+ expect(mockOfflineInterface.getCachedSections).toBeCalledTimes(1);
182
178
 
179
+ // If this cb is not called, test should time out and fail
183
180
  done();
184
181
  };
185
-
186
182
  await act(async () => {
187
183
  await result.current.startRecording({
188
184
  onStarted: assertRecordingStarted,
189
185
  onError: assertRecordingError
190
186
  });
191
- }); // At this stage, recording should be 'pending'
187
+ });
192
188
 
193
- expect(result.current.recordingState).toBe('pending'); // Make sure all async assertions are called
189
+ // At this stage, recording should be 'pending'
190
+ expect(result.current.recordingState).toBe('pending');
194
191
 
192
+ // Make sure all async assertions are called
195
193
  expect.assertions(6);
196
194
  });
197
195
  it('handles an error starting the recording', async () => {
198
- const testOfflineInterface = { ...mockOfflineInterface,
196
+ const messageErrorOfflineInterface = {
197
+ ...mockOfflineInterface,
199
198
  startRecording: failedMessageRecordingMock
200
199
  };
201
-
202
- const wrapper = (_ref5) => {
200
+ const wrapper = _ref5 => {
203
201
  let {
204
202
  children
205
203
  } = _ref5;
206
204
  return /*#__PURE__*/React.createElement(OfflineProvider, {
207
- offlineInterface: testOfflineInterface
205
+ offlineInterface: messageErrorOfflineInterface
208
206
  }, children);
209
207
  };
210
-
211
208
  const {
212
209
  result
213
210
  } = renderHook(() => useCacheableSection('err'), {
@@ -218,72 +215,71 @@ it('handles an error starting the recording', async () => {
218
215
  });
219
216
  it('handles remove and updates sections', async () => {
220
217
  const sectionId = 'one';
221
- const testOfflineInterface = { ...mockOfflineInterface,
218
+ const sectionOpsOfflineInterface = {
219
+ ...mockOfflineInterface,
222
220
  getCachedSections: jest.fn().mockResolvedValueOnce([{
223
221
  sectionId: sectionId,
224
222
  lastUpdated: new Date()
225
223
  }]).mockResolvedValueOnce([])
226
224
  };
227
-
228
- const wrapper = (_ref6) => {
225
+ const wrapper = _ref6 => {
229
226
  let {
230
227
  children
231
228
  } = _ref6;
232
229
  return /*#__PURE__*/React.createElement(OfflineProvider, {
233
- offlineInterface: testOfflineInterface
230
+ offlineInterface: sectionOpsOfflineInterface
234
231
  }, children);
235
232
  };
236
-
237
233
  const {
238
- result,
239
- waitFor
234
+ result
240
235
  } = renderHook(() => useCacheableSection(sectionId), {
241
236
  wrapper
242
- }); // Wait for state to sync with indexedDB
237
+ });
243
238
 
244
- await waitFor(() => result.current.isCached === true);
239
+ // Wait for state to sync with indexedDB
240
+ await waitFor(() => expect(result.current.isCached).toBe(true));
245
241
  let success;
246
242
  await act(async () => {
247
243
  success = await result.current.remove();
248
244
  });
249
- expect(success).toBe(true); // Test that 'isCached' gets updated
250
-
251
- expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2);
252
- await waitFor(() => result.current.isCached === false);
245
+ expect(success).toBe(true);
246
+ // Test that 'isCached' gets updated
247
+ expect(sectionOpsOfflineInterface.getCachedSections).toBeCalledTimes(2);
248
+ await waitFor(() => expect(result.current.isCached).toBe(false));
253
249
  expect(result.current.isCached).toBe(false);
254
250
  expect(result.current.lastUpdated).toBeUndefined();
255
251
  });
256
252
  it('handles a change in ID', async () => {
257
- const testOfflineInterface = { ...mockOfflineInterface,
253
+ const idChangeOfflineInterface = {
254
+ ...mockOfflineInterface,
258
255
  getCachedSections: jest.fn().mockResolvedValue([{
259
256
  sectionId: 'id-one',
260
257
  lastUpdated: new Date()
261
258
  }])
262
259
  };
263
-
264
- const wrapper = (_ref7) => {
260
+ const wrapper = _ref7 => {
265
261
  let {
266
262
  children
267
263
  } = _ref7;
268
264
  return /*#__PURE__*/React.createElement(OfflineProvider, {
269
- offlineInterface: testOfflineInterface
265
+ offlineInterface: idChangeOfflineInterface
270
266
  }, children);
271
267
  };
272
-
273
268
  const {
274
269
  result,
275
- waitFor,
276
270
  rerender
277
271
  } = renderHook(id => useCacheableSection(id), {
278
272
  wrapper,
279
273
  initialProps: 'id-one'
280
- }); // Wait for state to sync with indexedDB
274
+ });
281
275
 
282
- await waitFor(() => result.current.isCached === true);
283
- rerender('id-two'); // Test that 'isCached' gets updated
284
- // expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2)
276
+ // Wait for state to sync with indexedDB
277
+ await waitFor(() => expect(result.current.isCached).toBe(true));
278
+ rerender('id-two');
285
279
 
286
- await waitFor(() => result.current.isCached === false);
280
+ // Test that 'isCached' gets updated
281
+ // expect(idChangeOfflineInterface.getCachedSections).toBeCalledTimes(2)
282
+ await waitFor(() => expect(result.current.isCached).toBe(false));
287
283
  expect(result.current.isCached).toBe(false);
288
284
  expect(result.current.lastUpdated).toBeUndefined();
289
285
  });
@@ -1,11 +1,11 @@
1
- import { renderHook, act } from '@testing-library/react-hooks';
1
+ import { renderHook, act } from '@testing-library/react';
2
2
  import React from 'react';
3
3
  import { mockOfflineInterface } from '../../utils/test-mocks';
4
4
  import { OfflineProvider } from '../offline-provider';
5
5
  import { useOnlineStatusMessage } from '../online-status-message';
6
6
  describe('useOnlineStatusMessage', () => {
7
7
  it('should allow the online status to be updated ', () => {
8
- const wrapper = (_ref) => {
8
+ const wrapper = _ref => {
9
9
  let {
10
10
  children
11
11
  } = _ref;
@@ -13,7 +13,6 @@ describe('useOnlineStatusMessage', () => {
13
13
  offlineInterface: mockOfflineInterface
14
14
  }, children);
15
15
  };
16
-
17
16
  const {
18
17
  result
19
18
  } = renderHook(() => useOnlineStatusMessage(), {
@@ -1,7 +1,9 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React, { useEffect, useCallback, useMemo } from 'react';
3
3
  import { createStore, useGlobalState, useGlobalStateMutation, GlobalStateProvider } from './global-state-service';
4
- import { useOfflineInterface } from './offline-interface'; // Functions in here use the global state service to manage cacheable section
4
+ import { useOfflineInterface } from './offline-interface';
5
+
6
+ // Functions in here use the global state service to manage cacheable section
5
7
  // state in a performant way
6
8
 
7
9
  /**
@@ -17,19 +19,19 @@ function getSectionsById(sectionsArray) {
17
19
  sectionId,
18
20
  lastUpdated
19
21
  } = _ref;
20
- return { ...result,
22
+ return {
23
+ ...result,
21
24
  [sectionId]: {
22
25
  lastUpdated
23
26
  }
24
27
  };
25
28
  }, {});
26
29
  }
30
+
27
31
  /**
28
32
  * Create a store for Cacheable Section state.
29
33
  * Expected to be used in app adapter
30
34
  */
31
-
32
-
33
35
  export function createCacheableSectionStore() {
34
36
  const initialState = {
35
37
  recordingStates: {},
@@ -37,40 +39,39 @@ export function createCacheableSectionStore() {
37
39
  };
38
40
  return createStore(initialState);
39
41
  }
42
+
40
43
  /**
41
44
  * Helper hook that returns a value that will persist between renders but makes
42
45
  * sure to only set its initial state once.
43
46
  * See https://gist.github.com/amcgee/42bb2fa6d5f79e607f00e6dccc733482
44
47
  */
45
-
46
48
  function useConst(factory) {
47
49
  const ref = React.useRef(null);
48
-
49
50
  if (ref.current === null) {
50
51
  ref.current = factory();
51
52
  }
52
-
53
53
  return ref.current;
54
54
  }
55
+
55
56
  /**
56
57
  * Provides context for a global state context which will track cached
57
58
  * sections' status and cacheable sections' recording states, which will
58
59
  * determine how that component will render. The provider will be a part of
59
60
  * the OfflineProvider.
60
61
  */
61
-
62
-
63
62
  export function CacheableSectionProvider(_ref2) {
64
63
  let {
65
64
  children
66
65
  } = _ref2;
67
66
  const offlineInterface = useOfflineInterface();
68
- const store = useConst(createCacheableSectionStore); // On load, get sections and add to store
67
+ const store = useConst(createCacheableSectionStore);
69
68
 
69
+ // On load, get sections and add to store
70
70
  useEffect(() => {
71
71
  if (offlineInterface) {
72
72
  offlineInterface.getCachedSections().then(sections => {
73
- store.mutate(state => ({ ...state,
73
+ store.mutate(state => ({
74
+ ...state,
74
75
  cachedSections: getSectionsById(sections)
75
76
  }));
76
77
  });
@@ -83,7 +84,6 @@ export function CacheableSectionProvider(_ref2) {
83
84
  CacheableSectionProvider.propTypes = {
84
85
  children: PropTypes.node
85
86
  };
86
-
87
87
  /**
88
88
  * Uses an optimized global state to manage 'recording state' values without
89
89
  * unnecessarily rerendering all consuming components
@@ -94,17 +94,21 @@ CacheableSectionProvider.propTypes = {
94
94
  export function useRecordingState(id) {
95
95
  const recordingStateSelector = useCallback(state => state.recordingStates[id], [id]);
96
96
  const [recordingState] = useGlobalState(recordingStateSelector);
97
- const setRecordingStateMutationCreator = useCallback(newState => state => ({ ...state,
98
- recordingStates: { ...state.recordingStates,
97
+ const setRecordingStateMutationCreator = useCallback(newState => state => ({
98
+ ...state,
99
+ recordingStates: {
100
+ ...state.recordingStates,
99
101
  [id]: newState
100
102
  }
101
103
  }), [id]);
102
104
  const setRecordingState = useGlobalStateMutation(setRecordingStateMutationCreator);
103
105
  const removeRecordingStateMutationCreator = useCallback(() => state => {
104
- const recordingStates = { ...state.recordingStates
106
+ const recordingStates = {
107
+ ...state.recordingStates
105
108
  };
106
109
  delete recordingStates[id];
107
- return { ...state,
110
+ return {
111
+ ...state,
108
112
  recordingStates
109
113
  };
110
114
  }, [id]);
@@ -115,16 +119,17 @@ export function useRecordingState(id) {
115
119
  removeRecordingState
116
120
  }), [recordingState, setRecordingState, removeRecordingState]);
117
121
  }
122
+
118
123
  /**
119
124
  * Returns a function that syncs cached sections in the global state
120
125
  * with IndexedDB, so that IndexedDB is the single source of truth
121
126
  *
122
127
  * @returns {Function} syncCachedSections
123
128
  */
124
-
125
129
  function useSyncCachedSections() {
126
130
  const offlineInterface = useOfflineInterface();
127
- const setCachedSectionsMutationCreator = useCallback(cachedSections => state => ({ ...state,
131
+ const setCachedSectionsMutationCreator = useCallback(cachedSections => state => ({
132
+ ...state,
128
133
  cachedSections
129
134
  }), []);
130
135
  const setCachedSections = useGlobalStateMutation(setCachedSectionsMutationCreator);
@@ -133,7 +138,6 @@ function useSyncCachedSections() {
133
138
  setCachedSections(getSectionsById(sections));
134
139
  }, [offlineInterface, setCachedSections]);
135
140
  }
136
-
137
141
  /**
138
142
  * Uses global state to manage an object of cached sections' statuses
139
143
  *
@@ -143,6 +147,7 @@ export function useCachedSections() {
143
147
  const [cachedSections] = useGlobalState(state => state.cachedSections);
144
148
  const syncCachedSections = useSyncCachedSections();
145
149
  const offlineInterface = useOfflineInterface();
150
+
146
151
  /**
147
152
  * Uses offline interface to remove a section from IndexedDB and Cache
148
153
  * Storage.
@@ -150,14 +155,11 @@ export function useCachedSections() {
150
155
  * Returns a promise that resolves to `true` if a section is found and
151
156
  * deleted, or `false` if asection with the specified ID does not exist.
152
157
  */
153
-
154
158
  const removeById = useCallback(async id => {
155
159
  const success = await offlineInterface.removeSection(id);
156
-
157
160
  if (success) {
158
161
  await syncCachedSections();
159
162
  }
160
-
161
163
  return success;
162
164
  }, [offlineInterface, syncCachedSections]);
163
165
  return useMemo(() => ({
@@ -166,7 +168,6 @@ export function useCachedSections() {
166
168
  syncCachedSections
167
169
  }), [cachedSections, removeById, syncCachedSections]);
168
170
  }
169
-
170
171
  /**
171
172
  * Uses global state to manage the cached status of just one section, which
172
173
  * prevents unnecessary rerenders of consuming components
@@ -179,6 +180,7 @@ export function useCachedSection(id) {
179
180
  const syncCachedSections = useSyncCachedSections();
180
181
  const offlineInterface = useOfflineInterface();
181
182
  const lastUpdated = status && status.lastUpdated;
183
+
182
184
  /**
183
185
  * Uses offline interface to remove a section from IndexedDB and Cache
184
186
  * Storage.
@@ -186,14 +188,11 @@ export function useCachedSection(id) {
186
188
  * Returns `true` if a section is found and deleted, or `false` if a
187
189
  * section with the specified ID does not exist.
188
190
  */
189
-
190
191
  const remove = useCallback(async () => {
191
192
  const success = await offlineInterface.removeSection(id);
192
-
193
193
  if (success) {
194
194
  await syncCachedSections();
195
195
  }
196
-
197
196
  return success;
198
197
  }, [offlineInterface, id, syncCachedSections]);
199
198
  return useMemo(() => ({
@@ -1,5 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React, { useCallback, useEffect, useMemo } from 'react';
3
+ import { flushSync } from 'react-dom';
3
4
  import { useRecordingState, useCachedSection } from './cacheable-section-state';
4
5
  import { useOfflineInterface } from './offline-interface';
5
6
  const recordingStates = {
@@ -8,7 +9,6 @@ const recordingStates = {
8
9
  recording: 'recording',
9
10
  error: 'error'
10
11
  };
11
-
12
12
  /**
13
13
  * Returns the main controls for a cacheable section and manages recording
14
14
  * state, which affects the render state of the CacheableSection component.
@@ -35,9 +35,8 @@ export function useCacheableSection(id) {
35
35
  // On mount, add recording state for this ID to context if needed
36
36
  if (!recordingState) {
37
37
  setRecordingState(recordingStates.default);
38
- } // On unnmount, remove recording state if not recording
39
-
40
-
38
+ }
39
+ // On unnmount, remove recording state if not recording
41
40
  return () => {
42
41
  if (recordingState && recordingState !== recordingStates.recording && recordingState !== recordingStates.pending) {
43
42
  removeRecordingState();
@@ -70,21 +69,30 @@ export function useCacheableSection(id) {
70
69
  sectionId: id,
71
70
  recordingTimeoutDelay,
72
71
  onStarted: () => {
73
- onRecordingStarted();
72
+ // Flush this state update synchronously so that the
73
+ // right recordingState is set before any other callbacks
74
+ flushSync(() => {
75
+ onRecordingStarted();
76
+ });
74
77
  onStarted && onStarted();
75
78
  },
76
79
  onCompleted: () => {
77
- onRecordingCompleted();
80
+ flushSync(() => {
81
+ onRecordingCompleted();
82
+ });
78
83
  onCompleted && onCompleted();
79
84
  },
80
85
  onError: error => {
81
- onRecordingError(error);
86
+ flushSync(() => {
87
+ onRecordingError(error);
88
+ });
82
89
  onError && onError(error);
83
90
  }
84
91
  }).then(() => setRecordingState(recordingStates.pending));
85
- }, [id, offlineInterface, onRecordingCompleted, onRecordingError, onRecordingStarted, setRecordingState]); // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
86
- // but provided through this hook for convenience
92
+ }, [id, offlineInterface, onRecordingCompleted, onRecordingError, onRecordingStarted, setRecordingState]);
87
93
 
94
+ // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
95
+ // but provided through this hook for convenience
88
96
  return useMemo(() => ({
89
97
  recordingState,
90
98
  startRecording,
@@ -93,7 +101,6 @@ export function useCacheableSection(id) {
93
101
  remove
94
102
  }), [recordingState, startRecording, lastUpdated, isCached, remove]);
95
103
  }
96
-
97
104
  /**
98
105
  * Used to wrap the relevant component to be recorded and saved offline.
99
106
  * Depending on the recording state of the section, this wrapper will
@@ -114,18 +121,19 @@ export function CacheableSection(_ref) {
114
121
  // Accesses recording state that useCacheableSection controls
115
122
  const {
116
123
  recordingState
117
- } = useRecordingState(id); // The following causes the component to reload in the event of a recording
124
+ } = useRecordingState(id);
125
+
126
+ // The following causes the component to reload in the event of a recording
118
127
  // error; the state will be cleared next time recording moves to pending.
119
128
  // It fixes a component getting stuck while rendered without data after
120
129
  // failing a recording while offline.
121
130
  // Errors can be handled in the `onError` callback to `startRecording`.
122
-
123
131
  if (recordingState === recordingStates.error) {
124
132
  return /*#__PURE__*/React.createElement(React.Fragment, null, children);
125
- } // Handling rendering with the following conditions prevents an unncessary
126
- // rerender after successful recording
127
-
133
+ }
128
134
 
135
+ // Handling rendering with the following conditions prevents an unncessary
136
+ // rerender after successful recording
129
137
  return /*#__PURE__*/React.createElement(React.Fragment, null, recordingState === recordingStates.recording && loadingMask, recordingState !== recordingStates.pending && children);
130
138
  }
131
139
  CacheableSection.propTypes = {