@oimdb/core 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt CHANGED
@@ -25,11 +25,11 @@
25
25
  > `getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`, `findSingleSlot(storeSlot)`,
26
26
  > `clearSingleSlotContent(storeSlot)`. No key is `void` or `undefined`: `TKey extends NonNullable<unknown>`.
27
27
  >
28
- > Every collection is `createOIMSlotCollectionService(db, { slotContentService })` and every single is
29
- > `createOIMSingleSlotService(db, { slotContentService })`. The content service decides what the slots hold:
30
- > `createOIMSlotContentEntityService<TEntity>(db, …)` for entities, `new OIMSlotContentSetBucketService()`,
28
+ > Every collection is `createOIMSlotCollectionService({ slotContentService })` and every single is
29
+ > `createOIMSingleSlotService({ slotContentService })`. The content service decides what the slots hold:
30
+ > `createOIMSlotContentEntityService<TEntity>(…)` for entities, `new OIMSlotContentSetBucketService()`,
31
31
  > `new OIMSlotContentPositionalArrayBucketService()` or `new OIMSlotContentSortedArrayBucketService({ … })` for
32
- > buckets. An index is `createOIMSlotCollectionIndexService(db, { entityService, keyOf | columns, slotContentService? })`;
32
+ > buckets. An index is `createOIMSlotCollectionIndexService({ entityService, keyOf | columns, slotContentService? })`;
33
33
  > its `keyOf` is the index's key of an entity, not the collection's. An array key needs a tree store:
34
34
  > `slotStoreService` on the collection, `bucketSlotStoreService` on the index.
