@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,67 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { _resetRegistry, useSessionStorage } from '../index'
|
|
3
|
+
|
|
4
|
+
describe('useSessionStorage', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
sessionStorage.clear()
|
|
7
|
+
_resetRegistry()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
sessionStorage.clear()
|
|
12
|
+
_resetRegistry()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns default value when key is not in storage', () => {
|
|
16
|
+
const step = useSessionStorage('step', 0)
|
|
17
|
+
expect(step()).toBe(0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('reads existing value from sessionStorage', () => {
|
|
21
|
+
sessionStorage.setItem('step', JSON.stringify(3))
|
|
22
|
+
const step = useSessionStorage('step', 0)
|
|
23
|
+
expect(step()).toBe(3)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('.set() updates signal and sessionStorage', () => {
|
|
27
|
+
const step = useSessionStorage('step', 0)
|
|
28
|
+
step.set(5)
|
|
29
|
+
expect(step()).toBe(5)
|
|
30
|
+
expect(JSON.parse(sessionStorage.getItem('step')!)).toBe(5)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('.remove() clears from storage and resets to default', () => {
|
|
34
|
+
const step = useSessionStorage('step', 0)
|
|
35
|
+
step.set(5)
|
|
36
|
+
step.remove()
|
|
37
|
+
expect(step()).toBe(0)
|
|
38
|
+
expect(sessionStorage.getItem('step')).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns same signal instance for same key', () => {
|
|
42
|
+
const a = useSessionStorage('step', 0)
|
|
43
|
+
const b = useSessionStorage('step', 0)
|
|
44
|
+
expect(a).toBe(b)
|
|
45
|
+
})
|
|
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' })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('handles corrupt storage values gracefully', () => {
|
|
54
|
+
sessionStorage.setItem('broken', '{{invalid')
|
|
55
|
+
const value = useSessionStorage('broken', 'default')
|
|
56
|
+
expect(value()).toBe('default')
|
|
57
|
+
})
|
|
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')
|
|
63
|
+
expect(local).not.toBe(session)
|
|
64
|
+
expect(local()).toBe('local-default')
|
|
65
|
+
expect(session()).toBe('session-default')
|
|
66
|
+
})
|
|
67
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Signal } from '@pyreon/reactivity'
|
|
2
|
+
|
|
3
|
+
// ─── Storage Signal ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A signal backed by a storage backend. Behaves like a normal signal
|
|
7
|
+
* but persists writes to the underlying storage mechanism.
|
|
8
|
+
*/
|
|
9
|
+
export interface StorageSignal<T> extends Signal<T> {
|
|
10
|
+
/** Remove the value from storage and reset to the default value */
|
|
11
|
+
remove(): void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Shared Options ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base options shared by all storage hooks.
|
|
18
|
+
*/
|
|
19
|
+
export interface StorageOptions<T> {
|
|
20
|
+
/** Custom serializer — default: JSON.stringify */
|
|
21
|
+
serializer?: (value: T) => string
|
|
22
|
+
/** Custom deserializer — default: JSON.parse */
|
|
23
|
+
deserializer?: (raw: string) => T
|
|
24
|
+
/** Called when deserialization fails — returns fallback or void for default */
|
|
25
|
+
onError?: (error: Error) => T | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Cookie Options ──────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for the useCookie hook.
|
|
32
|
+
*/
|
|
33
|
+
export interface CookieOptions<T> extends StorageOptions<T> {
|
|
34
|
+
/** Max age in seconds */
|
|
35
|
+
maxAge?: number
|
|
36
|
+
/** Expiry date (alternative to maxAge) */
|
|
37
|
+
expires?: Date
|
|
38
|
+
/** Cookie path — default: '/' */
|
|
39
|
+
path?: string
|
|
40
|
+
/** Cookie domain */
|
|
41
|
+
domain?: string
|
|
42
|
+
/** HTTPS only — default: false */
|
|
43
|
+
secure?: boolean
|
|
44
|
+
/** SameSite policy — default: 'lax' */
|
|
45
|
+
sameSite?: 'strict' | 'lax' | 'none'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── IndexedDB Options ───────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for the useIndexedDB hook.
|
|
52
|
+
*/
|
|
53
|
+
export interface IndexedDBOptions<T> extends StorageOptions<T> {
|
|
54
|
+
/** Database name — default: 'pyreon-storage' */
|
|
55
|
+
dbName?: string
|
|
56
|
+
/** Object store name — default: 'kv' */
|
|
57
|
+
storeName?: string
|
|
58
|
+
/** Write debounce in ms — default: 100 */
|
|
59
|
+
debounceMs?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Custom Storage Backend ──────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Interface for a custom storage backend used with createStorage.
|
|
66
|
+
*/
|
|
67
|
+
export interface StorageBackend {
|
|
68
|
+
/** Read a raw string value by key. Return null if not found. */
|
|
69
|
+
get(key: string): string | null
|
|
70
|
+
/** Write a raw string value by key */
|
|
71
|
+
set(key: string, value: string): void
|
|
72
|
+
/** Remove a value by key */
|
|
73
|
+
remove(key: string): void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Async variant for backends like IndexedDB.
|
|
78
|
+
*/
|
|
79
|
+
export interface AsyncStorageBackend {
|
|
80
|
+
/** Read a raw string value by key */
|
|
81
|
+
get(key: string): Promise<string | null>
|
|
82
|
+
/** Write a raw string value by key */
|
|
83
|
+
set(key: string, value: string): Promise<void>
|
|
84
|
+
/** Remove a value by key */
|
|
85
|
+
remove(key: string): Promise<void>
|
|
86
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StorageOptions } from './types'
|
|
2
|
+
|
|
3
|
+
// ─── SSR Detection ───────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if we're running in a browser environment.
|
|
7
|
+
*/
|
|
8
|
+
export function isBrowser(): boolean {
|
|
9
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ─── Serialization ───────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Serialize a value to a string for storage.
|
|
16
|
+
*/
|
|
17
|
+
export function serialize<T>(
|
|
18
|
+
value: T,
|
|
19
|
+
serializer?: StorageOptions<T>['serializer'],
|
|
20
|
+
): string {
|
|
21
|
+
if (serializer) return serializer(value)
|
|
22
|
+
return JSON.stringify(value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Deserialize a raw string from storage back to a typed value.
|
|
27
|
+
* Returns the default value if deserialization fails.
|
|
28
|
+
*/
|
|
29
|
+
export function deserialize<T>(
|
|
30
|
+
raw: string,
|
|
31
|
+
defaultValue: T,
|
|
32
|
+
deserializer?: StorageOptions<T>['deserializer'],
|
|
33
|
+
onError?: StorageOptions<T>['onError'],
|
|
34
|
+
): T {
|
|
35
|
+
try {
|
|
36
|
+
if (deserializer) return deserializer(raw)
|
|
37
|
+
return JSON.parse(raw) as T
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (onError) {
|
|
40
|
+
const result = onError(e as Error)
|
|
41
|
+
return result !== undefined ? result : defaultValue
|
|
42
|
+
}
|
|
43
|
+
return defaultValue
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Safe Storage Access ─────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Safely get a Web Storage instance (localStorage or sessionStorage).
|
|
51
|
+
* Returns null if not available (SSR, security restrictions, etc.).
|
|
52
|
+
*/
|
|
53
|
+
export function getWebStorage(type: 'local' | 'session'): Storage | null {
|
|
54
|
+
if (!isBrowser()) return null
|
|
55
|
+
try {
|
|
56
|
+
const storage =
|
|
57
|
+
type === 'local' ? window.localStorage : window.sessionStorage
|
|
58
|
+
// Test that it actually works (can throw in private browsing)
|
|
59
|
+
const testKey = '__pyreon_storage_test__'
|
|
60
|
+
storage.setItem(testKey, '1')
|
|
61
|
+
storage.removeItem(testKey)
|
|
62
|
+
return storage
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|