master
AdityaAngajala Publish 0.3.0 6d39e2e 2mo ago
function finiteNumber(value, fallback) {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
}

function batchBounds(traces) {
  let minimum = Number.POSITIVE_INFINITY;
  let maximum = Number.NEGATIVE_INFINITY;
  for (const trace of traces) {
    const minY = finiteNumber(trace?.minY, Number.NaN);
    const maxY = finiteNumber(trace?.maxY, Number.NaN);
    if (Number.isFinite(minY) && Number.isFinite(maxY)) {
      minimum = Math.min(minimum, minY, maxY);
      maximum = Math.max(maximum, minY, maxY);
      continue;
    }
    for (const point of trace?.points || []) {
      const y = Number(point?.[1]);
      if (!Number.isFinite(y)) continue;
      minimum = Math.min(minimum, y);
      maximum = Math.max(maximum, y);
    }
  }
  return [
    Number.isFinite(minimum) ? minimum : 0,
    Number.isFinite(maximum) ? maximum : 0,
  ];
}

/**
 * Keeps pre-generated trace geometry off the SVG until its page is close
 * enough to the viewport to participate in reveal ordering.
 *
 * Batches are always materialized in publication order. That preserves the
 * reveal hash index and therefore the deterministic spawn order of an
 * unstaged append stream.
 */
export function createPageTraceStager(options = {}) {
  const materialize = options.materialize;
  if (typeof materialize !== "function") {
    throw new TypeError("materialize must be a function");
  }

  let pending = [];
  let pendingHead = 0;
  let materializedBatches = 0;
  let materializedTraces = 0;
  let bufferedTraces = 0;
  let generatedTraces = 0;
  let through = Number.NEGATIVE_INFINITY;
  let destroyed = false;

  function compact() {
    if (!pendingHead) return;
    pending = pending.slice(pendingHead);
    pendingHead = 0;
  }

  function flush() {
    if (destroyed) return 0;
    let appended = 0;
    while (pendingHead < pending.length) {
      const batch = pending[pendingHead];
      if (batch.minY > through) break;
      pendingHead += 1;
      bufferedTraces -= batch.traces.length;
      materializedBatches += 1;
      materializedTraces += batch.traces.length;
      appended += batch.traces.length;
      materialize(batch.traces, batch);
    }
    if (pendingHead > 64 && pendingHead > pending.length / 2) compact();
    return appended;
  }

  const api = {
    stage(traces) {
      if (destroyed) return 0;
      if (!Array.isArray(traces)) throw new TypeError("traces must be an array");
      if (!traces.length) return 0;
      const [minY, maxY] = batchBounds(traces);
      const batch = Object.freeze({
        traces,
        minY,
        maxY,
        index: materializedBatches + pending.length - pendingHead,
      });
      pending.push(batch);
      bufferedTraces += traces.length;
      generatedTraces += traces.length;
      return flush();
    },

    materializeThrough(worldY) {
      if (destroyed) return 0;
      through = Math.max(through, finiteNumber(worldY, through));
      return flush();
    },

    clear() {
      pending.length = 0;
      pendingHead = 0;
      materializedBatches = 0;
      materializedTraces = 0;
      bufferedTraces = 0;
      generatedTraces = 0;
      through = Number.NEGATIVE_INFINITY;
      return api;
    },

    destroy() {
      if (destroyed) return;
      destroyed = true;
      pending.length = 0;
      pendingHead = 0;
      bufferedTraces = 0;
    },

    get stats() {
      return Object.freeze({
        generatedTraces,
        materializedTraces,
        bufferedTraces,
        materializedBatches,
        bufferedBatches: pending.length - pendingHead,
        materializedThroughWorldY: Number.isFinite(through) ? through : null,
      });
    },
  };

  return api;
}