Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.5.0] - 2026-02-26

### Added

- **Plugin system** — extend svstate with reusable behaviors via lifecycle hooks (`onInit`, `onChange`, `onValidation`, `onSnapshot`, `onAction`, `onRollback`, `onReset`, `destroy`)
- **`persistPlugin`** — automatically save and restore state to localStorage (or any custom storage) with throttled writes, schema versioning, migration support, and include/exclude field filtering
- **`autosavePlugin`** — auto-save state after a period of inactivity, on a fixed interval, or when the browser tab is hidden; exposes `saveNow()` and `isSaving()` methods
- **`devtoolsPlugin`** — log all state events (changes, snapshots, actions, rollbacks) to the browser console for easier debugging
- **`historyPlugin`** — sync selected state fields to URL search parameters, keeping the browser history in step with your app state
- **`syncPlugin`** — broadcast state changes across browser tabs in real time using BroadcastChannel
- **`undoRedoPlugin`** — adds redo capability on top of the built-in rollback, with `redo()`, `canRedo()`, and a reactive `redoStack` store
- **`analyticsPlugin`** — buffer and batch state events (changes, actions, snapshots) for sending to analytics services
- `destroy()` return value from `createSvState` — call it to clean up all plugin resources and cancel pending async validations
- New plugin type exports: `SvStatePlugin`, `PluginContext`, `PluginStores`, `ChangeEvent`, `ActionEvent`

## [1.4.1] - 2026-02-13

### Added
Expand Down
65 changes: 62 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,29 @@ npm run all # format → lint → ts:check → build

Note: The demo has its own `node_modules` and uses Zod for some validation examples.

## Documentation Files

- `README.md` - Main documentation: features, API reference, examples, plugin guide
- `FAQ.md` - Frequently asked questions: common patterns, troubleshooting, Zod integration, per-field dirty tracking
- `docs/llms.txt` - LLM-oriented documentation with demo page descriptions and code snippets

## Architecture

### Core Files

- `src/index.ts` - Public exports: `createSvState`, validator builders, types (`Snapshot`, `EffectContext`, `SnapshotFunction`, `SvStateOptions`, `Validator`, `AsyncValidator`, `AsyncValidatorFunction`, `AsyncErrors`, `DirtyFields`)
- `src/state.svelte.ts` - Main `createSvState<T, V, P>()` function with snapshot/undo system and async validation
- `src/index.ts` - Public exports: `createSvState`, validator builders, plugin types and built-in plugins, types (`Snapshot`, `EffectContext`, `SnapshotFunction`, `SvStateOptions`, `Validator`, `AsyncValidator`, `AsyncValidatorFunction`, `AsyncErrors`, `DirtyFields`, `SvStatePlugin`, `PluginContext`, `PluginStores`, `ChangeEvent`, `ActionEvent`)
- `src/state.svelte.ts` - Main `createSvState<T, V, P>()` function with snapshot/undo system, async validation, and plugin integration
- `src/proxy.ts` - `ChangeProxy` deep reactive proxy implementation
- `src/validators.ts` - Fluent validator builders (string, number, array, date)
- `src/plugin.ts` - Plugin type definitions (`SvStatePlugin`, `PluginContext`, `PluginStores`, `ChangeEvent`, `ActionEvent`)
- `src/plugins/` - Built-in plugins: `persistPlugin`, `autosavePlugin`, `devtoolsPlugin`, `historyPlugin`, `syncPlugin`, `undoRedoPlugin`, `analyticsPlugin`

### createSvState Function (src/state.svelte.ts)

The main export creates a validated state object with snapshot/undo support:

```typescript
const { data, execute, state, rollback, rollbackTo, reset } = createSvState(init, actuators?, options?);
const { data, execute, state, rollback, rollbackTo, reset, destroy } = createSvState(init, actuators?, options?);
```

