@oimdb/core 3.5.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,32 @@ 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
+
87
117
  ### Ordered list command stream
88
118
 
89
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.
@@ -104,19 +134,36 @@ stream.getEntitiesByKey('main'); // (TEntity | undefined)[] — holes preserved
104
134
 
105
135
  `count` defaults to 1 (read `count ?? 1`). A whole-list change collapses to a single `reset`. Reads: `getPksByKey` / `getSlotsByKey` / `getEntitiesByKey`.
106
136
 
107
- **Mapping the list to your own elements** — `createOIMOrderedListMappedCommandStream(source, { create, destroy? })` projects element-wise: the same commands with `item` replaced by `create(slot)`'s result (a DOM node, view model, …). Identity is positional — a `move` reuses the **same** mapped instance (never recreated); `create` runs on insert/set/reset/initial build, `destroy` on remove / replaced-set / reset / `destroy()`. 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(...)`. New exports: `IOIMOrderedListCommandSource`, `TOIMOrderedListMapOptions`, `OIMOrderedListMappedCommandStream`, `createOIMOrderedListMappedCommandStream`.
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`.
108
138
 
109
139
  ```ts
110
- const nodes = createOIMOrderedListMappedCommandStream(stream, {
111
- create: (slot) => engine.makeNode(slot.item),
112
- destroy: (node) => engine.dropNode(node),
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
+ }
113
145
  });
114
- nodes.subscribeCommands('main', () => engine.apply(nodes.consumeCommands('main'))); // cmd.item is your node
115
146
  nodes.getItemsByKey('main'); // current mapped elements — initial render
116
147
  ```
117
148
 
118
149
  > Content updates (entity fields changed) are not list commands — they arrive via the collection's per-pk subscription. The stream carries structure only.
119
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
+
120
167
  ### DX Layer — Collection Model
121
168
 
122
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.5.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",