@pyreon/storage 0.9.0 → 0.11.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.
@@ -1,8 +1,8 @@
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
- describe('useStorage (localStorage)', () => {
5
+ describe("useStorage (localStorage)", () => {
6
6
  beforeEach(() => {
7
7
  localStorage.clear()
8
8
  _resetRegistry()
@@ -13,203 +13,203 @@ describe('useStorage (localStorage)', () => {
13
13
  _resetRegistry()
14
14
  })
15
15
 
16
- it('returns default value when key is not in storage', () => {
17
- const theme = useStorage('theme', 'light')
18
- expect(theme()).toBe('light')
16
+ it("returns default value when key is not in storage", () => {
17
+ const theme = useStorage("theme", "light")
18
+ expect(theme()).toBe("light")
19
19
  })
20
20
 
21
- it('reads existing value from localStorage', () => {
22
- localStorage.setItem('theme', JSON.stringify('dark'))
23
- const theme = useStorage('theme', 'light')
24
- expect(theme()).toBe('dark')
21
+ it("reads existing value from localStorage", () => {
22
+ localStorage.setItem("theme", JSON.stringify("dark"))
23
+ const theme = useStorage("theme", "light")
24
+ expect(theme()).toBe("dark")
25
25
  })
26
26
 
27
- it('.set() updates signal and localStorage', () => {
28
- const theme = useStorage('theme', 'light')
29
- theme.set('dark')
30
- expect(theme()).toBe('dark')
31
- expect(JSON.parse(localStorage.getItem('theme')!)).toBe('dark')
27
+ it(".set() updates signal and localStorage", () => {
28
+ const theme = useStorage("theme", "light")
29
+ theme.set("dark")
30
+ expect(theme()).toBe("dark")
31
+ expect(JSON.parse(localStorage.getItem("theme")!)).toBe("dark")
32
32
  })
33
33
 
34
- it('.update() updates signal and localStorage', () => {
35
- const count = useStorage('count', 0)
34
+ it(".update() updates signal and localStorage", () => {
35
+ const count = useStorage("count", 0)
36
36
  count.update((n) => n + 1)
37
37
  expect(count()).toBe(1)
38
- expect(JSON.parse(localStorage.getItem('count')!)).toBe(1)
38
+ expect(JSON.parse(localStorage.getItem("count")!)).toBe(1)
39
39
  })
40
40
 
41
- it('.peek() reads without subscribing', () => {
42
- const theme = useStorage('theme', 'light')
43
- expect(theme.peek()).toBe('light')
41
+ it(".peek() reads without subscribing", () => {
42
+ const theme = useStorage("theme", "light")
43
+ expect(theme.peek()).toBe("light")
44
44
  })
45
45
 
46
- it('.remove() clears from storage and resets to default', () => {
47
- const theme = useStorage('theme', 'light')
48
- theme.set('dark')
49
- expect(theme()).toBe('dark')
46
+ it(".remove() clears from storage and resets to default", () => {
47
+ const theme = useStorage("theme", "light")
48
+ theme.set("dark")
49
+ expect(theme()).toBe("dark")
50
50
 
51
51
  theme.remove()
52
- expect(theme()).toBe('light')
53
- expect(localStorage.getItem('theme')).toBeNull()
52
+ expect(theme()).toBe("light")
53
+ expect(localStorage.getItem("theme")).toBeNull()
54
54
  })
55
55
 
56
- it('returns same signal instance for same key (deduplication)', () => {
57
- const a = useStorage('theme', 'light')
58
- const b = useStorage('theme', 'light')
56
+ it("returns same signal instance for same key (deduplication)", () => {
57
+ const a = useStorage("theme", "light")
58
+ const b = useStorage("theme", "light")
59
59
  expect(a).toBe(b)
60
60
  })
61
61
 
62
- it('returns different signals for different keys', () => {
63
- const a = useStorage('theme', 'light')
64
- const b = useStorage('lang', 'en')
62
+ it("returns different signals for different keys", () => {
63
+ const a = useStorage("theme", "light")
64
+ const b = useStorage("lang", "en")
65
65
  expect(a).not.toBe(b)
66
66
  })
67
67
 
68
- it('works with objects', () => {
69
- const prefs = useStorage('prefs', { sidebar: true, density: 'comfortable' })
70
- expect(prefs()).toEqual({ sidebar: true, density: 'comfortable' })
68
+ it("works with objects", () => {
69
+ const prefs = useStorage("prefs", { sidebar: true, density: "comfortable" })
70
+ expect(prefs()).toEqual({ sidebar: true, density: "comfortable" })
71
71
 
72
- prefs.set({ sidebar: false, density: 'compact' })
73
- expect(prefs()).toEqual({ sidebar: false, density: 'compact' })
74
- expect(JSON.parse(localStorage.getItem('prefs')!)).toEqual({
72
+ prefs.set({ sidebar: false, density: "compact" })
73
+ expect(prefs()).toEqual({ sidebar: false, density: "compact" })
74
+ expect(JSON.parse(localStorage.getItem("prefs")!)).toEqual({
75
75
  sidebar: false,
76
- density: 'compact',
76
+ density: "compact",
77
77
  })
78
78
  })
79
79
 
80
- it('works with arrays', () => {
81
- const items = useStorage('items', [1, 2, 3])
80
+ it("works with arrays", () => {
81
+ const items = useStorage("items", [1, 2, 3])
82
82
  expect(items()).toEqual([1, 2, 3])
83
83
 
84
84
  items.set([4, 5])
85
85
  expect(items()).toEqual([4, 5])
86
86
  })
87
87
 
88
- it('works with booleans', () => {
89
- const flag = useStorage('flag', false)
88
+ it("works with booleans", () => {
89
+ const flag = useStorage("flag", false)
90
90
  flag.set(true)
91
91
  expect(flag()).toBe(true)
92
- expect(JSON.parse(localStorage.getItem('flag')!)).toBe(true)
92
+ expect(JSON.parse(localStorage.getItem("flag")!)).toBe(true)
93
93
  })
94
94
 
95
- it('works with numbers', () => {
96
- const count = useStorage('count', 42)
95
+ it("works with numbers", () => {
96
+ const count = useStorage("count", 42)
97
97
  expect(count()).toBe(42)
98
98
  })
99
99
 
100
- it('handles corrupt storage values gracefully', () => {
101
- localStorage.setItem('broken', 'not valid json{{{')
102
- const value = useStorage('broken', 'fallback')
103
- expect(value()).toBe('fallback')
100
+ it("handles corrupt storage values gracefully", () => {
101
+ localStorage.setItem("broken", "not valid json{{{")
102
+ const value = useStorage("broken", "fallback")
103
+ expect(value()).toBe("fallback")
104
104
  })
105
105
 
106
- it('calls onError when deserialization fails', () => {
107
- localStorage.setItem('broken', '{invalid')
106
+ it("calls onError when deserialization fails", () => {
107
+ localStorage.setItem("broken", "{invalid")
108
108
  const errors: Error[] = []
109
- const value = useStorage('broken', 'default', {
109
+ const value = useStorage("broken", "default", {
110
110
  onError: (e) => {
111
111
  errors.push(e)
112
112
  return undefined
113
113
  },
114
114
  })
115
- expect(value()).toBe('default')
115
+ expect(value()).toBe("default")
116
116
  expect(errors).toHaveLength(1)
117
117
  })
118
118
 
119
- it('onError can return a custom fallback', () => {
120
- localStorage.setItem('broken', '{invalid')
121
- const value = useStorage('broken', 'default', {
122
- onError: () => 'custom-fallback',
119
+ it("onError can return a custom fallback", () => {
120
+ localStorage.setItem("broken", "{invalid")
121
+ const value = useStorage("broken", "default", {
122
+ onError: () => "custom-fallback",
123
123
  })
124
- expect(value()).toBe('custom-fallback')
124
+ expect(value()).toBe("custom-fallback")
125
125
  })
126
126
 
127
- it('custom serializer/deserializer work', () => {
128
- const date = useStorage('date', new Date('2025-01-01'), {
127
+ it("custom serializer/deserializer work", () => {
128
+ const date = useStorage("date", new Date("2025-01-01"), {
129
129
  serializer: (d) => d.toISOString(),
130
130
  deserializer: (s) => new Date(s),
131
131
  })
132
132
 
133
- expect(date()).toEqual(new Date('2025-01-01'))
133
+ expect(date()).toEqual(new Date("2025-01-01"))
134
134
 
135
- const newDate = new Date('2025-06-15')
135
+ const newDate = new Date("2025-06-15")
136
136
  date.set(newDate)
137
- expect(localStorage.getItem('date')).toBe('2025-06-15T00:00:00.000Z')
137
+ expect(localStorage.getItem("date")).toBe("2025-06-15T00:00:00.000Z")
138
138
  })
139
139
 
140
- it('is reactive — works in effects', () => {
141
- const theme = useStorage('theme', 'light')
140
+ it("is reactive — works in effects", () => {
141
+ const theme = useStorage("theme", "light")
142
142
  const values: string[] = []
143
143
 
144
144
  effect(() => {
145
145
  values.push(theme())
146
146
  })
147
147
 
148
- expect(values).toEqual(['light'])
148
+ expect(values).toEqual(["light"])
149
149
 
150
- theme.set('dark')
151
- expect(values).toEqual(['light', 'dark'])
150
+ theme.set("dark")
151
+ expect(values).toEqual(["light", "dark"])
152
152
  })
153
153
 
154
- it('.subscribe() works', () => {
155
- const theme = useStorage('theme', 'light')
154
+ it(".subscribe() works", () => {
155
+ const theme = useStorage("theme", "light")
156
156
  let callCount = 0
157
157
  const unsub = theme.subscribe(() => {
158
158
  callCount++
159
159
  })
160
160
 
161
- theme.set('dark')
161
+ theme.set("dark")
162
162
  expect(callCount).toBeGreaterThanOrEqual(1)
163
163
  unsub()
164
164
  })
165
165
 
166
- it('.debug() returns debug info', () => {
167
- const theme = useStorage('theme', 'light')
166
+ it(".debug() returns debug info", () => {
167
+ const theme = useStorage("theme", "light")
168
168
  const info = theme.debug()
169
- expect(info.value).toBe('light')
169
+ expect(info.value).toBe("light")
170
170
  })
171
171
 
172
- it('.label can be set and read', () => {
173
- const theme = useStorage('theme', 'light')
174
- theme.label = 'theme-signal'
175
- expect(theme.label).toBe('theme-signal')
172
+ it(".label can be set and read", () => {
173
+ const theme = useStorage("theme", "light")
174
+ theme.label = "theme-signal"
175
+ expect(theme.label).toBe("theme-signal")
176
176
  })
177
177
 
178
- it('cross-tab sync via storage event', () => {
179
- const theme = useStorage('theme', 'light')
178
+ it("cross-tab sync via storage event", () => {
179
+ const theme = useStorage("theme", "light")
180
180
 
181
181
  // Simulate storage event from another tab
182
- const event = Object.assign(new Event('storage'), {
183
- key: 'theme',
184
- newValue: JSON.stringify('dark'),
182
+ const event = Object.assign(new Event("storage"), {
183
+ key: "theme",
184
+ newValue: JSON.stringify("dark"),
185
185
  storageArea: localStorage,
186
186
  })
187
187
  window.dispatchEvent(event)
188
188
 
189
- expect(theme()).toBe('dark')
189
+ expect(theme()).toBe("dark")
190
190
  })
191
191
 
192
- it('cross-tab sync with null newValue resets to default', () => {
193
- const theme = useStorage('theme', 'light')
194
- theme.set('dark')
192
+ it("cross-tab sync with null newValue resets to default", () => {
193
+ const theme = useStorage("theme", "light")
194
+ theme.set("dark")
195
195
 
196
- const event = Object.assign(new Event('storage'), {
197
- key: 'theme',
196
+ const event = Object.assign(new Event("storage"), {
197
+ key: "theme",
198
198
  newValue: null,
199
199
  storageArea: localStorage,
200
200
  })
201
201
  window.dispatchEvent(event)
202
202
 
203
- expect(theme()).toBe('light')
203
+ expect(theme()).toBe("light")
204
204
  })
205
205
 
206
- it('after remove(), a new useStorage call creates a fresh signal', () => {
207
- const a = useStorage('temp', 'first')
208
- a.set('modified')
206
+ it("after remove(), a new useStorage call creates a fresh signal", () => {
207
+ const a = useStorage("temp", "first")
208
+ a.set("modified")
209
209
  a.remove()
210
210
 
211
- const b = useStorage('temp', 'second')
212
- expect(b()).toBe('second')
211
+ const b = useStorage("temp", "second")
212
+ expect(b()).toBe("second")
213
213
  expect(a).not.toBe(b)
214
214
  })
215
215
  })
@@ -1,7 +1,7 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
- import { _resetRegistry, useSessionStorage } from '../index'
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
2
+ import { _resetRegistry, useSessionStorage } from "../index"
3
3
 
4
- describe('useSessionStorage', () => {
4
+ describe("useSessionStorage", () => {
5
5
  beforeEach(() => {
6
6
  sessionStorage.clear()
7
7
  _resetRegistry()
@@ -12,56 +12,56 @@ describe('useSessionStorage', () => {
12
12
  _resetRegistry()
13
13
  })
14
14
 
15
- it('returns default value when key is not in storage', () => {
16
- const step = useSessionStorage('step', 0)
15
+ it("returns default value when key is not in storage", () => {
16
+ const step = useSessionStorage("step", 0)
17
17
  expect(step()).toBe(0)
18
18
  })
19
19
 
20
- it('reads existing value from sessionStorage', () => {
21
- sessionStorage.setItem('step', JSON.stringify(3))
22
- const step = useSessionStorage('step', 0)
20
+ it("reads existing value from sessionStorage", () => {
21
+ sessionStorage.setItem("step", JSON.stringify(3))
22
+ const step = useSessionStorage("step", 0)
23
23
  expect(step()).toBe(3)
24
24
  })
25
25
 
26
- it('.set() updates signal and sessionStorage', () => {
27
- const step = useSessionStorage('step', 0)
26
+ it(".set() updates signal and sessionStorage", () => {
27
+ const step = useSessionStorage("step", 0)
28
28
  step.set(5)
29
29
  expect(step()).toBe(5)
30
- expect(JSON.parse(sessionStorage.getItem('step')!)).toBe(5)
30
+ expect(JSON.parse(sessionStorage.getItem("step")!)).toBe(5)
31
31
  })
32
32
 
33
- it('.remove() clears from storage and resets to default', () => {
34
- const step = useSessionStorage('step', 0)
33
+ it(".remove() clears from storage and resets to default", () => {
34
+ const step = useSessionStorage("step", 0)
35
35
  step.set(5)
36
36
  step.remove()
37
37
  expect(step()).toBe(0)
38
- expect(sessionStorage.getItem('step')).toBeNull()
38
+ expect(sessionStorage.getItem("step")).toBeNull()
39
39
  })
40
40
 
41
- it('returns same signal instance for same key', () => {
42
- const a = useSessionStorage('step', 0)
43
- const b = useSessionStorage('step', 0)
41
+ it("returns same signal instance for same key", () => {
42
+ const a = useSessionStorage("step", 0)
43
+ const b = useSessionStorage("step", 0)
44
44
  expect(a).toBe(b)
45
45
  })
46
46
 
47
- it('works with objects', () => {
48
- const form = useSessionStorage('form-draft', { name: '', email: '' })
49
- form.set({ name: 'Alice', email: 'alice@example.com' })
50
- expect(form()).toEqual({ name: 'Alice', email: 'alice@example.com' })
47
+ it("works with objects", () => {
48
+ const form = useSessionStorage("form-draft", { name: "", email: "" })
49
+ form.set({ name: "Alice", email: "alice@example.com" })
50
+ expect(form()).toEqual({ name: "Alice", email: "alice@example.com" })
51
51
  })
52
52
 
53
- it('handles corrupt storage values gracefully', () => {
54
- sessionStorage.setItem('broken', '{{invalid')
55
- const value = useSessionStorage('broken', 'default')
56
- expect(value()).toBe('default')
53
+ it("handles corrupt storage values gracefully", () => {
54
+ sessionStorage.setItem("broken", "{{invalid")
55
+ const value = useSessionStorage("broken", "default")
56
+ expect(value()).toBe("default")
57
57
  })
58
58
 
59
- it('does not share signals with localStorage', async () => {
60
- const { useStorage } = await import('../local')
61
- const local = useStorage('key', 'local-default')
62
- const session = useSessionStorage('key', 'session-default')
59
+ it("does not share signals with localStorage", async () => {
60
+ const { useStorage } = await import("../local")
61
+ const local = useStorage("key", "local-default")
62
+ const session = useSessionStorage("key", "session-default")
63
63
  expect(local).not.toBe(session)
64
- expect(local()).toBe('local-default')
65
- expect(session()).toBe('session-default')
64
+ expect(local()).toBe("local-default")
65
+ expect(session()).toBe("session-default")
66
66
  })
67
67
  })
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Signal } from '@pyreon/reactivity'
1
+ import type { Signal } from "@pyreon/reactivity"
2
2
 
3
3
  // ─── Storage Signal ──────────────────────────────────────────────────────────
4
4
 
@@ -42,7 +42,7 @@ export interface CookieOptions<T> extends StorageOptions<T> {
42
42
  /** HTTPS only — default: false */
43
43
  secure?: boolean
44
44
  /** SameSite policy — default: 'lax' */
45
- sameSite?: 'strict' | 'lax' | 'none'
45
+ sameSite?: "strict" | "lax" | "none"
46
46
  }
47
47
 
48
48
  // ─── IndexedDB Options ───────────────────────────────────────────────────────
package/src/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { StorageOptions } from './types'
1
+ import type { StorageOptions } from "./types"
2
2
 
3
3
  // ─── SSR Detection ───────────────────────────────────────────────────────────
4
4
 
@@ -6,7 +6,7 @@ import type { StorageOptions } from './types'
6
6
  * Check if we're running in a browser environment.
7
7
  */
8
8
  export function isBrowser(): boolean {
9
- return typeof window !== 'undefined' && typeof document !== 'undefined'
9
+ return typeof window !== "undefined" && typeof document !== "undefined"
10
10
  }
11
11
 
12
12
  // ─── Serialization ───────────────────────────────────────────────────────────
@@ -14,10 +14,7 @@ export function isBrowser(): boolean {
14
14
  /**
15
15
  * Serialize a value to a string for storage.
16
16
  */
17
- export function serialize<T>(
18
- value: T,
19
- serializer?: StorageOptions<T>['serializer'],
20
- ): string {
17
+ export function serialize<T>(value: T, serializer?: StorageOptions<T>["serializer"]): string {
21
18
  if (serializer) return serializer(value)
22
19
  return JSON.stringify(value)
23
20
  }
@@ -29,8 +26,8 @@ export function serialize<T>(
29
26
  export function deserialize<T>(
30
27
  raw: string,
31
28
  defaultValue: T,
32
- deserializer?: StorageOptions<T>['deserializer'],
33
- onError?: StorageOptions<T>['onError'],
29
+ deserializer?: StorageOptions<T>["deserializer"],
30
+ onError?: StorageOptions<T>["onError"],
34
31
  ): T {
35
32
  try {
36
33
  if (deserializer) return deserializer(raw)
@@ -50,14 +47,13 @@ export function deserialize<T>(
50
47
  * Safely get a Web Storage instance (localStorage or sessionStorage).
51
48
  * Returns null if not available (SSR, security restrictions, etc.).
52
49
  */
53
- export function getWebStorage(type: 'local' | 'session'): Storage | null {
50
+ export function getWebStorage(type: "local" | "session"): Storage | null {
54
51
  if (!isBrowser()) return null
55
52
  try {
56
- const storage =
57
- type === 'local' ? window.localStorage : window.sessionStorage
53
+ const storage = type === "local" ? window.localStorage : window.sessionStorage
58
54
  // Test that it actually works (can throw in private browsing)
59
- const testKey = '__pyreon_storage_test__'
60
- storage.setItem(testKey, '1')
55
+ const testKey = "__pyreon_storage_test__"
56
+ storage.setItem(testKey, "1")
61
57
  storage.removeItem(testKey)
62
58
  return storage
63
59
  } catch {