@pyreon/storage 0.11.4 → 0.11.6

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.
@@ -1,12 +1,12 @@
1
- import { effect } from "@pyreon/reactivity"
2
- import { afterEach, beforeEach, describe, expect, it } from "vitest"
3
- import { _resetRegistry, useStorage } from "../index"
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("useStorage — cross-tab sync", () => {
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("updates signal when storage event fires with a new value", () => {
21
- const theme = useStorage("theme", "light")
22
- expect(theme()).toBe("light")
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("storage"), {
26
- key: "theme",
27
- newValue: JSON.stringify("dark"),
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("dark")
32
+ expect(theme()).toBe('dark')
33
33
  })
34
34
 
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")
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("storage"), {
42
- key: "theme",
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("light")
48
+ expect(theme()).toBe('light')
49
49
  })
50
50
 
51
- it("ignores storage events for unregistered keys", () => {
52
- const theme = useStorage("theme", "light")
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("storage"), {
56
- key: "unrelated-key",
57
- newValue: JSON.stringify("value"),
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("light")
62
+ expect(theme()).toBe('light')
63
63
  })
64
64
 
65
- it("ignores storage events with null key", () => {
66
- const theme = useStorage("theme", "light")
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("storage"), {
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("light")
76
+ expect(theme()).toBe('light')
77
77
  })
78
78
 
79
- it("triggers reactive effect on cross-tab update", () => {
80
- const theme = useStorage("theme", "light")
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(["light"])
87
+ expect(values).toEqual(['light'])
88
88
 
89
89
  window.dispatchEvent(
90
- Object.assign(new Event("storage"), {
91
- key: "theme",
92
- newValue: JSON.stringify("dark"),
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(["light", "dark"])
97
+ expect(values).toEqual(['light', 'dark'])
98
98
  })
99
99
 
100
- it("handles corrupt JSON in storage event gracefully (falls back to default)", () => {
101
- const count = useStorage("count", 0)
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("storage"), {
105
- key: "count",
106
- newValue: "{invalid json",
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("syncs object values across tabs", () => {
116
- const prefs = useStorage("prefs", { sidebar: true, density: "comfortable" })
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("storage"), {
120
- key: "prefs",
121
- newValue: JSON.stringify({ sidebar: false, density: "compact" }),
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: "compact" })
126
+ expect(prefs()).toEqual({ sidebar: false, density: 'compact' })
127
127
  })
128
128
 
129
- it("handles multiple rapid storage events", () => {
130
- const count = useStorage("count", 0)
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("storage"), {
135
- key: "count",
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("cross-tab sync works independently for different keys", () => {
146
- const theme = useStorage("theme", "light")
147
- const lang = useStorage("lang", "en")
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("storage"), {
151
- key: "theme",
152
- newValue: JSON.stringify("dark"),
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("dark")
158
- expect(lang()).toBe("en") // unchanged
157
+ expect(theme()).toBe('dark')
158
+ expect(lang()).toBe('en') // unchanged
159
159
  })
160
160
  })
@@ -1,7 +1,7 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest"
2
- import { _resetRegistry, createStorage, useMemoryStorage } from "../index"
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
+ import { _resetRegistry, createStorage, useMemoryStorage } from '../index'
3
3
 
4
- describe("createStorage", () => {
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("creates a working storage hook from a custom backend", () => {
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("key", "default")
22
- expect(sig()).toBe("default")
21
+ const sig = useCustom('key', 'default')
22
+ expect(sig()).toBe('default')
23
23
 
24
- sig.set("updated")
25
- expect(sig()).toBe("updated")
26
- expect(store.get("key")).toBe(JSON.stringify("updated"))
24
+ sig.set('updated')
25
+ expect(sig()).toBe('updated')
26
+ expect(store.get('key')).toBe(JSON.stringify('updated'))
27
27
  })
28
28
 
29
- it("reads existing values from the backend", () => {
29
+ it('reads existing values from the backend', () => {
30
30
  const store = new Map<string, string>()
31
- store.set("key", JSON.stringify("existing"))
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("key", "default")
40
- expect(sig()).toBe("existing")
39
+ const sig = useCustom('key', 'default')
40
+ expect(sig()).toBe('existing')
41
41
  })
42
42
 
43
- it(".remove() clears from backend and resets signal", () => {
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("key", "default")
52
- sig.set("updated")
51
+ const sig = useCustom('key', 'default')
52
+ sig.set('updated')
53
53
  sig.remove()
54
54
 
55
- expect(sig()).toBe("default")
56
- expect(store.has("key")).toBe(false)
55
+ expect(sig()).toBe('default')
56
+ expect(store.has('key')).toBe(false)
57
57
  })
58
58
 
59
- it("deduplicates signals for same backend + key", () => {
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
- "test-backend",
67
+ 'test-backend',
68
68
  )
69
69
 
70
- const a = useCustom("key", "default")
71
- const b = useCustom("key", "default")
70
+ const a = useCustom('key', 'default')
71
+ const b = useCustom('key', 'default')
72
72
  expect(a).toBe(b)
73
73
  })
74
74
 
75
- it("handles backend read errors gracefully", () => {
75
+ it('handles backend read errors gracefully', () => {
76
76
  const useCustom = createStorage({
77
77
  get: () => {
78
- throw new Error("read failed")
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("key", "fallback")
89
- expect(sig()).toBe("fallback")
88
+ const sig = useCustom('key', 'fallback')
89
+ expect(sig()).toBe('fallback')
90
90
  })
91
91
 
92
- it("handles backend write errors gracefully", () => {
92
+ it('handles backend write errors gracefully', () => {
93
93
  const useCustom = createStorage({
94
94
  get: () => null,
95
95
  set: () => {
96
- throw new Error("write failed")
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("key", "default")
103
+ const sig = useCustom('key', 'default')
104
104
  // Should not throw — signal still updates
105
- sig.set("new-value")
106
- expect(sig()).toBe("new-value")
105
+ sig.set('new-value')
106
+ expect(sig()).toBe('new-value')
107
107
  })
108
108
 
109
- it(".subscribe() delegates to underlying signal", () => {
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("sub-key", "a")
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("b")
122
+ sig.set('b')
123
123
  expect(called).toBe(true)
124
124
  unsub()
125
125
  })
126
126
 
127
- it(".direct() delegates to underlying signal", () => {
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("dir-key", "a")
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("b")
140
+ sig.set('b')
141
141
  expect(called).toBe(true)
142
142
  unsub()
143
143
  })
144
144
 
145
- it(".debug() and .label work", () => {
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("debug-key", "test")
154
- sig.label = "my-signal"
155
- expect(sig.label).toBe("my-signal")
156
- expect(sig.debug().value).toBe("test")
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("supports custom serializer/deserializer", () => {
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("date", new Date("2025-01-01"), {
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("2025-06-15")
172
+ const newDate = new Date('2025-06-15')
173
173
  date.set(newDate)
174
- expect(date().toISOString()).toBe("2025-06-15T00:00:00.000Z")
174
+ expect(date().toISOString()).toBe('2025-06-15T00:00:00.000Z')
175
175
  })
176
176
  })
177
177
 
178
- describe("useMemoryStorage", () => {
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("works as an in-memory storage", () => {
188
- const sig = useMemoryStorage("key", "default")
189
- expect(sig()).toBe("default")
187
+ it('works as an in-memory storage', () => {
188
+ const sig = useMemoryStorage('key', 'default')
189
+ expect(sig()).toBe('default')
190
190
 
191
- sig.set("updated")
192
- expect(sig()).toBe("updated")
191
+ sig.set('updated')
192
+ expect(sig()).toBe('updated')
193
193
  })
194
194
 
195
- it("deduplicates signals", () => {
196
- const a = useMemoryStorage("key", "default")
197
- const b = useMemoryStorage("key", "default")
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(".remove() resets to default", () => {
202
- const sig = useMemoryStorage("temp", "initial")
203
- sig.set("changed")
201
+ it('.remove() resets to default', () => {
202
+ const sig = useMemoryStorage('temp', 'initial')
203
+ sig.set('changed')
204
204
  sig.remove()
205
- expect(sig()).toBe("initial")
205
+ expect(sig()).toBe('initial')
206
206
  })
207
207
  })
@@ -1,7 +1,7 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
- import { _resetDBCache, _resetRegistry, useIndexedDB } from "../index"
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { _resetDBCache, _resetRegistry, useIndexedDB } from '../index'
3
3
 
4
- describe("useIndexedDB — debounced writes", () => {
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("signal updates immediately, IDB write is debounced", () => {
18
- const draft = useIndexedDB("debounce-test", "initial", { debounceMs: 50 })
19
- draft.set("updated")
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("updated")
21
+ expect(draft()).toBe('updated')
22
22
  })
23
23
 
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")
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("third")
31
+ expect(draft()).toBe('third')
32
32
  })
33
33
 
34
- it("custom debounceMs is respected", () => {
35
- const fast = useIndexedDB("fast", "", { debounceMs: 10 })
36
- fast.set("value")
37
- expect(fast()).toBe("value")
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("slow", "", { debounceMs: 5000 })
40
- slow.set("value")
41
- expect(slow()).toBe("value")
39
+ const slow = useIndexedDB('slow', '', { debounceMs: 5000 })
40
+ slow.set('value')
41
+ expect(slow()).toBe('value')
42
42
  })
43
43
 
44
- it("default debounceMs is 100", () => {
45
- const sig = useIndexedDB("default-debounce", "initial")
46
- sig.set("value")
47
- expect(sig()).toBe("value")
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("update() also triggers debounced write", () => {
51
- const count = useIndexedDB("update-debounce", 0, { debounceMs: 50 })
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("remove() cancels pending debounced write", () => {
59
- const draft = useIndexedDB("remove-cancel", "default", { debounceMs: 200 })
60
- draft.set("pending-value")
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("default")
62
+ expect(draft()).toBe('default')
63
63
  })
64
64
 
65
- it("custom dbName and storeName options", () => {
66
- const sig = useIndexedDB("custom-db-key", "val", {
67
- dbName: "my-app-db",
68
- storeName: "my-store",
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("updated")
71
- expect(sig()).toBe("updated")
70
+ sig.set('updated')
71
+ expect(sig()).toBe('updated')
72
72
  })
73
73
 
74
- it("custom serializer/deserializer with IndexedDB", () => {
75
- const sig = useIndexedDB("date-idb", new Date("2025-01-01"), {
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("2025-06-15")
80
+ const newDate = new Date('2025-06-15')
81
81
  sig.set(newDate)
82
- expect(sig().toISOString()).toBe("2025-06-15T00:00:00.000Z")
82
+ expect(sig().toISOString()).toBe('2025-06-15T00:00:00.000Z')
83
83
  })
84
84
 
85
- it(".subscribe() works with debounced signals", () => {
86
- const sig = useIndexedDB("sub-idb", "a", { debounceMs: 50 })
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("b")
91
+ sig.set('b')
92
92
  expect(called).toBe(true)
93
93
  unsub()
94
94
  })