@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.
- package/build/cjs/__tests__/integration.test.js +337 -0
- package/build/cjs/index.js +39 -1
- package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +131 -0
- package/build/cjs/lib/__tests__/offline-provider.test.js +127 -0
- package/build/cjs/lib/__tests__/use-cacheable-section.test.js +227 -0
- package/build/cjs/lib/cacheable-section-state.js +218 -0
- package/build/cjs/lib/cacheable-section.js +156 -0
- package/build/cjs/lib/clear-sensitive-caches.js +87 -0
- package/build/cjs/lib/global-state-service.js +95 -0
- package/build/cjs/lib/offline-interface.js +86 -0
- package/build/cjs/lib/offline-provider.js +53 -0
- package/build/cjs/types.js +0 -1
- package/build/cjs/utils/__tests__/render-counter.test.js +55 -0
- package/build/cjs/utils/render-counter.js +26 -0
- package/build/cjs/utils/test-mocks.js +40 -0
- package/build/es/__tests__/integration.test.js +327 -0
- package/build/es/index.js +5 -1
- package/build/es/lib/__tests__/clear-sensitive-caches.test.js +123 -0
- package/build/es/lib/__tests__/offline-provider.test.js +117 -0
- package/build/es/lib/__tests__/use-cacheable-section.test.js +218 -0
- package/build/es/lib/cacheable-section-state.js +199 -0
- package/build/es/lib/cacheable-section.js +137 -0
- package/build/es/lib/clear-sensitive-caches.js +78 -0
- package/build/es/lib/global-state-service.js +70 -0
- package/build/es/lib/offline-interface.js +65 -0
- package/build/es/lib/offline-provider.js +40 -0
- package/build/es/types.js +0 -1
- package/build/es/utils/__tests__/render-counter.test.js +40 -0
- package/build/es/utils/render-counter.js +11 -0
- package/build/es/utils/test-mocks.js +30 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/lib/cacheable-section-state.d.ts +66 -0
- package/build/types/lib/cacheable-section.d.ts +52 -0
- package/build/types/lib/clear-sensitive-caches.d.ts +16 -0
- package/build/types/lib/global-state-service.d.ts +16 -0
- package/build/types/lib/offline-interface.d.ts +26 -0
- package/build/types/lib/offline-provider.d.ts +19 -0
- package/build/types/types.d.ts +50 -0
- package/build/types/utils/render-counter.d.ts +10 -0
- package/build/types/utils/test-mocks.d.ts +11 -0
- 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
|
+
}
|