refactor: Replace vendored svelte-persistent-store with shared persisted rune
History now uses the shared persist.svelte helpers, the MacFJA store copy is deleted, and the esserializer dependency it required is dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ed06c07c06
commit
c9cfaa10e7
@@ -60,7 +60,6 @@
|
|||||||
"eslint-plugin-svelte": "^3.19.0",
|
"eslint-plugin-svelte": "^3.19.0",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.3",
|
"eslint-plugin-tailwindcss": "^3.18.3",
|
||||||
"eslint-plugin-unicorn": "^65.0.0",
|
"eslint-plugin-unicorn": "^65.0.0",
|
||||||
"esserializer": "^1.3.11",
|
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
|||||||
Generated
-8
@@ -195,9 +195,6 @@ importers:
|
|||||||
eslint-plugin-unicorn:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^65.0.0
|
specifier: ^65.0.0
|
||||||
version: 65.0.0(eslint@10.4.1(jiti@1.21.7))
|
version: 65.0.0(eslint@10.4.1(jiti@1.21.7))
|
||||||
esserializer:
|
|
||||||
specifier: ^1.3.11
|
|
||||||
version: 1.3.11
|
|
||||||
globals:
|
globals:
|
||||||
specifier: ^17.6.0
|
specifier: ^17.6.0
|
||||||
version: 17.6.0
|
version: 17.6.0
|
||||||
@@ -2007,9 +2004,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
|
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
esserializer@1.3.11:
|
|
||||||
resolution: {integrity: sha512-slnPPuTEaYYHqJOvtBVldmQRlKkJlOCMytd3Il9UNeI9DHlHV5c1wqyUe9f4LZCk69+jZm6zjhwPVNIj8/1I4g==}
|
|
||||||
|
|
||||||
estraverse@5.3.0:
|
estraverse@5.3.0:
|
||||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -5699,8 +5693,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
|
|
||||||
esserializer@1.3.11: {}
|
|
||||||
|
|
||||||
estraverse@5.3.0: {}
|
estraverse@5.3.0: {}
|
||||||
|
|
||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { HistoryEntry, HistoryType, Optional, State } from '$lib/types';
|
import type { HistoryEntry, HistoryType, Optional, State } from '$lib/types';
|
||||||
|
import { persisted, readJSON, type Persisted } from '$lib/util/persist.svelte';
|
||||||
import { inputState } from '$lib/util/state.svelte';
|
import { inputState } from '$lib/util/state.svelte';
|
||||||
import { logEvent } from '$lib/util/stats';
|
import { logEvent } from '$lib/util/stats';
|
||||||
import { generateSlug } from 'random-word-slugs';
|
import { generateSlug } from 'random-word-slugs';
|
||||||
@@ -7,44 +8,6 @@ import { v4 as uuidV4 } from 'uuid';
|
|||||||
const MAX_AUTO_HISTORY_LENGTH = 30;
|
const MAX_AUTO_HISTORY_LENGTH = 30;
|
||||||
const AUTO_SAVE_INTERVAL = 60_000;
|
const AUTO_SAVE_INTERVAL = 60_000;
|
||||||
|
|
||||||
const hasStorage = (): boolean => typeof window !== 'undefined' && !!window.localStorage;
|
|
||||||
|
|
||||||
const readJSON = <T>(key: string, fallback: T): T => {
|
|
||||||
if (!hasStorage()) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const raw = window.localStorage.getItem(key);
|
|
||||||
return raw === null ? fallback : (JSON.parse(raw) as T);
|
|
||||||
} catch {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeJSON = (key: string, value: unknown): void => {
|
|
||||||
if (hasStorage()) {
|
|
||||||
window.localStorage.setItem(key, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Persisted<T> {
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A localStorage-backed reactive value. Reads on init, writes on every set.
|
|
||||||
const persisted = <T>(key: string, initial: T): Persisted<T> => {
|
|
||||||
let value = $state<T>(readJSON(key, initial));
|
|
||||||
return {
|
|
||||||
get value() {
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
set value(next: T) {
|
|
||||||
value = next;
|
|
||||||
writeJSON(key, next);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto = persisted<HistoryEntry[]>('autoHistoryStore', []);
|
const auto = persisted<HistoryEntry[]>('autoHistoryStore', []);
|
||||||
const manual = persisted<HistoryEntry[]>('manualHistoryStore', []);
|
const manual = persisted<HistoryEntry[]>('manualHistoryStore', []);
|
||||||
const mode = persisted<HistoryType>('autoHistoryMode', 'manual');
|
const mode = persisted<HistoryType>('autoHistoryMode', 'manual');
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
// Copied from https://github.com/MacFJA/svelte-persistent-store
|
|
||||||
// # The MIT License (MIT)
|
|
||||||
|
|
||||||
// Copyright (c) 2021 [MacFJA](https://github.com/MacFJA)
|
|
||||||
|
|
||||||
// > Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// > of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// > in the Software without restriction, including without limitation the rights
|
|
||||||
// > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// > copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// > furnished to do so, subject to the following conditions:
|
|
||||||
// >
|
|
||||||
// > The above copyright notice and this permission notice shall be included in
|
|
||||||
// > all copies or substantial portions of the Software.
|
|
||||||
// >
|
|
||||||
// > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// > THE SOFTWARE.
|
|
||||||
|
|
||||||
import ESSerializer from 'esserializer';
|
|
||||||
import type { Writable } from 'svelte/store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disabled warnings about missing/unavailable storages
|
|
||||||
*/
|
|
||||||
export function disableWarnings(): void {
|
|
||||||
noWarnings = true;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* If set to true, no warning will be emitted if the requested Storage is not found.
|
|
||||||
* This option can be useful when the lib is used on a server.
|
|
||||||
*/
|
|
||||||
let noWarnings = false;
|
|
||||||
/**
|
|
||||||
* List of storages where the warning have already been displayed.
|
|
||||||
*/
|
|
||||||
const alreadyWarnFor: string[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a log to indicate that the requested Storage have not been found.
|
|
||||||
* @param {string} storageName
|
|
||||||
*/
|
|
||||||
const warnStorageNotFound = (storageName: string) => {
|
|
||||||
const isProduction = typeof process !== 'undefined' && process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
if (!noWarnings && !alreadyWarnFor.includes(storageName) && !isProduction) {
|
|
||||||
let message = `Unable to find the ${storageName}. No data will be persisted.`;
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
message +=
|
|
||||||
'\n' +
|
|
||||||
'Are you running on a server? Most of storages are not available while running on a server.';
|
|
||||||
}
|
|
||||||
console.warn(message);
|
|
||||||
alreadyWarnFor.push(storageName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const serialize = (value: unknown): string => ESSerializer.serialize(value);
|
|
||||||
const deserialize = (value?: string | null): unknown => {
|
|
||||||
// @TODO: to remove in the next major
|
|
||||||
if (value === 'undefined') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== null && value !== undefined) {
|
|
||||||
try {
|
|
||||||
return ESSerializer.deserialize(value);
|
|
||||||
} catch {
|
|
||||||
// Do nothing
|
|
||||||
// use the value "as is"
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch {
|
|
||||||
// Do nothing
|
|
||||||
// use the value "as is"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A store that keep it's value in time.
|
|
||||||
*/
|
|
||||||
export interface PersistentStore<T> extends Writable<T> {
|
|
||||||
/**
|
|
||||||
* Delete the store value from the persistent storage
|
|
||||||
*/
|
|
||||||
delete(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage interface
|
|
||||||
*/
|
|
||||||
export interface StorageInterface<T> {
|
|
||||||
/**
|
|
||||||
* Get a value from the storage.
|
|
||||||
*
|
|
||||||
* If the value doesn't exists in the storage, `null` should be returned.
|
|
||||||
* This method MUST be synchronous.
|
|
||||||
* @param key The key/name of the value to retrieve
|
|
||||||
*/
|
|
||||||
getValue(key: string): T | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a value in the storage.
|
|
||||||
* @param key The key/name of the value to save
|
|
||||||
* @param value The value to save
|
|
||||||
*/
|
|
||||||
setValue(key: string, value: T): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a value from the storage
|
|
||||||
* @param key The key/name of the value to remove
|
|
||||||
*/
|
|
||||||
deleteValue(key: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelfUpdateStorageInterface<T> extends StorageInterface<T> {
|
|
||||||
/**
|
|
||||||
* Add a listener to the storage values changes
|
|
||||||
* @param {string} key The key to listen
|
|
||||||
* @param {(newValue: T) => void} listener The listener callback function
|
|
||||||
*/
|
|
||||||
addListener(key: string, listener: (newValue: T) => void): void;
|
|
||||||
/**
|
|
||||||
* Remove a listener from the storage values changes
|
|
||||||
* @param {string} key The key that was listened
|
|
||||||
* @param {(newValue: T) => void} listener The listener callback function to remove
|
|
||||||
*/
|
|
||||||
removeListener(key: string, listener: (newValue: T) => void): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a store persistent
|
|
||||||
* @param {Writable<*>} store The store to enhance
|
|
||||||
* @param {StorageInterface} storage The storage to use
|
|
||||||
* @param {string} key The name of the data key
|
|
||||||
*/
|
|
||||||
export function persist<T>(
|
|
||||||
store: Writable<T>,
|
|
||||||
storage: StorageInterface<T>,
|
|
||||||
key: string
|
|
||||||
): PersistentStore<T> {
|
|
||||||
const initialValue = storage.getValue(key);
|
|
||||||
|
|
||||||
if (null !== initialValue) {
|
|
||||||
store.set(initialValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('addListener' in storage) {
|
|
||||||
(storage as SelfUpdateStorageInterface<T>).addListener(key, (newValue) => {
|
|
||||||
store.set(newValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
store.subscribe((value) => {
|
|
||||||
storage.setValue(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...store,
|
|
||||||
delete() {
|
|
||||||
storage.deleteValue(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBrowserStorage(
|
|
||||||
browserStorage: Storage,
|
|
||||||
listenExternalChanges = false
|
|
||||||
): SelfUpdateStorageInterface<any> {
|
|
||||||
const listeners: { key: string; listener: (newValue: any) => void }[] = [];
|
|
||||||
const listenerFunction = (event: StorageEvent) => {
|
|
||||||
const eventKey = event.key;
|
|
||||||
if (event.storageArea === browserStorage) {
|
|
||||||
for (const { listener } of listeners.filter(({ key }) => key === eventKey)) {
|
|
||||||
listener(deserialize(event.newValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const connect = () => {
|
|
||||||
if (listenExternalChanges && typeof window !== 'undefined' && window.addEventListener) {
|
|
||||||
window.addEventListener('storage', listenerFunction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const disconnect = () => {
|
|
||||||
if (listenExternalChanges && typeof window !== 'undefined' && window.removeEventListener) {
|
|
||||||
window.removeEventListener('storage', listenerFunction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
addListener(key: string, listener: (newValue: any) => void) {
|
|
||||||
listeners.push({ key, listener });
|
|
||||||
if (listeners.length === 1) {
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteValue(key: string) {
|
|
||||||
browserStorage.removeItem(key);
|
|
||||||
},
|
|
||||||
getValue(key: string): any {
|
|
||||||
const value = browserStorage.getItem(key);
|
|
||||||
return deserialize(value);
|
|
||||||
},
|
|
||||||
removeListener(key: string, listener: (newValue: any) => void) {
|
|
||||||
const index = listeners.indexOf({ key, listener });
|
|
||||||
if (index !== -1) {
|
|
||||||
listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
if (listeners.length === 0) {
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setValue(key: string, value: any) {
|
|
||||||
browserStorage.setItem(key, serialize(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage implementation that use the browser local storage
|
|
||||||
* @param listenExternalChanges - Update the store if the localStorage is updated from another page
|
|
||||||
*/
|
|
||||||
export function localStorage<T>(listenExternalChanges = false): StorageInterface<T> {
|
|
||||||
if (typeof window !== 'undefined' && window.localStorage) {
|
|
||||||
return getBrowserStorage(window.localStorage, listenExternalChanges);
|
|
||||||
}
|
|
||||||
warnStorageNotFound('window.localStorage');
|
|
||||||
return noopStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage implementation that do nothing
|
|
||||||
*/
|
|
||||||
export function noopStorage<T>(): StorageInterface<T> {
|
|
||||||
return {
|
|
||||||
getValue(): null {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
deleteValue() {
|
|
||||||
// Do nothing
|
|
||||||
},
|
|
||||||
setValue() {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user