Skip to content

Commit c450043

Browse files
committed
feat: US-013 - Versioning support
1 parent 0557299 commit c450043

7 files changed

Lines changed: 861 additions & 18 deletions

File tree

packages/core/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,13 +297,17 @@ export type {
297297
DentryStatInfo,
298298
FsMetadataStore,
299299
FsBlockStore,
300+
FsMetadataStoreVersioning,
301+
VersionMeta,
302+
VersionInfo,
303+
RetentionPolicy,
300304
} from "./vfs/types.js";
301305
export { InMemoryMetadataStore } from "./vfs/memory-metadata.js";
302306
export { InMemoryBlockStore } from "./vfs/memory-block-store.js";
303307
export { SqliteMetadataStore } from "./vfs/sqlite-metadata.js";
304308
export type { SqliteMetadataStoreOptions } from "./vfs/sqlite-metadata.js";
305309
export { createChunkedVfs } from "./vfs/chunked-vfs.js";
306-
export type { ChunkedVfsOptions } from "./vfs/chunked-vfs.js";
310+
export type { ChunkedVfsOptions, ChunkedVfsVersioning } from "./vfs/chunked-vfs.js";
307311
export { HostBlockStore } from "./vfs/host-block-store.js";
308312

309313
// VFS conformance test suite.

