Commit 99c63ff7 authored by Grant's avatar Grant
Browse files

reduce queries for pixel undo calculations

parent 98397326
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -3,7 +3,6 @@
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "tsx -r dotenv/config src/index.ts"
  },
  "keywords": [],
+13 −3
Original line number Diff line number Diff line
@@ -17,10 +17,20 @@ export const query = {
      [end]
    );
  },
  pixelCovering: (x: number, y: number, date: Date) => {
  /** Batch: get all pixel history for a set of (x,y) positions, ordered by createdAt ASC per position.
   *  Used to avoid N+1 queries when resolving covering pixels for deleted pixels. */
  batchPixelHistory: (positions: { x: number; y: number }[]) => {
    if (positions.length === 0) return Promise.resolve({ rows: [] as DBPixel[] });
    const params: number[] = [];
    const placeholders = positions
      .map((p) => {
        params.push(p.x, p.y);
        return `($${params.length - 1}::int, $${params.length}::int)`;
      })
      .join(", ");
    return client.query<DBPixel>(
      `SELECT * FROM "Pixel" WHERE x = $1 AND y = $2 AND "deletedAt" IS NULL AND "createdAt" < $3 ORDER BY "createdAt" DESC LIMIT 1`,
      [x, y, date]
      `SELECT * FROM "Pixel" WHERE (x, y) IN (${placeholders}) ORDER BY x, y, "createdAt" ASC`,
      params
    );
  },
};
+82 −22
Original line number Diff line number Diff line
@@ -158,44 +158,104 @@ const getFilenameForFrameID = (frameId: number) =>
  "0".repeat(Math.max(0, 5 - (frameId + "").length)) + frameId;

const getPixelsAsLog = async (start: number, end: number) => {
  let pixels: Pixel[] = [];
  const dbpixels = (
    await query.pixelsWithinTimeframe(new Date(start), new Date(end))
  ).rows;

  for (const dbpixel of dbpixels) {
    let pixel: Pixel = {
      x: dbpixel.x,
      y: dbpixel.y,
      who: dbpixel.userId,
      date: 0,
      hex: "",
    };
  // Separate created pixels (new placements) from deleted pixels (removals)
  const deletedPixels: DBPixel[] = [];

  for (const dbpixel of dbpixels) {
    if (
      dbpixel.createdAt.getTime() >= start &&
      dbpixel.createdAt.getTime() < end
    ) {
      // this was created within the timeframe
      pixel.hex = dbpixel.color;
      pixel.date = dbpixel.createdAt.getTime();
      // created within timeframe — handled below
    } else if (dbpixel.deletedAt) {
      deletedPixels.push(dbpixel);
    } else {
      if (dbpixel.deletedAt) {
        // this is a deleted pixel that was in the timeframe
      log("received pixel but not deleted or in timeframe", dbpixel);
    }
  }

        const coveredPixel: DBPixel | undefined = (
          await query.pixelCovering(dbpixel.x, dbpixel.y, dbpixel.createdAt)
        ).rows[0];
  // Batch-resolve covering pixels for all deleted pixels in ONE query
  // instead of N individual queries
  let coveringColorCache = new Map<string, string>();
  if (deletedPixels.length > 0) {
    // Collect unique (x, y) positions from all deleted pixels
    const seen = new Set<string>();
    const positions: { x: number; y: number }[] = [];
    for (const dp of deletedPixels) {
      const key = dp.x + "," + dp.y;
      if (!seen.has(key)) {
        seen.add(key);
        positions.push({ x: dp.x, y: dp.y });
      }
    }

        pixel.hex = coveredPixel?.color || "ffffff";
        pixel.date = dbpixel.deletedAt.getTime();
      } else {
        log("received pixel but not deleted or in timeframe", dbpixel);
    // Get ALL pixel history at these positions in one batch query
    const allPixels = (await query.batchPixelHistory(positions)).rows;

    // Group history by (x, y), already ordered by createdAt ASC from SQL
    const historyByPosition = new Map<string, DBPixel[]>();
    for (const p of allPixels) {
      const key = p.x + "," + p.y;
      let list = historyByPosition.get(key);
      if (!list) {
        list = [];
        historyByPosition.set(key, list);
      }
      list.push(p);
    }

    // For each deleted pixel, walk backwards through the position's history
    // to find the pixel that was active just before this one was placed
    for (const dp of deletedPixels) {
      const key = dp.x + "," + dp.y;
      const history = historyByPosition.get(key);
      if (!history) {
        coveringColorCache.set(key + ":" + dp.createdAt.getTime(), "ffffff");
        continue;
      }
      // History is ordered createdAt ASC; walk backwards to find
      // the latest pixel created before dp was placed
      let covering = "ffffff";
      for (let i = history.length - 1; i >= 0; i--) {
        if (history[i].createdAt.getTime() < dp.createdAt.getTime()) {
          covering = history[i].color;
          break;
        }
      }
      coveringColorCache.set(key + ":" + dp.createdAt.getTime(), covering);
    }
  }

    pixels.push(pixel);
  // Build event list preserving original chronological order
  const pixels: Pixel[] = [];
  for (const dbpixel of dbpixels) {
    if (
      dbpixel.createdAt.getTime() >= start &&
      dbpixel.createdAt.getTime() < end
    ) {
      // this was created within the timeframe
      pixels.push({
        x: dbpixel.x,
        y: dbpixel.y,
        who: dbpixel.userId,
        date: dbpixel.createdAt.getTime(),
        hex: dbpixel.color,
      });
    } else if (dbpixel.deletedAt) {
      // this is a deleted pixel that was in the timeframe
      const cacheKey = dbpixel.x + "," + dbpixel.y + ":" + dbpixel.createdAt.getTime();
      pixels.push({
        x: dbpixel.x,
        y: dbpixel.y,
        who: dbpixel.userId,
        date: dbpixel.deletedAt.getTime(),
        hex: coveringColorCache.get(cacheKey) || "ffffff",
      });
    }
  }

  return pixels;