master
AdityaAngajala Publish 0.3.0 6d39e2e 2mo ago
import { generateCircuitField } from "./circuit-geometry-v9.js?geometry=1";
import { createStreamingCircuitField } from "./circuit-field-stream.js";

function finiteNumber(value, fallback) {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
}

function traceBounds(points) {
  let minimum = Number.POSITIVE_INFINITY;
  let maximum = Number.NEGATIVE_INFINITY;
  for (const point of points) {
    minimum = Math.min(minimum, point[1]);
    maximum = Math.max(maximum, point[1]);
  }
  return [minimum, maximum];
}

function initialTraceAdapter(trace, index) {
  const [minY, maxY] = traceBounds(trace.points);
  return Object.freeze({
    ...trace,
    id: `initial:${index}`,
    chunkId: 0,
    ownerChunk: 0,
    minY,
    maxY,
  });
}

function continuationTraceAdapter(trace, translateY) {
  return Object.freeze({
    ...trace,
    id: `continuation:${trace.id}`,
    chunkId: trace.chunkId + 1,
    ownerChunk: trace.ownerChunk + 1,
    points: trace.points.map(([x, y]) => [x, y + translateY]),
    minY: trace.minY + translateY,
    maxY: trace.maxY + translateY,
  });
}

/**
 * Combines the canonical finite generator with one persistent continuation.
 *
 * The first ensure publishes the unchanged finite geometry through adapter
 * copies. Later batches come from one deterministic streaming field whose
 * fixed chunk height equals the finite page's normalized height.
 */
export function createHybridCircuitField(options = {}) {
  let config;
  let initialField;
  let initialBatch;
  let continuation;
  let unsubscribeContinuation;
  let continuationTranslateY;
  let generationTolerance;
  let retained;
  let callbacks;
  let initialPublished;
  let totalPruned;
  let destroyed = false;

  function assertLive() {
    if (destroyed) throw new Error("Hybrid circuit field has been destroyed");
  }

  function notify(batch) {
    for (const callback of callbacks) callback(batch, api);
  }

  function initialize(nextOptions) {
    unsubscribeContinuation?.();
    continuation?.destroy();

    config = { ...nextOptions };
    const requestedWidth = finiteNumber(config.width, 900);
    const requestedInitialPageHeight = finiteNumber(
      config.initialPageHeight,
      finiteNumber(config.height, 450),
    );
    initialField = generateCircuitField({
      ...config,
      width: requestedWidth,
      height: requestedInitialPageHeight,
    });
    const pitch = initialField.height / Math.max(1, initialField.rows - 1);
    const continuationRows = Math.max(1, initialField.rows - 1);
    continuationTranslateY = initialField.height - pitch;
    generationTolerance = Math.max(1e-9, pitch * 1e-7);
    initialBatch = Object.freeze(
      initialField.traces.map(initialTraceAdapter),
    );
    retained = [];
    initialPublished = false;
    totalPruned = 0;

    continuation = createStreamingCircuitField({
      ...config,
      width: requestedWidth,
      seed: `${config.seed ?? "nitrogen"}:continuation`,
      chunkRows: continuationRows,
    });
    unsubscribeContinuation = continuation.onAppend((traces) => {
      const batch = Object.freeze(
        traces.map((trace) => continuationTraceAdapter(trace, continuationTranslateY)),
      );
      retained.push(...batch);
      notify(batch);
    });
  }

  function publishInitial() {
    if (initialPublished) return [];
    initialPublished = true;
    retained.push(...initialBatch);
    notify(initialBatch);
    return initialBatch;
  }

  const api = {
    ensureThrough(worldY) {
      assertLive();
      const target = Math.max(0, finiteNumber(worldY, 0));
      const appended = [...publishInitial()];
      const continuationTarget = Math.max(0, target - initialField.height);
      const tolerantTarget = Math.max(0, continuationTarget - generationTolerance);
      if (tolerantTarget > continuation.generatedThrough) {
        const before = retained.length;
        continuation.ensureThrough(tolerantTarget);
        appended.push(...retained.slice(before));
      }
      return appended;
    },

    onAppend(callback) {
      assertLive();
      if (typeof callback !== "function") throw new TypeError("callback must be a function");
      callbacks.add(callback);
      return () => callbacks.delete(callback);
    },

    pruneBefore(worldY) {
      assertLive();
      const numericBoundary = Number(worldY);
      const boundary = Number.isNaN(numericBoundary) ? 0 : numericBoundary;
      const removed = [];
      let writeIndex = 0;
      for (const trace of retained) {
        if (trace.maxY < boundary) {
          removed.push(trace.id);
          continue;
        }
        retained[writeIndex] = trace;
        writeIndex += 1;
      }
      retained.length = writeIndex;
      continuation.pruneBefore(boundary - continuationTranslateY);
      totalPruned += removed.length;
      return removed;
    },

    reset(nextOptions = {}) {
      assertLive();
      initialize({ ...config, ...nextOptions });
      return api;
    },

    destroy() {
      if (destroyed) return;
      destroyed = true;
      unsubscribeContinuation?.();
      continuation?.destroy();
      callbacks.clear();
      retained = [];
    },

    get width() {
      return initialField.width;
    },

    get height() {
      return initialField.height + continuation.generatedThrough;
    },

    get generatedThrough() {
      return initialField.height + continuation.generatedThrough;
    },

    get frontier() {
      return initialField.height + continuation.generatedThrough;
    },

    get traces() {
      return retained;
    },

    get retainedTraces() {
      return retained;
    },

    get initialField() {
      return initialField;
    },

    get stats() {
      const streamStats = continuation.stats;
      const initialTraceCount = initialField.traces.length;
      return Object.freeze({
        ...streamStats,
        chunks: streamStats.chunks + 1,
        columns: initialField.columns,
        chunkRows: initialField.rows - 1,
        pitch: initialField.height / Math.max(1, initialField.rows - 1),
        width: initialField.width,
        generatedThrough: initialField.height + continuation.generatedThrough,
        totalPublished: (initialPublished ? initialTraceCount : 0)
          + streamStats.totalPublished,
        retainedTraces: retained.length,
        totalPruned,
        initialPublished,
        initialPageHeight: initialField.height,
        initialPageRows: initialField.rows,
        initialPageTraceCount: initialTraceCount,
      });
    },
  };

  callbacks = new Set();
  initialize(options);
  return api;
}