packages/core/src/test/metadata-store-conformance.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import type {
2424
FsMetadataStore,
25+
FsMetadataStoreVersioning,
2526
InodeMeta,
2627
CreateInodeAttrs,
2728
} from "../vfs/types.js";
@@ -549,5 +550,183 @@ export function defineMetadataStoreTests(
549550
expect(target).toBe("/some/target");
550551
});
551552
});
553+
554+
// ---------------------------------------------------------------
555+
// Versioning (gated)
556+
// ---------------------------------------------------------------
557+
558+
describe.skipIf(!config.capabilities.versioning)("versioning", () => {
559+
function getVersioningStore(): FsMetadataStoreVersioning {
560+
return store as unknown as FsMetadataStoreVersioning;
561+
}
562+
563+
test("createVersion returns incrementing version numbers", async () => {
564+
const ino = await store.createInode(fileAttrs());
565+
await store.updateInode(ino, { size: 100, storageMode: "inline", inlineContent: new Uint8Array(100) });
566+
567+
const vs = getVersioningStore();
568+
const v1 = await vs.createVersion(ino);
569+
const v2 = await vs.createVersion(ino);
570+
const v3 = await vs.createVersion(ino);
571+
expect(v1).toBe(1);
572+
expect(v2).toBe(2);
573+
expect(v3).toBe(3);
574+
});
575+
576+
test("listVersions returns all versions newest first", async () => {
577+
const ino = await store.createInode(fileAttrs());
578+
await store.updateInode(ino, { size: 10, storageMode: "inline", inlineContent: new Uint8Array(10) });
579+
580+
const vs = getVersioningStore();
581+
await vs.createVersion(ino);
582+
await store.updateInode(ino, { size: 20 });
583+
await vs.createVersion(ino);
584+
await store.updateInode(ino, { size: 30 });
585+
await vs.createVersion(ino);
586+
587+
const versions = await vs.listVersions(ino);
588+
expect(versions.length).toBe(3);
589+
expect(versions[0]!.version).toBe(3);
590+
expect(versions[1]!.version).toBe(2);
591+
expect(versions[2]!.version).toBe(1);
592+
expect(versions[0]!.size).toBe(30);
593+
expect(versions[1]!.size).toBe(20);
594+
expect(versions[2]!.size).toBe(10);
595+
});
596+
597+
test("getVersion returns correct metadata", async () => {
598+
const ino = await store.createInode(fileAttrs());
599+
const content = new Uint8Array([1, 2, 3, 4, 5]);
600+
await store.updateInode(ino, { size: 5, storageMode: "inline", inlineContent: content });
601+
602+
const vs = getVersioningStore();
603+
const v = await vs.createVersion(ino);
604+
605+
const meta = await vs.getVersion(ino, v);
606+
expect(meta).not.toBeNull();
607+
expect(meta!.version).toBe(v);
608+
expect(meta!.size).toBe(5);
609+
expect(meta!.storageMode).toBe("inline");
610+
expect(meta!.inlineContent).toEqual(content);
611+
expect(meta!.createdAt).toBeGreaterThan(0);
612+
});
613+
614+
test("getVersion returns null for nonexistent version", async () => {
615+
const ino = await store.createInode(fileAttrs());
616+
617+
const vs = getVersioningStore();
618+
const meta = await vs.getVersion(ino, 999);
619+
expect(meta).toBeNull();
620+
});
621+
622+
test("getVersionChunkMap returns chunk keys at snapshot time", async () => {
623+
const ino = await store.createInode(fileAttrs());
624+
await store.updateInode(ino, { storageMode: "chunked", inlineContent: null, size: 2048 });
625+
await store.setChunkKey(ino, 0, "ino/0/abc");
626+
await store.setChunkKey(ino, 1, "ino/1/def");
627+
628+
const vs = getVersioningStore();
629+
const v = await vs.createVersion(ino);
630+
631+
const chunkMap = await vs.getVersionChunkMap(ino, v);
632+
expect(chunkMap.length).toBe(2);
633+
expect(chunkMap[0]!.chunkIndex).toBe(0);
634+
expect(chunkMap[0]!.key).toBe("ino/0/abc");
635+
expect(chunkMap[1]!.chunkIndex).toBe(1);
636+
expect(chunkMap[1]!.key).toBe("ino/1/def");
637+
});
638+
639+
test("restoreVersion reverts current chunk map", async () => {
640+
const ino = await store.createInode(fileAttrs());
641+
await store.updateInode(ino, { storageMode: "chunked", inlineContent: null, size: 1024 });
642+
await store.setChunkKey(ino, 0, "ino/0/v1key");
643+
644+
const vs = getVersioningStore();
645+
const v1 = await vs.createVersion(ino);
646+
647+
// Write new data.
648+
await store.setChunkKey(ino, 0, "ino/0/v2key");
649+
await store.setChunkKey(ino, 1, "ino/1/v2key");
650+
await store.updateInode(ino, { size: 2048 });
651+
652+
// Restore to v1.
653+
await vs.restoreVersion(ino, v1);
654+
655+
// Verify chunk map is restored.
656+
const chunks = await store.getAllChunkKeys(ino);
657+
expect(chunks.length).toBe(1);
658+
expect(chunks[0]!.key).toBe("ino/0/v1key");
659+
660+
// Verify inode size is restored.
661+
const meta = await store.getInode(ino);
662+
expect(meta!.size).toBe(1024);
663+
});
664+
665+
test("deleteVersions removes specified versions", async () => {
666+
const ino = await store.createInode(fileAttrs());
667+
await store.updateInode(ino, { size: 10, storageMode: "inline", inlineContent: new Uint8Array(10) });
668+
669+
const vs = getVersioningStore();
670+
const v1 = await vs.createVersion(ino);
671+
const v2 = await vs.createVersion(ino);
672+
const v3 = await vs.createVersion(ino);
673+
674+
await vs.deleteVersions(ino, [v1, v2]);
675+
676+
const remaining = await vs.listVersions(ino);
677+
expect(remaining.length).toBe(1);
678+
expect(remaining[0]!.version).toBe(v3);
679+
});
680+
681+
test("deleteVersions returns orphaned block keys", async () => {
682+
const ino = await store.createInode(fileAttrs());
683+
await store.updateInode(ino, { storageMode: "chunked", inlineContent: null, size: 1024 });
684+
await store.setChunkKey(ino, 0, "ino/0/v1key");
685+
686+
const vs = getVersioningStore();
687+
const v1 = await vs.createVersion(ino);
688+
689+
// Write new chunk keys.
690+
await store.setChunkKey(ino, 0, "ino/0/v2key");
691+
692+
// Delete v1. "ino/0/v1key" is no longer referenced by any version or current state.
693+
const orphaned = await vs.deleteVersions(ino, [v1]);
694+
expect(orphaned).toContain("ino/0/v1key");
695+
expect(orphaned).not.toContain("ino/0/v2key");
696+
});
697+
698+
test("deleteVersions does not return keys still referenced by remaining versions", async () => {
699+
const ino = await store.createInode(fileAttrs());
700+
await store.updateInode(ino, { storageMode: "chunked", inlineContent: null, size: 1024 });
701+
await store.setChunkKey(ino, 0, "ino/0/sharedkey");
702+
703+
const vs = getVersioningStore();
704+
const v1 = await vs.createVersion(ino);
705+
const v2 = await vs.createVersion(ino);
706+
707+
// Both v1 and v2 reference "ino/0/sharedkey". Delete v1.
708+
const orphaned = await vs.deleteVersions(ino, [v1]);
709+
expect(orphaned).not.toContain("ino/0/sharedkey");
710+
});
711+
712+
test("createVersion + write new data: old version has old size", async () => {
713+
const ino = await store.createInode(fileAttrs());
714+
await store.updateInode(ino, { size: 10, storageMode: "inline", inlineContent: new Uint8Array(10) });
715+
716+
const vs = getVersioningStore();
717+
const v1 = await vs.createVersion(ino);
718+
719+
// Write new data (larger).
720+
await store.updateInode(ino, { size: 50, inlineContent: new Uint8Array(50) });
721+
722+
// Old version still has old size.
723+
const meta = await vs.getVersion(ino, v1);
724+
expect(meta!.size).toBe(10);
725+
726+
// No new version was created automatically.
727+
const versions = await vs.listVersions(ino);
728+
expect(versions.length).toBe(1);
729+
});
730+
});
552731
});
553732
}

0 commit comments

Comments
 (0)