chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* The bundle object index, checked against real git: every object the pack
* reader materializes must appear in the index with the extent, size, type
* and delta base a standalone reader needs.
*/
import { execFile } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { parseBundle } from '../src/bundle.js';
import { indexPacks } from '../src/objects.js';
import {
BASE_EXTERNAL,
BASE_NONE,
buildBundleIndex,
decodePackIndex,
encodePackIndex,
} from '../src/pack-index.js';
const execFileAsync = promisify(execFile);
const env = {
.../** @type {Record<string, string>} */ (process.env),
GIT_AUTHOR_NAME: 'Test',
GIT_AUTHOR_EMAIL: 'test@example.com',
GIT_COMMITTER_NAME: 'Test',
GIT_COMMITTER_EMAIL: 'test@example.com',
};
/**
* @param {string} cwd
* @param {string[]} args
*/
async function runGit(cwd, args) {
const { stdout } = await execFileAsync('git', args, { cwd, env });
return stdout.trim();
}
/** @type {string} */
let dir = '';
/** @type {string} */
let repo = '';
/** @type {Uint8Array} */
let full = new Uint8Array();
/** @type {Uint8Array} */
let incremental = new Uint8Array();
beforeAll(async () => {
dir = mkdtempSync(join(tmpdir(), 'pdsjs-pack-index-'));
repo = join(dir, 'repo');
await runGit(dir, ['init', '-b', 'main', repo]);
// Enough repeated content that the pack holds deltas.
const lines = Array.from({ length: 200 }, (_, i) => `line ${i}`).join('\n');
writeFileSync(join(repo, 'a.txt'), `${lines}\n`);
writeFileSync(join(repo, 'b.txt'), `${lines}\nb\n`);
await runGit(repo, ['add', '.']);
await runGit(repo, ['commit', '-m', 'one']);
const first = await runGit(repo, ['rev-parse', 'HEAD']);
await runGit(repo, ['bundle', 'create', join(dir, 'b1.bundle'), 'main']);
writeFileSync(join(repo, 'a.txt'), `${lines}\nchanged\n`);
await runGit(repo, ['add', '.']);
await runGit(repo, ['commit', '-m', 'two']);
await runGit(repo, [
'bundle',
'create',
join(dir, 'b2.bundle'),
`^${first}`,
'main',
]);
full = new Uint8Array(readFileSync(join(dir, 'b1.bundle')));
incremental = new Uint8Array(readFileSync(join(dir, 'b2.bundle')));
});
afterAll(() => {
rmSync(dir, { recursive: true, force: true });
});
describe('buildBundleIndex', () => {
it('lists every object the pack reader materializes', async () => {
const index = await buildBundleIndex(full);
const objects = await indexPacks([parseBundle(full).pack]);
expect(index.entries.length).toBe(objects.size);
for (const entry of index.entries) {
const object = objects.get(entry.sha);
expect(object, entry.sha).toBeDefined();
expect(entry.type).toBe(object?.type);
expect(entry.size).toBe(object?.data.length);
}
});
it('reports the pack start and ascending entry extents', async () => {
const index = await buildBundleIndex(full);
const { pack } = parseBundle(full);
expect(index.packStart).toBe(full.length - pack.length);
// The first entry follows the 12-byte pack header; extents ascend and
// the last one ends before the 20-byte pack trailer.
expect(index.entries[0].offset).toBe(12);
for (let i = 1; i < index.entries.length; i++) {
expect(index.entries[i].offset).toBeGreaterThan(
index.entries[i - 1].offset,
);
}
const last = index.entries[index.entries.length - 1];
expect(last.offset).toBeLessThan(pack.length - 20);
});
it('points every in-pack delta base at a listed entry', async () => {
const index = await buildBundleIndex(full);
const offsets = new Set(index.entries.map((entry) => entry.offset));
let deltas = 0;
for (const entry of index.entries) {
if (entry.base === BASE_NONE) continue;
deltas++;
if (entry.base !== BASE_EXTERNAL) {
expect(offsets.has(entry.base), `base of ${entry.sha}`).toBe(true);
}
}
expect(deltas).toBeGreaterThan(0);
});
it('marks a cross-bundle base external and names its target', async () => {
// The incremental bundle rewrites a.txt, so its pack deltas against
// prerequisite objects it does not contain. Building its index needs
// those bases, which come from the chain's earlier pack here.
const prior = await indexPacks([parseBundle(full).pack]);
const index = await buildBundleIndex(incremental, async (sha) => {
return prior.get(sha) ?? null;
});
const offsets = new Set(index.entries.map((entry) => entry.offset));
let external = 0;
for (const entry of index.entries) {
if (entry.base === BASE_NONE) continue;
if (entry.base === BASE_EXTERNAL) {
external++;
continue;
}
expect(offsets.has(entry.base), `base of ${entry.sha}`).toBe(true);
}
expect(external).toBeGreaterThan(0);
});
it('round-trips through encode and decode', async () => {
const index = await buildBundleIndex(full);
const decoded = decodePackIndex(encodePackIndex(index));
expect(decoded).toEqual(index);
});
it('rejects bytes that are not an index', () => {
expect(() => decodePackIndex(new Uint8Array([1, 2, 3]))).toThrow(
/not a pack index/,
);
expect(() => decodePackIndex(full.subarray(0, 64))).toThrow(
/not a pack index/,
);
});
});
describe('indexPacks retention', () => {
/**
* Every entry the walk names, in order, whatever it keeps.
* @param {object} [options]
*/
async function walk(options) {
/** @type {Array<{sha: string, type: string, size: number}>} */
const seen = [];
const kept = await indexPacks(
[parseBundle(full).pack],
(sha, meta) => seen.push({ sha, type: meta.type, size: meta.size }),
undefined,
options,
);
return { seen, kept };
}
it('names the same objects whatever it retains', async () => {
const all = await walk({ retain: 'all' });
const none = await walk({ retain: 'none' });
expect(none.seen).toEqual(all.seen);
expect(none.kept.size).toBe(0);
expect(all.kept.size).toBe(all.seen.length);
});
it('keeps the commit graph alone for a history walk', async () => {
const { seen, kept } = await walk({ retain: 'commits' });
const history = seen.filter(
(entry) => entry.type === 'commit' || entry.type === 'tag',
);
expect(history.length).toBeGreaterThan(0);
expect(kept.size).toBe(history.length);
for (const entry of history) {
expect(kept.get(entry.sha)?.type, entry.sha).toBe(entry.type);
}
});
it('builds an evicted delta base again from the pack', async () => {
// A one-byte budget keeps no base between entries, so every delta walks
// its chain back down to a whole object.
const all = await walk({ retain: 'all' });
const starved = await walk({ retain: 'none', cacheBytes: 1 });
expect(starved.seen).toEqual(all.seen);
});
it('resolves a cross-bundle base under a starved cache', async () => {
const prior = await indexPacks([parseBundle(full).pack]);
const index = await buildBundleIndex(incremental, async (sha) => {
return prior.get(sha) ?? null;
});
/** @type {string[]} */
const seen = [];
await indexPacks(
[parseBundle(incremental).pack],
(sha) => seen.push(sha),
async (sha) => prior.get(sha) ?? null,
{ retain: 'none', cacheBytes: 1 },
);
expect(seen).toEqual(index.entries.map((entry) => entry.sha));
});
});