@dhis2/app-service-offline 2.10.0 → 2.12.0

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 (41) hide show
  1. package/build/cjs/__tests__/integration.test.js +337 -0
  2. package/build/cjs/index.js +39 -1
  3. package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +131 -0
  4. package/build/cjs/lib/__tests__/offline-provider.test.js +127 -0
  5. package/build/cjs/lib/__tests__/use-cacheable-section.test.js +227 -0
  6. package/build/cjs/lib/cacheable-section-state.js +218 -0
  7. package/build/cjs/lib/cacheable-section.js +156 -0
  8. package/build/cjs/lib/clear-sensitive-caches.js +87 -0
  9. package/build/cjs/lib/global-state-service.js +95 -0
  10. package/build/cjs/lib/offline-interface.js +86 -0
  11. package/build/cjs/lib/offline-provider.js +53 -0
  12. package/build/cjs/types.js +0 -1
  13. package/build/cjs/utils/__tests__/render-counter.test.js +55 -0
  14. package/build/cjs/utils/render-counter.js +26 -0
  15. package/build/cjs/utils/test-mocks.js +40 -0
  16. package/build/es/__tests__/integration.test.js +327 -0
  17. package/build/es/index.js +5 -1
  18. package/build/es/lib/__tests__/clear-sensitive-caches.test.js +123 -0
  19. package/build/es/lib/__tests__/offline-provider.test.js +117 -0
  20. package/build/es/lib/__tests__/use-cacheable-section.test.js +218 -0
  21. package/build/es/lib/cacheable-section-state.js +199 -0
  22. package/build/es/lib/cacheable-section.js +137 -0
  23. package/build/es/lib/clear-sensitive-caches.js +78 -0
  24. package/build/es/lib/global-state-service.js +70 -0
  25. package/build/es/lib/offline-interface.js +65 -0
  26. package/build/es/lib/offline-provider.js +40 -0
  27. package/build/es/types.js +0 -1
  28. package/build/es/utils/__tests__/render-counter.test.js +40 -0
  29. package/build/es/utils/render-counter.js +11 -0
  30. package/build/es/utils/test-mocks.js +30 -0
  31. package/build/types/index.d.ts +4 -0
  32. package/build/types/lib/cacheable-section-state.d.ts +66 -0
  33. package/build/types/lib/cacheable-section.d.ts +52 -0
  34. package/build/types/lib/clear-sensitive-caches.d.ts +16 -0
  35. package/build/types/lib/global-state-service.d.ts +16 -0
  36. package/build/types/lib/offline-interface.d.ts +26 -0
  37. package/build/types/lib/offline-provider.d.ts +19 -0
  38. package/build/types/types.d.ts +50 -0
  39. package/build/types/utils/render-counter.d.ts +10 -0
  40. package/build/types/utils/test-mocks.d.ts +11 -0
  41. package/package.json +2 -2
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ var _react = require("@testing-library/react");
4
+
5
+ var _react2 = _interopRequireDefault(require("react"));
6
+
7
+ var _testMocks = require("../../utils/test-mocks");
8
+
9
+ var _cacheableSection = require("../cacheable-section");
10
+
11
+ var _cacheableSectionState = require("../cacheable-section-state");
12
+
13
+ var _offlineProvider = require("../offline-provider");
14
+
15
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
+
17
+ // Suppress 'act' warning for these tests
18
+ const originalError = console.error;
19
+ beforeEach(() => {
20
+ jest.spyOn(console, 'error').mockImplementation((...args) => {
21
+ const pattern = /Warning: An update to .* inside a test was not wrapped in act/;
22
+
23
+ if (typeof args[0] === 'string' && pattern.test(args[0])) {
24
+ return;
25
+ }
26
+
27
+ return originalError.call(console, ...args);
28
+ });
29
+ });
30
+ afterEach(() => {
31
+ jest.clearAllMocks() // syntax needed to appease typescript
32
+ ;
33
+ console.error.mockRestore();
34
+ });
35
+ describe('Testing offline provider', () => {
36
+ it('Should render without failing', () => {
37
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
38
+ offlineInterface: _testMocks.mockOfflineInterface
39
+ }, /*#__PURE__*/_react2.default.createElement("div", {
40
+ "data-testid": "test-div"
41
+ })));
42
+ expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
43
+ });
44
+ it('Should initialize the offline interface with an update prompt', () => {
45
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
46
+ offlineInterface: _testMocks.mockOfflineInterface
47
+ }));
48
+ expect(_testMocks.mockOfflineInterface.init).toHaveBeenCalledTimes(1); // Expect to have been called with a 'promptUpdate' function
49
+
50
+ const arg = _testMocks.mockOfflineInterface.init.mock.calls[0][0];
51
+ expect(arg).toHaveProperty('promptUpdate');
52
+ expect(typeof arg['promptUpdate']).toBe('function');
53
+ });
54
+ it('Should sync cached sections with indexedDB', async () => {
55
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
56
+ getCachedSections: jest.fn().mockResolvedValue([{
57
+ sectionId: '1',
58
+ lastUpdated: 'date1'
59
+ }, {
60
+ sectionId: '2',
61
+ lastUpdated: 'date2'
62
+ }])
63
+ };
64
+
65
+ const CachedSections = () => {
66
+ const {
67
+ cachedSections
68
+ } = (0, _cacheableSectionState.useCachedSections)();
69
+ return /*#__PURE__*/_react2.default.createElement("div", {
70
+ "data-testid": "sections"
71
+ }, JSON.stringify(cachedSections));
72
+ };
73
+
74
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
75
+ offlineInterface: testOfflineInterface
76
+ }, /*#__PURE__*/_react2.default.createElement(CachedSections, null)));
77
+ const {
78
+ getByTestId
79
+ } = _react.screen;
80
+ expect(testOfflineInterface.getCachedSections).toHaveBeenCalled();
81
+ await (0, _react.waitFor)(() => getByTestId('sections').textContent !== '{}');
82
+ const textContent = JSON.parse(getByTestId('sections').textContent || '');
83
+ expect(textContent).toEqual({
84
+ 1: {
85
+ lastUpdated: 'date1'
86
+ },
87
+ 2: {
88
+ lastUpdated: 'date2'
89
+ }
90
+ });
91
+ });
92
+ it('Should provide the relevant contexts to consumers', () => {
93
+ const TestConsumer = () => {
94
+ (0, _cacheableSection.useCacheableSection)('id');
95
+ return /*#__PURE__*/_react2.default.createElement(_cacheableSection.CacheableSection, {
96
+ loadingMask: /*#__PURE__*/_react2.default.createElement("div", null),
97
+ id: 'id'
98
+ }, /*#__PURE__*/_react2.default.createElement("div", {
99
+ "data-testid": "test-div"
100
+ }));
101
+ };
102
+
103
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
104
+ offlineInterface: _testMocks.mockOfflineInterface
105
+ }, /*#__PURE__*/_react2.default.createElement(TestConsumer, null)));
106
+ expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
107
+ });
108
+ it('Should render without failing when no offlineInterface is provided', () => {
109
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, null, /*#__PURE__*/_react2.default.createElement("div", {
110
+ "data-testid": "test-div"
111
+ })));
112
+ expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
113
+ });
114
+ it('Should render without failing if PWA is not enabled', () => {
115
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
116
+ pwaEnabled: false
117
+ };
118
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
119
+ offlineInterface: testOfflineInterface
120
+ }, /*#__PURE__*/_react2.default.createElement("div", {
121
+ "data-testid": "test-div"
122
+ }))); // Init should still be called - see comments in offline-provider.js
123
+
124
+ expect(testOfflineInterface.init).toHaveBeenCalled();
125
+ expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
126
+ });
127
+ });
@@ -0,0 +1,227 @@
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 _cacheableSection = require("../cacheable-section");
10
+
11
+ var _offlineProvider = require("../offline-provider");
12
+
13
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
+
15
+ /* eslint-disable react/display-name, react/prop-types */
16
+ // Suppress 'act' warning for these tests
17
+ const originalError = console.error;
18
+ beforeEach(() => {
19
+ jest.spyOn(console, 'error').mockImplementation((...args) => {
20
+ const pattern = /Warning: An update to .* inside a test was not wrapped in act/;
21
+
22
+ if (typeof args[0] === 'string' && pattern.test(args[0])) {
23
+ return;
24
+ }
25
+
26
+ return originalError.call(console, ...args);
27
+ });
28
+ });
29
+ afterEach(() => {
30
+ jest.clearAllMocks() // This syntax appeases typescript:
31
+ ;
32
+ console.error.mockRestore();
33
+ });
34
+ it('renders in the default state initially', () => {
35
+ const {
36
+ result
37
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('one'), {
38
+ wrapper: ({
39
+ children
40
+ }) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
41
+ offlineInterface: _testMocks.mockOfflineInterface
42
+ }, children)
43
+ });
44
+ expect(result.current.recordingState).toBe('default');
45
+ expect(result.current.isCached).toBe(false);
46
+ expect(result.current.lastUpdated).toBeUndefined();
47
+ });
48
+ it('handles a successful recording', async done => {
49
+ const [sectionId, timeoutDelay] = ['one', 1234];
50
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
51
+ getCachedSections: jest.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([{
52
+ sectionId: sectionId,
53
+ lastUpdated: new Date()
54
+ }])
55
+ };
56
+ const {
57
+ result,
58
+ waitFor
59
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)(sectionId), {
60
+ wrapper: ({
61
+ children
62
+ }) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
63
+ offlineInterface: testOfflineInterface
64
+ }, children)
65
+ });
66
+
67
+ const assertRecordingStarted = () => {
68
+ expect(result.current.recordingState).toBe('recording');
69
+ };
70
+
71
+ const assertRecordingCompleted = async () => {
72
+ expect(result.current.recordingState).toBe('default'); // Test that 'isCached' gets updated
73
+
74
+ expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2);
75
+ await waitFor(() => result.current.isCached === true);
76
+ expect(result.current.isCached).toBe(true);
77
+ expect(result.current.lastUpdated).toBeInstanceOf(Date); // If this cb is not called, test should time out and fail
78
+
79
+ done();
80
+ };
81
+
82
+ await (0, _reactHooks.act)(async () => {
83
+ await result.current.startRecording({
84
+ onStarted: assertRecordingStarted,
85
+ onCompleted: assertRecordingCompleted,
86
+ recordingTimeoutDelay: timeoutDelay
87
+ });
88
+ }); // At this stage, recording should be 'pending'
89
+
90
+ expect(result.current.recordingState).toBe('pending'); // Check correct options sent to offline interface
91
+
92
+ const options = _testMocks.mockOfflineInterface.startRecording.mock.calls[0][0];
93
+ expect(options.sectionId).toBe(sectionId);
94
+ expect(options.recordingTimeoutDelay).toBe(timeoutDelay);
95
+ expect(typeof options.onStarted).toBe('function');
96
+ expect(typeof options.onCompleted).toBe('function');
97
+ expect(typeof options.onError).toBe('function'); // Make sure all async assertions are called
98
+
99
+ expect.assertions(11);
100
+ });
101
+ it('handles a recording that encounters an error', async done => {
102
+ // Suppress the expected error from console (in addition to 'act' warning)
103
+ jest.spyOn(console, 'error').mockImplementation((...args) => {
104
+ const actPattern = /Warning: An update to .* inside a test was not wrapped in act/;
105
+ const errPattern = /Error during recording/;
106
+ const matchesPattern = actPattern.test(args[0]) || errPattern.test(args[0]);
107
+
108
+ if (typeof args[0] === 'string' && matchesPattern) {
109
+ return;
110
+ }
111
+
112
+ return originalError.call(console, ...args);
113
+ });
114
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
115
+ startRecording: _testMocks.errorRecordingMock
116
+ };
117
+ const {
118
+ result
119
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('one'), {
120
+ wrapper: ({
121
+ children
122
+ }) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
123
+ offlineInterface: testOfflineInterface
124
+ }, children)
125
+ });
126
+
127
+ const assertRecordingStarted = () => {
128
+ expect(result.current.recordingState).toBe('recording');
129
+ };
130
+
131
+ const assertRecordingError = error => {
132
+ expect(result.current.recordingState).toBe('error');
133
+ expect(error.message).toMatch(/test err/); // see errorRecordingMock
134
+
135
+ expect(console.error).toHaveBeenCalledWith('Error during recording:', error); // Expect only one call, from initialization:
136
+
137
+ expect(_testMocks.mockOfflineInterface.getCachedSections).toBeCalledTimes(1); // If this cb is not called, test should time out and fail
138
+
139
+ done();
140
+ };
141
+
142
+ await (0, _reactHooks.act)(async () => {
143
+ await result.current.startRecording({
144
+ onStarted: assertRecordingStarted,
145
+ onError: assertRecordingError
146
+ });
147
+ }); // At this stage, recording should be 'pending'
148
+
149
+ expect(result.current.recordingState).toBe('pending'); // Make sure all async assertions are called
150
+
151
+ expect.assertions(6);
152
+ });
153
+ it('handles an error starting the recording', async () => {
154
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
155
+ startRecording: _testMocks.failedMessageRecordingMock
156
+ };
157
+ const {
158
+ result
159
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('err'), {
160
+ wrapper: ({
161
+ children
162
+ }) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
163
+ offlineInterface: testOfflineInterface
164
+ }, children)
165
+ });
166
+ await expect(result.current.startRecording()).rejects.toThrow('Failed message' // from failedMessageRecordingMock
167
+ );
168
+ });
169
+ it('handles remove and updates sections', async () => {
170
+ const sectionId = 'one';
171
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
172
+ getCachedSections: jest.fn().mockResolvedValueOnce([{
173
+ sectionId: sectionId,
174
+ lastUpdated: new Date()
175
+ }]).mockResolvedValueOnce([])
176
+ };
177
+ const {
178
+ result,
179
+ waitFor
180
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)(sectionId), {
181
+ wrapper: ({
182
+ children
183
+ }) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
184
+ offlineInterface: testOfflineInterface
185
+ }, children)
186
+ }); // Wait for state to sync with indexedDB
187
+
188
+ await waitFor(() => result.current.isCached === true);
189
+ let success;
190
+ await (0, _reactHooks.act)(async () => {
191
+ success = await result.current.remove();
192
+ });
193
+ expect(success).toBe(true); // Test that 'isCached' gets updated
194
+
195
+ expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2);
196
+ await waitFor(() => result.current.isCached === false);
197
+ expect(result.current.isCached).toBe(false);
198
+ expect(result.current.lastUpdated).toBeUndefined();
199
+ });
200
+ it('handles a change in ID', async () => {
201
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
202
+ getCachedSections: jest.fn().mockResolvedValue([{
203
+ sectionId: 'id-one',
204
+ lastUpdated: new Date()
205
+ }])
206
+ };
207
+ const {
208
+ result,
209
+ waitFor,
210
+ rerender
211
+ } = (0, _reactHooks.renderHook)((...args) => (0, _cacheableSection.useCacheableSection)(...args), {
212
+ wrapper: ({
213
+ children
214
+ }) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
215
+ offlineInterface: testOfflineInterface
216
+ }, children),
217
+ initialProps: 'id-one'
218
+ }); // Wait for state to sync with indexedDB
219
+
220
+ await waitFor(() => result.current.isCached === true);
221
+ rerender('id-two'); // Test that 'isCached' gets updated
222
+ // expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2)
223
+
224
+ await waitFor(() => result.current.isCached === false);
225
+ expect(result.current.isCached).toBe(false);
226
+ expect(result.current.lastUpdated).toBeUndefined();
227
+ });
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createCacheableSectionStore = createCacheableSectionStore;
7
+ exports.CacheableSectionProvider = CacheableSectionProvider;
8
+ exports.useRecordingState = useRecordingState;
9
+ exports.useCachedSections = useCachedSections;
10
+ exports.useCachedSection = useCachedSection;
11
+
12
+ var _propTypes = _interopRequireDefault(require("prop-types"));
13
+
14
+ var _react = _interopRequireDefault(require("react"));
15
+
16
+ var _globalStateService = require("./global-state-service");
17
+
18
+ var _offlineInterface = require("./offline-interface");
19
+
20
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
+
22
+ /**
23
+ * Helper that transforms an array of cached section objects from the IndexedDB
24
+ * into an object of values keyed by section ID
25
+ *
26
+ * @param {Array} list - An array of section objects
27
+ * @returns {Object} An object of sections, keyed by ID
28
+ */
29
+ function getSectionsById(sectionsArray) {
30
+ return sectionsArray.reduce((result, {
31
+ sectionId,
32
+ lastUpdated
33
+ }) => ({ ...result,
34
+ [sectionId]: {
35
+ lastUpdated
36
+ }
37
+ }), {});
38
+ }
39
+ /**
40
+ * Create a store for Cacheable Section state.
41
+ * Expected to be used in app adapter
42
+ */
43
+
44
+
45
+ function createCacheableSectionStore() {
46
+ const initialState = {
47
+ recordingStates: {},
48
+ cachedSections: {}
49
+ };
50
+ return (0, _globalStateService.createStore)(initialState);
51
+ }
52
+ /**
53
+ * Helper hook that returns a value that will persist between renders but makes
54
+ * sure to only set its initial state once.
55
+ * See https://gist.github.com/amcgee/42bb2fa6d5f79e607f00e6dccc733482
56
+ */
57
+
58
+
59
+ function useConst(factory) {
60
+ const ref = _react.default.useRef(null);
61
+
62
+ if (ref.current === null) {
63
+ ref.current = factory();
64
+ }
65
+
66
+ return ref.current;
67
+ }
68
+ /**
69
+ * Provides context for a global state context which will track cached
70
+ * sections' status and cacheable sections' recording states, which will
71
+ * determine how that component will render. The provider will be a part of
72
+ * the OfflineProvider.
73
+ */
74
+
75
+
76
+ function CacheableSectionProvider({
77
+ children
78
+ }) {
79
+ const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
80
+ const store = useConst(createCacheableSectionStore); // On load, get sections and add to store
81
+
82
+ _react.default.useEffect(() => {
83
+ if (offlineInterface) {
84
+ offlineInterface.getCachedSections().then(sections => {
85
+ store.mutate(state => ({ ...state,
86
+ cachedSections: getSectionsById(sections)
87
+ }));
88
+ });
89
+ }
90
+ }, [store, offlineInterface]);
91
+
92
+ return /*#__PURE__*/_react.default.createElement(_globalStateService.GlobalStateProvider, {
93
+ store: store
94
+ }, children);
95
+ }
96
+
97
+ CacheableSectionProvider.propTypes = {
98
+ children: _propTypes.default.node
99
+ };
100
+
101
+ /**
102
+ * Uses an optimized global state to manage 'recording state' values without
103
+ * unnecessarily rerendering all consuming components
104
+ *
105
+ * @param {String} id - ID of the cacheable section to track
106
+ * @returns {Object} { recordingState: String, setRecordingState: Function, removeRecordingState: Function}
107
+ */
108
+ function useRecordingState(id) {
109
+ const [recordingState] = (0, _globalStateService.useGlobalState)(state => state.recordingStates[id]);
110
+ const setRecordingState = (0, _globalStateService.useGlobalStateMutation)(newState => state => ({ ...state,
111
+ recordingStates: { ...state.recordingStates,
112
+ [id]: newState
113
+ }
114
+ }));
115
+ const removeRecordingState = (0, _globalStateService.useGlobalStateMutation)(() => state => {
116
+ const recordingStates = { ...state.recordingStates
117
+ };
118
+ delete recordingStates[id];
119
+ return { ...state,
120
+ recordingStates
121
+ };
122
+ });
123
+ return {
124
+ recordingState,
125
+ setRecordingState,
126
+ removeRecordingState
127
+ };
128
+ }
129
+ /**
130
+ * Returns a function that syncs cached sections in the global state
131
+ * with IndexedDB, so that IndexedDB is the single source of truth
132
+ *
133
+ * @returns {Function} syncCachedSections
134
+ */
135
+
136
+
137
+ function useSyncCachedSections() {
138
+ const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
139
+ const setCachedSections = (0, _globalStateService.useGlobalStateMutation)(cachedSections => state => ({ ...state,
140
+ cachedSections
141
+ }));
142
+ return async function syncCachedSections() {
143
+ const sections = await offlineInterface.getCachedSections();
144
+ setCachedSections(getSectionsById(sections));
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Uses global state to manage an object of cached sections' statuses
150
+ *
151
+ * @returns {Object} { cachedSections: Object, removeSection: Function }
152
+ */
153
+ function useCachedSections() {
154
+ const [cachedSections] = (0, _globalStateService.useGlobalState)(state => state.cachedSections);
155
+ const syncCachedSections = useSyncCachedSections();
156
+ const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
157
+ /**
158
+ * Uses offline interface to remove a section from IndexedDB and Cache
159
+ * Storage.
160
+ *
161
+ * Returns a promise that resolves to `true` if a section is found and
162
+ * deleted, or `false` if asection with the specified ID does not exist.
163
+ */
164
+
165
+ async function removeById(id) {
166
+ const success = await offlineInterface.removeSection(id);
167
+
168
+ if (success) {
169
+ await syncCachedSections();
170
+ }
171
+
172
+ return success;
173
+ }
174
+
175
+ return {
176
+ cachedSections,
177
+ removeById,
178
+ syncCachedSections
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Uses global state to manage the cached status of just one section, which
184
+ * prevents unnecessary rerenders of consuming components
185
+ *
186
+ * @param {String} id
187
+ * @returns {Object} { lastUpdated: Date, remove: Function }
188
+ */
189
+ function useCachedSection(id) {
190
+ const [status] = (0, _globalStateService.useGlobalState)(state => state.cachedSections[id]);
191
+ const syncCachedSections = useSyncCachedSections();
192
+ const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
193
+ const lastUpdated = status && status.lastUpdated;
194
+ /**
195
+ * Uses offline interface to remove a section from IndexedDB and Cache
196
+ * Storage.
197
+ *
198
+ * Returns `true` if a section is found and deleted, or `false` if a
199
+ * section with the specified ID does not exist.
200
+ */
201
+
202
+ async function remove() {
203
+ const success = await offlineInterface.removeSection(id);
204
+
205
+ if (success) {
206
+ await syncCachedSections();
207
+ }
208
+
209
+ return success;
210
+ }
211
+
212
+ return {
213
+ lastUpdated,
214
+ isCached: !!lastUpdated,
215
+ remove,
216
+ syncCachedSections
217
+ };
218
+ }