diff --git a/demo/public/llms.txt b/demo/public/llms.txt
index 1a196c4..8fa4da1 100644
--- a/demo/public/llms.txt
+++ b/demo/public/llms.txt
@@ -2,6 +2,8 @@
Interactive demo app showcasing svstate features - a Svelte 5 library for deep reactive state with validation, snapshot/undo, and side effects.
+Every page's "Fill with Valid Data" button uses `batch()` to set all its fields as one unit — one validation pass and, on pages with an effect-driven `snapshot()` call (SnapshotDemo, PluginUndoRedo, PluginDevtools, PluginAutosaveAnalytics), a single undo point for the whole fill instead of one per field.
+
## Demo Pages
### BasicValidation
@@ -196,9 +198,9 @@ const {
### SnapshotDemo
-Shows snapshot creation for undo functionality with rollback() support.
+Shows snapshot creation for undo functionality with rollback() support, and batch() for grouping several field changes into one undo point.
-**APIs:** createSvState(), snapshot(), rollback(), reset()
+**APIs:** createSvState(), snapshot(), rollback(), rollbackTo(), reset(), batch()
**Stores:** errors, hasErrors, isDirty, snapshots
```typescript
@@ -212,21 +214,38 @@ const sourceData = {
const {
data,
+ batch,
reset,
rollback,
+ rollbackTo,
state: { errors, hasErrors, isDirty, snapshots }
-} = createSvState(sourceData, {
- validator: (source) => ({
- firstName: stringValidator(source.firstName).prepare('trim').required().minLength(2).maxLength(30).getError(),
- lastName: stringValidator(source.lastName).prepare('trim').required().minLength(2).maxLength(30).getError(),
- email: stringValidator(source.email).prepare('trim').required().email().getError(),
- phone: stringValidator(source.phone).prepare('trim').required().minLength(10).getError(),
- bio: stringValidator(source.bio).maxLength(200).getError()
- }),
- effect: ({ snapshot, property }) => {
- snapshot(`Changed ${property}`);
- }
-});
+} = createSvState(
+ sourceData,
+ {
+ validator: (source) => ({
+ firstName: stringValidator(source.firstName).prepare('trim').required().minLength(2).maxLength(30).getError(),
+ lastName: stringValidator(source.lastName).prepare('trim').required().minLength(2).maxLength(30).getError(),
+ email: stringValidator(source.email).prepare('trim').required().email().getError(),
+ phone: stringValidator(source.phone).prepare('trim').required().minLength(10).getError(),
+ bio: stringValidator(source.bio).maxLength(200).getError()
+ }),
+ effect: ({ snapshot, property }) => {
+ snapshot(`Changed ${property}`);
+ }
+ },
+ { maxSnapshots: 5 }
+);
+
+// batch() applies several field changes as one unit: the effect still fires
+// per field, but the snapshot() calls it makes collapse into ONE snapshot,
+// titled after the first field touched — one undo point for the whole fill.
+const fillWithValidData = () => {
+ batch((draft) => {
+ draft.firstName = 'John';
+ draft.lastName = 'Doe';
+ draft.email = 'john.doe@example.com';
+ });
+};
```
```svelte
@@ -238,6 +257,9 @@ const {
+
+
+
@@ -344,13 +366,13 @@ const {
### OptionsDemo
-Interactive playground for configuring createSvState options like debouncing and error persistence.
+Interactive playground for configuring createSvState options like debouncing and error persistence, plus a validate() control that shows sync validation bypassing the debounce.
-**APIs:** createSvState() with options parameter
+**APIs:** createSvState() with options parameter, validate()
**Options:** resetDirtyOnAction, debounceValidation, persistActionError
```typescript
-const { data, execute, state } = createSvState(
+const { data, execute, validate, state } = createSvState(
sourceData,
{
validator: (source) => ({ /* ... */ }),
@@ -382,6 +404,12 @@ const { data, execute, state } = createSvState(
// persistActionError behavior
// With false (default): actionError clears when data changes
// With true: actionError remains until next execute() call
+
+// validate() bypasses the debounce entirely — useful right before submit
+// even when debounceValidation is set to something long like 5000
+const handleValidateNow = () => {
+ const { errors, hasErrors } = validate();
+};
```
---
@@ -535,18 +563,23 @@ const { data, state: { errors, hasErrors, isDirty } } = createSvState(createSour
### PluginDevtools
-Shows devtoolsPlugin console logging and a custom log-mirror plugin that captures all lifecycle events in-page.
+Shows devtoolsPlugin console logging, a custom log-mirror plugin that captures all lifecycle events in-page, and onPluginError demonstrating that a throwing plugin hook is isolated (caught, doesn't abort the mutation or block other plugins).
-**APIs:** createSvState(), devtoolsPlugin(), custom SvStatePlugin
+**APIs:** createSvState(), devtoolsPlugin(), custom SvStatePlugin, onPluginError option
**Stores:** errors, hasErrors, isDirty, snapshots, actionInProgress
```typescript
import { createSvState, devtoolsPlugin, type SvStatePlugin, type ChangeEvent } from 'svstate';
+let simulatePluginError = false;
+
// Custom plugin that mirrors events to an in-page log
const logMirrorPlugin: SvStatePlugin = {
name: 'log-mirror',
onChange(event: ChangeEvent) {
+ // A "Simulate plugin error" checkbox makes this hook throw on demand,
+ // to demonstrate plugin isolation without breaking the rest of the demo
+ if (simulatePluginError) throw new Error(`Simulated failure while mirroring "${event.property}"`);
addLog('change', `${event.property}: "${event.oldValue}" → "${event.currentValue}"`);
},
onValidation(errors) {
@@ -579,7 +612,11 @@ const { data, execute, reset, rollback, state } = createSvState(
logValidation: true // Also log validation events
}),
logMirrorPlugin
- ]
+ ],
+ // A throwing plugin hook doesn't abort the mutation — it's caught here
+ onPluginError: (error, pluginName, hook) => {
+ lastPluginError = `[${pluginName}/${hook}] ${error instanceof Error ? error.message : String(error)}`;
+ }
}
);
```
@@ -588,7 +625,7 @@ const { data, execute, reset, rollback, state } = createSvState(
### PluginPersistSync
-Settings form with persistPlugin (localStorage) and syncPlugin (cross-tab sync via BroadcastChannel).
+Settings form with persistPlugin (localStorage) and syncPlugin (cross-tab sync via BroadcastChannel). Restored state becomes the new baseline — isDirty is false and reset() returns to the restored values, not the pre-restore defaults.
**APIs:** createSvState(), persistPlugin(), syncPlugin()
**Stores:** errors, hasErrors, isDirty
@@ -599,7 +636,11 @@ import { createSvState, persistPlugin, syncPlugin } from 'svstate';
const persist = persistPlugin({
key: 'svstate-demo-settings',
throttle: 300,
- exclude: ['notifications'] // Don't persist this field
+ exclude: ['notifications'], // Don't persist this field
+ onError: (error) => {
+ // Storage write failures (e.g. QuotaExceededError), surfaced in the sidebar
+ lastPersistError = error instanceof Error ? error.message : String(error);
+ }
});
const sync = syncPlugin({
@@ -688,7 +729,11 @@ const analytics = analyticsPlugin({
},
batchSize: 10, // Flush after 10 events
flushInterval: 10000, // Or every 10 seconds
- include: ['change', 'action', 'snapshot'] // Filter event types
+ include: ['change', 'action', 'snapshot'], // Filter event types
+ onError: (error) => {
+ // onFlush threw or rejected, surfaced in the sidebar
+ lastAnalyticsError = error instanceof Error ? error.message : String(error);
+ }
});
const { data, execute, state } = createSvState(
diff --git a/docs/llms.txt b/docs/llms.txt
index 1a196c4..8fa4da1 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -2,6 +2,8 @@
Interactive demo app showcasing svstate features - a Svelte 5 library for deep reactive state with validation, snapshot/undo, and side effects.
+Every page's "Fill with Valid Data" button uses `batch()` to set all its fields as one unit — one validation pass and, on pages with an effect-driven `snapshot()` call (SnapshotDemo, PluginUndoRedo, PluginDevtools, PluginAutosaveAnalytics), a single undo point for the whole fill instead of one per field.
+
## Demo Pages
### BasicValidation
@@ -196,9 +198,9 @@ const {
### SnapshotDemo
-Shows snapshot creation for undo functionality with rollback() support.
+Shows snapshot creation for undo functionality with rollback() support, and batch() for grouping several field changes into one undo point.
-**APIs:** createSvState(), snapshot(), rollback(), reset()
+**APIs:** createSvState(), snapshot(), rollback(), rollbackTo(), reset(), batch()
**Stores:** errors, hasErrors, isDirty, snapshots
```typescript
@@ -212,21 +214,38 @@ const sourceData = {
const {
data,
+ batch,
reset,
rollback,
+ rollbackTo,
state: { errors, hasErrors, isDirty, snapshots }
-} = createSvState(sourceData, {
- validator: (source) => ({
- firstName: stringValidator(source.firstName).prepare('trim').required().minLength(2).maxLength(30).getError(),
- lastName: stringValidator(source.lastName).prepare('trim').required().minLength(2).maxLength(30).getError(),
- email: stringValidator(source.email).prepare('trim').required().email().getError(),
- phone: stringValidator(source.phone).prepare('trim').required().minLength(10).getError(),
- bio: stringValidator(source.bio).maxLength(200).getError()
- }),
- effect: ({ snapshot, property }) => {
- snapshot(`Changed ${property}`);
- }
-});
+} = createSvState(
+ sourceData,
+ {
+ validator: (source) => ({
+ firstName: stringValidator(source.firstName).prepare('trim').required().minLength(2).maxLength(30).getError(),
+ lastName: stringValidator(source.lastName).prepare('trim').required().minLength(2).maxLength(30).getError(),
+ email: stringValidator(source.email).prepare('trim').required().email().getError(),
+ phone: stringValidator(source.phone).prepare('trim').required().minLength(10).getError(),
+ bio: stringValidator(source.bio).maxLength(200).getError()
+ }),
+ effect: ({ snapshot, property }) => {
+ snapshot(`Changed ${property}`);
+ }
+ },
+ { maxSnapshots: 5 }
+);
+
+// batch() applies several field changes as one unit: the effect still fires
+// per field, but the snapshot() calls it makes collapse into ONE snapshot,
+// titled after the first field touched — one undo point for the whole fill.
+const fillWithValidData = () => {
+ batch((draft) => {
+ draft.firstName = 'John';
+ draft.lastName = 'Doe';
+ draft.email = 'john.doe@example.com';
+ });
+};
```
```svelte
@@ -238,6 +257,9 @@ const {
+
+
+
@@ -344,13 +366,13 @@ const {
### OptionsDemo
-Interactive playground for configuring createSvState options like debouncing and error persistence.
+Interactive playground for configuring createSvState options like debouncing and error persistence, plus a validate() control that shows sync validation bypassing the debounce.
-**APIs:** createSvState() with options parameter
+**APIs:** createSvState() with options parameter, validate()
**Options:** resetDirtyOnAction, debounceValidation, persistActionError
```typescript
-const { data, execute, state } = createSvState(
+const { data, execute, validate, state } = createSvState(
sourceData,
{
validator: (source) => ({ /* ... */ }),
@@ -382,6 +404,12 @@ const { data, execute, state } = createSvState(
// persistActionError behavior
// With false (default): actionError clears when data changes
// With true: actionError remains until next execute() call
+
+// validate() bypasses the debounce entirely — useful right before submit
+// even when debounceValidation is set to something long like 5000
+const handleValidateNow = () => {
+ const { errors, hasErrors } = validate();
+};
```
---
@@ -535,18 +563,23 @@ const { data, state: { errors, hasErrors, isDirty } } = createSvState(createSour
### PluginDevtools
-Shows devtoolsPlugin console logging and a custom log-mirror plugin that captures all lifecycle events in-page.
+Shows devtoolsPlugin console logging, a custom log-mirror plugin that captures all lifecycle events in-page, and onPluginError demonstrating that a throwing plugin hook is isolated (caught, doesn't abort the mutation or block other plugins).
-**APIs:** createSvState(), devtoolsPlugin(), custom SvStatePlugin
+**APIs:** createSvState(), devtoolsPlugin(), custom SvStatePlugin, onPluginError option
**Stores:** errors, hasErrors, isDirty, snapshots, actionInProgress
```typescript
import { createSvState, devtoolsPlugin, type SvStatePlugin, type ChangeEvent } from 'svstate';
+let simulatePluginError = false;
+
// Custom plugin that mirrors events to an in-page log
const logMirrorPlugin: SvStatePlugin = {
name: 'log-mirror',
onChange(event: ChangeEvent) {
+ // A "Simulate plugin error" checkbox makes this hook throw on demand,
+ // to demonstrate plugin isolation without breaking the rest of the demo
+ if (simulatePluginError) throw new Error(`Simulated failure while mirroring "${event.property}"`);
addLog('change', `${event.property}: "${event.oldValue}" → "${event.currentValue}"`);
},
onValidation(errors) {
@@ -579,7 +612,11 @@ const { data, execute, reset, rollback, state } = createSvState(
logValidation: true // Also log validation events
}),
logMirrorPlugin
- ]
+ ],
+ // A throwing plugin hook doesn't abort the mutation — it's caught here
+ onPluginError: (error, pluginName, hook) => {
+ lastPluginError = `[${pluginName}/${hook}] ${error instanceof Error ? error.message : String(error)}`;
+ }
}
);
```
@@ -588,7 +625,7 @@ const { data, execute, reset, rollback, state } = createSvState(
### PluginPersistSync
-Settings form with persistPlugin (localStorage) and syncPlugin (cross-tab sync via BroadcastChannel).
+Settings form with persistPlugin (localStorage) and syncPlugin (cross-tab sync via BroadcastChannel). Restored state becomes the new baseline — isDirty is false and reset() returns to the restored values, not the pre-restore defaults.
**APIs:** createSvState(), persistPlugin(), syncPlugin()
**Stores:** errors, hasErrors, isDirty
@@ -599,7 +636,11 @@ import { createSvState, persistPlugin, syncPlugin } from 'svstate';
const persist = persistPlugin({
key: 'svstate-demo-settings',
throttle: 300,
- exclude: ['notifications'] // Don't persist this field
+ exclude: ['notifications'], // Don't persist this field
+ onError: (error) => {
+ // Storage write failures (e.g. QuotaExceededError), surfaced in the sidebar
+ lastPersistError = error instanceof Error ? error.message : String(error);
+ }
});
const sync = syncPlugin({
@@ -688,7 +729,11 @@ const analytics = analyticsPlugin({
},
batchSize: 10, // Flush after 10 events
flushInterval: 10000, // Or every 10 seconds
- include: ['change', 'action', 'snapshot'] // Filter event types
+ include: ['change', 'action', 'snapshot'], // Filter event types
+ onError: (error) => {
+ // onFlush threw or rejected, surfaced in the sidebar
+ lastAnalyticsError = error instanceof Error ? error.message : String(error);
+ }
});
const { data, execute, state } = createSvState(