├── .gitignore ├── LICENSE ├── README.md └── persistentStore.ts /.gitignore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # persistent-svelte-store 2 | 3 | ## Description 4 | A generic persistent writable store, built from scratch in TypeScript according to the Svelte store contract. Store value is stored in `localStorage` as a JSON string, but this is transparent to user. The store is reactive across tabs or windows using the Broadcast Channel API. 5 | 6 | ## Example 7 | Create a store and supply a type 8 | >`stores.ts` 9 | >```typescript 10 | > import { persistentWritable } from "./persistentStore"; 11 | > export const store = persistentWritable("storeKey", {}); 12 | Use like any writable store 13 | >`App.svelte` 14 | >```typescript 15 | > 20 | 21 | ## Sources 22 | To learn more about Svelte stores see the [Svelte Docs](https://svelte.dev/docs). 23 | 24 | ## Usage 25 | Available for use freely under the [Unlicense License](https://unlicense.org/). 26 | -------------------------------------------------------------------------------- /persistentStore.ts: -------------------------------------------------------------------------------- 1 | /** -------------------------- persistentStore.ts -------------------------- */ 2 | 3 | export type PersistentWritable = { 4 | subscribe: (subscription: (value: T) => void) => () => void; 5 | set: (value: T) => void; 6 | update: (update_func: (curr: T) => T) => void; 7 | }; 8 | 9 | /** A generic persistent store according to the Svelte store contract 10 | * 11 | * @example 12 | * import { persistentWritable } from "./persistentStore"; 13 | * export const store = persistentWritable("storeKey", {}); 14 | * 15 | * $store = { id: 1 }; 16 | * console.log($store.id); 17 | * 18 | * @template T - Should be a type JSON.stringify can process 19 | * @param {string} storeKey - A unique key in localStorage for the store. 20 | * This will also be the channel name in Broadcast API. 21 | * @param {T} initialValue - Initial value of store 22 | * @returns {PersistentWritable} - A persistent writable store 23 | */ 24 | export const persistentWritable = (storeKey: string, initialValue: T): PersistentWritable => { 25 | let subscriptions: ((value: T) => void)[] = []; 26 | let storeValue: T; 27 | 28 | const safeParse = (jsonString: string) => { 29 | try { 30 | return JSON.parse(jsonString); 31 | } catch (error: unknown) { 32 | if (error instanceof Error) { 33 | console.log(error) 34 | } 35 | } 36 | } 37 | 38 | const safeSetItem = (key: string, value: T) => { 39 | try { 40 | localStorage.setItem(key, JSON.stringify(value)); 41 | } catch (error) { 42 | if (error instanceof Error) { 43 | console.log(error); 44 | } 45 | } 46 | } 47 | 48 | const safeGetItem = (key: string) => { 49 | try { 50 | return localStorage.getItem(key); 51 | } catch (error) { 52 | if (error instanceof Error) { 53 | console.log(error); 54 | } 55 | } 56 | } 57 | 58 | let currentStoreString = safeGetItem(storeKey); 59 | if (currentStoreString === null || currentStoreString === undefined) { 60 | storeValue = initialValue; 61 | safeSetItem(storeKey, storeValue); 62 | } else { 63 | storeValue = safeParse(safeGetItem(storeKey)); 64 | } 65 | 66 | let storeChannel = new BroadcastChannel(storeKey); 67 | storeChannel.onmessage = event => { 68 | storeValue = safeParse(safeGetItem(storeKey)); 69 | if (event.data === storeKey) { 70 | subscriptions.forEach(subscriptions => subscriptions(storeValue)); 71 | } 72 | } 73 | 74 | // Subscribes function and returns an unsubscribe function 75 | const subscribe = (subscription: (value: T) => void) => { 76 | subscription(storeValue); 77 | subscriptions = [...subscriptions, subscription]; 78 | 79 | // If subscribers go from 0 to 1 (after dropping to 0 before) recreate channel 80 | if (subscription.length === 1 && storeChannel === null) { 81 | storeChannel = new BroadcastChannel(storeKey); 82 | } 83 | const unsubscribe = () => { 84 | subscriptions = subscriptions.filter(s => s != subscription); 85 | 86 | // If subsccribers go from 1 to 0 close channel 87 | if (storeChannel && subscription.length === 0) { 88 | storeChannel.close(); 89 | storeChannel = null; 90 | } 91 | } 92 | return unsubscribe; 93 | } 94 | 95 | // Sets stringified value in local storage and calls subscriptions 96 | const set = (value: T) => { 97 | storeValue = value; 98 | safeSetItem(storeKey, value); 99 | subscriptions.forEach(subscription => subscription(storeValue)); 100 | 101 | if (storeChannel) { 102 | storeChannel.postMessage(storeKey); 103 | } 104 | } 105 | 106 | // Updates store value according to input function 107 | const update = 108 | (update_func: (curr: T) => T) => set(update_func(storeValue)); 109 | return { subscribe, set, update }; 110 | } 111 | --------------------------------------------------------------------------------