Skip to the content

Capsid menu

src/r2.ts

65 lines and 6 functions in DrDustinEdwards/d1-dump, at commit e15f966 on 2026-10-23. This file on GitHub.

Functions

The functions of r2.ts
FunctionLinesComplexityCoveredCRAPMutation
putJsonStreamed13 to 54794%WatchCRAP 7Watch77.3%
flush20 to 241100%OkCRAP 1Ok100%
write25 to 343100%OkCRAP 3Ok100%
<anonymous:49>49 to 5110%OkCRAP 2Critical0%
listAllKeys56 to 654100%OkCRAP 4Critical40%
deleteInChunks67 to 702100%OkCRAP 2Watch71.4%

Imports

It imports src/types.ts. Imported by src/dump.ts, src/runs.ts.

Source

import type { R2Like, R2UploadedPartLike } from "./types.js";

// R2 requires every part except the last to be the same size, and at least 5 MiB.
const MULTIPART_PART_BYTES = 8 * 1024 * 1024;
// R2's bulk delete refuses more than 1000 keys in one call rather than truncating.
const R2_DELETE_MAX = 1000;

export const JSON_TYPE = { httpMetadata: { contentType: "application/json" } };

// Writes `head`, then the rows a page at a time, then "]}", as one multipart object.
// The bytes equal JSON.stringify of the whole object, holding at most one part and one
// page in memory.
export async function putJsonStreamed(bucket: R2Like, key: string, head: string, pages: AsyncIterable<unknown[]>): Promise<number> {
  const upload = await bucket.createMultipartUpload(key, JSON_TYPE);
  const parts: R2UploadedPartLike[] = [];
  const encoder = new TextEncoder();
  let part = new Uint8Array(MULTIPART_PART_BYTES);
  let filled = 0;
  let rows = 0;
  const flush = async () => {
    parts.push(await upload.uploadPart(parts.length + 1, part.slice(0, filled)));
    part = new Uint8Array(MULTIPART_PART_BYTES);
    filled = 0;
  };
  const write = async (text: string) => {
    let bytes = encoder.encode(text);
    while (bytes.length > 0) {
      const n = Math.min(bytes.length, MULTIPART_PART_BYTES - filled);
      part.set(bytes.subarray(0, n), filled);
      filled += n;
      bytes = bytes.subarray(n);
      if (filled === MULTIPART_PART_BYTES) await flush();
    }
  };
  try {
    await write(head);
    for await (const page of pages) {
      for (const row of page) {
        await write((rows === 0 ? "" : ",") + JSON.stringify(row));
        rows++;
      }
    }
    await write("]}");
    if (filled > 0 || parts.length === 0) await flush();
    await upload.complete(parts);
    return rows;
  } catch (err) {
    // The abort's own failure is reported with the cause, never in place of it.
    await upload.abort().catch((abortErr: unknown) => {
      throw new AggregateError([err, abortErr], `d1-dump: writing ${key} failed, and so did aborting its upload`);
    });
    throw err;
  }
}

export async function listAllKeys(bucket: R2Like, prefix: string): Promise<string[]> {
  const keys: string[] = [];
  let cursor: string | undefined;
  do {
    const page = await bucket.list({ prefix, cursor });
    for (const obj of page.objects) keys.push(obj.key);
    cursor = page.truncated ? page.cursor : undefined;
  } while (cursor);
  return keys;
}

export async function deleteInChunks(bucket: R2Like, keys: string[]): Promise<number> {
  for (let i = 0; i < keys.length; i += R2_DELETE_MAX) await bucket.delete(keys.slice(i, i + R2_DELETE_MAX));
  return keys.length;
}