@pyreon/storage 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +552 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +540 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +220 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +50 -0
- package/src/clear.ts +72 -0
- package/src/cookie.ts +165 -0
- package/src/custom.ts +128 -0
- package/src/index.ts +51 -0
- package/src/indexed-db.ts +205 -0
- package/src/local.ts +143 -0
- package/src/registry.ts +66 -0
- package/src/session.ts +58 -0
- package/src/tests/clear.test.ts +104 -0
- package/src/tests/cookie.test.ts +149 -0
- package/src/tests/custom.test.ts +207 -0
- package/src/tests/indexed-db.test.ts +74 -0
- package/src/tests/local.test.ts +215 -0
- package/src/tests/session.test.ts +67 -0
- package/src/types.ts +86 -0
- package/src/utils.ts +66 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { getEntry, removeEntry, setEntry } from './registry'
|
|
3
|
+
import type { IndexedDBOptions, StorageSignal } from './types'
|
|
4
|
+
import { deserialize, isBrowser, serialize } from './utils'
|
|
5
|
+
|
|
6
|
+
// ─── Database management ─────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const dbCache = new Map<string, Promise<IDBDatabase>>()
|
|
9
|
+
|
|
10
|
+
function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
|
|
11
|
+
const cacheKey = `${dbName}:${storeName}`
|
|
12
|
+
const cached = dbCache.get(cacheKey)
|
|
13
|
+
if (cached) return cached
|
|
14
|
+
|
|
15
|
+
const promise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
16
|
+
const request = indexedDB.open(dbName, 1)
|
|
17
|
+
|
|
18
|
+
request.onupgradeneeded = () => {
|
|
19
|
+
const db = request.result
|
|
20
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
21
|
+
db.createObjectStore(storeName)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
request.onsuccess = () => resolve(request.result)
|
|
26
|
+
request.onerror = () => reject(request.error)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
dbCache.set(cacheKey, promise)
|
|
30
|
+
return promise
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function idbGet(
|
|
34
|
+
db: IDBDatabase,
|
|
35
|
+
storeName: string,
|
|
36
|
+
key: string,
|
|
37
|
+
): Promise<string | null> {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const tx = db.transaction(storeName, 'readonly')
|
|
40
|
+
const store = tx.objectStore(storeName)
|
|
41
|
+
const request = store.get(key)
|
|
42
|
+
request.onsuccess = () =>
|
|
43
|
+
resolve(request.result !== undefined ? (request.result as string) : null)
|
|
44
|
+
request.onerror = () => reject(request.error)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function idbSet(
|
|
49
|
+
db: IDBDatabase,
|
|
50
|
+
storeName: string,
|
|
51
|
+
key: string,
|
|
52
|
+
value: string,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const tx = db.transaction(storeName, 'readwrite')
|
|
56
|
+
const store = tx.objectStore(storeName)
|
|
57
|
+
const request = store.put(value, key)
|
|
58
|
+
request.onsuccess = () => resolve()
|
|
59
|
+
request.onerror = () => reject(request.error)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function idbDelete(
|
|
64
|
+
db: IDBDatabase,
|
|
65
|
+
storeName: string,
|
|
66
|
+
key: string,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const tx = db.transaction(storeName, 'readwrite')
|
|
70
|
+
const store = tx.objectStore(storeName)
|
|
71
|
+
const request = store.delete(key)
|
|
72
|
+
request.onsuccess = () => resolve()
|
|
73
|
+
request.onerror = () => reject(request.error)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── useIndexedDB ────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Reactive signal backed by IndexedDB. Suitable for large or structured
|
|
81
|
+
* data that exceeds localStorage limits. Writes are debounced.
|
|
82
|
+
*
|
|
83
|
+
* The signal starts with `defaultValue` and updates asynchronously
|
|
84
|
+
* when the stored value is read from IndexedDB.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const draft = useIndexedDB('article-draft', { title: '', body: '' })
|
|
89
|
+
* draft() // { title: '', body: '' } initially, then stored value
|
|
90
|
+
* draft.set({ title: 'My Post', body: '...' }) // signal updates immediately, IDB write is debounced
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function useIndexedDB<T>(
|
|
94
|
+
key: string,
|
|
95
|
+
defaultValue: T,
|
|
96
|
+
options: IndexedDBOptions<T> = {},
|
|
97
|
+
): StorageSignal<T> {
|
|
98
|
+
// Return existing signal if already registered
|
|
99
|
+
const existing = getEntry<T>('indexeddb', key)
|
|
100
|
+
if (existing) return existing.signal
|
|
101
|
+
|
|
102
|
+
const dbName = options.dbName ?? 'pyreon-storage'
|
|
103
|
+
const storeName = options.storeName ?? 'kv'
|
|
104
|
+
const debounceMs = options.debounceMs ?? 100
|
|
105
|
+
|
|
106
|
+
const sig = signal<T>(defaultValue)
|
|
107
|
+
|
|
108
|
+
// Async initial load
|
|
109
|
+
if (isBrowser() && typeof indexedDB !== 'undefined') {
|
|
110
|
+
openDB(dbName, storeName)
|
|
111
|
+
.then((db) => idbGet(db, storeName, key))
|
|
112
|
+
.then((raw) => {
|
|
113
|
+
if (raw !== null) {
|
|
114
|
+
const value = deserialize(
|
|
115
|
+
raw,
|
|
116
|
+
defaultValue,
|
|
117
|
+
options.deserializer,
|
|
118
|
+
options.onError,
|
|
119
|
+
)
|
|
120
|
+
sig.set(value)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.catch(() => {
|
|
124
|
+
// IndexedDB not available — signal keeps defaultValue
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Debounced write
|
|
129
|
+
let writeTimer: ReturnType<typeof setTimeout> | null = null
|
|
130
|
+
let pendingValue: T | undefined
|
|
131
|
+
|
|
132
|
+
function flushWrite(): void {
|
|
133
|
+
if (pendingValue === undefined) return
|
|
134
|
+
const value = pendingValue
|
|
135
|
+
pendingValue = undefined
|
|
136
|
+
|
|
137
|
+
if (!isBrowser() || typeof indexedDB === 'undefined') return
|
|
138
|
+
|
|
139
|
+
openDB(dbName, storeName)
|
|
140
|
+
.then((db) =>
|
|
141
|
+
idbSet(db, storeName, key, serialize(value, options.serializer)),
|
|
142
|
+
)
|
|
143
|
+
.catch(() => {
|
|
144
|
+
// Write failed — signal still has the correct value
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function scheduleWrite(value: T): void {
|
|
149
|
+
pendingValue = value
|
|
150
|
+
if (writeTimer !== null) clearTimeout(writeTimer)
|
|
151
|
+
writeTimer = setTimeout(flushWrite, debounceMs)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Build the storage signal
|
|
155
|
+
const storageSig = (() => sig()) as unknown as StorageSignal<T>
|
|
156
|
+
|
|
157
|
+
storageSig.peek = () => sig.peek()
|
|
158
|
+
storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)
|
|
159
|
+
storageSig.direct = (updater: () => void) => sig.direct(updater)
|
|
160
|
+
storageSig.debug = () => sig.debug()
|
|
161
|
+
|
|
162
|
+
Object.defineProperty(storageSig, 'label', {
|
|
163
|
+
get: () => sig.label,
|
|
164
|
+
set: (v: string | undefined) => {
|
|
165
|
+
sig.label = v
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
storageSig.set = (value: T) => {
|
|
170
|
+
sig.set(value)
|
|
171
|
+
scheduleWrite(value)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
storageSig.update = (fn: (current: T) => T) => {
|
|
175
|
+
const newValue = fn(sig.peek())
|
|
176
|
+
storageSig.set(newValue)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
storageSig.remove = () => {
|
|
180
|
+
sig.set(defaultValue)
|
|
181
|
+
pendingValue = undefined
|
|
182
|
+
if (writeTimer !== null) clearTimeout(writeTimer)
|
|
183
|
+
|
|
184
|
+
if (isBrowser() && typeof indexedDB !== 'undefined') {
|
|
185
|
+
openDB(dbName, storeName)
|
|
186
|
+
.then((db) => idbDelete(db, storeName, key))
|
|
187
|
+
.catch(() => {
|
|
188
|
+
// Delete failed — signal already reset
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
removeEntry('indexeddb', key)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
setEntry('indexeddb', key, storageSig, defaultValue)
|
|
196
|
+
|
|
197
|
+
return storageSig
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Reset the database cache. For testing only.
|
|
202
|
+
*/
|
|
203
|
+
export function _resetDBCache(): void {
|
|
204
|
+
dbCache.clear()
|
|
205
|
+
}
|
package/src/local.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { getEntry, removeEntry, setEntry } from './registry'
|
|
3
|
+
import type { StorageOptions, StorageSignal } from './types'
|
|
4
|
+
import { deserialize, getWebStorage, isBrowser, serialize } from './utils'
|
|
5
|
+
|
|
6
|
+
// ─── Cross-tab sync ──────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
let listenerAttached = false
|
|
9
|
+
|
|
10
|
+
function attachStorageListener(): void {
|
|
11
|
+
if (listenerAttached || !isBrowser()) return
|
|
12
|
+
listenerAttached = true
|
|
13
|
+
|
|
14
|
+
window.addEventListener('storage', (e) => {
|
|
15
|
+
if (!e.key) return
|
|
16
|
+
const entry = getEntry('local', e.key)
|
|
17
|
+
if (!entry) return
|
|
18
|
+
|
|
19
|
+
const newValue =
|
|
20
|
+
e.newValue !== null
|
|
21
|
+
? deserialize(e.newValue, entry.defaultValue)
|
|
22
|
+
: entry.defaultValue
|
|
23
|
+
|
|
24
|
+
entry.signal.set(newValue)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── useStorage ──────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reactive signal backed by localStorage. Automatically syncs across
|
|
32
|
+
* browser tabs via the native `storage` event.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const theme = useStorage('theme', 'light')
|
|
37
|
+
* theme() // 'light' (or stored value)
|
|
38
|
+
* theme.set('dark') // updates signal + localStorage
|
|
39
|
+
* theme.remove() // clears storage, resets to default
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function useStorage<T>(
|
|
43
|
+
key: string,
|
|
44
|
+
defaultValue: T,
|
|
45
|
+
options?: StorageOptions<T>,
|
|
46
|
+
): StorageSignal<T> {
|
|
47
|
+
// Return existing signal if already registered
|
|
48
|
+
const existing = getEntry<T>('local', key)
|
|
49
|
+
if (existing) return existing.signal
|
|
50
|
+
|
|
51
|
+
const storage = getWebStorage('local')
|
|
52
|
+
|
|
53
|
+
// Read initial value from storage
|
|
54
|
+
let initialValue = defaultValue
|
|
55
|
+
if (storage) {
|
|
56
|
+
const raw = storage.getItem(key)
|
|
57
|
+
if (raw !== null) {
|
|
58
|
+
initialValue = deserialize(
|
|
59
|
+
raw,
|
|
60
|
+
defaultValue,
|
|
61
|
+
options?.deserializer,
|
|
62
|
+
options?.onError,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sig = signal<T>(initialValue)
|
|
68
|
+
|
|
69
|
+
// Create the storage signal by extending the base signal
|
|
70
|
+
const storageSig = createStorageSignal(
|
|
71
|
+
sig,
|
|
72
|
+
key,
|
|
73
|
+
defaultValue,
|
|
74
|
+
'local',
|
|
75
|
+
options,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
setEntry('local', key, storageSig, defaultValue)
|
|
79
|
+
attachStorageListener()
|
|
80
|
+
|
|
81
|
+
return storageSig
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Storage Signal Factory ──────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wraps a base signal with storage persistence behavior.
|
|
88
|
+
* Used by both useStorage and useSessionStorage.
|
|
89
|
+
*/
|
|
90
|
+
export function createStorageSignal<T>(
|
|
91
|
+
sig: ReturnType<typeof signal<T>>,
|
|
92
|
+
key: string,
|
|
93
|
+
defaultValue: T,
|
|
94
|
+
backend: 'local' | 'session',
|
|
95
|
+
options?: StorageOptions<T>,
|
|
96
|
+
): StorageSignal<T> {
|
|
97
|
+
const storage = getWebStorage(backend)
|
|
98
|
+
|
|
99
|
+
// The callable signal function (read)
|
|
100
|
+
const storageSig = (() => sig()) as unknown as StorageSignal<T>
|
|
101
|
+
|
|
102
|
+
// Delegate all signal methods
|
|
103
|
+
storageSig.peek = () => sig.peek()
|
|
104
|
+
storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)
|
|
105
|
+
storageSig.direct = (updater: () => void) => sig.direct(updater)
|
|
106
|
+
storageSig.debug = () => sig.debug()
|
|
107
|
+
|
|
108
|
+
Object.defineProperty(storageSig, 'label', {
|
|
109
|
+
get: () => sig.label,
|
|
110
|
+
set: (v: string | undefined) => {
|
|
111
|
+
sig.label = v
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Override set to persist
|
|
116
|
+
storageSig.set = (value: T) => {
|
|
117
|
+
sig.set(value)
|
|
118
|
+
if (storage) {
|
|
119
|
+
try {
|
|
120
|
+
storage.setItem(key, serialize(value, options?.serializer))
|
|
121
|
+
} catch {
|
|
122
|
+
// Storage full or blocked — signal still updates
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Override update to persist
|
|
128
|
+
storageSig.update = (fn: (current: T) => T) => {
|
|
129
|
+
const newValue = fn(sig.peek())
|
|
130
|
+
storageSig.set(newValue)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add remove method
|
|
134
|
+
storageSig.remove = () => {
|
|
135
|
+
sig.set(defaultValue)
|
|
136
|
+
if (storage) {
|
|
137
|
+
storage.removeItem(key)
|
|
138
|
+
}
|
|
139
|
+
removeEntry(backend, key)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return storageSig
|
|
143
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StorageSignal } from './types'
|
|
2
|
+
|
|
3
|
+
// ─── Signal Registry ─────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
interface RegistryEntry<T = unknown> {
|
|
6
|
+
signal: StorageSignal<T>
|
|
7
|
+
defaultValue: T
|
|
8
|
+
backend: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const registry = new Map<string, RegistryEntry>()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a composite key from backend type + storage key to avoid
|
|
15
|
+
* collisions between different backends using the same key name.
|
|
16
|
+
*/
|
|
17
|
+
function registryKey(backend: string, key: string): string {
|
|
18
|
+
return `${backend}:${key}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get an existing signal from the registry.
|
|
23
|
+
*/
|
|
24
|
+
export function getEntry<T>(
|
|
25
|
+
backend: string,
|
|
26
|
+
key: string,
|
|
27
|
+
): RegistryEntry<T> | undefined {
|
|
28
|
+
return registry.get(registryKey(backend, key)) as RegistryEntry<T> | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a new signal in the registry.
|
|
33
|
+
*/
|
|
34
|
+
export function setEntry<T>(
|
|
35
|
+
backend: string,
|
|
36
|
+
key: string,
|
|
37
|
+
signal: StorageSignal<T>,
|
|
38
|
+
defaultValue: T,
|
|
39
|
+
): void {
|
|
40
|
+
registry.set(registryKey(backend, key), { signal, defaultValue, backend })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove an entry from the registry.
|
|
45
|
+
*/
|
|
46
|
+
export function removeEntry(backend: string, key: string): void {
|
|
47
|
+
registry.delete(registryKey(backend, key))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all entries for a specific backend.
|
|
52
|
+
*/
|
|
53
|
+
export function getEntriesByBackend(backend: string): RegistryEntry[] {
|
|
54
|
+
const entries: RegistryEntry[] = []
|
|
55
|
+
for (const entry of registry.values()) {
|
|
56
|
+
if (entry.backend === backend) entries.push(entry)
|
|
57
|
+
}
|
|
58
|
+
return entries
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear all entries from the registry. Used for testing.
|
|
63
|
+
*/
|
|
64
|
+
export function _resetRegistry(): void {
|
|
65
|
+
registry.clear()
|
|
66
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { createStorageSignal } from './local'
|
|
3
|
+
import { getEntry, setEntry } from './registry'
|
|
4
|
+
import type { StorageOptions, StorageSignal } from './types'
|
|
5
|
+
import { deserialize, getWebStorage } from './utils'
|
|
6
|
+
|
|
7
|
+
// ─── useSessionStorage ───────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reactive signal backed by sessionStorage. Scoped to the current
|
|
11
|
+
* browser tab — does not sync across tabs.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const step = useSessionStorage('wizard-step', 0)
|
|
16
|
+
* step() // 0 (or stored value)
|
|
17
|
+
* step.set(3) // updates signal + sessionStorage
|
|
18
|
+
* step.remove() // clears storage, resets to default
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useSessionStorage<T>(
|
|
22
|
+
key: string,
|
|
23
|
+
defaultValue: T,
|
|
24
|
+
options?: StorageOptions<T>,
|
|
25
|
+
): StorageSignal<T> {
|
|
26
|
+
// Return existing signal if already registered
|
|
27
|
+
const existing = getEntry<T>('session', key)
|
|
28
|
+
if (existing) return existing.signal
|
|
29
|
+
|
|
30
|
+
const storage = getWebStorage('session')
|
|
31
|
+
|
|
32
|
+
// Read initial value from storage
|
|
33
|
+
let initialValue = defaultValue
|
|
34
|
+
if (storage) {
|
|
35
|
+
const raw = storage.getItem(key)
|
|
36
|
+
if (raw !== null) {
|
|
37
|
+
initialValue = deserialize(
|
|
38
|
+
raw,
|
|
39
|
+
defaultValue,
|
|
40
|
+
options?.deserializer,
|
|
41
|
+
options?.onError,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sig = signal<T>(initialValue)
|
|
47
|
+
const storageSig = createStorageSignal(
|
|
48
|
+
sig,
|
|
49
|
+
key,
|
|
50
|
+
defaultValue,
|
|
51
|
+
'session',
|
|
52
|
+
options,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
setEntry('session', key, storageSig, defaultValue)
|
|
56
|
+
|
|
57
|
+
return storageSig
|
|
58
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
_resetRegistry,
|
|
4
|
+
clearStorage,
|
|
5
|
+
removeStorage,
|
|
6
|
+
useSessionStorage,
|
|
7
|
+
useStorage,
|
|
8
|
+
} from '../index'
|
|
9
|
+
|
|
10
|
+
describe('removeStorage', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
localStorage.clear()
|
|
13
|
+
sessionStorage.clear()
|
|
14
|
+
_resetRegistry()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
localStorage.clear()
|
|
19
|
+
sessionStorage.clear()
|
|
20
|
+
_resetRegistry()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('removes a localStorage entry via signal', () => {
|
|
24
|
+
const theme = useStorage('theme', 'light')
|
|
25
|
+
theme.set('dark')
|
|
26
|
+
|
|
27
|
+
removeStorage('theme')
|
|
28
|
+
expect(theme()).toBe('light')
|
|
29
|
+
expect(localStorage.getItem('theme')).toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('removes a sessionStorage entry', () => {
|
|
33
|
+
const step = useSessionStorage('step', 0)
|
|
34
|
+
step.set(3)
|
|
35
|
+
|
|
36
|
+
removeStorage('step', { type: 'session' })
|
|
37
|
+
expect(step()).toBe(0)
|
|
38
|
+
expect(sessionStorage.getItem('step')).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('removes raw localStorage even without a signal', () => {
|
|
42
|
+
localStorage.setItem('orphan', 'value')
|
|
43
|
+
removeStorage('orphan')
|
|
44
|
+
expect(localStorage.getItem('orphan')).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('removes raw sessionStorage even without a signal', () => {
|
|
48
|
+
sessionStorage.setItem('orphan', 'value')
|
|
49
|
+
removeStorage('orphan', { type: 'session' })
|
|
50
|
+
expect(sessionStorage.getItem('orphan')).toBeNull()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('removes cookie without a signal', () => {
|
|
54
|
+
removeStorage('orphan-cookie', { type: 'cookie' })
|
|
55
|
+
// Should not throw even when cookie doesn't exist
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('clearStorage', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
localStorage.clear()
|
|
62
|
+
sessionStorage.clear()
|
|
63
|
+
_resetRegistry()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
localStorage.clear()
|
|
68
|
+
sessionStorage.clear()
|
|
69
|
+
_resetRegistry()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('clears all managed localStorage entries', () => {
|
|
73
|
+
const a = useStorage('a', 1)
|
|
74
|
+
const b = useStorage('b', 2)
|
|
75
|
+
a.set(10)
|
|
76
|
+
b.set(20)
|
|
77
|
+
|
|
78
|
+
clearStorage()
|
|
79
|
+
expect(a()).toBe(1)
|
|
80
|
+
expect(b()).toBe(2)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('clears all managed sessionStorage entries', () => {
|
|
84
|
+
const a = useSessionStorage('a', 'x')
|
|
85
|
+
const b = useSessionStorage('b', 'y')
|
|
86
|
+
a.set('modified')
|
|
87
|
+
b.set('modified')
|
|
88
|
+
|
|
89
|
+
clearStorage('session')
|
|
90
|
+
expect(a()).toBe('x')
|
|
91
|
+
expect(b()).toBe('y')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('clears all backends with "all"', () => {
|
|
95
|
+
const local = useStorage('l', 'default')
|
|
96
|
+
const session = useSessionStorage('s', 'default')
|
|
97
|
+
local.set('changed')
|
|
98
|
+
session.set('changed')
|
|
99
|
+
|
|
100
|
+
clearStorage('all')
|
|
101
|
+
expect(local()).toBe('default')
|
|
102
|
+
expect(session()).toBe('default')
|
|
103
|
+
})
|
|
104
|
+
})
|