Working example on GitHubSingletonExample.tsx
Singleton Pattern
Reusing storage instances by name. Useful for shared state and when a storage is created in several places (React components, modules).
The examples use the end-to-end domain TodoState = { todos: Todo[]; filter: Filter } (see the
MemoryStorage section).
Enabling Singleton
typescript
import { MemoryStorage } from 'synapse-storage/core'
// First instance — creates the storage
const storage1 = new MemoryStorage<TodoState>({
name: 'my-todo',
singleton: { enabled: true },
initialState: { todos: [], filter: 'completed' },
})
await storage1.initialize()
// Second instance with the SAME name — gets the same object
const storage2 = new MemoryStorage<TodoState>({
name: 'my-todo',
singleton: { enabled: true },
initialState: { todos: [], filter: 'all' }, // ignored (FIRST_WINS by default)
})
await storage2.initialize()
storage2.get('filter') // 'completed' (the same instance!)
storage1 === storage2 // true
// Works with MemoryStorage, LocalStorage, IndexedDB
// Default singleton key: `${storageType}_${name}` (memory_my-todo)Merge strategies (mergeStrategy)
typescript
import { MemoryStorage, ConfigMergeStrategy } from 'synapse-storage/core'
const storage = new MemoryStorage<TodoState>({
name: 'my-todo',
singleton: {
enabled: true,
mergeStrategy: ConfigMergeStrategy.FIRST_WINS, // default
},
initialState: { todos: [], filter: 'all' },
})
// All strategies:
// FIRST_WINS (default)
// The first initialState wins, subsequent ones are ignored
// DEEP_MERGE
// Recursive merge of initialState:
// s1: { todos: [], filter: 'all' }
// s2: { filter: 'active' }
// → { todos: [], filter: 'all' } (the first one's fields take priority)
// OVERRIDE
// The last configuration overrides (except name)
// WARN_AND_USE_FIRST
// Like FIRST_WINS, but with a console.warn on conflicts
// STRICT
// Throws an Error if initialState differsCustom key (singleton.key)
typescript
// Default key: `${storageType}_${name}`
// Two storages with the same name but a different key — different instances
const active = new MemoryStorage<TodoState>({
name: 'todo-board',
singleton: { enabled: true, key: 'board-active' },
initialState: { todos: [], filter: 'active' },
})
const archive = new MemoryStorage<TodoState>({
name: 'todo-board', // the same name!
singleton: { enabled: true, key: 'board-archive' }, // a different key
initialState: { todos: [], filter: 'completed' },
})
active === archive // false (different keys → different instances)Singleton in React
typescript
import { useStorageSubscribe } from 'synapse-storage/react'
// Two components create a storage with the same name — a single instance
const sharedStorage = new MemoryStorage<TodoState>({
name: 'shared-todo',
singleton: { enabled: true },
initialState: { todos: [], filter: 'all' },
})
sharedStorage.initialize()
function ComponentA() {
const count = useStorageSubscribe(sharedStorage, (s) => s.todos.length)
return <div>tasks: {count} <button onClick={() => sharedStorage.update((s) => { s.todos.push(createTodo('From A')) })}>Add</button></div>
}
function ComponentB() {
// Creates a "new" storage — but gets the same singleton
const sameStorage = new MemoryStorage<TodoState>({
name: 'shared-todo',
singleton: { enabled: true },
initialState: { todos: [], filter: 'all' },
})
const count = useStorageSubscribe(sameStorage, (s) => s.todos.length)
// count here = the same as in ComponentA
return <div>tasks: {count}</div>
}Full SingletonOptions configuration
typescript
interface SingletonOptions {
enabled: boolean // enable singleton
mergeStrategy?: ConfigMergeStrategy // merge strategy (default: FIRST_WINS)
warnOnConflict?: boolean // console warning (default: true)
key?: string // custom key (default: `${type}_${name}`)
}
// The ConfigMergeStrategy enum:
enum ConfigMergeStrategy {
STRICT = 'strict',
FIRST_WINS = 'first_wins',
DEEP_MERGE = 'deep_merge',
OVERRIDE = 'override',
WARN_AND_USE_FIRST = 'warn_and_use_first',
}