hanzio 1.2.0 → 2.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 +786 -239
- package/dist/api-wrapper/builders.d.ts +4 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +39 -2
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +4 -5
- package/dist/api-wrapper/types.d.ts +103 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +19 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-181e6vfw.js +29 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-srt06xd2.js +15 -0
- package/dist/index-wjyb9bsd.js +263 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1130
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +739 -3145
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +44 -1428
- package/dist/src/secrets/vite.js +6 -39
- package/dist/src/secrets/worker.js +10 -273
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +340 -167
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +31 -24
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +78 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +32 -25
- package/src/api-wrapper/create-client.ts +171 -41
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +107 -2
- package/src/api-wrapper/headers.ts +11 -6
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +42 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +41 -43
- package/src/api-wrapper/types.ts +185 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +132 -147
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +58 -38
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/loaders.js +0 -1529
- package/dist/zod/index.js +0 -210
package/src/state/index.ts
CHANGED
|
@@ -80,6 +80,25 @@ export interface StateMachineConfig<
|
|
|
80
80
|
// Global validator that runs for all transitions
|
|
81
81
|
globalValidator?: ValidatorFn<TState, TContext, TAction>
|
|
82
82
|
|
|
83
|
+
// Validators for specific actions, run after the global and state validators
|
|
84
|
+
actionValidators?: Partial<
|
|
85
|
+
Record<TAction, ValidatorFn<TState, TContext, TAction>>
|
|
86
|
+
>
|
|
87
|
+
|
|
88
|
+
// State returned by getInitialState(). Defaults to the first key of stateTransitions
|
|
89
|
+
initialState?: TState
|
|
90
|
+
|
|
91
|
+
// Called after a successful action (after its handler). Throwing fails the action
|
|
92
|
+
onTransition?: (
|
|
93
|
+
context: TContext,
|
|
94
|
+
from: TState,
|
|
95
|
+
to: TState,
|
|
96
|
+
action: TAction
|
|
97
|
+
) => void | Promise<void>
|
|
98
|
+
|
|
99
|
+
// Write the new state into item[stateKey] after a successful action
|
|
100
|
+
applyTransition?: boolean
|
|
101
|
+
|
|
83
102
|
// Action handlers for each action
|
|
84
103
|
actions?: {
|
|
85
104
|
[K in TAction]?: ActionHandlerFn<
|
|
@@ -143,20 +162,18 @@ export class SimpleStateMachine<
|
|
|
143
162
|
Partial<Record<TAction, ValidationResult>>
|
|
144
163
|
> {
|
|
145
164
|
const actionsMap = this.config.stateActionMap[this.getCurrentStatus()] || {}
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
targetState as TState,
|
|
153
|
-
{ action: action as TAction }
|
|
165
|
+
const entries = (
|
|
166
|
+
Object.entries(actionsMap) as [TAction, TState | undefined][]
|
|
167
|
+
).filter((entry): entry is [TAction, TState] => !!entry[1])
|
|
168
|
+
const results = await Promise.all(
|
|
169
|
+
entries.map(([action, targetState]) =>
|
|
170
|
+
this.validateTransition(targetState, { action })
|
|
154
171
|
)
|
|
172
|
+
)
|
|
155
173
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return result
|
|
174
|
+
return Object.fromEntries(
|
|
175
|
+
entries.map(([action], i) => [action, results[i]])
|
|
176
|
+
) as Partial<Record<TAction, ValidationResult>>
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
/**
|
|
@@ -170,34 +187,48 @@ export class SimpleStateMachine<
|
|
|
170
187
|
const possibleTransitions =
|
|
171
188
|
this.config.stateTransitions[this.getCurrentStatus()] || []
|
|
172
189
|
const actionsMap = this.config.stateActionMap[this.getCurrentStatus()] || {}
|
|
173
|
-
const
|
|
190
|
+
const results = await Promise.all(
|
|
191
|
+
possibleTransitions.map(async (targetState) => {
|
|
192
|
+
const actionsToTarget = (
|
|
193
|
+
Object.entries(actionsMap) as [TAction, TState | undefined][]
|
|
194
|
+
).filter(([, t]) => t === targetState)
|
|
195
|
+
|
|
196
|
+
if (actionsToTarget.length === 0) {
|
|
197
|
+
return this.validateTransition(targetState)
|
|
198
|
+
}
|
|
174
199
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
200
|
+
const rs = await Promise.all(
|
|
201
|
+
actionsToTarget.map(([action]) =>
|
|
202
|
+
this.validateTransition(targetState, { action })
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
// true if any action validates, otherwise the last action's error
|
|
206
|
+
return rs.includes(true) ? true : (rs[rs.length - 1] as string)
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return Object.fromEntries(
|
|
211
|
+
possibleTransitions.map((targetState, i) => [targetState, results[i]])
|
|
212
|
+
) as Partial<Record<TState, ValidationResult>>
|
|
213
|
+
}
|
|
179
214
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Checks whether an action can be executed from the current state
|
|
217
|
+
*
|
|
218
|
+
* @returns true, or the reason the action is not allowed
|
|
219
|
+
*/
|
|
220
|
+
async canExecute(action: TAction): Promise<ValidationResult> {
|
|
221
|
+
const actionsMap = this.config.stateActionMap[this.getCurrentStatus()]
|
|
222
|
+
if (!actionsMap) {
|
|
223
|
+
return `No actions defined for state '${this.getCurrentStatus()}'`
|
|
224
|
+
}
|
|
184
225
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const r = await this.validateTransition(targetState, {
|
|
189
|
-
action: actionKey
|
|
190
|
-
})
|
|
191
|
-
if (r === true) {
|
|
192
|
-
anyOk = true
|
|
193
|
-
break
|
|
194
|
-
}
|
|
195
|
-
lastError = r
|
|
196
|
-
}
|
|
197
|
-
result[targetState] = anyOk ? true : lastError
|
|
226
|
+
const targetState = actionsMap[action]
|
|
227
|
+
if (!targetState) {
|
|
228
|
+
return `Action '${action}' not allowed in state '${this.getCurrentStatus()}'`
|
|
198
229
|
}
|
|
199
230
|
|
|
200
|
-
return
|
|
231
|
+
return this.validateTransition(targetState, { action })
|
|
201
232
|
}
|
|
202
233
|
|
|
203
234
|
/**
|
|
@@ -211,55 +242,35 @@ export class SimpleStateMachine<
|
|
|
211
242
|
action: A,
|
|
212
243
|
data: TActionDataMap[A]
|
|
213
244
|
): Promise<ActionExecutionResult<TState, TActionResultMap[A]>> {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
error: `No actions defined for state '${this.getCurrentStatus()}'`
|
|
219
|
-
}
|
|
245
|
+
const fromState = this.getCurrentStatus()
|
|
246
|
+
const validationResult = await this.canExecute(action)
|
|
247
|
+
if (validationResult !== true) {
|
|
248
|
+
return { success: false, error: validationResult }
|
|
220
249
|
}
|
|
221
250
|
|
|
222
|
-
const targetState =
|
|
223
|
-
if (!targetState) {
|
|
224
|
-
return {
|
|
225
|
-
success: false,
|
|
226
|
-
error: `Action '${action}' not allowed in state '${this.getCurrentStatus()}'`
|
|
227
|
-
}
|
|
228
|
-
}
|
|
251
|
+
const targetState = this.config.stateActionMap[fromState][action] as TState
|
|
229
252
|
|
|
230
|
-
|
|
231
|
-
action
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
} catch (error) {
|
|
245
|
-
return {
|
|
246
|
-
success: false,
|
|
247
|
-
error: error instanceof Error ? error.message : String(error)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
253
|
+
try {
|
|
254
|
+
const result = await this.config.actions?.[action]?.(this.item, data)
|
|
255
|
+
await this.config.onTransition?.(
|
|
256
|
+
this.item,
|
|
257
|
+
fromState,
|
|
258
|
+
targetState,
|
|
259
|
+
action
|
|
260
|
+
)
|
|
261
|
+
if (this.config.applyTransition) {
|
|
262
|
+
this.item[this.stateKey] = targetState as TContext[StateKey<
|
|
263
|
+
TContext,
|
|
264
|
+
TState
|
|
265
|
+
>]
|
|
250
266
|
}
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
return { success: true, newState: targetState, result }
|
|
268
|
+
} catch (error) {
|
|
253
269
|
return {
|
|
254
|
-
success:
|
|
255
|
-
|
|
270
|
+
success: false,
|
|
271
|
+
error: error instanceof Error ? error.message : String(error)
|
|
256
272
|
}
|
|
257
273
|
}
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
success: false,
|
|
261
|
-
error: validationResult
|
|
262
|
-
}
|
|
263
274
|
}
|
|
264
275
|
|
|
265
276
|
/**
|
|
@@ -301,7 +312,21 @@ export class SimpleStateMachine<
|
|
|
301
312
|
// Run state-specific validator if provided
|
|
302
313
|
const stateValidator = this.config.validators?.[toState]
|
|
303
314
|
if (stateValidator) {
|
|
304
|
-
|
|
315
|
+
const stateResult = await stateValidator(
|
|
316
|
+
this.item,
|
|
317
|
+
this.getCurrentStatus(),
|
|
318
|
+
toState,
|
|
319
|
+
action
|
|
320
|
+
)
|
|
321
|
+
if (stateResult !== true) {
|
|
322
|
+
return stateResult
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Run action-specific validator if validating a specific action
|
|
327
|
+
const actionValidator = action && this.config.actionValidators?.[action]
|
|
328
|
+
if (actionValidator) {
|
|
329
|
+
return await actionValidator(
|
|
305
330
|
this.item,
|
|
306
331
|
this.getCurrentStatus(),
|
|
307
332
|
toState,
|
|
@@ -324,8 +349,35 @@ export class SimpleStateMachine<
|
|
|
324
349
|
* @returns The initial state
|
|
325
350
|
*/
|
|
326
351
|
getInitialState(): TState | undefined {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
return this.config.initialState ?? this.getStates()[0]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Defines a state machine config once and binds it to items on demand
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* const orderMachine = createStateMachine<Status, Action, Order>(config)
|
|
361
|
+
* const machine = orderMachine.for(order, 'status')
|
|
362
|
+
*/
|
|
363
|
+
export function createStateMachine<
|
|
364
|
+
TState extends string,
|
|
365
|
+
TAction extends string,
|
|
366
|
+
TContext,
|
|
367
|
+
TActionDataMap extends ActionDataMap<TAction> = ActionDataMap<TAction>,
|
|
368
|
+
TActionResultMap extends ActionResultMap<TAction> = ActionResultMap<TAction>
|
|
369
|
+
>(
|
|
370
|
+
config: StateMachineConfig<
|
|
371
|
+
TState,
|
|
372
|
+
TAction,
|
|
373
|
+
TContext,
|
|
374
|
+
TActionDataMap,
|
|
375
|
+
TActionResultMap
|
|
376
|
+
>
|
|
377
|
+
) {
|
|
378
|
+
return {
|
|
379
|
+
config,
|
|
380
|
+
for: (item: TContext, stateKey: StateKey<TContext, TState>) =>
|
|
381
|
+
new SimpleStateMachine(config, item, stateKey)
|
|
330
382
|
}
|
|
331
383
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** The part of the Web `Storage` API (`localStorage`, `sessionStorage`) used here. */
|
|
2
|
+
export interface SyncStorageLike {
|
|
3
|
+
getItem(key: string): string | null
|
|
4
|
+
setItem(key: string, value: string): void
|
|
5
|
+
removeItem(key: string): void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** An async key-value store such as React Native's `AsyncStorage`. */
|
|
9
|
+
export interface AsyncStorageLike {
|
|
10
|
+
getItem(key: string): Promise<string | null>
|
|
11
|
+
setItem(key: string, value: string): Promise<void>
|
|
12
|
+
removeItem(key: string): Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StorageItemOptions<T> {
|
|
16
|
+
key: string
|
|
17
|
+
/** Returned when nothing is stored or the stored value can't be read. */
|
|
18
|
+
fallback: T
|
|
19
|
+
/** Validates the deserialized value, e.g. `schema.parse`. Throwing means `fallback`. */
|
|
20
|
+
parse?: (raw: unknown) => T
|
|
21
|
+
/** Defaults to `JSON.stringify`. */
|
|
22
|
+
serialize?: (value: T) => string
|
|
23
|
+
/** Defaults to `JSON.parse`. */
|
|
24
|
+
deserialize?: (raw: string) => unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface StorageItem<T> {
|
|
28
|
+
readonly key: string
|
|
29
|
+
get(): T
|
|
30
|
+
set(value: T): void
|
|
31
|
+
remove(): void
|
|
32
|
+
/**
|
|
33
|
+
* Calls `listener` when another tab changes this key (the `storage` event).
|
|
34
|
+
* A no-op outside browsers. Returns an unsubscribe function.
|
|
35
|
+
*/
|
|
36
|
+
subscribe(listener: (value: T) => void): () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AsyncStorageItem<T> {
|
|
40
|
+
readonly key: string
|
|
41
|
+
get(): Promise<T>
|
|
42
|
+
set(value: T): Promise<void>
|
|
43
|
+
remove(): Promise<void>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A `Map`-backed `SyncStorageLike`, for tests, SSR, or as a stand-in. */
|
|
47
|
+
export function createMemoryStorage(): SyncStorageLike {
|
|
48
|
+
const map = new Map<string, string>()
|
|
49
|
+
return {
|
|
50
|
+
getItem: (key) => map.get(key) ?? null,
|
|
51
|
+
setItem: (key, value) => {
|
|
52
|
+
map.set(key, String(value))
|
|
53
|
+
},
|
|
54
|
+
removeItem: (key) => {
|
|
55
|
+
map.delete(key)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type StorageEventLike = { key: string | null; storageArea: unknown }
|
|
61
|
+
type EventTargetLike = Record<
|
|
62
|
+
'addEventListener' | 'removeEventListener',
|
|
63
|
+
(type: 'storage', listener: (event: StorageEventLike) => void) => void
|
|
64
|
+
>
|
|
65
|
+
|
|
66
|
+
let sharedMemoryStorage: SyncStorageLike | undefined
|
|
67
|
+
|
|
68
|
+
// Reading `localStorage` itself throws in sandboxed iframes and some private modes.
|
|
69
|
+
function defaultStorage(): SyncStorageLike {
|
|
70
|
+
try {
|
|
71
|
+
const storage = (globalThis as { localStorage?: SyncStorageLike })
|
|
72
|
+
.localStorage
|
|
73
|
+
if (storage) return storage
|
|
74
|
+
} catch {}
|
|
75
|
+
sharedMemoryStorage ??= createMemoryStorage()
|
|
76
|
+
return sharedMemoryStorage
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createCodec<T>(options: StorageItemOptions<T>) {
|
|
80
|
+
const {
|
|
81
|
+
fallback,
|
|
82
|
+
parse = (raw) => raw as T,
|
|
83
|
+
serialize = JSON.stringify,
|
|
84
|
+
deserialize = JSON.parse
|
|
85
|
+
} = options
|
|
86
|
+
return {
|
|
87
|
+
serialize,
|
|
88
|
+
decode(raw: string | null): T {
|
|
89
|
+
if (raw === null) return fallback
|
|
90
|
+
try {
|
|
91
|
+
return parse(deserialize(raw))
|
|
92
|
+
} catch {
|
|
93
|
+
return fallback
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A typed value persisted in `localStorage` (or any `SyncStorageLike`). Never
|
|
101
|
+
* throws: missing, corrupt or invalid data reads as `fallback`, and when the
|
|
102
|
+
* storage throws (quota, Safari private mode, sandboxed iframes) the latest
|
|
103
|
+
* value is kept in memory for this item, so reads stay consistent this session.
|
|
104
|
+
*/
|
|
105
|
+
export function createStorageItem<T>(
|
|
106
|
+
options: StorageItemOptions<T> & { storage?: SyncStorageLike }
|
|
107
|
+
): StorageItem<T> {
|
|
108
|
+
const { key, fallback, storage = defaultStorage() } = options
|
|
109
|
+
const { serialize, decode } = createCodec(options)
|
|
110
|
+
// Set when a write failed; wins over whatever the storage still holds.
|
|
111
|
+
let mirror: { value: T } | undefined
|
|
112
|
+
|
|
113
|
+
const write = (value: T, action: () => void) => {
|
|
114
|
+
try {
|
|
115
|
+
action()
|
|
116
|
+
mirror = undefined
|
|
117
|
+
} catch {
|
|
118
|
+
mirror = { value }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const get = () => {
|
|
123
|
+
if (mirror) return mirror.value
|
|
124
|
+
try {
|
|
125
|
+
return decode(storage.getItem(key))
|
|
126
|
+
} catch {
|
|
127
|
+
return fallback
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
key,
|
|
133
|
+
get,
|
|
134
|
+
set: (value) => write(value, () => storage.setItem(key, serialize(value))),
|
|
135
|
+
remove: () => write(fallback, () => storage.removeItem(key)),
|
|
136
|
+
subscribe(listener) {
|
|
137
|
+
const target = globalThis as unknown as Partial<EventTargetLike>
|
|
138
|
+
if (!target.addEventListener || !target.removeEventListener) {
|
|
139
|
+
return () => {}
|
|
140
|
+
}
|
|
141
|
+
const onStorage = (event: StorageEventLike) => {
|
|
142
|
+
// `key` is null when the other tab called `storage.clear()`.
|
|
143
|
+
if (event.storageArea !== storage) return
|
|
144
|
+
if (event.key !== key && event.key !== null) return
|
|
145
|
+
mirror = undefined
|
|
146
|
+
listener(get())
|
|
147
|
+
}
|
|
148
|
+
target.addEventListener('storage', onStorage)
|
|
149
|
+
return () => target.removeEventListener?.('storage', onStorage)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `createStorageItem` for async stores such as React Native's `AsyncStorage`.
|
|
156
|
+
* Never rejects; same fallback and in-memory mirror behavior.
|
|
157
|
+
*/
|
|
158
|
+
export function createAsyncStorageItem<T>(
|
|
159
|
+
options: StorageItemOptions<T> & { storage: AsyncStorageLike }
|
|
160
|
+
): AsyncStorageItem<T> {
|
|
161
|
+
const { key, fallback, storage } = options
|
|
162
|
+
const { serialize, decode } = createCodec(options)
|
|
163
|
+
let mirror: { value: T } | undefined
|
|
164
|
+
|
|
165
|
+
const write = async (value: T, action: () => Promise<void>) => {
|
|
166
|
+
try {
|
|
167
|
+
await action()
|
|
168
|
+
mirror = undefined
|
|
169
|
+
} catch {
|
|
170
|
+
mirror = { value }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
key,
|
|
176
|
+
async get() {
|
|
177
|
+
if (mirror) return mirror.value
|
|
178
|
+
try {
|
|
179
|
+
return decode(await storage.getItem(key))
|
|
180
|
+
} catch {
|
|
181
|
+
return fallback
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
set: (value) => write(value, () => storage.setItem(key, serialize(value))),
|
|
185
|
+
remove: () => write(fallback, () => storage.removeItem(key))
|
|
186
|
+
}
|
|
187
|
+
}
|