npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oimdb/core

v1.4.1

Published

Core in-memory data library with type-safe indices, reactive subscriptions, and event processing

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 collection
    • OIMCollectionByPksSelector: Watch multiple entities from collection
    • OIMObjectValueByKeySelector: Watch single key from reactive object
    • OIMEntitiesByIndexKey*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 SetBased

When to use which:

  • SetBased: Use when you frequently add/remove individual items (addPks/removePks are efficient) and order doesn't matter
  • ArrayBased: Use when you typically replace the entire array (setPks is 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 existing

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: '[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 changes

Event 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 performance

Effects, 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: treat compute() as a pure function over current state. Doing writes inside compute() 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, call destroy() 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. Override areEqual in custom selectors if needed.

Scheduler Types

Choose the right scheduler for your use case:

  • microtask: Most common - executes before next browser render
  • timeout: Configurable delay for custom batching strategies
  • animationFrame: 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) setPks operation (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/removePks are available, they are less efficient (O(n)) than for SetBased indexes. For ArrayBased indexes, prefer setPks for 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 collection
  • updateEventEmitter: OIMUpdateEventEmitter<TPk> - Key-specific subscriptions
  • Event batching/deduplication is handled internally by OIMUpdateEventEmitter

Methods:

  • upsertOne(entity: TEntity): void - Insert or update single entity
  • upsertMany(entities: TEntity[]): void - Insert or update multiple entities
  • removeOne(entity: TEntity): void - Remove single entity
  • removeMany(entities: TEntity[]): void - Remove multiple entities
  • getOneByPk(pk: TPk): TEntity | undefined - Get entity by primary key
  • getManyByPks(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 index
  • updateEventEmitter: 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 index
  • updateEventEmitter: 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 ArrayBased
  • addPks(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 functions
  • isEmpty: boolean - Whether queue is empty

Methods:

  • enqueue(fn: () => void): void - Add function to queue
  • flush(): void - Execute all queued functions
  • clear(): void - Clear queue without executing
  • destroy(): 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'): OIMEventQueueSchedulerMicrotask
  • create(type: 'animationFrame'): OIMEventQueueSchedulerAnimationFrame
  • create(type: 'timeout', options?: { delay: number }): OIMEventQueueSchedulerTimeout
  • create(type: 'immediate'): OIMEventQueueSchedulerImmediate
  • createMicrotask(): OIMEventQueueSchedulerMicrotask
  • createAnimationFrame(): OIMEventQueueSchedulerAnimationFrame
  • createTimeout(delay?: number): OIMEventQueueSchedulerTimeout
  • createImmediate(): OIMEventQueueSchedulerImmediate

Types

TOIMCollectionOptions<TEntity, TPk>

Collection configuration options:

  • selectPk?: TOIMPkSelector<TEntity, TPk> - Primary key selector function
  • store?: OIMCollectionStore<TEntity, TPk> - Storage backend
  • updateEntity?: TOIMEntityUpdater<TEntity> - Entity update strategy

TOIMEntityUpdater<TEntity>

Entity update function signature:

(newEntity: TEntity, oldEntity: TEntity) => TEntity

TOIMSchedulerType

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.