remote-date-synchronizer.js

import { RemoteDate } from "./remote-date.js";
import { estimateRemoteDateEpochWithAccountedNetworkDelay } from "./estimate-remote-date-epoch-with-accounted-network-delay.js";

/**
 * @typedef {Object} FetchRemoteOptions
 * @property {AbortSignal} signal
 *
 * @typedef {Object} FetchRemoteResult
 * @property {Date} remoteDate
 * @property {number} serverStartProcessingMonotonicTimeInMs
 * @property {number} serverEndProcessingMonotonicTimeInMs
 *
 * @callback FetchRemote
 * @param {FetchRemoteOptions} options
 * @returns {Promise<FetchRemoteResult>}
 */

/** @class */
export class RemoteDateSynchronizer {
  /**
   * @type {FetchRemote | null}
   */
  #fetchRemote = null;

  /**
   * @type {AbortController | null}
   */
  #fetchRemoteAbortController = null;

  /**
   * @type {RemoteDate}
   */
  #remoteDate;

  /**
   * @typedef {Object} RemoteDateSynchronizerOptions
   * @property {FetchRemote} fetchRemote
   * @property {RemoteDate} remoteDate
   */

  /**
   * @param {RemoteDateSynchronizerOptions} options
   */
  constructor({ fetchRemote, remoteDate }) {
    this.#fetchRemote = fetchRemote;
    this.#remoteDate = remoteDate;
  }

  /**
   * Synchronizes the local RemoteDate instance with a remote server's date.
   * It uses a `fetchRemote` function to communicate with the server and estimates the remote date by
   * accounting for network delays during the data transmission.
   * @returns {Promise<void>}
   */
  async syncWithRemote() {
    if (!this.#fetchRemote) {
      throw new Error(
        `${RemoteDate.name} is not initialized yet with the fetchRemote function.`
      );
    }

    if (this.#fetchRemoteAbortController) {
      this.#fetchRemoteAbortController.abort();
    }
    this.#fetchRemoteAbortController = new AbortController();

    const syncStartMonotonicTimeInMs = performance.now();
    const {
      serverStartProcessingMonotonicTimeInMs,
      remoteDate,
      serverEndProcessingMonotonicTimeInMs,
    } = await this.#fetchRemote({
      signal: this.#fetchRemoteAbortController.signal,
    });
    const syncEndMonotonicTimeInMs = performance.now();

    const estimatedRemoteDateWhenSyncEnded =
      estimateRemoteDateEpochWithAccountedNetworkDelay({
        receivedRemoteDate: remoteDate,
        syncStartMonotonicTimeInMs,
        serverStartProcessingMonotonicTimeInMs,
        serverEndProcessingMonotonicTimeInMs,
        syncEndMonotonicTimeInMs,
      });

    this.#remoteDate.setRemoteTime({
      referencingDate: new Date(estimatedRemoteDateWhenSyncEnded),
      referencingMonotonicTime: syncEndMonotonicTimeInMs,
    });
  }

  /**
   * Cleans up any ongoing or future remote fetch operations.
   * @returns {void}
   */
  destroy() {
    this.#fetchRemote = null;
    if (this.#fetchRemoteAbortController) {
      this.#fetchRemoteAbortController.abort();
      this.#fetchRemoteAbortController = null;
    }
  }
}