@oimdb/core
v1.4.1
Published
Core in-memory data library with type-safe indices, reactive subscriptions, and event processing
Maintainers
Readme
@oimdb/core
Core in-memory data library providing reactive collections, intelligent indexing, and configurable event processing. This package offers the foundational building blocks for building high-performance, event-driven in-memory databases with type-safe operations and automatic change notifications.
🚀 Installation
npm install @oimdb/core📦 What's Included
This package exports all the core classes, interfaces, and types needed to build reactive in-memory database solutions:
Core Classes
- OIMReactiveCollection: Reactive entity storage with automatic change notifications
- OIMRICollection: Reactive collection with integrated indexing capabilities
- OIMReactiveIndexManualSetBased: Reactive index with Set-based storage (efficient for incremental updates)
- OIMReactiveIndexManualArrayBased: Reactive index with Array-based storage (efficient for full replacements)
- OIMEventQueue: Configurable event processing queue with scheduler integration
- OIMCollection: Base collection with CRUD operations and event emission
Event System
- OIMUpdateEventEmitter: Key-based subscriptions with batching/deduplication (no buffering if there are no subscribers)
- OIMEventEmitter: Generic type-safe event emitter
- Schedulers: Multiple event processing strategies (microtask, timeout, animationFrame, immediate)
Reactive Primitives
- OIMEffect: Reactive effects that run when dependencies change
- OIMComputed: Derived values that recompute when dependencies change
- OIMSelector: Value watchers that deliver updates only when values actually change
OIMCollectionByPkSelector: Watch single entity from collectionOIMCollectionByPksSelector: Watch multiple entities from collectionOIMObjectValueByKeySelector: Watch single key from reactive objectOIMEntitiesByIndexKey*Selector: Watch entities by index key
Storage & Indexing
- OIMCollectionStoreMapDriven: Map-based storage backend
- OIMIndexManualSetBased: Set-based manual index (returns
Set<TPk>) - OIMIndexManualArrayBased: Array-based manual index (returns
TPk[]) - OIMIndexStoreMapDrivenSetBased: Set-based index storage backend
- OIMIndexStoreMapDrivenArrayBased: Array-based index storage backend
- OIMMap2Keys: Two-key mapping utilities for complex indexing
Abstract Classes & Interfaces
- OIMCollectionStore: Storage backend interface
- OIMEventQueueScheduler: Event processing scheduler interface
- OIMIndexSetBased: Base Set-based index interface (returns
Set<TPk>) - OIMIndexArrayBased: Base Array-based index interface (returns
TPk[]) - OIMReactiveIndexSetBased: Reactive Set-based index interface
- OIMReactiveIndexArrayBased: Reactive Array-based index interface
Types & Enums
- TOIM*: Generic types for collections, indices, events, and schedulers
- EOIM*: Enums for event types and scheduler types
- IOIM*: Interfaces for event handlers and scheduler events
🔧 Basic Usage
Creating a Reactive Collection
import {
OIMReactiveCollection,
OIMEventQueue,
OIMEventQueueSchedulerFactory
} from '@oimdb/core';
interface User {
id: string;
name: string;
email: string;
}
// Create event queue with microtask scheduler
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
// Create reactive collection
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (user) => user.id
});
// Subscribe to key-specific updates
users.updateEventEmitter.subscribeOnKey('user1', () => {
console.log('User1 changed!');
});
// Subscribe to multiple keys
users.updateEventEmitter.subscribeOnKeys(['user1', 'user2'], () => {
console.log('Users changed!');
});
// CRUD operations
users.upsertOne({ id: 'user1', name: 'John Doe', email: '[email protected]' });
users.upsertMany([
{ id: 'user2', name: 'Jane Smith', email: '[email protected]' },
{ id: 'user3', name: 'Bob Wilson', email: '[email protected]' }
]);
// Query operations
const user = users.getOneByPk('user1');
const multipleUsers = users.getManyByPks(['user1', 'user2']);Creating a Reactive Index
OIMDB provides two types of indexes optimized for different use cases:
SetBased Indexes (for incremental updates)
import { OIMReactiveIndexManualSetBased, OIMEventQueue } from '@oimdb/core';
// Create Set-based reactive index for user roles
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
const userRoleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
// Subscribe to specific index key changes
userRoleIndex.updateEventEmitter.subscribeOnKey('admin', () => {
console.log('Admin users changed:', userRoleIndex.getPksByKey('admin')); // Set<string>
});
// Build the index manually
userRoleIndex.setPks('admin', ['user1']);
userRoleIndex.setPks('user', ['user2', 'user3']);
// Add more users to existing roles (efficient for Set-based)
userRoleIndex.addPks('admin', ['user2']);
// Query the index - returns Set
const adminUsers = userRoleIndex.index.getPksByKey('admin'); // Set(['user1', 'user2'])
const regularUsers = userRoleIndex.index.getPksByKey('user'); // Set(['user2', 'user3'])
// Remove users from roles (efficient for Set-based)
userRoleIndex.removePks('admin', ['user1']);ArrayBased Indexes (for full replacements)
import { OIMReactiveIndexManualArrayBased, OIMEventQueue } from '@oimdb/core';
// Create Array-based reactive index for deck cards
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
const cardsByDeckIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
// Subscribe to specific index key changes
cardsByDeckIndex.updateEventEmitter.subscribeOnKey('deck1', () => {
console.log('Deck cards changed:', cardsByDeckIndex.getPksByKey('deck1')); // string[]
});
// Build the index manually - set full array
cardsByDeckIndex.setPks('deck1', ['card1', 'card2', 'card3']);
// Query the index - returns Array
const deckCards = cardsByDeckIndex.index.getPksByKey('deck1'); // ['card1', 'card2', 'card3']
// For Array-based indexes, prefer setPks for updates (addPks/removePks are available but less efficient)
cardsByDeckIndex.setPks('deck1', ['card1', 'card2', 'card4']); // Full replacement (recommended)
// cardsByDeckIndex.addPks('deck1', ['card5']); // Works but less efficient than SetBasedWhen to use which:
- SetBased: Use when you frequently add/remove individual items (
addPks/removePksare efficient) and order doesn't matter - ArrayBased: Use when you typically replace the entire array (
setPksis more efficient, no diff computation needed) or when you need to preserve element order/sorting
Event Queue and Schedulers
import {
OIMEventQueue,
OIMEventQueueSchedulerFactory,
TOIMSchedulerType
} from '@oimdb/core';
// Create event queues with different schedulers
const microtaskQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.create('microtask')
});
const timeoutQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.create('timeout', { delay: 100 })
});
const animationFrameQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.create('animationFrame')
});
const immediateQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.create('immediate')
});
// Manual queue operations
const manualQueue = new OIMEventQueue(); // No scheduler
manualQueue.enqueue(() => console.log('Task 1'));
manualQueue.enqueue(() => console.log('Task 2'));
// Manually flush when ready
manualQueue.flush();
// Queue introspection
console.log('Queue length:', manualQueue.length);
console.log('Is empty:', manualQueue.isEmpty);🏗️ Advanced Usage
Reactive Collection with Indexes (OIMRICollection)
import {
OIMRICollection,
OIMReactiveIndexManual,
OIMEventQueue,
OIMEventQueueSchedulerFactory
} from '@oimdb/core';
interface User {
id: string;
name: string;
email: string;
teamId: string;
role: 'admin' | 'user';
}
// Create event queue
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
// Create indexes (choose SetBased or ArrayBased based on your needs)
const teamIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
const roleIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
// Create collection with indexes
const users = new OIMRICollection(queue, {
collectionOpts: {
selectPk: (user: User) => user.id
},
indexes: {
byTeam: teamIndex,
byRole: roleIndex
}
});
// Subscribe to index changes
users.indexes.byTeam.updateEventEmitter.subscribeOnKey('engineering', (pks) => {
console.log('Engineering team changed:', pks);
});
// Add users and update indexes
users.upsertMany([
{ id: 'u1', name: 'John', email: '[email protected]', teamId: 'engineering', role: 'admin' },
{ id: 'u2', name: 'Jane', email: '[email protected]', teamId: 'engineering', role: 'user' }
]);
// Update indexes manually
users.indexes.byTeam.setPks('engineering', ['u1', 'u2']);
users.indexes.byRole.setPks('admin', ['u1']);Custom Entity Updater
import {
TOIMEntityUpdater,
OIMReactiveCollection,
OIMEventQueue
} from '@oimdb/core';
// Custom deep merge updater
const deepMergeUpdater: TOIMEntityUpdater<User> = (newEntity, oldEntity) => {
const result = { ...oldEntity };
for (const [key, value] of Object.entries(newEntity)) {
if (value !== undefined) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
result[key] = deepMergeUpdater(value, result[key] || {});
} else {
result[key] = value;
}
}
}
return result;
};
// Use custom updater with reactive collection
const queue = new OIMEventQueue();
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (user) => user.id,
updateEntity: deepMergeUpdater
});
// Now updates will use deep merge logic
users.upsertOne({ id: 'user1', name: 'John' });
users.upsertOne({ id: 'user1', email: '[email protected]' }); // Merges with existingEvent Coalescing and Update Subscriptions
import {
OIMReactiveCollection,
OIMEventQueue,
OIMEventQueueSchedulerFactory
} from '@oimdb/core';
// Create collection with microtask scheduler for coalescing
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
const users = new OIMReactiveCollection<User, string>(queue);
// Subscribe to coalesced updates for specific keys
users.updateEventEmitter.subscribeOnKey('user1', () => {
console.log('User1 updated (coalesced)');
});
// Multiple rapid updates to same key will be coalesced
users.upsertOne({ id: 'user1', name: 'John' });
users.upsertOne({ id: 'user1', name: 'John Doe' });
users.upsertOne({ id: 'user1', email: '[email protected]' });
// Only one notification will fire (in next microtask)
// (No separate "coalescer" object exists: batching/deduplication is handled inside OIMUpdateEventEmitter.)🔄 Reactive Architecture
Event-Driven Updates
OIMDB core uses a reactive architecture where changes automatically trigger notifications to subscribers:
// Collection updates trigger events through the event queue
collection.upsertOne(entity) → updateEventEmitter → event queue → subscribers
// Key-specific subscriptions only notify when relevant data changes
updateEventEmitter.subscribeOnKey('user1', callback) // Only fires for user1 changesEvent Coalescing
Multiple rapid changes to the same entity are automatically coalesced:
// These three updates...
users.upsertOne({ id: 'user1', name: 'John' });
users.upsertOne({ id: 'user1', email: '[email protected]' });
users.upsertOne({ id: 'user1', role: 'admin' });
// ...result in only one notification with the final state
// This prevents unnecessary re-renders and improves performanceEffects, Computed, and the Event Lifecycle
OIMDB uses a single-pass flush boundary: queue.flush() executes the current batch of pending work.
Effects and computed values are scheduled through OIMComputativeRuntime, which is backed by the same queue. This keeps the public API simple and avoids a multi-phase flush model.
What is an Effect?
OIMEffect is the base reactive primitive: it subscribes to dependencies and calls run() when those dependencies change. It coalesces multiple invalidations during the same flush into a single run.
Basic example with reactive object:
import {
OIMEffect,
OIMComputativeRuntime,
OIMEventQueue,
OIMReactiveObject,
OIMEffectDependencyKeyedObject,
} from '@oimdb/core';
type TKey = 'a';
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const obj = new OIMReactiveObject<TKey, number>(queue);
const effect = new OIMEffect(runtime, {
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
run: () => {
console.log('obj.a changed');
},
});
obj.setProperty('a', 1);
queue.flush();
effect.destroy();
obj.destroy();
queue.destroy();Effect with collection dependency:
import {
OIMEffect,
OIMComputativeRuntime,
OIMEventQueue,
OIMReactiveCollection,
OIMEffectDependencyKeyedCollection,
} from '@oimdb/core';
interface User {
id: string;
name: string;
}
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (u) => u.id,
});
const effect = new OIMEffect(runtime, {
deps: [new OIMEffectDependencyKeyedCollection(users, 'user1')],
run: () => {
const user = users.getOneByPk('user1');
console.log('User1 changed:', user);
},
});
users.upsertOne({ id: 'user1', name: 'John' });
queue.flush();
effect.destroy();
users.destroy();
queue.destroy();Effect with index dependency:
import {
OIMEffect,
OIMComputativeRuntime,
OIMEventQueue,
OIMReactiveIndexManualSetBased,
OIMEffectDependencyKeyedIndex,
} from '@oimdb/core';
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
const effect = new OIMEffect(runtime, {
deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
run: () => {
const adminPks = roleIndex.getPksByKey('admin');
console.log('Admin users changed:', Array.from(adminPks));
},
});
roleIndex.setPks('admin', ['user1', 'user2']);
queue.flush();
effect.destroy();
roleIndex.destroy();
queue.destroy();Effect with multiple dependencies:
import {
OIMEffect,
OIMComputativeRuntime,
OIMEventQueue,
OIMReactiveObject,
OIMReactiveCollection,
OIMEffectDependencyKeyedObject,
OIMEffectDependencyKeyedCollection,
} from '@oimdb/core';
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const settings = new OIMReactiveObject<'theme' | 'lang', string>(queue);
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (u) => u.id,
});
const effect = new OIMEffect(runtime, {
deps: [
new OIMEffectDependencyKeyedObject(settings, ['theme', 'lang']),
new OIMEffectDependencyKeyedCollection(users, 'currentUser'),
],
run: () => {
const theme = settings.get('theme');
const user = users.getOneByPk('currentUser');
console.log('Settings or user changed:', { theme, user });
},
});
settings.setProperty('theme', 'dark');
users.upsertOne({ id: 'currentUser', name: 'John' });
queue.flush(); // Effect runs once, even though multiple deps changed
effect.destroy();
settings.destroy();
users.destroy();
queue.destroy();What is a Computed?
OIMComputed<T> is built on top of OIMEffect: it recomputes a derived value and emits update when the value changes.
Basic example:
import {
OIMComputed,
OIMEventQueue,
OIMComputativeRuntime,
OIMReactiveObject,
OIMEffectDependencyKeyedObject,
} from '@oimdb/core';
type TKey = 'a';
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const obj = new OIMReactiveObject<TKey, number>(queue);
const doubled = new OIMComputed<number>(runtime, {
compute: () => (obj.get('a') ?? 0) * 2,
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
});
obj.setProperty('a', 10);
queue.flush(); // run scheduled work
console.log(doubled.get()); // 20
// If you also subscribe to computed updates, delivery happens in the same drain flush:
let calls = 0;
doubled.updateEventEmitter.subscribeOnKey('value', () => {
calls++;
});
obj.setProperty('a', 11);
queue.flush(); // run scheduled work
console.log(calls); // 1
doubled.destroy();
obj.destroy();
queue.destroy();Computed with collection and index dependencies:
import {
OIMComputed,
OIMEventQueue,
OIMComputativeRuntime,
OIMReactiveCollection,
OIMReactiveIndexManualSetBased,
OIMEffectDependencyKeyedCollection,
OIMEffectDependencyKeyedIndex,
} from '@oimdb/core';
interface User {
id: string;
name: string;
role: string;
}
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (u) => u.id,
});
const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
// Computed that counts admin users
const adminCount = new OIMComputed<number>(runtime, {
compute: () => {
const adminPks = roleIndex.getPksByKey('admin');
return adminPks.size;
},
deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
});
// Computed that gets admin user names
const adminNames = new OIMComputed<string[]>(runtime, {
compute: () => {
const adminPks = Array.from(roleIndex.getPksByKey('admin'));
return adminPks
.map((pk) => users.getOneByPk(pk)?.name)
.filter((name): name is string => name !== undefined);
},
deps: [
new OIMEffectDependencyKeyedIndex(roleIndex, 'admin'),
new OIMEffectDependencyKeyedCollection(users, Array.from(roleIndex.getPksByKey('admin'))),
],
});
users.upsertMany([
{ id: 'u1', name: 'Alice', role: 'admin' },
{ id: 'u2', name: 'Bob', role: 'user' },
]);
roleIndex.setPks('admin', ['u1']);
queue.flush();
console.log(adminCount.get()); // 1
console.log(adminNames.get()); // ['Alice']
adminNames.destroy();
adminCount.destroy();
roleIndex.destroy();
users.destroy();
queue.destroy();Computed-to-Computed dependencies
For computed-to-computed dependencies you can use OIMEffectDependencyComputed.
import {
OIMComputed,
OIMEffect,
OIMComputativeRuntime,
OIMEffectDependencyComputed,
OIMEventQueue,
OIMReactiveObject,
OIMEffectDependencyKeyedObject,
} from '@oimdb/core';
type TKey = 'a';
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const obj = new OIMReactiveObject<TKey, number>(queue);
const A = new OIMComputed<number>(runtime, {
compute: () => (obj.get('a') ?? 0) + 1,
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
});
const B = new OIMComputed<number>(runtime, {
compute: () => A.get() * 2,
deps: [new OIMEffectDependencyComputed({ emitter: A.emitter, updateEventEmitter: A.updateEventEmitter })],
});
const effect = new OIMEffect(runtime, {
deps: [new OIMEffectDependencyComputed({ emitter: B.emitter, updateEventEmitter: B.updateEventEmitter })],
run: () => console.log('B changed'),
});
obj.setProperty('a', 1);
queue.flush(); // run scheduled work
effect.destroy();
B.destroy();
A.destroy();
obj.destroy();
queue.destroy();What are Selectors?
Selectors provide a convenient way to watch and react to changes in collections, objects, and indexes. They automatically handle subscription management and deliver updates only when values actually change.
Collection selector:
import {
OIMCollectionByPkSelector,
OIMCollectionByPksSelector,
OIMComputativeRuntime,
OIMEventQueue,
OIMReactiveCollection,
} from '@oimdb/core';
interface User {
id: string;
name: string;
}
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (u) => u.id,
});
// Watch a single user
const userSelector = new OIMCollectionByPkSelector(runtime, users, 'user1');
const unwatch = userSelector.watch((user) => {
console.log('User1 changed:', user);
});
users.upsertOne({ id: 'user1', name: 'John' });
queue.flush(); // Callback fires with { id: 'user1', name: 'John' }
// Watch multiple users
const usersSelector = new OIMCollectionByPksSelector(runtime, users, ['user1', 'user2']);
usersSelector.watch((users) => {
console.log('Users changed:', users);
});
users.upsertMany([
{ id: 'user1', name: 'John Doe' },
{ id: 'user2', name: 'Jane Smith' },
]);
queue.flush(); // Callback fires with array of users
unwatch(); // Stop watching
usersSelector.watch(() => {}); // Get unsubscribe function
users.destroy();
queue.destroy();Selector with index (entities by index key):
import {
OIMEntitiesByIndexKeySetBasedSelector,
OIMComputativeRuntime,
OIMEventQueue,
OIMReactiveCollection,
OIMReactiveIndexManualSetBased,
} from '@oimdb/core';
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (u) => u.id,
});
const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
// Watch all admin users
const adminUsersSelector = new OIMEntitiesByIndexKeySetBasedSelector(
runtime,
users,
roleIndex,
'admin'
);
adminUsersSelector.watch((adminUsers) => {
console.log('Admin users:', adminUsers.map((u) => u?.name));
});
users.upsertMany([
{ id: 'u1', name: 'Alice', role: 'admin' },
{ id: 'u2', name: 'Bob', role: 'admin' },
]);
roleIndex.setPks('admin', ['u1', 'u2']);
queue.flush(); // Callback fires with [Alice, Bob]
// When index changes, selector automatically resubscribes to new entities
roleIndex.setPks('admin', ['u1']);
queue.flush(); // Callback fires with [Alice]
adminUsersSelector.watch(() => {}); // Get unsubscribe function
roleIndex.destroy();
users.destroy();
queue.destroy();Object selector:
import {
OIMObjectValueByKeySelector,
OIMObjectValuesByKeysSelector,
OIMComputativeRuntime,
OIMEventQueue,
OIMReactiveObject,
} from '@oimdb/core';
const queue = new OIMEventQueue();
const runtime = new OIMComputativeRuntime(queue);
const settings = new OIMReactiveObject<'theme' | 'lang', string>(queue);
// Watch single key
const themeSelector = new OIMObjectValueByKeySelector(runtime, settings, 'theme');
themeSelector.watch((theme) => {
console.log('Theme changed:', theme);
});
// Watch multiple keys
const settingsSelector = new OIMObjectValuesByKeysSelector(runtime, settings, ['theme', 'lang']);
settingsSelector.watch((values) => {
console.log('Settings changed:', values); // [theme, lang]
});
settings.setProperty('theme', 'dark');
queue.flush();
settings.destroy();
queue.destroy();Key differences: Effects vs Selectors:
- Effects (
OIMEffect): Run side effects when dependencies change. Use for logging, API calls, UI updates. - Selectors (
OIMSelector): Watch and deliver values only when they actually change. Use for reactive data access with automatic change detection. - Computed (
OIMComputed): Derive values from dependencies. Use for calculated/transformed data.
Gotchas (read this once)
- Avoid cycles: if A depends on B and B depends on A (directly or indirectly), you can get endless invalidation/recompute. Keep your dependency graph acyclic.
- Keep
compute()pure: treatcompute()as a pure function over current state. Doing writes insidecompute()will create hard-to-debug re-entrancy. - Keep effects safe: if you need to write to stores or trigger IO, do it from
OIMEffect, but avoid creating endless update loops. - Always
destroy(): effects/computed/selectors subscribe to dependencies; if you create them dynamically, calldestroy()or use the unsubscribe function to unsubscribe and free memory. - Selectors deliver only on change: Selectors use equality checks (
areEqual) to avoid delivering the same value multiple times. OverrideareEqualin custom selectors if needed.
Scheduler Types
Choose the right scheduler for your use case:
microtask: Most common - executes before next browser rendertimeout: Configurable delay for custom batching strategiesanimationFrame: Syncs with browser rendering (60fps)immediate: Fastest execution using platform-specific APIs
Reactive Collection Hierarchy
OIMCollection (base)
├── OIMReactiveCollection (adds updateEventEmitter wired to the queue)
└── OIMRICollection (reactive collection + indexes)
OIMIndexSetBased (base for Set-based)
├── OIMIndexManualSetBased (manual Set-based index)
└── OIMReactiveIndexManualSetBased (reactive Set-based index with event emitter)
OIMIndexArrayBased (base for Array-based)
├── OIMIndexManualArrayBased (manual Array-based index)
└── OIMReactiveIndexManualArrayBased (reactive Array-based index with event emitter)⚡ Performance Characteristics
- Collections: O(1) primary key lookups using Map-based storage
- Reactive Collections: O(1) lookups + efficient event coalescing
- Indices: O(1) index lookups with lazy evaluation
- Event System: Smart coalescing prevents redundant notifications
- Memory: Efficient key-based subscriptions, no global listeners
- Schedulers: Configurable timing for optimal batching:
- Microtask: ~1-5ms delay, ideal for UI updates
- Immediate: <1ms, fastest execution
- Timeout: Custom delay for batching strategies
- AnimationFrame: 16ms, synced with 60fps rendering
Index Performance
SetBased Indexes (OIMReactiveIndexManualSetBased):
- Returns:
Set<TPk>for efficient membership checks - Best for: Frequent incremental updates using
addPks/removePks - Performance: O(1) add/remove operations, O(n) for
setPks(requires Set creation) - Use case: When you need to frequently add/remove individual items
ArrayBased Indexes (OIMReactiveIndexManualArrayBased):
- Returns:
TPk[]for direct array access - Best for: Full array replacements using
setPks - Performance: O(1)
setPksoperation (direct assignment, no diff computation) - Use case: When you typically replace the entire array (e.g., deck cards, ordered lists) or when you need to preserve element order/sorting
- Note: While
addPks/removePksare available, they are less efficient (O(n)) than for SetBased indexes. For ArrayBased indexes, prefersetPksfor better performance.
🔗 Integration Patterns
With React (@oimdb/react)
The core library integrates seamlessly with React through dedicated hooks:
import { useSelectEntitiesByPks, selectEntityByPk } from '@oimdb/react';
// React hooks automatically subscribe to reactive collections
const user = selectEntityByPk(users, 'user1');
const teamUsers = useSelectEntitiesByPks(users, userIds);With Redux (@oimdb/redux-adapter)
Migrate from Redux to OIMDB gradually or use both systems side-by-side with automatic two-way synchronization:
import { OIMDBAdapter } from '@oimdb/redux-adapter';
import { createStore, combineReducers, applyMiddleware } from 'redux';
// Create Redux adapter
const adapter = new OIMDBAdapter(queue);
// Create Redux reducer from OIMDB collection
const usersReducer = adapter.createCollectionReducer(users);
// Create middleware for automatic flushing
const middleware = adapter.createMiddleware();
// Use in existing Redux store
const store = createStore(
combineReducers({
users: usersReducer, // OIMDB-backed reducer
ui: uiReducer, // Existing Redux reducer
}),
applyMiddleware(middleware)
);
adapter.setStore(store);
// OIMDB changes automatically sync to Redux
// Redux actions automatically sync back to OIMDB with child reducers
// Middleware automatically flushes queue after each action - no manual flush needed!Key Benefits:
- 🔄 Gradual Migration: Migrate one collection at a time without breaking changes
- 🔄 Two-Way Sync: Automatic synchronization between OIMDB and Redux
- ⚡ Automatic Flushing: Middleware automatically processes events after Redux actions
- 📦 Production Ready: Battle-tested adapter optimized for large datasets
- 🎯 Flexible: Works with any Redux state structure via custom mappers
📖 See @oimdb/redux-adapter documentation for complete migration guide and examples.
Standalone Usage
Use core classes directly for maximum control:
// Manual subscription management
const unsubscribe = users.updateEventEmitter.subscribeOnKey('user1', () => {
// Handle user1 changes
});
// Clean up when done
unsubscribe();📚 API Reference
Core Classes
OIMReactiveCollection<TEntity, TPk>
Reactive collection with automatic change notifications and event coalescing.
Constructor:
new OIMReactiveCollection(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEntity, TPk>)Properties:
collection: OIMCollection<TEntity, TPk>- Underlying collectionupdateEventEmitter: OIMUpdateEventEmitter<TPk>- Key-specific subscriptions- Event batching/deduplication is handled internally by
OIMUpdateEventEmitter
Methods:
upsertOne(entity: TEntity): void- Insert or update single entityupsertMany(entities: TEntity[]): void- Insert or update multiple entitiesremoveOne(entity: TEntity): void- Remove single entityremoveMany(entities: TEntity[]): void- Remove multiple entitiesgetOneByPk(pk: TPk): TEntity | undefined- Get entity by primary keygetManyByPks(pks: readonly TPk[]): Map<TPk, TEntity | undefined>- Get multiple entities
OIMRICollection<TEntity, TPk, TIndexName, TIndexKey, TIndex, TReactiveIndex, TReactiveIndexMap>
Reactive collection with integrated indexing capabilities.
Constructor:
new OIMRICollection(queue: OIMEventQueue, opts: {
collectionOpts?: TOIMCollectionOptions<TEntity, TPk>;
indexes: TReactiveIndexMap;
})Properties:
indexes: TReactiveIndexMap- Named reactive indexes preserving index-to-name mapping- (inherits all OIMReactiveCollection properties)
OIMReactiveIndexManualSetBased<TKey, TPk>
Reactive Set-based index with manual key-to-entity mapping and change notifications. Returns Set<TPk> for efficient membership checks.
Constructor:
new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreSetBased<TKey, TPk>;
}
})Properties:
index: OIMIndexManualSetBased<TKey, TPk>- Underlying Set-based indexupdateEventEmitter: OIMUpdateEventEmitter<TKey>- Key-specific subscriptions
Methods:
setPks(key: TKey, pks: readonly TPk[]): void- Set primary keys for index key (replaces entire Set)addPks(key: TKey, pks: readonly TPk[]): void- Add primary keys to index key (efficient)removePks(key: TKey, pks: readonly TPk[]): void- Remove primary keys from index key (efficient)clear(key?: TKey): void- Clear all keys or specific key
Query:
index.getPksByKey(key: TKey): Set<TPk>- Returns Set of primary keys
OIMReactiveIndexManualArrayBased<TKey, TPk>
Reactive Array-based index with manual key-to-entity mapping and change notifications. Returns TPk[] for direct array access.
Constructor:
new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreArrayBased<TKey, TPk>;
}
})Properties:
index: OIMIndexManualArrayBased<TKey, TPk>- Underlying Array-based indexupdateEventEmitter: OIMUpdateEventEmitter<TKey>- Key-specific subscriptions
Methods:
setPks(key: TKey, pks: readonly TPk[]): void- Set primary keys for index key (direct assignment, no diff) - Recommended for ArrayBasedaddPks(key: TKey, pks: readonly TPk[]): void- Add primary keys to index key (O(n) operation, less efficient than SetBased)removePks(key: TKey, pks: readonly TPk[]): void- Remove primary keys from index key (O(n) operation, less efficient than SetBased)clear(key?: TKey): void- Clear all keys or specific key
Query:
index.getPksByKey(key: TKey): TPk[]- Returns Array of primary keys
Note: While addPks/removePks are available, they require array operations (Set creation, filtering) making them O(n) compared to O(1) for SetBased indexes. For ArrayBased indexes, prefer setPks for better performance when replacing the entire array.
OIMEventQueue
Event processing queue with configurable scheduling.
Constructor:
new OIMEventQueue(options?: TOIMEventQueueOptions)Properties:
length: number- Number of queued functionsisEmpty: boolean- Whether queue is empty
Methods:
enqueue(fn: () => void): void- Add function to queueflush(): void- Execute all queued functionsclear(): void- Clear queue without executingdestroy(): void- Clean up scheduler subscriptions
Schedulers
OIMEventQueueSchedulerFactory
Factory for creating different scheduler types:
import { TOIMSchedulerType } from '@oimdb/core';
// Available scheduler types
type TOIMSchedulerType = 'immediate' | 'microtask' | 'timeout' | 'animationFrame';Static Methods:
create(type: 'microtask'): OIMEventQueueSchedulerMicrotaskcreate(type: 'animationFrame'): OIMEventQueueSchedulerAnimationFramecreate(type: 'timeout', options?: { delay: number }): OIMEventQueueSchedulerTimeoutcreate(type: 'immediate'): OIMEventQueueSchedulerImmediatecreateMicrotask(): OIMEventQueueSchedulerMicrotaskcreateAnimationFrame(): OIMEventQueueSchedulerAnimationFramecreateTimeout(delay?: number): OIMEventQueueSchedulerTimeoutcreateImmediate(): OIMEventQueueSchedulerImmediate
Types
TOIMCollectionOptions<TEntity, TPk>
Collection configuration options:
selectPk?: TOIMPkSelector<TEntity, TPk>- Primary key selector functionstore?: OIMCollectionStore<TEntity, TPk>- Storage backendupdateEntity?: TOIMEntityUpdater<TEntity>- Entity update strategy
TOIMEntityUpdater<TEntity>
Entity update function signature:
(newEntity: TEntity, oldEntity: TEntity) => TEntityTOIMSchedulerType
Available scheduler types:
'microtask' | 'animationFrame' | 'timeout' | 'immediate'TOIMEventQueueOptions
Event queue configuration:
scheduler?: OIMEventQueueScheduler- Optional scheduler for automatic flushing
🧪 Testing
import {
OIMReactiveCollection,
OIMEventQueue,
OIMEventQueueSchedulerFactory
} from '@oimdb/core';
describe('OIMReactiveCollection', () => {
let users: OIMReactiveCollection<User, string>;
let queue: OIMEventQueue;
beforeEach(() => {
queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
users = new OIMReactiveCollection(queue, {
selectPk: (user) => user.id
});
});
it('should upsert and retrieve entities', () => {
const user = { id: 'user1', name: 'John', email: '[email protected]' };
users.upsertOne(user);
expect(users.getOneByPk('user1')).toEqual(user);
});
it('should notify subscribers of changes', (done) => {
users.updateEventEmitter.subscribeOnKey('user1', () => {
done(); // Test passes when callback is called
});
users.upsertOne({ id: 'user1', name: 'John' });
queue.flush(); // Trigger immediate flush for testing
});
});🤝 Contributing
This package is part of the OIMDB ecosystem. See the main project repository for contribution guidelines.
📄 License
MIT License - see LICENSE file for details.
