kinetex 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +413 -52
  2. package/dist/browser/kinetex.esm.js +38 -22
  3. package/dist/browser/kinetex.js +3134 -787
  4. package/dist/browser/kinetex.min.js +38 -22
  5. package/dist/cjs/aws-sigv4.js +140 -33
  6. package/dist/cjs/cache.js +58 -7
  7. package/dist/cjs/circuit-breaker.js +53 -4
  8. package/dist/cjs/client.js +726 -188
  9. package/dist/cjs/cookie-parser.js +131 -6
  10. package/dist/cjs/cookie-store.js +125 -28
  11. package/dist/cjs/core.js +691 -70
  12. package/dist/cjs/dedup.js +142 -21
  13. package/dist/cjs/digest.js +195 -38
  14. package/dist/cjs/graphql.js +166 -35
  15. package/dist/cjs/headers.js +327 -46
  16. package/dist/cjs/interceptors.js +227 -24
  17. package/dist/cjs/lifecycle.js +92 -59
  18. package/dist/cjs/logging.js +160 -24
  19. package/dist/cjs/mod.js +3 -2
  20. package/dist/cjs/pagination.js +247 -22
  21. package/dist/cjs/progress.js +178 -30
  22. package/dist/cjs/proxy.js +429 -0
  23. package/dist/cjs/response.js +506 -110
  24. package/dist/cjs/socks5.js +136 -15
  25. package/dist/cjs/sse.js +173 -43
  26. package/dist/cjs/url.js +191 -45
  27. package/dist/cjs/utils.js +335 -73
  28. package/dist/cjs/ws.js +62 -12
  29. package/dist/esm/aws-sigv4.js +140 -33
  30. package/dist/esm/aws-sigv4.js.map +1 -1
  31. package/dist/esm/cache.js +58 -7
  32. package/dist/esm/cache.js.map +1 -1
  33. package/dist/esm/circuit-breaker.js +53 -4
  34. package/dist/esm/circuit-breaker.js.map +1 -1
  35. package/dist/esm/client.js +726 -188
  36. package/dist/esm/client.js.map +1 -1
  37. package/dist/esm/cookie-parser.js +131 -6
  38. package/dist/esm/cookie-parser.js.map +1 -1
  39. package/dist/esm/cookie-store.js +125 -28
  40. package/dist/esm/cookie-store.js.map +1 -1
  41. package/dist/esm/core.js +691 -70
  42. package/dist/esm/core.js.map +1 -1
  43. package/dist/esm/dedup.js +142 -21
  44. package/dist/esm/dedup.js.map +1 -1
  45. package/dist/esm/digest.js +195 -38
  46. package/dist/esm/digest.js.map +1 -1
  47. package/dist/esm/graphql.js +166 -35
  48. package/dist/esm/graphql.js.map +1 -1
  49. package/dist/esm/headers.js +327 -46
  50. package/dist/esm/headers.js.map +1 -1
  51. package/dist/esm/interceptors.js +227 -24
  52. package/dist/esm/interceptors.js.map +1 -1
  53. package/dist/esm/lifecycle.js +92 -59
  54. package/dist/esm/lifecycle.js.map +1 -1
  55. package/dist/esm/logging.js +160 -24
  56. package/dist/esm/logging.js.map +1 -1
  57. package/dist/esm/mod.js +3 -2
  58. package/dist/esm/mod.js.map +1 -1
  59. package/dist/esm/pagination.js +247 -22
  60. package/dist/esm/pagination.js.map +1 -1
  61. package/dist/esm/progress.js +178 -30
  62. package/dist/esm/progress.js.map +1 -1
  63. package/dist/esm/proxy.js +430 -0
  64. package/dist/esm/proxy.js.map +1 -0
  65. package/dist/esm/response.js +506 -110
  66. package/dist/esm/response.js.map +1 -1
  67. package/dist/esm/socks5.js +136 -15
  68. package/dist/esm/socks5.js.map +1 -1
  69. package/dist/esm/sse.js +173 -43
  70. package/dist/esm/sse.js.map +1 -1
  71. package/dist/esm/types.js.map +1 -1
  72. package/dist/esm/url.js +191 -45
  73. package/dist/esm/url.js.map +1 -1
  74. package/dist/esm/utils.js +335 -73
  75. package/dist/esm/utils.js.map +1 -1
  76. package/dist/esm/ws.js +62 -12
  77. package/dist/esm/ws.js.map +1 -1
  78. package/dist/types/aws-sigv4.d.ts.map +1 -1
  79. package/dist/types/cache.d.ts +19 -1
  80. package/dist/types/cache.d.ts.map +1 -1
  81. package/dist/types/circuit-breaker.d.ts +15 -2
  82. package/dist/types/circuit-breaker.d.ts.map +1 -1
  83. package/dist/types/client.d.ts +69 -11
  84. package/dist/types/client.d.ts.map +1 -1
  85. package/dist/types/cookie-parser.d.ts +0 -17
  86. package/dist/types/cookie-parser.d.ts.map +1 -1
  87. package/dist/types/cookie-store.d.ts.map +1 -1
  88. package/dist/types/core.d.ts +216 -25
  89. package/dist/types/core.d.ts.map +1 -1
  90. package/dist/types/dedup.d.ts +38 -3
  91. package/dist/types/dedup.d.ts.map +1 -1
  92. package/dist/types/digest.d.ts +17 -37
  93. package/dist/types/digest.d.ts.map +1 -1
  94. package/dist/types/graphql.d.ts.map +1 -1
  95. package/dist/types/headers.d.ts +68 -31
  96. package/dist/types/headers.d.ts.map +1 -1
  97. package/dist/types/interceptors.d.ts +126 -38
  98. package/dist/types/interceptors.d.ts.map +1 -1
  99. package/dist/types/lifecycle.d.ts +19 -2
  100. package/dist/types/lifecycle.d.ts.map +1 -1
  101. package/dist/types/logging.d.ts +22 -3
  102. package/dist/types/logging.d.ts.map +1 -1
  103. package/dist/types/mod.d.ts +5 -3
  104. package/dist/types/mod.d.ts.map +1 -1
  105. package/dist/types/pagination.d.ts +0 -25
  106. package/dist/types/pagination.d.ts.map +1 -1
  107. package/dist/types/progress.d.ts +1 -1
  108. package/dist/types/progress.d.ts.map +1 -1
  109. package/dist/types/proxy.d.ts +50 -0
  110. package/dist/types/proxy.d.ts.map +1 -0
  111. package/dist/types/response.d.ts +49 -2
  112. package/dist/types/response.d.ts.map +1 -1
  113. package/dist/types/socks5.d.ts.map +1 -1
  114. package/dist/types/sse.d.ts.map +1 -1
  115. package/dist/types/types.d.ts +219 -26
  116. package/dist/types/types.d.ts.map +1 -1
  117. package/dist/types/url.d.ts +0 -25
  118. package/dist/types/url.d.ts.map +1 -1
  119. package/dist/types/utils.d.ts +69 -1
  120. package/dist/types/utils.d.ts.map +1 -1
  121. package/dist/types/ws.d.ts +0 -1
  122. package/dist/types/ws.d.ts.map +1 -1
  123. package/package.json +10 -4
