11# @stackflow/plugin-stack-persistence
22
3- Persist a Stackflow navigation snapshot beyond the lifetime of the JavaScript
4- runtime and restore it when the stack starts again. The package is
5- framework-neutral: it uses the ` @stackflow/core ` plugin contract and leaves the
6- storage medium, serialization, record lifetime, and reuse policy to your
7- application.
3+ Applications often need to preserve a user's navigation context across a page
4+ reload or JavaScript runtime replacement. Reconstructing only the initial
5+ Activity loses the navigation history and any Steps recorded in the stack.
6+
7+ ` @stackflow/plugin-stack-persistence ` saves a complete Stackflow snapshot and
8+ restores it when the stack starts again. The package is framework-neutral and
9+ leaves the storage medium, serialization, record lifetime, and reuse policy to
10+ your application.
811
912## Installation
1013
1114``` bash
1215yarn add @stackflow/plugin-stack-persistence
1316```
1417
15- This package requires ` @stackflow/core ` 3.x.
16-
1718## Setup
1819
19- Create a synchronous loader, an asynchronous saver, and a strategy that
20- validates stored metadata and decides whether its snapshot can be reused.
20+ Add ` stackPersistencePlugin() ` to your Stackflow configuration with a storage
21+ and reuse strategy:
22+
23+ ``` typescript
24+ import { stackPersistencePlugin } from " @stackflow/plugin-stack-persistence" ;
25+ import { stackflow } from " @stackflow/react" ;
26+ import { ArticleActivity } from " ./ArticleActivity" ;
27+ import { HomeActivity } from " ./HomeActivity" ;
28+ import { snapshotStorage , snapshotStrategy } from " ./persistence" ;
29+ import { config } from " ./stackflow.config" ;
30+
31+ const { Stack } = stackflow ({
32+ config ,
33+ components: {
34+ HomeActivity ,
35+ ArticleActivity ,
36+ },
37+ plugins: [
38+ stackPersistencePlugin ({
39+ storage: snapshotStorage ,
40+ strategy: snapshotStrategy ,
41+ }),
42+ ],
43+ });
44+ ```
45+
46+ ## Usage
47+
48+ The storage must provide a synchronous loader and an asynchronous saver. The
49+ strategy validates stored metadata and decides whether its snapshot can be
50+ reused.
2151
2252The following example stores snapshots in ` localStorage ` , rejects records from
2353another application version, and expires records after seven days:
@@ -28,7 +58,6 @@ import type {
2858 StackSnapshotStorage ,
2959 StackSnapshotStrategy ,
3060} from " @stackflow/plugin-stack-persistence" ;
31- import { stackPersistencePlugin } from " @stackflow/plugin-stack-persistence" ;
3261
3362const STORAGE_KEY = " stackflow.snapshot" ;
3463const APP_VERSION = 1 as const ;
@@ -39,7 +68,7 @@ type SnapshotMetadata = {
3968 savedAt: number ;
4069};
4170
42- const storage : StackSnapshotStorage <SnapshotMetadata > = {
71+ export const snapshotStorage : StackSnapshotStorage <SnapshotMetadata > = {
4372 load() {
4473 if (typeof window === " undefined" ) return null ;
4574
@@ -56,7 +85,7 @@ const storage: StackSnapshotStorage<SnapshotMetadata> = {
5685 },
5786};
5887
59- const strategy : StackSnapshotStrategy <SnapshotMetadata > = {
88+ export const snapshotStrategy : StackSnapshotStrategy <SnapshotMetadata > = {
6089 metadata: {
6190 create() {
6291 return {
@@ -92,36 +121,6 @@ const strategy: StackSnapshotStrategy<SnapshotMetadata> = {
92121 return Date .now () - record .metadata .savedAt < MAX_AGE_MS ;
93122 },
94123};
95-
96- export const persistencePlugin = stackPersistencePlugin ({
97- storage ,
98- strategy ,
99- onRecordLoadError(error ) {
100- console .warn (" Could not read the saved Stackflow snapshot" , error );
101- },
102- onRecordSaveError(error ) {
103- console .error (" Could not save the Stackflow snapshot" , error );
104- },
105- });
106- ```
107-
108- Add the plugin to an existing Stackflow configuration:
109-
110- ``` typescript
111- import { stackflow } from " @stackflow/react" ;
112- import { ArticleActivity } from " ./ArticleActivity" ;
113- import { HomeActivity } from " ./HomeActivity" ;
114- import { persistencePlugin } from " ./persistence" ;
115- import { config } from " ./stackflow.config" ;
116-
117- const { Stack } = stackflow ({
118- config ,
119- components: {
120- HomeActivity ,
121- ArticleActivity ,
122- },
123- plugins: [persistencePlugin ],
124- });
125124```
126125
127126## Behavior
@@ -137,30 +136,11 @@ record is present, the plugin:
1371363 . provides the snapshot to Stackflow when the strategy returns ` true ` .
138137
139138Returning ` null ` from ` storage.load() ` , returning ` false ` from ` shouldReuse() ` ,
140- or returning ` { ok: false } ` from ` metadata.parse() ` causes Stackflow to use its
141- normal initial stack. A thrown ` storage.load() ` error has the same fallback and
142- is reported as ` StackSnapshotRecordLoadError ` through ` onRecordLoadError ` .
143- Metadata parse failures are reported as ` StackSnapshotMetadataParseError ` .
144-
145- After the plugin accepts a record, core still validates and replays its
146- snapshot against the current Stackflow configuration. ` onLoadError ` controls
147- what happens when that step fails:
148-
149- ``` typescript
150- stackPersistencePlugin ({
151- storage ,
152- strategy ,
153- onLoadError({ error , initialContext }) {
154- reportSnapshotError (error , initialContext );
155-
156- return { policy: " propagate" };
157- },
158- });
159- ```
160-
161- The default policy is ` { policy: "recover" } ` , which discards the unusable
162- snapshot and creates the normal initial stack. Return ` { policy: "propagate" } `
163- to let the core ` SnapshotLoadError ` abort stack creation.
139+ returning ` { ok: false } ` from ` metadata.parse() ` , or throwing from
140+ ` storage.load() ` causes Stackflow to use its normal initial stack. After the
141+ plugin accepts a record, core still validates and replays its snapshot against
142+ the current Stackflow configuration. An unusable snapshot also falls back to
143+ the normal initial stack by default.
164144
165145` storage.load() ` , metadata parsing, the reuse decision, and snapshot loading
166146are all synchronous. Prepare data before creating the stack when the backing
@@ -178,9 +158,7 @@ callback receives both values.
178158` storage.save() ` runs asynchronously and does not block navigation. The plugin
179159does not wait for an earlier save before starting a later one, so storage backed
180160by asynchronous I/O must prevent an older request from overwriting a newer
181- record. A rejected save is wrapped in ` StackSnapshotRecordSaveError ` and sent
182- to ` onRecordSaveError ` . Without a handler, the wrapped error is rethrown from
183- the promise rejection.
161+ record.
184162
185163The storage owns serialization. Ensure that the selected codec can represent
186164the values carried by your application's snapshot events and metadata.
@@ -203,7 +181,7 @@ The composed strategy stores a versioned metadata envelope. On load, it
203181requires exactly the same strategy keys, parses each strategy's metadata, and
204182reuses the snapshot only when every ` shouldReuse() ` call returns ` true ` .
205183
206- ## Error handling
184+ ### Error handling
207185
208186- ` onRecordLoadError ` receives ` StackSnapshotRecordLoadError ` when
209187 ` storage.load() ` throws and ` StackSnapshotMetadataParseError ` when
@@ -212,25 +190,51 @@ reuses the snapshot only when every `shouldReuse()` call returns `true`.
212190- ` onLoadError ` receives core ` SnapshotLoadError ` values for snapshots that
213191 cannot be loaded with the current configuration. It recovers by default.
214192- ` onRecordSaveError ` receives ` StackSnapshotRecordSaveError ` when the promise
215- returned by ` storage.save() ` rejects.
193+ returned by ` storage.save() ` rejects. Without a handler, the wrapped error is
194+ rethrown from the promise rejection.
216195
217196The error wrappers expose the original value as ` cause ` for record load/save
218197errors and as ` detail ` for metadata parse errors. Exceptions thrown directly by
219198` metadata.parse() ` or ` shouldReuse() ` are outside these recovery callbacks and
220199propagate during stack creation. Return ` { ok: false, detail } ` or ` false ` for
221200expected rejection paths.
222201
223- ## Public API
202+ To abort stack creation instead of recovering from a core snapshot-load error,
203+ return ` { policy: "propagate" } ` :
224204
225- ### ` stackPersistencePlugin(options) `
205+ ``` typescript
206+ stackPersistencePlugin ({
207+ storage: snapshotStorage ,
208+ strategy: snapshotStrategy ,
209+ onLoadError({ error }) {
210+ console .error (" Could not restore the Stackflow snapshot" , error );
226211
227- Creates a Stackflow core plugin. ` options ` contains:
212+ return { policy: " propagate" };
213+ },
214+ });
215+ ```
216+
217+ ## API
228218
229- - ` storage ` — required ` StackSnapshotStorage<Metadata> ` implementation;
230- - ` strategy ` — required ` StackSnapshotStrategy<Metadata> ` implementation;
231- - ` onRecordLoadError ` — optional storage-load and metadata-parse error handler;
232- - ` onRecordSaveError ` — optional save-rejection handler; and
233- - ` onLoadError ` — optional core snapshot-load policy handler.
219+ ### ` stackPersistencePlugin() `
220+
221+ ``` typescript
222+ function stackPersistencePlugin<Metadata >(
223+ options : StackPersistencePluginOptions <Metadata >,
224+ ): StackflowPlugin ;
225+ ```
226+
227+ Creates a Stackflow core plugin.
228+
229+ | Option | Description |
230+ | --- | --- |
231+ | ` storage ` | Required ` StackSnapshotStorage<Metadata> ` implementation. |
232+ | ` strategy ` | Required ` StackSnapshotStrategy<Metadata> ` implementation. |
233+ | ` onRecordLoadError ` | Handles storage-load and metadata-parse errors. |
234+ | ` onRecordSaveError ` | Handles storage-save rejections. |
235+ | ` onLoadError ` | Chooses whether to recover from or propagate a core snapshot-load error. |
236+
237+ The options type is exported as ` StackPersistencePluginOptions ` .
234238
235239Only one Stackflow plugin can provide a non-null snapshot during stack
236240creation. If this plugin accepts a record while another plugin also provides a
@@ -274,9 +278,19 @@ type Result<Value> =
274278 | { ok: false ; detail? : unknown };
275279```
276280
277- ` composeStrategies() ` returns another ` StackSnapshotStrategy ` , so composed
278- strategies can be passed to ` stackPersistencePlugin() ` without special setup.
279- The inferred envelope type is exported as ` StrategiesMetadata ` .
281+ ### ` composeStrategies() `
282+
283+ ``` typescript
284+ function composeStrategies<
285+ const Strategies extends Record <string , StackSnapshotStrategy <any >>,
286+ >(
287+ strategies : Strategies ,
288+ ): StackSnapshotStrategy <StrategiesMetadata <Strategies >>;
289+ ```
290+
291+ Combines keyed strategies into another ` StackSnapshotStrategy ` . The composed
292+ strategy can be passed to ` stackPersistencePlugin() ` without special setup,
293+ and its inferred metadata envelope type is exported as ` StrategiesMetadata ` .
280294
281295### Error classes
282296
0 commit comments