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
6 changes: 3 additions & 3 deletions .github/workflows/ci-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.x, 22.x, 20.x]
node-version: [26.x, 24.x, 22.x]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/ci-full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.x, 22.x, 20.x]
node-version: [26.x, 24.x, 22.x]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
Expand Down Expand Up @@ -48,9 +48,9 @@ jobs:
version: ${{ steps.create_tag.outputs.version }}

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: jaywcjlove/create-tag-action@main
- uses: jaywcjlove/create-tag-action@v2.3.0
id: create_tag
with:
package-path: ./package.json
Expand All @@ -62,8 +62,8 @@ jobs:
if: needs.tag.outputs.status == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ 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.6] - 2026-07-31

### Changed

- **Updated all development dependencies** — ESLint, eslint-plugin-unicorn, Vite, and related tooling bumped to their latest versions
- **Renamed parameters for clarity** — `snapshot(title, replace)` is now `snapshot(title, shouldReplace)` and `requiredIf(cond)` is now `requiredIf(shouldRequire)`; both are positional parameters, so no code changes are required

### Fixed

- **`.integer()` validator now rejects unsafe integers** — `numberValidator().integer()` uses `Number.isSafeInteger()` instead of `Number.isInteger()`, so values outside the safe integer range are correctly flagged instead of silently passing
- **Demo build no longer warns about deprecated Vite config** — the demo's `vite.config.ts` now uses `import.meta.dirname` and a JSON import attribute instead of `__dirname` and a bare JSON import

