OIMDB: Reactive In-Memory Database for JavaScript
OIMDB (Object In-Memory Database) is a reactive in-memory database library that provides normalized entity storage, intelligent indexing, and automatic change notifications. Unlike traditional state managers that copy entire state trees, OIMDB uses O(1) Map-based lookups and efficient event coalescing to deliver high-performance state management.
Why OIMDB?
Traditional state management approaches like Redux or MobX have limitations:
- Tree copying overhead: Immutable updates require copying large state trees, causing GC pressure
- No built-in indexing: Querying related data requires manual filtering or memoization
- Fragile batching: UI updates are batched inconsistently across frameworks
- Complex coordination: Multiple reducers need ad-hoc messaging to coordinate updates
OIMDB solves these problems with:
- Normalized storage: Entities stored by primary key in Maps (O(1) lookups)
- Reactive indexes: Manual indexes for efficient queries (e.g., "all posts by author")
- Event coalescing: Multiple rapid updates to the same entity trigger only one notification
- Configurable scheduling: Choose when events fire (microtask, animationFrame, timeout, immediate)
Installation
npm install @oimdb/core
Core Concepts
Collections: Normalized Entity Storage
Collections store entities by primary key, providing O(1) lookups:
import {
OIMReactiveCollection,
OIMEventQueue,
OIMEventQueueSchedulerFactory
} from '@oimdb/core';
interface User {
id: string;
name: string;
email: string;
}
// Create event queue with microtask scheduler (most common)
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
// Create reactive collection
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (user) => user.id
});
// CRUD operations
users.upsertOne({ id: 'user1', name: 'John Doe', email: 'john@example.com' });
users.upsertMany([
{ id: 'user2', name: 'Jane Smith', email: 'jane@example.com' },
{ id: 'user3', name: 'Bob Wilson', email: 'bob@example.com' }
]);
// O(1) lookups
const user = users.getOneByPk('user1');
const multipleUsers = users.getManyByPks(['user1', 'user2']);
Reactive Updates: Key-Specific Subscriptions
Subscribe to changes for specific entities:
// Subscribe to changes for a specific user
users.updateEventEmitter.subscribeOnKey('user1', () => {
console.log('User1 changed!');
});
// Subscribe to changes for multiple users
users.updateEventEmitter.subscribeOnKeys(['user1', 'user2'], () => {
console.log('Users changed!');
});
// Updates trigger notifications
users.upsertOne({ id: 'user1', name: 'John Updated' });
// Notification fires in next microtask
Indexes: Efficient Queries
OIMDB provides two index types optimized for different use cases:
SetBased Indexes: For Incremental Updates
Use when you frequently add/remove individual items:
import { OIMReactiveIndexManualSetBased } from '@oimdb/core';
// Create Set-based index for user roles
const userRoleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
// Build the index
userRoleIndex.setPks('admin', ['user1']);
userRoleIndex.setPks('user', ['user2', 'user3']);
// Efficient incremental updates
userRoleIndex.addPks('admin', ['user2']); // O(1)
userRoleIndex.removePks('admin', ['user1']); // O(1)
// Query returns Set<TPk>
const adminUsers = userRoleIndex.index.getPksByKey('admin'); // Set(['user1', 'user2'])
ArrayBased Indexes: For Full Replacements
Use when you typically replace entire arrays (e.g., ordered lists):
import { OIMReactiveIndexManualArrayBased } from '@oimdb/core';
// Create Array-based index for deck cards
const cardsByDeckIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
// Set full array (O(1) - direct assignment, no diff computation)
cardsByDeckIndex.setPks('deck1', ['card1', 'card2', 'card3']);
// Query returns TPk[]
const deckCards = cardsByDeckIndex.index.getPksByKey('deck1'); // ['card1', 'card2', 'card3']
// For ArrayBased, prefer setPks for updates
cardsByDeckIndex.setPks('deck1', ['card1', 'card2', 'card4']); // Recommended
// addPks/removePks work but are O(n) - less efficient than SetBased
When to use which:
- SetBased: Frequent add/remove operations, order doesn't matter
- ArrayBased: Full array replacements, need to preserve order/sorting
Event Coalescing: Performance Optimization
Multiple rapid updates to the same entity are automatically coalesced into a single notification:
// These three updates...
users.upsertOne({ id: 'user1', name: 'John' });
users.upsertOne({ id: 'user1', email: 'john@test.com' });
users.upsertOne({ id: 'user1', role: 'admin' });
// ...result in only one notification with the final state
// This prevents unnecessary re-renders and improves performance
Event Queue and Schedulers
Control when events fire with different schedulers:
// Microtask (most common) - executes before next browser render
const microtaskQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
// AnimationFrame - syncs with browser rendering (60fps)
const animationFrameQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createAnimationFrame()
});
// Timeout - configurable delay for custom batching
const timeoutQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createTimeout(100)
});
// Immediate - fastest execution
const immediateQueue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createImmediate()
});
// Manual queue (no scheduler)
const manualQueue = new OIMEventQueue();
manualQueue.enqueue(() => console.log('Task 1'));
manualQueue.flush(); // Execute when ready
Advanced Patterns
Collections with Indexes
Use OIMRICollection to combine collections with indexes:
import {
OIMRICollection,
OIMReactiveIndexManualSetBased,
OIMReactiveIndexManualArrayBased
} from '@oimdb/core';
interface User {
id: string;
name: string;
teamId: string;
role: 'admin' | 'user';
}
// Create indexes
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);
});
// Update indexes manually
users.indexes.byTeam.setPks('engineering', ['u1', 'u2']);
Custom Entity Updaters
Customize how entities are merged on update:
import { TOIMEntityUpdater } from '@oimdb/core';
// 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
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (user) => user.id,
updateEntity: deepMergeUpdater
});
// Updates merge with existing
users.upsertOne({ id: 'user1', name: 'John' });
users.upsertOne({ id: 'user1', email: 'john@example.com' }); // Merges with existing