hanzio 1.2.0 → 2.1.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.
Files changed (155) hide show
  1. package/README.md +786 -239
  2. package/dist/api-wrapper/builders.d.ts +4 -3
  3. package/dist/api-wrapper/create-client.d.ts +1 -1
  4. package/dist/api-wrapper/describe.d.ts +16 -0
  5. package/dist/api-wrapper/endpoint.d.ts +18 -0
  6. package/dist/api-wrapper/errors.d.ts +39 -2
  7. package/dist/api-wrapper/index.d.ts +5 -0
  8. package/dist/api-wrapper/otel.d.ts +44 -0
  9. package/dist/api-wrapper/paginate.d.ts +12 -0
  10. package/dist/api-wrapper/redaction.d.ts +2 -0
  11. package/dist/api-wrapper/shared.d.ts +16 -2
  12. package/dist/api-wrapper/testing.d.ts +23 -0
  13. package/dist/api-wrapper/transport.d.ts +4 -5
  14. package/dist/api-wrapper/types.d.ts +103 -18
  15. package/dist/array/index.d.ts +50 -4
  16. package/dist/cache/createCache.d.ts +31 -0
  17. package/dist/cache/index.d.ts +19 -2
  18. package/dist/color/contrast.d.ts +40 -0
  19. package/dist/color/index.d.ts +2 -0
  20. package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
  21. package/dist/cool-console-log/index.d.ts +34 -5
  22. package/dist/crypto/encoding.d.ts +18 -0
  23. package/dist/crypto/hash.d.ts +33 -0
  24. package/dist/crypto/index.d.ts +4 -0
  25. package/dist/crypto/sealer.d.ts +25 -0
  26. package/dist/crypto/webhook.d.ts +20 -0
  27. package/dist/date/index.d.ts +46 -2
  28. package/dist/emitter/index.d.ts +21 -0
  29. package/dist/error/index.d.ts +12 -0
  30. package/dist/format/index.d.ts +102 -0
  31. package/dist/function/index.d.ts +60 -0
  32. package/dist/guard/index.d.ts +20 -0
  33. package/dist/i18n/index.d.ts +37 -0
  34. package/dist/index-0zvtcbzy.js +224 -0
  35. package/dist/index-8r2vdzjg.js +87 -0
  36. package/dist/index-hy7kbb99.js +157 -0
  37. package/dist/index-hyt6c56h.js +290 -0
  38. package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
  39. package/dist/index-k9yx6bne.js +89 -0
  40. package/dist/index-kj1phgrx.js +169 -0
  41. package/dist/index-ktynw3vs.js +37 -0
  42. package/dist/index-mnkcfyx8.js +78 -0
  43. package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
  44. package/dist/index-rp5pqghz.js +157 -0
  45. package/dist/index-w882zry3.js +29 -0
  46. package/dist/index.d.ts +7 -7
  47. package/dist/jwt/index.d.ts +50 -2
  48. package/dist/math/index.d.ts +2 -2
  49. package/dist/math/linearRegressionTrend.d.ts +14 -1
  50. package/dist/math/scale.d.ts +8 -0
  51. package/dist/object/index.d.ts +19 -0
  52. package/dist/p-queue/index.d.ts +1 -1
  53. package/dist/promise/index.d.ts +97 -2
  54. package/dist/secrets/core.d.ts +7 -0
  55. package/dist/secrets/index.d.ts +3 -1
  56. package/dist/secrets/public-config.d.ts +7 -0
  57. package/dist/secrets/vite.d.ts +1 -0
  58. package/dist/secrets/worker.d.ts +1 -0
  59. package/dist/sitemap/index.d.ts +48 -6
  60. package/dist/src/api-wrapper/index.js +60 -1130
  61. package/dist/src/api-wrapper/testing.js +20 -0
  62. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  63. package/dist/src/crypto/index.js +195 -0
  64. package/dist/src/format/index.js +211 -0
  65. package/dist/src/i18n/index.js +36 -0
  66. package/dist/src/index.js +739 -3145
  67. package/dist/src/jwt/index.js +138 -92
  68. package/dist/src/p-queue/index.js +9 -437
  69. package/dist/src/secrets/index.js +50 -1428
  70. package/dist/src/secrets/vite.js +10 -39
  71. package/dist/src/secrets/worker.js +14 -273
  72. package/dist/src/sitemap/index.js +183 -511
  73. package/dist/src/storage/index.js +119 -0
  74. package/dist/src/zod/index.js +340 -167
  75. package/dist/state/index.d.ts +21 -0
  76. package/dist/storage/index.d.ts +58 -0
  77. package/dist/string/index.d.ts +75 -2
  78. package/dist/typedswitch/index.d.ts +31 -24
  79. package/dist/types/index.d.ts +11 -0
  80. package/dist/url/index.d.ts +50 -1
  81. package/dist/zod/index.d.ts +32 -3
  82. package/dist/zod/introspect.d.ts +19 -0
  83. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  84. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  85. package/dist/zod/zodToTypeString.d.ts +8 -3
  86. package/package.json +78 -7
  87. package/src/api-wrapper/actions-impl.ts +11 -72
  88. package/src/api-wrapper/builders.ts +32 -25
  89. package/src/api-wrapper/create-client.ts +171 -41
  90. package/src/api-wrapper/describe.ts +157 -0
  91. package/src/api-wrapper/endpoint.ts +26 -0
  92. package/src/api-wrapper/error-reporting.ts +7 -1
  93. package/src/api-wrapper/errors.ts +107 -2
  94. package/src/api-wrapper/headers.ts +11 -6
  95. package/src/api-wrapper/index.ts +5 -0
  96. package/src/api-wrapper/otel.ts +139 -0
  97. package/src/api-wrapper/paginate.ts +28 -0
  98. package/src/api-wrapper/redaction.ts +42 -0
  99. package/src/api-wrapper/shared.ts +21 -2
  100. package/src/api-wrapper/testing.ts +48 -0
  101. package/src/api-wrapper/transport.ts +41 -43
  102. package/src/api-wrapper/types.ts +185 -18
  103. package/src/array/index.ts +195 -42
  104. package/src/cache/createCache.ts +157 -0
  105. package/src/cache/index.ts +132 -147
  106. package/src/color/contrast.ts +163 -0
  107. package/src/color/index.ts +22 -0
  108. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  109. package/src/cool-console-log/index.ts +128 -41
  110. package/src/crypto/encoding.ts +83 -0
  111. package/src/crypto/hash.ts +170 -0
  112. package/src/crypto/index.ts +26 -0
  113. package/src/crypto/sealer.ts +168 -0
  114. package/src/crypto/webhook.ts +86 -0
  115. package/src/date/index.ts +109 -34
  116. package/src/emitter/index.ts +82 -0
  117. package/src/error/index.ts +63 -0
  118. package/src/format/index.ts +389 -0
  119. package/src/function/index.ts +201 -0
  120. package/src/guard/index.ts +80 -0
  121. package/src/i18n/index.ts +125 -0
  122. package/src/index.ts +11 -7
  123. package/src/jwt/index.ts +199 -125
  124. package/src/math/index.ts +5 -10
  125. package/src/math/linearRegressionTrend.ts +33 -8
  126. package/src/math/scale.ts +17 -0
  127. package/src/object/index.ts +99 -0
  128. package/src/p-queue/index.ts +2 -0
  129. package/src/promise/index.ts +304 -9
  130. package/src/secrets/core.ts +72 -16
  131. package/src/secrets/index.ts +16 -4
  132. package/src/secrets/public-config.ts +47 -0
  133. package/src/secrets/vite.ts +2 -0
  134. package/src/secrets/worker.ts +2 -0
  135. package/src/sitemap/index.ts +293 -130
  136. package/src/state/index.ts +133 -81
  137. package/src/storage/index.ts +187 -0
  138. package/src/string/index.ts +261 -23
  139. package/src/typedswitch/index.ts +58 -38
  140. package/src/types/index.ts +14 -0
  141. package/src/url/index.ts +146 -1
  142. package/src/zod/index.ts +105 -9
  143. package/src/zod/introspect.ts +103 -0
  144. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  145. package/src/zod/zodMinMaxFilter.ts +37 -13
  146. package/src/zod/zodToTypeString.ts +184 -181
  147. package/dist/api-wrapper/index.js +0 -250
  148. package/dist/gzip/index.d.ts +0 -2
  149. package/dist/gzip/index.js +0 -16
  150. package/dist/index.js +0 -1823
  151. package/dist/jwt/index.js +0 -19
  152. package/dist/sitemap/index.js +0 -552
  153. package/dist/src/gzip/index.js +0 -16
  154. package/dist/src/secrets/loaders.js +0 -1529
  155. package/dist/zod/index.js +0 -210
