aboutsummaryrefslogtreecommitdiffstats
path: root/lib/image.ts
blob: 80bc65c3800e8368e62993d34b8314de3b6b6d1e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { warn } from "$lib/logging";

// Decode + resize an image in a throwaway child process (lib/image-worker.ts)
// so a native decoder crash cannot take the app down. Resolves with the webp
// bytes; rejects with a short message on any failure (bad image, crash,
// timeout). Never import sharp in the main process — that is the whole point.
const WORKER = process.env.IMAGE_WORKER || "/home/bun/app/lib/image-worker.ts";
const TIMEOUT_MS = 20_000;
const MAX_OUTPUT = 20_000_000;

export const convert = async (input: Buffer, width: number): Promise<Buffer> => {
  const proc = Bun.spawn([process.execPath, WORKER, String(width)], {
    stdin: new Blob([input]),
    stdout: "pipe",
    stderr: "pipe",
  });

  let timedOut = false;
  const timer = setTimeout(() => {
    timedOut = true;
    proc.kill("SIGKILL");
  }, TIMEOUT_MS);

  try {
    const [out, errText, code] = await Promise.all([
      new Response(proc.stdout).arrayBuffer(),
      new Response(proc.stderr).text(),
      proc.exited,
    ]);

    if (timedOut) throw new Error("image conversion timed out");

    if (code !== 0 || proc.signalCode) {
      // Exit 2 is the worker's own "sharp rejected this file"; anything else
      // (signal, Bun panic banner, exit 1) is the decoder dying — log loudly,
      // this is exactly the event the isolation exists for.
      const reason = errText.trim().split("\n").filter(Boolean).slice(-1)[0] || "";
      if (code !== 2)
        warn(`IMAGE_WORKER_CRASH code=${code} signal=${proc.signalCode} ${reason.slice(0, 200)}`);
      throw new Error(code === 2 && reason ? reason.slice(0, 200) : "image conversion failed");
    }

    if (!out.byteLength || out.byteLength > MAX_OUTPUT)
      throw new Error("image conversion produced no output");

    return Buffer.from(out);
  } finally {
    clearTimeout(timer);
  }
};