@oimdb/core 1.4.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt ADDED
@@ -0,0 +1,429 @@
1
+ # OIMDB
2
+
3
+ > **For AI assistants:** You are reading the OIMDB API reference. Read this file fully before writing any OIMDB code — the API has specific patterns that are easy to get wrong without context.
4
+ > After reading, look for `debug.ts` or `*.debug.ts` files in the project — they describe the specific data model (collections, indexes, relations, computeds) of the app you are working on.
5
+ > If `@oimdb/mcp` is in the project's MCP config, call `oimdb_inspect` for live runtime state from the browser.
6
+
7
+ OIMDB is a TypeScript in-memory reactive database. It stores entities in typed collections, maintains indexes, and propagates changes through a reactive graph (computed values, effects, selectors) controlled by an explicit event queue. All change delivery is batched: subscriptions fire on `queue.flush()`, never mid-mutation.
8
+
9
+ ## Packages
10
+
11
+ | Package | Purpose |
12
+ |---|---|
13
+ | `@oimdb/core` | Collections, indexes, event queue, reactive primitives |
14
+ | `@oimdb/persist` | Persist/hydrate collections to localStorage, IndexedDB, memory |
15
+ | `@oimdb/react` | React hooks for collections and selectors |
16
+ | `@oimdb/async` | Async-aware reactive collection and index variants |
17
+ | `@oimdb/devtools` | Debug registry: name collections/indexes/computeds, inspect runtime state |
18
+ | `@oimdb/snapshot-manager` | Snapshot/restore collection state |
19
+ | `@oimdb/redux-adapter` | Bridge OIMDB collections into a Redux store |
20
+
21
+ ## Core Concepts
22
+
23
+ ### Event Queue
24
+
25
+ All reactive updates are batched. Mutations accumulate, then `queue.flush()` delivers all changes at once.
26
+
27
+ ```ts
28
+ import { OIMEventQueue, OIMEventQueueSchedulerImmediate } from '@oimdb/core';
29
+
30
+ const queue = new OIMEventQueue({
31
+ scheduler: new OIMEventQueueSchedulerImmediate(), // flush on next microtask
32
+ });
33
+ // or: OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerTimeout, OIMEventQueueSchedulerMicrotask
34
+ ```
35
+
36
+ ### Collections
37
+
38
+ `OIMCollection` — base CRUD store. `OIMReactiveCollection` — same + emits events on flush.
39
+
40
+ ```ts
41
+ import { OIMReactiveCollection } from '@oimdb/core';
42
+
43
+ interface User { id: string; name: string; role: 'admin' | 'member'; }
44
+
45
+ const users = new OIMReactiveCollection<User, string>(queue, {
46
+ selectPk: (u) => u.id,
47
+ });
48
+
49
+ users.upsertMany([
50
+ { id: 'u1', name: 'Alice', role: 'admin' },
51
+ { id: 'u2', name: 'Bob', role: 'member' },
52
+ ]);
53
+ users.upsertOne({ id: 'u1', name: 'Alice Smith', role: 'admin' }); // update
54
+ users.removeOne({ id: 'u2', name: 'Bob', role: 'member' });
55
+
56
+ users.getOneByPk('u1'); // → entity or undefined
57
+ users.getAll(); // → TEntity[]
58
+ users.getAllPks(); // → TPk[]
59
+ ```
60
+
61
+ ### Indexes
62
+
63
+ Indexes map keys to sets or arrays of PKs. Two storage shapes:
64
+ - **Set-based** — unordered, deduped (`OIMIndexManualSetBased`, `OIMReactiveIndexManualSetBased`)
65
+ - **Array-based** — ordered, with duplicates (`OIMIndexManualArrayBased`, `OIMReactiveIndexManualArrayBased`)
66
+
67
+ ```ts
68
+ import { OIMReactiveIndexManualSetBased } from '@oimdb/core';
69
+
70
+ const byRole = new OIMReactiveIndexManualSetBased<'admin' | 'member', string>(queue);
71
+
72
+ byRole.setSlots('admin', [users.getSlotByPk('u1')!]);
73
+ byRole.getPksByKey('admin'); // → Set { 'u1' }
74
+
75
+ byRole.subscribeOnKey('admin', () => {
76
+ console.log('admin set changed');
77
+ });
78
+ queue.flush(); // delivers subscription
79
+ ```
80
+
81
+ ### DX Layer — Collection Model
82
+
83
+ `createOIMCollectionContext` wires queue + collection + index factory + selectors together.
84
+
85
+ ```ts
86
+ import { createOIMCollectionContext } from '@oimdb/core';
87
+
88
+ const { collection, indexFactory, select } =
89
+ createOIMCollectionContext<User, string>(queue, { selectPk: (u) => u.id });
90
+
91
+ // Derived index: built automatically from entity data
92
+ const byRole = indexFactory.derivedSetIndex((u) => [u.role]);
93
+
94
+ // Reactive selectors
95
+ const adminUsers = select.entitiesBySetIndexKey(byRole, 'admin');
96
+ adminUsers.getValue(); // → (User | undefined)[]
97
+ adminUsers.watch((users) => { /* called on queue.flush() when value changes */ });
98
+
99
+ const u1 = select.byPk('u1');
100
+ u1.getValue(); // → User | undefined
101
+ ```
102
+
103
+ ### Computed Values
104
+
105
+ Derived values that recompute lazily when dependencies change. Recomputation is scheduled on `queue.flush()`.
106
+
107
+ ```ts
108
+ import { OIMComputeRuntime, OIMComputed, OIMEffectDependencyKeyedCollection } from '@oimdb/core';
109
+
110
+ const runtime = new OIMComputeRuntime(queue);
111
+
112
+ const adminCount = new OIMComputed<number>(runtime, {
113
+ compute: () => byRole.getPksByKey('admin').size,
114
+ deps: [new OIMEffectDependencyKeyedCollection(users, /* pk */ 'u1')],
115
+ });
116
+
117
+ adminCount.get(); // → number (triggers recompute if dirty)
118
+ adminCount.getIfReady(); // → number | undefined (no recompute)
119
+ adminCount.isReady; // → boolean
120
+ adminCount.needsRecompute; // → boolean
121
+ ```
122
+
123
+ ### Effects
124
+
125
+ Side effects that run when dependencies change, on `queue.flush()`.
126
+
127
+ ```ts
128
+ import { OIMEffect, OIMEffectDependencyComputed } from '@oimdb/core';
129
+
130
+ const logEffect = new OIMEffect(runtime, {
131
+ deps: [new OIMEffectDependencyComputed(adminCount)],
132
+ run: () => {
133
+ console.log('admin count:', adminCount.get());
134
+ },
135
+ });
136
+ ```
137
+
138
+ **Dependency types:**
139
+ - `OIMEffectDependencyKeyedCollection(collection, pk)` — watch a specific PK
140
+ - `OIMEffectDependencyKeyedObject(object, key)` — watch a key in a reactive object
141
+ - `OIMEffectDependencyKeyedIndex(index, key)` — watch a key in an index
142
+ - `OIMEffectDependencyComputed(computed)` — depend on another computed
143
+
144
+ ### Objects
145
+
146
+ Key-value stores for singleton data (settings, flags, etc.).
147
+
148
+ ```ts
149
+ import { OIMReactiveObject } from '@oimdb/core';
150
+
151
+ const settings = new OIMReactiveObject<'theme' | 'lang', string>(queue);
152
+ settings.setProperty('theme', 'dark');
153
+ settings.get('theme'); // → 'dark' | undefined
154
+ settings.getAll(); // → { theme: 'dark' }
155
+ ```
156
+
157
+ ## React (`@oimdb/react`)
158
+
159
+ ```bash
160
+ npm install @oimdb/react
161
+ ```
162
+
163
+ All hooks use `useSyncExternalStore` and re-render only when watched data changes.
164
+
165
+ ```ts
166
+ import {
167
+ OIMCollectionsProvider,
168
+ useOIMCollectionsContext,
169
+ useSelectEntityByPk,
170
+ useSelectEntitiesByPks,
171
+ useSelectEntitiesByIndexKeySetBased,
172
+ useSelectEntitiesByIndexKeyArrayBased,
173
+ useSelectPksByIndexKeySetBased,
174
+ useSelectValueByObjectKey,
175
+ } from '@oimdb/react';
176
+ ```
177
+
178
+ ### Setup
179
+
180
+ ```ts
181
+ // store.ts — outside React
182
+ const queue = new OIMEventQueue();
183
+ const { collection: users, indexFactory } =
184
+ createOIMCollectionContext<User, string>(queue, { selectPk: (u) => u.id });
185
+ const byTeam = indexFactory.derivedSetIndex((u) => [u.teamId]);
186
+ export const collections = { users };
187
+
188
+ // App.tsx
189
+ <OIMCollectionsProvider collections={collections}>
190
+ <App />
191
+ </OIMCollectionsProvider>
192
+
193
+ // Component.tsx
194
+ type AppCollections = typeof collections;
195
+ const { users } = useOIMCollectionsContext<AppCollections>();
196
+ ```
197
+
198
+ ### Hook reference
199
+
200
+ | Hook | What it watches |
201
+ |---|---|
202
+ | `useSelectEntityByPk(collection, pk)` | One entity by PK |
203
+ | `useSelectEntitiesByPks(collection, pks)` | Multiple entities by PKs |
204
+ | `useSelectEntitiesByIndexKeySetBased(collection, index, key)` | Entities via set index key |
205
+ | `useSelectEntitiesByIndexKeysSetBased(collection, index, keys)` | Entities via multiple set index keys |
206
+ | `useSelectEntitiesByIndexKeyArrayBased(collection, index, key)` | Entities via array index key (ordered) |
207
+ | `useSelectEntitiesByIndexKeysArrayBased(collection, index, keys)` | Entities via multiple array index keys |
208
+ | `useSelectPksByIndexKeySetBased(index, key)` | Just PKs from set index (no collection needed) |
209
+ | `useSelectPksByIndexKeyArrayBased(index, key)` | Just PKs from array index |
210
+ | `useSelectValueByObjectKey(object, key)` | One key from `OIMReactiveObject` |
211
+ | `useSelectValuesByObjectKeys(object, keys)` | Multiple keys from `OIMReactiveObject` |
212
+
213
+ ## Persist (`@oimdb/persist`)
214
+
215
+ ```ts
216
+ import { createLocalStoragePersistor } from '@oimdb/persist';
217
+
218
+ const persistor = createLocalStoragePersistor({});
219
+
220
+ // Persist entire collection as one key
221
+ persistor.collection(users).entry({ storageKey: 'app:users' });
222
+
223
+ // Persist at a nested path within one key
224
+ persistor.collection(users).path({ storageKey: 'app', path: ['collections', 'users'] });
225
+ persistor.object(settings).path({ storageKey: 'app', path: ['settings'] });
226
+
227
+ // Persist each record separately (good for large collections)
228
+ persistor.collection(users).records({ bucketName: 'users' }); // memory/indexedDb only
229
+
230
+ await persistor.hydrate(); // load from storage into collections
231
+ persistor.start(); // autosave on every queue.flush()
232
+ ```
233
+
234
+ ### Error handling
235
+
236
+ ```ts
237
+ const persistor = createLocalStoragePersistor({
238
+ onError: (error, { resource, operation }) => {
239
+ console.error(`Persist ${operation} failed`, error);
240
+ },
241
+ });
242
+ ```
243
+
244
+ ### Schema versioning / migration
245
+
246
+ ```ts
247
+ import { createVersionedCodec } from '@oimdb/persist';
248
+
249
+ const codec = createVersionedCodec({
250
+ version: 2,
251
+ migrations: {
252
+ 1: (data) => ({ ...data, newField: 'default' }), // v0 → v1
253
+ 2: (data) => renameField(data), // v1 → v2
254
+ },
255
+ });
256
+
257
+ persistor.collection(users).entry({ storageKey: 'users', codec });
258
+ ```
259
+
260
+ ## Devtools (`@oimdb/devtools`)
261
+
262
+ Register collections and computeds by name. Keep registration in separate `*.debug.ts` files — excluded from production.
263
+
264
+ ```ts
265
+ // src/store/users.debug.ts
266
+ import { registry } from '@oimdb/devtools';
267
+ import { usersCollection, byRole } from './users';
268
+
269
+ registry.collection('users', usersCollection, {
270
+ indexes: { byRole },
271
+ });
272
+ ```
273
+
274
+ ```ts
275
+ // src/store/tasks.debug.ts
276
+ import { registry } from '@oimdb/devtools';
277
+ import { tasksCollection, byAssignee, openTaskCount } from './tasks';
278
+
279
+ registry
280
+ .collection('tasks', tasksCollection, {
281
+ indexes: { byAssignee },
282
+ relations: { assigneeId: 'users' }, // FK: tasks.assigneeId → users
283
+ })
284
+ .computed('openTaskCount', openTaskCount);
285
+ ```
286
+
287
+ ```ts
288
+ // src/debug.ts — single debug entry point
289
+ import './store/users.debug';
290
+ import './store/tasks.debug';
291
+
292
+ import { registry } from '@oimdb/devtools';
293
+ if (typeof window !== 'undefined') {
294
+ (window as Record<string, unknown>).__OIMDB_DEV__ = registry;
295
+ }
296
+ ```
297
+
298
+ ```ts
299
+ registry.dump(); // prints full state summary to console
300
+ registry.inspect(); // returns structured JSON
301
+ ```
302
+
303
+ When a computed dep's source is not registered, it shows as `(unregistered)` in `dump()` — a signal to go add it to the debug file.
304
+
305
+ Install the browser extension from `packages/browser-extension/` (load unpacked in Chrome) for a live DevTools panel.
306
+
307
+ ## Naming Conventions
308
+
309
+ | Kind | Prefix | Example |
310
+ |---|---|---|
311
+ | Class | `OIM` | `OIMCollection` |
312
+ | Interface | `IOIM` | `IOIMCollectionStore` |
313
+ | Type | `TOIM` | `TOIMCollectionOptions` |
314
+ | Enum | `EOIM` | `EOIMCollectionEventType` |
315
+
316
+ Variable names: drop the prefix, start lowercase. Context goes first: `prevCollectionIndexFactory`, not `collectionIndexFactoryPrev`.
317
+
318
+ ## File Conventions
319
+
320
+ - One file → one named export
321
+ - Filename matches export name exactly
322
+ - Types → `src/types/`, Interfaces → `src/interfaces/`, Abstract classes → `src/abstract/`
323
+
324
+ ## Common Patterns
325
+
326
+ ### Wire up a complete feature
327
+
328
+ ```ts
329
+ const queue = new OIMEventQueue({ scheduler: new OIMEventQueueSchedulerImmediate() });
330
+
331
+ const { collection: tasks, indexFactory, select } =
332
+ createOIMCollectionContext<Task, string>(queue, { selectPk: (t) => t.id });
333
+
334
+ const byStatus = indexFactory.derivedSetIndex((t) => [t.status]);
335
+ const byAssignee = indexFactory.derivedSetIndex((t) => [t.assigneeId]);
336
+
337
+ const runtime = new OIMComputeRuntime(queue);
338
+
339
+ const openCount = new OIMComputed<number>(runtime, {
340
+ compute: () => byStatus.getPksByKey('open').size,
341
+ deps: [new OIMEffectDependencyKeyedIndex(byStatus, 'open')],
342
+ });
343
+ ```
344
+
345
+ ### Subscribe to a collection index
346
+
347
+ ```ts
348
+ const unsubscribe = byStatus.subscribeOnKey('open', () => {
349
+ console.log('open tasks:', byStatus.getPksByKey('open'));
350
+ });
351
+ // ...later
352
+ unsubscribe();
353
+ ```
354
+
355
+ ### Destroy lifecycle (always call destroy in reverse order)
356
+
357
+ ```ts
358
+ logEffect.destroy();
359
+ openCount.destroy();
360
+ byStatus.destroy();
361
+ tasks.destroy();
362
+ queue.destroy();
363
+ ```
364
+
365
+ ## DevTools UI (`@oimdb/mcp devtools`)
366
+
367
+ For human developers — runs a local web UI at `http://localhost:7433`:
368
+
369
+ ```bash
370
+ npx @oimdb/mcp devtools
371
+ ```
372
+
373
+ Auto-opens in the browser. Shows live collections, computeds, and dep status from the running app. Requires `registry.connect()` in `debug.ts`.
374
+
375
+ ## MCP Setup (`@oimdb/mcp`)
376
+
377
+ `@oimdb/mcp` lets AI assistants query live OIMDB state from the running browser tab.
378
+
379
+ ### Check if MCP is already configured
380
+
381
+ Look for `mcpServers.oimdb` in `.claude/settings.json` (project) or `~/.claude/settings.json` (global). If present, call `oimdb_inspect` directly — no setup needed.
382
+
383
+ ### Configure MCP (if not already done)
384
+
385
+ Add to the project MCP config and ask the user to reload their editor:
386
+
387
+ - Claude Code: `.claude/settings.json`
388
+ - Cursor: `.cursor/mcp.json`
389
+
390
+ ```json
391
+ {
392
+ "mcpServers": {
393
+ "oimdb": {
394
+ "command": "npx",
395
+ "args": ["-y", "@oimdb/mcp"]
396
+ }
397
+ }
398
+ }
399
+ ```
400
+
401
+ ### Connect the browser
402
+
403
+ The project's `debug.ts` (or `src/debug.ts`) must call `registry.connect()`:
404
+
405
+ ```ts
406
+ import { registry } from '@oimdb/devtools';
407
+ registry.connect(); // ws://localhost:7432
408
+ ```
409
+
410
+ If this line is missing, add it and ask the user to reload the page.
411
+
412
+ ### Available tools
413
+
414
+ | Tool | Use it when |
415
+ |---|---|
416
+ | `oimdb_inspect` | Need full state — collections, computeds, deps |
417
+ | `oimdb_dump` | Need a quick readable summary |
418
+ | `oimdb_collection` | Debugging one specific collection |
419
+ | `oimdb_computed` | Debugging why a computed is stale or wrong |
420
+
421
+ ### One-shot CDP (no MCP config needed)
422
+
423
+ If Chrome is running with `--remote-debugging-port=9222`:
424
+
425
+ ```bash
426
+ npx @oimdb/mcp cdp
427
+ ```
428
+
429
+ Prints full `inspect()` JSON to stdout. Use when MCP is not configured.
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "1.4.1",
3
+ "version": "2.0.0",
4
4
  "description": "Core in-memory data library with type-safe indices, reactive subscriptions, and event processing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "sideEffects": false,
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
10
13
  "repository": {
11
14
  "type": "git",
12
15
  "url": "git+https://github.com/abaikov/oimdb.git"
@@ -27,10 +30,12 @@
27
30
  "test": "jest",
28
31
  "type-check": "tsc --noEmit",
29
32
  "clean": "rimraf dist",
33
+ "prepack": "node -e \"require('fs').copyFileSync('../../llms.txt', 'llms.txt')\"",
30
34
  "prepublishOnly": "npm run type-check && npm test && npm run build"
31
35
  },
32
36
  "files": [
33
- "dist"
37
+ "dist",
38
+ "llms.txt"
34
39
  ],
35
40
  "keywords": [
36
41
  "oimdb",