├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── __mocks__ └── react-native-ntp-client.js ├── __tests__ ├── behavior.test.js └── config.test.js ├── index.js ├── jest.config.js ├── package-lock.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # testing stuff 2 | __mocks__/**/*.js 3 | __tests__/**/*.js 4 | coverage/**/*.js 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "clearInterval": false, 4 | "console": false, 5 | "module": true, 6 | "require": false, 7 | "setInterval": false 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "ecmaVersion": 5 12 | }, 13 | "rules": { 14 | "no-console": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Artem Russkikh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-clock-sync 2 | Sync clock across mobile devices using NTP. Compatible with React Native. Based on https://www.npmjs.com/package/@smarterservices/smarterclock 3 | 4 | Used to ensure the time used is in sync across distributed systems. The sync is achieved by the following process: 5 | 6 | * Fetches the time from an NTP server. 7 | * Adjusts for network latency and transfer time 8 | * Computes the delta between the NTP server and the system clock and stores the delta for later use. 9 | * Uses all the stored deltas to get the average time drift from UTC. 10 | * Allows for specifying multiple NTP servers as backups in case of network errors. 11 | * Ability to get historical details on (un)successful syncs, errors, and raw time values 12 | * Ability to take instance 'offline' (effectively pausing network activity) 13 | 14 | ## Getting Started 15 | Install the module: 16 | 17 | ``` 18 | npm install react-native-clock-sync --save 19 | ``` 20 | 21 | Link native dependencies of [react-native-udp](https://github.com/tradle/react-native-udp#install): 22 | 23 | ``` 24 | react-native link react-native-udp 25 | ``` 26 | 27 | ## Usage 28 | 29 | Import the module into your codebase 30 | 31 | ```javascript 32 | import clockSync from 'react-native-clock-sync' 33 | ``` 34 | 35 | Create an instance of the clock object passing in the required params. See the options section below for options that can be used. 36 | 37 | ```javascript 38 | var options = {}; 39 | 40 | // create a new instance 41 | var clock = new clockSync(options); 42 | 43 | // get the current unix timestamp 44 | var currentTime = clock.getTime(); 45 | 46 | console.log(currentTime); 47 | ``` 48 | 49 | ## Options 50 | 51 | The clock constructor can accept the following options. **all options are optional** 52 | 53 | ##### Basic Options 54 | 55 | * `syncDelay` (+number) : The time (in seconds) between each call to an NTP server to get the latest UTC timestamp. Defaults to `300` (which is 5 minutes) if not present, zero, or supplied value is not a number. **Supplied value must be > 0** 56 | * `history` (+int) : The number of delta values that should be maintained and used for calculating your local time drift. Defaults to `10` if not present, zero, or supplied value is not a number. **Supplied value must be > 0** 57 | * `startOnline` (boolean) : A flag to control network activity upon clockSync instantiation. Defaults to `true`. (immediate NTP server fetch attempt) 58 | 59 | ```javascript 60 | { 61 | "syncDelay" : 60, 62 | "history": 10, 63 | "startOnline": false, 64 | ... 65 | } 66 | ``` 67 | 68 | ##### Server Options 69 | 70 | * `cycleServers` (boolean) : A flag to allow for 'wrapping around' back to the beginning of the servers list (if > 1 are specified). Upon a network error, clockSync will attempt to use the next server in the list until it reaches the end. When `cycleServers === true`, it will wrap back to the first item and move through the list again. Defaults to `false` (advance to last item and remain there regardless of additional errors encountered) 71 | * `servers` (array) : An optional array of NTP servers to use when looking up time. If no *servers* key exists in the *config* object, the default NTP configuration will be `pool.ntp.org` at port `123`. Otherwise, items in the array may be in **any** of the following forms (mixed values are allowed): 72 | * (string) `"ntp.server.name"` - when a single string value is provided, it will be automatically associated with the default port number `123` 73 | * (object) with the keys `server` and `port`. Only `server` is **required**. If `port` is omitted, it will be defaulted to `123`. Server values must be strings. Port values must be numbers. 74 | 75 | These are some examples of acceptable server configurations: 76 | ```javascript 77 | { 78 | "cycleServers": true, 79 | "servers" : [{"server": "pool.ntp.org", "port": 123}] 80 | } 81 | 82 | // all default port 83 | { 84 | "servers": [ 85 | "foo.bar.com", 86 | "baz.bat.qux", 87 | "pool.ntp.org" 88 | ] 89 | } 90 | 91 | // formats may be mixed 92 | { 93 | "servers": [ 94 | "foo.bar.baz", /* default port */ 95 | {"server": "a.b.c"}, /* default port */ 96 | "x.y.z", /* default port */ 97 | {"server": "aaa.bbb.ccc", "port": 456} /* fully specified */ 98 | ] 99 | } 100 | ``` 101 | 102 | 103 | 104 | ## Example 105 | 106 | ```javascript 107 | import clockSync from 'react-native-clock-sync' 108 | var clock = new clockSync({}); 109 | 110 | var syncTime = clock.getTime(); 111 | console.log('SyncTime:' + syncTime); 112 | 113 | setInterval(function() { 114 | var localTime = new Date().getTime(); 115 | var syncTime = clock.getTime(); 116 | var drift = parseInt(localTime) - parseInt(syncTime); 117 | 118 | console.log('SyncTime:' + syncTime + ' vs LocalTime: ' + localTime + ' Difference: ' + drift + 'ms'); 119 | }, 5000); 120 | ``` 121 | 122 | ## Methods 123 | 124 | ### getHistory() 125 | 126 | Returns an `Object` of historical details generated as *clockSync* runs. It includes several fields that can be used to determine the behavior of a running *clockSync* instance. Each call represents an individual 'snapshot' of the current *clockSync* instance. History is not updated when instance is *offline*. 127 | 128 | #### Fields 129 | 130 | * `currentConsecutiveErrorCount` (int) : Count of current string of errors since entering an error state (`isInErrorState === true`). Resets to `0` upon successful sync. 131 | * `currentServer` (object) : Object containing server info of the server that will be used for the next sync. Props are: 132 | * `server` (string) : the NTP server name 133 | * `port` (int) : the NTP port 134 | * `deltas` (array<object>) : This array will contain a 'rolling' list of raw time values returned from each successful NTP server sync wrapped in a simple object with the following keys: (**note:** array length is limited to `config.history`; oldest at `index 0`) 135 | * `dt` (+/- int) : The calculated delta (in ms) between local time and the value returned from NTP. 136 | * `ntp` (int) : The unix epoch-relative time (in ms) returned from the NTP server. (raw value returned from server) **note**: ```ntp + -1(dt) = local time of sync``` 137 | * `errors` (array<object>) : This array will contain a 'rolling' list of any errors that have occurred during sync attempts. (**note:** array length is limited to `config.history`; oldest at `index 0`). The object contains typical fields found in JS `Error`s as well as additional information. 138 | * `name` (string) : JavaScript Error name 139 | * `message` (string) : JavaScript Error message 140 | * `server` (object) : The server that encountered the sync error. Same keys as `currentServer` object. (possibly different values) 141 | * `stack` (string) : JavaScript Error stack trace (if available) 142 | * `time` (int) : The **local** unix epoch-relative timestamp when error was encountered (in ms) 143 | * `isInErrorState` (boolean) : Flag indicating if the last attempted sync was an error (`true`) Resets to `false` upon successful sync. 144 | * `lastSyncTime` (int) : The **local** unix epoch-relative timestamp of last successful sync (in ms) 145 | * `lastNtpTime` (int) : The **NTP** unix epoch-relative timestamp of the last successful sync (raw value returned from server) 146 | * `lastError` (object) : The error info of the last sync error that was encountered. Object keys are same as objects in the `errors` array. 147 | * `lifetimeErrorCount` (int) : A running total of all errors encountered since *clockSync* instance was created. 148 | * `maxConsecutiveErrorCount` (int) : Greatest number of errors in a single error state (before a successful sync). 149 | 150 | #### Example 151 | 152 | ```javascript 153 | // sample return value of getHistory 154 | // dummy values, actual types 155 | { 156 | currentConsecutiveErrorCount: 1, 157 | currentServer: { 158 | server: 'good.fake.server', 159 | port: 123 160 | }, 161 | deltas: [ 162 | { 163 | dt: -169, 164 | ntp: 1544681340812 165 | }, 166 | { 167 | dt: 487, 168 | ntp: 1544681470828 169 | } 170 | ], 171 | errors: [ 172 | { 173 | name: 'Error', 174 | message: 'Mock Error', 175 | server: { 176 | server: 'FAIL.FAIL.FAIL', 177 | port: 456 178 | }, 179 | stack: 'Error: Mock Error\n at Object.getNetworkTime (/Users/xyz/rnative/react-native-clock-sync/__mocks__/react-native-ntp-client.js:37:10)\n at clockSync.getNetworkTime [as getDelta] (/Users/xyz/rnative/react-native-clock-sync/index.js:103:17)\n at clockSync.getDelta (/Users/xyz/rnative/react-native-clock-sync/index.js:226:8)\n ... (rest of stack omitted for brevity)', 180 | time: 1544681598417 181 | }, 182 | { 183 | name: 'Error', 184 | message: 'Mock Error', 185 | server: { 186 | server: 'FAIL.FAIL.FAIL', 187 | port: 666 188 | }, 189 | stack: 'Error: Mock Error...(rest of stack omitted for brevity)', 190 | time: 1544681706941 191 | } 192 | ], 193 | isInErrorState: true, 194 | lastSyncTime: 1544681470341, 195 | lastNtpTime: 1544681470828, 196 | lastError: { 197 | name: 'Error', 198 | message: 'Mock Error', 199 | server: { 200 | server: 'FAIL.FAIL.FAIL', 201 | port: 666 202 | }, 203 | stack: 'Error: Mock Error\n at Object.getNetworkTime (/Users/xyz/rnative/react-native-clock-sync/__mocks__/react-native-ntp-client.js:37:10)\n at clockSync.getNetworkTime [as getDelta] (/Users/xyz/rnative/react-native-clock-sync/index.js:103:17)\n at clockSync.getDelta (/Users/xyz/rnative/react-native-clock-sync/index.js:226:8)\n ... (rest of stack omitted for brevity)', 204 | time: 1544681598417 205 | }, 206 | lifetimeErrorCount: 6, 207 | maxConsecutiveErrorCount: 2 208 | } 209 | 210 | ``` 211 | 212 | ### getIsOnline() 213 | 214 | Returns the current `boolean` network status of the clockSync instance. `false` indicates that no network activity will be performed/NTP servers will not be contacted. 215 | 216 | ### getTime() 217 | 218 | Returns unix timestamp based on delta values between server and your local time. This is the time that can be used instead of ```new Date().getTime()``` 219 | 220 | #### Example 221 | 222 | ```javascript 223 | clock.getTime(); 224 | ``` 225 | 226 | ### setOnline(boolean) 227 | 228 | Sets the current (per-instance) network status. Passing an argument of `true` (if the current status is `false`) will cause the instance to immediately attempt an NTP fetch, and resume the internal update timer at a frequency determined by the `syncDelay` config parameter (or its default). Conversely, passing `false` (when current is `true`) immediately stops the internal timer and prevents any further network activity. **NOTE:** Calling this method with an argument that matches the instance's current network state results in a no-op. 229 | 230 | #### Offline behavior 231 | 232 | When set to *offline*, calls to `getTime()` will return the current device time adjusted by whatever values are currently in the history. (or no adjustment if the history is empty/NTP has never been fetched) 233 | 234 | Calls to `syncTime()` are effectively a no-op in offline mode. No NTP fetch will be performed, and no updates to the local time history will be made (to prevent polluting the running average drift). 235 | 236 | #### Example 237 | 238 | When dealing with mobile development, it is sometimes necessary to respond to changes in network availability. `setOnline` provides a convenient 'hook' to do so, preventing unnecessary errors and timeouts. 239 | 240 | React Native allows for watching device network status. Which can be used with `setOnline` like so: 241 | 242 | ```javascript 243 | import clockSync from 'react-native-clock-sync'; 244 | import { NetInfo } from 'react-native'; 245 | 246 | // start in offline state 247 | const config = { 248 | startOnline: false 249 | }; 250 | 251 | const clock = new clockSync(config); 252 | 253 | // set initial state 254 | NetInfo.isConnected.fetch().then(isConnected => { 255 | clock.setOnline(isConnected); 256 | }); 257 | 258 | // this handler will receive the device's network status changes 259 | function handleConnectivityChange (isConnected) { 260 | clock.setOnline(isConnected); 261 | } 262 | 263 | // register handler with react-native 264 | NetInfo.isConnected.addEventListener('connectionChange', handleConnectivityChange); 265 | ``` 266 | 267 | **NOTE:** The example above does not account for rapid changes in network state. You may wish to add additional handling to 'de-bounce' such changes. Also, remember to remove the listener and set your clockSync instance to *offline* when done (un-mounting components, shutting down, etc.) 268 | 269 | ### syncTime( *[callback]* ) 270 | 271 | An on-demand method that will force a sync with an NTP server. Will not sync or update when *offline*. 272 | 273 | **NOTE:** You generally do not need to invoke a manual sync since *clockSync* automatically runs sync according to the specified `syncDelay` interval (or its default). 274 | 275 | An optional callback function may be supplied to monitor completion/failure of the requested sync. Callback will accept a single `boolean` parameter that indicates success or failure of the requested sync. Callback will always be given `false` when instance is *offline*. 276 | 277 | ```javascript 278 | clock.syncTime(); 279 | ``` 280 | 281 | or 282 | 283 | ```javascript 284 | function cb(success) { 285 | console.log("sync was" + (success ? "" : " not") + " a success"); 286 | } 287 | 288 | clock.syncTime(cb); 289 | ``` 290 | -------------------------------------------------------------------------------- /__mocks__/react-native-ntp-client.js: -------------------------------------------------------------------------------- 1 | // __mocks__/react-native-ntp-client.js 2 | 'use strict'; 3 | 4 | const client = jest.genMockFromModule('react-native-ntp-client'); 5 | 6 | // a fake NTP server domain that triggers the error callback in 'getNetworkTime' 7 | const MOCK_FAILING_SERVER = 'FAIL.FAIL.FAIL'; 8 | const MAX_ABS_JITTER_MS = 500; 9 | 10 | // internal value to mock an NTP server's delta time in ms 11 | let __offset_ms = 0; 12 | 13 | // custom method to allow tests to set a server delta time 14 | // can be +/- (values in milliseconds) 15 | function __setOffsetMS(ms) { 16 | __offset_ms = ms; 17 | } 18 | 19 | let __jitter = false; 20 | 21 | // jitter used to simulate random delta between local and ntp times 22 | // typically shouldn't use jitter when offset !== 0 23 | function __useJitter(j) { 24 | __jitter = j; 25 | } 26 | 27 | function __reset() { 28 | __offset_ms = 0; 29 | __jitter = false; 30 | } 31 | 32 | // custom getNetworkTime that simply calls callback 33 | // after generating a time value, or error 34 | function getNetworkTime(s, p, cb) { 35 | if (cb) { 36 | if (s === MOCK_FAILING_SERVER) { 37 | cb(new Error('Mock Error'), null); 38 | } else { 39 | let jitter = 0; 40 | if (__jitter) { 41 | jitter = Math.floor(Math.random() * ((MAX_ABS_JITTER_MS * 2) + 1)) - MAX_ABS_JITTER_MS; 42 | } 43 | cb(null, new Date( Date.now() + jitter + __offset_ms )); 44 | } 45 | } 46 | } 47 | 48 | /**** mocked API ****/ 49 | client.MOCK_FAILING_SERVER = MOCK_FAILING_SERVER; 50 | client.__setOffsetMS = __setOffsetMS; 51 | client.__useJitter = __useJitter; 52 | client.__resetForTest = __reset; 53 | // overrides 54 | client.getNetworkTime = getNetworkTime; 55 | 56 | module.exports = client; 57 | -------------------------------------------------------------------------------- /__tests__/behavior.test.js: -------------------------------------------------------------------------------- 1 | const clockSync = require('../index'); 2 | 3 | // react-native-ntp-client is mocked in __mocks__ 4 | const client = require('react-native-ntp-client'); 5 | 6 | // a 'mock' deterministic version of Date 'now' 7 | let staticTimeMS = 0; // unix epoch 8 | const origDateNow = Date.now.bind(global.Date); 9 | const mockDateNow = jest.fn(() => { 10 | return staticTimeMS++; 11 | }); 12 | 13 | describe('v1.1.0 behavior', () => { 14 | 15 | beforeAll(() => { 16 | // install mock Date.now 17 | global.Date.now = mockDateNow; 18 | }); 19 | 20 | afterAll(() => { 21 | // restore original Date functions 22 | global.Date.now = origDateNow; 23 | }); 24 | 25 | beforeEach(() => { 26 | client.__resetForTest(); 27 | }); 28 | 29 | test('mock date', () => { 30 | expect(Date.now()).toBe(0); 31 | expect(Date.now()).toBe(1); 32 | expect(Date.now()).toBe(2); 33 | expect(Date.now()).toBe(3); 34 | expect(Date.now()).toBe(4); 35 | }); 36 | 37 | test('cycleServers true', () => { 38 | const config = { 39 | cycleServers: true, 40 | servers: [ 41 | 'foo.bar.com', 42 | {server: 'bar.baz.gov', port: 666}, 43 | 'a.b.c', 44 | 'acb.xyz.def.uvw' 45 | ] 46 | }; 47 | const cs = new clockSync(config); 48 | // 2 shifts -- idx 2 49 | cs.shiftServer(); 50 | cs.shiftServer(); 51 | expect(cs.currentIndex).toBe(2); 52 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[2]); 53 | expect(cs.historyDetails.currentServer.port).toBe(cs.client.defaultNtpPort); 54 | // 3 more shifts -- idx 1 55 | cs.shiftServer(); 56 | cs.shiftServer(); 57 | cs.shiftServer(); 58 | expect(cs.currentIndex).toBe(1); 59 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[1].server); 60 | expect(cs.historyDetails.currentServer.port).toBe(config.servers[1].port); 61 | }); 62 | 63 | test('cycleServers false', () => { 64 | const config = { 65 | cycleServers: false, 66 | servers: [ 67 | 'foo.bar.com', 68 | {server: 'bar.baz.gov', port: 666}, 69 | 'a.b.c', 70 | 'acb.xyz.def.uvw' 71 | ] 72 | }; 73 | const cs = new clockSync(config); 74 | // 2 shifts -- idx 2 75 | cs.shiftServer(); 76 | cs.shiftServer(); 77 | expect(cs.currentIndex).toBe(2); 78 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[2]); 79 | expect(cs.historyDetails.currentServer.port).toBe(cs.client.defaultNtpPort); 80 | // 3 more shifts -- idx (config.servers.length - 1) 81 | var lastIdx = config.servers.length - 1; 82 | cs.shiftServer(); 83 | cs.shiftServer(); 84 | cs.shiftServer(); 85 | expect(cs.currentIndex).toBe(lastIdx); 86 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[lastIdx]); 87 | expect(cs.historyDetails.currentServer.port).toBe(cs.client.defaultNtpPort); 88 | }); 89 | 90 | test('setOnline true; startOnline false', done => { 91 | const offset = -1000; 92 | client.__setOffsetMS(offset); // ensure mock ntp is slow 93 | const config = { 94 | startOnline: false 95 | }; 96 | const cs = new clockSync(config); 97 | expect(cs.isOnline).toBe(false); 98 | expect(cs.historyDetails.deltas).toHaveLength(0); // no initial sync 99 | expect(cs.tickId).toBeNull(); 100 | 101 | // 1st callback for offline delta time 102 | function cb(dt) { 103 | expect(dt).toBe(0); 104 | cs.setOnline(true); // will trigger 1st update to cs.historyDetails.deltas array 105 | cs.getDelta(cb2); // will trigger 2nd update to cs.historyDetails.deltas array 106 | } 107 | 108 | // 2nd callback for online delta time; fires 'test done' 109 | function cb2(dt) { 110 | expect(cs.isOnline).toBe(true); 111 | expect(cs.historyDetails.deltas).toHaveLength(2); 112 | expect(cs.tickId).not.toBeNull(); 113 | expect(dt).toBeLessThanOrEqual(offset); 114 | done(); 115 | } 116 | 117 | // start chain of callbacks by inspecting the offline delta time 118 | cs.getDelta(cb); 119 | }); 120 | 121 | test('setOnline false; startOnline true', done => { 122 | const offset = -2000; 123 | client.__setOffsetMS(offset); // ensure mock ntp is slow 124 | const config = { 125 | startOnline: true 126 | }; 127 | const cs = new clockSync(config); 128 | expect(cs.isOnline).toBe(true); 129 | expect(cs.historyDetails.deltas).toHaveLength(1); // 1 initial sync 130 | expect(cs.tickId).not.toBeNull(); 131 | 132 | // 1st callback for online delta time 133 | function cb(dt) { 134 | expect(dt).toBeLessThanOrEqual(offset); 135 | cs.setOnline(false); 136 | cs.getDelta(cb2); 137 | } 138 | 139 | // 2nd callback for offline delta time; fires 'test done' 140 | function cb2(dt) { 141 | expect(cs.isOnline).toBe(false); 142 | expect(cs.historyDetails.deltas).toHaveLength(2); // initial sync + 1st getDelta call 143 | expect(cs.tickId).toBeNull(); 144 | expect(dt).toBe(0); 145 | done(); 146 | } 147 | 148 | // start chain of callbacks by inspecting the online delta time 149 | cs.getDelta(cb); // will push 1 update into delta 150 | }); 151 | 152 | test('setOnline false; startOnline false', done => { 153 | const offset = -3000; 154 | client.__setOffsetMS(offset); // ensure mock ntp is slow 155 | const config = { 156 | startOnline: false 157 | }; 158 | const cs = new clockSync(config); 159 | expect(cs.isOnline).toBe(false); 160 | expect(cs.historyDetails.deltas).toHaveLength(0); 161 | expect(cs.tickId).toBeNull(); 162 | 163 | // 1st callback for offline delta time 164 | function cb(dt) { 165 | expect(dt).toBe(0); 166 | cs.setOnline(false); // should have no effect 167 | cs.getDelta(cb2); 168 | } 169 | 170 | // 2nd callback for offline delta time; fires 'test done' 171 | function cb2(dt) { 172 | expect(cs.isOnline).toBe(false); 173 | expect(cs.historyDetails.deltas).toHaveLength(0); // should still be empty 174 | expect(cs.tickId).toBeNull(); 175 | expect(dt).toBe(0); 176 | done(); 177 | } 178 | 179 | // start chain of callbacks by inspecting the online delta time 180 | cs.getDelta(cb); 181 | }); 182 | 183 | test('setOnline true; startOnline true', done => { 184 | const offset = -4000; 185 | client.__setOffsetMS(offset); // ensure mock ntp is slow 186 | const config = { 187 | startOnline: true 188 | }; 189 | const cs = new clockSync(config); 190 | expect(cs.isOnline).toBe(true); 191 | expect(cs.historyDetails.deltas).toHaveLength(1); // initial sync 192 | expect(cs.tickId).not.toBeNull(); 193 | var tid = cs.tickId; 194 | 195 | // 1st callback for online delta time 196 | function cb(dt) { 197 | expect(dt).toBeLessThanOrEqual(offset); 198 | cs.setOnline(true); // should have no effect 199 | cs.getDelta(cb2); // adds 3rd update to delta 200 | } 201 | 202 | // 2nd callback for offline delta time; fires 'test done' 203 | function cb2(dt) { 204 | expect(cs.isOnline).toBe(true); 205 | expect(cs.historyDetails.deltas).toHaveLength(3); 206 | expect(cs.tickId).not.toBeNull(); 207 | expect(cs.tickId).toBe(tid); // timer id should be the same 208 | expect(dt).toBeLessThanOrEqual(offset); 209 | done(); 210 | } 211 | 212 | // start chain of callbacks by inspecting the online delta time 213 | cs.getDelta(cb); // adds 2nd update to delta 214 | }); 215 | 216 | test('getTime method', () => { 217 | const config = { 218 | startOnline: false 219 | }; 220 | const cs = new clockSync(config); 221 | expect(cs.historyDetails.deltas).toHaveLength(0); 222 | // pre-fill delta array 223 | cs.historyDetails.deltas = [ 224 | {dt:5000}, 225 | {dt:10000}, 226 | {dt:5000}, 227 | {dt:2500} 228 | ]; // avg === 5625 229 | const now_ish = Date.now(); 230 | const time = cs.getTime(); 231 | // time should be AT LEAST 5625 GREATER THAN now_ish 232 | expect(time - now_ish).toBeGreaterThanOrEqual(5625); 233 | }); 234 | 235 | test('computeDeltaAndUpdateHistory method', () => { 236 | const config = { 237 | startOnline: false 238 | }; 239 | const cs = new clockSync(config); 240 | expect(cs.historyDetails.deltas).toHaveLength(0); // empty delta array 241 | cs.computeDeltaAndUpdateHistory(new Date()); 242 | expect(cs.historyDetails.deltas).toHaveLength(1); // delta updated once 243 | expect(cs.historyDetails.deltas[0]).toEqual(expect.objectContaining({ 244 | dt: expect.any(Number), 245 | ntp: expect.any(Number) 246 | })); 247 | expect(cs.historyDetails.deltas[0].ntp).not.toBe(0); 248 | cs.computeDeltaAndUpdateHistory(new Date(Date.now() - 50000)); 249 | expect(cs.historyDetails.deltas).toHaveLength(2); // delta updated once more 250 | expect(cs.historyDetails.deltas[1]).toEqual(expect.objectContaining({ 251 | dt: expect.any(Number), 252 | ntp: expect.any(Number) 253 | })); 254 | expect(cs.historyDetails.deltas[1].dt).not.toBe(0); 255 | expect(cs.historyDetails.deltas[1].ntp).not.toBe(0); 256 | expect(cs.historyDetails.deltas[1].dt).not.toBe(cs.historyDetails.deltas[0].dt); 257 | expect(cs.historyDetails.deltas[1].ntp).not.toBe(cs.historyDetails.deltas[0].ntp); 258 | }); 259 | 260 | test('syncTime method', () => { 261 | const cb = jest.fn(x => x); 262 | const config = { 263 | cycleServers: true, 264 | servers: [ 265 | 'good.ok.server', 266 | client.MOCK_FAILING_SERVER 267 | ], 268 | startOnline: false 269 | }; 270 | const cs = new clockSync(config); 271 | 272 | // offline behavior 273 | cs.syncTime(cb); 274 | expect(cb.mock.calls.length).toBe(1); 275 | expect(cb.mock.results[0].value).toBe(false); 276 | expect(cs.historyDetails.deltas).toHaveLength(0); 277 | expect(cs.historyDetails.errors).toHaveLength(0); 278 | expect(cs.historyDetails.currentConsecutiveErrorCount).toBe(0); 279 | expect(cs.historyDetails.isInErrorState).toBe(false); 280 | expect(cs.historyDetails.lastSyncTime).toBeNull(); 281 | expect(cs.historyDetails.lastNtpTime).toBeNull(); 282 | expect(cs.historyDetails.lastError).toBeNull(); 283 | expect(cs.historyDetails.lifetimeErrorCount).toBe(0); 284 | expect(cs.historyDetails.maxConsecutiveErrorCount).toBe(0); 285 | 286 | // online behavior - no errors 287 | cs.setOnline(true); // creates 1st delta 288 | cs.syncTime(cb); // creates 2nd delta 289 | expect(cb.mock.calls.length).toBe(2); 290 | expect(cb.mock.results[1].value).toBe(true); 291 | expect(cs.historyDetails.deltas).toHaveLength(2); 292 | expect(cs.historyDetails.deltas[0]).toEqual(expect.objectContaining({ 293 | dt: expect.any(Number), 294 | ntp: expect.any(Number) 295 | })); 296 | expect(cs.historyDetails.errors).toHaveLength(0); 297 | expect(cs.historyDetails.currentConsecutiveErrorCount).toBe(0); 298 | expect(cs.historyDetails.isInErrorState).toBe(false); 299 | expect(cs.historyDetails.lastSyncTime).not.toBeNull(); 300 | expect(cs.historyDetails.lastNtpTime).not.toBeNull(); 301 | expect(cs.historyDetails.lastError).toBeNull(); 302 | expect(cs.historyDetails.lifetimeErrorCount).toBe(0); 303 | expect(cs.historyDetails.maxConsecutiveErrorCount).toBe(0); 304 | 305 | let lastSyncTime = cs.historyDetails.lastSyncTime; 306 | let lastNtpTime = cs.historyDetails.lastNtpTime; 307 | 308 | // online behavior - errors 309 | cs.shiftServer(); // manually advance to the bad server 310 | cs.syncTime(cb); // creates error 311 | expect(cb.mock.calls.length).toBe(3); 312 | expect(cb.mock.results[2].value).toBe(false); 313 | expect(cs.historyDetails.deltas).toHaveLength(2); // no change 314 | expect(cs.historyDetails.errors).toHaveLength(1); 315 | expect(cs.historyDetails.errors[0]).toEqual(expect.objectContaining({ 316 | name: expect.any(String), 317 | message: expect.any(String), 318 | server: expect.objectContaining({ 319 | server: config.servers[1], /* server that failed */ 320 | port: client.defaultNtpPort 321 | }), 322 | stack: expect.any(String), 323 | time: expect.any(Number) 324 | })); 325 | expect(cs.historyDetails.currentConsecutiveErrorCount).toBe(1); 326 | expect(cs.historyDetails.isInErrorState).toBe(true); 327 | expect(cs.historyDetails.lastSyncTime).toBe(lastSyncTime); // no change 328 | expect(cs.historyDetails.lastNtpTime).toBe(lastNtpTime); // no change 329 | expect(cs.historyDetails.lastError).toEqual(cs.historyDetails.errors[0]); 330 | expect(cs.historyDetails.lifetimeErrorCount).toBe(1); 331 | expect(cs.historyDetails.maxConsecutiveErrorCount).toBe(1); 332 | 333 | // sync again, on good server 334 | cs.syncTime(cb); 335 | expect(cb.mock.calls.length).toBe(4); 336 | expect(cb.mock.results[3].value).toBe(true); 337 | expect(cs.historyDetails.deltas).toHaveLength(3); 338 | expect(cs.historyDetails.errors).toHaveLength(1); // no change 339 | expect(cs.historyDetails.currentConsecutiveErrorCount).toBe(0); 340 | expect(cs.historyDetails.isInErrorState).toBe(false); 341 | expect(cs.historyDetails.lastSyncTime).not.toBe(lastSyncTime); 342 | expect(cs.historyDetails.lastNtpTime).not.toBe(lastNtpTime); 343 | expect(cs.historyDetails.lifetimeErrorCount).toBe(1); // no change 344 | expect(cs.historyDetails.maxConsecutiveErrorCount).toBe(1); // no change 345 | }); 346 | 347 | describe('timer-based behavior', () => { 348 | 349 | beforeEach(() => { 350 | jest.useFakeTimers(); 351 | }); 352 | 353 | test('getHistory method; deltas array', () => { 354 | const config = { 355 | history: 5, 356 | startOnline: false, 357 | syncDelay: 5 358 | }; 359 | const cs = new clockSync(config); 360 | expect(setInterval).not.toBeCalled(); 361 | let h = cs.getHistory(); 362 | expect(h).toEqual(expect.objectContaining({ 363 | deltas: expect.any(Array) 364 | })); 365 | expect(h.deltas).toHaveLength(0); 366 | cs.setOnline(true); 367 | h = cs.getHistory(); 368 | expect(h.deltas).toHaveLength(1); 369 | cs.setOnline(false); 370 | // advance by 1 tick 371 | jest.advanceTimersByTime(config.syncDelay * 1000); 372 | h = cs.getHistory(); 373 | expect(h.deltas).toHaveLength(1); // no new updates 374 | cs.setOnline(true); 375 | // advance ticks beyond history limit 376 | jest.advanceTimersByTime(config.syncDelay * 1000 * (config.history + 2)); 377 | h = cs.getHistory(); 378 | expect(h.deltas).toHaveLength(config.history); 379 | }); 380 | 381 | test('getHistory method; deltas array values', () => { 382 | const offset = -3000; 383 | client.__setOffsetMS(offset); // ensure mock ntp is slow 384 | // 1.1.0 default startOnline true 385 | const config = { 386 | history: 5, 387 | syncDelay: 5 388 | }; 389 | const cs = new clockSync(config); 390 | expect(setInterval).toBeCalled(); 391 | let h = cs.getHistory(); 392 | expect(h).toEqual(expect.objectContaining({ 393 | deltas: expect.any(Array) 394 | })); 395 | expect(h.deltas).toHaveLength(1); 396 | expect(h.deltas[0]).toEqual(expect.objectContaining({ 397 | dt: expect.any(Number), 398 | ntp: expect.any(Number) 399 | })); 400 | expect(h.deltas[0].dt).not.toBe(0); 401 | expect(h.deltas[0].dt).toBeLessThanOrEqual(offset); 402 | 403 | // make sure ntp times are different even when test execution runs really fast 404 | client.__setOffsetMS(0); 405 | client.__useJitter(true); 406 | 407 | // advance ticks beyond history limit 408 | jest.advanceTimersByTime(config.syncDelay * 1000 * (config.history + 2)); 409 | h = cs.getHistory(); 410 | expect(h.deltas).toHaveLength(config.history); 411 | let prevNtp = 999999; 412 | h.deltas.forEach(d => { 413 | expect(d).toEqual(expect.objectContaining({ 414 | dt: expect.any(Number), 415 | ntp: expect.any(Number) 416 | })); 417 | expect(d.dt).not.toBe(0); 418 | expect(d.ntp).not.toBe(prevNtp); 419 | prevNtp = d.ntp; 420 | }); 421 | 422 | }); 423 | 424 | test('getHistory method; details', () => { 425 | client.__setOffsetMS(0); 426 | client.__useJitter(true); 427 | const config = { 428 | cycleServers: true, 429 | history: 5, 430 | servers: [ 431 | 'ok.fake.server', 432 | {server: client.MOCK_FAILING_SERVER, port: 666}, 433 | {server: client.MOCK_FAILING_SERVER, port: 456}, 434 | 'good.fake.server' 435 | ], 436 | startOnline: false, 437 | syncDelay: 5 438 | }; 439 | const cs = new clockSync(config); 440 | expect(setInterval).not.toBeCalled(); 441 | let h = cs.getHistory(); 442 | 443 | // validate the shape of the history object 444 | expect(h).toEqual(expect.objectContaining({ 445 | currentConsecutiveErrorCount: 0, 446 | currentServer: expect.objectContaining({ 447 | server: config.servers[0], 448 | port: client.defaultNtpPort 449 | }), 450 | deltas: expect.any(Array), 451 | errors: expect.any(Array), 452 | isInErrorState: false, 453 | lastSyncTime: null, 454 | lastNtpTime: null, 455 | lastError: null, 456 | lifetimeErrorCount: 0, 457 | maxConsecutiveErrorCount: 0 458 | })); 459 | expect(h.deltas).toHaveLength(0); 460 | 461 | // single update via setOnline 462 | cs.setOnline(true); 463 | h = cs.getHistory(); 464 | expect(h.deltas).toHaveLength(1); 465 | expect(h.deltas[0]).toEqual(expect.objectContaining({ 466 | dt: expect.any(Number), 467 | ntp: expect.any(Number) 468 | })); 469 | expect(h.errors).toHaveLength(0); 470 | expect(h.currentConsecutiveErrorCount).toBe(0); 471 | expect(h.isInErrorState).toBe(false); 472 | expect(h.lastSyncTime).not.toBeNull(); 473 | expect(h.lastNtpTime).not.toBeNull(); 474 | expect(h.lastError).toBeNull(); 475 | expect(h.lifetimeErrorCount).toBe(0); 476 | expect(h.maxConsecutiveErrorCount).toBe(0); 477 | 478 | let lastSyncTime = h.lastSyncTime; 479 | let lastNtpTime = h.lastNtpTime; 480 | 481 | // trigger an error: 482 | // manually shift to bad server 483 | cs.shiftServer(); 484 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[1].server); 485 | // advance 2 ticks (should have 2 errors) 486 | jest.advanceTimersByTime(config.syncDelay * 1000 * 2); 487 | h = cs.getHistory(); 488 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[3]); // server should have advanced 2 times 489 | expect(h.deltas).toHaveLength(1); // last 2 syncs were error, no update to deltas 490 | expect(h.errors).toHaveLength(2); 491 | // 1st error 492 | expect(h.errors[0]).toEqual(expect.objectContaining({ 493 | name: expect.any(String), 494 | message: expect.any(String), 495 | server: expect.objectContaining({ 496 | server: config.servers[1].server, /* server that failed */ 497 | port: config.servers[1].port 498 | }), 499 | stack: expect.any(String), 500 | time: expect.any(Number) 501 | })); 502 | // 2nd error 503 | expect(h.errors[1]).toEqual(expect.objectContaining({ 504 | name: expect.any(String), 505 | message: expect.any(String), 506 | server: expect.objectContaining({ 507 | server: config.servers[2].server, /* server that failed */ 508 | port: config.servers[2].port 509 | }), 510 | stack: expect.any(String), 511 | time: expect.any(Number) 512 | })); 513 | expect(h.currentConsecutiveErrorCount).toBe(2); 514 | expect(h.isInErrorState).toBe(true); 515 | expect(h.lastSyncTime).toBe(lastSyncTime); // no change 516 | expect(h.lastNtpTime).toBe(lastNtpTime); // no change 517 | expect(h.lastError).toEqual(h.errors[1]); // same properties 518 | expect(h.lifetimeErrorCount).toBe(2); 519 | expect(h.maxConsecutiveErrorCount).toBe(2); 520 | 521 | // current server is good, advance ticks by one more than limit 522 | jest.advanceTimersByTime(config.syncDelay * 1000 * config.history); 523 | h = cs.getHistory(); 524 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[3]); // server should not have advanced 525 | // deltas should have populated 526 | expect(h.deltas).toHaveLength(config.history); // should be at limit 527 | expect(h.lastSyncTime).not.toBe(lastSyncTime); 528 | expect(h.lastNtpTime).not.toBe(lastNtpTime); 529 | // error flags should be clear 530 | expect(h.currentConsecutiveErrorCount).toBe(0); 531 | expect(h.isInErrorState).toBe(false); 532 | // errors should not have updated 533 | expect(h.errors).toHaveLength(2); // no change 534 | expect(h.lastError).toEqual(h.errors[1]); // no change 535 | expect(h.lifetimeErrorCount).toBe(2); // no change 536 | expect(h.maxConsecutiveErrorCount).toBe(2); // no change 537 | 538 | let lastDelta = h.deltas[config.history - 1]; 539 | lastSyncTime = h.lastSyncTime; 540 | lastNtpTime = h.lastNtpTime; 541 | 542 | // manually advance to cause 4 more errors (pushing over limit) 543 | // 2 shifts (cycleServers true) should get us back to the 1st bad server 544 | cs.shiftServer(); 545 | cs.shiftServer(); 546 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[1].server); 547 | // advance 2 ticks (2 errors) 548 | jest.advanceTimersByTime(config.syncDelay * 1000 * 2); 549 | // back at last good server, manually shift again 550 | cs.shiftServer(); 551 | cs.shiftServer(); 552 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[1].server); 553 | // advance 2 ticks (2 more errors) 554 | jest.advanceTimersByTime(config.syncDelay * 1000 * 2); 555 | h = cs.getHistory(); 556 | // no more deltas should have populated 557 | expect(h.deltas).toHaveLength(config.history); // should be at limit 558 | expect(h.lastSyncTime).toBe(lastSyncTime); // no change 559 | expect(h.lastNtpTime).toBe(lastNtpTime); // no change 560 | // error flags should be set again 561 | expect(h.currentConsecutiveErrorCount).toBe(4); 562 | expect(h.isInErrorState).toBe(true); 563 | // errors should have updated 564 | expect(h.errors).toHaveLength(config.history); // should be at limit 565 | expect(h.lastError).toEqual(h.errors[config.history - 1]); 566 | expect(h.lifetimeErrorCount).toBe(6); 567 | expect(h.maxConsecutiveErrorCount).toBe(4); 568 | 569 | }); 570 | 571 | test('interval with startOnline false -> true -> false', () => { 572 | const config = { 573 | startOnline: false 574 | }; 575 | const cs = new clockSync(config); 576 | expect(setInterval).not.toBeCalled(); 577 | cs.setOnline(true); 578 | expect(setInterval).toHaveBeenCalledTimes(1); 579 | cs.setOnline(false); 580 | expect(setInterval).toHaveBeenCalledTimes(1); 581 | expect(clearInterval).toBeCalled(); 582 | }); 583 | 584 | test('interval with startOnline true -> false -> true', () => { 585 | // 1.1.0 startOnline default true 586 | const cs = new clockSync(); 587 | expect(setInterval).toBeCalled(); 588 | cs.setOnline(false); 589 | expect(clearInterval).toBeCalled(); 590 | expect(setInterval).toHaveBeenCalledTimes(1); 591 | cs.setOnline(true); 592 | expect(setInterval).toHaveBeenCalledTimes(2); 593 | }); 594 | 595 | test('interval callback invocation', () => { 596 | // 1.1.0 startOnline default true 597 | const config = { 598 | syncDelay: 5 599 | }; 600 | const cs = new clockSync(config); 601 | const spy = jest.spyOn(cs, 'getDelta'); 602 | expect(setInterval).toBeCalled(); 603 | expect(spy).not.toBeCalled(); // missed 1st call in constructor 604 | jest.advanceTimersByTime(config.syncDelay * 1000); 605 | expect(spy).toBeCalled(); 606 | }); 607 | 608 | test('ntp error handled; cycleServers false', () => { 609 | // 1.1.0 startOnline default true, cycleServers false 610 | const config = { 611 | servers: [ 612 | client.MOCK_FAILING_SERVER, 613 | 'foo.bar.com' 614 | ], 615 | syncDelay: 5 616 | }; 617 | const cs = new clockSync(config); // construction should cause a shiftServer 618 | expect(setInterval).toBeCalled(); 619 | expect(cs.currentIndex).toBe(1); 620 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[1]); 621 | jest.advanceTimersByTime(config.syncDelay * 1000); // another syncTime should cause shiftServer to hold at final server 622 | expect(cs.currentIndex).toBe(1); 623 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[1]); 624 | }); 625 | 626 | test('ntp error handled; cycleServers true', () => { 627 | // 1.1.0 startOnline default true 628 | const config = { 629 | cycleServers: true, 630 | servers: [ 631 | client.MOCK_FAILING_SERVER, 632 | client.MOCK_FAILING_SERVER, 633 | client.MOCK_FAILING_SERVER 634 | ], 635 | syncDelay: 5 636 | }; 637 | const cs = new clockSync(config); // construction should cause a shiftServer 638 | const spy = jest.spyOn(cs, 'shiftServer'); // NOTE: shiftServer was called once before we started inspecting it 639 | expect(setInterval).toBeCalled(); 640 | expect(cs.currentIndex).toBe(1); 641 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[1]); 642 | jest.advanceTimersByTime(config.syncDelay * 1000 * 2); // another 2 syncTime(s) 643 | expect(spy.mock.calls.length).toBe(2); 644 | expect(cs.currentIndex).toBe(0); // should have wrapped around 645 | expect(cs.historyDetails.currentServer.server).toBe(config.servers[0]); 646 | }); 647 | 648 | }); 649 | 650 | }); 651 | -------------------------------------------------------------------------------- /__tests__/config.test.js: -------------------------------------------------------------------------------- 1 | const clockSync = require('../index'); 2 | 3 | // react-native-ntp-client is mocked in __mocks__ 4 | const client = require('react-native-ntp-client'); 5 | 6 | describe('Configuration parsing and behavior', () => { 7 | 8 | beforeEach(() => { 9 | client.__resetForTest(); 10 | }); 11 | 12 | test('initialize default clockSync instance (undef config)', () => { 13 | const cs = new clockSync(); 14 | expect(cs.currentIndex).toEqual(0); 15 | expect(cs.historyDetails.currentServer).toMatchObject({ 16 | server: 'pool.ntp.org', 17 | port: 123 18 | }); 19 | expect(cs.cycleServers).toBe(false); 20 | expect(cs.isOnline).toBe(true); 21 | expect(cs.tickId).not.toBeNull(); 22 | expect(cs.tickRate).toBe(300000); 23 | expect(cs.historyDetails.deltas).toHaveLength(1); // due to mock 24 | expect(cs.limit).toBe(10); 25 | expect(cs.getTime()).toBeGreaterThan(0); 26 | }); 27 | 28 | test('initialize default clockSync instance (empty config)', () => { 29 | const cs = new clockSync({}); 30 | expect(cs.currentIndex).toEqual(0); 31 | expect(cs.historyDetails.currentServer).toMatchObject({ 32 | server: 'pool.ntp.org', 33 | port: 123 34 | }); 35 | expect(cs.cycleServers).toBe(false); 36 | expect(cs.isOnline).toBe(true); 37 | expect(cs.tickId).not.toBeNull(); 38 | expect(cs.tickRate).toBe(300000); 39 | expect(cs.historyDetails.deltas).toHaveLength(1); // due to mock 40 | expect(cs.limit).toBe(10); 41 | expect(cs.getTime()).toBeGreaterThan(0); 42 | }); 43 | 44 | test('initialize misc. history config values', () => { 45 | function make(config) { 46 | return () => (new clockSync(config)); 47 | } 48 | // errors 49 | expect( make( {history: -300} )).toThrow(); 50 | expect( make( {history: -12} )).toThrow(); 51 | expect( make( {history: '-300'} )).toThrow(); 52 | // invalid, set to default 53 | let c = make({history: 'one'})(); 54 | expect(c.limit).toBe(10); 55 | c = make({history: 0})(); 56 | expect(c.limit).toBe(10); 57 | c = make({history: '0'})(); 58 | expect(c.limit).toBe(10); 59 | c = make({history: '-0.9'})(); // special case. gets truncated to an int -0 60 | expect(c.limit).toBe(10); 61 | // valid 62 | c = make({history: '15'})(); 63 | expect(c.limit).toBe(15); 64 | c = make({history: 250})(); 65 | expect(c.limit).toBe(250); 66 | c = make({history: 25.670})(); // floats are truncated 67 | expect(c.limit).toBe(25); 68 | }); 69 | 70 | test('initialize misc. syncDelay config values', () => { 71 | function make(config) { 72 | return () => (new clockSync(config)); 73 | } 74 | // errors 75 | expect( make( {syncDelay: -300} )).toThrow(); 76 | expect( make( {syncDelay: -12} )).toThrow(); 77 | expect( make( {syncDelay: '-300'} )).toThrow(); 78 | expect( make( {syncDelay: '-0.9'} )).toThrow(); 79 | // invalid, set to default 80 | const def = 300 * 1000; 81 | let c = make({syncDelay: 'one'})(); 82 | expect(c.tickRate).toBe(def); 83 | c = make({syncDelay: 0})(); 84 | expect(c.tickRate).toBe(def); 85 | c = make({syncDelay: '0'})(); 86 | expect(c.tickRate).toBe(def); 87 | // valid 88 | c = make({syncDelay: '15'})(); 89 | expect(c.tickRate).toBe(15 * 1000); 90 | c = make({syncDelay: 250})(); 91 | expect(c.tickRate).toBe(250 * 1000); 92 | c = make({syncDelay: 25.670})(); 93 | expect(c.tickRate).toBe(25.670 * 1000); 94 | }); 95 | 96 | describe('initialize clockSync instance with VALID server configs', () => { 97 | 98 | test('array of string servers', () => { 99 | const validConfig = { 100 | servers: [ 101 | 'foo.bar.com', 102 | 'bar.baz.gov', 103 | 'a.b.c', 104 | 'acb.xyz.def.uvw' 105 | ] 106 | }; 107 | const cs = new clockSync(validConfig); 108 | expect(cs.currentIndex).toEqual(0); 109 | expect(cs.historyDetails.currentServer).toMatchObject({ 110 | server: 'foo.bar.com', 111 | port: 123 112 | }); 113 | expect(cs.cycleServers).toBe(false); 114 | expect(cs.isOnline).toBe(true); 115 | expect(cs.tickId).not.toBeNull(); 116 | expect(cs.tickRate).toBe(300000); 117 | expect(cs.historyDetails.deltas).toHaveLength(1); // due to mock 118 | expect(cs.limit).toBe(10); 119 | expect(cs.getTime()).toBeGreaterThan(0); 120 | }); 121 | 122 | test('array of server-only objects', () => { 123 | const validConfig = { 124 | servers: [ 125 | {server: 'foo.bar.com'}, 126 | {server: 'bar.baz.gov'}, 127 | {server: 'a.b.c'}, 128 | {server: 'acb.xyz.def.uvw'} 129 | ] 130 | }; 131 | const cs = new clockSync(validConfig); 132 | expect(cs.currentIndex).toEqual(0); 133 | expect(cs.historyDetails.currentServer).toMatchObject({ 134 | server: 'foo.bar.com', 135 | port: 123 136 | }); 137 | expect(cs.cycleServers).toBe(false); 138 | expect(cs.isOnline).toBe(true); 139 | expect(cs.tickId).not.toBeNull(); 140 | expect(cs.tickRate).toBe(300000); 141 | expect(cs.historyDetails.deltas).toHaveLength(1); // due to mock 142 | expect(cs.limit).toBe(10); 143 | expect(cs.getTime()).toBeGreaterThan(0); 144 | }); 145 | 146 | test('array of server & port objects', () => { 147 | const validConfig = { 148 | servers: [ 149 | {server: 'foo.bar.com', port: 123}, 150 | {server: 'bar.baz.gov', port: 567}, 151 | {port: 999, server: 'a.b.c'}, 152 | {port: 1337, server: 'acb.xyz.def.uvw'} 153 | ] 154 | }; 155 | const cs = new clockSync(validConfig); 156 | expect(cs.currentIndex).toEqual(0); 157 | expect(cs.historyDetails.currentServer).toMatchObject({ 158 | server: 'foo.bar.com', 159 | port: 123 160 | }); 161 | expect(cs.cycleServers).toBe(false); 162 | expect(cs.isOnline).toBe(true); 163 | expect(cs.tickId).not.toBeNull(); 164 | expect(cs.tickRate).toBe(300000); 165 | expect(cs.historyDetails.deltas).toHaveLength(1); // due to mock 166 | expect(cs.limit).toBe(10); 167 | expect(cs.getTime()).toBeGreaterThan(0); 168 | }); 169 | 170 | test('array of mixed-value server config data', () => { 171 | const validConfig = { 172 | servers: [ 173 | 'foo.bar.com', 174 | {server: 'bar.baz.gov', port: 567}, 175 | {server: 'a.b.c'}, 176 | {port: 1337, server: 'acb.xyz.def.uvw'} 177 | ] 178 | }; 179 | const cs = new clockSync(validConfig); 180 | expect(cs.currentIndex).toEqual(0); 181 | expect(cs.historyDetails.currentServer).toMatchObject({ 182 | server: 'foo.bar.com', 183 | port: 123 184 | }); 185 | expect(cs.cycleServers).toBe(false); 186 | expect(cs.isOnline).toBe(true); 187 | expect(cs.tickId).not.toBeNull(); 188 | expect(cs.tickRate).toBe(300000); 189 | expect(cs.historyDetails.deltas).toHaveLength(1); // due to mock 190 | expect(cs.limit).toBe(10); 191 | expect(cs.getTime()).toBeGreaterThan(0); 192 | }); 193 | 194 | }); 195 | 196 | describe('initialize clockSync instance with INVALID server configs', () => { 197 | 198 | test('undefined servers array', () => { 199 | const invalidConfig = { 200 | servers: undefined 201 | }; 202 | function make() { 203 | return new clockSync(invalidConfig) 204 | } 205 | expect(make).toThrow(); 206 | }); 207 | 208 | test('empty servers array', () => { 209 | const invalidConfig = { 210 | servers: [] 211 | }; 212 | function make() { 213 | return new clockSync(invalidConfig) 214 | } 215 | expect(make).toThrow(); 216 | }); 217 | 218 | test('bad string in servers array', () => { 219 | const invalidConfig = { 220 | servers: [ 221 | 'foo.bar.com', /* good */ 222 | ''/* bad */ 223 | ] 224 | }; 225 | function make() { 226 | return new clockSync(invalidConfig) 227 | } 228 | expect(make).toThrow(); 229 | }); 230 | 231 | test('bad empty object in servers array', () => { 232 | const invalidConfig = { 233 | servers: [ 234 | {server: 'foo.bar.com'}, /* good */ 235 | {}/* bad */ 236 | ] 237 | }; 238 | function make() { 239 | return new clockSync(invalidConfig) 240 | } 241 | expect(make).toThrow(); 242 | }); 243 | 244 | test('invalid object in servers array 1', () => { 245 | const invalidConfig = { 246 | servers: [ 247 | {server: 'foo.bar.com'}, /* good */ 248 | {wrongKey: 'a.b.c', port: 123}/* bad */ 249 | ] 250 | }; 251 | function make() { 252 | return new clockSync(invalidConfig) 253 | } 254 | expect(make).toThrow(); 255 | }); 256 | 257 | test('invalid object in servers array 2', () => { 258 | const invalidConfig = { 259 | servers: [ 260 | {server: 'foo.bar.com'}, /* good */ 261 | {server: 'a.b.c', port: '123'}/* bad */ 262 | ] 263 | }; 264 | function make() { 265 | return new clockSync(invalidConfig) 266 | } 267 | expect(make).toThrow(); 268 | }); 269 | 270 | test('invalid value in (mixed format) servers array', () => { 271 | const invalidConfig = { 272 | servers: [ 273 | 'foo.bar.com', /* good */ 274 | {server: 'a.b.c', port: 567}, /* good */ 275 | {server: 'x.y.z'}, /* good */ 276 | {} /* bad */ 277 | ] 278 | }; 279 | function make() { 280 | return new clockSync(invalidConfig) 281 | } 282 | expect(make).toThrow(); 283 | }); 284 | 285 | }) 286 | 287 | describe('initialize with misc. config values', () => { 288 | 289 | test('original API; syncDelay, history', () => { 290 | const config = { 291 | syncDelay: 60, 292 | history: 5 293 | }; 294 | const cs = new clockSync(config); 295 | expect(cs.tickRate).toBe(config.syncDelay * 1000); 296 | expect(cs.limit).toBe(config.history); 297 | expect(cs.historyDetails.deltas).toHaveLength(1); // default initial sync 298 | }); 299 | 300 | test('v1.1.0 API additions; cycleServers, startOnline, getIsOnline', () => { 301 | const config = { 302 | cycleServers: true, 303 | startOnline: false 304 | }; 305 | const cs = new clockSync(config); 306 | expect(cs.cycleServers).toBe(config.cycleServers); 307 | expect(cs.isOnline).toBe(config.startOnline); 308 | expect(cs.getIsOnline()).toBe(false); 309 | expect(cs.historyDetails.deltas).toHaveLength(0); // no initial sync 310 | }); 311 | 312 | }); 313 | 314 | }); 315 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var clockSync = function (config) { 2 | this.client = require('react-native-ntp-client'); 3 | if (!config) { 4 | config = {}; 5 | } 6 | 7 | // flexible parsing for server configs 8 | if (config.hasOwnProperty('servers')) { 9 | this.ntpServers = []; 10 | try { 11 | config.servers.forEach(function (s, i) { 12 | if (typeof s === 'string' && s.length > 0) { 13 | this.ntpServers.push({ 14 | server: s, 15 | port: this.client.defaultNtpPort 16 | }); 17 | } else if (typeof s === 'object') { 18 | if (s.server && typeof s.server === 'string') { 19 | if (s.port) { 20 | if (typeof s.port !== 'number' || s.port <= 0) { 21 | throw new Error('Invalid port number specified at index: ' + i); 22 | } 23 | } 24 | this.ntpServers.push({ 25 | server: s.server, 26 | port: s.port || this.client.defaultNtpPort 27 | }); 28 | } else { 29 | throw new Error('Missing server string at index: ' + i); 30 | } 31 | } else { 32 | throw new Error('Invalid config item at index: ' + i); 33 | } 34 | }, this); 35 | if (this.ntpServers.length === 0) { 36 | throw new Error('No servers provided in config object'); 37 | } 38 | } catch (e) { 39 | throw new Error('Malformed \'config.servers\' array: ' + e.message); 40 | } 41 | } else { 42 | this.ntpServers = [{ 43 | server: this.client.defaultNtpServer, 44 | port: this.client.defaultNtpPort 45 | }]; 46 | } 47 | 48 | this.currentIndex = 0; 49 | this.cycleServers = config.cycleServers || false; 50 | this.isOnline = (config.hasOwnProperty('startOnline') ? config.startOnline : true); 51 | this.limit = parseInt(config.history) || 10; 52 | if (this.limit <= 0) { throw new Error('\'config.history\' must be greater than 0'); } 53 | this.tickId = null; 54 | this.tickRate = parseFloat(config.syncDelay) || 300; 55 | if (this.tickRate <= 0) { throw new Error('\'config.syncDelay\' must be greater than 0'); } 56 | this.tickRate = this.tickRate * 1000; 57 | 58 | // runtime tracking 59 | this.historyDetails = { 60 | currentConsecutiveErrorCount: 0, 61 | currentServer: this.ntpServers[this.currentIndex], 62 | deltas: [], 63 | errors: [], 64 | isInErrorState: false, 65 | lastSyncTime: null, 66 | lastNtpTime: null, 67 | lastError: null, 68 | lifetimeErrorCount: 0, 69 | maxConsecutiveErrorCount: 0 70 | }; 71 | 72 | if (this.isOnline) { 73 | this.syncTime(); 74 | this.startTick(); 75 | } 76 | }; 77 | 78 | /** 79 | * @private 80 | */ 81 | clockSync.prototype.computeDeltaAndUpdateHistory = function (ntpDate) { 82 | var tempServerTime = ntpDate.getTime(); 83 | var tempLocalTime = Date.now(); 84 | var dt = tempServerTime - tempLocalTime; 85 | if (this.historyDetails.deltas.length === this.limit) { 86 | this.historyDetails.deltas.shift(); 87 | } 88 | this.historyDetails.deltas.push({ 89 | dt: dt, 90 | ntp: tempServerTime 91 | }); 92 | this.historyDetails.lastSyncTime = tempLocalTime; 93 | this.historyDetails.lastNtpTime = tempServerTime; 94 | return dt; 95 | }; 96 | 97 | /** 98 | * @private 99 | */ 100 | clockSync.prototype.getDelta = function (callback) { 101 | if (this.isOnline) { 102 | var fetchingServer = Object.assign({}, this.historyDetails.currentServer); 103 | this.client.getNetworkTime(this.historyDetails.currentServer.server, this.historyDetails.currentServer.port, function (err, date) { 104 | if (err) { 105 | this.shiftServer(); 106 | var ex = err; 107 | if (!ex) { 108 | ex = new Error('unknown error'); 109 | } else if (!(ex instanceof Error)) { 110 | if (typeof ex === 'string') { 111 | ex = new Error(ex); 112 | } else { 113 | ex = new Error(ex.toString()); 114 | } 115 | } 116 | if (callback) { 117 | callback(ex, fetchingServer); 118 | } 119 | } else { 120 | var delta = this.computeDeltaAndUpdateHistory(date); 121 | if (callback) { 122 | callback(delta, fetchingServer); 123 | } 124 | } 125 | }.bind(this)) 126 | } else { 127 | if (callback) { 128 | callback(0); 129 | } 130 | } 131 | }; 132 | 133 | clockSync.prototype.getHistory = function () { 134 | // fast way to deep clone since we know the stuff inside 135 | // is JSON serializable 136 | return JSON.parse(JSON.stringify(this.historyDetails)); 137 | }; 138 | 139 | clockSync.prototype.getIsOnline = function () { 140 | return this.isOnline; 141 | }; 142 | 143 | clockSync.prototype.getTime = function () { 144 | var sum = this.historyDetails.deltas.reduce(function (a, b) { 145 | return a + b.dt; 146 | }, 0); 147 | var avg = Math.round(sum / this.historyDetails.deltas.length) || 0; 148 | return (Date.now() + avg); 149 | }; 150 | 151 | clockSync.prototype.setOnline = function (online) { 152 | if (online && !this.isOnline) { 153 | this.isOnline = true; 154 | this.syncTime(); 155 | this.startTick(); 156 | } else if (!online && this.isOnline) { 157 | clearInterval(this.tickId); 158 | this.tickId = null; 159 | this.isOnline = false; 160 | } 161 | }; 162 | 163 | /** 164 | * @private 165 | */ 166 | clockSync.prototype.shiftServer = function () { 167 | if (this.cycleServers && this.ntpServers.length > 1) { 168 | this.currentIndex++; 169 | this.currentIndex %= this.ntpServers.length; 170 | } 171 | else if (this.ntpServers[this.currentIndex + 1]) { 172 | this.currentIndex++; 173 | } 174 | this.historyDetails.currentServer = this.ntpServers[this.currentIndex]; 175 | }; 176 | 177 | /** 178 | * @private 179 | */ 180 | clockSync.prototype.startTick = function () { 181 | if (!this.tickId) { 182 | this.tickId = setInterval(function () { 183 | this.syncTime(); 184 | }.bind(this), this.tickRate); 185 | } 186 | }; 187 | 188 | clockSync.prototype.syncTime = function (userCallback) { 189 | function internalCallback(result, server) { 190 | var success = false; 191 | if (this.isOnline) { 192 | if (typeof result === 'number') { 193 | success = true; 194 | this.historyDetails.currentConsecutiveErrorCount = 0; 195 | this.historyDetails.isInErrorState = false; 196 | } else if (result instanceof Error) { 197 | // extract Error data 198 | var ed = { 199 | name: result.name, 200 | message: result.message, 201 | server: server, 202 | stack: result.stack, 203 | time: Date.now() 204 | }; 205 | this.historyDetails.currentConsecutiveErrorCount++; 206 | if (this.historyDetails.errors.length === this.limit) { 207 | this.historyDetails.errors.shift(); 208 | } 209 | this.historyDetails.errors.push(ed); 210 | this.historyDetails.isInErrorState = true; 211 | this.historyDetails.lastError = ed; 212 | this.historyDetails.lifetimeErrorCount++; 213 | this.historyDetails.maxConsecutiveErrorCount = Math.max( 214 | this.historyDetails.maxConsecutiveErrorCount, 215 | this.historyDetails.currentConsecutiveErrorCount 216 | ); 217 | } 218 | } 219 | 220 | if (userCallback) { 221 | userCallback(success); 222 | } 223 | } 224 | 225 | this.getDelta(internalCallback.bind(this)); 226 | }; 227 | 228 | module.exports = clockSync; 229 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/var/folders/gg/qfp5y5t96v97vc044f3q171h0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // An array of directory names to be searched recursively up from the requiring module's location 61 | // moduleDirectories: [ 62 | // "node_modules" 63 | // ], 64 | 65 | // An array of file extensions your modules use 66 | // moduleFileExtensions: [ 67 | // "js", 68 | // "json", 69 | // "jsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names that allow to stub out resources with a single module 74 | // moduleNameMapper: {}, 75 | 76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 77 | // modulePathIgnorePatterns: [], 78 | 79 | // Activates notifications for test results 80 | // notify: false, 81 | 82 | // An enum that specifies notification mode. Requires { notify: true } 83 | // notifyMode: "always", 84 | 85 | // A preset that is used as a base for Jest's configuration 86 | // preset: null, 87 | 88 | // Run tests from one or more projects 89 | // projects: null, 90 | 91 | // Use this configuration option to add custom reporters to Jest 92 | // reporters: undefined, 93 | 94 | // Automatically reset mock state between every test 95 | // resetMocks: false, 96 | 97 | // Reset the module registry before running each individual test 98 | // resetModules: false, 99 | 100 | // A path to a custom resolver 101 | // resolver: null, 102 | 103 | // Automatically restore mock state between every test 104 | // restoreMocks: false, 105 | 106 | // The root directory that Jest should scan for tests and modules within 107 | // rootDir: null, 108 | 109 | // A list of paths to directories that Jest should use to search for files in 110 | // roots: [ 111 | // "" 112 | // ], 113 | 114 | // Allows you to use a custom runner instead of Jest's default test runner 115 | // runner: "jest-runner", 116 | 117 | // The paths to modules that run some code to configure or set up the testing environment before each test 118 | // setupFiles: [], 119 | 120 | // The path to a module that runs some code to configure or set up the testing framework before each test 121 | // setupTestFrameworkScriptFile: null, 122 | 123 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 124 | // snapshotSerializers: [], 125 | 126 | // The test environment that will be used for testing 127 | // testEnvironment: "jest-environment-jsdom", 128 | 129 | // Options that will be passed to the testEnvironment 130 | // testEnvironmentOptions: {}, 131 | 132 | // Adds a location field to test results 133 | // testLocationInResults: false, 134 | 135 | // The glob patterns Jest uses to detect test files 136 | // testMatch: [ 137 | // "**/__tests__/**/*.js?(x)", 138 | // "**/?(*.)+(spec|test).js?(x)" 139 | // ], 140 | 141 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 142 | // testPathIgnorePatterns: [ 143 | // "/node_modules/" 144 | // ], 145 | 146 | // The regexp pattern Jest uses to detect test files 147 | // testRegex: "", 148 | 149 | // This option allows the use of a custom results processor 150 | // testResultsProcessor: null, 151 | 152 | // This option allows use of a custom test runner 153 | // testRunner: "jasmine2", 154 | 155 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 156 | // testURL: "http://localhost", 157 | 158 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 159 | // timers: "real", 160 | 161 | // A map from regular expressions to paths to transformers 162 | // transform: null, 163 | 164 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 165 | // transformIgnorePatterns: [ 166 | // "/node_modules/" 167 | // ], 168 | 169 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 170 | // unmockedModulePathPatterns: undefined, 171 | 172 | // Indicates whether each individual test should be reported during the run 173 | // verbose: null, 174 | 175 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 176 | // watchPathIgnorePatterns: [], 177 | 178 | // Whether to use watchman for file crawling 179 | // watchman: true, 180 | }; 181 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-clock-sync", 3 | "version": "1.1.0-a", 4 | "description": "Sync clock across mobile devices using NTP", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "test": "jest --verbose --silent" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/artem-russkikh/react-native-clock-sync.git" 13 | }, 14 | "keywords": [ 15 | "react-native", 16 | "ntp", 17 | "clock", 18 | "sync" 19 | ], 20 | "author": "jordan l piepkow", 21 | "contributors": [ 22 | "Artem Russkikh (http://artem-russkikh.ru/)", 23 | "superguineapig (https://github.com/superguineapig)" 24 | ], 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/artem-russkikh/react-native-clock-sync/issues" 28 | }, 29 | "homepage": "https://github.com/artem-russkikh/react-native-clock-sync#readme", 30 | "dependencies": { 31 | "react-native-ntp-client": "0.5.5" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^5.9.0", 35 | "jest": "^23.6.0" 36 | } 37 | } 38 | --------------------------------------------------------------------------------