master
AdityaAngajala Publish 0.3.0 6d39e2e 2mo ago
const EMPTY_STATS = Object.freeze({
  chunks: 0,
  columns: 0,
  chunkRows: 0,
  pitch: 0,
  width: 0,
  generatedThrough: 0,
  occupiedCells: 0,
  occupancyFill: 0,
  occupiedCellsRetained: 0,
  occupiedCellsPruned: 0,
  occupancyRowsRetained: 0,
  occupancyRowsPruned: 0,
  flowVorticesRetained: 0,
  flowVorticesPruned: 0,
  generationCacheMinimumRow: 1,
  totalPublished: 0,
  retainedTraces: 0,
  totalPruned: 0,
  rejectedRoutes: 0,
  routeGuardRows: 0,
  occupancyRetainedForSafety: true,
  initialPublished: false,
  initialPageHeight: 0,
  initialPageRows: 0,
  initialPageTraceCount: 0,
});

function deferred() {
  let resolve;
  let reject;
  const promise = new Promise((resolvePromise, rejectPromise) => {
    resolve = resolvePromise;
    reject = rejectPromise;
  });
  promise.catch(() => {});
  return {
    promise,
    resolve,
    reject,
    settled: false,
  };
}

function abortError(message) {
  if (typeof DOMException === "function") {
    return new DOMException(message, "AbortError");
  }
  const error = new Error(message);
  error.name = "AbortError";
  return error;
}

function errorFromPayload(payload, fallbackMessage) {
  const error = new Error(payload?.message || fallbackMessage);
  error.name = payload?.name || "Error";
  if (payload?.stack) error.stack = payload.stack;
  return error;
}

function immutableStats(stats) {
  return Object.freeze({ ...EMPTY_STATS, ...(stats || {}) });
}

function immutableTrace(trace) {
  return Object.freeze(trace);
}

function notify(callbacks, value, api) {
  for (const callback of callbacks) {
    try {
      callback(value, api);
    } catch (error) {
      console.error("Circuit field worker callback failed", error);
    }
  }
}

/**
 * Main-thread client for circuit-field-hybrid-worker-v9.js.
 *
 * The returned facade is available immediately. Await `ready` before reading
 * normalized dimensions. `ensureThrough()` resolves after all append callbacks
 * for its requested frontier have run.
 */
