Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
307f1713c8 |
@@ -73,7 +73,7 @@ if (photo) {
|
|||||||
console.log(`original at ${path}`);
|
console.log(`original at ${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lib.close();
|
await lib.close();
|
||||||
```
|
```
|
||||||
|
|
||||||
The lower-level `Client` (login, session serialization, and the raw
|
The lower-level `Client` (login, session serialization, and the raw
|
||||||
@@ -563,7 +563,10 @@ and pass it. The three pools default to 10 / 5 / 25 (see Request pools below).
|
|||||||
`lib.status()` returns a `LibraryStatus` (collection/file counts, last
|
`lib.status()` returns a `LibraryStatus` (collection/file counts, last
|
||||||
refresh/ML times and errors, originals usage and effective limit, precache
|
refresh/ML times and errors, originals usage and effective limit, precache
|
||||||
progress, and `closed`). `lib.close()` stops the background timer; it is
|
progress, and `closed`). `lib.close()` stops the background timer; it is
|
||||||
idempotent, and an in-flight refresh is left to finish.
|
idempotent, and an in-flight refresh is left to finish. The promise it returns
|
||||||
|
resolves once that refresh, including its cache write, has finished, so the
|
||||||
|
cache directory can then be removed; precache fetches already running are not
|
||||||
|
waited for.
|
||||||
|
|
||||||
### Default reads vs. fresh reads
|
### Default reads vs. fresh reads
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ Tag v1.0.0.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-09-23: Fixed two intermittently failing library tests (issue 90).
|
||||||
|
`Library.close()` now returns a promise that resolves once an in-flight
|
||||||
|
refresh, including its cache write, has finished; the library tests await it,
|
||||||
|
so `afterEach` no longer removes the cache directory while a refresh is still
|
||||||
|
writing into it. The precache test waits for both fills to report "done"
|
||||||
|
instead of for its stub source to be called, which happened before the cache
|
||||||
|
recorded the file.
|
||||||
- 2026-09-23: Made the download deadline an idle deadline (issue 24).
|
- 2026-09-23: Made the download deadline an idle deadline (issue 24).
|
||||||
`downloadTimeoutMs` now aborts a file or thumbnail download only after no
|
`downloadTimeoutMs` now aborts a file or thumbnail download only after no
|
||||||
bytes have arrived for that long, default 60 seconds, instead of bounding the
|
bytes have arrived for that long, default 60 seconds, instead of bounding the
|
||||||
|
|||||||
@@ -560,14 +560,19 @@ export class Library {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop the background timer. Idempotent. An in-flight refresh is left to
|
// Stop the background timer. Idempotent. An in-flight refresh is left to
|
||||||
// finish; it will not schedule another cycle once closed.
|
// finish; it will not schedule another cycle once closed. The returned
|
||||||
close(): void {
|
// promise resolves once that refresh, including its cache write, has
|
||||||
|
// finished, so a caller can then remove the cache directory. A refresh
|
||||||
|
// failure is reported through `status()`, not thrown here. Precache
|
||||||
|
// fetches already running are not waited for.
|
||||||
|
async close(): Promise<void> {
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
this.precache?.close();
|
this.precache?.close();
|
||||||
if (this.timer !== undefined) {
|
if (this.timer !== undefined) {
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this.timer = undefined;
|
this.timer = undefined;
|
||||||
}
|
}
|
||||||
|
await this.cycle?.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleNext(): void {
|
private scheduleNext(): void {
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
* short interval and `vi.waitFor`: a fake clock cannot settle the real
|
* short interval and `vi.waitFor`: a fake clock cannot settle the real
|
||||||
* fsync-and-rename cache write, and empty diffs never write, so the eventual
|
* fsync-and-rename cache write, and empty diffs never write, so the eventual
|
||||||
* state is stable to poll for.
|
* state is stable to poll for.
|
||||||
|
*
|
||||||
|
* A refresh changes RAM before it writes the cache file, so a polled state can
|
||||||
|
* be visible while that write is still running. Every test therefore awaits
|
||||||
|
* `close()`, which waits for the in-flight refresh, before `afterEach` removes
|
||||||
|
* the directory.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
@@ -193,7 +198,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
expect(reloaded.getFile(1, 1001)?.id).toBe(1001);
|
expect(reloaded.getFile(1, 1001)?.id).toBe(1001);
|
||||||
expect(reloaded.collectionsSinceTime).toBe(100);
|
expect(reloaded.collectionsSinceTime).toBe(100);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,7 +229,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
expect(client.collectionsSinceTimes.length).toBe(collectionCalls);
|
expect(client.collectionsSinceTimes.length).toBe(collectionCalls);
|
||||||
expect(client.filesCalls.length).toBe(fileCalls);
|
expect(client.filesCalls.length).toBe(fileCalls);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -271,7 +276,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
{ timeout: 2000, interval: 5 },
|
{ timeout: 2000, interval: 5 },
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,7 +308,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
// never re-fetched.
|
// never re-fetched.
|
||||||
expect(client.filesCalls).toEqual([]);
|
expect(client.filesCalls).toEqual([]);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,7 +362,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
{ timeout: 2000, interval: 5 },
|
{ timeout: 2000, interval: 5 },
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -402,7 +407,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
await new Promise((r) => setTimeout(r, FAST_INTERVAL * 1000 * 4));
|
await new Promise((r) => setTimeout(r, FAST_INTERVAL * 1000 * 4));
|
||||||
expect(saveSpy).toHaveBeenCalledTimes(2);
|
expect(saveSpy).toHaveBeenCalledTimes(2);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
saveSpy.mockRestore();
|
saveSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -462,7 +467,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
{ timeout: 2000, interval: 5 },
|
{ timeout: 2000, interval: 5 },
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -488,7 +493,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -502,9 +507,14 @@ describe("Library.open and background refresh", () => {
|
|||||||
seed.putFile(file(1001, 1, 400));
|
seed.putFile(file(1001, 1, 400));
|
||||||
await seed.save();
|
await seed.save();
|
||||||
|
|
||||||
// The server never answers this run's first refresh.
|
// The server does not answer this run's first refresh until the test
|
||||||
|
// is done with it.
|
||||||
|
let answerFirstFetch: (page: CollectionsPage) => void = () => {};
|
||||||
const client = new MockClient();
|
const client = new MockClient();
|
||||||
client.collectionsSince = () => new Promise<CollectionsPage>(() => {});
|
client.collectionsSince = () =>
|
||||||
|
new Promise<CollectionsPage>((resolve) => {
|
||||||
|
answerFirstFetch = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
// open() must resolve from the cache without blocking on the network,
|
// open() must resolve from the cache without blocking on the network,
|
||||||
// and reads must serve the seeded copy.
|
// and reads must serve the seeded copy.
|
||||||
@@ -517,7 +527,9 @@ describe("Library.open and background refresh", () => {
|
|||||||
expect(lib.status().lastRefreshAt).toBeUndefined();
|
expect(lib.status().lastRefreshAt).toBeUndefined();
|
||||||
expect(lib.status().lastError).toBeUndefined();
|
expect(lib.status().lastError).toBeUndefined();
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
// close() waits for the outstanding refresh, so let it finish.
|
||||||
|
answerFirstFetch({ collections: [], deleted: [], cursor: 500 });
|
||||||
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -564,7 +576,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
expect(lib.listFiles(1).map((f) => f.id)).toEqual([1001]);
|
expect(lib.listFiles(1).map((f) => f.id)).toEqual([1001]);
|
||||||
expect(lib.status().lastRefreshAt).toBeGreaterThan(0);
|
expect(lib.status().lastRefreshAt).toBeGreaterThan(0);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -619,7 +631,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
);
|
);
|
||||||
expect(reloaded.getFile(1, 1001)?.id).toBe(1001);
|
expect(reloaded.getFile(1, 1001)?.id).toBe(1001);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
saveSpy.mockRestore();
|
saveSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -634,8 +646,8 @@ describe("Library.open and background refresh", () => {
|
|||||||
});
|
});
|
||||||
const callsAfterOpen = client.collectionsSinceTimes.length;
|
const callsAfterOpen = client.collectionsSinceTimes.length;
|
||||||
|
|
||||||
lib.close();
|
await lib.close();
|
||||||
lib.close(); // second close must not throw
|
await lib.close(); // second close must not throw
|
||||||
expect(lib.status().closed).toBe(true);
|
expect(lib.status().closed).toBe(true);
|
||||||
|
|
||||||
// No further refreshes fire once closed.
|
// No further refreshes fire once closed.
|
||||||
@@ -659,7 +671,7 @@ describe("Library.open and background refresh", () => {
|
|||||||
expect(lib.cacheDirectory.startsWith(xdg)).toBe(true);
|
expect(lib.cacheDirectory.startsWith(xdg)).toBe(true);
|
||||||
expect(lib.cacheDirectory.endsWith(String(USER_ID))).toBe(true);
|
expect(lib.cacheDirectory.endsWith(String(USER_ID))).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (prev === undefined) delete process.env.XDG_CACHE_HOME;
|
if (prev === undefined) delete process.env.XDG_CACHE_HOME;
|
||||||
|
|||||||
@@ -402,35 +402,47 @@ describe("Precache through Library.open", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it("starts both precaches from open() and reports them in status()", async () => {
|
it("starts both precaches from open() and reports them in status()", async () => {
|
||||||
const thumbFetched = new Set<number>();
|
|
||||||
const origFetched = new Set<number>();
|
|
||||||
const source: ContentSource = {
|
const source: ContentSource = {
|
||||||
original: async ({ file: f, destination }) => {
|
original: async ({ destination }) => {
|
||||||
origFetched.add(f.id);
|
|
||||||
await writeFile(destination, Buffer.alloc(10, 1));
|
await writeFile(destination, Buffer.alloc(10, 1));
|
||||||
return { bytesWritten: 10 };
|
return { bytesWritten: 10 };
|
||||||
},
|
},
|
||||||
thumbnail: async ({ file: f, destination }) => {
|
thumbnail: async ({ destination }) => {
|
||||||
thumbFetched.add(f.id);
|
|
||||||
await writeFile(destination, Buffer.alloc(10, 1));
|
await writeFile(destination, Buffer.alloc(10, 1));
|
||||||
return { bytesWritten: 10 };
|
return { bytesWritten: 10 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// Each fill reports "done" once the cache has recorded its files. The
|
||||||
|
// source returning is not enough: the cache records a file only after
|
||||||
|
// it has checked it on disk.
|
||||||
|
const finished = new Set<string>();
|
||||||
|
let bothFinished!: () => void;
|
||||||
|
const precached = new Promise<void>((r) => (bothFinished = r));
|
||||||
const lib = await Library.open({
|
const lib = await Library.open({
|
||||||
client: new MockClient(),
|
client: new MockClient(),
|
||||||
cacheDirectory: join(root, "cache"),
|
cacheDirectory: join(root, "cache"),
|
||||||
contentSource: source,
|
contentSource: source,
|
||||||
refreshIntervalSeconds: 3600,
|
refreshIntervalSeconds: 3600,
|
||||||
|
onProgress: (e) => {
|
||||||
|
if (
|
||||||
|
e.status === "done" &&
|
||||||
|
(e.operation === "precacheThumbnails" ||
|
||||||
|
e.operation === "precacheOriginals")
|
||||||
|
) {
|
||||||
|
finished.add(e.operation);
|
||||||
|
if (finished.size === 2) bothFinished();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Every file's thumbnail is precached; the favorite (file 3) and the
|
// Every file's thumbnail is precached; the favorite (file 3) and the
|
||||||
// week's files (1, 2) all have their originals precached.
|
// week's files (1, 2) all have their originals precached.
|
||||||
await until(() => thumbFetched.size === 3 && origFetched.size === 3);
|
await precached;
|
||||||
const status = lib.status();
|
const status = lib.status();
|
||||||
expect(status.thumbnailsTotal).toBe(3);
|
expect(status.thumbnailsTotal).toBe(3);
|
||||||
expect(status.thumbnailsCached).toBe(3);
|
expect(status.thumbnailsCached).toBe(3);
|
||||||
expect(status.originalsPinned).toBe(3);
|
expect(status.originalsPinned).toBe(3);
|
||||||
expect(status.originalsCached).toBe(3);
|
expect(status.originalsCached).toBe(3);
|
||||||
lib.close();
|
await lib.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user