@oimdb/core 3.8.0 → 3.9.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 CHANGED
@@ -13,6 +13,7 @@ OIMDB is a TypeScript in-memory reactive database. It stores entities in typed c
13
13
  | `@oimdb/core` | Collections, indexes, event queue, reactive primitives |
14
14
  | `@oimdb/persist` | Storage-agnostic persist/hydrate engine (source adapters, resources, versioned codec). Backends ship separately: `@oimdb/persist-memory`, `-localstorage`, `-idb`, `-async-kv`, `-json` |
15
15
  | `@oimdb/react` | React hooks for collections and selectors |
16
+ | `@oimdb/exodra` | [Exodra](https://exodra.org) adapter: OIMDB selectors/computeds → read-only `TExoBindable` (no React) |
16
17
  | `@oimdb/async` | Async-aware reactive collection and index variants |
17
18
  | `@oimdb/devtools` | Debug registry: name collections/indexes/computeds, inspect runtime state |
18
19
  | `@oimdb/snapshot-manager` | Snapshot/restore collection state |
@@ -133,7 +134,7 @@ queue.flush();
133
134
  - **Ordered variant:** `indexFactory.compositeArrayIndex()` — same key path, but buckets are ordered slot arrays (`getPksByKey` returns an array in write order). Use it when order matters per composite key (e.g. `[channelId, threadId]` messages).
134
135
  - **Selectors:** `select.entitiesByCompositeSetIndexKey(index, path)` / `select.entitiesByCompositeArrayIndexKey(index, path)` → `(TEntity | undefined)[]`, tracking both index and entity changes.
135
136
  - **React:** `useSelectEntitiesByCompositeIndexKey{Set,Array}Based(collection, index, path)` and `useSelectPksByCompositeIndexKey{Set,Array}Based(index, path)`. Unlike the primitive hooks (which stabilize the key by `===`), these stabilize the path **by content**, so a freshly built `['p1','admin']` on every render does not re-subscribe.
136
- - New exports: `OIMReactiveCollectionIndexCompositeTrie{Set,Array}Based`, `OIMIndexStoreTrieDriven{Set,Array}Based` (inject via `indexOptions.store` for a custom key path store), `OIMKeyPathCarrierResolver`, `OIMTrieMap` (the generic N-level trie util), types `TOIMKeyPath` / `TOIMKey`. The keyed-emitter / index generics accept `TKey extends TOIMKey` (= `TOIMPk | TOIMKeyPath`) — a pure widening; primitive keys are unaffected.
137
+ - New exports: `OIMReactiveCollectionIndexCompositeTrie{Set,Array}Based`, `OIMIndexStoreTrieDriven{Set,Array}Based` (inject via `indexOptions.store` for a custom key path store), `OIMKeyPathCarrierProvider`, `OIMTrieMap` (the generic N-level trie util), types `TOIMKeyPath` / `TOIMKey`. The keyed-emitter / index generics accept `TKey extends TOIMKey` (= `TOIMPk | TOIMKeyPath`) — a pure widening; primitive keys are unaffected.
137
138
 
138
139
  ### Composite PRIMARY key (collection keyed by a tuple)
139
140
 
@@ -256,6 +257,8 @@ const logEffect = new OIMEffect(runtime, {
256
257
  - `OIMEffectDependencyKeyedIndex(index, key)` — watch a key in an index
257
258
  - `OIMEffectDependencyComputed(computed)` — depend on another computed
258
259
 
260
+ Each keyed dependency watches exactly **one** key — the key argument is the whole key, so a composite key/PK path `[a, b]` is one key (never split into `a`, `b`). For a composite-PK collection or composite index, pass the tuple: `new OIMEffectDependencyKeyedCollection(reqs, ['t1', 'open'])`. To depend on several keys, add several dependencies to the effect's `deps` array.
261
+
259
262
  ### Objects
260
263
 
261
264
  Key-value stores for singleton data (settings, flags, etc.).
@@ -346,6 +349,42 @@ Signal hooks: `useSelectEntityByPkSignal(collection, pk)`, `useSelectPksByIndexK
346
349
 
347
350
  Trade-offs (use only when every reader is subscription-driven): NOT Concurrent-safe; the entity reference is stable across changes, so `React.memo` on entities, prev/next diffing, time-travel, and the default uSES hooks on the same collection won't see updates. Default to the standard immutable hooks; reach for this only when a profile shows the data layer is the bottleneck.
348
351
 
352
+ ## Exodra (`@oimdb/exodra`)
353
+
354
+ ```bash
355
+ npm install @oimdb/exodra @exodra/reactivity @oimdb/core
356
+ ```
357
+
358
+ The [Exodra](https://exodra.org) counterpart of `@oimdb/react`: instead of a hook + re-render, every OIMDB source becomes a read-only Exodra bindable (`TExoBindable<T> = { getValue(): T; subscribe(update): () => void }`). No React. Built on `@oimdb/core`'s `OIMSelector` (`{ getValue, watch }`) — the fine-grained subscription already lives there, so this is a thin adapter. Bindables are read-only, lazy (subscribe upstream only while they have a downstream subscriber → cost O(visible)) and SSR-safe (`getValue()` reads fresh, valid without an active subscription).
359
+
360
+ ```ts
361
+ import { fromSelector, bindSelectors, fromComputed, combine, entityRows } from '@oimdb/exodra';
362
+ import { bindable, derive } from '@exodra/reactivity';
363
+
364
+ // wrap any OIMSelector
365
+ const alice = fromSelector(kit.select.byPk('u1')); // TExoBindable<User | undefined>
366
+
367
+ // mirror OIMCollectionSelectors → bindables; keys accept a bindable (reactive key re-points the sub)
368
+ const bound = bindSelectors(kit.select);
369
+ const selectedTeam = bindable('team-1');
370
+ const members = bound.entitiesBySetIndexKey(usersByTeam, selectedTeam);
371
+ // <ul bindable={{ children: entityRows(memberPks, pk => bound.byPk(pk), (u, pk) => renderRow(u)) }} />
372
+ ```
373
+
374
+ | Export | Purpose |
375
+ |---|---|
376
+ | `fromSelector(selector, opts?)` | primary adapter: `OIMSelector<T>` → `TExoBindable<T>` |
377
+ | `fromOimdb(read, subscribe, opts?)` | escape hatch for a raw read/subscribe pair |
378
+ | `fromSelectorFactory(key, makeSelector, opts?)` | reactive key (`TKey \| bindable`) → rebuilds the selector on change |
379
+ | `fromComputed(computed, opts?)` | `OIMComputed<T>` → `TExoBindable<T>` |
380
+ | `combine(sources, fn)` | multi-source derive (Exodra's own `derive` is single-source) |
381
+ | `bindSelectors(selectors, defaults?)` | mirror of `OIMCollectionSelectors`, each method returning a bindable |
382
+ | `keyedChildren(source, { key, render })` / `entityRows(order, bindEntity, render)` | identity-stable list children (field edit = reconcile no-op, focus-safe) |
383
+ | `listFromCommandStream(stream, key, render)` | O(delta) path: OIMDB ordered command stream → Exodra `bindableList` (commands map 1:1 to `TExoListOp`) |
384
+ | `read*/subscribe*` (6 fns) | low-level pairs mirroring an inline app bridge 1:1 (find-replace migration) |
385
+
386
+ Options `{ equals?, alwaysNotify? }`: `equals` defaults to `Object.is`; `alwaysNotify: true` for in-place entity updaters (stable ref, where `Object.is` would swallow the update). `bindSelectors` takes `kit.select` (not a bare collection) because the collection's queue is private and the selectors own a queue-bound compute runtime. Exodra bucket bindings subscribe at *build* (not `onExoMount`); the low-level `subscribe*` fns cover `onExoMount`-scoped needs. Write-side is intentionally omitted (apps write via orchestration, e.g. cnstra, not from views).
387
+
349
388
  ## Persist (`@oimdb/persist`)
350
389
 
351
390
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.8.0",
3
+ "version": "3.9.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",
@@ -29,6 +29,7 @@
29
29
  "build": "tsup src/index.ts --dts --format esm,cjs",
30
30
  "test": "jest",
31
31
  "type-check": "tsc --noEmit",
32
+ "bench": "tsx scripts/run-bench.ts",
32
33
  "clean": "rimraf dist",
33
34
  "prepack": "node -e \"require('fs').copyFileSync('../../llms.txt', 'llms.txt')\"",
34
35
  "prepublishOnly": "npm run type-check && npm test && npm run build"