@daltonr/pathwrite-store-http
v0.8.0
Published
Persistence adapters for PathEngine — LocalStorageStore (browser) and HttpStore (REST API)
Downloads
749
Maintainers
Readme
@daltonr/pathwrite-store-http
Persistence adapters for PathEngine. Supports both browser-local storage (LocalStorageStore) and REST API storage (HttpStore). Both implement the same PathStore interface, so they're interchangeable — switch backends without changing your code.
Installation
npm install @daltonr/pathwrite-core @daltonr/pathwrite-store-http
# Plus your framework adapter:
npm install @daltonr/pathwrite-vue # or react / angular / svelteStorage Adapters
| Adapter | Storage location | Best for |
|---------|------------------|----------|
| LocalStorageStore | Browser localStorage (or in-memory fallback) | Quick prototyping, single-device sessions, offline-capable wizards |
| HttpStore | REST API server | Multi-device sessions, team collaboration, persistent backend storage |
Both adapters implement the PathStore interface (save, load, delete), so you can swap between them without rewriting your wizard logic.
Exports
import {
// Storage adapters
LocalStorageStore, // Browser-local storage (localStorage or in-memory fallback)
HttpStore, // REST API storage
// Observer factory
httpPersistence, // Returns a PathObserver that auto-saves state
// Load/restore orchestration
restoreOrStart, // Handles the load/restore-or-start pattern
// Types
LocalStorageStoreOptions,
StorageAdapter, // Interface for custom storage backends
HttpStoreOptions,
HttpPersistenceOptions,
RestoreOrStartOptions,
ObserverStrategy,
PathStore, // Interface that both adapters implement
// Re-exported from core
PathData, PathDefinition, PathEvent, PathObserver,
PathEngineOptions, PathSnapshot, PathStep,
PathStepContext, SerializedPathState,
matchesStrategy,
} from "@daltonr/pathwrite-store-http";Quick Start: LocalStorageStore
Browser-local persistence — perfect for prototyping and single-device wizards:
import { LocalStorageStore, httpPersistence, restoreOrStart } from "@daltonr/pathwrite-store-http";
const store = new LocalStorageStore({ prefix: "myapp:" });
const key = `user:${userId}:onboarding`;
const { engine, restored } = await restoreOrStart({
store,
key,
path: onboardingWizard,
initialData: { name: "", email: "" },
observers: [
httpPersistence({ store, key, strategy: "onNext" }),
],
});
// engine is a plain PathEngine — pass it to any adapter
const { snapshot, next } = usePath({ engine });
if (restored) {
console.log("Welcome back! Resuming from step", engine.snapshot()?.stepId);
}LocalStorageStore Options
const store = new LocalStorageStore({
// Optional: prefix for all keys (default: "@daltonr/pathwrite:")
prefix: "myapp:",
// Optional: custom storage backend
// - undefined (default): uses global localStorage, falls back to in-memory if unavailable
// - sessionStorage: uses sessionStorage instead of localStorage
// - null: forces in-memory fallback (useful for SSR/testing)
// - custom StorageAdapter: any object with getItem/setItem/removeItem/getAllKeys
storage: sessionStorage,
});LocalStorageStore Methods
| Method | Description |
|--------|-------------|
| save(key, state) | Save a snapshot to storage |
| load(key) | Load a snapshot (returns null if not found) |
| delete(key) | Delete a snapshot |
| list() | Return all keys saved under this store's prefix |
| clear() | Delete all snapshots under this store's prefix |
Example: Session picker
const store = new LocalStorageStore({ prefix: "wizard:" });
// List all saved sessions
const sessionKeys = await store.list();
console.log(sessionKeys); // → ["user:1:session", "user:2:session"]
// Load a specific session
const { engine, restored } = await restoreOrStart({
store,
key: sessionKeys[0],
path: myPath,
observers: [httpPersistence({ store, key: sessionKeys[0] })],
});
// Clear all sessions (e.g., on logout)
await store.clear();Quick Start: HttpStore
REST API persistence — for multi-device sessions and server-backed storage:
import { HttpStore, httpPersistence, restoreOrStart } from "@daltonr/pathwrite-store-http";
const store = new HttpStore({ baseUrl: "/api/wizard" });
const key = `user:${userId}:onboarding`;
const { engine, restored } = await restoreOrStart({
store,
key,
path: onboardingWizard,
initialData: { name: "", email: "" },
observers: [httpPersistence({ store, key })],
});The one-call approach — restoreOrStart
For most use cases, one async call is all you need. Works with both LocalStorageStore and HttpStore:
import { LocalStorageStore, httpPersistence, restoreOrStart } from "@daltonr/pathwrite-store-http";
const store = new LocalStorageStore({ prefix: "myapp:" });
// Or: const store = new HttpStore({ baseUrl: "/api/wizard" });
const key = `user:${userId}:onboarding`;
const { engine, restored } = await restoreOrStart({
store,
key,
path: onboardingWizard,
initialData: { name: "", email: "" },
observers: [
httpPersistence({ store, key, strategy: "onNext" }),
],
});
// engine is a plain PathEngine — pass it to any adapter
const { snapshot, next } = usePath({ engine });
if (restored) {
console.log("Welcome back! Resuming from step", engine.snapshot()?.stepId);
}restoreOrStart returns { engine, restored }:
engine— aPathEnginepre-wired with persistence, ready to pass tousePath({ engine })or<PathShell :engine="engine">restored: boolean—trueif state was loaded from storage,falseif it started fresh
Vue example
<script setup lang="ts">
import { PathShell } from "@daltonr/pathwrite-vue";
import { LocalStorageStore, httpPersistence, restoreOrStart } from "@daltonr/pathwrite-store-http";
const store = new LocalStorageStore({ prefix: "myapp:" });
const key = `user:${userId}:onboarding`;
const { engine } = await restoreOrStart({
store,
key,
path: onboardingWizard,
initialData: { name: "", email: "" },
observers: [httpPersistence({ store, key })],
});
</script>
<template>
<PathShell :path="onboardingWizard" :engine="engine" />
</template>React example
import { LocalStorageStore, httpPersistence, restoreOrStart } from "@daltonr/pathwrite-store-http";
const store = new LocalStorageStore({ prefix: "myapp:" });
const key = `user:${userId}:onboarding`;
// In a route loader or equivalent — runs before the component mounts
export async function loader() {
return restoreOrStart({
store,
key,
path: onboardingWizard,
initialData: { name: "", email: "" },
observers: [httpPersistence({ store, key })],
});
}
function WizardPage({ engine }: { engine: PathEngine }) {
// engine is ready — no loading state needed
return <PathShell engine={engine} path={onboardingWizard} steps={{ ... }} />;
}The observer approach — httpPersistence
httpPersistence() returns a PathObserver — a plain function (event, engine) => void. Pass it to PathEngine via the observers option:
import { PathEngine } from "@daltonr/pathwrite-core";
import { HttpStore, httpPersistence } from "@daltonr/pathwrite-store-http";
const store = new HttpStore({ baseUrl: "/api/wizard" });
const engine = new PathEngine({
observers: [
httpPersistence({ store, key: "user:123:onboarding", strategy: "onNext" }),
// add as many observers as you like — logger, analytics, SQL capture, etc.
],
});
await engine.start(myPath, initialData);For restoration, pass observers through fromState():
import { PathEngine } from "@daltonr/pathwrite-core";
const saved = await store.load("user:123:onboarding");
const engine = saved
? PathEngine.fromState(saved, pathDefs, { observers: [httpPersistence({ store, key: "user:123:onboarding" })] })
: new PathEngine({ observers: [httpPersistence({ store, key: "user:123:onboarding" })] });
if (!saved) await engine.start(myPath, initialData);This is exactly what createPersistedEngine does internally — use it directly if you need more control over the load step or want to add other observers.
Persistence strategies
| Strategy | Saves when | Best for |
|---|---|---|
| "onNext" (default) | next() completes navigation to a new step | Text-heavy forms — 1 save per step |
| "onEveryChange" | Any stateChanged event (data changes + navigation) | Checkbox/dropdown wizards, crash protection |
| "onSubPathComplete" | A sub-path finishes and the parent resumes | Wizards with nested sub-flows |
| "onComplete" | The entire path completes | Audit trail / record-keeping only |
| "manual" | Never automatically | Full control — call store.save(key, engine.exportState()!) yourself |
"onEveryChange"+ text inputs? AdddebounceMs: 500to avoid saving on every keystroke.
httpPersistence({
store,
key: "user:123:onboarding",
strategy: "onEveryChange",
debounceMs: 500, // collapse rapid keystrokes into one save
})HttpStore
HttpStore is a thin REST transport. It has three methods:
| Method | HTTP verb | Default URL |
|---|---|---|
| store.save(key, state) | PUT | ${baseUrl}/state/${encodeURIComponent(key)} |
| store.load(key) | GET | ${baseUrl}/state/${encodeURIComponent(key)} |
| store.delete(key) | DELETE | ${baseUrl}/state/${encodeURIComponent(key)} |
Returns null from load() when the server responds with 404.
Options
const store = new HttpStore({
baseUrl: "/api/wizard",
// Optional: custom URL builders
saveUrl: (key) => `/v2/paths/${key}`,
loadUrl: (key) => `/v2/paths/${key}`,
deleteUrl: (key) => `/v2/paths/${key}`,
// Optional: auth headers (static object or async function)
headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),
// Optional: custom fetch (for testing, SSR, etc.)
fetch: myCustomFetch,
// Optional: transport-level error callback
onError: (err, operation, key) => console.error(`${operation} failed for ${key}:`, err),
});Multiple observers
Observers compose freely. Each one is a plain function, independent of all others:
import { PathEngine } from "@daltonr/pathwrite-core";
import { HttpStore, httpPersistence } from "@daltonr/pathwrite-store-http";
const store = new HttpStore({ baseUrl: "/api/wizard" });
const logger: PathObserver = (event) =>
console.log(`[wizard] ${event.type}`, 'cause' in event ? event.cause : '');
const analytics: PathObserver = (event) => {
if (event.type === "stateChanged" && !event.snapshot.isNavigating) {
trackEvent("wizard_step", { stepId: event.snapshot.stepId });
}
};
const engine = new PathEngine({
observers: [
httpPersistence({ store, key: "user:123:onboarding" }),
logger,
analytics,
],
});API server contract
Your API server must handle these three endpoints for the default URL scheme:
| Endpoint | Method | Body | Success response |
|---|---|---|---|
| /state/:key | PUT | JSON SerializedPathState | 200 OK |
| /state/:key | GET | — | 200 OK + JSON body, or 404 if not found |
| /state/:key | DELETE | — | 200 OK or 404 |
The state is automatically deleted from the server when the path completes (except with the "onComplete" strategy, which saves a final record instead).
Callbacks
httpPersistence({
store,
key: "user:123:onboarding",
onSaveSuccess: () => console.log("Saved ✓"),
onSaveError: (err) => toast.error(`Save failed: ${err.message}`),
});createPersistedEngine accepts the same onSaveSuccess / onSaveError options and passes them through to the persistence observer.
© 2026 Devjoy Ltd. MIT License.
