@oimdb/core
Core in-memory data library providing reactive collections, intelligent indexing, and configurable event processing.
For AI Assistants
If you are an AI assistant working on a project that uses OIMDB, read the full API reference before writing any code. It covers all packages, key concepts, code patterns, and naming conventions — the API has specific patterns that are easy to get wrong without context.
The file ships with this package. Read it at:
node_modules/@oimdb/core/llms.txt
Or fetch from CDN if working without local files:
https://cdn.jsdelivr.net/npm/@oimdb/core/llms.txt
If the project has @oimdb/devtools installed, also look for debug.ts or *.debug.ts files — they register the app's specific collections, indexes, and computeds by name and describe their relations. This is the fastest way to understand the specific data model you're working with.
Cursor users: Cursor does not index node_modules, so it will not find this README automatically. Add a .cursor/rules/oimdb.mdc file to your project:
Read node_modules/@oimdb/core/llms.txt before writing any OIMDB code.
Installation
npm install @oimdb/coreWhat'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
- OIMCollectionRelations: Helper for creating collection-bound indexes and ordered lists next to a collection
- OIMReactiveIndexManualSetBased: Reactive index with Set-based storage (efficient for incremental updates)
- OIMReactiveIndexManualArrayBased: Reactive index with Array-based storage (efficient for full replacements)
- OIMReactiveCollectionIndexManualSetBased: Collection-bound Set-based index with safe PK-oriented writes
- OIMReactiveCollectionIndexManualArrayBased: Collection-bound Array-based index with safe PK-oriented writes
- OIMOrderedListCommandStream: Slot-first ordered per-key lists with incremental commands for imperative consumers
- OIMCollectionOrderedListCommandStream: Collection-bound ordered lists with PK writes and slot/entity command payloads
- 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 (stores slots, projects to
Set<TPk>) - OIMIndexManualArrayBased: Array-based manual index (stores slots, projects to
TPk[]) - OIMIndexManualOrderedArrayBased: Slot-first manual ordered Array-based index with
pushSlot,insertSlotAt,removeAt,move, andresetSlots - OIMIndexStoreMapDrivenSetBased: Set-based slot index storage backend
- OIMIndexStoreMapDrivenArrayBased: Array-based slot 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 (slot-backed, projects to
Set<TPk>) - OIMIndexArrayBased: Base Array-based index interface (slot-backed, projects to
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
DX Collection Model
Use the dx factories when you want a concise entrypoint without changing the underlying model. The collection still owns entities, and relations still live next to it.
import {
createOIMCollectionKit,
OIMEventQueue,
OIMEventQueueSchedulerFactory
} from '@oimdb/core';
type User = {
id: string;
name: string;
teamId: string;
};
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
const users = createOIMCollectionKit<User, string>(queue, {
selectPk: user => user.id,
});
const usersByTeam = users.indexFactory.derivedSetIndex(user => user.teamId);
const teamUsers = users.select.entitiesBySetIndexKey(usersByTeam, 'team1');
users.collection.upsertMany([
{ id: 'u1', name: 'Alice', teamId: 'team1' },
{ id: 'u2', name: 'Bob', teamId: 'team1' },
]);
teamUsers.watch(value => {
console.log(value); // Alice, Bob
});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 return canonical slots.
const user1Slot = users.upsertOne({
id: 'user1',
name: 'John Doe',
email: 'john@example.com'
});
const otherSlots = users.upsertMany([
{ id: 'user2', name: 'Jane Smith', email: 'jane@example.com' },
{ id: 'user3', name: 'Bob Wilson', email: 'bob@example.com' }
]);
// Updating an entity keeps the same slot object and updates slot.item.
const updatedUser1Slot = users.upsertOne({
id: 'user1',
name: 'John Smith',
email: 'john@example.com'
});
console.log(user1Slot === updatedUser1Slot); // true
// Query operations
const user = users.getOneByPk('user1');
const multipleUsers = users.getManyByPks(['user1', 'user2']);
const userSlot = users.getSlotByPk('user1');Creating a Reactive Index
OIMDB provides two types of indexes optimized for different use cases:
Indexes are slot-backed internally and expose PK projections through getPksByKey (Set<TPk> for SetBased, TPk[] for ArrayBased). Raw indexes can be used as standalone PK/slot structures. When an index belongs to a collection, prefer OIMReactiveCollectionIndexManual*; it binds the collection at construction time so PK writes resolve canonical collection slots without a later lifecycle setter.
For most app-level entity relations, start with createOIMCollectionKit(...).indexFactory.derivedSetIndex(...) or .derivedArrayIndex(...). The manual indexes below are the advanced path for externally maintained memberships such as search results, permissions, or server-provided order.
SetBased Indexes (for incremental updates)
import {
OIMReactiveCollection,
OIMReactiveCollectionIndexManualSetBased,
OIMEventQueue
} from '@oimdb/core';
type User = { id: string; role: string };
// Create Set-based reactive index for user roles
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (user) => user.id
});
const userRoleIndex =
new OIMReactiveCollectionIndexManualSetBased<string, string, User>(
queue,
{ collection: users }
);
// 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 {
OIMReactiveCollection,
OIMReactiveCollectionIndexManualArrayBased,
OIMEventQueue
} from '@oimdb/core';
type Card = { id: string; deckId: string };
// Create Array-based reactive index for deck cards
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
});
const cards = new OIMReactiveCollection<Card, string>(queue, {
selectPk: (card) => card.id
});
const cardsByDeckIndex =
new OIMReactiveCollectionIndexManualArrayBased<string, string, Card>(
queue,
{ collection: cards }
);
// 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
Ordered List Command Streams
Use OIMCollectionOrderedListCommandStream when you need an ordered list per key, PK-oriented writes, canonical collection slots, and incremental commands for an imperative renderer or external store. Use OIMOrderedListCommandStream directly only when you already manage slots yourself.
import {
OIMEventQueue,
OIMReactiveCollection,
OIMCollectionOrderedListCommandStream
} from '@oimdb/core';
type Card = { id: string; title: string };
const queue = new OIMEventQueue();
const cards = new OIMReactiveCollection<Card, string>(queue, {
selectPk: card => card.id,
});
const cardsByDeck = new OIMCollectionOrderedListCommandStream<
string,
string,
Card
>(queue, { collection: cards });
cards.upsertMany([
{ id: 'card1', title: 'Intro' },
{ id: 'card2', title: 'Details' },
{ id: 'card3', title: 'Summary' },
]);
cardsByDeck.commandsEventEmitter.subscribeOnKey('deck1', () => {
const commands = cardsByDeck.consumeCommands('deck1');
for (const command of commands) {
switch (command.type) {
case 'insert':
// Insert command.item (the slot; entity is command.item.item)
// at command.index
break;
case 'remove':
// Remove command.count ?? 1 elements starting at command.index
break;
case 'move':
// Move command.count ?? 1 elements from command.from to command.to
break;
case 'set':
// Replace the single element at command.index with command.item
break;
case 'reset':
// Replace the whole list with command.items (slots)
break;
}
}
});
cardsByDeck.set('deck1', ['card1', 'card2']);
cardsByDeck.move('deck1', 1, 0);
cardsByDeck.push('deck1', 'card3');
queue.flush(); // Delivers one batched command notification for deck1
console.log(cardsByDeck.getPksByKey('deck1')); // ['card2', 'card1', 'card3']
console.log(cardsByDeck.getEntitiesByKey('deck1')); // [{ id: 'card2', ... }, ...]If you mutate cardsByDeck.index directly, the stream cannot know the exact operation and emits a set command so consumers can resync.
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
Collections with Bound Indexes
import {
OIMReactiveCollection,
OIMReactiveCollectionIndexManualSetBased,
OIMReactiveCollectionIndexManualArrayBased,
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()
});
// Collections own entities.
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: (user: User) => user.id
});
// Indexes live next to collections and bind to them at construction time.
const indexes = {
usersByTeam: new OIMReactiveCollectionIndexManualSetBased<
string,
string,
User
>(queue, { collection: users }),
usersByRole: new OIMReactiveCollectionIndexManualArrayBased<
string,
string,
User
>(queue, { collection: users })
};
// Subscribe to index changes
indexes.usersByTeam.updateEventEmitter.subscribeOnKey('engineering', () => {
console.log('Engineering team changed:', indexes.usersByTeam.getPksByKey('engineering'));
});
// Add users and update indexes
users.upsertMany([
{ id: 'u1', name: 'John', email: 'john@test.com', teamId: 'engineering', role: 'admin' },
{ id: 'u2', name: 'Jane', email: 'jane@test.com', teamId: 'engineering', role: 'user' }
]);
// Update indexes manually
indexes.usersByTeam.setPks('engineering', ['u1', 'u2']);
indexes.usersByRole.setPks('admin', ['u1']);Collection Relations Helper
Use createOIMCollectionRelations when you want the same clean model with less constructor noise. It does not store indexes inside the collection; it only keeps the shared queue + collection binding for related structures that live next to the collection.
import {
createOIMCollectionRelations,
OIMReactiveCollection,
OIMEventQueue
} from '@oimdb/core';
type User = {
id: string;
name: string;
teamId: string;
};
type Card = {
id: string;
deckId: string;
position: number;
};
const queue = new OIMEventQueue();
const users = new OIMReactiveCollection<User, string>(queue, {
selectPk: user => user.id,
});
const userRelations = createOIMCollectionRelations(queue, users);
const cards = new OIMReactiveCollection<Card, string>(queue, {
selectPk: card => card.id,
});
const cardRelations = createOIMCollectionRelations(queue, cards);
// Derived indexes update themselves from collection writes.
const usersByTeam = userRelations.derivedSetIndex(user => user.teamId);
const cardsByDeck = cardRelations.derivedArrayIndex(
card => card.deckId,
{ orderBy: card => card.position }
);
// Manual relations are still available when membership comes from outside.
const searchResults = userRelations.arrayBasedIndex<string>();
const orderedUsers = userRelations.orderedList<string>();
users.upsertMany([
{ id: 'u1', name: 'Alice', teamId: 'team1' },
{ id: 'u2', name: 'Bob', teamId: 'team1' },
]);
cards.upsertMany([
{ id: 'c1', deckId: 'deck1', position: 2 },
{ id: 'c2', deckId: 'deck1', position: 1 },
]);
searchResults.setPks('query:alice', ['u1']);
orderedUsers.set('visible', ['u1', 'u2']);
queue.flush();
console.log(usersByTeam.getEntitiesByKey('team1')); // Alice, Bob
console.log(cardsByDeck.getPksByKey('deck1')); // ['c2', 'c1']
console.log(orderedUsers.getPksByKey('visible')); // ['u1', 'u2']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: 'john@example.com' }); // Merges with existingBuilt-in updaters
Two updater strategies ship as factories — pass the result as updateEntity:
import { createMergeEntityUpdater, createInPlaceEntityUpdater } from '@oimdb/core';
// Default: immutable shallow merge — `{ ...prev, ...draft }`. Each update produces
// a NEW entity object (required for React's Object.is / useSyncExternalStore).
const a = new OIMReactiveCollection<User, string>(queue, {
selectPk: (u) => u.id,
updateEntity: createMergeEntityUpdater(),
});
// In-place: `Object.assign(prev, draft)` — mutates the existing object, no
// per-update allocation. The entity reference is STABLE across changes, so it
// only works with subscription-driven readers (e.g. the `*Signal` hooks in
// @oimdb/react), not with Object.is/React.memo diffing. Fastest on the data
// layer for update-heavy workloads.
const b = new OIMReactiveCollection<User, string>(queue, {
selectPk: (u) => u.id,
updateEntity: createInPlaceEntityUpdater(),
});Event 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: 'john@example.com' });
// 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: '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 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,
OIMReactiveCollection,
OIMReactiveCollectionIndexManualSetBased,
OIMEffectDependencyKeyedIndex,
} 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 roleIndex = new OIMReactiveCollectionIndexManualSetBased<string, string, User>(
queue,
{ collection: users }
);
const effect = new OIMEffect(runtime, {
deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
run: () => {
const adminPks = roleIndex.getPksByKey('admin');
console.log('Admin users changed:', Array.from(adminPks));
},
});
users.upsertMany([
{ id: 'user1', name: 'Alice' },
{ id: 'user2', name: 'Bob' },
]);
roleIndex.setPks('admin', ['user1', 'user2']);
queue.flush();
effect.destroy();
roleIndex.destroy();
users.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,
OIMReactiveCollectionIndexManualSetBased,
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 OIMReactiveCollectionIndexManualSetBased<string, string, User>(
queue,
{ collection: users }
);
// 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,
OIMReactiveCollectionIndexManualSetBased,
} 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 OIMReactiveCollectionIndexManualSetBased<string, string, User>(
queue,
{ collection: users }
);
// 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; upserts return canonical slots)
└── OIMReactiveCollection (adds updateEventEmitter wired to the queue)
OIMIndexSetBased (base for Set-based)
├── OIMIndexManualSetBased (manual Set-based index)
├── OIMReactiveIndexManualSetBased (reactive Set-based index with event emitter)
└── OIMReactiveCollectionIndexManualSetBased (collection-bound reactive Set-based index)
OIMIndexArrayBased (base for Array-based)
├── OIMIndexManualArrayBased (manual Array-based index)
├── OIMIndexManualOrderedArrayBased (slot-first manual ordered Array-based index)
├── OIMCollectionIndexManualOrderedArrayBased (collection-bound ordered Array-based index)
├── OIMReactiveIndexManualArrayBased (reactive Array-based index with event emitter)
└── OIMReactiveCollectionIndexManualArrayBased (collection-bound reactive Array-based index)
OIMOrderedListCommandStream (slot-first ordered-list command stream)
OIMCollectionOrderedListCommandStream (collection-bound ordered-list command stream)
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 (OIMReactiveCollectionIndexManualSetBased):
- Storage: Set of canonical entity slots
- PK projection:
getPksByKeyreturnsSet<TPk>for efficient membership checks - Entity reads: selectors/hooks read
slot.itemdirectly, avoiding per-item collection lookups - 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 (OIMReactiveCollectionIndexManualArrayBased):
- Storage: Array of canonical entity slots
- PK projection:
getPksByKeyreturnsTPk[]for direct array access - Entity reads: selectors/hooks read
slot.itemdirectly, preserving array order - 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)
npm install @oimdb/reactCreate your collections outside React, wire them up once, then use hooks inside components:
import { createOIMCollectionKit, OIMEventQueue } from '@oimdb/core';
import {
OIMCollectionsProvider,
useOIMCollectionsContext,
useSelectEntityByPk,
useSelectEntitiesByIndexKeySetBased,
} from '@oimdb/react';
// --- store.ts (created once, outside React) ---
const queue = new OIMEventQueue();
const { collection: users, indexFactory } =
createOIMCollectionKit<User, string>(queue, { selectPk: (u) => u.id });
const byTeam = indexFactory.derivedSetIndex((u) => [u.teamId]);
export const collections = { users };
export { byTeam };
// --- App.tsx ---
function App() {
return (
<OIMCollectionsProvider collections={collections}>
<TeamList teamId="team1" />
</OIMCollectionsProvider>
);
}
// --- TeamList.tsx ---
type AppCollections = typeof collections;
function TeamList({ teamId }: { teamId: string }) {
const { users } = useOIMCollectionsContext<AppCollections>();
const members = useSelectEntitiesByIndexKeySetBased(users, byTeam, teamId);
return (
<ul>
{members?.map((u) => u && <li key={u.id}>{u.name}</li>)}
</ul>
);
}
function UserCard({ userId }: { userId: string }) {
const { users } = useOIMCollectionsContext<AppCollections>();
const user = useSelectEntityByPk(users, userId);
return <span>{user?.name}</span>;
}All hooks use useSyncExternalStore internally and re-render only when the specific data they watch actually changes.
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
DX Factories
createOIMReactiveCollection<TEntity, TPk>(queue, opts?)
Creates an OIMReactiveCollection<TEntity, TPk> with less constructor noise.
createOIMCollectionKit<TEntity, TPk>(queue, opts?)
Creates a small facade:
type TOIMCollectionKit<TEntity, TPk> = {
queue: OIMEventQueue;
collection: OIMReactiveCollection<TEntity, TPk>;
relations: OIMCollectionRelations<TEntity, TPk>;
select: OIMCollectionSelectors<TEntity, TPk>;
};This is a DX entrypoint only: indexes and lists are still created as separate relation objects, not stored inside the collection.
OIMCollectionSelectors<TEntity, TPk>
DX facade for reactive read selectors backed by one OIMComputativeRuntime.
Methods:
byPk(pk)- CreateOIMCollectionByPkSelector<TEntity, TPk>byPks(pks)- CreateOIMCollectionByPksSelector<TEntity, TPk>entitiesBySetIndexKey(index, key)- Create an entity selector for a Set-based reactive index keyentitiesByArrayIndexKey(index, key)- Create an entity selector for an Array-based reactive index key
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): TOIMEntitySlot<TEntity, TPk>- Insert or update single entity and return its canonical slotupsertOneByPk(pk: TPk, entity: Partial<TEntity>): TOIMEntitySlot<TEntity, TPk>- Insert or update by primary key and return its canonical slotupsertMany(entities: TEntity[]): TOIMEntitySlot<TEntity, TPk>[]- Insert or update multiple entities and return canonical slotsremoveOne(entity: TEntity): void- Remove single entityremoveMany(entities: TEntity[]): void- Remove multiple entitiesgetOneByPk(pk: TPk): TEntity | undefined- Get entity by primary keygetManyByPks(pks: readonly TPk[]): TEntity[]- Get existing entities for primary keysgetSlotByPk(pk: TPk): OIMEntitySlot<TEntity, TPk> | undefined- Get the canonical slot for a primary keygetSlotsByPks(pks: readonly TPk[]): OIMEntitySlot<TEntity, TPk>[]- Get existing canonical slots for primary keys
OIMCollectionRelations<TEntity, TPk>
Factory helper for collection-bound relations. It keeps queue + collection together for construction only; created indexes/lists still live next to the collection, not inside it.
Constructor:
new OIMCollectionRelations(queue: OIMEventQueue, collection: OIMReactiveCollection<TEntity, TPk>)Factory:
createOIMCollectionRelations(queue, collection)Methods:
setBasedIndex<TKey>()- CreateOIMReactiveCollectionIndexManualSetBased<TKey, TPk, TEntity>derivedSetIndex<TKey>(selectIndexKeys)- CreateOIMDerivedCollectionIndexSetBased<TKey, TPk, TEntity>arrayBasedIndex<TKey>()- CreateOIMReactiveCollectionIndexManualArrayBased<TKey, TPk, TEntity>derivedArrayIndex<TKey>(selectIndexKeys, { orderBy?, compareEntities? })- CreateOIMDerivedCollectionIndexArrayBased<TKey, TPk, TEntity>orderedIndex<TKey>()- CreateOIMCollectionIndexManualOrderedArrayBased<TKey, TPk, TEntity>orderedList<TKey>()- CreateOIMCollectionOrderedListCommandStream<TKey, TPk, TEntity>
OIMReactiveIndexManualSetBased<TKey, TPk>
Reactive Set-based index with manual slot writes and change notifications. Use this as a raw slot-first primitive; use OIMReactiveCollectionIndexManualSetBased for PK-oriented writes.
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:
setSlots(key: TKey, slots: Iterable<TOIMAnyEntitySlot<TPk>>): void- Set canonical slots directlyclear(key?: TKey): void- Clear all keys or specific key
Query:
index.getPksByKey(key: TKey): Set<TPk>- Returns Set projection of primary keysindex.getSlotsByKey(key: TKey): ReadonlySet<TOIMAnyEntitySlot<TPk>>- Returns stored slots for fast entity reads
OIMReactiveCollectionIndexManualSetBased<TKey, TPk, TEntity>
Collection-bound Set-based index. Use this when setPks/addPks/removePks should resolve PKs to canonical slots from a collection.
Constructor:
new OIMReactiveCollectionIndexManualSetBased(queue: OIMEventQueue, opts: {
collection: OIMReactiveCollection<TEntity, TPk>;
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreSetBased<TKey, TPk>;
};
} | {
resolveSlot: TOIMEntitySlotResolver<TPk>;
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreSetBased<TKey, TPk>;
};
})Pass exactly one binding: collection for normal collection-bound indexes, or resolveSlot for custom slot resolution.
Methods:
setPks(key: TKey, pks: readonly TPk[]): void- Set primary keys for index keyaddPks(key: TKey, pks: readonly TPk[]): void- Add primary keys to index keyremovePks(key: TKey, pks: readonly TPk[]): void- Remove primary keys from index keysetSlots(key: TKey, slots: Iterable<TOIMAnyEntitySlot<TPk>>): void- Set canonical slots directlyclear(key?: TKey): void- Clear all keys or specific key
OIMDerivedCollectionIndexSetBased<TKey, TPk, TEntity>
Collection-bound Set-based index that derives membership from collection entities. Use this when index keys come from entity fields and should stay in sync automatically.
Constructor:
new OIMDerivedCollectionIndexSetBased(queue, collection, {
selectIndexKeys: (entity: TEntity) => TKey | readonly TKey[] | undefined | null;
buildInitial?: boolean; // defaults to true
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreSetBased<TKey, TPk>;
};
})Methods:
rebuildFromCollection(): void- Rebuild all derived membership from current collection slots- all read/subscription methods from
OIMReactiveCollectionIndexManualSetBased
OIMReactiveIndexManualArrayBased<TKey, TPk>
Reactive Array-based index with manual slot writes and change notifications. Use this as a raw slot-first primitive; use OIMReactiveCollectionIndexManualArrayBased for PK-oriented writes.
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:
setSlots(key: TKey, slots: TOIMAnyEntitySlot<TPk>[]): void- Set canonical slots directlyclear(key?: TKey): void- Clear all keys or specific key
Query:
index.getPksByKey(key: TKey): TPk[]- Returns Array projection of primary keysindex.getSlotsByKey(key: TKey): readonly TOIMAnyEntitySlot<TPk>[]- Returns stored slots for fast entity reads
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.
OIMDerivedCollectionIndexArrayBased<TKey, TPk, TEntity>
Collection-bound Array-based index that derives ordered membership from collection entities. Use this for UI lists where entities define both grouping and order.
Constructor:
new OIMDerivedCollectionIndexArrayBased(queue, collection, {
selectIndexKeys: (entity: TEntity) => TKey | readonly TKey[] | undefined | null;
buildInitial?: boolean; // defaults to true
orderBy?: (entity: TEntity) => string | number | bigint | boolean;
compareEntities?: (a: TEntity, b: TEntity) => number;
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreArrayBased<TKey, TPk>;
};
})compareEntities takes priority over orderBy. Without either option, arrays keep collection slot iteration order.
Methods:
rebuildFromCollection(): void- Rebuild all derived ordered membership from current collection slots- all read/subscription methods from
OIMReactiveCollectionIndexManualArrayBased
OIMReactiveCollectionIndexManualArrayBased<TKey, TPk, TEntity>
Collection-bound Array-based index. Use this when ordered PK arrays should resolve to canonical slots from a collection.
Constructor:
new OIMReactiveCollectionIndexManualArrayBased(queue: OIMEventQueue, opts: {
collection: OIMReactiveCollection<TEntity, TPk>;
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreArrayBased<TKey, TPk>;
};
} | {
resolveSlot: TOIMEntitySlotResolver<TPk>;
indexOptions?: {
comparePks?: TOIMIndexComparator<TPk>;
store?: OIMIndexStoreArrayBased<TKey, TPk>;
};
})Pass exactly one binding: collection for normal collection-bound indexes, or resolveSlot for custom slot resolution.
Methods:
setPks(key: TKey, pks: readonly TPk[]): void- Set primary keys for index keyaddPks(key: TKey, pks: readonly TPk[]): void- Add primary keys to index keyremovePks(key: TKey, pks: readonly TPk[]): void- Remove primary keys from index keysetSlots(key: TKey, slots: TOIMAnyEntitySlot<TPk>[]): void- Set canonical slots directlyclear(key?: TKey): void- Clear all keys or specific key
OIMIndexManualOrderedArrayBased<TKey, TPk>
Slot-first manual ordered Array-based index with direct list operations and update events. Use this when you need low-level ordered storage without a command stream.
Constructor:
new OIMIndexManualOrderedArrayBased<TKey, TPk>()Methods:
pushSlot(key: TKey, slot: TOIMAnyEntitySlot<TPk>): number- Append a slot and return its inserted indexinsertSlotAt(key: TKey, index: number, slot: TOIMAnyEntitySlot<TPk>): number- Insert a slot at an index, clamped to the list boundsremoveAt(key: TKey, index: number): TOIMAnyEntitySlot<TPk> | undefined- Remove and return the slot at an indexmove(key: TKey, fromIndex: number, toIndex: number): TOIMAnyEntitySlot<TPk> | undefined- Move a slot within the listresetSlots(key: TKey, slots: readonly TOIMAnyEntitySlot<TPk>[]): void- Replace the whole ordered slot list for a keyclear(key?: TKey): void- Clear all keys or a specific key
Query:
getPksByKey(key: TKey): TPk[]- Returns the ordered primary-key arraygetSlotsByKey(key: TKey): readonly TOIMAnyEntitySlot<TPk>[]- Returns the stored ordered slotsgetEntitiesByKey<TEntity>(key: TKey): (TEntity | undefined)[]- Returns entities from stored slots, aligned 1:1 with the pks (holes areundefined)
OIMCollectionIndexManualOrderedArrayBased<TKey, TPk, TEntity>
Collection-bound ordered Array-based index. Use this when ordered PK writes should resolve to canonical slots from a collection.
Methods:
push(key: TKey, pk: TPk): number- Append a PK as its canonical slotinsertAt(key: TKey, index: number, pk: TPk): number- Insert a PK as its canonical slotreset(key: TKey, pks: readonly TPk[]): void- Replace the whole ordered list from PKsgetEntitiesByKey(key: TKey): (TEntity | undefined)[]- Returns entities from canonical slots (holes areundefined)
OIMOrderedListCommandStream<TKey, TPk, TEntity>
Slot-first ordered per-key list with a command stream for imperative consumers. It stores data in an OIMIndexManualOrderedArrayBased and emits position-addressed TOIMOrderedListCommand<Slot> batches through commandsEventEmitter. Each command's item is the entity slot (read the entity via item.item):
type TOIMOrderedListCommand<TItem> =
| { type: 'insert'; index: number; item: TItem }
| { type: 'remove'; index: number; count?: number } // count may be > 1
| { type: 'move'; from: number; to: number; count?: number }
| { type: 'set'; index: number; item: TItem } // replace one element
| { type: 'reset'; items: readonly TItem[] }; // replace whole listConstructor:
new OIMOrderedListCommandStream(
queue: OIMEventQueue,
index?: OIMIndexManualOrderedArrayBased<TKey, TPk>
)Properties:
index: OIMIndexManualOrderedArrayBased<TKey, TPk>- Underlying ordered index and source of truthcommandsEventEmitter: OIMUpdateEventEmitter<TKey>- Key-specific command notifications delivered after queue flush
Methods: (Slot = TOIMEntitySlot<TEntity, TPk>)
setSlots(key: TKey, slots: readonly Slot[]): void- Replace the whole ordered list and emit aresetcommandpushSlot(key: TKey, slot: Slot): void- Append a slot and emit aninsertcommandinsertSlotAt(key: TKey, index: number, slot: Slot): void- Insert a slot and emit aninsertcommandsetSlotAt(key: TKey, index: number, slot: Slot): void- Replace the slot atindexin place and emit asetcommandremoveAt(key: TKey, index: number): void- Remove by index and emit aremovecommandremoveRange(key: TKey, index: number, count: number): void- Removecountconsecutive elements fromindexand emit aremovecommand withcountmove(key: TKey, fromIndex: number, toIndex: number): void- Move within the list and emit amovecommandmoveRange(key: TKey, from: number, to: number, count: number): void- Movecountconsecutive elements and emit amovecommand withcount(tois post-extraction space)consumeCommands(key: TKey): TOIMOrderedListCommand<Slot>[]- Read buffered commands for a key inside the notification handlergetBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<Slot>[]- Peek at buffered commands without clearing themgetPksByKey(key: TKey): readonly TPk[]- Read the current ordered listgetSlotsByKey(key: TKey): readonly TOIMEntitySlot<TEntity, TPk>[]- Read current ordered slotsgetEntitiesByKey(key: TKey): (TEntity | undefined)[]- Read current ordered entities (holes areundefined)clear(key?: TKey): void- Clear all keys or a specific keydestroy(): void- Dispose subscriptions and clear state
OIMCollectionOrderedListCommandStream<TKey, TPk, TEntity>
Collection-bound ordered-list command stream. Public writes use PKs that resolve to canonical collection slots; emitted commands carry the slot as item (entity via item.item).
Methods:
set(key: TKey, pks: readonly TPk[]): void- Replace the whole ordered list from PKs (emitsreset)setAt(key: TKey, index: number, pk: TPk): void- Replace the element atindexwith the slot forpk(emitsset)push(key: TKey, pk: TPk): void- Append a PKinsertAt(key: TKey, index: number, pk: TPk): void- Insert a PK- all read/command methods from
OIMOrderedListCommandStream
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: 'john@example.com' };
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.