@oimdb/core 3.6.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/README.md +20 -0
- package/dist/index.cjs +1332 -270
- package/dist/index.d.cts +984 -230
- package/dist/index.d.ts +984 -230
- package/dist/index.js +1314 -270
- package/llms.txt +44 -1
- package/package.json +1 -1
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.
|
|
@@ -156,7 +185,7 @@ const stream = createOIMOrderedListCommandStreamDiffDriven(queue, cardsByDeck);
|
|
|
156
185
|
stream.subscribeCommands('deck1', () => engine.apply(stream.consumeCommands('deck1'))); // insert/move/remove
|
|
157
186
|
```
|
|
158
187
|
|
|
159
|
-
**Mapping a plain index to stable objects** — for an ordinary set/array index (no commands), `createOIMIndexSlotMap(index, create)` / `OIMIndexSlotMap` memoizes `create(slot)` by slot identity (a `WeakMap`): the same slot returns the same object reference on every read, so `React.memo` / `Object.is` diffing works and view-models are reused. Same plain-function shape as the ordered mapper, no `destroy` (plain values, GC reclaims). `getByKey(key)` → mapped objects; `subscribeOnKey`/`subscribeOnKeys` passthrough to the index. New exports: `OIMIndexSlotMap`, `createOIMIndexSlotMap`, `IOIMIndexSlotSource`.
|
|
188
|
+
**Mapping a plain index to stable objects** — for an ordinary set/array index (no commands), `createOIMIndexSlotMap(index, create)` / `OIMIndexSlotMap` memoizes `create(slot)` by slot identity (a `WeakMap`): the same slot returns the same object reference on every read, so `React.memo` / `Object.is` diffing works and view-models are reused. Same plain-function shape as the ordered mapper, no `destroy` (plain values, GC reclaims). `getByKey(key)` → mapped objects; `.map(create)` chains; `subscribeOnKey`/`subscribeOnKeys` passthrough to the index. For a keyless global index use `createOIMGlobalIndexSlotMap(index, create)` / `OIMGlobalIndexSlotMap` — same memo with `getAll()` and `subscribe()` instead of the keyed variants. New exports: `OIMIndexSlotMap`, `createOIMIndexSlotMap`, `IOIMIndexSlotSource`, `OIMGlobalIndexSlotMap`, `createOIMGlobalIndexSlotMap`, `IOIMGlobalIndexSlotSource`.
|
|
160
189
|
|
|
161
190
|
```ts
|
|
162
191
|
const usersByTeam = users.indexFactory.derivedSetIndex((u) => u.teamId);
|
|
@@ -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`:
|