"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const dbIvm = require("@tanstack/db-ivm");
const index = require("../compiler/index.cjs");
const index$1 = require("../../collection/index.cjs");
const errors = require("../../errors.cjs");
const scheduler = require("../../scheduler.cjs");
const transactions = require("../../transactions.cjs");
const collectionSubscriber = require("./collection-subscriber.cjs");
const collectionRegistry = require("./collection-registry.cjs");
const internal = require("./internal.cjs");
const utils = require("./utils.cjs");
let liveQueryCollectionCounter = 0;
class CollectionConfigBuilder {
  constructor(config) {
    this.config = config;
    this.compiledAliasToCollectionId = {};
    this.resultKeys = /* @__PURE__ */ new WeakMap();
    this.orderByIndices = /* @__PURE__ */ new WeakMap();
    this.isGraphRunning = false;
    this.runCount = 0;
    this.isInErrorState = false;
    this.aliasDependencies = {};
    this.builderDependencies = /* @__PURE__ */ new Set();
    this.pendingGraphRuns = /* @__PURE__ */ new Map();
    this.subscriptions = {};
    this.lazySourcesCallbacks = {};
    this.lazySources = /* @__PURE__ */ new Set();
    this.optimizableOrderByCollections = {};
    this.id = config.id || `live-query-${++liveQueryCollectionCounter}`;
    this.query = utils.buildQueryFromConfig({
      query: config.query,
      requireObjectResult: true
    });
    this.collections = utils.extractCollectionsFromQuery(this.query);
    const collectionAliasesById = utils.extractCollectionAliases(this.query);
    this.collectionByAlias = {};
    for (const [collectionId, aliases] of collectionAliasesById.entries()) {
      const collection = this.collections[collectionId];
      if (!collection) continue;
      for (const alias of aliases) {
        this.collectionByAlias[alias] = collection;
      }
    }
    if (this.query.orderBy && this.query.orderBy.length > 0) {
      this.compare = createOrderByComparator(this.orderByIndices);
    }
    this.compareOptions = this.config.defaultStringCollation ?? utils.extractCollectionFromSource(this.query).compareOptions;
    this.compileBasePipeline();
  }
  /**
   * Recursively checks if a query or any of its subqueries contains joins
   */
  hasJoins(query) {
    if (query.join && query.join.length > 0) {
      return true;
    }
    if (query.from.type === `queryRef`) {
      if (this.hasJoins(query.from.query)) {
        return true;
      }
    }
    return false;
  }
  getConfig() {
    return {
      id: this.id,
      getKey: this.config.getKey || ((item) => this.resultKeys.get(item)),
      sync: this.getSyncConfig(),
      compare: this.compare,
      defaultStringCollation: this.compareOptions,
      gcTime: this.config.gcTime || 5e3,
      // 5 seconds by default for live queries
      schema: this.config.schema,
      onInsert: this.config.onInsert,
      onUpdate: this.config.onUpdate,
      onDelete: this.config.onDelete,
      startSync: this.config.startSync,
      singleResult: this.query.singleResult,
      utils: {
        getRunCount: this.getRunCount.bind(this),
        setWindow: this.setWindow.bind(this),
        getWindow: this.getWindow.bind(this),
        [internal.LIVE_QUERY_INTERNAL]: {
          getBuilder: () => this,
          hasCustomGetKey: !!this.config.getKey,
          hasJoins: this.hasJoins(this.query),
          hasDistinct: !!this.query.distinct
        }
      }
    };
  }
  setWindow(options) {
    if (!this.windowFn) {
      throw new errors.SetWindowRequiresOrderByError();
    }
    this.currentWindow = options;
    this.windowFn(options);
    this.maybeRunGraphFn?.();
    if (this.liveQueryCollection?.isLoadingSubset) {
      return new Promise((resolve) => {
        const unsubscribe = this.liveQueryCollection.on(
          `loadingSubset:change`,
          (event) => {
            if (!event.isLoadingSubset) {
              unsubscribe();
              resolve();
            }
          }
        );
      });
    }
    return true;
  }
  getWindow() {
    if (!this.windowFn || !this.currentWindow) {
      return void 0;
    }
    return {
      offset: this.currentWindow.offset ?? 0,
      limit: this.currentWindow.limit ?? 0
    };
  }
  /**
   * Resolves a collection alias to its collection ID.
   *
   * Uses a two-tier lookup strategy:
   * 1. First checks compiled aliases (includes subquery inner aliases)
   * 2. Falls back to declared aliases from the query's from/join clauses
   *
   * @param alias - The alias to resolve (e.g., "employee", "manager")
   * @returns The collection ID that the alias references
   * @throws {Error} If the alias is not found in either lookup
   */
  getCollectionIdForAlias(alias) {
    const compiled = this.compiledAliasToCollectionId[alias];
    if (compiled) {
      return compiled;
    }
    const collection = this.collectionByAlias[alias];
    if (collection) {
      return collection.id;
    }
    throw new Error(`Unknown source alias "${alias}"`);
  }
  isLazyAlias(alias) {
    return this.lazySources.has(alias);
  }
  // The callback function is called after the graph has run.
  // This gives the callback a chance to load more data if needed,
  // that's used to optimize orderBy operators that set a limit,
  // in order to load some more data if we still don't have enough rows after the pipeline has run.
  // That can happen because even though we load N rows, the pipeline might filter some of these rows out
  // causing the orderBy operator to receive less than N rows or even no rows at all.
  // So this callback would notice that it doesn't have enough rows and load some more.
  // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.
  maybeRunGraph(callback) {
    if (this.isGraphRunning) {
      return;
    }
    if (!this.currentSyncConfig || !this.currentSyncState) {
      throw new Error(
        `maybeRunGraph called without active sync session. This should not happen.`
      );
    }
    this.isGraphRunning = true;
    try {
      const { begin, commit } = this.currentSyncConfig;
      const syncState = this.currentSyncState;
      if (this.isInErrorState) {
        return;
      }
      if (syncState.subscribedToAllCollections) {
        let callbackCalled = false;
        while (syncState.graph.pendingWork()) {
          syncState.graph.run();
          syncState.flushPendingChanges?.();
          callback?.();
          callbackCalled = true;
        }
        if (!callbackCalled) {
          callback?.();
        }
        if (syncState.messagesCount === 0) {
          begin();
          commit();
        }
        this.updateLiveQueryStatus(this.currentSyncConfig);
      }
    } finally {
      this.isGraphRunning = false;
    }
  }
  /**
   * Schedules a graph run with the transaction-scoped scheduler.
   * Ensures each builder runs at most once per transaction, with automatic dependency tracking
   * to run parent queries before child queries. Outside a transaction, runs immediately.
   *
   * Multiple calls during a transaction are coalesced into a single execution.
   * Dependencies are auto-discovered from subscribed live queries, or can be overridden.
   * Load callbacks are combined when entries merge.
   *
   * Uses the current sync session's config and syncState from instance properties.
   *
   * @param callback - Optional callback to load more data if needed (returns true when done)
   * @param options - Optional scheduling configuration
   * @param options.contextId - Transaction ID to group work; defaults to active transaction
   * @param options.jobId - Unique identifier for this job; defaults to this builder instance
   * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies
   * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies
   */
  scheduleGraphRun(callback, options) {
    const contextId = options?.contextId ?? transactions.getActiveTransaction()?.id;
    const jobId = options?.jobId ?? this;
    const dependentBuilders = (() => {
      if (options?.dependencies) {
        return options.dependencies;
      }
      const deps = new Set(this.builderDependencies);
      if (options?.alias) {
        const aliasDeps = this.aliasDependencies[options.alias];
        if (aliasDeps) {
          for (const dep of aliasDeps) {
            deps.add(dep);
          }
        }
      }
      deps.delete(this);
      return Array.from(deps);
    })();
    if (contextId) {
      for (const dep of dependentBuilders) {
        if (typeof dep.scheduleGraphRun === `function`) {
          dep.scheduleGraphRun(void 0, { contextId });
        }
      }
    }
    if (!this.currentSyncConfig || !this.currentSyncState) {
      throw new Error(
        `scheduleGraphRun called without active sync session. This should not happen.`
      );
    }
    let pending = contextId ? this.pendingGraphRuns.get(contextId) : void 0;
    if (!pending) {
      pending = {
        loadCallbacks: /* @__PURE__ */ new Set()
      };
      if (contextId) {
        this.pendingGraphRuns.set(contextId, pending);
      }
    }
    if (callback) {
      pending.loadCallbacks.add(callback);
    }
    const pendingToPass = contextId ? void 0 : pending;
    scheduler.transactionScopedScheduler.schedule({
      contextId,
      jobId,
      dependencies: dependentBuilders,
      run: () => this.executeGraphRun(contextId, pendingToPass)
    });
  }
  /**
   * Clears pending graph run state for a specific context.
   * Called when the scheduler clears a context (e.g., transaction rollback/abort).
   */
  clearPendingGraphRun(contextId) {
    this.pendingGraphRuns.delete(contextId);
  }
  /**
   * Returns true if this builder has a pending graph run for the given context.
   */
  hasPendingGraphRun(contextId) {
    return this.pendingGraphRuns.has(contextId);
  }
  /**
   * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.
   * Clears the pending state BEFORE execution so that any re-schedules during the run
   * create fresh state and don't interfere with the current execution.
   * Uses instance sync state - if sync has ended, gracefully returns without executing.
   *
   * @param contextId - Optional context ID to look up pending state
   * @param pendingParam - For immediate execution (no context), pending state is passed directly
   */
  executeGraphRun(contextId, pendingParam) {
    const pending = pendingParam ?? (contextId ? this.pendingGraphRuns.get(contextId) : void 0);
    if (contextId) {
      this.pendingGraphRuns.delete(contextId);
    }
    if (!pending) {
      return;
    }
    if (!this.currentSyncConfig || !this.currentSyncState) {
      return;
    }
    this.incrementRunCount();
    const combinedLoader = () => {
      let allDone = true;
      let firstError;
      pending.loadCallbacks.forEach((loader) => {
        try {
          allDone = loader() && allDone;
        } catch (error) {
          allDone = false;
          firstError ??= error;
        }
      });
      if (firstError) {
        throw firstError;
      }
      return allDone;
    };
    this.maybeRunGraph(combinedLoader);
  }
  getSyncConfig() {
    return {
      rowUpdateMode: `full`,
      sync: this.syncFn.bind(this)
    };
  }
  incrementRunCount() {
    this.runCount++;
  }
  getRunCount() {
    return this.runCount;
  }
  syncFn(config) {
    this.liveQueryCollection = config.collection;
    this.currentSyncConfig = config;
    const syncState = {
      messagesCount: 0,
      subscribedToAllCollections: false,
      unsubscribeCallbacks: /* @__PURE__ */ new Set()
    };
    const fullSyncState = this.extendPipelineWithChangeProcessing(
      config,
      syncState
    );
    this.currentSyncState = fullSyncState;
    this.unsubscribeFromSchedulerClears = scheduler.transactionScopedScheduler.onClear(
      (contextId) => {
        this.clearPendingGraphRun(contextId);
      }
    );
    const loadingSubsetUnsubscribe = config.collection.on(
      `loadingSubset:change`,
      (event) => {
        if (!event.isLoadingSubset) {
          this.updateLiveQueryStatus(config);
        }
      }
    );
    syncState.unsubscribeCallbacks.add(loadingSubsetUnsubscribe);
    const loadSubsetDataCallbacks = this.subscribeToAllCollections(
      config,
      fullSyncState
    );
    this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks);
    this.scheduleGraphRun(loadSubsetDataCallbacks);
    return () => {
      syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
      this.currentSyncConfig = void 0;
      this.currentSyncState = void 0;
      this.pendingGraphRuns.clear();
      this.graphCache = void 0;
      this.inputsCache = void 0;
      this.pipelineCache = void 0;
      this.sourceWhereClausesCache = void 0;
      this.includesCache = void 0;
      this.lazySources.clear();
      this.optimizableOrderByCollections = {};
      this.lazySourcesCallbacks = {};
      Object.keys(this.subscriptions).forEach(
        (key) => delete this.subscriptions[key]
      );
      this.compiledAliasToCollectionId = {};
      this.unsubscribeFromSchedulerClears?.();
      this.unsubscribeFromSchedulerClears = void 0;
    };
  }
  /**
   * Compiles the query pipeline with all declared aliases.
   */
  compileBasePipeline() {
    this.graphCache = new dbIvm.D2();
    this.inputsCache = Object.fromEntries(
      Object.keys(this.collectionByAlias).map((alias) => [
        alias,
        this.graphCache.newInput()
      ])
    );
    const compilation = index.compileQuery(
      this.query,
      this.inputsCache,
      this.collections,
      this.subscriptions,
      this.lazySourcesCallbacks,
      this.lazySources,
      this.optimizableOrderByCollections,
      (windowFn) => {
        this.windowFn = windowFn;
      }
    );
    this.pipelineCache = compilation.pipeline;
    this.sourceWhereClausesCache = compilation.sourceWhereClauses;
    this.compiledAliasToCollectionId = compilation.aliasToCollectionId;
    this.includesCache = compilation.includes;
    const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(
      (alias) => !Object.hasOwn(this.inputsCache, alias)
    );
    if (missingAliases.length > 0) {
      throw new errors.MissingAliasInputsError(missingAliases);
    }
  }
  maybeCompileBasePipeline() {
    if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {
      this.compileBasePipeline();
    }
    return {
      graph: this.graphCache,
      inputs: this.inputsCache,
      pipeline: this.pipelineCache
    };
  }
  extendPipelineWithChangeProcessing(config, syncState) {
    const { begin, commit } = config;
    const { graph, inputs, pipeline } = this.maybeCompileBasePipeline();
    let pendingChanges = /* @__PURE__ */ new Map();
    pipeline.pipe(
      dbIvm.output((data) => {
        const messages = data.getInner();
        syncState.messagesCount += messages.length;
        messages.reduce(accumulateChanges, pendingChanges);
      })
    );
    const includesState = this.setupIncludesOutput(
      this.includesCache,
      syncState
    );
    syncState.flushPendingChanges = () => {
      const hasParentChanges = pendingChanges.size > 0;
      const hasChildChanges = hasPendingIncludesChanges(includesState);
      if (!hasParentChanges && !hasChildChanges) {
        return;
      }
      let changesToApply = pendingChanges;
      if (this.config.getKey) {
        const merged = /* @__PURE__ */ new Map();
        for (const [, changes] of pendingChanges) {
          const customKey = this.config.getKey(changes.value);
          const existing = merged.get(customKey);
          if (existing) {
            existing.inserts += changes.inserts;
            existing.deletes += changes.deletes;
            if (changes.inserts > 0) {
              existing.value = changes.value;
              if (changes.orderByIndex !== void 0) {
                existing.orderByIndex = changes.orderByIndex;
              }
            }
          } else {
            merged.set(customKey, { ...changes });
          }
        }
        changesToApply = merged;
      }
      if (hasParentChanges) {
        begin();
        changesToApply.forEach(this.applyChanges.bind(this, config));
        commit();
      }
      pendingChanges = /* @__PURE__ */ new Map();
      flushIncludesState(
        includesState,
        config.collection,
        this.id,
        hasParentChanges ? changesToApply : null,
        config
      );
    };
    graph.finalize();
    syncState.graph = graph;
    syncState.inputs = inputs;
    syncState.pipeline = pipeline;
    return syncState;
  }
  /**
   * Sets up output callbacks for includes child pipelines.
   * Each includes entry gets its own output callback that accumulates child changes,
   * and a child registry that maps correlation key → child Collection.
   */
  setupIncludesOutput(includesEntries, syncState) {
    if (!includesEntries || includesEntries.length === 0) {
      return [];
    }
    return includesEntries.map((entry) => {
      const state = {
        fieldName: entry.fieldName,
        childCorrelationField: entry.childCorrelationField,
        hasOrderBy: entry.hasOrderBy,
        materialization: entry.materialization,
        scalarField: entry.scalarField,
        childRegistry: /* @__PURE__ */ new Map(),
        pendingChildChanges: /* @__PURE__ */ new Map(),
        correlationToParentKeys: /* @__PURE__ */ new Map()
      };
      entry.pipeline.pipe(
        dbIvm.output((data) => {
          const messages = data.getInner();
          syncState.messagesCount += messages.length;
          for (const [[childKey, tupleData], multiplicity] of messages) {
            const [childResult, _orderByIndex, correlationKey, parentContext] = tupleData;
            const routingKey = computeRoutingKey(correlationKey, parentContext);
            let byChild = state.pendingChildChanges.get(routingKey);
            if (!byChild) {
              byChild = /* @__PURE__ */ new Map();
              state.pendingChildChanges.set(routingKey, byChild);
            }
            const existing = byChild.get(childKey) || {
              deletes: 0,
              inserts: 0,
              value: childResult,
              orderByIndex: _orderByIndex
            };
            if (multiplicity < 0) {
              existing.deletes += Math.abs(multiplicity);
            } else if (multiplicity > 0) {
              existing.inserts += multiplicity;
              existing.value = childResult;
            }
            byChild.set(childKey, existing);
          }
        })
      );
      if (entry.childCompilationResult.includes) {
        state.nestedSetups = setupNestedPipelines(
          entry.childCompilationResult.includes,
          syncState
        );
        state.nestedRoutingIndex = /* @__PURE__ */ new Map();
        state.nestedRoutingReverseIndex = /* @__PURE__ */ new Map();
      }
      return state;
    });
  }
  applyChanges(config, changes, key) {
    const { write, collection } = config;
    const { deletes, inserts, value, orderByIndex } = changes;
    this.resultKeys.set(value, key);
    if (orderByIndex !== void 0) {
      this.orderByIndices.set(value, orderByIndex);
    }
    if (inserts && deletes === 0) {
      write({
        value,
        type: `insert`
      });
    } else if (
      // Insert & update(s) (updates are a delete & insert)
      inserts > deletes || // Just update(s) but the item is already in the collection (so
      // was inserted previously).
      inserts === deletes && collection.has(collection.getKeyFromItem(value))
    ) {
      write({
        value,
        type: `update`
      });
    } else if (deletes > 0) {
      write({
        value,
        type: `delete`
      });
    } else {
      throw new Error(
        `Could not apply changes: ${JSON.stringify(changes)}. This should never happen.`
      );
    }
  }
  /**
   * Handle status changes from source collections
   */
  handleSourceStatusChange(config, collectionId, event) {
    const { status } = event;
    if (status === `error`) {
      this.transitionToError(
        `Source collection '${collectionId}' entered error state`
      );
      return;
    }
    if (status === `cleaned-up`) {
      this.transitionToError(
        `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. Live queries prevent automatic GC, so this was likely a manual cleanup() call.`
      );
      return;
    }
    this.updateLiveQueryStatus(config);
  }
  /**
   * Update the live query status based on source collection statuses
   */
  updateLiveQueryStatus(config) {
    const { markReady } = config;
    if (this.isInErrorState) {
      return;
    }
    const subscribedToAll = this.currentSyncState?.subscribedToAllCollections;
    const allReady = this.allCollectionsReady();
    const isLoading = this.liveQueryCollection?.isLoadingSubset;
    if (subscribedToAll && allReady && !isLoading) {
      markReady();
    }
  }
  /**
   * Transition the live query to error state
   */
  transitionToError(message) {
    this.isInErrorState = true;
    console.error(`[Live Query Error] ${message}`);
    this.liveQueryCollection?._lifecycle.setStatus(`error`);
  }
  allCollectionsReady() {
    return Object.values(this.collections).every(
      (collection) => collection.isReady()
    );
  }
  /**
   * Creates per-alias subscriptions enabling self-join support.
   * Each alias gets its own subscription with independent filters, even for the same collection.
   * Example: `{ employee: col, manager: col }` creates two separate subscriptions.
   */
  subscribeToAllCollections(config, syncState) {
    const compiledAliases = Object.entries(this.compiledAliasToCollectionId);
    if (compiledAliases.length === 0) {
      throw new Error(
        `Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`
      );
    }
    const loaders = compiledAliases.map(([alias, collectionId]) => {
      const collection = this.collectionByAlias[alias] ?? this.collections[collectionId];
      const dependencyBuilder = collectionRegistry.getCollectionBuilder(collection);
      if (dependencyBuilder && dependencyBuilder !== this) {
        this.aliasDependencies[alias] = [dependencyBuilder];
        this.builderDependencies.add(dependencyBuilder);
      } else {
        this.aliasDependencies[alias] = [];
      }
      const collectionSubscriber$1 = new collectionSubscriber.CollectionSubscriber(
        alias,
        collectionId,
        collection,
        this
      );
      const statusUnsubscribe = collection.on(`status:change`, (event) => {
        this.handleSourceStatusChange(config, collectionId, event);
      });
      syncState.unsubscribeCallbacks.add(statusUnsubscribe);
      const subscription = collectionSubscriber$1.subscribe();
      this.subscriptions[alias] = subscription;
      const loadMore = collectionSubscriber$1.loadMoreIfNeeded.bind(
        collectionSubscriber$1,
        subscription
      );
      return loadMore;
    });
    const loadSubsetDataCallbacks = () => {
      loaders.map((loader) => loader());
      return true;
    };
    syncState.subscribedToAllCollections = true;
    return loadSubsetDataCallbacks;
  }
}
function createOrderByComparator(orderByIndices) {
  return (val1, val2) => {
    const index1 = orderByIndices.get(val1);
    const index2 = orderByIndices.get(val2);
    if (index1 && index2) {
      if (index1 < index2) {
        return -1;
      } else if (index1 > index2) {
        return 1;
      } else {
        return 0;
      }
    }
    return 0;
  };
}
function materializesInline(state) {
  return state.materialization !== `collection`;
}
function materializeIncludedValue(state, entry) {
  if (!entry) {
    if (state.materialization === `array`) {
      return [];
    }
    if (state.materialization === `concat`) {
      return ``;
    }
    return void 0;
  }
  if (state.materialization === `collection`) {
    return entry.collection;
  }
  const rows = [...entry.collection.toArray];
  const values = state.scalarField ? rows.map((row) => row?.[state.scalarField]) : rows;
  if (state.materialization === `array`) {
    return values;
  }
  return values.map((value) => String(value ?? ``)).join(``);
}
function setupNestedPipelines(includes, syncState) {
  return includes.map((entry) => {
    const buffer = /* @__PURE__ */ new Map();
    entry.pipeline.pipe(
      dbIvm.output((data) => {
        const messages = data.getInner();
        syncState.messagesCount += messages.length;
        for (const [[childKey, tupleData], multiplicity] of messages) {
          const [childResult, _orderByIndex, correlationKey, parentContext] = tupleData;
          const routingKey = computeRoutingKey(correlationKey, parentContext);
          let byChild = buffer.get(routingKey);
          if (!byChild) {
            byChild = /* @__PURE__ */ new Map();
            buffer.set(routingKey, byChild);
          }
          const existing = byChild.get(childKey) || {
            deletes: 0,
            inserts: 0,
            value: childResult,
            orderByIndex: _orderByIndex
          };
          if (multiplicity < 0) {
            existing.deletes += Math.abs(multiplicity);
          } else if (multiplicity > 0) {
            existing.inserts += multiplicity;
            existing.value = childResult;
          }
          byChild.set(childKey, existing);
        }
      })
    );
    const setup = {
      compilationResult: entry,
      buffer
    };
    if (entry.childCompilationResult.includes) {
      setup.nestedSetups = setupNestedPipelines(
        entry.childCompilationResult.includes,
        syncState
      );
    }
    return setup;
  });
}
function createPerEntryIncludesStates(setups) {
  return setups.map((setup) => {
    const state = {
      fieldName: setup.compilationResult.fieldName,
      childCorrelationField: setup.compilationResult.childCorrelationField,
      hasOrderBy: setup.compilationResult.hasOrderBy,
      materialization: setup.compilationResult.materialization,
      scalarField: setup.compilationResult.scalarField,
      childRegistry: /* @__PURE__ */ new Map(),
      pendingChildChanges: /* @__PURE__ */ new Map(),
      correlationToParentKeys: /* @__PURE__ */ new Map()
    };
    if (setup.nestedSetups) {
      state.nestedSetups = setup.nestedSetups;
      state.nestedRoutingIndex = /* @__PURE__ */ new Map();
      state.nestedRoutingReverseIndex = /* @__PURE__ */ new Map();
    }
    return state;
  });
}
function drainNestedBuffers(state) {
  const dirtyCorrelationKeys = /* @__PURE__ */ new Set();
  if (!state.nestedSetups) return dirtyCorrelationKeys;
  for (let i = 0; i < state.nestedSetups.length; i++) {
    const setup = state.nestedSetups[i];
    const toDelete = [];
    for (const [nestedCorrelationKey, childChanges] of setup.buffer) {
      const parentCorrelationKey = state.nestedRoutingIndex.get(nestedCorrelationKey);
      if (parentCorrelationKey === void 0) {
        continue;
      }
      const entry = state.childRegistry.get(parentCorrelationKey);
      if (!entry || !entry.includesStates) {
        continue;
      }
      const entryState = entry.includesStates[i];
      for (const [childKey, changes] of childChanges) {
        let byChild = entryState.pendingChildChanges.get(nestedCorrelationKey);
        if (!byChild) {
          byChild = /* @__PURE__ */ new Map();
          entryState.pendingChildChanges.set(nestedCorrelationKey, byChild);
        }
        const existing = byChild.get(childKey);
        if (existing) {
          existing.inserts += changes.inserts;
          existing.deletes += changes.deletes;
          if (changes.inserts > 0) {
            existing.value = changes.value;
            if (changes.orderByIndex !== void 0) {
              existing.orderByIndex = changes.orderByIndex;
            }
          }
        } else {
          byChild.set(childKey, { ...changes });
        }
      }
      dirtyCorrelationKeys.add(parentCorrelationKey);
      toDelete.push(nestedCorrelationKey);
    }
    for (const key of toDelete) {
      setup.buffer.delete(key);
    }
  }
  return dirtyCorrelationKeys;
}
function updateRoutingIndex(state, correlationKey, childChanges) {
  if (!state.nestedSetups) return;
  for (const setup of state.nestedSetups) {
    for (const [, change] of childChanges) {
      if (change.inserts > 0) {
        const nestedRouting = change.value[index.INCLUDES_ROUTING]?.[setup.compilationResult.fieldName];
        const nestedCorrelationKey = nestedRouting?.correlationKey;
        const nestedParentContext = nestedRouting?.parentContext ?? null;
        const nestedRoutingKey = computeRoutingKey(
          nestedCorrelationKey,
          nestedParentContext
        );
        if (nestedCorrelationKey != null) {
          state.nestedRoutingIndex.set(nestedRoutingKey, correlationKey);
          let reverseSet = state.nestedRoutingReverseIndex.get(correlationKey);
          if (!reverseSet) {
            reverseSet = /* @__PURE__ */ new Set();
            state.nestedRoutingReverseIndex.set(correlationKey, reverseSet);
          }
          reverseSet.add(nestedRoutingKey);
        }
      } else if (change.deletes > 0 && change.inserts === 0) {
        const nestedRouting2 = change.value[index.INCLUDES_ROUTING]?.[setup.compilationResult.fieldName];
        const nestedCorrelationKey = nestedRouting2?.correlationKey;
        const nestedParentContext2 = nestedRouting2?.parentContext ?? null;
        const nestedRoutingKey = computeRoutingKey(
          nestedCorrelationKey,
          nestedParentContext2
        );
        if (nestedCorrelationKey != null) {
          state.nestedRoutingIndex.delete(nestedRoutingKey);
          const reverseSet = state.nestedRoutingReverseIndex.get(correlationKey);
          if (reverseSet) {
            reverseSet.delete(nestedRoutingKey);
            if (reverseSet.size === 0) {
              state.nestedRoutingReverseIndex.delete(correlationKey);
            }
          }
        }
      }
    }
  }
}
function cleanRoutingIndexOnDelete(state, correlationKey) {
  if (!state.nestedRoutingReverseIndex) return;
  const nestedKeys = state.nestedRoutingReverseIndex.get(correlationKey);
  if (nestedKeys) {
    for (const nestedKey of nestedKeys) {
      state.nestedRoutingIndex.delete(nestedKey);
    }
    state.nestedRoutingReverseIndex.delete(correlationKey);
  }
}
function hasNestedBufferChanges(setups) {
  for (const setup of setups) {
    if (setup.buffer.size > 0) return true;
    if (setup.nestedSetups && hasNestedBufferChanges(setup.nestedSetups))
      return true;
  }
  return false;
}
function computeRoutingKey(correlationKey, parentContext) {
  if (parentContext == null) return correlationKey;
  return JSON.stringify([correlationKey, parentContext]);
}
function createChildCollectionEntry(parentId, fieldName, correlationKey, hasOrderBy, nestedSetups) {
  const resultKeys = /* @__PURE__ */ new WeakMap();
  const orderByIndices = hasOrderBy ? /* @__PURE__ */ new WeakMap() : null;
  let syncMethods = null;
  const compare = orderByIndices ? createOrderByComparator(orderByIndices) : void 0;
  const collection = index$1.createCollection({
    id: `__child-collection:${parentId}-${fieldName}-${dbIvm.serializeValue(correlationKey)}`,
    getKey: (item) => resultKeys.get(item),
    compare,
    sync: {
      rowUpdateMode: `full`,
      sync: (methods) => {
        syncMethods = methods;
        return () => {
          syncMethods = null;
        };
      }
    },
    startSync: true
  });
  const entry = {
    collection,
    get syncMethods() {
      return syncMethods;
    },
    resultKeys,
    orderByIndices
  };
  if (nestedSetups) {
    entry.includesStates = createPerEntryIncludesStates(nestedSetups);
  }
  return entry;
}
function flushIncludesState(includesState, parentCollection, parentId, parentChanges, parentSyncMethods) {
  for (const state of includesState) {
    if (parentChanges) {
      for (const [parentKey, changes] of parentChanges) {
        if (changes.inserts > 0) {
          const parentResult = changes.value;
          const routing = parentResult[index.INCLUDES_ROUTING]?.[state.fieldName];
          const correlationKey = routing?.correlationKey;
          const parentContext = routing?.parentContext ?? null;
          const routingKey = computeRoutingKey(correlationKey, parentContext);
          if (correlationKey != null) {
            if (!state.childRegistry.has(routingKey)) {
              const entry = createChildCollectionEntry(
                parentId,
                state.fieldName,
                routingKey,
                state.hasOrderBy,
                state.nestedSetups
              );
              state.childRegistry.set(routingKey, entry);
            }
            let parentKeys = state.correlationToParentKeys.get(routingKey);
            if (!parentKeys) {
              parentKeys = /* @__PURE__ */ new Set();
              state.correlationToParentKeys.set(routingKey, parentKeys);
            }
            parentKeys.add(parentKey);
            const childValue = materializeIncludedValue(
              state,
              state.childRegistry.get(routingKey)
            );
            parentResult[state.fieldName] = childValue;
            const storedParent = parentCollection.get(parentKey);
            if (storedParent && storedParent !== parentResult) {
              storedParent[state.fieldName] = childValue;
            }
          }
        }
      }
    }
    const affectedCorrelationKeys = materializesInline(state) ? new Set(state.pendingChildChanges.keys()) : null;
    const entriesWithChildChanges = /* @__PURE__ */ new Map();
    if (state.pendingChildChanges.size > 0) {
      for (const [correlationKey, childChanges] of state.pendingChildChanges) {
        let entry = state.childRegistry.get(correlationKey);
        if (!entry) {
          entry = createChildCollectionEntry(
            parentId,
            state.fieldName,
            correlationKey,
            state.hasOrderBy,
            state.nestedSetups
          );
          state.childRegistry.set(correlationKey, entry);
        }
        if (state.materialization === `collection`) {
          attachChildCollectionToParent(
            parentCollection,
            state.fieldName,
            correlationKey,
            state.correlationToParentKeys,
            entry.collection
          );
        }
        if (entry.syncMethods) {
          entry.syncMethods.begin();
          for (const [childKey, change] of childChanges) {
            entry.resultKeys.set(change.value, childKey);
            if (entry.orderByIndices && change.orderByIndex !== void 0) {
              entry.orderByIndices.set(change.value, change.orderByIndex);
            }
            if (change.inserts > 0 && change.deletes === 0) {
              entry.syncMethods.write({ value: change.value, type: `insert` });
            } else if (change.inserts > change.deletes || change.inserts === change.deletes && entry.syncMethods.collection.has(
              entry.syncMethods.collection.getKeyFromItem(change.value)
            )) {
              entry.syncMethods.write({ value: change.value, type: `update` });
            } else if (change.deletes > 0) {
              entry.syncMethods.write({ value: change.value, type: `delete` });
            }
          }
          entry.syncMethods.commit();
        }
        updateRoutingIndex(state, correlationKey, childChanges);
        entriesWithChildChanges.set(correlationKey, { entry, childChanges });
      }
      state.pendingChildChanges.clear();
    }
    const dirtyFromBuffers = drainNestedBuffers(state);
    for (const [, { entry, childChanges }] of entriesWithChildChanges) {
      if (entry.includesStates) {
        flushIncludesState(
          entry.includesStates,
          entry.collection,
          entry.collection.id,
          childChanges,
          entry.syncMethods
        );
      }
    }
    for (const correlationKey of dirtyFromBuffers) {
      if (entriesWithChildChanges.has(correlationKey)) continue;
      const entry = state.childRegistry.get(correlationKey);
      if (entry?.includesStates) {
        flushIncludesState(
          entry.includesStates,
          entry.collection,
          entry.collection.id,
          null,
          entry.syncMethods
        );
      }
    }
    const inlineReEmitKeys = materializesInline(state) ? /* @__PURE__ */ new Set([...affectedCorrelationKeys || [], ...dirtyFromBuffers]) : null;
    if (parentSyncMethods && inlineReEmitKeys && inlineReEmitKeys.size > 0) {
      const events = [];
      for (const correlationKey of inlineReEmitKeys) {
        const parentKeys = state.correlationToParentKeys.get(correlationKey);
        if (!parentKeys) continue;
        const entry = state.childRegistry.get(correlationKey);
        for (const parentKey of parentKeys) {
          const item = parentCollection.get(parentKey);
          if (item) {
            const key = parentSyncMethods.collection.getKeyFromItem(item);
            const previousValue = { ...item };
            item[state.fieldName] = materializeIncludedValue(state, entry);
            events.push({
              type: `update`,
              key,
              value: item,
              previousValue
            });
          }
        }
      }
      if (events.length > 0) {
        const changesManager = parentCollection._changes;
        changesManager.emitEvents(events, true);
      }
    }
    if (parentChanges) {
      for (const [parentKey, changes] of parentChanges) {
        if (changes.deletes > 0 && changes.inserts === 0) {
          const routing = changes.value[index.INCLUDES_ROUTING]?.[state.fieldName];
          const correlationKey = routing?.correlationKey;
          const parentContext = routing?.parentContext ?? null;
          const routingKey = computeRoutingKey(correlationKey, parentContext);
          if (correlationKey != null) {
            const parentKeys = state.correlationToParentKeys.get(routingKey);
            if (parentKeys) {
              parentKeys.delete(parentKey);
              if (parentKeys.size === 0) {
                cleanRoutingIndexOnDelete(state, routingKey);
                state.childRegistry.delete(routingKey);
                state.correlationToParentKeys.delete(routingKey);
              }
            }
          }
        }
      }
    }
  }
  if (parentChanges) {
    for (const [, changes] of parentChanges) {
      delete changes.value[index.INCLUDES_ROUTING];
    }
  }
}
function hasPendingIncludesChanges(states) {
  for (const state of states) {
    if (state.pendingChildChanges.size > 0) return true;
    if (state.nestedSetups && hasNestedBufferChanges(state.nestedSetups))
      return true;
  }
  return false;
}
function attachChildCollectionToParent(parentCollection, fieldName, correlationKey, correlationToParentKeys, childCollection) {
  const parentKeys = correlationToParentKeys.get(correlationKey);
  if (!parentKeys) return;
  for (const parentKey of parentKeys) {
    const item = parentCollection.get(parentKey);
    if (item) {
      item[fieldName] = childCollection;
    }
  }
}
function accumulateChanges(acc, [[key, tupleData], multiplicity]) {
  const [value, orderByIndex] = tupleData;
  const changes = acc.get(key) || {
    deletes: 0,
    inserts: 0,
    value,
    orderByIndex
  };
  if (multiplicity < 0) {
    changes.deletes += Math.abs(multiplicity);
  } else if (multiplicity > 0) {
    changes.inserts += multiplicity;
    changes.value = value;
    if (orderByIndex !== void 0) {
      changes.orderByIndex = orderByIndex;
    }
  }
  acc.set(key, changes);
  return acc;
}
exports.CollectionConfigBuilder = CollectionConfigBuilder;
//# sourceMappingURL=collection-config-builder.cjs.map
