@pyreon/storage 0.10.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.
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +3 -3
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +12 -6
- package/src/clear.ts +13 -13
- package/src/cookie.ts +16 -20
- package/src/custom.ts +9 -18
- package/src/index.ts +9 -9
- package/src/indexed-db.ts +21 -41
- package/src/local.ts +15 -28
- package/src/registry.ts +2 -5
- package/src/session.ts +10 -21
- package/src/tests/clear.test.ts +43 -43
- package/src/tests/cookie.test.ts +66 -66
- package/src/tests/custom.test.ts +59 -59
- package/src/tests/indexed-db.test.ts +36 -36
- package/src/tests/local.test.ts +98 -98
- package/src/tests/session.test.ts +31 -31
- package/src/types.ts +2 -2
- package/src/utils.ts +9 -13
package/src/indexed-db.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { getEntry, removeEntry, setEntry } from
|
|
3
|
-
import type { IndexedDBOptions, StorageSignal } from
|
|
4
|
-
import { deserialize, isBrowser, serialize } from
|
|
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
5
|
|
|
6
6
|
// ─── Database management ─────────────────────────────────────────────────────
|
|
7
7
|
|
|
@@ -30,13 +30,9 @@ function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
|
|
|
30
30
|
return promise
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function idbGet(
|
|
34
|
-
db: IDBDatabase,
|
|
35
|
-
storeName: string,
|
|
36
|
-
key: string,
|
|
37
|
-
): Promise<string | null> {
|
|
33
|
+
function idbGet(db: IDBDatabase, storeName: string, key: string): Promise<string | null> {
|
|
38
34
|
return new Promise((resolve, reject) => {
|
|
39
|
-
const tx = db.transaction(storeName,
|
|
35
|
+
const tx = db.transaction(storeName, "readonly")
|
|
40
36
|
const store = tx.objectStore(storeName)
|
|
41
37
|
const request = store.get(key)
|
|
42
38
|
request.onsuccess = () =>
|
|
@@ -45,14 +41,9 @@ function idbGet(
|
|
|
45
41
|
})
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
function idbSet(
|
|
49
|
-
db: IDBDatabase,
|
|
50
|
-
storeName: string,
|
|
51
|
-
key: string,
|
|
52
|
-
value: string,
|
|
53
|
-
): Promise<void> {
|
|
44
|
+
function idbSet(db: IDBDatabase, storeName: string, key: string, value: string): Promise<void> {
|
|
54
45
|
return new Promise((resolve, reject) => {
|
|
55
|
-
const tx = db.transaction(storeName,
|
|
46
|
+
const tx = db.transaction(storeName, "readwrite")
|
|
56
47
|
const store = tx.objectStore(storeName)
|
|
57
48
|
const request = store.put(value, key)
|
|
58
49
|
request.onsuccess = () => resolve()
|
|
@@ -60,13 +51,9 @@ function idbSet(
|
|
|
60
51
|
})
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
function idbDelete(
|
|
64
|
-
db: IDBDatabase,
|
|
65
|
-
storeName: string,
|
|
66
|
-
key: string,
|
|
67
|
-
): Promise<void> {
|
|
54
|
+
function idbDelete(db: IDBDatabase, storeName: string, key: string): Promise<void> {
|
|
68
55
|
return new Promise((resolve, reject) => {
|
|
69
|
-
const tx = db.transaction(storeName,
|
|
56
|
+
const tx = db.transaction(storeName, "readwrite")
|
|
70
57
|
const store = tx.objectStore(storeName)
|
|
71
58
|
const request = store.delete(key)
|
|
72
59
|
request.onsuccess = () => resolve()
|
|
@@ -96,27 +83,22 @@ export function useIndexedDB<T>(
|
|
|
96
83
|
options: IndexedDBOptions<T> = {},
|
|
97
84
|
): StorageSignal<T> {
|
|
98
85
|
// Return existing signal if already registered
|
|
99
|
-
const existing = getEntry<T>(
|
|
86
|
+
const existing = getEntry<T>("indexeddb", key)
|
|
100
87
|
if (existing) return existing.signal
|
|
101
88
|
|
|
102
|
-
const dbName = options.dbName ??
|
|
103
|
-
const storeName = options.storeName ??
|
|
89
|
+
const dbName = options.dbName ?? "pyreon-storage"
|
|
90
|
+
const storeName = options.storeName ?? "kv"
|
|
104
91
|
const debounceMs = options.debounceMs ?? 100
|
|
105
92
|
|
|
106
93
|
const sig = signal<T>(defaultValue)
|
|
107
94
|
|
|
108
95
|
// Async initial load
|
|
109
|
-
if (isBrowser() && typeof indexedDB !==
|
|
96
|
+
if (isBrowser() && typeof indexedDB !== "undefined") {
|
|
110
97
|
openDB(dbName, storeName)
|
|
111
98
|
.then((db) => idbGet(db, storeName, key))
|
|
112
99
|
.then((raw) => {
|
|
113
100
|
if (raw !== null) {
|
|
114
|
-
const value = deserialize(
|
|
115
|
-
raw,
|
|
116
|
-
defaultValue,
|
|
117
|
-
options.deserializer,
|
|
118
|
-
options.onError,
|
|
119
|
-
)
|
|
101
|
+
const value = deserialize(raw, defaultValue, options.deserializer, options.onError)
|
|
120
102
|
sig.set(value)
|
|
121
103
|
}
|
|
122
104
|
})
|
|
@@ -134,12 +116,10 @@ export function useIndexedDB<T>(
|
|
|
134
116
|
const value = pendingValue
|
|
135
117
|
pendingValue = undefined
|
|
136
118
|
|
|
137
|
-
if (!isBrowser() || typeof indexedDB ===
|
|
119
|
+
if (!isBrowser() || typeof indexedDB === "undefined") return
|
|
138
120
|
|
|
139
121
|
openDB(dbName, storeName)
|
|
140
|
-
.then((db) =>
|
|
141
|
-
idbSet(db, storeName, key, serialize(value, options.serializer)),
|
|
142
|
-
)
|
|
122
|
+
.then((db) => idbSet(db, storeName, key, serialize(value, options.serializer)))
|
|
143
123
|
.catch(() => {
|
|
144
124
|
// Write failed — signal still has the correct value
|
|
145
125
|
})
|
|
@@ -159,7 +139,7 @@ export function useIndexedDB<T>(
|
|
|
159
139
|
storageSig.direct = (updater: () => void) => sig.direct(updater)
|
|
160
140
|
storageSig.debug = () => sig.debug()
|
|
161
141
|
|
|
162
|
-
Object.defineProperty(storageSig,
|
|
142
|
+
Object.defineProperty(storageSig, "label", {
|
|
163
143
|
get: () => sig.label,
|
|
164
144
|
set: (v: string | undefined) => {
|
|
165
145
|
sig.label = v
|
|
@@ -181,7 +161,7 @@ export function useIndexedDB<T>(
|
|
|
181
161
|
pendingValue = undefined
|
|
182
162
|
if (writeTimer !== null) clearTimeout(writeTimer)
|
|
183
163
|
|
|
184
|
-
if (isBrowser() && typeof indexedDB !==
|
|
164
|
+
if (isBrowser() && typeof indexedDB !== "undefined") {
|
|
185
165
|
openDB(dbName, storeName)
|
|
186
166
|
.then((db) => idbDelete(db, storeName, key))
|
|
187
167
|
.catch(() => {
|
|
@@ -189,10 +169,10 @@ export function useIndexedDB<T>(
|
|
|
189
169
|
})
|
|
190
170
|
}
|
|
191
171
|
|
|
192
|
-
removeEntry(
|
|
172
|
+
removeEntry("indexeddb", key)
|
|
193
173
|
}
|
|
194
174
|
|
|
195
|
-
setEntry(
|
|
175
|
+
setEntry("indexeddb", key, storageSig, defaultValue)
|
|
196
176
|
|
|
197
177
|
return storageSig
|
|
198
178
|
}
|
package/src/local.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { getEntry, removeEntry, setEntry } from
|
|
3
|
-
import type { StorageOptions, StorageSignal } from
|
|
4
|
-
import { deserialize, getWebStorage, isBrowser, serialize } from
|
|
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
5
|
|
|
6
6
|
// ─── Cross-tab sync ──────────────────────────────────────────────────────────
|
|
7
7
|
|
|
@@ -11,15 +11,13 @@ function attachStorageListener(): void {
|
|
|
11
11
|
if (listenerAttached || !isBrowser()) return
|
|
12
12
|
listenerAttached = true
|
|
13
13
|
|
|
14
|
-
window.addEventListener(
|
|
14
|
+
window.addEventListener("storage", (e) => {
|
|
15
15
|
if (!e.key) return
|
|
16
|
-
const entry = getEntry(
|
|
16
|
+
const entry = getEntry("local", e.key)
|
|
17
17
|
if (!entry) return
|
|
18
18
|
|
|
19
19
|
const newValue =
|
|
20
|
-
e.newValue !== null
|
|
21
|
-
? deserialize(e.newValue, entry.defaultValue)
|
|
22
|
-
: entry.defaultValue
|
|
20
|
+
e.newValue !== null ? deserialize(e.newValue, entry.defaultValue) : entry.defaultValue
|
|
23
21
|
|
|
24
22
|
entry.signal.set(newValue)
|
|
25
23
|
})
|
|
@@ -45,37 +43,26 @@ export function useStorage<T>(
|
|
|
45
43
|
options?: StorageOptions<T>,
|
|
46
44
|
): StorageSignal<T> {
|
|
47
45
|
// Return existing signal if already registered
|
|
48
|
-
const existing = getEntry<T>(
|
|
46
|
+
const existing = getEntry<T>("local", key)
|
|
49
47
|
if (existing) return existing.signal
|
|
50
48
|
|
|
51
|
-
const storage = getWebStorage(
|
|
49
|
+
const storage = getWebStorage("local")
|
|
52
50
|
|
|
53
51
|
// Read initial value from storage
|
|
54
52
|
let initialValue = defaultValue
|
|
55
53
|
if (storage) {
|
|
56
54
|
const raw = storage.getItem(key)
|
|
57
55
|
if (raw !== null) {
|
|
58
|
-
initialValue = deserialize(
|
|
59
|
-
raw,
|
|
60
|
-
defaultValue,
|
|
61
|
-
options?.deserializer,
|
|
62
|
-
options?.onError,
|
|
63
|
-
)
|
|
56
|
+
initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)
|
|
64
57
|
}
|
|
65
58
|
}
|
|
66
59
|
|
|
67
60
|
const sig = signal<T>(initialValue)
|
|
68
61
|
|
|
69
62
|
// Create the storage signal by extending the base signal
|
|
70
|
-
const storageSig = createStorageSignal(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
defaultValue,
|
|
74
|
-
'local',
|
|
75
|
-
options,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
setEntry('local', key, storageSig, defaultValue)
|
|
63
|
+
const storageSig = createStorageSignal(sig, key, defaultValue, "local", options)
|
|
64
|
+
|
|
65
|
+
setEntry("local", key, storageSig, defaultValue)
|
|
79
66
|
attachStorageListener()
|
|
80
67
|
|
|
81
68
|
return storageSig
|
|
@@ -91,7 +78,7 @@ export function createStorageSignal<T>(
|
|
|
91
78
|
sig: ReturnType<typeof signal<T>>,
|
|
92
79
|
key: string,
|
|
93
80
|
defaultValue: T,
|
|
94
|
-
backend:
|
|
81
|
+
backend: "local" | "session",
|
|
95
82
|
options?: StorageOptions<T>,
|
|
96
83
|
): StorageSignal<T> {
|
|
97
84
|
const storage = getWebStorage(backend)
|
|
@@ -105,7 +92,7 @@ export function createStorageSignal<T>(
|
|
|
105
92
|
storageSig.direct = (updater: () => void) => sig.direct(updater)
|
|
106
93
|
storageSig.debug = () => sig.debug()
|
|
107
94
|
|
|
108
|
-
Object.defineProperty(storageSig,
|
|
95
|
+
Object.defineProperty(storageSig, "label", {
|
|
109
96
|
get: () => sig.label,
|
|
110
97
|
set: (v: string | undefined) => {
|
|
111
98
|
sig.label = v
|
package/src/registry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageSignal } from
|
|
1
|
+
import type { StorageSignal } from "./types"
|
|
2
2
|
|
|
3
3
|
// ─── Signal Registry ─────────────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -21,10 +21,7 @@ function registryKey(backend: string, key: string): string {
|
|
|
21
21
|
/**
|
|
22
22
|
* Get an existing signal from the registry.
|
|
23
23
|
*/
|
|
24
|
-
export function getEntry<T>(
|
|
25
|
-
backend: string,
|
|
26
|
-
key: string,
|
|
27
|
-
): RegistryEntry<T> | undefined {
|
|
24
|
+
export function getEntry<T>(backend: string, key: string): RegistryEntry<T> | undefined {
|
|
28
25
|
return registry.get(registryKey(backend, key)) as RegistryEntry<T> | undefined
|
|
29
26
|
}
|
|
30
27
|
|
package/src/session.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { createStorageSignal } from
|
|
3
|
-
import { getEntry, setEntry } from
|
|
4
|
-
import type { StorageOptions, StorageSignal } from
|
|
5
|
-
import { deserialize, getWebStorage } from
|
|
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
6
|
|
|
7
7
|
// ─── useSessionStorage ───────────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -24,35 +24,24 @@ export function useSessionStorage<T>(
|
|
|
24
24
|
options?: StorageOptions<T>,
|
|
25
25
|
): StorageSignal<T> {
|
|
26
26
|
// Return existing signal if already registered
|
|
27
|
-
const existing = getEntry<T>(
|
|
27
|
+
const existing = getEntry<T>("session", key)
|
|
28
28
|
if (existing) return existing.signal
|
|
29
29
|
|
|
30
|
-
const storage = getWebStorage(
|
|
30
|
+
const storage = getWebStorage("session")
|
|
31
31
|
|
|
32
32
|
// Read initial value from storage
|
|
33
33
|
let initialValue = defaultValue
|
|
34
34
|
if (storage) {
|
|
35
35
|
const raw = storage.getItem(key)
|
|
36
36
|
if (raw !== null) {
|
|
37
|
-
initialValue = deserialize(
|
|
38
|
-
raw,
|
|
39
|
-
defaultValue,
|
|
40
|
-
options?.deserializer,
|
|
41
|
-
options?.onError,
|
|
42
|
-
)
|
|
37
|
+
initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
const sig = signal<T>(initialValue)
|
|
47
|
-
const storageSig = createStorageSignal(
|
|
48
|
-
sig,
|
|
49
|
-
key,
|
|
50
|
-
defaultValue,
|
|
51
|
-
'session',
|
|
52
|
-
options,
|
|
53
|
-
)
|
|
42
|
+
const storageSig = createStorageSignal(sig, key, defaultValue, "session", options)
|
|
54
43
|
|
|
55
|
-
setEntry(
|
|
44
|
+
setEntry("session", key, storageSig, defaultValue)
|
|
56
45
|
|
|
57
46
|
return storageSig
|
|
58
47
|
}
|
package/src/tests/clear.test.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
2
2
|
import {
|
|
3
3
|
_resetRegistry,
|
|
4
4
|
clearStorage,
|
|
5
5
|
removeStorage,
|
|
6
6
|
useSessionStorage,
|
|
7
7
|
useStorage,
|
|
8
|
-
} from
|
|
8
|
+
} from "../index"
|
|
9
9
|
|
|
10
|
-
describe(
|
|
10
|
+
describe("removeStorage", () => {
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
localStorage.clear()
|
|
13
13
|
sessionStorage.clear()
|
|
@@ -20,43 +20,43 @@ describe('removeStorage', () => {
|
|
|
20
20
|
_resetRegistry()
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
it(
|
|
24
|
-
const theme = useStorage(
|
|
25
|
-
theme.set(
|
|
23
|
+
it("removes a localStorage entry via signal", () => {
|
|
24
|
+
const theme = useStorage("theme", "light")
|
|
25
|
+
theme.set("dark")
|
|
26
26
|
|
|
27
|
-
removeStorage(
|
|
28
|
-
expect(theme()).toBe(
|
|
29
|
-
expect(localStorage.getItem(
|
|
27
|
+
removeStorage("theme")
|
|
28
|
+
expect(theme()).toBe("light")
|
|
29
|
+
expect(localStorage.getItem("theme")).toBeNull()
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it(
|
|
33
|
-
const step = useSessionStorage(
|
|
32
|
+
it("removes a sessionStorage entry", () => {
|
|
33
|
+
const step = useSessionStorage("step", 0)
|
|
34
34
|
step.set(3)
|
|
35
35
|
|
|
36
|
-
removeStorage(
|
|
36
|
+
removeStorage("step", { type: "session" })
|
|
37
37
|
expect(step()).toBe(0)
|
|
38
|
-
expect(sessionStorage.getItem(
|
|
38
|
+
expect(sessionStorage.getItem("step")).toBeNull()
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it(
|
|
42
|
-
localStorage.setItem(
|
|
43
|
-
removeStorage(
|
|
44
|
-
expect(localStorage.getItem(
|
|
41
|
+
it("removes raw localStorage even without a signal", () => {
|
|
42
|
+
localStorage.setItem("orphan", "value")
|
|
43
|
+
removeStorage("orphan")
|
|
44
|
+
expect(localStorage.getItem("orphan")).toBeNull()
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
it(
|
|
48
|
-
sessionStorage.setItem(
|
|
49
|
-
removeStorage(
|
|
50
|
-
expect(sessionStorage.getItem(
|
|
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
51
|
})
|
|
52
52
|
|
|
53
|
-
it(
|
|
54
|
-
removeStorage(
|
|
53
|
+
it("removes cookie without a signal", () => {
|
|
54
|
+
removeStorage("orphan-cookie", { type: "cookie" })
|
|
55
55
|
// Should not throw even when cookie doesn't exist
|
|
56
56
|
})
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
describe(
|
|
59
|
+
describe("clearStorage", () => {
|
|
60
60
|
beforeEach(() => {
|
|
61
61
|
localStorage.clear()
|
|
62
62
|
sessionStorage.clear()
|
|
@@ -69,9 +69,9 @@ describe('clearStorage', () => {
|
|
|
69
69
|
_resetRegistry()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it(
|
|
73
|
-
const a = useStorage(
|
|
74
|
-
const b = useStorage(
|
|
72
|
+
it("clears all managed localStorage entries", () => {
|
|
73
|
+
const a = useStorage("a", 1)
|
|
74
|
+
const b = useStorage("b", 2)
|
|
75
75
|
a.set(10)
|
|
76
76
|
b.set(20)
|
|
77
77
|
|
|
@@ -80,25 +80,25 @@ describe('clearStorage', () => {
|
|
|
80
80
|
expect(b()).toBe(2)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
it(
|
|
84
|
-
const a = useSessionStorage(
|
|
85
|
-
const b = useSessionStorage(
|
|
86
|
-
a.set(
|
|
87
|
-
b.set(
|
|
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
88
|
|
|
89
|
-
clearStorage(
|
|
90
|
-
expect(a()).toBe(
|
|
91
|
-
expect(b()).toBe(
|
|
89
|
+
clearStorage("session")
|
|
90
|
+
expect(a()).toBe("x")
|
|
91
|
+
expect(b()).toBe("y")
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
it('clears all backends with "all"', () => {
|
|
95
|
-
const local = useStorage(
|
|
96
|
-
const session = useSessionStorage(
|
|
97
|
-
local.set(
|
|
98
|
-
session.set(
|
|
99
|
-
|
|
100
|
-
clearStorage(
|
|
101
|
-
expect(local()).toBe(
|
|
102
|
-
expect(session()).toBe(
|
|
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
103
|
})
|
|
104
104
|
})
|
package/src/tests/cookie.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from
|
|
2
|
-
import { _resetRegistry, setCookieSource, useCookie } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
2
|
+
import { _resetRegistry, setCookieSource, useCookie } from "../index"
|
|
3
3
|
|
|
4
4
|
function clearAllCookies(): void {
|
|
5
|
-
for (const cookie of document.cookie.split(
|
|
6
|
-
const name = cookie.split(
|
|
5
|
+
for (const cookie of document.cookie.split(";")) {
|
|
6
|
+
const name = cookie.split("=")[0]?.trim()
|
|
7
7
|
if (name) {
|
|
8
8
|
// biome-ignore lint/suspicious/noDocumentCookie: test cleanup requires direct cookie access
|
|
9
9
|
document.cookie = `${name}=; max-age=0; path=/`
|
|
@@ -11,7 +11,7 @@ function clearAllCookies(): void {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
describe(
|
|
14
|
+
describe("useCookie", () => {
|
|
15
15
|
beforeEach(() => {
|
|
16
16
|
_resetRegistry()
|
|
17
17
|
clearAllCookies()
|
|
@@ -22,128 +22,128 @@ describe('useCookie', () => {
|
|
|
22
22
|
clearAllCookies()
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
it(
|
|
26
|
-
const locale = useCookie(
|
|
27
|
-
expect(locale()).toBe(
|
|
25
|
+
it("returns default value when cookie does not exist", () => {
|
|
26
|
+
const locale = useCookie("locale", "en")
|
|
27
|
+
expect(locale()).toBe("en")
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
it(
|
|
30
|
+
it("reads existing cookie value", () => {
|
|
31
31
|
// biome-ignore lint/suspicious/noDocumentCookie: test setup requires direct cookie access
|
|
32
|
-
document.cookie = `locale=${encodeURIComponent(JSON.stringify(
|
|
33
|
-
const locale = useCookie(
|
|
34
|
-
expect(locale()).toBe(
|
|
32
|
+
document.cookie = `locale=${encodeURIComponent(JSON.stringify("de"))}; path=/`
|
|
33
|
+
const locale = useCookie("locale", "en")
|
|
34
|
+
expect(locale()).toBe("de")
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
it(
|
|
38
|
-
const locale = useCookie(
|
|
39
|
-
locale.set(
|
|
40
|
-
expect(locale()).toBe(
|
|
41
|
-
expect(document.cookie).toContain(
|
|
37
|
+
it(".set() updates signal and writes cookie", () => {
|
|
38
|
+
const locale = useCookie("locale", "en")
|
|
39
|
+
locale.set("fr")
|
|
40
|
+
expect(locale()).toBe("fr")
|
|
41
|
+
expect(document.cookie).toContain("locale")
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
it(
|
|
45
|
-
const locale = useCookie(
|
|
46
|
-
locale.set(
|
|
44
|
+
it(".remove() deletes cookie and resets to default", () => {
|
|
45
|
+
const locale = useCookie("locale", "en")
|
|
46
|
+
locale.set("fr")
|
|
47
47
|
locale.remove()
|
|
48
|
-
expect(locale()).toBe(
|
|
48
|
+
expect(locale()).toBe("en")
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it(
|
|
52
|
-
const a = useCookie(
|
|
53
|
-
const b = useCookie(
|
|
51
|
+
it("returns same signal instance for same key", () => {
|
|
52
|
+
const a = useCookie("locale", "en")
|
|
53
|
+
const b = useCookie("locale", "en")
|
|
54
54
|
expect(a).toBe(b)
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
it(
|
|
58
|
-
const prefs = useCookie(
|
|
57
|
+
it("works with objects", () => {
|
|
58
|
+
const prefs = useCookie("prefs", { dark: false })
|
|
59
59
|
prefs.set({ dark: true })
|
|
60
60
|
expect(prefs()).toEqual({ dark: true })
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
it(
|
|
64
|
-
const count = useCookie(
|
|
63
|
+
it(".update() works", () => {
|
|
64
|
+
const count = useCookie("count", 0)
|
|
65
65
|
count.update((n) => n + 1)
|
|
66
66
|
expect(count()).toBe(1)
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
it(
|
|
70
|
-
const locale = useCookie(
|
|
71
|
-
expect(locale.peek()).toBe(
|
|
69
|
+
it(".peek() reads without subscribing", () => {
|
|
70
|
+
const locale = useCookie("locale", "en")
|
|
71
|
+
expect(locale.peek()).toBe("en")
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
it(
|
|
75
|
-
const locale = useCookie(
|
|
76
|
-
locale.set(
|
|
77
|
-
expect(document.cookie).toContain(
|
|
74
|
+
it("respects maxAge option", () => {
|
|
75
|
+
const locale = useCookie("locale", "en", { maxAge: 3600 })
|
|
76
|
+
locale.set("de")
|
|
77
|
+
expect(document.cookie).toContain("locale")
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
it(
|
|
81
|
-
const token = useCookie(
|
|
82
|
-
token.set(
|
|
83
|
-
expect(token()).toBe(
|
|
80
|
+
it("respects secure option", () => {
|
|
81
|
+
const token = useCookie("token", "", { secure: true })
|
|
82
|
+
token.set("abc123")
|
|
83
|
+
expect(token()).toBe("abc123")
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
it(
|
|
86
|
+
it("respects expires option", () => {
|
|
87
87
|
const future = new Date(Date.now() + 86400000)
|
|
88
|
-
const sig = useCookie(
|
|
89
|
-
sig.set(
|
|
90
|
-
expect(sig()).toBe(
|
|
88
|
+
const sig = useCookie("exp", "val", { expires: future })
|
|
89
|
+
sig.set("updated")
|
|
90
|
+
expect(sig()).toBe("updated")
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
it(
|
|
94
|
-
const sig = useCookie(
|
|
95
|
-
sig.set(
|
|
96
|
-
expect(sig()).toBe(
|
|
93
|
+
it("respects domain option on set and remove", () => {
|
|
94
|
+
const sig = useCookie("dom", "val", { domain: "example.com" })
|
|
95
|
+
sig.set("updated")
|
|
96
|
+
expect(sig()).toBe("updated")
|
|
97
97
|
sig.remove()
|
|
98
|
-
expect(sig()).toBe(
|
|
98
|
+
expect(sig()).toBe("val")
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
-
it(
|
|
102
|
-
const sig = useCookie(
|
|
101
|
+
it(".subscribe() works", () => {
|
|
102
|
+
const sig = useCookie("sub", "a")
|
|
103
103
|
let called = false
|
|
104
104
|
const unsub = sig.subscribe(() => {
|
|
105
105
|
called = true
|
|
106
106
|
})
|
|
107
|
-
sig.set(
|
|
107
|
+
sig.set("b")
|
|
108
108
|
expect(called).toBe(true)
|
|
109
109
|
unsub()
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
it(
|
|
113
|
-
const sig = useCookie(
|
|
112
|
+
it(".direct() works", () => {
|
|
113
|
+
const sig = useCookie("dir", "a")
|
|
114
114
|
let called = false
|
|
115
115
|
const unsub = sig.direct(() => {
|
|
116
116
|
called = true
|
|
117
117
|
})
|
|
118
|
-
sig.set(
|
|
118
|
+
sig.set("b")
|
|
119
119
|
expect(called).toBe(true)
|
|
120
120
|
unsub()
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
-
it(
|
|
124
|
-
const sig = useCookie(
|
|
125
|
-
expect(sig.debug().value).toBe(
|
|
123
|
+
it(".debug() returns debug info", () => {
|
|
124
|
+
const sig = useCookie("dbg", "test")
|
|
125
|
+
expect(sig.debug().value).toBe("test")
|
|
126
126
|
})
|
|
127
127
|
|
|
128
|
-
it(
|
|
129
|
-
const sig = useCookie(
|
|
130
|
-
sig.label =
|
|
131
|
-
expect(sig.label).toBe(
|
|
128
|
+
it(".label can be set and read", () => {
|
|
129
|
+
const sig = useCookie("lbl", "val")
|
|
130
|
+
sig.label = "my-cookie"
|
|
131
|
+
expect(sig.label).toBe("my-cookie")
|
|
132
132
|
})
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
describe(
|
|
135
|
+
describe("setCookieSource (SSR)", () => {
|
|
136
136
|
beforeEach(() => {
|
|
137
137
|
_resetRegistry()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
afterEach(() => {
|
|
141
141
|
_resetRegistry()
|
|
142
|
-
setCookieSource(
|
|
142
|
+
setCookieSource("")
|
|
143
143
|
})
|
|
144
144
|
|
|
145
|
-
it(
|
|
146
|
-
setCookieSource(
|
|
147
|
-
expect(() => setCookieSource(
|
|
145
|
+
it("stores server cookie string without throwing", () => {
|
|
146
|
+
setCookieSource("locale=de; theme=dark")
|
|
147
|
+
expect(() => setCookieSource("foo=bar")).not.toThrow()
|
|
148
148
|
})
|
|
149
149
|
})
|