Skip to main content

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

OptionTypeDefaultDescription
storageStorage– (required)Storage backend. Must implement getItem, setItem, and removeItem.
keystring– (required)The key used to read and write in the storage backend.
includestring[]all keysOnly persist these fact keys.
excludestring[][]Exclude these fact keys from persistence.
debouncenumber100Milliseconds to debounce saves.
onRestore(data: Record<string, unknown>) => voidCalled after state is restored from storage.
onSave(data: Record<string, unknown>) => voidCalled after state is saved to storage.
onError(error: Error) => voidCalled 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

Previous
DevTools

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven State Management for TypeScript