@systemfsoftware/xstate-store 0.0.0-stage → 5.0.0-beta.6
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/CHANGELOG.md +7 -0
- package/LICENSE +22 -0
- package/README.md +526 -2
- package/dist/index.d.ts +147 -0
- package/dist/index.mjs +70 -0
- package/dist/persist.d.ts +256 -0
- package/dist/persist.mjs +630 -0
- package/dist/reset.d.ts +51 -0
- package/dist/reset.mjs +61 -0
- package/dist/store-BMZdYhoE.mjs +755 -0
- package/dist/types-0n0RpfhK.d.ts +442 -0
- package/dist/undo.d.ts +77 -0
- package/dist/undo.mjs +222 -0
- package/dist/validate.d.ts +35 -0
- package/dist/validate.mjs +138 -0
- package/dist/validationError-Ndy-qk0W.mjs +6 -0
- package/package.json +80 -3
package/dist/persist.mjs
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import { n as assertNoInternalEventTypeCollisions } from "./store-BMZdYhoE.mjs";
|
|
2
|
+
// src/persist.ts
|
|
3
|
+
const PERSIST_INTERNALS = Symbol.for("xstate-store-persist");
|
|
4
|
+
const PERSIST_REVISION = Symbol("xstate-store-persist-revision");
|
|
5
|
+
const PERSIST_REHYDRATE_EVENT_TYPE = "__persist.rehydrate";
|
|
6
|
+
const noopStorage = {
|
|
7
|
+
getItem: () => null,
|
|
8
|
+
setItem: () => {},
|
|
9
|
+
removeItem: () => {}
|
|
10
|
+
};
|
|
11
|
+
function getStorage(options) {
|
|
12
|
+
return options.storage ?? createJSONStorage(() => localStorage);
|
|
13
|
+
}
|
|
14
|
+
function isEventStrategy(options) {
|
|
15
|
+
return options.strategy === "event";
|
|
16
|
+
}
|
|
17
|
+
function serializeSnapshotValue(options, value) {
|
|
18
|
+
return options.serialize ? options.serialize(value) : JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
function deserializeSnapshotValue(options, str) {
|
|
21
|
+
return options.deserialize ? options.deserialize(str) : JSON.parse(str);
|
|
22
|
+
}
|
|
23
|
+
function serializeEventValue(options, value) {
|
|
24
|
+
return options.serialize ? options.serialize(value) : JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
function deserializeEventValue(options, str) {
|
|
27
|
+
return options.deserialize ? options.deserialize(str) : JSON.parse(str);
|
|
28
|
+
}
|
|
29
|
+
function mergeContext(options, persistedContext, currentContext) {
|
|
30
|
+
return options.merge ? options.merge(persistedContext, currentContext) : {
|
|
31
|
+
...currentContext,
|
|
32
|
+
...persistedContext
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function migrateSnapshotIfNeeded(options, stored) {
|
|
36
|
+
const currentVersion = options.version ?? 0;
|
|
37
|
+
if (stored.version !== currentVersion && options.migrate) return options.migrate(stored.context, stored.version);
|
|
38
|
+
return stored.context;
|
|
39
|
+
}
|
|
40
|
+
function migrateEventsIfNeeded(options, stored) {
|
|
41
|
+
const currentVersion = options.version ?? 0;
|
|
42
|
+
if (stored.version !== currentVersion && options.migrate) return options.migrate(stored.events, stored.version);
|
|
43
|
+
return stored.events;
|
|
44
|
+
}
|
|
45
|
+
function enqueueWrite(internals, write) {
|
|
46
|
+
const result = internals.pendingWrite ? internals.pendingWrite.then(write, write) : write();
|
|
47
|
+
if (result instanceof Promise) {
|
|
48
|
+
const pending = result.finally(() => {
|
|
49
|
+
if (internals.pendingWrite === pending) internals.pendingWrite = null;
|
|
50
|
+
});
|
|
51
|
+
internals.pendingWrite = pending;
|
|
52
|
+
return pending;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeSnapshotToStorage(internals, context) {
|
|
56
|
+
const options = internals.options;
|
|
57
|
+
const { storage } = internals;
|
|
58
|
+
const contextToPersist = options.pick ? options.pick(context) : context;
|
|
59
|
+
const value = {
|
|
60
|
+
context: contextToPersist,
|
|
61
|
+
version: options.version ?? 0
|
|
62
|
+
};
|
|
63
|
+
return enqueueWrite(internals, () => {
|
|
64
|
+
try {
|
|
65
|
+
const serialized = serializeSnapshotValue(options, value);
|
|
66
|
+
const result = storage.setItem(options.name, serialized);
|
|
67
|
+
if (result instanceof Promise) return result.then(() => options.onDone?.(contextToPersist)).catch((err) => options.onError?.(err));
|
|
68
|
+
options.onDone?.(contextToPersist);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
options.onError?.(err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function writeEventsToStorage(internals, events, checkpoint) {
|
|
75
|
+
const options = internals.options;
|
|
76
|
+
const { storage } = internals;
|
|
77
|
+
const value = {
|
|
78
|
+
events,
|
|
79
|
+
version: options.version ?? 0
|
|
80
|
+
};
|
|
81
|
+
if (checkpoint !== void 0) value.checkpoint = checkpoint;
|
|
82
|
+
return enqueueWrite(internals, () => {
|
|
83
|
+
try {
|
|
84
|
+
const serialized = serializeEventValue(options, value);
|
|
85
|
+
const result = storage.setItem(options.name, serialized);
|
|
86
|
+
if (result instanceof Promise) return result.then(() => options.onDone?.(events)).catch((err) => options.onError?.(err));
|
|
87
|
+
options.onDone?.(events);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
options.onError?.(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function createInternals(options) {
|
|
94
|
+
const internals = {
|
|
95
|
+
options,
|
|
96
|
+
storage: getStorage(options),
|
|
97
|
+
pendingContext: null,
|
|
98
|
+
pendingEvents: null,
|
|
99
|
+
pendingCheckpoint: null,
|
|
100
|
+
flushTimeoutId: null,
|
|
101
|
+
pendingWrite: null,
|
|
102
|
+
lastScheduledRevision: 0,
|
|
103
|
+
flush: () => {
|
|
104
|
+
if (internals.flushTimeoutId !== null) {
|
|
105
|
+
clearTimeout(internals.flushTimeoutId);
|
|
106
|
+
internals.flushTimeoutId = null;
|
|
107
|
+
}
|
|
108
|
+
if (isEventStrategy(options)) {
|
|
109
|
+
if (internals.pendingEvents !== null) {
|
|
110
|
+
const events = internals.pendingEvents;
|
|
111
|
+
const checkpoint = internals.pendingCheckpoint;
|
|
112
|
+
internals.pendingEvents = null;
|
|
113
|
+
internals.pendingCheckpoint = null;
|
|
114
|
+
return writeEventsToStorage(internals, events, checkpoint);
|
|
115
|
+
}
|
|
116
|
+
} else if (internals.pendingContext !== null) {
|
|
117
|
+
const context = internals.pendingContext;
|
|
118
|
+
internals.pendingContext = null;
|
|
119
|
+
return writeSnapshotToStorage(internals, context);
|
|
120
|
+
}
|
|
121
|
+
return internals.pendingWrite ?? void 0;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return internals;
|
|
125
|
+
}
|
|
126
|
+
function persistSnapshotFromLogic(logic, options) {
|
|
127
|
+
assertNoInternalEventTypeCollisions(logic.eventTypes, [PERSIST_REHYDRATE_EVENT_TYPE], "persist");
|
|
128
|
+
const internals = createInternals(options);
|
|
129
|
+
const { storage } = internals;
|
|
130
|
+
const throttleMs = options.throttle ?? 0;
|
|
131
|
+
return {
|
|
132
|
+
...logic,
|
|
133
|
+
eventTypes: logic.eventTypes,
|
|
134
|
+
getInitialSnapshot: () => {
|
|
135
|
+
const baseSnapshot = logic.getInitialSnapshot();
|
|
136
|
+
if (options.skipHydration) return {
|
|
137
|
+
...baseSnapshot,
|
|
138
|
+
_persist: { hydrated: false },
|
|
139
|
+
[PERSIST_INTERNALS]: internals
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
const storedValue = storage.getItem(options.name);
|
|
143
|
+
if (storedValue instanceof Promise) {
|
|
144
|
+
storedValue.catch((error) => options.onError?.(error));
|
|
145
|
+
return {
|
|
146
|
+
...baseSnapshot,
|
|
147
|
+
_persist: { hydrated: false },
|
|
148
|
+
[PERSIST_INTERNALS]: internals
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (storedValue === null) return {
|
|
152
|
+
...baseSnapshot,
|
|
153
|
+
_persist: { hydrated: true },
|
|
154
|
+
[PERSIST_INTERNALS]: internals
|
|
155
|
+
};
|
|
156
|
+
const mergedContext = mergeContext(options, migrateSnapshotIfNeeded(options, deserializeSnapshotValue(options, storedValue)), baseSnapshot.context);
|
|
157
|
+
return {
|
|
158
|
+
...baseSnapshot,
|
|
159
|
+
context: mergedContext,
|
|
160
|
+
_persist: { hydrated: true },
|
|
161
|
+
[PERSIST_INTERNALS]: internals
|
|
162
|
+
};
|
|
163
|
+
} catch (err) {
|
|
164
|
+
options.onError?.(err);
|
|
165
|
+
return {
|
|
166
|
+
...baseSnapshot,
|
|
167
|
+
_persist: { hydrated: true },
|
|
168
|
+
[PERSIST_INTERNALS]: internals
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
transition: (snapshot, event) => {
|
|
173
|
+
if (event.type === PERSIST_REHYDRATE_EVENT_TYPE) {
|
|
174
|
+
const rawState = event.state;
|
|
175
|
+
if (!rawState) return [{
|
|
176
|
+
...snapshot,
|
|
177
|
+
_persist: {
|
|
178
|
+
...snapshot._persist,
|
|
179
|
+
hydrated: true
|
|
180
|
+
}
|
|
181
|
+
}, []];
|
|
182
|
+
try {
|
|
183
|
+
const mergedContext = mergeContext(options, migrateSnapshotIfNeeded(options, deserializeSnapshotValue(options, rawState)), snapshot.context);
|
|
184
|
+
return [{
|
|
185
|
+
...snapshot,
|
|
186
|
+
context: mergedContext,
|
|
187
|
+
_persist: {
|
|
188
|
+
...snapshot._persist,
|
|
189
|
+
hydrated: true
|
|
190
|
+
}
|
|
191
|
+
}, []];
|
|
192
|
+
} catch (err) {
|
|
193
|
+
options.onError?.(err);
|
|
194
|
+
return [{
|
|
195
|
+
...snapshot,
|
|
196
|
+
_persist: {
|
|
197
|
+
...snapshot._persist,
|
|
198
|
+
hydrated: true
|
|
199
|
+
}
|
|
200
|
+
}, []];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const [nextSnapshot, effects] = logic.transition(snapshot, event);
|
|
204
|
+
const revision = (snapshot[PERSIST_REVISION] ?? 0) + 1;
|
|
205
|
+
const snapshotWithMeta = {
|
|
206
|
+
...nextSnapshot,
|
|
207
|
+
[PERSIST_REVISION]: revision,
|
|
208
|
+
_persist: snapshot._persist ?? { hydrated: false },
|
|
209
|
+
[PERSIST_INTERNALS]: internals
|
|
210
|
+
};
|
|
211
|
+
if (!snapshotWithMeta._persist?.hydrated) return [snapshotWithMeta, effects];
|
|
212
|
+
if (options.filter && !options.filter(event)) return [snapshotWithMeta, effects];
|
|
213
|
+
const persistEffect = () => {
|
|
214
|
+
if (revision <= internals.lastScheduledRevision) return;
|
|
215
|
+
internals.lastScheduledRevision = revision;
|
|
216
|
+
if (throttleMs > 0) {
|
|
217
|
+
internals.pendingContext = nextSnapshot.context;
|
|
218
|
+
if (internals.flushTimeoutId === null) internals.flushTimeoutId = setTimeout(() => {
|
|
219
|
+
internals.flush();
|
|
220
|
+
}, throttleMs);
|
|
221
|
+
} else writeSnapshotToStorage(internals, nextSnapshot.context);
|
|
222
|
+
};
|
|
223
|
+
return [snapshotWithMeta, [persistEffect, ...effects]];
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function persistEventFromLogic(logic, options) {
|
|
228
|
+
assertNoInternalEventTypeCollisions(logic.eventTypes, [PERSIST_REHYDRATE_EVENT_TYPE], "persist");
|
|
229
|
+
const internals = createInternals(options);
|
|
230
|
+
const { storage } = internals;
|
|
231
|
+
const throttleMs = options.throttle ?? 0;
|
|
232
|
+
const maxEvents = options.maxEvents ?? Infinity;
|
|
233
|
+
function replayEvents(baseSnapshot, events) {
|
|
234
|
+
let current = baseSnapshot;
|
|
235
|
+
for (const ev of events) {
|
|
236
|
+
const [next] = logic.transition(current, ev);
|
|
237
|
+
current = next;
|
|
238
|
+
}
|
|
239
|
+
return current;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
...logic,
|
|
243
|
+
eventTypes: logic.eventTypes,
|
|
244
|
+
getInitialSnapshot: () => {
|
|
245
|
+
const baseSnapshot = logic.getInitialSnapshot();
|
|
246
|
+
if (options.skipHydration) return {
|
|
247
|
+
...baseSnapshot,
|
|
248
|
+
_persistEvents: [],
|
|
249
|
+
_persistCheckpoint: null,
|
|
250
|
+
_persist: { hydrated: false },
|
|
251
|
+
[PERSIST_INTERNALS]: internals
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
const storedValue = storage.getItem(options.name);
|
|
255
|
+
if (storedValue instanceof Promise) {
|
|
256
|
+
storedValue.catch((error) => options.onError?.(error));
|
|
257
|
+
return {
|
|
258
|
+
...baseSnapshot,
|
|
259
|
+
_persistEvents: [],
|
|
260
|
+
_persistCheckpoint: null,
|
|
261
|
+
_persist: { hydrated: false },
|
|
262
|
+
[PERSIST_INTERNALS]: internals
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (storedValue === null) return {
|
|
266
|
+
...baseSnapshot,
|
|
267
|
+
_persistEvents: [],
|
|
268
|
+
_persistCheckpoint: null,
|
|
269
|
+
_persist: { hydrated: true },
|
|
270
|
+
[PERSIST_INTERNALS]: internals
|
|
271
|
+
};
|
|
272
|
+
const parsed = deserializeEventValue(options, storedValue);
|
|
273
|
+
const events = migrateEventsIfNeeded(options, parsed);
|
|
274
|
+
return {
|
|
275
|
+
...replayEvents(parsed.checkpoint ? {
|
|
276
|
+
...baseSnapshot,
|
|
277
|
+
context: parsed.checkpoint
|
|
278
|
+
} : baseSnapshot, events),
|
|
279
|
+
_persistEvents: events,
|
|
280
|
+
_persistCheckpoint: parsed.checkpoint ?? null,
|
|
281
|
+
_persist: { hydrated: true },
|
|
282
|
+
[PERSIST_INTERNALS]: internals
|
|
283
|
+
};
|
|
284
|
+
} catch (err) {
|
|
285
|
+
options.onError?.(err);
|
|
286
|
+
return {
|
|
287
|
+
...baseSnapshot,
|
|
288
|
+
_persistEvents: [],
|
|
289
|
+
_persistCheckpoint: null,
|
|
290
|
+
_persist: { hydrated: true },
|
|
291
|
+
[PERSIST_INTERNALS]: internals
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
transition: (snapshot, event) => {
|
|
296
|
+
if (event.type === PERSIST_REHYDRATE_EVENT_TYPE) {
|
|
297
|
+
const rawState = event.state;
|
|
298
|
+
if (!rawState) return [{
|
|
299
|
+
...snapshot,
|
|
300
|
+
_persistEvents: snapshot._persistEvents ?? [],
|
|
301
|
+
_persistCheckpoint: snapshot._persistCheckpoint ?? null,
|
|
302
|
+
_persist: {
|
|
303
|
+
...snapshot._persist,
|
|
304
|
+
hydrated: true
|
|
305
|
+
}
|
|
306
|
+
}, []];
|
|
307
|
+
try {
|
|
308
|
+
const parsed = deserializeEventValue(options, rawState);
|
|
309
|
+
const events = migrateEventsIfNeeded(options, parsed);
|
|
310
|
+
const baseSnapshot = logic.getInitialSnapshot();
|
|
311
|
+
return [{
|
|
312
|
+
...replayEvents(parsed.checkpoint ? {
|
|
313
|
+
...baseSnapshot,
|
|
314
|
+
context: parsed.checkpoint
|
|
315
|
+
} : baseSnapshot, events),
|
|
316
|
+
[PERSIST_REVISION]: snapshot[PERSIST_REVISION],
|
|
317
|
+
_persistEvents: events,
|
|
318
|
+
_persistCheckpoint: parsed.checkpoint ?? null,
|
|
319
|
+
_persist: {
|
|
320
|
+
...snapshot._persist,
|
|
321
|
+
hydrated: true
|
|
322
|
+
},
|
|
323
|
+
[PERSIST_INTERNALS]: internals
|
|
324
|
+
}, []];
|
|
325
|
+
} catch (err) {
|
|
326
|
+
options.onError?.(err);
|
|
327
|
+
return [{
|
|
328
|
+
...snapshot,
|
|
329
|
+
_persistEvents: snapshot._persistEvents ?? [],
|
|
330
|
+
_persistCheckpoint: snapshot._persistCheckpoint ?? null,
|
|
331
|
+
_persist: {
|
|
332
|
+
...snapshot._persist,
|
|
333
|
+
hydrated: true
|
|
334
|
+
}
|
|
335
|
+
}, []];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const [nextSnapshot, effects] = logic.transition(snapshot, event);
|
|
339
|
+
const prevEvents = snapshot._persistEvents ?? [];
|
|
340
|
+
const prevCheckpoint = snapshot._persistCheckpoint ?? null;
|
|
341
|
+
const revision = (snapshot[PERSIST_REVISION] ?? 0) + 1;
|
|
342
|
+
const snapshotWithMeta = {
|
|
343
|
+
...nextSnapshot,
|
|
344
|
+
[PERSIST_REVISION]: revision,
|
|
345
|
+
_persistEvents: prevEvents,
|
|
346
|
+
_persistCheckpoint: prevCheckpoint,
|
|
347
|
+
_persist: snapshot._persist ?? { hydrated: false },
|
|
348
|
+
[PERSIST_INTERNALS]: internals
|
|
349
|
+
};
|
|
350
|
+
if (!snapshotWithMeta._persist?.hydrated) return [snapshotWithMeta, effects];
|
|
351
|
+
let nextEvents = [...prevEvents, event];
|
|
352
|
+
let nextCheckpoint = prevCheckpoint;
|
|
353
|
+
if (nextEvents.length > maxEvents) {
|
|
354
|
+
const droppedEvents = nextEvents.slice(0, -maxEvents);
|
|
355
|
+
const baseSnapshot = logic.getInitialSnapshot();
|
|
356
|
+
nextCheckpoint = replayEvents(prevCheckpoint ? {
|
|
357
|
+
...baseSnapshot,
|
|
358
|
+
context: prevCheckpoint
|
|
359
|
+
} : baseSnapshot, droppedEvents).context;
|
|
360
|
+
nextEvents = nextEvents.slice(-maxEvents);
|
|
361
|
+
}
|
|
362
|
+
const snapshotWithEvents = {
|
|
363
|
+
...snapshotWithMeta,
|
|
364
|
+
_persistEvents: nextEvents,
|
|
365
|
+
_persistCheckpoint: nextCheckpoint
|
|
366
|
+
};
|
|
367
|
+
const persistEffect = () => {
|
|
368
|
+
if (revision <= internals.lastScheduledRevision) return;
|
|
369
|
+
internals.lastScheduledRevision = revision;
|
|
370
|
+
if (throttleMs > 0) {
|
|
371
|
+
internals.pendingEvents = nextEvents;
|
|
372
|
+
internals.pendingCheckpoint = nextCheckpoint;
|
|
373
|
+
if (internals.flushTimeoutId === null) internals.flushTimeoutId = setTimeout(() => {
|
|
374
|
+
internals.flush();
|
|
375
|
+
}, throttleMs);
|
|
376
|
+
} else writeEventsToStorage(internals, nextEvents, nextCheckpoint);
|
|
377
|
+
};
|
|
378
|
+
return [snapshotWithEvents, [persistEffect, ...effects]];
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function persistFromLogic(logic, options) {
|
|
383
|
+
if (isEventStrategy(options)) return persistEventFromLogic(logic, options);
|
|
384
|
+
return persistSnapshotFromLogic(logic, options);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Creates a store extension that persists context to storage.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
*
|
|
391
|
+
* ```ts
|
|
392
|
+
* import { persist } from '@systemfsoftware/xstate-store/persist';
|
|
393
|
+
*
|
|
394
|
+
* const store = createStore({
|
|
395
|
+
* context: { count: 0 },
|
|
396
|
+
* on: { inc: (ctx) => ({ count: ctx.count + 1 }) }
|
|
397
|
+
* }).with(persist({ name: 'my-store' }));
|
|
398
|
+
* ```
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
*
|
|
402
|
+
* ```ts
|
|
403
|
+
* // Throttled writes with callbacks
|
|
404
|
+
* const store = createStore({
|
|
405
|
+
* context: { count: 0 },
|
|
406
|
+
* on: { inc: (ctx) => ({ count: ctx.count + 1 }) }
|
|
407
|
+
* }).with(
|
|
408
|
+
* persist({
|
|
409
|
+
* name: 'my-store',
|
|
410
|
+
* throttle: 1000,
|
|
411
|
+
* onDone: (ctx) => console.log('Persisted:', ctx),
|
|
412
|
+
* onError: (err) => console.error('Persist failed:', err)
|
|
413
|
+
* })
|
|
414
|
+
* );
|
|
415
|
+
* ```
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
*
|
|
419
|
+
* ```ts
|
|
420
|
+
* // Async storage with manual rehydration
|
|
421
|
+
* import {
|
|
422
|
+
* persist,
|
|
423
|
+
* rehydrateStore,
|
|
424
|
+
* createJSONStorage
|
|
425
|
+
* } from '@systemfsoftware/xstate-store/persist';
|
|
426
|
+
*
|
|
427
|
+
* const store = createStore({
|
|
428
|
+
* context: { count: 0 },
|
|
429
|
+
* on: { inc: (ctx) => ({ count: ctx.count + 1 }) }
|
|
430
|
+
* }).with(
|
|
431
|
+
* persist({
|
|
432
|
+
* name: 'my-store',
|
|
433
|
+
* storage: createJSONStorage(() => AsyncStorage),
|
|
434
|
+
* skipHydration: true
|
|
435
|
+
* })
|
|
436
|
+
* );
|
|
437
|
+
*
|
|
438
|
+
* await rehydrateStore(store);
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
function persist(options) {
|
|
442
|
+
return (logic) => persistFromLogic(logic, options);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Creates a storage adapter with error handling and SSR safety.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
*
|
|
449
|
+
* ```ts
|
|
450
|
+
* import { createJSONStorage } from '@systemfsoftware/xstate-store/persist';
|
|
451
|
+
*
|
|
452
|
+
* // Safe for SSR — returns noop storage if localStorage is unavailable
|
|
453
|
+
* const storage = createJSONStorage(() => localStorage);
|
|
454
|
+
*
|
|
455
|
+
* // Async storage (React Native)
|
|
456
|
+
* const asyncStorage = createJSONStorage(() => AsyncStorage);
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
function createJSONStorage(getStorage) {
|
|
460
|
+
let storage;
|
|
461
|
+
try {
|
|
462
|
+
storage = getStorage();
|
|
463
|
+
} catch {
|
|
464
|
+
return noopStorage;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
getItem: (name) => {
|
|
468
|
+
try {
|
|
469
|
+
const result = storage?.getItem(name);
|
|
470
|
+
if (result instanceof Promise) return result.catch(() => null);
|
|
471
|
+
return result ?? null;
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
setItem: (name, value) => {
|
|
477
|
+
try {
|
|
478
|
+
const result = storage?.setItem(name, value);
|
|
479
|
+
if (result instanceof Promise) return result.catch(() => {});
|
|
480
|
+
return result;
|
|
481
|
+
} catch {}
|
|
482
|
+
},
|
|
483
|
+
removeItem: (name) => {
|
|
484
|
+
try {
|
|
485
|
+
const result = storage?.removeItem(name);
|
|
486
|
+
if (result instanceof Promise) return result.catch(() => {});
|
|
487
|
+
return result;
|
|
488
|
+
} catch {}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Removes persisted data from storage for a store that uses the `persist`
|
|
494
|
+
* extension.
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
*
|
|
498
|
+
* ```ts
|
|
499
|
+
* import { clearStorage } from '@systemfsoftware/xstate-store/persist';
|
|
500
|
+
*
|
|
501
|
+
* clearStorage(store);
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
function clearStorage(store) {
|
|
505
|
+
const internals = store.getSnapshot()?.[PERSIST_INTERNALS];
|
|
506
|
+
if (!internals) throw new Error("clearStorage: store does not have a persist extension");
|
|
507
|
+
if (internals.flushTimeoutId !== null) {
|
|
508
|
+
clearTimeout(internals.flushTimeoutId);
|
|
509
|
+
internals.flushTimeoutId = null;
|
|
510
|
+
}
|
|
511
|
+
internals.pendingContext = null;
|
|
512
|
+
internals.pendingEvents = null;
|
|
513
|
+
internals.pendingCheckpoint = null;
|
|
514
|
+
return enqueueWrite(internals, () => internals.storage.removeItem(internals.options.name));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Flushes pending throttled updates and waits for already queued async writes.
|
|
518
|
+
* Synchronous storage writes complete before this function returns.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
*
|
|
522
|
+
* ```ts
|
|
523
|
+
* import { flushStorage } from '@systemfsoftware/xstate-store/persist';
|
|
524
|
+
*
|
|
525
|
+
* // Force write before page unload
|
|
526
|
+
* window.addEventListener('beforeunload', () => {
|
|
527
|
+
* flushStorage(store);
|
|
528
|
+
* });
|
|
529
|
+
* ```
|
|
530
|
+
*/
|
|
531
|
+
function flushStorage(store) {
|
|
532
|
+
const internals = store.getSnapshot()?.[PERSIST_INTERNALS];
|
|
533
|
+
if (!internals) throw new Error("flushStorage: store does not have a persist extension");
|
|
534
|
+
return internals.flush();
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Returns whether the store has been hydrated from storage.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
*
|
|
541
|
+
* ```ts
|
|
542
|
+
* import { isHydrated } from '@systemfsoftware/xstate-store/persist';
|
|
543
|
+
*
|
|
544
|
+
* if (isHydrated(store)) {
|
|
545
|
+
* // Safe to read persisted state
|
|
546
|
+
* }
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
function isHydrated(store) {
|
|
550
|
+
return store.getSnapshot()?._persist?.hydrated === true;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Reads persisted state from (potentially async) storage and rehydrates the
|
|
554
|
+
* store. Use this for async storage adapters or when `skipHydration` is
|
|
555
|
+
* `true`.
|
|
556
|
+
*
|
|
557
|
+
* @example
|
|
558
|
+
*
|
|
559
|
+
* ```ts
|
|
560
|
+
* import { rehydrateStore } from '@systemfsoftware/xstate-store/persist';
|
|
561
|
+
*
|
|
562
|
+
* const store = createStore({ ... }).with(persist({
|
|
563
|
+
* name: 'my-store',
|
|
564
|
+
* storage: createJSONStorage(() => AsyncStorage),
|
|
565
|
+
* skipHydration: true
|
|
566
|
+
* }));
|
|
567
|
+
*
|
|
568
|
+
* await rehydrateStore(store);
|
|
569
|
+
* ```
|
|
570
|
+
*/
|
|
571
|
+
async function rehydrateStore(store) {
|
|
572
|
+
const internals = store.getSnapshot()?.[PERSIST_INTERNALS];
|
|
573
|
+
if (!internals) throw new Error("rehydrateStore: store does not have a persist extension");
|
|
574
|
+
const data = await internals.storage.getItem(internals.options.name);
|
|
575
|
+
store.send({
|
|
576
|
+
type: PERSIST_REHYDRATE_EVENT_TYPE,
|
|
577
|
+
state: data
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function assertBroadcastChannelAvailable() {
|
|
581
|
+
if (typeof BroadcastChannel === "undefined") throw new Error("createBroadcastStorage: BroadcastChannel is not available in this environment");
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Wraps a storage adapter so writes are announced across same-origin tabs.
|
|
585
|
+
*
|
|
586
|
+
* Pair with `subscribeToBroadcastStorage(...)` on persisted stores that should
|
|
587
|
+
* rehydrate when another tab writes to the same storage key.
|
|
588
|
+
*/
|
|
589
|
+
function createBroadcastStorage(baseStorage, options) {
|
|
590
|
+
assertBroadcastChannelAvailable();
|
|
591
|
+
const channel = new BroadcastChannel(options?.channel ?? "xstate-store");
|
|
592
|
+
return {
|
|
593
|
+
channel,
|
|
594
|
+
getItem: (name) => baseStorage.getItem(name),
|
|
595
|
+
setItem: (name, value) => {
|
|
596
|
+
const result = baseStorage.setItem(name, value);
|
|
597
|
+
const broadcast = () => {
|
|
598
|
+
channel.postMessage({
|
|
599
|
+
type: "xstate-store-update",
|
|
600
|
+
name
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
if (result instanceof Promise) return result.then(broadcast);
|
|
604
|
+
broadcast();
|
|
605
|
+
return result;
|
|
606
|
+
},
|
|
607
|
+
removeItem: (name) => baseStorage.removeItem(name)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Subscribes a persisted store to broadcast storage updates.
|
|
612
|
+
*
|
|
613
|
+
* Returns an unsubscribe function that closes the broadcast listener.
|
|
614
|
+
*/
|
|
615
|
+
function subscribeToBroadcastStorage(store) {
|
|
616
|
+
const internals = store.getSnapshot()?.[PERSIST_INTERNALS];
|
|
617
|
+
if (!internals) throw new Error("subscribeToBroadcastStorage: store does not have a persist extension");
|
|
618
|
+
const channel = internals.storage.channel;
|
|
619
|
+
if (typeof BroadcastChannel === "undefined" || !(channel instanceof BroadcastChannel)) throw new Error("subscribeToBroadcastStorage: store storage must be wrapped with createBroadcastStorage()");
|
|
620
|
+
const storeName = internals.options.name;
|
|
621
|
+
const handler = (event) => {
|
|
622
|
+
const data = event.data;
|
|
623
|
+
if (data && data.type === "xstate-store-update" && data.name === storeName) rehydrateStore(store);
|
|
624
|
+
};
|
|
625
|
+
channel.addEventListener("message", handler);
|
|
626
|
+
return () => {
|
|
627
|
+
channel.removeEventListener("message", handler);
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
export { clearStorage, createBroadcastStorage, createJSONStorage, flushStorage, isHydrated, persist, rehydrateStore, subscribeToBroadcastStorage };
|
package/dist/reset.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { B as StoreContext, U as StoreExtension, _ as EventObject, v as EventPayloadMap } from "./types-0n0RpfhK.js";
|
|
2
|
+
interface ResetOptions<TContext extends StoreContext> {
|
|
3
|
+
/**
|
|
4
|
+
* Custom reset function. Receives the initial context and the current
|
|
5
|
+
* context, returns the context to reset to.
|
|
6
|
+
*
|
|
7
|
+
* Defaults to returning the initial context (full reset).
|
|
8
|
+
*/
|
|
9
|
+
to?: (initialContext: TContext, currentContext: TContext) => TContext;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a store extension that adds a `reset` trigger to reset the store
|
|
13
|
+
* context to its initial state.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { reset } from '@systemfsoftware/xstate-store/reset';
|
|
19
|
+
*
|
|
20
|
+
* const store = createStore({
|
|
21
|
+
* context: { count: 0 },
|
|
22
|
+
* on: { inc: (ctx) => ({ count: ctx.count + 1 }) }
|
|
23
|
+
* }).with(reset());
|
|
24
|
+
*
|
|
25
|
+
* store.trigger.inc();
|
|
26
|
+
* store.trigger.reset(); // count = 0
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Partial reset: keep some fields
|
|
33
|
+
* const store = createStore({
|
|
34
|
+
* context: { count: 0, user: null as string | null },
|
|
35
|
+
* on: {
|
|
36
|
+
* inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }),
|
|
37
|
+
* login: (ctx, e: { user: string }) => ({ ...ctx, user: e.user })
|
|
38
|
+
* }
|
|
39
|
+
* }).with(
|
|
40
|
+
* reset({
|
|
41
|
+
* to: (initial, current) => ({ ...initial, user: current.user })
|
|
42
|
+
* })
|
|
43
|
+
* );
|
|
44
|
+
*
|
|
45
|
+
* store.trigger.reset(); // resets count, keeps user
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function reset<TContext extends StoreContext, TEventPayloadMap extends EventPayloadMap, TEmitted extends EventObject>(options?: ResetOptions<TContext>): StoreExtension<TContext, TEventPayloadMap, {
|
|
49
|
+
reset: null;
|
|
50
|
+
}, TEmitted>;
|
|
51
|
+
export {};
|