Plain-record library snapshot and change subscription (closes #43)
check / check (push) Successful in 30s
check / check (push) Successful in 30s
Adds a synchronous, key-free snapshot() returning LibrarySnapshot (one PhotoRecord per fileID, deduped, newest first) with edited-name/time precedence (pubMagicMetadata over basic metadata) in milliseconds, plus AlbumRecord (favorites identified by type). subscribe({onChange}) delivers LibraryChange (changed/removed albums and files, refreshedAt) only when a refresh changes something; unsubscribe stops delivery. Built on the existing refresh loop; served from RAM, safe to send over IPC.
Model: opus-4-8
This commit was merged in pull request #62.
This commit is contained in:
@@ -23,6 +23,14 @@ import { join } from "node:path";
|
||||
import envPaths from "env-paths";
|
||||
|
||||
import { MetadataStore } from "./store.js";
|
||||
import {
|
||||
deriveRecords,
|
||||
snapshotFrom,
|
||||
diffRecords,
|
||||
type DerivedRecords,
|
||||
type LibrarySnapshot,
|
||||
type LibraryChange,
|
||||
} from "./records.js";
|
||||
import type { CollectionsPage, FilesPage } from "../client.js";
|
||||
import type { Collection, EnteFile } from "../model/types.js";
|
||||
|
||||
@@ -91,6 +99,12 @@ export class Library {
|
||||
private closed = false;
|
||||
private lastRefreshAt?: number;
|
||||
private lastError?: string;
|
||||
// The plain-record projection as of the last refresh, and the GUI change
|
||||
// subscribers. A refresh that alters the projection notifies each with the
|
||||
// delta; `lastRecords` is kept current every refresh so a subscriber that
|
||||
// joins later diffs against the state its own `snapshot()` already returned.
|
||||
private readonly subscribers = new Set<(change: LibraryChange) => void>();
|
||||
private lastRecords: DerivedRecords;
|
||||
// RAM holds changes disk has not yet accepted (an earlier save failed).
|
||||
// Cleared only when a save actually succeeds; keeps the store trying to
|
||||
// persist and the failure visible in `status()` until then.
|
||||
@@ -112,6 +126,7 @@ export class Library {
|
||||
this.downloadDirectory = args.downloadDirectory;
|
||||
this.intervalMs = args.intervalMs;
|
||||
this.onProgress = args.onProgress;
|
||||
this.lastRecords = this.deriveNow();
|
||||
}
|
||||
|
||||
// Load the cache and start the refresh loop. With an empty cache the first
|
||||
@@ -172,6 +187,28 @@ export class Library {
|
||||
return this.store.getFile(collectionID, fileID);
|
||||
}
|
||||
|
||||
// A synchronous, RAM-only projection of the whole library into plain
|
||||
// records (no keys), the surface the GUI reads across IPC. Photos are
|
||||
// deduplicated to one record per file and ordered newest first.
|
||||
snapshot(): LibrarySnapshot {
|
||||
return snapshotFrom(this.deriveNow(), Date.now());
|
||||
}
|
||||
|
||||
// Deliver a `LibraryChange` whenever a refresh alters the projection. A
|
||||
// refresh that changes nothing delivers nothing. The returned handle's
|
||||
// `unsubscribe` stops delivery.
|
||||
subscribe(args: { onChange: (change: LibraryChange) => void }): {
|
||||
unsubscribe: () => void;
|
||||
} {
|
||||
const { onChange } = args;
|
||||
this.subscribers.add(onChange);
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.subscribers.delete(onChange);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
status(): LibraryStatus {
|
||||
let files = 0;
|
||||
const collections = this.store.listCollections();
|
||||
@@ -292,6 +329,20 @@ export class Library {
|
||||
|
||||
if (changed) this.unsaved = true;
|
||||
|
||||
// Reproject and notify subscribers of the delta. This tracks RAM (what
|
||||
// reads see), so it fires whether or not the save below succeeds; a
|
||||
// save failure surfaces separately through `status().lastError`.
|
||||
// `lastRecords` advances every changed refresh so the next diff is
|
||||
// against current state.
|
||||
if (changed) {
|
||||
const next = this.deriveNow();
|
||||
if (this.subscribers.size > 0) {
|
||||
const change = diffRecords(this.lastRecords, next, Date.now());
|
||||
if (change) this.notify(change);
|
||||
}
|
||||
this.lastRecords = next;
|
||||
}
|
||||
|
||||
// Persist whenever RAM holds changes disk has not accepted — including
|
||||
// changes an earlier cycle staged whose save failed. `unsaved` clears
|
||||
// only once a save lands, so a save failure both stays visible through
|
||||
@@ -304,6 +355,25 @@ export class Library {
|
||||
}
|
||||
}
|
||||
|
||||
// Gather every file membership and project the store into by-id records.
|
||||
private deriveNow(): DerivedRecords {
|
||||
const collections = this.store.listCollections();
|
||||
const files: EnteFile[] = [];
|
||||
for (const c of collections) files.push(...this.store.listFiles(c.id));
|
||||
return deriveRecords(collections, files);
|
||||
}
|
||||
|
||||
private notify(change: LibraryChange): void {
|
||||
for (const onChange of this.subscribers) {
|
||||
// A misbehaving subscriber must not break the loop or its peers.
|
||||
try {
|
||||
onChange(change);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emit(event: RefreshEvent): void {
|
||||
if (!this.onProgress) return;
|
||||
// A misbehaving callback must not break the refresh loop.
|
||||
|
||||
Reference in New Issue
Block a user