Skip to content

Commit 390e757

Browse files
docs(plugin-stack-persistence): align README structure (FEP-2672)
1 parent 37053dd commit 390e757

1 file changed

Lines changed: 96 additions & 82 deletions

File tree

‎extensions/plugin-stack-persistence/README.md‎

Lines changed: 96 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,53 @@
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
1215
yarn 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

2252
The following example stores snapshots in `localStorage`, rejects records from
2353
another 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

3362
const STORAGE_KEY = "stackflow.snapshot";
3463
const 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:
137136
3. provides the snapshot to Stackflow when the strategy returns `true`.
138137

139138
Returning `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
166146
are 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
179159
does not wait for an earlier save before starting a later one, so storage backed
180160
by 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

185163
The storage owns serialization. Ensure that the selected codec can represent
186164
the 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
203181
requires exactly the same strategy keys, parses each strategy's metadata, and
204182
reuses 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

217196
The error wrappers expose the original value as `cause` for record load/save
218197
errors and as `detail` for metadata parse errors. Exceptions thrown directly by
219198
`metadata.parse()` or `shouldReuse()` are outside these recovery callbacks and
220199
propagate during stack creation. Return `{ ok: false, detail }` or `false` for
221200
expected 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

235239
Only one Stackflow plugin can provide a non-null snapshot during stack
236240
creation. 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

Comments
 (0)