export function createStreamingCircuitFieldWorker(options = {}, clientOptions = {}) {
  const appendCallbacks = new Set();
  const fallbackCallbacks = new Set();
  const statusCallbacks = new Set();
  const pendingEnsures = new Map();
  const pendingPrunes = new Map();
  const retained = [];

  let currentOptions = { ...options };
  let worker;
  let workerEpoch = 0;
  let requestSequence = 0;
  let readyState;
  let lastStats = EMPTY_STATS;
  let fallbackReason = null;
  let state = "starting";
  let destroyed = false;

  const workerUrl = clientOptions.workerUrl
    || new URL("./circuit-field-hybrid-worker-v9.js", import.meta.url);
  const workerName = clientOptions.name || "circuit-field-hybrid-v9";

  function setState(nextState) {
    if (state === nextState) return;
    state = nextState;
    queueMicrotask(() => notify(statusCallbacks, nextState, api));
  }

  function rejectPending(error) {
    for (const request of pendingEnsures.values()) request.reject(error);
    for (const request of pendingPrunes.values()) request.reject(error);
    pendingEnsures.clear();
    pendingPrunes.clear();
  }

  function terminateWorker() {
    if (!worker) return;
    const target = worker;
    worker = undefined;
    try {
      target.postMessage({ type: "destroy" });
    } catch {
      // The worker may already have stopped.
    }
    target.terminate();
  }

  function signalFallback(kind, error, phase = "startup") {
    if (destroyed || state === "failed") return;
    const normalizedError = error instanceof Error
      ? error
      : errorFromPayload(error, "Circuit field worker failed");
    fallbackReason = Object.freeze({
      kind,
      phase,
      error: normalizedError,
      options: Object.freeze({ ...currentOptions }),
      synchronousFallbackAvailable: true,
    });
    setState("failed");
    terminateWorker();
    if (readyState && !readyState.settled) {
      readyState.settled = true;
      readyState.reject(normalizedError);
    }
    rejectPending(normalizedError);
    notify(fallbackCallbacks, fallbackReason, api);
  }

  function createWorkerInstance() {
    if (typeof clientOptions.workerFactory === "function") {
      return clientOptions.workerFactory(workerUrl, {
        type: "module",
        name: workerName,
      });
    }
    if (typeof Worker !== "function") {
      throw new Error("Web Workers are unavailable");
    }
    return new Worker(workerUrl, {
      type: "module",
      name: workerName,
    });
  }

  function handleAppend(message) {
    lastStats = immutableStats(message.stats);
    const batch = Object.freeze(
      (message.traces || []).map(immutableTrace),
    );
    retained.push(...batch);
    notify(appendCallbacks, batch, api);
  }

  function handleEnsured(message) {
    lastStats = immutableStats(message.stats);
    const request = pendingEnsures.get(message.requestId);
    if (!request) return;
    pendingEnsures.delete(message.requestId);
    request.resolve(Object.freeze({
      target: message.target,
      generatedThrough: message.generatedThrough,
      stats: lastStats,
    }));
    if (!pendingEnsures.size) setState("ready");
  }

  function handlePruned(message) {
    lastStats = immutableStats(message.stats);
    const request = pendingPrunes.get(message.requestId);
    if (!request) return;
    pendingPrunes.delete(message.requestId);
    const removedIds = Object.freeze((message.removedIds || []).slice());
    if (removedIds.length) {
      const removed = new Set(removedIds);
      let writeIndex = 0;
      for (const trace of retained) {
        if (removed.has(trace.id)) continue;
        retained[writeIndex] = trace;
        writeIndex += 1;
      }
      retained.length = writeIndex;
    }
    request.resolve(removedIds);
  }

  function handleMessage(event, epoch) {
    if (destroyed || epoch !== workerEpoch) return;
    const message = event.data || {};
    if (message.type === "ready") {
      lastStats = immutableStats(message.stats);
      setState("ready");
      if (!readyState.settled) {
        readyState.settled = true;
        readyState.resolve(api);
      }
    } else if (message.type === "append") {
      handleAppend(message);
    } else if (message.type === "ensured") {
      handleEnsured(message);
    } else if (message.type === "pruned") {
      handlePruned(message);
    } else if (message.type === "fatal") {
      signalFallback(
        "worker-runtime-error",
        errorFromPayload(message.error, "Circuit field worker failed"),
        message.phase,
      );
    } else {
      signalFallback(
        "worker-protocol-error",
        new Error(`Unknown worker response: ${String(message.type)}`),
        "message",
      );
    }
  }

  function startWorker() {
    workerEpoch += 1;
    const epoch = workerEpoch;
    readyState = deferred();
    fallbackReason = null;
    setState("starting");

    try {
      worker = createWorkerInstance();
      worker.addEventListener("message", (event) => handleMessage(event, epoch));
      worker.addEventListener("error", (event) => {
        if (epoch !== workerEpoch || destroyed) return;
        event.preventDefault?.();
        signalFallback(
          "worker-runtime-error",
          event.error || new Error(event.message || "Circuit field worker crashed"),
          "runtime",
        );
      });
      worker.addEventListener("messageerror", () => {
        if (epoch !== workerEpoch || destroyed) return;
        signalFallback(
          "worker-message-error",
          new Error("Circuit field worker returned an unreadable message"),
          "message",
        );
      });
      worker.postMessage({
        type: "init",
        options: currentOptions,
      });
    } catch (error) {
      signalFallback("worker-unavailable", error, "startup");
    }
    return readyState.promise;
  }

  async function ensureThrough(worldY) {
    if (destroyed) throw new Error("Streaming circuit field worker has been destroyed");
    const epoch = workerEpoch;
    await readyState.promise;
    if (destroyed) throw new Error("Streaming circuit field worker has been destroyed");
    if (epoch !== workerEpoch) throw abortError("Circuit field worker was reset");
    if (state === "failed" || !worker) {
      throw fallbackReason?.error || new Error("Circuit field worker is unavailable");
    }

    const target = Math.max(0, Number(worldY) || 0);
    if (lastStats.initialPublished && lastStats.generatedThrough >= target) {
      return Object.freeze({
        target,
        generatedThrough: lastStats.generatedThrough,
        stats: lastStats,
      });
    }

    const requestId = ++requestSequence;
    const request = deferred();
    pendingEnsures.set(requestId, request);
    setState("generating");
    try {
      worker.postMessage({ type: "ensure", requestId, target });
    } catch (error) {
      signalFallback("worker-message-error", error, "ensure");
    }
    return request.promise;
  }

  async function pruneBefore(worldY) {
    if (destroyed) throw new Error("Streaming circuit field worker has been destroyed");
    const epoch = workerEpoch;
    await readyState.promise;
    if (destroyed) throw new Error("Streaming circuit field worker has been destroyed");
    if (epoch !== workerEpoch) throw abortError("Circuit field worker was reset");
    if (state === "failed" || !worker) {
      throw fallbackReason?.error || new Error("Circuit field worker is unavailable");
    }

    const requestId = ++requestSequence;
    const request = deferred();
    pendingPrunes.set(requestId, request);
    try {
      worker.postMessage({
        type: "prune",
        requestId,
        worldY: Number(worldY) || 0,
      });
    } catch (error) {
      signalFallback("worker-message-error", error, "prune");
    }
    return request.promise;
  }

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

  function onFallback(callback) {
    if (typeof callback !== "function") throw new TypeError("callback must be a function");
    fallbackCallbacks.add(callback);
    if (fallbackReason) {
      const reason = fallbackReason;
      const epoch = workerEpoch;
      queueMicrotask(() => {
        if (
          destroyed
          || epoch !== workerEpoch
          || fallbackReason !== reason
          || !fallbackCallbacks.has(callback)
        ) return;
        notify([callback], reason, api);
      });
    }
    return () => fallbackCallbacks.delete(callback);
  }

  function onStatus(callback) {
    if (typeof callback !== "function") throw new TypeError("callback must be a function");
    statusCallbacks.add(callback);
    return () => statusCallbacks.delete(callback);
  }

  function reset(nextOptions = {}) {
    if (destroyed) throw new Error("Streaming circuit field worker has been destroyed");
    const resetError = abortError("Circuit field worker was reset");
    terminateWorker();
    if (readyState && !readyState.settled) {
      readyState.settled = true;
      readyState.reject(resetError);
    }
    rejectPending(resetError);
    retained.length = 0;
    lastStats = EMPTY_STATS;
    currentOptions = { ...currentOptions, ...nextOptions };
    return startWorker();
  }

  function destroy() {
    if (destroyed) return;
    destroyed = true;
    const destroyedError = abortError("Circuit field worker was destroyed");
    terminateWorker();
    if (readyState && !readyState.settled) {
      readyState.settled = true;
      readyState.reject(destroyedError);
    }
    rejectPending(destroyedError);
    appendCallbacks.clear();
    fallbackCallbacks.clear();
    statusCallbacks.clear();
    retained.length = 0;
    state = "destroyed";
  }

  const api = {
    ensureThrough,
    onAppend,
    onFallback,
    onStatus,
    pruneBefore,
    reset,
    destroy,
    get ready() { return readyState.promise; },
    get status() { return state; },
    get fallbackReason() { return fallbackReason; },
    get width() { return lastStats.width; },
    get height() { return lastStats.generatedThrough; },
    get generatedThrough() { return lastStats.generatedThrough; },
    get frontier() { return lastStats.generatedThrough; },
    get traces() { return retained; },
    get retainedTraces() { return retained; },
    get stats() { return lastStats; },
  };

  startWorker();
  return api;
}