Quick Start
This guide walks through the core mental model: a shared event queue, a collection model, indexes, and selectors.
1. Create an Event Queue
All reactive components share one queue. The scheduler controls when subscribers run:
import { OIMEventQueue, OIMEventQueueSchedulerFactory } from '@oimdb/core';
const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask(),
});
Common schedulers:
| Scheduler | Use case |
|---|---|
createMicrotask() | Default — delivers on the next microtask (after the current sync code, before paint) |
createTimeout(0) | Macrotask delay — batch many writes before notifying |
createAnimationFrame() | UI-friendly, aligned with paint (~60fps) |
createImmediate() | Macrotask — setImmediate in Node, falling back to MessageChannel then setTimeout(0) in browsers; runs after microtasks |
createSync() | Debug only — flushes synchronously per write, no batching |
The first four are asynchronous. For synchronous delivery in normal code, call queue.flush() directly (e.g. in tests).
createSync() is a diagnostic tool, not for production: it strips all coalescing and delivers on every write, in the same call stack. Swap it in at the root when you suspect a bug is caused by batch timing — if the bug disappears under sync delivery it's coalescing-related, if it persists it isn't. It exposes intermediate un-coalesced states, and a cyclic reactive graph can overflow the stack under it.
2. Create a Collection Model
The DX model keeps the core pieces together without storing indexes inside the collection:
import { createOIMCollectionKit } from '@oimdb/core';
type User = {
id: string;
name: string;
email: string;
teamId: string;
};
const users = createOIMCollectionKit<User, string>(queue, {
selectPk: (user) => user.id,
});
Writes return canonical slots — stable object references that indexes can hold:
const slot = users.collection.upsertOne({
id: 'user1',
name: 'John',
email: 'john@example.com',
teamId: 'team-a',
});
// Updating keeps the same slot object
const updatedSlot = users.collection.upsertOne({
id: 'user1',
name: 'John Doe',
email: 'john@example.com',
teamId: 'team-a',
});
console.log(slot === updatedSlot); // true
3. Subscribe to Changes
Subscriptions are key-scoped — you only pay for keys you watch:
users.collection.updateEventEmitter.subscribeOnKey('user1', () => {
console.log('user1 changed');
});
// Coalesced: fires at most once per flush, however many subscribed keys changed.
users.collection.updateEventEmitter.subscribeOnKeys(['user1', 'user2'], () => {
console.log('user1 or user2 changed');
});
After mutations, flush the queue (or wait for the scheduler):
users.collection.upsertOne({
id: 'user1',
name: 'Jane',
email: 'jane@example.com',
teamId: 'team-a',
});
await queue.flush();
4. Add an Index and Selector
Derived indexes maintain membership from entity data. Selectors give you reactive reads:
const usersByTeam = users.indexFactory.derivedSetIndex((user) => user.teamId);
const teamUsers = users.select.entitiesBySetIndexKey(usersByTeam, 'team-a');
teamUsers.watch((value) => {
console.log(value);
});
5. Process Events
Multiple writes to the same key coalesce into a single notification per flush:
users.collection.upsertOne({
id: 'user1',
name: 'A',
email: 'a@example.com',
teamId: 'team-a',
});
users.collection.upsertOne({
id: 'user1',
name: 'B',
email: 'b@example.com',
teamId: 'team-a',
});
users.collection.upsertOne({
id: 'user1',
name: 'C',
email: 'c@example.com',
teamId: 'team-a',
});
await queue.flush();
// subscribeOnKey('user1') fires once
What's Next
- Core Model — collection model, slots, and event semantics
- Indexes and Selectors — derived indexes and reactive reads
- Performance Guide — benchmarks and tuning
- Packages — integration packages (React, Redux, async)