35
35
  > Position reads and writes (`getBucketMemberSlotAt`, `getBucketMemberSlotPosition`, `insertBucketMemberSlotAt`,
@@ -39,7 +39,7 @@
39
39
  > (`createOIMSlotCollectionSortedArrayIndexService`); on every other bucket its order is the caller's. A sorted list
40
40
  > over any index is `createOIMSortedComputedService`. The render script is `OIMBucketCommands`.
41
41
  > The channel has no class: its operations are on `OIMChannelService.INSTANCE` and take a channel slot.
42
- > Effects are `OIMEffectService.INSTANCE.createEffect(db, sourceSlots, sourceComputeds, body)`.
42
+ > Effects are `OIMEffectService.INSTANCE.createEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`.
43
43
  > Every stateless singleton is a static `INSTANCE` on its class. Module-level defaults are on `OIMDefaults`.
44
44
  >
45
45
  > Check every symbol you write against [the full export list](#full-export-list).
@@ -50,7 +50,7 @@
50
50
 
51
51
  ## Contents
52
52
 
53
- - [The three layers](#the-three-layers)
53
+ - [The domains](#the-domains)
54
54
  - [The doctrine](#the-doctrine) — the four doors, the injected seams, the idle windows, the collectors, forks
55
55
  - [The runtime: the channel slot](#the-runtime-the-channel-slot)
56
56
  - [`OIMDB` and the three doors](#oimdb-and-the-three-doors)
@@ -81,10 +81,10 @@ block and the one under [One derivation per key](#one-derivation-per-key). Block
81
81
  marked `ts signature` are listings of an API and are not compiled.
82
82
 
83
83
  ```ts setup
84
- import { OIMChannelService, OIMContentChangeListenerService, OIMDB, OIMDefaults, OIMEffectService, OIMSlotContentPositionalArrayBucketService, OIMSlotContentSetBucketService, OIMSlotMapStoreService, OIMSingleSlotStoreService, OIMSlotTreeStoreService, OIMUnusedSlotCollectorService, createOIMBucketMemoComputedService, createOIMComputedService, createOIMSortedComputedService, createOIMEntityWriterInPlace, createOIMEntityWriterReplacing, createOIMSlotCollectionIndexService, createOIMSlotCollectionSortedArrayIndexService, createOIMSlotCollectionService, createOIMSlotContentEntityService, createOIMSingleSlotService, createOIMSingleComputedService, createOIMUnusedSlotCollectorSlot, oimIsSlotEmpty, oimIsSlotReleased, oimPatchEntityColumn, oimPatchEntityColumns, oimPutEntityColumns, oimSameValueZeroGuard, OIMBucketCommands, OIMSlotEarliestReleasePolicy, OIMSlotAfterWriteReleasePolicy, OIMSlotAfterIdleReleasePolicy, OIMThrowAtOnceErrorPolicy, type TOIMSlotStoreTree } from '@oimdb/core';
84
+ import { OIMChannelService, OIMSlotContentChangeListenerService, OIMDB, OIMDefaults, OIMEffectService, OIMSlotContentPositionalArrayBucketService, OIMSlotContentSetBucketService, OIMSlotMapStoreService, OIMSingleSlotStoreService, OIMSlotTreeStoreService, OIMUnusedSlotCollectorService, createOIMBucketMemoComputedService, createOIMComputedService, createOIMSortedComputedService, createOIMInPlaceEntityWriter, createOIMReplacingEntityWriter, createOIMSlotCollectionIndexService, createOIMSlotCollectionSortedArrayIndexService, createOIMSlotCollectionService, createOIMSlotContentEntityService, createOIMSingleSlotService, createOIMSingleComputedService, createOIMUnusedSlotCollectorSlot, oimIsSlotEmpty, oimIsSlotReleased, oimPatchEntityColumn, oimPatchEntityColumns, oimPutEntityColumns, oimSameValueZeroGuard, OIMBucketCommands, OIMSlotEarliestReleasePolicy, OIMSlotAfterWriteReleasePolicy, OIMSlotAfterIdleReleasePolicy, OIMThrowAtOnceErrorPolicy, type TOIMSlotStoreTree, OIMScalarKeyTreeWalk, OIMCompositeKeyTreeWalk } from '@oimdb/core';
85
85
  import type {
86
86
  IOIMEntityWriter,
87
- TOIMSlotEditable,
87
+ TOIMEditable,
88
88
  TOIMBucketSlotsArrayStore,
89
89
  TOIMBucketSlot,
90
90
  TOIMBucketSlotsSetStore,
@@ -111,15 +111,15 @@ declare const payload: readonly Card[];
111
111
  const db = new OIMDB();
112
112
  const channelSlot = db.defaultChannelSlot;
113
113
  const effectService = OIMEffectService.INSTANCE;
114
- const cardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db) });
114
+ const cardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
115
115
  const cards = cardService.createStoreSlot(db);
116
116
  const slot = cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
117
117
 
118
- const orderedService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>() });
118
+ const orderedService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>() });
119
119
  const ordered = orderedService.createStoreSlot(db);
120
120
  const orderedBucketSlot = orderedService.holdSlotByKey(ordered, 'top');
121
121
 
122
- const pageService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
122
+ const pageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
123
123
  const page = pageService.createStoreSlot(db);
124
124
  const todo = pageService.holdSlotByKey(page, 'todo');
125
125
  const done = pageService.holdSlotByKey(page, 'done');
@@ -127,19 +127,27 @@ const done = pageService.holdSlotByKey(page, 'done');
127
127
 
128
128
  ---
129
129
 
130
- ## The three layers
130
+ ## The domains
131
131
 
132
- | Layer | Contents |
133
- |---|---|
134
- | `src/engine/<domain>/` | the channel (`OIMChannelService`, `TOIMChannelSlot`), the flush scheduler (`OIMFlushSchedulerService`), the slot owner (`OIMSlotOwner`, and `OIMSlotOwnerService`, a type-only export), `OIMContentChangeListenerService`, the entity content service `OIMSlotContentEntityService`, `OIMComputedValueService`, `OIMEffectService`, the column owner (abstract `OIMColumnSlotOwner` with the strategies `OIMPushColumnSlotOwner` and `OIMPullColumnSlotOwner`), the key stores and key tree walks, the index keys services, `TOIMStoreSlot`, `TOIMComputedSlot`, `TOIMEffect`, the collector services and slots, and the release policies |
135
- | `src/facade/<domain>/` | `facade/slot/`: `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `OIMSingleSlotService`, `OIMWritableSingleSlotService`; `facade/column/`: `OIMColumnSlotService`; `facade/bucket/`: `OIMSlotContentBucketService` (abstract), `OIMSlotContentSetBucketService`, `OIMSlotContentArrayBucketService` (abstract), `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService`, `OIMBucketCommands`; `facade/index/`: `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`; `facade/computed/`: `OIMComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService` |
136
- | `src/factories/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`; `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`, `createOIMSlotCollectionIndexService`, `createOIMSlotCollectionSortedArrayIndexService`, `createOIMComputedService`, `createOIMSingleComputedService`, `createOIMSortedComputedService`, `createOIMBucketMemoComputedService`, `createOIMEntityWriterInPlace`, `createOIMEntityWriterReplacing` |
132
+ `src/` holds one folder per domain and `index.ts`, the entire public surface; a factory lives in
133
+ the domain of what it builds. Where a file goes is in `packages/core/src/README.md`.
137
134
 
138
- **No class sits directly in `engine/` or `facade/`.** The directory is the
139
- domain: `engine/bucket/`, `engine/channel/`, `engine/collector/`, `engine/column/`, `engine/computed/`,
140
- `engine/entity/`, `engine/error/`, `engine/index/`, `engine/key/`, `engine/schedule/`, `engine/slot/`,
141
- `engine/store/`, each with its own `constants/`, `interfaces/`, `types/` and `utils/` beside its classes; and
142
- `facade/bucket/`, `facade/column/`, `facade/computed/`, `facade/index/`, `facade/slot/`.
135
+ | Domain | Contents |
136
+ |---|---|
137
+ | `slot/` | `OIMSlotOwner`, `OIMSlotOwnerService` (type-only export), `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `OIMSingleSlotService`, `OIMWritableSingleSlotService`, `OIMSlotContentChangeListenerService`, `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `TOIMSlot`, `TOIMEditable` |
138
+ | `store/` | `TOIMStoreSlot`, the slot store services |
139
+ | `channel/` | `OIMChannelService`, `TOIMChannelSlot` |
140
+ | `effect/` | `OIMEffectService`, `TOIMEffect` |
141
+ | `computed/` | `OIMComputedValueService`, `OIMComputedService`, `OIMSingleComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService`, the source-tracking computed services, their factories, `TOIMComputedSlot` |
142
+ | `entity/` | `OIMSlotContentEntityService`, `createOIMSlotContentEntityService`, `createOIMInPlaceEntityWriter`, `createOIMReplacingEntityWriter` |
143
+ | `column/` | abstract `OIMColumnSlotOwner` with `OIMPushColumnSlotOwner` and `OIMPullColumnSlotOwner`, `OIMColumnSlotService` |
144
+ | `bucket/` | `OIMSlotContentBucketService` (abstract), `OIMSlotContentSetBucketService`, `OIMSlotContentArrayBucketService` (abstract), `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService`, `OIMBucketCommands` |
145
+ | `index/` | `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`, their factories, the index keys services |
146
+ | `key/` | the key tree walks |
147
+ | `collector/` | the release policies, `OIMSlotReleaseWheel`, `OIMSlotRefetchWheel`, `OIMUnusedSlotCollectorService` |
148
+ | `schedule/` | `OIMFlushSchedulerService` |
149
+ | `error/` | the error policies |
150
+ | `db/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`, the default constants |
143
151
 
144
152
  **A service holds no data; state lives on slots.** A collection service is built once per collection type
145
153
  and holds only configuration and other services: its slot owner, its store service and its content
@@ -167,7 +175,7 @@ the caller who just got it.
167
175
 
168
176
  ```ts
169
177
  cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'Two', rank: 2, deck: 'spades' }); // write
170
- effectService.createEffect(db, [slot], [], () => { paint(slot.entity); }); // subscribe
178
+ effectService.createEffect(db, () => { paint(slot.entity); }, [slot]); // subscribe
171
179
  cardService.holdSlotByKey(cards, 'c1'); // hold, by key -> the slot, holdCount up by one
172
180
  const rankColumn = cardService.columnSlotService.holdColumn(slot, 'rank'); // hold, on a column
173
181
  cardService.columnSlotService.unholdColumn(rankColumn); // give the column back
@@ -247,9 +255,9 @@ interface IOIMSlotContentWriter<TSlot, TContent> {
247
255
  writeSlotContent(slot: TSlot, content: TContent): boolean;
248
256
  clearSlotContent(slot: TSlot): void;
249
257
  }
250
- interface IOIMSlotContentService<TSlot, TSlotOwnerService extends IOIMSlotOwner = IOIMSlotOwner> {
258
+ interface IOIMSlotContentService<TSlot> {
251
259
  isSlotEmpty(slot: TSlot): boolean;
252
- releaseSlotContent(slot: TSlot, slotOwnerService: TSlotOwnerService): void;
260
+ releaseSlotContent(slot: TSlot): void;
253
261
  hasReference(slot: TSlot): boolean;
254
262
  }
255
263
  ```
@@ -257,8 +265,8 @@ interface IOIMSlotContentService<TSlot, TSlotOwnerService extends IOIMSlotOwner
257
265
  A content service with `createSlot` goes into `createOIMSlotCollectionService`, one with `createSingleSlot` into
258
266
  `createOIMSingleSlotService`; the entity and bucket content services have both. `hasReference` answers whether
259
267
  something outside `holdCount` and `effects` keeps the slot (a bucket's members). Every write door asserts the
260
- identity is still filed (`assertWritable`), and `releaseSlotContent` refuses inside a flush when the slot still
261
- has content.
268
+ identity is still filed (`assertWritable`). `releaseSlotContent` is what the slot owner calls when it releases a
269
+ slot nothing holds, and it throws on a slot that is still held.
262
270
 
263
271
  ### The injected seams
264
272
 
@@ -269,7 +277,7 @@ object.
269
277
  ```ts signature
270
278
  interface IOIMSlotEmptinessPolicy<TSlot> { isEmpty(slot: TSlot): boolean; }
271
279
 
272
- interface IOIMSlotStamp<TSlot> { stamp(slotEditable: TOIMSlotEditable<TSlot>): void; }
280
+ interface IOIMSlotStamp<TSlot> { stamp(editableSlot: TOIMEditable<TSlot>): void; }
273
281
  ```
274
282
 
275
283
  **What a kind of content answers is not a seam.** Mint, write, clear and release
@@ -332,7 +340,7 @@ true; `OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`, `OIMSlotAtUnho
332
340
  `onSlotIdle` is still called when the last subscriber leaves.
333
341
 
334
342
  A policy decides whether and when a slot goes; it does no work itself. A
335
- factory, and `OIMDB` (whose `slotReleasePolicy` option every factory reads as its default), takes ONE
343
+ factory, and an `OIMDefaults` (whose `slotReleasePolicy` a factory given it as `defaults` reads as its default), takes ONE
336
344
  through the option `slotReleasePolicy`. With none, the slot owner is built with
337
345
  `OIMSlotAtEmptyReleasePolicy`, so a slot with a value stays for good and an empty
338
346
  one with nothing holding it goes; there is no "no policy" branch on any path.
@@ -375,9 +383,18 @@ a freshly minted slot that is already due goes to the unused-slot collector and
375
383
  leaves at the end of the frame. A window above zero is filed in that policy's own
376
384
  queue.
377
385
 
386
+ **Two writes keep their slot until the unused-slot collector flushes, and this is deliberate.**
387
+ (1) a value that the collection's emptiness policy calls empty, and (2) a write under a zero window
388
+ (`OIMSlotAfterWriteReleasePolicy` / `OIMSlotAfterIdleReleasePolicy` with `0`) on a slot with no subscriber.
389
+ Released at once, the slot `writeSlotContentByKey` returned would be dead on the next line: a second write
390
+ to it throws, and handing it to a bucket throws ("an identity handed to a bucket was released") — which is
391
+ what persist's restore does with the slots `writeInBatch` returns. The collector gives the hold back when it
392
+ flushes — at the end of `db.flush()`, or when a collector scope (`OIMUnusedSlotCollectorService.INSTANCE.run`)
393
+ closes — and the slot is released there.
394
+
378
395
  ```ts
379
396
 
380
- const releasingDb = new OIMDB({
397
+ const releasingDefaults = new OIMDefaults({
381
398
  slotReleasePolicy: new OIMSlotEarliestReleasePolicy([
382
399
  new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: 60_000 }),
383
400
  new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
@@ -406,7 +423,7 @@ is allowed to move it: a write arriving after the subscriber left would push the
406
423
  deadline out and destroy the idle policy's verdict, and the last subscriber
407
424
  leaving after a write would destroy the write policy's. Both directions are
408
425
  pinned by
409
- `engine/collector/tests/which-release-marker-each-collector-may-move.test.ts`;
426
+ `collector/tests/which-release-marker-each-collector-may-move.test.ts`;
410
427
  collapse the two markers into one and six of its nine cases go red.
411
428
 
412
429
  **The release policy's `isSlotReleasable(slot)`** is what the release guard asks
@@ -485,8 +502,8 @@ that caller; one that throws on the collector's own timer tick has no caller and
485
502
 
486
503
  ```ts
487
504
 
488
- const oneSinkDb = new OIMDB({
489
- errorPolicy: new OIMThrowAtOnceErrorPolicy(),
505
+ const oneSinkDb = new OIMDB({ errorPolicy: new OIMThrowAtOnceErrorPolicy() });
506
+ const idleReleasingDefaults = new OIMDefaults({
490
507
  slotReleasePolicy: new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
491
508
  });
492
509
  ```
@@ -517,13 +534,10 @@ asks `isEmptySlotReleasable` instead, which every policy but
517
534
  `OIMSlotNeverReleasePolicy` answers `true`. So only `OIMSlotNeverReleasePolicy`
518
535
  keeps an identity that holds nothing.
519
536
 
520
- **A release that falls inside a flush waits for it to end.** Releasing a slot
521
- with a value clears that value, which is a write, and every service
522
- refuses writes for the whole of a flush of its channel (`channelSlot.isFlushing`; refusal is per channel). So when the
523
- last hold goes during a frame — an effect body disposing another effect, say — the
524
- slot is handed to the unused-slot collector instead, which holds it and gives it
525
- back once the flush is over; the release then runs. An empty slot needs no write
526
- and goes at once.
537
+ **A release that falls inside a flush happens at once.** A slot at `holdCount` 0 has no subscriber
538
+ (a subscription is a hold), so clearing its value wakes nobody, and a release notifies no content-change
539
+ listener. When the last hold goes during a frame — an effect body disposing another effect, say — the slot is
540
+ released there, whatever its kind. A later `holdSlotByKey` for the same key in that flush mints a new slot.
527
541
 
528
542
  *The `owner !== this` line costs no lookup. A KEYLESS slot — a single's, filed in an
529
543
  `OIMSingleSlotStoreService` store — goes through the same guard under its service's policy: an
@@ -540,7 +554,7 @@ one whole window later. **A give-back is NOT that moment**: `unholdSlot` stamps
540
554
  nothing, so a slot held through an expired sweep goes at the give-back.
541
555
 
542
556
  ```ts
543
- const effect = effectService.createEffect(db, [slot], [], () => { paint(slot.entity); });
557
+ const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
544
558
  // …the window passes, sweeps run, nothing happens to this slot…
545
559
  effectService.disposeEffect(effect);
546
560
  oimIsSlotReleased(slot); // false — a whole new window starts here
@@ -551,7 +565,7 @@ releases the stale index, which gives back one reference per entity slot it name
551
565
  each entity slot whose own window already ran out is taken by the ordinary guard the
552
566
  instant that reference departs.
553
567
 
554
- Pinned by `engine/collector/tests/what-a-stale-index-takes-with-it.test.ts`,
568
+ Pinned by `collector/tests/what-a-stale-index-takes-with-it.test.ts`,
555
569
  `…/what-an-identity-nobody-subscribed-to-costs-after-a-while.test.ts` and
556
570
  `…/what-a-subscribed-row-costs-the-release-clock.test.ts`.
557
571
 
@@ -678,12 +692,12 @@ OIMUnusedSlotCollectorService.INSTANCE.flush(unusedSlotCollectorSlot); // now
678
692
  get that.** A computed takes a hold on each of its own direct sources at the mint
679
693
  and gives them back when it is released, so an arena holds the top and the chain holds itself.
680
694
 
681
- Pinned by `engine/collector/tests/what-a-named-arena-keeps-cached.test.ts`: the
695
+ Pinned by `collector/tests/what-a-named-arena-keeps-cached.test.ts`: the
682
696
  derivation survives a settle, is the SAME instance on the next read (built once),
683
697
  is rebuilt only after the arena is flushed, and `open`/`close` nest so an inner
684
698
  scope ending drops nothing an outer one stands on.
685
699
 
686
- `@oimdb/react`'s `useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)` does not use an arena: it takes a
700
+ `@oimdb/react`'s `useOIMSlotsByKeys(storeSlot, keys)` does not use an arena: it takes a
687
701
  plain `holdSlotByKey` per key on commit and gives the same holds back on unmount or when the key list moves.
688
702
 
689
703
  ```ts signature
@@ -712,11 +726,11 @@ path. There is no `if (inPlace)` anywhere in the engine.
712
726
 
713
727
  | Fork | Strategies |
714
728
  |---|---|
715
- | how a write lands | `createOIMEntityWriterInPlace()` / `createOIMEntityWriterReplacing()` |
729
+ | how a write lands | `createOIMInPlaceEntityWriter()` / `createOIMReplacingEntityWriter()` |
716
730
  | how a bucket holds slots | `OIMSlotContentSetBucketService` / `OIMSlotContentPositionalArrayBucketService` (the positions are the caller's) |
717
731
  | what naming a slot records | `bucketMemberSlotsArrayService: OIMBucketMemberSlotsArrayService` (one per mention, the default) / `new OIMBucketUniqueMemberSlotsArrayService<TSlot>()` (one per slot), on either array bucket |
718
732
  | which writes re-sort a sorted list | `sortColumns` (only a write changing one of them on a member) / `comparator` alone (any member write), the options of `createOIMSortedComputedService` |
719
- | how a key is addressed | `OIMDefaults.KEY_TREE_WALK_SCALAR` / `OIMDefaults.KEY_TREE_WALK_COMPOSITE` |
733
+ | how a key is addressed | `OIMScalarKeyTreeWalk.INSTANCE` / `OIMCompositeKeyTreeWalk.INSTANCE` |
720
734
  | when a slot may go | `OIMSlotAfterIdleReleasePolicy` / `OIMSlotAfterWriteReleasePolicy`, either or both |
721
735
  | who decides a bucket's slots | a bucket collection (`createOIMSlotCollectionService` over a bucket content service, the caller) / an index (`createOIMSlotCollectionIndexService`, filed from the entity's own key) |
722
736
 
@@ -727,7 +741,7 @@ V8 still caches each one and can still inline
727
741
  (`--max-valid-polymorphic-map-count`, default 4). Five or more is megamorphic: no
728
742
  per-site cache and no inlining through it. The kinds that count: release policies
729
743
  (the `IOIMSlotReleasePolicy` strategies), set-driven vs array-driven buckets and indexes, entity
730
- writers (`createOIMEntityWriterInPlace` / `createOIMEntityWriterReplacing`) and
744
+ writers (`createOIMInPlaceEntityWriter` / `createOIMReplacingEntityWriter`) and
731
745
  `TOIMEntityInserter`, `TOIMSameValueGuard`, `IOIMSlotEmptinessPolicy`, and
732
746
  `TOIMChannelScheduler`. Two or three of one kind is fine; five release policies in
733
747
  one process is not. The pair
@@ -742,12 +756,12 @@ is reported to that channel. No service holds a channel; there is no `channel` o
742
756
  `unusedSlotCollector` option:
743
757
 
744
758
  ```ts
745
- const cardContentService = createOIMSlotContentEntityService<Card>(db);
746
- const handBuiltCardService = createOIMSlotCollectionService(db, { slotContentService: cardContentService });
759
+ const cardContentService = createOIMSlotContentEntityService<Card>();
760
+ const handBuiltCardService = createOIMSlotCollectionService({ slotContentService: cardContentService });
747
761
  handBuiltCardService.createStoreSlot(db);
748
762
  ```
749
763
 
750
- A single entity is `createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<T>(db) })`,
764
+ A single entity is `createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<T>() })`,
751
765
  and its store slot takes the db's channel too and always reports.
752
766
 
753
767
  ### A bucket's slots are the caller's
@@ -776,10 +790,6 @@ type TOIMDBOptions = {
776
790
  onBeforeFlush?: () => void; // start of the flush, before the frame; READ ONLY, a write is refused
777
791
  getNowMs?: () => number; // the release collectors' clock; absent = Date.now
778
792
  releaseTimer?: TOIMTimer; // the release collectors' timer; absent = an unref'd setTimeout
779
- entityWriter?: IOIMEntityWriter; // default entity writer for every entity factory
780
- indexKeysService?: IOIMIndexKeysService;
781
- slotStamp?: IOIMSlotStamp<TOIMSlot>;
782
- slotReleasePolicy?: IOIMSlotReleasePolicy; // default release policy for every factory
783
793
  };
784
794
 
785
795
  db.clock // TOIMChannelClock, the same object as db.defaultChannelSlot.clock
@@ -858,7 +868,7 @@ Every door below is on `OIMChannelService.INSTANCE` and takes the channel slot f
858
868
  The channel slot's own fields that a caller reads: `frame` (`TOIMChannelFrame`, `{ isRunning, openedAtRevision,
859
869
  openedAtEpoch, … }`; a frame carries no number, the clock reading it opened at is its boundary), `clock`
860
870
  (`TOIMChannelClock`, the `{revision, epoch}` pair, shared by every channel of one db; an epoch holds
861
- `OIM_REVISIONS_PER_EPOCH` (2^30) revisions, so the counter stays a V8 small integer under pointer compression),
871
+ `OIM_REVISIONS_PER_EPOCH` (2^30) revisions, so the counter stays a V8 small integer under pointer compression; the epoch is an int32 that wraps, compared as `(epoch − sinceEpoch) | 0`),
862
872
  `collectorSlot`, `errorPolicy`, `isFlushing`. Everything else on it is engine state.
863
873
 
864
874
  ### A subscription hears only what happens after it
@@ -873,11 +883,11 @@ after it.
873
883
  cardService.writeSlotContentByKey(cards, 'a', { id: 'a', title: 'A', rank: 1, deck: 'spades' });
874
884
  const entitySlot = cardService.getSlotByKey(cards, 'a');
875
885
 
876
- effectService.createEffect(db, [entitySlot], [], () => { /* NEVER runs for the upsert above */ });
886
+ effectService.createEffect(db, () => { /* NEVER runs for the upsert above */ }, [entitySlot]);
877
887
  db.flush();
878
888
 
879
- // A first value is `createEffectImmediate`'s job, not `createEffect`'s:
880
- effectService.createEffectImmediate(db, [entitySlot], [], () => { paint(entitySlot.entity); }); // runs now
889
+ // A first value is `createAndRunEffect`'s job, not `createEffect`'s:
890
+ effectService.createAndRunEffect(db, () => { paint(entitySlot.entity); }, [entitySlot]); // runs now
881
891
  ```
882
892
 
883
893
  A later write in that same batch IS delivered — "after I subscribed" is decided
@@ -890,11 +900,11 @@ Told after **one** frame — the first that ends after the call — and then gon
890
900
  The place that asks decides which frame that is.
891
901
 
892
902
  ```ts
893
- effectService.createEffect(db, [slot], [], () => {
903
+ effectService.createEffect(db, () => {
894
904
  paint(slot.entity);
895
905
  // the tail of THIS frame, after every other effect of it
896
906
  OIMChannelService.INSTANCE.subscribeOnceAfterFrame(channelSlot, () => { measureAndScroll(); });
897
- });
907
+ }, [slot]);
898
908
  ```
899
909
 
900
910
  - **Registered from inside an effect body it runs in the tail of that very
@@ -927,10 +937,10 @@ cardService.writeInBatch(cards, () => {
927
937
  });
928
938
  ```
929
939
 
930
- - `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `writeInBatch` is the
931
- `try/finally` form and returns what its function returns.
940
+ - `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `closeBatch` with no batch open
941
+ throws. `writeInBatch` is the `try/finally` form and returns what its function returns.
932
942
  - **What the batch changes is who hears the writes, and when.** A content-change listener on the store slot (an
933
- index on the collection) hears every write of the batch in ONE call, `onContentChangedMany`, when the
943
+ index on the collection) hears every write of the batch in ONE call, `onManyContentsChanged`, when the
934
944
  outermost batch closes. Code inside the batch reads the index as it was before the batch.
935
945
  - Effects are not batched by it: every write marks its slot on the channel as usual, and the frame delivers each
936
946
  identity once whatever the batch did. A `flush()` inside a batch is allowed; the index catches up when the
@@ -1013,7 +1023,7 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
1013
1023
  ### Subscribing
1014
1024
 
1015
1025
  ```ts
1016
- const effect = effectService.createEffect(db, [slot], [], () => { paint(slot.entity); });
1026
+ const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
1017
1027
  effectService.disposeEffect(effect);
1018
1028
  ```
1019
1029
 
@@ -1048,10 +1058,10 @@ function is the undo**: it is called before the next run of that body and by
1048
1058
  `disposeEffect`.
1049
1059
 
1050
1060
  ```ts
1051
- effectService.createEffect(db, [slot], [], () => {
1061
+ effectService.createEffect(db, () => {
1052
1062
  const timer = setInterval(poll, 1000);
1053
1063
  return () => clearInterval(timer);
1054
- });
1064
+ }, [slot]);
1055
1065
  ```
1056
1066
 
1057
1067
  The return is checked with `typeof`, not for truthiness — `() => set.add(key)`
@@ -1059,14 +1069,28 @@ hands back the `Set` — so anything that is not a function is discarded. The un
1059
1069
  is cleared before it is called, so one that throws is not called again by the
1060
1070
  `disposeEffect` behind it.
1061
1071
 
1062
- **Sources found at read: `createEffectWithSourceTracking(db, body)`.** The second way to build
1063
- an effect takes no source lists. The body receives an `OIMEffectSourceTracker`, and what it
1072
+ **A derivation that finds its sources as it reads:** `createOIMSourceTrackingComputedService<TKey, TValue>({ derive, … })` and
1073
+ `createOIMSingleSourceTrackingComputedService<TValue>({ derive, … })`. `derive: TOIMSourceTrackingComputedDerive<TValue, TSlot>` is
1074
+ `(computedSlot, sourceTracker: OIMSourceTracker) => TValue`; there is no `createSources`, and `createComputedStoreSlot(db)` takes no
1075
+ per-store `derive`. Every derive holds what it read and gives back what it no longer reads (new holds first). The slot
1076
+ (`TOIMSourceTrackingComputedSlot<TValue, TKey>` / `TOIMSourceTrackingComputedContentSlot<TValue>`, `sourceTracker` set) is read
1077
+ through `sourceTracker.readSourceComputedValue` (in `createSourceTrackingEffect` or another source-tracking derive) or through `createEffect`'s fifth list; `createEffect`'s `sourceComputeds` and
1078
+ a `createSources` list refuse it by type, because a declared effect subscribes once to the slots under a derivation. When a derive
1079
+ switches sources, a source-tracking effect or derivation over it re-subscribes even when the value held. A body that cannot read
1080
+ through a tracker may pass them as the fifth list, `createEffect(db, body, sourceSlots, sourceComputeds, sourceTrackingComputeds)`;
1081
+ such an effect derives them at creation and re-subscribes when their root slots are replaced. Prefer `createSourceTrackingEffect`: the
1082
+ root check runs after every run of such an effect. An effect with no fifth list pays nothing for it. Options otherwise as
1083
+ `createOIMComputedService` / `createOIMSingleComputedService` (`sameValueGuard`, `releaseValue`, `initialValue`, `slotReleasePolicy`,
1084
+ column options).
1085
+
1086
+ **Sources found at read: `createSourceTrackingEffect(db, body)`.** The second way to build
1087
+ an effect takes no source lists. The body receives an `OIMSourceTracker`, and what it
1064
1088
  reads through the tracker is what that run depends on — signal-style tracking.
1065
1089
 
1066
1090
  ```ts signature
1067
- createEffectWithSourceTracking(channelMap: IOIMChannelMap, body: TOIMEffectBodyWithSourceTracking): TOIMEffect;
1068
- type TOIMEffectBodyWithSourceTracking = (effectSourceTracker: OIMEffectSourceTracker) => (() => void) | void;
1069
- class OIMEffectSourceTracker { // exported as a type; the service builds it
1091
+ createSourceTrackingEffect(channelMap: IOIMChannelMap, body: TOIMSourceTrackingEffectBody): TOIMEffect;
1092
+ type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
1093
+ class OIMSourceTracker { // exported as a type; the service builds it
1070
1094
  readSourceSlot<TSlot extends TOIMSlot>(slot: TSlot): TSlot; // files the slot, returns it
1071
1095
  readSourceComputedValue<TValue>(computedSlot: TOIMComputedSlot<TValue, unknown>): TValue; // files it, returns its value
1072
1096
  }
@@ -1074,9 +1098,9 @@ class OIMEffectSourceTracker { // exported as a type; the service b
1074
1098
 
1075
1099
  ```ts
1076
1100
  const fallback = cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'King', rank: 13, deck: 'hearts' });
1077
- effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
1078
- const isFirst = effectSourceTracker.readSourceSlot(slot).entity?.rank === 1;
1079
- paint(effectSourceTracker.readSourceSlot(isFirst ? slot : fallback).entity);
1101
+ effectService.createSourceTrackingEffect(db, sourceTracker => {
1102
+ const isFirst = sourceTracker.readSourceSlot(slot).entity?.rank === 1;
1103
+ paint(sourceTracker.readSourceSlot(isFirst ? slot : fallback).entity);
1080
1104
  });
1081
1105
  ```
1082
1106
 
@@ -1089,7 +1113,7 @@ effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
1089
1113
  creation. A returned function is the undo, exactly as for `TOIMEffectBody`.
1090
1114
 
1091
1115
  ```ts
1092
- const onRank = effectService.createEffect(db, [cardService.columnSlotService.holdColumn(slot, 'rank')], [], () => { paint(slot.entity); });
1116
+ const onRank = effectService.createEffect(db, () => { paint(slot.entity); }, [cardService.columnSlotService.holdColumn(slot, 'rank')]);
1093
1117
  ```
1094
1118
 
1095
1119
  **A column effect takes the `TOIMColumnSlot` as a root, not a slot and a name.** Minting the
@@ -1115,13 +1139,13 @@ per call; get it once per view, not per entity. `service.holdColumnGroup(slot, c
1115
1139
  the group's column slot on `slot`, and `service.unholdColumn(columnSlot)` gives it back. A write wakes it once
1116
1140
  if it changes any of the group's columns and not at all if it changes only others.
1117
1141
 
1118
- **`columnNames` picks the column slot store.** `createOIMSlotCollectionService(db, { slotContentService, columnNames: ['rank', 'label'] })`
1142
+ **`columnNames` picks the column slot store.** `createOIMSlotCollectionService({ slotContentService, columnNames: ['rank', 'label'] })`
1119
1143
  — and `columnNames` on any factory's options — files the service's column slots by ordinal: every named column
1120
1144
  gets a number, a group gets one at its first slot, and an entity slot's column slots sit in an array by
1121
1145
  number instead of a `Map` per entity slot, which is cheaper to create and give back. `holdColumn` and
1122
1146
  `getColumnGroup` are typed on those names, and a slot for any other column throws. Without `columnNames` the
1123
1147
  store is the `columnSlotStoreService` option, defaulting to the shared map-driven
1124
- `OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()`, and any name is accepted.
1148
+ a shared map-driven store, and any name is accepted.
1125
1149
 
1126
1150
  ---
1127
1151
 
@@ -1148,29 +1172,56 @@ frame, with no further code. Every mutation goes through a domain neuron — a w
1148
1172
  from outside the orchestrator is the mistake, not a case to cover with a fallback.
1149
1173
  Full page: `website/docs/guides/with-cnstra.md`.
1150
1174
 
1151
- The doors are free functions, one module each, so a consumer ships only the one they import. The collection,
1152
- single and index factories take `db` first and ONE options object second; the computed factories take only
1153
- the options object. Each builds a SERVICE,
1175
+ The doors are free functions, one module each, so a consumer ships only the one they import. Every factory
1176
+ takes ONE options object and no `db`; the `db` goes to `createStoreSlot(db)`. Each builds a SERVICE,
1154
1177
  once per collection type; the store slot is made separately:
1155
1178
 
1156
1179
  | Door | Builds | Store slot |
1157
1180
  |---|---|---|
1158
- | `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(db, { entityWriter?, slotEmptinessPolicy? }?)` | `OIMSlotContentEntityService<TEntity, TKey, TColumnName>`, the entity content service; it creates keyed and single entity slots alike | — |
1159
- | `createOIMSlotCollectionService(db, { slotContentService, slotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSlotCollectionService` over any keyed content service (entity or bucket): the read doors, `writeSlotContentByKey`, the batch doors and `columnSlotService`. `slotStoreService` defaults to a Map store and is REQUIRED by type for an array key (`OIMSlotTreeStoreService`) | `createStoreSlot(db)` |
1160
- | `createOIMSingleSlotService(db, { slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSingleSlotService` over any content service with `createSingleSlot` (entity or bucket) | `createStoreSlot(db)` |
1161
- | `createOIMSlotCollectionIndexService(db, { entityService, columns? \| keyOf?, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMSlotCollectionIndexService`, over the entity collection service whose store slots it will index (it takes a `Pick` of `getSlots`); `slotContentService` defaults to an `OIMSlotContentPositionalArrayBucketService`, pass `new OIMSlotContentSetBucketService()` for an unordered one. A scalar key is inferred from ONE inline column or from `keyOf`'s return; an array key (two or more columns, or an array-returning `keyOf`) needs `bucketSlotStoreService: new OIMSlotTreeStoreService(…)` | `createIndexStoreSlot(entityStoreSlot)` |
1162
- | `createOIMSlotCollectionSortedArrayIndexService(db, { entityService, columns? \| keyOf?, sortColumns, … })` | `OIMSlotCollectionSortedArrayIndexService`: the index whose buckets are kept in the order of `sortColumns` (`readonly TOIMSortColumn<TEntity>[]`, required) through an `OIMSlotContentSortedArrayBucketService`. See [Sorted at write](#sorted-at-write) | `createIndexStoreSlot(entityStoreSlot)` |
1181
+ | `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({ entityWriter?, slotEmptinessPolicy? }?)` | `OIMSlotContentEntityService<TEntity, TKey, TColumnName>`, the entity content service; it creates keyed and single entity slots alike | — |
1182
+ | `createOIMSlotCollectionService({ slotContentService, slotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSlotCollectionService` over any keyed content service (entity or bucket): the read doors, `writeSlotContentByKey`, the batch doors and `columnSlotService`. `slotStoreService` defaults to a Map store and is REQUIRED by type for an array key (`OIMSlotTreeStoreService`) | `createStoreSlot(db)` |
1183
+ | `createOIMSingleSlotService({ slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSingleSlotService` over any content service with `createSingleSlot` (entity or bucket) | `createStoreSlot(db)` |
1184
+ | `createOIMSlotCollectionIndexService({ entityService, columns? \| keyOf?, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMSlotCollectionIndexService`, over the entity collection service whose store slots it will index (it takes a `Pick` of `getSlots`); `slotContentService` defaults to an `OIMSlotContentPositionalArrayBucketService`, pass `new OIMSlotContentSetBucketService()` for an unordered one. A scalar key is inferred from ONE inline column or from `keyOf`'s return; an array key (two or more columns, or an array-returning `keyOf`) needs `bucketSlotStoreService: new OIMSlotTreeStoreService(…)` | `createIndexStoreSlot(entityStoreSlot)` |
1185
+ | `createOIMSlotCollectionSortedArrayIndexService({ entityService, columns? \| keyOf?, sortColumns, … })` | `OIMSlotCollectionSortedArrayIndexService`: the index whose buckets are kept in the order of `sortColumns` (`readonly TOIMSortColumn<TEntity>[]`, required) through an `OIMSlotContentSortedArrayBucketService`. See [Sorted at write](#sorted-at-write) | `createIndexStoreSlot(entityStoreSlot)` |
1163
1186
  | `createOIMComputedService({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService?, columnNames? })` | `OIMComputedService<TKey, TValue, TComputedStore>`; its reads are on `computedService.computedCollectionService`; `derive` and `createSources` may instead be given per store slot; `computedSlotStoreService` is REQUIRED for a non-Map store (a single: `OIMSingleSlotStoreService`); see [One derivation per key](#one-derivation-per-key) | `createComputedStoreSlot(db, derive?, createSources?)` |
1164
1187
  | `createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMSortedComputedService` — per key, the entity slots of the index bucket under that key, sorted; see [Sorted lists](#sorted-lists) | `createSortedStoreSlot(indexStoreSlot)` |
1165
1188
  | `createOIMBucketMemoComputedService({ bucketCollectionService, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMBucketMemoComputedService` — per key, the bucket's slots as one array; see [The order of a bucket](#the-order-of-a-bucket-as-one-derivation-per-key) | `createBucketMemoStoreSlot(bucketStoreSlot)` |
1166
1189
 
1167
- `entityWriter` and `slotReleasePolicy` on these doors default to the
1168
- db's, so naming one overrides it for that service alone.
1190
+ `entityWriter` and `slotReleasePolicy` on these doors default to those of the
1191
+ `defaults` option, so naming one overrides it for that service alone.
1192
+
1193
+ **A store aggregate is the store slot with its store methods, so a call does not pass the store slot.**
1194
+ `service.createStoreAggregate(storeSlot)` wraps a store slot you have (the collection, single and index
1195
+ services each return their own aggregate); `createOIMSlotCollectionStoreAggregate(db, options)` and
1196
+ `createOIMSingleSlotStoreAggregate(db, options)` build service + store slot + aggregate in one call, with the
1197
+ options of `createOIMSlotCollectionService` / `createOIMSingleSlotService` (a new service per call: for many
1198
+ store slots of one type, build the service once).
1199
+
1200
+ ```ts
1201
+ import { createOIMSlotCollectionStoreAggregate } from '@oimdb/core';
1202
+
1203
+ const deckStoreAggregate = createOIMSlotCollectionStoreAggregate(db, { slotContentService: createOIMSlotContentEntityService<Card>() });
1204
+ const aceSlot = deckStoreAggregate.writeSlotContentByKey('c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' }); // = service.writeSlotContentByKey(storeSlot, 'c1', …)
1205
+ deckStoreAggregate.slotContentService.updateEntity(aceSlot, { rank: 14 });
1206
+ deckStoreAggregate.storeSlot; // for hooks, indexes and computeds
1207
+ deckStoreAggregate.slotService; // the service itself
1208
+ ```
1209
+
1210
+ | Aggregate | Methods (the service's, minus the store slot argument) |
1211
+ |---|---|
1212
+ | `OIMSlotCollectionStoreAggregate` | `findSlotByKey`, `hasSlotContentByKey`, `getSlotByKey`, `holdSlotByKey`, `getSlots`, `getSlotCount`, `openBatch`, `closeBatch`, `writeInBatch` |
1213
+ | `OIMWritableSlotCollectionStoreAggregate` | + `writeSlotContentByKey` |
1214
+ | `OIMSingleSlotStoreAggregate` | `findSingleSlot`, `hasSingleSlotContent`, `getSingleSlot`, `holdSingleSlot` |
1215
+ | `OIMWritableSingleSlotStoreAggregate` | + `writeSingleSlotContent`, `clearSingleSlotContent` |
1216
+ | `OIMSlotCollectionIndexStoreAggregate` | the collection methods + `getEntitiesByKey`, `getSlotsByKeys`, `holdSlotsByKeys`, `get/holdBucketColumnByKey`, `get/holdAnyBucketColumnByKey`, `disposeIndexStoreSlot` |
1217
+
1218
+ Every aggregate carries `storeSlot`, `slotService` (typed as the service it came from), `slotContentService`.
1219
+ Methods that take a slot stay on their service.
1169
1220
 
1170
1221
  **A DOOR ALWAYS PASSES; IT NEVER LETS A CLASS REACH FOR A GLOBAL.** Every option
1171
1222
  a service would otherwise answer with `?? OIMDefaults.x` is in the object its
1172
1223
  door hands it, so there is one answer to each question and not two. Pinned in
1173
- `src/factories/tests/which-answer-a-collection-the-root-mints-gets.test.ts`,
1224
+ `src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts`,
1174
1225
  which also fails when an option arrives on a service and is not forwarded,
1175
1226
  when the root mints anything but the channel and the collector, or when a public
1176
1227
  member appears on the root that is not one of the seven it owns.
@@ -1180,7 +1231,7 @@ member appears on the root that is not one of the seven it owns.
1180
1231
  slot, and the script lives for one frame.
1181
1232
 
1182
1233
  ```ts
1183
- const freshPageService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1234
+ const freshPageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1184
1235
  const freshPage = freshPageService.createStoreSlot(db);
1185
1236
  const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
1186
1237
  // freshBucketSlot.commands and freshBucketSlot.isWholeListRecorded carry the script; read them in an effect on the bucket slot
@@ -1190,56 +1241,41 @@ const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
1190
1241
 
1191
1242
  ## `OIMDefaults` — where an optional dependency comes from
1192
1243
 
1193
- Every optional property of the factories falls back to an entry
1194
- on `OIMDefaults`. It is a class of static `OIMDefaultValue` fields and nothing else — never
1195
- construct one, never subclass it.
1244
+ `OIMDefaults` is an instance holding the strategies factories fall back on when their own options leave
1245
+ them out. Build one with what you want shared and pass it as `defaults` to every factory that should use
1246
+ it; a factory given none uses `OIMDefaults.INSTANCE`, which is empty. A field left undefined falls back to
1247
+ the built-in default. Nothing is global and nothing can be replaced after the fact.
1196
1248
 
1197
- | Accessor | Type | What it hands back |
1198
- |---|---|---|
1199
- | `OIMDefaults.COLUMN_SLOT_STORE_SERVICE` | `IOIMSlotStoreService<string, TOIMColumnSlot>` | a map-driven store service for column slots; the factories pass it to every service they build without `columnNames` |
1200
- | `OIMDefaults.COMPUTED_COLUMN_SLOT_STORE_SERVICE` | `IOIMSlotStoreService<string, TOIMComputedColumnSlot>` | a map-driven store service for the column slots of computed services, keyed or single; the computed factories pass it to every one they build without `columnNames` |
1201
- | `OIMDefaults.INDEX_KEYS_SERVICE` | `IOIMIndexKeysService` | `OIMAppendingIndexKeysService`; the factories pass it when `db.indexKeysService` is not set |
1202
- | `OIMDefaults.KEY_TREE_WALK_SCALAR` | `OIMScalarKeyTreeWalk` | the chain-of-values walk |
1203
- | `OIMDefaults.KEY_TREE_WALK_COMPOSITE` | `OIMCompositeKeyTreeWalk` | the chain-of-identities walk |
1204
- | `OIMDefaults.STANDALONE_SLOT_OWNER` | `OIMSlotOwner` | the owner of a slot nobody filed |
1205
- | `OIMDefaults.ENTITY_WRITER` | `IOIMEntityWriter` | `createOIMEntityWriterInPlace()` |
1206
- | `OIMDefaults.ENTITY_SLOT_EMPTINESS` | `IOIMSlotEmptinessPolicy<TOIMEntitySlot>` | "empty means `slot.entity === undefined`" |
1207
- | `OIMDefaults.STORE_SLOT_LISTENER` | `IOIMStoreSlotListener` | `OIMNoStoreSlotListener`: hears nothing. Not a constructor option: every service reads it when it is built and calls `onStoreSlotCreated(storeSlot)` for each store slot it creates. A tool that wants to see every store slot sets its own with `setValue` before the first service is built; once one has been built, `setValue` throws |
1208
-
1209
- **NOTHING IS CONSTRUCTED AT MODULE LOAD.** Every entry is an `OIMDefaultValue`
1210
- whose `getValue()` mints on its first call (`??=`) and hands the same instance back
1211
- for the rest of the process. A consumer that never reaches for a key tree walk never
1212
- builds one.
1213
-
1214
- Each has a matching `setValue(value)`, which is how a process replaces a default
1215
- everywhere at once:
1249
+ ```ts signature
1250
+ new OIMDefaults({
1251
+ slotStamp?: IOIMSlotStamp<TOIMSlot>; // none
1252
+ slotReleasePolicy?: IOIMSlotReleasePolicy; // none: a slot with a value stays
1253
+ entityWriter?: IOIMEntityWriter; // createOIMInPlaceEntityWriter()
1254
+ entitySlotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot>; // "empty means slot.entity === undefined"
1255
+ indexKeysService?: IOIMIndexKeysService; // OIMAppendingIndexKeysService
1256
+ columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // a shared map-driven store
1257
+ computedColumnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // a shared map-driven store
1258
+ storeSlotListener?: IOIMStoreSlotListener; // OIMNoStoreSlotListener: hears nothing
1259
+ }?) // TOIMDefaultsOptions
1260
+ ```
1216
1261
 
1217
1262
  ```ts
1218
- OIMDefaults.ENTITY_WRITER.setValue(createOIMEntityWriterReplacing());
1219
- ```
1220
-
1221
- **`setValue` THROWS ONCE `getValue` HAS HANDED THE FIRST ONE OUT.** `getValue`
1222
- marks the entry as handed out; `setValue` refuses after that, by name —
1223
- `OIMDefaults.ENTITY_WRITER has already been handed out, and whoever took it keeps
1224
- it …`, the name in the message being the field's upper-case name. Whoever took it keeps
1225
- it, so replacing it then would leave two implementations live in one process,
1226
- the older one inside every object built before the call, and the second half of
1227
- the program would disagree with the first about what a write does.
1228
-
1229
- Setting it before anything reads it works, and so does setting it twice before
1230
- the first read. Each property is tracked on its own, so calling
1231
- `OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()` does not close
1232
- `OIMDefaults.KEY_TREE_WALK_SCALAR`. **Set every default you intend to replace at
1233
- startup**, before you construct an `OIMDB` or a service. Pinned by
1234
- `packages/core/src/factories/tests/what-a-default-replaced-too-late-does.test.ts`.
1235
-
1236
- **Each default is its own module, so reaching one retains only its arm.** Every
1237
- default lives in its own file under `packages/core/src/factories/defaults/` with its
1238
- own `OIMDefaultValue`; a service reads the modules it needs and
1239
- neither key tree walk enters its bundle. `OIMDefaults` itself delegates to all of
1240
- them, so importing `OIMDefaults` retains every arm.
1241
- `packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one
1242
- door to its own arm, and the defaults class to all of them"*.
1263
+ const replacingDefaults = new OIMDefaults({ entityWriter: createOIMReplacingEntityWriter() });
1264
+ const replacingCardService = createOIMSlotCollectionService({
1265
+ defaults: replacingDefaults,
1266
+ slotContentService: createOIMSlotContentEntityService<Card>({ defaults: replacingDefaults }),
1267
+ });
1268
+ ```
1269
+
1270
+ An option given to a factory wins over `defaults`; `defaults` wins over the built-in default. A tool that
1271
+ wants to hear every store slot (devtools' `oimDevDefaults`) is a `defaults` with a `storeSlotListener`.
1272
+
1273
+ **Each built-in default is its own module, so reaching one retains only its arm.** Every built-in default
1274
+ lives in its own file under `packages/core/src/db/constants/` as an `OIMDefaultValue` whose `getValue()`
1275
+ builds it on the first call and hands the same instance back after; a factory reads the modules it needs.
1276
+ `OIMDefaults` imports none of them, so importing it retains no arm.
1277
+ `packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one door to its own arm, and
1278
+ the defaults class to none of them"*.
1243
1279
 
1244
1280
  ---
1245
1281
 
@@ -1251,17 +1287,17 @@ with its slot owner; the collection service (`OIMSlotCollectionService`) over it
1251
1287
  Neither holds an entity: each `createStoreSlot(db)` is one instance of that type's store.
1252
1288
 
1253
1289
  ```ts signature
1254
- createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(db, {
1255
- entityWriter?: IOIMEntityWriter; // db.entityWriter, then OIMDefaults.ENTITY_WRITER
1256
- slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; // OIMDefaults.ENTITY_SLOT_EMPTINESS
1290
+ createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({
1291
+ entityWriter?: IOIMEntityWriter; // defaults.entityWriter, then the in-place writer
1292
+ slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; // defaults.entitySlotEmptinessPolicy, then "entity is undefined"
1257
1293
  }?): OIMSlotContentEntityService<TEntity, TKey, TColumnName>
1258
1294
 
1259
- createOIMSlotCollectionService(db, {
1295
+ createOIMSlotCollectionService({
1260
1296
  slotContentService,
1261
1297
  slotStoreService?: IOIMSlotStoreService<TKey, TSlot, TStore>; // Map; REQUIRED for an array key
1262
- slotReleasePolicy?: IOIMSlotReleasePolicy; // db.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
1298
+ slotReleasePolicy?: IOIMSlotReleasePolicy; // defaults.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
1263
1299
  slotSubscriptionListener?: IOIMSlotSubscriptionListener; // OIMNoSlotSubscriptionListener
1264
- columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // OIMDefaults.COLUMN_SLOT_STORE_SERVICE
1300
+ columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // defaults.columnSlotStoreService, then a shared map store
1265
1301
  columnNames?: readonly TColumnName[]; // the ordinal column store over those names
1266
1302
  }): OIMWritableSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, TEntity, …>
1267
1303
  collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>, typeof collectionService>
@@ -1278,7 +1314,7 @@ derive-driven column slots, each a computed slot over its carrier (`OIMPullColum
1278
1314
  Example, over the `db`, `cardService`, `cards` and `slot` of the setup block:
1279
1315
 
1280
1316
  ```ts
1281
- const effect = effectService.createEffect(db, [slot], [], () => { console.log(slot.entity?.title); });
1317
+ const effect = effectService.createEffect(db, () => { console.log(slot.entity?.title); }, [slot]);
1282
1318
  cardService.slotContentService.updateEntity(slot, { title: 'Ace of Spades' });
1283
1319
  db.flush();
1284
1320
  effectService.disposeEffect(effect);
@@ -1287,15 +1323,15 @@ effectService.disposeEffect(effect);
1287
1323
  Assembled with a store service of your own:
1288
1324
 
1289
1325
  ```ts
1290
- const baseCardService = createOIMSlotCollectionService(db, {
1291
- slotContentService: createOIMSlotContentEntityService<Card, string>(db),
1326
+ const baseCardService = createOIMSlotCollectionService({
1327
+ slotContentService: createOIMSlotContentEntityService<Card, string>(),
1292
1328
  slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, string>>(),
1293
1329
  });
1294
1330
  const baseCards = baseCardService.createStoreSlot(db);
1295
1331
  ```
1296
1332
 
1297
1333
  The writer is omitted because the db and `OIMDefaults` supply it, and so is the column store. The slot stamp
1298
- is not an option of any factory: it is `new OIMDB({ slotStamp })`, for every collection of that db.
1334
+ is not an option of any factory: it is `new OIMDefaults({ slotStamp })`, passed as `defaults`.
1299
1335
 
1300
1336
  ### Public properties
1301
1337
 
@@ -1309,7 +1345,7 @@ A store slot is not a slot, so nothing holds, subscribes or wakes it: there is n
1309
1345
  `effectService.createEffect(db, [cards], …)` that hears the population. "Something in this population changed" is an
1310
1346
  index over the store slot and a subscription to its bucket — the bucket's door
1311
1347
  is written when an entity slot joins or leaves it — or a content-change listener on the store slot
1312
- (`OIMContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames)`,
1348
+ (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames)`,
1313
1349
  removed by the same listener object). The slot owner is what `slot.owner` points at for every slot the type
1314
1350
  filed, in every one of its store slots; `slot.storeSlot` names which store slot.
1315
1351
 
@@ -1500,7 +1536,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
1500
1536
  - **The hand mutation already compiles.** `TOIMEntitySlot` declares
1501
1537
  `readonly entity: TEntity | undefined`, and `readonly` forbids REPLACING the
1502
1538
  object, not writing its columns — `entity.title = 'two'` needs no cast, only
1503
- the usual narrowing of `| undefined`. `TOIMSlotEditable<TSlot>` is what strips
1539
+ the usual narrowing of `| undefined`. `TOIMEditable<TSlot>` is what strips
1504
1540
  `readonly` from the field itself, which is what assigning `slot.entity` needs.
1505
1541
  - **With a list, only the named columns wake. Without it, every watched column
1506
1542
  does** — the column owner walks the slot's whole column store, and a
@@ -1515,7 +1551,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
1515
1551
  effect on the slot itself and an index over the store slot both see a hand mutation.
1516
1552
  - **It re-stamps emptiness** whenever emptiness can depend on the mutation — with no column list,
1517
1553
  on a slot whose `entity` is `undefined`, and on a content service with a `slotEmptinessPolicy` other than
1518
- `OIMSlotEntityPresenceEmptinessPolicy.INSTANCE`, a release policy that releases written slots, or a db with a
1554
+ "the entity is `undefined`", a release policy that releases written slots, or a db with a
1519
1555
  `slotStamp` — so a hand mutation that emptied the entity leaves `oimIsSlotEmpty(slot)`
1520
1556
  true rather than lying, and the release guard can take the identity when it should.
1521
1557
  - **It refuses inside a frame**, through the same `assertWritable` →
@@ -1531,7 +1567,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
1531
1567
  A **writer** is the operations that move an entity's columns, and nothing
1532
1568
  else: it reaches no channel, takes no holds and knows nothing about lifetime.
1533
1569
  Both strategies ship as factories; the fork is chosen once, as the content service's `entityWriter`
1534
- (or for every collection of a db, `new OIMDB({ entityWriter })`).
1570
+ (or for every factory given it, `new OIMDefaults({ entityWriter })` as `defaults`).
1535
1571
 
1536
1572
  ```ts signature
1537
1573
  interface IOIMEntityWriter {
@@ -1548,13 +1584,13 @@ interface IOIMEntityWriter {
1548
1584
  type TOIMEntityWriterOptions = {
1549
1585
  sameValueGuard?: TOIMSameValueGuard;
1550
1586
  entityInserter?: TOIMEntityInserter;
1551
- putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimPutEntityColumnsGuarded
1552
- patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimPatchEntityColumnsGuarded
1553
- patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimPatchEntityColumnGuarded
1587
+ putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimGuardedPutEntityColumns
1588
+ patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimGuardedPatchEntityColumns
1589
+ patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimGuardedPatchEntityColumn
1554
1590
  };
1555
1591
 
1556
- createOIMEntityWriterInPlace(options?: TOIMEntityWriterOptions): IOIMEntityWriter // stored object keeps its identity
1557
- createOIMEntityWriterReplacing(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
1592
+ createOIMInPlaceEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // stored object keeps its identity
1593
+ createOIMReplacingEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
1558
1594
  ```
1559
1595
 
1560
1596
  `entityInserter` is the first-write strategy: `oimInsertEntityWithoutCopy` (the default) stores
@@ -1582,9 +1618,9 @@ of a key that already has a value and `slotContentService.writeSlotContent(slot,
1582
1618
  write that drops a column is ONE channel write of the slot, and the replacing writer
1583
1619
  builds ONE new object.
1584
1620
 
1585
- Pick `createOIMEntityWriterReplacing` when something downstream compares by
1621
+ Pick `createOIMReplacingEntityWriter` when something downstream compares by
1586
1622
  reference — a React `useSyncExternalStore` snapshot, a memo, a shallow-equal
1587
- selector. Under `createOIMEntityWriterInPlace` the entity reference never moves,
1623
+ selector. Under `createOIMInPlaceEntityWriter` the entity reference never moves,
1588
1624
  so a value comparison never fires and a binding needs a counter bumped inside its
1589
1625
  handler. Replacing allocates one object per changed write; in-place allocates
1590
1626
  none.
@@ -1609,10 +1645,10 @@ oimPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, c
1609
1645
  targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
1610
1646
  oimPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
1611
1647
  targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
1612
- oimPutEntityColumnsGuarded(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
1648
+ oimGuardedPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
1613
1649
  targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
1614
- oimPatchEntityColumnsGuarded(slot, patch: object, sameValueGuard, changedColumnNames, targetOf) // same signature as the unguarded walk
1615
- oimPatchEntityColumnGuarded(slot, columnName: string, value: unknown, sameValueGuard, changedColumnNames, targetOf)
1650
+ oimGuardedPatchEntityColumns(slot, patch: object, sameValueGuard, changedColumnNames, targetOf) // same signature as the unguarded walk
1651
+ oimGuardedPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, changedColumnNames, targetOf)
1616
1652
  ```
1617
1653
 
1618
1654
  Each compares every column through `sameValueGuard`, writes the moved ones into
@@ -1625,14 +1661,14 @@ ships two arms:
1625
1661
 
1626
1662
  | Option | Default (unguarded) | Guarded arm |
1627
1663
  |---|---|---|
1628
- | `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `oimPutEntityColumnsGuarded` — skips both |
1629
- | `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `oimPatchEntityColumnsGuarded` — skips a `__proto__` key and a key the patch does not own |
1630
- | `patchEntityColumn` | `oimPatchEntityColumn` | `oimPatchEntityColumnGuarded` — refuses the column name `__proto__` |
1664
+ | `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `oimGuardedPutEntityColumns` — skips both |
1665
+ | `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `oimGuardedPatchEntityColumns` — skips a `__proto__` key and a key the patch does not own |
1666
+ | `patchEntityColumn` | `oimPatchEntityColumn` | `oimGuardedPatchEntityColumn` — refuses the column name `__proto__` |
1631
1667
 
1632
1668
  Take the guarded arms, at a cost on every write, for values that can carry a `__proto__` key or are class
1633
- instances — `createOIMEntityWriterInPlace({ putEntityColumns: oimPutEntityColumnsGuarded,
1634
- patchEntityColumns: oimPatchEntityColumnsGuarded, patchEntityColumn: oimPatchEntityColumnGuarded })`, the
1635
- same options on `createOIMEntityWriterReplacing`. The writer decides only where the result goes — this one
1669
+ instances — `createOIMInPlaceEntityWriter({ putEntityColumns: oimGuardedPutEntityColumns,
1670
+ patchEntityColumns: oimGuardedPatchEntityColumns, patchEntityColumn: oimGuardedPatchEntityColumn })`, the
1671
+ same options on `createOIMReplacingEntityWriter`. The writer decides only where the result goes — this one
1636
1672
  stores every value frozen:
1637
1673
 
1638
1674
  ```ts
@@ -1640,11 +1676,11 @@ const copyColumns = (entity: Record<string, unknown>): Record<string, unknown> =
1640
1676
 
1641
1677
  function storeFrozen(slot: TOIMEntityContentSlot, next: Record<string, unknown> | undefined): boolean {
1642
1678
  if (next === undefined) return false;
1643
- (slot as TOIMSlotEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
1679
+ (slot as TOIMEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
1644
1680
  return true;
1645
1681
  }
1646
1682
 
1647
- const replacingWriter = createOIMEntityWriterReplacing();
1683
+ const replacingWriter = createOIMReplacingEntityWriter();
1648
1684
 
1649
1685
  const freezingWriter: IOIMEntityWriter = {
1650
1686
  fillEntity: (slot, entity) => { storeFrozen(slot, copyColumns(entity as Record<string, unknown>)); },
@@ -1672,8 +1708,8 @@ A single entity — one value under no key, such as settings or a session — is
1672
1708
  which files at most one slot per store slot. It has no key and no by-key doors: its doors name the one slot.
1673
1709
 
1674
1710
  ```ts signature
1675
- createOIMSingleSlotService(db, {
1676
- slotContentService: createOIMSlotContentEntityService<TEntity>(db),
1711
+ createOIMSingleSlotService({
1712
+ slotContentService: createOIMSlotContentEntityService<TEntity>(),
1677
1713
  slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
1678
1714
  }): OIMWritableSingleSlotService<TOIMEntityContentSlot<TEntity>, TEntity, …>
1679
1715
 
@@ -1697,12 +1733,12 @@ with no value until something writes it. Its store slot is a `TOIMSingleSlotStor
1697
1733
  ```ts
1698
1734
  type Settings = { theme: 'light' | 'dark'; zoom: number };
1699
1735
 
1700
- const settingsService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<Settings>(db) });
1736
+ const settingsService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<Settings>() });
1701
1737
  const settings = settingsService.createStoreSlot(db);
1702
1738
  settingsService.writeSingleSlotContent(settings, { theme: 'light', zoom: 1 });
1703
1739
  const settingsSlot = settingsService.holdSingleSlot(settings);
1704
1740
  const themeColumn = settingsService.columnSlotService.holdColumn(settingsSlot, 'theme');
1705
- const themeEffect = effectService.createEffect(db, [themeColumn], [], () => { console.log(settingsSlot.entity?.theme); });
1741
+ const themeEffect = effectService.createEffect(db, () => { console.log(settingsSlot.entity?.theme); }, [themeColumn]);
1706
1742
  settingsService.slotContentService.updateEntity(settingsSlot, { theme: 'dark' });
1707
1743
  db.flush();
1708
1744
  effectService.disposeEffect(themeEffect);
@@ -1720,7 +1756,7 @@ The slot is of the keyed entity slot's literal shape (with `key: undefined` and
1720
1756
  `storeSlot` is `settings` — so
1721
1757
  `effectService.createEffect(db, [settingsSlot], [], …)` works and a column of it is a root like any other.
1722
1758
  Its key is `undefined`, and its store is `{ slot }` (`TOIMSingleSlotStore`). Its lifetime is the
1723
- `slotReleasePolicy` (the db's, then `OIMSlotAtEmptyReleasePolicy`), so a single entity that holds a value
1759
+ `slotReleasePolicy` (`defaults.slotReleasePolicy`, then `OIMSlotAtEmptyReleasePolicy`), so a single entity that holds a value
1724
1760
  stays; one emptied and unheld is released, and the slot `getSingleSlot` mints next holds no entity. The value
1725
1761
  is stored by the writer's `entityInserter`, which by default keeps the object handed in rather than a copy.
1726
1762
 
@@ -1732,7 +1768,7 @@ A bucket collection is, like an entity collection, a content service and the col
1732
1768
  The content service is the strategy — how a bucket keeps its member slots — and it is chosen at the factory:
1733
1769
 
1734
1770
  ```ts signature
1735
- createOIMSlotCollectionService(db, {
1771
+ createOIMSlotCollectionService({
1736
1772
  slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or one of the others below
1737
1773
  slotStoreService?: IOIMSlotStoreService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore>; // Map; REQUIRED for an array key
1738
1774
  slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
@@ -1752,7 +1788,7 @@ of the collection service take that store slot first, and the bucket doors of th
1752
1788
  bucket slot.
1753
1789
 
1754
1790
  ```ts
1755
- const colourService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
1791
+ const colourService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
1756
1792
  const colours = colourService.createStoreSlot(db);
1757
1793
  const red = colourService.holdSlotByKey(colours, 'red');
1758
1794
  colourService.slotContentService.writeSlotContent(red, [slot, cardService.getSlotByKey(cards, 'c2')]);
@@ -1970,10 +2006,10 @@ content service already writes the bucket slot on every mutation, so a consumer
1970
2006
  reads the script inside the effect:
1971
2007
 
1972
2008
  ```ts
1973
- effectService.createEffect(db, [todo], [], () => {
2009
+ effectService.createEffect(db, () => {
1974
2010
  if (todo.isWholeListRecorded) render(Array.from(pageService.slotContentService.getBucketMemberSlots(todo)));
1975
2011
  else applyToTheRenderedList(todo.commands ?? []);
1976
- });
2012
+ }, [todo]);
1977
2013
  ```
1978
2014
 
1979
2015
  `TOIMBucketSlot` carries two fields for it, and a consumer reads both:
@@ -2019,21 +2055,21 @@ One bucket under no key is a store slot of an `OIMSingleSlotService` over a buck
2019
2055
  `OIMSingleSlotStoreService.INSTANCE` as the store service:
2020
2056
 
2021
2057
  ```ts signature
2022
- createOIMSingleSlotService(db, {
2058
+ createOIMSingleSlotService({
2023
2059
  slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or the set one
2024
2060
  slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
2025
2061
  }): OIMWritableSingleSlotService<TOIMBucketContentSlot<TSlots>, readonly TBucketMemberSlot[], …>
2026
2062
  ```
2027
2063
 
2028
2064
  ```ts
2029
- const handService = createOIMSingleSlotService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
2065
+ const handService = createOIMSingleSlotService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
2030
2066
  const hand = handService.createStoreSlot(db);
2031
2067
  const handSlot = handService.holdSingleSlot(hand);
2032
2068
  handService.slotContentService.writeSlotContent(handSlot, [slot]);
2033
2069
  handService.slotContentService.insertBucketMemberSlotAt(handSlot, 0, cardService.getSlotByKey(cards, 'c2'));
2034
- effectService.disposeEffect(effectService.createEffect(db, [handSlot], [], () => {
2070
+ effectService.disposeEffect(effectService.createEffect(db, () => {
2035
2071
  render(Array.from(handService.slotContentService.getBucketMemberSlots(handSlot)));
2036
- }));
2072
+ }, [handSlot]));
2037
2073
  handSlot.owner.unholdSlot(handSlot);
2038
2074
  ```
2039
2075
 
@@ -2052,7 +2088,7 @@ slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `OI
2052
2088
  ### The index that fills itself — `OIMSlotCollectionIndexService`
2053
2089
 
2054
2090
  ```ts signature
2055
- createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>(db, {
2091
+ createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>({
2056
2092
  entityService: Pick<OIMSlotCollectionService<TSourceKey, TOIMEntitySlot<TEntity, TSourceKey>, TSourceStore, unknown>, 'getSlots'>;
2057
2093
  ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>; // { columns } | { keyOf } | { keyOf, columns }
2058
2094
  slotContentService?: OIMSlotContentBucketService<TOIMEntitySlot<TEntity>, TSlots>; // default: array-driven, no commands
@@ -2098,21 +2134,21 @@ own. With none named it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: the bucket st
2098
2134
  maintained on every write.
2099
2135
 
2100
2136
  ```ts
2101
- const byDeckService = createOIMSlotCollectionIndexService(db, {
2137
+ const byDeckService = createOIMSlotCollectionIndexService({
2102
2138
  entityService: cardService,
2103
2139
  keyOf: (card: Card) => card.deck,
2104
2140
  });
2105
2141
  const byDeck = byDeckService.createIndexStoreSlot(cards);
2106
2142
 
2107
2143
  const spades = byDeckService.holdSlotByKey(byDeck, 'spades');
2108
- effectService.createEffect(db, [spades], [], () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))));
2144
+ effectService.createEffect(db, () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))), [spades]);
2109
2145
  ```
2110
2146
 
2111
2147
  A set-driven index names its content service with the bucket member slot type and the index key type spelled
2112
2148
  out — `new OIMSlotContentSetBucketService()` infers `<TOIMSlot, TOIMStoreKey>` and fails to type-check:
2113
2149
 
2114
2150
  ```ts
2115
- const bySuitService = createOIMSlotCollectionIndexService(db, {
2151
+ const bySuitService = createOIMSlotCollectionIndexService({
2116
2152
  entityService: cardService,
2117
2153
  columns: ['deck'],
2118
2154
  slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>, string>(),
@@ -2121,7 +2157,7 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
2121
2157
  ```
2122
2158
 
2123
2159
  - **Passing the entity store slot to `createIndexStoreSlot` is the whole wiring.** It registers
2124
- a content-change listener on that store slot (`OIMContentChangeListenerService.INSTANCE.addContentChangeListener`);
2160
+ a content-change listener on that store slot (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener`);
2125
2161
  the entity services know nothing about indexes. Call `createIndexStoreSlot` twice over one entity
2126
2162
  store slot and it files into both. `entityService` is used only for its `getSlots`, to fill a new bucket
2127
2163
  from the source.
@@ -2133,8 +2169,8 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
2133
2169
  ones `keyOf` reads. With `columns`, a patch or put whose changed columns include none of
2134
2170
  them keeps the entity's key and calls nothing; a write of the whole value (a fill, a
2135
2171
  clear, `markEntityChanged(slot)` with no names) always reads the key again.
2136
- - **A key of several columns**: `createOIMSlotCollectionIndexService(db, { entityService, columns: ['deck', 'suit'],
2137
- bucketSlotStoreService: new OIMSlotTreeStoreService(OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()) })` makes the key
2172
+ - **A key of several columns**: `createOIMSlotCollectionIndexService({ entityService, columns: ['deck', 'suit'],
2173
+ bucketSlotStoreService: new OIMSlotTreeStoreService(OIMScalarKeyTreeWalk.INSTANCE) })` makes the key
2138
2174
  the array of those values in order, typed as a tuple. Only that store knows the key is an array (its
2139
2175
  `isSameKey`), so a fresh array with the same parts moves nothing. `createOIMSlotCollectionIndexService` takes a scalar key
2140
2176
  unless it is handed a `bucketSlotStoreService`: two columns or an array `keyOf` without one do not compile.
@@ -2190,13 +2226,13 @@ the bucket slot (`getSlotByKey` / `holdSlotByKey`). A reader that wants both sub
2190
2226
  to both, and the subscriber picks by which slots it puts in `createEffect`.
2191
2227
 
2192
2228
  ```ts
2193
- const deckIndexService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
2229
+ const deckIndexService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
2194
2230
  const deckIndex = deckIndexService.createIndexStoreSlot(cards);
2195
2231
  const spadesBucket = deckIndexService.holdSlotByKey(deckIndex, 'spades');
2196
2232
  const spadesAnyColumn = deckIndexService.holdAnyBucketColumnByKey(deckIndex, 'spades');
2197
- const entitiesEffect = effectService.createEffect(db, [spadesBucket, spadesAnyColumn], [], () => {
2233
+ const entitiesEffect = effectService.createEffect(db, () => {
2198
2234
  console.log(deckIndexService.getEntitiesByKey(deckIndex, 'spades'));
2199
- });
2235
+ }, [spadesBucket, spadesAnyColumn]);
2200
2236
  effectService.disposeEffect(entitiesEffect);
2201
2237
  deckIndexService.columnSlotService.unholdColumn(spadesAnyColumn);
2202
2238
  spadesBucket.owner.unholdSlot(spadesBucket);
@@ -2215,7 +2251,7 @@ needs the whole list each time: an export, an aggregate over the entities, a log
2215
2251
  It costs nothing until someone asks for one: a write is checked against it only after
2216
2252
  the index has been asked for the first any-column slot.
2217
2253
 
2218
- **The content-change hook.** `OIMContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames): void`
2254
+ **The content-change hook.** `OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames): void`
2219
2255
  and `removeContentChangeListener(storeSlot, listener): boolean` file and unfile a listener on that one store
2220
2256
  slot, by the listener object. `addReadColumn` / `removeReadColumn` / `addReadAnyColumn` / `removeReadAnyColumn`
2221
2257
  declare reads without a listener of their own, which is how an index declares its bucket columns.
@@ -2231,15 +2267,15 @@ every column write is added with `readColumnNames` `undefined`. **The filter is
2231
2267
  slot, not per listener**: once a write touches a column ANY listener of that store reads, every
2232
2268
  listener of that store is called, so a listener that declared a narrower set still checks
2233
2269
  `changedColumns` itself, as `OIMSlotCollectionIndexService` does against its key columns.
2234
- `IOIMContentChangeListener<TSlot, TPatch>` has two methods:
2270
+ `IOIMSlotContentChangeListener<TSlot, TPatch>` has two methods:
2235
2271
  `onContentChanged(slot, patch, changedColumns)` and
2236
- `onContentChangedMany(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
2272
+ `onManyContentsChanged(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
2237
2273
  parallel `OIMChannelQueue`s read with `getEntryCount()` / `getEntryAt(i)`, valid only during the
2238
2274
  call, so a listener copies what it keeps — `patch` is what an update
2239
2275
  wrote, `changedColumns` the names `markEntityChanged(slot, columns)` was given, either
2240
2276
  `undefined` when the write carries none. A single write outside a batch delivers one `onContentChanged` as soon as it is filed; inside
2241
2277
  `writeInBatch` (or `openBatch` / `closeBatch`) on the collection the writes are queued on the store slot and
2242
- delivered as ONE `onContentChangedMany` when the outermost batch closes. Code inside the batch reads the index
2278
+ delivered as ONE `onManyContentsChanged` when the outermost batch closes. Code inside the batch reads the index
2243
2279
  as it was before the batch.
2244
2280
  This hook is the only seam an index uses; an index of your own is a listener.
2245
2281
 
@@ -2254,10 +2290,10 @@ return the same kind of slot, written when any column of any member changes. A g
2254
2290
  `slot.owner.unholdSlot(slot)`.
2255
2291
 
2256
2292
  ```ts
2257
- const decksService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
2293
+ const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
2258
2294
  const decks = decksService.createIndexStoreSlot(cards);
2259
2295
  const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
2260
- effectService.disposeEffect(effectService.createEffect(db, [rankOfSpades], [], () => {}));
2296
+ effectService.disposeEffect(effectService.createEffect(db, () => {}, [rankOfSpades]));
2261
2297
  decksService.columnSlotService.unholdColumn(rankOfSpades);
2262
2298
  ```
2263
2299
 
@@ -2266,7 +2302,7 @@ decksService.columnSlotService.unholdColumn(rankOfSpades);
2266
2302
  ```ts signature
2267
2303
  createOIMSortedComputedService<TKey, TEntity extends object, TSlots, TSourceKey, TSourceStore, TComputedStore?>({
2268
2304
  indexService: OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore>;
2269
- sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { column: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
2305
+ sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
2270
2306
  comparator?: (left: TEntity, right: TEntity) => number;
2271
2307
  slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
2272
2308
  columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
@@ -2309,13 +2345,13 @@ slots sorted. A hold is given back with `slot.owner.unholdSlot(slot)`.
2309
2345
  to a member as with `comparator` alone.
2310
2346
 
2311
2347
  ```ts
2312
- const cardsInDeckService = createOIMSlotCollectionIndexService(db, {
2348
+ const cardsInDeckService = createOIMSlotCollectionIndexService({
2313
2349
  entityService: cardService,
2314
2350
  keyOf: (card: Card) => card.deck,
2315
2351
  });
2316
2352
  createOIMSortedComputedService({
2317
2353
  indexService: cardsInDeckService,
2318
- sortColumns: [{ column: 'title', direction: 'asc' }, { column: 'rank', direction: 'asc' }],
2354
+ sortColumns: [{ columnName: 'title', direction: 'asc' }, { columnName: 'rank', direction: 'asc' }],
2319
2355
  comparator: (l, r) => l.title.localeCompare(r.title, 'de') || l.rank - r.rank,
2320
2356
  }).createSortedStoreSlot(cardsInDeckService.createIndexStoreSlot(cards));
2321
2357
  // a write to `title` or `rank` re-sorts; a write to `note` does not
@@ -2328,17 +2364,17 @@ SECOND array of `createEffect`. A re-sort that yields the same sequence
2328
2364
  (`oimSameSlotSequence`) keeps the previous array, so its identity is stable.
2329
2365
 
2330
2366
  ```ts
2331
- const decksService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
2367
+ const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
2332
2368
  const decks = decksService.createIndexStoreSlot(cards);
2333
2369
  const byRankInDeckService = createOIMSortedComputedService({
2334
2370
  indexService: decksService,
2335
- sortColumns: [{ column: 'rank', direction: 'asc' }],
2371
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }],
2336
2372
  });
2337
2373
  const byRankInDeck = byRankInDeckService.createSortedStoreSlot(decks);
2338
2374
  const spadesByRank = byRankInDeckService.computedCollectionService.holdSlotByKey(byRankInDeck, 'spades');
2339
- const sortedEffect = effectService.createEffect(db, [], [spadesByRank], () => {
2375
+ const sortedEffect = effectService.createEffect(db, () => {
2340
2376
  render(spadesByRank.computedValueService.getValueForSlot(spadesByRank));
2341
- });
2377
+ }, [], [spadesByRank]);
2342
2378
  effectService.disposeEffect(sortedEffect);
2343
2379
  spadesByRank.owner.unholdSlot(spadesByRank);
2344
2380
  ```
@@ -2350,7 +2386,7 @@ index. `createOIMSlotCollectionSortedArrayIndexService` keeps the index's own bu
2350
2386
  the user picks one at construction.
2351
2387
 
2352
2388
  ```ts signature
2353
- createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>(db, {
2389
+ createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>({
2354
2390
  entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as createOIMSlotCollectionIndexService, scalar key
2355
2391
  sortColumns: readonly TOIMSortColumn<TEntity>[]; // REQUIRED
2356
2392
  bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService?, columnNames?,
@@ -2359,10 +2395,10 @@ createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKe
2359
2395
 
2360
2396
  ```ts
2361
2397
  declare const someCardSlot: TOIMEntitySlot<Card, TOIMStoreKey>;
2362
- const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService(db, {
2398
+ const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService({
2363
2399
  entityService: cardService,
2364
2400
  columns: ['deck'],
2365
- sortColumns: [{ column: 'rank', direction: 'asc' }, { column: 'id', direction: 'desc' }],
2401
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }, { columnName: 'id', direction: 'desc' }],
2366
2402
  });
2367
2403
  const cardsByDeck = cardsByDeckService.createIndexStoreSlot(cards);
2368
2404
  const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
@@ -2399,8 +2435,8 @@ filed under now, so a moved entity slot leaves its old bucket in O(1), also unde
2399
2435
  writer. Only the index writes it, through the `IOIMIndexKeysService` (`get(slot, position)`,
2400
2436
  `set(slot, position, indexKey)`), at the position `createIndexStoreSlot` allocated
2401
2437
  (`indexStoreSlot.indexKeysPosition`). Positions are allocated per entity store slot
2402
- (`sourceStoreSlot.indexKeysPositions`). Pick the strategy once per db, `new OIMDB({ indexKeysService })`
2403
- (default `OIMDefaults.INDEX_KEYS_SERVICE.getValue()`):
2438
+ (`sourceStoreSlot.indexKeysPositions`). Pick the strategy with `new OIMDefaults({ indexKeysService })` passed as `defaults`
2439
+ (default `OIMAppendingIndexKeysService.INSTANCE`):
2404
2440
 
2405
2441
  | strategy | on `disposeIndexStoreSlot` | cost |
2406
2442
  |---|---|---|
@@ -2429,7 +2465,7 @@ type TOIMComputedSources = {
2429
2465
  In a `createSources` result (of `createOIMComputedService`'s options, or of `createStoreSlot`) both `sourceSlots` and `sourceComputeds` are optional; a missing one means none.
2430
2466
 
2431
2467
  ```ts signature
2432
- type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<TOIMDerivation> & {
2468
+ type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<TOIMComputedDerivation> & {
2433
2469
  readonly owner: IOIMSlotOwner;
2434
2470
  readonly computedValueService: IOIMComputedValueService<TValue>;
2435
2471
  readonly value: TValue;
@@ -2475,17 +2511,25 @@ class OIMComputedService<TKey, TValue, TComputedStore> {
2475
2511
  }
2476
2512
 
2477
2513
  createOIMComputedService<TKey, TValue, TComputedStore = Map<TKey, TOIMComputedSlot<TValue, TKey>>>({
2478
- createSources?: (key: TKey, storeSlot: TOIMStoreSlot | undefined) => Partial<TOIMComputedSources>;
2514
+ createSources?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>;
2479
2515
  derive?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue;
2480
2516
  sameValueGuard?: TOIMSameValueGuard<TValue>; // default Object.is
2517
+ releaseValue?: TOIMComputedValueReleaser<TValue>; // default: gives back nothing
2481
2518
  initialValue?: TValue;
2482
2519
  slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
2483
- columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // OIMDefaults.COMPUTED_COLUMN_SLOT_STORE_SERVICE
2520
+ columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // defaults.computedColumnSlotStoreService, then a shared map store
2484
2521
  columnNames?: readonly Extract<keyof TValue, string>[];
2485
2522
  computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore>; // Map; REQUIRED for any other store
2486
2523
  }): OIMComputedService<TKey, TValue, TComputedStore>
2487
2524
  ```
2488
2525
 
2526
+ **`releaseValue(value)` gives back what a derived value holds** (a request in flight, a timer). It is called
2527
+ with the previous value when `derive` produced one `sameValueGuard` calls different; with a new object the
2528
+ guard calls equal (the slot keeps the old one, the new one is dropped); with the current value when the
2529
+ computed slot is released; with the value a `derive` returned after its own slot was released (not stored).
2530
+ It is not called when `derive` returns the object the slot already holds, and never with `initialValue`.
2531
+ `createOIMSingleComputedService` takes it too.
2532
+
2489
2533
  The factory takes no db: a computed service is configuration, and each store slot takes its channel slot at
2490
2534
  `createComputedStoreSlot(db, …)`.
2491
2535
 
@@ -2523,8 +2567,7 @@ A single computed store slot has no key and a store of one slot. `getSingleSlot(
2523
2567
  next flush unless something holds it — and `holdSlotByKey(storeSlot, undefined)` holds the live slot, minting
2524
2568
  one if the previous is gone. A computed slot goes at its last unhold once no effect reads it
2525
2569
  (`OIMSlotAtUnholdReleasePolicy`, the default), and gives back the holds it took on its
2526
- sources. A last unhold during a flush releases at the end of that flush, through the
2527
- db's unused-slot collector, as for every non-empty slot. The holds a computed takes on its sources and
2570
+ sources. A last unhold during a flush releases it at once, as for every slot. The holds a computed takes on its sources and
2528
2571
  gives back are one stateless service, shared by both shapes.
2529
2572
 
2530
2573
  **`createSources` takes the key and the store slot, and nothing else. There is no `keep`.** A build that
@@ -2567,7 +2610,7 @@ over the carrier computed, its value `carrier.value[column]` (a tuple of the gro
2567
2610
  the whole value for the any-column), compared with `Object.is`. It is a `TOIMComputedColumnSlot`
2568
2611
  (`TOIMComputedSlot<unknown, string>`), so it goes in the SECOND array of `createEffect`, and its value
2569
2612
  is read off `columnSlot.value`. It wakes an effect only when that field of the derived value changes.
2570
- Pinned by `facade/computed/tests/what-a-computed-column-wakes-on.test.ts`.
2613
+ Pinned by `computed/tests/what-a-computed-column-wakes-on.test.ts`.
2571
2614
 
2572
2615
  ```ts
2573
2616
  type Summary = { top: number; count: number };
@@ -2579,7 +2622,7 @@ const summary = summaryService.createComputedStoreSlot(
2579
2622
  );
2580
2623
  const summarySingleSlotService = summaryService.computedSingleSlotService;
2581
2624
  const summaryTop = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(summary), 'top');
2582
- const topEffect = effectService.createEffect(db, [], [summaryTop], () => { paint(slot.entity); });
2625
+ const topEffect = effectService.createEffect(db, () => { paint(slot.entity); }, [], [summaryTop]);
2583
2626
  effectService.disposeEffect(topEffect);
2584
2627
  summarySingleSlotService.columnSlotService.unholdColumn(summaryTop);
2585
2628
  ```
@@ -2619,7 +2662,7 @@ const orderService = createOIMBucketMemoComputedService({
2619
2662
  });
2620
2663
  const order = orderService.createBucketMemoStoreSlot(ordered);
2621
2664
  const deck = orderService.computedCollectionService.holdSlotByKey(order, 'spades'); // mints the bucket too, and holds it
2622
- effectService.createEffect(db, [], [deck], () => render(deck.computedValueService.getValueForSlot(deck)));
2665
+ effectService.createEffect(db, () => render(deck.computedValueService.getValueForSlot(deck)), [], [deck]);
2623
2666
  ```
2624
2667
 
2625
2668
  ---
@@ -2680,8 +2723,8 @@ are holding; neither is something you build by hand.
2680
2723
  | Store | Key type |
2681
2724
  |---|---|
2682
2725
  | `OIMSlotMapStoreService<TSlot>` | `TOIMStoreKey` = `string \| number` |
2683
- | `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` | `readonly TOIMStoreKey[]` — **a chain of VALUES** |
2684
- | `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue()` | `readonly TOIMEntitySlot[]` — **a chain of entity SLOTS** |
2726
+ | `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `OIMScalarKeyTreeWalk.INSTANCE` | `readonly TOIMStoreKey[]` — **a chain of VALUES** |
2727
+ | `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `OIMCompositeKeyTreeWalk.INSTANCE` | `readonly TOIMEntitySlot[]` — **a chain of entity SLOTS** |
2685
2728
  | `OIMSingleSlotStoreService.INSTANCE` | `void` — one slot per store slot, its store a `TOIMSingleSlotStore` (`{ slot }`); what every single is built on |
2686
2729
 
2687
2730
  Two stores for keys, one per shape of address, and the single-slot store for no key. The chain store descends one `Map`
@@ -2722,8 +2765,8 @@ deletes nothing and returns `undefined`.
2722
2765
 
2723
2766
  | Walk | Class | Shared instance | What a composite key part is | Holds? |
2724
2767
  |---|---|---|---|---|
2725
- | scalar | `OIMScalarKeyTreeWalk` | `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` | `TOIMStoreKey` | no |
2726
- | composite | `OIMCompositeKeyTreeWalk` | `OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue()` | `TOIMEntitySlot` | **yes — one hold per edge** |
2768
+ | scalar | `OIMScalarKeyTreeWalk` | `OIMScalarKeyTreeWalk.INSTANCE` | `TOIMStoreKey` | no |
2769
+ | composite | `OIMCompositeKeyTreeWalk` | `OIMCompositeKeyTreeWalk.INSTANCE` | `TOIMEntitySlot` | **yes — one hold per edge** |
2727
2770
 
2728
2771
  **The hold is the whole difference.** `OIMCompositeKeyTreeWalk.mintNodeAt` calls
2729
2772
  `holdCompositeKeyPart` the first time it creates an edge, which runs
@@ -2745,10 +2788,10 @@ type TTallyStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Tally,
2745
2788
 
2746
2789
  const tallyService = createOIMSlotCollectionService<
2747
2790
  ReturnType<typeof createOIMSlotContentEntityService<Tally, TScalarChain>>, TTallyStore
2748
- >(db, {
2749
- slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(db),
2791
+ >({
2792
+ slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(),
2750
2793
  slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
2751
- OIMDefaults.KEY_TREE_WALK_SCALAR.getValue(),
2794
+ OIMScalarKeyTreeWalk.INSTANCE,
2752
2795
  ),
2753
2796
  });
2754
2797
  const tallies = tallyService.createStoreSlot(db);
@@ -2775,10 +2818,10 @@ type TEnrolmentStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<En
2775
2818
 
2776
2819
  const enrolmentService = createOIMSlotCollectionService<
2777
2820
  ReturnType<typeof createOIMSlotContentEntityService<Enrolment, TChain>>, TEnrolmentStore
2778
- >(db, {
2779
- slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(db),
2821
+ >({
2822
+ slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(),
2780
2823
  slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
2781
- OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue(),
2824
+ OIMCompositeKeyTreeWalk.INSTANCE,
2782
2825
  ),
2783
2826
  });
2784
2827
  const enrolments = enrolmentService.createStoreSlot(db);
@@ -2788,7 +2831,7 @@ enrolmentService.getSlotByKey(enrolments, [alice, admins]);
2788
2831
 
2789
2832
  **Never put a scalar in a composite-walk key.** It is a type error, and
2790
2833
  past a cast it throws a TypeError inside the mint, setting `holdCount` on a
2791
- string, after the edge is already in the tree. `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` is
2834
+ string, after the edge is already in the tree. `OIMScalarKeyTreeWalk.INSTANCE` is
2792
2835
  the walk for that key — the same store class, the other strategy.
2793
2836
 
2794
2837
  **Compare a chain with the strategy that matches it**: `oimSameSlotSequence` for identities
@@ -2801,8 +2844,7 @@ address" for the address the tree is looking at.
2801
2844
  ## Lifetime — the owner protocol
2802
2845
 
2803
2846
  **`slot.owner` is the only reference a slot carries, and it is never
2804
- `undefined`.** Every slot minted anywhere has one; a slot nothing filed carries
2805
- `OIMDefaults.STANDALONE_SLOT_OWNER.getValue()`, a slot a collection filed carries that collection type's slot owner (an `OIMSlotOwnerService`) — keyed or single alike — and a slot that has been released KEEPS its owner
2847
+ `undefined`.** Every slot minted anywhere has one: a slot a collection filed carries that collection type's slot owner (an `OIMSlotOwnerService`) — keyed or single alike — and a slot that has been released KEEPS its owner
2806
2848
  and raises the RELEASED flag in `slot.slotFlags` instead, which the doors below read.
2807
2849
 
2808
2850
  ```ts signature
@@ -2828,18 +2870,15 @@ At a deadline the release queue reads its MARKER off the slot, decides, and call
2828
2870
 
2829
2871
  **`OIMSlotOwner`** is the base class and it is concrete: it implements all five.
2830
2872
  Its `releaseSlotIfUnheld` does nothing, its
2831
- `unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMSlotEditable` with no
2873
+ `unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMEditable` with no
2832
2874
  guard at all (it exists so a library that adds its own release wheel can keep its own bit in
2833
- `slot.slotFlags`; the engine owns bits `1`, `2`, `4`, `8` and `16`, so a library starts at `32`;
2875
+ `slot.slotFlags`; the engine owns bits `1`, `2`, `4`, `8`, `16` and `32`, so a library starts at `64`;
2834
2876
  it is on the owner so a subclass can hook every such mutation), and a `protected
2835
2877
  onSlotIdle` does nothing; a `protected assertNotReleased` is what every refusing
2836
2878
  door above calls. `OIMSlotOwnerService` overrides `onSlotIdle` and
2837
2879
  `releaseSlotIfUnheld`, asking its release policy and its content service; the clock is the slot's channel slot's.
2838
- Extend `OIMSlotOwner` when you own slots that no collection files.
2839
- **`OIMDefaults.STANDALONE_SLOT_OWNER`** is one shared instance of it — the owner of a
2840
- computed slot nobody has filed. It never releases anything, which is exactly what
2841
- "filed nowhere, so nothing can unfile it" means. A single's slot is not such a slot: its
2842
- owner is its collection type's slot owner, and that release policy applies to it.
2880
+ A single's slot is owned the same way: its owner is its collection type's slot owner, and that release policy
2881
+ applies to it.
2843
2882
 
2844
2883
  ### The column owner
2845
2884
 
@@ -2877,12 +2916,12 @@ so a slot with any live column slot is never released.
2877
2916
  ### Disposers
2878
2917
 
2879
2918
  The disposers this package hands back are the unsubscribes `OIMChannelService.INSTANCE.subscribeAfterEachFrame`
2880
- and its siblings return, and the one `effectService.subscribeEffect(db, sourceSlots, sourceComputeds, body)`
2919
+ and its siblings return, and the one `effectService.subscribeEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`
2881
2920
  returns. An effect from `createEffect` has none, so the caller writes
2882
2921
  `() => effectService.disposeEffect(effect)`. A hold is given back through its owner, not through a disposer.
2883
2922
 
2884
2923
  ```ts
2885
- const effect = effectService.createEffect(db, [slot], [], () => { paint(slot.entity); });
2924
+ const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
2886
2925
  const disposers: TOIMDisposer[] = [];
2887
2926
  disposers.push(() => { effectService.disposeEffect(effect); });
2888
2927
  disposers.push(() => { slot.owner.unholdSlot(slot); });
@@ -2933,15 +2972,15 @@ leave the longest.
2933
2972
  type TOIMStoreKey = string | number;
2934
2973
  type TOIMDisposer = () => void;
2935
2974
  type TOIMEffectBody = () => (() => void) | void; // a returned function is the undo
2936
- type TOIMEffectBodyWithSourceTracking = (effectSourceTracker: OIMEffectSourceTracker) => (() => void) | void;
2975
+ type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
2937
2976
  type TOIMSameValueGuard<TValue = unknown> = (prev: TValue, next: TValue) => boolean;
2938
2977
  type TOIMChannelScheduler = (flush: () => void) => void;
2939
- type TOIMSortColumn<TEntity extends object> = { column: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' };
2978
+ type TOIMSortColumn<TEntity extends object> = { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' };
2940
2979
  type TOIMSortableColumnName<TEntity extends object> = // a column whose values are string | number | bigint
2941
2980
  { [TColumn in keyof TEntity]-?: NonNullable<TEntity[TColumn]> extends string | number | bigint ? TColumn : never }[keyof TEntity] & string;
2942
2981
 
2943
2982
  // EVERY property is `readonly`. A slot is written through its owner and its
2944
- // service, never by assigning to it; `TOIMSlotEditable` is how the engine
2983
+ // service, never by assigning to it; `TOIMEditable` is how the engine
2945
2984
  // itself drops that guard where it is the one mutator.
2946
2985
  type TOIMSlot = {
2947
2986
  readonly effects: TOIMEffect[] | undefined;
@@ -2952,9 +2991,9 @@ type TOIMSlot = {
2952
2991
  readonly releaseDueAfterWriteMs: number; // -1 until a write stamps it
2953
2992
  readonly releaseDueAfterIdleMs: number; // -1 until idleness stamps it
2954
2993
  readonly releaseDueAfterUnholdMs: number; // -1 until the last unhold stamps it
2955
- readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2
2994
+ readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2; bits up to 32 are the engine's
2956
2995
  readonly columnSlotStore: unknown; // the column slots this slot carries; undefined while none
2957
- readonly storeSlot: TOIMStoreSlot | undefined;
2996
+ readonly storeSlot: TOIMStoreSlot; // the store slot it belongs to; a column slot's is its carrier's
2958
2997
  readonly owner: IOIMSlotOwner; // never undefined, never cleared
2959
2998
  };
2960
2999
 
@@ -2964,7 +3003,7 @@ type TOIMSlot = {
2964
3003
  type TOIMStoreSlot<TStore = unknown, TSlot extends TOIMSlot = TOIMSlot, TPatch = unknown> = {
2965
3004
  readonly store: TStore;
2966
3005
  readonly channelSlot: TOIMChannelSlot;
2967
- readonly contentChangeListeners: IOIMContentChangeListener<TSlot, TPatch>[] | undefined;
3006
+ readonly contentChangeListeners: IOIMSlotContentChangeListener<TSlot, TPatch>[] | undefined;
2968
3007
  readonly contentChangeListenerReadColumnNames: (readonly string[] | undefined)[] | undefined;
2969
3008
  readonly indexKeysPositions: TOIMIndexKeysPositions | undefined;
2970
3009
  readonly sourceStoreSlot: TOIMStoreSlot | undefined; // an index store slot's source
@@ -2981,7 +3020,7 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
2981
3020
  TOIMStoreSlot<TStore, TBucketSlot, TBucketPatch> & {
2982
3021
  readonly sourceStoreSlot: TOIMStoreSlot<TSourceStore, TSourceSlot, TSourcePatch>;
2983
3022
  readonly indexKeysPosition: number;
2984
- readonly contentChangeListener: IOIMContentChangeListener<TSourceSlot, TSourcePatch>;
3023
+ readonly contentChangeListener: IOIMSlotContentChangeListener<TSourceSlot, TSourcePatch>;
2985
3024
  readonly requestedKeys: readonly TKey[] | undefined;
2986
3025
  readonly bucketColumnNames: Set<string>;
2987
3026
  readonly isAnyBucketColumnRead: boolean;
@@ -2991,10 +3030,10 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
2991
3030
  type TOIMComputedStoreSlot<TStore = unknown, TValue = unknown, TKey = unknown> =
2992
3031
  TOIMStoreSlot<TStore, TOIMComputedSlot<TValue, TKey>> & {
2993
3032
  readonly derive: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue) | undefined;
2994
- readonly createSources: ((key: TKey, storeSlot: TOIMStoreSlot | undefined) => Partial<TOIMComputedSources>) | undefined;
3033
+ readonly createSources: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>) | undefined;
2995
3034
  };
2996
3035
 
2997
- type TOIMSlotEditable<TSlot> = { -readonly [K in keyof TSlot]: TSlot[K] };
3036
+ type TOIMEditable<TSlot> = { -readonly [K in keyof TSlot]: TSlot[K] };
2998
3037
 
2999
3038
  type TOIMKeyedSlot<TKey = unknown> = TOIMSlot & { readonly key: TKey };
3000
3039
  type TOIMIndexableSlot<TKey = unknown> = TOIMKeyedSlot<TKey> & { readonly indexKeys: unknown[] | undefined };
@@ -3006,7 +3045,7 @@ type TOIMEntitySlot<TEntity extends object = object, TKey = unknown> = TOIMIndex
3006
3045
 
3007
3046
  // `data` belongs to the consumer: the core never reads or writes it, and clearing or
3008
3047
  // removing the slot (`slotContentService.clearSlotContent(slot)`) does not reset it. Each index keeps its
3009
- // last key for the entity slot in `indexKeys`, through the db's `IOIMIndexKeysService`.
3048
+ // last key for the entity slot in `indexKeys`, through the index's `IOIMIndexKeysService`.
3010
3049
 
3011
3050
  type TOIMColumnSlot = TOIMSlot & {
3012
3051
  readonly carrierSlot: TOIMSlot; // the entity slot or bucket slot it hangs off
@@ -3040,14 +3079,14 @@ type TOIMEntityColumnName<TEntity extends object> =
3040
3079
  [TEntity] extends [never] ? never
3041
3080
  : object extends TEntity ? string : keyof TEntity & string;
3042
3081
 
3043
- type TOIMDerivation = TOIMComputedSources & { // what a computed slot and a TOIMEffect share
3082
+ type TOIMComputedDerivation = TOIMComputedSources & { // what a computed slot and a TOIMEffect share
3044
3083
  ranAtRevision: number; ranAtEpoch: number;
3045
3084
  };
3046
3085
 
3047
3086
  type TOIMTimer = (run: () => void, inMs: number) => TOIMDisposer;
3048
3087
  ```
3049
3088
 
3050
- **`TOIMSlotEditable<TSlot>` strips `readonly` from every property.** It is exported
3089
+ **`TOIMEditable<TSlot>` strips `readonly` from every property.** It is exported
3051
3090
  because the rule "data has one mutator" is enforced by the slot types being
3052
3091
  readonly everywhere else: a class that IS the sole mutator of a slot's property
3053
3092
  casts through this type at the one place it writes, and every other reader is
@@ -3069,7 +3108,11 @@ that matches what you are comparing; they are not interchangeable.
3069
3108
 
3070
3109
  ## Full export list
3071
3110
 
3072
- **218 names, every export of `packages/core/src/index.ts`, grouped by topic.**
3111
+ **237 names, every export of `packages/core/src/index.ts`, grouped by topic.**
3112
+
3113
+ *Store aggregates* — `OIMSlotCollectionStoreAggregate` · `OIMWritableSlotCollectionStoreAggregate` ·
3114
+ `OIMSingleSlotStoreAggregate` · `OIMWritableSingleSlotStoreAggregate` · `OIMSlotCollectionIndexStoreAggregate` ·
3115
+ `createOIMSlotCollectionStoreAggregate` · `createOIMSingleSlotStoreAggregate`
3073
3116
 
3074
3117
  *Channel and database slots* — `OIMChannelService` · `TOIMChannelSlot` · `TOIMChannelSlotOptions` · `createOIMChannelSlot` ·
3075
3118
  `IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot` · `IOIMFlushSchedulerListener`
@@ -3086,7 +3129,7 @@ that matches what you are comparing; they are not interchangeable.
3086
3129
  `OIMSlotOwnerService` (**type only**) · `TOIMSlotOwnerServiceOptions` · `IOIMSlotContentService` · `IOIMSlotContentWriter` ·
3087
3130
  `IOIMKeyedSlotCreator` · `IOIMSingleSlotCreator` · `IOIMSlotFilingService` · `TOIMSlotContentServiceKey` ·
3088
3131
  `TOIMSlotContentServiceSlot` · `TOIMSlotContentServiceSingleSlot` · `TOIMSlotContentServiceContent` ·
3089
- `TOIMSlotContentServicePatch` · `OIMContentChangeListenerService`
3132
+ `TOIMSlotContentServicePatch` · `OIMSlotContentChangeListenerService`
3090
3133
 
3091
3134
  *Content services* — `OIMSlotContentEntityService` · `TOIMSlotContentEntityServiceOptions` ·
3092
3135
  `createOIMSlotContentEntityService` · `TOIMEntitySlotOwnerService` · `TOIMEntityContentSlot` · `OIMSlotContentBucketService` ·
@@ -3108,20 +3151,20 @@ that matches what you are comparing; they are not interchangeable.
3108
3151
  written after that clock reading — `channelSlot.clock` read earlier.
3109
3152
 
3110
3153
  *Derivations* — `OIMEffectService` · `IOIMEffectService` · `TOIMEffect` (**type only** —
3111
- `OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `TOIMEffectBodyWithSourceTracking` ·
3112
- `OIMEffectSourceTracker` (**type only** — the service builds it) · `TOIMComputedSlot` · `TOIMComputedSources` ·
3113
- `OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `TOIMDerivation`
3154
+ `OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `TOIMSourceTrackingEffectBody` ·
3155
+ `OIMSourceTracker` (**type only** — the service builds it) · `TOIMComputedSlot` · `TOIMComputedSources` · `TOIMComputedValueReleaser` · `OIMSourceTrackingComputedService` · `OIMSingleSourceTrackingComputedService` · `createOIMSourceTrackingComputedService` · `createOIMSingleSourceTrackingComputedService` · `TOIMSourceTrackingComputedSlot` · `TOIMSourceTrackingComputedContentSlot` · `TOIMSourceTrackingComputedDerive` · `OIMSlotContentSourceTrackingComputedService` (**type only**) · `TOIMSourceTrackingComputedDerivationOptions` · `TOIMComputedServiceFactoryOptions` ·
3156
+ `OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `TOIMComputedDerivation`
3114
3157
 
3115
3158
  `oimRefreshSourceComputeds(sourceComputeds)` brings every computed in the list current. A binding
3116
3159
  read outside an effect (`useOIMValue` in `@oimdb/react`, `exoBinding` in `@oimdb/exodra`) calls it
3117
3160
  on its `sourceComputeds` before `derive`, so the derive never reads a stale computed.
3118
3161
 
3119
- *Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMSlotEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
3162
+ *Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
3120
3163
  `TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `OIMColumnSlotOwner` ·
3121
3164
  `OIMPushColumnSlotOwner` · `OIMPullColumnSlotOwner` · `TOIMColumnSlotOwnerOptions` · `TOIMBucketSlot` ·
3122
3165
  `TOIMSameValueGuard` · `OIMSlotOwner` ·
3123
3166
  `IOIMSlotOwner` · `IOIMSlotEmptinessPolicy` ·
3124
- `IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMContentChangeListener`
3167
+ `IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMSlotContentChangeListener`
3125
3168
 
3126
3169
  `oimIsSlotEmpty(slot): boolean` reads the EMPTY bit of `slot.slotFlags`.
3127
3170
  `oimIsSlotReleased(slot): boolean` reads its RELEASED bit.
@@ -3134,9 +3177,9 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
3134
3177
  `OIMSlotAtUnholdReleasePolicy` · `OIMSlotAtEmptyReleasePolicy` · `OIMSlotNeverReleasePolicy` · `OIMSlotEarliestReleasePolicy` ·
3135
3178
  `TOIMTimer` · `oimSliceOfMs` · `OIM_SLICE_LENGTH_MS`
3136
3179
 
3137
- *Entity writers* — `IOIMEntityWriter` · `createOIMEntityWriterInPlace` ·
3138
- `createOIMEntityWriterReplacing` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
3139
- `oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimPatchEntityColumnsGuarded` · `oimPatchEntityColumnGuarded` · `oimPutEntityColumns` · `oimPutEntityColumnsGuarded` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
3180
+ *Entity writers* — `IOIMEntityWriter` · `createOIMInPlaceEntityWriter` ·
3181
+ `createOIMReplacingEntityWriter` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
3182
+ `oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimGuardedPatchEntityColumns` · `oimGuardedPatchEntityColumn` · `oimPutEntityColumns` · `oimGuardedPutEntityColumns` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
3140
3183
 
3141
3184
  *Buckets* — `OIMBucketCommands` · `TOIMBucketCommandsOptions` · `IOIMBucketCommands` ·
3142
3185
  `IOIMBucketMemberSlotsArrayService` · `OIMBucketMemberSlotsArrayService` · `OIMBucketUniqueMemberSlotsArrayService` ·
@@ -3160,13 +3203,12 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
3160
3203
  `TOIMSlotStoreTree` · `IOIMKeyTreeWalk` · `TOIMCompositeKeyNode` ·
3161
3204
  `OIMKeyTreeWalk` · `OIMCompositeKeyTreeWalk` · `OIMScalarKeyTreeWalk`
3162
3205
 
3163
- *Assemblies* — `OIMDB` · `OIMDefaults` · `OIMDefaultValue` · `TOIMFactoryOptions`
3206
+ *Assemblies* — `OIMDB` · `OIMDefaults` · `TOIMDefaultsOptions` · `OIMDefaultValue` · `TOIMFactoryOptions`
3164
3207
 
3165
3208
  *Comparison and diffing* — `oimSameValueZeroGuard` ·
3166
3209
  `oimSameSlotSequence` · `oimSameKeySequence` · `oimOrderedListCommandsOfTransition` · `TOIMOrderedListCommand`
3167
3210
 
3168
- *Shared defaults* — `OIMDefaults.STANDALONE_SLOT_OWNER`,
3169
- `OIMDefaults.KEY_TREE_WALK_SCALAR` and `OIMDefaults.KEY_TREE_WALK_COMPOSITE`.
3211
+ *Shared defaults* — `OIMScalarKeyTreeWalk.INSTANCE` and `OIMCompositeKeyTreeWalk.INSTANCE`.
3170
3212
 
3171
3213
  **Nothing else is exported.** If a symbol is not on this list, it does not exist.
3172
3214
 
@@ -3174,18 +3216,18 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
3174
3216
 
3175
3217
  ## Known defects
3176
3218
 
3177
- ### A scalar in a `KEY_TREE_WALK_COMPOSITE` key throws
3219
+ ### A scalar in a key of the composite key tree walk throws
3178
3220
 
3179
- `OIMSlotTreeStoreService` built with `OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue()` keys on
3221
+ `OIMSlotTreeStoreService` built with `OIMCompositeKeyTreeWalk.INSTANCE` keys on
3180
3222
  `readonly TOIMEntitySlot[]`, so a string or a number in the key throws
3181
3223
  a TypeError past a cast, setting `holdCount` on a string. The store for a tuple
3182
3224
  of scalars is `OIMSlotTreeStoreService` built with
3183
- `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` —
3225
+ `OIMScalarKeyTreeWalk.INSTANCE` —
3184
3226
  see **Key stores** above. The fix is the other walk, never a cast.
3185
3227
 
3186
3228
  ### `TOIMEffect` is a type-only export
3187
3229
 
3188
- `effectService.createEffect(...)` and `effectService.createEffectImmediate(...)` are the whole of
3230
+ `effectService.createEffect(...)` and `effectService.createAndRunEffect(...)` are the whole of
3189
3231
  legitimate construction. An object literal typed `TOIMEffect` subscribes to nothing and
3190
3232
  schedules nothing: when one of its sources already moved in the open batch, a
3191
3233
  later write to that source is swallowed by the coalescing guard and the effect
@@ -3199,8 +3241,8 @@ Every `@oimdb` package ships both formats — `dist/index.js` for `import`,
3199
3241
  condition so a CommonJS consumer's declarations match its runtime), both named
3200
3242
  in the `exports` map. A runtime loads a package once per format, so a process
3201
3243
  that reaches the core both ways would hold two copies of every class and every
3202
- module-level value — two `OIMDefaults`, so a default set through one never
3203
- reaches a service built by the other.
3244
+ module-level value — two sets of built-in defaults and two `OIMDefaults` classes,
3245
+ so services built by the two copies share nothing.
3204
3246
 
3205
3247
  So `src/index.ts` registers its copy under `Symbol.for('@oimdb/core')` on
3206
3248
  `globalThis`, and the second copy to load throws
@@ -3218,26 +3260,27 @@ core objects around — through both spellings in one process. Pick one.
3218
3260
 
3219
3261
  ## Other packages
3220
3262
 
3221
- `@oimdb/react`, `@oimdb/persist` (with its five backends) and `@oimdb/exodra` build
3222
- on the current core — persist and exodra have sections of their own below. Every
3263
+ `@oimdb/react`, `@oimdb/persist` (with its five backends), `@oimdb/exodra` and `@oimdb/browser` build
3264
+ on the current core — persist, exodra and browser have sections of their own below. Every
3223
3265
  remaining package imports only names the core exports. Read a package's own
3224
3266
  source before generating code against it.
3225
3267
 
3226
3268
  `@oimdb/react` — `<OIMDBProvider db={db}>` carries the `OIMDB` (`useOIMDB()`) and nothing else: the effect
3227
3269
  service is stateless (`OIMEffectService.INSTANCE`) and every hook subscribes with
3228
3270
  `OIMEffectService.INSTANCE.subscribeEffect(db, …)`. A hook that resolves or takes a slot filed on another `OIMDB`
3229
- throws, since nothing below would wake. Every hook that reads a collection takes the collection service, typed as a
3230
- `Pick` of `OIMSlotCollectionService` with only the members it calls, and then the store slot:
3231
- `useOIMEntity(entityCollectionService, entityStoreSlot, key)`,
3232
- `useOIMField(entityCollectionService, slot, name)` (calls `getColumn(slot, name)`; no store slot, the slot carries it),
3233
- `useOIMBucketSlots(bucketCollectionService, bucketSlot)` and
3234
- `useOIMBucketSlotsByKey(bucketCollectionService, bucketStoreSlot, key)` (both read
3235
- `bucketCollectionService.slotContentService.getBucketMemberSlots`; an index service and its index store slot fit too), and
3271
+ throws, since nothing below would wake. Every hook that reads a collection by key takes the store slot alone and
3272
+ calls the service it carries, `storeSlot.slotService`, typed as a `Pick` of `OIMSlotCollectionService` with only
3273
+ the members it calls (a store aggregate passes its `storeSlot`):
3274
+ `useOIMEntity(entityStoreSlot, key)` and
3275
+ `useOIMBucketSlotsByKey(bucketStoreSlot, key)` (reads `bucketStoreSlot.slotService.slotContentService.getBucketMemberSlots`;
3276
+ an index store slot fits too). The hooks over a slot you already hold take the service and the slot:
3277
+ `useOIMField(entityCollectionService, slot, name)` (calls `getColumn(slot, name)`),
3278
+ `useOIMBucketSlots(bucketCollectionService, bucketSlot)`, and
3236
3279
  `useOIMValue(binding)`, where a binding is `{ sourceSlots?, sourceComputeds?, derive }` (a missing list means none)
3237
3280
  and must keep its identity across renders.
3238
3281
  `useOIMComputedValue(computedSlot: TOIMComputedSlot<TValue>): TValue` is the one-computed case: it subscribes to
3239
3282
  the slot, settles it and reads `computedSlot.value`, with no binding to memoise.
3240
- `useOIMComputedValueByKey(computedService.computedCollectionService, computedStoreSlot, key)` reads one key of a
3283
+ `useOIMComputedValueByKey(computedStoreSlot, key)` reads one key of a
3241
3284
  computed store slot (plain, sorted, bucket memo) with the same by-key body as `useOIMEntity` /
3242
3285
  `useOIMBucketSlotsByKey`: resolve on commit with `holdSlotByKey`, effect over that slot, resolve-hold given back,
3243
3286
  a key built inline settled by meaning.
@@ -3246,16 +3289,15 @@ The computed SERVICE is the caller's, built once at module scope with `createOIM
3246
3289
  slot per mount — `computedService.createComputedStoreSlot(db, derive, createSources)` in a `useMemo` on `deps`;
3247
3290
  `derive` / `createSources` may be `undefined` when the service has them.
3248
3291
  `useOIMSingleComputedValue(computedService, derive, createSources, deps): TValue` is the same for a void-keyed
3249
- service whose `computedSlotStoreService` is `OIMSingleSlotStoreService.INSTANCE`, read through
3250
- `useOIMComputedValueByKey(computedService.computedCollectionService, computedStoreSlot, undefined)`, so a flush
3251
- between render and subscribe is safe.
3252
- `useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)` holds the identities its keys name for as long as the
3292
+ service built with `createOIMSingleComputedService`, its slot read through the store slot's single slot service,
3293
+ so a flush between render and subscribe is safe.
3294
+ `useOIMSlotsByKeys(storeSlot, keys)` holds the identities its keys name for as long as the
3253
3295
  component is mounted (a `holdSlotByKey` per key on commit, the same holds given back on unmount or when the list
3254
3296
  moves) and subscribes to nothing; it needs no provider.
3255
- Singles are not keyed collections: `createOIMSingleSlotService(db, { slotContentService })` returns an
3297
+ Singles are not keyed collections: `createOIMSingleSlotService({ slotContentService })` returns an
3256
3298
  `OIMWritableSingleSlotService` (`getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`), read by
3257
- `useOIMSingleEntity(singleSlotService, singleSlotStoreSlot): TEntity | undefined` and
3258
- `useOIMSingleBucketSlots(singleSlotService, singleSlotStoreSlot): readonly TOIMEntitySlot<TEntity>[]` — the by-key
3299
+ `useOIMSingleEntity(singleSlotStoreSlot): TEntity | undefined` and
3300
+ `useOIMSingleBucketSlots(singleSlotStoreSlot): readonly TOIMEntitySlot<TEntity>[]` — the by-key
3259
3301
  mechanics without a key (getSingleSlot on render, holdSingleSlot on commit, resolve-hold given back once the
3260
3302
  effect exists; the bucket members are copied per render). See `packages/react/README.md`.
3261
3303
 
@@ -3264,11 +3306,12 @@ re-read the imports rather than trusting this table.
3264
3306
 
3265
3307
  | Package | Imports from the core | Imports a name the core does not export? |
3266
3308
  |---|---|---|
3267
- | `@oimdb/persist` | `IOIMChannelMap`, `IOIMContentChangeListener`, `IOIMEntityWriter`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMContentChangeListenerService`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMChannelClock`, `TOIMChannelSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMSingleSlotStore`, `TOIMSingleSlotStoreSlot`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimIsAnySlotChangedAfter`, `oimIsSlotReleased`, `oimSameValueZeroGuard`, `oimUnholdSlotsIfAlive` | no |
3268
- | `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMContentChangeListener`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMContentChangeListenerService`, `OIMEffectService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntityColumnName`, `TOIMEntitySlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
3309
+ | `@oimdb/persist` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `IOIMEntityWriter`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMChannelClock`, `TOIMChannelSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMSingleSlotStore`, `TOIMSingleSlotStoreSlot`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimIsAnySlotChangedAfter`, `oimIsSlotReleased`, `oimSameValueZeroGuard`, `oimUnholdSlotsIfAlive` | no |
3310
+ | `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntityColumnName`, `TOIMEntitySlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
3269
3311
  | `@oimdb/exodra` | `IOIMChannelMap`, `OIMChannelService`, `OIMEffectService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMComputedSlot`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMOrderedListCommand`, `TOIMSlot`, `TOIMStoreKey`, `oimOrderedListCommandsOfTransition`, `oimRefreshSourceComputeds`, `oimSameKeySequence` | no |
3270
3312
  | `@oimdb/react` | `IOIMChannelMap`, `OIMComputedService`, `OIMDB`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMComputedSlot`, `TOIMComputedSources`, `TOIMComputedStoreSlot`, `TOIMDisposer`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`, `TOIMEntitySlot`, `TOIMIndexableSlot`, `TOIMKeyedSlot`, `TOIMSingleSlotStore`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimRefreshSourceComputeds`, `oimSameKeySequence`, `oimUnholdSlotsIfAlive` | no |
3271
3313
  | `@oimdb/devtools` | `IOIMChannelMap`, `IOIMStoreSlotListener`, `OIMChannelService`, `OIMComputedService`, `OIMDefaults`, `OIMSlotCollectionService`, `TOIMBucketSlot`, `TOIMComputedStoreSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEntitySlot`, `TOIMKeyedSlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
3314
+ | `@oimdb/browser` | `IOIMSlotSubscriptionListener`, `OIMChannelService`, `OIMDefaults`, `OIMSingleSlotService`, `OIMSlotContentEntityService`, `TOIMDisposer`, `TOIMEntityContentSlot`, `TOIMSlot`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`, `oimIsSlotReleased` | no |
3272
3315
  | `@oimdb/async` | — | deprecated stub, `export {}` |
3273
3316
  | `@oimdb/snapshot-manager` | — | empty stub |
3274
3317
 
@@ -3306,9 +3349,9 @@ import {
3306
3349
  import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
3307
3350
 
3308
3351
  const persistEntityWriter = new OIMPersistEntityWriter<Card, TOIMStoreKey>({
3309
- entityWriter: createOIMEntityWriterInPlace(),
3352
+ entityWriter: createOIMInPlaceEntityWriter(),
3310
3353
  });
3311
- const persistedCardCollectionService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db, { entityWriter: persistEntityWriter }) });
3354
+ const persistedCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>({ entityWriter: persistEntityWriter }) });
3312
3355
  const persistedCardEntityStoreSlot = persistedCardCollectionService.createStoreSlot(db);
3313
3356
 
3314
3357
  const persistMemoryBackend = new OIMPersistMemoryBackend();
@@ -3471,9 +3514,9 @@ import {
3471
3514
  import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
3472
3515
 
3473
3516
  type TDeckCardSlot = TOIMEntitySlot<Card, TOIMStoreKey>;
3474
- const deckCardCollectionService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db) });
3517
+ const deckCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
3475
3518
  const deckCardEntityStoreSlot = deckCardCollectionService.createStoreSlot(db);
3476
- const deckBucketService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
3519
+ const deckBucketService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
3477
3520
  const deckBucketStoreSlot = deckBucketService.createStoreSlot(db);
3478
3521
  const spadesBucketSlot = deckBucketService.holdSlotByKey(deckBucketStoreSlot, 'spades');
3479
3522
 
@@ -3722,7 +3765,7 @@ import type { TOIMPersistVersionedShape } from '@oimdb/persist';
3722
3765
 
3723
3766
  type TSettings = { theme: 'light' | 'dark'; density: 'compact' | 'comfortable' };
3724
3767
 
3725
- const settingsSingleEntityService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<TSettings>(db) });
3768
+ const settingsSingleEntityService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<TSettings>() });
3726
3769
  const settingsEntityStoreSlot = settingsSingleEntityService.createStoreSlot(db);
3727
3770
  settingsSingleEntityService.writeSingleSlotContent(settingsEntityStoreSlot, { theme: 'light', density: 'compact' });
3728
3771
  const itemByStorageKey = new Map<string, string>();
@@ -3869,9 +3912,9 @@ own properties, so a stored `__proto__` is data.
3869
3912
  re-reads every member and every watched slot, and the source keeps one effect per
3870
3913
  member entity slot.
3871
3914
  - **The memory and JSON backends keep values by reference.** With
3872
- `createOIMEntityWriterInPlace()` the stored entity is the live entity object, so an
3915
+ `createOIMInPlaceEntityWriter()` the stored entity is the live entity object, so an
3873
3916
  in-place write moves the stored value before any commit, and `dehydrate()` returns
3874
- the live objects. Use `createOIMEntityWriterReplacing()` with
3917
+ the live objects. Use `createOIMReplacingEntityWriter()` with
3875
3918
  `entityInserter: oimInsertEntityWithCopy` where the stored value must stay
3876
3919
  what was committed, and serialize a dump at once.
3877
3920
  - **Hydrate without a reconcile replaces.** The collection source deletes every
@@ -3892,7 +3935,7 @@ only on the type-only `@exodra/reactivity-types`.
3892
3935
 
3893
3936
  ### One door: `exoBinding(channelMap, binding)`
3894
3937
 
3895
- A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, sourceSlots, sourceComputeds, onChange)` —
3938
+ A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, onChange, sourceSlots, sourceComputeds)` —
3896
3939
  and the value is read off the slots afterwards. So one adapter covers an entity, a
3897
3940
  column, a bucket's slots and a derivation:
3898
3941
 
@@ -3948,7 +3991,7 @@ resolved before its entity arrived, an entity since removed.
3948
3991
 
3949
3992
  - `exoSource(read, subscribe)` — the lazy, ref-counted base. For sources that are
3950
3993
  **not** this engine's.
3951
- - `exoCombine(sources, fn)` — fan-in across any `TOIMExoReadable`. Not glitch-free;
3994
+ - `exoCombine(fn, sources)` — fan-in across any `TOIMExoReadable`. Not glitch-free;
3952
3995
  keep heavy fan-in inside one computed.
3953
3996
  - `exoFollowKey(key, makeBindable)` — repoints as a selection moves. The factory
3954
3997
  must return a `TOIMExoDisposable`.
@@ -3982,6 +4025,29 @@ memoised per list, so two lists showing one entity slot render it twice.
3982
4025
 
3983
4026
  ---
3984
4027
 
4028
+ ## `@oimdb/browser`
4029
+
4030
+ Browser state as single slots. Each factory returns an `OIMSingleSlotService` (`TOIMBrowserSingleSlotService<TEntity>`)
4031
+ built with a slot subscription listener: the browser listener is added when the slot gets its first
4032
+ subscriber (directly or through a computed that reads it) and removed when the last one goes; the current
4033
+ value is written at the first subscription, and a write that would land during a flush is made right after it.
4034
+
4035
+ ```ts signature
4036
+ createOIMBrowserOnlineSingleSlotService(options?: { defaults?: OIMDefaults }): TOIMBrowserSingleSlotService<TOIMBrowserOnlineEntity> // { isOnline }
4037
+ createOIMBrowserFocusSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserFocusEntity> // { isFocused }: document.hasFocus()
4038
+ createOIMBrowserVisibilitySingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserVisibilityEntity> // { isVisible }
4039
+ createOIMBrowserLocationSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserLocationEntity> // { href, pathname, search, hash }
4040
+ ```
4041
+
4042
+ Read it like any single: `const onlineSlot = onlineService.getSingleSlot(onlineService.createStoreSlot(db))`, then
4043
+ an effect over `[onlineSlot]`, or `useOIMSingleEntity(online)` in React. `isOnline` is
4044
+ `navigator.onLine` (a network interface exists, not a reachable internet); the location follows the
4045
+ Navigation API's `currententrychange` where it exists and `popstate` + `hashchange` otherwise, so without it a
4046
+ `history.pushState` is not seen until the next of those events. Without the API a sensor needs, the slot holds
4047
+ `true` (or an empty location). `OIMBrowserSlotSubscriptionListener` is the base class of the four listeners.
4048
+
4049
+ ---
4050
+
3985
4051
  ## Performance
3986
4052
 
3987
4053
  **No figure may be quoted for this engine, and none is quoted anywhere in this
@@ -4014,7 +4080,7 @@ The one quantity that needs no measurement, because it is arithmetic rather than
4014
4080
  a reading: emptying a bucket one slot at a time on an array-driven bucket costs
4015
4081
  n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `writeSlotContent`
4016
4082
  and `clearSlotContent` go through one `drainSlots`. Counted, not timed, in
4017
- `packages/core/src/engine/bucket/array/tests/everyone-out-at-once-and-not-one-at-a-time.test.ts`, which
4083
+ `packages/core/src/bucket/array/tests/everyone-out-at-once-and-not-one-at-a-time.test.ts`, which
4018
4084
  also pins `drainSlots` at zero element writes.
4019
4085
 
4020
4086
  Fast properties are checked, not timed. Every slot kind is one object literal with