All files / utils/tick-time tick-time.js

83.49% Statements 86/103
66.66% Branches 4/6
57.14% Functions 4/7
83.49% Lines 86/103

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 1041x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                         1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
import { IterableWeakSet } from '../algorithms/iterable-weak-struct.js'
import { timeNowFrame } from '../algorithms/time.utils.js'
 
/**
 * A margin that improves the reliability of the next time value to change
 * due to possible protections to fingerprinting on the client side
 *
 * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now}
 */
const MARGIN_MILLIS = 100
 
/**
 * @param {TimeTickInstance["data"]} newInstance - target time tick instance
 */
function checkTick (newInstance) {
  const { callbacks, tickingElements, timeoutNumber } = newInstance
  if (timeoutNumber === undefined && tickingElements.size > 0 && callbacks.size > 0) {
    const nextSecond = 1000 - timeNowFrame() % 1000
    newInstance.timeoutNumber = setTimeout(() => {
      for (const tickCallback of callbacks) {
        tickCallback({
          targets: [...tickingElements],
          untick: (el) => tickingElements.delete(el),
        })
      }
      newInstance.timeoutNumber = undefined
      checkTick(newInstance)
    }, nextSecond + MARGIN_MILLIS)
  }
}
 
 
/**
 * Builds a time tick instance
 */
function buildTimeTick () {
  const data = {
    /** @type {IterableWeakSet<Element>} */
    tickingElements: new IterableWeakSet(),
    /** @type {ReturnType<typeof setTimeout> | undefined} */
    timeoutNumber: undefined,
    /** @type {Set<TimeTickCallback>} */
    callbacks: new Set(),
  }
 
  const api = {
 
    /** @param {Element} element - target element */
    tickElement: (element) => {
      data.tickingElements.add(element)
      checkTick(data)
    },
 
    /** @param {Element} element - target element */
    untickElement: (element) => {
      data.tickingElements.delete(element)
    },
 
    /** @param {TimeTickCallback} callback - callback to trigger each time tick */
    addCallback: (callback) => {
      data.callbacks.add(callback)
      checkTick(data)
    },
 
    /** @param {TimeTickCallback} callback - callback to trigger each time tick */
    removeCallback: (callback) => {
      data.callbacks.delete(callback)
    },
  }
 
  return { api, data }
}
 
 
/** @type {TimeTickInstance} */
let timeTickInstances
 
/**
 * @returns {TimeTickApi} singleton time tick instance
 */
export function timeTick () {
  timeTickInstances ??= buildTimeTick()
  return timeTickInstances.api
}
 
/**
 * @typedef {object} TimeTickCallbackParams
 * @property {Element[]} targets - target ticking elements
 * @property {(el: Element) => void} untick - stop ticking an element, does nothing if already not ticking
 */
 
/**
 * @typedef {ReturnType<typeof buildTimeTick>} TimeTickInstance
 */
/**
 * @typedef {TimeTickInstance["api"]} TimeTickApi
 */
 
 
/**
 * @callback TimeTickCallback
 * @param {TimeTickCallbackParams} param
 */