@dhis2/app-service-offline 3.8.0 → 3.9.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 (61) hide show
  1. package/build/cjs/__tests__/integration.test.js +37 -24
  2. package/build/cjs/index.js +18 -10
  3. package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +5 -1
  4. package/build/cjs/lib/__tests__/{online-status.test.js → network-status.test.js} +41 -20
  5. package/build/cjs/lib/__tests__/offline-provider.test.js +5 -1
  6. package/build/cjs/lib/__tests__/use-cacheable-section.test.js +77 -35
  7. package/build/cjs/lib/__tests__/{use-online-staus-message.test.js → use-online-status-message.test.js} +8 -5
  8. package/build/cjs/lib/cacheable-section-state.js +17 -13
  9. package/build/cjs/lib/cacheable-section.js +14 -12
  10. package/build/cjs/lib/clear-sensitive-caches.js +9 -5
  11. package/build/cjs/lib/dhis2-connection-status/dev-debug-log.js +26 -0
  12. package/build/cjs/lib/dhis2-connection-status/dhis2-connection-status.js +224 -0
  13. package/build/cjs/lib/dhis2-connection-status/dhis2-connection-status.test.js +841 -0
  14. package/build/cjs/lib/dhis2-connection-status/index.js +19 -0
  15. package/build/cjs/lib/dhis2-connection-status/is-ping-available.js +43 -0
  16. package/build/cjs/lib/dhis2-connection-status/is-ping-available.test.js +76 -0
  17. package/build/cjs/lib/dhis2-connection-status/smart-interval.js +211 -0
  18. package/build/cjs/lib/dhis2-connection-status/use-ping-query.js +21 -0
  19. package/build/cjs/lib/global-state-service.js +16 -11
  20. package/build/cjs/lib/{online-status.js → network-status.js} +8 -5
  21. package/build/cjs/lib/offline-interface.js +7 -4
  22. package/build/cjs/lib/offline-provider.js +9 -5
  23. package/build/cjs/lib/online-status-message.js +5 -4
  24. package/build/cjs/utils/render-counter.js +6 -4
  25. package/build/cjs/utils/test-mocks.js +17 -10
  26. package/build/es/__tests__/integration.test.js +37 -24
  27. package/build/es/index.js +5 -3
  28. package/build/es/lib/__tests__/clear-sensitive-caches.test.js +5 -1
  29. package/build/es/lib/__tests__/{online-status.test.js → network-status.test.js} +35 -14
  30. package/build/es/lib/__tests__/offline-provider.test.js +5 -1
  31. package/build/es/lib/__tests__/use-cacheable-section.test.js +77 -34
  32. package/build/es/lib/__tests__/{use-online-staus-message.test.js → use-online-status-message.test.js} +8 -5
  33. package/build/es/lib/cacheable-section-state.js +14 -10
  34. package/build/es/lib/cacheable-section.js +13 -11
  35. package/build/es/lib/clear-sensitive-caches.js +8 -4
  36. package/build/es/lib/dhis2-connection-status/dev-debug-log.js +20 -0
  37. package/build/es/lib/dhis2-connection-status/dhis2-connection-status.js +194 -0
  38. package/build/es/lib/dhis2-connection-status/dhis2-connection-status.test.js +831 -0
  39. package/build/es/lib/dhis2-connection-status/index.js +1 -0
  40. package/build/es/lib/dhis2-connection-status/is-ping-available.js +36 -0
  41. package/build/es/lib/dhis2-connection-status/is-ping-available.test.js +73 -0
  42. package/build/es/lib/dhis2-connection-status/smart-interval.js +199 -0
  43. package/build/es/lib/dhis2-connection-status/use-ping-query.js +12 -0
  44. package/build/es/lib/global-state-service.js +15 -10
  45. package/build/es/lib/{online-status.js → network-status.js} +7 -4
  46. package/build/es/lib/offline-interface.js +7 -4
  47. package/build/es/lib/offline-provider.js +8 -5
  48. package/build/es/lib/online-status-message.js +4 -3
  49. package/build/es/utils/render-counter.js +6 -4
  50. package/build/es/utils/test-mocks.js +16 -9
  51. package/build/types/index.d.ts +2 -1
  52. package/build/types/lib/dhis2-connection-status/dev-debug-log.d.ts +9 -0
  53. package/build/types/lib/dhis2-connection-status/dhis2-connection-status.d.ts +28 -0
  54. package/build/types/lib/dhis2-connection-status/index.d.ts +1 -0
  55. package/build/types/lib/dhis2-connection-status/is-ping-available.d.ts +14 -0
  56. package/build/types/lib/dhis2-connection-status/smart-interval.d.ts +18 -0
  57. package/build/types/lib/dhis2-connection-status/use-ping-query.d.ts +1 -0
  58. package/build/types/lib/{online-status.d.ts → network-status.d.ts} +3 -3
  59. package/build/types/types.d.ts +6 -0
  60. package/build/types/utils/test-mocks.d.ts +2 -0
  61. package/package.json +2 -2
