@oimdb/core 4.0.1 → 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/README.md CHANGED
@@ -60,9 +60,9 @@ still on CommonJS, which is why both are here.
60
60
  throws.** `src/index.ts` registers its copy under `Symbol.for('@oimdb/core')` on
61
61
  `globalThis`, and a second copy that finds another one there throws
62
62
  `@oimdb/core is loaded twice in this process`. Without this check the split would be silent:
63
- each copy keeps its own `OIMDefaults`, so a default set through one
64
- (`OIMDefaults.ENTITY_WRITER.setValue(createOIMEntityWriterReplacing())`) never reaches a
65
- service built by the other, which keeps writing in place. Objects built by
63
+ each copy keeps its own built-in defaults and its own `OIMDefaults` class, so an `OIMDefaults` built
64
+ with one copy's class is a stranger to the other copy's factories, and two services built by the two copies
65
+ share no default instance. Objects built by
66
66
  either copy carry their own methods, so a channel, a service and an effect
67
67
  from different copies still deliver to each other; only the defaults diverge.
68
68
 
@@ -75,7 +75,7 @@ first sentence on its own points at the one thing that is not wrong.
75
75
 
76
76
  The mechanism is one line: a runtime loads a package once per format, so
77
77
  `import` in one file and `require` in another holds two complete copies of every
78
- class and every module-level value, such as the `OIMDefaults` statics or a React context.
78
+ class and every module-level value, such as `OIMDefaults.INSTANCE` or a React context.
79
79
 
80
80
  The rule is one line too: pick `import` or `require` for `@oimdb/core`, and use the
81
81
  same one everywhere in the process, including inside every `@oimdb` package
@@ -90,24 +90,36 @@ What each consumer ships, bundled with esbuild (minified, tree-shaken, ESM).
90
90
  <!-- bundle-size:begin -->
91
91
  | Consumer | Minified | Minified + gzip |
92
92
  |---|---:|---:|
93
- | Everything `@oimdb/core` exports | 103361 B | 25288 B |
94
- | One entity store slot: `OIMDB`, `createOIMEntityService`, `upsertEntity`, `getSlotByKey` | 41485 B | 10710 B |
95
- | The same, plus one effect and `db.flush()` | 50131 B | 12876 B |
96
- | The same, plus one index: `createOIMIndexService` | 71765 B | 17728 B |
97
- | An ordered bucket with commands: `createOIMBucketService`, `OIMSlotContentBucketServiceArrayDriven`, `OIMBucketCommands` | 56072 B | 13880 B |
93
+ | Everything `@oimdb/core` exports | 102669 B | 24555 B |
94
+ | One entity store slot: `OIMDB`, `createOIMSlotCollectionService`, `createOIMSlotContentEntityService`, `writeSlotContentByKey`, `getSlotByKey` | 35413 B | 9161 B |
95
+ | The same, plus one effect and `db.flush()` | 45582 B | 11543 B |
96
+ | The same, plus one index: `createOIMSlotCollectionIndexService` | 65835 B | 16192 B |
97
+ | An ordered bucket with commands: `OIMSlotContentPositionalArrayBucketService`, `OIMBucketCommands` | 48469 B | 12117 B |
98
98
  <!-- bundle-size:end -->
99
99
 
100
100
  ---
101
101
 
102
- ## The three layers
102
+ ## The domains
103
103
 
104
- `src/index.ts` is the entire public surface, and it is divided in three:
104
+ `src/` holds one folder per domain and `index.ts`, the entire public surface; a factory lives in
105
+ the domain of what it builds. Where a file goes is in `packages/core/src/README.md`.
105
106
 
106
- | Layer | What is in it |
107
+ | Domain | Contents |
107
108
  |---|---|
108
- | `src/engine/` | The primitives, each a stateless service over a plain struct: `OIMChannelService.INSTANCE` over a `TOIMChannelSlot` (frames, the revision, subframes), `OIMEffectService.INSTANCE` (effects, handed the `db` on every call), `OIMSlotOwnerService` (the owner of every slot a collection filed: holds, release, emptiness, columns), the column owners (abstract `OIMColumnOwner`, `OIMColumnOwnerPushDriven` written by the collection, `OIMColumnOwnerPullDriven` derived by a computed), the content services (`OIMSlotContentEntityService` for entities, `OIMComputedValueService` for computeds), the key stores and key tree walks, the release policies, the collectors (`OIMUnusedSlotCollectorService`, `OIMReleaseSlotCollectorService*`) over the collector slot, `TOIMStoreSlot`, `TOIMComputedSlot`, `TOIMEffect`. |
109
- | `src/facade/` | What a caller holds. The collection services `OIMSlotCollectionService` (keyed) and `OIMSingleSlotService` (one slot, no key), each with its `slotContentService`; the bucket content services `OIMSlotContentBucketServiceSetDriven`, `OIMSlotContentBucketServiceArrayDriven`, `OIMSlotContentBucketServiceSortedArrayDriven` and `OIMBucketCommands`; the auto-index `OIMIndexService` and `OIMIndexServiceSortedArrayDriven`; `OIMComputedService`, `OIMComputedServiceSorted`, `OIMComputedServiceBucketMemo`. |
110
- | `src/factories/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`, and the doors that build a service: the base ones, `createOIMSlotCollectionService`, `createOIMSingleSlotService` and `createOIMSlotContentEntityService`, and the convenience ones over them, `createOIMEntityService`, `createOIMSingleEntityService`, `createOIMBucketService`, `createOIMSingleBucketService`, `createOIMIndexService`, `createOIMIndexServiceByCompositeKey`, `createOIMIndexServiceSortedArrayDriven`, `createOIMComputedService`, `createOIMComputedServiceSorted`, `createOIMComputedServiceBucketMemo`, `createOIMEntityWriterInPlace`, `createOIMEntityWriterReplacing`. |
109
+ | `slot/` | `OIMSlotOwner`, `OIMSlotOwnerService` (type-only export), `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `OIMSingleSlotService`, `OIMWritableSingleSlotService`, `OIMSlotContentChangeListenerService`, `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `TOIMSlot`, `TOIMEditable` |
110
+ | `store/` | `TOIMStoreSlot`, the slot store services |
111
+ | `channel/` | `OIMChannelService`, `TOIMChannelSlot` |
112
+ | `effect/` | `OIMEffectService`, `TOIMEffect` |
113
+ | `computed/` | `OIMComputedValueService`, `OIMComputedService`, `OIMSingleComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService`, the source-tracking computed services, their factories, `TOIMComputedSlot` |
114
+ | `entity/` | `OIMSlotContentEntityService`, `createOIMSlotContentEntityService`, `createOIMInPlaceEntityWriter`, `createOIMReplacingEntityWriter` |
115
+ | `column/` | abstract `OIMColumnSlotOwner` with `OIMPushColumnSlotOwner` and `OIMPullColumnSlotOwner`, `OIMColumnSlotService` |
116
+ | `bucket/` | `OIMSlotContentBucketService` (abstract), `OIMSlotContentSetBucketService`, `OIMSlotContentArrayBucketService` (abstract), `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService`, `OIMBucketCommands` |
117
+ | `index/` | `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`, their factories, the index keys services |
118
+ | `key/` | the key tree walks |
119
+ | `collector/` | the release policies, `OIMSlotReleaseWheel`, `OIMSlotRefetchWheel`, `OIMUnusedSlotCollectorService` |
120
+ | `schedule/` | `OIMFlushSchedulerService` |
121
+ | `error/` | the error policies |
122
+ | `db/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`, the default constants |
111
123
 
112
124
  **A service holds no data; a store slot is one instance.** A service is built once per
113
125
  type, and `cardService.createStoreSlot(db)` returns a `TOIMStoreSlot`, a plain object whose
@@ -116,7 +128,7 @@ service has two halves. Its own doors read and mint slots, and take the store sl
116
128
  `cardService.getSlotByKey(cards, pk)`, `cardService.holdSlotByKey(cards, pk)`,
117
129
  `cardService.findSlotByKey(cards, pk)`, `cardService.getSlots(cards)`,
118
130
  `cardService.getSlotCount(cards)`, plus the column doors (`getColumn`, `holdColumn`, …). Its
119
- `slotContentService` holds the content and every write: `cardService.slotContentService.upsertEntity(cards, card)`
131
+ `slotContentService` holds the content and every write: `cardService.slotContentService.writeSlotContentByKey(cards, card)`
120
132
  takes the store slot, and a door handed a slot takes only the slot, because the slot carries its
121
133
  own store slot (`slot.storeSlot`): `cardService.slotContentService.updateEntity(slot, patch)`.
122
134
  The owner of the slots is reached through `slot.owner` (an `IOIMSlotOwner`: hold, unhold,
@@ -127,60 +139,14 @@ release, subscribe), never through the service.
127
139
  ## The shortest thing that delivers
128
140
 
129
141
  ```typescript setup
130
- import {
131
- OIMBucketCommands,
132
- OIMBucketArrayMembershipUnique,
133
- OIMChannelService,
134
- OIMDB,
135
- OIMDefaults,
136
- OIMEffectService,
137
- OIMErrorPolicyLogAndContinue,
138
- OIMSlotContentBucketServiceArrayDriven,
139
- OIMSlotContentBucketServiceSetDriven,
140
- OIMSlotReleasePolicyAfterIdle,
141
- OIMSlotReleasePolicyAfterWrite,
142
- OIMSlotReleasePolicyEarliest,
143
- OIMSlotStoreServiceMapDriven,
144
- OIMSlotStoreServiceSingleSlotDriven,
145
- OIMSlotStoreServiceTreeDriven,
146
- OIMUnusedSlotCollectorService,
147
- createOIMUnusedSlotCollectorSlot,
148
- oimInsertEntityWithCopy,
149
- oimIsSlotReleased,
150
- createOIMBucketService,
151
- createOIMComputedServiceBucketMemo,
152
- createOIMComputedService,
153
- createOIMComputedServiceSorted,
154
- createOIMEntityService,
155
- createOIMEntityWriterInPlace,
156
- createOIMIndexService,
157
- createOIMIndexServiceSortedArrayDriven,
158
- createOIMSingleEntityService,
159
- createOIMSlotCollectionService,
160
- createOIMSlotContentEntityService,
161
- oimOrderedListCommandsOfTransition,
162
- oimSameSlotSequence,
163
- oimSameValueZeroGuard,
164
- type TOIMComputedSlot,
165
- type TOIMBucketSlotsArrayUniqueStore,
166
- type TOIMBucketSlotsArrayStore,
167
- type TOIMBucketSlot,
168
- type TOIMBucketSlotsSetStore,
169
- type TOIMDisposer,
170
- type TOIMEntityColumnName,
171
- type TOIMEntitySlot,
172
- type TOIMOrderedListCommand,
173
- type TOIMSingleSlotStore,
174
- type TOIMSlotStoreTree,
175
- type TOIMStoreKey,
176
- } from '@oimdb/core';
142
+ import { OIMBucketCommands, OIMChannelService, OIMDB, OIMDefaults, OIMEffectService, OIMLogAndContinueErrorPolicy, OIMSlotContentPositionalArrayBucketService, OIMBucketUniqueMemberSlotsArrayService, OIMSlotContentSetBucketService, OIMSlotAfterIdleReleasePolicy, OIMSlotAfterWriteReleasePolicy, OIMSlotEarliestReleasePolicy, createOIMSingleSlotService, createOIMSingleComputedService, OIMSlotMapStoreService, OIMSingleSlotStoreService, OIMSlotTreeStoreService, OIMUnusedSlotCollectorService, createOIMUnusedSlotCollectorSlot, oimInsertEntityWithCopy, oimIsSlotReleased, createOIMBucketMemoComputedService, createOIMComputedService, createOIMSortedComputedService, createOIMInPlaceEntityWriter, createOIMSlotCollectionIndexService, createOIMSlotCollectionSortedArrayIndexService, createOIMSlotCollectionService, createOIMSlotContentEntityService, oimOrderedListCommandsOfTransition, oimSameSlotSequence, oimSameValueZeroGuard, type TOIMComputedSlot, type TOIMBucketSlotsArrayStore, type TOIMBucketSlot, type TOIMBucketSlotsSetStore, type TOIMDisposer, type TOIMEntityColumnName, type TOIMEntitySlot, type TOIMOrderedListCommand, type TOIMSingleSlotStore, type TOIMSlotStoreTree, type TOIMStoreKey, createOIMReplacingEntityWriter, OIMScalarKeyTreeWalk, OIMCompositeKeyTreeWalk } from '@oimdb/core';
177
143
 
178
144
  type Card = { id: string; deck: string; title: string; rank: number };
179
145
  type CardSlot = TOIMEntitySlot<Card>;
180
146
 
181
147
  const db = new OIMDB();
182
148
  const effectService = OIMEffectService.INSTANCE;
183
- const cardService = createOIMEntityService<Card>(db, { keyOf: (card) => card.id });
149
+ const cardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
184
150
  const cards = cardService.createStoreSlot(db);
185
151
  const slot = cardService.getSlotByKey(cards, 'c1');
186
152
  const byRank = (leftBucketMemberSlot: TOIMEntitySlot<Card>, rightBucketMemberSlot: TOIMEntitySlot<Card>): number =>
@@ -188,12 +154,12 @@ const byRank = (leftBucketMemberSlot: TOIMEntitySlot<Card>, rightBucketMemberSlo
188
154
  ```
189
155
 
190
156
  ```typescript
191
- cardService.slotContentService.upsertEntity(cards, { id: 'c1', deck: 'spades', title: 'Ace', rank: 1 });
157
+ cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', deck: 'spades', title: 'Ace', rank: 1 });
192
158
 
193
159
  let seen = '';
194
- const effect = effectService.createEffect(db, [slot], [], () => {
160
+ const effect = effectService.createEffect(db, () => {
195
161
  seen = slot.entity?.title ?? '';
196
- });
162
+ }, [slot]);
197
163
 
198
164
  cardService.slotContentService.updateEntity(slot, { title: 'Ace of Spades' });
199
165
  db.flush(); // seen === 'Ace of Spades'
@@ -203,7 +169,7 @@ effectService.disposeEffect(effect);
203
169
 
204
170
  `OIMDB` is the shared state and nothing else: one `clock` (`{ revision, epoch }`), one
205
171
  `defaultChannelSlot` (a `TOIMChannelSlot`, the struct a channel's state lives in), one
206
- `collectorSlot` (the unused-slot collector's slot and one slot per release collector), and the
172
+ `collectorSlot` (the unused-slot collector's slot and the release wheels, made on first use), and the
207
173
  `entityWriter`, `slotStamp`, `slotReleasePolicy` and `indexKeysService` its constructor is handed,
208
174
  which it keeps as the default that every door below reads. It is an `IOIMChannelMap`, so
209
175
  anything that takes a `channelMap` takes the `db`. `flush()` is
@@ -215,6 +181,11 @@ and `OIMEffectService.INSTANCE` are stateless; every call is handed the channel
215
181
  whose `defaultChannelSlot` it is). A store slot carries its own channel slot as
216
182
  `storeSlot.channelSlot`, and an effect as `effect.channelSlot`.
217
183
 
184
+ **One channel per database; for isolation, a separate `OIMDB`.** A second channel adds nothing a
185
+ database does not already do: a write wakes every effect on the slots it moved at its own
186
+ channel's flush, whichever channel those effects belong to, so a second channel on the same clock
187
+ isolates neither time nor state. A separate `new OIMDB()` has its own clock, collectors and channel.
188
+
218
189
  There is no default scheduler, on purpose. A test calls `db.flush()`; an
219
190
  application gives that call to the orchestrator. With cnstra that is one line:
220
191
  a drain listener that calls `db.flush()` at the end of every synchronous turn of every
@@ -224,29 +195,26 @@ Every mutation goes through a domain neuron. See
224
195
  [With cnstra](https://oimdb.org/docs/guides/with-cnstra).
225
196
 
226
197
  The doors are free functions, one module each, and each builds a **service**, once per
227
- type; an instance is the store slot that service creates. A door over a collection takes
228
- `db` first and ONE options object second. On the bucket door the object is optional while the
229
- key is a `TOIMStoreKey`. With an array key the type requires it, and it must include a
230
- `bucketSlotStoreService` (`OIMSlotStoreServiceTreeDriven`), because a map-driven store compares
231
- arrays by reference; that argument list is `TOIMBucketCollectionFactoryArguments`. A collection
232
- under no key at all is a single service: `createOIMSingleEntityService` /
233
- `createOIMSingleBucketService`; see [The six shapes](#the-six-shapes). An index takes a scalar key
234
- through `createOIMIndexService` and an array key (several columns) through
235
- `createOIMIndexServiceByCompositeKey`, which sets the tree-driven store itself. The computed doors
198
+ type; an instance is the store slot that service creates. A collection, a single and an index take
199
+ `db` first and ONE options object second. Every collection is `createOIMSlotCollectionService` over a content
200
+ service, and every single is `createOIMSingleSlotService`; an array key needs a tree store
201
+ (`slotStoreService: new OIMSlotTreeStoreService(…)`, or `bucketSlotStoreService` on an index), because a
202
+ map-driven store compares arrays by reference. See [The six shapes](#the-six-shapes). The computed doors
236
203
  take no `db`: a computed reaches its channel through the store slot it derives from, or through
237
204
  the `db` handed to `createComputedStoreSlot`.
238
205
 
239
206
  ```typescript signature