## [1.5.5] - 2026-06-02

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ effect: ({ snapshot, property }) => {

**Important:** The effect callback must be synchronous. Returning a Promise throws an error.

- `snapshot(title, replace = true)` - Creates a snapshot; if `replace=true` and last snapshot has same title, replaces it (debouncing)
- `snapshot(title, shouldReplace = true)` - Creates a snapshot; if `shouldReplace=true` and last snapshot has same title, replaces it (debouncing)
- Initial state is saved as first snapshot with title `"Initial"`
- Successful action execution resets snapshots with current state as new initial
- `rollback()`, `rollbackTo()`, and `reset()` trigger validation after restoring state
Expand Down
8 changes: 4 additions & 4 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,15 +457,15 @@ reset(); // Returns to original state

---

### What does the `replace` parameter in `snapshot(title, replace)` do?
### What does the `shouldReplace` parameter in `snapshot(title, shouldReplace)` do?

When `replace` is `true` (default), consecutive snapshots with the same title replace each other instead of stacking. This prevents snapshot bloat during rapid typing:
When `shouldReplace` is `true` (default), consecutive snapshots with the same title replace each other instead of stacking. This prevents snapshot bloat during rapid typing:

```typescript
effect: ({ snapshot }) => {
// User types "Hello" quickly
// Without replace: 5 snapshots ("H", "He", "Hel", "Hell", "Hello")
// With replace: 1 snapshot ("Hello")
// Without replacing: 5 snapshots ("H", "He", "Hel", "Hell", "Hello")
// With replacing: 1 snapshot ("Hello")
snapshot('Typing in name field'); // Same title = replaces previous
};

Expand Down
51 changes: 25 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-green.svg)](https://nodejs.org/)
[![Svelte 5](https://img.shields.io/badge/Svelte-5-orange.svg)](https://svelte.dev/)
[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
[![Tests](https://img.shields.io/badge/tests-500%2B-brightgreen.svg)]()
[![Coverage](https://img.shields.io/badge/coverage-%3E98%25-brightgreen.svg)]()
[![Tests](https://img.shields.io/badge/tests-500%2B-brightgreen.svg)](<>)
[![Coverage](https://img.shields.io/badge/coverage-%3E98%25-brightgreen.svg)](<>)

> **Deep reactive proxy with validation, snapshot/undo, side effects, and plugins — built for complex, real-world applications.**

Expand Down Expand Up @@ -82,9 +82,7 @@ const customer = $state({
import { createSvState, stringValidator, numberValidator } from 'svstate';

const { data, state, rollback, rollbackTo, reset, execute } = createSvState(customer, {
validator: (source) => ({
/* validation that mirrors your structure */
}),
validator: (source) => ({/* validation that mirrors your structure */}),
effect: ({ snapshot, property, currentValue, oldValue }) => {
console.log(`${property} changed from ${oldValue} to ${currentValue}`);
snapshot(`Changed ${property}`); // Create undo point
Expand Down Expand Up @@ -367,7 +365,7 @@ $snapshots.forEach((s, i) => console.log(`${i}: ${s.title}`));

**Key features:**

- 📸 `snapshot(title, replace?)` — create undo points
- 📸 `snapshot(title, shouldReplace?)` — create undo points
- ⏪ `rollback(steps)` — undo N changes
- 🏷️ `rollbackTo(title)` — jump to a named snapshot
- 🔄 `reset()` — return to initial state
Expand Down Expand Up @@ -1033,25 +1031,26 @@ function removeTag(index: number) {
Creates a supercharged state object.

**Returns:**
| Property | Type | Description |
|----------|------|-------------|
| `data` | `T` | Deep reactive proxy — bind directly, methods preserved |
| `execute(params?)` | `(P?) => Promise<void>` | Run the configured action |
| `rollback(steps?)` | `(n?: number) => void` | Undo N changes (default: 1) |
| `rollbackTo(title)` | `(title: string) => boolean` | Roll back to last snapshot with matching title |
| `reset()` | `() => void` | Return to initial state |
| `destroy()` | `() => void` | Cleanup plugins and cancel async validations |
| `state.errors` | `Readable<V>` | Sync validation errors store |
| `state.hasErrors` | `Readable<boolean>` | Has sync errors? |
| `state.isDirty` | `Readable<boolean>` | Has state changed? (derived from `isDirtyByField`) |
| `state.isDirtyByField` | `Readable<DirtyFields>` | Per-field dirty tracking (dot-notation paths) |
| `state.actionInProgress` | `Readable<boolean>` | Is action running? |
| `state.actionError` | `Readable<Error>` | Last action error |
| `state.snapshots` | `Readable<Snapshot[]>` | Undo history |
| `state.asyncErrors` | `Readable<AsyncErrors>` | Async validation errors (keyed by path) |
| `state.hasAsyncErrors` | `Readable<boolean>` | Has async errors? |
| `state.asyncValidating` | `Readable<string[]>` | Paths currently validating |
| `state.hasCombinedErrors` | `Readable<boolean>` | Has sync OR async errors? |

| Property | Type | Description |
| ------------------------- | ---------------------------- | ------------------------------------------------------ |
| `data` | `T` | Deep reactive proxy — bind directly, methods preserved |
| `execute(params?)` | `(P?) => Promise<void>` | Run the configured action |
| `rollback(steps?)` | `(n?: number) => void` | Undo N changes (default: 1) |
| `rollbackTo(title)` | `(title: string) => boolean` | Roll back to last snapshot with matching title |
| `reset()` | `() => void` | Return to initial state |
| `destroy()` | `() => void` | Cleanup plugins and cancel async validations |
| `state.errors` | `Readable<V>` | Sync validation errors store |
| `state.hasErrors` | `Readable<boolean>` | Has sync errors? |
| `state.isDirty` | `Readable<boolean>` | Has state changed? (derived from `isDirtyByField`) |
| `state.isDirtyByField` | `Readable<DirtyFields>` | Per-field dirty tracking (dot-notation paths) |
| `state.actionInProgress` | `Readable<boolean>` | Is action running? |
| `state.actionError` | `Readable<Error>` | Last action error |
| `state.snapshots` | `Readable<Snapshot[]>` | Undo history |
| `state.asyncErrors` | `Readable<AsyncErrors>` | Async validation errors (keyed by path) |
| `state.hasAsyncErrors` | `Readable<boolean>` | Has async errors? |
| `state.asyncValidating` | `Readable<string[]>` | Paths currently validating |
| `state.hasCombinedErrors` | `Readable<boolean>` | Has sync OR async errors? |

### Built-in Validators

Expand Down Expand Up @@ -1093,7 +1092,7 @@ import type {
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `Validator` | Nested object type for validation errors — leaf values are error strings (empty = valid) |
| `EffectContext<T>` | Context object passed to effect callbacks: `{ snapshot, target, property, currentValue, oldValue }` |
| `SnapshotFunction` | Type for the `snapshot(title, replace?)` function used in effects |
| `SnapshotFunction` | Type for the `snapshot(title, shouldReplace?)` function used in effects |
| `Snapshot<T>` | Shape of a snapshot entry: `{ title: string; data: T }` |
| `SvStateOptions` | Configuration options type for `createSvState` |
| `AsyncValidator<T>` | Object mapping property paths to async validator functions |
Expand Down
16 changes: 15 additions & 1 deletion demo/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default [
},
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'),
...svelte.configs.recommended,
unicorn.configs.all,
unicorn.configs.recommended,
{
plugins: {
'@typescript-eslint': typescriptEslint,
Expand Down Expand Up @@ -66,6 +66,20 @@ export default [
'svelte/prefer-const': 'error',
'unicorn/filename-case': 'off',
'unicorn/prefer-global-this': 'off',
// Svelte store auto-subscriptions (e.g. `$errors`) aren't declared JS variables,
// so this rule can't see them and flags every optional-chain access as unbound.
'unicorn/no-optional-chaining-on-undeclared-variable': 'off',
// Reassigning top-level `$state`/`let` bindings from event handlers and effects
// is the standard Svelte 5 component pattern, not the module-level mutation
// anti-pattern this rule targets.
'unicorn/no-top-level-assignment-in-function': 'off',
// The demo pages intentionally show plain object literals with methods that use
// `this` (see CalculatedClass.svelte) to demonstrate svstate's prototype-preserving
// deep clone — a valid, common JS pattern this rule doesn't allow.
'unicorn/no-this-outside-of-class': 'off',
// Several pages embed Svelte template snippets as documentation strings (e.g.
// `formSourceCode`), whose `{expr}` markup this rule misreads as a forgotten `$`.
'unicorn/no-incorrect-template-string-interpolation': 'off',
'no-alert': 'error',
'no-console': 'error',
'no-debugger': 'error'
Expand Down
Loading