~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
125 lines · 3.6 KB Raw History
// The streamed writer against a real filesystem: bytes staged in `.tmp`,
// filed under the CID at commit, gone on abort.
import { mkdtemp, readdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createFsBlobs } from '../src/index.js';

const DID = 'did:plc:writer';
const CID = 'bafkreidemowriter';

/** @type {string} */
let baseDir;
/** @type {ReturnType<typeof createFsBlobs>} */
let blobs;

beforeEach(async () => {
  baseDir = await mkdtemp(join(tmpdir(), 'pdsjs-blobs-'));
  blobs = createFsBlobs(baseDir);
});

afterEach(async () => {
  await rm(baseDir, { recursive: true, force: true });
});

/**
 * @returns {Promise<string[]>}
 */
async function tempEntries() {
  try {
    return await readdir(join(baseDir, DID, '.tmp'));
  } catch {
    return [];
  }
}

describe('createWriter', () => {
  it('stages chunks and files them under the cid', async () => {
    const writer = await /** @type {any} */ (blobs).createWriter(DID);
    await writer.write(new Uint8Array([1, 2, 3]));
    await writer.write(new Uint8Array([4, 5]));
    await writer.commit(CID, 'video/mp4');

    const stored = await blobs.get(DID, CID);
    expect(stored?.mimeType).toBe('video/mp4');
    expect(stored?.data).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
    expect(await tempEntries()).toEqual([]);
  });

  it('stores an empty blob', async () => {
    const writer = await /** @type {any} */ (blobs).createWriter(DID);
    await writer.commit(CID, 'application/octet-stream');

    const stored = await blobs.get(DID, CID);
    expect(stored?.data).toEqual(new Uint8Array(0));
  });

  it('leaves nothing behind on abort', async () => {
    const writer = await /** @type {any} */ (blobs).createWriter(DID);
    await writer.write(new Uint8Array([1, 2, 3]));
    await writer.abort();

    expect(await blobs.get(DID, CID)).toBe(null);
    expect(await tempEntries()).toEqual([]);
  });

  it('a committed blob deletes like any other', async () => {
    const writer = await /** @type {any} */ (blobs).createWriter(DID);
    await writer.write(new Uint8Array([9]));
    await writer.commit(CID, 'image/png');

    await blobs.delete(DID, CID);
    expect(await blobs.get(DID, CID)).toBe(null);
  });
});

/**
 * @param {ReadableStream<Uint8Array>} stream
 * @returns {Promise<Uint8Array>}
 */
async function drain(stream) {
  const reader = stream.getReader();
  /** @type {Uint8Array[]} */
  const chunks = [];
  let size = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    size += value.length;
  }
  const out = new Uint8Array(size);
  let at = 0;
  for (const chunk of chunks) {
    out.set(chunk, at);
    at += chunk.length;
  }
  return out;
}

describe('getStream', () => {
  const DATA = new Uint8Array([10, 11, 12, 13, 14, 15]);

  it('streams the whole blob with its type', async () => {
    await blobs.put(DID, CID, DATA, 'video/mp4');
    const found = await /** @type {any} */ (blobs).getStream(DID, CID);

    expect(found.mimeType).toBe('video/mp4');
    expect(await drain(found.body)).toEqual(DATA);
  });

  it('streams an inclusive byte range', async () => {
    await blobs.put(DID, CID, DATA, 'video/mp4');
    const found = await /** @type {any} */ (blobs).getStream(DID, CID, {
      start: 1,
      end: 3,
    });

    expect(await drain(found.body)).toEqual(new Uint8Array([11, 12, 13]));
  });

  it('answers null for a blob it does not hold', async () => {
    expect(await /** @type {any} */ (blobs).getStream(DID, CID)).toBe(null);
  });
});