@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.
- package/build/cjs/__tests__/integration.test.js +51 -82
- package/build/cjs/index.js +0 -7
- package/build/cjs/lib/__tests__/cacheable-section-state.test.js +7 -14
- package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +17 -20
- package/build/cjs/lib/__tests__/network-status.test.js +135 -148
- package/build/cjs/lib/__tests__/offline-provider.test.js +12 -22
- package/build/cjs/lib/__tests__/use-cacheable-section.test.js +87 -98
- package/build/cjs/lib/__tests__/use-online-status-message.test.js +7 -14
- package/build/cjs/lib/cacheable-section-state.js +27 -38
- package/build/cjs/lib/cacheable-section.js +26 -27
- package/build/cjs/lib/clear-sensitive-caches.js +14 -24
- package/build/cjs/lib/dhis2-connection-status/dev-debug-log.js +1 -3
- package/build/cjs/lib/dhis2-connection-status/dhis2-connection-status.js +27 -58
- package/build/cjs/lib/dhis2-connection-status/dhis2-connection-status.test.js +287 -230
- package/build/cjs/lib/dhis2-connection-status/index.js +0 -1
- package/build/cjs/lib/dhis2-connection-status/is-ping-available.js +0 -6
- package/build/cjs/lib/dhis2-connection-status/is-ping-available.test.js +0 -1
- package/build/cjs/lib/dhis2-connection-status/smart-interval.js +35 -49
- package/build/cjs/lib/dhis2-connection-status/use-ping-query.js +4 -5
- package/build/cjs/lib/global-state-service.js +9 -27
- package/build/cjs/lib/network-status.js +10 -13
- package/build/cjs/lib/offline-interface.js +3 -14
- package/build/cjs/lib/offline-provider.js +1 -12
- package/build/cjs/lib/online-status-message.js +5 -17
- package/build/cjs/setupRTL.js +1 -1
- package/build/cjs/utils/__tests__/render-counter.test.js +3 -12
- package/build/cjs/utils/render-counter.js +2 -10
- package/build/cjs/utils/test-mocks.js +13 -18
- package/build/es/__tests__/integration.test.js +51 -74
- package/build/es/index.js +2 -2
- package/build/es/lib/__tests__/cacheable-section-state.test.js +2 -4
- package/build/es/lib/__tests__/clear-sensitive-caches.test.js +19 -16
- package/build/es/lib/__tests__/network-status.test.js +105 -114
- package/build/es/lib/__tests__/offline-provider.test.js +13 -15
- package/build/es/lib/__tests__/use-cacheable-section.test.js +69 -73
- package/build/es/lib/__tests__/use-online-status-message.test.js +2 -3
- package/build/es/lib/cacheable-section-state.js +25 -26
- package/build/es/lib/cacheable-section.js +23 -15
- package/build/es/lib/clear-sensitive-caches.js +13 -21
- package/build/es/lib/dhis2-connection-status/dev-debug-log.js +1 -3
- package/build/es/lib/dhis2-connection-status/dhis2-connection-status.js +26 -37
- package/build/es/lib/dhis2-connection-status/dhis2-connection-status.test.js +223 -159
- package/build/es/lib/dhis2-connection-status/is-ping-available.js +0 -5
- package/build/es/lib/dhis2-connection-status/smart-interval.js +34 -42
- package/build/es/lib/dhis2-connection-status/use-ping-query.js +6 -3
- package/build/es/lib/global-state-service.js +6 -12
- package/build/es/lib/network-status.js +10 -9
- package/build/es/lib/offline-interface.js +0 -3
- package/build/es/lib/offline-provider.js +0 -3
- package/build/es/lib/online-status-message.js +3 -2
- package/build/es/setupRTL.js +1 -1
- package/build/es/utils/__tests__/render-counter.test.js +2 -4
- package/build/es/utils/render-counter.js +1 -3
- package/build/es/utils/test-mocks.js +8 -9
- package/build/types/lib/cacheable-section.d.ts +1 -1
- package/build/types/lib/dhis2-connection-status/dhis2-connection-status.d.ts +1 -1
- package/build/types/lib/network-status.d.ts +1 -1
- package/build/types/lib/online-status-message.d.ts +1 -1
- package/build/types/types.d.ts +1 -1
- package/package.json +4 -4
|
@@ -1,32 +1,31 @@
|
|
|
1
|
-
import { renderHook, act } from '@testing-library/react
|
|
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';
|
|
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()
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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');
|
|
106
|
-
|
|
107
|
-
|
|
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);
|
|
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
|
-
});
|
|
121
|
+
});
|
|
122
122
|
|
|
123
|
-
|
|
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');
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
});
|
|
187
|
+
});
|
|
192
188
|
|
|
193
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
});
|
|
237
|
+
});
|
|
243
238
|
|
|
244
|
-
|
|
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);
|
|
250
|
-
|
|
251
|
-
expect(
|
|
252
|
-
await waitFor(() => result.current.isCached
|
|
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
|
|
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:
|
|
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
|
-
});
|
|
274
|
+
});
|
|
281
275
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
276
|
+
// Wait for state to sync with indexedDB
|
|
277
|
+
await waitFor(() => expect(result.current.isCached).toBe(true));
|
|
278
|
+
rerender('id-two');
|
|
285
279
|
|
|
286
|
-
|
|
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
|
|
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 =
|
|
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';
|
|
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 {
|
|
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);
|
|
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 => ({
|
|
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 => ({
|
|
98
|
-
|
|
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 = {
|
|
106
|
+
const recordingStates = {
|
|
107
|
+
...state.recordingStates
|
|
105
108
|
};
|
|
106
109
|
delete recordingStates[id];
|
|
107
|
-
return {
|
|
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 => ({
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
flushSync(() => {
|
|
81
|
+
onRecordingCompleted();
|
|
82
|
+
});
|
|
78
83
|
onCompleted && onCompleted();
|
|
79
84
|
},
|
|
80
85
|
onError: error => {
|
|
81
|
-
|
|
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]);
|
|
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);
|
|
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
|
-
}
|
|
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 = {
|