Lighten library cache reports and heartbeat cleanup
This commit is contained in:
@@ -145,6 +145,79 @@ describe('game-file import lifecycle', () => {
|
||||
});
|
||||
});
|
||||
describe('verified host distribution', () => {
|
||||
it('reuses published metadata across host reports without reading or parsing a large manifest', () => {
|
||||
const version = begin();
|
||||
const manifest = JSON.stringify(Array.from({ length: 20_000 }, (_, i) => ({ ...file, path: `zone/file-${i}.ff` })));
|
||||
const size = file.size * 20_000;
|
||||
state.handle!.prepare("UPDATE game_file_versions SET state='ready',size_bytes=?,manifest=?,manifest_sha256='large-checksum' WHERE id=?")
|
||||
.run(size, manifest, version.id);
|
||||
const otherHost = createHost({ name: 'Other reporting host' });
|
||||
const parse = JSON.parse;
|
||||
const parsing = vi.spyOn(JSON, 'parse').mockImplementation((value, reviver) => {
|
||||
if (value === manifest) throw new Error('Cache reports must not deserialize manifests.');
|
||||
return parse(value, reviver);
|
||||
});
|
||||
const preparing = vi.spyOn(state.handle!, 'prepare');
|
||||
try {
|
||||
for (const id of [hostId, otherHost.id, hostId]) {
|
||||
updateHostLibraryCaches(id, [{ libraryVersionId: version.id, status: 'ready', manifestSha256: 'large-checksum' }]);
|
||||
expect(getHostLibraryCache(id, version.id)).toMatchObject({ status: 'ready', bytesDownloaded: size, bytesTotal: size });
|
||||
}
|
||||
const reads = preparing.mock.calls.map(([sql]) => sql).filter(sql => sql.includes('FROM game_file_versions'));
|
||||
expect(reads).toHaveLength(1);
|
||||
expect(state.handle!.prepare(reads[0]).columns().map(column => column.name)).toEqual(['state', 'sizeBytes', 'manifestSha256']);
|
||||
updateHostLibraryCaches(hostId, [{ libraryVersionId: version.id, status: 'ready', manifestSha256: 'wrong' }]);
|
||||
expect(getHostLibraryCache(hostId, version.id)?.status).toBe('failed');
|
||||
expect(() => updateHostLibraryCaches(hostId, [{ libraryVersionId: version.id, status: 'downloading', bytesDownloaded: size + 1 }]))
|
||||
.toThrow('Invalid library download progress');
|
||||
} finally { parsing.mockRestore(); preparing.mockRestore(); }
|
||||
});
|
||||
|
||||
it('does not cache missing or unpublished versions', () => {
|
||||
updateHostLibraryCaches(hostId, [{ libraryVersionId: 'future', status: 'ready', manifestSha256: 'checksum' }]);
|
||||
const version = begin();
|
||||
state.handle!.prepare("UPDATE game_file_versions SET id='future' WHERE id=?").run(version.id);
|
||||
updateHostLibraryCaches(hostId, [{ libraryVersionId: 'future', status: 'ready', manifestSha256: 'checksum' }]);
|
||||
expect(getHostLibraryCache(hostId, 'future')).toBeNull();
|
||||
state.handle!.prepare("UPDATE game_file_versions SET state='ready',size_bytes=123,manifest_sha256='checksum' WHERE id='future'").run();
|
||||
updateHostLibraryCaches(hostId, [{ libraryVersionId: 'future', status: 'ready', manifestSha256: 'checksum' }]);
|
||||
expect(getHostLibraryCache(hostId, 'future')).toMatchObject({ status: 'ready', bytesTotal: 123 });
|
||||
});
|
||||
|
||||
it('does not retain metadata from a rolled-back publication', () => {
|
||||
const version = begin();
|
||||
expect(() => state.handle!.transaction(() => {
|
||||
state.handle!.prepare("UPDATE game_file_versions SET state='ready',size_bytes=123,manifest_sha256='temporary' WHERE id=?").run(version.id);
|
||||
updateHostLibraryCaches(hostId, [{ libraryVersionId: version.id, status: 'ready', manifestSha256: 'temporary' }]);
|
||||
throw new Error('rollback');
|
||||
})()).toThrow('rollback');
|
||||
updateHostLibraryCaches(hostId, [{ libraryVersionId: version.id, status: 'ready', manifestSha256: 'temporary' }]);
|
||||
expect(getHostLibraryCache(hostId, version.id)).toBeNull();
|
||||
state.handle!.prepare("UPDATE game_file_versions SET state='ready',size_bytes=456,manifest_sha256='published' WHERE id=?").run(version.id);
|
||||
updateHostLibraryCaches(hostId, [{ libraryVersionId: version.id, status: 'ready', manifestSha256: 'published' }]);
|
||||
expect(getHostLibraryCache(hostId, version.id)).toMatchObject({ status: 'ready', bytesTotal: 456 });
|
||||
});
|
||||
|
||||
it('keeps metadata caches separate for each database connection', () => {
|
||||
const seed = () => {
|
||||
listLibraries();
|
||||
legacyLibrary('cache-library', 't6');
|
||||
legacyVersion('cache-version', 'cache-library', '2025-01-01', '2025-01-01');
|
||||
};
|
||||
const report = () => updateHostLibraryCaches(hostId, [{ libraryVersionId: 'cache-version', status: 'ready', manifestSha256: 'checksum' }]);
|
||||
seed(); report();
|
||||
const original = state.handle!;
|
||||
state.handle = new Database(':memory:');
|
||||
try {
|
||||
seed();
|
||||
const preparing = vi.spyOn(state.handle, 'prepare');
|
||||
try {
|
||||
report();
|
||||
expect(preparing.mock.calls.filter(([sql]) => sql.includes('FROM game_file_versions'))).toHaveLength(1);
|
||||
} finally { preparing.mockRestore(); }
|
||||
} finally { state.handle.close(); state.handle = original; }
|
||||
});
|
||||
|
||||
it('deduplicates active downloads and blocks readiness without exact manifest verification', async () => {
|
||||
const version = await published();
|
||||
const operation = queueLibraryDownload(hostId, version.id);
|
||||
|
||||
Reference in New Issue
Block a user