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 |
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 | 
6 | 
7 | 
8 | 
9 | 
10 | 
11 | 
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 |
70 |
71 |
75 |
76 |
80 |
81 |
85 |
86 |
132 |
133 |
134 |
135 | The current name is: {$classExample.getName()}.
136 | The current object type is: {$classExample.constructor.name}.
137 |
138 |
139 |
140 |
141 |
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 |
--------------------------------------------------------------------------------