@@ -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 result: Partial<Record<TAction, ValidationResult>> = {}
147
-
148
- for (const [action, targetState] of Object.entries(actionsMap)) {
149
- if (!targetState) continue
150
-
151
- const validationResult = await this.validateTransition(
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
- result[action as TAction] = validationResult
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 result: Partial<Record<TState, ValidationResult>> = {}
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
- for (const targetState of possibleTransitions) {
176
- const actionsToTarget = (
177
- Object.entries(actionsMap) as [TAction, TState | undefined][]
178
- ).filter(([, t]) => t === targetState)
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
- if (actionsToTarget.length === 0) {
181
- result[targetState] = await this.validateTransition(targetState)
182
- continue
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
- let lastError: ValidationResult = true
186
- let anyOk = false
187
- for (const [actionKey] of actionsToTarget) {
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 result
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 actionsMap = this.config.stateActionMap[this.getCurrentStatus()]
215
- if (!actionsMap) {
216
- return {
217
- success: false,
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 = actionsMap[action]
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
- const validationResult = await this.validateTransition(targetState, {
231
- action
232
- })
233
-
234
- if (validationResult === true) {
235
- // If there's a custom action handler, call it with the provided data
236
- if (this.config.actions?.[action]) {
237
- try {
238
- const result = await this.config.actions[action]!(this.item, data)
239
- return {
240
- success: true,
241
- newState: targetState,
242
- result
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
- // Default behavior if no action handler
267
+ return { success: true, newState: targetState, result }
268
+ } catch (error) {
253
269
  return {
254
- success: true,
255
- newState: targetState
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
- return await stateValidator(
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
- // This implementation just returns the first state
328
- // You might want to customize this based on your needs
329
- return this.getStates()[0]
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
+ }