@pyreon/storage 0.11.5 → 0.11.7
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/README.md +10 -10
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +3 -3
- package/package.json +14 -14
- package/src/clear.ts +13 -13
- package/src/cookie.ts +15 -15
- package/src/custom.ts +7 -7
- package/src/index.ts +9 -9
- package/src/indexed-db.ts +16 -16
- package/src/local.ts +12 -12
- package/src/registry.ts +1 -1
- package/src/session.ts +9 -9
- package/src/tests/clear-remove.test.ts +77 -77
- package/src/tests/clear.test.ts +43 -43
- package/src/tests/cookie-options.test.ts +92 -92
- package/src/tests/cookie.test.ts +66 -66
- package/src/tests/cross-tab-sync.test.ts +58 -58
- package/src/tests/custom.test.ts +59 -59
- package/src/tests/indexed-db-debounce.test.ts +43 -43
- package/src/tests/indexed-db.test.ts +36 -36
- package/src/tests/local.test.ts +98 -98
- package/src/tests/memory-storage.test.ts +94 -94
- package/src/tests/session.test.ts +31 -31
- package/src/types.ts +2 -2
- package/src/utils.ts +9 -9
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { effect } from
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it } from
|
|
3
|
-
import { _resetRegistry, useStorage } from
|
|
1
|
+
import { effect } from '@pyreon/reactivity'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
3
|
+
import { _resetRegistry, useStorage } from '../index'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Tests for cross-tab synchronization via the native `storage` event.
|
|
7
7
|
* These test the listener that useStorage attaches to `window.addEventListener('storage', ...)`.
|
|
8
8
|
*/
|
|
9
|
-
describe(
|
|
9
|
+
describe('useStorage — cross-tab sync', () => {
|
|
10
10
|
beforeEach(() => {
|
|
11
11
|
localStorage.clear()
|
|
12
12
|
_resetRegistry()
|
|
@@ -17,93 +17,93 @@ describe("useStorage — cross-tab sync", () => {
|
|
|
17
17
|
_resetRegistry()
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
it(
|
|
21
|
-
const theme = useStorage(
|
|
22
|
-
expect(theme()).toBe(
|
|
20
|
+
it('updates signal when storage event fires with a new value', () => {
|
|
21
|
+
const theme = useStorage('theme', 'light')
|
|
22
|
+
expect(theme()).toBe('light')
|
|
23
23
|
|
|
24
24
|
window.dispatchEvent(
|
|
25
|
-
Object.assign(new Event(
|
|
26
|
-
key:
|
|
27
|
-
newValue: JSON.stringify(
|
|
25
|
+
Object.assign(new Event('storage'), {
|
|
26
|
+
key: 'theme',
|
|
27
|
+
newValue: JSON.stringify('dark'),
|
|
28
28
|
storageArea: localStorage,
|
|
29
29
|
}),
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
expect(theme()).toBe(
|
|
32
|
+
expect(theme()).toBe('dark')
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
it(
|
|
36
|
-
const theme = useStorage(
|
|
37
|
-
theme.set(
|
|
38
|
-
expect(theme()).toBe(
|
|
35
|
+
it('resets to default when storage event fires with null newValue (key deleted)', () => {
|
|
36
|
+
const theme = useStorage('theme', 'light')
|
|
37
|
+
theme.set('dark')
|
|
38
|
+
expect(theme()).toBe('dark')
|
|
39
39
|
|
|
40
40
|
window.dispatchEvent(
|
|
41
|
-
Object.assign(new Event(
|
|
42
|
-
key:
|
|
41
|
+
Object.assign(new Event('storage'), {
|
|
42
|
+
key: 'theme',
|
|
43
43
|
newValue: null,
|
|
44
44
|
storageArea: localStorage,
|
|
45
45
|
}),
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
expect(theme()).toBe(
|
|
48
|
+
expect(theme()).toBe('light')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it(
|
|
52
|
-
const theme = useStorage(
|
|
51
|
+
it('ignores storage events for unregistered keys', () => {
|
|
52
|
+
const theme = useStorage('theme', 'light')
|
|
53
53
|
|
|
54
54
|
window.dispatchEvent(
|
|
55
|
-
Object.assign(new Event(
|
|
56
|
-
key:
|
|
57
|
-
newValue: JSON.stringify(
|
|
55
|
+
Object.assign(new Event('storage'), {
|
|
56
|
+
key: 'unrelated-key',
|
|
57
|
+
newValue: JSON.stringify('value'),
|
|
58
58
|
storageArea: localStorage,
|
|
59
59
|
}),
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
expect(theme()).toBe(
|
|
62
|
+
expect(theme()).toBe('light')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
-
it(
|
|
66
|
-
const theme = useStorage(
|
|
65
|
+
it('ignores storage events with null key', () => {
|
|
66
|
+
const theme = useStorage('theme', 'light')
|
|
67
67
|
|
|
68
68
|
window.dispatchEvent(
|
|
69
|
-
Object.assign(new Event(
|
|
69
|
+
Object.assign(new Event('storage'), {
|
|
70
70
|
key: null,
|
|
71
71
|
newValue: null,
|
|
72
72
|
storageArea: localStorage,
|
|
73
73
|
}),
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
-
expect(theme()).toBe(
|
|
76
|
+
expect(theme()).toBe('light')
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
it(
|
|
80
|
-
const theme = useStorage(
|
|
79
|
+
it('triggers reactive effect on cross-tab update', () => {
|
|
80
|
+
const theme = useStorage('theme', 'light')
|
|
81
81
|
const values: string[] = []
|
|
82
82
|
|
|
83
83
|
effect(() => {
|
|
84
84
|
values.push(theme())
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
-
expect(values).toEqual([
|
|
87
|
+
expect(values).toEqual(['light'])
|
|
88
88
|
|
|
89
89
|
window.dispatchEvent(
|
|
90
|
-
Object.assign(new Event(
|
|
91
|
-
key:
|
|
92
|
-
newValue: JSON.stringify(
|
|
90
|
+
Object.assign(new Event('storage'), {
|
|
91
|
+
key: 'theme',
|
|
92
|
+
newValue: JSON.stringify('dark'),
|
|
93
93
|
storageArea: localStorage,
|
|
94
94
|
}),
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
expect(values).toEqual([
|
|
97
|
+
expect(values).toEqual(['light', 'dark'])
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
it(
|
|
101
|
-
const count = useStorage(
|
|
100
|
+
it('handles corrupt JSON in storage event gracefully (falls back to default)', () => {
|
|
101
|
+
const count = useStorage('count', 0)
|
|
102
102
|
|
|
103
103
|
window.dispatchEvent(
|
|
104
|
-
Object.assign(new Event(
|
|
105
|
-
key:
|
|
106
|
-
newValue:
|
|
104
|
+
Object.assign(new Event('storage'), {
|
|
105
|
+
key: 'count',
|
|
106
|
+
newValue: '{invalid json',
|
|
107
107
|
storageArea: localStorage,
|
|
108
108
|
}),
|
|
109
109
|
)
|
|
@@ -112,27 +112,27 @@ describe("useStorage — cross-tab sync", () => {
|
|
|
112
112
|
expect(count()).toBe(0)
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
it(
|
|
116
|
-
const prefs = useStorage(
|
|
115
|
+
it('syncs object values across tabs', () => {
|
|
116
|
+
const prefs = useStorage('prefs', { sidebar: true, density: 'comfortable' })
|
|
117
117
|
|
|
118
118
|
window.dispatchEvent(
|
|
119
|
-
Object.assign(new Event(
|
|
120
|
-
key:
|
|
121
|
-
newValue: JSON.stringify({ sidebar: false, density:
|
|
119
|
+
Object.assign(new Event('storage'), {
|
|
120
|
+
key: 'prefs',
|
|
121
|
+
newValue: JSON.stringify({ sidebar: false, density: 'compact' }),
|
|
122
122
|
storageArea: localStorage,
|
|
123
123
|
}),
|
|
124
124
|
)
|
|
125
125
|
|
|
126
|
-
expect(prefs()).toEqual({ sidebar: false, density:
|
|
126
|
+
expect(prefs()).toEqual({ sidebar: false, density: 'compact' })
|
|
127
127
|
})
|
|
128
128
|
|
|
129
|
-
it(
|
|
130
|
-
const count = useStorage(
|
|
129
|
+
it('handles multiple rapid storage events', () => {
|
|
130
|
+
const count = useStorage('count', 0)
|
|
131
131
|
|
|
132
132
|
for (let i = 1; i <= 5; i++) {
|
|
133
133
|
window.dispatchEvent(
|
|
134
|
-
Object.assign(new Event(
|
|
135
|
-
key:
|
|
134
|
+
Object.assign(new Event('storage'), {
|
|
135
|
+
key: 'count',
|
|
136
136
|
newValue: JSON.stringify(i),
|
|
137
137
|
storageArea: localStorage,
|
|
138
138
|
}),
|
|
@@ -142,19 +142,19 @@ describe("useStorage — cross-tab sync", () => {
|
|
|
142
142
|
expect(count()).toBe(5)
|
|
143
143
|
})
|
|
144
144
|
|
|
145
|
-
it(
|
|
146
|
-
const theme = useStorage(
|
|
147
|
-
const lang = useStorage(
|
|
145
|
+
it('cross-tab sync works independently for different keys', () => {
|
|
146
|
+
const theme = useStorage('theme', 'light')
|
|
147
|
+
const lang = useStorage('lang', 'en')
|
|
148
148
|
|
|
149
149
|
window.dispatchEvent(
|
|
150
|
-
Object.assign(new Event(
|
|
151
|
-
key:
|
|
152
|
-
newValue: JSON.stringify(
|
|
150
|
+
Object.assign(new Event('storage'), {
|
|
151
|
+
key: 'theme',
|
|
152
|
+
newValue: JSON.stringify('dark'),
|
|
153
153
|
storageArea: localStorage,
|
|
154
154
|
}),
|
|
155
155
|
)
|
|
156
156
|
|
|
157
|
-
expect(theme()).toBe(
|
|
158
|
-
expect(lang()).toBe(
|
|
157
|
+
expect(theme()).toBe('dark')
|
|
158
|
+
expect(lang()).toBe('en') // unchanged
|
|
159
159
|
})
|
|
160
160
|
})
|
package/src/tests/custom.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from
|
|
2
|
-
import { _resetRegistry, createStorage, useMemoryStorage } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { _resetRegistry, createStorage, useMemoryStorage } from '../index'
|
|
3
3
|
|
|
4
|
-
describe(
|
|
4
|
+
describe('createStorage', () => {
|
|
5
5
|
beforeEach(() => {
|
|
6
6
|
_resetRegistry()
|
|
7
7
|
})
|
|
@@ -10,7 +10,7 @@ describe("createStorage", () => {
|
|
|
10
10
|
_resetRegistry()
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
it(
|
|
13
|
+
it('creates a working storage hook from a custom backend', () => {
|
|
14
14
|
const store = new Map<string, string>()
|
|
15
15
|
const useCustom = createStorage({
|
|
16
16
|
get: (k) => store.get(k) ?? null,
|
|
@@ -18,17 +18,17 @@ describe("createStorage", () => {
|
|
|
18
18
|
remove: (k) => store.delete(k),
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
const sig = useCustom(
|
|
22
|
-
expect(sig()).toBe(
|
|
21
|
+
const sig = useCustom('key', 'default')
|
|
22
|
+
expect(sig()).toBe('default')
|
|
23
23
|
|
|
24
|
-
sig.set(
|
|
25
|
-
expect(sig()).toBe(
|
|
26
|
-
expect(store.get(
|
|
24
|
+
sig.set('updated')
|
|
25
|
+
expect(sig()).toBe('updated')
|
|
26
|
+
expect(store.get('key')).toBe(JSON.stringify('updated'))
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
it(
|
|
29
|
+
it('reads existing values from the backend', () => {
|
|
30
30
|
const store = new Map<string, string>()
|
|
31
|
-
store.set(
|
|
31
|
+
store.set('key', JSON.stringify('existing'))
|
|
32
32
|
|
|
33
33
|
const useCustom = createStorage({
|
|
34
34
|
get: (k) => store.get(k) ?? null,
|
|
@@ -36,11 +36,11 @@ describe("createStorage", () => {
|
|
|
36
36
|
remove: (k) => store.delete(k),
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
const sig = useCustom(
|
|
40
|
-
expect(sig()).toBe(
|
|
39
|
+
const sig = useCustom('key', 'default')
|
|
40
|
+
expect(sig()).toBe('existing')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it(
|
|
43
|
+
it('.remove() clears from backend and resets signal', () => {
|
|
44
44
|
const store = new Map<string, string>()
|
|
45
45
|
const useCustom = createStorage({
|
|
46
46
|
get: (k) => store.get(k) ?? null,
|
|
@@ -48,15 +48,15 @@ describe("createStorage", () => {
|
|
|
48
48
|
remove: (k) => store.delete(k),
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
const sig = useCustom(
|
|
52
|
-
sig.set(
|
|
51
|
+
const sig = useCustom('key', 'default')
|
|
52
|
+
sig.set('updated')
|
|
53
53
|
sig.remove()
|
|
54
54
|
|
|
55
|
-
expect(sig()).toBe(
|
|
56
|
-
expect(store.has(
|
|
55
|
+
expect(sig()).toBe('default')
|
|
56
|
+
expect(store.has('key')).toBe(false)
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
it(
|
|
59
|
+
it('deduplicates signals for same backend + key', () => {
|
|
60
60
|
const store = new Map<string, string>()
|
|
61
61
|
const useCustom = createStorage(
|
|
62
62
|
{
|
|
@@ -64,18 +64,18 @@ describe("createStorage", () => {
|
|
|
64
64
|
set: (k, v) => store.set(k, v),
|
|
65
65
|
remove: (k) => store.delete(k),
|
|
66
66
|
},
|
|
67
|
-
|
|
67
|
+
'test-backend',
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
const a = useCustom(
|
|
71
|
-
const b = useCustom(
|
|
70
|
+
const a = useCustom('key', 'default')
|
|
71
|
+
const b = useCustom('key', 'default')
|
|
72
72
|
expect(a).toBe(b)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
it(
|
|
75
|
+
it('handles backend read errors gracefully', () => {
|
|
76
76
|
const useCustom = createStorage({
|
|
77
77
|
get: () => {
|
|
78
|
-
throw new Error(
|
|
78
|
+
throw new Error('read failed')
|
|
79
79
|
},
|
|
80
80
|
set: () => {
|
|
81
81
|
// intentional no-op for error test
|
|
@@ -85,28 +85,28 @@ describe("createStorage", () => {
|
|
|
85
85
|
},
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
-
const sig = useCustom(
|
|
89
|
-
expect(sig()).toBe(
|
|
88
|
+
const sig = useCustom('key', 'fallback')
|
|
89
|
+
expect(sig()).toBe('fallback')
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
it(
|
|
92
|
+
it('handles backend write errors gracefully', () => {
|
|
93
93
|
const useCustom = createStorage({
|
|
94
94
|
get: () => null,
|
|
95
95
|
set: () => {
|
|
96
|
-
throw new Error(
|
|
96
|
+
throw new Error('write failed')
|
|
97
97
|
},
|
|
98
98
|
remove: () => {
|
|
99
99
|
// intentional no-op for error test
|
|
100
100
|
},
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
const sig = useCustom(
|
|
103
|
+
const sig = useCustom('key', 'default')
|
|
104
104
|
// Should not throw — signal still updates
|
|
105
|
-
sig.set(
|
|
106
|
-
expect(sig()).toBe(
|
|
105
|
+
sig.set('new-value')
|
|
106
|
+
expect(sig()).toBe('new-value')
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
-
it(
|
|
109
|
+
it('.subscribe() delegates to underlying signal', () => {
|
|
110
110
|
const store = new Map<string, string>()
|
|
111
111
|
const useCustom = createStorage({
|
|
112
112
|
get: (k) => store.get(k) ?? null,
|
|
@@ -114,17 +114,17 @@ describe("createStorage", () => {
|
|
|
114
114
|
remove: (k) => store.delete(k),
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
const sig = useCustom(
|
|
117
|
+
const sig = useCustom('sub-key', 'a')
|
|
118
118
|
let called = false
|
|
119
119
|
const unsub = sig.subscribe(() => {
|
|
120
120
|
called = true
|
|
121
121
|
})
|
|
122
|
-
sig.set(
|
|
122
|
+
sig.set('b')
|
|
123
123
|
expect(called).toBe(true)
|
|
124
124
|
unsub()
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
it(
|
|
127
|
+
it('.direct() delegates to underlying signal', () => {
|
|
128
128
|
const store = new Map<string, string>()
|
|
129
129
|
const useCustom = createStorage({
|
|
130
130
|
get: (k) => store.get(k) ?? null,
|
|
@@ -132,17 +132,17 @@ describe("createStorage", () => {
|
|
|
132
132
|
remove: (k) => store.delete(k),
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
const sig = useCustom(
|
|
135
|
+
const sig = useCustom('dir-key', 'a')
|
|
136
136
|
let called = false
|
|
137
137
|
const unsub = sig.direct(() => {
|
|
138
138
|
called = true
|
|
139
139
|
})
|
|
140
|
-
sig.set(
|
|
140
|
+
sig.set('b')
|
|
141
141
|
expect(called).toBe(true)
|
|
142
142
|
unsub()
|
|
143
143
|
})
|
|
144
144
|
|
|
145
|
-
it(
|
|
145
|
+
it('.debug() and .label work', () => {
|
|
146
146
|
const store = new Map<string, string>()
|
|
147
147
|
const useCustom = createStorage({
|
|
148
148
|
get: (k) => store.get(k) ?? null,
|
|
@@ -150,13 +150,13 @@ describe("createStorage", () => {
|
|
|
150
150
|
remove: (k) => store.delete(k),
|
|
151
151
|
})
|
|
152
152
|
|
|
153
|
-
const sig = useCustom(
|
|
154
|
-
sig.label =
|
|
155
|
-
expect(sig.label).toBe(
|
|
156
|
-
expect(sig.debug().value).toBe(
|
|
153
|
+
const sig = useCustom('debug-key', 'test')
|
|
154
|
+
sig.label = 'my-signal'
|
|
155
|
+
expect(sig.label).toBe('my-signal')
|
|
156
|
+
expect(sig.debug().value).toBe('test')
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
-
it(
|
|
159
|
+
it('supports custom serializer/deserializer', () => {
|
|
160
160
|
const store = new Map<string, string>()
|
|
161
161
|
const useCustom = createStorage({
|
|
162
162
|
get: (k) => store.get(k) ?? null,
|
|
@@ -164,18 +164,18 @@ describe("createStorage", () => {
|
|
|
164
164
|
remove: (k) => store.delete(k),
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
-
const date = useCustom(
|
|
167
|
+
const date = useCustom('date', new Date('2025-01-01'), {
|
|
168
168
|
serializer: (d) => d.toISOString(),
|
|
169
169
|
deserializer: (s) => new Date(s),
|
|
170
170
|
})
|
|
171
171
|
|
|
172
|
-
const newDate = new Date(
|
|
172
|
+
const newDate = new Date('2025-06-15')
|
|
173
173
|
date.set(newDate)
|
|
174
|
-
expect(date().toISOString()).toBe(
|
|
174
|
+
expect(date().toISOString()).toBe('2025-06-15T00:00:00.000Z')
|
|
175
175
|
})
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
describe(
|
|
178
|
+
describe('useMemoryStorage', () => {
|
|
179
179
|
beforeEach(() => {
|
|
180
180
|
_resetRegistry()
|
|
181
181
|
})
|
|
@@ -184,24 +184,24 @@ describe("useMemoryStorage", () => {
|
|
|
184
184
|
_resetRegistry()
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
it(
|
|
188
|
-
const sig = useMemoryStorage(
|
|
189
|
-
expect(sig()).toBe(
|
|
187
|
+
it('works as an in-memory storage', () => {
|
|
188
|
+
const sig = useMemoryStorage('key', 'default')
|
|
189
|
+
expect(sig()).toBe('default')
|
|
190
190
|
|
|
191
|
-
sig.set(
|
|
192
|
-
expect(sig()).toBe(
|
|
191
|
+
sig.set('updated')
|
|
192
|
+
expect(sig()).toBe('updated')
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it(
|
|
196
|
-
const a = useMemoryStorage(
|
|
197
|
-
const b = useMemoryStorage(
|
|
195
|
+
it('deduplicates signals', () => {
|
|
196
|
+
const a = useMemoryStorage('key', 'default')
|
|
197
|
+
const b = useMemoryStorage('key', 'default')
|
|
198
198
|
expect(a).toBe(b)
|
|
199
199
|
})
|
|
200
200
|
|
|
201
|
-
it(
|
|
202
|
-
const sig = useMemoryStorage(
|
|
203
|
-
sig.set(
|
|
201
|
+
it('.remove() resets to default', () => {
|
|
202
|
+
const sig = useMemoryStorage('temp', 'initial')
|
|
203
|
+
sig.set('changed')
|
|
204
204
|
sig.remove()
|
|
205
|
-
expect(sig()).toBe(
|
|
205
|
+
expect(sig()).toBe('initial')
|
|
206
206
|
})
|
|
207
207
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
2
|
-
import { _resetDBCache, _resetRegistry, useIndexedDB } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { _resetDBCache, _resetRegistry, useIndexedDB } from '../index'
|
|
3
3
|
|
|
4
|
-
describe(
|
|
4
|
+
describe('useIndexedDB — debounced writes', () => {
|
|
5
5
|
beforeEach(() => {
|
|
6
6
|
_resetRegistry()
|
|
7
7
|
_resetDBCache()
|
|
@@ -14,81 +14,81 @@ describe("useIndexedDB — debounced writes", () => {
|
|
|
14
14
|
vi.useRealTimers()
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
it(
|
|
18
|
-
const draft = useIndexedDB(
|
|
19
|
-
draft.set(
|
|
17
|
+
it('signal updates immediately, IDB write is debounced', () => {
|
|
18
|
+
const draft = useIndexedDB('debounce-test', 'initial', { debounceMs: 50 })
|
|
19
|
+
draft.set('updated')
|
|
20
20
|
// Signal is synchronous
|
|
21
|
-
expect(draft()).toBe(
|
|
21
|
+
expect(draft()).toBe('updated')
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
it(
|
|
25
|
-
const draft = useIndexedDB(
|
|
26
|
-
draft.set(
|
|
27
|
-
draft.set(
|
|
28
|
-
draft.set(
|
|
24
|
+
it('multiple rapid sets coalesce into one write (only last value)', () => {
|
|
25
|
+
const draft = useIndexedDB('coalesce-test', '', { debounceMs: 100 })
|
|
26
|
+
draft.set('first')
|
|
27
|
+
draft.set('second')
|
|
28
|
+
draft.set('third')
|
|
29
29
|
|
|
30
30
|
// Signal reflects latest immediately
|
|
31
|
-
expect(draft()).toBe(
|
|
31
|
+
expect(draft()).toBe('third')
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
it(
|
|
35
|
-
const fast = useIndexedDB(
|
|
36
|
-
fast.set(
|
|
37
|
-
expect(fast()).toBe(
|
|
34
|
+
it('custom debounceMs is respected', () => {
|
|
35
|
+
const fast = useIndexedDB('fast', '', { debounceMs: 10 })
|
|
36
|
+
fast.set('value')
|
|
37
|
+
expect(fast()).toBe('value')
|
|
38
38
|
|
|
39
|
-
const slow = useIndexedDB(
|
|
40
|
-
slow.set(
|
|
41
|
-
expect(slow()).toBe(
|
|
39
|
+
const slow = useIndexedDB('slow', '', { debounceMs: 5000 })
|
|
40
|
+
slow.set('value')
|
|
41
|
+
expect(slow()).toBe('value')
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
it(
|
|
45
|
-
const sig = useIndexedDB(
|
|
46
|
-
sig.set(
|
|
47
|
-
expect(sig()).toBe(
|
|
44
|
+
it('default debounceMs is 100', () => {
|
|
45
|
+
const sig = useIndexedDB('default-debounce', 'initial')
|
|
46
|
+
sig.set('value')
|
|
47
|
+
expect(sig()).toBe('value')
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it(
|
|
51
|
-
const count = useIndexedDB(
|
|
50
|
+
it('update() also triggers debounced write', () => {
|
|
51
|
+
const count = useIndexedDB('update-debounce', 0, { debounceMs: 50 })
|
|
52
52
|
count.update((n) => n + 1)
|
|
53
53
|
count.update((n) => n + 1)
|
|
54
54
|
count.update((n) => n + 1)
|
|
55
55
|
expect(count()).toBe(3)
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
it(
|
|
59
|
-
const draft = useIndexedDB(
|
|
60
|
-
draft.set(
|
|
58
|
+
it('remove() cancels pending debounced write', () => {
|
|
59
|
+
const draft = useIndexedDB('remove-cancel', 'default', { debounceMs: 200 })
|
|
60
|
+
draft.set('pending-value')
|
|
61
61
|
draft.remove()
|
|
62
|
-
expect(draft()).toBe(
|
|
62
|
+
expect(draft()).toBe('default')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
-
it(
|
|
66
|
-
const sig = useIndexedDB(
|
|
67
|
-
dbName:
|
|
68
|
-
storeName:
|
|
65
|
+
it('custom dbName and storeName options', () => {
|
|
66
|
+
const sig = useIndexedDB('custom-db-key', 'val', {
|
|
67
|
+
dbName: 'my-app-db',
|
|
68
|
+
storeName: 'my-store',
|
|
69
69
|
})
|
|
70
|
-
sig.set(
|
|
71
|
-
expect(sig()).toBe(
|
|
70
|
+
sig.set('updated')
|
|
71
|
+
expect(sig()).toBe('updated')
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
it(
|
|
75
|
-
const sig = useIndexedDB(
|
|
74
|
+
it('custom serializer/deserializer with IndexedDB', () => {
|
|
75
|
+
const sig = useIndexedDB('date-idb', new Date('2025-01-01'), {
|
|
76
76
|
serializer: (d) => d.toISOString(),
|
|
77
77
|
deserializer: (s) => new Date(s),
|
|
78
78
|
debounceMs: 50,
|
|
79
79
|
})
|
|
80
|
-
const newDate = new Date(
|
|
80
|
+
const newDate = new Date('2025-06-15')
|
|
81
81
|
sig.set(newDate)
|
|
82
|
-
expect(sig().toISOString()).toBe(
|
|
82
|
+
expect(sig().toISOString()).toBe('2025-06-15T00:00:00.000Z')
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
it(
|
|
86
|
-
const sig = useIndexedDB(
|
|
85
|
+
it('.subscribe() works with debounced signals', () => {
|
|
86
|
+
const sig = useIndexedDB('sub-idb', 'a', { debounceMs: 50 })
|
|
87
87
|
let called = false
|
|
88
88
|
const unsub = sig.subscribe(() => {
|
|
89
89
|
called = true
|
|
90
90
|
})
|
|
91
|
-
sig.set(
|
|
91
|
+
sig.set('b')
|
|
92
92
|
expect(called).toBe(true)
|
|
93
93
|
unsub()
|
|
94
94
|
})
|