@oimdb/core 4.1.0 → 4.2.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,7 +692,7 @@ 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.
@@ -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
@@ -858,7 +872,7 @@ Every door below is on `OIMChannelService.INSTANCE` and takes the channel slot f
858
872
  The channel slot's own fields that a caller reads: `frame` (`TOIMChannelFrame`, `{ isRunning, openedAtRevision,
859
873
  openedAtEpoch, … }`; a frame carries no number, the clock reading it opened at is its boundary), `clock`
860
874
  (`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),
875
+ `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
876
  `collectorSlot`, `errorPolicy`, `isFlushing`. Everything else on it is engine state.
863
877
 
864
878
  ### A subscription hears only what happens after it
@@ -873,11 +887,11 @@ after it.
873
887
  cardService.writeSlotContentByKey(cards, 'a', { id: 'a', title: 'A', rank: 1, deck: 'spades' });
874
888
  const entitySlot = cardService.getSlotByKey(cards, 'a');
875
889
 
876
- effectService.createEffect(db, [entitySlot], [], () => { /* NEVER runs for the upsert above */ });
890
+ effectService.createEffect(db, () => { /* NEVER runs for the upsert above */ }, [entitySlot]);
877
891
  db.flush();
878
892
 
879
- // A first value is `createEffectImmediate`'s job, not `createEffect`'s:
880
- effectService.createEffectImmediate(db, [entitySlot], [], () => { paint(entitySlot.entity); }); // runs now
893
+ // A first value is `createAndRunEffect`'s job, not `createEffect`'s:
894
+ effectService.createAndRunEffect(db, () => { paint(entitySlot.entity); }, [entitySlot]); // runs now
881
895
  ```
882
896
 
883
897
  A later write in that same batch IS delivered — "after I subscribed" is decided
@@ -890,11 +904,11 @@ Told after **one** frame — the first that ends after the call — and then gon
890
904
  The place that asks decides which frame that is.
891
905
 
892
906
  ```ts
893
- effectService.createEffect(db, [slot], [], () => {
907
+ effectService.createEffect(db, () => {
894
908
  paint(slot.entity);
895
909
  // the tail of THIS frame, after every other effect of it
896
910
  OIMChannelService.INSTANCE.subscribeOnceAfterFrame(channelSlot, () => { measureAndScroll(); });
897
- });
911
+ }, [slot]);
898
912
  ```
899
913
 
900
914
  - **Registered from inside an effect body it runs in the tail of that very
@@ -927,10 +941,10 @@ cardService.writeInBatch(cards, () => {
927
941
  });
928
942
  ```
929
943
 
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.
944
+ - `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `closeBatch` with no batch open
945
+ throws. `writeInBatch` is the `try/finally` form and returns what its function returns.
932
946
  - **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
947
+ index on the collection) hears every write of the batch in ONE call, `onManyContentsChanged`, when the
934
948
  outermost batch closes. Code inside the batch reads the index as it was before the batch.
935
949
  - Effects are not batched by it: every write marks its slot on the channel as usual, and the frame delivers each
936
950
  identity once whatever the batch did. A `flush()` inside a batch is allowed; the index catches up when the
@@ -1013,7 +1027,7 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
1013
1027
  ### Subscribing
1014
1028
 
1015
1029
  ```ts
1016
- const effect = effectService.createEffect(db, [slot], [], () => { paint(slot.entity); });
1030
+ const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
1017
1031
  effectService.disposeEffect(effect);
1018
1032
  ```
1019
1033
 
@@ -1048,10 +1062,10 @@ function is the undo**: it is called before the next run of that body and by
1048
1062
  `disposeEffect`.
1049
1063
 
1050
1064
  ```ts
1051
- effectService.createEffect(db, [slot], [], () => {
1065
+ effectService.createEffect(db, () => {
1052
1066
  const timer = setInterval(poll, 1000);
1053
1067
  return () => clearInterval(timer);
1054
- });
1068
+ }, [slot]);
1055
1069
  ```
1056
1070
 
1057
1071
  The return is checked with `typeof`, not for truthiness — `() => set.add(key)`
@@ -1059,14 +1073,28 @@ hands back the `Set` — so anything that is not a function is discarded. The un
1059
1073
  is cleared before it is called, so one that throws is not called again by the
1060
1074
  `disposeEffect` behind it.
1061
1075
 
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
1076
+ **A derivation that finds its sources as it reads:** `createOIMSourceTrackingComputedService<TKey, TValue>({ derive, … })` and
1077
+ `createOIMSingleSourceTrackingComputedService<TValue>({ derive, … })`. `derive: TOIMSourceTrackingComputedDerive<TValue, TSlot>` is
1078
+ `(computedSlot, sourceTracker: OIMSourceTracker) => TValue`; there is no `createSources`, and `createComputedStoreSlot(db)` takes no
1079
+ per-store `derive`. Every derive holds what it read and gives back what it no longer reads (new holds first). The slot
1080
+ (`TOIMSourceTrackingComputedSlot<TValue, TKey>` / `TOIMSourceTrackingComputedContentSlot<TValue>`, `sourceTracker` set) is read
1081
+ through `sourceTracker.readSourceComputedValue` (in `createSourceTrackingEffect` or another source-tracking derive) or through `createEffect`'s fifth list; `createEffect`'s `sourceComputeds` and
1082
+ a `createSources` list refuse it by type, because a declared effect subscribes once to the slots under a derivation. When a derive
1083
+ switches sources, a source-tracking effect or derivation over it re-subscribes even when the value held. A body that cannot read
1084
+ through a tracker may pass them as the fifth list, `createEffect(db, body, sourceSlots, sourceComputeds, sourceTrackingComputeds)`;
1085
+ such an effect derives them at creation and re-subscribes when their root slots are replaced. Prefer `createSourceTrackingEffect`: the
1086
+ root check runs after every run of such an effect. An effect with no fifth list pays nothing for it. Options otherwise as
1087
+ `createOIMComputedService` / `createOIMSingleComputedService` (`sameValueGuard`, `releaseValue`, `initialValue`, `slotReleasePolicy`,
1088
+ column options).
1089
+
1090
+ **Sources found at read: `createSourceTrackingEffect(db, body)`.** The second way to build
1091
+ an effect takes no source lists. The body receives an `OIMSourceTracker`, and what it
1064
1092
  reads through the tracker is what that run depends on — signal-style tracking.
1065
1093
 
1066
1094
  ```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
1095
+ createSourceTrackingEffect(channelMap: IOIMChannelMap, body: TOIMSourceTrackingEffectBody): TOIMEffect;
1096
+ type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
1097
+ class OIMSourceTracker { // exported as a type; the service builds it
1070
1098
  readSourceSlot<TSlot extends TOIMSlot>(slot: TSlot): TSlot; // files the slot, returns it
1071
1099
  readSourceComputedValue<TValue>(computedSlot: TOIMComputedSlot<TValue, unknown>): TValue; // files it, returns its value
1072
1100
  }
@@ -1074,9 +1102,9 @@ class OIMEffectSourceTracker { // exported as a type; the service b
1074
1102
 
1075
1103
  ```ts
1076
1104
  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);
1105
+ effectService.createSourceTrackingEffect(db, sourceTracker => {
1106
+ const isFirst = sourceTracker.readSourceSlot(slot).entity?.rank === 1;
1107
+ paint(sourceTracker.readSourceSlot(isFirst ? slot : fallback).entity);
1080
1108
  });
1081
1109
  ```
1082
1110
 
@@ -1089,7 +1117,7 @@ effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
1089
1117
  creation. A returned function is the undo, exactly as for `TOIMEffectBody`.
1090
1118
 
1091
1119
  ```ts
