Skip to main content

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:

SchedulerUse 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