chronokinesis 7.0.1 → 8.0.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/README.md CHANGED
@@ -7,6 +7,7 @@ Mock time and date for traveling and freezing. Inspired and borrowed from [timek
7
7
  <!-- toc -->
8
8
 
9
9
  - [Introduction](#introduction)
10
+ - [Examples](#examples)
10
11
  - [API Reference](#api-reference)
11
12
  - [`freeze([...args])`](#freezeargs)
12
13
  - [`travel([...args])`](#travelargs)
@@ -15,21 +16,26 @@ Mock time and date for traveling and freezing. Inspired and borrowed from [timek
15
16
  - [`isKeepingTime()`](#iskeepingtime)
16
17
  - [`timezone(timeZone[, ...args])`](#timezonetimezone-args)
17
18
  - [`new TimeZoneTraveller(timeZone)`](#new-timezonetravellertimezone)
18
- - [`timezone.freeze([...args])`](#timezonefreezeargs)
19
- - [`timezone.travel([...args])`](#timezonetravelargs)
20
- - [`timezone.reset()`](#timezonereset)
21
- - [`timezone.defrost()`](#timezonedefrost)
19
+ - [`timezone.freeze([...args])`](#timezonefreezeargs)
20
+ - [`timezone.travel([...args])`](#timezonetravelargs)
21
+ - [`timezone.reset()`](#timezonereset)
22
+ - [`timezone.defrost()`](#timezonedefrost)
22
23
  - [Distributions](#distributions)
23
24
  - [Nodejs require](#nodejs-require)
24
25
  - [Browser (UMD)](#browser-umd)
26
+ - [High-resolution clocks](#high-resolution-clocks)
27
+ - [chronokinesis vs `node:test` mock timers](#chronokinesis-vs-nodetest-mock-timers)
28
+ - [Caveats when mocking high-resolution clocks](#caveats-when-mocking-high-resolution-clocks)
25
29
  - [Acknowledgements](#acknowledgements)
26
30
 
27
31
  <!-- tocstop -->
28
32
 
29
- # Introduction
33
+ ## Introduction
30
34
 
31
35
  Mock `Date` and `Date.now` in order to help you test time-dependent code. Provides `travel`, `freeze`, and timezone functionality for your Node.js tests.
32
36
 
37
+ ## Examples
38
+
33
39
  ```javascript
34
40
  import * as ck from 'chronokinesis';
35
41
 
@@ -87,9 +93,9 @@ setTimeout(() => {
87
93
  }, 2000);
88
94
  ```
89
95
 
90
- # API Reference
96
+ ## API Reference
91
97
 
92
- ## `freeze([...args])`
98
+ ### `freeze([...args])`
93
99
 
94
100
  Freeze point in time. Calls can be made with the same arguments as the `Date` constructor.
95
101
 
@@ -108,7 +114,7 @@ setTimeout(() => {
108
114
  }, 2000);
109
115
  ```
110
116
 
111
- ## `travel([...args])`
117
+ ### `travel([...args])`
112
118
 
113
119
  Time travel to another era. Calls can be made with the same arguments as the `Date` constructor
114
120
 
@@ -145,7 +151,7 @@ setTimeout(function () {
145
151
  }, 1500);
146
152
  ```
147
153
 
148
- ## `defrost()`
154
+ ### `defrost()`
149
155
 
150
156
  Defrost a frozen point in time. Used in combination with travelling will start ticking the clock.
151
157
 
@@ -168,7 +174,7 @@ setTimeout(() => {
168
174
  }, 2000);
169
175
  ```
170
176
 
171
- ## `reset()`
177
+ ### `reset()`
172
178
 
173
179
  Resets Date to current glory.
174
180
 
@@ -184,7 +190,7 @@ ck.reset();
184
190
  console.log(new Date());
185
191
  ```
186
192
 
187
- ## `isKeepingTime()`
193
+ ### `isKeepingTime()`
188
194
 
189
195
  Utility function to see if we still travel or freeze time.
190
196
 
@@ -198,7 +204,7 @@ console.log(ck.isKeepingTime() ? 'Is' : 'Not', 'keeping time');
198
204
  ck.reset();
199
205
  ```
200
206
 
201
- ## `timezone(timeZone[, ...args])`
207
+ ### `timezone(timeZone[, ...args])`
202
208
 
203
209
  Travel to time zone.
204
210
 
@@ -221,7 +227,7 @@ tz.freeze();
221
227
  ck.reset();
222
228
  ```
223
229
 
224
- ## `new TimeZoneTraveller(timeZone)`
230
+ ### `new TimeZoneTraveller(timeZone)`
225
231
 
226
232
  Time zone traveller api.
227
233
 
@@ -251,20 +257,63 @@ Same as [#reset](#reset)
251
257
 
252
258
  Same as [#defrost](#defrost)
253
259
 
254
- # Distributions
260
+ ## Distributions
255
261
 
256
262
  The module is prepared for browser and nodejs.
257
263
 
258
- ## Nodejs require
264
+ ### Nodejs require
259
265
 
260
266
  ```js
261
267
  const ck = require('chronokinesis');
262
268
  ```
263
269
 
264
- ## Browser (UMD)
270
+ ### Browser (UMD)
265
271
 
266
272
  Use `dist/chronokinesis.cjs`. Sets global property `chronokinesis`.
267
273
 
268
- # Acknowledgements
274
+ ## High-resolution clocks
275
+
276
+ `process.hrtime`, `process.hrtime.bigint()` and `performance.now()` are mocked alongside `Date` while timekeeping is active. The fake clocks stay on the native monotonic scale — each `freeze(T)` / `travel(T)` call shifts them by `(T − previousMockedMs)`, so readings before and after the mock are directly comparable. `freeze()` additionally locks the value so repeated reads return the same result. `reset()` clears the shift and restores the native functions by identity.
277
+
278
+ Because the fake and native axes are aligned, a pre-mock baseline can be diffed meaningfully against a reading taken after travelling forward:
279
+
280
+ ```javascript
281
+ import * as ck from 'chronokinesis';
282
+
283
+ const baseHr = process.hrtime.bigint();
284
+ const basePerf = performance.now();
285
+ const baseMs = Date.now();
286
+
287
+ ck.freeze(baseMs);
288
+ ck.travel(baseMs + 1000);
289
+
290
+ console.log(Number(process.hrtime.bigint() - baseHr) / 1e6); // ≈ 1000 (ms)
291
+ console.log(performance.now() - basePerf); // ≈ 1000 (ms)
292
+
293
+ ck.reset();
294
+ ```
295
+
296
+ ## chronokinesis vs `node:test` mock timers
297
+
298
+ chronokinesis mocks **clocks only** — `Date`, `process.hrtime`, `performance.now`. Timers (`setTimeout`, `setInterval`, `setImmediate`) still run on the real wall clock; a `setTimeout(fn, 10)` under `freeze()` still fires ~10ms later in real time.
299
+
300
+ Node's built-in `node:test` provides `mock.timers` which is different: it mocks both the clock **and** the timer queue, and `mock.timers.tick(ms)` synchronously advances time and fires any timers scheduled within that window — no real waiting required.
301
+
302
+ Rule of thumb:
303
+
304
+ - Use **chronokinesis** to pin the wall-clock/timezone to a specific moment while letting real timers run — e.g. you want `new Date()` to return 1980-01-01 but your `setTimeout(..., 10)` should still take 10ms.
305
+ - Use **`node:test` `mock.timers`** to fast-forward deterministically through timer-based logic without waiting real time.
306
+ - Don't enable both simultaneously; they both own `Date`.
307
+
308
+ ## Caveats when mocking high-resolution clocks
309
+
310
+ Swapping `process.hrtime` and `performance.now` is generally safe — `fetch`, `AbortSignal.timeout`, `setTimeout`, `setInterval`, and other libuv-backed APIs are unaffected (they use `uv_hrtime`, which chronokinesis does not touch). A few things are worth knowing:
311
+
312
+ - **`performance.mark` + `performance.measure` across a mock boundary are meaningless.** A mark captured before `freeze()`/`travel()` and a measure taken after produces a fabricated duration — the hrtime axis shifted by the mock delta in between.
313
+ - **Long-lived resources with hrtime-based idle or keep-alive timers** (database pools, HTTP keep-alive, gRPC deadlines) may misbehave if they were acquired before the mock. Prefer to mock before creating such resources and reset after tearing them down.
314
+ - **Tracing / metrics / structured logging** (pino, OpenTelemetry, APM agents) will emit bogus timestamps under mock. Fine for unit tests; don't assert on telemetry timing in mocked phases.
315
+ - **Monotonicity holds within a single `freeze()` or `travel()` phase**, but not across them — `reset()` snaps back to native, and `travel(past)` can move values backwards relative to earlier readings.
316
+
317
+ ## Acknowledgements
269
318
 
270
319
  chronokinesis initial code is inspired and borrowed from [timekeeper](https://github.com/vesln/timekeeper)
@@ -15,10 +15,28 @@
15
15
  const NativeDate = Date[kNativeDate] || Date;
16
16
  const nativeGetTimezoneOffset = NativeDate.prototype.getTimezoneOffset;
17
17
 
18
+ /* c8 ignore start -- environment-capability guards; false branches only hit in browser (isomorphic test) */
19
+ const nativeProcess = typeof process !== 'undefined' ? process : null;
20
+ const nativeHrtime = nativeProcess && typeof nativeProcess.hrtime === 'function' ? nativeProcess.hrtime : null;
21
+ const nativeHrtimeBigint = nativeHrtime && typeof nativeHrtime.bigint === 'function' ? nativeHrtime.bigint : null;
22
+ const nativePerformance = typeof performance !== 'undefined' ? performance : null;
23
+ const nativePerformanceNow = nativePerformance && typeof nativePerformance.now === 'function' ? nativePerformance.now : null;
24
+
25
+ // Anchor native perf.now against native hrtime at module load so that later reads
26
+ // can reconstruct real native perf.now even when process.hrtime has been swapped
27
+ // (Node <22 wires performance.now to process.hrtime internally).
28
+ const perfAnchorMs = nativePerformanceNow ? nativePerformanceNow.call(nativePerformance) : 0;
29
+ const hrtimeAnchorNs = nativeHrtimeBigint ? nativeHrtimeBigint.call(nativeProcess) : 0n;
30
+ /* c8 ignore stop */
31
+
18
32
  let freezedAt = null;
19
33
  let traveledTo = null;
20
34
  let started = null;
21
35
  let iana = null;
36
+ let hrtimeOffset = 0n;
37
+ let performanceNowOffset = 0;
38
+ let freezedHrtimeNs = null;
39
+ let freezedPerformanceNow = null;
22
40
 
23
41
  function FakeDate(...args) {
24
42
  const length = args.length;
@@ -57,16 +75,21 @@
57
75
 
58
76
  function freeze(...args) {
59
77
  useFakeDate();
60
- freezedAt = instantiate(Date, args);
61
- if (isNaN(freezedAt)) {
78
+ const target = instantiate(Date, args);
79
+ if (isNaN(target)) {
62
80
  reset();
63
81
  throw new TypeError('Chronokinesis cannot freeze to invalid date, check your arguments. Chronokinesis is reset');
64
82
  }
83
+ const prevMockedMs = currentMockedMs();
84
+ freezedAt = target;
85
+ shiftAndLockFakeClocks(target.getTime() - prevMockedMs);
65
86
  return freezedAt;
66
87
  }
67
88
 
68
89
  function defrost() {
69
90
  freezedAt = null;
91
+ freezedHrtimeNs = null;
92
+ freezedPerformanceNow = null;
70
93
  }
71
94
 
72
95
  function travel(...args) {
@@ -77,13 +100,21 @@
77
100
  throw new TypeError('Chronokinesis cannot travel to invalid date, check your arguments. Chronokinesis is reset');
78
101
  }
79
102
 
80
- traveledTo = travelToDate.getTime();
81
- started = NativeDate.now();
103
+ const prevMockedMs = currentMockedMs();
104
+ const targetMs = travelToDate.getTime();
105
+ const deltaMs = targetMs - prevMockedMs;
106
+ if (nativeHrtimeBigint) hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
107
+ if (nativePerformanceNow) performanceNowOffset += deltaMs;
82
108
 
83
109
  if (freezedAt) {
84
110
  freezedAt = travelToDate;
111
+ if (nativeHrtimeBigint) freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
112
+ if (nativePerformanceNow) freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
85
113
  }
86
114
 
115
+ traveledTo = targetMs;
116
+ started = NativeDate.now();
117
+
87
118
  return travelToDate;
88
119
  }
89
120
 
@@ -93,6 +124,10 @@
93
124
  started = null;
94
125
  traveledTo = null;
95
126
  iana = null;
127
+ hrtimeOffset = 0n;
128
+ performanceNowOffset = 0;
129
+ freezedHrtimeNs = null;
130
+ freezedPerformanceNow = null;
96
131
  }
97
132
 
98
133
  function isKeepingTime() {
@@ -151,16 +186,78 @@
151
186
 
152
187
  function useFakeDate() {
153
188
  Date = FakeDate;
189
+ if (nativeHrtime) nativeProcess.hrtime = fakeHrtime;
190
+ if (nativePerformanceNow) {
191
+ Object.defineProperty(nativePerformance, 'now', { value: fakePerformanceNow, configurable: true, writable: true });
192
+ }
154
193
  }
155
194
 
156
195
  function useNativeDate() {
157
196
  Date = FakeDate[kNativeDate];
197
+ if (nativeHrtime) nativeProcess.hrtime = nativeHrtime;
198
+ if (nativePerformanceNow) {
199
+ Object.defineProperty(nativePerformance, 'now', { value: nativePerformanceNow, configurable: true, writable: true });
200
+ }
158
201
  }
159
202
 
160
203
  function time() {
161
204
  return traveledTo + (NativeDate.now() - started);
162
205
  }
163
206
 
207
+ function currentMockedMs() {
208
+ if (freezedAt) return freezedAt.getTime();
209
+ if (traveledTo !== null) return time();
210
+ return NativeDate.now();
211
+ }
212
+
213
+ function nativePerformanceNowFromHrtime() {
214
+ if (nativeHrtimeBigint) {
215
+ const elapsedNs = nativeHrtimeBigint.call(nativeProcess) - hrtimeAnchorNs;
216
+ return perfAnchorMs + Number(elapsedNs) / 1_000_000;
217
+ }
218
+ /* c8 ignore next -- browser fallback; covered by isomorphic test, not by Node c8 */
219
+ return nativePerformanceNow.call(nativePerformance);
220
+ }
221
+
222
+ function shiftAndLockFakeClocks(deltaMs) {
223
+ if (nativeHrtimeBigint) {
224
+ hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
225
+ freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
226
+ }
227
+ if (nativePerformanceNow) {
228
+ performanceNowOffset += deltaMs;
229
+ freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
230
+ }
231
+ }
232
+
233
+ function fakeHrtimeNs() {
234
+ if (freezedHrtimeNs !== null) return freezedHrtimeNs;
235
+ return nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
236
+ }
237
+
238
+ function fakeHrtime(prev) {
239
+ const total = fakeHrtimeNs();
240
+ const sec = Number(total / 1_000_000_000n);
241
+ const ns = Number(total % 1_000_000_000n);
242
+ if (!prev) return [sec, ns];
243
+ let ds = sec - prev[0];
244
+ let dn = ns - prev[1];
245
+ if (dn < 0) {
246
+ ds -= 1;
247
+ dn += 1_000_000_000;
248
+ }
249
+ return [ds, dn];
250
+ }
251
+
252
+ fakeHrtime.bigint = function fakeHrtimeBigint() {
253
+ return fakeHrtimeNs();
254
+ };
255
+
256
+ function fakePerformanceNow() {
257
+ if (freezedPerformanceNow !== null) return freezedPerformanceNow;
258
+ return nativePerformanceNowFromHrtime() + performanceNowOffset;
259
+ }
260
+
164
261
  function instantiate(type, args) {
165
262
  const ctorArgs = args.slice();
166
263
  ctorArgs.unshift(null);
package/dist/index.cjs CHANGED
@@ -11,10 +11,28 @@ const kNativeDate = Symbol.for('chronokinesis native date');
11
11
  const NativeDate = Date[kNativeDate] || Date;
12
12
  const nativeGetTimezoneOffset = NativeDate.prototype.getTimezoneOffset;
13
13
 
14
+ /* c8 ignore start -- environment-capability guards; false branches only hit in browser (isomorphic test) */
15
+ const nativeProcess = typeof process !== 'undefined' ? process : null;
16
+ const nativeHrtime = nativeProcess && typeof nativeProcess.hrtime === 'function' ? nativeProcess.hrtime : null;
17
+ const nativeHrtimeBigint = nativeHrtime && typeof nativeHrtime.bigint === 'function' ? nativeHrtime.bigint : null;
18
+ const nativePerformance = typeof performance !== 'undefined' ? performance : null;
19
+ const nativePerformanceNow = nativePerformance && typeof nativePerformance.now === 'function' ? nativePerformance.now : null;
20
+
21
+ // Anchor native perf.now against native hrtime at module load so that later reads
22
+ // can reconstruct real native perf.now even when process.hrtime has been swapped
23
+ // (Node <22 wires performance.now to process.hrtime internally).
24
+ const perfAnchorMs = nativePerformanceNow ? nativePerformanceNow.call(nativePerformance) : 0;
25
+ const hrtimeAnchorNs = nativeHrtimeBigint ? nativeHrtimeBigint.call(nativeProcess) : 0n;
26
+ /* c8 ignore stop */
27
+
14
28
  let freezedAt = null;
15
29
  let traveledTo = null;
16
30
  let started = null;
17
31
  let iana = null;
32
+ let hrtimeOffset = 0n;
33
+ let performanceNowOffset = 0;
34
+ let freezedHrtimeNs = null;
35
+ let freezedPerformanceNow = null;
18
36
 
19
37
  function FakeDate(...args) {
20
38
  const length = args.length;
@@ -53,16 +71,21 @@ FakeDate.now = function fakeNow() {
53
71
 
54
72
  function freeze(...args) {
55
73
  useFakeDate();
56
- freezedAt = instantiate(Date, args);
57
- if (isNaN(freezedAt)) {
74
+ const target = instantiate(Date, args);
75
+ if (isNaN(target)) {
58
76
  reset();
59
77
  throw new TypeError('Chronokinesis cannot freeze to invalid date, check your arguments. Chronokinesis is reset');
60
78
  }
79
+ const prevMockedMs = currentMockedMs();
80
+ freezedAt = target;
81
+ shiftAndLockFakeClocks(target.getTime() - prevMockedMs);
61
82
  return freezedAt;
62
83
  }
63
84
 
64
85
  function defrost() {
65
86
  freezedAt = null;
87
+ freezedHrtimeNs = null;
88
+ freezedPerformanceNow = null;
66
89
  }
67
90
 
68
91
  function travel(...args) {
@@ -73,13 +96,21 @@ function travel(...args) {
73
96
  throw new TypeError('Chronokinesis cannot travel to invalid date, check your arguments. Chronokinesis is reset');
74
97
  }
75
98
 
76
- traveledTo = travelToDate.getTime();
77
- started = NativeDate.now();
99
+ const prevMockedMs = currentMockedMs();
100
+ const targetMs = travelToDate.getTime();
101
+ const deltaMs = targetMs - prevMockedMs;
102
+ if (nativeHrtimeBigint) hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
103
+ if (nativePerformanceNow) performanceNowOffset += deltaMs;
78
104
 
79
105
  if (freezedAt) {
80
106
  freezedAt = travelToDate;
107
+ if (nativeHrtimeBigint) freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
108
+ if (nativePerformanceNow) freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
81
109
  }
82
110
 
111
+ traveledTo = targetMs;
112
+ started = NativeDate.now();
113
+
83
114
  return travelToDate;
84
115
  }
85
116
 
@@ -89,6 +120,10 @@ function reset() {
89
120
  started = null;
90
121
  traveledTo = null;
91
122
  iana = null;
123
+ hrtimeOffset = 0n;
124
+ performanceNowOffset = 0;
125
+ freezedHrtimeNs = null;
126
+ freezedPerformanceNow = null;
92
127
  }
93
128
 
94
129
  function isKeepingTime() {
@@ -147,16 +182,78 @@ function timezone(timeZone, ...args) {
147
182
 
148
183
  function useFakeDate() {
149
184
  Date = FakeDate;
185
+ if (nativeHrtime) nativeProcess.hrtime = fakeHrtime;
186
+ if (nativePerformanceNow) {
187
+ Object.defineProperty(nativePerformance, 'now', { value: fakePerformanceNow, configurable: true, writable: true });
188
+ }
150
189
  }
151
190
 
152
191
  function useNativeDate() {
153
192
  Date = FakeDate[kNativeDate];
193
+ if (nativeHrtime) nativeProcess.hrtime = nativeHrtime;
194
+ if (nativePerformanceNow) {
195
+ Object.defineProperty(nativePerformance, 'now', { value: nativePerformanceNow, configurable: true, writable: true });
196
+ }
154
197
  }
155
198
 
156
199
  function time() {
157
200
  return traveledTo + (NativeDate.now() - started);
158
201
  }
159
202
 
203
+ function currentMockedMs() {
204
+ if (freezedAt) return freezedAt.getTime();
205
+ if (traveledTo !== null) return time();
206
+ return NativeDate.now();
207
+ }
208
+
209
+ function nativePerformanceNowFromHrtime() {
210
+ if (nativeHrtimeBigint) {
211
+ const elapsedNs = nativeHrtimeBigint.call(nativeProcess) - hrtimeAnchorNs;
212
+ return perfAnchorMs + Number(elapsedNs) / 1_000_000;
213
+ }
214
+ /* c8 ignore next -- browser fallback; covered by isomorphic test, not by Node c8 */
215
+ return nativePerformanceNow.call(nativePerformance);
216
+ }
217
+
218
+ function shiftAndLockFakeClocks(deltaMs) {
219
+ if (nativeHrtimeBigint) {
220
+ hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
221
+ freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
222
+ }
223
+ if (nativePerformanceNow) {
224
+ performanceNowOffset += deltaMs;
225
+ freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
226
+ }
227
+ }
228
+
229
+ function fakeHrtimeNs() {
230
+ if (freezedHrtimeNs !== null) return freezedHrtimeNs;
231
+ return nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
232
+ }
233
+
234
+ function fakeHrtime(prev) {
235
+ const total = fakeHrtimeNs();
236
+ const sec = Number(total / 1_000_000_000n);
237
+ const ns = Number(total % 1_000_000_000n);
238
+ if (!prev) return [sec, ns];
239
+ let ds = sec - prev[0];
240
+ let dn = ns - prev[1];
241
+ if (dn < 0) {
242
+ ds -= 1;
243
+ dn += 1_000_000_000;
244
+ }
245
+ return [ds, dn];
246
+ }
247
+
248
+ fakeHrtime.bigint = function fakeHrtimeBigint() {
249
+ return fakeHrtimeNs();
250
+ };
251
+
252
+ function fakePerformanceNow() {
253
+ if (freezedPerformanceNow !== null) return freezedPerformanceNow;
254
+ return nativePerformanceNowFromHrtime() + performanceNowOffset;
255
+ }
256
+
160
257
  function instantiate(type, args) {
161
258
  const ctorArgs = args.slice();
162
259
  ctorArgs.unshift(null);
package/index.js CHANGED
@@ -9,10 +9,28 @@ const kNativeDate = Symbol.for('chronokinesis native date');
9
9
  const NativeDate = Date[kNativeDate] || Date;
10
10
  const nativeGetTimezoneOffset = NativeDate.prototype.getTimezoneOffset;
11
11
 
12
+ /* c8 ignore start -- environment-capability guards; false branches only hit in browser (isomorphic test) */
13
+ const nativeProcess = typeof process !== 'undefined' ? process : null;
14
+ const nativeHrtime = nativeProcess && typeof nativeProcess.hrtime === 'function' ? nativeProcess.hrtime : null;
15
+ const nativeHrtimeBigint = nativeHrtime && typeof nativeHrtime.bigint === 'function' ? nativeHrtime.bigint : null;
16
+ const nativePerformance = typeof performance !== 'undefined' ? performance : null;
17
+ const nativePerformanceNow = nativePerformance && typeof nativePerformance.now === 'function' ? nativePerformance.now : null;
18
+
19
+ // Anchor native perf.now against native hrtime at module load so that later reads
20
+ // can reconstruct real native perf.now even when process.hrtime has been swapped
21
+ // (Node <22 wires performance.now to process.hrtime internally).
22
+ const perfAnchorMs = nativePerformanceNow ? nativePerformanceNow.call(nativePerformance) : 0;
23
+ const hrtimeAnchorNs = nativeHrtimeBigint ? nativeHrtimeBigint.call(nativeProcess) : 0n;
24
+ /* c8 ignore stop */
25
+
12
26
  let freezedAt = null;
13
27
  let traveledTo = null;
14
28
  let started = null;
15
29
  let iana = null;
30
+ let hrtimeOffset = 0n;
31
+ let performanceNowOffset = 0;
32
+ let freezedHrtimeNs = null;
33
+ let freezedPerformanceNow = null;
16
34
 
17
35
  export function FakeDate(...args) {
18
36
  const length = args.length;
@@ -51,16 +69,21 @@ FakeDate.now = function fakeNow() {
51
69
 
52
70
  export function freeze(...args) {
53
71
  useFakeDate();
54
- freezedAt = instantiate(Date, args);
55
- if (isNaN(freezedAt)) {
72
+ const target = instantiate(Date, args);
73
+ if (isNaN(target)) {
56
74
  reset();
57
75
  throw new TypeError('Chronokinesis cannot freeze to invalid date, check your arguments. Chronokinesis is reset');
58
76
  }
77
+ const prevMockedMs = currentMockedMs();
78
+ freezedAt = target;
79
+ shiftAndLockFakeClocks(target.getTime() - prevMockedMs);
59
80
  return freezedAt;
60
81
  }
61
82
 
62
83
  export function defrost() {
63
84
  freezedAt = null;
85
+ freezedHrtimeNs = null;
86
+ freezedPerformanceNow = null;
64
87
  }
65
88
 
66
89
  export function travel(...args) {
@@ -71,13 +94,21 @@ export function travel(...args) {
71
94
  throw new TypeError('Chronokinesis cannot travel to invalid date, check your arguments. Chronokinesis is reset');
72
95
  }
73
96
 
74
- traveledTo = travelToDate.getTime();
75
- started = NativeDate.now();
97
+ const prevMockedMs = currentMockedMs();
98
+ const targetMs = travelToDate.getTime();
99
+ const deltaMs = targetMs - prevMockedMs;
100
+ if (nativeHrtimeBigint) hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
101
+ if (nativePerformanceNow) performanceNowOffset += deltaMs;
76
102
 
77
103
  if (freezedAt) {
78
104
  freezedAt = travelToDate;
105
+ if (nativeHrtimeBigint) freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
106
+ if (nativePerformanceNow) freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
79
107
  }
80
108
 
109
+ traveledTo = targetMs;
110
+ started = NativeDate.now();
111
+
81
112
  return travelToDate;
82
113
  }
83
114
 
@@ -87,6 +118,10 @@ export function reset() {
87
118
  started = null;
88
119
  traveledTo = null;
89
120
  iana = null;
121
+ hrtimeOffset = 0n;
122
+ performanceNowOffset = 0;
123
+ freezedHrtimeNs = null;
124
+ freezedPerformanceNow = null;
90
125
  }
91
126
 
92
127
  export function isKeepingTime() {
@@ -145,16 +180,78 @@ export function timezone(timeZone, ...args) {
145
180
 
146
181
  function useFakeDate() {
147
182
  Date = FakeDate;
183
+ if (nativeHrtime) nativeProcess.hrtime = fakeHrtime;
184
+ if (nativePerformanceNow) {
185
+ Object.defineProperty(nativePerformance, 'now', { value: fakePerformanceNow, configurable: true, writable: true });
186
+ }
148
187
  }
149
188
 
150
189
  function useNativeDate() {
151
190
  Date = FakeDate[kNativeDate];
191
+ if (nativeHrtime) nativeProcess.hrtime = nativeHrtime;
192
+ if (nativePerformanceNow) {
193
+ Object.defineProperty(nativePerformance, 'now', { value: nativePerformanceNow, configurable: true, writable: true });
194
+ }
152
195
  }
153
196
 
154
197
  function time() {
155
198
  return traveledTo + (NativeDate.now() - started);
156
199
  }
157
200
 
201
+ function currentMockedMs() {
202
+ if (freezedAt) return freezedAt.getTime();
203
+ if (traveledTo !== null) return time();
204
+ return NativeDate.now();
205
+ }
206
+
207
+ function nativePerformanceNowFromHrtime() {
208
+ if (nativeHrtimeBigint) {
209
+ const elapsedNs = nativeHrtimeBigint.call(nativeProcess) - hrtimeAnchorNs;
210
+ return perfAnchorMs + Number(elapsedNs) / 1_000_000;
211
+ }
212
+ /* c8 ignore next -- browser fallback; covered by isomorphic test, not by Node c8 */
213
+ return nativePerformanceNow.call(nativePerformance);
214
+ }
215
+
216
+ function shiftAndLockFakeClocks(deltaMs) {
217
+ if (nativeHrtimeBigint) {
218
+ hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
219
+ freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
220
+ }
221
+ if (nativePerformanceNow) {
222
+ performanceNowOffset += deltaMs;
223
+ freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
224
+ }
225
+ }
226
+
227
+ function fakeHrtimeNs() {
228
+ if (freezedHrtimeNs !== null) return freezedHrtimeNs;
229
+ return nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
230
+ }
231
+
232
+ function fakeHrtime(prev) {
233
+ const total = fakeHrtimeNs();
234
+ const sec = Number(total / 1_000_000_000n);
235
+ const ns = Number(total % 1_000_000_000n);
236
+ if (!prev) return [sec, ns];
237
+ let ds = sec - prev[0];
238
+ let dn = ns - prev[1];
239
+ if (dn < 0) {
240
+ ds -= 1;
241
+ dn += 1_000_000_000;
242
+ }
243
+ return [ds, dn];
244
+ }
245
+
246
+ fakeHrtime.bigint = function fakeHrtimeBigint() {
247
+ return fakeHrtimeNs();
248
+ };
249
+
250
+ function fakePerformanceNow() {
251
+ if (freezedPerformanceNow !== null) return freezedPerformanceNow;
252
+ return nativePerformanceNowFromHrtime() + performanceNowOffset;
253
+ }
254
+
158
255
  function instantiate(type, args) {
159
256
  const ctorArgs = args.slice();
160
257
  ctorArgs.unshift(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chronokinesis",
3
- "version": "7.0.1",
3
+ "version": "8.0.0",
4
4
  "description": "Module for testing time-dependent code",
5
5
  "author": {
6
6
  "name": "Pål Edman",
@@ -44,6 +44,8 @@
44
44
  "keywords": [
45
45
  "date",
46
46
  "time",
47
+ "hrtime",
48
+ "performance",
47
49
  "timezone",
48
50
  "fake",
49
51
  "mock",
@@ -58,6 +60,7 @@
58
60
  "c8": "^10.1.2",
59
61
  "chai": "^6.2.0",
60
62
  "eslint": "^9.3.0",
63
+ "happy-dom": "^17.6.1",
61
64
  "lodash.clonedeep": "^4.5.0",
62
65
  "luxon": "^3.7.1",
63
66
  "markdown-toc": "^1.2.0",
@@ -65,6 +68,6 @@
65
68
  "moment": "^2.30.1",
66
69
  "prettier": "^3.2.5",
67
70
  "rollup": "^4.12.1",
68
- "texample": "^0.0.8"
71
+ "texample": "^0.1.0"
69
72
  }
70
73
  }