npm package: @oimdb/core
Description: Core in-memory data library with type-safe indices, reactive subscriptions, and event processing
Author: Andrei Baikov
Latest version: 1.4.1

# @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

```bash
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

```typescript
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: 'john@example.com' });
users.upsertMany([
    { id: 'user2', name: 'Jane Smith', email: 'jane@example.com' },
    { id: 'user3', name: 'Bob Wilson', email: 'bob@example.com' }
]);

// 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)

```typescript
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)

```typescript
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

```typescript
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)

```typescript
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: 'john@test.com', teamId: 'engineering', role: 'admin' },
    { id: 'u2', name: 'Jane', email: 'jane@test.com', teamId: 'engineering', role: 'user' }
]);

// Update indexes manually
users.indexes.byTeam.setPks('engineering', ['u1', 'u2']);
users.indexes.byRole.setPks('admin', ['u1']);
```

### Custom Entity Updater

```typescript
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 existing
```

### Event Coalescing and Update Subscriptions

```typescript
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:

```typescript
// 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:

```typescript
// These three updates...
users.upsertOne({ id: 'user1', name: 'John' });
users.upsertOne({ id: 'user1', email: 'john@test.com' });
users.upsertOne({ id: 'user1', role: 'admin' });

// ...result in only one notification with the final state
// This prevents unnecessary re-renders and improves performance
```

### 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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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`.

```typescript
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:**

```typescript
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):**

```typescript
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:**

```typescript
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:

```typescript
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:

```typescript
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](../redux-adapter/README.md) for complete migration guide and examples.

### Standalone Usage

Use core classes directly for maximum control:

```typescript
// 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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:

```typescript
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:
```typescript
(newEntity: TEntity, oldEntity: TEntity) => TEntity
```

#### `TOIMSchedulerType`
Available scheduler types:
```typescript
'microtask' | 'animationFrame' | 'timeout' | 'immediate'
```

#### `TOIMEventQueueOptions`
Event queue configuration:
- `scheduler?: OIMEventQueueScheduler` - Optional scheduler for automatic flushing

## 🧪 Testing

```typescript
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.