diff --git a/package.json b/package.json index 178a5570..b3227c64 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "eslint-plugin-svelte": "^3.19.0", "eslint-plugin-tailwindcss": "^3.18.3", "eslint-plugin-unicorn": "^65.0.0", - "esserializer": "^1.3.11", "globals": "^17.6.0", "husky": "^9.1.7", "jsdom": "^29.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2de0c6cd..d130501a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,9 +195,6 @@ importers: eslint-plugin-unicorn: specifier: ^65.0.0 version: 65.0.0(eslint@10.4.1(jiti@1.21.7)) - esserializer: - specifier: ^1.3.11 - version: 1.3.11 globals: specifier: ^17.6.0 version: 17.6.0 @@ -2007,9 +2004,6 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} - esserializer@1.3.11: - resolution: {integrity: sha512-slnPPuTEaYYHqJOvtBVldmQRlKkJlOCMytd3Il9UNeI9DHlHV5c1wqyUe9f4LZCk69+jZm6zjhwPVNIj8/1I4g==} - estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -5699,8 +5693,6 @@ snapshots: dependencies: estraverse: 5.3.0 - esserializer@1.3.11: {} - estraverse@5.3.0: {} estree-walker@2.0.2: diff --git a/src/lib/components/History/historyState.svelte.ts b/src/lib/components/History/historyState.svelte.ts index d2dba2af..616ce8ec 100644 --- a/src/lib/components/History/historyState.svelte.ts +++ b/src/lib/components/History/historyState.svelte.ts @@ -1,4 +1,5 @@ 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 { logEvent } from '$lib/util/stats'; import { generateSlug } from 'random-word-slugs'; @@ -7,44 +8,6 @@ import { v4 as uuidV4 } from 'uuid'; const MAX_AUTO_HISTORY_LENGTH = 30; const AUTO_SAVE_INTERVAL = 60_000; -const hasStorage = (): boolean => typeof window !== 'undefined' && !!window.localStorage; - -const readJSON = (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 { - value: T; -} - -// A localStorage-backed reactive value. Reads on init, writes on every set. -const persisted = (key: string, initial: T): Persisted => { - let value = $state(readJSON(key, initial)); - return { - get value() { - return value; - }, - set value(next: T) { - value = next; - writeJSON(key, next); - } - }; -}; - const auto = persisted('autoHistoryStore', []); const manual = persisted('manualHistoryStore', []); const mode = persisted('autoHistoryMode', 'manual'); diff --git a/src/lib/util/persist.ts b/src/lib/util/persist.ts deleted file mode 100644 index be3a3b44..00000000 --- a/src/lib/util/persist.ts +++ /dev/null @@ -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 extends Writable { - /** - * Delete the store value from the persistent storage - */ - delete(): void; -} - -/** - * Storage interface - */ -export interface StorageInterface { - /** - * 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 extends StorageInterface { - /** - * 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( - store: Writable, - storage: StorageInterface, - key: string -): PersistentStore { - const initialValue = storage.getValue(key); - - if (null !== initialValue) { - store.set(initialValue); - } - - if ('addListener' in storage) { - (storage as SelfUpdateStorageInterface).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 { - 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(listenExternalChanges = false): StorageInterface { - if (typeof window !== 'undefined' && window.localStorage) { - return getBrowserStorage(window.localStorage, listenExternalChanges); - } - warnStorageNotFound('window.localStorage'); - return noopStorage(); -} - -/** - * Storage implementation that do nothing - */ -export function noopStorage(): StorageInterface { - return { - getValue(): null { - return null; - }, - deleteValue() { - // Do nothing - }, - setValue() { - // Do nothing - } - }; -}