import { Lock } from "@rocicorp/lock";
import { unreachable } from "../../../shared/src/asserts.js";
import { serverToClient } from "../../../zero-schema/src/name-mapper.js";
import { toPrimaryKeyString, toMutationResponseKey, toDesiredQueriesKey, toGotQueriesKey } from "./keys.js";
class PokeHandler {
  #replicachePoke;
  #onPokeError;
  #clientID;
  #lc;
  #receivingPoke = void 0;
  #pokeBuffer = [];
  #pokePlaybackLoopRunning = false;
  #lastScheduledTimestamp = 0;
  // Serializes calls to this.#replicachePoke otherwise we can cause out of
  // order poke errors.
  #pokeLock = new Lock();
  #schema;
  #serverToClient;
  #mutationTracker;
  constructor(replicachePoke, onPokeError, clientID, schema, lc, mutationTracker) {
    this.#replicachePoke = replicachePoke;
    this.#onPokeError = onPokeError;
    this.#clientID = clientID;
    this.#schema = schema;
    this.#serverToClient = serverToClient(schema.tables);
    this.#lc = lc.withContext("PokeHandler");
    this.#mutationTracker = mutationTracker;
  }
  handlePokeStart(pokeStart) {
    if (this.#receivingPoke) {
      this.#handlePokeError(
        `pokeStart ${JSON.stringify(
          pokeStart
        )} while still receiving  ${JSON.stringify(
          this.#receivingPoke.pokeStart
        )} `
      );
      return;
    }
    this.#receivingPoke = {
      pokeStart,
      parts: []
    };
  }
  handlePokePart(pokePart) {
    if (pokePart.pokeID !== this.#receivingPoke?.pokeStart.pokeID) {
      this.#handlePokeError(
        `pokePart for ${pokePart.pokeID}, when receiving ${this.#receivingPoke?.pokeStart.pokeID}`
      );
      return;
    }
    this.#receivingPoke.parts.push(pokePart);
    return pokePart.lastMutationIDChanges?.[this.#clientID];
  }
  handlePokeEnd(pokeEnd) {
    if (pokeEnd.pokeID !== this.#receivingPoke?.pokeStart.pokeID) {
      this.#handlePokeError(
        `pokeEnd for ${pokeEnd.pokeID}, when receiving ${this.#receivingPoke?.pokeStart.pokeID}`
      );
      return;
    }
    if (pokeEnd.cancel) {
      this.#receivingPoke = void 0;
      return;
    }
    this.#pokeBuffer.push({ ...this.#receivingPoke, pokeEnd });
    this.#receivingPoke = void 0;
    if (!this.#pokePlaybackLoopRunning) {
      this.#startPlaybackLoop();
    }
  }
  handleDisconnect() {
    this.#lc.debug?.("clearing due to disconnect");
    this.#clear();
  }
  #startPlaybackLoop() {
    this.#lc.debug?.("starting playback loop");
    this.#pokePlaybackLoopRunning = true;
    setTimeout(this.#scheduledCallback, 0);
  }
  #scheduledCallback = async () => {
    const lc = this.#lc.withContext(
      "scheduledAt",
      Math.floor(performance.now())
    );
    if (this.#pokeBuffer.length === 0) {
      lc.debug?.("stopping playback loop");
      this.#pokePlaybackLoopRunning = false;
      return;
    }
    setTimeout(this.#scheduledCallback, 0);
    const start = performance.now();
    lc.debug?.(
      "scheduled callback fired, processing pokes. Since last callback",
      start - this.#lastScheduledTimestamp
    );
    this.#lastScheduledTimestamp = start;
    await this.#processPokesForFrame(lc);
    lc.debug?.("processing pokes took", performance.now() - start);
  };
  #processPokesForFrame(lc) {
    return this.#pokeLock.withLock(async () => {
      const now = Date.now();
      lc.debug?.("got poke lock at", now);
      lc.debug?.("merging", this.#pokeBuffer.length);
      try {
        const merged = mergePokes(
          this.#pokeBuffer,
          this.#schema,
          this.#serverToClient
        );
        this.#pokeBuffer.length = 0;
        if (merged === void 0) {
          lc.debug?.("frame is empty");
          return;
        }
        const start = performance.now();
        lc.debug?.("poking replicache");
        await this.#replicachePoke(merged);
        lc.debug?.("poking replicache took", performance.now() - start);
        if (!("error" in merged.pullResponse)) {
          const lmid = merged.pullResponse.lastMutationIDChanges[this.#clientID];
          if (lmid !== void 0) {
            this.#mutationTracker.lmidAdvanced(lmid);
          }
        }
      } catch (e) {
        this.#handlePokeError(e);
      }
    });
  }
  #handlePokeError(e) {
    if (String(e).includes("unexpected base cookie for poke")) {
      this.#lc.debug?.("clearing due to", e);
    } else {
      this.#lc.error?.("clearing due to unexpected poke error", e);
    }
    this.#clear();
    this.#onPokeError();
  }
  #clear() {
    this.#receivingPoke = void 0;
    this.#pokeBuffer.length = 0;
  }
}
function mergePokes(pokeBuffer, schema, serverToClient2) {
  if (pokeBuffer.length === 0) {
    return void 0;
  }
  const { baseCookie } = pokeBuffer[0].pokeStart;
  const lastPoke = pokeBuffer[pokeBuffer.length - 1];
  const { cookie } = lastPoke.pokeEnd;
  const mergedPatch = [];
  const mergedLastMutationIDChanges = {};
  const mutationResults = [];
  let prevPokeEnd = void 0;
  for (const pokeAccumulator of pokeBuffer) {
    if (prevPokeEnd && pokeAccumulator.pokeStart.baseCookie && pokeAccumulator.pokeStart.baseCookie > prevPokeEnd.cookie) {
      throw Error(
        `unexpected cookie gap ${JSON.stringify(prevPokeEnd)} ${JSON.stringify(
          pokeAccumulator.pokeStart
        )}`
      );
    }
    prevPokeEnd = pokeAccumulator.pokeEnd;
    for (const pokePart of pokeAccumulator.parts) {
      if (pokePart.lastMutationIDChanges) {
        for (const [clientID, lastMutationID] of Object.entries(
          pokePart.lastMutationIDChanges
        )) {
          mergedLastMutationIDChanges[clientID] = lastMutationID;
        }
      }
      if (pokePart.desiredQueriesPatches) {
        for (const [clientID, queriesPatch] of Object.entries(
          pokePart.desiredQueriesPatches
        )) {
          for (const op of queriesPatch) {
            mergedPatch.push(
              queryPatchOpToReplicachePatchOp(
                op,
                (hash) => toDesiredQueriesKey(clientID, hash)
              )
            );
          }
        }
      }
      if (pokePart.gotQueriesPatch) {
        for (const op of pokePart.gotQueriesPatch) {
          mergedPatch.push(
            queryPatchOpToReplicachePatchOp(op, toGotQueriesKey)
          );
        }
      }
      if (pokePart.rowsPatch) {
        for (const p of pokePart.rowsPatch) {
          mergedPatch.push(
            rowsPatchOpToReplicachePatchOp(p, schema, serverToClient2)
          );
        }
      }
      if (pokePart.mutationsPatch) {
        for (const op of pokePart.mutationsPatch) {
          mergedPatch.push(mutationPatchOpToReplicachePatchOp(op));
        }
      }
    }
  }
  const ret = {
    baseCookie,
    pullResponse: {
      lastMutationIDChanges: mergedLastMutationIDChanges,
      patch: mergedPatch,
      cookie
    }
  };
  if (mutationResults.length > 0) {
    ret.mutationResults = mutationResults;
  }
  return ret;
}
function queryPatchOpToReplicachePatchOp(op, toKey) {
  switch (op.op) {
    case "clear":
      return op;
    case "del":
      return {
        op: "del",
        key: toKey(op.hash)
      };
    case "put":
      return {
        op: "put",
        key: toKey(op.hash),
        value: null
      };
    default:
      unreachable();
  }
}
function mutationPatchOpToReplicachePatchOp(op) {
  switch (op.op) {
    case "put":
      return {
        op: "put",
        key: toMutationResponseKey(op.mutation.id),
        value: op.mutation.result
      };
    case "del":
      return {
        op: "del",
        key: toMutationResponseKey(op.id)
      };
  }
}
function rowsPatchOpToReplicachePatchOp(op, schema, serverToClient2) {
  if (op.op === "clear") {
    return op;
  }
  const tableName = serverToClient2.tableName(op.tableName, op);
  switch (op.op) {
    case "del":
      return {
        op: "del",
        key: toPrimaryKeyString(
          tableName,
          schema.tables[tableName].primaryKey,
          serverToClient2.row(op.tableName, op.id)
        )
      };
    case "put":
      return {
        op: "put",
        key: toPrimaryKeyString(
          tableName,
          schema.tables[tableName].primaryKey,
          serverToClient2.row(op.tableName, op.value)
        ),
        value: serverToClient2.row(op.tableName, op.value)
      };
    case "update":
      return {
        op: "update",
        key: toPrimaryKeyString(
          tableName,
          schema.tables[tableName].primaryKey,
          serverToClient2.row(op.tableName, op.id)
        ),
        merge: op.merge ? serverToClient2.row(op.tableName, op.merge) : void 0,
        constrain: serverToClient2.columns(op.tableName, op.constrain)
      };
    default:
      unreachable();
  }
}
export {
  PokeHandler,
  mergePokes,
  mutationPatchOpToReplicachePatchOp
};
//# sourceMappingURL=zero-poke-handler.js.map
