@oimdb/core 3.4.0 → 3.6.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
@@ -28,9 +28,13 @@ All reactive updates are batched. Mutations accumulate, then `queue.flush()` del
28
28
  import { OIMEventQueue, OIMEventQueueSchedulerImmediate } from '@oimdb/core';
29
29
 
30
30
  const queue = new OIMEventQueue({
31
- scheduler: new OIMEventQueueSchedulerImmediate(), // flush on next microtask
31
+ scheduler: new OIMEventQueueSchedulerImmediate(), // macrotask: setImmediate (Node), fallback MessageChannel / setTimeout(0) (browser)
32
32
  });
33
- // or: OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerTimeout, OIMEventQueueSchedulerMicrotask
33
+ // or: OIMEventQueueSchedulerMicrotask (next microtask, before paint — lowest latency),
34
+ // OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerTimeout
35
+ // Async schedulers coalesce; call queue.flush() directly for sync delivery (e.g. tests).
36
+ // OIMEventQueueSchedulerSync (createSync()) flushes synchronously per write, NO batching —
37
+ // debug/diagnostic only: swap in at the root to test if a bug is coalescing-related.
34
38
  ```
35
39
 
36
40
  ### Collections
@@ -84,6 +88,82 @@ Membership writes are incremental (touch only the changed pks):
84
88
  - `setPks` / `addPks` resolve each pk → slot (O(1) lookup); if you already hold slots (`collection.upsertMany` returns them) use `setSlots` / `appendSlots` to skip the lookup.
85
89
  - `getEntitiesByKey(key)` returns `(TEntity | undefined)[]` — positional holes for not-yet-loaded pks, aligned with `getPksByKey` (not dropped), so you can render a loading state.
86
90
 
91
+ ### Global (keyless / whole-collection) indexes
92
+
93
+ When you need ONE index/ordered list over the **whole collection** ("all") there is no meaningful key. Use a **Global** index instead of inventing a phantom key like `'all'`. Same two shapes (array-based ordered / set-based unordered) and two modes (manual / derived), but keyless: `getPks()` / `getSlots()` / `getEntities()` take no key, and `subscribe(handler)` replaces `subscribeOnKey`. Internally a Global index holds ONE bucket with a single-carrier emitter (no key→carrier map / hashing), so it is strictly cheaper than a keyed index with one key.
94
+
95
+ Classes mirror the keyed ones with `Global` inserted: `OIMGlobalIndexManual{Array,Set}Based` (raw slots), `OIMReactiveGlobalIndexManual{Array,Set}Based`, `OIMReactiveCollectionGlobalIndexManual{Array,Set}Based` (pk writes), `OIMDerivedCollectionGlobalIndex{Array,Set}Based`.
96
+
97
+ ```ts
98
+ // Manual, collection-bound (pk writes) — e.g. a server-provided page order
99
+ const list = indexFactory.arrayBasedGlobalIndex();
100
+ list.setPks(['u2', 'u1']); // also addPks / removePks (no key)
101
+ list.getPks(); // ['u2', 'u1']
102
+ list.subscribe(() => { /* one bucket changed */ });
103
+ queue.flush();
104
+
105
+ // Derived — auto-tracks the whole collection, ordered
106
+ const recent = indexFactory.derivedArrayGlobalIndex({ orderBy: (u) => u.createdAt });
107
+ recent.getEntities(); // (User | undefined)[] in order, kept in sync
108
+
109
+ // Derived set — whole-collection membership; `filter` narrows it
110
+ const active = indexFactory.derivedSetGlobalIndex({ filter: (u) => !u.archived });
111
+ ```
112
+
113
+ - Derived options: `orderBy` / `compareEntities` (array only) for order, `filter: (e) => boolean` to include a subset (default: entity exists), `buildInitial` (default true).
114
+ - Selectors: `select.entitiesByArrayGlobalIndex(list)` / `select.entitiesBySetGlobalIndex(set)` → `(TEntity | undefined)[]` (no key).
115
+ - React: `useSelectEntitiesByGlobalIndex{Array,Set}Based(collection, index)`, `useSelectPksByGlobalIndex{Array,Set}Based(index)`, plus `…Signal` variants — all drop the `key` argument.
116
+
117
+ ### Ordered list command stream
118
+
119
+ 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.
120
+
121
+ ```ts
122
+ import { createOIMOrderedListMappedCommandStream } from '@oimdb/core';
123
+
124
+ stream.push('main', 'u1'); // append by pk; also insertAt/setAt/removeAt/removeRange/moveRange/set
125
+ stream.subscribeCommands('main', () => {
126
+ for (const cmd of stream.consumeCommands('main')) {
127
+ // { type:'insert', index, item } | { type:'remove', index, count? }
128
+ // | { type:'move', from, to, count? } | { type:'set', index, item }
129
+ // | { type:'reset', items } — item is the entity slot; entity is item.item
130
+ }
131
+ });
132
+ stream.getEntitiesByKey('main'); // (TEntity | undefined)[] — holes preserved
133
+ ```
134
+
135
+ `count` defaults to 1 (read `count ?? 1`). A whole-list change collapses to a single `reset`. Reads: `getPksByKey` / `getSlotsByKey` / `getEntitiesByKey`.
136
+
137
+ **Mapping the list to your own elements** — `createOIMOrderedListMappedCommandStream(source, create)` projects element-wise: the same commands with `item` replaced by `create(slot)`'s result (a DOM node, view model, …). `create` is a plain function (no options object). Identity is positional — a `move` reuses the **same** mapped instance (never recreated); `create` runs on insert/set/reset/initial build. No teardown callback: the leaving element IS the `remove`/`set`/`reset` command the consumer applies against its own mirror, so cleanup belongs there. It needs no queue (rides the source's batching) and is itself an `IOIMOrderedListCommandSource`, so it consumes like the raw stream and chains via `.map(create)`. New exports: `IOIMOrderedListCommandSource`, `OIMOrderedListMappedCommandStream`, `createOIMOrderedListMappedCommandStream`.
138
+
139
+ ```ts
140
+ const nodes = createOIMOrderedListMappedCommandStream(stream, (slot) => engine.makeNode(slot.item));
141
+ nodes.subscribeCommands('main', () => {
142
+ for (const cmd of nodes.consumeCommands('main')) {
143
+ // cmd.item is your node; tear it down on remove/set/reset from your own mirror
144
+ }
145
+ });
146
+ nodes.getItemsByKey('main'); // current mapped elements — initial render
147
+ ```
148
+
149
+ > Content updates (entity fields changed) are not list commands — they arrive via the collection's per-pk subscription. The stream carries structure only.
150
+
151
+ **Deriving commands from an index** — the stream above is written imperatively. To instead get commands from an index whose order is maintained automatically (`derivedArrayIndex`, or any reactive array-based index replaced via `setPks`), use `createOIMOrderedListCommandStreamDiffDriven(queue, index, opts?)` / `OIMOrderedListCommandStreamDiffDriven`. On each per-key index change it diffs the previous order against the new one and emits the `insert`/`move`/`remove` that transforms it — identity by pk (a reorder is a `move` of the same slot, never a recreate), delivered on `AFTER_FLUSH`. An order-preserving change emits nothing. `opts.resetThreshold` (default `0` = always diff): below that fraction of shared pks it emits one `reset` instead of many edits. Output is an `IOIMOrderedListCommandSource`, so `createOIMOrderedListMappedCommandStream` composes on top to carry your own nodes. New exports: `OIMOrderedListCommandStreamDiffDriven`, `createOIMOrderedListCommandStreamDiffDriven`, `TOIMOrderedListDiffOptions`, `diffOrderedListByPk` (pure diff), `OIMOrderedListCommandBuffer` (shared abstract base of both streams).
152
+
153
+ ```ts
154
+ const cardsByDeck = cards.indexFactory.derivedArrayIndex((c) => c.deckId, { orderBy: (c) => c.position });
155
+ const stream = createOIMOrderedListCommandStreamDiffDriven(queue, cardsByDeck);
156
+ stream.subscribeCommands('deck1', () => engine.apply(stream.consumeCommands('deck1'))); // insert/move/remove
157
+ ```
158
+
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`.
160
+
161
+ ```ts
162
+ const usersByTeam = users.indexFactory.derivedSetIndex((u) => u.teamId);
163
+ const rows = createOIMIndexSlotMap(usersByTeam, (slot) => makeRow(slot.item));
164
+ rows.subscribeOnKey('team1', () => { for (const row of rows.getByKey('team1')) { /* stable per slot */ } });
165
+ ```
166
+
87
167
  ### DX Layer — Collection Model
88
168
 
89
169
  `createOIMCollectionKit` wires queue + collection + index factory + selectors together.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.4.0",
3
+ "version": "3.6.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",