@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/README.md +46 -46
- package/dist/index.cjs +1424 -146
- package/dist/index.d.cts +741 -29
- package/dist/index.d.ts +741 -29
- package/dist/index.js +1397 -145
- package/llms.txt +82 -2
- package/package.json +1 -1
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(), //
|
|
31
|
+
scheduler: new OIMEventQueueSchedulerImmediate(), // macrotask: setImmediate (Node), fallback MessageChannel / setTimeout(0) (browser)
|
|
32
32
|
});
|
|
33
|
-
// or:
|
|
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.
|