@oimdb/core 3.7.0 → 3.8.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
@@ -114,6 +114,35 @@ const active = indexFactory.derivedSetGlobalIndex({ filter: (u) => !u.archived }
114
114
  - Selectors: `select.entitiesByArrayGlobalIndex(list)` / `select.entitiesBySetGlobalIndex(set)` → `(TEntity | undefined)[]` (no key).
115
115
  - React: `useSelectEntitiesByGlobalIndex{Array,Set}Based(collection, index)`, `useSelectPksByGlobalIndex{Array,Set}Based(index)`, plus `…Signal` variants — all drop the `key` argument.
116
116
 
117
+ ### Composite (key-path) indexes
118
+
119
+ When the key is a **combination of values** (e.g. `[userId, projectId, role]`) rather than one primitive, use a **composite** set-based index instead of concatenating a string key. Create it with `indexFactory.compositeSetIndex()`. The key is a `TOIMKeyPath` — an ordered, **arbitrary-length** tuple of primitive segments (`readonly (string | number)[]`). Every method that takes a `key` takes a path array instead; everything else (pk writes, slot resolution, reactivity, `getEntitiesByKey` holes) is identical to `setBasedIndex`.
120
+
121
+ ```ts
122
+ const membersByScope = indexFactory.compositeSetIndex();
123
+ membersByScope.setPks(['u1', 'p1', 'admin'], ['m1', 'm2']); // also addPks / removePks
124
+ membersByScope.getPksByKey(['u1', 'p1', 'admin']); // Set { 'm1', 'm2' }
125
+ membersByScope.getEntitiesByKey(['u1', 'p1', 'admin']); // (Member | undefined)[]
126
+ membersByScope.subscribeOnKey(['u1', 'p1', 'admin'], () => {/* … */});
127
+ queue.flush();
128
+ ```
129
+
130
+ - **Matched by content, not reference / string.** A freshly built `['u1','p1','admin']` resolves to the same bucket as the one written earlier — no need to reuse the array instance. Segments keep their own type (`1` ≠ `'1'`) and no separator can collide (`['a|b','c']` ≠ `['a','b|c']`), unlike a `"a|b|c"` string key.
131
+ - **Full path is the lookup unit.** O(arity) — effectively O(1) for a fixed arity — a lookup walks one native-`Map` level per segment. No partial/prefix queries (subscribe/lookup are by the whole path). Paths of different lengths coexist (`['a','b']` and `['a','b','c']` never collide).
132
+ - **No perf cost to primitive indexes.** It is a distinct opt-in index: `setBasedIndex` and all others keep their native-`Map` fast path untouched. The trie cost is paid only where you use a composite index.
133
+ - **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
+ - **Selectors:** `select.entitiesByCompositeSetIndexKey(index, path)` / `select.entitiesByCompositeArrayIndexKey(index, path)` → `(TEntity | undefined)[]`, tracking both index and entity changes.
135
+ - **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
+
138
+ ### Composite PRIMARY key (collection keyed by a tuple)
139
+
140
+ Beyond composite index *keys*, a **collection's PK itself** can be a composite path. Pass a trie store: `new OIMReactiveCollection(queue, { selectPk: e => [e.userId, e.projectId], store: new OIMCollectionStoreTrieDriven() })`. Then `getOneByPk([1,10])`, `subscribeOnKey([1,10])`, `removeOneByPk([1,10])` all match by content; the store interns each logical PK to one canonical `slot.pk` reference so downstream native `Set<pk>` stays correct. Primitive-PK collections keep `OIMCollectionStoreMapDriven` (native `Map`) untouched — no regression.
141
+
142
+ - **Indexes work over a composite-PK collection.** `indexFactory.setBasedIndex()` (or `compositeSetIndex`) on such a collection indexes composite PKs: `byRole.setPks('admin', [[1,10],[2,10]])`, `getPksByKey`, `addPks`, `removePks([[2,10]])` all match PKs by content (the per-bucket pk→slot membership uses the collection's trie key domain).
143
+ - **Serialization boundaries.** `@oimdb/persist` (stores entities as an array, re-derives the PK via `selectPk` on hydrate) and `@oimdb/snapshot-manager` (stores the PK as a value) round-trip a composite PK with **no codec** — just pass the composite-PK collection. **`@oimdb/redux-adapter`** keys Redux state by string (`Record<pk, entity>`), so a composite PK needs an `IOIMPkCodec`: pass it to `createCollectionReducer(collection, child?, mapper?, pkCodec)` — one codec drives both the default mapper (encode: `entities` keyed by JSON-encoded PK, `ids` keep the raw tuple) AND child write-back / linked-index sync (decode: Redux string key → raw PK). `OIMPkCodecKeyPath` is the ready codec. (Or pass `mapper: createDefaultCollectionMapper(codec)` directly for a read-only projection.)
144
+ - New exports: `OIMCollectionStoreTrieDriven`, `OIMKeyDomainTrie` / `OIMKeyDomainNative` (`IOIMKeyDomain`), `OIMTrieSet`, `IOIMPkCodec` / `OIMPkCodecKeyPath`. The collection/index PK generics accept `TPk extends TOIMKey`.
145
+
117
146
  ### Ordered list command stream
118
147
 
119
148
  For an ordered per-key list that emits **position-addressed commands** (insert / remove / move / set / reset) instead of a new array to diff — for imperative renderers that apply edits in place. `OIMCollectionOrderedListCommandStream` (pk writes, via `indexFactory.orderedList()`) or `OIMOrderedListCommandStream` (slot writes). Commands are not in the event payload — subscribe, then pull synchronously.
@@ -462,6 +491,8 @@ unsubscribe();
462
491
 
463
492
  ### Destroy lifecycle (always call destroy in reverse order)
464
493
 
494
+ Teardown is LIFO: dispose dependents before what they depend on — indexes / streams / selectors before the collection, the collection before the queue. By hand:
495
+
465
496
  ```ts
466
497
  logEffect.destroy();
467
498
  openCount.destroy();
@@ -470,6 +501,18 @@ tasks.destroy();
470
501
  queue.destroy();
471
502
  ```
472
503
 
504
+ **`OIMDisposeScope`** does this for you — register as you build, tear everything down in one LIFO call. It accepts both `{ destroy(): void }` objects AND bare `() => void` unsubscribe functions (selectors' `watch`, per-key `subscribeOnKey`, `emitter.on`, scheduler tasks expose only the function form). `add(x)` returns `x` for inline capture; idempotent; every item is disposed even if one throws (first error rethrown after); `child()` nests a sub-scope. `createOIMDisposeScope()` factory. New exports: `OIMDisposeScope`, `createOIMDisposeScope`, `IOIMDisposable` (`{ destroy(): void }`), `TOIMDisposable` (`IOIMDisposable | (() => void)`).
505
+
506
+ ```ts
507
+ const scope = new OIMDisposeScope();
508
+ const queue = scope.add(new OIMEventQueue());
509
+ const tasks = scope.add(new OIMReactiveCollection(queue, { selectPk: t => t.id }));
510
+ const byStatus = scope.add(tasks.indexFactory.setBasedIndex());
511
+ scope.add(select.entitiesBySetIndexKey(byStatus, 'open').watch(render));
512
+ // …later — one call, correct LIFO order, no manual bookkeeping:
513
+ scope.destroy();
514
+ ```
515
+
473
516
  ## DevTools UI (`@oimdb/mcp devtools`)
474
517
 
475
518
  For human developers — runs a local web UI at `http://localhost:7433`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.7.0",
3
+ "version": "3.8.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",