@@ -166,6 +166,102 @@ describe('webSocketReducer', () => {
166166 expect ( state . hrmData . find ( ( d ) => d . clientId === '2' ) ) . toBeUndefined ( )
167167 expect ( state . hrmData [ 0 ] . clientId ) . toBe ( '1' )
168168 } )
169+
170+ it ( 'should compensate for clock skew using serverTimestamp' , ( ) => {
171+ const serverNow = 1000000
172+ const clientNow = 1060000 // Client is 60s ahead
173+ const updatedAt = 990000 // Data is 10s old on server
174+
175+ const dateSpy = jest . spyOn ( Date , 'now' ) . mockReturnValue ( clientNow )
176+
177+ const action : ServerMessage = {
178+ type : 'HRM_UPDATE' ,
179+ payload : [ { ...baseUser , updatedAt } ] ,
180+ serverTimestamp : serverNow ,
181+ }
182+
183+ const state = reducer ( INITIAL_STATE , action )
184+
185+ // Expected updatedAt: 990000 + (1060000 - 1000000) = 990000 + 60000 = 1050000
186+ // 1050000 is 10s before clientNow, so it correctly preserves the 10s age.
187+ expect ( state . hrmData [ 0 ] . updatedAt ) . toBe ( 1050000 )
188+
189+ dateSpy . mockRestore ( )
190+ } )
191+
192+ it ( 'should compensate for clock skew for NEW users in HRM_UPDATE' , ( ) => {
193+ const serverNow = 1000000
194+ const clientNow = 1060000
195+ const updatedAt = 990000
196+
197+ const dateSpy = jest . spyOn ( Date , 'now' ) . mockReturnValue ( clientNow )
198+
199+ const action : ServerMessage = {
200+ type : 'HRM_UPDATE' ,
201+ payload : [ { ...baseUser , clientId : 'new-user' , updatedAt } ] ,
202+ serverTimestamp : serverNow ,
203+ }
204+
205+ const state = reducer ( INITIAL_STATE , action )
206+ expect ( state . hrmData [ 0 ] . clientId ) . toBe ( 'new-user' )
207+ expect ( state . hrmData [ 0 ] . updatedAt ) . toBe ( 1050000 )
208+
209+ dateSpy . mockRestore ( )
210+ } )
211+ } )
212+
213+ describe ( 'INITIAL_STATE action clock skew' , ( ) => {
214+ it ( 'should compensate for clock skew in INITIAL_STATE' , ( ) => {
215+ const serverNow = 1000000
216+ const clientNow = 1060000
217+ const updatedAt = 990000
218+
219+ const dateSpy = jest . spyOn ( Date , 'now' ) . mockReturnValue ( clientNow )
220+
221+ const action : ServerMessage = {
222+ type : 'INITIAL_STATE' ,
223+ payload : {
224+ hrmData : [ { ...baseUser , updatedAt } ] as HrmStreamData [ ] ,
225+ timerData : INITIAL_STATE . timerData ,
226+ spotifyData : INITIAL_STATE . spotifyData ,
227+ } ,
228+ serverTimestamp : serverNow ,
229+ }
230+
231+ const state = reducer ( INITIAL_STATE , action )
232+ expect ( state . hrmData [ 0 ] . updatedAt ) . toBe ( 1050000 )
233+
234+ dateSpy . mockRestore ( )
235+ } )
236+ } )
237+
238+ describe ( 'ACTIVE_ALERTS_UPDATE action clock skew' , ( ) => {
239+ it ( 'should compensate for clock skew in active alerts' , ( ) => {
240+ const serverNow = 1000000
241+ const clientNow = 1060000
242+ const alertTimestamp = 990000
243+
244+ const dateSpy = jest . spyOn ( Date , 'now' ) . mockReturnValue ( clientNow )
245+
246+ const action : ServerMessage = {
247+ type : 'ACTIVE_ALERTS_UPDATE' ,
248+ payload : [
249+ {
250+ clientId : '1' ,
251+ code : 'HRM_STALE' ,
252+ message : 'Stale' ,
253+ severity : 'warning' ,
254+ timestamp : alertTimestamp ,
255+ } ,
256+ ] ,
257+ serverTimestamp : serverNow ,
258+ }
259+
260+ const state = reducer ( INITIAL_STATE , action )
261+ expect ( state . activeAlerts [ 0 ] . timestamp ) . toBe ( 1050000 )
262+
263+ dateSpy . mockRestore ( )
264+ } )
169265 } )
170266
171267 describe ( 'DEVICE_OFFLINE action' , ( ) => {
0 commit comments