Skip to content

Commit 2c9f832

Browse files
committed
Add IndexedDB Storage
1 parent 526547c commit 2c9f832

11 files changed

+167
-20
lines changed

.docs/01-Basic.md

+23
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,27 @@
5959
<footer>
6060
<input name="email"><label><input type="checkbox" bind:checked={$newsLetterSubscription}> I want to receive news by email</label>
6161
</footer>
62+
```
63+
64+
## indexedDB storage
65+
66+
```html
67+
<script>
68+
import { persist, indexedDBStorage } from "@macfja/svelte-persistent-store"
69+
import { writable } from "svelte/store"
70+
71+
const layout = persist(writable('2column'), indexedDBStorage(), 'myapp-layout')
72+
</script>
73+
74+
<aside>
75+
<label>
76+
<span>Select the application layout:</span>
77+
<select bind:value={$layout}>
78+
<option value="2column">2 columns (Right)</option>
79+
<option value="2column-left">2 columns (Left)</option>
80+
<option value="stacked">Stacked</option>
81+
<option value="stacked-rev">Stacked Reversed</option>
82+
</select>
83+
</label>
84+
</aside>
6285
```

.docs/05-New-Async-Storage.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Create a new asynchronous storage
2+
3+
There is a workaround (trickery) to work with asynchronous data storage.
4+
(Remember, `StorageInterface.getValue` should synchronously return a value)
5+
6+
The idea is to use the `SelfUpdateStorageInterface` interface to deliver the value when it finally arrived.
7+
8+
The `IndexedDBStorage` use this workaround.
9+
10+
## Quick example
11+
12+
```js
13+
function myStorage<T>(): SelfUpdateStorageInterface<T> {
14+
const listeners: Array<{key: string, listener: (newValue: T) => void}> = []
15+
const listenerFunction = (eventKey: string, newValue: T) => {
16+
listeners
17+
.filter(({key}) => key === eventKey)
18+
.forEach(({listener}) => listener(newValue))
19+
}
20+
return {
21+
getValue(key: string): T | null {
22+
readRealStorageWithPromise(key).then(value => listenerFunction(key, value))
23+
return null // Tell the store to use current decorated store value
24+
},
25+
// ... addListener, removeListener, setValue, deleteValue
26+
}
27+
}
28+
```

.docs/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88

99
## How to
1010

11-
- [Add new storage](04-New-Storage.md)
11+
- [Add new storage](04-New-Storage.md)
12+
- [Add an asynchronous storage](05-New-Async-Storage.md)

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
- Add external change listener for SessionStorage and LocalStorage
1212
- Add documentation
13+
- Add IndexedDB Storage
1314

1415
## [1.0.2]
1516

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ $name = 'Jeanne Doe'
2323

2424
## Storages
2525

26-
There are 3 storages built-in:
26+
There are 4 storages built-in:
2727

2828
- `localStorage()`, that use `window.localStorage` to save values
2929
- `sessionStorage()`, that use `window.sessionStorage` to save values
3030
- `cookieStorage()`, that use `document.cookie` to save values
31+
- `indexedDBStorage()`, that use `window.indexedDB` to save values
3132

3233
You can add more storages, you just need to implement the interface `StorageInterface`
3334

package-lock.json

+6-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"README.md"
1313
],
1414
"dependencies": {
15+
"idb-keyval": "^5.0.4",
1516
"js-cookies": "^1.0.4"
1617
},
1718
"devDependencies": {
@@ -62,6 +63,7 @@
6263
"persistent",
6364
"localStorage",
6465
"sessionStorage",
66+
"indexedDB",
6567
"cookie",
6668
"svelte",
6769
"sveltejs"

pagesconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
{
2020
"title": "How to",
2121
"pages": [
22-
{"title": "Add new storage", "source": "/.docs/04-New-Storage.md"}
22+
{"title": "Add new storage", "source": "/.docs/04-New-Storage.md"},
23+
{"title": "Add an asynchronous storage", "source": ".docs/05-New-Async-Storage.md"}
2324
]
2425
}
2526
]

src/index.ts

+80-14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { get, set, createStore, del } from "idb-keyval"
12
import Cookies from "js-cookies/src/cookies.js"
23
import type { Writable } from "svelte/store"
34

@@ -41,14 +42,16 @@ export interface StorageInterface<T> {
4142
export interface SelfUpdateStorageInterface<T> extends StorageInterface<T> {
4243
/**
4344
* Add a listener to the storage values changes
44-
* @param {(key: string) => void} listener The listener callback function
45+
* @param {string} key The key to listen
46+
* @param {(newValue: T) => void} listener The listener callback function
4547
*/
46-
addListener(listener: (key: string) => void): void;
48+
addListener(key: string, listener: (newValue: T) => void): void;
4749
/**
4850
* Remove a listener from the storage values changes
49-
* @param {(key: string) => void} listener The listener callback function to remove
51+
* @param {string} key The key that was listened
52+
* @param {(newValue: T) => void} listener The listener callback function to remove
5053
*/
51-
removeListener(listener: (key: string) => void): void;
54+
removeListener(key: string, listener: (newValue: T) => void): void;
5255
}
5356

5457
/**
@@ -65,10 +68,8 @@ export function persist<T>(store: Writable<T>, storage: StorageInterface<T>, key
6568
}
6669

6770
if ((storage as SelfUpdateStorageInterface<T>).addListener) {
68-
(storage as SelfUpdateStorageInterface<T>).addListener(eventKey => {
69-
if (eventKey === key) {
70-
store.set(storage.getValue(key))
71-
}
71+
(storage as SelfUpdateStorageInterface<T>).addListener(key, newValue => {
72+
store.set(newValue)
7273
})
7374
}
7475

@@ -85,10 +86,13 @@ export function persist<T>(store: Writable<T>, storage: StorageInterface<T>, key
8586
}
8687

8788
function getBrowserStorage(browserStorage: Storage, listenExternalChanges = false): SelfUpdateStorageInterface<any> {
88-
const listeners: Array<(key: string) => void> = []
89+
const listeners: Array<{key: string, listener: (newValue: any) => void}> = []
8990
const listenerFunction = (event: StorageEvent) => {
91+
const eventKey = event.key
9092
if (event.storageArea === browserStorage) {
91-
listeners.forEach(call => call(event.key))
93+
listeners
94+
.filter(({key}) => key === eventKey)
95+
.forEach(({listener}) => listener(JSON.parse(event.newValue)))
9296
}
9397
}
9498
const connect = () => {
@@ -103,14 +107,14 @@ function getBrowserStorage(browserStorage: Storage, listenExternalChanges = fals
103107
}
104108

105109
return {
106-
addListener(listener: (key: string) => void) {
107-
listeners.push(listener)
110+
addListener(key: string, listener: (newValue: any) => void) {
111+
listeners.push({key, listener})
108112
if (listeners.length === 1) {
109113
connect()
110114
}
111115
},
112-
removeListener(listener: (key: string) => void) {
113-
const index = listeners.indexOf(listener)
116+
removeListener(key: string, listener: (newValue: any) => void) {
117+
const index = listeners.indexOf({key, listener})
114118
if (index !== -1) {
115119
listeners.splice(index, 1)
116120
}
@@ -184,6 +188,48 @@ export function cookieStorage(): StorageInterface<any> {
184188
}
185189
}
186190

191+
/**
192+
* Storage implementation that use the browser IndexedDB
193+
*/
194+
export function indexedDBStorage<T>(): SelfUpdateStorageInterface<T> {
195+
if (typeof indexedDB !== "object" || typeof window === "undefined" || typeof window?.indexedDB !== "object") {
196+
console.warn("Unable to find the IndexedDB. No data will be persisted.")
197+
return noopSelfUpdateStorage()
198+
}
199+
200+
const database = createStore("svelte-persist", "persist")
201+
const listeners: Array<{key: string, listener: (newValue: T) => void}> = []
202+
const listenerFunction = (eventKey: string, newValue: T) => {
203+
if (newValue === undefined) {
204+
return
205+
}
206+
listeners
207+
.filter(({key}) => key === eventKey)
208+
.forEach(({listener}) => listener(newValue))
209+
}
210+
return {
211+
addListener(key: string, listener: (newValue: any) => void) {
212+
listeners.push({key, listener})
213+
},
214+
removeListener(key: string, listener: (newValue: any) => void) {
215+
const index = listeners.indexOf({key, listener})
216+
if (index !== -1) {
217+
listeners.splice(index, 1)
218+
}
219+
},
220+
getValue(key: string): T | null {
221+
get(key, database).then(value => listenerFunction(key, value))
222+
return null
223+
},
224+
setValue(key: string, value: T): void {
225+
set(key, value, database)
226+
},
227+
deleteValue(key: string): void {
228+
del(key, database)
229+
}
230+
}
231+
}
232+
187233
/**
188234
* Storage implementation that do nothing
189235
*/
@@ -199,4 +245,24 @@ export function noopStorage(): StorageInterface<any> {
199245
// Do nothing
200246
}
201247
}
248+
}
249+
250+
function noopSelfUpdateStorage(): SelfUpdateStorageInterface<any> {
251+
return {
252+
addListener() {
253+
// Do nothing
254+
},
255+
removeListener() {
256+
// Do nothing
257+
},
258+
getValue(): null {
259+
return null
260+
},
261+
deleteValue() {
262+
// Do nothing
263+
},
264+
setValue() {
265+
// Do nothing
266+
}
267+
}
202268
}

tests/e2e.ts

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ fixture("Svelte Persistent Storage")
66
const cookieInput = Selector('#cookieInput'),
77
localInput = Selector('#localInput'),
88
sessionInput = Selector('#sessionInput'),
9+
indexedInput = Selector('#indexedInput'),
910
documentCookie = Selector('#documentCookie'),
1011
reloadButton = Selector('#reloadButton'),
1112
clearButton = Selector('#clearButton')
@@ -17,6 +18,7 @@ test("Initial state", async t => {
1718
.expect(cookieInput.value).eql("John")
1819
.expect(localInput.value).eql('Foo')
1920
.expect(sessionInput.value).eql('Bar')
21+
.expect(indexedInput.value).eql('Hello')
2022
.expect(documentCookie.textContent).contains("sps-userName=%22John%22")
2123
})
2224

@@ -54,4 +56,15 @@ test("Session storage", async t => {
5456
.click(clearButton)
5557
.click(reloadButton)
5658
.expect(sessionInput.value).eql("Bar")
59+
})
60+
61+
test("IndexedDB storage", async t => {
62+
await t
63+
.expect(indexedInput.value).eql("Hello")
64+
.typeText(indexedInput, " World")
65+
.click(reloadButton)
66+
.expect(indexedInput.value).eql("Hello World")
67+
.click(clearButton)
68+
.click(reloadButton)
69+
.expect(indexedInput.value).eql("Hello")
5770
})

tests/src/App.svelte

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script>
2-
import {persist, cookieStorage, localStorage, sessionStorage} from "../../src/index"
2+
import {persist, cookieStorage, localStorage, sessionStorage, indexedDBStorage} from "../../src/index"
33
import { writable } from "svelte/store"
44
55
let cookieExample = persist(writable('John'), cookieStorage(), 'sps-userName')
66
let localExample = persist(writable('Foo'), localStorage(), 'sps-action')
77
let sessionExample = persist(writable('Bar'), sessionStorage(), 'sps-call')
8+
let indexedDBExample = persist(writable('Hello'), indexedDBStorage(), 'sps-data')
89
910
let cookie = ''
1011
@@ -31,6 +32,11 @@
3132
<label>Enter a word: <input id="sessionInput" bind:value={$sessionExample}></label>
3233
</fieldset>
3334

35+
<fieldset>
36+
<legend>IndexedDB Storage</legend>
37+
<label>Enter a word: <input id="indexedInput" bind:value={$indexedDBExample}></label>
38+
</fieldset>
39+
3440
<button id="reloadButton" on:click={() => window.location.reload()}>Reload the page</button>
3541

36-
<button id="clearButton" on:click={() => {cookieExample.delete(); localExample.delete(); sessionExample.delete()}}>Clear storages</button>
42+
<button id="clearButton" on:click={() => {cookieExample.delete(); localExample.delete(); sessionExample.delete(); indexedDBExample.delete()}}>Clear storages</button>

0 commit comments

Comments
 (0)