react-wire-persisted 2.0.1 → 3.0.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/README.md +121 -28
- package/client.js +1 -1
- package/client.mjs +6 -5
- package/dist/index.d.ts +197 -0
- package/dist/index.js +241 -305
- package/dist/react-wire-persisted.js +241 -305
- package/dist/react-wire-persisted.js.map +1 -0
- package/dist/react-wire-persisted.umd.cjs +2 -1
- package/dist/react-wire-persisted.umd.cjs.map +1 -0
- package/nextjs.js +4 -27
- package/package.json +91 -75
- package/src/components/{HydrationProvider.jsx → HydrationProvider.tsx} +13 -3
- package/src/components/index.tsx +1 -0
- package/src/global.d.ts +16 -0
- package/src/hooks/{useHydration.js → useHydration.ts} +19 -26
- package/src/index.ts +9 -0
- package/src/providers/LocalStorageProvider.ts +145 -0
- package/src/providers/MemoryStorageProvider.ts +14 -0
- package/src/providers/{StorageProvider.js → RWPStorageProvider.ts} +42 -39
- package/src/react-wire-persisted.ts +257 -0
- package/src/types.ts +19 -0
- package/src/utils/fakeLocalStorage.ts +17 -0
- package/src/utils/{index.js → index.ts} +5 -7
- package/src/utils/{isomorphic.js → isomorphic.ts} +24 -11
- package/src/utils/keys.ts +49 -0
- package/src/components/HydrationProvider.js +0 -45
- package/src/components/index.js +0 -1
- package/src/index.js +0 -6
- package/src/providers/LocalStorageProvider.js +0 -194
- package/src/providers/MemoryStorageProvider.js +0 -20
- package/src/react-wire-persisted.js +0 -136
- package/src/utils/fakeLocalStorage.js +0 -16
- package/src/utils/keys.js +0 -49
|
@@ -1,102 +1,105 @@
|
|
|
1
|
-
|
|
2
1
|
/**
|
|
3
2
|
* Base class to allow storage access
|
|
4
|
-
* @see `LocalStorageProvider.
|
|
3
|
+
* @see `LocalStorageProvider.ts` for an example implementation
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: WIP next PR will switch to TypeScript */
|
|
6
|
+
export abstract class RWPStorageProvider {
|
|
7
|
+
namespace: string | null
|
|
8
|
+
registry: Record<string, unknown>
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Initializes the class
|
|
10
|
-
* @param {String} namespace Namespace to prefix all keys with. Mostly used for the logging
|
|
12
|
+
* @param {String} namespace Namespace to prefix all keys with. Mostly used for the logging and reset functions
|
|
11
13
|
* @param {Object} registry (Optional) Initialize the storage provider with an existing registry
|
|
12
14
|
*/
|
|
13
|
-
constructor(namespace, registry) {
|
|
14
|
-
|
|
15
|
+
protected constructor(namespace: string, registry: Record<string, unknown>) {
|
|
15
16
|
// Simulate being an abstract class
|
|
16
|
-
if (new.target ===
|
|
17
|
+
if (new.target === RWPStorageProvider)
|
|
17
18
|
throw TypeError(`StorageProvider is abstract. Extend this class to implement it`)
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
this.namespace = namespace || null
|
|
20
21
|
this.registry = registry || /* istanbul ignore next */ {}
|
|
21
|
-
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
/**
|
|
25
25
|
* Sets the namespace for this storage provider, and migrates
|
|
26
26
|
* all stored values to the new namespace
|
|
27
27
|
* @param {String} namespace New namespace for this storage provider
|
|
28
28
|
*/
|
|
29
29
|
/* istanbul ignore next */
|
|
30
|
-
setNamespace(namespace)
|
|
31
|
-
|
|
30
|
+
abstract setNamespace(namespace: string | null): void
|
|
31
|
+
|
|
32
32
|
/**
|
|
33
|
-
* Registers an item with
|
|
34
|
-
* @param {String} key
|
|
35
|
-
* @param {*} initialValue
|
|
33
|
+
* Registers an item with its initial value. This is used for logging, resetting, etc.
|
|
34
|
+
* @param {String} key InternalStorage item's key
|
|
35
|
+
* @param {*} initialValue InternalStorage item's initial value
|
|
36
36
|
*/
|
|
37
|
-
register(key, initialValue) {
|
|
37
|
+
register<T>(key: string, initialValue: T | null) {
|
|
38
38
|
this.registry[key] = initialValue
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
/**
|
|
42
42
|
* Reads an item from storage
|
|
43
43
|
* @param {String} key Key for the item to retrieve
|
|
44
44
|
*/
|
|
45
45
|
/* istanbul ignore next */
|
|
46
|
-
getItem(key)
|
|
47
|
-
|
|
46
|
+
abstract getItem<T>(key: string): T | null
|
|
47
|
+
|
|
48
48
|
/**
|
|
49
49
|
* Stores a value
|
|
50
50
|
* @param {String} key Item's storage key
|
|
51
51
|
* @param {String} value Item's value to store
|
|
52
52
|
*/
|
|
53
53
|
/* istanbul ignore next */
|
|
54
|
-
setItem(key, value)
|
|
55
|
-
|
|
54
|
+
abstract setItem<T>(key: string, value: T | null): void
|
|
55
|
+
|
|
56
56
|
/**
|
|
57
57
|
* Removes an item from storage
|
|
58
58
|
* @param {String} key Item's storage key
|
|
59
59
|
* @param {Boolean} fromRegistry (Optional) If the item should also be removed from the registry
|
|
60
60
|
*/
|
|
61
61
|
/* istanbul ignore next */
|
|
62
|
-
removeItem(key, fromRegistry
|
|
63
|
-
|
|
62
|
+
abstract removeItem(key: string, fromRegistry: boolean): void
|
|
63
|
+
|
|
64
64
|
/**
|
|
65
|
-
* Gets all stored keys
|
|
65
|
+
* Gets all stored keys and values
|
|
66
66
|
* If a `namespace` was set, only keys prefixed with the namespace will be returned
|
|
67
67
|
*/
|
|
68
68
|
/* istanbul ignore next */
|
|
69
|
-
getAll()
|
|
70
|
-
|
|
69
|
+
abstract getAll(): Record<string, unknown>
|
|
70
|
+
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
72
|
+
*
|
|
73
73
|
* @param {Boolean} useInitialValues If values should be replaced with their initial values. If false, keys are removed
|
|
74
74
|
* @param {String[]} excludedKeys (Optional) List of keys to exclude
|
|
75
75
|
* @param {Boolean} clearRegistry (Optional) If the registry should also be cleared
|
|
76
76
|
*/
|
|
77
77
|
/* istanbul ignore next */
|
|
78
|
-
_resetAll(
|
|
79
|
-
|
|
80
|
-
excludedKeys = [],
|
|
81
|
-
clearRegistry = false
|
|
82
|
-
) {}
|
|
83
|
-
|
|
78
|
+
abstract _resetAll(useInitialValues: boolean, excludedKeys: string[], clearRegistry: boolean): void
|
|
79
|
+
|
|
84
80
|
/**
|
|
85
81
|
* Resets all values to their initial values
|
|
86
82
|
* If a `namespace` is set, only keys prefixed with the namespace will be reset
|
|
87
83
|
* @param {String[]} excludedKeys (Optional) List of keys to exclude
|
|
88
84
|
*/
|
|
89
85
|
/* istanbul ignore next */
|
|
90
|
-
resetAll(excludedKeys
|
|
91
|
-
|
|
86
|
+
abstract resetAll(excludedKeys: string[]): void
|
|
87
|
+
|
|
92
88
|
/**
|
|
93
89
|
* Removes all items from local storage.
|
|
94
90
|
* If a `namespace` is set, only keys prefixed with the namespace will be removed
|
|
95
91
|
* @param {String[]} excludedKeys (Optional) List of keys to exclude
|
|
96
92
|
*/
|
|
97
93
|
/* istanbul ignore next */
|
|
98
|
-
removeAll(excludedKeys
|
|
99
|
-
|
|
94
|
+
abstract removeAll(excludedKeys: string[]): void
|
|
95
|
+
|
|
96
|
+
upgradeToRealStorage(): boolean {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
isUsingFakeStorage(): boolean {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
export default
|
|
105
|
+
export default RWPStorageProvider
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createWire, type Defined, type Wire } from '@forminator/react-wire'
|
|
2
|
+
import LocalStorageProvider from '@/providers/LocalStorageProvider'
|
|
3
|
+
import type RWPStorageProvider from '@/providers/RWPStorageProvider'
|
|
4
|
+
import type { PersistedWire, RWPOptions, WireLikeObject } from '@/types'
|
|
5
|
+
import { getHasHydratedStorage, getIsClient, markStorageAsHydrated } from '@/utils'
|
|
6
|
+
|
|
7
|
+
// Generate unique instance ID
|
|
8
|
+
const instanceId = Math.random().toString(36).substring(7)
|
|
9
|
+
const rwpLog = (...args: unknown[]) => {
|
|
10
|
+
if (typeof globalThis !== 'undefined' && globalThis.__RWP_LOGGING_ENABLED__ !== false) {
|
|
11
|
+
console.log(...args)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const defaultOptions: RWPOptions = {
|
|
16
|
+
logging: {
|
|
17
|
+
enabled: false,
|
|
18
|
+
},
|
|
19
|
+
storageProvider: LocalStorageProvider,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let options: RWPOptions = { ...defaultOptions }
|
|
23
|
+
let storage: RWPStorageProvider
|
|
24
|
+
const pendingLogs: unknown[][] = []
|
|
25
|
+
|
|
26
|
+
// Set global logging flag on startup
|
|
27
|
+
if (typeof globalThis !== 'undefined' && globalThis.__RWP_LOGGING_ENABLED__ === undefined) {
|
|
28
|
+
globalThis.__RWP_LOGGING_ENABLED__ = defaultOptions.logging.enabled
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
rwpLog('[RWP] Module initialized, instance ID:', instanceId)
|
|
32
|
+
|
|
33
|
+
// Make storage global so all instances share the same storage after upgrade
|
|
34
|
+
rwpLog('[RWP] About to check global storage, instanceId:', instanceId)
|
|
35
|
+
try {
|
|
36
|
+
if (!globalThis.__RWP_STORAGE__) {
|
|
37
|
+
rwpLog('[RWP] Creating global storage in instance:', instanceId)
|
|
38
|
+
globalThis.__RWP_STORAGE__ = new LocalStorageProvider('__internal_rwp_storage__')
|
|
39
|
+
} else {
|
|
40
|
+
rwpLog('[RWP] Using existing global storage in instance:', instanceId)
|
|
41
|
+
}
|
|
42
|
+
storage = globalThis.__RWP_STORAGE__
|
|
43
|
+
rwpLog('[RWP] InternalStorage assigned successfully')
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (globalThis.__RWP_LOGGING_ENABLED__) console.error('[RWP] Error setting up global storage:', error)
|
|
46
|
+
storage = new LocalStorageProvider('__internal_rwp_storage__')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Use a global registry to handle multiple module instances
|
|
50
|
+
// This ensures all instances share the same wire registry
|
|
51
|
+
if (typeof globalThis !== 'undefined') {
|
|
52
|
+
if (!globalThis.__RWP_REGISTERED_WIRES__) {
|
|
53
|
+
rwpLog('[RWP] Creating global registeredWires in instance:', instanceId)
|
|
54
|
+
globalThis.__RWP_REGISTERED_WIRES__ = new Map()
|
|
55
|
+
} else {
|
|
56
|
+
rwpLog('[RWP] Using existing global registeredWires in instance:', instanceId)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Registry to track wire instances for hydration refresh
|
|
61
|
+
const registeredWires = globalThis.__RWP_REGISTERED_WIRES__ || new Map<string, WireLikeObject>()
|
|
62
|
+
rwpLog('[RWP] registeredWires Map reference in instance:', instanceId, 'size:', registeredWires.size)
|
|
63
|
+
|
|
64
|
+
export const getNamespace = (): string | null => storage.namespace
|
|
65
|
+
|
|
66
|
+
export const getStorage = (): RWPStorageProvider => storage
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sets the namespace for the storage provider
|
|
70
|
+
*
|
|
71
|
+
* @param {String} namespace The namespace for the storage provider
|
|
72
|
+
*/
|
|
73
|
+
export const setNamespace = (namespace: string) => {
|
|
74
|
+
rwpLog('[RWP] setNamespace() called with:', namespace, 'registered wires before:', registeredWires.size)
|
|
75
|
+
const currentNamespace = namespace || getNamespace()
|
|
76
|
+
|
|
77
|
+
if (!currentNamespace) throw new Error('react-wire-persisted: Cannot set namespace to null or undefined')
|
|
78
|
+
|
|
79
|
+
storage.setNamespace(namespace)
|
|
80
|
+
storage = new LocalStorageProvider(currentNamespace)
|
|
81
|
+
rwpLog(`[RWP] setNamespace() done, registered wires after:`, registeredWires.size)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const getOptions = (): RWPOptions => options
|
|
85
|
+
|
|
86
|
+
export const setOptions = (value: Partial<RWPOptions>) => {
|
|
87
|
+
options = {
|
|
88
|
+
...options,
|
|
89
|
+
...value,
|
|
90
|
+
}
|
|
91
|
+
// Update global logging flag
|
|
92
|
+
if (typeof globalThis !== 'undefined') {
|
|
93
|
+
globalThis.__RWP_LOGGING_ENABLED__ = options.logging.enabled
|
|
94
|
+
}
|
|
95
|
+
/* istanbul ignore next */
|
|
96
|
+
if (options.logging.enabled) {
|
|
97
|
+
console.info('Flushing', pendingLogs.length, 'pending logs')
|
|
98
|
+
while (pendingLogs.length)
|
|
99
|
+
/* istanbul ignore next */
|
|
100
|
+
console.log(...(pendingLogs.shift() || []))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Refresh all registered wires by reading from storage
|
|
106
|
+
* Called after storage upgrade to sync wires with persisted values
|
|
107
|
+
*/
|
|
108
|
+
const refreshAllWires = () => {
|
|
109
|
+
rwpLog('[RWP] refreshAllWires() called in instance:', instanceId, 'registered wires:', registeredWires.size)
|
|
110
|
+
log('react-wire-persisted: refreshAllWires() called, registered wires:', registeredWires.size)
|
|
111
|
+
|
|
112
|
+
registeredWires.forEach((wire: Wire<unknown> | WireLikeObject, key: string) => {
|
|
113
|
+
const storedValue = storage.getItem(key)
|
|
114
|
+
const currentValue = wire.getValue()
|
|
115
|
+
|
|
116
|
+
rwpLog('[RWP] Checking wire', key, {
|
|
117
|
+
storedValue,
|
|
118
|
+
currentValue,
|
|
119
|
+
willUpdate: storedValue !== null && storedValue !== currentValue,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
log('react-wire-persisted: Checking wire', key, {
|
|
123
|
+
storedValue,
|
|
124
|
+
currentValue,
|
|
125
|
+
willUpdate: storedValue !== null && storedValue !== currentValue,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (storedValue !== null && storedValue !== currentValue) {
|
|
129
|
+
rwpLog('[RWP] Refreshing wire', key, 'with stored value', storedValue)
|
|
130
|
+
log('react-wire-persisted: Refreshing wire', key, 'with stored value', storedValue)
|
|
131
|
+
wire.setValue(storedValue)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Attempts to upgrade the storage provider from fake storage to real localStorage
|
|
138
|
+
* This should be called on the client side after hydration
|
|
139
|
+
*
|
|
140
|
+
* @returns true if upgrade was successful
|
|
141
|
+
*/
|
|
142
|
+
export const upgradeStorage = (): boolean => {
|
|
143
|
+
rwpLog('[RWP] upgradeStorage() called in instance:', instanceId, {
|
|
144
|
+
isClient: getIsClient(),
|
|
145
|
+
isUsingFakeStorage: storage.isUsingFakeStorage(),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
log('react-wire-persisted: upgradeStorage() called', {
|
|
149
|
+
isClient: getIsClient(),
|
|
150
|
+
isUsingFakeStorage: storage.isUsingFakeStorage(),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (!getIsClient()) return false
|
|
154
|
+
|
|
155
|
+
const upgraded = storage.upgradeToRealStorage()
|
|
156
|
+
|
|
157
|
+
rwpLog('[RWP] upgradeToRealStorage() returned', upgraded)
|
|
158
|
+
log('react-wire-persisted: upgradeToRealStorage() returned', upgraded)
|
|
159
|
+
|
|
160
|
+
if (upgraded) {
|
|
161
|
+
markStorageAsHydrated()
|
|
162
|
+
rwpLog('[RWP] Upgraded to real localStorage, calling refreshAllWires()')
|
|
163
|
+
log('react-wire-persisted: Upgraded to real localStorage after hydration')
|
|
164
|
+
|
|
165
|
+
// Refresh all wires with stored values
|
|
166
|
+
refreshAllWires()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return upgraded
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const log = (...args: unknown[]) => {
|
|
173
|
+
/* istanbul ignore next */
|
|
174
|
+
if (options.logging.enabled)
|
|
175
|
+
/* istanbul ignore next */
|
|
176
|
+
console.log(...args)
|
|
177
|
+
else pendingLogs.push(args)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates a persisted Wire using the `RWPStorageProvider` that is currently set
|
|
182
|
+
* Defaults to `localStorage` via `LocalStorageProvider`
|
|
183
|
+
*
|
|
184
|
+
* @param {String} key Unique key for storing this value
|
|
185
|
+
* @param {*} value Initial value of this Wire
|
|
186
|
+
* @returns A new Wire decorated with localStorage functionality
|
|
187
|
+
*/
|
|
188
|
+
export const createPersistedWire = <T = null>(key: string, value: T = null as T): PersistedWire<T> => {
|
|
189
|
+
rwpLog('[RWP] createPersistedWire() called in instance:', instanceId, 'key:', key, 'value:', value)
|
|
190
|
+
|
|
191
|
+
// This check helps ensure no accidental key typos occur
|
|
192
|
+
if (!key) throw new Error(`createPersistedWire: Key cannot be a falsey value (${key}}`)
|
|
193
|
+
|
|
194
|
+
// Track this writable entry so we can easily clear all
|
|
195
|
+
storage.register(key, value)
|
|
196
|
+
|
|
197
|
+
// The actual Wire backing object
|
|
198
|
+
const wire = createWire<T>(value)
|
|
199
|
+
|
|
200
|
+
const getValue = () => wire.getValue()
|
|
201
|
+
|
|
202
|
+
const setValue = (newValue: Defined<T>) => {
|
|
203
|
+
rwpLog(
|
|
204
|
+
'[RWP] setValue called in instance:',
|
|
205
|
+
instanceId,
|
|
206
|
+
'key:',
|
|
207
|
+
key,
|
|
208
|
+
'isUsingFakeStorage:',
|
|
209
|
+
storage.isUsingFakeStorage(),
|
|
210
|
+
)
|
|
211
|
+
storage.setItem(key, newValue)
|
|
212
|
+
return wire.setValue(newValue)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const subscribe = (callback: (value: Defined<T>) => void) => {
|
|
216
|
+
return wire.subscribe(callback)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Always start with default value to ensure SSR consistency
|
|
220
|
+
let initialValue: T = value
|
|
221
|
+
|
|
222
|
+
// Only read from storage if we've hydrated OR if storage is already using real localStorage
|
|
223
|
+
// (prevents hydration mismatch in SSR, but allows normal behavior in client-only apps)
|
|
224
|
+
const canReadStorage = getHasHydratedStorage() || !storage.isUsingFakeStorage()
|
|
225
|
+
|
|
226
|
+
if (canReadStorage && getIsClient()) {
|
|
227
|
+
const storedValue = storage.getItem<T>(key)
|
|
228
|
+
|
|
229
|
+
if (storedValue !== null) initialValue = storedValue
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
log('react-wire-persisted: create', key, {
|
|
233
|
+
value,
|
|
234
|
+
initialValue,
|
|
235
|
+
hasHydratedStorage: getHasHydratedStorage(),
|
|
236
|
+
isUsingFakeStorage: storage.isUsingFakeStorage(),
|
|
237
|
+
canReadStorage,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
if (initialValue !== value && initialValue !== undefined) setValue(initialValue as Defined<T>)
|
|
241
|
+
|
|
242
|
+
// Register wire for post-hydration refresh
|
|
243
|
+
registeredWires.set(key, {
|
|
244
|
+
getValue,
|
|
245
|
+
setValue,
|
|
246
|
+
subscribe,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
rwpLog('[RWP] Wire registered, total wires:', registeredWires.size, 'keys:', Array.from(registeredWires.keys()))
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...wire,
|
|
253
|
+
getValue,
|
|
254
|
+
setValue,
|
|
255
|
+
subscribe,
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Wire } from '@forminator/react-wire'
|
|
2
|
+
import type RWPStorageProvider from '@/providers/RWPStorageProvider'
|
|
3
|
+
|
|
4
|
+
export type RWPOptions = {
|
|
5
|
+
logging: { enabled: boolean }
|
|
6
|
+
storageProvider?: typeof RWPStorageProvider
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type PersistedWire<T> = Wire<T>
|
|
10
|
+
|
|
11
|
+
export interface InternalStorage {
|
|
12
|
+
getItem: (key: string) => string | null
|
|
13
|
+
setItem: (key: string, value: string) => void
|
|
14
|
+
removeItem: (key: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type AnyStorage = InternalStorage | Storage
|
|
18
|
+
|
|
19
|
+
export type WireLikeObject = Pick<Wire<unknown>, 'getValue' | 'setValue' | 'subscribe'>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InternalStorage } from '@/types'
|
|
2
|
+
|
|
3
|
+
const storage: Record<string, string> = {
|
|
4
|
+
__IS_FAKE_LOCAL_STORAGE__: 'true',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const fakeLocalStorage: InternalStorage = {
|
|
8
|
+
getItem: (key: string): string | null => storage[key],
|
|
9
|
+
setItem: (key: string, value: string): void => {
|
|
10
|
+
storage[key] = value
|
|
11
|
+
},
|
|
12
|
+
removeItem: (key: string): void => {
|
|
13
|
+
delete storage[key]
|
|
14
|
+
},
|
|
15
|
+
// Make Object.keys() work properly for _resetAll method
|
|
16
|
+
...storage,
|
|
17
|
+
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
export * from './keys'
|
|
2
1
|
export * from './fakeLocalStorage'
|
|
3
2
|
export * from './isomorphic'
|
|
3
|
+
export * from './keys'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Checks if a value is a primitive type
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @param {*} val Value to check
|
|
9
9
|
* @returns {Boolean} True if value is a primitive type
|
|
10
10
|
*/
|
|
11
|
-
export const isPrimitive = val => {
|
|
12
|
-
|
|
11
|
+
export const isPrimitive = (val: unknown): boolean => {
|
|
13
12
|
const type = typeof val
|
|
14
|
-
|
|
13
|
+
|
|
15
14
|
if (val === null) return true
|
|
16
15
|
if (Array.isArray(val)) return false
|
|
17
16
|
if (type === 'object') return false
|
|
18
|
-
|
|
17
|
+
|
|
19
18
|
return type !== 'function'
|
|
20
|
-
|
|
21
19
|
}
|
|
@@ -2,45 +2,58 @@
|
|
|
2
2
|
* Utilities for handling server-side rendering and client-side hydration
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
let isClient = false
|
|
6
|
-
let hasHydrated = false
|
|
5
|
+
let isClient: boolean = false
|
|
6
|
+
let hasHydrated: boolean = false
|
|
7
|
+
let hasHydratedStorage: boolean = false
|
|
7
8
|
|
|
8
9
|
// Detect if we're running in a browser environment
|
|
9
10
|
if (typeof window !== 'undefined') {
|
|
10
11
|
isClient = true
|
|
11
12
|
|
|
12
13
|
// Mark as hydrated when the DOM is ready
|
|
13
|
-
if (document.readyState === 'loading')
|
|
14
|
+
if (document.readyState === 'loading')
|
|
14
15
|
document.addEventListener('DOMContentLoaded', () => {
|
|
15
16
|
hasHydrated = true
|
|
16
17
|
})
|
|
17
|
-
|
|
18
|
-
hasHydrated = true
|
|
19
|
-
}
|
|
18
|
+
else hasHydrated = true
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* Check if we're running in a browser environment
|
|
24
23
|
*/
|
|
25
|
-
export const getIsClient = () => isClient
|
|
24
|
+
export const getIsClient = (): boolean => isClient
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* Check if the client has finished hydrating
|
|
29
28
|
*/
|
|
30
|
-
export const getHasHydrated = () => hasHydrated
|
|
29
|
+
export const getHasHydrated = (): boolean => hasHydrated
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if storage has been hydrated (safe to read from real localStorage)
|
|
33
|
+
*/
|
|
34
|
+
export const getHasHydratedStorage = (): boolean => hasHydratedStorage
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mark storage as hydrated (called after upgradeStorage)
|
|
38
|
+
*/
|
|
39
|
+
export const markStorageAsHydrated = (): void => {
|
|
40
|
+
hasHydratedStorage = true
|
|
41
|
+
}
|
|
31
42
|
|
|
32
43
|
/**
|
|
33
44
|
* Check if localStorage is available and safe to use
|
|
34
45
|
*/
|
|
35
|
-
export const isLocalStorageAvailable = () => {
|
|
46
|
+
export const isLocalStorageAvailable = (): boolean => {
|
|
36
47
|
if (!isClient) return false
|
|
37
48
|
|
|
38
49
|
try {
|
|
39
50
|
const testKey = '__rwp_test__'
|
|
51
|
+
|
|
40
52
|
window.localStorage.setItem(testKey, 'test')
|
|
41
53
|
window.localStorage.removeItem(testKey)
|
|
54
|
+
|
|
42
55
|
return true
|
|
43
|
-
} catch (
|
|
56
|
+
} catch (_) {
|
|
44
57
|
return false
|
|
45
58
|
}
|
|
46
|
-
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convenience map of keys
|
|
3
|
+
*/
|
|
4
|
+
const storageKeys: Record<string, string> = {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds a key to the keys map
|
|
8
|
+
*
|
|
9
|
+
* @param {String} value Key name
|
|
10
|
+
*/
|
|
11
|
+
export const addKey = (value: string): void => {
|
|
12
|
+
storageKeys[value] = value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Adds a key to the keys map
|
|
17
|
+
* (Alias for `addKey`)
|
|
18
|
+
*
|
|
19
|
+
* @param {String} value Key name
|
|
20
|
+
*/
|
|
21
|
+
export const key = (value: string) => addKey(value)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convenience method to get internally managed storage keys
|
|
25
|
+
*
|
|
26
|
+
* @returns {Object} InternalStorage keys map
|
|
27
|
+
*/
|
|
28
|
+
export const getKeys = (): Record<string, string> => storageKeys
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Helper utility to prefix all keys in a map to use a namespace
|
|
32
|
+
*
|
|
33
|
+
* @param {String} namespace InternalStorage namespace prefix
|
|
34
|
+
* @param {Object} keys (Optional) InternalStorage key/values. Defaults to the internally managed keys map
|
|
35
|
+
*/
|
|
36
|
+
export const getPrefixedKeys = (namespace: string, keys: Record<string, string> | null = null) => {
|
|
37
|
+
const items = keys || storageKeys
|
|
38
|
+
|
|
39
|
+
if (!namespace) return items
|
|
40
|
+
|
|
41
|
+
return Object.keys(items).reduce(
|
|
42
|
+
(acc, it) => {
|
|
43
|
+
acc[it] = `${namespace}.${items[it]}`
|
|
44
|
+
|
|
45
|
+
return acc
|
|
46
|
+
},
|
|
47
|
+
{} as Record<string, string>,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
4
|
-
import { upgradeStorage } from '../react-wire-persisted'
|
|
5
|
-
import { getIsClient, getHasHydrated } from '../utils'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* A Next.js App Router compatible component that handles automatic storage upgrade
|
|
9
|
-
* after hydration. Use this in your root layout.
|
|
10
|
-
*
|
|
11
|
-
* @param {Object} props Component props
|
|
12
|
-
* @param {React.ReactNode} props.children Child components to render
|
|
13
|
-
* @param {Function} props.onUpgrade Callback called when storage is upgraded
|
|
14
|
-
* @param {Boolean} props.autoUpgrade Whether to automatically upgrade storage (default: true)
|
|
15
|
-
*/
|
|
16
|
-
export function HydrationProvider({ children, onUpgrade, autoUpgrade = true }) {
|
|
17
|
-
const hasUpgraded = useRef(false)
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!autoUpgrade || hasUpgraded.current || !getIsClient()) {
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const attemptUpgrade = () => {
|
|
25
|
-
if (getHasHydrated() && !hasUpgraded.current) {
|
|
26
|
-
const upgraded = upgradeStorage()
|
|
27
|
-
|
|
28
|
-
if (upgraded) {
|
|
29
|
-
hasUpgraded.current = true
|
|
30
|
-
onUpgrade?.()
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Try to upgrade immediately if already hydrated
|
|
36
|
-
attemptUpgrade()
|
|
37
|
-
|
|
38
|
-
// Also try after a short delay to ensure DOM is ready
|
|
39
|
-
const timeoutId = setTimeout(attemptUpgrade, 0)
|
|
40
|
-
|
|
41
|
-
return () => clearTimeout(timeoutId)
|
|
42
|
-
}, [autoUpgrade, onUpgrade])
|
|
43
|
-
|
|
44
|
-
return children
|
|
45
|
-
}
|
package/src/components/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { HydrationProvider } from './HydrationProvider.js'
|