From 99e597d94dd9c8cf901e85ef2e501433e403caf1 Mon Sep 17 00:00:00 2001 From: MareStare Date: Tue, 4 Mar 2025 04:03:14 +0000 Subject: [PATCH] Add autocomplete history store --- assets/js/autocomplete/history/store.ts | 95 +++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 assets/js/autocomplete/history/store.ts diff --git a/assets/js/autocomplete/history/store.ts b/assets/js/autocomplete/history/store.ts new file mode 100644 index 00000000..18cfed02 --- /dev/null +++ b/assets/js/autocomplete/history/store.ts @@ -0,0 +1,95 @@ +import store from '../../utils/store'; +/** + * The root JSON object that contains the history records and is persisted to disk. + */ +interface History { + /** + * Used to track the version of the schema layout just in case we do any + * breaking changes to this schema so that we can properly migrate old + * search history data. It's also used to prevent older versions of + * the frontend code from trying to use the newer incompatible schema they + * know nothing about (extremely improbable, but just in case). + */ + schemaVersion: 1; + + /** + * The list of history records sorted from the last recently used to the oldest unused. + */ + records: string[]; +} + +/** + * History store backend is responsible for parsing and serializing the data + * to/from `localStorage`. It handles versioning of the schema, and transparently + * disables writing to the storage if the schema version is unknown to prevent + * data loss (extremely improbable, but just in case). + */ +export class HistoryStore { + private writable: boolean = true; + private readonly key: string; + + constructor(key: string) { + this.key = key; + } + + read(): string[] { + return this.extractRecords(store.get(this.key)); + } + + write(records: string[]): void { + if (!this.writable) { + return; + } + + const history: History = { + schemaVersion: 1, + records, + }; + + const start = performance.now(); + store.set(this.key, history); + + const end = performance.now(); + console.debug( + `Writing ${records.length} history records to the localStorage took ${end - start}ms. ` + + `Records: ${records.length}`, + ); + } + + /** + * Extracts the records from the history. To do this, we first need to migrate + * the history object to the latest schema version if necessary. + */ + private extractRecords(history: History | null): string[] { + // `null` here means we are starting from the initial state (empty list of records). + if (history === null) { + return []; + } + + // We have only one version at the time of this writing, so we don't need + // to do any migration yet. Hopefully we never need to do a breaking change + // and this stays at version `1` forever. + const latestSchemaVersion = 1; + + switch (history.schemaVersion) { + case latestSchemaVersion: + return history.records; + default: + // It's very unlikely that we ever hit this branch. + console.warn( + `Unknown search history schema version: '${history.schemaVersion}'. ` + + `This frontend code was built with the maximum supported schema version ` + + `'${latestSchemaVersion}'. The search history will be disabled for this ` + + `session to prevent potential history data loss. The cause of the version ` + + `mismatch may be that a newer version of the frontend code is running in a ` + + `separate tab, or you were mistakenly served with an older version of the ` + + `frontend code.`, + ); + + // Disallow writing to the storage to prevent data loss. + this.writable = false; + + return []; + } + } +}