@@ -20,6 +20,7 @@
20
20
  * - Async iterator interface
21
21
  * - Readable progress formatting (human-readable bytes, speed)
22
22
  */
23
+ import { perfNow } from "./utils.js";
23
24
  // ============================================================================
24
25
  // §2 PROGRESS TRACKER
25
26
  // ============================================================================
@@ -48,8 +49,16 @@ export class ProgressTracker {
48
49
  smoothedRate = 0;
49
50
  smoothing;
50
51
  _done = false;
51
- // Throttle state
52
- lastEmitMs = 0;
52
+ // Throttle state.
53
+ //
54
+ // `-Infinity` rather than 0: the gate is `now - lastEmitMs >= minIntervalMs`
55
+ // and `now` comes from `performance.now()`, which is time since *process*
56
+ // start. With a 0 baseline the very first update passes only once the
57
+ // process has been alive for a full interval, so for the first second of
58
+ // any CLI or serverless invocation **no progress event was emitted at all**.
59
+ // A transfer starting at t=200 ms and finishing at t=800 ms reported
60
+ // nothing but the forced final snapshot — a progress bar that never moves.
61
+ lastEmitMs = -Infinity;
53
62
  minIntervalMs;
54
63
  onProgress;
55
64
  constructor(total, options = {}) {
@@ -118,8 +127,13 @@ export class ProgressTracker {
118
127
  this.lastMs = now;
119
128
  this.lastLoaded = this.loaded;
120
129
  }
130
+ // Clamped at BOTH ends. The upper clamp was there; a negative chunk — a
131
+ // caller bug, or a Content-Length revised downward mid-transfer — put
132
+ // `percent` below zero and every progress line rendered "(-50.0%)". A
133
+ // running total of transferred bytes cannot be meaningfully negative, so
134
+ // the ratio is bounded to [0, 100].
121
135
  const percent = this.total !== null && this.total > 0
122
- ? Math.min(100, (this.loaded / this.total) * 100)
136
+ ? Math.min(100, Math.max(0, (this.loaded / this.total) * 100))
123
137
  : null;
124
138
  const eta = this.total !== null && this.smoothedRate > 0
125
139
  ? Math.max(0, ((this.total - this.loaded) / this.smoothedRate) * 1000)
@@ -185,6 +199,13 @@ export function withUploadProgress(body, total, options = {}) {
185
199
  // demand, so backpressure reaches the source.
186
200
  let reader = null;
187
201
  let onUploadAbort = null;
202
+ // Distinguishes an end-of-stream from a cancellation, which a pending
203
+ // `read()` reports identically. `withDownloadProgress` already kept this
204
+ // flag; without it here, aborting an upload ran the `done` branch below —
205
+ // `tracker.complete()` and `controller.close()` — on the path that was
206
+ // aborting, so the transfer was still announced as finished and 100% done
207
+ // even after removing the `complete()` from the abort handler itself.
208
+ let aborted = false;
188
209
  const detach = () => {
189
210
  if (onUploadAbort) {
190
211
  options.signal?.removeEventListener("abort", onUploadAbort);
@@ -200,10 +221,19 @@ export function withUploadProgress(body, total, options = {}) {
200
221
  }
201
222
  reader = source.getReader();
202
223
  onUploadAbort = () => {
224
+ if (aborted)
225
+ return;
226
+ aborted = true;
203
227
  const r = reader;
204
228
  reader = null;
205
229
  void r?.cancel("aborted").catch(() => { });
206
- tracker.complete();
230
+ // The tracker is deliberately NOT completed here. `complete()` reports
231
+ // `done: true` and, because `done` is what forces the final percent,
232
+ // `percent: 100`: an upload aborted 10 bytes into a 100-byte body was
233
+ // announced to every `onProgress` listener as a finished, 100%-complete
234
+ // transfer. The caller learns the real outcome from the AbortError the
235
+ // stream is errored with, which is the same contract
236
+ // `withDownloadProgress` already followed on abort.
207
237
  controller.error(new DOMException("Upload aborted", "AbortError"));
208
238
  };
209
239
  // Removed in detach() on close/cancel/error so repeated uploads sharing
@@ -211,16 +241,26 @@ export function withUploadProgress(body, total, options = {}) {
211
241
  options.signal?.addEventListener("abort", onUploadAbort, { once: true });
212
242
  },
213
243
  async pull(controller) {
214
- if (!reader)
244
+ // Captured once, before the await. The abort handler nulls the shared
245
+ // `reader` while a read is pending, and re-reading the variable after
246
+ // the await meant `releaseLock()` below dereferenced `null`.
247
+ const r = reader;
248
+ if (!r)
215
249
  return;
216
250
  try {
217
- const { done, value } = await reader.read();
251
+ const { done, value } = await r.read();
218
252
  if (done) {
219
253
  detach();
220
- reader.releaseLock();
221
- reader = null;
222
- tracker.complete();
223
- controller.close();
254
+ if (reader === r)
255
+ reader = null;
256
+ r.releaseLock();
257
+ // `done` here can just as well mean "cancelled by the abort
258
+ // handler", in which case the stream is already errored and the
259
+ // transfer must not be reported as a completed one.
260
+ if (!aborted) {
261
+ tracker.complete();
262
+ controller.close();
263
+ }
224
264
  return;
225
265
  }
226
266
  tracker.update(value.byteLength);
@@ -228,7 +268,10 @@ export function withUploadProgress(body, total, options = {}) {
228
268
  }
229
269
  catch (err) {
230
270
  detach();
231
- tracker.complete();
271
+ if (reader === r)
272
+ reader = null;
273
+ if (!aborted)
274
+ tracker.complete();
232
275
  controller.error(err);
233
276
  }
234
277
  },
@@ -302,15 +345,22 @@ export function withDownloadProgress(response, options = {}) {
302
345
  options.signal?.addEventListener("abort", onAbort, { once: true });
303
346
  },
304
347
  async pull(controller) {
305
- if (!reader)
348
+ // Captured before the await, for the same reason as the upload wrapper:
349
+ // the abort handler nulls the shared `reader` while a read is pending.
350
+ const r = reader;
351
+ if (!r)
306
352
  return;
307
353
  try {
308
- const { done, value } = await reader.read();
354
+ const { done, value } = await r.read();
309
355
  if (done) {
310
356
  detach();
311
- reader.releaseLock();
312
- reader = null;
313
- tracker.complete();
357
+ if (reader === r)
358
+ reader = null;
359
+ r.releaseLock();
360
+ // Ensure tracker is marked as complete even on error
361
+ if (!aborted) {
362
+ tracker.complete();
363
+ }
314
364
  controller.close();
315
365
  return;
316
366
  }
@@ -319,6 +369,8 @@ export function withDownloadProgress(response, options = {}) {
319
369
  }
320
370
  catch (err) {
321
371
  detach();
372
+ if (reader === r)
373
+ reader = null;
322
374
  // Ensure tracker is marked as complete even on error
323
375
  if (!aborted) {
324
376
  tracker.complete();
@@ -408,10 +460,23 @@ export async function collectStream(stream, total, options = {}) {
408
460
  // Handle already-aborted signal
409
461
  if (options.signal?.aborted) {
410
462
  reader.cancel("aborted").catch(() => { });
463
+ reader.releaseLock();
411
464
  throw new DOMException("Stream aborted", "AbortError");
412
465
  }
466
+ // An abort has to REJECT, not merely stop the reads.
467
+ //
468
+ // `reader.cancel()` resolves the pending `read()` with `{ done: true }`,
469
+ // which is indistinguishable from an ordinary end-of-stream: the loop broke
470
+ // out, `tracker.complete()` ran, and the truncated buffer was returned as a
471
+ // successful result. A caller collecting a 100-byte body under an abort that
472
+ // arrived after 3 bytes got `data: 3 bytes, isDone: true` and no error, so
473
+ // the partial body was stored as if it were the whole one. The sibling API
474
+ // `streamWithProgress` throws `AbortError` for exactly this case, so the two
475
+ // disagreed about the same event.
476
+ let aborted = false;
413
477
  // Register abort handler (removed in the finally below — leak fix)
414
478
  const abortHandler = () => {
479
+ aborted = true;
415
480
  reader.cancel("aborted").catch(() => { });
416
481
  };
417
482
  if (options.signal) {
@@ -420,11 +485,17 @@ export async function collectStream(stream, total, options = {}) {
420
485
  try {
421
486
  while (true) {
422
487
  const { done, value } = await reader.read();
488
+ // Once the abort handler has cancelled the reader, this read — including
489
+ // one already in flight — resolves `{ done: true }`, so the loop always
490
+ // breaks and the check below is reached. There is no path that skips it.
423
491
  if (done)
424
492
  break;
425
493
  chunks.push(value);
426
494
  tracker.update(value.byteLength);
427
495
  }
496
+ // `done` above can equally mean "cancelled by the abort handler".
497
+ if (aborted)
498
+ throw new DOMException("Stream aborted", "AbortError");
428
499
  }
429
500
  finally {
430
501
  reader.releaseLock();
@@ -483,6 +554,17 @@ export class MultiPartProgressAggregator {
483
554
  * @returns A ProgressTracker instance for this part.
484
555
  */
485
556
  createPartTracker(partIndex, partSize, options = {}) {
557
+ // "Create or retrieve", as this method documents itself. It always
558
+ // created: a second call for the same index built a fresh tracker and
559
+ // overwrote the stored one, so the bytes already recorded for that part
560
+ // were silently discarded and the aggregate fell back to 0 — a caller
561
+ // re-acquiring a tracker it had lost a handle to erased the part's
562
+ // progress rather than recovering it. The first call's `partSize` wins,
563
+ // because changing it would restate a total the bytes were counted
564
+ // against.
565
+ const existing = this.trackers.get(partIndex);
566
+ if (existing)
567
+ return existing;
486
568
  this.totals.set(partIndex, partSize);
487
569
  const tracker = new ProgressTracker(partSize, {
488
570
  ...options,
@@ -528,7 +610,13 @@ export class MultiPartProgressAggregator {
528
610
  maxElapsed = snap.elapsed;
529
611
  }
530
612
  const hasTotal = totalBytes > 0;
531
- const percent = hasTotal ? Math.min(100, (totalLoaded / totalBytes) * 100) : null;
613
+ // Bounded at BOTH ends, for the same reason `ProgressTracker._snapshot`
614
+ // bounds its own: a negative chunk — a caller bug, or a `Content-Length`
615
+ // revised downward mid-transfer — put the aggregate below zero and
616
+ // rendered "(-50.0%)". The tracker was clamped in an earlier round and
617
+ // the aggregate that sums the same snapshots was not, so the overall view
618
+ // was the one place the fix did not reach.
619
+ const percent = hasTotal ? Math.max(0, Math.min(100, (totalLoaded / totalBytes) * 100)) : null;
532
620
  const eta = hasTotal && totalRate > 0
533
621
  ? Math.max(0, ((totalBytes - totalLoaded) / totalRate) * 1000)
534
622
  : null;
@@ -574,6 +662,19 @@ export function xhrFetch(url, options) {
574
662
  reject(new Error("XMLHttpRequest is not available in this runtime. Use fetch() instead."));
575
663
  return;
576
664
  }
665
+ // `XHRResult.body` is a string, but `responseType` is typed with the whole
666
+ // XHR enum — and everything outside "" / "text" hands back a parsed value
667
+ // rather than text. `String(xhr.response)` turned an ArrayBuffer, a Blob
668
+ // and a parsed JSON object alike into the literal string
669
+ // "[object Object]", which is indistinguishable from a body and is not one:
670
+ // the upload path is the reason this function exists, and a binary
671
+ // response came back as seven characters of nothing. Refused here, before
672
+ // anything is constructed, so the request is never opened.
673
+ if (options.responseType && options.responseType !== "text") {
674
+ reject(new TypeError(`xhrFetch: responseType "${options.responseType}" cannot produce a text body. ` +
675
+ `XHRResult.body is a string — use "text", or omit responseType.`));
676
+ return;
677
+ }
577
678
  const xhr = new _XHR();
578
679
  const uploadTracker = new ProgressTracker(null, {
579
680
  ...(options.onUploadProgress !== undefined ? { onProgress: options.onUploadProgress } : {}),
@@ -636,11 +737,32 @@ export function xhrFetch(url, options) {
636
737
  // Parse response headers
637
738
  const rawHeaders = xhr.getAllResponseHeaders();
638
739
  const headers = {};
639
- for (const line of rawHeaders.trim().split(/[\r\n]+/)) {
640
- const idx = line.indexOf(": ");
641
- if (idx !== -1) {
642
- headers[line.slice(0, idx).toLowerCase()] = line.slice(idx + 2);
740
+ for (const line of rawHeaders.split(/[\r\n]+/)) {
741
+ // Split on the colon, not on `": "`. The separator is the *last* space
742
+ // before the value, so `trim()` first stripped the single space of an
743
+ // empty value — `"X-Empty: \r\n"` became `"X-Empty:"`, the search
744
+ // for `": "` missed it, and the header was dropped. A response whose
745
+ // headers were all empty-valued normalized to `{}`.
746
+ const idx = line.indexOf(":");
747
+ if (idx <= 0)
748
+ continue;
749
+ const name = line.slice(0, idx).trim().toLowerCase();
750
+ if (name === "")
751
+ continue;
752
+ const value = line.slice(idx + 1).trim();
753
+ // `__proto__` is a legal header name; a [[Set]] would send it to the
754
+ // inherited setter, which ignores a primitive, and the header would
755
+ // vanish rather than overwrite anything.
756
+ if (name === "__proto__") {
757
+ Object.defineProperty(headers, name, {
758
+ value,
759
+ writable: true,
760
+ enumerable: true,
761
+ configurable: true,
762
+ });
763
+ continue;
643
764
  }
765
+ headers[name] = value;
644
766
  }
645
767
  resolve({
646
768
  status: xhr.status,
@@ -662,7 +784,20 @@ export function xhrFetch(url, options) {
662
784
  detach();
663
785
  reject(new DOMException("Request aborted", "AbortError"));
664
786
  });
665
- xhr.send(options.body ?? null);
787
+ try {
788
+ xhr.send(options.body ?? null);
789
+ }
790
+ catch (err) {
791
+ // A body the implementation refuses — an invalid state, a type it will
792
+ // not put on the wire — throws straight out of the executor. The promise
793
+ // rejects with it, so the caller sees an error, but every handler above
794
+ // had already been wired and none of them runs on this path: the
795
+ // signal's `abort` listener stayed attached, holding this XHR and these
796
+ // closures alive for as long as the caller keeps the signal. The detach
797
+ // that every other exit performs happens here too.
798
+ detach();
799
+ throw err;
800
+ }
666
801
  });
667
802
  }
668
803
  // ============================================================================
@@ -678,13 +813,26 @@ const UNITS = ["B", "KB", "MB", "GB", "TB"];
678
813
  * @returns Formatted string (e.g. "1.18 MB").
679
814
  */
680
815
  export function formatBytes(bytes, decimals = 2) {
816
+ if (Number.isNaN(bytes))
817
+ return "NaN B";
818
+ if (bytes === Infinity)
819
+ return "∞";
820
+ if (bytes === -Infinity)
821
+ return "-∞";
681
822
  if (bytes === 0)
682
823
  return "0 B";
683
824
  const k = 1024;
684
825
  const dm = Math.max(0, decimals);
685
- const i = Math.floor(Math.log(bytes) / Math.log(k));
686
- const unit = UNITS[Math.min(i, UNITS.length - 1)];
687
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${unit}`;
826
+ const sign = bytes < 0 ? "-" : "";
827
+ const mag = Math.abs(bytes);
828
+ // Clamp the unit index into range. Math.log(0.5) / Math.log(1024) floors to
829
+ // -1, and Math.log of a negative or NaN is NaN — either one indexed past the
830
+ // end of UNITS and rendered "512 undefined" or "NaN undefined". A transfer
831
+ // rate below 1 byte/s is ordinary on a slow link, so formatRate — and with
832
+ // it every progress line — was affected.
833
+ const i = Math.min(Math.max(0, Math.floor(Math.log(mag) / Math.log(k))), UNITS.length - 1);
834
+ const unit = UNITS[i];
835
+ return `${sign}${parseFloat((mag / Math.pow(k, i)).toFixed(dm))} ${unit}`;
688
836
  }
689
837
  /**
690
838
  * Format a transfer rate as human-readable bytes/sec.
@@ -720,7 +868,7 @@ export function formatETA(ms) {
720
868
  * Build a human-readable progress string.
721
869
  *
722
870
  * Output format: "{loaded} / {total} ({percent}) @ {rate} ETA {eta}"
723
- * Example: "4.56 MB / 10.00 MB (45.6%) at 1.00 MB/s ETA 5s"
871
+ * Example: "488.28 KB / 976.56 KB (50.0%) @ 97.66 KB/s ETA 5s"
724
872
  *
725
873
  * @param snap Progress snapshot
726
874
  * @returns Formatted progress string
@@ -763,7 +911,10 @@ export function formatProgress(snap) {
763
911
  */
764
912
  export function throttleProgress(fn, hz = 10) {
765
913
  const minInterval = hz > 0 ? 1000 / hz : 0;
766
- let lastCall = 0;
914
+ // See ProgressTracker.lastEmitMs: a 0 baseline measures against process
915
+ // start, so the first call was dropped for the first interval of the
916
+ // process's life.
917
+ let lastCall = -Infinity;
767
918
  return (snap) => {
768
919
  const now = perfNow();
769
920
  // Always emit final snapshot
@@ -776,6 +927,3 @@ export function throttleProgress(fn, hz = 10) {
776
927
  // ============================================================================
777
928
  // §10 UTILITIES
778
929
  // ============================================================================
779
- function perfNow() {
780
- return typeof performance !== "undefined" ? performance.now() : Date.now();
781
- }