1092
- const onRank = effectService.createEffect(db, [cardService.columnSlotService.holdColumn(slot, 'rank')], [], () => { paint(slot.entity); });
1120
+ const onRank = effectService.createEffect(db, () => { paint(slot.entity); }, [cardService.columnSlotService.holdColumn(slot, 'rank')]);
1093
1121
  ```
1094
1122
 
1095
1123
  **A column effect takes the `TOIMColumnSlot` as a root, not a slot and a name.** Minting the
@@ -1115,13 +1143,13 @@ per call; get it once per view, not per entity. `service.holdColumnGroup(slot, c
1115
1143
  the group's column slot on `slot`, and `service.unholdColumn(columnSlot)` gives it back. A write wakes it once
1116
1144
  if it changes any of the group's columns and not at all if it changes only others.
1117
1145
 
1118
- **`columnNames` picks the column slot store.** `createOIMSlotCollectionService(db, { slotContentService, columnNames: ['rank', 'label'] })`
1146
+ **`columnNames` picks the column slot store.** `createOIMSlotCollectionService({ slotContentService, columnNames: ['rank', 'label'] })`
1119
1147
  — and `columnNames` on any factory's options — files the service's column slots by ordinal: every named column
1120
1148
  gets a number, a group gets one at its first slot, and an entity slot's column slots sit in an array by
1121
1149
  number instead of a `Map` per entity slot, which is cheaper to create and give back. `holdColumn` and
1122
1150
  `getColumnGroup` are typed on those names, and a slot for any other column throws. Without `columnNames` the
1123
1151
  store is the `columnSlotStoreService` option, defaulting to the shared map-driven
1124
- `OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()`, and any name is accepted.
1152
+ a shared map-driven store, and any name is accepted.
1125
1153
 
1126
1154
  ---
1127
1155
 
@@ -1155,11 +1183,11 @@ once per collection type; the store slot is made separately:
1155
1183
 
1156
1184
  | Door | Builds | Store slot |
1157
1185
  |---|---|---|
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)` |
1186
+ | `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({ entityWriter?, slotEmptinessPolicy? }?)` | `OIMSlotContentEntityService<TEntity, TKey, TColumnName>`, the entity content service; it creates keyed and single entity slots alike | — |
1187
+ | `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)` |
1188
+ | `createOIMSingleSlotService({ slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSingleSlotService` over any content service with `createSingleSlot` (entity or bucket) | `createStoreSlot(db)` |
1189
+ | `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)` |
1190
+ | `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
1191
  | `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
1192
  | `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
1193
  | `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)` |
@@ -1170,7 +1198,7 @@ db's, so naming one overrides it for that service alone.
1170
1198
  **A DOOR ALWAYS PASSES; IT NEVER LETS A CLASS REACH FOR A GLOBAL.** Every option
1171
1199
  a service would otherwise answer with `?? OIMDefaults.x` is in the object its
1172
1200
  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`,
1201
+ `src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts`,
1174
1202
  which also fails when an option arrives on a service and is not forwarded,
1175
1203
  when the root mints anything but the channel and the collector, or when a public
1176
1204
  member appears on the root that is not one of the seven it owns.
@@ -1180,7 +1208,7 @@ member appears on the root that is not one of the seven it owns.
1180
1208
  slot, and the script lives for one frame.
1181
1209
 
1182
1210
  ```ts
1183
- const freshPageService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1211
+ const freshPageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1184
1212
  const freshPage = freshPageService.createStoreSlot(db);
1185
1213
  const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
1186
1214
  // freshBucketSlot.commands and freshBucketSlot.isWholeListRecorded carry the script; read them in an effect on the bucket slot
@@ -1190,56 +1218,41 @@ const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
1190
1218
 
1191
1219
  ## `OIMDefaults` — where an optional dependency comes from
1192
1220
 
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.
1221
+ `OIMDefaults` is an instance holding the strategies factories fall back on when their own options leave
1222
+ them out. Build one with what you want shared and pass it as `defaults` to every factory that should use
1223
+ it; a factory given none uses `OIMDefaults.INSTANCE`, which is empty. A field left undefined falls back to
1224
+ the built-in default. Nothing is global and nothing can be replaced after the fact.
1196
1225
 
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:
1226
+ ```ts signature
1227
+ new OIMDefaults({
1228
+ slotStamp?: IOIMSlotStamp<TOIMSlot>; // none
1229
+ slotReleasePolicy?: IOIMSlotReleasePolicy; // none: a slot with a value stays
1230
+ entityWriter?: IOIMEntityWriter; // createOIMInPlaceEntityWriter()
1231
+ entitySlotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot>; // "empty means slot.entity === undefined"
1232
+ indexKeysService?: IOIMIndexKeysService; // OIMAppendingIndexKeysService
1233
+ columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // a shared map-driven store
1234
+ computedColumnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // a shared map-driven store
1235
+ storeSlotListener?: IOIMStoreSlotListener; // OIMNoStoreSlotListener: hears nothing
1236
+ }?) // TOIMDefaultsOptions
1237
+ ```
1216
1238
 
1217
1239
  ```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"*.
1240
+ const replacingDefaults = new OIMDefaults({ entityWriter: createOIMReplacingEntityWriter() });
1241
+ const replacingCardService = createOIMSlotCollectionService({
1242
+ defaults: replacingDefaults,
1243
+ slotContentService: createOIMSlotContentEntityService<Card>({ defaults: replacingDefaults }),
1244
+ });
1245
+ ```
1246
+
1247
+ An option given to a factory wins over `defaults`; `defaults` wins over the built-in default. A tool that
1248
+ wants to hear every store slot (devtools' `oimDevDefaults`) is a `defaults` with a `storeSlotListener`.
1249
+
1250
+ **Each built-in default is its own module, so reaching one retains only its arm.** Every built-in default
1251
+ lives in its own file under `packages/core/src/db/constants/` as an `OIMDefaultValue` whose `getValue()`
1252
+ builds it on the first call and hands the same instance back after; a factory reads the modules it needs.
1253
+ `OIMDefaults` imports none of them, so importing it retains no arm.
1254
+ `packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one door to its own arm, and
1255
+ the defaults class to none of them"*.
1243
1256
 
1244
1257
  ---
1245
1258
 
@@ -1251,17 +1264,17 @@ with its slot owner; the collection service (`OIMSlotCollectionService`) over it
1251
1264
  Neither holds an entity: each `createStoreSlot(db)` is one instance of that type's store.
1252
1265
 
1253
1266
  ```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
1267
+ createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({
1268
+ entityWriter?: IOIMEntityWriter; // defaults.entityWriter, then the in-place writer
1269
+ slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; // defaults.entitySlotEmptinessPolicy, then "entity is undefined"
1257
1270
  }?): OIMSlotContentEntityService<TEntity, TKey, TColumnName>
1258
1271
 
1259
- createOIMSlotCollectionService(db, {
1272
+ createOIMSlotCollectionService({
1260
1273
  slotContentService,
1261
1274
  slotStoreService?: IOIMSlotStoreService<TKey, TSlot, TStore>; // Map; REQUIRED for an array key
1262
- slotReleasePolicy?: IOIMSlotReleasePolicy; // db.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
1275
+ slotReleasePolicy?: IOIMSlotReleasePolicy; // defaults.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
1263
1276
  slotSubscriptionListener?: IOIMSlotSubscriptionListener; // OIMNoSlotSubscriptionListener
1264
- columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // OIMDefaults.COLUMN_SLOT_STORE_SERVICE
1277
+ columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // defaults.columnSlotStoreService, then a shared map store
1265
1278
  columnNames?: readonly TColumnName[]; // the ordinal column store over those names
1266
1279
  }): OIMWritableSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, TEntity, …>
1267
1280
  collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>, typeof collectionService>
@@ -1278,7 +1291,7 @@ derive-driven column slots, each a computed slot over its carrier (`OIMPullColum
1278
1291
  Example, over the `db`, `cardService`, `cards` and `slot` of the setup block:
1279
1292
 
1280
1293
  ```ts
1281
- const effect = effectService.createEffect(db, [slot], [], () => { console.log(slot.entity?.title); });
1294
+ const effect = effectService.createEffect(db, () => { console.log(slot.entity?.title); }, [slot]);
1282
1295
  cardService.slotContentService.updateEntity(slot, { title: 'Ace of Spades' });
1283
1296
  db.flush();
1284
1297
  effectService.disposeEffect(effect);
@@ -1287,15 +1300,15 @@ effectService.disposeEffect(effect);
1287
1300
  Assembled with a store service of your own:
1288
1301
 
1289
1302
  ```ts
1290
- const baseCardService = createOIMSlotCollectionService(db, {
1291
- slotContentService: createOIMSlotContentEntityService<Card, string>(db),
1303
+ const baseCardService = createOIMSlotCollectionService({
1304
+ slotContentService: createOIMSlotContentEntityService<Card, string>(),
1292
1305
  slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, string>>(),
1293
1306
  });
1294
1307
  const baseCards = baseCardService.createStoreSlot(db);
1295
1308
  ```
1296
1309
 
1297
1310
  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.
1311
+ is not an option of any factory: it is `new OIMDefaults({ slotStamp })`, passed as `defaults`.
1299
1312
 
1300
1313
  ### Public properties
1301
1314
 
@@ -1309,7 +1322,7 @@ A store slot is not a slot, so nothing holds, subscribes or wakes it: there is n
1309
1322
  `effectService.createEffect(db, [cards], …)` that hears the population. "Something in this population changed" is an
1310
1323
  index over the store slot and a subscription to its bucket — the bucket's door
1311
1324
  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)`,
1325
+ (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames)`,
1313
1326
  removed by the same listener object). The slot owner is what `slot.owner` points at for every slot the type
1314
1327
  filed, in every one of its store slots; `slot.storeSlot` names which store slot.
1315
1328
 
@@ -1500,7 +1513,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
1500
1513
  - **The hand mutation already compiles.** `TOIMEntitySlot` declares
1501
1514
  `readonly entity: TEntity | undefined`, and `readonly` forbids REPLACING the
1502
1515
  object, not writing its columns — `entity.title = 'two'` needs no cast, only
1503
- the usual narrowing of `| undefined`. `TOIMSlotEditable<TSlot>` is what strips
1516
+ the usual narrowing of `| undefined`. `TOIMEditable<TSlot>` is what strips
1504
1517
  `readonly` from the field itself, which is what assigning `slot.entity` needs.
1505
1518
  - **With a list, only the named columns wake. Without it, every watched column
1506
1519
  does** — the column owner walks the slot's whole column store, and a
@@ -1515,7 +1528,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
1515
1528
  effect on the slot itself and an index over the store slot both see a hand mutation.
1516
1529
  - **It re-stamps emptiness** whenever emptiness can depend on the mutation — with no column list,
1517
1530
  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
1531
+ "the entity is `undefined`", a release policy that releases written slots, or a db with a
1519
1532
  `slotStamp` — so a hand mutation that emptied the entity leaves `oimIsSlotEmpty(slot)`
1520
1533
  true rather than lying, and the release guard can take the identity when it should.
1521
1534
  - **It refuses inside a frame**, through the same `assertWritable` →
@@ -1531,7 +1544,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
1531
1544
  A **writer** is the operations that move an entity's columns, and nothing
1532
1545
  else: it reaches no channel, takes no holds and knows nothing about lifetime.
1533
1546
  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 })`).
1547
+ (or for every factory given it, `new OIMDefaults({ entityWriter })` as `defaults`).
1535
1548
 
1536
1549
  ```ts signature
1537
1550
  interface IOIMEntityWriter {
@@ -1548,13 +1561,13 @@ interface IOIMEntityWriter {
1548
1561
  type TOIMEntityWriterOptions = {
1549
1562
  sameValueGuard?: TOIMSameValueGuard;
1550
1563
  entityInserter?: TOIMEntityInserter;
1551
- putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimPutEntityColumnsGuarded
1552
- patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimPatchEntityColumnsGuarded
1553
- patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimPatchEntityColumnGuarded
1564
+ putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimGuardedPutEntityColumns
1565
+ patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimGuardedPatchEntityColumns
1566
+ patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimGuardedPatchEntityColumn
1554
1567
  };
1555
1568
 
1556
- createOIMEntityWriterInPlace(options?: TOIMEntityWriterOptions): IOIMEntityWriter // stored object keeps its identity
1557
- createOIMEntityWriterReplacing(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
1569
+ createOIMInPlaceEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // stored object keeps its identity
1570
+ createOIMReplacingEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
1558
1571
  ```
1559
1572
 
1560
1573
  `entityInserter` is the first-write strategy: `oimInsertEntityWithoutCopy` (the default) stores
@@ -1582,9 +1595,9 @@ of a key that already has a value and `slotContentService.writeSlotContent(slot,
1582
1595
  write that drops a column is ONE channel write of the slot, and the replacing writer
1583
1596
  builds ONE new object.
1584
1597
 
1585
- Pick `createOIMEntityWriterReplacing` when something downstream compares by
1598
+ Pick `createOIMReplacingEntityWriter` when something downstream compares by
1586
1599
  reference — a React `useSyncExternalStore` snapshot, a memo, a shallow-equal
1587
- selector. Under `createOIMEntityWriterInPlace` the entity reference never moves,
1600
+ selector. Under `createOIMInPlaceEntityWriter` the entity reference never moves,
1588
1601
  so a value comparison never fires and a binding needs a counter bumped inside its
1589
1602
  handler. Replacing allocates one object per changed write; in-place allocates
1590
1603
  none.
@@ -1609,10 +1622,10 @@ oimPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, c
1609
1622
  targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
1610
1623
  oimPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
1611
1624
  targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
1612
- oimPutEntityColumnsGuarded(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
1625
+ oimGuardedPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
1613
1626
  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)
1627
+ oimGuardedPatchEntityColumns(slot, patch: object, sameValueGuard, changedColumnNames, targetOf) // same signature as the unguarded walk
1628
+ oimGuardedPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, changedColumnNames, targetOf)
1616
1629
  ```
1617
1630
 
1618
1631
  Each compares every column through `sameValueGuard`, writes the moved ones into
@@ -1625,14 +1638,14 @@ ships two arms:
1625
1638
 
1626
1639
  | Option | Default (unguarded) | Guarded arm |
1627
1640
  |---|---|---|
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__` |
1641
+ | `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `oimGuardedPutEntityColumns` — skips both |
1642
+ | `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `oimGuardedPatchEntityColumns` — skips a `__proto__` key and a key the patch does not own |
1643
+ | `patchEntityColumn` | `oimPatchEntityColumn` | `oimGuardedPatchEntityColumn` — refuses the column name `__proto__` |
1631
1644
 
1632
1645
  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
1646
+ instances — `createOIMInPlaceEntityWriter({ putEntityColumns: oimGuardedPutEntityColumns,
1647
+ patchEntityColumns: oimGuardedPatchEntityColumns, patchEntityColumn: oimGuardedPatchEntityColumn })`, the
1648
+ same options on `createOIMReplacingEntityWriter`. The writer decides only where the result goes — this one
1636
1649
  stores every value frozen:
1637
1650
 
1638
1651
  ```ts
@@ -1640,11 +1653,11 @@ const copyColumns = (entity: Record<string, unknown>): Record<string, unknown> =
1640
1653
 
1641
1654
  function storeFrozen(slot: TOIMEntityContentSlot, next: Record<string, unknown> | undefined): boolean {
1642
1655
  if (next === undefined) return false;
1643
- (slot as TOIMSlotEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
1656
+ (slot as TOIMEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
1644
1657
  return true;
1645
1658
  }
1646
1659
 
1647
- const replacingWriter = createOIMEntityWriterReplacing();
1660
+ const replacingWriter = createOIMReplacingEntityWriter();
1648
1661
 
1649
1662
  const freezingWriter: IOIMEntityWriter = {
1650
1663
  fillEntity: (slot, entity) => { storeFrozen(slot, copyColumns(entity as Record<string, unknown>)); },
@@ -1672,8 +1685,8 @@ A single entity — one value under no key, such as settings or a session — is
1672
1685
  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
1686
 
1674
1687
  ```ts signature
1675
- createOIMSingleSlotService(db, {
1676
- slotContentService: createOIMSlotContentEntityService<TEntity>(db),
1688
+ createOIMSingleSlotService({
1689
+ slotContentService: createOIMSlotContentEntityService<TEntity>(),
1677
1690
  slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
1678
1691
  }): OIMWritableSingleSlotService<TOIMEntityContentSlot<TEntity>, TEntity, …>
1679
1692
 
@@ -1697,12 +1710,12 @@ with no value until something writes it. Its store slot is a `TOIMSingleSlotStor
1697
1710
  ```ts
1698
1711
  type Settings = { theme: 'light' | 'dark'; zoom: number };
1699
1712
 
1700
- const settingsService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<Settings>(db) });
1713
+ const settingsService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<Settings>() });
1701
1714
  const settings = settingsService.createStoreSlot(db);
1702
1715
  settingsService.writeSingleSlotContent(settings, { theme: 'light', zoom: 1 });
1703
1716
  const settingsSlot = settingsService.holdSingleSlot(settings);
1704
1717
  const themeColumn = settingsService.columnSlotService.holdColumn(settingsSlot, 'theme');
1705
- const themeEffect = effectService.createEffect(db, [themeColumn], [], () => { console.log(settingsSlot.entity?.theme); });
1718
+ const themeEffect = effectService.createEffect(db, () => { console.log(settingsSlot.entity?.theme); }, [themeColumn]);
1706
1719
  settingsService.slotContentService.updateEntity(settingsSlot, { theme: 'dark' });
1707
1720
  db.flush();
1708
1721
  effectService.disposeEffect(themeEffect);
@@ -1720,7 +1733,7 @@ The slot is of the keyed entity slot's literal shape (with `key: undefined` and
1720
1733
  `storeSlot` is `settings` — so
1721
1734
  `effectService.createEffect(db, [settingsSlot], [], …)` works and a column of it is a root like any other.
1722
1735
  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
1736
+ `slotReleasePolicy` (`defaults.slotReleasePolicy`, then `OIMSlotAtEmptyReleasePolicy`), so a single entity that holds a value
1724
1737
  stays; one emptied and unheld is released, and the slot `getSingleSlot` mints next holds no entity. The value
1725
1738
  is stored by the writer's `entityInserter`, which by default keeps the object handed in rather than a copy.
1726
1739
 
@@ -1732,7 +1745,7 @@ A bucket collection is, like an entity collection, a content service and the col
1732
1745
  The content service is the strategy — how a bucket keeps its member slots — and it is chosen at the factory:
1733
1746
 
1734
1747
  ```ts signature
1735
- createOIMSlotCollectionService(db, {
1748
+ createOIMSlotCollectionService({
1736
1749
  slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or one of the others below
1737
1750
  slotStoreService?: IOIMSlotStoreService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore>; // Map; REQUIRED for an array key
1738
1751
  slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
@@ -1752,7 +1765,7 @@ of the collection service take that store slot first, and the bucket doors of th
1752
1765
  bucket slot.
1753
1766
 
1754
1767
  ```ts
1755
- const colourService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
1768
+ const colourService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
1756
1769
  const colours = colourService.createStoreSlot(db);
1757
1770
  const red = colourService.holdSlotByKey(colours, 'red');
1758
1771
  colourService.slotContentService.writeSlotContent(red, [slot, cardService.getSlotByKey(cards, 'c2')]);
@@ -1970,10 +1983,10 @@ content service already writes the bucket slot on every mutation, so a consumer
1970
1983
  reads the script inside the effect:
1971
1984
 
1972
1985
  ```ts
1973
- effectService.createEffect(db, [todo], [], () => {
1986
+ effectService.createEffect(db, () => {
1974
1987
  if (todo.isWholeListRecorded) render(Array.from(pageService.slotContentService.getBucketMemberSlots(todo)));
1975
1988
  else applyToTheRenderedList(todo.commands ?? []);
1976
- });
1989
+ }, [todo]);
1977
1990
  ```
1978
1991
 
1979
1992
  `TOIMBucketSlot` carries two fields for it, and a consumer reads both:
@@ -2019,21 +2032,21 @@ One bucket under no key is a store slot of an `OIMSingleSlotService` over a buck
2019
2032
  `OIMSingleSlotStoreService.INSTANCE` as the store service:
2020
2033
 
2021
2034
  ```ts signature
2022
- createOIMSingleSlotService(db, {
2035
+ createOIMSingleSlotService({
2023
2036
  slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or the set one
2024
2037
  slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
2025
2038
  }): OIMWritableSingleSlotService<TOIMBucketContentSlot<TSlots>, readonly TBucketMemberSlot[], …>
2026
2039
  ```
2027
2040
 
2028
2041
  ```ts
2029
- const handService = createOIMSingleSlotService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
2042
+ const handService = createOIMSingleSlotService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
2030
2043
  const hand = handService.createStoreSlot(db);
2031
2044
  const handSlot = handService.holdSingleSlot(hand);
2032
2045
  handService.slotContentService.writeSlotContent(handSlot, [slot]);
2033
2046
  handService.slotContentService.insertBucketMemberSlotAt(handSlot, 0, cardService.getSlotByKey(cards, 'c2'));
2034
- effectService.disposeEffect(effectService.createEffect(db, [handSlot], [], () => {
2047
+ effectService.disposeEffect(effectService.createEffect(db, () => {
2035
2048
  render(Array.from(handService.slotContentService.getBucketMemberSlots(handSlot)));
2036
- }));
2049
+ }, [handSlot]));
2037
2050
  handSlot.owner.unholdSlot(handSlot);
2038
2051
  ```
2039
2052
 
@@ -2052,7 +2065,7 @@ slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `OI
2052
2065
  ### The index that fills itself — `OIMSlotCollectionIndexService`
2053
2066
 
2054
2067
  ```ts signature
2055
- createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>(db, {
2068
+ createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>({
2056
2069
  entityService: Pick<OIMSlotCollectionService<TSourceKey, TOIMEntitySlot<TEntity, TSourceKey>, TSourceStore, unknown>, 'getSlots'>;
2057
2070
  ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>; // { columns } | { keyOf } | { keyOf, columns }
2058
2071
  slotContentService?: OIMSlotContentBucketService<TOIMEntitySlot<TEntity>, TSlots>; // default: array-driven, no commands
@@ -2098,21 +2111,21 @@ own. With none named it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: the bucket st
2098
2111
  maintained on every write.
2099
2112
 
2100
2113
  ```ts
2101
- const byDeckService = createOIMSlotCollectionIndexService(db, {
2114
+ const byDeckService = createOIMSlotCollectionIndexService({
2102
2115
  entityService: cardService,
2103
2116
  keyOf: (card: Card) => card.deck,
2104
2117
  });
2105
2118
  const byDeck = byDeckService.createIndexStoreSlot(cards);
2106
2119
 
2107
2120
  const spades = byDeckService.holdSlotByKey(byDeck, 'spades');
2108
- effectService.createEffect(db, [spades], [], () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))));
2121
+ effectService.createEffect(db, () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))), [spades]);
2109
2122
  ```
2110
2123
 
2111
2124
  A set-driven index names its content service with the bucket member slot type and the index key type spelled
2112
2125
  out — `new OIMSlotContentSetBucketService()` infers `<TOIMSlot, TOIMStoreKey>` and fails to type-check:
2113
2126
 
2114
2127
  ```ts
2115
- const bySuitService = createOIMSlotCollectionIndexService(db, {
2128
+ const bySuitService = createOIMSlotCollectionIndexService({
2116
2129
  entityService: cardService,
2117
2130
  columns: ['deck'],
2118
2131
  slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>, string>(),
@@ -2121,7 +2134,7 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
2121
2134
  ```
2122
2135
 
2123
2136
  - **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`);
2137
+ a content-change listener on that store slot (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener`);
2125
2138
  the entity services know nothing about indexes. Call `createIndexStoreSlot` twice over one entity
2126
2139
  store slot and it files into both. `entityService` is used only for its `getSlots`, to fill a new bucket
2127
2140
  from the source.
@@ -2133,8 +2146,8 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
2133
2146
  ones `keyOf` reads. With `columns`, a patch or put whose changed columns include none of
2134
2147
  them keeps the entity's key and calls nothing; a write of the whole value (a fill, a
2135
2148
  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
2149
+ - **A key of several columns**: `createOIMSlotCollectionIndexService({ entityService, columns: ['deck', 'suit'],
2150
+ bucketSlotStoreService: new OIMSlotTreeStoreService(OIMScalarKeyTreeWalk.INSTANCE) })` makes the key
2138
2151
  the array of those values in order, typed as a tuple. Only that store knows the key is an array (its
2139
2152
  `isSameKey`), so a fresh array with the same parts moves nothing. `createOIMSlotCollectionIndexService` takes a scalar key
2140
2153
  unless it is handed a `bucketSlotStoreService`: two columns or an array `keyOf` without one do not compile.
@@ -2190,13 +2203,13 @@ the bucket slot (`getSlotByKey` / `holdSlotByKey`). A reader that wants both sub
2190
2203
  to both, and the subscriber picks by which slots it puts in `createEffect`.
2191
2204
 
2192
2205
  ```ts
2193
- const deckIndexService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
2206
+ const deckIndexService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
2194
2207
  const deckIndex = deckIndexService.createIndexStoreSlot(cards);
2195
2208
  const spadesBucket = deckIndexService.holdSlotByKey(deckIndex, 'spades');
2196
2209
  const spadesAnyColumn = deckIndexService.holdAnyBucketColumnByKey(deckIndex, 'spades');
2197
- const entitiesEffect = effectService.createEffect(db, [spadesBucket, spadesAnyColumn], [], () => {
2210
+ const entitiesEffect = effectService.createEffect(db, () => {
2198
2211
  console.log(deckIndexService.getEntitiesByKey(deckIndex, 'spades'));
2199
- });
2212
+ }, [spadesBucket, spadesAnyColumn]);
2200
2213
  effectService.disposeEffect(entitiesEffect);
2201
2214
  deckIndexService.columnSlotService.unholdColumn(spadesAnyColumn);
2202
2215
  spadesBucket.owner.unholdSlot(spadesBucket);
@@ -2215,7 +2228,7 @@ needs the whole list each time: an export, an aggregate over the entities, a log
2215
2228
  It costs nothing until someone asks for one: a write is checked against it only after
2216
2229
  the index has been asked for the first any-column slot.
2217
2230
 
2218
- **The content-change hook.** `OIMContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames): void`
2231
+ **The content-change hook.** `OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames): void`
2219
2232
  and `removeContentChangeListener(storeSlot, listener): boolean` file and unfile a listener on that one store
2220
2233
  slot, by the listener object. `addReadColumn` / `removeReadColumn` / `addReadAnyColumn` / `removeReadAnyColumn`
2221
2234
  declare reads without a listener of their own, which is how an index declares its bucket columns.
@@ -2231,15 +2244,15 @@ every column write is added with `readColumnNames` `undefined`. **The filter is
2231
2244
  slot, not per listener**: once a write touches a column ANY listener of that store reads, every
2232
2245
  listener of that store is called, so a listener that declared a narrower set still checks
2233
2246
  `changedColumns` itself, as `OIMSlotCollectionIndexService` does against its key columns.
2234
- `IOIMContentChangeListener<TSlot, TPatch>` has two methods:
2247
+ `IOIMSlotContentChangeListener<TSlot, TPatch>` has two methods:
2235
2248
  `onContentChanged(slot, patch, changedColumns)` and
2236
- `onContentChangedMany(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
2249
+ `onManyContentsChanged(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
2237
2250
  parallel `OIMChannelQueue`s read with `getEntryCount()` / `getEntryAt(i)`, valid only during the
2238
2251
  call, so a listener copies what it keeps — `patch` is what an update
2239
2252
  wrote, `changedColumns` the names `markEntityChanged(slot, columns)` was given, either
2240
2253
  `undefined` when the write carries none. A single write outside a batch delivers one `onContentChanged` as soon as it is filed; inside
2241
2254
  `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
2255
+ delivered as ONE `onManyContentsChanged` when the outermost batch closes. Code inside the batch reads the index
2243
2256
  as it was before the batch.
2244
2257
  This hook is the only seam an index uses; an index of your own is a listener.
2245
2258
 
@@ -2254,10 +2267,10 @@ return the same kind of slot, written when any column of any member changes. A g
2254
2267
  `slot.owner.unholdSlot(slot)`.
2255
2268
 
2256
2269
  ```ts
2257
- const decksService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
2270
+ const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
2258
2271
  const decks = decksService.createIndexStoreSlot(cards);
2259
2272
  const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
2260
- effectService.disposeEffect(effectService.createEffect(db, [rankOfSpades], [], () => {}));
2273
+ effectService.disposeEffect(effectService.createEffect(db, () => {}, [rankOfSpades]));
2261
2274
  decksService.columnSlotService.unholdColumn(rankOfSpades);
2262
2275
  ```
2263
2276
 
@@ -2266,7 +2279,7 @@ decksService.columnSlotService.unholdColumn(rankOfSpades);
2266
2279
  ```ts signature
2267
2280
  createOIMSortedComputedService<TKey, TEntity extends object, TSlots, TSourceKey, TSourceStore, TComputedStore?>({
2268
2281
  indexService: OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore>;
2269
- sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { column: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
2282
+ sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
2270
2283
  comparator?: (left: TEntity, right: TEntity) => number;
2271
2284
  slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
2272
2285
  columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
@@ -2309,13 +2322,13 @@ slots sorted. A hold is given back with `slot.owner.unholdSlot(slot)`.
2309
2322
  to a member as with `comparator` alone.
2310
2323
 
2311
2324
  ```ts
2312
- const cardsInDeckService = createOIMSlotCollectionIndexService(db, {
2325
+ const cardsInDeckService = createOIMSlotCollectionIndexService({
2313
2326
  entityService: cardService,
2314
2327
  keyOf: (card: Card) => card.deck,
2315
2328
  });
2316
2329
  createOIMSortedComputedService({
2317
2330
  indexService: cardsInDeckService,
2318
- sortColumns: [{ column: 'title', direction: 'asc' }, { column: 'rank', direction: 'asc' }],
2331
+ sortColumns: [{ columnName: 'title', direction: 'asc' }, { columnName: 'rank', direction: 'asc' }],
2319
2332
  comparator: (l, r) => l.title.localeCompare(r.title, 'de') || l.rank - r.rank,
2320
2333
  }).createSortedStoreSlot(cardsInDeckService.createIndexStoreSlot(cards));
2321
2334
  // a write to `title` or `rank` re-sorts; a write to `note` does not
@@ -2328,17 +2341,17 @@ SECOND array of `createEffect`. A re-sort that yields the same sequence
2328
2341
  (`oimSameSlotSequence`) keeps the previous array, so its identity is stable.
2329
2342
 
2330
2343
  ```ts
2331
- const decksService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
2344
+ const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
2332
2345
  const decks = decksService.createIndexStoreSlot(cards);
2333
2346
  const byRankInDeckService = createOIMSortedComputedService({
2334
2347
  indexService: decksService,
2335
- sortColumns: [{ column: 'rank', direction: 'asc' }],
2348
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }],
2336
2349
  });
2337
2350
  const byRankInDeck = byRankInDeckService.createSortedStoreSlot(decks);
2338
2351
  const spadesByRank = byRankInDeckService.computedCollectionService.holdSlotByKey(byRankInDeck, 'spades');
2339
- const sortedEffect = effectService.createEffect(db, [], [spadesByRank], () => {
2352
+ const sortedEffect = effectService.createEffect(db, () => {
2340
2353
  render(spadesByRank.computedValueService.getValueForSlot(spadesByRank));
2341
- });
2354
+ }, [], [spadesByRank]);
2342
2355
  effectService.disposeEffect(sortedEffect);
2343
2356
  spadesByRank.owner.unholdSlot(spadesByRank);
2344
2357
  ```
@@ -2350,7 +2363,7 @@ index. `createOIMSlotCollectionSortedArrayIndexService` keeps the index's own bu
2350
2363
  the user picks one at construction.
2351
2364
 
2352
2365
  ```ts signature
2353
- createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>(db, {
2366
+ createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>({
2354
2367
  entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as createOIMSlotCollectionIndexService, scalar key
2355
2368
  sortColumns: readonly TOIMSortColumn<TEntity>[]; // REQUIRED
2356
2369
  bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService?, columnNames?,
@@ -2359,10 +2372,10 @@ createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKe
2359
2372
 
2360
2373
  ```ts
2361
2374
  declare const someCardSlot: TOIMEntitySlot<Card, TOIMStoreKey>;
2362
- const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService(db, {
2375
+ const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService({
2363
2376
  entityService: cardService,
2364
2377
  columns: ['deck'],
2365
- sortColumns: [{ column: 'rank', direction: 'asc' }, { column: 'id', direction: 'desc' }],
2378
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }, { columnName: 'id', direction: 'desc' }],
2366
2379
  });
2367
2380
  const cardsByDeck = cardsByDeckService.createIndexStoreSlot(cards);
2368
2381
  const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
@@ -2399,8 +2412,8 @@ filed under now, so a moved entity slot leaves its old bucket in O(1), also unde
2399
2412
  writer. Only the index writes it, through the `IOIMIndexKeysService` (`get(slot, position)`,
2400
2413
  `set(slot, position, indexKey)`), at the position `createIndexStoreSlot` allocated
2401
2414
  (`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()`):
2415
+ (`sourceStoreSlot.indexKeysPositions`). Pick the strategy with `new OIMDefaults({ indexKeysService })` passed as `defaults`
2416
+ (default `OIMAppendingIndexKeysService.INSTANCE`):
2404
2417
 
2405
2418
  | strategy | on `disposeIndexStoreSlot` | cost |
2406
2419
  |---|---|---|
@@ -2429,7 +2442,7 @@ type TOIMComputedSources = {
2429
2442
  In a `createSources` result (of `createOIMComputedService`'s options, or of `createStoreSlot`) both `sourceSlots` and `sourceComputeds` are optional; a missing one means none.
2430
2443
 
2431
2444
  ```ts signature
2432
- type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<TOIMDerivation> & {
2445
+ type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<TOIMComputedDerivation> & {
2433
2446
  readonly owner: IOIMSlotOwner;
2434
2447
  readonly computedValueService: IOIMComputedValueService<TValue>;
2435
2448
  readonly value: TValue;
@@ -2475,17 +2488,25 @@ class OIMComputedService<TKey, TValue, TComputedStore> {
2475
2488
  }
2476
2489
 
2477
2490
  createOIMComputedService<TKey, TValue, TComputedStore = Map<TKey, TOIMComputedSlot<TValue, TKey>>>({
2478
- createSources?: (key: TKey, storeSlot: TOIMStoreSlot | undefined) => Partial<TOIMComputedSources>;
2491
+ createSources?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>;
2479
2492
  derive?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue;
2480
2493
  sameValueGuard?: TOIMSameValueGuard<TValue>; // default Object.is
2494
+ releaseValue?: TOIMComputedValueReleaser<TValue>; // default: gives back nothing
2481
2495
  initialValue?: TValue;
2482
2496
  slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
2483
- columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // OIMDefaults.COMPUTED_COLUMN_SLOT_STORE_SERVICE
2497
+ columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // defaults.computedColumnSlotStoreService, then a shared map store
2484
2498
  columnNames?: readonly Extract<keyof TValue, string>[];
2485
2499
  computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore>; // Map; REQUIRED for any other store
2486
2500
  }): OIMComputedService<TKey, TValue, TComputedStore>
2487
2501
  ```
2488
2502
 
2503
+ **`releaseValue(value)` gives back what a derived value holds** (a request in flight, a timer). It is called
2504
+ with the previous value when `derive` produced one `sameValueGuard` calls different; with a new object the
2505
+ guard calls equal (the slot keeps the old one, the new one is dropped); with the current value when the
2506
+ computed slot is released; with the value a `derive` returned after its own slot was released (not stored).
2507
+ It is not called when `derive` returns the object the slot already holds, and never with `initialValue`.
2508
+ `createOIMSingleComputedService` takes it too.
2509
+
2489
2510
  The factory takes no db: a computed service is configuration, and each store slot takes its channel slot at
2490
2511
  `createComputedStoreSlot(db, …)`.
2491
2512
 
@@ -2523,8 +2544,7 @@ A single computed store slot has no key and a store of one slot. `getSingleSlot(
2523
2544
  next flush unless something holds it — and `holdSlotByKey(storeSlot, undefined)` holds the live slot, minting
2524
2545
  one if the previous is gone. A computed slot goes at its last unhold once no effect reads it
2525
2546
  (`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
2547
+ 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
2548
  gives back are one stateless service, shared by both shapes.
2529
2549
 
2530
2550
  **`createSources` takes the key and the store slot, and nothing else. There is no `keep`.** A build that
@@ -2567,7 +2587,7 @@ over the carrier computed, its value `carrier.value[column]` (a tuple of the gro
2567
2587
  the whole value for the any-column), compared with `Object.is`. It is a `TOIMComputedColumnSlot`
2568
2588
  (`TOIMComputedSlot<unknown, string>`), so it goes in the SECOND array of `createEffect`, and its value
2569
2589
  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`.
2590
+ Pinned by `computed/tests/what-a-computed-column-wakes-on.test.ts`.
2571
2591
 
2572
2592
  ```ts
2573
2593
  type Summary = { top: number; count: number };
@@ -2579,7 +2599,7 @@ const summary = summaryService.createComputedStoreSlot(
2579
2599
  );
2580
2600
  const summarySingleSlotService = summaryService.computedSingleSlotService;
2581
2601
  const summaryTop = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(summary), 'top');
2582
- const topEffect = effectService.createEffect(db, [], [summaryTop], () => { paint(slot.entity); });
2602
+ const topEffect = effectService.createEffect(db, () => { paint(slot.entity); }, [], [summaryTop]);
2583
2603
  effectService.disposeEffect(topEffect);
2584
2604
  summarySingleSlotService.columnSlotService.unholdColumn(summaryTop);
2585
2605
  ```
@@ -2619,7 +2639,7 @@ const orderService = createOIMBucketMemoComputedService({
2619
2639
  });
2620
2640
  const order = orderService.createBucketMemoStoreSlot(ordered);
2621
2641
  const deck = orderService.computedCollectionService.holdSlotByKey(order, 'spades'); // mints the bucket too, and holds it
2622
- effectService.createEffect(db, [], [deck], () => render(deck.computedValueService.getValueForSlot(deck)));
2642
+ effectService.createEffect(db, () => render(deck.computedValueService.getValueForSlot(deck)), [], [deck]);
2623
2643
  ```
2624
2644
 
2625
2645
  ---
@@ -2680,8 +2700,8 @@ are holding; neither is something you build by hand.
2680
2700
  | Store | Key type |
2681
2701
  |---|---|
2682
2702
  | `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** |
2703
+ | `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `OIMScalarKeyTreeWalk.INSTANCE` | `readonly TOIMStoreKey[]` — **a chain of VALUES** |
2704
+ | `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `OIMCompositeKeyTreeWalk.INSTANCE` | `readonly TOIMEntitySlot[]` — **a chain of entity SLOTS** |
2685
2705
  | `OIMSingleSlotStoreService.INSTANCE` | `void` — one slot per store slot, its store a `TOIMSingleSlotStore` (`{ slot }`); what every single is built on |
2686
2706
 
2687
2707
  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 +2742,8 @@ deletes nothing and returns `undefined`.
2722
2742
 
2723
2743
  | Walk | Class | Shared instance | What a composite key part is | Holds? |
2724
2744
  |---|---|---|---|---|
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** |
2745
+ | scalar | `OIMScalarKeyTreeWalk` | `OIMScalarKeyTreeWalk.INSTANCE` | `TOIMStoreKey` | no |
2746
+ | composite | `OIMCompositeKeyTreeWalk` | `OIMCompositeKeyTreeWalk.INSTANCE` | `TOIMEntitySlot` | **yes — one hold per edge** |
2727
2747
 
2728
2748
  **The hold is the whole difference.** `OIMCompositeKeyTreeWalk.mintNodeAt` calls
2729
2749
  `holdCompositeKeyPart` the first time it creates an edge, which runs
@@ -2745,10 +2765,10 @@ type TTallyStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Tally,
2745
2765
 
2746
2766
  const tallyService = createOIMSlotCollectionService<
2747
2767
  ReturnType<typeof createOIMSlotContentEntityService<Tally, TScalarChain>>, TTallyStore
2748
- >(db, {
2749
- slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(db),
2768
+ >({
2769
+ slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(),
2750
2770
  slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
2751
- OIMDefaults.KEY_TREE_WALK_SCALAR.getValue(),
2771
+ OIMScalarKeyTreeWalk.INSTANCE,
2752
2772
  ),
2753
2773
  });
2754
2774
  const tallies = tallyService.createStoreSlot(db);
@@ -2775,10 +2795,10 @@ type TEnrolmentStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<En
2775
2795
 
2776
2796
  const enrolmentService = createOIMSlotCollectionService<
2777
2797
  ReturnType<typeof createOIMSlotContentEntityService<Enrolment, TChain>>, TEnrolmentStore
2778
- >(db, {
2779
- slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(db),
2798
+ >({
2799
+ slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(),
2780
2800
  slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
2781
- OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue(),
2801
+ OIMCompositeKeyTreeWalk.INSTANCE,
2782
2802
  ),
2783
2803
  });
2784
2804
  const enrolments = enrolmentService.createStoreSlot(db);
@@ -2788,7 +2808,7 @@ enrolmentService.getSlotByKey(enrolments, [alice, admins]);
2788
2808
 
2789
2809
  **Never put a scalar in a composite-walk key.** It is a type error, and
2790
2810
  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
2811
+ string, after the edge is already in the tree. `OIMScalarKeyTreeWalk.INSTANCE` is
2792
2812
  the walk for that key — the same store class, the other strategy.
2793
2813
 
2794
2814
  **Compare a chain with the strategy that matches it**: `oimSameSlotSequence` for identities
@@ -2801,8 +2821,7 @@ address" for the address the tree is looking at.
2801
2821
  ## Lifetime — the owner protocol
2802
2822
 
2803
2823
  **`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
2824
+ `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
2825
  and raises the RELEASED flag in `slot.slotFlags` instead, which the doors below read.
2807
2826
 
2808
2827
  ```ts signature
@@ -2828,18 +2847,15 @@ At a deadline the release queue reads its MARKER off the slot, decides, and call
2828
2847
 
2829
2848
  **`OIMSlotOwner`** is the base class and it is concrete: it implements all five.
2830
2849
  Its `releaseSlotIfUnheld` does nothing, its
2831
- `unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMSlotEditable` with no
2850
+ `unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMEditable` with no
2832
2851
  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`;
2852
+ `slot.slotFlags`; the engine owns bits `1`, `2`, `4`, `8`, `16` and `32`, so a library starts at `64`;
2834
2853
  it is on the owner so a subclass can hook every such mutation), and a `protected
2835
2854
  onSlotIdle` does nothing; a `protected assertNotReleased` is what every refusing
2836
2855
  door above calls. `OIMSlotOwnerService` overrides `onSlotIdle` and
2837
2856
  `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.
2857
+ A single's slot is owned the same way: its owner is its collection type's slot owner, and that release policy
2858
+ applies to it.
2843
2859
 
2844
2860
  ### The column owner
2845
2861
 
@@ -2877,12 +2893,12 @@ so a slot with any live column slot is never released.
2877
2893
  ### Disposers
2878
2894
 
2879
2895
  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)`
2896
+ and its siblings return, and the one `effectService.subscribeEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`
2881
2897
  returns. An effect from `createEffect` has none, so the caller writes
2882
2898
  `() => effectService.disposeEffect(effect)`. A hold is given back through its owner, not through a disposer.
2883
2899
 
2884
2900
  ```ts
2885
- const effect = effectService.createEffect(db, [slot], [], () => { paint(slot.entity); });
2901
+ const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
2886
2902
  const disposers: TOIMDisposer[] = [];
2887
2903
  disposers.push(() => { effectService.disposeEffect(effect); });
2888
2904
  disposers.push(() => { slot.owner.unholdSlot(slot); });
@@ -2933,15 +2949,15 @@ leave the longest.
2933
2949
  type TOIMStoreKey = string | number;
2934
2950
  type TOIMDisposer = () => void;
2935
2951
  type TOIMEffectBody = () => (() => void) | void; // a returned function is the undo
2936
- type TOIMEffectBodyWithSourceTracking = (effectSourceTracker: OIMEffectSourceTracker) => (() => void) | void;
2952
+ type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
2937
2953
  type TOIMSameValueGuard<TValue = unknown> = (prev: TValue, next: TValue) => boolean;
2938
2954
  type TOIMChannelScheduler = (flush: () => void) => void;
2939
- type TOIMSortColumn<TEntity extends object> = { column: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' };
2955
+ type TOIMSortColumn<TEntity extends object> = { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' };
2940
2956
  type TOIMSortableColumnName<TEntity extends object> = // a column whose values are string | number | bigint
2941
2957
  { [TColumn in keyof TEntity]-?: NonNullable<TEntity[TColumn]> extends string | number | bigint ? TColumn : never }[keyof TEntity] & string;
2942
2958
 
2943
2959
  // 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
2960
+ // service, never by assigning to it; `TOIMEditable` is how the engine
2945
2961
  // itself drops that guard where it is the one mutator.
2946
2962
  type TOIMSlot = {
2947
2963
  readonly effects: TOIMEffect[] | undefined;
@@ -2952,9 +2968,9 @@ type TOIMSlot = {
2952
2968
  readonly releaseDueAfterWriteMs: number; // -1 until a write stamps it
2953
2969
  readonly releaseDueAfterIdleMs: number; // -1 until idleness stamps it
2954
2970
  readonly releaseDueAfterUnholdMs: number; // -1 until the last unhold stamps it
2955
- readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2
2971
+ readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2; bits up to 32 are the engine's
2956
2972
  readonly columnSlotStore: unknown; // the column slots this slot carries; undefined while none
2957
- readonly storeSlot: TOIMStoreSlot | undefined;
2973
+ readonly storeSlot: TOIMStoreSlot; // the store slot it belongs to; a column slot's is its carrier's
2958
2974
  readonly owner: IOIMSlotOwner; // never undefined, never cleared
2959
2975
  };
2960
2976
 
@@ -2964,7 +2980,7 @@ type TOIMSlot = {
2964
2980
  type TOIMStoreSlot<TStore = unknown, TSlot extends TOIMSlot = TOIMSlot, TPatch = unknown> = {
2965
2981
  readonly store: TStore;
2966
2982
  readonly channelSlot: TOIMChannelSlot;
2967
- readonly contentChangeListeners: IOIMContentChangeListener<TSlot, TPatch>[] | undefined;
2983
+ readonly contentChangeListeners: IOIMSlotContentChangeListener<TSlot, TPatch>[] | undefined;
2968
2984
  readonly contentChangeListenerReadColumnNames: (readonly string[] | undefined)[] | undefined;
2969
2985
  readonly indexKeysPositions: TOIMIndexKeysPositions | undefined;
2970
2986
  readonly sourceStoreSlot: TOIMStoreSlot | undefined; // an index store slot's source
@@ -2981,7 +2997,7 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
2981
2997
  TOIMStoreSlot<TStore, TBucketSlot, TBucketPatch> & {
2982
2998
  readonly sourceStoreSlot: TOIMStoreSlot<TSourceStore, TSourceSlot, TSourcePatch>;
2983
2999
  readonly indexKeysPosition: number;
2984
- readonly contentChangeListener: IOIMContentChangeListener<TSourceSlot, TSourcePatch>;
3000
+ readonly contentChangeListener: IOIMSlotContentChangeListener<TSourceSlot, TSourcePatch>;
2985
3001
  readonly requestedKeys: readonly TKey[] | undefined;
2986
3002
  readonly bucketColumnNames: Set<string>;
2987
3003
  readonly isAnyBucketColumnRead: boolean;
@@ -2991,10 +3007,10 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
2991
3007
  type TOIMComputedStoreSlot<TStore = unknown, TValue = unknown, TKey = unknown> =
2992
3008
  TOIMStoreSlot<TStore, TOIMComputedSlot<TValue, TKey>> & {
2993
3009
  readonly derive: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue) | undefined;
2994
- readonly createSources: ((key: TKey, storeSlot: TOIMStoreSlot | undefined) => Partial<TOIMComputedSources>) | undefined;
3010
+ readonly createSources: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>) | undefined;
2995
3011
  };
2996
3012
 
2997
- type TOIMSlotEditable<TSlot> = { -readonly [K in keyof TSlot]: TSlot[K] };
3013
+ type TOIMEditable<TSlot> = { -readonly [K in keyof TSlot]: TSlot[K] };
2998
3014
 
2999
3015
  type TOIMKeyedSlot<TKey = unknown> = TOIMSlot & { readonly key: TKey };
3000
3016
  type TOIMIndexableSlot<TKey = unknown> = TOIMKeyedSlot<TKey> & { readonly indexKeys: unknown[] | undefined };
@@ -3006,7 +3022,7 @@ type TOIMEntitySlot<TEntity extends object = object, TKey = unknown> = TOIMIndex
3006
3022
 
3007
3023
  // `data` belongs to the consumer: the core never reads or writes it, and clearing or
3008
3024
  // 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`.
3025
+ // last key for the entity slot in `indexKeys`, through the index's `IOIMIndexKeysService`.
3010
3026
 
3011
3027
  type TOIMColumnSlot = TOIMSlot & {
3012
3028
  readonly carrierSlot: TOIMSlot; // the entity slot or bucket slot it hangs off
@@ -3040,14 +3056,14 @@ type TOIMEntityColumnName<TEntity extends object> =
3040
3056
  [TEntity] extends [never] ? never
3041
3057
  : object extends TEntity ? string : keyof TEntity & string;
3042
3058
 
3043
- type TOIMDerivation = TOIMComputedSources & { // what a computed slot and a TOIMEffect share
3059
+ type TOIMComputedDerivation = TOIMComputedSources & { // what a computed slot and a TOIMEffect share
3044
3060
  ranAtRevision: number; ranAtEpoch: number;
3045
3061
  };
3046
3062
 
3047
3063
  type TOIMTimer = (run: () => void, inMs: number) => TOIMDisposer;
3048
3064
  ```
3049
3065
 
3050
- **`TOIMSlotEditable<TSlot>` strips `readonly` from every property.** It is exported
3066
+ **`TOIMEditable<TSlot>` strips `readonly` from every property.** It is exported
3051
3067
  because the rule "data has one mutator" is enforced by the slot types being
3052
3068
  readonly everywhere else: a class that IS the sole mutator of a slot's property
3053
3069
  casts through this type at the one place it writes, and every other reader is
@@ -3069,7 +3085,7 @@ that matches what you are comparing; they are not interchangeable.
3069
3085
 
3070
3086
  ## Full export list
3071
3087
 
3072
- **218 names, every export of `packages/core/src/index.ts`, grouped by topic.**
3088
+ **230 names, every export of `packages/core/src/index.ts`, grouped by topic.**
3073
3089
 
3074
3090
  *Channel and database slots* — `OIMChannelService` · `TOIMChannelSlot` · `TOIMChannelSlotOptions` · `createOIMChannelSlot` ·
3075
3091
  `IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot` · `IOIMFlushSchedulerListener`
@@ -3086,7 +3102,7 @@ that matches what you are comparing; they are not interchangeable.
3086
3102
  `OIMSlotOwnerService` (**type only**) · `TOIMSlotOwnerServiceOptions` · `IOIMSlotContentService` · `IOIMSlotContentWriter` ·
3087
3103
  `IOIMKeyedSlotCreator` · `IOIMSingleSlotCreator` · `IOIMSlotFilingService` · `TOIMSlotContentServiceKey` ·
3088
3104
  `TOIMSlotContentServiceSlot` · `TOIMSlotContentServiceSingleSlot` · `TOIMSlotContentServiceContent` ·
3089
- `TOIMSlotContentServicePatch` · `OIMContentChangeListenerService`
3105
+ `TOIMSlotContentServicePatch` · `OIMSlotContentChangeListenerService`
3090
3106
 
3091
3107
  *Content services* — `OIMSlotContentEntityService` · `TOIMSlotContentEntityServiceOptions` ·
3092
3108
  `createOIMSlotContentEntityService` · `TOIMEntitySlotOwnerService` · `TOIMEntityContentSlot` · `OIMSlotContentBucketService` ·
@@ -3108,20 +3124,20 @@ that matches what you are comparing; they are not interchangeable.
3108
3124
  written after that clock reading — `channelSlot.clock` read earlier.
3109
3125
 
3110
3126
  *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`
3127
+ `OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `TOIMSourceTrackingEffectBody` ·
3128
+ `OIMSourceTracker` (**type only** — the service builds it) · `TOIMComputedSlot` · `TOIMComputedSources` · `TOIMComputedValueReleaser` · `OIMSourceTrackingComputedService` · `OIMSingleSourceTrackingComputedService` · `createOIMSourceTrackingComputedService` · `createOIMSingleSourceTrackingComputedService` · `TOIMSourceTrackingComputedSlot` · `TOIMSourceTrackingComputedContentSlot` · `TOIMSourceTrackingComputedDerive` · `OIMSlotContentSourceTrackingComputedService` (**type only**) · `TOIMSourceTrackingComputedDerivationOptions` · `TOIMComputedServiceFactoryOptions` ·
3129
+ `OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `TOIMComputedDerivation`
3114
3130
 
3115
3131
  `oimRefreshSourceComputeds(sourceComputeds)` brings every computed in the list current. A binding
3116
3132
  read outside an effect (`useOIMValue` in `@oimdb/react`, `exoBinding` in `@oimdb/exodra`) calls it
3117
3133
  on its `sourceComputeds` before `derive`, so the derive never reads a stale computed.
3118
3134
 
3119
- *Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMSlotEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
3135
+ *Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
3120
3136
  `TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `OIMColumnSlotOwner` ·
3121
3137
  `OIMPushColumnSlotOwner` · `OIMPullColumnSlotOwner` · `TOIMColumnSlotOwnerOptions` · `TOIMBucketSlot` ·
3122
3138
  `TOIMSameValueGuard` · `OIMSlotOwner` ·
3123
3139
  `IOIMSlotOwner` · `IOIMSlotEmptinessPolicy` ·
3124
- `IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMContentChangeListener`
3140
+ `IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMSlotContentChangeListener`
3125
3141
 
3126
3142
  `oimIsSlotEmpty(slot): boolean` reads the EMPTY bit of `slot.slotFlags`.
3127
3143
  `oimIsSlotReleased(slot): boolean` reads its RELEASED bit.
@@ -3134,9 +3150,9 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
3134
3150
  `OIMSlotAtUnholdReleasePolicy` · `OIMSlotAtEmptyReleasePolicy` · `OIMSlotNeverReleasePolicy` · `OIMSlotEarliestReleasePolicy` ·
3135
3151
  `TOIMTimer` · `oimSliceOfMs` · `OIM_SLICE_LENGTH_MS`
3136
3152
 
3137
- *Entity writers* — `IOIMEntityWriter` · `createOIMEntityWriterInPlace` ·
3138
- `createOIMEntityWriterReplacing` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
3139
- `oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimPatchEntityColumnsGuarded` · `oimPatchEntityColumnGuarded` · `oimPutEntityColumns` · `oimPutEntityColumnsGuarded` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
3153
+ *Entity writers* — `IOIMEntityWriter` · `createOIMInPlaceEntityWriter` ·
3154
+ `createOIMReplacingEntityWriter` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
3155
+ `oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimGuardedPatchEntityColumns` · `oimGuardedPatchEntityColumn` · `oimPutEntityColumns` · `oimGuardedPutEntityColumns` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
3140
3156
 
3141
3157
  *Buckets* — `OIMBucketCommands` · `TOIMBucketCommandsOptions` · `IOIMBucketCommands` ·
3142
3158
  `IOIMBucketMemberSlotsArrayService` · `OIMBucketMemberSlotsArrayService` · `OIMBucketUniqueMemberSlotsArrayService` ·
@@ -3160,13 +3176,12 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
3160
3176
  `TOIMSlotStoreTree` · `IOIMKeyTreeWalk` · `TOIMCompositeKeyNode` ·
3161
3177
  `OIMKeyTreeWalk` · `OIMCompositeKeyTreeWalk` · `OIMScalarKeyTreeWalk`
3162
3178
 
3163
- *Assemblies* — `OIMDB` · `OIMDefaults` · `OIMDefaultValue` · `TOIMFactoryOptions`
3179
+ *Assemblies* — `OIMDB` · `OIMDefaults` · `TOIMDefaultsOptions` · `OIMDefaultValue` · `TOIMFactoryOptions`
3164
3180
 
3165
3181
  *Comparison and diffing* — `oimSameValueZeroGuard` ·
3166
3182
  `oimSameSlotSequence` · `oimSameKeySequence` · `oimOrderedListCommandsOfTransition` · `TOIMOrderedListCommand`
3167
3183
 
3168
- *Shared defaults* — `OIMDefaults.STANDALONE_SLOT_OWNER`,
3169
- `OIMDefaults.KEY_TREE_WALK_SCALAR` and `OIMDefaults.KEY_TREE_WALK_COMPOSITE`.
3184
+ *Shared defaults* — `OIMScalarKeyTreeWalk.INSTANCE` and `OIMCompositeKeyTreeWalk.INSTANCE`.
3170
3185
 
3171
3186
  **Nothing else is exported.** If a symbol is not on this list, it does not exist.
3172
3187
 
@@ -3174,18 +3189,18 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
3174
3189
 
3175
3190
  ## Known defects
3176
3191
 
3177
- ### A scalar in a `KEY_TREE_WALK_COMPOSITE` key throws
3192
+ ### A scalar in a key of the composite key tree walk throws
3178
3193
 
3179
- `OIMSlotTreeStoreService` built with `OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue()` keys on
3194
+ `OIMSlotTreeStoreService` built with `OIMCompositeKeyTreeWalk.INSTANCE` keys on
3180
3195
  `readonly TOIMEntitySlot[]`, so a string or a number in the key throws
3181
3196
  a TypeError past a cast, setting `holdCount` on a string. The store for a tuple
3182
3197
  of scalars is `OIMSlotTreeStoreService` built with
3183
- `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` —
3198
+ `OIMScalarKeyTreeWalk.INSTANCE` —
3184
3199
  see **Key stores** above. The fix is the other walk, never a cast.
3185
3200
 
3186
3201
  ### `TOIMEffect` is a type-only export
3187
3202
 
3188
- `effectService.createEffect(...)` and `effectService.createEffectImmediate(...)` are the whole of
3203
+ `effectService.createEffect(...)` and `effectService.createAndRunEffect(...)` are the whole of
3189
3204
  legitimate construction. An object literal typed `TOIMEffect` subscribes to nothing and
3190
3205
  schedules nothing: when one of its sources already moved in the open batch, a
3191
3206
  later write to that source is swallowed by the coalescing guard and the effect
@@ -3199,8 +3214,8 @@ Every `@oimdb` package ships both formats — `dist/index.js` for `import`,
3199
3214
  condition so a CommonJS consumer's declarations match its runtime), both named
3200
3215
  in the `exports` map. A runtime loads a package once per format, so a process
3201
3216
  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.
3217
+ module-level value — two sets of built-in defaults and two `OIMDefaults` classes,
3218
+ so services built by the two copies share nothing.
3204
3219
 
3205
3220
  So `src/index.ts` registers its copy under `Symbol.for('@oimdb/core')` on
3206
3221
  `globalThis`, and the second copy to load throws
@@ -3218,8 +3233,8 @@ core objects around — through both spellings in one process. Pick one.
3218
3233
 
3219
3234
  ## Other packages
3220
3235
 
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
3236
+ `@oimdb/react`, `@oimdb/persist` (with its five backends), `@oimdb/exodra` and `@oimdb/browser` build
3237
+ on the current core — persist, exodra and browser have sections of their own below. Every
3223
3238
  remaining package imports only names the core exports. Read a package's own
3224
3239
  source before generating code against it.
3225
3240
 
@@ -3252,7 +3267,7 @@ between render and subscribe is safe.
3252
3267
  `useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)` holds the identities its keys name for as long as the
3253
3268
  component is mounted (a `holdSlotByKey` per key on commit, the same holds given back on unmount or when the list
3254
3269
  moves) and subscribes to nothing; it needs no provider.
3255
- Singles are not keyed collections: `createOIMSingleSlotService(db, { slotContentService })` returns an
3270
+ Singles are not keyed collections: `createOIMSingleSlotService({ slotContentService })` returns an
3256
3271
  `OIMWritableSingleSlotService` (`getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`), read by
3257
3272
  `useOIMSingleEntity(singleSlotService, singleSlotStoreSlot): TEntity | undefined` and
3258
3273
  `useOIMSingleBucketSlots(singleSlotService, singleSlotStoreSlot): readonly TOIMEntitySlot<TEntity>[]` — the by-key
@@ -3264,11 +3279,12 @@ re-read the imports rather than trusting this table.
3264
3279
 
3265
3280
  | Package | Imports from the core | Imports a name the core does not export? |
3266
3281
  |---|---|---|
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 |
3282
+ | `@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 |
3283
+ | `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntityColumnName`, `TOIMEntitySlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
3269
3284
  | `@oimdb/exodra` | `IOIMChannelMap`, `OIMChannelService`, `OIMEffectService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMComputedSlot`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMOrderedListCommand`, `TOIMSlot`, `TOIMStoreKey`, `oimOrderedListCommandsOfTransition`, `oimRefreshSourceComputeds`, `oimSameKeySequence` | no |
3270
3285
  | `@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
3286
  | `@oimdb/devtools` | `IOIMChannelMap`, `IOIMStoreSlotListener`, `OIMChannelService`, `OIMComputedService`, `OIMDefaults`, `OIMSlotCollectionService`, `TOIMBucketSlot`, `TOIMComputedStoreSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEntitySlot`, `TOIMKeyedSlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
3287
+ | `@oimdb/browser` | `IOIMSlotSubscriptionListener`, `OIMChannelService`, `OIMDefaults`, `OIMSingleSlotService`, `OIMSlotContentEntityService`, `TOIMDisposer`, `TOIMEntityContentSlot`, `TOIMSlot`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`, `oimIsSlotReleased` | no |
3272
3288
  | `@oimdb/async` | — | deprecated stub, `export {}` |
3273
3289
  | `@oimdb/snapshot-manager` | — | empty stub |
3274
3290
 
@@ -3306,9 +3322,9 @@ import {
3306
3322
  import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
3307
3323
 
3308
3324
  const persistEntityWriter = new OIMPersistEntityWriter<Card, TOIMStoreKey>({
3309
- entityWriter: createOIMEntityWriterInPlace(),
3325
+ entityWriter: createOIMInPlaceEntityWriter(),
3310
3326
  });
3311
- const persistedCardCollectionService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db, { entityWriter: persistEntityWriter }) });
3327
+ const persistedCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>({ entityWriter: persistEntityWriter }) });
3312
3328
  const persistedCardEntityStoreSlot = persistedCardCollectionService.createStoreSlot(db);
3313
3329
 
3314
3330
  const persistMemoryBackend = new OIMPersistMemoryBackend();
@@ -3471,9 +3487,9 @@ import {
3471
3487
  import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
3472
3488
 
3473
3489
  type TDeckCardSlot = TOIMEntitySlot<Card, TOIMStoreKey>;
3474
- const deckCardCollectionService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db) });
3490
+ const deckCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
3475
3491
  const deckCardEntityStoreSlot = deckCardCollectionService.createStoreSlot(db);
3476
- const deckBucketService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
3492
+ const deckBucketService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
3477
3493
  const deckBucketStoreSlot = deckBucketService.createStoreSlot(db);
3478
3494
  const spadesBucketSlot = deckBucketService.holdSlotByKey(deckBucketStoreSlot, 'spades');
3479
3495
 
@@ -3722,7 +3738,7 @@ import type { TOIMPersistVersionedShape } from '@oimdb/persist';
3722
3738
 
3723
3739
  type TSettings = { theme: 'light' | 'dark'; density: 'compact' | 'comfortable' };
3724
3740
 
3725
- const settingsSingleEntityService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<TSettings>(db) });
3741
+ const settingsSingleEntityService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<TSettings>() });
3726
3742
  const settingsEntityStoreSlot = settingsSingleEntityService.createStoreSlot(db);
3727
3743
  settingsSingleEntityService.writeSingleSlotContent(settingsEntityStoreSlot, { theme: 'light', density: 'compact' });
3728
3744
  const itemByStorageKey = new Map<string, string>();
@@ -3869,9 +3885,9 @@ own properties, so a stored `__proto__` is data.
3869
3885
  re-reads every member and every watched slot, and the source keeps one effect per
3870
3886
  member entity slot.
3871
3887
  - **The memory and JSON backends keep values by reference.** With
3872
- `createOIMEntityWriterInPlace()` the stored entity is the live entity object, so an
3888
+ `createOIMInPlaceEntityWriter()` the stored entity is the live entity object, so an
3873
3889
  in-place write moves the stored value before any commit, and `dehydrate()` returns
3874
- the live objects. Use `createOIMEntityWriterReplacing()` with
3890
+ the live objects. Use `createOIMReplacingEntityWriter()` with
3875
3891
  `entityInserter: oimInsertEntityWithCopy` where the stored value must stay
3876
3892
  what was committed, and serialize a dump at once.
3877
3893
  - **Hydrate without a reconcile replaces.** The collection source deletes every
@@ -3892,7 +3908,7 @@ only on the type-only `@exodra/reactivity-types`.
3892
3908
 
3893
3909
  ### One door: `exoBinding(channelMap, binding)`
3894
3910
 
3895
- A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, sourceSlots, sourceComputeds, onChange)` —
3911
+ A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, onChange, sourceSlots, sourceComputeds)` —
3896
3912
  and the value is read off the slots afterwards. So one adapter covers an entity, a
3897
3913
  column, a bucket's slots and a derivation:
3898
3914
 
@@ -3948,7 +3964,7 @@ resolved before its entity arrived, an entity since removed.
3948
3964
 
3949
3965
  - `exoSource(read, subscribe)` — the lazy, ref-counted base. For sources that are
3950
3966
  **not** this engine's.
3951
- - `exoCombine(sources, fn)` — fan-in across any `TOIMExoReadable`. Not glitch-free;
3967
+ - `exoCombine(fn, sources)` — fan-in across any `TOIMExoReadable`. Not glitch-free;
3952
3968
  keep heavy fan-in inside one computed.
3953
3969
  - `exoFollowKey(key, makeBindable)` — repoints as a selection moves. The factory
3954
3970
  must return a `TOIMExoDisposable`.
@@ -3982,6 +3998,29 @@ memoised per list, so two lists showing one entity slot render it twice.
3982
3998
 
3983
3999
  ---
3984
4000
 
4001
+ ## `@oimdb/browser`
4002
+
4003
+ Browser state as single slots. Each factory returns an `OIMSingleSlotService` (`TOIMBrowserSingleSlotService<TEntity>`)
4004
+ built with a slot subscription listener: the browser listener is added when the slot gets its first
4005
+ subscriber (directly or through a computed that reads it) and removed when the last one goes; the current
4006
+ value is written at the first subscription, and a write that would land during a flush is made right after it.
4007
+
4008
+ ```ts signature
4009
+ createOIMBrowserOnlineSingleSlotService(options?: { defaults?: OIMDefaults }): TOIMBrowserSingleSlotService<TOIMBrowserOnlineEntity> // { isOnline }
4010
+ createOIMBrowserFocusSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserFocusEntity> // { isFocused }: document.hasFocus()
4011
+ createOIMBrowserVisibilitySingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserVisibilityEntity> // { isVisible }
4012
+ createOIMBrowserLocationSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserLocationEntity> // { href, pathname, search, hash }
4013
+ ```
4014
+
4015
+ Read it like any single: `const onlineSlot = onlineService.getSingleSlot(onlineService.createStoreSlot(db))`, then
4016
+ an effect over `[onlineSlot]`, or `useOIMSingleEntity(onlineService, online)` in React. `isOnline` is
4017
+ `navigator.onLine` (a network interface exists, not a reachable internet); the location follows the
4018
+ Navigation API's `currententrychange` where it exists and `popstate` + `hashchange` otherwise, so without it a
4019
+ `history.pushState` is not seen until the next of those events. Without the API a sensor needs, the slot holds
4020
+ `true` (or an empty location). `OIMBrowserSlotSubscriptionListener` is the base class of the four listeners.
4021
+
4022
+ ---
4023
+
3985
4024
  ## Performance
3986
4025
 
3987
4026
  **No figure may be quoted for this engine, and none is quoted anywhere in this
@@ -4014,7 +4053,7 @@ The one quantity that needs no measurement, because it is arithmetic rather than
4014
4053
  a reading: emptying a bucket one slot at a time on an array-driven bucket costs
4015
4054
  n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `writeSlotContent`
4016
4055
  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
4056
+ `packages/core/src/bucket/array/tests/everyone-out-at-once-and-not-one-at-a-time.test.ts`, which
4018
4057
  also pins `drainSlots` at zero element writes.
4019
4058
 
4020
4059
  Fast properties are checked, not timed. Every slot kind is one object literal with