@@ -0,0 +1,194 @@
1
+ import { useConfig } from '@dhis2/app-service-config';
2
+ import PropTypes from 'prop-types';
3
+ import React, { useCallback, useState, useRef, useMemo, useEffect, useContext } from 'react';
4
+ import { useOfflineInterface } from '../offline-interface';
5
+ import { devDebugLog } from './dev-debug-log';
6
+ import { isPingAvailable } from './is-ping-available';
7
+ import createSmartInterval from './smart-interval';
8
+ import { usePingQuery } from './use-ping-query'; // Utils for saving 'last connected' datetime in local storage
9
+
10
+ const lastConnectedKey = 'dhis2.lastConnected';
11
+ export const getLastConnectedKey = appName => appName ? `${lastConnectedKey}.${appName}` : lastConnectedKey;
12
+
13
+ const updateLastConnected = appName => {
14
+ // use Date.now() because it's easier to mock for easier unit testing
15
+ const now = new Date(Date.now());
16
+ localStorage.setItem(getLastConnectedKey(appName), now.toUTCString());
17
+ return now;
18
+ };
19
+
20
+ const getLastConnected = appName => {
21
+ const lastConnected = localStorage.getItem(getLastConnectedKey(appName));
22
+ return lastConnected ? new Date(lastConnected) : null;
23
+ };
24
+
25
+ const clearLastConnected = appName => {
26
+ localStorage.removeItem(getLastConnectedKey(appName));
27
+ };
28
+
29
+ const Dhis2ConnectionStatusContext = /*#__PURE__*/React.createContext({
30
+ isConnected: true,
31
+ isDisconnected: false,
32
+ lastConnected: null
33
+ });
34
+ /**
35
+ * Provides a boolean indicating client's connection to the DHIS2 server,
36
+ * which is different from connection to the internet.
37
+ *
38
+ * The context provider subscribes to messages from the SW tracking successes
39
+ * and failures of requests to the DHIS2 server to determine connection status,
40
+ * and then will initiate periodic pings if there are no incidental requests in
41
+ * order to check the connection consistently
42
+ */
43
+
44
+ export const Dhis2ConnectionStatusProvider = (_ref) => {
45
+ let {
46
+ children
47
+ } = _ref;
48
+ const offlineInterface = useOfflineInterface();
49
+ const {
50
+ appName,
51
+ serverVersion
52
+ } = useConfig(); // The offline interface persists the latest update from the SW so that
53
+ // this hook can initialize to an accurate value. The App Adapter in the
54
+ // platform waits for this value to be populated before rendering the
55
+ // the App Runtime provider (including this), but if that is not done,
56
+ // `latestIsConnected` may be `null` depending on the outcome of race
57
+ // conditions between the SW and the React component tree.
58
+
59
+ const [isConnected, setIsConnected] = useState(offlineInterface.latestIsConnected);
60
+ const ping = usePingQuery();
61
+ const smartIntervalRef = useRef(null);
62
+ /**
63
+ * Update state, reset ping backoff if changed, and update
64
+ * the lastConnected value in localStorage
65
+ */
66
+
67
+ const updateConnectedState = useCallback(newIsConnected => {
68
+ // use 'set' with a function as param to get latest isConnected
69
+ // without needing it as a dependency for useCallback
70
+ setIsConnected(prevIsConnected => {
71
+ devDebugLog('[D2CS] updating state:', {
72
+ prevIsConnected,
73
+ newIsConnected
74
+ });
75
+
76
+ if (newIsConnected !== prevIsConnected) {
77
+ var _smartIntervalRef$cur;
78
+
79
+ // if value changed, reset ping interval to initial delay
80
+ (_smartIntervalRef$cur = smartIntervalRef.current) === null || _smartIntervalRef$cur === void 0 ? void 0 : _smartIntervalRef$cur.reset();
81
+
82
+ if (newIsConnected) {
83
+ // Need to clear this here so it doesn't affect another
84
+ // session that starts while offline
85
+ clearLastConnected(appName);
86
+ } else {
87
+ updateLastConnected(appName);
88
+ }
89
+ }
90
+
91
+ return newIsConnected;
92
+ });
93
+ }, [appName]); // Note that the SW is configured to not cache ping requests and won't
94
+ // trigger `handleChange` below to avoid redundant signals. This also
95
+ // helps to detect the connectivity status when the SW is not available
96
+ // for some reason (maybe private browsing, first installation, or
97
+ // insecure browser context)
98
+
99
+ const pingAndHandleStatus = useCallback(() => {
100
+ return ping().then(() => {
101
+ // Ping is successful; set 'connected'
102
+ updateConnectedState(true);
103
+ }).catch(err => {
104
+ console.error('Ping failed:', err.message);
105
+ updateConnectedState(false);
106
+ });
107
+ }, [ping, updateConnectedState]);
108
+ /** Called when SW reports updates from incidental network traffic */
109
+
110
+ const onUpdate = useCallback((_ref2) => {
111
+ var _smartIntervalRef$cur2;
112
+
113
+ let {
114
+ isConnected: newIsConnected
115
+ } = _ref2;
116
+ devDebugLog('[D2CS] handling update from sw'); // Snooze ping timer to reduce pings since we know state from SW
117
+
118
+ (_smartIntervalRef$cur2 = smartIntervalRef.current) === null || _smartIntervalRef$cur2 === void 0 ? void 0 : _smartIntervalRef$cur2.snooze();
119
+ updateConnectedState(newIsConnected);
120
+ }, [updateConnectedState]);
121
+ useEffect(() => {
122
+ // If the /api/ping endpoint is not available on this instance, skip
123
+ // pinging with the smart interval. Just use the service worker
124
+ if (!serverVersion || !isPingAvailable(serverVersion)) {
125
+ return;
126
+ } // Only create the smart interval once
127
+
128
+
129
+ const smartInterval = createSmartInterval({
130
+ // don't ping if window isn't focused or visible
131
+ initialPauseValue: !document.hasFocus() || document.visibilityState !== 'visible',
132
+ callback: pingAndHandleStatus
133
+ });
134
+ smartIntervalRef.current = smartInterval;
135
+
136
+ const handleBlur = () => smartInterval.pause();
137
+
138
+ const handleFocus = () => smartInterval.resume(); // On offline event, ping immediately to test server connection.
139
+ // Only do this when going offline -- it's theoretically no-cost
140
+ // for both online and offline servers. Pinging when going online
141
+ // can be costly for clients connecting over the internet to online
142
+ // servers.
143
+
144
+
145
+ const handleOffline = () => smartInterval.invokeCallbackImmediately();
146
+
147
+ window.addEventListener('blur', handleBlur);
148
+ window.addEventListener('focus', handleFocus);
149
+ window.addEventListener('offline', handleOffline);
150
+ return () => {
151
+ window.removeEventListener('blur', handleBlur);
152
+ window.removeEventListener('focus', handleFocus);
153
+ window.removeEventListener('offline', handleOffline); // clean up smart interval
154
+
155
+ smartInterval.clear();
156
+ };
157
+ }, [pingAndHandleStatus, serverVersion]);
158
+ useEffect(() => {
159
+ const unsubscribe = offlineInterface.subscribeToDhis2ConnectionStatus({
160
+ onUpdate
161
+ });
162
+ return () => {
163
+ unsubscribe();
164
+ };
165
+ }, [offlineInterface, onUpdate]); // Memoize this value to prevent unnecessary rerenders of context provider
166
+
167
+ const contextValue = useMemo(() => ({
168
+ // in the unlikely circumstance that offlineInterface.latestIsConnected
169
+ // is `null` when this initializes, fail safe by defaulting to
170
+ // `isConnected: false`
171
+ isConnected: Boolean(isConnected),
172
+ isDisconnected: !isConnected,
173
+ lastConnected: isConnected ? null : // Only evaluate if disconnected, since local storage
174
+ // is synchronous and disk-based.
175
+ // If lastConnected is not set in localStorage though, set it.
176
+ // (relevant on startup)
177
+ getLastConnected(appName) || updateLastConnected(appName)
178
+ }), [isConnected, appName]);
179
+ return /*#__PURE__*/React.createElement(Dhis2ConnectionStatusContext.Provider, {
180
+ value: contextValue
181
+ }, children);
182
+ };
183
+ Dhis2ConnectionStatusProvider.propTypes = {
184
+ children: PropTypes.node
185
+ };
186
+ export const useDhis2ConnectionStatus = () => {
187
+ const context = useContext(Dhis2ConnectionStatusContext);
188
+
189
+ if (!context) {
190
+ throw new Error('useDhis2ConnectionStatus must be used within a Dhis2ConnectionStatus provider');
191
+ }
192
+
193
+ return context;
194
+ };