**Returns:**
Expand All @@ -97,6 +105,7 @@ const { data, execute, state, rollback, rollbackTo, reset } = createSvState(init
- `rollback(steps?)` - Undo N steps (default 1), restores state and triggers validation
- `rollbackTo(title)` - Roll back to the last snapshot matching `title`, returns `boolean` (true if found)
- `reset()` - Return to initial snapshot, triggers validation
- `destroy()` - Cleanup function: calls plugin `destroy` hooks in reverse order, cancels async validations
- `state` - Object containing reactive stores:
- `errors: Readable<V | undefined>` - Validation errors (sync)
- `hasErrors: Readable<boolean>` - Whether any sync validation errors exist
Expand Down Expand Up @@ -129,6 +138,7 @@ const { data, execute, state, rollback, rollbackTo, reset } = createSvState(init
- `clearAsyncErrorsOnChange: boolean` (default: `true`) - Clear async error for a path when that property changes
- `maxConcurrentAsyncValidations: number` (default: `4`) - Maximum concurrent async validators running simultaneously
- `maxSnapshots: number` (default: `50`) - Maximum number of snapshots to keep; oldest non-Initial snapshots are trimmed when exceeded. `0` = unlimited.
- `plugins: SvStatePlugin<any>[]` (default: `[]`) - Array of plugins to extend behavior (see Plugin System)

### Snapshot/Undo System

Expand Down Expand Up @@ -181,6 +191,45 @@ type AsyncValidator<T> = {
- Parent triggers child: validator for `"user.email"` triggers when `user` changes
- Child triggers parent: validator for `"user"` triggers when `user.email` changes

### Plugin System (src/plugin.ts, src/plugins/)

Plugins extend `createSvState` via lifecycle hooks. They are registered via `options.plugins` array.

**`SvStatePlugin<T>` interface:**

- `name: string` - Required unique identifier
- `onInit?(context: PluginContext<T>)` - Called once after state is fully initialized
- `onChange?(event: ChangeEvent<T>)` - Called on every property mutation (`{ target, property, currentValue, oldValue }`)
- `onValidation?(errors: Validator | undefined)` - Called after sync validation runs
- `onSnapshot?(snapshot: Snapshot<T>)` - Called when a snapshot is created
- `onAction?(event: ActionEvent)` - Called before (`{ phase: 'before', params }`) and after (`{ phase: 'after', error? }`) action execution
- `onRollback?(snapshot: Snapshot<T>)` - Called after rollback/rollbackTo completes
- `onReset?()` - Called after reset completes
- `destroy?()` - Called on `destroy()`, in **reverse** plugin array order

**`PluginContext<T>` (received by `onInit`):**

- `data: T` - The live reactive proxy
- `state: PluginStores<T>` - All readable stores (errors, isDirty, snapshots, etc.)
- `options: Readonly<SvStateOptions>` - Resolved options
- `snapshot: SnapshotFunction` - Create snapshots programmatically

**Hook execution:** Hooks are called in plugin array order (first to last), except `destroy` which runs last-to-first. All hooks are optional.

**Internal implementation:** `createSvState` uses a `callPlugins(hook, ...args)` helper that iterates plugins and calls matching hook functions.

**Built-in plugins (src/plugins/):**

| Plugin | File | Purpose | Key options |
| ----------------- | -------------- | -------------------------------------------- | ------------------------------------------------------------------------ |
| `persistPlugin` | `persist.ts` | Persist state to localStorage/custom storage | `key`, `storage`, `throttle`, `version`, `migrate`, `include`, `exclude` |
| `autosavePlugin` | `autosave.ts` | Auto-save after idle/interval | `save` (required), `idle`, `interval`, `saveOnDestroy`, `onlyWhenDirty` |
| `devtoolsPlugin` | `devtools.ts` | Console logging of all events | `name`, `collapsed`, `logValidation`, `enabled` |
| `historyPlugin` | `history.ts` | Sync state fields to URL params | `fields` (required), `mode`, `serialize`, `deserialize` |
| `syncPlugin` | `sync.ts` | Cross-tab sync via BroadcastChannel | `key` (required), `throttle`, `merge` |
| `undoRedoPlugin` | `undo-redo.ts` | Redo stack on top of built-in rollback | No required options; exposes `redo()`, `canRedo()`, `redoStack` |
| `analyticsPlugin` | `analytics.ts` | Batch event buffering for analytics | `onFlush` (required), `batchSize`, `flushInterval`, `include` |

### Deep Clone System (src/state.svelte.ts)

The `deepClone` function preserves object prototypes using `Object.create(Object.getPrototypeOf(object))`. This allows state objects to include methods that operate on `this`:
Expand Down Expand Up @@ -271,6 +320,16 @@ Test files go in `test/` directory:
- `*.test.ts` - Pure TypeScript tests (validators, proxy)
- `*.test.svelte.ts` - Tests using Svelte 5 runes (`$state`, `$derived`, etc.)

Current test files:

- `validators.test.ts` - Fluent validator builder tests (~320 cases)
- `proxy.test.ts` - ChangeProxy deep proxy tests
- `state.test.svelte.ts` - Core createSvState tests (~90 cases)
- `async-validation.test.svelte.ts` - Async validator tests
- `performance.test.svelte.ts` - Performance/stress tests
- `plugins.test.svelte.ts` - Plugin system integration tests
- `plugins-analytics.test.svelte.ts`, `plugins-autosave.test.svelte.ts`, `plugins-devtools.test.svelte.ts`, `plugins-history.test.svelte.ts`, `plugins-persist.test.svelte.ts`, `plugins-sync.test.svelte.ts`, `plugins-undo-redo.test.svelte.ts` - Per-plugin tests

Vitest is configured with:

- Globals enabled (no imports needed for `describe`, `it`, `expect`)
Expand Down
Loading