quak files COLLECTION fetches that collection's files alone (closes #217)
check / check (push) Canceled after 0s

`quak files COLLECTION` used to wait for a full refresh, which on an
empty cache fetched the files of every collection before it listed one.
It now reads the collection list, then calls the new
`Library.refreshCollection()`, which brings that one collection up to
date in the store as a refresh would, and saves it. It sends no request
for the files when the stored copy is current, and it keeps a newer copy
that a refresh stores while it fetches. It leaves the refresh cursor
alone, so the next refresh still fetches every other collection that
changed. `quak files` with no argument still waits for a refresh.

Model: opus-5-5
This commit is contained in:
2026-10-26 00:02:01 +00:00
parent 657266a60e
commit daa47068bc
8 changed files with 331 additions and 84 deletions
+51 -7
View File
@@ -20,9 +20,12 @@
//
// `freshCollections()` is the fresh read of the collection list alone (issue
// #210): one collections request, no collection's files, and nothing stored,
// for a caller such as `quak collections` that needs no more. A caller that
// asks for each refresh it needs can also open the library with
// `refreshOnOpen: false`, so that `open()` starts none.
// for a caller such as `quak collections` that needs no more.
// `refreshCollection()` brings one collection from that list up to date in the
// store (issue #217): it fetches that collection's files alone, as a refresh
// would, for a caller such as `quak files COLLECTION`. A caller that asks for
// each refresh it needs can also open the library with `refreshOnOpen: false`,
// so that `open()` starts none.
//
// A refresh reports through `onProgress` each collection whose files it has
// fetched, so a large account, whose first refresh fetches the files of every
@@ -625,6 +628,39 @@ export class Library {
return collections;
}
// Bring one collection up to date in the store (issue #217), given its
// record from `freshCollections()`. It does for that collection what a
// refresh does for a changed one, and fetches no other collection's files.
// It sends no request when the stored copy's `updationTime` is as new.
// Otherwise it fetches the files changed since the stored copy, or all of
// them when none is stored, then commits and saves them. A refresh already
// running is waited for first.
async refreshCollection(collection: Collection): Promise<void> {
if (this.closed) throw new Error("the library is closed");
await this.cycle?.catch(() => {});
const known = this.store.getCollection(collection.id);
if (known && collection.updationTime <= known.updationTime) return;
const filePage = await this.client.filesSince({
collectionID: collection.id,
collectionKey: collection.key,
sinceTime: known ? known.updationTime : 0,
});
// A refresh that ran during the fetch may have stored a copy as new as
// `collection` or newer. Committing this result would replace it.
const stored = this.store.getCollection(collection.id);
if (stored && collection.updationTime <= stored.updationTime) return;
// The stored cursor stays where it is, so the next refresh still
// fetches the files of every other collection that changed.
await this.commit(
{
collections: [collection],
deleted: [],
cursor: this.store.collectionsSinceTime,
},
[{ collectionID: collection.id, page: filePage }],
);
}
// Back up every in-scope file to `opts.downloadDirectory`, or else the
// library's, each original at its save path, with a durable failure
// ledger (issue #51). Takes the lock in that directory first, unless the
@@ -766,10 +802,7 @@ export class Library {
// Fetch every change since the stored cursor, then commit. All network
// reads happen before any store mutation, so a fetch that throws leaves the
// store untouched and the previous snapshot intact. A collection or file
// that does not decrypt is left out instead, as if deleted, with the reason
// kept in the store: the cursor moves past it, so no later refresh sees it
// again until the server's copy changes.
// store untouched and the previous snapshot intact.
private async refreshOnce(): Promise<void> {
const page = await this.client.collectionsSince({
sinceTime: this.store.collectionsSinceTime,
@@ -802,6 +835,17 @@ export class Library {
});
}
await this.commit(page, filePages);
}
// Commit a collections page and the file pages fetched for its collections
// to the store, then save. A collection or file that does not decrypt is
// left out, as if deleted, with the reason kept in the store, and no later
// refresh sees it again until the server's copy changes.
private async commit(
page: CollectionsPage,
filePages: { collectionID: number; page: FilesPage }[],
): Promise<void> {
// Network work done; commit to the store and persist only if something
// actually changed.
let changed = false;