240
- createOIMEntityService<Card, TColumnName?, TKey?, TStore?>(db, { keyOf, entityWriter?, slotEmptiness?, slotReleasePolicy?, columnSlotStoreService? | columnNames?, entitySlotStoreService? })
241
- createOIMSingleEntityService<Card, TColumnName?>(db, { entityWriter?, slotEmptiness?, slotReleasePolicy?, columnSlotStoreService? | columnNames? }?)
242
- createOIMBucketService<TBucketMemberSlot?, TKey?, TSlots?, TSlotContentService?, TStore?>(db, { slotContentService?, bucketSlotStoreService?, maxCommandsPerSlot?, slotReleasePolicy?, columnSlotStoreService? | columnNames? }?)
243
- createOIMSingleBucketService<TBucketMemberSlot?, TSlots?, TSlotContentService?>(db, { slotContentService?, maxCommandsPerSlot?, slotReleasePolicy?, columnSlotStoreService? | columnNames? }?)
244
- createOIMIndexService(db, { entityService, columns | keyOf, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
245
- createOIMIndexServiceByCompositeKey(db, { entityService, columns, slotContentService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
246
- createOIMIndexServiceSortedArrayDriven(db, { entityService, sortColumns, columns | keyOf, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
247
- createOIMComputedService<TKey, TValue, TComputedStore?>({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? | columnNames? }) // a non-Map store: computedSlotStoreService required
248
- createOIMComputedServiceSorted({ indexService, sortColumns?, comparator?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
249
- createOIMComputedServiceBucketMemo({ bucketCollectionService, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
207
+ createOIMSlotContentEntityService<Card, TKey?, TColumnName?>({ entityWriter?, slotEmptinessPolicy? }?)
208
+ new OIMSlotContentSetBucketService<TBucketMemberSlot, TKey?>({ bucketCommands? })
209
+ new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot, TKey?>({ bucketCommands?, bucketMemberSlotsArrayService? })
210
+ createOIMSlotCollectionService({ slotContentService, slotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? | columnNames? })
211
+ createOIMSingleSlotService({ slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? | columnNames? })
212
+ createOIMSlotCollectionIndexService({ entityService, columns | keyOf, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
213
+ createOIMSlotCollectionSortedArrayIndexService({ entityService, sortColumns, columns | keyOf, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
214
+ createOIMComputedService<TKey, TValue, TComputedStore?>({ createSources?, derive?, sameValueGuard?, releaseValue?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? | columnNames? }) // a non-Map store: computedSlotStoreService required
215
+ createOIMSingleComputedService<TValue>({ createSources?, derive?, sameValueGuard?, releaseValue?, initialValue?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
216
+ createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
217
+ createOIMBucketMemoComputedService({ bucketCollectionService, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
250
218
 
251
219
  cardService.createStoreSlot(db) // an entity or bucket store slot, reporting to db.defaultChannelSlot
252
220
  indexService.createIndexStoreSlot(cards) // the index's bucket store slot over one entity store slot
@@ -255,17 +223,12 @@ orderService.createBucketMemoStoreSlot(bucketStoreSlot) // one derivation per bu
255
223
  computedService.createComputedStoreSlot(db, derive?, createSources?)
256
224
  ```
257
225
 
258
- **The convenience doors sit on two base ones.** `createOIMEntityService(db, options)` is
259
- `createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService(db, options) })`,
260
- and `createOIMSingleEntityService` is the same over `createOIMSingleSlotService`. The content
261
- service is where everything per type is fixed, once: the `keyOf`, the entity writer, the
262
- emptiness rule, the store service, and the slot owner built from `db.slotStamp`, the release
263
- policy and the column options. The collection service only lends that content service's owner
264
- and store service its doors. Call the base doors when the content service is built elsewhere and
265
- shared, or when you want the content service in hand before the collection:
226
+ **The content service is where everything per type is fixed, once:** the entity writer and the emptiness rule.
227
+ The collection service adds the store service, the release policy and the column options, and builds the slot
228
+ owner from them and `defaults.slotStamp`. Build the content service first when it is shared:
266
229
 
267
230
  ```typescript
268
- const cardContentService = createOIMSlotContentEntityService<Card>(db, { keyOf: (card) => card.id });
231
+ const cardContentService = createOIMSlotContentEntityService<Card>();
269
232
  const sameShapeCardService = createOIMSlotCollectionService({ slotContentService: cardContentService });
270
233
  const sameShapeCards = sameShapeCardService.createStoreSlot(db);
271
234
  ```
@@ -282,16 +245,16 @@ reaches for a default of its own.
282
245
  The doors are free functions, not methods on `OIMDB`, so a consumer who wires
283
246
  nothing does not import the bucket machinery. This is pinned by
284
247
  `tests/what-an-import-retains.test.ts` (the root retains no door) and by
285
- `src/factories/tests/which-answer-a-collection-the-root-mints-gets.test.ts`.
248
+ `src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts`.
286
249
 
287
- **A bucket's content service decides how it holds its slots.** `createOIMBucketService` takes it
288
- as `slotContentService`. With none, it builds an `OIMSlotContentBucketServiceArrayDriven`
250
+ **A bucket's content service decides how it holds its slots.** `createOIMSlotCollectionService` takes it
251
+ as `slotContentService`. With none, it builds an `OIMSlotContentPositionalArrayBucketService`
289
252
  (insertion order, never re-decided) with an `OIMBucketCommands` that records what each position
290
253
  write did; `maxCommandsPerSlot` caps that record. Pass
291
- `new OIMSlotContentBucketServiceSetDriven<TMember>()` for a bucket that keeps its slots in a set,
292
- and `new OIMSlotContentBucketServiceSortedArrayDriven({ compareBucketMemberSlots })` for one kept in
254
+ `new OIMSlotContentSetBucketService<TMember>()` for a bucket that keeps its slots in a set,
255
+ and `new OIMSlotContentSortedArrayBucketService({ compareBucketMemberSlots })` for one kept in
293
256
  a comparator's order. A bucket's reads and writes are on that content service:
294
- `bucketService.slotContentService.writeBucketMemberSlots(bucketSlot, slots)`.
257
+ `bucketService.slotContentService.writeSlotContent(bucketSlot, slots)`.
295
258
 
296
259
  **A bucket never sorts, except a sorted one.** The order of an array-driven bucket is what the
297
260
  caller put in: `addBucketMemberSlot` appends, `insertBucketMemberSlotAt` inserts, a move moves.
@@ -300,9 +263,9 @@ See [A sorted list is a computed over an index](#a-sorted-list-is-a-computed-ove
300
263
  for the index whose buckets sort themselves, see [A sorted index: sorted at write](#a-sorted-index-sorted-at-write).
301
264
 
302
265
  Every property of a door's options is a choice made once at construction: **where identities
303
- live** (`entitySlotStoreService`, the strategy every store slot of the service files its slots
266
+ live** (`slotStoreService`, the strategy every store slot of the service files its slots
304
267
  with), **how a key comes out of an entity** (`keyOf`), **how a write lands** (`entityWriter`),
305
- **what counts as empty** (`slotEmptiness`), **when a slot goes** (`slotReleasePolicy`), and
268
+ **what counts as empty** (`slotEmptinessPolicy`), **when a slot goes** (`slotReleasePolicy`), and
306
269
  **where column slots live** (`columnSlotStoreService`, or `columnNames` for the ordinal store; the
307
270
  slot owner makes its own column owner over it). Everything but `keyOf` falls back to the `db` or
308
271
  to a lazy `OIMDefaultValue` on [`OIMDefaults`](#every-optional-dependency-has-a-default).
@@ -316,66 +279,57 @@ There is no argument that turns delivery off. A store slot is created on a `db`
316
279
  unused-slot collector a read hands its mint to is that channel slot's `collectorSlot`.
317
280
 
318
281
  ```typescript
319
- const deliveringCardService = createOIMEntityService<Card>(db, {
320
- keyOf: (card) => card.id,
321
- entitySlotStoreService: new OIMSlotStoreServiceMapDriven<TOIMEntitySlot<Card, TOIMStoreKey>>(),
322
- columnSlotStoreService: OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue(),
323
- });
282
+ const deliveringCardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>(), slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, TOIMStoreKey>>() });
324
283
  const deliveringCards = deliveringCardService.createStoreSlot(db);
325
284
  ```
326
285
 
327
286
  The entity collection overrides nothing. `createOIMSlotContentEntityService` builds an
328
- `OIMSlotContentEntityService`, whose constructor builds the type's `OIMSlotOwnerService` and hands
329
- it a private content strategy (`createSlot`, `isSlotEmpty`, `releaseSlotContent`,
330
- `hasReference`), which is how the owner asks the content about a slot without the content's write
331
- doors being reachable from the owner. `createOIMSlotCollectionService` then wraps that content
332
- service's owner and store service in an `OIMSlotCollectionService`.
333
-
334
- `keyOf` is the one required option. `entityWriter` falls back to `db.entityWriter`, then to
335
- `OIMDefaults.ENTITY_WRITER`; `slotEmptiness` to `OIMDefaults.ENTITY_SLOT_EMPTINESS`;
336
- `slotReleasePolicy` to `db.slotReleasePolicy`, then to nothing, which is the default of "a slot
337
- with a value stays for good" described under **The injected seams**; the column store to
338
- `OIMDefaults.COLUMN_SLOT_STORE_SERVICE`.
287
+ `OIMSlotContentEntityService`; `createOIMSlotCollectionService` builds an `OIMSlotCollectionService` over it,
288
+ and that constructor builds the type's `OIMSlotOwnerService`. The owner asks the content about a slot
289
+ through `IOIMSlotContentService` (`isSlotEmpty`, `releaseSlotContent`, `hasReference`); the collection
290
+ service mints through the content service's `createSlot`.
291
+
292
+ `keyOf` is the one required option. `entityWriter` falls back to `defaults.entityWriter`, then to
293
+ the in-place writer; `slotEmptinessPolicy` to `defaults.entitySlotEmptinessPolicy`, then to "the entity
294
+ is `undefined`"; `slotReleasePolicy` to `defaults.slotReleasePolicy`, then to nothing, which is the
295
+ default of "a slot with a value stays for good" described under **The injected seams**; the column store
296
+ to `defaults.columnSlotStoreService`, then to a shared map-driven store.
339
297
 
340
298
  A single entity is the same content service under a `void` key, built by
341
- `createOIMSingleEntityService`, and it reports the same way.
299
+ `createOIMSingleSlotService`, and it reports the same way.
342
300
 
343
301
  ---
344
302
 
345
303
  ## Every optional dependency has a default
346
304
 
347
- `OIMDefaults` is where a default that would otherwise be an object lives. It is a
348
- class of static `OIMDefaultValue` fields and nothing else; you never construct one.
305
+ `OIMDefaults` holds the strategies a factory falls back on when its own options leave them out. Build
306
+ one with the strategies you want shared and pass it as the `defaults` option of every factory that
307
+ should use them; a factory given none uses `OIMDefaults.INSTANCE`, which is empty. A field left
308
+ undefined falls back to the built-in default.
349
309
 
350
- | Accessor | Type | What it hands back |
310
+ | Field | Type | Built-in default |
351
311
  |---|---|---|
352
- | `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` |
353
- | `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` |
354
- | `OIMDefaults.KEY_TREE_WALK_SCALAR` | `OIMKeyTreeWalkScalar` | the chain-of-values walk |
355
- | `OIMDefaults.KEY_TREE_WALK_COMPOSITE` | `OIMKeyTreeWalkComposite` | the chain-of-identities walk |
356
- | `OIMDefaults.STANDALONE_SLOT_OWNER` | `OIMSlotOwner` | the owner of a slot nobody filed |
357
- | `OIMDefaults.ENTITY_WRITER` | `IOIMEntityWriter` | `createOIMEntityWriterInPlace()` |
358
- | `OIMDefaults.ENTITY_SLOT_EMPTINESS` | `IOIMSlotEmptiness<TOIMEntitySlot>` | "empty means `slot.entity === undefined`" |
359
- | `OIMDefaults.INDEX_KEYS_SERVICE` | `IOIMIndexKeysService` | an `OIMIndexKeysServiceAppending`; see [Index keys strategy](#index-keys-strategy) |
360
- | `OIMDefaults.STORE_SLOT_LISTENER` | `IOIMStoreSlotListener` | `OIMStoreSlotListenerNone`, which hears nothing. It is not a constructor option: every service reads it when built and calls `onStoreSlotCreated(storeSlot)` for each store slot it creates. Set your own with `setValue` before the first service is built; afterwards `setValue` throws |
361
-
362
- **Nothing is constructed at module load.** Every entry is an `OIMDefaultValue`
363
- whose `getValue()` mints on its first call (`??=`) and hands back the same instance
364
- for the rest of the process, so a consumer that never reaches for a key tree walk
365
- never builds one.
366
-
367
- Each has a matching `setValue(value)`, which is how a process replaces a default
368
- everywhere at once. **`setValue` throws once `getValue` has handed the first one
369
- out**, naming the property: `OIMDefaults.ENTITY_WRITER has already been handed
370
- out`. Whoever took the default keeps it, so a late replacement would leave two
371
- implementations live in one process, with the older one inside every object built
372
- before the call.
373
-
374
- Setting a default before the first read works, and so does setting it twice before then. Each
375
- property is tracked on its own, so calling `OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()` does not close
376
- `OIMDefaults.KEY_TREE_WALK_SCALAR`. Set every default you mean to replace at startup, before
377
- any `OIMDB` or service exists.
378
- `src/factories/tests/what-a-default-replaced-too-late-does.test.ts` pins this.
312
+ | `slotStamp` | `IOIMSlotStamp<TOIMSlot>` | none: no stamp |
313
+ | `slotReleasePolicy` | `IOIMSlotReleasePolicy` | none: a slot with a value stays for good |
314
+ | `entityWriter` | `IOIMEntityWriter` | `createOIMInPlaceEntityWriter()` |
315
+ | `entitySlotEmptinessPolicy` | `IOIMSlotEmptinessPolicy<TOIMEntityContentSlot>` | "empty means `slot.entity === undefined`" |
316
+ | `indexKeysService` | `IOIMIndexKeysService` | an `OIMAppendingIndexKeysService`; see [Index keys strategy](#index-keys-strategy) |
317
+ | `columnSlotStoreService` | `IOIMSlotStoreService<string, TOIMColumnSlot>` | a shared map-driven store service for column slots |
318
+ | `computedColumnSlotStoreService` | `IOIMSlotStoreService<string, TOIMComputedColumnSlot>` | a shared map-driven store service for the column slots of computed services |
319
+ | `storeSlotListener` | `IOIMStoreSlotListener` | `OIMNoStoreSlotListener`, which hears nothing; a service calls `onStoreSlotCreated(storeSlot)` for each store slot it creates |
320
+
321
+ ```ts
322
+ const replacingDefaults = new OIMDefaults({ entityWriter: createOIMReplacingEntityWriter() });
323
+ const replacingCardService = createOIMSlotCollectionService({
324
+ defaults: replacingDefaults,
325
+ slotContentService: createOIMSlotContentEntityService<Card>({ defaults: replacingDefaults }),
326
+ });
327
+ ```
328
+
329
+ **The built-in defaults cost nothing until used.** Each is an `OIMDefaultValue` whose `getValue()`
330
+ builds it on its first call and hands back the same instance for the rest of the process, and
331
+ `OIMDefaults` itself imports none of them, so a bundle carries only the defaults its factories read.
332
+ `src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts` pins which door reads which.
379
333
 
380
334
  **One module names every default, so calling one `getValue()` retains all of them.** A
381
335
  service drags the map-driven store service and both key tree walks into a
@@ -397,20 +351,20 @@ const card: Card = { id: 'c1', deck: 'spades', title: 'Ace', rank: 1 };
397
351
  const key = 'c1';
398
352
  const body = (): void => {};
399
353
 
400
- cardService.slotContentService.upsertEntity(cards, card); // write: the value is the reference
401
- cardService.slotContentService.insertEntity(cards, key, card); // write: the same door, by key
354
+ cardService.writeSlotContentByKey(cards, card.id, card); // write: the value is the reference
355
+ cardService.writeSlotContentByKey(cards, key, card); // write: the same door, by key
402
356
  const entitySlot = cardService.holdSlotByKey(cards, key); // hold: a counted reference; give it back with entitySlot.owner.unholdSlot(entitySlot)
403
- effectService.createEffect(db, [entitySlot], [], body); // subscribe: the effect is the reference
404
- const rankColumn = cardService.holdColumn(entitySlot, 'rank'); // hold, on a column
405
- cardService.unholdColumn(rankColumn); // give the column back
357
+ effectService.createEffect(db, body, [entitySlot]); // subscribe: the effect is the reference
358
+ const rankColumn = cardService.columnSlotService.holdColumn(entitySlot, 'rank'); // hold, on a column
359
+ cardService.columnSlotService.unholdColumn(rankColumn); // give the column back
406
360
  cardService.getSlotByKey(cards, key); // read: the collector's hold is the reference
407
361
  ```
408
362
 
409
363
  **The read that does not mint is `findSlotByKey(storeSlot, key)`.** It answers `undefined`
410
364
  for a key with nothing filed, which is how a caller changes or removes a key without minting
411
- it: `const s = cardService.findSlotByKey(cards, key); if (s !== undefined) cardService.slotContentService.deleteEntity(s);`.
365
+ it: `const s = cardService.findSlotByKey(cards, key); if (s !== undefined) cardService.slotContentService.clearSlotContent(s);`.
412
366
  It hands back only what is already filed and leaves no reference behind, so it is not a door
413
- that mints. `cardService.slotContentService.findEntityByKey(storeSlot, key)` and
367
+ that mints. `cardService.slotContentService.findSlotByKey(storeSlot, key)` and
414
368
  `cardService.hasSlotContentByKey(storeSlot, key)` mint nothing either.
415
369
 
416
370
  **`getSlotByKey(storeSlot, key)` is the fourth door, and the collector is what admits it.** It
@@ -418,20 +372,20 @@ mints what is missing and hands the slot to the unused-slot collector of the sto
418
372
  (`OIMUnusedSlotCollectorService.INSTANCE.holdSlotUntilFlush(storeSlot.channelSlot.collectorSlot.unusedSlotCollectorSlot, slot)`),
419
373
  which takes the hold on the caller's behalf and gives it back at the next `flush()`.
420
374
  The reference is still left behind, but the collector owns it instead of the
421
- caller, which makes it safe to use and safe to forget. `insertEntity` relies on the same
375
+ caller, which makes it safe to use and safe to forget. `writeSlotContentByKey` relies on the same
422
376
  window when the content it wrote landed empty: the
423
377
  mint is tracked, so the identity it handed back stands until the collector gives
424
378
  it back, and goes then if nothing took it.
425
379
 
426
380
  **The write door also does not file a key whose content is refused.** On a miss,
427
- `insertEntity` mints, writes inside a `try`, and on a throw runs
381
+ `writeSlotContentByKey` mints, writes inside a `try`, and on a throw runs
428
382
  the release guard on what it just minted before re-raising, so the caller's error
429
383
  is the only thing that leaves. *Filing first and writing second is the obvious
430
- alternative, and it is wrong. One `upsertEntity` of an unseen key from inside a
384
+ alternative, and it is wrong. One `writeSlotContentByKey` of an unseen key from inside a
431
385
  subscriber body (the service's write opens with the frame guard)
432
386
  would leave the key filed holding nothing, with nothing in a position to let it go.
433
- `getSlotByKey` would answer a slot with no value, `insertEntities` would refuse for the life of the
434
- store slot, and nothing would throw except the refusal the caller had asked for.* The
387
+ `getSlotByKey` would answer a slot with no value, and nothing would throw except the refusal the
388
+ caller had asked for.* The
435
389
  unwind is the release guard rather than a `store.remove` because the write may
436
390
  have half landed, and the hit path never mints and stays outside the guard.
437
391
 
@@ -442,15 +396,10 @@ happen and the content was already there. The door completed and the caller is
442
396
  handed the identity. Whether an identity that nothing keeps is worth keeping is
443
397
  the owner's question, asked through its release guard.
444
398
 
445
- So `insertEntity` is **total**: it always hands back the identity it resolved or
399
+ So `writeSlotContentByKey` is **total**: it always hands back the identity it resolved or
446
400
  minted, whatever it decided about the content, because a door that mints must
447
401
  leave a reference behind.
448
402
 
449
- `fillEntities(storeSlot, keys, entities)` is the companion and does not carry that rule, because it returns
450
- `void` and hands out no reference at all. On a throw it runs the release guard
451
- over everything it filed and keeps only what something holds. This is bounded by the
452
- precondition that the store slot was empty when the call began.
453
-
454
403
  A fifth door is admitted by this test and by nothing else: the act has to leave
455
404
  a reference behind that nothing can take away under its caller. A helper over an
456
405
  existing door does not qualify. It is a **second spelling** of one, and two
@@ -465,9 +414,9 @@ A reference is given back by whoever took it. The terms are:
465
414
  | `holdCount` | `owner.unholdSlot(slot)`, or `unholdColumn(columnSlot)` for a column; the unused-slot collector's `flush` for a `getSlotByKey`; `OIMEffectService.INSTANCE.disposeEffect(effect)`, for the hold an effect takes on each root the caller named |
466
415
  | `effects` | `OIMEffectService.INSTANCE.disposeEffect(effect)` |
467
416
  | a held column | the last column effect being disposed, through the column's owner (`slot.owner.unsubscribeFromSlot`) |
468
- | the value itself | `entityService.slotContentService.deleteEntity(slot)` |
469
- | a slot in a bucket | `bucketService.slotContentService.clearBucketMemberSlots` / `writeBucketMemberSlots` / a positional removal |
470
- | a composite-key edge | `OIMKeyTreeWalkComposite.deleteKey` unlinking that composite key part |
417
+ | the value itself | `entityService.slotContentService.clearSlotContent(slot)` |
418
+ | a slot in a bucket | `bucketService.slotContentService.clearSlotContent` / `writeSlotContent` / a positional removal |
419
+ | a composite-key edge | `OIMCompositeKeyTreeWalk.deleteKey` unlinking that composite key part |
471
420
 
472
421
  When the last of them goes, the identity's **owner** unfiles it. There is no
473
422
  release API and no owner that a caller assigns. `releaseSlotIfUnheld(slot)` is **public**
@@ -492,46 +441,40 @@ own call. See [Ending the channel whole](#ending-the-channel-whole).
492
441
  ### A service composes
493
442
 
494
443
  Every collection is three parts: a **store service** (`IOIMSlotStoreService`: the slots by
495
- key, in the store each store slot carries; `OIMSlotStoreServiceMapDriven`,
496
- `OIMSlotStoreServiceTreeDriven`, `OIMSlotStoreServiceSingleSlotDriven`), a **slot owner**
444
+ key, in the store each store slot carries; `OIMSlotMapStoreService`,
445
+ `OIMSlotTreeStoreService`, `OIMSingleSlotStoreService`), a **slot owner**
497
446
  (`OIMSlotOwnerService`, an `OIMSlotOwner`: the owner of every slot it filed, with holds, release,
498
- emptiness, the channel's subframes and the column owner) and a **content service**
447
+ emptiness and the column owner) and a **content service**
499
448
  (`OIMSlotContentEntityService`, an `OIMSlotContentBucketService*`, or a computed's
500
449
  `OIMComputedValueService`), with the collection service (`OIMSlotCollectionService` /
501
- `OIMSingleSlotService`) lending the first two their by-key doors. The owner asks the content
502
- about a slot through a content strategy the content service hands it, never through an override:
450
+ `OIMSingleSlotService`) building the owner and lending it the by-key doors. The owner asks the content
451
+ about a slot through `IOIMSlotContentService`, never through an override:
503
452
 
504
- | Strategy member | Called by the owner when |
453
+ | Member | Called when |
505
454
  |---|---|
506
- | `createSlot(key, storeSlot, slotOwnerService)` | a key is minted |
455
+ | `createSlot(key, storeSlot, slotOwner)` / `createSingleSlot(storeSlot, slotOwner)` | a key (or a single) is minted, by the collection service |
507
456
  | `isSlotEmpty(slot)` | emptiness is settled after a write |
508
- | `releaseSlotContent(slot, slotOwnerService)` | the identity is going. Give back what its content held |
457
+ | `releaseSlotContent(slot)` | the identity is going. Give back what its content held. Throws on a held slot |
509
458
  | `hasReference(slot)` | something outside `holdCount` and `effects` may keep it |
510
459
 
511
- The strategy is private to the content service: the owner holds it, the caller never sees it.
512
- An entity content service builds its owner itself, in its constructor, so the owner and the
513
- content are fixed together once per type; a bucket content service hands a factory
514
- (`createOIMBucketService`) its owner through `createSlotOwnerService`.
515
-
516
- The entity content service is the example to follow in the package: it supplies the content
517
- answers, the owner unfiles through the store service, and every write door opens the channel's
518
- content subframe around it.
460
+ The content service's write doors reach the owner through `slot.owner`; the owner files and unfiles
461
+ through the store service.
519
462
 
520
463
  ### The injected seams
521
464
 
522
465
  ```typescript signature
523
- interface IOIMSlotEmptiness<TSlot> { isEmpty(slot: TSlot): boolean; }
466
+ interface IOIMSlotEmptinessPolicy<TSlot> { isEmpty(slot: TSlot): boolean; }
524
467
 
525
- interface IOIMSlotStamp<TSlot> { stamp(slotEditable: TOIMSlotEditable<TSlot>): void; }
468
+ interface IOIMSlotStamp<TSlot> { stamp(editableSlot: TOIMEditable<TSlot>): void; }
526
469
  ```
527
470
 
528
- **`IOIMSlotEmptiness`** decides what counts as empty. The answer is written once
471
+ **`IOIMSlotEmptinessPolicy`** decides what counts as empty. The answer is written once
529
472
  per write into the EMPTY flag of `slot.slotFlags` and never recomputed at a read. An entity content
530
- service defaults to "the entity is `undefined`" (`OIMDefaults.ENTITY_SLOT_EMPTINESS`); a bucket
473
+ service defaults to "the entity is `undefined`" (or `defaults.entitySlotEmptinessPolicy`); a bucket
531
474
  content service answers "`getBucketMemberSlotCount(bucketSlot) === 0`"; a computed answers
532
475
  `false`.
533
476
 
534
- Emptiness decides lifetime, never the write path. An `upsertEntity` onto an entity slot that
477
+ Emptiness decides lifetime, never the write path. An `writeSlotContentByKey` onto an entity slot that
535
478
  still carries an entity merges into it even when emptiness calls it empty (a soft
536
479
  delete, a seed), so a column that the new entity drops is woken and an unchanged one is
537
480
  not. Only a slot with no entity at all is filled whole.
@@ -541,10 +484,10 @@ settled, and it chains: each stamp takes the `next` one and calls it first. Use
541
484
  for stamps that have nothing to do with release; release has its own policy.
542
485
 
543
486
  **`IOIMSlotReleasePolicy`** decides whether and when a slot goes, and does no
544
- work itself. A door and `OIMDB` take ONE through `slotReleasePolicy`; a door given none takes
545
- `db.slotReleasePolicy`. With neither, the slot owner is built with `OIMSlotReleasePolicyAtEmpty`:
487
+ work itself. A door and an `OIMDefaults` take ONE through `slotReleasePolicy`; a door given none takes
488
+ `defaults.slotReleasePolicy`. With neither, the slot owner is built with `OIMSlotAtEmptyReleasePolicy`:
546
489
  a slot with a value stays for good. A computed service defaults to
547
- `OIMSlotReleasePolicyAtUnhold`. Two readonly flags on the policy, `releasesWrittenSlots` and
490
+ `OIMSlotAtUnholdReleasePolicy`. Two readonly flags on the policy, `releasesWrittenSlots` and
548
491
  `schedulesReleaseOnWrite`, are read once by the owner at construction, so a policy that never
549
492
  releases on a write costs a write nothing.
550
493
 
@@ -553,50 +496,50 @@ policy moves only its own. `-1` means unset.
553
496
 
554
497
  | Marker | Moved by | Not moved by |
555
498
  |---|---|---|
556
- | `releaseDueAfterWriteMs` | every write, under `OIMSlotReleasePolicyAfterWrite` | a subscriber arriving or leaving |
557
- | `releaseDueAfterIdleMs` | the first content of a slot with no subscriber, and the departure of the last subscriber, under `OIMSlotReleasePolicyAfterIdle` | a later write |
558
- | `releaseDueAfterUnholdMs` | every last unhold, under `OIMSlotReleasePolicyAfterUnhold` | a write |
499
+ | `releaseDueAfterWriteMs` | every write, under `OIMSlotAfterWriteReleasePolicy` | a subscriber arriving or leaving |
500
+ | `releaseDueAfterIdleMs` | the first content of a slot with no subscriber, and the departure of the last subscriber, under `OIMSlotAfterIdleReleasePolicy` | a later write |
501
+ | `releaseDueAfterUnholdMs` | every last unhold, under `OIMSlotAfterUnholdReleasePolicy` | a write |
559
502
 
560
503
  A slot goes on the SOONEST of those that are set, and a hold outranks them all.
561
504
  **One number cannot carry both.** A write arriving after the subscriber left
562
505
  would overwrite the idle policy's verdict, and the last subscriber leaving after a
563
506
  write would overwrite the write policy's. Both directions are pinned by
564
- `src/engine/collector/tests/which-release-marker-each-collector-may-move.test.ts`.
507
+ `src/collector/tests/which-release-marker-each-collector-may-move.test.ts`.
565
508
 
566
509
  **The POLICY answers `isSlotReleasable(slot)`**, and only for a slot that is not
567
510
  empty and has nothing left keeping it (no holds, no references). `true` releases
568
511
  the slot now. `false` keeps it, and a policy with a finite deadline has already
569
512
  filed the slot into its own release queue; a slot whose marker is `-1` is kept
570
- with nothing scheduled. `OIMSlotReleasePolicyAtUnhold` always answers `true`, and
571
- `OIMSlotReleasePolicyEarliest` asks every member and answers `true` if any member does.
513
+ with nothing scheduled. `OIMSlotAtUnholdReleasePolicy` always answers `true`, and
514
+ `OIMSlotEarliestReleasePolicy` asks every member and answers `true` if any member does.
572
515
  An **empty** slot is asked about through `isEmptySlotReleasable(slot)`: every
573
- policy except `OIMSlotReleasePolicyNever` answers `true`. A service given no
574
- policy is built with `OIMSlotReleasePolicyAtEmpty`: it keeps a non-empty slot and
516
+ policy except `OIMSlotNeverReleasePolicy` answers `true`. A service given no
517
+ policy is built with `OIMSlotAtEmptyReleasePolicy`: it keeps a non-empty slot and
575
518
  lets an empty one go, and no path branches on a missing policy. The owner has no
576
519
  "how long is left" question.
577
520
 
578
521
  ### Releasing an identity on a clock — opt-in, and off it costs nothing
579
522
 
580
- Four policies ship, plus `OIMSlotReleasePolicyEarliest`, which combines them. The difference between
523
+ Four policies ship, plus `OIMSlotEarliestReleasePolicy`, which combines them. The difference between
581
524
  them is WHEN a slot goes. There is no flag, and no branch on the sweep path.
582
525
 
583
526
  | Policy | A slot goes |
584
527
  |---|---|
585
- | `new OIMSlotReleasePolicyAfterWrite({ keepAfterWriteMs })` | once its last write is older than the window |
586
- | `new OIMSlotReleasePolicyAfterIdle({ keepAfterIdleMs })` | once nothing has watched it for the window |
587
- | `new OIMSlotReleasePolicyAfterUnhold({ keepAfterUnholdMs })` | once the window after its LAST unhold has passed; a hold taken again inside it keeps the slot |
588
- | `OIMSlotReleasePolicyAtUnhold.INSTANCE` | at the last unhold, even with a value |
589
- | `OIMSlotReleasePolicyAtEmpty.INSTANCE` | once it has no value and nothing holds it; a computed, never empty, stays while its owner lives |
590
- | `OIMSlotReleasePolicyNever.INSTANCE` | never, empty or not; a deleted key's slot stays filed |
591
- | `new OIMSlotReleasePolicyEarliest([...])` | when the earliest of several deadlines falls due — a `min` over deadlines |
592
-
593
- A policy holds only its window. The clock, the timer and the queues are the `db`'s: each `OIMDB`
594
- has a `collectorSlot` with one release collector slot per windowed policy
595
- (`afterWriteReleaseSlotCollectorSlot`, `afterIdleReleaseSlotCollectorSlot`,
596
- `afterUnholdReleaseSlotCollectorSlot`), built from `new OIMDB({ getNowMs, releaseTimer, errorPolicy })`.
528
+ | `new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs, slotReleaseDueFramePolicy? })` | once its last write is older than the window; with `OIMSlotReleaseManualDueFramePolicy`, only writes inside its open frame count |
529
+ | `new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs })` | once nothing has watched it for the window |
530
+ | `new OIMSlotAfterUnholdReleasePolicy({ keepAfterUnholdMs })` | once the window after its LAST unhold has passed; a hold taken again inside it keeps the slot |
531
+ | `OIMSlotAtUnholdReleasePolicy.INSTANCE` | at the last unhold, even with a value |
532
+ | `OIMSlotAtEmptyReleasePolicy.INSTANCE` | once it has no value and nothing holds it; a computed, never empty, stays while its owner lives |
533
+ | `OIMSlotNeverReleasePolicy.INSTANCE` | never, empty or not; a deleted key's slot stays filed |
534
+ | `new OIMSlotEarliestReleasePolicy([...])` | when the earliest of several deadlines falls due — a `min` over deadlines |
535
+
536
+ A policy holds only its window. The clock, the timer and the wheels are the `db`'s: each `OIMDB`
537
+ has a `collectorSlot` with one release wheel per windowed policy
538
+ (`afterWriteSlotReleaseWheel`, `afterIdleSlotReleaseWheel`,
539
+ `afterUnholdSlotReleaseWheel`), built from `new OIMDB({ getNowMs, releaseTimer, errorPolicy })`.
597
540
  A policy reaches the one for a slot through the slot's channel
598
541
  (`slot.storeSlot.channelSlot.collectorSlot`). To control time in tests, pass `getNowMs` and
599
- `releaseTimer` to the `OIMDB`. `OIMSlotReleasePolicyAtUnhold` is the zero window and touches no
542
+ `releaseTimer` to the `OIMDB`. `OIMSlotAtUnholdReleasePolicy` is the zero window and touches no
600
543
  collector.
601
544
 
602
545
  ```typescript signature
@@ -619,10 +562,10 @@ leaves at the end of the frame. A window above zero is filed in that policy's ow
619
562
  queue.
620
563
 
621
564
  ```typescript
622
- const releasingDb = new OIMDB({
623
- slotReleasePolicy: new OIMSlotReleasePolicyEarliest([
624
- new OIMSlotReleasePolicyAfterWrite({ keepAfterWriteMs: 60_000 }),
625
- new OIMSlotReleasePolicyAfterIdle({ keepAfterIdleMs: 30_000 }),
565
+ const releasingDefaults = new OIMDefaults({
566
+ slotReleasePolicy: new OIMSlotEarliestReleasePolicy([
567
+ new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: 60_000 }),
568
+ new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
626
569
  ]),
627
570
  });
628
571
  ```
@@ -637,7 +580,7 @@ const releasingDb = new OIMDB({
637
580
  the `db`'s `collectorSlot`, and each slot carries its own queue and its own timer: two queues,
638
581
  two timers.
639
582
  Whichever deadline falls due first takes the slot, and nothing has to be
640
- reconciled. `OIMSlotReleasePolicyEarliest` answers a `min` over its members'
583
+ reconciled. `OIMSlotEarliestReleasePolicy` answers a `min` over its members'
641
584
  deadlines, so a sweep of either queue reaches the same verdict.
642
585
 
643
586
  ### When a window opens, and when the clock is not running at all
@@ -650,7 +593,7 @@ A window opens in **exactly two places**:
650
593
  2. **when the LAST SUBSCRIBER leaves**: `OIMSlotOwner.unsubscribeFromSlot` takes the
651
594
  last entry out of `slot.effects` and runs the idle step, which opens a window.
652
595
 
653
- Both ask the policy's `onSlotIdle(slot)`. Under `OIMSlotReleasePolicyAfterIdle`
596
+ Both ask the policy's `onSlotIdle(slot)`. Under `OIMSlotAfterIdleReleasePolicy`
654
597
  it stamps `releaseDueAfterIdleMs = getNowMs() + keepAfterIdleMs` and files the slot
655
598
  into the policy's queue. An **empty** slot gets no window, and the release guard
656
599
  takes it without asking a clock.
@@ -677,7 +620,7 @@ does not re-file it. Two consequences follow:
677
620
  that those entities' own expired windows already answer for, so the entities go in the
678
621
  same sweep.
679
622
 
680
- Pinned by `src/engine/collector/tests/what-a-stale-index-takes-with-it.test.ts`,
623
+ Pinned by `src/collector/tests/what-a-stale-index-takes-with-it.test.ts`,
681
624
  `…/what-an-identity-nobody-subscribed-to-costs-after-a-while.test.ts` and
682
625
  `…/what-a-subscribed-row-costs-the-release-clock.test.ts`.
683
626
 
@@ -690,22 +633,21 @@ identity that holds nothing, and a held one never reaches the question at all.
690
633
  ### The collectors
691
634
 
692
635
  A release policy decides; the collector under it does the work of waiting and
693
- sweeping. A collector is a stateless service over a collector slot, and every `OIMDB` builds its
694
- slots once, in `db.collectorSlot`:
636
+ sweeping. Every `OIMDB` builds its collectors once, in `db.collectorSlot`:
695
637
 
696
638
  ```typescript signature
697
639
  new OIMDB({ getNowMs?, releaseTimer?, errorPolicy? }) // getNowMs defaults to Date.now, releaseTimer to an unref'd setTimeout
698
640
  db.collectorSlot.unusedSlotCollectorSlot // OIMUnusedSlotCollectorService.INSTANCE
699
- db.collectorSlot.afterWriteReleaseSlotCollectorSlot // OIMReleaseSlotCollectorServiceAfterWrite.INSTANCE
700
- db.collectorSlot.afterIdleReleaseSlotCollectorSlot // OIMReleaseSlotCollectorServiceAfterIdle.INSTANCE
701
- db.collectorSlot.afterUnholdReleaseSlotCollectorSlot // OIMReleaseSlotCollectorServiceAfterUnhold.INSTANCE
641
+ oimMintAfterWriteSlotReleaseWheel(db.collectorSlot) // an OIMSlotReleaseWheel, created on first use
642
+ oimMintAfterIdleSlotReleaseWheel(db.collectorSlot) // likewise
643
+ oimMintAfterUnholdSlotReleaseWheel(db.collectorSlot) // likewise
702
644
  ```
703
645
 
704
- A release that throws inside a `flush(releaseSlotCollectorSlot)` called by hand throws to that
705
- caller. A release that throws on the collector's own timer tick has no caller, so it goes to the
706
- collector slot's `errorPolicy`, which is the `errorPolicy` the `OIMDB` was built with.
646
+ A release that throws inside a `flush()` called by hand throws to that caller. A release that
647
+ throws on the wheel's own timer tick has no caller, so it goes to the wheel's `errorPolicy`, which
648
+ is the `errorPolicy` the `OIMDB` was built with.
707
649
 
708
- Every collector extends `OIMFlushSchedulerService`, which owns the schedule and cancel logic around
650
+ Every wheel extends `OIMFlushSchedulerService`, which owns the schedule and cancel logic around
709
651
  a `TOIMTimer = (run: () => void, inMs: number) => TOIMDisposer`, keeping the standing run on the
710
652
  slot it schedules. Protected `schedule(slot, delayMs)` does nothing when a scheduled run is already
711
653
  standing. A callback that is no longer the one the base handed the timer (a run the host could not
@@ -715,29 +657,29 @@ once per schedule, and cancel and re-schedule only when a shorter window arrives
715
657
  `releaseTimer` to the `OIMDB` to drive the collectors off a frame callback, a worker message or a
716
658
  test clock.
717
659
 
718
- **`OIMReleaseSlotCollectorService`** is the abstract clock the three windowed collectors extend.
719
- Its slot (`TOIMReleaseSlotCollectorSlot`) carries the queue, `getNowMs` and the `errorPolicy`. It
720
- sweeps the queue on `flush(slot)` and re-schedules to the next deadline;
721
- `cancelAllSlotReleases(slot)` cancels and empties the queue, and the next scheduled release
722
- schedules it again. The one thing a subclass supplies is `getReleaseDueMs(slot)`, the marker its
723
- policy answers for. The default timer calls `unref` where the platform has it, so an idle process
724
- is never held open.
660
+ **`OIMSlotWheel`** is the abstract clock: its slices, timer, `getNowMs` and `errorPolicy` are fields
661
+ of the object. It sweeps on `flush()` and re-schedules to the next deadline; `cancelAllSlots()`
662
+ takes the timer down and drops every filing, and the next filing arms it again. The default timer
663
+ calls `unref` where the platform has it, so an idle process is never held open.
725
664
 
726
- **`OIMReleaseSlotQueueService`** files slots into **time slices**, not one entry each.
665
+ **`OIMSlotReleaseWheel`** is the wheel a release policy files into. Which marker it reads and writes
666
+ (`getReleaseDueMs`, `setReleaseDueMs`) and which bit of `slot.slotFlags` says a slot is on it are
667
+ given at construction. It files slots into **time slices**, not one entry each.
727
668
  `oimSliceOfMs(ms)` is `Math.floor(ms / OIM_SLICE_LENGTH_MS)` and `OIM_SLICE_LENGTH_MS`
728
- is `1024`, so every slot due inside the same ~1s bucket shares one `Set` and one
729
- timer. Its doors take the release collector slot and the collector whose marker they read, so
730
- two queues over two markers stay independent: `scheduleSlotRelease`, `cancelSlotRelease`,
731
- `isSlotReleaseScheduled`, `getNextReleaseSliceNumber`, `releaseDueUpToSliceNumber` and
732
- `cancelAllSlotReleases`. `scheduleSlotRelease` reads the marker and files at it, so a filing can
669
+ is `1024`, so every slot due inside the same ~1s bucket shares one array and one
670
+ timer. Filing is a push and a bit, and cancelling clears the bit and leaves the entry for the
671
+ sweep to skip. Its doors are `markSlotReleaseDue`, `scheduleSlotRelease`, `isSlotReleasable`,
672
+ `cancelSlotRelease`, `isSlotReleaseScheduled`, `getNextSliceNumber`, `sweepUpToSliceNumber`,
673
+ `flush` and `cancelAllSlots`. `scheduleSlotRelease` reads the marker and files at it, so a filing can
733
674
  neither shorten nor extend a deadline, and a marker of `-1` files nothing.
734
675
 
735
- **`releaseDueUpToSliceNumber` is where the release decision is taken, and it belongs to
736
- the queue, not the owner.** For each slot in a due slice it skips a released one,
676
+ **The sweep is where the release decision is taken, and it belongs to the wheel, not the owner.**
677
+ For each slot in a due slice it skips a stale entry and a released slot,
737
678
  reads its own marker, drops the slot on `-1`, **re-files it itself** into
738
679
  `oimSliceOfMs(getReleaseDueMs)` when that moment is still ahead, and otherwise calls
739
680
  `slot.owner.releaseSlotIfUnheld(slot)`, which asks the policy's `isSlotReleasable`
740
- and so declines while the other policy still keeps the slot. The
681
+ and so declines while the other policy still keeps the slot. `markSlotReleaseDueNow(slot)`
682
+ invalidates: the marker becomes now, and a slot nothing holds or watches goes at once. The
741
683
  owner files nothing from inside a sweep; it opens a window only on its own events:
742
684
  a mint, and the departure of the last subscriber. The sweep raises a floor for its
743
685
  duration, so any re-file lands in the next slice and cannot loop.
@@ -769,7 +711,7 @@ gives them back when released, so the arena holds the top and the chain holds
769
711
  itself. Use one arena per screen, per component or per job: a scope's cache pins what
770
712
  that scope read and nothing else.
771
713
 
772
- Pinned by `src/engine/collector/tests/what-a-named-arena-keeps-cached.test.ts`.
714
+ Pinned by `src/collector/tests/what-a-named-arena-keeps-cached.test.ts`.
773
715
 
774
716
  Neither policy means *unread*: no read is reported to the clock. A subscriber
775
717
  never loses data mid-read, because an effect holds every root it reaches, so the
@@ -795,25 +737,25 @@ a store slot that holds one slot under no key, or many, a store slot that files
795
737
 
796
738
  | kind | single | collection |
797
739
  |---|---|---|
798
- | entity | `createOIMSingleEntityService<TEntity>(db, …)`, then `createStoreSlot(db)` | `createOIMEntityService<TEntity>(db, { keyOf, … })`, then `createStoreSlot(db)` |
799
- | bucket | `createOIMSingleBucketService<TMember>(db, { slotContentService?, … })`, then `createStoreSlot(db)` | `createOIMBucketService<TMember>(db, { slotContentService?, … })`, then `createStoreSlot(db)` |
800
- | computed | `createOIMComputedService<void, TValue, TOIMSingleSlotStore<TOIMComputedSlot<TValue, void>>>({ computedSlotStoreService: new OIMSlotStoreServiceSingleSlotDriven<…>(), … })`, then `createComputedStoreSlot(db, derive?, createSources?)` | `createOIMComputedService<TKey, TValue>({ … })`, then `createComputedStoreSlot(db, derive?, createSources?)` |
740
+ | entity | `createOIMSingleSlotService<TEntity>(…)`, then `createStoreSlot(db)` | `createOIMSlotCollectionService<TEntity>({ keyOf, … })`, then `createStoreSlot(db)` |
741
+ | bucket | `createOIMSingleSlotService<TMember>({ slotContentService?, … })`, then `createStoreSlot(db)` | `createOIMSlotCollectionService<TMember>({ slotContentService?, … })`, then `createStoreSlot(db)` |
742
+ | computed | `createOIMComputedService<void, TValue, TOIMSingleSlotStore<TOIMComputedSlot<TValue, void>>>({ computedSlotStoreService: new OIMSingleSlotStoreService<…>(), … })`, then `createComputedStoreSlot(db, derive?, createSources?)` | `createOIMComputedService<TKey, TValue>({ … })`, then `createComputedStoreSlot(db, derive?, createSources?)` |
801
743
 
802
744
  A single is not a second content API. Its content service is the same class with `TKey = void`,
803
- over the slot store service `OIMSlotStoreServiceSingleSlotDriven`, whose store,
745
+ over the slot store service `OIMSingleSlotStoreService`, whose store,
804
746
  `TOIMSingleSlotStore<TSlot>`, is `{ slot: TSlot | undefined }`; its store slot is a
805
747
  `TOIMSingleSlotStoreSlot`. The collection half is `OIMSingleSlotService` instead of
806
748
  `OIMSlotCollectionService`, and it has no key in any door: `getSingleSlot(storeSlot)`,
807
749
  `holdSingleSlot(storeSlot)` (given back with `slot.owner.unholdSlot(slot)`),
808
750
  `findSingleSlot(storeSlot)`, which mints nothing, `hasSingleSlotContent(storeSlot)`, and the same
809
751
  column doors as a collection. The content doors are the collection's:
810
- `singleService.slotContentService.upsertEntity(storeSlot, entity)`,
811
- `singleService.slotContentService.findEntityByKey(storeSlot, undefined)`. The single entity
752
+ `singleService.slotContentService.writeSlotContentByKey(storeSlot, entity)`,
753
+ `singleService.slotContentService.findSlotByKey(storeSlot, undefined)`. The single entity
812
754
  door builds its content service with `keyOf: () => undefined` and the single-slot store.
813
755
 
814
756
  A single behaves as a collection of at most one slot, and invents nothing of its own.
815
757
  `createStoreSlot(db)` mints no slot: the first get or hold mints it, exactly as in a
816
- collection. An entity single's first value arrives through `upsertEntity`, an ordinary write
758
+ collection. An entity single's first value arrives through `writeSlotContentByKey`, an ordinary write
817
759
  that is announced to whoever subscribed. Its slot is released like any slot of its service,
818
760
  under the service's release policy, and the next get mints a fresh one. A store slot is not
819
761
  a slot: it has no owner, no holds and no effects, so never hold or subscribe it, and never
@@ -829,14 +771,14 @@ are reached through its `computedCollectionService` (an `OIMSlotCollectionServic
829
771
  `slot.computedValueService.getValueForSlot(slot)`, and a hold is given back with
830
772
  `slot.owner.unholdSlot(slot)`.
831
773
 
832
- A single bucket is built like a keyed one, with `createOIMSingleBucketService`.
774
+ A single bucket is built like a keyed one, with `createOIMSingleSlotService`.
833
775
  `singleBucketService.getSingleSlot(storeSlot)` is the `TOIMBucketSlot` to subscribe effects to
834
776
  or hand to a reader. The member doors live on its `slotContentService` and take the bucket slot
835
777
  first: `getBucketMemberSlots(bucketSlot)`, `getBucketMemberSlotCount(bucketSlot)`,
836
778
  `hasBucketMemberSlot(bucketSlot, bucketMemberSlot)`,
837
779
  `addBucketMemberSlot(s)(bucketSlot, …)`, `removeBucketMemberSlot(s)(bucketSlot, …)`,
838
- `writeBucketMemberSlots(bucketSlot, bucketMemberSlots): boolean`,
839
- `clearBucketMemberSlots(bucketSlot)`, `fillBucketMemberSlots`, `markContentChanged(bucketSlot)` and
780
+ `writeSlotContent(bucketSlot, bucketMemberSlots): boolean`,
781
+ `clearSlotContent(bucketSlot)`, `fillBucketMemberSlots`, `markContentChanged(bucketSlot)` and
840
782
  `markBucketColumnsChanged(bucketSlot, patch, changedColumns)`. The array-driven content service
841
783
  adds `getBucketMemberSlotAt(bucketSlot, index)`,
842
784
  `getBucketMemberSlotPosition(bucketSlot, bucketMemberSlot)`, `insertBucketMemberSlotAt`,
@@ -857,13 +799,13 @@ argument, not a subclass.
857
799
 
858
800
  ```typescript signature
859
801
  // A scalar key: string | number.
860
- new OIMSlotStoreServiceMapDriven<TOIMEntitySlot<Card, TOIMStoreKey>>()
802
+ new OIMSlotMapStoreService<TOIMEntitySlot<Card, TOIMStoreKey>>()
861
803
 
862
804
  // A composite key of VALUES, descended one composite key part at a time.
863
- new OIMSlotStoreServiceTreeDriven<TOIMStoreKey, TOIMKeyedSlot<readonly TOIMStoreKey[]>>(OIMDefaults.KEY_TREE_WALK_SCALAR.getValue())
805
+ new OIMSlotTreeStoreService<TOIMStoreKey, TOIMKeyedSlot<readonly TOIMStoreKey[]>>(OIMScalarKeyTreeWalk.INSTANCE)
864
806
 
865
807
  // A composite key of IDENTITIES, descended the same way — and holding each.
866
- new OIMSlotStoreServiceTreeDriven<TOIMEntitySlot, TOIMKeyedSlot<readonly TOIMEntitySlot[]>>(OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue())
808
+ new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMKeyedSlot<readonly TOIMEntitySlot[]>>(OIMCompositeKeyTreeWalk.INSTANCE)
867
809
  ```
868
810
 
869
811
  There are two stores, one per shape of address, and no store is ever asked at run time
@@ -885,15 +827,11 @@ type TScalarChain = readonly TOIMStoreKey[];
885
827
  type Tally = { deck: string; day: string; count: number };
886
828
  type TTallyStore = TOIMSlotStoreTree<TOIMEntitySlot<Tally, TScalarChain>, TOIMStoreKey>;
887
829
 
888
- const tallyService = createOIMEntityService<Tally, TOIMEntityColumnName<Tally>, TScalarChain, TTallyStore>(db, {
889
- entitySlotStoreService:
890
- new OIMSlotStoreServiceTreeDriven<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
891
- OIMDefaults.KEY_TREE_WALK_SCALAR.getValue(),
892
- ),
893
- keyOf: (t) => [t.deck, t.day], // the two names, in order
894
- });
830
+ const tallyService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(), slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
831
+ OIMScalarKeyTreeWalk.INSTANCE,
832
+ ) });
895
833
  const tallies = tallyService.createStoreSlot(db);
896
- tallyService.slotContentService.upsertEntity(tallies, { deck: 'spades', day: 'mon', count: 1 });
834
+ tallyService.writeSlotContentByKey(tallies, ((t) => [t.deck, t.day])({ deck: 'spades', day: 'mon', count: 1 }), { deck: 'spades', day: 'mon', count: 1 });
897
835
  tallyService.getSlotByKey(tallies, ['spades', 'mon']);
898
836
  ```
899
837
 
@@ -912,28 +850,24 @@ type Enrolment = {
912
850
  };
913
851
  type TChain = readonly TOIMEntitySlot[];
914
852
 
915
- const userService = createOIMEntityService<User>(db, { keyOf: (user) => user.id });
853
+ const userService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<User>() });
916
854
  const users = userService.createStoreSlot(db);
917
- const groupService = createOIMEntityService<Group>(db, { keyOf: (group) => group.id });
855
+ const groupService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Group>() });
918
856
  const groups = groupService.createStoreSlot(db);
919
- const alice = userService.slotContentService.upsertEntity(users, { id: 'alice', name: 'Alice' });
920
- const admins = groupService.slotContentService.upsertEntity(groups, { id: 'admins', name: 'Admins' });
857
+ const alice = userService.writeSlotContentByKey(users, 'alice', { id: 'alice', name: 'Alice' });
858
+ const admins = groupService.writeSlotContentByKey(groups, 'admins', { id: 'admins', name: 'Admins' });
921
859
 
922
860
  type TEnrolmentStore = TOIMSlotStoreTree<TOIMEntitySlot<Enrolment, TChain>, TOIMEntitySlot>;
923
861
 
924
- const enrolmentService = createOIMEntityService<Enrolment, TOIMEntityColumnName<Enrolment>, TChain, TEnrolmentStore>(db, {
925
- entitySlotStoreService:
926
- new OIMSlotStoreServiceTreeDriven<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
927
- OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue(),
928
- ),
929
- keyOf: (m) => [m.user, m.group], // the two identities, in order
930
- });
862
+ const enrolmentService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(), slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
863
+ OIMCompositeKeyTreeWalk.INSTANCE,
864
+ ) });
931
865
  const enrolments = enrolmentService.createStoreSlot(db);
932
- enrolmentService.slotContentService.upsertEntity(enrolments, { user: alice, group: admins, since: 2026 });
866
+ enrolmentService.writeSlotContentByKey(enrolments, ((m) => [m.user, m.group])({ user: alice, group: admins, since: 2026 }), { user: alice, group: admins, since: 2026 });
933
867
  enrolmentService.getSlotByKey(enrolments, [alice, admins]);
934
868
  ```
935
869
 
936
- The two descents are `OIMKeyTreeWalkScalar` and `OIMKeyTreeWalkComposite`, and
870
+ The two descents are `OIMScalarKeyTreeWalk` and `OIMCompositeKeyTreeWalk`, and
937
871
  `OIMDefaults` holds one stateless instance of each. The composite walk
938
872
  is the one whose `holdCompositeKeyPart` runs `entitySlot.owner.holdSlot(entitySlot)` when `mintNodeAt` creates an edge, and
939
873
  whose `unholdCompositeKeyPart` runs `entitySlot.owner.unholdSlot(entitySlot)` (unless `oimIsSlotReleased(entitySlot)`) on exactly the edges `deleteKey` unlinked; the scalar walk holds nothing.
@@ -945,7 +879,7 @@ Both walks throw `an empty key is not a key` on `[]`.
945
879
  **A scalar is not a composite key part of a composite-walk key.** It is a compile
946
880
  error, and past a cast it throws inside the mint (`Cannot create property
947
881
  'holdCount' on string`) with the edge already in the tree. Hand the store
948
- `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` instead of using a cast.
882
+ `OIMScalarKeyTreeWalk.INSTANCE` instead of using a cast.
949
883
 
950
884
  **Compare a chain with the strategy that matches it.** `oimSameSlotSequence` walks identities by
951
885
  reference. Using it on a chain of values is the `NaN` defect: it answers
@@ -956,32 +890,28 @@ goes to `oimSameKeySequence`, which compares each key by SameValueZero, the way
956
890
 
957
891
  ## Writing
958
892
 
959
- Every write on an entity collection is on its `slotContentService`, and is stated in terms of an
960
- **entity** or of an **identity you already hold**. There is no write by key: a key is resolved
961
- to a slot once, with `findSlotByKey` (mints nothing) or `getSlotByKey` / `holdSlotByKey` on the
962
- collection, and the write goes through the slot. A door by key takes the store slot first; a door
963
- on a slot takes only the slot.
893
+ The by-key write is on the collection service; every other write on an entity collection is on its
894
+ `slotContentService` and goes through an **identity you already hold**. A key from outside is resolved
895
+ to a slot once, with `findSlotByKey` (mints nothing) or `getSlotByKey` / `holdSlotByKey`.
964
896
 
965
- | Call on `entityService.slotContentService` | What it does |
897
+ | Call | What it does |
966
898
  |---|---|
967
- | `upsertEntity(storeSlot, entity, changedEntitySlots?)` | Write a whole value. A key with no value gets one and every column counts as new; a key that has one is brought to the value given, and columns the new value does not carry are taken away. **The identity always comes back**, whatever the write changed. |
968
- | `upsertEntities(storeSlot, entities, changedEntitySlots?)` | The loop. Correct at any moment. **Returns the slot of every entity, in input order**, whether or not its write changed anything — so `bucketService.slotContentService.writeBucketMemberSlots(bucketSlot, entityService.slotContentService.upsertEntities(storeSlot, entities))` files every entity. |
969
- | `insertEntity(storeSlot, key, entity, changedEntitySlots?)` | `upsertEntity` under a key you name instead of `keyOf`'s. |
970
- | `insertEntities(storeSlot, entities)` / `fillEntities(storeSlot, keys, entities)` | A **first load only** — refuses unless the store slot is empty, so it checks emptiness once instead of per entity. No `changed`: a first load changes every entity. |
971
- | `writeEntity(slot, entity)` | A whole value through an identity you hold; `false` when nothing changed. |
899
+ | `entityService.writeSlotContentByKey(storeSlot, key, entity, changedEntitySlots?)` | Write a whole value under the key you name. A key with no value gets one and every column counts as new; a key that has one is brought to the value given, and columns the new value does not carry are taken away. **The identity always comes back**, whatever the write changed. |
900
+ | `writeSlotContent(slot, entity)` | A whole value through an identity you hold; `false` when nothing changed. |
972
901
  | `updateEntity(slot, patch)` | Patch through an identity you hold. Only the columns the patch actually moved are routed. |
973
902
  | `updateEntityColumn(slot, columnName, value)` | One column through an identity you hold, with no patch object to build and walk. |
974
903
  | `deleteEntityColumns(slot, columnNames)` | Take those columns off the value. |
975
- | `deleteEntity(slot)` | The **value** leaves; the identity stays for as long as anything still refers to it. |
976
- | `deleteEntities(storeSlot, slots)` | The batch form. One change span for the whole batch, like `upsertEntities` — the store slot is named to the channel once for the batch, not once per entity. |
977
- | `clearAllEntities(storeSlot)` | Every value out. Identities something still holds survive without one. |
978
- | `findEntityByKey(storeSlot, key)` | The entity itself rather than the slot. Mints nothing. |
904
+ | `clearSlotContent(slot)` | The **value** leaves; the identity stays for as long as anything still refers to it. |
905
+ | `entityService.findSlotByKey(storeSlot, key)?.entity` | The entity itself rather than the slot. Mints nothing. |
979
906
  | `markEntityChanged(slot, changedColumns?)` | Report a change you made to `slot.entity` by hand. |
980
907
 
981
908
  By key, compose the two halves: `const s = entityService.findSlotByKey(storeSlot, key); if (s !== undefined) entityService.slotContentService.updateEntity(s, patch);`.
982
909
 
910
+ There are no bulk writes: many entities are a loop of these inside `entityService.writeInBatch(storeSlot, () => …)`,
911
+ so an index on the collection hears them in one call when the batch closes.
912
+
983
913
  **Who owns the entity is a strategy you pick, and there are two strategies.** The
984
- writer takes an `entityInsertStrategy` through `TOIMEntityWriterOptions`, so the choice is
914
+ writer takes an `entityInserter` through `TOIMEntityWriterOptions`, so the choice is
985
915
  made once at construction and costs nothing per write:
986
916
 
987
917
  | strategy | what lands in the slot | what the caller keeps |
@@ -991,10 +921,7 @@ made once at construction and costs nothing per write:
991
921
  | `oimInsertEntityWithSanitizedCopy` | a one-level copy with an own `__proto__` key removed | the same; pick it when you insert untrusted JSON (`JSON.parse` of other users' data) |
992
922
 
993
923
  ```typescript
994
- const copiedCardService = createOIMEntityService<Card>(db, {
995
- keyOf: (card) => card.id,
996
- entityWriter: createOIMEntityWriterInPlace({ entityInsertStrategy: oimInsertEntityWithCopy }),
997
- });
924
+ const copiedCardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>({ entityWriter: createOIMInPlaceEntityWriter({ entityInserter: oimInsertEntityWithCopy }) }) });
998
925
  ```
999
926
 
1000
927
  **A copy is one level deep, and no deeper.** A nested object or
@@ -1016,7 +943,7 @@ markEntityChanged(
1016
943
  ): void
1017
944
  ```
1018
945
 
1019
- `upsertEntity` and `updateEntity` work out what moved: the writer walks the incoming
946
+ `writeSlotContentByKey` and `updateEntity` work out what moved: the writer walks the incoming
1020
947
  value (`oimPatchEntityColumns`, `oimPutEntityColumns`) and puts every column through `sameValueGuard`. A caller who edited the stored object
1021
948
  already knows what moved, and this door takes that answer instead of deriving
1022
949
  it, so there is no patch object, no copy and no per-column comparison.
@@ -1029,7 +956,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
1029
956
  **The hand mutation needs no cast.** `TOIMEntitySlot` declares
1030
957
  `readonly entity: TEntity | undefined`, and `readonly` forbids replacing the
1031
958
  object, not writing its columns. You still have to narrow the `| undefined`.
1032
- `TOIMSlotEditable<TSlot>` is the type that strips `readonly` from the field
959
+ `TOIMEditable<TSlot>` is the type that strips `readonly` from the field
1033
960
  itself, which is what assigning `slot.entity` needs.
1034
961
 
1035
962
  **The list is the whole point, and leaving it out gives it back.** Named columns
@@ -1042,7 +969,7 @@ per-column walk.
1042
969
 
1043
970
  **An empty list wakes nothing at all** (not the columns, not the entity, not the
1044
971
  store slot), because `[]` is the caller saying that nothing moved, and the call returns
1045
- before the subframe opens.
972
+ before it writes anything.
1046
973
 
1047
974
  **It re-stamps emptiness** itself when the list is left out, when the entity is gone, or when
1048
975
  the service recomputes emptiness on a column write. A hand
@@ -1058,7 +985,7 @@ A misspelt column name, a bare runtime `string`, and a slot of a different entit
1058
985
  type are each a compile error, because the parameter is `readonly (keyof TEntity & string)[]`.
1059
986
 
1060
987
  **A removal leaves the buckets standing.** A slot does not know which buckets it
1061
- is in, so `deleteEntity` cannot take it out of them. A bucket's slots belong to the caller: the
988
+ is in, so `clearSlotContent` cannot take it out of them. A bucket's slots belong to the caller: the
1062
989
  caller who put the slot in a bucket takes it out.
1063
990
 
1064
991
  ### Which entities actually changed: the `changed` array
@@ -1067,7 +994,7 @@ caller who put the slot in a bucket takes it out.
1067
994
  const payload: Card[] = [];
1068
995
  const reindex = (changedSlot: TOIMEntitySlot<Card, TOIMStoreKey>): void => {};
1069
996
  const changed: TOIMEntitySlot<Card, TOIMStoreKey>[] = [];
1070
- cardService.slotContentService.upsertEntities(cards, payload, changed);
997
+ cardService.writeInBatch(cards, () => (payload).map(entity => cardService.writeSlotContentByKey(cards, entity.id, entity, changed)));
1071
998
  for (const changedSlot of changed) reindex(changedSlot); // the re-sent entities are not in here
1072
999
  changed.length = 0;
1073
1000
  ```
@@ -1076,7 +1003,7 @@ The array is **optional, yours, and appended to**. The engine never reads it
1076
1003
  and never clears it. A slot is pushed exactly when the write put in content different
1077
1004
  from what that slot already held, so one array reused across a whole batch comes
1078
1005
  out as the list of entities that a bucket pass owes work for. The same parameter
1079
- stated by key is `cardService.slotContentService.insertEntity(cards, key, entity, changed)`.
1006
+ stated by key is `cardService.slotContentService.writeSlotContentByKey(cards, key, entity, changed)`.
1080
1007
 
1081
1008
  It exists because `sameValueGuard` already refused those writes column by column,
1082
1009
  and without this parameter the verdict would stay inside the engine. A consumer that owns a
@@ -1086,8 +1013,8 @@ by the rule above, and a `{ slot, changed }` record would allocate once per
1086
1013
  write on the path this parameter exists to make cheaper. A caller who does not want it pays
1087
1014
  one `undefined` compare per write.
1088
1015
 
1089
- One layer down, the slot doors that write a whole content (`writeEntity(slot, entity)`,
1090
- `writeBucketMemberSlots(bucketSlot, bucketMemberSlots)`) compute it, and their boolean is **a fact about the content and says nothing about the
1016
+ One layer down, the slot doors that write a whole content (`writeSlotContent(slot, entity)`,
1017
+ `writeSlotContent(bucketSlot, bucketMemberSlots)`) compute it, and their boolean is **a fact about the content and says nothing about the
1091
1018
  identity**: `false` does not mean "this slot may go". That question is answered by `oimIsSlotEmpty(slot)`
1092
1019
  and the grace left on the idle window, asked by the release guard.
1093
1020
 
@@ -1095,19 +1022,19 @@ and the grace left on the idle window, asked by the release guard.
1095
1022
 
1096
1023
  ```typescript signature
1097
1024
  interface IOIMEntityWriter {
1098
- readonly fillEntity: (slot: TOIMEntitySlot, entity: object) => void;
1099
- readonly putEntity: (slot: TOIMEntitySlot, entity: object, changedColumnNames: string[] | undefined) => boolean;
1100
- readonly patchEntity: (slot: TOIMEntitySlot, patch: object, changedColumnNames: string[] | undefined) => boolean;
1025
+ readonly fillEntity: (slot: TOIMEntityContentSlot, entity: object) => void;
1026
+ readonly putEntity: (slot: TOIMEntityContentSlot, entity: object, changedColumnNames: string[] | undefined) => boolean;
1027
+ readonly patchEntity: (slot: TOIMEntityContentSlot, patch: object, changedColumnNames: string[] | undefined) => boolean;
1101
1028
  readonly patchColumn: (
1102
- slot: TOIMEntitySlot, columnName: string, value: unknown, changedColumnNames: string[] | undefined,
1029
+ slot: TOIMEntityContentSlot, columnName: string, value: unknown, changedColumnNames: string[] | undefined,
1103
1030
  ) => boolean;
1104
- readonly deleteColumns: (slot: TOIMEntitySlot, names: readonly string[]) => readonly string[];
1105
- readonly clearEntity: (slot: TOIMEntitySlot) => void;
1031
+ readonly deleteColumns: (slot: TOIMEntityContentSlot, names: readonly string[]) => readonly string[];
1032
+ readonly clearEntity: (slot: TOIMEntityContentSlot) => void;
1106
1033
  }
1107
1034
 
1108
- createOIMEntityWriterInPlace(options?) // the stored object keeps its identity
1109
- createOIMEntityWriterReplacing(options?) // every write that changed something produces a new object
1110
- // TOIMEntityWriterOptions = { sameValueGuard?: TOIMSameValueGuard; entityInsertStrategy?: TOIMEntityInsertStrategy; putEntityColumns?: typeof oimPutEntityColumns }
1035
+ createOIMInPlaceEntityWriter(options?) // the stored object keeps its identity
1036
+ createOIMReplacingEntityWriter(options?) // every write that changed something produces a new object
1037
+ // TOIMEntityWriterOptions = { sameValueGuard?: TOIMSameValueGuard; entityInserter?: TOIMEntityInserter; putEntityColumns?: typeof oimPutEntityColumns }
1111
1038
  ```
1112
1039
 
1113
1040
  A writer moves an entity's columns and does nothing else: it reaches no channel,
@@ -1125,9 +1052,9 @@ A writer of your own can reuse the column walks that both shipped writers run,
1125
1052
  `oimPatchEntityColumns`, `oimPatchEntityColumn` (one column, behind `patchColumn`) and
1126
1053
  `oimPutEntityColumns`. The whole-entity put is itself the writers' `putEntityColumns` option:
1127
1054
  `oimPutEntityColumns` is the default and skips neither `__proto__` keys nor inherited stored
1128
- columns; `oimPutEntityColumnsGuarded` keeps both guards, at a measured cost on every put, for
1055
+ columns; `oimGuardedPutEntityColumns` keeps both guards, at a measured cost on every put, for
1129
1056
  values that can carry a `__proto__` key or are class instances
1130
- (`createOIMEntityWriterInPlace({ putEntityColumns: oimPutEntityColumnsGuarded })`). Each returns the object
1057
+ (`createOIMInPlaceEntityWriter({ putEntityColumns: oimGuardedPutEntityColumns })`). Each returns the object
1131
1058
  `targetOf` handed back with the moved columns written into it, or `undefined`
1132
1059
  when nothing moved, so the writer decides only where the result goes. The
1133
1060
  [docs](https://oimdb.org/docs/core/writing#writing-your-own-writer) build one that
@@ -1136,10 +1063,10 @@ stores every value frozen.
1136
1063
  `putEntity` is PUT and `patchEntity` is PATCH, as in HTTP. PUT makes the stored
1137
1064
  entity the value handed in: columns that changed are written and columns the value
1138
1065
  does not have are cleared to `undefined`, in one call, and the names it returns cover both. PATCH writes
1139
- only the columns the patch names. `upsertEntity` of a key that has a value and
1140
- `writeEntity(slot, entity)` are PUT, so a whole-entity write that drops a column writes
1066
+ only the columns the patch names. `writeSlotContentByKey` of a key that has a value and
1067
+ `writeSlotContent(slot, entity)` are PUT, so a whole-entity write that drops a column writes
1141
1068
  the slot to the channel once, and the replacing writer builds one new object, not
1142
- two. This is counted in `src/facade/entity/tests/what-a-put-that-drops-a-column-costs.test.ts`.
1069
+ two. This is counted in `src/entity/tests/what-a-put-that-drops-a-column-costs.test.ts`.
1143
1070
 
1144
1071
  Neither writer `delete`s. A column that a PUT drops or `deleteColumns` takes away is
1145
1072
  written `undefined`, into the stored object in place or into a spread copy when
@@ -1149,8 +1076,8 @@ replacing, so the entity keeps its hidden class: the key stays in
1149
1076
  does not report it, and taking the entity away does not wake its readers.
1150
1077
 
1151
1078
  The choice is made once, when the entity content service is built (`entityWriter`, else
1152
- `db.entityWriter`), so there is no `if (inPlace)` anywhere on any write path; the object already
1153
- is the answer. The default is `createOIMEntityWriterInPlace()`. Pass `createOIMEntityWriterReplacing()` when
1079
+ `defaults.entityWriter`), so there is no `if (inPlace)` anywhere on any write path; the object already
1080
+ is the answer. The default is `createOIMInPlaceEntityWriter()`. Pass `createOIMReplacingEntityWriter()` when
1154
1081
  something downstream compares by reference.
1155
1082
 
1156
1083
  ---
@@ -1165,7 +1092,7 @@ A channel is the runtime: the frame counter, the revision, and the delivery pass
1165
1092
  const sentReports: unknown[] = [];
1166
1093
  const scheduledDb = new OIMDB({
1167
1094
  channelScheduler: (flush) => { void Promise.resolve().then(flush); },
1168
- errorPolicy: new OIMErrorPolicyLogAndContinue({ onError: (error) => { sentReports.push(error); } }),
1095
+ errorPolicy: new OIMLogAndContinueErrorPolicy({ onError: (error) => { sentReports.push(error); } }),
1169
1096
  onBeforeFlush: () => { /* the flush has begun; read only, no writes */ },
1170
1097
  });
1171
1098
  ```
@@ -1174,9 +1101,9 @@ With no `channelScheduler`, nothing is ever delivered until you call `db.flush()
1174
1101
  yourself. A second independent channel is a second `OIMDB`.
1175
1102
 
1176
1103
  **Errors thrown by effects and handlers go to the error policy.** The default,
1177
- `OIMErrorPolicyLogAndContinue`, logs each one with `console.error`, hands it to the
1104
+ `OIMLogAndContinueErrorPolicy`, logs each one with `console.error`, hands it to the
1178
1105
  optional `onError` (Sentry, the devtools registry), and finishes the rest of the
1179
- flush. `OIMErrorPolicyThrowAtOnce` instead throws the first one out of `flush()`;
1106
+ flush. `OIMThrowAtOnceErrorPolicy` instead throws the first one out of `flush()`;
1180
1107
  effects the frame had not reached stay queued for the next flush. An effect whose
1181
1108
  body or cleanup threw has not taken the change it woke for: it is stamped only
1182
1109
  after the body returns, so its next wake runs that change again. It is never
@@ -1188,8 +1115,7 @@ the next read; a computed with no sources derives on its first read. A throw nev
1188
1115
  for frames: the next write after an aborted frame asks for one. A throw in one
1189
1116
  release of a sweep leaves the rest of that slice on the release clock for the next
1190
1117
  sweep. A release that falls inside a flush (an effect body disposing another
1191
- effect) waits: the slot goes to the unused-slot collector and is released once the
1192
- flush is over. Code that keeps
1118
+ effect) happens at once: nothing holds the slot, so nothing hears it go. Code that keeps
1193
1119
  derived state (an index's `keyOf`, an emptiness check) is outside the
1194
1120
  policy: a throw there leaves state consistent and is thrown in either mode. An
1195
1121
  index whose `keyOf` throws on an entity files that entity under no key, finishes the rest
@@ -1198,7 +1124,7 @@ of the write or batch, then rethrows; the next write it accepts files the entity
1198
1124
  **Subscribing is building an effect.** There is no `subscribe` helper over this,
1199
1125
  and there must not be one. An effect is stamped with the channel clock at birth,
1200
1126
  so `effectService.createEffect` hears only what moves after it and never delivers a first
1201
- value. `effectService.createEffectImmediate(db, roots, computeds, body)` is the door that runs
1127
+ value. `effectService.createAndRunEffect(db, roots, computeds, body)` is the door that runs
1202
1128
  the body once at creation. An effect is a plain `TOIMEffect` object with no methods (its
1203
1129
  channel is `effect.channelSlot`); `OIMEffectService.INSTANCE` does everything to it. There is
1204
1130
  one effect service for the whole process, and every door is handed the `db` (any
@@ -1206,14 +1132,14 @@ one effect service for the whole process, and every door is handed the `db` (any
1206
1132
 
1207
1133
  ```typescript
1208
1134
  // Sources are named in two lists: root slots, and derivations.
1209
- const effect = effectService.createEffect(db, [slot], [totalService.computedCollectionService.getSlotByKey(total, undefined)], () => {});
1135
+ const effect = effectService.createEffect(db, () => {}, [slot], [totalService.computedSingleSlotService.getSingleSlot(total)]);
1210
1136
  effectService.disposeEffect(effect);
1211
1137
  // The second list is typed `readonly TOIMComputedSlot[]` — pass slots taken from
1212
1138
  // a computed service's `computedCollectionService` straight in.
1213
1139
 
1214
1140
  // One column of one slot, woken when that named column moves and by nothing else.
1215
1141
  // It takes the COLUMN SLOT, which the column owner mints and holds.
1216
- const onRank = effectService.createEffect(db, [cardService.holdColumn(slot, 'rank')], [], () => {});
1142
+ const onRank = effectService.createEffect(db, () => {}, [cardService.columnSlotService.holdColumn(slot, 'rank')]);
1217
1143
  ```
1218
1144
 
1219
1145
  The body's type is `TOIMEffectBody`, which is `() => (() => void) | void`. **A returned
@@ -1223,10 +1149,10 @@ so a body that opened something on each run has somewhere to close it.
1223
1149
  ```typescript
1224
1150
  const poll = (): void => {};
1225
1151
 
1226
- effectService.createEffect(db, [slot], [], () => {
1152
+ effectService.createEffect(db, () => {
1227
1153
  const timer = setInterval(poll, 1000);
1228
1154
  return () => clearInterval(timer);
1229
- });
1155
+ }, [slot]);
1230
1156
  ```
1231
1157
 
1232
1158
  The return is checked with `typeof` rather than for truthiness, because an
@@ -1234,16 +1160,16 @@ expression-bodied arrow hands back whatever the expression evaluated to
1234
1160
  (`() => set.add(key)` returns the `Set`), and anything that is not a function is
1235
1161
  discarded.
1236
1162
 
1237
- **An effect can also find its sources as it reads.** `createEffectWithSourceTracking(db, body)`
1238
- takes no source lists: the body gets an `OIMEffectSourceTracker`, and what it reads through
1163
+ **An effect can also find its sources as it reads.** `createSourceTrackingEffect(db, body)`
1164
+ takes no source lists: the body gets an `OIMSourceTracker`, and what it reads through
1239
1165
  `readSourceSlot(slot)` (which files the slot and returns it) and
1240
1166
  `readSourceComputedValue(computedSlot)` (which files the derivation and returns its value)
1241
- is what that run depends on. The body's type is `TOIMEffectBodyWithSourceTracking`.
1167
+ is what that run depends on. The body's type is `TOIMSourceTrackingEffectBody`.
1242
1168
 
1243
1169
  ```typescript
1244
- effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
1245
- const isFirst = effectSourceTracker.readSourceSlot(slot).entity?.rank === 1;
1246
- console.log(isFirst ? effectSourceTracker.readSourceSlot(slot).entity?.title : 'not first');
1170
+ effectService.createSourceTrackingEffect(db, sourceTracker => {
1171
+ const isFirst = sourceTracker.readSourceSlot(slot).entity?.rank === 1;
1172
+ console.log(isFirst ? sourceTracker.readSourceSlot(slot).entity?.title : 'not first');
1247
1173
  });
1248
1174
  ```
1249
1175
 
@@ -1282,13 +1208,13 @@ Here is how an effect is filed:
1282
1208
  `getColumn(slot, columnName)` reads the same column slot without a hold (a mint is held by
1283
1209
  the unused-slot collector until the next flush ends). **The column doors are on the collection
1284
1210
  service** (`OIMSlotCollectionService` and `OIMSingleSlotService`), and they reach the slot owner's
1285
- column owner (`slotOwnerService.columnOwner`), which holds the column slot store and the column
1211
+ column owner (`slotOwnerService.columnSlotOwner`), which holds the column slot store and the column
1286
1212
  groups. With `columnNames` named the store is an array by ordinal, otherwise a Map. The column
1287
1213
  owner is abstract and has two strategies, chosen by the door that builds the owner:
1288
- write-driven column slots (`OIMColumnOwnerPushDriven`), woken by the marks a write makes (entity
1214
+ write-driven column slots (`OIMPushColumnSlotOwner`), woken by the marks a write makes (entity
1289
1215
  and bucket collections, keyed or single), and derive-driven column slots
1290
- (`OIMColumnOwnerPullDriven`), each a computed slot over its carrier (`OIMComputedService`, so
1291
- `OIMComputedServiceSorted` and `OIMComputedServiceBucketMemo` too, keyed or single; see
1216
+ (`OIMPullColumnSlotOwner`), each a computed slot over its carrier (`OIMComputedService`, so
1217
+ `OIMSortedComputedService` and `OIMBucketMemoComputedService` too, keyed or single; see
1292
1218
  [Computed columns](#computed-columns)). A write-driven column slot is a
1293
1219
  `TOIMColumnSlot` — `{ carrierSlot, key }` plus the slot fields — and its carrier is
1294
1220
  whatever slot it hangs off: an entity slot, or a bucket slot (see
@@ -1307,13 +1233,13 @@ frozen object per set of names on that service; get it once per view, not per en
1307
1233
  and `unholdColumn(columnSlot)` gives it back. A write wakes it once if it changes any of the
1308
1234
  group's columns and not at all if it changes only others.
1309
1235
 
1310
- **`columnNames` picks the column slot store.** `createOIMEntityService<Card, 'rank' | 'title'>(db, { keyOf, columnNames: ['rank', 'title'] })`
1236
+ **`columnNames` picks the column slot store.** `createOIMSlotCollectionService<Card, 'rank' | 'title'>({ keyOf, columnNames: ['rank', 'title'] })`
1311
1237
  keeps each entity's column slots in an array by column number instead of a `Map` per entity,
1312
1238
  which makes an entity's column store cheaper to create and give back; that is what a scrolling view pays.
1313
1239
  `holdColumn` and `getColumnGroup` are typed on the named columns, and a slot for any other
1314
1240
  column throws. `columnNames` is on the options of every door — entity, bucket, index, computed —
1315
1241
  keyed or single. Without `columnNames` the store is the `columnSlotStoreService` option, and the
1316
- doors default it to the shared map-driven `OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()`.
1242
+ doors default it to the shared map-driven a shared map-driven store.
1317
1243
 
1318
1244
  ### Frame rules
1319
1245
 
@@ -1333,7 +1259,7 @@ doors default it to the shared map-driven `OIMDefaults.COLUMN_SLOT_STORE_SERVICE
1333
1259
  - **A door asks once and then writes without asking again.** `OIMChannelService.write` is
1334
1260
  `assertNotFlushing` plus `writeWithoutFlushCheck`, and a content service door
1335
1261
  that opened with `assertWritable` uses the second directly. The per-entity ask
1336
- cannot be removed: user code (`keyOf`, an index's `keyOf`, `slotEmptiness.isEmpty`, an
1262
+ cannot be removed: user code (`keyOf`, an index's `keyOf`, `slotEmptinessPolicy.isEmpty`, an
1337
1263
  `IOIMEntityWriter`) runs inside the door and can call `flush()`, so each entity is
1338
1264
  a fresh question. `tests/who-may-write-without-asking-the-channel.test.ts` pins
1339
1265
  who may use the door that skips the check.
@@ -1348,53 +1274,30 @@ doors default it to the shared map-driven `OIMDefaults.COLUMN_SLOT_STORE_SERVICE
1348
1274
  handler hands the work to a microtask or a later callback instead of writing
1349
1275
  itself. A refusal raised from a tail handler goes to the error policy, not out of
1350
1276
  the write call.
1351
- - **The frame cannot close while a subframe is open.** A subframe is one logical
1352
- change inside the frame. `OIMChannelService.INSTANCE.openSubframe(channelSlot)` and
1353
- `closeSubframe(channelSlot)` bracket it, and a write that asks for a frame while one is open is owed it by
1354
- the `closeSubframe` that brings the count to zero. **`flush()` called while a
1355
- subframe is open throws** (`a subframe is open`), whether it comes from a `keyOf` or a
1356
- listener that a write runs, or from between a caller's own open and close. Subframes are counted,
1357
- because a subframe is routinely inside another one, and closed from a `finally`,
1358
- so a subframe that threw part-way still delivers what landed. It is not the frame
1359
- guard seen from the other side: that guard refuses a *write* made while a frame closes, while this
1360
- refuses a *frame* asked for while a subframe is open. A content service's write doors open
1361
- a content subframe, which opens the channel's subframe and delivers the content-change
1362
- listeners at its outermost close.
1277
+ - **A batch changes who hears the writes, not when the frame runs.** `openBatch(storeSlot)` /
1278
+ `closeBatch(storeSlot)` on a collection service, or `writeInBatch(storeSlot, write)`, count a depth on
1279
+ the store slot. While it is open, the content changes are queued there, and the outermost close hands
1280
+ the content-change listeners (an index) the whole batch in one call. A flush inside a batch runs as
1281
+ usual; the index catches up when the batch closes. Nothing is rolled back if a listener throws.
1363
1282
  - **Every bucket door wakes the bucket the same way.** Each bucket content service
1364
1283
  door that changed the bucket (adds, removes, positional inserts and removes, both moves,
1365
- `writeBucketMemberSlots`, `clearBucketMemberSlots`)
1366
- files it once and sets the bucket's EMPTY flag at once. The outermost subframe close
1367
- stamps it once, tells the release policy `onSlotWritten` once, then asks whether
1368
- it may go. So `hasSlotContentByKey` is accurate inside an open batch, a batch that mixes
1369
- doors stamps the bucket once, a move renews a write deadline, and a door that
1370
- changed nothing stamps nothing and tells the policy nothing. Commands are still
1371
- recorded at the door, where the position is true.
1284
+ `writeSlotContent`, `clearSlotContent`) settles its EMPTY flag, writes it to the channel, tells the
1285
+ content-change listeners and asks whether it may go. A door that changed nothing stamps nothing and
1286
+ tells nobody. Commands are recorded at the door, where the position is true.
1372
1287
  - **A released identity is refused before the bucket changes.** Every door that
1373
1288
  takes a member checks first whether what it was handed is released (`oimIsSlotReleased`), and throws, so a
1374
1289
  refused door leaves the members, their holds and the EMPTY flag as they were. The check costs one
1375
1290
  field read per member handed in.
1376
- - **The span belongs to the door.** `writeBucketMemberSlots`, `clearBucketMemberSlots`, `insertEntities`, `fillEntities`,
1377
- `updateEntity` (when it is more than one channel write) and `markEntityChanged` each open exactly one; the entity writer's `fillEntity`,
1378
- `putEntity`, `patchEntity`, `deleteColumns` and `clearEntity` open none. So one `upsertEntities` opens
1379
- one, and one `insertEntities` opens one however many entities it carries. A write that is exactly
1380
- one channel write opens none, because one write cannot be seen half-done and there is nobody to batch for:
1381
- `upsertEntity` of a new key, `updateEntity`/`updateEntityColumn`/`writeEntity` and `deleteEntity`
1382
- when the store slot has no content listener, no column of the
1383
- entity is read and emptiness is not recomputed on a column write. `deleteEntityColumns`
1384
- always opens one, because it always names the columns it took. The content service's
1385
- `releaseSlotContent` is the exception and keeps its own, because `releaseSlotIfUnheld`
1386
- reaches it from the release queue through no door at all.
1387
-
1388
1291
  ### `subscribeOnceAfterFrame` — one frame, and the caller picks which
1389
1292
 
1390
1293
  ```typescript
1391
1294
  const paint = (card: Card | undefined): void => {};
1392
1295
  const measureAndScroll = (): void => {};
1393
1296
 
1394
- effectService.createEffect(db, [slot], [], () => {
1297
+ effectService.createEffect(db, () => {
1395
1298
  paint(slot.entity);
1396
1299
  OIMChannelService.INSTANCE.subscribeOnceAfterFrame(db.defaultChannelSlot, () => { measureAndScroll(); });
1397
- });
1300
+ }, [slot]);
1398
1301
  ```
1399
1302
 
1400
1303
  The handler is told after **one** frame, the first that ends after the call, and is then gone.
@@ -1407,7 +1310,7 @@ inside its effect and is answered at the end of that same frame.
1407
1310
 
1408
1311
  It is **not** `subscribeAfterEachFrame` plus a disposal. The effect runs again
1409
1312
  next frame and registers again, so a standing handler accumulates one more per run:
1410
- `src/engine/channel/tests/a-handler-that-wants-one-frame-and-not-every-frame.test.ts` measures the
1313
+ `src/channel/tests/a-handler-that-wants-one-frame-and-not-every-frame.test.ts` measures the
1411
1314
  one-shot at 1, 2, 3 calls over three frames against the standing handler's 1, 3,
1412
1315
  6. The one-shot handler also comes off *before* its body runs, so a handler that throws is not left
1413
1316
  standing for the next frame.
@@ -1466,7 +1369,7 @@ that stays refused, and nothing may ask for one slot to go.
1466
1369
  ## Buckets: the slots are the caller's
1467
1370
 
1468
1371
  A **bucket** is an identity whose content is other identities. A
1469
- **bucket collection** (`createOIMBucketService`) files buckets by key, in each store slot it
1372
+ **bucket collection** (`createOIMSlotCollectionService`) files buckets by key, in each store slot it
1470
1373
  creates, and its `slotContentService` holds each bucket's member slots. It is **manual by default**, and that
1471
1374
  is a claim about what a bucket's slots ARE; it is not a missing feature. They are
1472
1375
  a fact about the **response** a caller is composing (a search result, a page, a
@@ -1478,38 +1381,36 @@ computeds), and the bucket's type parameter is that slot type,
1478
1381
  `getEntitiesByKey` is there because its members are entity slots.
1479
1382
 
1480
1383
  ```typescript
1481
- const byDeckService = createOIMBucketService<
1482
- CardSlot, TOIMStoreKey, TOIMBucketSlotsSetStore<CardSlot>, OIMSlotContentBucketServiceSetDriven<CardSlot>
1483
- >(db, { slotContentService: new OIMSlotContentBucketServiceSetDriven<CardSlot>() });
1384
+ const byDeckService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentSetBucketService<CardSlot>() });
1484
1385
  const byDeck = byDeckService.createStoreSlot(db);
1485
1386
 
1486
- const aceSlot = cardService.slotContentService.upsertEntity(cards, { id: 'c1', deck: 'red', title: 'Ace', rank: 1 });
1487
- const kingSlot = cardService.slotContentService.upsertEntity(cards, { id: 'c2', deck: 'red', title: 'King', rank: 13 });
1387
+ const aceSlot = cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', deck: 'red', title: 'Ace', rank: 1 });
1388
+ const kingSlot = cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', deck: 'red', title: 'King', rank: 13 });
1488
1389
 
1489
1390
  const red = byDeckService.holdSlotByKey(byDeck, 'red');
1490
1391
  const deckContentService = byDeckService.slotContentService;
1491
- deckContentService.writeBucketMemberSlots(red, [aceSlot, kingSlot]);
1392
+ deckContentService.writeSlotContent(red, [aceSlot, kingSlot]);
1492
1393
  db.flush();
1493
1394
 
1494
1395
  for (const cardSlot of deckContentService.getBucketMemberSlots(red)) { console.log(cardSlot.entity?.title); }
1495
1396
  deckContentService.getBucketMemberSlotCount(red);
1496
1397
 
1497
- deckContentService.clearBucketMemberSlots(red);
1398
+ deckContentService.clearSlotContent(red);
1498
1399
  red.owner.unholdSlot(red);
1499
1400
  ```
1500
1401
 
1501
1402
  The store slot carries the keyed store: there is no `buckets` property to reach
1502
1403
  through, and `holdSlotByKey(byDeck, key)` hands back the bucket itself instead of a
1503
1404
  `{ slot, release }` record. The door's optional `slotReleasePolicy` decides whether and when
1504
- its buckets go; `OIMSlotReleasePolicyAtUnhold.INSTANCE` drops a bucket at the last give-back.
1405
+ its buckets go; `OIMSlotAtUnholdReleasePolicy.INSTANCE` drops a bucket at the last give-back.
1505
1406
 
1506
- `writeBucketMemberSlots` is idempotent: when you re-send an unchanged list, the
1407
+ `writeSlotContent` is idempotent: when you re-send an unchanged list, the
1507
1408
  content service compares it (it is the only object that knows whether the caller's order is part
1508
1409
  of what it holds at all) and returns without evicting anything.
1509
1410
 
1510
1411
  ### Which door: you hold the answer, or you hold the delta
1511
1412
 
1512
- `writeBucketMemberSlots` is for a caller who has **the list that should be there** and does not
1413
+ `writeSlotContent` is for a caller who has **the list that should be there** and does not
1513
1414
  know what changed: a search result, a page from a server, a filter re-run. Its
1514
1415
  cost is proportional to **what the bucket holds**, not to the change. The
1515
1416
  difference builds a map over the whole incoming list and walks the standing one,
@@ -1530,7 +1431,7 @@ set-driven bucket removes in the order you name.
1530
1431
 
1531
1432
  **`addBucketMemberSlots` removes nothing.** If the authoritative list shrank and you call
1532
1433
  `addBucketMemberSlots`, the entities that left stay in the bucket, held. Not knowing what left is
1533
- exactly when `writeBucketMemberSlots` is the right door, and its cost is the price of not
1434
+ exactly when `writeSlotContent` is the right door, and its cost is the price of not
1534
1435
  knowing.
1535
1436
 
1536
1437
  The content service commits the list through its protected
@@ -1551,7 +1452,7 @@ empties it once the flush has fully ended, so a script can only ever describe th
1551
1452
  frame a consumer was woken for. A bucket written after that frame (from an
1552
1453
  after-each-frame handler) keeps its script for the next one: the reset clears only a script
1553
1454
  whose `commandsStartedAtRevision`/`commandsStartedAtEpoch` is older than the reading the
1554
- channel slot's last frame opened at. `clearBucketMemberSlots` drains the bucket first and
1455
+ channel slot's last frame opened at. `clearSlotContent` drains the bucket first and
1555
1456
  marks it changed only when the drain removed something, so an already-empty bucket records
1556
1457
  nothing for a call that reports nothing.
1557
1458
 
@@ -1560,47 +1461,43 @@ is created on a `db` and reaches its channel slot through `storeSlot.channelSlot
1560
1461
  strategy that tells nobody. `OIMChannelService.INSTANCE.write` carries the frame guard, which is
1561
1462
  why a whole-bucket write cannot report from inside a flush.
1562
1463
 
1563
- ### How a bucket holds its slots is a fork with two strategies
1464
+ ### How a bucket holds its slots is a fork of content services
1564
1465
 
1565
1466
  | Content service | Storage | One `addBucketMemberSlot` records |
1566
1467
  |---|---|---|
1567
- | `OIMSlotContentBucketServiceSetDriven` | `Set` | one per slot |
1568
- | `OIMSlotContentBucketServiceArrayDriven({ bucketArrayMembership?, bucketCommands? })` | list, plus whatever the list adds | the ordered list's answer |
1569
- | `OIMSlotContentBucketServiceSortedArrayDriven({ compareBucketMemberSlots })` | list ordered by the comparator (`sortedCount` marks the sorted prefix) | one per mention |
1468
+ | `OIMSlotContentSetBucketService` | `Set` | one per slot |
1469
+ | `OIMSlotContentPositionalArrayBucketService({ bucketCommands?, bucketMemberSlotsArrayService? })` | list, plus a `Set` with the unique member slots service | one per mention; one per slot, order kept, with `OIMBucketUniqueMemberSlotsArrayService` |
1470
+ | `OIMSlotContentSortedArrayBucketService({ compareBucketMemberSlots })` | list ordered by the comparator (`sortedCount` marks the sorted prefix) | one per mention |
1570
1471
 
1571
1472
  The positions are the caller's. No read re-decides the order, and the position
1572
1473
  doors below are the only way the list moves.
1573
1474
 
1574
- ### And what naming a slot RECORDS is the array-driven service's own fork
1475
+ ### And what naming a slot RECORDS is a member slots service
1575
1476
 
1576
- | Ordered list | Storage it builds | One `addBucketMemberSlot` counts |
1577
- |---|---|---|
1578
- | `OIMBucketArrayMembershipNonUnique.INSTANCE` (the default) | `TOIMBucketSlotsArrayStore` | one per *mention* — a slot named twice is two |
1579
- | `new OIMBucketArrayMembershipUnique()` | `TOIMBucketSlotsArrayUniqueStore` (list + `Set`) | one per slot, order kept |
1477
+ | `bucketMemberSlotsArrayService` | One `addBucketMemberSlot` counts |
1478
+ |---|---|
1479
+ | `OIMBucketMemberSlotsArrayService` (the default) | one per *mention* — a slot named twice is two |
1480
+ | `new OIMBucketUniqueMemberSlotsArrayService()` | one per slot, order kept |
1580
1481
 
1581
1482
  ```typescript
1582
- const dedupedContentService = new OIMSlotContentBucketServiceArrayDriven<
1583
- CardSlot, TOIMBucketSlotsArrayUniqueStore<CardSlot>
1584
- >({
1585
- bucketArrayMembership: new OIMBucketArrayMembershipUnique<CardSlot>(),
1483
+ const dedupedContentService = new OIMSlotContentPositionalArrayBucketService<CardSlot>({
1484
+ bucketMemberSlotsArrayService: new OIMBucketUniqueMemberSlotsArrayService<CardSlot>(),
1586
1485
  });
1587
1486
  ```
1588
1487
 
1589
- `IOIMBucketArrayMembership` is `createStore`, `addSlot`, `removeSlot`, `hasSlot`, `clearSlots`,
1590
- `containsTheSameSequence`. Only the array-driven content services take one. Their
1591
- `hasBucketMemberSlot` asks the membership's `hasSlot`: `OIMBucketArrayMembershipUnique` answers
1592
- from its `Set`, `OIMBucketArrayMembershipNonUnique` walks the array.
1488
+ The member slots service is the only writer of an array bucket's list and the `Set` beside it, so the two never
1489
+ disagree. With the unique one `hasBucketMemberSlot` answers from the `Set`; with the default it walks the array.
1593
1490
 
1594
1491
  ### Positions the caller decides, and the commands that record them
1595
1492
 
1596
- `createOIMBucketService<CardSlot>(db)` with no `slotContentService` is the bucket collection to
1493
+ `createOIMSlotCollectionService<CardSlot>()` with no `slotContentService` is the bucket collection to
1597
1494
  take when the positions are yours: its default content service is
1598
- `OIMSlotContentBucketServiceArrayDriven` with an `OIMBucketCommands` recording over it.
1495
+ `OIMSlotContentPositionalArrayBucketService` with an `OIMBucketCommands` recording over it.
1599
1496
 
1600
1497
  The position doors follow; a position they return is valid only until the next write. The reads
1601
1498
  `getBucketMemberSlotAt(bucketSlot, index)` and `getBucketMemberSlotPosition(bucketSlot, entitySlot)`
1602
1499
  belong to the array-based content services, and the five writes to
1603
- `OIMSlotContentBucketServiceArrayDriven` (`OIMSlotContentBucketServiceSetDriven` has none of
1500
+ `OIMSlotContentPositionalArrayBucketService` (`OIMSlotContentSetBucketService` has none of
1604
1501
  them, because a `Set` has no place to name, so asking one does not compile):
1605
1502
  `insertBucketMemberSlotAt(bucketSlot, at, entitySlot)`, `removeBucketMemberSlotAt(bucketSlot, at)`,
1606
1503
  `removeBucketMemberSlotRange(bucketSlot, at, count)`, `moveBucketMemberSlotToPosition(bucketSlot, from, to)` and
@@ -1610,8 +1507,8 @@ of the list; `moveBucketMemberSlotToPosition` refuses `from === to` and either e
1610
1507
  range; `moveBucketMemberSlotRangeToPosition` reads its landing in the list **without** the run
1611
1508
  in it. A position or count that is `NaN` or a fraction throws before anything
1612
1509
  moves; `±Infinity` clamps. The default ordered slot list records one place per *mention*, so
1613
- inserting a slot the bucket already holds gives it a second position. Hand the
1614
- content service an `OIMBucketArrayMembershipUnique` when a position must name one entity; a
1510
+ inserting a slot the bucket already holds gives it a second position. Take
1511
+ `OIMBucketUniqueMemberSlotsArrayService` when a position must name one entity; a
1615
1512
  repeat insert into it changes nothing and records nothing.
1616
1513
 
1617
1514
  The content service's protected `removeSlot(bucketSlot, slot)` hands back a `number`: WHERE
@@ -1639,17 +1536,17 @@ from the bucket's for good.
1639
1536
  consumer patches instead of rebuilding:
1640
1537
 
1641
1538
  ```typescript
1642
- const pageService = createOIMBucketService<CardSlot>(db);
1539
+ const pageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1643
1540
  const page = pageService.createStoreSlot(db);
1644
1541
  const bucket = pageService.holdSlotByKey(page, 'p1');
1645
1542
 
1646
1543
  const rebuildFrom = (slots: Iterable<CardSlot>): void => {};
1647
1544
  const applyToTheRenderedList = (commands: readonly TOIMOrderedListCommand[]): void => {};
1648
1545
 
1649
- effectService.createEffect(db, [bucket], [], () => {
1546
+ effectService.createEffect(db, () => {
1650
1547
  if (bucket.isWholeListRecorded) rebuildFrom(pageService.slotContentService.getBucketMemberSlots(bucket));
1651
1548
  else applyToTheRenderedList(bucket.commands ?? []);
1652
- });
1549
+ }, [bucket]);
1653
1550
  ```
1654
1551
 
1655
1552
  The content service already writes the bucket slot on every mutation, so the consumer
@@ -1666,11 +1563,11 @@ a frame has opened after it (`channelSlot.frame.openedAtRevision`/`openedAtEpoch
1666
1563
  recorder clears a stale script before recording into it, so a write that lands after a
1667
1564
  flush, ahead of the recorder's reset, starts a new script instead of extending one
1668
1565
  that was already read. `isWholeListRecorded` is set by a write
1669
- of the whole bucket (`writeBucketMemberSlots`, `clearBucketMemberSlots`, `moveBucketMemberSlotRangeToPosition`), by a
1566
+ of the whole bucket (`writeSlotContent`, `clearSlotContent`, `moveBucketMemberSlotRangeToPosition`), by a
1670
1567
  removal from a bucket whose content service keeps no positions and so has no index to
1671
1568
  patch, and by
1672
1569
  the script outgrowing the list it describes. `maxCommandsPerSlot` on
1673
- `TOIMBucketCommandsOptions` (and on `createOIMBucketService`'s options) is that ratio. It defaults to
1570
+ `TOIMBucketCommandsOptions` (and on `createOIMSlotCollectionService`'s options) is that ratio. It defaults to
1674
1571
  `OIM_MAX_COMMANDS_PER_SLOT`, which is 1, the same ratio
1675
1572
  `oimOrderedListCommandsOfTransition` takes as `maxCommandsPerSlot`. A consumer that animates moves cannot
1676
1573
  use a whole-list answer, because it costs them every move they were going to animate.
@@ -1680,7 +1577,7 @@ grows.
1680
1577
  **Recording is chosen when the content service is built.** `bucketCommands` on
1681
1578
  `TOIMSlotContentBucketServiceOptions` is read once into a readonly field:
1682
1579
  `new OIMBucketCommands({ maxCommandsPerSlot })`. If you leave it out, the content service holds
1683
- `OIMBucketCommandsNone.INSTANCE`, whose methods are empty and take positions instead of a
1580
+ `OIMNoBucketCommands.INSTANCE`, whose methods are empty and take positions instead of a
1684
1581
  command object, so a bucket that nobody renders positionally allocates no command. There is no
1685
1582
  setter, so a second recorder cannot silently replace the first. The first script recorded on a
1686
1583
  channel slot subscribes one reset to `OIMChannelService.INSTANCE.subscribeAfterEachFlush` for
@@ -1694,7 +1591,7 @@ per-bucket state, so **one instance may serve two bucket collections**.
1694
1591
  ### The other strategy: an index that fills itself
1695
1592
 
1696
1593
  ```typescript
1697
- const indexService = createOIMIndexService(db, {
1594
+ const indexService = createOIMSlotCollectionIndexService({
1698
1595
  entityService: cardService,
1699
1596
  keyOf: (card: Card) => card.deck, // which key an entity belongs to
1700
1597
  });
@@ -1703,10 +1600,10 @@ const index = indexService.createIndexStoreSlot(cards);
1703
1600
  const render = (slots: unknown[]): void => {};
1704
1601
 
1705
1602
  const spades = indexService.holdSlotByKey(index, 'spades'); // the hold mints the bucket and fills it
1706
- effectService.createEffect(db, [spades], [], () => render(Array.from(indexService.slotContentService.getBucketMemberSlots(spades))));
1603
+ effectService.createEffect(db, () => render(Array.from(indexService.slotContentService.getBucketMemberSlots(spades))), [spades]);
1707
1604
  ```
1708
1605
 
1709
- It **is** a bucket collection (`OIMIndexService` extends `OIMSlotCollectionService`), and
1606
+ It **is** a bucket collection (`OIMSlotCollectionIndexService` extends `OIMSlotCollectionService`), and
1710
1607
  `createIndexStoreSlot(cards)` makes its store slot for one entity store slot and files every
1711
1608
  entity of it into the buckets on write, from `keyOf`. You read through the index service
1712
1609
  (`getSlotByKey`, `holdSlotByKey`, `getSlotsByKeys`, `holdSlotsByKeys`, `getSlots`,
@@ -1715,11 +1612,11 @@ entity of it into the buckets on write, from `keyOf`. You read through the index
1715
1612
  `hasBucketMemberSlot`, and on an array-based content service `getBucketMemberSlotAt` and
1716
1613
  `getBucketMemberSlotPosition`). That `slotContentService` is narrowed to those reads, so the
1717
1614
  index's buckets cannot be written from outside. The default content service is
1718
- `OIMSlotContentBucketServiceArrayDriven`; pass `slotContentService:
1719
- new OIMSlotContentBucketServiceSetDriven()` for buckets that keep no positions. Passing
1615
+ `OIMSlotContentPositionalArrayBucketService`; pass `slotContentService:
1616
+ new OIMSlotContentSetBucketService()` for buckets that keep no positions. Passing
1720
1617
  `entityService` and then the entity store slot is the whole wiring: `createIndexStoreSlot(cards)`
1721
1618
  registers a content-change listener on that store slot
1722
- (`OIMContentChangeListenerService.INSTANCE.addContentChangeListener(cards, …)`), and the entity
1619
+ (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(cards, …)`), and the entity
1723
1620
  service knows nothing about indexes.
1724
1621
  `disposeIndexStoreSlot(index)` takes that listener off, empties the buckets and gives back every
1725
1622
  entity held. After that, `getSlotByKey` and `holdSlotByKey` throw, but the two give-backs
@@ -1736,9 +1633,8 @@ path for rendering**, because one edit of one entity wakes the whole list. Subsc
1736
1633
  to the bucket and each entity to its own slot. The any-column slot costs
1737
1634
  something only while somebody holds or subscribes to it.
1738
1635
 
1739
- Batch doors (`upsertEntities`, `insertEntities`/`fillEntities`, `deleteEntities`,
1740
- `clearAllEntities`) notify the index once per batch. The index groups entities by key and moves each
1741
- group with one bucket write.
1636
+ Writes inside `writeInBatch` (or `openBatch` / `closeBatch`) notify the index once per batch. The index
1637
+ groups entities by key and moves each group with one bucket write.
1742
1638
 
1743
1639
  Know these four things before choosing it:
1744
1640
 
@@ -1746,16 +1642,16 @@ Know these four things before choosing it:
1746
1642
  door (`getSlotByKey`, `holdSlotByKey`, `getSlotsByKeys`, `holdSlotsByKeys`) is filled from the source
1747
1643
  entities written before it, without a flush, so minting inside an effect body does not
1748
1644
  throw. `getSlotsByKeys` / `holdSlotsByKeys` fill all new buckets in ONE pass over the source. How long the bucket lives then is the
1749
- collection's ordinary business: the `slotReleasePolicy` option, or `db.slotReleasePolicy`
1750
- when you name none; with neither, the bucket is kept for good. `OIMSlotReleasePolicyAfterIdle`
1751
- cools it on the queue, and `OIMSlotReleasePolicyAtUnhold.INSTANCE` ends it at
1645
+ collection's ordinary business: the `slotReleasePolicy` option, or `defaults.slotReleasePolicy`
1646
+ when you name none; with neither, the bucket is kept for good. `OIMSlotAfterIdleReleasePolicy`
1647
+ cools it on the queue, and `OIMSlotAtUnholdReleasePolicy.INSTANCE` ends it at
1752
1648
  the last give-back. The index injects no lifetime of its own.
1753
1649
  - **The key comes from `columns`, `keyOf`, or both**, the same pair as the sorter's
1754
1650
  `sortColumns` / `comparator`. The key type is inferred from the columns (`TOIMIndexKey`):
1755
1651
  `{ columns: ['deck'] }` is keyed by that one column's value and refuses an array;
1756
- `createOIMIndexServiceByCompositeKey(db, { entityService, columns: ['deck', 'suit'] })` is keyed by the
1652
+ `createOIMSlotCollectionIndexService({ entityService, columns: ['deck', 'suit'] })` is keyed by the
1757
1653
  tuple of their values and builds the bucket collection on a tree-driven store
1758
- (`OIMSlotStoreServiceTreeDriven`) by itself, while `createOIMIndexService` without a
1654
+ (`OIMSlotTreeStoreService`) by itself, while `createOIMSlotCollectionIndexService` without a
1759
1655
  `bucketSlotStoreService` refuses two columns. When the index asks
1760
1656
  whether a key moved, it asks its own `isSameKey(key, otherKey)`, which
1761
1657
  every collection answers from its own slot store service.
@@ -1773,33 +1669,33 @@ Know these four things before choosing it:
1773
1669
  ### A sorted list is a computed over an index
1774
1670
 
1775
1671
  ```typescript
1776
- const indexService = createOIMIndexService(db, {
1672
+ const indexService = createOIMSlotCollectionIndexService({
1777
1673
  entityService: cardService,
1778
1674
  keyOf: (card: Card) => card.deck,
1779
1675
  });
1780
1676
  const index = indexService.createIndexStoreSlot(cards);
1781
1677
 
1782
- const byRankInDeckService = createOIMComputedServiceSorted({
1678
+ const byRankInDeckService = createOIMSortedComputedService({
1783
1679
  indexService,
1784
- sortColumns: [{ column: 'rank', direction: 'asc' }],
1680
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }],
1785
1681
  });
1786
1682
  const byRankInDeck = byRankInDeckService.createSortedStoreSlot(index);
1787
1683
 
1788
1684
  const spades = byRankInDeckService.computedCollectionService.holdSlotByKey(byRankInDeck, 'spades');
1789
- effectService.createEffect(db, [], [spades], () => { console.log(spades.computedValueService.getValueForSlot(spades).map((entitySlot) => entitySlot.entity?.title)); });
1685
+ effectService.createEffect(db, () => { console.log(spades.computedValueService.getValueForSlot(spades).map((entitySlot) => entitySlot.entity?.title)); }, [], [spades]);
1790
1686
  spades.owner.unholdSlot(spades);
1791
1687
  ```
1792
1688
 
1793
- `createOIMComputedServiceSorted({ indexService, sortColumns?, comparator?, slotReleasePolicy? })`
1689
+ `createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy? })`
1794
1690
  takes no `db`: its store slot is created from the index store slot and runs on that one's
1795
- channel slot. It returns an `OIMComputedServiceSorted`, whose
1691
+ channel slot. It returns an `OIMSortedComputedService`, whose
1796
1692
  `createSortedStoreSlot(indexStoreSlot)` makes one store slot over one index store slot. Its doors
1797
1693
  are on its `computedCollectionService`: `getSlotByKey`, `holdSlotByKey`, `findSlotByKey`, each
1798
1694
  with that store slot first. A key's value, read with
1799
1695
  `computedSlot.computedValueService.getValueForSlot(computedSlot)`, is that key's entity slots,
1800
- sorted. `indexService` is the `OIMIndexService` whose buckets it sorts.
1696
+ sorted. `indexService` is the `OIMSlotCollectionIndexService` whose buckets it sorts.
1801
1697
 
1802
- - `sortColumns` is `readonly TOIMSortColumn<TEntity>[]`, each `{ column, direction: 'asc' | 'desc' }`.
1698
+ - `sortColumns` is `readonly TOIMSortColumn<TEntity>[]`, each `{ columnName, direction: 'asc' | 'desc' }`.
1803
1699
  `column` is a `TOIMSortableColumnName<TEntity>`, meaning a column whose values are strings,
1804
1700
  numbers or bigints. **Columns decide which writes re-sort.**
1805
1701
  - `comparator` is `(left: TEntity, right: TEntity) => number`. **It decides the order.**
@@ -1813,8 +1709,8 @@ sorted. `indexService` is the `OIMIndexService` whose buckets it sorts.
1813
1709
  `sortColumns` omits does not re-sort. Use both when the order needs a rule
1814
1710
  `oimColumnsComparator` cannot express (a locale compare, a tie-break) and the sort
1815
1711
  should still wake only for the columns that rule reads.
1816
- - `slotReleasePolicy` (default `OIMSlotReleasePolicyAtUnhold.INSTANCE`) decides whether a key's
1817
- list outlives its last hold. `OIMSlotReleasePolicyNever.INSTANCE` keeps every key ever
1712
+ - `slotReleasePolicy` (default `OIMSlotAtUnholdReleasePolicy.INSTANCE`) decides whether a key's
1713
+ list outlives its last hold. `OIMSlotNeverReleasePolicy.INSTANCE` keeps every key ever
1818
1714
  read and re-sorts only on a read that finds it changed, which suits a view that switches
1819
1715
  between groups. Give the index the same policy, or its bucket is rebuilt instead.
1820
1716
 
@@ -1832,28 +1728,28 @@ The other strategy keeps the index's own buckets in order, so a read needs no co
1832
1728
  The consumer picks one at construction.
1833
1729
 
1834
1730
  ```typescript
1835
- const cardsByDeckService = createOIMIndexServiceSortedArrayDriven(db, {
1731
+ const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService({
1836
1732
  entityService: cardService,
1837
1733
  columns: ['deck'],
1838
- sortColumns: [{ column: 'rank', direction: 'asc' }, { column: 'id', direction: 'desc' }],
1734
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }, { columnName: 'id', direction: 'desc' }],
1839
1735
  });
1840
1736
  const cardsByDeck = cardsByDeckService.createIndexStoreSlot(cards);
1841
1737
  const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
1842
1738
  Array.from(cardsByDeckService.slotContentService.getBucketMemberSlots(spades));
1843
1739
  ```
1844
1740
 
1845
- `createOIMIndexServiceSortedArrayDriven(db, { entityService, columns? | keyOf?, sortColumns, slotReleasePolicy?, bucketSlotStoreService? })`
1846
- returns an `OIMIndexServiceSortedArrayDriven`, whose content service is an
1847
- `OIMSlotContentBucketServiceSortedArrayDriven`. `sortColumns` is the same `readonly TOIMSortColumn<TEntity>[]`
1848
- that `createOIMComputedServiceSorted` takes.
1741
+ `createOIMSlotCollectionSortedArrayIndexService({ entityService, columns? | keyOf?, sortColumns, slotReleasePolicy?, bucketSlotStoreService? })`
1742
+ returns an `OIMSlotCollectionSortedArrayIndexService`, whose content service is an
1743
+ `OIMSlotContentSortedArrayBucketService`. `sortColumns` is the same `readonly TOIMSortColumn<TEntity>[]`
1744
+ that `createOIMSortedComputedService` takes.
1849
1745
 
1850
1746
  - Writes append to an unsorted tail of the bucket; the first read settles it, by binary
1851
1747
  insertion for a short tail or by one sort for a long one.
1852
1748
  - The index re-files a member when one of its sort columns changes, and only then.
1853
1749
  - `getBucketMemberSlots`, `getBucketMemberSlotAt` and `getBucketMemberSlotPosition` answer in order.
1854
- - The bucket has no positional writes, and its bucket commands are always `OIMBucketCommandsNone.INSTANCE`.
1750
+ - The bucket has no positional writes, and its bucket commands are always `OIMNoBucketCommands.INSTANCE`.
1855
1751
  - Sort-at-write suits point updates, where a rank change re-files one member. Sort-at-read
1856
- (`createOIMComputedServiceSorted` over any index) suits bulk loads.
1752
+ (`createOIMSortedComputedService` over any index) suits bulk loads.
1857
1753
 
1858
1754
  ### Bucket column slots
1859
1755
 
@@ -1864,23 +1760,23 @@ bucket slot, and it is written when a write changes that column on any member of
1864
1760
  bucket. `markBucketColumnsChanged(bucketSlot, patch, changedColumns)` on the bucket content
1865
1761
  service is the door the index calls, and the door for whoever changes the members of a
1866
1762
  bucket slot they hold, a single bucket's included. The read side of this is the
1867
- `OIMIndexService`: it carries its buckets' doors, the bucket column slot doors and the
1763
+ `OIMSlotCollectionIndexService`: it carries its buckets' doors, the bucket column slot doors and the
1868
1764
  bucket any-column slot doors (`getAnyBucketColumnByKey(indexStoreSlot, key)`,
1869
1765
  `holdAnyBucketColumnByKey(indexStoreSlot, key)`; both are given back with
1870
1766
  `indexService.unholdColumn`), and its `slotContentService` carries the member reads,
1871
1767
  the positional ones of the array-based kinds included.
1872
1768
 
1873
1769
  ```typescript
1874
- const decksService = createOIMIndexService(db, {
1770
+ const decksService = createOIMSlotCollectionIndexService({
1875
1771
  entityService: cardService,
1876
1772
  keyOf: (card: Card) => card.deck,
1877
1773
  });
1878
1774
  const decks = decksService.createIndexStoreSlot(cards);
1879
1775
 
1880
1776
  const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
1881
- const onRankInSpades = effectService.createEffect(db, [rankOfSpades], [], () => {});
1777
+ const onRankInSpades = effectService.createEffect(db, () => {}, [rankOfSpades]);
1882
1778
  effectService.disposeEffect(onRankInSpades);
1883
- decksService.unholdColumn(rankOfSpades);
1779
+ decksService.columnSlotService.unholdColumn(rankOfSpades);
1884
1780
  ```
1885
1781
 
1886
1782
  ### Index keys strategy
@@ -1890,24 +1786,24 @@ leaves its old bucket in O(1). Only an index writes it, through its `IOIMIndexKe
1890
1786
  (`get(slot, position)`, `set(slot, position, indexKey)`), at the position its index store
1891
1787
  slot was given (`TOIMIndexStoreSlot.indexKeysPosition`, allocated from the source store slot's
1892
1788
  `indexKeysPositions`). Choose the service with the `indexKeysService` option of `OIMDB`; every
1893
- index the factories build on that `db` takes it (default `OIMDefaults.INDEX_KEYS_SERVICE.getValue()`).
1789
+ index the factories build on that `db` takes it (default `OIMAppendingIndexKeysService.INSTANCE`).
1894
1790
 
1895
- - `OIMIndexKeysServiceAppending` (default): a disposed index's position is never
1791
+ - `OIMAppendingIndexKeysService` (default): a disposed index's position is never
1896
1792
  reused, and its stale entry stays on each entity slot. A slot's `indexKeys` grows to the highest
1897
1793
  position any index wrote on it, so after K indexes are made and disposed, a slot the
1898
1794
  last one filed carries an array of K, and a stale entry keeps its key value
1899
1795
  reachable. A write pays nothing for a disposed index: the store slot keeps only the
1900
1796
  live listeners, in the order they were added.
1901
- - `OIMIndexKeysServiceReclaiming`: on dispose, it wipes that position on every entity slot of the
1797
+ - `OIMReclaimingIndexKeysService`: on dispose, it wipes that position on every entity slot of the
1902
1798
  source store slot (O(N)) and reuses it.
1903
1799
 
1904
- Switch to `OIMIndexKeysServiceReclaiming` only if you create indexes dynamically and often.
1800
+ Switch to `OIMReclaimingIndexKeysService` only if you create indexes dynamically and often.
1905
1801
  Creating indexes that way is expensive; better not to.
1906
1802
 
1907
1803
  ### Rendering an ordered bucket
1908
1804
 
1909
1805
  ```typescript setup
1910
- const orderedBucketsService = createOIMBucketService<CardSlot>(db);
1806
+ const orderedBucketsService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1911
1807
  const orderedBuckets = orderedBucketsService.createStoreSlot(db);
1912
1808
  ```
1913
1809
 
@@ -1930,11 +1826,10 @@ for (const command of oimOrderedListCommandsOfTransition(whatIRendered, Array.fr
1930
1826
  is typed `Iterable` and not an array. A `prev` kept live is the same array as `next`
1931
1827
  on the following frame, so the diff comes back empty, no reorder is ever drawn,
1932
1828
  and nothing throws. Keep the `Array.from(...)` copy, or subscribe to
1933
- `createOIMComputedServiceBucketMemo`, whose value is a fresh array on every write
1829
+ `createOIMBucketMemoComputedService`, whose value is a fresh array on every write
1934
1830
  that changed the list and the same array while it did not.
1935
1831
 
1936
- A list may name one slot twice: `OIMBucketArrayMembershipNonUnique`, the default strategy
1937
- of `OIMSlotContentBucketServiceArrayDriven`, records every mention. The mentions of one identity
1832
+ A list may name one slot twice: `OIMSlotContentPositionalArrayBucketService` records every mention. The mentions of one identity
1938
1833
  are matched in the order they appear, and the script reproduces the new list
1939
1834
  mention for mention. The minimal move count is claimed for lists whose identities
1940
1835
  are distinct.
@@ -1948,8 +1843,7 @@ channel service.
1948
1843
  ## Derivations
1949
1844
 
1950
1845
  ```typescript setup
1951
- const totalService = createOIMComputedService<void, number, TOIMSingleSlotStore<TOIMComputedSlot<number, void>>>({
1952
- computedSlotStoreService: new OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<number, void>>(),
1846
+ const totalService = createOIMSingleComputedService<number>({
1953
1847
  sameValueGuard: oimSameValueZeroGuard, // did the value move?
1954
1848
  });
1955
1849
  const total = totalService.createComputedStoreSlot(
@@ -1959,7 +1853,7 @@ const total = totalService.createComputedStoreSlot(
1959
1853
  sourceSlots: [slot], // root slots it reads
1960
1854
  }),
1961
1855
  );
1962
- const totalSlot = totalService.computedCollectionService.getSlotByKey(total, undefined);
1856
+ const totalSlot = totalService.computedSingleSlotService.getSingleSlot(total);
1963
1857
  ```
1964
1858
 
1965
1859
  `createSources` returns `sourceSlots` (root slots it reads) and `sourceComputeds` (derivations it reads). Both keys are optional; a missing one means none.
@@ -1988,14 +1882,14 @@ this engine (`createEffect`'s second argument, and `sourceComputeds` in
1988
1882
  it is. Inside `derive`, `computedSlot.value` is the previous value:
1989
1883
 
1990
1884
  ```typescript
1991
- effectService.createEffect(db, [], [totalSlot], () => { console.log(totalSlot.computedValueService.getValueForSlot(totalSlot)); });
1885
+ effectService.createEffect(db, () => { console.log(totalSlot.computedValueService.getValueForSlot(totalSlot)); }, [], [totalSlot]);
1992
1886
  ```
1993
1887
 
1994
1888
  ### One derivation per key
1995
1889
 
1996
1890
  ```typescript setup
1997
1891
  const ranksService = createOIMComputedService<TOIMStoreKey, number>({
1998
- createSources: (id) => ({ sourceSlots: [cardService.getSlotByKey(cards, id)] }),
1892
+ createSources: ({ key }) => ({ sourceSlots: [cardService.getSlotByKey(cards, key)] }),
1999
1893
  derive: (computedSlot) => cardService.getSlotByKey(cards, computedSlot.key).entity?.rank ?? -1,
2000
1894
  sameValueGuard: oimSameValueZeroGuard,
2001
1895
  initialValue: -1,
@@ -2017,23 +1911,21 @@ holds every entry of `sourceSlots` and `sourceComputeds`, and
2017
1911
  the content strategy's `releaseSlotContent` gives all of them back. So the source list IS the dependency
2018
1912
  list, and a source that a build closes over without naming is a source that nothing holds.
2019
1913
 
2020
- A derivation is never empty: `OIMSlotContentComputedStrategy.isSlotEmpty` answers `false`,
1914
+ A derivation is never empty: `OIMSlotContentComputedService.isSlotEmpty` answers `false`,
2021
1915
  so a filed derivation goes only when nothing holds it,
2022
1916
  nothing watches it, and the grace is `0`. That is what
2023
- `OIMSlotReleasePolicyAtUnhold.INSTANCE` answers, and it is the default when no `slotReleasePolicy` is given.
2024
- A derivation let go of while the channel is flushing is not released on the spot. It is
2025
- handed to the channel's unused-slot collector and released when the flush ends, the same
2026
- deferral that every other non-empty slot gets.
1917
+ `OIMSlotAtUnholdReleasePolicy.INSTANCE` answers, and it is the default when no `slotReleasePolicy` is given.
1918
+ A derivation let go of while the channel is flushing is released on the spot, like every other slot.
2027
1919
  `createOIMComputedService` takes a `slotReleasePolicy` as an optional key of its options object.
2028
1920
 
2029
1921
  `createOIMComputedService` is `OIMComputedService` with its generics
2030
1922
  inferred and its computed slot store service filled with a map-driven one unless you pass
2031
- `computedSlotStoreService`, which a `void` key requires (`OIMSlotStoreServiceSingleSlotDriven`); both are exported.
1923
+ `computedSlotStoreService`, which a `void` key requires (`OIMSingleSlotStoreService`); both are exported.
2032
1924
 
2033
1925
  ### Computed columns
2034
1926
 
2035
1927
  A computed's value can be subscribed to by field. On the `computedCollectionService` of
2036
- `OIMComputedService` (and of `OIMComputedServiceSorted` and `OIMComputedServiceBucketMemo`),
1928
+ `OIMComputedService` (and of `OIMSortedComputedService` and `OIMBucketMemoComputedService`),
2037
1929
  keyed or single, where a single takes `getSlotByKey(storeSlot, undefined)` as the slot, sit `getColumn(slot, columnName)`, `holdColumn(slot, columnName)`,
2038
1930
  `getColumnGroup(names)`, `holdColumnGroup(slot, group)`, `unholdColumn(columnSlot)` and the any-column
2039
1931
  pair `getAnyColumn(slot)` / `holdAnyColumn(slot)`. Column
@@ -2046,19 +1938,17 @@ the group's fields for a group, ordered by the sorted column names; the whole va
2046
1938
 
2047
1939
  ```typescript
2048
1940
  type TCardSummary = { top: number; count: number };
2049
- const cardSummaryService = createOIMComputedService<void, TCardSummary, TOIMSingleSlotStore<TOIMComputedSlot<TCardSummary, void>>>({
2050
- computedSlotStoreService: new OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<TCardSummary, void>>(),
2051
- });
1941
+ const cardSummaryService = createOIMSingleComputedService<TCardSummary>();
2052
1942
  const cardSummary = cardSummaryService.createComputedStoreSlot(
2053
1943
  db,
2054
1944
  () => ({ top: slot.entity?.rank ?? -1, count: cardService.getSlotCount(cards) }),
2055
1945
  () => ({ sourceSlots: [slot] }),
2056
1946
  );
2057
- const summaryCollectionService = cardSummaryService.computedCollectionService;
2058
- const topColumn = summaryCollectionService.holdColumn(summaryCollectionService.getSlotByKey(cardSummary, undefined), 'top');
2059
- const onTop = effectService.createEffect(db, [], [topColumn], () => { console.log(topColumn.value); });
1947
+ const summarySingleSlotService = cardSummaryService.computedSingleSlotService;
1948
+ const topColumn = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(cardSummary), 'top');
1949
+ const onTop = effectService.createEffect(db, () => { console.log(topColumn.value); }, [], [topColumn]);
2060
1950
  effectService.disposeEffect(onTop);
2061
- summaryCollectionService.unholdColumn(topColumn);
1951
+ summarySingleSlotService.columnSlotService.unholdColumn(topColumn);
2062
1952
  ```
2063
1953
 
2064
1954
  ### The order of a bucket is one of those derivations
@@ -2067,14 +1957,14 @@ Subscribe to the DERIVATION instead of the bucket when a consumer wants the
2067
1957
  list as an array it can compare by identity:
2068
1958
 
2069
1959
  ```typescript
2070
- const orderService = createOIMComputedServiceBucketMemo({
1960
+ const orderService = createOIMBucketMemoComputedService({
2071
1961
  bucketCollectionService: orderedBucketsService,
2072
1962
  });
2073
1963
  const order = orderService.createBucketMemoStoreSlot(orderedBuckets);
2074
1964
  const render = (slots: readonly CardSlot[]): void => {};
2075
1965
 
2076
1966
  const deck = orderService.computedCollectionService.holdSlotByKey(order, 'spades'); // mints and holds the bucket too
2077
- effectService.createEffect(db, [], [deck], () => render(deck.computedValueService.getValueForSlot(deck)));
1967
+ effectService.createEffect(db, () => render(deck.computedValueService.getValueForSlot(deck)), [], [deck]);
2078
1968
  ```
2079
1969
 
2080
1970
  Its value is the bucket's slots in the bucket's order, produced on the read that
@@ -2087,7 +1977,7 @@ The source list is ONE slot, the bucket, and it never grows: a change to its slo
2087
1977
  is a move of the bucket's value, not of the derivation's sources.
2088
1978
 
2089
1979
  A sorted list is the other derivation of this kind:
2090
- [`createOIMComputedServiceSorted`](#a-sorted-list-is-a-computed-over-an-index) over an index.
1980
+ [`createOIMSortedComputedService`](#a-sorted-list-is-a-computed-over-an-index) over an index.
2091
1981
 
2092
1982
  ---
2093
1983
 
@@ -2096,29 +1986,27 @@ A sorted list is the other derivation of this kind:
2096
1986
  ```typescript
2097
1987
  type Settings = { theme: string; fontSize: number };
2098
1988
 
2099
- const settingsService = createOIMSingleEntityService<Settings>(db, {
2100
- columnNames: ['theme', 'fontSize'],
2101
- });
1989
+ const settingsService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<Settings>(), columnNames: ['theme', 'fontSize'] });
2102
1990
  const settings = settingsService.createStoreSlot(db);
2103
1991
  const settingsContentService = settingsService.slotContentService;
2104
- settingsContentService.upsertEntity(settings, { theme: 'dark', fontSize: 14 });
1992
+ settingsService.writeSingleSlotContent(settings, { theme: 'dark', fontSize: 14 });
2105
1993
  const settingsSlot = settingsService.holdSingleSlot(settings);
2106
1994
 
2107
- effectService.createEffect(db, [settingsSlot], [], () => { console.log(settingsSlot.entity?.theme); });
2108
- effectService.createEffect(db, [settingsService.holdColumn(settingsSlot, 'theme')], [], () => {});
1995
+ effectService.createEffect(db, () => { console.log(settingsSlot.entity?.theme); }, [settingsSlot]);
1996
+ effectService.createEffect(db, () => {}, [settingsService.columnSlotService.holdColumn(settingsSlot, 'theme')]);
2109
1997
 
2110
1998
  settingsContentService.updateEntity(settingsSlot, { theme: 'light' });
2111
1999
  settingsContentService.deleteEntityColumns(settingsSlot, ['fontSize']); // the thing a patch cannot say
2112
- settingsContentService.writeEntity(settingsSlot, { theme: 'dark', fontSize: 16 }); // columns it had and this does not, go
2000
+ settingsService.slotContentService.writeSlotContent(settingsSlot, { theme: 'dark', fontSize: 16 }); // columns it had and this does not, go
2113
2001
  db.flush();
2114
2002
  ```
2115
2003
 
2116
- A standalone record is a single service: `createOIMSingleEntityService` is
2004
+ A standalone record is a single service: `createOIMSingleSlotService` is
2117
2005
  `createOIMSingleSlotService` over `createOIMSlotContentEntityService`, with the store fixed to
2118
- `OIMSlotStoreServiceSingleSlotDriven` and no `keyOf`. Every option is optional:
2119
- `entityWriter` and `slotEmptiness` fall back to `OIMDefaults`, and `columnNames` names the
2006
+ `OIMSingleSlotStoreService` and no `keyOf`. Every option is optional:
2007
+ `entityWriter` and `slotEmptinessPolicy` fall back to `defaults`, then to the built-in defaults, and `columnNames` names the
2120
2008
  columns and files their slots in the ordinal store. The entity is
2121
- written through `slotContentService.upsertEntity(settings, entity)`, an ordinary write announced
2009
+ written through `slotContentService.writeSlotContentByKey(settings, entity)`, an ordinary write announced
2122
2010
  to whoever subscribed, not handed to the options or to `createStoreSlot(db)`, which mints
2123
2011
  nothing. The column doors sit on the single service itself: `getColumn(slot, columnName)`,
2124
2012
  `holdColumn(slot, columnName)`, `getColumnGroup(names)`, `holdColumnGroup(slot, group)`,
@@ -2135,7 +2023,7 @@ always reports.
2135
2023
 
2136
2024
  Keep your own `Map` or `WeakMap`, or use `TOIMEntitySlot.data`
2137
2025
  (`unknown[] | undefined`). It belongs to you, and the core never reads or writes
2138
- it. Clearing or removing the slot (`slotContentService.deleteEntity(slot)`, `slotContentService.deleteEntities(storeSlot, …)`)
2026
+ it. Clearing the slot (`slotContentService.clearSlotContent(slot)`)
2139
2027
  does not reset it either, so clear what you put there yourself. Each index keeps its last key for
2140
2028
  the entity in a separate field, `indexKeys`, which is read and written only through the
2141
2029
  index's `IOIMIndexKeysService` (`get` / `set`).
@@ -2153,7 +2041,7 @@ const body = (): void => {};
2153
2041
  const entitySlot = cardService.holdSlotByKey(cards, 'c1');
2154
2042
 
2155
2043
  const disposers: TOIMDisposer[] = [];
2156
- disposers.push(effectService.subscribeEffect(db, [entitySlot], [], body));
2044
+ disposers.push(effectService.subscribeEffect(db, body, [entitySlot]));
2157
2045
  disposers.push(() => { entitySlot.owner.unholdSlot(entitySlot); });
2158
2046
  for (let i = disposers.length - 1; i >= 0; i--) disposers[i]();
2159
2047
  ```
@@ -2165,87 +2053,91 @@ a throwing disposer does not stop the rest. The engine ships no scope to do that
2165
2053
 
2166
2054
  ## Export reference
2167
2055
 
2168
- **200 names. This list is `src/index.ts`; nothing else is exported.**
2056
+ **230 names. This list is `src/index.ts`; nothing else is exported.**
2169
2057
 
2170
2058
  **The channel slot** — `OIMChannelService`, `TOIMChannelSlot`, `TOIMChannelSlotOptions`, `createOIMChannelSlot`,
2171
2059
  `IOIMChannelMap`, `TOIMChannelFrame`, `TOIMChannelClock`, `TOIMChannelScheduler`, `OIMChannelQueue` (**type only**),
2172
2060
  `oimIsAnySlotChangedAfter` (whether any of the slots changed after a given revision and epoch).
2173
2061
 
2174
- **Scheduling and errors** — `OIMFlushSchedulerService`, `TOIMFlushSchedulerSlot`, `TOIMTimer`, `TOIMDisposer`,
2175
- `IOIMErrorPolicy`, `OIMErrorPolicyLogAndContinue`, `TOIMErrorPolicyLogAndContinueOptions`, `OIMErrorPolicyThrowAtOnce`.
2062
+ **Scheduling and errors** — `OIMFlushSchedulerService`, `TOIMFlushSchedulerSlot`, `IOIMFlushSchedulerListener`, `TOIMTimer`, `TOIMDisposer`,
2063
+ `IOIMErrorPolicy`, `OIMLogAndContinueErrorPolicy`, `TOIMLogAndContinueErrorPolicyOptions`, `OIMThrowAtOnceErrorPolicy`.
2176
2064
 
2177
2065
  **Release policies and collectors** — `IOIMSlotReleasePolicy`,
2178
- `OIMSlotReleasePolicyAfterWrite`, `TOIMSlotReleasePolicyAfterWriteOptions`,
2179
- `OIMSlotReleasePolicyAfterIdle`, `TOIMSlotReleasePolicyAfterIdleOptions`,
2180
- `OIMSlotReleasePolicyAfterUnhold`, `TOIMSlotReleasePolicyAfterUnholdOptions`,
2181
- `OIMSlotReleasePolicyAtUnhold`, `OIMSlotReleasePolicyAtEmpty`, `OIMSlotReleasePolicyNever`, `OIMSlotReleasePolicyEarliest`,
2066
+ `OIMSlotAfterWriteReleasePolicy`, `TOIMSlotAfterWriteReleasePolicyOptions`,
2067
+ `IOIMSlotReleaseDueFramePolicy`, `OIMSlotReleaseAutoDueFramePolicy`, `OIMSlotReleaseManualDueFramePolicy`,
2068
+ `OIMSlotAfterIdleReleasePolicy`, `TOIMSlotAfterIdleReleasePolicyOptions`,
2069
+ `OIMSlotAfterUnholdReleasePolicy`, `TOIMSlotAfterUnholdReleasePolicyOptions`,
2070
+ `OIMSlotAtUnholdReleasePolicy`, `OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`, `OIMSlotEarliestReleasePolicy`,
2182
2071
  `OIMUnusedSlotCollectorService`, `TOIMUnusedSlotCollectorSlot`, `createOIMUnusedSlotCollectorSlot`,
2183
2072
  `TOIMCollectorSlot`, `TOIMCollectorSlotOptions`, `createOIMCollectorSlot`,
2184
- `OIMReleaseSlotCollectorService`, `OIMReleaseSlotCollectorServiceAfterIdle`, `OIMReleaseSlotCollectorServiceAfterUnhold`,
2185
- `OIMReleaseSlotCollectorServiceAfterWrite`, `OIMReleaseSlotQueueService`, `TOIMReleaseSlotCollectorSlot`,
2186
- `createOIMReleaseSlotCollectorSlot`, `oimSliceOfMs`, `OIM_SLICE_LENGTH_MS`.
2187
-
2188
- **Slot services** — `OIMSlotColumnService` (the column doors both slot services inherit), `OIMSlotCollectionService`, `TOIMSlotCollectionServiceOptions`, `createOIMSlotCollectionService`,
2189
- `OIMSingleSlotService`, `TOIMSingleSlotServiceOptions`, `createOIMSingleSlotService`, `OIMSlotOwnerService` (**type only**),
2190
- `TOIMSlotOwnerServiceOptions`, `IOIMSlotContentStrategy`, `IOIMSlotContentFlushStrategy`, `IOIMSubframeCloseListener`,
2191
- `OIMContentChangeListenerService`, `IOIMContentChangeListener`, `IOIMStoreSlotListener`, `OIMStoreSlotListenerNone`.
2192
-
2193
- **Slots and their owners** — `TOIMSlot`, `TOIMStoreSlot`, `TOIMSingleSlotStoreSlot`, `TOIMSlotEditable`, `TOIMKeyedSlot`,
2194
- `TOIMIndexableSlot`, `OIMSlotOwner`, `IOIMSlotOwner`, `IOIMSlotEmptiness`, `IOIMSlotStamp`, `TOIMSameValueGuard`,
2073
+ `OIMSlotWheel`, `TOIMSlotWheelOptions`, `OIMSlotReleaseWheel`, `TOIMSlotReleaseWheelOptions`,
2074
+ `oimMintAfterWriteSlotReleaseWheel`, `oimMintAfterIdleSlotReleaseWheel`, `oimMintAfterUnholdSlotReleaseWheel`,
2075
+ `OIMSlotRefetchWheel`, `TOIMSlotRefetchWheelOptions`, `IOIMSlotRefetchListener`,
2076
+ `oimSliceOfMs`, `OIM_SLICE_LENGTH_MS`.
2077
+
2078
+ **Slot services** — `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `TOIMSlotCollectionServiceOptions`, `createOIMSlotCollectionService`,
2079
+ `OIMSingleSlotService`, `OIMWritableSingleSlotService`, `TOIMSingleSlotServiceOptions`, `createOIMSingleSlotService`,
2080
+ `OIMColumnSlotService` (the column doors, `service.columnSlotService`), `OIMSlotOwnerService` (**type only**),
2081
+ `TOIMSlotOwnerServiceOptions`, `IOIMSlotContentService`, `IOIMSlotContentWriter`, `IOIMKeyedSlotCreator`, `IOIMSingleSlotCreator`,
2082
+ `IOIMSlotFilingService`, `TOIMSlotContentServiceKey`, `TOIMSlotContentServiceSlot`, `TOIMSlotContentServiceSingleSlot`,
2083
+ `TOIMSlotContentServiceContent`, `TOIMSlotContentServicePatch`,
2084
+ `OIMSlotContentChangeListenerService`, `IOIMSlotContentChangeListener`, `IOIMStoreSlotListener`, `OIMNoStoreSlotListener`,
2085
+ `IOIMSlotSubscriptionListener`, `OIMNoSlotSubscriptionListener`.
2086
+
2087
+ **Slots and their owners** — `TOIMSlot`, `TOIMStoreSlot`, `TOIMSingleSlotStoreSlot`, `TOIMEditable`, `TOIMKeyedSlot`,
2088
+ `TOIMIndexableSlot`, `OIMSlotOwner`, `IOIMSlotOwner`, `IOIMSlotEmptinessPolicy`, `IOIMSlotStamp`, `TOIMSameValueGuard`,
2195
2089
  `oimSameValueZeroGuard`, `oimIsSlotEmpty`, `oimIsSlotReleased`, `oimHoldSlots`, `oimUnholdSlotsIfAlive`,
2196
2090
  `OIM_FROZEN_EMPTY_SLOTS`.
2197
2091
 
2198
2092
  **Columns** — `TOIMColumnSlot`, `TOIMComputedColumnSlot`, `TOIMColumnGroup`, `TOIMColumnReaders`,
2199
- `OIMColumnOwner`, `OIMColumnOwnerPushDriven` (**type only**), `OIMColumnOwnerPullDriven`, `TOIMColumnOwnerOptions`.
2093
+ `OIMColumnSlotOwner`, `OIMPushColumnSlotOwner` (**type only**), `OIMPullColumnSlotOwner`, `TOIMColumnSlotOwnerOptions`.
2200
2094
 
2201
2095
  **Entities** — `OIMSlotContentEntityService`, `TOIMSlotContentEntityServiceOptions`, `TOIMEntitySlotOwnerService`,
2202
- `TOIMEntitySlot`, `createOIMSlotContentEntityService`, `TOIMSlotContentEntityServiceFactoryOptions`, and the convenience wrappers `createOIMEntityService` and
2203
- `createOIMSingleEntityService`; `IOIMEntityWriter`, `createOIMEntityWriterInPlace`,
2204
- `createOIMEntityWriterReplacing`, `TOIMEntityWriterOptions`, `TOIMEntityInsertStrategy`,
2096
+ `TOIMEntitySlot`, `TOIMEntityContentSlot`, `createOIMSlotContentEntityService`; `IOIMEntityWriter`, `createOIMInPlaceEntityWriter`,
2097
+ `createOIMReplacingEntityWriter`, `TOIMEntityWriterOptions`, `TOIMEntityInserter`,
2205
2098
  `oimInsertEntityWithCopy`, `oimInsertEntityWithoutCopy`, `oimInsertEntityWithSanitizedCopy`, `oimPatchEntityColumns`,
2206
- `oimPatchEntityColumn`, `oimPatchEntityColumnsGuarded`, `oimPatchEntityColumnGuarded`, `oimPutEntityColumns`,
2207
- `oimPutEntityColumnsGuarded`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`.
2208
-
2209
- **Buckets** — `OIMSlotContentBucketService`, `OIMSlotContentBucketServiceArrayBased`,
2210
- `OIMSlotContentBucketServiceArrayDriven`, `OIMSlotContentBucketServiceSetDriven`,
2211
- `OIMSlotContentBucketServiceSortedArrayDriven`, `TOIMSlotContentBucketServiceOptions`,
2212
- `TOIMSlotContentBucketServiceSortedArrayDrivenOptions`, `TOIMBucketSlot`, `TOIMBucketSlotOwnerService`,
2213
- the convenience wrappers `createOIMBucketService` and `createOIMSingleBucketService`,
2214
- `TOIMBucketCollectionFactoryArguments`, `OIMBucketCommands`, `TOIMBucketCommandsOptions`, `IOIMBucketCommands`,
2215
- `IOIMBucketArrayMembership`, `OIMBucketArrayMembershipNonUnique`, `OIMBucketArrayMembershipUnique`,
2216
- `TOIMBucketSlotsSetStore`, `TOIMBucketSlotsArrayStore`, `TOIMBucketSlotsSortedArrayStore`, `TOIMBucketSlotsArrayUniqueStore`,
2099
+ `oimPatchEntityColumn`, `oimGuardedPatchEntityColumns`, `oimGuardedPatchEntityColumn`, `oimPutEntityColumns`,
2100
+ `oimGuardedPutEntityColumns`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`.
2101
+
2102
+ **Buckets** — `OIMSlotContentBucketService`, `OIMSlotContentArrayBucketService`,
2103
+ `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSetBucketService`,
2104
+ `OIMSlotContentSortedArrayBucketService`, `TOIMSlotContentBucketServiceOptions`,
2105
+ `TOIMSlotContentArrayBucketServiceOptions`, `TOIMSlotContentSortedArrayBucketServiceOptions`, `TOIMBucketSlot`,
2106
+ `TOIMBucketContentSlot`, `TOIMBucketSlotOwnerService`, `OIMBucketCommands`, `TOIMBucketCommandsOptions`, `IOIMBucketCommands`,
2107
+ `IOIMBucketMemberSlotsArrayService`, `OIMBucketMemberSlotsArrayService`, `OIMBucketUniqueMemberSlotsArrayService`,
2108
+ `TOIMBucketSlotsSetStore`, `TOIMBucketSlotsArrayStore`,
2217
2109
  `OIM_NOTHING_WAS_INSERTED`, `OIM_NOTHING_WAS_REMOVED`, `OIM_REMOVED_WITHOUT_A_POSITION`.
2218
2110
 
2219
- **Indexes** — `OIMIndexService`, `OIMIndexServiceSortedArrayDriven`, `TOIMIndexServiceOptions`, `TOIMIndexStoreSlot`,
2220
- `TOIMIndexServiceStoreSlot`, `TOIMIndexServiceFactoryOptions`, `createOIMIndexService`, `createOIMIndexServiceByCompositeKey`,
2221
- `createOIMIndexServiceSortedArrayDriven`, `TOIMIndexKeyOptions`, `TOIMIndexKey`,
2222
- `IOIMIndexKeysService`, `TOIMIndexKeysPositions`, `OIMIndexKeysService`, `OIMIndexKeysServiceAppending`,
2223
- `OIMIndexKeysServiceReclaiming`.
2111
+ **Indexes** — `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`, `TOIMSlotCollectionIndexServiceOptions`, `TOIMIndexStoreSlot`,
2112
+ `TOIMSlotCollectionIndexServiceStoreSlot`, `TOIMSlotCollectionIndexServiceFactoryOptions`, `createOIMSlotCollectionIndexService`, `createOIMSlotCollectionIndexService`,
2113
+ `createOIMSlotCollectionSortedArrayIndexService`, `TOIMIndexKeyOptions`, `TOIMIndexKey`,
2114
+ `IOIMIndexKeysService`, `TOIMIndexKeysPositions`, `OIMIndexKeysService`, `OIMAppendingIndexKeysService`,
2115
+ `OIMReclaimingIndexKeysService`.
2224
2116
 
2225
- **The graph** — `OIMEffectService`, `IOIMEffectService`, `TOIMEffect`, `TOIMEffectBody`, `TOIMEffectBodyWithSourceTracking`,
2226
- `OIMEffectSourceTracker` (**type only**), `TOIMComputedSlot`, `TOIMComputedSources`, `TOIMDerivation`, `TOIMComputedStoreSlot`,
2227
- `OIMComputedValueService`, `IOIMComputedValueService`, `TOIMComputedValueServiceOptions`, `oimRefreshSourceComputeds` (brings every computed in a `sourceComputeds` list current; call it before a `derive` that runs outside an effect, as `useOIMValue` does).
2117
+ **The graph** — `OIMEffectService`, `IOIMEffectService`, `TOIMEffect`, `TOIMEffectBody`, `TOIMSourceTrackingEffectBody`,
2118
+ `OIMSourceTracker` (**type only**), `TOIMComputedSlot`, `TOIMComputedSources`, `TOIMComputedValueReleaser`, `OIMSourceTrackingComputedService`, `OIMSingleSourceTrackingComputedService`, `createOIMSourceTrackingComputedService`, `createOIMSingleSourceTrackingComputedService`, `TOIMSourceTrackingComputedSlot`, `TOIMSourceTrackingComputedContentSlot`, `TOIMSourceTrackingComputedDerive`, `OIMSlotContentSourceTrackingComputedService` (**type only**), `TOIMSourceTrackingComputedDerivationOptions`, `TOIMComputedServiceFactoryOptions`, `TOIMComputedDerivation`, `TOIMComputedStoreSlot`,
2119
+ `OIMComputedValueService`, `IOIMComputedValueService`, `TOIMComputedValueServiceOptions`, `TOIMComputedContentSlot`,
2120
+ `OIMSlotContentComputedService` (**type only**), `TOIMSlotContentComputedServiceOptions`, `oimRefreshSourceComputeds` (brings every computed in a `sourceComputeds` list current; call it before a `derive` that runs outside an effect, as `useOIMValue` does).
2228
2121
 
2229
2122
  **Computed** — `OIMComputedService`, `TOIMComputedServiceOptions`, `createOIMComputedService`,
2230
- `OIMComputedServiceSorted`, `TOIMComputedServiceSortedOptions`, `createOIMComputedServiceSorted`, `TOIMSortedComputedSlot`,
2231
- `OIMComputedServiceBucketMemo`, `TOIMComputedServiceBucketMemoOptions`, `createOIMComputedServiceBucketMemo`,
2232
- `TOIMBucketMemoComputedSlot`, `TOIMSortColumn`, `TOIMSortableColumnName`, `oimColumnsComparator`.
2123
+ `OIMSortedComputedService`, `TOIMSortedComputedServiceOptions`, `createOIMSortedComputedService`, `TOIMSortedComputedSlot`,
2124
+ `OIMBucketMemoComputedService`, `TOIMBucketMemoComputedServiceOptions`, `createOIMBucketMemoComputedService`,
2125
+ `TOIMBucketMemoComputedSlot`, `OIMSingleComputedService`, `createOIMSingleComputedService`, `TOIMComputedDerivationOptions`,
2126
+ `TOIMSortColumn`, `TOIMSortableColumnName`, `oimColumnsComparator`.
2233
2127
 
2234
2128
  **Stores and keys** — `TOIMStoreKey`, `IOIMSlotStoreService`,
2235
- `OIMSlotStoreServiceMapDriven`, `OIMSlotStoreServiceTreeDriven`,
2236
- `OIMSlotStoreServiceSingleSlotDriven`, `TOIMSingleSlotStore`, `TOIMDefaultSlotStore`, `TOIMSlotStoreTree`, `IOIMKeyTreeWalk`, `TOIMCompositeKeyNode`,
2237
- `OIMKeyTreeWalk`, `OIMKeyTreeWalkComposite`, `OIMKeyTreeWalkScalar`.
2129
+ `OIMSlotMapStoreService`, `OIMSlotTreeStoreService`,
2130
+ `OIMSingleSlotStoreService`, `TOIMSingleSlotStore`, `TOIMDefaultSlotStore`, `TOIMSlotStoreTree`, `IOIMKeyTreeWalk`, `TOIMCompositeKeyNode`,
2131
+ `OIMKeyTreeWalk`, `OIMCompositeKeyTreeWalk`, `OIMScalarKeyTreeWalk`.
2238
2132
 
2239
- **The root and its defaults** — `OIMDB`, `TOIMDBOptions`, `OIMDefaults`, `OIMDefaultValue`, `TOIMFactoryOptions`.
2133
+ **The root and its defaults** — `OIMDB`, `TOIMDBOptions`, `OIMDefaults`, `TOIMDefaultsOptions`, `OIMDefaultValue`, `TOIMFactoryOptions`.
2240
2134
 
2241
2135
  **Comparison and diffing** — `oimSameSlotSequence`, `oimSameKeySequence`, `oimOrderedListCommandsOfTransition`,
2242
2136
  `TOIMOrderedListCommand`.
2243
2137
 
2244
- **Shared stateless defaults** — the statics of `OIMDefaults`: `COLUMN_SLOT_STORE_SERVICE`,
2245
- `COMPUTED_COLUMN_SLOT_STORE_SERVICE`, `KEY_TREE_WALK_SCALAR`, `KEY_TREE_WALK_COMPOSITE`,
2246
- `STANDALONE_SLOT_OWNER`, `ENTITY_WRITER`, `ENTITY_SLOT_EMPTINESS`, `INDEX_KEYS_SERVICE`,
2247
- `STORE_SLOT_LISTENER`. Each is minted on its first `getValue()` instead of at module load, so a
2248
- consumer that names none of them constructs none of them. The cost of the single door is described under **Every optional
2138
+ **Shared stateless defaults** — `OIMDefaults` (an instance passed as `defaults`, `OIMDefaults.INSTANCE`
2139
+ the empty one) and `OIMDefaultValue`, the lazy holder of each built-in default, which is built on its
2140
+ first `getValue()` instead of at module load, so a consumer that reaches none of them constructs none of them. The cost of the single door is described under **Every optional
2249
2141
  dependency has a default** above.
2250
2142
 
2251
2143
  That is the whole of `src/index.ts`. If a name is not on this list, it is not