@oimdb/core 1.5.0 → 2.0.1

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 CHANGED
@@ -1,6 +1,32 @@
1
1
  # @oimdb/core
2
2
 
3
- Core in-memory data library providing reactive collections, intelligent indexing, and configurable event processing. This package offers the foundational building blocks for building high-performance, event-driven in-memory databases with type-safe operations and automatic change notifications.
3
+ Core in-memory data library providing reactive collections, intelligent indexing, and configurable event processing.
4
+
5
+ 📖 **[Full documentation](https://abaikov.github.io/oimdb/)**
6
+
7
+ ## For AI Assistants
8
+
9
+ If you are an AI assistant working on a project that uses OIMDB, read the full API reference before writing any code. It covers all packages, key concepts, code patterns, and naming conventions — the API has specific patterns that are easy to get wrong without context.
10
+
11
+ The file ships with this package. Read it at:
12
+
13
+ ```
14
+ node_modules/@oimdb/core/llms.txt
15
+ ```
16
+
17
+ Or fetch from CDN if working without local files:
18
+
19
+ ```
20
+ https://cdn.jsdelivr.net/npm/@oimdb/core/llms.txt
21
+ ```
22
+
23
+ If the project has `@oimdb/devtools` installed, also look for `debug.ts` or `*.debug.ts` files — they register the app's specific collections, indexes, and computeds by name and describe their relations. This is the fastest way to understand the specific data model you're working with.
24
+
25
+ **Cursor users:** Cursor does not index `node_modules`, so it will not find this README automatically. Add a `.cursor/rules/oimdb.mdc` file to your project:
26
+
27
+ ```
28
+ Read node_modules/@oimdb/core/llms.txt before writing any OIMDB code.
29
+ ```
4
30
 
5
31
  ## 🚀 Installation
6
32
 
@@ -14,9 +40,13 @@ This package exports all the core classes, interfaces, and types needed to build
14
40
 
15
41
  ### Core Classes
16
42
  - **OIMReactiveCollection**: Reactive entity storage with automatic change notifications
17
- - **OIMRICollection**: Reactive collection with integrated indexing capabilities
43
+ - **OIMCollectionRelations**: Helper for creating collection-bound indexes and ordered lists next to a collection
18
44
  - **OIMReactiveIndexManualSetBased**: Reactive index with Set-based storage (efficient for incremental updates)
19
45
  - **OIMReactiveIndexManualArrayBased**: Reactive index with Array-based storage (efficient for full replacements)
46
+ - **OIMReactiveCollectionIndexManualSetBased**: Collection-bound Set-based index with safe PK-oriented writes
47
+ - **OIMReactiveCollectionIndexManualArrayBased**: Collection-bound Array-based index with safe PK-oriented writes
48
+ - **OIMOrderedListCommandStream**: Slot-first ordered per-key lists with incremental commands for imperative consumers
49
+ - **OIMCollectionOrderedListCommandStream**: Collection-bound ordered lists with PK writes and slot/entity command payloads
20
50
  - **OIMEventQueue**: Configurable event processing queue with scheduler integration
21
51
  - **OIMCollection**: Base collection with CRUD operations and event emission
22
52
 
@@ -38,6 +68,7 @@ This package exports all the core classes, interfaces, and types needed to build
38
68
  - **OIMCollectionStoreMapDriven**: Map-based storage backend
39
69
  - **OIMIndexManualSetBased**: Set-based manual index (stores slots, projects to `Set<TPk>`)
40
70
  - **OIMIndexManualArrayBased**: Array-based manual index (stores slots, projects to `TPk[]`)
71
+ - **OIMIndexManualOrderedArrayBased**: Slot-first manual ordered Array-based index with `pushSlot`, `insertSlotAt`, `removeAt`, `move`, and `resetSlots`
41
72
  - **OIMIndexStoreMapDrivenSetBased**: Set-based slot index storage backend
42
73
  - **OIMIndexStoreMapDrivenArrayBased**: Array-based slot index storage backend
43
74
  - **OIMMap2Keys**: Two-key mapping utilities for complex indexing
@@ -57,6 +88,43 @@ This package exports all the core classes, interfaces, and types needed to build
57
88
 
58
89
  ## 🔧 Basic Usage
59
90
 
91
+ ### DX Collection Model
92
+
93
+ Use the `dx` factories when you want a concise entrypoint without changing the underlying model. The collection still owns entities, and relations still live next to it.
94
+
95
+ ```typescript
96
+ import {
97
+ createOIMCollectionContext,
98
+ OIMEventQueue,
99
+ OIMEventQueueSchedulerFactory
100
+ } from '@oimdb/core';
101
+
102
+ type User = {
103
+ id: string;
104
+ name: string;
105
+ teamId: string;
106
+ };
107
+
108
+ const queue = new OIMEventQueue({
109
+ scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
110
+ });
111
+ const users = createOIMCollectionContext<User, string>(queue, {
112
+ selectPk: user => user.id,
113
+ });
114
+
115
+ const usersByTeam = users.indexFactory.derivedSetIndex(user => user.teamId);
116
+ const teamUsers = users.select.entitiesBySetIndexKey(usersByTeam, 'team1');
117
+
118
+ users.collection.upsertMany([
119
+ { id: 'u1', name: 'Alice', teamId: 'team1' },
120
+ { id: 'u2', name: 'Bob', teamId: 'team1' },
121
+ ]);
122
+
123
+ teamUsers.watch(value => {
124
+ console.log(value); // Alice, Bob
125
+ });
126
+ ```
127
+
60
128
  ### Creating a Reactive Collection
61
129
 
62
130
  ```typescript
@@ -92,35 +160,63 @@ users.updateEventEmitter.subscribeOnKeys(['user1', 'user2'], () => {
92
160
  console.log('Users changed!');
93
161
  });
94
162
 
95
- // CRUD operations
96
- users.upsertOne({ id: 'user1', name: 'John Doe', email: 'john@example.com' });
97
- users.upsertMany([
163
+ // CRUD operations return canonical slots.
164
+ const user1Slot = users.upsertOne({
165
+ id: 'user1',
166
+ name: 'John Doe',
167
+ email: 'john@example.com'
168
+ });
169
+ const otherSlots = users.upsertMany([
98
170
  { id: 'user2', name: 'Jane Smith', email: 'jane@example.com' },
99
171
  { id: 'user3', name: 'Bob Wilson', email: 'bob@example.com' }
100
172
  ]);
101
173
 
174
+ // Updating an entity keeps the same slot object and updates slot.item.
175
+ const updatedUser1Slot = users.upsertOne({
176
+ id: 'user1',
177
+ name: 'John Smith',
178
+ email: 'john@example.com'
179
+ });
180
+ console.log(user1Slot === updatedUser1Slot); // true
181
+
102
182
  // Query operations
103
183
  const user = users.getOneByPk('user1');
104
184
  const multipleUsers = users.getManyByPks(['user1', 'user2']);
185
+ const userSlot = users.getSlotByPk('user1');
105
186
  ```
106
187
 
107
188
  ### Creating a Reactive Index
108
189
 
109
190
  OIMDB provides two types of indexes optimized for different use cases:
110
191
 
111
- Indexes are slot-backed internally. Collections own canonical `{ pk, item }` slots, while indexes store references to those slots. `getPksByKey` remains available as a PK projection (`Set<TPk>` for SetBased, `TPk[]` for ArrayBased), and entity-by-index selectors/hooks read `slot.item` directly for faster reads.
192
+ Indexes are slot-backed internally and expose PK projections through `getPksByKey` (`Set<TPk>` for SetBased, `TPk[]` for ArrayBased). Raw indexes can be used as standalone PK/slot structures. When an index belongs to a collection, prefer `OIMReactiveCollectionIndexManual*`; it binds the collection at construction time so PK writes resolve canonical collection slots without a later lifecycle setter.
193
+
194
+ For most app-level entity relations, start with `createOIMCollectionContext(...).indexFactory.derivedSetIndex(...)` or `.derivedArrayIndex(...)`. The manual indexes below are the advanced path for externally maintained memberships such as search results, permissions, or server-provided order.
112
195
 
113
196
  #### SetBased Indexes (for incremental updates)
114
197
 
115
198
  ```typescript
116
- import { OIMReactiveIndexManualSetBased, OIMEventQueue } from '@oimdb/core';
199
+ import {
200
+ OIMReactiveCollection,
201
+ OIMReactiveCollectionIndexManualSetBased,
202
+ OIMEventQueue
203
+ } from '@oimdb/core';
204
+
205
+ type User = { id: string; role: string };
117
206
 
118
207
  // Create Set-based reactive index for user roles
119
208
  const queue = new OIMEventQueue({
120
209
  scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
121
210
  });
122
211
 
123
- const userRoleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
212
+ const users = new OIMReactiveCollection<User, string>(queue, {
213
+ selectPk: (user) => user.id
214
+ });
215
+ const userRoleIndex =
216
+ new OIMReactiveCollectionIndexManualSetBased<string, string, User>(
217
+ queue,
218
+ { collection: users }
219
+ );
124
220
 
125
221
  // Subscribe to specific index key changes
126
222
  userRoleIndex.updateEventEmitter.subscribeOnKey('admin', () => {
@@ -145,14 +241,27 @@ userRoleIndex.removePks('admin', ['user1']);
145
241
  #### ArrayBased Indexes (for full replacements)
146
242
 
147
243
  ```typescript
148
- import { OIMReactiveIndexManualArrayBased, OIMEventQueue } from '@oimdb/core';
244
+ import {
245
+ OIMReactiveCollection,
246
+ OIMReactiveCollectionIndexManualArrayBased,
247
+ OIMEventQueue
248
+ } from '@oimdb/core';
249
+
250
+ type Card = { id: string; deckId: string };
149
251
 
150
252
  // Create Array-based reactive index for deck cards
151
253
  const queue = new OIMEventQueue({
152
254
  scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
153
255
  });
154
256
 
155
- const cardsByDeckIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
257
+ const cards = new OIMReactiveCollection<Card, string>(queue, {
258
+ selectPk: (card) => card.id
259
+ });
260
+ const cardsByDeckIndex =
261
+ new OIMReactiveCollectionIndexManualArrayBased<string, string, Card>(
262
+ queue,
263
+ { collection: cards }
264
+ );
156
265
 
157
266
  // Subscribe to specific index key changes
158
267
  cardsByDeckIndex.updateEventEmitter.subscribeOnKey('deck1', () => {
@@ -174,6 +283,68 @@ cardsByDeckIndex.setPks('deck1', ['card1', 'card2', 'card4']); // Full replaceme
174
283
  - **SetBased**: Use when you frequently add/remove individual items (`addPks`/`removePks` are efficient) and order doesn't matter
175
284
  - **ArrayBased**: Use when you typically replace the entire array (`setPks` is more efficient, no diff computation needed) or when you need to preserve element order/sorting
176
285
 
286
+ #### Ordered List Command Streams
287
+
288
+ Use `OIMCollectionOrderedListCommandStream` when you need an ordered list per key, PK-oriented writes, canonical collection slots, and incremental commands for an imperative renderer or external store. Use `OIMOrderedListCommandStream` directly only when you already manage slots yourself.
289
+
290
+ ```typescript
291
+ import {
292
+ OIMEventQueue,
293
+ OIMReactiveCollection,
294
+ OIMCollectionOrderedListCommandStream
295
+ } from '@oimdb/core';
296
+
297
+ type Card = { id: string; title: string };
298
+
299
+ const queue = new OIMEventQueue();
300
+ const cards = new OIMReactiveCollection<Card, string>(queue, {
301
+ selectPk: card => card.id,
302
+ });
303
+ const cardsByDeck = new OIMCollectionOrderedListCommandStream<
304
+ string,
305
+ string,
306
+ Card
307
+ >(queue, { collection: cards });
308
+
309
+ cards.upsertMany([
310
+ { id: 'card1', title: 'Intro' },
311
+ { id: 'card2', title: 'Details' },
312
+ { id: 'card3', title: 'Summary' },
313
+ ]);
314
+
315
+ cardsByDeck.commandsEventEmitter.subscribeOnKey('deck1', () => {
316
+ const commands = cardsByDeck.consumeCommands('deck1');
317
+
318
+ for (const command of commands) {
319
+ switch (command.type) {
320
+ case 'insert':
321
+ // Insert command.pk / command.slot.item at command.index
322
+ break;
323
+ case 'remove':
324
+ // Remove command.pk from command.index
325
+ break;
326
+ case 'move':
327
+ // Move command.pk from command.fromIndex to command.toIndex
328
+ break;
329
+ case 'set':
330
+ // Replace the whole list with command.pks or command.slots
331
+ break;
332
+ }
333
+ }
334
+ });
335
+
336
+ cardsByDeck.set('deck1', ['card1', 'card2']);
337
+ cardsByDeck.move('deck1', 1, 0);
338
+ cardsByDeck.push('deck1', 'card3');
339
+
340
+ queue.flush(); // Delivers one batched command notification for deck1
341
+
342
+ console.log(cardsByDeck.getPksByKey('deck1')); // ['card2', 'card1', 'card3']
343
+ console.log(cardsByDeck.getEntitiesByKey('deck1')); // [{ id: 'card2', ... }, ...]
344
+ ```
345
+
346
+ If you mutate `cardsByDeck.index` directly, the stream cannot know the exact operation and emits a `set` command so consumers can resync.
347
+
177
348
  ### Event Queue and Schedulers
178
349
 
179
350
  ```typescript
@@ -216,12 +387,13 @@ console.log('Is empty:', manualQueue.isEmpty);
216
387
 
217
388
  ## 🏗️ Advanced Usage
218
389
 
219
- ### Reactive Collection with Indexes (OIMRICollection)
390
+ ### Collections with Bound Indexes
220
391
 
221
392
  ```typescript
222
393
  import {
223
- OIMRICollection,
224
- OIMReactiveIndexManual,
394
+ OIMReactiveCollection,
395
+ OIMReactiveCollectionIndexManualSetBased,
396
+ OIMReactiveCollectionIndexManualArrayBased,
225
397
  OIMEventQueue,
226
398
  OIMEventQueueSchedulerFactory
227
399
  } from '@oimdb/core';
@@ -239,24 +411,28 @@ const queue = new OIMEventQueue({
239
411
  scheduler: OIMEventQueueSchedulerFactory.createMicrotask()
240
412
  });
241
413
 
242
- // Create indexes (choose SetBased or ArrayBased based on your needs)
243
- const teamIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
244
- const roleIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
245
-
246
- // Create collection with indexes
247
- const users = new OIMRICollection(queue, {
248
- collectionOpts: {
249
- selectPk: (user: User) => user.id
250
- },
251
- indexes: {
252
- byTeam: teamIndex,
253
- byRole: roleIndex
254
- }
414
+ // Collections own entities.
415
+ const users = new OIMReactiveCollection<User, string>(queue, {
416
+ selectPk: (user: User) => user.id
255
417
  });
256
418
 
419
+ // Indexes live next to collections and bind to them at construction time.
420
+ const indexes = {
421
+ usersByTeam: new OIMReactiveCollectionIndexManualSetBased<
422
+ string,
423
+ string,
424
+ User
425
+ >(queue, { collection: users }),
426
+ usersByRole: new OIMReactiveCollectionIndexManualArrayBased<
427
+ string,
428
+ string,
429
+ User
430
+ >(queue, { collection: users })
431
+ };
432
+
257
433
  // Subscribe to index changes
258
- users.indexes.byTeam.updateEventEmitter.subscribeOnKey('engineering', (pks) => {
259
- console.log('Engineering team changed:', pks);
434
+ indexes.usersByTeam.updateEventEmitter.subscribeOnKey('engineering', () => {
435
+ console.log('Engineering team changed:', indexes.usersByTeam.getPksByKey('engineering'));
260
436
  });
261
437
 
262
438
  // Add users and update indexes
@@ -266,8 +442,70 @@ users.upsertMany([
266
442
  ]);
267
443
 
268
444
  // Update indexes manually
269
- users.indexes.byTeam.setPks('engineering', ['u1', 'u2']);
270
- users.indexes.byRole.setPks('admin', ['u1']);
445
+ indexes.usersByTeam.setPks('engineering', ['u1', 'u2']);
446
+ indexes.usersByRole.setPks('admin', ['u1']);
447
+ ```
448
+
449
+ ### Collection Relations Helper
450
+
451
+ Use `createOIMCollectionRelations` when you want the same clean model with less constructor noise. It does not store indexes inside the collection; it only keeps the shared `queue + collection` binding for related structures that live next to the collection.
452
+
453
+ ```typescript
454
+ import {
455
+ createOIMCollectionRelations,
456
+ OIMReactiveCollection,
457
+ OIMEventQueue
458
+ } from '@oimdb/core';
459
+
460
+ type User = {
461
+ id: string;
462
+ name: string;
463
+ teamId: string;
464
+ };
465
+
466
+ type Card = {
467
+ id: string;
468
+ deckId: string;
469
+ position: number;
470
+ };
471
+
472
+ const queue = new OIMEventQueue();
473
+ const users = new OIMReactiveCollection<User, string>(queue, {
474
+ selectPk: user => user.id,
475
+ });
476
+ const userRelations = createOIMCollectionRelations(queue, users);
477
+ const cards = new OIMReactiveCollection<Card, string>(queue, {
478
+ selectPk: card => card.id,
479
+ });
480
+ const cardRelations = createOIMCollectionRelations(queue, cards);
481
+
482
+ // Derived indexes update themselves from collection writes.
483
+ const usersByTeam = userRelations.derivedSetIndex(user => user.teamId);
484
+ const cardsByDeck = cardRelations.derivedArrayIndex(
485
+ card => card.deckId,
486
+ { orderBy: card => card.position }
487
+ );
488
+
489
+ // Manual relations are still available when membership comes from outside.
490
+ const searchResults = userRelations.arrayBasedIndex<string>();
491
+ const orderedUsers = userRelations.orderedList<string>();
492
+
493
+ users.upsertMany([
494
+ { id: 'u1', name: 'Alice', teamId: 'team1' },
495
+ { id: 'u2', name: 'Bob', teamId: 'team1' },
496
+ ]);
497
+ cards.upsertMany([
498
+ { id: 'c1', deckId: 'deck1', position: 2 },
499
+ { id: 'c2', deckId: 'deck1', position: 1 },
500
+ ]);
501
+
502
+ searchResults.setPks('query:alice', ['u1']);
503
+ orderedUsers.set('visible', ['u1', 'u2']);
504
+ queue.flush();
505
+
506
+ console.log(usersByTeam.getEntitiesByKey('team1')); // Alice, Bob
507
+ console.log(cardsByDeck.getPksByKey('deck1')); // ['c2', 'c1']
508
+ console.log(orderedUsers.getPksByKey('visible')); // ['u1', 'u2']
271
509
  ```
272
510
 
273
511
  ### Custom Entity Updater
@@ -454,13 +692,25 @@ import {
454
692
  OIMEffect,
455
693
  OIMComputativeRuntime,
456
694
  OIMEventQueue,
457
- OIMReactiveIndexManualSetBased,
695
+ OIMReactiveCollection,
696
+ OIMReactiveCollectionIndexManualSetBased,
458
697
  OIMEffectDependencyKeyedIndex,
459
698
  } from '@oimdb/core';
460
699
 
700
+ interface User {
701
+ id: string;
702
+ name: string;
703
+ }
704
+
461
705
  const queue = new OIMEventQueue();
462
706
  const runtime = new OIMComputativeRuntime(queue);
463
- const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
707
+ const users = new OIMReactiveCollection<User, string>(queue, {
708
+ selectPk: (u) => u.id,
709
+ });
710
+ const roleIndex = new OIMReactiveCollectionIndexManualSetBased<string, string, User>(
711
+ queue,
712
+ { collection: users }
713
+ );
464
714
 
465
715
  const effect = new OIMEffect(runtime, {
466
716
  deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
@@ -470,11 +720,16 @@ const effect = new OIMEffect(runtime, {
470
720
  },
471
721
  });
472
722
 
723
+ users.upsertMany([
724
+ { id: 'user1', name: 'Alice' },
725
+ { id: 'user2', name: 'Bob' },
726
+ ]);
473
727
  roleIndex.setPks('admin', ['user1', 'user2']);
474
728
  queue.flush();
475
729
 
476
730
  effect.destroy();
477
731
  roleIndex.destroy();
732
+ users.destroy();
478
733
  queue.destroy();
479
734
  ```
480
735
 
@@ -572,7 +827,7 @@ import {
572
827
  OIMEventQueue,
573
828
  OIMComputativeRuntime,
574
829
  OIMReactiveCollection,
575
- OIMReactiveIndexManualSetBased,
830
+ OIMReactiveCollectionIndexManualSetBased,
576
831
  OIMEffectDependencyKeyedCollection,
577
832
  OIMEffectDependencyKeyedIndex,
578
833
  } from '@oimdb/core';
@@ -588,7 +843,10 @@ const runtime = new OIMComputativeRuntime(queue);
588
843
  const users = new OIMReactiveCollection<User, string>(queue, {
589
844
  selectPk: (u) => u.id,
590
845
  });
591
- const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
846
+ const roleIndex = new OIMReactiveCollectionIndexManualSetBased<string, string, User>(
847
+ queue,
848
+ { collection: users }
849
+ );
592
850
 
593
851
  // Computed that counts admin users
594
852
  const adminCount = new OIMComputed<number>(runtime, {
@@ -736,7 +994,7 @@ import {
736
994
  OIMComputativeRuntime,
737
995
  OIMEventQueue,
738
996
  OIMReactiveCollection,
739
- OIMReactiveIndexManualSetBased,
997
+ OIMReactiveCollectionIndexManualSetBased,
740
998
  } from '@oimdb/core';
741
999
 
742
1000
  const queue = new OIMEventQueue();
@@ -744,7 +1002,10 @@ const runtime = new OIMComputativeRuntime(queue);
744
1002
  const users = new OIMReactiveCollection<User, string>(queue, {
745
1003
  selectPk: (u) => u.id,
746
1004
  });
747
- const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
1005
+ const roleIndex = new OIMReactiveCollectionIndexManualSetBased<string, string, User>(
1006
+ queue,
1007
+ { collection: users }
1008
+ );
748
1009
 
749
1010
  // Watch all admin users
750
1011
  const adminUsersSelector = new OIMEntitiesByIndexKeySetBasedSelector(
@@ -835,17 +1096,23 @@ Choose the right scheduler for your use case:
835
1096
  ### Reactive Collection Hierarchy
836
1097
 
837
1098
  ```
838
- OIMCollection (base)
839
- ├── OIMReactiveCollection (adds updateEventEmitter wired to the queue)
840
- └── OIMRICollection (reactive collection + indexes)
1099
+ OIMCollection (base; upserts return canonical slots)
1100
+ └── OIMReactiveCollection (adds updateEventEmitter wired to the queue)
841
1101
 
842
1102
  OIMIndexSetBased (base for Set-based)
843
1103
  ├── OIMIndexManualSetBased (manual Set-based index)
844
- └── OIMReactiveIndexManualSetBased (reactive Set-based index with event emitter)
1104
+ ├── OIMReactiveIndexManualSetBased (reactive Set-based index with event emitter)
1105
+ └── OIMReactiveCollectionIndexManualSetBased (collection-bound reactive Set-based index)
845
1106
 
846
1107
  OIMIndexArrayBased (base for Array-based)
847
1108
  ├── OIMIndexManualArrayBased (manual Array-based index)
848
- └── OIMReactiveIndexManualArrayBased (reactive Array-based index with event emitter)
1109
+ ├── OIMIndexManualOrderedArrayBased (slot-first manual ordered Array-based index)
1110
+ ├── OIMCollectionIndexManualOrderedArrayBased (collection-bound ordered Array-based index)
1111
+ ├── OIMReactiveIndexManualArrayBased (reactive Array-based index with event emitter)
1112
+ └── OIMReactiveCollectionIndexManualArrayBased (collection-bound reactive Array-based index)
1113
+
1114
+ OIMOrderedListCommandStream (slot-first ordered-list command stream)
1115
+ OIMCollectionOrderedListCommandStream (collection-bound ordered-list command stream)
849
1116
  ```
850
1117
 
851
1118
  ## ⚡ Performance Characteristics
@@ -863,7 +1130,7 @@ OIMIndexArrayBased (base for Array-based)
863
1130
 
864
1131
  ### Index Performance
865
1132
 
866
- **SetBased Indexes** (`OIMReactiveIndexManualSetBased`):
1133
+ **SetBased Indexes** (`OIMReactiveCollectionIndexManualSetBased`):
867
1134
  - **Storage**: Set of canonical entity slots
868
1135
  - **PK projection**: `getPksByKey` returns `Set<TPk>` for efficient membership checks
869
1136
  - **Entity reads**: selectors/hooks read `slot.item` directly, avoiding per-item collection lookups
@@ -871,7 +1138,7 @@ OIMIndexArrayBased (base for Array-based)
871
1138
  - **Performance**: O(1) add/remove operations, O(n) for `setPks` (requires Set creation)
872
1139
  - **Use case**: When you need to frequently add/remove individual items
873
1140
 
874
- **ArrayBased Indexes** (`OIMReactiveIndexManualArrayBased`):
1141
+ **ArrayBased Indexes** (`OIMReactiveCollectionIndexManualArrayBased`):
875
1142
  - **Storage**: Array of canonical entity slots
876
1143
  - **PK projection**: `getPksByKey` returns `TPk[]` for direct array access
877
1144
  - **Entity reads**: selectors/hooks read `slot.item` directly, preserving array order
@@ -884,16 +1151,60 @@ OIMIndexArrayBased (base for Array-based)
884
1151
 
885
1152
  ### With React (@oimdb/react)
886
1153
 
887
- The core library integrates seamlessly with React through dedicated hooks:
1154
+ ```bash
1155
+ npm install @oimdb/react
1156
+ ```
1157
+
1158
+ Create your collections outside React, wire them up once, then use hooks inside components:
888
1159
 
889
1160
  ```typescript
890
- import { useSelectEntitiesByPks, selectEntityByPk } from '@oimdb/react';
1161
+ import { createOIMCollectionContext, OIMEventQueue } from '@oimdb/core';
1162
+ import {
1163
+ OIMCollectionsProvider,
1164
+ useOIMCollectionsContext,
1165
+ useSelectEntityByPk,
1166
+ useSelectEntitiesByIndexKeySetBased,
1167
+ } from '@oimdb/react';
1168
+
1169
+ // --- store.ts (created once, outside React) ---
1170
+ const queue = new OIMEventQueue();
1171
+ const { collection: users, indexFactory } =
1172
+ createOIMCollectionContext<User, string>(queue, { selectPk: (u) => u.id });
1173
+ const byTeam = indexFactory.derivedSetIndex((u) => [u.teamId]);
1174
+ export const collections = { users };
1175
+ export { byTeam };
1176
+
1177
+ // --- App.tsx ---
1178
+ function App() {
1179
+ return (
1180
+ <OIMCollectionsProvider collections={collections}>
1181
+ <TeamList teamId="team1" />
1182
+ </OIMCollectionsProvider>
1183
+ );
1184
+ }
1185
+
1186
+ // --- TeamList.tsx ---
1187
+ type AppCollections = typeof collections;
1188
+
1189
+ function TeamList({ teamId }: { teamId: string }) {
1190
+ const { users } = useOIMCollectionsContext<AppCollections>();
1191
+ const members = useSelectEntitiesByIndexKeySetBased(users, byTeam, teamId);
1192
+ return (
1193
+ <ul>
1194
+ {members?.map((u) => u && <li key={u.id}>{u.name}</li>)}
1195
+ </ul>
1196
+ );
1197
+ }
891
1198
 
892
- // React hooks automatically subscribe to reactive collections
893
- const user = selectEntityByPk(users, 'user1');
894
- const teamUsers = useSelectEntitiesByPks(users, userIds);
1199
+ function UserCard({ userId }: { userId: string }) {
1200
+ const { users } = useOIMCollectionsContext<AppCollections>();
1201
+ const user = useSelectEntityByPk(users, userId);
1202
+ return <span>{user?.name}</span>;
1203
+ }
895
1204
  ```
896
1205
 
1206
+ All hooks use `useSyncExternalStore` internally and re-render only when the specific data they watch actually changes.
1207
+
897
1208
  ### With Redux (@oimdb/redux-adapter)
898
1209
 
899
1210
  Migrate from Redux to OIMDB gradually or use both systems side-by-side with automatic two-way synchronization:
@@ -952,6 +1263,34 @@ unsubscribe();
952
1263
 
953
1264
  ## 📚 API Reference
954
1265
 
1266
+ ### DX Factories
1267
+
1268
+ #### `createOIMReactiveCollection<TEntity, TPk>(queue, opts?)`
1269
+ Creates an `OIMReactiveCollection<TEntity, TPk>` with less constructor noise.
1270
+
1271
+ #### `createOIMCollectionContext<TEntity, TPk>(queue, opts?)`
1272
+ Creates a small facade:
1273
+
1274
+ ```typescript
1275
+ type TOIMCollectionContext<TEntity, TPk> = {
1276
+ queue: OIMEventQueue;
1277
+ collection: OIMReactiveCollection<TEntity, TPk>;
1278
+ relations: OIMCollectionRelations<TEntity, TPk>;
1279
+ select: OIMCollectionSelectors<TEntity, TPk>;
1280
+ };
1281
+ ```
1282
+
1283
+ This is a DX entrypoint only: indexes and lists are still created as separate relation objects, not stored inside the collection.
1284
+
1285
+ #### `OIMCollectionSelectors<TEntity, TPk>`
1286
+ DX facade for reactive read selectors backed by one `OIMComputativeRuntime`.
1287
+
1288
+ **Methods:**
1289
+ - `byPk(pk)` - Create `OIMCollectionByPkSelector<TEntity, TPk>`
1290
+ - `byPks(pks)` - Create `OIMCollectionByPksSelector<TEntity, TPk>`
1291
+ - `entitiesBySetIndexKey(index, key)` - Create an entity selector for a Set-based reactive index key
1292
+ - `entitiesByArrayIndexKey(index, key)` - Create an entity selector for an Array-based reactive index key
1293
+
955
1294
  ### Core Classes
956
1295
 
957
1296
  #### `OIMReactiveCollection<TEntity, TPk>`
@@ -968,8 +1307,9 @@ new OIMReactiveCollection(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEn
968
1307
  - Event batching/deduplication is handled internally by `OIMUpdateEventEmitter`
969
1308
 
970
1309
  **Methods:**
971
- - `upsertOne(entity: TEntity): void` - Insert or update single entity
972
- - `upsertMany(entities: TEntity[]): void` - Insert or update multiple entities
1310
+ - `upsertOne(entity: TEntity): TOIMEntitySlot<TEntity, TPk>` - Insert or update single entity and return its canonical slot
1311
+ - `upsertOneByPk(pk: TPk, entity: Partial<TEntity>): TOIMEntitySlot<TEntity, TPk>` - Insert or update by primary key and return its canonical slot
1312
+ - `upsertMany(entities: TEntity[]): TOIMEntitySlot<TEntity, TPk>[]` - Insert or update multiple entities and return canonical slots
973
1313
  - `removeOne(entity: TEntity): void` - Remove single entity
974
1314
  - `removeMany(entities: TEntity[]): void` - Remove multiple entities
975
1315
  - `getOneByPk(pk: TPk): TEntity | undefined` - Get entity by primary key
@@ -977,23 +1317,29 @@ new OIMReactiveCollection(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEn
977
1317
  - `getSlotByPk(pk: TPk): OIMEntitySlot<TEntity, TPk> | undefined` - Get the canonical slot for a primary key
978
1318
  - `getSlotsByPks(pks: readonly TPk[]): OIMEntitySlot<TEntity, TPk>[]` - Get existing canonical slots for primary keys
979
1319
 
980
- #### `OIMRICollection<TEntity, TPk, TIndexName, TIndexKey, TIndex, TReactiveIndex, TReactiveIndexMap>`
981
- Reactive collection with integrated indexing capabilities.
1320
+ #### `OIMCollectionRelations<TEntity, TPk>`
1321
+ Factory helper for collection-bound relations. It keeps `queue + collection` together for construction only; created indexes/lists still live next to the collection, not inside it.
982
1322
 
983
1323
  **Constructor:**
984
1324
  ```typescript
985
- new OIMRICollection(queue: OIMEventQueue, opts: {
986
- collectionOpts?: TOIMCollectionOptions<TEntity, TPk>;
987
- indexes: TReactiveIndexMap;
988
- })
1325
+ new OIMCollectionRelations(queue: OIMEventQueue, collection: OIMReactiveCollection<TEntity, TPk>)
989
1326
  ```
990
1327
 
991
- **Properties:**
992
- - `indexes: TReactiveIndexMap` - Named reactive indexes preserving index-to-name mapping
993
- - *(inherits all OIMReactiveCollection properties)*
1328
+ **Factory:**
1329
+ ```typescript
1330
+ createOIMCollectionRelations(queue, collection)
1331
+ ```
1332
+
1333
+ **Methods:**
1334
+ - `setBasedIndex<TKey>()` - Create `OIMReactiveCollectionIndexManualSetBased<TKey, TPk, TEntity>`
1335
+ - `derivedSetIndex<TKey>(selectIndexKeys)` - Create `OIMDerivedCollectionIndexSetBased<TKey, TPk, TEntity>`
1336
+ - `arrayBasedIndex<TKey>()` - Create `OIMReactiveCollectionIndexManualArrayBased<TKey, TPk, TEntity>`
1337
+ - `derivedArrayIndex<TKey>(selectIndexKeys, { orderBy?, compareEntities? })` - Create `OIMDerivedCollectionIndexArrayBased<TKey, TPk, TEntity>`
1338
+ - `orderedIndex<TKey>()` - Create `OIMCollectionIndexManualOrderedArrayBased<TKey, TPk, TEntity>`
1339
+ - `orderedList<TKey>()` - Create `OIMCollectionOrderedListCommandStream<TKey, TPk, TEntity>`
994
1340
 
995
1341
  #### `OIMReactiveIndexManualSetBased<TKey, TPk>`
996
- Reactive Set-based index with manual key-to-entity mapping and change notifications. Stores canonical slots internally and projects to `Set<TPk>` for PK reads.
1342
+ Reactive Set-based index with manual slot writes and change notifications. Use this as a raw slot-first primitive; use `OIMReactiveCollectionIndexManualSetBased` for PK-oriented writes.
997
1343
 
998
1344
  **Constructor:**
999
1345
  ```typescript
@@ -1001,7 +1347,6 @@ new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
1001
1347
  indexOptions?: {
1002
1348
  comparePks?: TOIMIndexComparator<TPk>;
1003
1349
  store?: OIMIndexStoreSetBased<TKey, TPk>;
1004
- resolveSlot?: TOIMEntitySlotResolver<TPk>;
1005
1350
  }
1006
1351
  })
1007
1352
  ```
@@ -1011,9 +1356,6 @@ new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
1011
1356
  - `updateEventEmitter: OIMUpdateEventEmitter<TKey>` - Key-specific subscriptions
1012
1357
 
1013
1358
  **Methods:**
1014
- - `setPks(key: TKey, pks: readonly TPk[]): void` - Set primary keys for index key (replaces entire Set)
1015
- - `addPks(key: TKey, pks: readonly TPk[]): void` - Add primary keys to index key (efficient)
1016
- - `removePks(key: TKey, pks: readonly TPk[]): void` - Remove primary keys from index key (efficient)
1017
1359
  - `setSlots(key: TKey, slots: Iterable<TOIMAnyEntitySlot<TPk>>): void` - Set canonical slots directly
1018
1360
  - `clear(key?: TKey): void` - Clear all keys or specific key
1019
1361
 
@@ -1021,8 +1363,56 @@ new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
1021
1363
  - `index.getPksByKey(key: TKey): Set<TPk>` - Returns Set projection of primary keys
1022
1364
  - `index.getSlotsByKey(key: TKey): ReadonlySet<TOIMAnyEntitySlot<TPk>>` - Returns stored slots for fast entity reads
1023
1365
 
1366
+ #### `OIMReactiveCollectionIndexManualSetBased<TKey, TPk, TEntity>`
1367
+ Collection-bound Set-based index. Use this when `setPks`/`addPks`/`removePks` should resolve PKs to canonical slots from a collection.
1368
+
1369
+ **Constructor:**
1370
+ ```typescript
1371
+ new OIMReactiveCollectionIndexManualSetBased(queue: OIMEventQueue, opts: {
1372
+ collection: OIMReactiveCollection<TEntity, TPk>;
1373
+ indexOptions?: {
1374
+ comparePks?: TOIMIndexComparator<TPk>;
1375
+ store?: OIMIndexStoreSetBased<TKey, TPk>;
1376
+ };
1377
+ } | {
1378
+ resolveSlot: TOIMEntitySlotResolver<TPk>;
1379
+ indexOptions?: {
1380
+ comparePks?: TOIMIndexComparator<TPk>;
1381
+ store?: OIMIndexStoreSetBased<TKey, TPk>;
1382
+ };
1383
+ })
1384
+ ```
1385
+
1386
+ Pass exactly one binding: `collection` for normal collection-bound indexes, or `resolveSlot` for custom slot resolution.
1387
+
1388
+ **Methods:**
1389
+ - `setPks(key: TKey, pks: readonly TPk[]): void` - Set primary keys for index key
1390
+ - `addPks(key: TKey, pks: readonly TPk[]): void` - Add primary keys to index key
1391
+ - `removePks(key: TKey, pks: readonly TPk[]): void` - Remove primary keys from index key
1392
+ - `setSlots(key: TKey, slots: Iterable<TOIMAnyEntitySlot<TPk>>): void` - Set canonical slots directly
1393
+ - `clear(key?: TKey): void` - Clear all keys or specific key
1394
+
1395
+ #### `OIMDerivedCollectionIndexSetBased<TKey, TPk, TEntity>`
1396
+ Collection-bound Set-based index that derives membership from collection entities. Use this when index keys come from entity fields and should stay in sync automatically.
1397
+
1398
+ **Constructor:**
1399
+ ```typescript
1400
+ new OIMDerivedCollectionIndexSetBased(queue, collection, {
1401
+ selectIndexKeys: (entity: TEntity) => TKey | readonly TKey[] | undefined | null;
1402
+ buildInitial?: boolean; // defaults to true
1403
+ indexOptions?: {
1404
+ comparePks?: TOIMIndexComparator<TPk>;
1405
+ store?: OIMIndexStoreSetBased<TKey, TPk>;
1406
+ };
1407
+ })
1408
+ ```
1409
+
1410
+ **Methods:**
1411
+ - `rebuildFromCollection(): void` - Rebuild all derived membership from current collection slots
1412
+ - all read/subscription methods from `OIMReactiveCollectionIndexManualSetBased`
1413
+
1024
1414
  #### `OIMReactiveIndexManualArrayBased<TKey, TPk>`
1025
- Reactive Array-based index with manual key-to-entity mapping and change notifications. Stores canonical slots internally and projects to `TPk[]` for PK reads.
1415
+ Reactive Array-based index with manual slot writes and change notifications. Use this as a raw slot-first primitive; use `OIMReactiveCollectionIndexManualArrayBased` for PK-oriented writes.
1026
1416
 
1027
1417
  **Constructor:**
1028
1418
  ```typescript
@@ -1030,7 +1420,6 @@ new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
1030
1420
  indexOptions?: {
1031
1421
  comparePks?: TOIMIndexComparator<TPk>;
1032
1422
  store?: OIMIndexStoreArrayBased<TKey, TPk>;
1033
- resolveSlot?: TOIMEntitySlotResolver<TPk>;
1034
1423
  }
1035
1424
  })
1036
1425
  ```
@@ -1040,9 +1429,6 @@ new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
1040
1429
  - `updateEventEmitter: OIMUpdateEventEmitter<TKey>` - Key-specific subscriptions
1041
1430
 
1042
1431
  **Methods:**
1043
- - `setPks(key: TKey, pks: readonly TPk[]): void` - Set primary keys for index key (direct assignment, no diff) - **Recommended for ArrayBased**
1044
- - `addPks(key: TKey, pks: readonly TPk[]): void` - Add primary keys to index key (O(n) operation, less efficient than SetBased)
1045
- - `removePks(key: TKey, pks: readonly TPk[]): void` - Remove primary keys from index key (O(n) operation, less efficient than SetBased)
1046
1432
  - `setSlots(key: TKey, slots: TOIMAnyEntitySlot<TPk>[]): void` - Set canonical slots directly
1047
1433
  - `clear(key?: TKey): void` - Clear all keys or specific key
1048
1434
 
@@ -1052,6 +1438,126 @@ new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
1052
1438
 
1053
1439
  **Note**: While `addPks`/`removePks` are available, they require array operations (Set creation, filtering) making them O(n) compared to O(1) for SetBased indexes. For ArrayBased indexes, prefer `setPks` for better performance when replacing the entire array.
1054
1440
 
1441
+ #### `OIMDerivedCollectionIndexArrayBased<TKey, TPk, TEntity>`
1442
+ Collection-bound Array-based index that derives ordered membership from collection entities. Use this for UI lists where entities define both grouping and order.
1443
+
1444
+ **Constructor:**
1445
+ ```typescript
1446
+ new OIMDerivedCollectionIndexArrayBased(queue, collection, {
1447
+ selectIndexKeys: (entity: TEntity) => TKey | readonly TKey[] | undefined | null;
1448
+ buildInitial?: boolean; // defaults to true
1449
+ orderBy?: (entity: TEntity) => string | number | bigint | boolean;
1450
+ compareEntities?: (a: TEntity, b: TEntity) => number;
1451
+ indexOptions?: {
1452
+ comparePks?: TOIMIndexComparator<TPk>;
1453
+ store?: OIMIndexStoreArrayBased<TKey, TPk>;
1454
+ };
1455
+ })
1456
+ ```
1457
+
1458
+ `compareEntities` takes priority over `orderBy`. Without either option, arrays keep collection slot iteration order.
1459
+
1460
+ **Methods:**
1461
+ - `rebuildFromCollection(): void` - Rebuild all derived ordered membership from current collection slots
1462
+ - all read/subscription methods from `OIMReactiveCollectionIndexManualArrayBased`
1463
+
1464
+ #### `OIMReactiveCollectionIndexManualArrayBased<TKey, TPk, TEntity>`
1465
+ Collection-bound Array-based index. Use this when ordered PK arrays should resolve to canonical slots from a collection.
1466
+
1467
+ **Constructor:**
1468
+ ```typescript
1469
+ new OIMReactiveCollectionIndexManualArrayBased(queue: OIMEventQueue, opts: {
1470
+ collection: OIMReactiveCollection<TEntity, TPk>;
1471
+ indexOptions?: {
1472
+ comparePks?: TOIMIndexComparator<TPk>;
1473
+ store?: OIMIndexStoreArrayBased<TKey, TPk>;
1474
+ };
1475
+ } | {
1476
+ resolveSlot: TOIMEntitySlotResolver<TPk>;
1477
+ indexOptions?: {
1478
+ comparePks?: TOIMIndexComparator<TPk>;
1479
+ store?: OIMIndexStoreArrayBased<TKey, TPk>;
1480
+ };
1481
+ })
1482
+ ```
1483
+
1484
+ Pass exactly one binding: `collection` for normal collection-bound indexes, or `resolveSlot` for custom slot resolution.
1485
+
1486
+ **Methods:**
1487
+ - `setPks(key: TKey, pks: readonly TPk[]): void` - Set primary keys for index key
1488
+ - `addPks(key: TKey, pks: readonly TPk[]): void` - Add primary keys to index key
1489
+ - `removePks(key: TKey, pks: readonly TPk[]): void` - Remove primary keys from index key
1490
+ - `setSlots(key: TKey, slots: TOIMAnyEntitySlot<TPk>[]): void` - Set canonical slots directly
1491
+ - `clear(key?: TKey): void` - Clear all keys or specific key
1492
+
1493
+ #### `OIMIndexManualOrderedArrayBased<TKey, TPk>`
1494
+ Slot-first manual ordered Array-based index with direct list operations and update events. Use this when you need low-level ordered storage without a command stream.
1495
+
1496
+ **Constructor:**
1497
+ ```typescript
1498
+ new OIMIndexManualOrderedArrayBased<TKey, TPk>()
1499
+ ```
1500
+
1501
+ **Methods:**
1502
+ - `pushSlot(key: TKey, slot: TOIMAnyEntitySlot<TPk>): number` - Append a slot and return its inserted index
1503
+ - `insertSlotAt(key: TKey, index: number, slot: TOIMAnyEntitySlot<TPk>): number` - Insert a slot at an index, clamped to the list bounds
1504
+ - `removeAt(key: TKey, index: number): TOIMAnyEntitySlot<TPk> | undefined` - Remove and return the slot at an index
1505
+ - `move(key: TKey, fromIndex: number, toIndex: number): TOIMAnyEntitySlot<TPk> | undefined` - Move a slot within the list
1506
+ - `resetSlots(key: TKey, slots: readonly TOIMAnyEntitySlot<TPk>[]): void` - Replace the whole ordered slot list for a key
1507
+ - `clear(key?: TKey): void` - Clear all keys or a specific key
1508
+
1509
+ **Query:**
1510
+ - `getPksByKey(key: TKey): TPk[]` - Returns the ordered primary-key array
1511
+ - `getSlotsByKey(key: TKey): readonly TOIMAnyEntitySlot<TPk>[]` - Returns the stored ordered slots
1512
+ - `getEntitiesByKey<TEntity>(key: TKey): TEntity[]` - Returns existing entities from stored slots
1513
+
1514
+ #### `OIMCollectionIndexManualOrderedArrayBased<TKey, TPk, TEntity>`
1515
+ Collection-bound ordered Array-based index. Use this when ordered PK writes should resolve to canonical slots from a collection.
1516
+
1517
+ **Methods:**
1518
+ - `push(key: TKey, pk: TPk): number` - Append a PK as its canonical slot
1519
+ - `insertAt(key: TKey, index: number, pk: TPk): number` - Insert a PK as its canonical slot
1520
+ - `reset(key: TKey, pks: readonly TPk[]): void` - Replace the whole ordered list from PKs
1521
+ - `getEntitiesByKey(key: TKey): TEntity[]` - Returns entities from canonical slots
1522
+
1523
+ #### `OIMOrderedListCommandStream<TKey, TPk, TEntity>`
1524
+ Slot-first ordered per-key list with a command stream for imperative consumers. It stores data in an `OIMIndexManualOrderedArrayBased` and emits `TOIMOrderedListCommand` batches through `commandsEventEmitter`.
1525
+
1526
+ **Constructor:**
1527
+ ```typescript
1528
+ new OIMOrderedListCommandStream(
1529
+ queue: OIMEventQueue,
1530
+ index?: OIMIndexManualOrderedArrayBased<TKey, TPk>
1531
+ )
1532
+ ```
1533
+
1534
+ **Properties:**
1535
+ - `index: OIMIndexManualOrderedArrayBased<TKey, TPk>` - Underlying ordered index and source of truth
1536
+ - `commandsEventEmitter: OIMUpdateEventEmitter<TKey>` - Key-specific command notifications delivered after queue flush
1537
+
1538
+ **Methods:**
1539
+ - `setSlots(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void` - Replace the whole ordered list and emit a `set` command
1540
+ - `pushSlot(key: TKey, slot: TOIMEntitySlot<TEntity, TPk>): void` - Append a slot and emit an `insert` command
1541
+ - `insertSlotAt(key: TKey, index: number, slot: TOIMEntitySlot<TEntity, TPk>): void` - Insert a slot and emit an `insert` command
1542
+ - `removeAt(key: TKey, index: number): void` - Remove by index and emit a `remove` command
1543
+ - `move(key: TKey, fromIndex: number, toIndex: number): void` - Move within the list and emit a `move` command
1544
+ - `consumeCommands(key: TKey): TOIMOrderedListCommand<TPk, TEntity>[]` - Read buffered commands for a key inside the notification handler
1545
+ - `getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<TPk, TEntity>[]` - Peek at buffered commands without clearing them
1546
+ - `getPksByKey(key: TKey): readonly TPk[]` - Read the current ordered list
1547
+ - `getSlotsByKey(key: TKey): readonly TOIMEntitySlot<TEntity, TPk>[]` - Read current ordered slots
1548
+ - `getEntitiesByKey(key: TKey): TEntity[]` - Read current ordered entities
1549
+ - `clear(key?: TKey): void` - Clear all keys or a specific key
1550
+ - `destroy(): void` - Dispose subscriptions and clear state
1551
+
1552
+ #### `OIMCollectionOrderedListCommandStream<TKey, TPk, TEntity>`
1553
+ Collection-bound ordered-list command stream. Public writes use PKs, commands include both `pk`/`pks` and canonical `slot`/`slots`.
1554
+
1555
+ **Methods:**
1556
+ - `set(key: TKey, pks: readonly TPk[]): void` - Replace the whole ordered list from PKs
1557
+ - `push(key: TKey, pk: TPk): void` - Append a PK
1558
+ - `insertAt(key: TKey, index: number, pk: TPk): void` - Insert a PK
1559
+ - all read/command methods from `OIMOrderedListCommandStream`
1560
+
1055
1561
  #### `OIMEventQueue`
1056
1562
  Event processing queue with configurable scheduling.
1057
1563