@dhis2/app-service-offline 3.11.0-alpha.1 → 3.11.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/lib/__tests__/cacheable-section-state.test.js +55 -0
- package/build/cjs/lib/__tests__/use-cacheable-section.test.js +38 -10
- package/build/cjs/lib/cacheable-section-state.js +29 -24
- package/build/cjs/lib/cacheable-section.js +15 -19
- package/build/cjs/lib/global-state-service.js +11 -8
- package/build/es/lib/__tests__/cacheable-section-state.test.js +47 -0
- package/build/es/lib/__tests__/use-cacheable-section.test.js +38 -10
- package/build/es/lib/cacheable-section-state.js +25 -23
- package/build/es/lib/cacheable-section.js +16 -20
- package/build/es/lib/global-state-service.js +12 -9
- package/package.json +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _reactHooks = require("@testing-library/react-hooks");
|
|
4
|
+
|
|
5
|
+
var _react = _interopRequireDefault(require("react"));
|
|
6
|
+
|
|
7
|
+
var _testMocks = require("../../utils/test-mocks");
|
|
8
|
+
|
|
9
|
+
var _cacheableSectionState = require("../cacheable-section-state");
|
|
10
|
+
|
|
11
|
+
var _offlineProvider = require("../offline-provider");
|
|
12
|
+
|
|
13
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
14
|
+
|
|
15
|
+
const wrapper = (_ref) => {
|
|
16
|
+
let {
|
|
17
|
+
children
|
|
18
|
+
} = _ref;
|
|
19
|
+
return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
20
|
+
offlineInterface: _testMocks.mockOfflineInterface
|
|
21
|
+
}, children);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
test('useRecordingState has stable references', () => {
|
|
25
|
+
const {
|
|
26
|
+
result,
|
|
27
|
+
rerender
|
|
28
|
+
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSectionState.useRecordingState)('one'), {
|
|
29
|
+
wrapper
|
|
30
|
+
});
|
|
31
|
+
const origRecordingState = result.current.recordingState;
|
|
32
|
+
const origSetRecordingState = result.current.setRecordingState;
|
|
33
|
+
const origRemoveRecordingState = result.current.removeRecordingState;
|
|
34
|
+
rerender();
|
|
35
|
+
expect(result.current.recordingState).toBe(origRecordingState);
|
|
36
|
+
expect(result.current.setRecordingState).toBe(origSetRecordingState);
|
|
37
|
+
expect(result.current.removeRecordingState).toBe(origRemoveRecordingState);
|
|
38
|
+
});
|
|
39
|
+
test('useCachedSection has stable references', () => {
|
|
40
|
+
const {
|
|
41
|
+
result,
|
|
42
|
+
rerender
|
|
43
|
+
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSectionState.useCachedSection)('one'), {
|
|
44
|
+
wrapper
|
|
45
|
+
});
|
|
46
|
+
const origIsCached = result.current.isCached;
|
|
47
|
+
const origLastUpdated = result.current.lastUpdated;
|
|
48
|
+
const origRemove = result.current.remove;
|
|
49
|
+
const origSyncCachedSections = result.current.syncCachedSections;
|
|
50
|
+
rerender();
|
|
51
|
+
expect(result.current.isCached).toBe(origIsCached);
|
|
52
|
+
expect(result.current.lastUpdated).toBe(origLastUpdated);
|
|
53
|
+
expect(result.current.remove).toBe(origRemove);
|
|
54
|
+
expect(result.current.syncCachedSections).toBe(origSyncCachedSections);
|
|
55
|
+
});
|
|
@@ -53,6 +53,34 @@ it('renders in the default state initially', () => {
|
|
|
53
53
|
expect(result.current.isCached).toBe(false);
|
|
54
54
|
expect(result.current.lastUpdated).toBeUndefined();
|
|
55
55
|
});
|
|
56
|
+
it('has stable references', () => {
|
|
57
|
+
const wrapper = (_ref2) => {
|
|
58
|
+
let {
|
|
59
|
+
children
|
|
60
|
+
} = _ref2;
|
|
61
|
+
return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
62
|
+
offlineInterface: _testMocks.mockOfflineInterface
|
|
63
|
+
}, children);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
result,
|
|
68
|
+
rerender
|
|
69
|
+
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('one'), {
|
|
70
|
+
wrapper
|
|
71
|
+
});
|
|
72
|
+
const origRecordingState = result.current.recordingState;
|
|
73
|
+
const origStartRecording = result.current.startRecording;
|
|
74
|
+
const origLastUpdated = result.current.lastUpdated;
|
|
75
|
+
const origIsCached = result.current.isCached;
|
|
76
|
+
const origRemove = result.current.remove;
|
|
77
|
+
rerender();
|
|
78
|
+
expect(result.current.recordingState).toBe(origRecordingState);
|
|
79
|
+
expect(result.current.startRecording).toBe(origStartRecording);
|
|
80
|
+
expect(result.current.lastUpdated).toBe(origLastUpdated);
|
|
81
|
+
expect(result.current.isCached).toBe(origIsCached);
|
|
82
|
+
expect(result.current.remove).toBe(origRemove);
|
|
83
|
+
});
|
|
56
84
|
it('handles a successful recording', async done => {
|
|
57
85
|
const [sectionId, timeoutDelay] = ['one', 1234];
|
|
58
86
|
const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
|
|
@@ -62,10 +90,10 @@ it('handles a successful recording', async done => {
|
|
|
62
90
|
}])
|
|
63
91
|
};
|
|
64
92
|
|
|
65
|
-
const wrapper = (
|
|
93
|
+
const wrapper = (_ref3) => {
|
|
66
94
|
let {
|
|
67
95
|
children
|
|
68
|
-
} =
|
|
96
|
+
} = _ref3;
|
|
69
97
|
return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
70
98
|
offlineInterface: testOfflineInterface
|
|
71
99
|
}, children);
|
|
@@ -134,10 +162,10 @@ it('handles a recording that encounters an error', async done => {
|
|
|
134
162
|
startRecording: _testMocks.errorRecordingMock
|
|
135
163
|
};
|
|
136
164
|
|
|
137
|
-
const wrapper = (
|
|
165
|
+
const wrapper = (_ref4) => {
|
|
138
166
|
let {
|
|
139
167
|
children
|
|
140
|
-
} =
|
|
168
|
+
} = _ref4;
|
|
141
169
|
return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
142
170
|
offlineInterface: testOfflineInterface
|
|
143
171
|
}, children);
|
|
@@ -180,10 +208,10 @@ it('handles an error starting the recording', async () => {
|
|
|
180
208
|
startRecording: _testMocks.failedMessageRecordingMock
|
|
181
209
|
};
|
|
182
210
|
|
|
183
|
-
const wrapper = (
|
|
211
|
+
const wrapper = (_ref5) => {
|
|
184
212
|
let {
|
|
185
213
|
children
|
|
186
|
-
} =
|
|
214
|
+
} = _ref5;
|
|
187
215
|
return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
188
216
|
offlineInterface: testOfflineInterface
|
|
189
217
|
}, children);
|
|
@@ -206,10 +234,10 @@ it('handles remove and updates sections', async () => {
|
|
|
206
234
|
}]).mockResolvedValueOnce([])
|
|
207
235
|
};
|
|
208
236
|
|
|
209
|
-
const wrapper = (
|
|
237
|
+
const wrapper = (_ref6) => {
|
|
210
238
|
let {
|
|
211
239
|
children
|
|
212
|
-
} =
|
|
240
|
+
} = _ref6;
|
|
213
241
|
return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
214
242
|
offlineInterface: testOfflineInterface
|
|
215
243
|
}, children);
|
|
@@ -242,10 +270,10 @@ it('handles a change in ID', async () => {
|
|
|
242
270
|
}])
|
|
243
271
|
};
|
|
244
272
|
|
|
245
|
-
const wrapper = (
|
|
273
|
+
const wrapper = (_ref7) => {
|
|
246
274
|
let {
|
|
247
275
|
children
|
|
248
|
-
} =
|
|
276
|
+
} = _ref7;
|
|
249
277
|
return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
250
278
|
offlineInterface: testOfflineInterface
|
|
251
279
|
}, children);
|
|
@@ -11,12 +11,16 @@ exports.useRecordingState = useRecordingState;
|
|
|
11
11
|
|
|
12
12
|
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
13
13
|
|
|
14
|
-
var _react =
|
|
14
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
15
15
|
|
|
16
16
|
var _globalStateService = require("./global-state-service");
|
|
17
17
|
|
|
18
18
|
var _offlineInterface = require("./offline-interface");
|
|
19
19
|
|
|
20
|
+
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
|
|
21
|
+
|
|
22
|
+
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
23
|
+
|
|
20
24
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
21
25
|
|
|
22
26
|
/**
|
|
@@ -83,7 +87,7 @@ function CacheableSectionProvider(_ref2) {
|
|
|
83
87
|
const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
|
|
84
88
|
const store = useConst(createCacheableSectionStore); // On load, get sections and add to store
|
|
85
89
|
|
|
86
|
-
_react.
|
|
90
|
+
(0, _react.useEffect)(() => {
|
|
87
91
|
if (offlineInterface) {
|
|
88
92
|
offlineInterface.getCachedSections().then(sections => {
|
|
89
93
|
store.mutate(state => ({ ...state,
|
|
@@ -92,7 +96,6 @@ function CacheableSectionProvider(_ref2) {
|
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
}, [store, offlineInterface]);
|
|
95
|
-
|
|
96
99
|
return /*#__PURE__*/_react.default.createElement(_globalStateService.GlobalStateProvider, {
|
|
97
100
|
store: store
|
|
98
101
|
}, children);
|
|
@@ -110,25 +113,28 @@ CacheableSectionProvider.propTypes = {
|
|
|
110
113
|
* @returns {Object} { recordingState: String, setRecordingState: Function, removeRecordingState: Function}
|
|
111
114
|
*/
|
|
112
115
|
function useRecordingState(id) {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
116
|
+
const recordingStateSelector = (0, _react.useCallback)(state => state.recordingStates[id], [id]);
|
|
117
|
+
const [recordingState] = (0, _globalStateService.useGlobalState)(recordingStateSelector);
|
|
118
|
+
const setRecordingStateMutationCreator = (0, _react.useCallback)(newState => state => ({ ...state,
|
|
115
119
|
recordingStates: { ...state.recordingStates,
|
|
116
120
|
[id]: newState
|
|
117
121
|
}
|
|
118
|
-
}));
|
|
119
|
-
const
|
|
122
|
+
}), [id]);
|
|
123
|
+
const setRecordingState = (0, _globalStateService.useGlobalStateMutation)(setRecordingStateMutationCreator);
|
|
124
|
+
const removeRecordingStateMutationCreator = (0, _react.useCallback)(() => state => {
|
|
120
125
|
const recordingStates = { ...state.recordingStates
|
|
121
126
|
};
|
|
122
127
|
delete recordingStates[id];
|
|
123
128
|
return { ...state,
|
|
124
129
|
recordingStates
|
|
125
130
|
};
|
|
126
|
-
});
|
|
127
|
-
|
|
131
|
+
}, [id]);
|
|
132
|
+
const removeRecordingState = (0, _globalStateService.useGlobalStateMutation)(removeRecordingStateMutationCreator);
|
|
133
|
+
return (0, _react.useMemo)(() => ({
|
|
128
134
|
recordingState,
|
|
129
135
|
setRecordingState,
|
|
130
136
|
removeRecordingState
|
|
131
|
-
};
|
|
137
|
+
}), [recordingState, setRecordingState, removeRecordingState]);
|
|
132
138
|
}
|
|
133
139
|
/**
|
|
134
140
|
* Returns a function that syncs cached sections in the global state
|
|
@@ -140,13 +146,14 @@ function useRecordingState(id) {
|
|
|
140
146
|
|
|
141
147
|
function useSyncCachedSections() {
|
|
142
148
|
const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
|
|
143
|
-
const
|
|
149
|
+
const setCachedSectionsMutationCreator = (0, _react.useCallback)(cachedSections => state => ({ ...state,
|
|
144
150
|
cachedSections
|
|
145
|
-
}));
|
|
146
|
-
|
|
151
|
+
}), []);
|
|
152
|
+
const setCachedSections = (0, _globalStateService.useGlobalStateMutation)(setCachedSectionsMutationCreator);
|
|
153
|
+
return (0, _react.useCallback)(async () => {
|
|
147
154
|
const sections = await offlineInterface.getCachedSections();
|
|
148
155
|
setCachedSections(getSectionsById(sections));
|
|
149
|
-
};
|
|
156
|
+
}, [offlineInterface, setCachedSections]);
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
/**
|
|
@@ -166,7 +173,7 @@ function useCachedSections() {
|
|
|
166
173
|
* deleted, or `false` if asection with the specified ID does not exist.
|
|
167
174
|
*/
|
|
168
175
|
|
|
169
|
-
|
|
176
|
+
const removeById = (0, _react.useCallback)(async id => {
|
|
170
177
|
const success = await offlineInterface.removeSection(id);
|
|
171
178
|
|
|
172
179
|
if (success) {
|
|
@@ -174,13 +181,12 @@ function useCachedSections() {
|
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
return success;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return {
|
|
184
|
+
}, [offlineInterface, syncCachedSections]);
|
|
185
|
+
return (0, _react.useMemo)(() => ({
|
|
180
186
|
cachedSections,
|
|
181
187
|
removeById,
|
|
182
188
|
syncCachedSections
|
|
183
|
-
};
|
|
189
|
+
}), [cachedSections, removeById, syncCachedSections]);
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
/**
|
|
@@ -203,7 +209,7 @@ function useCachedSection(id) {
|
|
|
203
209
|
* section with the specified ID does not exist.
|
|
204
210
|
*/
|
|
205
211
|
|
|
206
|
-
|
|
212
|
+
const remove = (0, _react.useCallback)(async () => {
|
|
207
213
|
const success = await offlineInterface.removeSection(id);
|
|
208
214
|
|
|
209
215
|
if (success) {
|
|
@@ -211,12 +217,11 @@ function useCachedSection(id) {
|
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
return success;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
220
|
+
}, [offlineInterface, id, syncCachedSections]);
|
|
221
|
+
return (0, _react.useMemo)(() => ({
|
|
217
222
|
lastUpdated,
|
|
218
223
|
isCached: !!lastUpdated,
|
|
219
224
|
remove,
|
|
220
225
|
syncCachedSections
|
|
221
|
-
};
|
|
226
|
+
}), [lastUpdated, remove, syncCachedSections]);
|
|
222
227
|
}
|
|
@@ -63,7 +63,18 @@ function useCacheableSection(id) {
|
|
|
63
63
|
};
|
|
64
64
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
const onRecordingStarted = (0, _react.useCallback)(() => {
|
|
67
|
+
setRecordingState(recordingStates.recording);
|
|
68
|
+
}, [setRecordingState]);
|
|
69
|
+
const onRecordingCompleted = (0, _react.useCallback)(() => {
|
|
70
|
+
setRecordingState(recordingStates.default);
|
|
71
|
+
syncCachedSections();
|
|
72
|
+
}, [setRecordingState, syncCachedSections]);
|
|
73
|
+
const onRecordingError = (0, _react.useCallback)(error => {
|
|
74
|
+
console.error('Error during recording:', error);
|
|
75
|
+
setRecordingState(recordingStates.error);
|
|
76
|
+
}, [setRecordingState]);
|
|
77
|
+
const startRecording = (0, _react.useCallback)(function () {
|
|
67
78
|
let {
|
|
68
79
|
recordingTimeoutDelay = 1000,
|
|
69
80
|
onStarted,
|
|
@@ -89,31 +100,16 @@ function useCacheableSection(id) {
|
|
|
89
100
|
onError && onError(error);
|
|
90
101
|
}
|
|
91
102
|
}).then(() => setRecordingState(recordingStates.pending));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function onRecordingStarted() {
|
|
95
|
-
setRecordingState(recordingStates.recording);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function onRecordingCompleted() {
|
|
99
|
-
setRecordingState(recordingStates.default);
|
|
100
|
-
syncCachedSections();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function onRecordingError(error) {
|
|
104
|
-
console.error('Error during recording:', error);
|
|
105
|
-
setRecordingState(recordingStates.error);
|
|
106
|
-
} // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
|
|
103
|
+
}, [id, offlineInterface, onRecordingCompleted, onRecordingError, onRecordingStarted, setRecordingState]); // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
|
|
107
104
|
// but provided through this hook for convenience
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
return {
|
|
106
|
+
return (0, _react.useMemo)(() => ({
|
|
111
107
|
recordingState,
|
|
112
108
|
startRecording,
|
|
113
109
|
lastUpdated,
|
|
114
110
|
isCached,
|
|
115
111
|
remove
|
|
116
|
-
};
|
|
112
|
+
}), [recordingState, startRecording, lastUpdated, isCached, remove]);
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
/**
|
|
@@ -74,12 +74,17 @@ const useGlobalState = function () {
|
|
|
74
74
|
(0, _react.useEffect)(() => {
|
|
75
75
|
// NEW: deep equality check before updating
|
|
76
76
|
const callback = state => {
|
|
77
|
-
const newSelectedState = selector(state); //
|
|
78
|
-
// deleted, but state does not update
|
|
77
|
+
const newSelectedState = selector(state); // Use this form to avoid having `selectedState` as a dep in here
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
setSelectedState(currentSelectedState => {
|
|
80
|
+
// Second condition handles case where a selected object gets
|
|
81
|
+
// deleted, but state does not update
|
|
82
|
+
if (!(0, _isEqual.default)(currentSelectedState, newSelectedState) || currentSelectedState === undefined) {
|
|
83
|
+
return newSelectedState;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return currentSelectedState;
|
|
87
|
+
});
|
|
83
88
|
};
|
|
84
89
|
|
|
85
90
|
store.subscribe(callback); // Make sure to update state when selector changes
|
|
@@ -87,9 +92,7 @@ const useGlobalState = function () {
|
|
|
87
92
|
callback(store.getState());
|
|
88
93
|
return () => store.unsubscribe(callback);
|
|
89
94
|
}, [store, selector]);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return [selectedState, store.mutate];
|
|
95
|
+
return (0, _react.useMemo)(() => [selectedState, store.mutate], [selectedState, store.mutate]);
|
|
93
96
|
};
|
|
94
97
|
|
|
95
98
|
exports.useGlobalState = useGlobalState;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { mockOfflineInterface } from '../../utils/test-mocks';
|
|
4
|
+
import { useCachedSection, useRecordingState } from '../cacheable-section-state';
|
|
5
|
+
import { OfflineProvider } from '../offline-provider';
|
|
6
|
+
|
|
7
|
+
const wrapper = (_ref) => {
|
|
8
|
+
let {
|
|
9
|
+
children
|
|
10
|
+
} = _ref;
|
|
11
|
+
return /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
12
|
+
offlineInterface: mockOfflineInterface
|
|
13
|
+
}, children);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
test('useRecordingState has stable references', () => {
|
|
17
|
+
const {
|
|
18
|
+
result,
|
|
19
|
+
rerender
|
|
20
|
+
} = renderHook(() => useRecordingState('one'), {
|
|
21
|
+
wrapper
|
|
22
|
+
});
|
|
23
|
+
const origRecordingState = result.current.recordingState;
|
|
24
|
+
const origSetRecordingState = result.current.setRecordingState;
|
|
25
|
+
const origRemoveRecordingState = result.current.removeRecordingState;
|
|
26
|
+
rerender();
|
|
27
|
+
expect(result.current.recordingState).toBe(origRecordingState);
|
|
28
|
+
expect(result.current.setRecordingState).toBe(origSetRecordingState);
|
|
29
|
+
expect(result.current.removeRecordingState).toBe(origRemoveRecordingState);
|
|
30
|
+
});
|
|
31
|
+
test('useCachedSection has stable references', () => {
|
|
32
|
+
const {
|
|
33
|
+
result,
|
|
34
|
+
rerender
|
|
35
|
+
} = renderHook(() => useCachedSection('one'), {
|
|
36
|
+
wrapper
|
|
37
|
+
});
|
|
38
|
+
const origIsCached = result.current.isCached;
|
|
39
|
+
const origLastUpdated = result.current.lastUpdated;
|
|
40
|
+
const origRemove = result.current.remove;
|
|
41
|
+
const origSyncCachedSections = result.current.syncCachedSections;
|
|
42
|
+
rerender();
|
|
43
|
+
expect(result.current.isCached).toBe(origIsCached);
|
|
44
|
+
expect(result.current.lastUpdated).toBe(origLastUpdated);
|
|
45
|
+
expect(result.current.remove).toBe(origRemove);
|
|
46
|
+
expect(result.current.syncCachedSections).toBe(origSyncCachedSections);
|
|
47
|
+
});
|
|
@@ -44,6 +44,34 @@ it('renders in the default state initially', () => {
|
|
|
44
44
|
expect(result.current.isCached).toBe(false);
|
|
45
45
|
expect(result.current.lastUpdated).toBeUndefined();
|
|
46
46
|
});
|
|
47
|
+
it('has stable references', () => {
|
|
48
|
+
const wrapper = (_ref2) => {
|
|
49
|
+
let {
|
|
50
|
+
children
|
|
51
|
+
} = _ref2;
|
|
52
|
+
return /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
53
|
+
offlineInterface: mockOfflineInterface
|
|
54
|
+
}, children);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
result,
|
|
59
|
+
rerender
|
|
60
|
+
} = renderHook(() => useCacheableSection('one'), {
|
|
61
|
+
wrapper
|
|
62
|
+
});
|
|
63
|
+
const origRecordingState = result.current.recordingState;
|
|
64
|
+
const origStartRecording = result.current.startRecording;
|
|
65
|
+
const origLastUpdated = result.current.lastUpdated;
|
|
66
|
+
const origIsCached = result.current.isCached;
|
|
67
|
+
const origRemove = result.current.remove;
|
|
68
|
+
rerender();
|
|
69
|
+
expect(result.current.recordingState).toBe(origRecordingState);
|
|
70
|
+
expect(result.current.startRecording).toBe(origStartRecording);
|
|
71
|
+
expect(result.current.lastUpdated).toBe(origLastUpdated);
|
|
72
|
+
expect(result.current.isCached).toBe(origIsCached);
|
|
73
|
+
expect(result.current.remove).toBe(origRemove);
|
|
74
|
+
});
|
|
47
75
|
it('handles a successful recording', async done => {
|
|
48
76
|
const [sectionId, timeoutDelay] = ['one', 1234];
|
|
49
77
|
const testOfflineInterface = { ...mockOfflineInterface,
|
|
@@ -53,10 +81,10 @@ it('handles a successful recording', async done => {
|
|
|
53
81
|
}])
|
|
54
82
|
};
|
|
55
83
|
|
|
56
|
-
const wrapper = (
|
|
84
|
+
const wrapper = (_ref3) => {
|
|
57
85
|
let {
|
|
58
86
|
children
|
|
59
|
-
} =
|
|
87
|
+
} = _ref3;
|
|
60
88
|
return /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
61
89
|
offlineInterface: testOfflineInterface
|
|
62
90
|
}, children);
|
|
@@ -125,10 +153,10 @@ it('handles a recording that encounters an error', async done => {
|
|
|
125
153
|
startRecording: errorRecordingMock
|
|
126
154
|
};
|
|
127
155
|
|
|
128
|
-
const wrapper = (
|
|
156
|
+
const wrapper = (_ref4) => {
|
|
129
157
|
let {
|
|
130
158
|
children
|
|
131
|
-
} =
|
|
159
|
+
} = _ref4;
|
|
132
160
|
return /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
133
161
|
offlineInterface: testOfflineInterface
|
|
134
162
|
}, children);
|
|
@@ -171,10 +199,10 @@ it('handles an error starting the recording', async () => {
|
|
|
171
199
|
startRecording: failedMessageRecordingMock
|
|
172
200
|
};
|
|
173
201
|
|
|
174
|
-
const wrapper = (
|
|
202
|
+
const wrapper = (_ref5) => {
|
|
175
203
|
let {
|
|
176
204
|
children
|
|
177
|
-
} =
|
|
205
|
+
} = _ref5;
|
|
178
206
|
return /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
179
207
|
offlineInterface: testOfflineInterface
|
|
180
208
|
}, children);
|
|
@@ -197,10 +225,10 @@ it('handles remove and updates sections', async () => {
|
|
|
197
225
|
}]).mockResolvedValueOnce([])
|
|
198
226
|
};
|
|
199
227
|
|
|
200
|
-
const wrapper = (
|
|
228
|
+
const wrapper = (_ref6) => {
|
|
201
229
|
let {
|
|
202
230
|
children
|
|
203
|
-
} =
|
|
231
|
+
} = _ref6;
|
|
204
232
|
return /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
205
233
|
offlineInterface: testOfflineInterface
|
|
206
234
|
}, children);
|
|
@@ -233,10 +261,10 @@ it('handles a change in ID', async () => {
|
|
|
233
261
|
}])
|
|
234
262
|
};
|
|
235
263
|
|
|
236
|
-
const wrapper = (
|
|
264
|
+
const wrapper = (_ref7) => {
|
|
237
265
|
let {
|
|
238
266
|
children
|
|
239
|
-
} =
|
|
267
|
+
} = _ref7;
|
|
240
268
|
return /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
241
269
|
offlineInterface: testOfflineInterface
|
|
242
270
|
}, children);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PropTypes from 'prop-types';
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useEffect, useCallback, useMemo } from 'react';
|
|
3
3
|
import { createStore, useGlobalState, useGlobalStateMutation, GlobalStateProvider } from './global-state-service';
|
|
4
4
|
import { useOfflineInterface } from './offline-interface'; // Functions in here use the global state service to manage cacheable section
|
|
5
5
|
// state in a performant way
|
|
@@ -67,7 +67,7 @@ export function CacheableSectionProvider(_ref2) {
|
|
|
67
67
|
const offlineInterface = useOfflineInterface();
|
|
68
68
|
const store = useConst(createCacheableSectionStore); // On load, get sections and add to store
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
useEffect(() => {
|
|
71
71
|
if (offlineInterface) {
|
|
72
72
|
offlineInterface.getCachedSections().then(sections => {
|
|
73
73
|
store.mutate(state => ({ ...state,
|
|
@@ -92,25 +92,28 @@ CacheableSectionProvider.propTypes = {
|
|
|
92
92
|
* @returns {Object} { recordingState: String, setRecordingState: Function, removeRecordingState: Function}
|
|
93
93
|
*/
|
|
94
94
|
export function useRecordingState(id) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
95
|
+
const recordingStateSelector = useCallback(state => state.recordingStates[id], [id]);
|
|
96
|
+
const [recordingState] = useGlobalState(recordingStateSelector);
|
|
97
|
+
const setRecordingStateMutationCreator = useCallback(newState => state => ({ ...state,
|
|
97
98
|
recordingStates: { ...state.recordingStates,
|
|
98
99
|
[id]: newState
|
|
99
100
|
}
|
|
100
|
-
}));
|
|
101
|
-
const
|
|
101
|
+
}), [id]);
|
|
102
|
+
const setRecordingState = useGlobalStateMutation(setRecordingStateMutationCreator);
|
|
103
|
+
const removeRecordingStateMutationCreator = useCallback(() => state => {
|
|
102
104
|
const recordingStates = { ...state.recordingStates
|
|
103
105
|
};
|
|
104
106
|
delete recordingStates[id];
|
|
105
107
|
return { ...state,
|
|
106
108
|
recordingStates
|
|
107
109
|
};
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
+
}, [id]);
|
|
111
|
+
const removeRecordingState = useGlobalStateMutation(removeRecordingStateMutationCreator);
|
|
112
|
+
return useMemo(() => ({
|
|
110
113
|
recordingState,
|
|
111
114
|
setRecordingState,
|
|
112
115
|
removeRecordingState
|
|
113
|
-
};
|
|
116
|
+
}), [recordingState, setRecordingState, removeRecordingState]);
|
|
114
117
|
}
|
|
115
118
|
/**
|
|
116
119
|
* Returns a function that syncs cached sections in the global state
|
|
@@ -121,13 +124,14 @@ export function useRecordingState(id) {
|
|
|
121
124
|
|
|
122
125
|
function useSyncCachedSections() {
|
|
123
126
|
const offlineInterface = useOfflineInterface();
|
|
124
|
-
const
|
|
127
|
+
const setCachedSectionsMutationCreator = useCallback(cachedSections => state => ({ ...state,
|
|
125
128
|
cachedSections
|
|
126
|
-
}));
|
|
127
|
-
|
|
129
|
+
}), []);
|
|
130
|
+
const setCachedSections = useGlobalStateMutation(setCachedSectionsMutationCreator);
|
|
131
|
+
return useCallback(async () => {
|
|
128
132
|
const sections = await offlineInterface.getCachedSections();
|
|
129
133
|
setCachedSections(getSectionsById(sections));
|
|
130
|
-
};
|
|
134
|
+
}, [offlineInterface, setCachedSections]);
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
/**
|
|
@@ -147,7 +151,7 @@ export function useCachedSections() {
|
|
|
147
151
|
* deleted, or `false` if asection with the specified ID does not exist.
|
|
148
152
|
*/
|
|
149
153
|
|
|
150
|
-
|
|
154
|
+
const removeById = useCallback(async id => {
|
|
151
155
|
const success = await offlineInterface.removeSection(id);
|
|
152
156
|
|
|
153
157
|
if (success) {
|
|
@@ -155,13 +159,12 @@ export function useCachedSections() {
|
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
return success;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return {
|
|
162
|
+
}, [offlineInterface, syncCachedSections]);
|
|
163
|
+
return useMemo(() => ({
|
|
161
164
|
cachedSections,
|
|
162
165
|
removeById,
|
|
163
166
|
syncCachedSections
|
|
164
|
-
};
|
|
167
|
+
}), [cachedSections, removeById, syncCachedSections]);
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
/**
|
|
@@ -184,7 +187,7 @@ export function useCachedSection(id) {
|
|
|
184
187
|
* section with the specified ID does not exist.
|
|
185
188
|
*/
|
|
186
189
|
|
|
187
|
-
|
|
190
|
+
const remove = useCallback(async () => {
|
|
188
191
|
const success = await offlineInterface.removeSection(id);
|
|
189
192
|
|
|
190
193
|
if (success) {
|
|
@@ -192,12 +195,11 @@ export function useCachedSection(id) {
|
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
return success;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
+
}, [offlineInterface, id, syncCachedSections]);
|
|
199
|
+
return useMemo(() => ({
|
|
198
200
|
lastUpdated,
|
|
199
201
|
isCached: !!lastUpdated,
|
|
200
202
|
remove,
|
|
201
203
|
syncCachedSections
|
|
202
|
-
};
|
|
204
|
+
}), [lastUpdated, remove, syncCachedSections]);
|
|
203
205
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PropTypes from 'prop-types';
|
|
2
|
-
import React, { useEffect } from 'react';
|
|
2
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
3
3
|
import { useRecordingState, useCachedSection } from './cacheable-section-state';
|
|
4
4
|
import { useOfflineInterface } from './offline-interface';
|
|
5
5
|
const recordingStates = {
|
|
@@ -45,7 +45,18 @@ export function useCacheableSection(id) {
|
|
|
45
45
|
};
|
|
46
46
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
const onRecordingStarted = useCallback(() => {
|
|
49
|
+
setRecordingState(recordingStates.recording);
|
|
50
|
+
}, [setRecordingState]);
|
|
51
|
+
const onRecordingCompleted = useCallback(() => {
|
|
52
|
+
setRecordingState(recordingStates.default);
|
|
53
|
+
syncCachedSections();
|
|
54
|
+
}, [setRecordingState, syncCachedSections]);
|
|
55
|
+
const onRecordingError = useCallback(error => {
|
|
56
|
+
console.error('Error during recording:', error);
|
|
57
|
+
setRecordingState(recordingStates.error);
|
|
58
|
+
}, [setRecordingState]);
|
|
59
|
+
const startRecording = useCallback(function () {
|
|
49
60
|
let {
|
|
50
61
|
recordingTimeoutDelay = 1000,
|
|
51
62
|
onStarted,
|
|
@@ -71,31 +82,16 @@ export function useCacheableSection(id) {
|
|
|
71
82
|
onError && onError(error);
|
|
72
83
|
}
|
|
73
84
|
}).then(() => setRecordingState(recordingStates.pending));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function onRecordingStarted() {
|
|
77
|
-
setRecordingState(recordingStates.recording);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function onRecordingCompleted() {
|
|
81
|
-
setRecordingState(recordingStates.default);
|
|
82
|
-
syncCachedSections();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function onRecordingError(error) {
|
|
86
|
-
console.error('Error during recording:', error);
|
|
87
|
-
setRecordingState(recordingStates.error);
|
|
88
|
-
} // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
|
|
85
|
+
}, [id, offlineInterface, onRecordingCompleted, onRecordingError, onRecordingStarted, setRecordingState]); // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
|
|
89
86
|
// but provided through this hook for convenience
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
return {
|
|
88
|
+
return useMemo(() => ({
|
|
93
89
|
recordingState,
|
|
94
90
|
startRecording,
|
|
95
91
|
lastUpdated,
|
|
96
92
|
isCached,
|
|
97
93
|
remove
|
|
98
|
-
};
|
|
94
|
+
}), [recordingState, startRecording, lastUpdated, isCached, remove]);
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import isEqual from 'lodash/isEqual';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import React, { useEffect, useCallback, useContext, useState } from 'react';
|
|
3
|
+
import React, { useEffect, useCallback, useContext, useState, useMemo } from 'react';
|
|
4
4
|
|
|
5
5
|
// This file creates a redux-like state management service using React context
|
|
6
6
|
// that minimizes unnecessary rerenders that consume the context.
|
|
@@ -52,12 +52,17 @@ export const useGlobalState = function () {
|
|
|
52
52
|
useEffect(() => {
|
|
53
53
|
// NEW: deep equality check before updating
|
|
54
54
|
const callback = state => {
|
|
55
|
-
const newSelectedState = selector(state); //
|
|
56
|
-
// deleted, but state does not update
|
|
55
|
+
const newSelectedState = selector(state); // Use this form to avoid having `selectedState` as a dep in here
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
setSelectedState(currentSelectedState => {
|
|
58
|
+
// Second condition handles case where a selected object gets
|
|
59
|
+
// deleted, but state does not update
|
|
60
|
+
if (!isEqual(currentSelectedState, newSelectedState) || currentSelectedState === undefined) {
|
|
61
|
+
return newSelectedState;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return currentSelectedState;
|
|
65
|
+
});
|
|
61
66
|
};
|
|
62
67
|
|
|
63
68
|
store.subscribe(callback); // Make sure to update state when selector changes
|
|
@@ -65,9 +70,7 @@ export const useGlobalState = function () {
|
|
|
65
70
|
callback(store.getState());
|
|
66
71
|
return () => store.unsubscribe(callback);
|
|
67
72
|
}, [store, selector]);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return [selectedState, store.mutate];
|
|
73
|
+
return useMemo(() => [selectedState, store.mutate], [selectedState, store.mutate]);
|
|
71
74
|
};
|
|
72
75
|
export function useGlobalStateMutation(mutationCreator) {
|
|
73
76
|
const store = useGlobalStateStore();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2/app-service-offline",
|
|
3
3
|
"description": "A runtime service for online/offline detection and offline caching",
|
|
4
|
-
"version": "3.11.
|
|
4
|
+
"version": "3.11.1",
|
|
5
5
|
"main": "./build/cjs/index.js",
|
|
6
6
|
"module": "./build/es/index.js",
|
|
7
7
|
"types": "build/types/index.d.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"coverage": "yarn test --coverage"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@dhis2/app-service-config": "3.11.
|
|
36
|
+
"@dhis2/app-service-config": "3.11.1",
|
|
37
37
|
"prop-types": "^15.7.2",
|
|
38
38
|
"react": "^16.8.6",
|
|
39
39
|
"react-dom": "^16.8.6"
|