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