Plugins
•4 min read
Persistence Plugin
Automatically persist facts to storage and restore them on init, with debounced saves, selective key filtering, and prototype pollution protection.
Basic Usage
import { persistencePlugin } from '@directive-run/core/plugins';
const system = createSystem({
module: myModule,
plugins: [
persistencePlugin({
// Where to store the data (any Storage-compatible backend)
storage: localStorage,
// The key used for getItem/setItem – pick something unique per app
key: 'my-app-state',
}),
],
});
system.start();
Both storage and key are required. On init the plugin reads from storage.getItem(key), parses the JSON, and restores matching facts. On every subsequent fact change it debounces a save back to storage.setItem(key, ...).
Options
| Option | Type | Default | Description |
|---|---|---|---|
storage | Storage | – (required) | Storage backend. Must implement getItem, setItem, and removeItem. |
key | string | – (required) | The key used to read and write in the storage backend. |
include | string[] | all keys | Only persist these fact keys. |
exclude | string[] | [] | Exclude these fact keys from persistence. |
debounce | number | 100 | Milliseconds to debounce saves. |
onRestore | (data: Record<string, unknown>) => void | – | Called after state is restored from storage. |
onSave | (data: Record<string, unknown>) => void | – | Called after state is saved to storage. |
onError | (error: Error) => void | – | Called on parse errors, storage failures, or security rejections. |
Storage Backends
Any object implementing the Storage interface (getItem, setItem, removeItem) works.
localStorage
Persists across tabs and browser restarts:
// Data survives browser restarts and is shared across tabs
persistencePlugin({
storage: localStorage,
key: 'my-app',
})
sessionStorage
Cleared when the tab closes:
// Data lives only for this tab session – gone when the user closes the tab
persistencePlugin({
storage: sessionStorage,
key: 'my-app',
})
Custom Storage
Implement the three required methods to back persistence with any store:
// Wrap any data store with the three required Storage methods
persistencePlugin({
storage: {
getItem: (key) => redis.get(key),
setItem: (key, value) => redis.set(key, value),
removeItem: (key) => redis.del(key),
},
key: 'my-app',
})
Selective Persistence
Use include to persist only specific keys:
persistencePlugin({
storage: localStorage,
key: 'my-app',
// Allowlist: only these facts are saved – everything else is ignored
include: ['user', 'preferences', 'cart'],
})
Or use exclude to skip sensitive or transient data:
persistencePlugin({
storage: localStorage,
key: 'my-app',
// Blocklist: persist everything except secrets and ephemeral state
exclude: ['token', 'tempData'],
})
When both are provided, exclude is checked first. A key must pass both filters to be persisted.
Debounce
Saves are debounced to avoid hammering storage on rapid updates. The default is 100 ms. Each new fact change resets the timer, so a burst of changes results in a single write after the burst settles.
persistencePlugin({
storage: localStorage,
key: 'my-app',
// Increase the debounce window to reduce write frequency on busy systems
debounce: 500, // wait 500ms after the last change
})
When the system is destroyed (onDestroy), any pending debounce timer is cleared and a final synchronous save runs immediately so no changes are lost.
Callbacks
onRestore
Called once during init after facts are restored from storage. Receives the parsed data object:
persistencePlugin({
storage: localStorage,
key: 'my-app',
// Fires once during init after saved facts are loaded back into the store
onRestore: (data) => {
console.log('Restored state:', Object.keys(data));
},
})
onSave
Called after every successful save. Receives the data object that was written:
persistencePlugin({
storage: localStorage,
key: 'my-app',
// Fires after each debounced write completes successfully
onSave: (data) => {
console.log('Saved', Object.keys(data).length, 'keys');
},
})
onError
Called on JSON parse failures, storage quota errors, or security rejections. Without this callback, errors are silently swallowed:
persistencePlugin({
storage: localStorage,
key: 'my-app',
// Catches JSON parse failures, quota exceeded, or prototype pollution rejections
onError: (error) => {
console.error('Persistence error:', error.message);
},
})
Security
The plugin includes built-in prototype pollution protection. Before restoring any data from storage, it validates the parsed object with isPrototypeSafe. If the data contains keys like __proto__, constructor, or prototype, the restore is rejected and onError is called with a descriptive error. This prevents a tampered storage entry from polluting object prototypes at runtime.
Next Steps
- Logging Plugin – console logging for lifecycle events
- DevTools Plugin – browser integration
- Plugin Overview – all built-in plugins

