├── .prettierrc ├── .gitignore ├── src ├── global.d.ts ├── index.ts ├── alias.ts ├── encryption.ts └── core.ts ├── .prettierignore ├── .npmignore ├── .editorconfig ├── tsconfig.json ├── .docs ├── tsconfig.json ├── README.md ├── How-To │ ├── 04-Disable-Warnings.md │ ├── 01-New-Storage.md │ ├── 02-New-Async-Storage.md │ ├── 05-Missing-Encryption-Behavior.md │ ├── 03-Store-Classes.md │ └── 06-Change-Serialization.md └── Examples │ ├── 04-Encrypted-Storage.md │ ├── 03-Self-Update-Storage.md │ ├── 02-Reuse-Store.md │ ├── 01-Basic.md │ └── 05-Use-Persisted-Store.md ├── tests ├── index.html ├── src │ └── App.svelte └── e2e.ts ├── typedoc.json ├── rollup.test.config.mjs ├── rollup.config.mjs ├── LICENSE.md ├── .github └── workflows │ └── quality.yml ├── .eslintrc.cjs ├── CONTRIBUTING.md ├── package.json ├── README.md └── CHANGELOG.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tests/build/ 3 | /dist/ 4 | /docs/ 5 | /types/ -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const process 2 | declare const window 3 | declare const document 4 | declare const chrome 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /CHANGELOG.md 2 | /CONTRIBUTING.md 3 | /LICENSE.md 4 | /dist 5 | /docs 6 | /node_modules 7 | /types 8 | /package-lock.json 9 | /tests/build 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tests/ 3 | /docs/ 4 | /.docs/ 5 | /src/ 6 | /*.js 7 | /*ignore 8 | /*.json 9 | !/package.json 10 | /CHANGELOG.md 11 | /CONTRIBUTING.md 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 4 11 | 12 | [*.{js,ts}] 13 | indent_size = 2 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/*"], 5 | "exclude": ["docs/*", "dist/*"], 6 | "compilerOptions": { 7 | "emitDeclarationOnly": true, 8 | "declaration": true, 9 | "declarationDir": "types", 10 | "sourceMap": false 11 | }, 12 | "files": ["src/index.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /.docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/*"], 5 | "exclude": ["docs/*", "dist/*"], 6 | "compilerOptions": { 7 | "emitDeclarationOnly": true, 8 | "declaration": true, 9 | "declarationDir": "../types", 10 | "sourceMap": false 11 | }, 12 | "stripInternal": true, 13 | "files": ["../src/index.ts", "../src/global.d.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Svelte app 7 | 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "includes": "./.docs/", 4 | "out": "docs", 5 | "tsconfig": ".docs/tsconfig.json", 6 | "excludeInternal": true, 7 | "excludePrivate": true, 8 | "visibilityFilters": { 9 | "protected": false, 10 | "private": false, 11 | "inherited": true, 12 | "external": true, 13 | "@alpha": false, 14 | "@beta": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rollup.test.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs" 2 | import resolve from "@rollup/plugin-node-resolve" 3 | import sucrase from "@rollup/plugin-sucrase" 4 | import svelte from "rollup-plugin-svelte" 5 | import autoPreprocess from "svelte-preprocess" 6 | 7 | export default { 8 | input: "tests/src/App.svelte", 9 | output: [{ file: "tests/build/app.js", format: "iife", name: "app" }], 10 | plugins: [ 11 | svelte({ 12 | preprocess: autoPreprocess(), 13 | }), 14 | sucrase({ 15 | exclude: ["dist/*"], 16 | include: ["src/*"], 17 | transforms: ["typescript"], 18 | }), 19 | commonjs({}), 20 | resolve({ 21 | extensions: [".mjs", ".js", ".json", ".node", ".ts", ".cjs"], 22 | }), 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /.docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Examples 4 | 5 | - [Basic](Examples/01-Basic.md) 6 | - [Reuse a store](Examples/02-Reuse-Store.md) 7 | - [Self updated Storage](Examples/03-Self-Update-Storage.md) 8 | - [Encrypt persisted data](Examples/04-Encrypted-Storage.md) 9 | - [Usage of persistent store](Examples/05-Use-Persisted-Store.md) 10 | 11 | ## How to 12 | 13 | - [Add new storage](How-To/01-New-Storage.md) 14 | - [Add an asynchronous storage](How-To/02-New-Async-Storage.md) 15 | - [Store a class](How-To/03-Store-Classes.md) 16 | - [Disable warnings](How-To/04-Disable-Warnings.md) 17 | - ~~[Change behavior with missing encryption](How-To/05-Missing-Encryption-Behavior.md)~~ 18 | - [Change the serialization functions](How-To/06-Change-Serialization.md) 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | addSerializableClass, 3 | createChromeStorage, 4 | createCookieStorage, 5 | createLocalStorage, 6 | createNoopStorage, 7 | createSessionStorage, 8 | createIndexedDBStorage, 9 | persist, 10 | disableWarnings, 11 | setSerialization, 12 | } from "./core" 13 | export { 14 | persistBrowserLocal, 15 | persistBrowserSession, 16 | persistCookie, 17 | localWritable, 18 | writable, 19 | sessionWritable, 20 | cookieWritable, 21 | } from "./alias" 22 | export { CHROME_STORAGE_TYPE } from "./core" 23 | export type { PersistentStore, StorageInterface, SelfUpdateStorageInterface } from "./core" 24 | export { createEncryptionStorage, createEncryptedStorage, noEncryptionBehavior, GCMEncryption } from "./encryption" 25 | export type { NO_ENCRYPTION_BEHAVIOR, Encryption } from "./encryption" 26 | -------------------------------------------------------------------------------- /.docs/How-To/04-Disable-Warnings.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Disabling warning about missing storage 3 | order: 4 4 | --- 5 | 6 | # Disabling warning about missing storage 7 | 8 | When using the library both on Client and Server side, you can have warnings messages about missing storage on the server. 9 | 10 | > Unable to find the xxxx. No data will be persisted. 11 | > Are you running on a server? Most of storages are not available while running on a server. 12 | 13 | Those message should only appear once, and only during development. 14 | 15 | But they can still be annoying. To remove those messages, an options is available: 16 | 17 | ```js 18 | import { disableWarnings } from "@macfja/svelte-persistent-store" 19 | disableWarnings() 20 | ``` 21 | 22 | Put this function call before calling the persistent storage or in the init/boot script. 23 | -------------------------------------------------------------------------------- /.docs/How-To/01-New-Storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Create new storage 3 | order: 1 4 | --- 5 | 6 | # Create new storage 7 | 8 | New storages can be added. They need to implement the interface `StorageInterface`. 9 | 10 | This interface expose all method needed by the store to persist data. 11 | 12 | The interface `SelfUpdateStorageInterface` allow to have a storage with value that can change from outside the application. 13 | 14 | ## Storage `StorageInterface` 15 | 16 | The only particularity in this interface, the method `StorageInterface.getValue` **MUST** be synchronous. 17 | 18 | ## Storage `SelfUpdateStorageInterface` 19 | 20 | The method `SelfUpdateStorageInterface.addListener` take as parameter a function that must be call every time the storage is updated (by an external source). 21 | This function take one parameter, the key that have been changed. 22 | The store will call the `StorageInterface.getValue` to get the new value 23 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs" 2 | import resolve from "@rollup/plugin-node-resolve" 3 | import sucrase from "@rollup/plugin-sucrase" 4 | import terser from "@rollup/plugin-terser" 5 | 6 | import packageJson from "./package.json" assert {type: "json"} 7 | 8 | const name = packageJson.name 9 | .replaceAll(/^(@\S+\/)?(svelte-)?(\S+)/g, "$3") 10 | .replaceAll(/^\w/g, (m) => m.toUpperCase()) 11 | .replaceAll(/-\w/g, (m) => m[1].toUpperCase()) 12 | 13 | const config = () => ({ 14 | input: "src/index.ts", 15 | output: [ 16 | { file: packageJson.module, format: "es", name }, 17 | { file: packageJson.main, format: "umd", name }, 18 | ], 19 | plugins: [ 20 | sucrase({ 21 | exclude: ["dist/*"], 22 | include: ["src/*"], 23 | transforms: ["typescript"], 24 | }), 25 | commonjs({}), 26 | resolve({ 27 | extensions: [".mjs", ".js", ".json", ".node", ".ts"], 28 | }), 29 | terser(), 30 | ], 31 | }) 32 | 33 | export default config() 34 | -------------------------------------------------------------------------------- /.docs/Examples/04-Encrypted-Storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Encrypt persisted data 3 | order: 4 4 | --- 5 | 6 | # Encrypt persisted data 7 | 8 | You can encrypt data that are persisted. 9 | 10 | The key is hashed (with the [[GCMEncryption]], hashing is done with an encryption). 11 | The data is encrypted. 12 | 13 | _**NOTE:** The encryption is not well adapted for cookie storage, as the data is encrypted, its size greatly increase, you will rapidly get to the allowed cookie size._ 14 | 15 | ## Examples 16 | 17 | ```html 18 | 25 | ``` 26 | 27 | ## Encryption key 28 | 29 | https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx 30 | -------------------------------------------------------------------------------- /.docs/How-To/02-New-Async-Storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Create a new asynchronous storage 3 | order: 2 4 | --- 5 | 6 | # Create a new asynchronous storage 7 | 8 | There is a workaround (trickery) to work with asynchronous data storage. 9 | (Remember, `StorageInterface.getValue` should synchronously return a value) 10 | 11 | The idea is to use the `SelfUpdateStorageInterface` interface to deliver the value when it finally arrived. 12 | 13 | The `IndexedDBStorage` use this workaround. 14 | 15 | ## Quick example 16 | 17 | ```js 18 | function myStorage(): SelfUpdateStorageInterface { 19 | const listeners: Array<{ key: string, listener: (newValue: T) => void }> = [] 20 | const listenerFunction = (eventKey: string, newValue: T) => { 21 | listeners.filter(({ key }) => key === eventKey).forEach(({ listener }) => listener(newValue)) 22 | } 23 | return { 24 | getValue(key: string): T | null { 25 | readRealStorageWithPromise(key).then((value) => listenerFunction(key, value)) 26 | return null // Tell the store to use current decorated store value 27 | }, 28 | // ... addListener, removeListener, setValue, deleteValue 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2021 [MacFJA](https://github.com/MacFJA) 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Quality tools 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup node with version 16 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Run Lint 23 | run: npm run lint 24 | 25 | test: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Setup node with version 16 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: 16 33 | - name: Install dependencies 34 | run: npm ci 35 | - name: Prepare test 36 | run: npm run pretest 37 | - name: Run tests 38 | uses: DevExpress/testcafe-action@latest 39 | with: 40 | version: "1.18.6" 41 | args: "all tests/e2e.ts --app 'npx sirv tests --port 5000'" 42 | skip-install: true 43 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | es6: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:import/errors", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier", 12 | "plugin:unicorn/recommended", 13 | "plugin:sonarjs/recommended", 14 | ], 15 | parserOptions: { 16 | ecmaVersion: 2019, 17 | sourceType: "module", 18 | }, 19 | rules: { 20 | "linebreak-style": ["error", "unix"], 21 | quotes: ["error", "double"], 22 | semi: ["error", "never"], 23 | "import/export": ["error"], 24 | "import/order": [ 25 | "error", 26 | { 27 | "newlines-between": "always", 28 | alphabetize: { order: "asc", caseInsensitive: true }, 29 | }, 30 | ], 31 | "import/newline-after-import": ["error"], 32 | "import/no-absolute-path": ["error"], 33 | "@typescript-eslint/no-explicit-any": ["off"], 34 | "import/no-unresolved": ["error", { ignore: ["svelte/store"] }], 35 | "unicorn/no-null": ["off"], 36 | "unicorn/no-array-for-each": ["off"], 37 | "unicorn/switch-case-braces": ["off"], 38 | }, 39 | settings: { 40 | "import/resolver": { 41 | node: { 42 | extensions: [".js", ".ts"], 43 | }, 44 | }, 45 | "import/extensions": [".js", ".ts", ".cjs"], 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /.docs/How-To/05-Missing-Encryption-Behavior.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Change behavior with missing encryption 3 | order: 5 4 | --- 5 | 6 | # This article is not applicable anymore. (since v2.2.0) 7 | 8 | --- 9 | 10 | --- 11 | 12 | --- 13 | 14 | # Change the behavior when the encryption is not possible 15 | 16 | The encryption library use the browser native WebCrypto library to work (or NodeJS Crypto library on a server). 17 | 18 | But there is case where the WebCrypto library may not be available, for example on a not secure URL. 19 | 20 | By default, this library will raise an exception. 21 | 22 | But you can change this with this code: 23 | 24 | ```js 25 | import { noEncryptionBehavior, NO_ENCRYPTION_BEHAVIOR } from "@macfja/svelte-persistent-store" 26 | noEncryptionBehavior(NO_ENCRYPTION_BEHAVIOR.NO_ENCRYPTION) 27 | ``` 28 | 29 | Put this function call before calling the persistent storage or in the init/boot script. 30 | 31 | ## List of behavior 32 | 33 | | Name | Numeric value | Comment | 34 | | ---------------------------------------- | ------------- | ----------------------------------------------------------------- | 35 | | [[NO_ENCRYPTION_BEHAVIOR.EXCEPTION]] | `0` | **(default)** Raise an exception | 36 | | [[NO_ENCRYPTION_BEHAVIOR.NO_ENCRYPTION]] | `1` | Use the wrapped Storage as-is. Data will be saved not encrypted | 37 | | [[NO_ENCRYPTION_BEHAVIOR.NO_STORAGE]] | `2` | Don't use any storage, so no not encrypted data will be persisted | 38 | -------------------------------------------------------------------------------- /.docs/How-To/03-Store-Classes.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Persist a JavaScript class 3 | order: 3 4 | --- 5 | 6 | # Persist a JavaScript class 7 | 8 | The storage of classes is a bit more involving that simple object or array. 9 | 10 | Class are complex object that contains functions and logic that do more than holding data. 11 | 12 | To store classes we need to serialize them into a special form that we will be able to reverse, to do so we need to register the class, so we know that we have something to do with it. 13 | 14 | ## Example 15 | 16 | ```html 17 | 47 | 48 | The current name is: {$classStore.getName()} 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /.docs/Examples/03-Self-Update-Storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Storage that can update themselves 3 | order: 3 4 | --- 5 | 6 | # Storage that can update themselves 7 | 8 | The **createSessionStorage** and **createLocalStorage** are able to listen changes made externally to the current Window. 9 | 10 | _**NOTE:** Change that are made from another browser window (browser tab) from the same website domain and same browser **ONLY**. 11 | Change made inside the same application instance (i.e. in another page of an SPA) are NOT considered as external changes!_ 12 | 13 | To activate this feature, just add `true` as the parameter of the `createLocalStorage` and `createSessionStorage`. 14 | 15 | ## Examples 16 | 17 | ### Local Storage 18 | 19 | ```html 20 | 27 | ``` 28 | 29 | If the another Window from the (same) browser change the value of the _createLocalStorage_ (from a Svelte application or not), the store will be updated. 30 | 31 | ### Session Storage 32 | 33 | ```html 34 | 41 | ``` 42 | 43 | If the another Window from the (same) browser change the value of the _createSessionStorage_ (from a Svelte application or not), the store will be updated. 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Reporting and improving 4 | 5 | ### Did you find a bug? 6 | 7 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/MacFJA/svelte-persistent-store/issues). 8 | 9 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/MacFJA/svelte-persistent-store/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible 10 | 11 | ### Did you write a patch that fixes a bug? 12 | 13 | * Open a new GitHub pull request with the patch. 14 | 15 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 16 | 17 | ### Do you have an idea to improve the application? 18 | 19 | * **Ensure the suggestion was not already ask** by searching on GitHub under [Issues](https://github.com/MacFJA/svelte-persistent-store/issues). 20 | 21 | * If you're unable to find an open issue about your feature, [open a new one](https://github.com/MacFJA/svelte-persistent-store/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible 22 | 23 | ### Do you want to contribute to the application documentation? 24 | 25 | * **Ensure the documentation improvement was not already submitted** by searching on GitHub under [Issues](https://github.com/MacFJA/svelte-persistent-store/issues). 26 | 27 | * If you're unable to find an open issue addressing this, clone the wiki git on your computer 28 | 29 | * [Open a new issue](https://github.com/MacFJA/svelte-persistent-store/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible and the patch for the documentation 30 | 31 | ## Coding conventions 32 | 33 | Check your code by running the command: 34 | ```sh 35 | npm run lint 36 | npm run test 37 | ``` 38 | The command will output any information worth knowing. No error should be left. 39 | 40 | ---- 41 | 42 | Thanks! -------------------------------------------------------------------------------- /.docs/How-To/06-Change-Serialization.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Change the serialization functions 3 | order: 6 4 | --- 5 | 6 | # Change the serialization functions 7 | 8 | The library allow different serialization functions to use to transform input data into string before saving then in the storage. 9 | 10 | - The default serialization library since version **`2.0.0`** is [`@macfja/serializer`](https://www.npmjs.com/package/@macfja/serializer). 11 | - In the version **`1.3.0`** the serializer was [`esserializer`](https://www.npmjs.com/package/esserializer). 12 | - In versions before **`1.3.0`** the serializer was the [Native JSON serialization](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON). 13 | 14 | ## Serialization functions signature 15 | 16 | To change the serialization, you need to use `setSerialization` from `@macfja/svelte-persistent-store`. 17 | 18 | ```typescript 19 | declare function setSerialization( 20 | serializer: (data: any) => string, 21 | deserializer: (input: string) => any, 22 | addSerializableClass?: (classConstructor: FunctionConstructor) => void 23 | ): void 24 | ``` 25 | 26 | - The `serializer` parameter **MUST** be a function that take one parameter which is the data to transform, and **MUST** return a string. 27 | - The `deserializer` parameter **MUST** be a function that take one parameter which is the serialized data to revert, and **MUST** return tha original data. 28 | - The `addSerializableClass` parameter is optional. It's a function that take one parameter which is a class. 29 | 30 | ## Examples 31 | 32 | ### Using JSON for serialization 33 | 34 | ```typescript 35 | import { setSerialization } from "@macfja/svelte-persistent-store" 36 | 37 | setSerialization(JSON.stringify, JSON.parse) 38 | ``` 39 | 40 | ### Using `esserializer` for serialization 41 | 42 | ```typescript 43 | import { setSerialization } from "@macfja/svelte-persistent-store" 44 | import ESSerializer from "esserializer" 45 | 46 | const allowed = [] 47 | setSerialization( 48 | ESSerializer.serialize, 49 | (data) => ESSerializer.deserialize(data, allowed), 50 | (aClass) => allowed.push(aClass) 51 | ) 52 | ``` 53 | 54 | ### Using `esserializer` for serialization as it was implemented in `1.3.0` 55 | 56 | ```typescript 57 | import { setSerialization } from "@macfja/svelte-persistent-store" 58 | import ESSerializer from "esserializer" 59 | 60 | const allowedClasses = [] 61 | setSerialization( 62 | ESSerializer.serialize, 63 | (value) => { 64 | if (value === "undefined") { 65 | return undefined 66 | } 67 | 68 | if (value !== null && value !== undefined) { 69 | try { 70 | return ESSerializer.deserialize(value, allowedClasses) 71 | } catch (e) { 72 | // Do nothing 73 | // use the value "as is" 74 | } 75 | try { 76 | return JSON.parse(value) 77 | } catch (e) { 78 | // Do nothing 79 | // use the value "as is" 80 | } 81 | } 82 | return value 83 | }, 84 | (classDef) => { 85 | allowedClasses.push(classDef) 86 | } 87 | ) 88 | ``` 89 | 90 | ### Using `@macfja/serializer` for serialization 91 | 92 | ```typescript 93 | import { setSerialization } from "@macfja/svelte-persistent-store" 94 | import { serialize, deserialize, addGlobalAllowedClass } from "@macfja/serializer" 95 | 96 | setSerialization(serialize, deserialize, addGlobalAllowedClass) 97 | ``` 98 | -------------------------------------------------------------------------------- /.docs/Examples/02-Reuse-Store.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Reuse a store across an application 3 | order: 2 4 | --- 5 | 6 | # Reuse a store across an application 7 | 8 | There are, at least, 3 ways to have the same store between components. 9 | 10 | 1. Recreate the same code definition in the second component 11 | 2. Create the store in one component `context="module"` scope 12 | 3. Create the store outside a component and import it 13 | 14 | **The 1st solution is NOT the best**. Each stores will be a different instance, so they won't share anything. 15 | If you change the store value in one component other won't see it (unless you unmount/mount the component again). 16 | You also expose yourself to data concurrency: each stores will erase the previously saved value of another store. 17 | 18 | The 2nd and 3rd are similar. The difference between the two is the "owning" of the store. 19 | If you declare it in the `context="module"` of a component you implicitly make this component as owner (for human point of view) as every times you need it you will import it from this component. 20 | Creating the component in a separate JS/TS file, the store is not _"attached"_ to any component. 21 | 22 | --- 23 | 24 | Here an example of those 3 implementations 25 | 26 | ## Duplication definition in every component 27 | 28 | ```html 29 | 30 | 38 | ``` 39 | 40 |   41 | 42 | ```html 43 | 44 | 55 | ``` 56 | 57 | ## Context module 58 | 59 | ```html 60 | 61 | 67 | 70 | ``` 71 | 72 |   73 | 74 | ```html 75 | 76 | 81 | ``` 82 | 83 | ## External definition 84 | 85 | ```js 86 | // stores.js 87 | import { persist, createLocalStorage } from "@macfja/svelte-persistent-store" 88 | import { writable } from "svelte/store" 89 | 90 | export let name = persist(writable("John"), createLocalStorage(), "name") 91 | ``` 92 | 93 |   94 | 95 | ```html 96 | 97 | 102 | ``` 103 | 104 |   105 | 106 | ```html 107 | 108 | 113 | ``` 114 | -------------------------------------------------------------------------------- /.docs/Examples/01-Basic.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Basic examples 3 | order: 1 4 | --- 5 | 6 | # Basic examples 7 | 8 | ## Local Storage 9 | 10 | ```html 11 | 17 | 27 | ``` 28 | 29 | ## Session storage 30 | 31 | ```html 32 | 45 | 46 |
47 |
48 | 49 |
50 |

51 |
52 | ``` 53 | 54 | ## Cookie storage 55 | 56 | ```html 57 | 63 | 64 |
65 | 66 | 67 |
68 | ``` 69 | 70 | ## indexedDB storage 71 | 72 | ```html 73 | 79 | 80 | 91 | ``` 92 | 93 | ## Chrome storage 94 | 95 | ```html 96 | 102 | 103 | 113 | ``` 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@macfja/svelte-persistent-store", 3 | "version": "2.4.2", 4 | "description": "A Svelte store that keep its value through pages and reloads", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "exports": { 8 | ".": { 9 | "types": "./types/index.d.ts", 10 | "import": "./dist/index.mjs", 11 | "require": "./dist/index.js" 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "files": [ 16 | "src/", 17 | "dist/", 18 | "types", 19 | "LICENSE.md", 20 | "README.md" 21 | ], 22 | "dependencies": { 23 | "@macfja/serializer": "^1.1.2", 24 | "browser-cookies": "^1.2.0", 25 | "idb-keyval": "^5.1.3 || ^6.2.1", 26 | "sjcl-codec-hex": "^1.0.0", 27 | "sjcl-es": "^2.0.0" 28 | }, 29 | "devDependencies": { 30 | "@rollup/plugin-commonjs": "^22.0.0 || ^25.0.2", 31 | "@rollup/plugin-node-resolve": "^13.3.0 || ^15.1.0", 32 | "@rollup/plugin-sucrase": "^4.0.4 || ^5.0.1", 33 | "@rollup/plugin-terser": "^0.4.3", 34 | "@rollup/plugin-typescript": "^8.2.5 || ^11.1.2", 35 | "@tsconfig/svelte": "^3.0.0 || ^5.0.0", 36 | "@typescript-eslint/eslint-plugin": "^5.30.5 || ^6.2.1", 37 | "@typescript-eslint/parser": "^5.30.5 || ^6.2.1", 38 | "eslint": "^8.19.0", 39 | "eslint-config-prettier": "^8.5.0", 40 | "eslint-plugin-import": "^2.24.0", 41 | "eslint-plugin-sonarjs": "^0.20.0", 42 | "eslint-plugin-sort-class-members": "^1.18.0", 43 | "eslint-plugin-unicorn": "^48.0.1", 44 | "eslint-plugin-unused-imports": "^3.0.0", 45 | "jscpd": "^3.5.9", 46 | "npm-run-all": "^4.1.5", 47 | "prettier": "^2.7.1", 48 | "prettier-plugin-svelte": "^2.7.0", 49 | "rollup": "^2.56.2 || ^3.26.0", 50 | "rollup-plugin-svelte": "^7.1.0", 51 | "sirv-cli": "^2.0.2", 52 | "svelte": "^3.42.1 || ^4.0.1 || ^5.0.0", 53 | "svelte-check": "^2.8.0 || 3.4.4", 54 | "svelte-preprocess": "^4.7.4 || ^5.0.4", 55 | "testcafe": "^1.18.3 <1.20.0 || 3.0.1", 56 | "tslib": "^2.3.0 | ^2.6.0", 57 | "typedoc": "^0.24.8", 58 | "typescript": "^4.7.4 || ^5.1.6" 59 | }, 60 | "peerDependencies": { 61 | "svelte": "^3.0 || ^4.0 || ^5.0" 62 | }, 63 | "scripts": { 64 | "doc": "typedoc", 65 | "lint": "run-p --aggregate-output --continue-on-error --print-label lint:*", 66 | "lint:prettier": "prettier --check ./**/*.{md,js,json,ts,yml} .eslintrc.cjs", 67 | "lint:eslint": "eslint src/ ./*.cjs ./*.mjs", 68 | "lint:cpd": "jscpd --mode strict --reporters consoleFull,console src/", 69 | "lint:tsc": "tsc --emitDeclarationOnly false --noEmit", 70 | "format": "prettier --write ./**/*.{md,js,json,ts,yml} .eslintrc.cjs", 71 | "pretest": "rollup -c rollup.test.config.mjs", 72 | "test": "testcafe all tests/e2e.ts --app 'npx sirv tests --port 5000 --host 0.0.0.0'", 73 | "prebuild": "tsc", 74 | "build": "rollup -c", 75 | "prepublishOnly": "npm run build" 76 | }, 77 | "repository": { 78 | "type": "git", 79 | "url": "git+https://github.com/macfja/svelte-persistent-store.git" 80 | }, 81 | "bugs": { 82 | "url": "https://github.com/macfja/svelte-persistent-store/issues" 83 | }, 84 | "homepage": "https://github.com/macfja/svelte-persistent-store#readme", 85 | "author": "MacFJA", 86 | "license": "MIT", 87 | "types": "types/index.d.ts", 88 | "keywords": [ 89 | "store", 90 | "persistent", 91 | "localStorage", 92 | "sessionStorage", 93 | "indexedDB", 94 | "persist", 95 | "encryptedStorage", 96 | "chromeStorage", 97 | "cookie", 98 | "svelte", 99 | "sveltejs" 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /src/alias.ts: -------------------------------------------------------------------------------- 1 | import { writable as svelteWritable } from "svelte/store" 2 | import type { Writable, StartStopNotifier } from "svelte/store" 3 | 4 | import type { PersistentStore, StorageInterface } from "./core" 5 | import { createCookieStorage, createLocalStorage, createSessionStorage, persist } from "./core" 6 | 7 | type StorageType = "cookie" | "local" | "session" 8 | const sharedStorage: { [type in StorageType]?: StorageInterface } = {} 9 | 10 | function getStorage(type: StorageType): StorageInterface { 11 | if (!Object.keys(sharedStorage).includes(type)) { 12 | switch (type) { 13 | case "cookie": 14 | sharedStorage[type] = createCookieStorage() 15 | break 16 | case "local": 17 | sharedStorage[type] = createLocalStorage() 18 | break 19 | case "session": 20 | sharedStorage[type] = createSessionStorage() 21 | break 22 | } 23 | } 24 | return sharedStorage[type] as StorageInterface 25 | } 26 | 27 | /** 28 | * Persist a store into a cookie 29 | * @param {Writable<*>} store The store to enhance 30 | * @param {string} cookieName The name of the cookie 31 | */ 32 | export function persistCookie(store: Writable, cookieName: string): PersistentStore { 33 | return persist(store, getStorage("cookie"), cookieName) 34 | } 35 | /** 36 | * Persist a store into the browser session storage 37 | * @param {Writable<*>} store The store to enhance 38 | * @param {string} key The name of the key in the browser session storage 39 | */ 40 | export function persistBrowserSession(store: Writable, key: string): PersistentStore { 41 | return persist(store, getStorage("session"), key) 42 | } 43 | /** 44 | * Persist a store into the browser local storage 45 | * @param {Writable<*>} store The store to enhance 46 | * @param {string} key The name of the key in the browser local storage 47 | */ 48 | export function persistBrowserLocal(store: Writable, key: string): PersistentStore { 49 | return persist(store, getStorage("local"), key) 50 | } 51 | 52 | /** 53 | * Create a standard Svelte store persisted in Browser LocalStorage 54 | * @param {string} key The key of the data to persist 55 | * @param {*} [initialValue] The initial data of the store 56 | * @param {StartStopNotifier<*>} [start] start and stop notifications for subscriptions 57 | * @return {PersistentStore<*>} 58 | */ 59 | export function localWritable(key: string, initialValue?: T, start?: StartStopNotifier): PersistentStore { 60 | return persistBrowserLocal(svelteWritable(initialValue, start), key) 61 | } 62 | /** 63 | * Create a standard Svelte store persisted in Browser LocalStorage. 64 | * (Alias of [[localWritable]]) 65 | * @param {string} key The key of the data to persist 66 | * @param {*} [initialValue] The initial data of the store 67 | * @param {StartStopNotifier<*>} [start] start and stop notifications for subscriptions 68 | * @return {PersistentStore<*>} 69 | */ 70 | export function writable(key: string, initialValue?: T, start?: StartStopNotifier): PersistentStore { 71 | return localWritable(key, initialValue, start) 72 | } 73 | /** 74 | * Create a standard Svelte store persisted in Browser SessionStorage 75 | * @param {string} key The key of the data to persist 76 | * @param {*} [initialValue] The initial data of the store 77 | * @param {StartStopNotifier<*>} [start] start and stop notifications for subscriptions 78 | * @return {PersistentStore<*>} 79 | */ 80 | export function sessionWritable(key: string, initialValue?: T, start?: StartStopNotifier): PersistentStore { 81 | return persistBrowserSession(svelteWritable(initialValue, start), key) 82 | } 83 | /** 84 | * Create a standard Svelte store persisted in cookie 85 | * @param {string} key The key of the data to persist 86 | * @param {*} [initialValue] The initial data of the store 87 | * @param {StartStopNotifier<*>} [start] start and stop notifications for subscriptions 88 | * @return {PersistentStore<*>} 89 | */ 90 | export function cookieWritable(key: string, initialValue?: T, start?: StartStopNotifier): PersistentStore { 91 | return persistCookie(svelteWritable(initialValue, start), key) 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Persistent store 2 | 3 | A Svelte store that keep its value through pages and reloads 4 | 5 | ![Github CI](https://github.com/macfja/svelte-persistent-store/workflows/Quality%20tools/badge.svg) 6 | ![GitHub Repo stars](https://img.shields.io/github/stars/macfja/svelte-persistent-store?style=social) 7 | ![NPM bundle size](https://img.shields.io/bundlephobia/minzip/@macfja/svelte-persistent-store) 8 | ![Download per week](https://img.shields.io/npm/dw/@macfja/svelte-persistent-store) 9 | ![License](https://img.shields.io/npm/l/@macfja/svelte-persistent-store) 10 | ![NPM version](https://img.shields.io/npm/v/@macfja/svelte-persistent-store) 11 | ![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/@macfja/svelte-persistent-store) 12 | 13 | ## Installation 14 | 15 | ``` 16 | npm install @macfja/svelte-persistent-store 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```javascript 22 | import { persist, createLocalStorage } from "@macfja/svelte-persistent-store" 23 | import { writable } from "svelte/store" 24 | 25 | let name = persist(writable("John"), createLocalStorage(), "name") 26 | 27 | $name = "Jeanne Doe" 28 | 29 | // if you reload the page the value of $name is 'Jeanne Doe' 30 | ``` 31 | 32 | ```javascript 33 | import { persistBrowserSession } from "@macfja/svelte-persistent-store" 34 | import { writable } from "svelte/store" 35 | 36 | let title = persistBrowserSession(writable("Unsaved"), "document-name") 37 | 38 | $title = "My Document" 39 | 40 | // if you reload the page the value of $title is 'My Document' 41 | ``` 42 | 43 | ```javascript 44 | import { writable } from "@macfja/svelte-persistent-store" 45 | 46 | // Create a wriatble store, persisted in browser LocalStorage, with the key `name` 47 | let name = writable("name", "John") 48 | 49 | $name = "Jeanne Doe" 50 | 51 | // if you reload the page the value of $name is 'Jeanne Doe' 52 | ``` 53 | 54 | ## Features 55 | 56 | - Multiple storages (Allow to have the best suited usage depending on your use case) 57 | - Work with any Svelte store 58 | - Work with classes, objects, primitive 59 | 60 | ## Storages 61 | 62 | There are 6 storages built-in: 63 | 64 | - `createLocalStorage()`, that use `window.localStorage` to save values 65 | - `createSessionStorage()`, that use `window.sessionStorage` to save values 66 | - `createCookieStorage()`, that use `document.cookie` to save values 67 | - `createIndexedDBStorage()`, that use `window.indexedDB` to save value 68 | - `createChromeStorage()`, that use `chrome.storage` to save values 69 | - `createEncryptedStorage()`, that wrap a storage to encrypt data (and key) 70 | 71 | You can add more storages, you just need to implement the interface `StorageInterface` 72 | 73 | ## Documentation 74 | 75 | Documentation and examples can be generated with `npm run doc`, next open `docs/index.html` with your favorite web browser. 76 | 77 | (Hint: If you don't want to generate the docs, a part of the example and documentation are available [here](.docs/README.md)) 78 | 79 | ### Types 80 | 81 | The persist function will return a new Store with type `PersistentStore`. 82 | 83 | The full signature of `persist` is: 84 | 85 | ```typescript 86 | declare function persist(store: Writable, storage: StorageInterface, key: string): PersistentStore 87 | ``` 88 | 89 | The persist function add a `delete` function on the store. 90 | 91 | More information about types can be found in the generated `types/index.d.ts` (`npm run prebuild`) or in the generated documentation (`npm run doc`). 92 | 93 | ## Backwards Compatibility Break 94 | 95 | ### `1.3.0` to `2.0.0` 96 | 97 | Data persisted in version `1.3.0` may not be deserializable with version `2.*`. 98 | 99 | If you have persisted store that contains Javascript class with version `1.3.0` of `@macfja/svelte-persistent-store` you will not be able to get the data by default. 100 | This is due to a change of data serialization. More information [here](.docs/How-To/06-Change-Serialization.md) 101 | 102 | ## Contributing 103 | 104 | Contributions are welcome. Please open up an issue or create PR if you would like to help out. 105 | 106 | Read more in the [Contributing file](CONTRIBUTING.md) 107 | 108 | ## License 109 | 110 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 111 | -------------------------------------------------------------------------------- /src/encryption.ts: -------------------------------------------------------------------------------- 1 | import toHex from "sjcl-codec-hex/from-bits" 2 | import fromHex from "sjcl-codec-hex/to-bits" 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import sjcl from "sjcl-es" 6 | 7 | import { deserialize, serialize } from "./core" 8 | import type { StorageInterface } from "./core" 9 | 10 | /** 11 | * The behavior when no encryption library is available when requesting an encrypted storage 12 | * @deprecated 13 | */ 14 | export enum NO_ENCRYPTION_BEHAVIOR { 15 | /** 16 | * Throw an exception 17 | */ 18 | EXCEPTION = 0, 19 | /** 20 | * Use the wrapped Storage as-is 21 | */ 22 | NO_ENCRYPTION = 1, 23 | /** 24 | * Don't use any storage, so no not encrypted data will be persisted 25 | */ 26 | NO_STORAGE = 2, 27 | } 28 | 29 | /** 30 | * Set the behavior when no encryption library is available when requesting an encrypted storage 31 | * @deprecated 32 | * @param behavior 33 | */ 34 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 35 | export function noEncryptionBehavior(behavior: NO_ENCRYPTION_BEHAVIOR): void { 36 | // Do nothing 37 | } 38 | 39 | /** 40 | * The encryption interface 41 | */ 42 | export interface Encryption { 43 | /** 44 | * Hash the input data 45 | * @param {string} data The data to hash 46 | * @return {string} The hashed data 47 | */ 48 | hash: (data: string) => string 49 | /** 50 | * Encrypt the input data. 51 | * 52 | * Must be reversible with `decrypt` function. 53 | * @param {string} data The data to encrypt 54 | * @return {string} The encrypted data 55 | */ 56 | encrypt: (data: string) => string 57 | /** 58 | * Decrypt the input data. 59 | * 60 | * Must be reversible with `encrypt` function. 61 | * @param {string} data The data to decrypt 62 | * @return {string} The decrypted data 63 | */ 64 | decrypt: (data: string) => string 65 | } 66 | 67 | /** 68 | * Encryption implementation with AES-256-GCM 69 | */ 70 | export class GCMEncryption implements Encryption { 71 | /** 72 | * The AES cipher to use for hashing, encrypting and decrypting 73 | * @private 74 | */ 75 | private readonly cipher 76 | 77 | /** 78 | * The GCM Encryption constructor 79 | * @param {string} encryptionKey The HEX key to use for encryption 80 | */ 81 | constructor(encryptionKey: string) { 82 | this.cipher = new sjcl.cipher.aes(fromHex(encryptionKey)) 83 | } 84 | 85 | /** 86 | * Encrypt the input data. 87 | * 88 | * @param {string} data The data to encrypt 89 | * @param {string} [iv] The IV to use during the encryption (default to "sps") 90 | * @return {string} The encrypted data 91 | */ 92 | encrypt(data: string, iv?: string | undefined): string { 93 | if (!iv) iv = "sps" 94 | const encodedIv = sjcl.codec.utf8String.toBits(iv) 95 | return ( 96 | toHex(sjcl.mode.gcm.encrypt(this.cipher, sjcl.codec.utf8String.toBits(data), encodedIv, [], 256)) + 97 | ":" + 98 | toHex(encodedIv) 99 | ) 100 | } 101 | 102 | /** 103 | * Encrypt the input data. 104 | * 105 | * The IV is extracted from the encrypted data. 106 | * 107 | * @param {string} data The data to decrypt 108 | * @return {string} The decrypted data 109 | */ 110 | decrypt(data: string): string { 111 | return sjcl.codec.utf8String.fromBits( 112 | sjcl.mode.gcm.decrypt(this.cipher, fromHex(data.split(":")[0]), fromHex(data.split(":")[1])) 113 | ) 114 | } 115 | 116 | /** 117 | * Hash the input data. 118 | * 119 | * Use the encrypt function with the IV set to "sps" 120 | * 121 | * @param {string} data The data to hash 122 | * @return {string} The hashed data 123 | */ 124 | hash(data: string): string { 125 | return this.encrypt(data, "sps") 126 | } 127 | } 128 | 129 | /** 130 | * Add encryption layer on a storage 131 | * @deprecated Use createEncryptionStorage instead 132 | * @param wrapped The storage to enhance 133 | * @param encryptionKey The encryption key to use on key and data 134 | */ 135 | export function createEncryptedStorage( 136 | wrapped: StorageInterface, 137 | encryptionKey: string 138 | ): StorageInterface { 139 | const encryption = new GCMEncryption(encryptionKey) 140 | return createEncryptionStorage(wrapped, encryption) 141 | } 142 | 143 | /** 144 | * Add encryption layer on a storage 145 | * @param wrapped The storage to enhance 146 | * @param encryption The encryption to use on key and data 147 | */ 148 | export function createEncryptionStorage( 149 | wrapped: StorageInterface, 150 | encryption: Encryption 151 | ): StorageInterface { 152 | return { 153 | getValue(key: string): T | null { 154 | const encryptedKey = encryption.hash(key) 155 | const storageValue = wrapped.getValue(encryptedKey) 156 | if (storageValue === null) return null 157 | return deserialize(encryption.decrypt(storageValue)) 158 | }, 159 | setValue(key: string, value: T): void { 160 | const encryptedKey = encryption.hash(key) 161 | const encryptedValue = encryption.encrypt(serialize(value)) 162 | wrapped.setValue(encryptedKey, encryptedValue) 163 | }, 164 | deleteValue(key: string): void { 165 | const encryptedKey = encryption.hash(key) 166 | wrapped.deleteValue(encryptedKey) 167 | }, 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /.docs/Examples/05-Use-Persisted-Store.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Usage of persistent store 3 | order: 5 4 | --- 5 | 6 | # Usage of persistent store 7 | 8 | There are a multiple way to use/create persistent stores. 9 | 10 | ## The long and configurable way 11 | 12 | This method allow to configure the storage by first creating it. 13 | This is particular useful for LocalStorage, SessionStorage, EncryptedStorage and ChromeStorage that have configuration. 14 | 15 | _**NOTE:** It's the only way to use EncryptedStorage and ChromeStorage_ 16 | 17 | ### Function call 18 | 19 | ```typescript 20 | import {writable} from "svelte/store" 21 | 22 | const persistedStore = persist( 23 | writable("my value"), 24 | <>, 25 | "my-data-key" 26 | ) 27 | ``` 28 | 29 | With: 30 | 31 | - `writable("my value")` the store you want to persist 32 | - `<>` one of 33 | - `createLocalStorage` 34 | - `createSessionStorage` 35 | - `createCookieStorage` 36 | - `createIndexedDBStorage` 37 | - `createEncryptedStorage` 38 | - `createChromeStorage` 39 | - `"my-data-key"` the key that will identify the store in the storage 40 | 41 | ### Functions signatures 42 | 43 | ```typescript 44 | declare function persist(store: Writable, storage: StorageInterface, key: string): PersistentStore 45 | 46 | declare function createLocalStorage(listenExternalChanges?: boolean): StorageInterface 47 | declare function createSessionStorage(listenExternalChanges?: boolean): StorageInterface 48 | declare function createCookieStorage(): StorageInterface 49 | declare function createIndexedDBStorage(): SelfUpdateStorageInterface 50 | declare function createEncryptedStorage( 51 | wrapped: StorageInterface, 52 | encryptionKey: string 53 | ): StorageInterface | SelfUpdateStorageInterface 54 | declare function createChromeStorage( 55 | storageType?: CHROME_STORAGE_TYPE, 56 | listenExternalChanges?: boolean 57 | ): SelfUpdateStorageInterface 58 | ``` 59 | 60 | - `createLocalStorage` and `createSessionStorage` take a boolean as theirs first parameter (`false` by default). If set to `true` the storage will listen for changes (of the stored value) in other pages 61 | - `createEncryptedStorage` have 2 parameters, the first one is the storage that you want to encrypt, the second is the encryption key to use 62 | - `createChromeStorage` have 2 optional parameters, the first one this the type of storage (local storage by default), the second (`false` by default) if set to `true` the storage will listen for changes (of the stored value) in other pages 63 | 64 | ## Preconfigured storage way 65 | 66 | This method allow to use pre-created storage, configured with default options. 67 | This avoids creating multiple times the same storage 68 | 69 | ### Function call 70 | 71 | ```typescript 72 | import {writable} from "svelte/store" 73 | 74 | const persistedStore = <>( 75 | writable("my value"), 76 | "my-data-key" 77 | ) 78 | ``` 79 | 80 | With: 81 | 82 | - `writable("my value")` the store you want to persist 83 | - `<>` one of 84 | - `persistBrowserLocal` 85 | - `persistBrowserSession` 86 | - `persistCookie` 87 | - `"my-data-key"` the key that will identify the store in the storage 88 | 89 | ### Functions signatures 90 | 91 | ```typescript 92 | declare function persistBrowserLocal(store: Writable, key: string): PersistentStore 93 | declare function persistBrowserSession(store: Writable, key: string): PersistentStore 94 | declare function persistCookie(store: Writable, cookieName: string): PersistentStore 95 | ``` 96 | 97 | ## Short way 98 | 99 | This method allow to quickly create a writable store without the boilerplate of creating a Svelte store and a Storage. 100 | 101 | ### Function call 102 | 103 | ```typescript 104 | const persistedStore = <>( 105 | "my-data-key", 106 | "my value" 107 | ) 108 | ``` 109 | 110 | With: 111 | 112 | - `"my value"` the value store you want to persist 113 | - `<>` one of 114 | - `localWritable` 115 | - `writable` 116 | - `sessionWritable` 117 | - `cookieWritable` 118 | - `"my-data-key"` the key that will identify the store in the storage 119 | 120 | ### Functions signatures 121 | 122 | ```typescript 123 | declare function localWritable(key: string, initialValue?: T): PersistentStore 124 | declare function writable(key: string, initialValue?: T): PersistentStore 125 | declare function sessionWritable(key: string, initialValue?: T): PersistentStore 126 | declare function cookieWritable(key: string, initialValue?: T): PersistentStore 127 | ``` 128 | 129 | - `writable` is an alias to `localWritable` 130 | 131 | --- 132 | 133 | ## About long format advantages 134 | 135 | The long format allow you to use High order function principle. 136 | 137 | The `persist` (and also `persistCookie`, `persistBrowserSession`, `persistBrowserLocal`) function is a high order function: it takes a parameter and return an augmented version of it. 138 | 139 | As High order function return an augmented version of their parameter, they can be chained. 140 | 141 | Imagine we have another lib that enhance a store (like `@macfja/svelte-invalidable`) we can chain them: 142 | 143 | ```typescript 144 | import { invalidable } from "@macfja/svelte-invalidable" 145 | import { persistBrowserLocal } from "@macfja/svelte-persistent-store" 146 | import { writable } from "svelte/store" 147 | 148 | const myStore = persistBrowserLocal( 149 | invalidable(writable(0), () => Math.random()), 150 | "last-random" 151 | ) 152 | 153 | // $myStore will return a number 154 | // myStore.invalidate() (added by @macfja/svelte-invalidable) still work 155 | // The value or myStore is saved in the browser localStorage 156 | ``` 157 | 158 | With the full format (`persist` only) you can also add encryption to a storage 159 | 160 | ```typescript 161 | import { persist, createLocalStorage, createEncryptedStorage } from "@macfja/svelte-persistent-store" 162 | import { writable } from "svelte/store" 163 | 164 | const storage = createEncryptedStorage(createLocalStorage(), "5368566D597133743677397A24432646") 165 | const myStore = persist(writable(0), storage, "my-data-key") 166 | ``` 167 | -------------------------------------------------------------------------------- /tests/src/App.svelte: -------------------------------------------------------------------------------- 1 | 62 | 63 |
64 | Cookie storage 65 |
66 | Current content of cookie: {cookie} 67 |
68 | 69 |
70 | 71 |
72 | LocalStorage 73 | 74 |
75 | 76 |
77 | SessionStorage 78 | 79 |
80 | 81 |
82 | IndexedDB Storage 83 | 84 |
85 | 86 |
87 | Special value 88 |
89 | Undefined value 90 |
91 | Session Storage 92 | {$undefinedExample}{typeof $undefinedExample} 93 |
94 |
95 | IndexedDB Storage 96 | {$undefinedExample2}{typeof $undefinedExample2} 97 |
98 |
99 | Cookie Storage 100 | {$undefinedExample3}{typeof $undefinedExample3} 101 |
102 |
103 |
104 | Null value 105 |
106 | Session Storage 107 | {$nullExample}{typeof $nullExample} 108 |
109 |
110 | IndexedDB Storage 111 | {$nullExample2}{typeof $nullExample2} 112 |
113 |
114 | Cookie Storage 115 | {$nullExample3}{typeof $nullExample3} 116 |
117 |
118 |
119 | Not scalar value 120 |
121 | Array 122 | {$arrayExample}{typeof $arrayExample} 123 | 124 |
125 |
126 | Object 127 | {$objectExample}{typeof $objectExample} 128 | 129 |
130 |
131 |
132 | 133 |
134 | Class transform 135 | The current name is: {$classExample.getName()}. 136 | The current object type is: {$classExample.constructor.name}. 137 | 138 |
139 | 140 |
141 | Storage raw data 142 | Data in store: {JSON.stringify($storageExample)}.
143 | Raw data in storage: {storageRawValue}.
144 | 145 | 146 |
147 | 148 | 149 | 150 | 151 | 166 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [2.4.2] 10 | 11 | ### Added 12 | 13 | - Add support for Svelte v5 14 | 15 | ## [2.4.1] 16 | 17 | ### Fixed 18 | 19 | - Shared storage eagerly created, nullify the purpose of `disableWarnings` ([Issue#56]) 20 | 21 | ## [2.4.0] 22 | 23 | ### Added 24 | 25 | - Cookie options ([Issue#52]) 26 | - (dev) More quality tools 27 | 28 | ## [2.3.2] 29 | 30 | ### Fixed 31 | 32 | - Typescript declaration not discovered ([Issue#50]) 33 | 34 | ## [2.3.1] 35 | 36 | ### Added 37 | 38 | - Add compatibility for Svelte 4 ([PR#47] + [Issue#48]) 39 | 40 | ### Changed 41 | 42 | - (dev) Use typedoc again to generate documentation 43 | - (dev) Update all dependencies to the last version 44 | 45 | ## [2.3.0] 46 | 47 | ### Fixed 48 | 49 | - Fix error with sandboxed storage (`localStorage` + `sessionStorage`) ([Issue#41]) 50 | - (doc) Typo in README ([PR#39]) + outdated example ([PR#40]) 51 | - (dev) Wrong typing ([PR#38]) 52 | 53 | ### Added 54 | 55 | - New function to create store (`localWritable`, `writable`, `sessionWritable`, `cookieWritable`) 56 | 57 | ### Changed 58 | 59 | - (dev) Move wrappers (+shorthand) to a dedicated file 60 | 61 | ## [2.2.1] 62 | 63 | ### Fixed 64 | 65 | - Fix class definition type not wide enough ([Issue#32]) 66 | 67 | ### Changed 68 | 69 | - (dev) Update Github actions versions. 70 | 71 | ## [2.2.0] 72 | 73 | ### Added 74 | 75 | - `createEncryptionStorage()` function to customize the encryption 76 | - `Encryption` Interface for the encryption definition 77 | 78 | ### Changed 79 | 80 | - Change from [`cyrup`](https://www.npmjs.com/package/cyrup) to [`sjcl-es`](https://www.npmjs.com/package/sjcl-es) and [`sjcl-codec-hex`](https://www.npmjs.com/package/sjcl-codec-hex) ([Issue#31]) 81 | 82 | ### Fixed 83 | 84 | - Error while compiling for SvelteKit ([Issue#31]) 85 | 86 | ### Deprecated 87 | 88 | - `createEncryptedStorage()` use `createEncryptionStorage()` instead 89 | - `NO_ENCRYPTION_BEHAVIOR` enum (no replacement) 90 | - `noEncryptionBehavior()` (no replacement, function do nothing) 91 | 92 | ## [2.1.0] 93 | 94 | ### Added 95 | 96 | - New storage `createChromeStorage` for Chrome Extension 97 | - Possibility to change the serialization functions ([Issue#26]) 98 | - Add note in README about BC break ([Issue#26]) 99 | - (dev) More quality tools 100 | 101 | ### Fixed 102 | 103 | - Change compilation (remove all `require` in ES build) ([Issue#23]) 104 | - Better detection of unavailable Crypto capacity 105 | - (dev) Don't allow Testcafe 1.20.* versions 106 | 107 | ### Changed 108 | 109 | - Upgrade the version of `@macfja/serializer` ([Issue#26]) 110 | - (dev) Use shorthand persist function in test 111 | - (dev) Refactoring of the listeners' creation/usage functions 112 | - (dev) Run prettier on existing code 113 | 114 | ### Removed 115 | 116 | - (dev) Remove unused file 117 | 118 | ## [2.0.0] 119 | 120 | ### Added 121 | 122 | - New alias for persisting into Browser local storage (`persistBrowserLocal`) 123 | - New alias for persisting into Browser session storage (`persistBrowserSession`) 124 | - New alias for persisting into cookie storage (`persistCookie`) 125 | - New storage (wrapper) `createEncryptedStorage` ([Issue#21]) 126 | - Add basic type definitions in README ([Issue#19]) 127 | 128 | ### Changed 129 | 130 | - Change name of functions that create storage 131 | - Change the data serializer ([Issue#18], [Issue#20]) 132 | - (dev) New lib to generate documentation 133 | - (dev) Validate code style on configuration files 134 | 135 | ### Removed 136 | 137 | - `noopStorage()` use `createNoopStorage()` instead 138 | - `localStorage()` use `createLocalStorage()` instead 139 | - `sessionStorage()` use `createSessionStorage()` instead 140 | - `indexedDBStorage()` use `createIndexedDBStorage()` instead 141 | 142 | ## Versions 1.x 143 |
144 | 145 | ## [1.3.0] 146 | 147 | ### Added 148 | 149 | - Possibility to disable console warnings ([Issue#9]) 150 | - `undefined` value not handled ([Issue#11]) 151 | 152 | ### Changed 153 | 154 | - Change how data are serialized/deserialized to handle class 155 | 156 | ### Fixed 157 | 158 | - Classes can't be persisted 159 | 160 | ## [1.2.0] 161 | 162 | ### Changed 163 | 164 | - Changed the Cookie lib to be able to set `SameSite` ([Issue#7], [PR#8]) 165 | - (DEV) Update the dependencies 166 | 167 | ## [1.1.1] 168 | 169 | ### Fixed 170 | 171 | - SyntaxError when the value can't be parsed as a JSON ([Issue#3]) 172 | 173 | ### Changed 174 | 175 | - Update (dev) dependencies version 176 | 177 | ## [1.1.0] 178 | 179 | ### Added 180 | 181 | - Add external change listener for SessionStorage and LocalStorage 182 | - Add documentation 183 | - Add IndexedDB Storage 184 | 185 | ## [1.0.2] 186 | 187 | ### Fixed 188 | 189 | - Add protection on global `document` variable 190 | 191 | ## [1.0.1] 192 | 193 | ### Added 194 | 195 | - Add `noop` Storage that do nothing 196 | 197 | ### Fixed 198 | 199 | - Add protection on global `window` variable 200 | 201 | ## [1.0.0] 202 | 203 | First version 204 | 205 |
206 | 207 | [Unreleased]: https://github.com/MacFJA/svelte-persistent-store/compare/2.4.2...HEAD 208 | [2.4.2]: https://github.com/MacFJA/svelte-persistent-store/compare/2.4.1...2.4.2 209 | [2.4.1]: https://github.com/MacFJA/svelte-persistent-store/compare/2.4.0...2.4.1 210 | [2.4.0]: https://github.com/MacFJA/svelte-persistent-store/compare/2.3.2...2.4.0 211 | [2.3.2]: https://github.com/MacFJA/svelte-persistent-store/compare/2.3.1...2.3.2 212 | [2.3.1]: https://github.com/MacFJA/svelte-persistent-store/compare/2.3.0...2.3.1 213 | [2.3.0]: https://github.com/MacFJA/svelte-persistent-store/compare/2.2.1...2.3.0 214 | [2.2.1]: https://github.com/MacFJA/svelte-persistent-store/compare/2.2.0...2.2.1 215 | [2.2.0]: https://github.com/MacFJA/svelte-persistent-store/compare/2.1.0...2.2.0 216 | [2.1.0]: https://github.com/MacFJA/svelte-persistent-store/compare/2.0.0...2.1.0 217 | [2.0.0]: https://github.com/MacFJA/svelte-persistent-store/compare/1.3.0...2.0.0 218 | [1.3.0]: https://github.com/MacFJA/svelte-persistent-store/compare/1.2.0...1.3.0 219 | [1.2.0]: https://github.com/MacFJA/svelte-persistent-store/compare/1.1.1...1.2.0 220 | [1.1.1]: https://github.com/MacFJA/svelte-persistent-store/compare/1.1.0...1.1.1 221 | [1.1.0]: https://github.com/MacFJA/svelte-persistent-store/compare/1.0.2...1.1.0 222 | [1.0.2]: https://github.com/MacFJA/svelte-persistent-store/compare/1.0.1...1.0.2 223 | [1.0.1]: https://github.com/MacFJA/svelte-persistent-store/compare/1.0.0...1.0.1 224 | [1.0.0]: https://github.com/MacFJA/svelte-persistent-store/releases/tag/1.0.0 225 | 226 | [Issue#3]: https://github.com/MacFJA/svelte-persistent-store/issues/3 227 | [Issue#7]: https://github.com/MacFJA/svelte-persistent-store/issues/7 228 | [Issue#9]: https://github.com/MacFJA/svelte-persistent-store/issues/9 229 | [Issue#11]: https://github.com/MacFJA/svelte-persistent-store/issues/11 230 | [Issue#18]: https://github.com/MacFJA/svelte-persistent-store/issues/18 231 | [Issue#19]: https://github.com/MacFJA/svelte-persistent-store/issues/19 232 | [Issue#20]: https://github.com/MacFJA/svelte-persistent-store/issues/20 233 | [Issue#21]: https://github.com/MacFJA/svelte-persistent-store/issues/21 234 | [Issue#23]: https://github.com/MacFJA/svelte-persistent-store/issues/23 235 | [Issue#26]: https://github.com/MacFJA/svelte-persistent-store/issues/26 236 | [Issue#31]: https://github.com/MacFJA/svelte-persistent-store/issues/31 237 | [Issue#32]: https://github.com/MacFJA/svelte-persistent-store/issues/32 238 | [Issue#41]: https://github.com/MacFJA/svelte-persistent-store/issues/41 239 | [Issue#48]: https://github.com/MacFJA/svelte-persistent-store/issues/48 240 | [Issue#50]: https://github.com/MacFJA/svelte-persistent-store/issues/50 241 | [Issue#52]: https://github.com/MacFJA/svelte-persistent-store/issues/52 242 | [Issue#56]: https://github.com/MacFJA/svelte-persistent-store/issues/56 243 | [PR#8]: https://github.com/MacFJA/svelte-persistent-store/pull/8 244 | [PR#38]: https://github.com/MacFJA/svelte-persistent-store/pull/38 245 | [PR#39]: https://github.com/MacFJA/svelte-persistent-store/pull/39 246 | [PR#40]: https://github.com/MacFJA/svelte-persistent-store/pull/40 247 | [PR#47]: https://github.com/MacFJA/svelte-persistent-store/pull/47 248 | -------------------------------------------------------------------------------- /tests/e2e.ts: -------------------------------------------------------------------------------- 1 | import { Selector } from "testcafe" 2 | 3 | fixture("Svelte Persistent Storage").page("http://localhost:5000") 4 | 5 | const cookieInput = Selector("#cookieInput"), 6 | localInput = Selector("#localInput"), 7 | sessionInput = Selector("#sessionInput"), 8 | indexedInput = Selector("#indexedInput"), 9 | documentCookie = Selector("#documentCookie"), 10 | undefinedValue = Selector("#undefinedTest code"), 11 | undefinedType = Selector("#undefinedTest var"), 12 | undefinedValue2 = Selector("#undefinedTest2 code"), 13 | undefinedType2 = Selector("#undefinedTest2 var"), 14 | undefinedValue3 = Selector("#undefinedTest3 code"), 15 | undefinedType3 = Selector("#undefinedTest3 var"), 16 | arrayValue = Selector("#arrayTest code"), 17 | arrayType = Selector("#arrayTest var"), 18 | arrayButton = Selector("#arrayTest button"), 19 | objectValue = Selector("#objectTest code"), 20 | objectType = Selector("#objectTest var"), 21 | objectButton = Selector("#objectTest button"), 22 | nullValue = Selector("#nullTest code"), 23 | nullType = Selector("#nullTest var"), 24 | nullValue2 = Selector("#nullTest2 code"), 25 | nullType2 = Selector("#nullTest2 var"), 26 | nullValue3 = Selector("#nullTest3 code"), 27 | nullType3 = Selector("#nullTest3 var"), 28 | classValue = Selector("#classValue"), 29 | className = Selector("#className"), 30 | classButton = Selector("#classButton"), 31 | storageValue = Selector("#storageValue"), 32 | storageRawValue = Selector("#storageRawValue"), 33 | storageJSONButton = Selector("#storageJSONButton"), 34 | storageSerializerButton = Selector("#storageSerializerButton"), 35 | reloadButton = Selector("#reloadButton"), 36 | clearButton = Selector("#clearButton") 37 | 38 | test("Initial state", async (t) => { 39 | await t 40 | .click(storageSerializerButton) 41 | .click(clearButton) 42 | .click(reloadButton) 43 | .expect(cookieInput.value) 44 | .eql("John") 45 | .expect(localInput.value) 46 | .eql("Foo") 47 | .expect(sessionInput.value) 48 | .eql("Bar") 49 | .expect(indexedInput.value) 50 | .eql("Hello") 51 | .expect(documentCookie.textContent) 52 | .contains("sps-userName=%22John%22") 53 | .expect(undefinedValue.textContent) 54 | .eql("undefined") 55 | .expect(undefinedType.textContent) 56 | .eql("undefined") 57 | .expect(undefinedValue2.textContent) 58 | .eql("undefined") 59 | .expect(undefinedType2.textContent) 60 | .eql("undefined") 61 | .expect(undefinedValue3.textContent) 62 | .eql("undefined") 63 | .expect(undefinedType3.textContent) 64 | .eql("undefined") 65 | .expect(arrayValue.textContent) 66 | .eql("1,2,3") 67 | .expect(arrayType.textContent) 68 | .eql("object") 69 | .expect(objectValue.textContent) 70 | .eql("[object Object]") 71 | .expect(objectType.textContent) 72 | .eql("object") 73 | .expect(nullValue.textContent) 74 | .eql("null") 75 | .expect(nullType.textContent) 76 | .eql("object") 77 | .expect(nullValue2.textContent) 78 | .eql("null") 79 | .expect(nullType2.textContent) 80 | .eql("object") 81 | .expect(nullValue3.textContent) 82 | .eql("null") 83 | .expect(nullType3.textContent) 84 | .eql("object") 85 | .expect(classValue.textContent) 86 | .eql("John") 87 | .expect(className.textContent) 88 | .eql("NameHolder") 89 | .expect(storageValue.textContent) 90 | .eql('{"foo":"bar","baz":{"hello":"world","foobar":[1,2,3]}}') 91 | .expect(storageRawValue.textContent) 92 | .eql( 93 | '{"foo":"bar","baz":{"hello":"world","foobar":["#$@__reference__2",1,2,3],"#$@__reference__":1},"#$@__reference__":0}' 94 | ) 95 | }) 96 | 97 | test("Cookie storage", async (t) => { 98 | await t 99 | .expect(cookieInput.value) 100 | .eql("John") 101 | .selectText(cookieInput) 102 | .typeText(cookieInput, "Foobar") 103 | .expect(documentCookie.textContent) 104 | .contains("sps-userName=%22Foobar%22") 105 | .click(reloadButton) 106 | .expect(cookieInput.value) 107 | .eql("Foobar") 108 | .expect(documentCookie.textContent) 109 | .contains("sps-userName=%22Foobar%22") 110 | .click(clearButton) 111 | .click(reloadButton) 112 | .expect(cookieInput.value) 113 | .eql("John") 114 | }) 115 | 116 | test("Local storage", async (t) => { 117 | await t 118 | .expect(localInput.value) 119 | .eql("Foo") 120 | .typeText(localInput, "bar") 121 | .click(reloadButton) 122 | .expect(localInput.value) 123 | .eql("Foobar") 124 | .click(clearButton) 125 | .click(reloadButton) 126 | .expect(localInput.value) 127 | .eql("Foo") 128 | }) 129 | 130 | test("Session storage", async (t) => { 131 | await t 132 | .expect(sessionInput.value) 133 | .eql("Bar") 134 | .typeText(sessionInput, "bar") 135 | .click(reloadButton) 136 | .expect(sessionInput.value) 137 | .eql("Barbar") 138 | .click(clearButton) 139 | .click(reloadButton) 140 | .expect(sessionInput.value) 141 | .eql("Bar") 142 | }) 143 | 144 | test("IndexedDB storage", async (t) => { 145 | await t 146 | .expect(indexedInput.value) 147 | .eql("Hello") 148 | .typeText(indexedInput, " World") 149 | .click(reloadButton) 150 | .expect(indexedInput.value) 151 | .eql("Hello World") 152 | .click(clearButton) 153 | .click(reloadButton) 154 | .expect(indexedInput.value) 155 | .eql("Hello") 156 | }) 157 | 158 | test("Undefined value in storage", async (t) => { 159 | await t 160 | .click(reloadButton) 161 | .expect(undefinedValue.textContent) 162 | .eql("undefined") 163 | .expect(undefinedType.textContent) 164 | .eql("undefined") 165 | .expect(undefinedValue2.textContent) 166 | .eql("undefined") 167 | .expect(undefinedType2.textContent) 168 | .eql("undefined") 169 | .expect(undefinedValue3.textContent) 170 | .eql("undefined") 171 | .expect(undefinedType3.textContent) 172 | .eql("undefined") 173 | }) 174 | 175 | test("Null value in storage", async (t) => { 176 | await t 177 | .click(reloadButton) 178 | .expect(nullValue.textContent) 179 | .eql("null") 180 | .expect(nullType.textContent) 181 | .eql("object") 182 | .expect(nullValue2.textContent) 183 | .eql("null") 184 | .expect(nullType2.textContent) 185 | .eql("object") 186 | .expect(nullValue3.textContent) 187 | .eql("null") 188 | .expect(nullType3.textContent) 189 | .eql("object") 190 | }) 191 | 192 | test("Array value in storage", async (t) => { 193 | await t 194 | .click(reloadButton) 195 | .expect(arrayValue.textContent) 196 | .eql("1,2,3") 197 | .expect(arrayType.textContent) 198 | .eql("object") 199 | .click(arrayButton) 200 | .expect(arrayValue.textContent) 201 | .eql("1,2,3,4") 202 | .expect(arrayType.textContent) 203 | .eql("object") 204 | .click(clearButton) 205 | .click(reloadButton) 206 | .expect(arrayValue.textContent) 207 | .eql("1,2,3") 208 | .expect(arrayType.textContent) 209 | .eql("object") 210 | }) 211 | 212 | test("Object value in storage", async (t) => { 213 | await t 214 | .click(reloadButton) 215 | .expect(objectValue.textContent) 216 | .eql("[object Object]") 217 | .expect(objectType.textContent) 218 | .eql("object") 219 | .click(objectButton) 220 | .expect(objectValue.textContent) 221 | .eql("[object Object]") 222 | .expect(objectType.textContent) 223 | .eql("object") 224 | .click(clearButton) 225 | .click(reloadButton) 226 | .expect(objectValue.textContent) 227 | .eql("[object Object]") 228 | .expect(objectType.textContent) 229 | .eql("object") 230 | }) 231 | 232 | test("Class transform", async (t) => { 233 | await t 234 | .expect(classValue.textContent) 235 | .eql("John") 236 | .expect(className.textContent) 237 | .eql("NameHolder") 238 | .click(classButton) 239 | .expect(classValue.textContent) 240 | .eql("Jeanne") 241 | .expect(className.textContent) 242 | .eql("NameHolder") 243 | .click(reloadButton) 244 | .expect(classValue.textContent) 245 | .eql("Jeanne") 246 | .expect(className.textContent) 247 | .eql("NameHolder") 248 | .click(clearButton) 249 | .click(reloadButton) 250 | .expect(classValue.textContent) 251 | .eql("John") 252 | .expect(className.textContent) 253 | .eql("NameHolder") 254 | }) 255 | 256 | test("Serialization functions", async (t) => { 257 | await t 258 | .expect(storageValue.textContent) 259 | .eql('{"foo":"bar","baz":{"hello":"world","foobar":[1,2,3]}}') 260 | .expect(storageRawValue.textContent) 261 | .eql( 262 | '{"foo":"bar","baz":{"hello":"world","foobar":["#$@__reference__2",1,2,3],"#$@__reference__":1},"#$@__reference__":0}' 263 | ) 264 | .click(storageJSONButton) 265 | .expect(storageValue.textContent) 266 | .eql('{"foo":"bar","baz":{"hello":"world","foobar":[1,2,3]}}') 267 | .expect(storageRawValue.textContent) 268 | .eql('{"foo":"bar","baz":{"hello":"world","foobar":[1,2,3]}}') 269 | .click(storageSerializerButton) 270 | .expect(storageValue.textContent) 271 | .eql('{"foo":"bar","baz":{"hello":"world","foobar":[1,2,3]}}') 272 | .expect(storageRawValue.textContent) 273 | .eql( 274 | '{"foo":"bar","baz":{"hello":"world","foobar":["#$@__reference__2",1,2,3],"#$@__reference__":1},"#$@__reference__":0}' 275 | ) 276 | }) 277 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import { 2 | serialize as defaultSerializer, 3 | deserialize as defaultDeserializer, 4 | addGlobalAllowedClass, 5 | type ClassDefinition, 6 | } from "@macfja/serializer" 7 | import { get as getCookie, set as setCookie, erase as removeCookie } from "browser-cookies" 8 | import type { CookieOptions } from "browser-cookies" 9 | import { get, set, createStore, del } from "idb-keyval" 10 | import type { Writable } from "svelte/store" 11 | 12 | /** 13 | * Disabled warnings about missing/unavailable storages 14 | */ 15 | export function disableWarnings(): void { 16 | noWarnings = true 17 | } 18 | 19 | /** 20 | * If set to true, no warning will be emitted if the requested Storage is not found. 21 | * This option can be useful when the lib is used on a server. 22 | */ 23 | let noWarnings = false 24 | 25 | /** 26 | * List of storages where the warning have already been displayed. 27 | */ 28 | const alreadyWarnFor: Array = [] 29 | 30 | const warnUser = (message: string) => { 31 | const isProduction = typeof process !== "undefined" && process.env?.NODE_ENV === "production" 32 | 33 | if (!noWarnings && !alreadyWarnFor.includes(message) && !isProduction) { 34 | if (typeof window === "undefined") { 35 | message += "\n" + "Are you running on a server? Most of storages are not available while running on a server." 36 | } 37 | console.warn(message) 38 | alreadyWarnFor.push(message) 39 | } 40 | } 41 | /** 42 | * Add a log to indicate that the requested Storage have not been found. 43 | * @param {string} storageName 44 | */ 45 | const warnStorageNotFound = (storageName: string) => { 46 | warnUser(`Unable to find the ${storageName}. No data will be persisted.`) 47 | } 48 | 49 | /** 50 | * Add a class to the allowed list of classes to be serialized 51 | * @param classDefinition The class to add to the list 52 | */ 53 | export function addSerializableClass(classDefinition: ClassDefinition): void { 54 | addSerializable(classDefinition) 55 | } 56 | 57 | /** 58 | * The function that will be used to serialize data 59 | * @internal 60 | * @private 61 | * @type {(data: any) => string} 62 | */ 63 | export let serialize = defaultSerializer 64 | /** 65 | * The function that will be used to deserialize data 66 | * @internal 67 | * @private 68 | * @type {(input: string) => any} 69 | */ 70 | export let deserialize = defaultDeserializer 71 | /** 72 | * The function used to add a class in the serializer allowed class 73 | * @type {(classConstructor: ClassDefinition) => void} 74 | */ 75 | let addSerializable = addGlobalAllowedClass 76 | 77 | /** 78 | * Set the serialization functions to use 79 | * @param {(data: any) => string} serializer The function to use to transform any data into a string 80 | * @param {(input: string) => any} deserializer The function to use to transform back string into the original data (reverse of the serializer) 81 | * @param {(classConstructor: ClassDefinition) => void} [addSerializableClass] The function to use to add a class in the serializer/deserializer allowed class 82 | */ 83 | export function setSerialization( 84 | serializer: (data: any) => string, 85 | deserializer: (input: string) => any, 86 | addSerializableClass?: (classConstructor: ClassDefinition) => void 87 | ): void { 88 | serialize = serializer 89 | deserialize = deserializer 90 | addSerializable = 91 | addSerializableClass ?? 92 | (() => { 93 | return 94 | }) 95 | } 96 | 97 | /** 98 | * A store that keep its value in time. 99 | */ 100 | export interface PersistentStore extends Writable { 101 | /** 102 | * Delete the store value from the persistent storage 103 | */ 104 | delete(): void 105 | } 106 | 107 | /** 108 | * Storage interface 109 | */ 110 | export interface StorageInterface { 111 | /** 112 | * Get a value from the storage. 113 | * 114 | * If the value doesn't exist in the storage, `null` should be returned. 115 | * This method MUST be synchronous. 116 | * @param key The key/name of the value to retrieve 117 | */ 118 | getValue(key: string): T | null 119 | 120 | /** 121 | * Save a value in the storage. 122 | * @param key The key/name of the value to save 123 | * @param value The value to save 124 | */ 125 | setValue(key: string, value: T): void 126 | 127 | /** 128 | * Remove a value from the storage 129 | * @param key The key/name of the value to remove 130 | */ 131 | deleteValue(key: string): void 132 | } 133 | 134 | export interface SelfUpdateStorageInterface extends StorageInterface { 135 | /** 136 | * Add a listener to the storage values changes 137 | * @param {string} key The key to listen 138 | * @param {(newValue: T) => void} listener The listener callback function 139 | */ 140 | addListener(key: string, listener: (newValue: T) => void): void 141 | /** 142 | * Remove a listener from the storage values changes 143 | * @param {string} key The key that was listened 144 | * @param {(newValue: T) => void} listener The listener callback function to remove 145 | */ 146 | removeListener(key: string, listener: (newValue: T) => void): void 147 | } 148 | 149 | /** 150 | * Make a store persistent 151 | * @param {Writable<*>} store The store to enhance 152 | * @param {StorageInterface} storage The storage to use 153 | * @param {string} key The name of the data key 154 | */ 155 | export function persist(store: Writable, storage: StorageInterface, key: string): PersistentStore { 156 | const initialValue = storage.getValue(key) 157 | 158 | if (null !== initialValue) { 159 | store.set(initialValue) 160 | } 161 | 162 | if ((storage as SelfUpdateStorageInterface).addListener) { 163 | ;(storage as SelfUpdateStorageInterface).addListener(key, (newValue) => { 164 | store.set(newValue) 165 | }) 166 | } 167 | 168 | store.subscribe((value) => { 169 | storage.setValue(key, value) 170 | }) 171 | 172 | return { 173 | ...store, 174 | delete() { 175 | storage.deleteValue(key) 176 | }, 177 | } 178 | } 179 | 180 | function noop() { 181 | return 182 | } 183 | 184 | /** 185 | * Create helper function to use asynchronous storage 186 | * @param {() => void} onFirst Function to run every time the number of listener goes from 0 to 1 187 | * @param {() => void} onEmptied Function to run every tie the number of listener goes from 1 to 0 188 | * @return {{callListeners: (eventKey: string, newValue: any) => void, addListener: (key: string, listener: (newValue: any) => void) => void, removeListener: (key: string, listener: (newValue: any) => void) => void}} 189 | */ 190 | function createListenerFunctions( 191 | onFirst: () => void = noop, 192 | onEmptied: () => void = noop 193 | ): { 194 | callListeners: (eventKey: string, newValue: T) => void 195 | addListener: (key: string, listener: (newValue: T) => void) => void 196 | removeListener: (key: string, listener: (newValue: T) => void) => void 197 | } { 198 | const listeners: Array<{ key: string; listener: (newValue: T) => void }> = [] 199 | return { 200 | callListeners(eventKey: string, newValue: T) { 201 | if (newValue === undefined) { 202 | return 203 | } 204 | listeners.filter(({ key }) => key === eventKey).forEach(({ listener }) => listener(newValue)) 205 | }, 206 | addListener(key: string, listener: (newValue: any) => void) { 207 | listeners.push({ key, listener }) 208 | if (listeners.length === 1) onFirst() 209 | }, 210 | removeListener(key: string, listener: (newValue: any) => void) { 211 | const index = listeners.indexOf({ key, listener }) 212 | if (index !== -1) { 213 | listeners.splice(index, 1) 214 | } 215 | if (listeners.length === 0) onEmptied() 216 | }, 217 | } 218 | } 219 | 220 | function getBrowserStorage(browserStorage: Storage, listenExternalChanges = false): SelfUpdateStorageInterface { 221 | const listenerFunction = (event: StorageEvent) => { 222 | const eventKey = event.key 223 | if (event.storageArea === browserStorage) { 224 | callListeners(eventKey, deserialize(event.newValue)) 225 | } 226 | } 227 | const connect = () => { 228 | if (listenExternalChanges && typeof window !== "undefined" && window?.addEventListener) { 229 | window.addEventListener("storage", listenerFunction) 230 | } 231 | } 232 | const disconnect = () => { 233 | if (listenExternalChanges && typeof window !== "undefined" && window?.removeEventListener) { 234 | window.removeEventListener("storage", listenerFunction) 235 | } 236 | } 237 | const { removeListener, callListeners, addListener } = createListenerFunctions(connect, disconnect) 238 | 239 | return { 240 | addListener, 241 | removeListener, 242 | getValue(key: string): any | null { 243 | const value = browserStorage.getItem(key) 244 | return deserialize(value) 245 | }, 246 | deleteValue(key: string) { 247 | browserStorage.removeItem(key) 248 | }, 249 | setValue(key: string, value: any) { 250 | browserStorage.setItem(key, serialize(value)) 251 | }, 252 | } 253 | } 254 | 255 | function windowStorageAvailable(name: "localStorage" | "sessionStorage" | "indexedDB"): boolean { 256 | try { 257 | return typeof window[name] === "object" 258 | } catch { 259 | return false 260 | } 261 | } 262 | 263 | /** 264 | * Storage implementation that use the browser local storage 265 | * @param {boolean} listenExternalChanges Update the store if the localStorage is updated from another page 266 | */ 267 | export function createLocalStorage(listenExternalChanges = false): StorageInterface { 268 | if (windowStorageAvailable("localStorage")) { 269 | return getBrowserStorage(window.localStorage, listenExternalChanges) 270 | } 271 | warnStorageNotFound("window.localStorage") 272 | return createNoopStorage() 273 | } 274 | 275 | /** 276 | * Storage implementation that use the browser session storage 277 | * @param {boolean} listenExternalChanges Update the store if the sessionStorage is updated from another page 278 | */ 279 | export function createSessionStorage(listenExternalChanges = false): StorageInterface { 280 | if (windowStorageAvailable("sessionStorage")) { 281 | return getBrowserStorage(window.sessionStorage, listenExternalChanges) 282 | } 283 | warnStorageNotFound("window.sessionStorage") 284 | return createNoopStorage() 285 | } 286 | 287 | /** 288 | * Storage implementation that use the browser cookies 289 | */ 290 | export function createCookieStorage(cookieOptions?: CookieOptions): StorageInterface { 291 | if (typeof document === "undefined" || typeof document?.cookie !== "string") { 292 | warnStorageNotFound("document.cookies") 293 | return createNoopStorage() 294 | } 295 | 296 | return { 297 | getValue(key: string): any | null { 298 | const value = getCookie(key) 299 | return deserialize(value) 300 | }, 301 | deleteValue(key: string) { 302 | removeCookie(key, { samesite: "Strict", ...cookieOptions }) 303 | }, 304 | setValue(key: string, value: any) { 305 | setCookie(key, serialize(value), { samesite: "Strict", ...cookieOptions }) 306 | }, 307 | } 308 | } 309 | 310 | /** 311 | * Storage implementation that use the browser IndexedDB 312 | */ 313 | export function createIndexedDBStorage(): SelfUpdateStorageInterface { 314 | if (typeof indexedDB !== "object" || !windowStorageAvailable("indexedDB")) { 315 | warnStorageNotFound("IndexedDB") 316 | return createNoopSelfUpdateStorage() 317 | } 318 | 319 | const { removeListener, callListeners, addListener } = createListenerFunctions() 320 | const database = createStore("svelte-persist", "persist") 321 | return { 322 | addListener, 323 | removeListener, 324 | getValue(key: string): T | null { 325 | get(key, database).then((value) => callListeners(key, deserialize(value) as T)) 326 | return null 327 | }, 328 | setValue(key: string, value: T): void { 329 | set(key, serialize(value), database) 330 | }, 331 | deleteValue(key: string): void { 332 | del(key, database) 333 | }, 334 | } 335 | } 336 | 337 | export enum CHROME_STORAGE_TYPE { 338 | LOCAL, 339 | SESSION, 340 | SYNC, 341 | } 342 | export function createChromeStorage( 343 | storageType: CHROME_STORAGE_TYPE = CHROME_STORAGE_TYPE.LOCAL, 344 | listenExternalChanges = false 345 | ): SelfUpdateStorageInterface { 346 | if (typeof chrome !== "object" || typeof chrome.storage !== "object") { 347 | warnStorageNotFound("ChromeStorage") 348 | return createNoopSelfUpdateStorage() 349 | } 350 | 351 | let area = "local" 352 | switch (storageType) { 353 | case CHROME_STORAGE_TYPE.LOCAL: 354 | area = "local" 355 | break 356 | case CHROME_STORAGE_TYPE.SYNC: 357 | area = "sync" 358 | break 359 | case CHROME_STORAGE_TYPE.SESSION: 360 | area = "session" 361 | break 362 | } 363 | 364 | function externalChangesListener(changes: Record, areaName) { 365 | if (areaName !== area) return 366 | for (const [key, { newValue }] of Object.entries(changes)) { 367 | callListeners(key, newValue) 368 | } 369 | } 370 | 371 | const { removeListener, callListeners, addListener } = createListenerFunctions( 372 | () => { 373 | if (listenExternalChanges) { 374 | chrome.storage.onChanged.addListener(externalChangesListener) 375 | } 376 | }, 377 | () => { 378 | if (listenExternalChanges) { 379 | chrome.storage.onChanged.removeListener(externalChangesListener) 380 | } 381 | } 382 | ) 383 | 384 | return { 385 | addListener, 386 | removeListener, 387 | getValue(key: string): T | null { 388 | chrome.storage[area].get([key], (result) => callListeners(key, result.key)) 389 | return null 390 | }, 391 | setValue(key: string, value: T): void { 392 | chrome.storage[area].set({ [key]: value }) 393 | }, 394 | deleteValue(key: string): void { 395 | chrome.storage[area].remove(key) 396 | }, 397 | } 398 | } 399 | 400 | /** 401 | * Storage implementation that do nothing 402 | */ 403 | export function createNoopStorage(): StorageInterface { 404 | return { 405 | getValue(): null { 406 | return null 407 | }, 408 | deleteValue() { 409 | // Do nothing 410 | }, 411 | setValue() { 412 | // Do nothing 413 | }, 414 | } 415 | } 416 | 417 | function createNoopSelfUpdateStorage(): SelfUpdateStorageInterface { 418 | return { 419 | ...createNoopStorage(), 420 | addListener() { 421 | // Do nothing 422 | }, 423 | removeListener() { 424 | // Do nothing 425 | }, 426 | } 427 | } 428 | --------------------------------------------------------------------------------