@oimdb/core 4.0.1 → 4.1.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
@@ -90,11 +90,11 @@ 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 | 98686 B | 24049 B |
94
+ | One entity store slot: `OIMDB`, `createOIMSlotCollectionService`, `createOIMSlotContentEntityService`, `writeSlotContentByKey`, `getSlotByKey` | 35379 B | 9243 B |
95
+ | The same, plus one effect and `db.flush()` | 44020 B | 11357 B |
96
+ | The same, plus one index: `createOIMSlotCollectionIndexService` | 64398 B | 16048 B |
97
+ | An ordered bucket with commands: `OIMSlotContentPositionalArrayBucketService`, `OIMBucketCommands` | 48515 B | 12218 B |
98
98
  <!-- bundle-size:end -->
99
99
 
100
100
  ---
@@ -105,9 +105,9 @@ What each consumer ships, bundled with esbuild (minified, tree-shaken, ESM).
105
105
 
106
106
  | Layer | What is in it |
107
107
  |---|---|
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`. |
108
+ | `src/engine/` | The primitives, each a stateless service over a plain struct: `OIMChannelService.INSTANCE` over a `TOIMChannelSlot` (frames, the revision), `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 `OIMColumnSlotOwner`, `OIMPushColumnSlotOwner` written by the collection, `OIMPullColumnSlotOwner` derived by a computed), the content services (`OIMSlotContentEntityService` for entities, `OIMComputedValueService` for computeds), the key stores and key tree walks, the release policies, the unused-slot collector (`OIMUnusedSlotCollectorService`) over the collector slot, the wheels (`OIMSlotReleaseWheel`, `OIMSlotRefetchWheel`), `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 `OIMSlotContentSetBucketService`, `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService` and `OIMBucketCommands`; the auto-index `OIMSlotCollectionIndexService` and `OIMSlotCollectionSortedArrayIndexService`; `OIMComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService`. |
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, `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `createOIMSlotCollectionIndexService`, `createOIMSlotCollectionSortedArrayIndexService`, `createOIMComputedService`, `createOIMSortedComputedService`, `createOIMBucketMemoComputedService`, `createOIMEntityWriterInPlace`, `createOIMEntityWriterReplacing`. |
111
111
 
112
112
  **A service holds no data; a store slot is one instance.** A service is built once per
113
113
  type, and `cardService.createStoreSlot(db)` returns a `TOIMStoreSlot`, a plain object whose
@@ -116,7 +116,7 @@ service has two halves. Its own doors read and mint slots, and take the store sl
116
116
  `cardService.getSlotByKey(cards, pk)`, `cardService.holdSlotByKey(cards, pk)`,
117
117
  `cardService.findSlotByKey(cards, pk)`, `cardService.getSlots(cards)`,
118
118
  `cardService.getSlotCount(cards)`, plus the column doors (`getColumn`, `holdColumn`, …). Its
119
- `slotContentService` holds the content and every write: `cardService.slotContentService.upsertEntity(cards, card)`
119
+ `slotContentService` holds the content and every write: `cardService.slotContentService.writeSlotContentByKey(cards, card)`
120
120
  takes the store slot, and a door handed a slot takes only the slot, because the slot carries its
121
121
  own store slot (`slot.storeSlot`): `cardService.slotContentService.updateEntity(slot, patch)`.
122
122
  The owner of the slots is reached through `slot.owner` (an `IOIMSlotOwner`: hold, unhold,
@@ -127,60 +127,14 @@ release, subscribe), never through the service.
127
127
  ## The shortest thing that delivers
128
128
 
129
129
  ```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';
130
+ 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, createOIMEntityWriterInPlace, 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 } from '@oimdb/core';
177
131
 
178
132
  type Card = { id: string; deck: string; title: string; rank: number };
179
133
  type CardSlot = TOIMEntitySlot<Card>;
180
134
 
181
135
  const db = new OIMDB();
182
136
  const effectService = OIMEffectService.INSTANCE;
183
- const cardService = createOIMEntityService<Card>(db, { keyOf: (card) => card.id });
137
+ const cardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db) });
184
138
  const cards = cardService.createStoreSlot(db);
185
139
  const slot = cardService.getSlotByKey(cards, 'c1');
186
140
  const byRank = (leftBucketMemberSlot: TOIMEntitySlot<Card>, rightBucketMemberSlot: TOIMEntitySlot<Card>): number =>
@@ -188,7 +142,7 @@ const byRank = (leftBucketMemberSlot: TOIMEntitySlot<Card>, rightBucketMemberSlo
188
142
  ```
189
143
 
190
144
  ```typescript
191
- cardService.slotContentService.upsertEntity(cards, { id: 'c1', deck: 'spades', title: 'Ace', rank: 1 });
145
+ cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', deck: 'spades', title: 'Ace', rank: 1 });
192
146
 
193
147
  let seen = '';
194
148
  const effect = effectService.createEffect(db, [slot], [], () => {
@@ -203,7 +157,7 @@ effectService.disposeEffect(effect);
203
157
 
204
158
  `OIMDB` is the shared state and nothing else: one `clock` (`{ revision, epoch }`), one
205
159
  `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
160
+ `collectorSlot` (the unused-slot collector's slot and the release wheels, made on first use), and the
207
161
  `entityWriter`, `slotStamp`, `slotReleasePolicy` and `indexKeysService` its constructor is handed,
208
162
  which it keeps as the default that every door below reads. It is an `IOIMChannelMap`, so
209
163
  anything that takes a `channelMap` takes the `db`. `flush()` is
@@ -215,6 +169,11 @@ and `OIMEffectService.INSTANCE` are stateless; every call is handed the channel
215
169
  whose `defaultChannelSlot` it is). A store slot carries its own channel slot as
216
170
  `storeSlot.channelSlot`, and an effect as `effect.channelSlot`.
217
171
 
172
+ **One channel per database; for isolation, a separate `OIMDB`.** A second channel adds nothing a
173
+ database does not already do: a write wakes every effect on the slots it moved at its own
174
+ channel's flush, whichever channel those effects belong to, so a second channel on the same clock
175
+ isolates neither time nor state. A separate `new OIMDB()` has its own clock, collectors and channel.
176
+
218
177
  There is no default scheduler, on purpose. A test calls `db.flush()`; an
219
178
  application gives that call to the orchestrator. With cnstra that is one line:
220
179
  a drain listener that calls `db.flush()` at the end of every synchronous turn of every
@@ -224,29 +183,26 @@ Every mutation goes through a domain neuron. See
224
183
  [With cnstra](https://oimdb.org/docs/guides/with-cnstra).
225
184
 
226
185
  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
186
+ type; an instance is the store slot that service creates. A collection, a single and an index take
187
+ `db` first and ONE options object second. Every collection is `createOIMSlotCollectionService` over a content
188
+ service, and every single is `createOIMSingleSlotService`; an array key needs a tree store
189
+ (`slotStoreService: new OIMSlotTreeStoreService(…)`, or `bucketSlotStoreService` on an index), because a
190
+ map-driven store compares arrays by reference. See [The six shapes](#the-six-shapes). The computed doors
236
191
  take no `db`: a computed reaches its channel through the store slot it derives from, or through
237
192
  the `db` handed to `createComputedStoreSlot`.
238
193
 
239
194
  ```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? })
195
+ createOIMSlotContentEntityService<Card, TKey?, TColumnName?>(db, { entityWriter?, slotEmptinessPolicy? }?)
196
+ new OIMSlotContentSetBucketService<TBucketMemberSlot, TKey?>({ bucketCommands? })
197
+ new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot, TKey?>({ bucketCommands?, bucketMemberSlotsArrayService? })
198
+ createOIMSlotCollectionService(db, { slotContentService, slotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? | columnNames? })
199
+ createOIMSingleSlotService(db, { slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? | columnNames? })
200
+ createOIMSlotCollectionIndexService(db, { entityService, columns | keyOf, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
201
+ createOIMSlotCollectionSortedArrayIndexService(db, { entityService, sortColumns, columns | keyOf, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
247
202
  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? })
203
+ createOIMSingleComputedService<TValue>({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
204
+ createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
205
+ createOIMBucketMemoComputedService({ bucketCollectionService, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
250
206
 
251
207
  cardService.createStoreSlot(db) // an entity or bucket store slot, reporting to db.defaultChannelSlot
252
208
  indexService.createIndexStoreSlot(cards) // the index's bucket store slot over one entity store slot
@@ -255,18 +211,13 @@ orderService.createBucketMemoStoreSlot(bucketStoreSlot) // one derivation per bu
255
211
  computedService.createComputedStoreSlot(db, derive?, createSources?)
256
212
  ```
257
213
 
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:
214
+ **The content service is where everything per type is fixed, once:** the entity writer and the emptiness rule.
215
+ The collection service adds the store service, the release policy and the column options, and builds the slot
216
+ owner from them and `db.slotStamp`. Build the content service first when it is shared:
266
217
 
267
218
  ```typescript
268
- const cardContentService = createOIMSlotContentEntityService<Card>(db, { keyOf: (card) => card.id });
269
- const sameShapeCardService = createOIMSlotCollectionService({ slotContentService: cardContentService });
219
+ const cardContentService = createOIMSlotContentEntityService<Card>(db);
220
+ const sameShapeCardService = createOIMSlotCollectionService(db, { slotContentService: cardContentService });
270
221
  const sameShapeCards = sameShapeCardService.createStoreSlot(db);
271
222
  ```
272
223
 
@@ -284,14 +235,14 @@ nothing does not import the bucket machinery. This is pinned by
284
235
  `tests/what-an-import-retains.test.ts` (the root retains no door) and by
285
236
  `src/factories/tests/which-answer-a-collection-the-root-mints-gets.test.ts`.
286
237
 
287
- **A bucket's content service decides how it holds its slots.** `createOIMBucketService` takes it
288
- as `slotContentService`. With none, it builds an `OIMSlotContentBucketServiceArrayDriven`
238
+ **A bucket's content service decides how it holds its slots.** `createOIMSlotCollectionService` takes it
239
+ as `slotContentService`. With none, it builds an `OIMSlotContentPositionalArrayBucketService`
289
240
  (insertion order, never re-decided) with an `OIMBucketCommands` that records what each position
290
241
  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
242
+ `new OIMSlotContentSetBucketService<TMember>()` for a bucket that keeps its slots in a set,
243
+ and `new OIMSlotContentSortedArrayBucketService({ compareBucketMemberSlots })` for one kept in
293
244
  a comparator's order. A bucket's reads and writes are on that content service:
294
- `bucketService.slotContentService.writeBucketMemberSlots(bucketSlot, slots)`.
245
+ `bucketService.slotContentService.writeSlotContent(bucketSlot, slots)`.
295
246
 
296
247
  **A bucket never sorts, except a sorted one.** The order of an array-driven bucket is what the
297
248
  caller put in: `addBucketMemberSlot` appends, `insertBucketMemberSlotAt` inserts, a move moves.
@@ -300,9 +251,9 @@ See [A sorted list is a computed over an index](#a-sorted-list-is-a-computed-ove
300
251
  for the index whose buckets sort themselves, see [A sorted index: sorted at write](#a-sorted-index-sorted-at-write).
301
252
 
302
253
  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
254
+ live** (`slotStoreService`, the strategy every store slot of the service files its slots
304
255
  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
256
+ **what counts as empty** (`slotEmptinessPolicy`), **when a slot goes** (`slotReleasePolicy`), and
306
257
  **where column slots live** (`columnSlotStoreService`, or `columnNames` for the ordinal store; the
307
258
  slot owner makes its own column owner over it). Everything but `keyOf` falls back to the `db` or
308
259
  to a lazy `OIMDefaultValue` on [`OIMDefaults`](#every-optional-dependency-has-a-default).
@@ -316,29 +267,24 @@ There is no argument that turns delivery off. A store slot is created on a `db`
316
267
  unused-slot collector a read hands its mint to is that channel slot's `collectorSlot`.
317
268
 
318
269
  ```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
- });
270
+ const deliveringCardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db), slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, TOIMStoreKey>>(), columnSlotStoreService: OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue() });
324
271
  const deliveringCards = deliveringCardService.createStoreSlot(db);
325
272
  ```
326
273
 
327
274
  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`.
275
+ `OIMSlotContentEntityService`; `createOIMSlotCollectionService` builds an `OIMSlotCollectionService` over it,
276
+ and that constructor builds the type's `OIMSlotOwnerService`. The owner asks the content about a slot
277
+ through `IOIMSlotContentService` (`isSlotEmpty`, `releaseSlotContent`, `hasReference`); the collection
278
+ service mints through the content service's `createSlot`.
333
279
 
334
280
  `keyOf` is the one required option. `entityWriter` falls back to `db.entityWriter`, then to
335
- `OIMDefaults.ENTITY_WRITER`; `slotEmptiness` to `OIMDefaults.ENTITY_SLOT_EMPTINESS`;
281
+ `OIMDefaults.ENTITY_WRITER`; `slotEmptinessPolicy` to `OIMDefaults.ENTITY_SLOT_EMPTINESS`;
336
282
  `slotReleasePolicy` to `db.slotReleasePolicy`, then to nothing, which is the default of "a slot
337
283
  with a value stays for good" described under **The injected seams**; the column store to
338
284
  `OIMDefaults.COLUMN_SLOT_STORE_SERVICE`.
339
285
 
340
286
  A single entity is the same content service under a `void` key, built by
341
- `createOIMSingleEntityService`, and it reports the same way.
287
+ `createOIMSingleSlotService`, and it reports the same way.
342
288
 
343
289
  ---
344
290
 
@@ -351,13 +297,13 @@ class of static `OIMDefaultValue` fields and nothing else; you never construct o
351
297
  |---|---|---|
352
298
  | `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
299
  | `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 |
300
+ | `OIMDefaults.KEY_TREE_WALK_SCALAR` | `OIMScalarKeyTreeWalk` | the chain-of-values walk |
301
+ | `OIMDefaults.KEY_TREE_WALK_COMPOSITE` | `OIMCompositeKeyTreeWalk` | the chain-of-identities walk |
356
302
  | `OIMDefaults.STANDALONE_SLOT_OWNER` | `OIMSlotOwner` | the owner of a slot nobody filed |
357
303
  | `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 |
304
+ | `OIMDefaults.ENTITY_SLOT_EMPTINESS` | `IOIMSlotEmptinessPolicy<TOIMEntitySlot>` | "empty means `slot.entity === undefined`" |
305
+ | `OIMDefaults.INDEX_KEYS_SERVICE` | `IOIMIndexKeysService` | an `OIMAppendingIndexKeysService`; see [Index keys strategy](#index-keys-strategy) |
306
+ | `OIMDefaults.STORE_SLOT_LISTENER` | `IOIMStoreSlotListener` | `OIMNoStoreSlotListener`, 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
307
 
362
308
  **Nothing is constructed at module load.** Every entry is an `OIMDefaultValue`
363
309
  whose `getValue()` mints on its first call (`??=`) and hands back the same instance
@@ -397,20 +343,20 @@ const card: Card = { id: 'c1', deck: 'spades', title: 'Ace', rank: 1 };
397
343
  const key = 'c1';
398
344
  const body = (): void => {};
399
345
 
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
346
+ cardService.writeSlotContentByKey(cards, card.id, card); // write: the value is the reference
347
+ cardService.writeSlotContentByKey(cards, key, card); // write: the same door, by key
402
348
  const entitySlot = cardService.holdSlotByKey(cards, key); // hold: a counted reference; give it back with entitySlot.owner.unholdSlot(entitySlot)
403
349
  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
350
+ const rankColumn = cardService.columnSlotService.holdColumn(entitySlot, 'rank'); // hold, on a column
351
+ cardService.columnSlotService.unholdColumn(rankColumn); // give the column back
406
352
  cardService.getSlotByKey(cards, key); // read: the collector's hold is the reference
407
353
  ```
408
354
 
409
355
  **The read that does not mint is `findSlotByKey(storeSlot, key)`.** It answers `undefined`
410
356
  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);`.
357
+ it: `const s = cardService.findSlotByKey(cards, key); if (s !== undefined) cardService.slotContentService.clearSlotContent(s);`.
412
358
  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
359
+ that mints. `cardService.slotContentService.findSlotByKey(storeSlot, key)` and
414
360
  `cardService.hasSlotContentByKey(storeSlot, key)` mint nothing either.
415
361
 
416
362
  **`getSlotByKey(storeSlot, key)` is the fourth door, and the collector is what admits it.** It
@@ -418,20 +364,20 @@ mints what is missing and hands the slot to the unused-slot collector of the sto
418
364
  (`OIMUnusedSlotCollectorService.INSTANCE.holdSlotUntilFlush(storeSlot.channelSlot.collectorSlot.unusedSlotCollectorSlot, slot)`),
419
365
  which takes the hold on the caller's behalf and gives it back at the next `flush()`.
420
366
  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
367
+ caller, which makes it safe to use and safe to forget. `writeSlotContentByKey` relies on the same
422
368
  window when the content it wrote landed empty: the
423
369
  mint is tracked, so the identity it handed back stands until the collector gives
424
370
  it back, and goes then if nothing took it.
425
371
 
426
372
  **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
373
+ `writeSlotContentByKey` mints, writes inside a `try`, and on a throw runs
428
374
  the release guard on what it just minted before re-raising, so the caller's error
429
375
  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
376
+ alternative, and it is wrong. One `writeSlotContentByKey` of an unseen key from inside a
431
377
  subscriber body (the service's write opens with the frame guard)
432
378
  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
379
+ `getSlotByKey` would answer a slot with no value, and nothing would throw except the refusal the
380
+ caller had asked for.* The
435
381
  unwind is the release guard rather than a `store.remove` because the write may
436
382
  have half landed, and the hit path never mints and stays outside the guard.
437
383
 
@@ -442,15 +388,10 @@ happen and the content was already there. The door completed and the caller is
442
388
  handed the identity. Whether an identity that nothing keeps is worth keeping is
443
389
  the owner's question, asked through its release guard.
444
390
 
445
- So `insertEntity` is **total**: it always hands back the identity it resolved or
391
+ So `writeSlotContentByKey` is **total**: it always hands back the identity it resolved or
446
392
  minted, whatever it decided about the content, because a door that mints must
447
393
  leave a reference behind.
448
394
 
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
395
  A fifth door is admitted by this test and by nothing else: the act has to leave
455
396
  a reference behind that nothing can take away under its caller. A helper over an
456
397
  existing door does not qualify. It is a **second spelling** of one, and two
@@ -465,9 +406,9 @@ A reference is given back by whoever took it. The terms are:
465
406
  | `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
407
  | `effects` | `OIMEffectService.INSTANCE.disposeEffect(effect)` |
467
408
  | 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 |
409
+ | the value itself | `entityService.slotContentService.clearSlotContent(slot)` |
410
+ | a slot in a bucket | `bucketService.slotContentService.clearSlotContent` / `writeSlotContent` / a positional removal |
411
+ | a composite-key edge | `OIMCompositeKeyTreeWalk.deleteKey` unlinking that composite key part |
471
412
 
472
413
  When the last of them goes, the identity's **owner** unfiles it. There is no
473
414
  release API and no owner that a caller assigns. `releaseSlotIfUnheld(slot)` is **public**
@@ -492,46 +433,40 @@ own call. See [Ending the channel whole](#ending-the-channel-whole).
492
433
  ### A service composes
493
434
 
494
435
  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**
436
+ key, in the store each store slot carries; `OIMSlotMapStoreService`,
437
+ `OIMSlotTreeStoreService`, `OIMSingleSlotStoreService`), a **slot owner**
497
438
  (`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**
439
+ emptiness and the column owner) and a **content service**
499
440
  (`OIMSlotContentEntityService`, an `OIMSlotContentBucketService*`, or a computed's
500
441
  `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:
442
+ `OIMSingleSlotService`) building the owner and lending it the by-key doors. The owner asks the content
443
+ about a slot through `IOIMSlotContentService`, never through an override:
503
444
 
504
- | Strategy member | Called by the owner when |
445
+ | Member | Called when |
505
446
  |---|---|
506
- | `createSlot(key, storeSlot, slotOwnerService)` | a key is minted |
447
+ | `createSlot(key, storeSlot, slotOwner)` / `createSingleSlot(storeSlot, slotOwner)` | a key (or a single) is minted, by the collection service |
507
448
  | `isSlotEmpty(slot)` | emptiness is settled after a write |
508
449
  | `releaseSlotContent(slot, slotOwnerService)` | the identity is going. Give back what its content held |
509
450
  | `hasReference(slot)` | something outside `holdCount` and `effects` may keep it |
510
451
 
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.
452
+ The content service's write doors reach the owner through `slot.owner`; the owner files and unfiles
453
+ through the store service.
519
454
 
520
455
  ### The injected seams
521
456
 
522
457
  ```typescript signature
523
- interface IOIMSlotEmptiness<TSlot> { isEmpty(slot: TSlot): boolean; }
458
+ interface IOIMSlotEmptinessPolicy<TSlot> { isEmpty(slot: TSlot): boolean; }
524
459
 
525
460
  interface IOIMSlotStamp<TSlot> { stamp(slotEditable: TOIMSlotEditable<TSlot>): void; }
526
461
  ```
527
462
 
528
- **`IOIMSlotEmptiness`** decides what counts as empty. The answer is written once
463
+ **`IOIMSlotEmptinessPolicy`** decides what counts as empty. The answer is written once
529
464
  per write into the EMPTY flag of `slot.slotFlags` and never recomputed at a read. An entity content
530
465
  service defaults to "the entity is `undefined`" (`OIMDefaults.ENTITY_SLOT_EMPTINESS`); a bucket
531
466
  content service answers "`getBucketMemberSlotCount(bucketSlot) === 0`"; a computed answers
532
467
  `false`.
533
468
 
534
- Emptiness decides lifetime, never the write path. An `upsertEntity` onto an entity slot that
469
+ Emptiness decides lifetime, never the write path. An `writeSlotContentByKey` onto an entity slot that
535
470
  still carries an entity merges into it even when emptiness calls it empty (a soft
536
471
  delete, a seed), so a column that the new entity drops is woken and an unchanged one is
537
472
  not. Only a slot with no entity at all is filled whole.
@@ -542,9 +477,9 @@ for stamps that have nothing to do with release; release has its own policy.
542
477
 
543
478
  **`IOIMSlotReleasePolicy`** decides whether and when a slot goes, and does no
544
479
  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`:
480
+ `db.slotReleasePolicy`. With neither, the slot owner is built with `OIMSlotAtEmptyReleasePolicy`:
546
481
  a slot with a value stays for good. A computed service defaults to
547
- `OIMSlotReleasePolicyAtUnhold`. Two readonly flags on the policy, `releasesWrittenSlots` and
482
+ `OIMSlotAtUnholdReleasePolicy`. Two readonly flags on the policy, `releasesWrittenSlots` and
548
483
  `schedulesReleaseOnWrite`, are read once by the owner at construction, so a policy that never
549
484
  releases on a write costs a write nothing.
550
485
 
@@ -553,9 +488,9 @@ policy moves only its own. `-1` means unset.
553
488
 
554
489
  | Marker | Moved by | Not moved by |
555
490
  |---|---|---|
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 |
491
+ | `releaseDueAfterWriteMs` | every write, under `OIMSlotAfterWriteReleasePolicy` | a subscriber arriving or leaving |
492
+ | `releaseDueAfterIdleMs` | the first content of a slot with no subscriber, and the departure of the last subscriber, under `OIMSlotAfterIdleReleasePolicy` | a later write |
493
+ | `releaseDueAfterUnholdMs` | every last unhold, under `OIMSlotAfterUnholdReleasePolicy` | a write |
559
494
 
560
495
  A slot goes on the SOONEST of those that are set, and a hold outranks them all.
561
496
  **One number cannot carry both.** A write arriving after the subscriber left
@@ -567,36 +502,36 @@ write would overwrite the write policy's. Both directions are pinned by
567
502
  empty and has nothing left keeping it (no holds, no references). `true` releases
568
503
  the slot now. `false` keeps it, and a policy with a finite deadline has already
569
504
  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.
505
+ with nothing scheduled. `OIMSlotAtUnholdReleasePolicy` always answers `true`, and
506
+ `OIMSlotEarliestReleasePolicy` asks every member and answers `true` if any member does.
572
507
  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
508
+ policy except `OIMSlotNeverReleasePolicy` answers `true`. A service given no
509
+ policy is built with `OIMSlotAtEmptyReleasePolicy`: it keeps a non-empty slot and
575
510
  lets an empty one go, and no path branches on a missing policy. The owner has no
576
511
  "how long is left" question.
577
512
 
578
513
  ### Releasing an identity on a clock — opt-in, and off it costs nothing
579
514
 
580
- Four policies ship, plus `OIMSlotReleasePolicyEarliest`, which combines them. The difference between
515
+ Four policies ship, plus `OIMSlotEarliestReleasePolicy`, which combines them. The difference between
581
516
  them is WHEN a slot goes. There is no flag, and no branch on the sweep path.
582
517
 
583
518
  | Policy | A slot goes |
584
519
  |---|---|
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 })`.
520
+ | `new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs, slotReleaseDueFramePolicy? })` | once its last write is older than the window; with `OIMSlotReleaseManualDueFramePolicy`, only writes inside its open frame count |
521
+ | `new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs })` | once nothing has watched it for the window |
522
+ | `new OIMSlotAfterUnholdReleasePolicy({ keepAfterUnholdMs })` | once the window after its LAST unhold has passed; a hold taken again inside it keeps the slot |
523
+ | `OIMSlotAtUnholdReleasePolicy.INSTANCE` | at the last unhold, even with a value |
524
+ | `OIMSlotAtEmptyReleasePolicy.INSTANCE` | once it has no value and nothing holds it; a computed, never empty, stays while its owner lives |
525
+ | `OIMSlotNeverReleasePolicy.INSTANCE` | never, empty or not; a deleted key's slot stays filed |
526
+ | `new OIMSlotEarliestReleasePolicy([...])` | when the earliest of several deadlines falls due — a `min` over deadlines |
527
+
528
+ A policy holds only its window. The clock, the timer and the wheels are the `db`'s: each `OIMDB`
529
+ has a `collectorSlot` with one release wheel per windowed policy
530
+ (`afterWriteSlotReleaseWheel`, `afterIdleSlotReleaseWheel`,
531
+ `afterUnholdSlotReleaseWheel`), built from `new OIMDB({ getNowMs, releaseTimer, errorPolicy })`.
597
532
  A policy reaches the one for a slot through the slot's channel
598
533
  (`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
534
+ `releaseTimer` to the `OIMDB`. `OIMSlotAtUnholdReleasePolicy` is the zero window and touches no
600
535
  collector.
601
536
 
602
537
  ```typescript signature
@@ -620,9 +555,9 @@ queue.
620
555
 
621
556
  ```typescript
622
557
  const releasingDb = new OIMDB({
623
- slotReleasePolicy: new OIMSlotReleasePolicyEarliest([
624
- new OIMSlotReleasePolicyAfterWrite({ keepAfterWriteMs: 60_000 }),
625
- new OIMSlotReleasePolicyAfterIdle({ keepAfterIdleMs: 30_000 }),
558
+ slotReleasePolicy: new OIMSlotEarliestReleasePolicy([
559
+ new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: 60_000 }),
560
+ new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
626
561
  ]),
627
562
  });
628
563
  ```
@@ -637,7 +572,7 @@ const releasingDb = new OIMDB({
637
572
  the `db`'s `collectorSlot`, and each slot carries its own queue and its own timer: two queues,
638
573
  two timers.
639
574
  Whichever deadline falls due first takes the slot, and nothing has to be
640
- reconciled. `OIMSlotReleasePolicyEarliest` answers a `min` over its members'
575
+ reconciled. `OIMSlotEarliestReleasePolicy` answers a `min` over its members'
641
576
  deadlines, so a sweep of either queue reaches the same verdict.
642
577
 
643
578
  ### When a window opens, and when the clock is not running at all
@@ -650,7 +585,7 @@ A window opens in **exactly two places**:
650
585
  2. **when the LAST SUBSCRIBER leaves**: `OIMSlotOwner.unsubscribeFromSlot` takes the
651
586
  last entry out of `slot.effects` and runs the idle step, which opens a window.
652
587
 
653
- Both ask the policy's `onSlotIdle(slot)`. Under `OIMSlotReleasePolicyAfterIdle`
588
+ Both ask the policy's `onSlotIdle(slot)`. Under `OIMSlotAfterIdleReleasePolicy`
654
589
  it stamps `releaseDueAfterIdleMs = getNowMs() + keepAfterIdleMs` and files the slot
655
590
  into the policy's queue. An **empty** slot gets no window, and the release guard
656
591
  takes it without asking a clock.
@@ -690,22 +625,21 @@ identity that holds nothing, and a held one never reaches the question at all.
690
625
  ### The collectors
691
626
 
692
627
  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`:
628
+ sweeping. Every `OIMDB` builds its collectors once, in `db.collectorSlot`:
695
629
 
696
630
  ```typescript signature
697
631
  new OIMDB({ getNowMs?, releaseTimer?, errorPolicy? }) // getNowMs defaults to Date.now, releaseTimer to an unref'd setTimeout
698
632
  db.collectorSlot.unusedSlotCollectorSlot // OIMUnusedSlotCollectorService.INSTANCE
699
- db.collectorSlot.afterWriteReleaseSlotCollectorSlot // OIMReleaseSlotCollectorServiceAfterWrite.INSTANCE
700
- db.collectorSlot.afterIdleReleaseSlotCollectorSlot // OIMReleaseSlotCollectorServiceAfterIdle.INSTANCE
701
- db.collectorSlot.afterUnholdReleaseSlotCollectorSlot // OIMReleaseSlotCollectorServiceAfterUnhold.INSTANCE
633
+ oimMintAfterWriteSlotReleaseWheel(db.collectorSlot) // an OIMSlotReleaseWheel, created on first use
634
+ oimMintAfterIdleSlotReleaseWheel(db.collectorSlot) // likewise
635
+ oimMintAfterUnholdSlotReleaseWheel(db.collectorSlot) // likewise
702
636
  ```
703
637
 
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.
638
+ A release that throws inside a `flush()` called by hand throws to that caller. A release that
639
+ throws on the wheel's own timer tick has no caller, so it goes to the wheel's `errorPolicy`, which
640
+ is the `errorPolicy` the `OIMDB` was built with.
707
641
 
708
- Every collector extends `OIMFlushSchedulerService`, which owns the schedule and cancel logic around
642
+ Every wheel extends `OIMFlushSchedulerService`, which owns the schedule and cancel logic around
709
643
  a `TOIMTimer = (run: () => void, inMs: number) => TOIMDisposer`, keeping the standing run on the
710
644
  slot it schedules. Protected `schedule(slot, delayMs)` does nothing when a scheduled run is already
711
645
  standing. A callback that is no longer the one the base handed the timer (a run the host could not
@@ -715,29 +649,29 @@ once per schedule, and cancel and re-schedule only when a shorter window arrives
715
649
  `releaseTimer` to the `OIMDB` to drive the collectors off a frame callback, a worker message or a
716
650
  test clock.
717
651
 
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.
652
+ **`OIMSlotWheel`** is the abstract clock: its slices, timer, `getNowMs` and `errorPolicy` are fields
653
+ of the object. It sweeps on `flush()` and re-schedules to the next deadline; `cancelAllSlots()`
654
+ takes the timer down and drops every filing, and the next filing arms it again. The default timer
655
+ calls `unref` where the platform has it, so an idle process is never held open.
725
656
 
726
- **`OIMReleaseSlotQueueService`** files slots into **time slices**, not one entry each.
657
+ **`OIMSlotReleaseWheel`** is the wheel a release policy files into. Which marker it reads and writes
658
+ (`getReleaseDueMs`, `setReleaseDueMs`) and which bit of `slot.slotFlags` says a slot is on it are
659
+ given at construction. It files slots into **time slices**, not one entry each.
727
660
  `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
661
+ is `1024`, so every slot due inside the same ~1s bucket shares one array and one
662
+ timer. Filing is a push and a bit, and cancelling clears the bit and leaves the entry for the
663
+ sweep to skip. Its doors are `markSlotReleaseDue`, `scheduleSlotRelease`, `isSlotReleasable`,
664
+ `cancelSlotRelease`, `isSlotReleaseScheduled`, `getNextSliceNumber`, `sweepUpToSliceNumber`,
665
+ `flush` and `cancelAllSlots`. `scheduleSlotRelease` reads the marker and files at it, so a filing can
733
666
  neither shorten nor extend a deadline, and a marker of `-1` files nothing.
734
667
 
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,
668
+ **The sweep is where the release decision is taken, and it belongs to the wheel, not the owner.**
669
+ For each slot in a due slice it skips a stale entry and a released slot,
737
670
  reads its own marker, drops the slot on `-1`, **re-files it itself** into
738
671
  `oimSliceOfMs(getReleaseDueMs)` when that moment is still ahead, and otherwise calls
739
672
  `slot.owner.releaseSlotIfUnheld(slot)`, which asks the policy's `isSlotReleasable`
740
- and so declines while the other policy still keeps the slot. The
673
+ and so declines while the other policy still keeps the slot. `markSlotReleaseDueNow(slot)`
674
+ invalidates: the marker becomes now, and a slot nothing holds or watches goes at once. The
741
675
  owner files nothing from inside a sweep; it opens a window only on its own events:
742
676
  a mint, and the departure of the last subscriber. The sweep raises a floor for its
743
677
  duration, so any re-file lands in the next slice and cannot loop.
@@ -795,25 +729,25 @@ a store slot that holds one slot under no key, or many, a store slot that files
795
729
 
796
730
  | kind | single | collection |
797
731
  |---|---|---|
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?)` |
732
+ | entity | `createOIMSingleSlotService<TEntity>(db, …)`, then `createStoreSlot(db)` | `createOIMSlotCollectionService<TEntity>(db, { keyOf, … })`, then `createStoreSlot(db)` |
733
+ | bucket | `createOIMSingleSlotService<TMember>(db, { slotContentService?, … })`, then `createStoreSlot(db)` | `createOIMSlotCollectionService<TMember>(db, { slotContentService?, … })`, then `createStoreSlot(db)` |
734
+ | 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
735
 
802
736
  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,
737
+ over the slot store service `OIMSingleSlotStoreService`, whose store,
804
738
  `TOIMSingleSlotStore<TSlot>`, is `{ slot: TSlot | undefined }`; its store slot is a
805
739
  `TOIMSingleSlotStoreSlot`. The collection half is `OIMSingleSlotService` instead of
806
740
  `OIMSlotCollectionService`, and it has no key in any door: `getSingleSlot(storeSlot)`,
807
741
  `holdSingleSlot(storeSlot)` (given back with `slot.owner.unholdSlot(slot)`),
808
742
  `findSingleSlot(storeSlot)`, which mints nothing, `hasSingleSlotContent(storeSlot)`, and the same
809
743
  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
744
+ `singleService.slotContentService.writeSlotContentByKey(storeSlot, entity)`,
745
+ `singleService.slotContentService.findSlotByKey(storeSlot, undefined)`. The single entity
812
746
  door builds its content service with `keyOf: () => undefined` and the single-slot store.
813
747
 
814
748
  A single behaves as a collection of at most one slot, and invents nothing of its own.
815
749
  `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
750
+ collection. An entity single's first value arrives through `writeSlotContentByKey`, an ordinary write
817
751
  that is announced to whoever subscribed. Its slot is released like any slot of its service,
818
752
  under the service's release policy, and the next get mints a fresh one. A store slot is not
819
753
  a slot: it has no owner, no holds and no effects, so never hold or subscribe it, and never
@@ -829,14 +763,14 @@ are reached through its `computedCollectionService` (an `OIMSlotCollectionServic
829
763
  `slot.computedValueService.getValueForSlot(slot)`, and a hold is given back with
830
764
  `slot.owner.unholdSlot(slot)`.
831
765
 
832
- A single bucket is built like a keyed one, with `createOIMSingleBucketService`.
766
+ A single bucket is built like a keyed one, with `createOIMSingleSlotService`.
833
767
  `singleBucketService.getSingleSlot(storeSlot)` is the `TOIMBucketSlot` to subscribe effects to
834
768
  or hand to a reader. The member doors live on its `slotContentService` and take the bucket slot
835
769
  first: `getBucketMemberSlots(bucketSlot)`, `getBucketMemberSlotCount(bucketSlot)`,
836
770
  `hasBucketMemberSlot(bucketSlot, bucketMemberSlot)`,
837
771
  `addBucketMemberSlot(s)(bucketSlot, …)`, `removeBucketMemberSlot(s)(bucketSlot, …)`,
838
- `writeBucketMemberSlots(bucketSlot, bucketMemberSlots): boolean`,
839
- `clearBucketMemberSlots(bucketSlot)`, `fillBucketMemberSlots`, `markContentChanged(bucketSlot)` and
772
+ `writeSlotContent(bucketSlot, bucketMemberSlots): boolean`,
773
+ `clearSlotContent(bucketSlot)`, `fillBucketMemberSlots`, `markContentChanged(bucketSlot)` and
840
774
  `markBucketColumnsChanged(bucketSlot, patch, changedColumns)`. The array-driven content service
841
775
  adds `getBucketMemberSlotAt(bucketSlot, index)`,
842
776
  `getBucketMemberSlotPosition(bucketSlot, bucketMemberSlot)`, `insertBucketMemberSlotAt`,
@@ -857,13 +791,13 @@ argument, not a subclass.
857
791
 
858
792
  ```typescript signature
859
793
  // A scalar key: string | number.
860
- new OIMSlotStoreServiceMapDriven<TOIMEntitySlot<Card, TOIMStoreKey>>()
794
+ new OIMSlotMapStoreService<TOIMEntitySlot<Card, TOIMStoreKey>>()
861
795
 
862
796
  // 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())
797
+ new OIMSlotTreeStoreService<TOIMStoreKey, TOIMKeyedSlot<readonly TOIMStoreKey[]>>(OIMDefaults.KEY_TREE_WALK_SCALAR.getValue())
864
798
 
865
799
  // 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())
800
+ new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMKeyedSlot<readonly TOIMEntitySlot[]>>(OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue())
867
801
  ```
868
802
 
869
803
  There are two stores, one per shape of address, and no store is ever asked at run time
@@ -885,15 +819,11 @@ type TScalarChain = readonly TOIMStoreKey[];
885
819
  type Tally = { deck: string; day: string; count: number };
886
820
  type TTallyStore = TOIMSlotStoreTree<TOIMEntitySlot<Tally, TScalarChain>, TOIMStoreKey>;
887
821
 
888
- const tallyService = createOIMEntityService<Tally, TOIMEntityColumnName<Tally>, TScalarChain, TTallyStore>(db, {
889
- entitySlotStoreService:
890
- new OIMSlotStoreServiceTreeDriven<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
822
+ const tallyService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(db), slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
891
823
  OIMDefaults.KEY_TREE_WALK_SCALAR.getValue(),
892
- ),
893
- keyOf: (t) => [t.deck, t.day], // the two names, in order
894
- });
824
+ ) });
895
825
  const tallies = tallyService.createStoreSlot(db);
896
- tallyService.slotContentService.upsertEntity(tallies, { deck: 'spades', day: 'mon', count: 1 });
826
+ tallyService.writeSlotContentByKey(tallies, ((t) => [t.deck, t.day])({ deck: 'spades', day: 'mon', count: 1 }), { deck: 'spades', day: 'mon', count: 1 });
897
827
  tallyService.getSlotByKey(tallies, ['spades', 'mon']);
898
828
  ```
899
829
 
@@ -912,28 +842,24 @@ type Enrolment = {
912
842
  };
913
843
  type TChain = readonly TOIMEntitySlot[];
914
844
 
915
- const userService = createOIMEntityService<User>(db, { keyOf: (user) => user.id });
845
+ const userService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<User>(db) });
916
846
  const users = userService.createStoreSlot(db);
917
- const groupService = createOIMEntityService<Group>(db, { keyOf: (group) => group.id });
847
+ const groupService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Group>(db) });
918
848
  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' });
849
+ const alice = userService.writeSlotContentByKey(users, 'alice', { id: 'alice', name: 'Alice' });
850
+ const admins = groupService.writeSlotContentByKey(groups, 'admins', { id: 'admins', name: 'Admins' });
921
851
 
922
852
  type TEnrolmentStore = TOIMSlotStoreTree<TOIMEntitySlot<Enrolment, TChain>, TOIMEntitySlot>;
923
853
 
924
- const enrolmentService = createOIMEntityService<Enrolment, TOIMEntityColumnName<Enrolment>, TChain, TEnrolmentStore>(db, {
925
- entitySlotStoreService:
926
- new OIMSlotStoreServiceTreeDriven<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
854
+ const enrolmentService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(db), slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
927
855
  OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue(),
928
- ),
929
- keyOf: (m) => [m.user, m.group], // the two identities, in order
930
- });
856
+ ) });
931
857
  const enrolments = enrolmentService.createStoreSlot(db);
932
- enrolmentService.slotContentService.upsertEntity(enrolments, { user: alice, group: admins, since: 2026 });
858
+ enrolmentService.writeSlotContentByKey(enrolments, ((m) => [m.user, m.group])({ user: alice, group: admins, since: 2026 }), { user: alice, group: admins, since: 2026 });
933
859
  enrolmentService.getSlotByKey(enrolments, [alice, admins]);
934
860
  ```
935
861
 
936
- The two descents are `OIMKeyTreeWalkScalar` and `OIMKeyTreeWalkComposite`, and
862
+ The two descents are `OIMScalarKeyTreeWalk` and `OIMCompositeKeyTreeWalk`, and
937
863
  `OIMDefaults` holds one stateless instance of each. The composite walk
938
864
  is the one whose `holdCompositeKeyPart` runs `entitySlot.owner.holdSlot(entitySlot)` when `mintNodeAt` creates an edge, and
939
865
  whose `unholdCompositeKeyPart` runs `entitySlot.owner.unholdSlot(entitySlot)` (unless `oimIsSlotReleased(entitySlot)`) on exactly the edges `deleteKey` unlinked; the scalar walk holds nothing.
@@ -956,32 +882,28 @@ goes to `oimSameKeySequence`, which compares each key by SameValueZero, the way
956
882
 
957
883
  ## Writing
958
884
 
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.
885
+ The by-key write is on the collection service; every other write on an entity collection is on its
886
+ `slotContentService` and goes through an **identity you already hold**. A key from outside is resolved
887
+ to a slot once, with `findSlotByKey` (mints nothing) or `getSlotByKey` / `holdSlotByKey`.
964
888
 
965
- | Call on `entityService.slotContentService` | What it does |
889
+ | Call | What it does |
966
890
  |---|---|
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. |
891
+ | `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. |
892
+ | `writeSlotContent(slot, entity)` | A whole value through an identity you hold; `false` when nothing changed. |
972
893
  | `updateEntity(slot, patch)` | Patch through an identity you hold. Only the columns the patch actually moved are routed. |
973
894
  | `updateEntityColumn(slot, columnName, value)` | One column through an identity you hold, with no patch object to build and walk. |
974
895
  | `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. |
896
+ | `clearSlotContent(slot)` | The **value** leaves; the identity stays for as long as anything still refers to it. |
897
+ | `entityService.findSlotByKey(storeSlot, key)?.entity` | The entity itself rather than the slot. Mints nothing. |
979
898
  | `markEntityChanged(slot, changedColumns?)` | Report a change you made to `slot.entity` by hand. |
980
899
 
981
900
  By key, compose the two halves: `const s = entityService.findSlotByKey(storeSlot, key); if (s !== undefined) entityService.slotContentService.updateEntity(s, patch);`.
982
901
 
902
+ There are no bulk writes: many entities are a loop of these inside `entityService.writeInBatch(storeSlot, () => …)`,
903
+ so an index on the collection hears them in one call when the batch closes.
904
+
983
905
  **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
906
+ writer takes an `entityInserter` through `TOIMEntityWriterOptions`, so the choice is
985
907
  made once at construction and costs nothing per write:
986
908
 
987
909
  | strategy | what lands in the slot | what the caller keeps |
@@ -991,10 +913,7 @@ made once at construction and costs nothing per write:
991
913
  | `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
914
 
993
915
  ```typescript
994
- const copiedCardService = createOIMEntityService<Card>(db, {
995
- keyOf: (card) => card.id,
996
- entityWriter: createOIMEntityWriterInPlace({ entityInsertStrategy: oimInsertEntityWithCopy }),
997
- });
916
+ const copiedCardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db, { entityWriter: createOIMEntityWriterInPlace({ entityInserter: oimInsertEntityWithCopy }) }) });
998
917
  ```
999
918
 
1000
919
  **A copy is one level deep, and no deeper.** A nested object or
@@ -1016,7 +935,7 @@ markEntityChanged(
1016
935
  ): void
1017
936
  ```
1018
937
 
1019
- `upsertEntity` and `updateEntity` work out what moved: the writer walks the incoming
938
+ `writeSlotContentByKey` and `updateEntity` work out what moved: the writer walks the incoming
1020
939
  value (`oimPatchEntityColumns`, `oimPutEntityColumns`) and puts every column through `sameValueGuard`. A caller who edited the stored object
1021
940
  already knows what moved, and this door takes that answer instead of deriving
1022
941
  it, so there is no patch object, no copy and no per-column comparison.
@@ -1042,7 +961,7 @@ per-column walk.
1042
961
 
1043
962
  **An empty list wakes nothing at all** (not the columns, not the entity, not the
1044
963
  store slot), because `[]` is the caller saying that nothing moved, and the call returns
1045
- before the subframe opens.
964
+ before it writes anything.
1046
965
 
1047
966
  **It re-stamps emptiness** itself when the list is left out, when the entity is gone, or when
1048
967
  the service recomputes emptiness on a column write. A hand
@@ -1058,7 +977,7 @@ A misspelt column name, a bare runtime `string`, and a slot of a different entit
1058
977
  type are each a compile error, because the parameter is `readonly (keyof TEntity & string)[]`.
1059
978
 
1060
979
  **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
980
+ is in, so `clearSlotContent` cannot take it out of them. A bucket's slots belong to the caller: the
1062
981
  caller who put the slot in a bucket takes it out.
1063
982
 
1064
983
  ### Which entities actually changed: the `changed` array
@@ -1067,7 +986,7 @@ caller who put the slot in a bucket takes it out.
1067
986
  const payload: Card[] = [];
1068
987
  const reindex = (changedSlot: TOIMEntitySlot<Card, TOIMStoreKey>): void => {};
1069
988
  const changed: TOIMEntitySlot<Card, TOIMStoreKey>[] = [];
1070
- cardService.slotContentService.upsertEntities(cards, payload, changed);
989
+ cardService.writeInBatch(cards, () => (payload).map(entity => cardService.writeSlotContentByKey(cards, entity.id, entity, changed)));
1071
990
  for (const changedSlot of changed) reindex(changedSlot); // the re-sent entities are not in here
1072
991
  changed.length = 0;
1073
992
  ```
@@ -1076,7 +995,7 @@ The array is **optional, yours, and appended to**. The engine never reads it
1076
995
  and never clears it. A slot is pushed exactly when the write put in content different
1077
996
  from what that slot already held, so one array reused across a whole batch comes
1078
997
  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)`.
998
+ stated by key is `cardService.slotContentService.writeSlotContentByKey(cards, key, entity, changed)`.
1080
999
 
1081
1000
  It exists because `sameValueGuard` already refused those writes column by column,
1082
1001
  and without this parameter the verdict would stay inside the engine. A consumer that owns a
@@ -1086,8 +1005,8 @@ by the rule above, and a `{ slot, changed }` record would allocate once per
1086
1005
  write on the path this parameter exists to make cheaper. A caller who does not want it pays
1087
1006
  one `undefined` compare per write.
1088
1007
 
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
1008
+ One layer down, the slot doors that write a whole content (`writeSlotContent(slot, entity)`,
1009
+ `writeSlotContent(bucketSlot, bucketMemberSlots)`) compute it, and their boolean is **a fact about the content and says nothing about the
1091
1010
  identity**: `false` does not mean "this slot may go". That question is answered by `oimIsSlotEmpty(slot)`
1092
1011
  and the grace left on the idle window, asked by the release guard.
1093
1012
 
@@ -1095,19 +1014,19 @@ and the grace left on the idle window, asked by the release guard.
1095
1014
 
1096
1015
  ```typescript signature
1097
1016
  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;
1017
+ readonly fillEntity: (slot: TOIMEntityContentSlot, entity: object) => void;
1018
+ readonly putEntity: (slot: TOIMEntityContentSlot, entity: object, changedColumnNames: string[] | undefined) => boolean;
1019
+ readonly patchEntity: (slot: TOIMEntityContentSlot, patch: object, changedColumnNames: string[] | undefined) => boolean;
1101
1020
  readonly patchColumn: (
1102
- slot: TOIMEntitySlot, columnName: string, value: unknown, changedColumnNames: string[] | undefined,
1021
+ slot: TOIMEntityContentSlot, columnName: string, value: unknown, changedColumnNames: string[] | undefined,
1103
1022
  ) => boolean;
1104
- readonly deleteColumns: (slot: TOIMEntitySlot, names: readonly string[]) => readonly string[];
1105
- readonly clearEntity: (slot: TOIMEntitySlot) => void;
1023
+ readonly deleteColumns: (slot: TOIMEntityContentSlot, names: readonly string[]) => readonly string[];
1024
+ readonly clearEntity: (slot: TOIMEntityContentSlot) => void;
1106
1025
  }
1107
1026
 
1108
1027
  createOIMEntityWriterInPlace(options?) // the stored object keeps its identity
1109
1028
  createOIMEntityWriterReplacing(options?) // every write that changed something produces a new object
1110
- // TOIMEntityWriterOptions = { sameValueGuard?: TOIMSameValueGuard; entityInsertStrategy?: TOIMEntityInsertStrategy; putEntityColumns?: typeof oimPutEntityColumns }
1029
+ // TOIMEntityWriterOptions = { sameValueGuard?: TOIMSameValueGuard; entityInserter?: TOIMEntityInserter; putEntityColumns?: typeof oimPutEntityColumns }
1111
1030
  ```
1112
1031
 
1113
1032
  A writer moves an entity's columns and does nothing else: it reaches no channel,
@@ -1136,8 +1055,8 @@ stores every value frozen.
1136
1055
  `putEntity` is PUT and `patchEntity` is PATCH, as in HTTP. PUT makes the stored
1137
1056
  entity the value handed in: columns that changed are written and columns the value
1138
1057
  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
1058
+ only the columns the patch names. `writeSlotContentByKey` of a key that has a value and
1059
+ `writeSlotContent(slot, entity)` are PUT, so a whole-entity write that drops a column writes
1141
1060
  the slot to the channel once, and the replacing writer builds one new object, not
1142
1061
  two. This is counted in `src/facade/entity/tests/what-a-put-that-drops-a-column-costs.test.ts`.
1143
1062
 
@@ -1165,7 +1084,7 @@ A channel is the runtime: the frame counter, the revision, and the delivery pass
1165
1084
  const sentReports: unknown[] = [];
1166
1085
  const scheduledDb = new OIMDB({
1167
1086
  channelScheduler: (flush) => { void Promise.resolve().then(flush); },
1168
- errorPolicy: new OIMErrorPolicyLogAndContinue({ onError: (error) => { sentReports.push(error); } }),
1087
+ errorPolicy: new OIMLogAndContinueErrorPolicy({ onError: (error) => { sentReports.push(error); } }),
1169
1088
  onBeforeFlush: () => { /* the flush has begun; read only, no writes */ },
1170
1089
  });
1171
1090
  ```
@@ -1174,9 +1093,9 @@ With no `channelScheduler`, nothing is ever delivered until you call `db.flush()
1174
1093
  yourself. A second independent channel is a second `OIMDB`.
1175
1094
 
1176
1095
  **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
1096
+ `OIMLogAndContinueErrorPolicy`, logs each one with `console.error`, hands it to the
1178
1097
  optional `onError` (Sentry, the devtools registry), and finishes the rest of the
1179
- flush. `OIMErrorPolicyThrowAtOnce` instead throws the first one out of `flush()`;
1098
+ flush. `OIMThrowAtOnceErrorPolicy` instead throws the first one out of `flush()`;
1180
1099
  effects the frame had not reached stay queued for the next flush. An effect whose
1181
1100
  body or cleanup threw has not taken the change it woke for: it is stamped only
1182
1101
  after the body returns, so its next wake runs that change again. It is never
@@ -1206,14 +1125,14 @@ one effect service for the whole process, and every door is handed the `db` (any
1206
1125
 
1207
1126
  ```typescript
1208
1127
  // Sources are named in two lists: root slots, and derivations.
1209
- const effect = effectService.createEffect(db, [slot], [totalService.computedCollectionService.getSlotByKey(total, undefined)], () => {});
1128
+ const effect = effectService.createEffect(db, [slot], [totalService.computedSingleSlotService.getSingleSlot(total)], () => {});
1210
1129
  effectService.disposeEffect(effect);
1211
1130
  // The second list is typed `readonly TOIMComputedSlot[]` — pass slots taken from
1212
1131
  // a computed service's `computedCollectionService` straight in.
1213
1132
 
1214
1133
  // One column of one slot, woken when that named column moves and by nothing else.
1215
1134
  // It takes the COLUMN SLOT, which the column owner mints and holds.
1216
- const onRank = effectService.createEffect(db, [cardService.holdColumn(slot, 'rank')], [], () => {});
1135
+ const onRank = effectService.createEffect(db, [cardService.columnSlotService.holdColumn(slot, 'rank')], [], () => {});
1217
1136
  ```
1218
1137
 
1219
1138
  The body's type is `TOIMEffectBody`, which is `() => (() => void) | void`. **A returned
@@ -1282,13 +1201,13 @@ Here is how an effect is filed:
1282
1201
  `getColumn(slot, columnName)` reads the same column slot without a hold (a mint is held by
1283
1202
  the unused-slot collector until the next flush ends). **The column doors are on the collection
1284
1203
  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
1204
+ column owner (`slotOwnerService.columnSlotOwner`), which holds the column slot store and the column
1286
1205
  groups. With `columnNames` named the store is an array by ordinal, otherwise a Map. The column
1287
1206
  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
1207
+ write-driven column slots (`OIMPushColumnSlotOwner`), woken by the marks a write makes (entity
1289
1208
  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
1209
+ (`OIMPullColumnSlotOwner`), each a computed slot over its carrier (`OIMComputedService`, so
1210
+ `OIMSortedComputedService` and `OIMBucketMemoComputedService` too, keyed or single; see
1292
1211
  [Computed columns](#computed-columns)). A write-driven column slot is a
1293
1212
  `TOIMColumnSlot` — `{ carrierSlot, key }` plus the slot fields — and its carrier is
1294
1213
  whatever slot it hangs off: an entity slot, or a bucket slot (see
@@ -1307,7 +1226,7 @@ frozen object per set of names on that service; get it once per view, not per en
1307
1226
  and `unholdColumn(columnSlot)` gives it back. A write wakes it once if it changes any of the
1308
1227
  group's columns and not at all if it changes only others.
1309
1228
 
1310
- **`columnNames` picks the column slot store.** `createOIMEntityService<Card, 'rank' | 'title'>(db, { keyOf, columnNames: ['rank', 'title'] })`
1229
+ **`columnNames` picks the column slot store.** `createOIMSlotCollectionService<Card, 'rank' | 'title'>(db, { keyOf, columnNames: ['rank', 'title'] })`
1311
1230
  keeps each entity's column slots in an array by column number instead of a `Map` per entity,
1312
1231
  which makes an entity's column store cheaper to create and give back; that is what a scrolling view pays.
1313
1232
  `holdColumn` and `getColumnGroup` are typed on the named columns, and a slot for any other
@@ -1333,7 +1252,7 @@ doors default it to the shared map-driven `OIMDefaults.COLUMN_SLOT_STORE_SERVICE
1333
1252
  - **A door asks once and then writes without asking again.** `OIMChannelService.write` is
1334
1253
  `assertNotFlushing` plus `writeWithoutFlushCheck`, and a content service door
1335
1254
  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
1255
+ cannot be removed: user code (`keyOf`, an index's `keyOf`, `slotEmptinessPolicy.isEmpty`, an
1337
1256
  `IOIMEntityWriter`) runs inside the door and can call `flush()`, so each entity is
1338
1257
  a fresh question. `tests/who-may-write-without-asking-the-channel.test.ts` pins
1339
1258
  who may use the door that skips the check.
@@ -1348,43 +1267,20 @@ doors default it to the shared map-driven `OIMDefaults.COLUMN_SLOT_STORE_SERVICE
1348
1267
  handler hands the work to a microtask or a later callback instead of writing
1349
1268
  itself. A refusal raised from a tail handler goes to the error policy, not out of
1350
1269
  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.
1270
+ - **A batch changes who hears the writes, not when the frame runs.** `openBatch(storeSlot)` /
1271
+ `closeBatch(storeSlot)` on a collection service, or `writeInBatch(storeSlot, write)`, count a depth on
1272
+ the store slot. While it is open, the content changes are queued there, and the outermost close hands
1273
+ the content-change listeners (an index) the whole batch in one call. A flush inside a batch runs as
1274
+ usual; the index catches up when the batch closes. Nothing is rolled back if a listener throws.
1363
1275
  - **Every bucket door wakes the bucket the same way.** Each bucket content service
1364
1276
  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.
1277
+ `writeSlotContent`, `clearSlotContent`) settles its EMPTY flag, writes it to the channel, tells the
1278
+ content-change listeners and asks whether it may go. A door that changed nothing stamps nothing and
1279
+ tells nobody. Commands are recorded at the door, where the position is true.
1372
1280
  - **A released identity is refused before the bucket changes.** Every door that
1373
1281
  takes a member checks first whether what it was handed is released (`oimIsSlotReleased`), and throws, so a
1374
1282
  refused door leaves the members, their holds and the EMPTY flag as they were. The check costs one
1375
1283
  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
1284
  ### `subscribeOnceAfterFrame` — one frame, and the caller picks which
1389
1285
 
1390
1286
  ```typescript
@@ -1466,7 +1362,7 @@ that stays refused, and nothing may ask for one slot to go.
1466
1362
  ## Buckets: the slots are the caller's
1467
1363
 
1468
1364
  A **bucket** is an identity whose content is other identities. A
1469
- **bucket collection** (`createOIMBucketService`) files buckets by key, in each store slot it
1365
+ **bucket collection** (`createOIMSlotCollectionService`) files buckets by key, in each store slot it
1470
1366
  creates, and its `slotContentService` holds each bucket's member slots. It is **manual by default**, and that
1471
1367
  is a claim about what a bucket's slots ARE; it is not a missing feature. They are
1472
1368
  a fact about the **response** a caller is composing (a search result, a page, a
@@ -1478,38 +1374,36 @@ computeds), and the bucket's type parameter is that slot type,
1478
1374
  `getEntitiesByKey` is there because its members are entity slots.
1479
1375
 
1480
1376
  ```typescript
1481
- const byDeckService = createOIMBucketService<
1482
- CardSlot, TOIMStoreKey, TOIMBucketSlotsSetStore<CardSlot>, OIMSlotContentBucketServiceSetDriven<CardSlot>
1483
- >(db, { slotContentService: new OIMSlotContentBucketServiceSetDriven<CardSlot>() });
1377
+ const byDeckService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentSetBucketService<CardSlot>() });
1484
1378
  const byDeck = byDeckService.createStoreSlot(db);
1485
1379
 
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 });
1380
+ const aceSlot = cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', deck: 'red', title: 'Ace', rank: 1 });
1381
+ const kingSlot = cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', deck: 'red', title: 'King', rank: 13 });
1488
1382
 
1489
1383
  const red = byDeckService.holdSlotByKey(byDeck, 'red');
1490
1384
  const deckContentService = byDeckService.slotContentService;
1491
- deckContentService.writeBucketMemberSlots(red, [aceSlot, kingSlot]);
1385
+ deckContentService.writeSlotContent(red, [aceSlot, kingSlot]);
1492
1386
  db.flush();
1493
1387
 
1494
1388
  for (const cardSlot of deckContentService.getBucketMemberSlots(red)) { console.log(cardSlot.entity?.title); }
1495
1389
  deckContentService.getBucketMemberSlotCount(red);
1496
1390
 
1497
- deckContentService.clearBucketMemberSlots(red);
1391
+ deckContentService.clearSlotContent(red);
1498
1392
  red.owner.unholdSlot(red);
1499
1393
  ```
1500
1394
 
1501
1395
  The store slot carries the keyed store: there is no `buckets` property to reach
1502
1396
  through, and `holdSlotByKey(byDeck, key)` hands back the bucket itself instead of a
1503
1397
  `{ 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.
1398
+ its buckets go; `OIMSlotAtUnholdReleasePolicy.INSTANCE` drops a bucket at the last give-back.
1505
1399
 
1506
- `writeBucketMemberSlots` is idempotent: when you re-send an unchanged list, the
1400
+ `writeSlotContent` is idempotent: when you re-send an unchanged list, the
1507
1401
  content service compares it (it is the only object that knows whether the caller's order is part
1508
1402
  of what it holds at all) and returns without evicting anything.
1509
1403
 
1510
1404
  ### Which door: you hold the answer, or you hold the delta
1511
1405
 
1512
- `writeBucketMemberSlots` is for a caller who has **the list that should be there** and does not
1406
+ `writeSlotContent` is for a caller who has **the list that should be there** and does not
1513
1407
  know what changed: a search result, a page from a server, a filter re-run. Its
1514
1408
  cost is proportional to **what the bucket holds**, not to the change. The
1515
1409
  difference builds a map over the whole incoming list and walks the standing one,
@@ -1530,7 +1424,7 @@ set-driven bucket removes in the order you name.
1530
1424
 
1531
1425
  **`addBucketMemberSlots` removes nothing.** If the authoritative list shrank and you call
1532
1426
  `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
1427
+ exactly when `writeSlotContent` is the right door, and its cost is the price of not
1534
1428
  knowing.
1535
1429
 
1536
1430
  The content service commits the list through its protected
@@ -1551,7 +1445,7 @@ empties it once the flush has fully ended, so a script can only ever describe th
1551
1445
  frame a consumer was woken for. A bucket written after that frame (from an
1552
1446
  after-each-frame handler) keeps its script for the next one: the reset clears only a script
1553
1447
  whose `commandsStartedAtRevision`/`commandsStartedAtEpoch` is older than the reading the
1554
- channel slot's last frame opened at. `clearBucketMemberSlots` drains the bucket first and
1448
+ channel slot's last frame opened at. `clearSlotContent` drains the bucket first and
1555
1449
  marks it changed only when the drain removed something, so an already-empty bucket records
1556
1450
  nothing for a call that reports nothing.
1557
1451
 
@@ -1560,47 +1454,43 @@ is created on a `db` and reaches its channel slot through `storeSlot.channelSlot
1560
1454
  strategy that tells nobody. `OIMChannelService.INSTANCE.write` carries the frame guard, which is
1561
1455
  why a whole-bucket write cannot report from inside a flush.
1562
1456
 
1563
- ### How a bucket holds its slots is a fork with two strategies
1457
+ ### How a bucket holds its slots is a fork of content services
1564
1458
 
1565
1459
  | Content service | Storage | One `addBucketMemberSlot` records |
1566
1460
  |---|---|---|
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 |
1461
+ | `OIMSlotContentSetBucketService` | `Set` | one per slot |
1462
+ | `OIMSlotContentPositionalArrayBucketService({ bucketCommands?, bucketMemberSlotsArrayService? })` | list, plus a `Set` with the unique member slots service | one per mention; one per slot, order kept, with `OIMBucketUniqueMemberSlotsArrayService` |
1463
+ | `OIMSlotContentSortedArrayBucketService({ compareBucketMemberSlots })` | list ordered by the comparator (`sortedCount` marks the sorted prefix) | one per mention |
1570
1464
 
1571
1465
  The positions are the caller's. No read re-decides the order, and the position
1572
1466
  doors below are the only way the list moves.
1573
1467
 
1574
- ### And what naming a slot RECORDS is the array-driven service's own fork
1468
+ ### And what naming a slot RECORDS is a member slots service
1575
1469
 
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 |
1470
+ | `bucketMemberSlotsArrayService` | One `addBucketMemberSlot` counts |
1471
+ |---|---|
1472
+ | `OIMBucketMemberSlotsArrayService` (the default) | one per *mention* — a slot named twice is two |
1473
+ | `new OIMBucketUniqueMemberSlotsArrayService()` | one per slot, order kept |
1580
1474
 
1581
1475
  ```typescript
1582
- const dedupedContentService = new OIMSlotContentBucketServiceArrayDriven<
1583
- CardSlot, TOIMBucketSlotsArrayUniqueStore<CardSlot>
1584
- >({
1585
- bucketArrayMembership: new OIMBucketArrayMembershipUnique<CardSlot>(),
1476
+ const dedupedContentService = new OIMSlotContentPositionalArrayBucketService<CardSlot>({
1477
+ bucketMemberSlotsArrayService: new OIMBucketUniqueMemberSlotsArrayService<CardSlot>(),
1586
1478
  });
1587
1479
  ```
1588
1480
 
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.
1481
+ The member slots service is the only writer of an array bucket's list and the `Set` beside it, so the two never
1482
+ disagree. With the unique one `hasBucketMemberSlot` answers from the `Set`; with the default it walks the array.
1593
1483
 
1594
1484
  ### Positions the caller decides, and the commands that record them
1595
1485
 
1596
- `createOIMBucketService<CardSlot>(db)` with no `slotContentService` is the bucket collection to
1486
+ `createOIMSlotCollectionService<CardSlot>(db)` with no `slotContentService` is the bucket collection to
1597
1487
  take when the positions are yours: its default content service is
1598
- `OIMSlotContentBucketServiceArrayDriven` with an `OIMBucketCommands` recording over it.
1488
+ `OIMSlotContentPositionalArrayBucketService` with an `OIMBucketCommands` recording over it.
1599
1489
 
1600
1490
  The position doors follow; a position they return is valid only until the next write. The reads
1601
1491
  `getBucketMemberSlotAt(bucketSlot, index)` and `getBucketMemberSlotPosition(bucketSlot, entitySlot)`
1602
1492
  belong to the array-based content services, and the five writes to
1603
- `OIMSlotContentBucketServiceArrayDriven` (`OIMSlotContentBucketServiceSetDriven` has none of
1493
+ `OIMSlotContentPositionalArrayBucketService` (`OIMSlotContentSetBucketService` has none of
1604
1494
  them, because a `Set` has no place to name, so asking one does not compile):
1605
1495
  `insertBucketMemberSlotAt(bucketSlot, at, entitySlot)`, `removeBucketMemberSlotAt(bucketSlot, at)`,
1606
1496
  `removeBucketMemberSlotRange(bucketSlot, at, count)`, `moveBucketMemberSlotToPosition(bucketSlot, from, to)` and
@@ -1610,8 +1500,8 @@ of the list; `moveBucketMemberSlotToPosition` refuses `from === to` and either e
1610
1500
  range; `moveBucketMemberSlotRangeToPosition` reads its landing in the list **without** the run
1611
1501
  in it. A position or count that is `NaN` or a fraction throws before anything
1612
1502
  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
1503
+ inserting a slot the bucket already holds gives it a second position. Take
1504
+ `OIMBucketUniqueMemberSlotsArrayService` when a position must name one entity; a
1615
1505
  repeat insert into it changes nothing and records nothing.
1616
1506
 
1617
1507
  The content service's protected `removeSlot(bucketSlot, slot)` hands back a `number`: WHERE
@@ -1639,7 +1529,7 @@ from the bucket's for good.
1639
1529
  consumer patches instead of rebuilding:
1640
1530
 
1641
1531
  ```typescript
1642
- const pageService = createOIMBucketService<CardSlot>(db);
1532
+ const pageService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1643
1533
  const page = pageService.createStoreSlot(db);
1644
1534
  const bucket = pageService.holdSlotByKey(page, 'p1');
1645
1535
 
@@ -1666,11 +1556,11 @@ a frame has opened after it (`channelSlot.frame.openedAtRevision`/`openedAtEpoch
1666
1556
  recorder clears a stale script before recording into it, so a write that lands after a
1667
1557
  flush, ahead of the recorder's reset, starts a new script instead of extending one
1668
1558
  that was already read. `isWholeListRecorded` is set by a write
1669
- of the whole bucket (`writeBucketMemberSlots`, `clearBucketMemberSlots`, `moveBucketMemberSlotRangeToPosition`), by a
1559
+ of the whole bucket (`writeSlotContent`, `clearSlotContent`, `moveBucketMemberSlotRangeToPosition`), by a
1670
1560
  removal from a bucket whose content service keeps no positions and so has no index to
1671
1561
  patch, and by
1672
1562
  the script outgrowing the list it describes. `maxCommandsPerSlot` on
1673
- `TOIMBucketCommandsOptions` (and on `createOIMBucketService`'s options) is that ratio. It defaults to
1563
+ `TOIMBucketCommandsOptions` (and on `createOIMSlotCollectionService`'s options) is that ratio. It defaults to
1674
1564
  `OIM_MAX_COMMANDS_PER_SLOT`, which is 1, the same ratio
1675
1565
  `oimOrderedListCommandsOfTransition` takes as `maxCommandsPerSlot`. A consumer that animates moves cannot
1676
1566
  use a whole-list answer, because it costs them every move they were going to animate.
@@ -1680,7 +1570,7 @@ grows.
1680
1570
  **Recording is chosen when the content service is built.** `bucketCommands` on
1681
1571
  `TOIMSlotContentBucketServiceOptions` is read once into a readonly field:
1682
1572
  `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
1573
+ `OIMNoBucketCommands.INSTANCE`, whose methods are empty and take positions instead of a
1684
1574
  command object, so a bucket that nobody renders positionally allocates no command. There is no
1685
1575
  setter, so a second recorder cannot silently replace the first. The first script recorded on a
1686
1576
  channel slot subscribes one reset to `OIMChannelService.INSTANCE.subscribeAfterEachFlush` for
@@ -1694,7 +1584,7 @@ per-bucket state, so **one instance may serve two bucket collections**.
1694
1584
  ### The other strategy: an index that fills itself
1695
1585
 
1696
1586
  ```typescript
1697
- const indexService = createOIMIndexService(db, {
1587
+ const indexService = createOIMSlotCollectionIndexService(db, {
1698
1588
  entityService: cardService,
1699
1589
  keyOf: (card: Card) => card.deck, // which key an entity belongs to
1700
1590
  });
@@ -1706,7 +1596,7 @@ const spades = indexService.holdSlotByKey(index, 'spades'); // the hold mints
1706
1596
  effectService.createEffect(db, [spades], [], () => render(Array.from(indexService.slotContentService.getBucketMemberSlots(spades))));
1707
1597
  ```
1708
1598
 
1709
- It **is** a bucket collection (`OIMIndexService` extends `OIMSlotCollectionService`), and
1599
+ It **is** a bucket collection (`OIMSlotCollectionIndexService` extends `OIMSlotCollectionService`), and
1710
1600
  `createIndexStoreSlot(cards)` makes its store slot for one entity store slot and files every
1711
1601
  entity of it into the buckets on write, from `keyOf`. You read through the index service
1712
1602
  (`getSlotByKey`, `holdSlotByKey`, `getSlotsByKeys`, `holdSlotsByKeys`, `getSlots`,
@@ -1715,8 +1605,8 @@ entity of it into the buckets on write, from `keyOf`. You read through the index
1715
1605
  `hasBucketMemberSlot`, and on an array-based content service `getBucketMemberSlotAt` and
1716
1606
  `getBucketMemberSlotPosition`). That `slotContentService` is narrowed to those reads, so the
1717
1607
  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
1608
+ `OIMSlotContentPositionalArrayBucketService`; pass `slotContentService:
1609
+ new OIMSlotContentSetBucketService()` for buckets that keep no positions. Passing
1720
1610
  `entityService` and then the entity store slot is the whole wiring: `createIndexStoreSlot(cards)`
1721
1611
  registers a content-change listener on that store slot
1722
1612
  (`OIMContentChangeListenerService.INSTANCE.addContentChangeListener(cards, …)`), and the entity
@@ -1736,9 +1626,8 @@ path for rendering**, because one edit of one entity wakes the whole list. Subsc
1736
1626
  to the bucket and each entity to its own slot. The any-column slot costs
1737
1627
  something only while somebody holds or subscribes to it.
1738
1628
 
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.
1629
+ Writes inside `writeInBatch` (or `openBatch` / `closeBatch`) notify the index once per batch. The index
1630
+ groups entities by key and moves each group with one bucket write.
1742
1631
 
1743
1632
  Know these four things before choosing it:
1744
1633
 
@@ -1747,15 +1636,15 @@ Know these four things before choosing it:
1747
1636
  entities written before it, without a flush, so minting inside an effect body does not
1748
1637
  throw. `getSlotsByKeys` / `holdSlotsByKeys` fill all new buckets in ONE pass over the source. How long the bucket lives then is the
1749
1638
  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
1639
+ when you name none; with neither, the bucket is kept for good. `OIMSlotAfterIdleReleasePolicy`
1640
+ cools it on the queue, and `OIMSlotAtUnholdReleasePolicy.INSTANCE` ends it at
1752
1641
  the last give-back. The index injects no lifetime of its own.
1753
1642
  - **The key comes from `columns`, `keyOf`, or both**, the same pair as the sorter's
1754
1643
  `sortColumns` / `comparator`. The key type is inferred from the columns (`TOIMIndexKey`):
1755
1644
  `{ 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
1645
+ `createOIMSlotCollectionIndexService(db, { entityService, columns: ['deck', 'suit'] })` is keyed by the
1757
1646
  tuple of their values and builds the bucket collection on a tree-driven store
1758
- (`OIMSlotStoreServiceTreeDriven`) by itself, while `createOIMIndexService` without a
1647
+ (`OIMSlotTreeStoreService`) by itself, while `createOIMSlotCollectionIndexService` without a
1759
1648
  `bucketSlotStoreService` refuses two columns. When the index asks
1760
1649
  whether a key moved, it asks its own `isSameKey(key, otherKey)`, which
1761
1650
  every collection answers from its own slot store service.
@@ -1773,13 +1662,13 @@ Know these four things before choosing it:
1773
1662
  ### A sorted list is a computed over an index
1774
1663
 
1775
1664
  ```typescript
1776
- const indexService = createOIMIndexService(db, {
1665
+ const indexService = createOIMSlotCollectionIndexService(db, {
1777
1666
  entityService: cardService,
1778
1667
  keyOf: (card: Card) => card.deck,
1779
1668
  });
1780
1669
  const index = indexService.createIndexStoreSlot(cards);
1781
1670
 
1782
- const byRankInDeckService = createOIMComputedServiceSorted({
1671
+ const byRankInDeckService = createOIMSortedComputedService({
1783
1672
  indexService,
1784
1673
  sortColumns: [{ column: 'rank', direction: 'asc' }],
1785
1674
  });
@@ -1790,14 +1679,14 @@ effectService.createEffect(db, [], [spades], () => { console.log(spades.computed
1790
1679
  spades.owner.unholdSlot(spades);
1791
1680
  ```
1792
1681
 
1793
- `createOIMComputedServiceSorted({ indexService, sortColumns?, comparator?, slotReleasePolicy? })`
1682
+ `createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy? })`
1794
1683
  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
1684
+ channel slot. It returns an `OIMSortedComputedService`, whose
1796
1685
  `createSortedStoreSlot(indexStoreSlot)` makes one store slot over one index store slot. Its doors
1797
1686
  are on its `computedCollectionService`: `getSlotByKey`, `holdSlotByKey`, `findSlotByKey`, each
1798
1687
  with that store slot first. A key's value, read with
1799
1688
  `computedSlot.computedValueService.getValueForSlot(computedSlot)`, is that key's entity slots,
1800
- sorted. `indexService` is the `OIMIndexService` whose buckets it sorts.
1689
+ sorted. `indexService` is the `OIMSlotCollectionIndexService` whose buckets it sorts.
1801
1690
 
1802
1691
  - `sortColumns` is `readonly TOIMSortColumn<TEntity>[]`, each `{ column, direction: 'asc' | 'desc' }`.
1803
1692
  `column` is a `TOIMSortableColumnName<TEntity>`, meaning a column whose values are strings,
@@ -1813,8 +1702,8 @@ sorted. `indexService` is the `OIMIndexService` whose buckets it sorts.
1813
1702
  `sortColumns` omits does not re-sort. Use both when the order needs a rule
1814
1703
  `oimColumnsComparator` cannot express (a locale compare, a tie-break) and the sort
1815
1704
  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
1705
+ - `slotReleasePolicy` (default `OIMSlotAtUnholdReleasePolicy.INSTANCE`) decides whether a key's
1706
+ list outlives its last hold. `OIMSlotNeverReleasePolicy.INSTANCE` keeps every key ever
1818
1707
  read and re-sorts only on a read that finds it changed, which suits a view that switches
1819
1708
  between groups. Give the index the same policy, or its bucket is rebuilt instead.
1820
1709
 
@@ -1832,7 +1721,7 @@ The other strategy keeps the index's own buckets in order, so a read needs no co
1832
1721
  The consumer picks one at construction.
1833
1722
 
1834
1723
  ```typescript
1835
- const cardsByDeckService = createOIMIndexServiceSortedArrayDriven(db, {
1724
+ const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService(db, {
1836
1725
  entityService: cardService,
1837
1726
  columns: ['deck'],
1838
1727
  sortColumns: [{ column: 'rank', direction: 'asc' }, { column: 'id', direction: 'desc' }],
@@ -1842,18 +1731,18 @@ const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
1842
1731
  Array.from(cardsByDeckService.slotContentService.getBucketMemberSlots(spades));
1843
1732
  ```
1844
1733
 
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.
1734
+ `createOIMSlotCollectionSortedArrayIndexService(db, { entityService, columns? | keyOf?, sortColumns, slotReleasePolicy?, bucketSlotStoreService? })`
1735
+ returns an `OIMSlotCollectionSortedArrayIndexService`, whose content service is an
1736
+ `OIMSlotContentSortedArrayBucketService`. `sortColumns` is the same `readonly TOIMSortColumn<TEntity>[]`
1737
+ that `createOIMSortedComputedService` takes.
1849
1738
 
1850
1739
  - Writes append to an unsorted tail of the bucket; the first read settles it, by binary
1851
1740
  insertion for a short tail or by one sort for a long one.
1852
1741
  - The index re-files a member when one of its sort columns changes, and only then.
1853
1742
  - `getBucketMemberSlots`, `getBucketMemberSlotAt` and `getBucketMemberSlotPosition` answer in order.
1854
- - The bucket has no positional writes, and its bucket commands are always `OIMBucketCommandsNone.INSTANCE`.
1743
+ - The bucket has no positional writes, and its bucket commands are always `OIMNoBucketCommands.INSTANCE`.
1855
1744
  - 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.
1745
+ (`createOIMSortedComputedService` over any index) suits bulk loads.
1857
1746
 
1858
1747
  ### Bucket column slots
1859
1748
 
@@ -1864,14 +1753,14 @@ bucket slot, and it is written when a write changes that column on any member of
1864
1753
  bucket. `markBucketColumnsChanged(bucketSlot, patch, changedColumns)` on the bucket content
1865
1754
  service is the door the index calls, and the door for whoever changes the members of a
1866
1755
  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
1756
+ `OIMSlotCollectionIndexService`: it carries its buckets' doors, the bucket column slot doors and the
1868
1757
  bucket any-column slot doors (`getAnyBucketColumnByKey(indexStoreSlot, key)`,
1869
1758
  `holdAnyBucketColumnByKey(indexStoreSlot, key)`; both are given back with
1870
1759
  `indexService.unholdColumn`), and its `slotContentService` carries the member reads,
1871
1760
  the positional ones of the array-based kinds included.
1872
1761
 
1873
1762
  ```typescript
1874
- const decksService = createOIMIndexService(db, {
1763
+ const decksService = createOIMSlotCollectionIndexService(db, {
1875
1764
  entityService: cardService,
1876
1765
  keyOf: (card: Card) => card.deck,
1877
1766
  });
@@ -1880,7 +1769,7 @@ const decks = decksService.createIndexStoreSlot(cards);
1880
1769
  const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
1881
1770
  const onRankInSpades = effectService.createEffect(db, [rankOfSpades], [], () => {});
1882
1771
  effectService.disposeEffect(onRankInSpades);
1883
- decksService.unholdColumn(rankOfSpades);
1772
+ decksService.columnSlotService.unholdColumn(rankOfSpades);
1884
1773
  ```
1885
1774
 
1886
1775
  ### Index keys strategy
@@ -1892,22 +1781,22 @@ slot was given (`TOIMIndexStoreSlot.indexKeysPosition`, allocated from the sourc
1892
1781
  `indexKeysPositions`). Choose the service with the `indexKeysService` option of `OIMDB`; every
1893
1782
  index the factories build on that `db` takes it (default `OIMDefaults.INDEX_KEYS_SERVICE.getValue()`).
1894
1783
 
1895
- - `OIMIndexKeysServiceAppending` (default): a disposed index's position is never
1784
+ - `OIMAppendingIndexKeysService` (default): a disposed index's position is never
1896
1785
  reused, and its stale entry stays on each entity slot. A slot's `indexKeys` grows to the highest
1897
1786
  position any index wrote on it, so after K indexes are made and disposed, a slot the
1898
1787
  last one filed carries an array of K, and a stale entry keeps its key value
1899
1788
  reachable. A write pays nothing for a disposed index: the store slot keeps only the
1900
1789
  live listeners, in the order they were added.
1901
- - `OIMIndexKeysServiceReclaiming`: on dispose, it wipes that position on every entity slot of the
1790
+ - `OIMReclaimingIndexKeysService`: on dispose, it wipes that position on every entity slot of the
1902
1791
  source store slot (O(N)) and reuses it.
1903
1792
 
1904
- Switch to `OIMIndexKeysServiceReclaiming` only if you create indexes dynamically and often.
1793
+ Switch to `OIMReclaimingIndexKeysService` only if you create indexes dynamically and often.
1905
1794
  Creating indexes that way is expensive; better not to.
1906
1795
 
1907
1796
  ### Rendering an ordered bucket
1908
1797
 
1909
1798
  ```typescript setup
1910
- const orderedBucketsService = createOIMBucketService<CardSlot>(db);
1799
+ const orderedBucketsService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1911
1800
  const orderedBuckets = orderedBucketsService.createStoreSlot(db);
1912
1801
  ```
1913
1802
 
@@ -1930,11 +1819,10 @@ for (const command of oimOrderedListCommandsOfTransition(whatIRendered, Array.fr
1930
1819
  is typed `Iterable` and not an array. A `prev` kept live is the same array as `next`
1931
1820
  on the following frame, so the diff comes back empty, no reorder is ever drawn,
1932
1821
  and nothing throws. Keep the `Array.from(...)` copy, or subscribe to
1933
- `createOIMComputedServiceBucketMemo`, whose value is a fresh array on every write
1822
+ `createOIMBucketMemoComputedService`, whose value is a fresh array on every write
1934
1823
  that changed the list and the same array while it did not.
1935
1824
 
1936
- A list may name one slot twice: `OIMBucketArrayMembershipNonUnique`, the default strategy
1937
- of `OIMSlotContentBucketServiceArrayDriven`, records every mention. The mentions of one identity
1825
+ A list may name one slot twice: `OIMSlotContentPositionalArrayBucketService` records every mention. The mentions of one identity
1938
1826
  are matched in the order they appear, and the script reproduces the new list
1939
1827
  mention for mention. The minimal move count is claimed for lists whose identities
1940
1828
  are distinct.
@@ -1948,8 +1836,7 @@ channel service.
1948
1836
  ## Derivations
1949
1837
 
1950
1838
  ```typescript setup
1951
- const totalService = createOIMComputedService<void, number, TOIMSingleSlotStore<TOIMComputedSlot<number, void>>>({
1952
- computedSlotStoreService: new OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<number, void>>(),
1839
+ const totalService = createOIMSingleComputedService<number>({
1953
1840
  sameValueGuard: oimSameValueZeroGuard, // did the value move?
1954
1841
  });
1955
1842
  const total = totalService.createComputedStoreSlot(
@@ -1959,7 +1846,7 @@ const total = totalService.createComputedStoreSlot(
1959
1846
  sourceSlots: [slot], // root slots it reads
1960
1847
  }),
1961
1848
  );
1962
- const totalSlot = totalService.computedCollectionService.getSlotByKey(total, undefined);
1849
+ const totalSlot = totalService.computedSingleSlotService.getSingleSlot(total);
1963
1850
  ```
1964
1851
 
1965
1852
  `createSources` returns `sourceSlots` (root slots it reads) and `sourceComputeds` (derivations it reads). Both keys are optional; a missing one means none.
@@ -1995,7 +1882,7 @@ effectService.createEffect(db, [], [totalSlot], () => { console.log(totalSlot.co
1995
1882
 
1996
1883
  ```typescript setup
1997
1884
  const ranksService = createOIMComputedService<TOIMStoreKey, number>({
1998
- createSources: (id) => ({ sourceSlots: [cardService.getSlotByKey(cards, id)] }),
1885
+ createSources: ({ key }) => ({ sourceSlots: [cardService.getSlotByKey(cards, key)] }),
1999
1886
  derive: (computedSlot) => cardService.getSlotByKey(cards, computedSlot.key).entity?.rank ?? -1,
2000
1887
  sameValueGuard: oimSameValueZeroGuard,
2001
1888
  initialValue: -1,
@@ -2017,10 +1904,10 @@ holds every entry of `sourceSlots` and `sourceComputeds`, and
2017
1904
  the content strategy's `releaseSlotContent` gives all of them back. So the source list IS the dependency
2018
1905
  list, and a source that a build closes over without naming is a source that nothing holds.
2019
1906
 
2020
- A derivation is never empty: `OIMSlotContentComputedStrategy.isSlotEmpty` answers `false`,
1907
+ A derivation is never empty: `OIMSlotContentComputedService.isSlotEmpty` answers `false`,
2021
1908
  so a filed derivation goes only when nothing holds it,
2022
1909
  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.
1910
+ `OIMSlotAtUnholdReleasePolicy.INSTANCE` answers, and it is the default when no `slotReleasePolicy` is given.
2024
1911
  A derivation let go of while the channel is flushing is not released on the spot. It is
2025
1912
  handed to the channel's unused-slot collector and released when the flush ends, the same
2026
1913
  deferral that every other non-empty slot gets.
@@ -2028,12 +1915,12 @@ deferral that every other non-empty slot gets.
2028
1915
 
2029
1916
  `createOIMComputedService` is `OIMComputedService` with its generics
2030
1917
  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.
1918
+ `computedSlotStoreService`, which a `void` key requires (`OIMSingleSlotStoreService`); both are exported.
2032
1919
 
2033
1920
  ### Computed columns
2034
1921
 
2035
1922
  A computed's value can be subscribed to by field. On the `computedCollectionService` of
2036
- `OIMComputedService` (and of `OIMComputedServiceSorted` and `OIMComputedServiceBucketMemo`),
1923
+ `OIMComputedService` (and of `OIMSortedComputedService` and `OIMBucketMemoComputedService`),
2037
1924
  keyed or single, where a single takes `getSlotByKey(storeSlot, undefined)` as the slot, sit `getColumn(slot, columnName)`, `holdColumn(slot, columnName)`,
2038
1925
  `getColumnGroup(names)`, `holdColumnGroup(slot, group)`, `unholdColumn(columnSlot)` and the any-column
2039
1926
  pair `getAnyColumn(slot)` / `holdAnyColumn(slot)`. Column
@@ -2046,19 +1933,17 @@ the group's fields for a group, ordered by the sorted column names; the whole va
2046
1933
 
2047
1934
  ```typescript
2048
1935
  type TCardSummary = { top: number; count: number };
2049
- const cardSummaryService = createOIMComputedService<void, TCardSummary, TOIMSingleSlotStore<TOIMComputedSlot<TCardSummary, void>>>({
2050
- computedSlotStoreService: new OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<TCardSummary, void>>(),
2051
- });
1936
+ const cardSummaryService = createOIMSingleComputedService<TCardSummary>();
2052
1937
  const cardSummary = cardSummaryService.createComputedStoreSlot(
2053
1938
  db,
2054
1939
  () => ({ top: slot.entity?.rank ?? -1, count: cardService.getSlotCount(cards) }),
2055
1940
  () => ({ sourceSlots: [slot] }),
2056
1941
  );
2057
- const summaryCollectionService = cardSummaryService.computedCollectionService;
2058
- const topColumn = summaryCollectionService.holdColumn(summaryCollectionService.getSlotByKey(cardSummary, undefined), 'top');
1942
+ const summarySingleSlotService = cardSummaryService.computedSingleSlotService;
1943
+ const topColumn = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(cardSummary), 'top');
2059
1944
  const onTop = effectService.createEffect(db, [], [topColumn], () => { console.log(topColumn.value); });
2060
1945
  effectService.disposeEffect(onTop);
2061
- summaryCollectionService.unholdColumn(topColumn);
1946
+ summarySingleSlotService.columnSlotService.unholdColumn(topColumn);
2062
1947
  ```
2063
1948
 
2064
1949
  ### The order of a bucket is one of those derivations
@@ -2067,7 +1952,7 @@ Subscribe to the DERIVATION instead of the bucket when a consumer wants the
2067
1952
  list as an array it can compare by identity:
2068
1953
 
2069
1954
  ```typescript
2070
- const orderService = createOIMComputedServiceBucketMemo({
1955
+ const orderService = createOIMBucketMemoComputedService({
2071
1956
  bucketCollectionService: orderedBucketsService,
2072
1957
  });
2073
1958
  const order = orderService.createBucketMemoStoreSlot(orderedBuckets);
@@ -2087,7 +1972,7 @@ The source list is ONE slot, the bucket, and it never grows: a change to its slo
2087
1972
  is a move of the bucket's value, not of the derivation's sources.
2088
1973
 
2089
1974
  A sorted list is the other derivation of this kind:
2090
- [`createOIMComputedServiceSorted`](#a-sorted-list-is-a-computed-over-an-index) over an index.
1975
+ [`createOIMSortedComputedService`](#a-sorted-list-is-a-computed-over-an-index) over an index.
2091
1976
 
2092
1977
  ---
2093
1978
 
@@ -2096,29 +1981,27 @@ A sorted list is the other derivation of this kind:
2096
1981
  ```typescript
2097
1982
  type Settings = { theme: string; fontSize: number };
2098
1983
 
2099
- const settingsService = createOIMSingleEntityService<Settings>(db, {
2100
- columnNames: ['theme', 'fontSize'],
2101
- });
1984
+ const settingsService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<Settings>(db), columnNames: ['theme', 'fontSize'] });
2102
1985
  const settings = settingsService.createStoreSlot(db);
2103
1986
  const settingsContentService = settingsService.slotContentService;
2104
- settingsContentService.upsertEntity(settings, { theme: 'dark', fontSize: 14 });
1987
+ settingsService.writeSingleSlotContent(settings, { theme: 'dark', fontSize: 14 });
2105
1988
  const settingsSlot = settingsService.holdSingleSlot(settings);
2106
1989
 
2107
1990
  effectService.createEffect(db, [settingsSlot], [], () => { console.log(settingsSlot.entity?.theme); });
2108
- effectService.createEffect(db, [settingsService.holdColumn(settingsSlot, 'theme')], [], () => {});
1991
+ effectService.createEffect(db, [settingsService.columnSlotService.holdColumn(settingsSlot, 'theme')], [], () => {});
2109
1992
 
2110
1993
  settingsContentService.updateEntity(settingsSlot, { theme: 'light' });
2111
1994
  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
1995
+ settingsService.slotContentService.writeSlotContent(settingsSlot, { theme: 'dark', fontSize: 16 }); // columns it had and this does not, go
2113
1996
  db.flush();
2114
1997
  ```
2115
1998
 
2116
- A standalone record is a single service: `createOIMSingleEntityService` is
1999
+ A standalone record is a single service: `createOIMSingleSlotService` is
2117
2000
  `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
2001
+ `OIMSingleSlotStoreService` and no `keyOf`. Every option is optional:
2002
+ `entityWriter` and `slotEmptinessPolicy` fall back to `OIMDefaults`, and `columnNames` names the
2120
2003
  columns and files their slots in the ordinal store. The entity is
2121
- written through `slotContentService.upsertEntity(settings, entity)`, an ordinary write announced
2004
+ written through `slotContentService.writeSlotContentByKey(settings, entity)`, an ordinary write announced
2122
2005
  to whoever subscribed, not handed to the options or to `createStoreSlot(db)`, which mints
2123
2006
  nothing. The column doors sit on the single service itself: `getColumn(slot, columnName)`,
2124
2007
  `holdColumn(slot, columnName)`, `getColumnGroup(names)`, `holdColumnGroup(slot, group)`,
@@ -2135,7 +2018,7 @@ always reports.
2135
2018
 
2136
2019
  Keep your own `Map` or `WeakMap`, or use `TOIMEntitySlot.data`
2137
2020
  (`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, …)`)
2021
+ it. Clearing the slot (`slotContentService.clearSlotContent(slot)`)
2139
2022
  does not reset it either, so clear what you put there yourself. Each index keeps its last key for
2140
2023
  the entity in a separate field, `indexKeys`, which is read and written only through the
2141
2024
  index's `IOIMIndexKeysService` (`get` / `set`).
@@ -2165,76 +2048,82 @@ a throwing disposer does not stop the rest. The engine ships no scope to do that
2165
2048
 
2166
2049
  ## Export reference
2167
2050
 
2168
- **200 names. This list is `src/index.ts`; nothing else is exported.**
2051
+ **218 names. This list is `src/index.ts`; nothing else is exported.**
2169
2052
 
2170
2053
  **The channel slot** — `OIMChannelService`, `TOIMChannelSlot`, `TOIMChannelSlotOptions`, `createOIMChannelSlot`,
2171
2054
  `IOIMChannelMap`, `TOIMChannelFrame`, `TOIMChannelClock`, `TOIMChannelScheduler`, `OIMChannelQueue` (**type only**),
2172
2055
  `oimIsAnySlotChangedAfter` (whether any of the slots changed after a given revision and epoch).
2173
2056
 
2174
- **Scheduling and errors** — `OIMFlushSchedulerService`, `TOIMFlushSchedulerSlot`, `TOIMTimer`, `TOIMDisposer`,
2175
- `IOIMErrorPolicy`, `OIMErrorPolicyLogAndContinue`, `TOIMErrorPolicyLogAndContinueOptions`, `OIMErrorPolicyThrowAtOnce`.
2057
+ **Scheduling and errors** — `OIMFlushSchedulerService`, `TOIMFlushSchedulerSlot`, `IOIMFlushSchedulerListener`, `TOIMTimer`, `TOIMDisposer`,
2058
+ `IOIMErrorPolicy`, `OIMLogAndContinueErrorPolicy`, `TOIMLogAndContinueErrorPolicyOptions`, `OIMThrowAtOnceErrorPolicy`.
2176
2059
 
2177
2060
  **Release policies and collectors** — `IOIMSlotReleasePolicy`,
2178
- `OIMSlotReleasePolicyAfterWrite`, `TOIMSlotReleasePolicyAfterWriteOptions`,
2179
- `OIMSlotReleasePolicyAfterIdle`, `TOIMSlotReleasePolicyAfterIdleOptions`,
2180
- `OIMSlotReleasePolicyAfterUnhold`, `TOIMSlotReleasePolicyAfterUnholdOptions`,
2181
- `OIMSlotReleasePolicyAtUnhold`, `OIMSlotReleasePolicyAtEmpty`, `OIMSlotReleasePolicyNever`, `OIMSlotReleasePolicyEarliest`,
2061
+ `OIMSlotAfterWriteReleasePolicy`, `TOIMSlotAfterWriteReleasePolicyOptions`,
2062
+ `IOIMSlotReleaseDueFramePolicy`, `OIMSlotReleaseAutoDueFramePolicy`, `OIMSlotReleaseManualDueFramePolicy`,
2063
+ `OIMSlotAfterIdleReleasePolicy`, `TOIMSlotAfterIdleReleasePolicyOptions`,
2064
+ `OIMSlotAfterUnholdReleasePolicy`, `TOIMSlotAfterUnholdReleasePolicyOptions`,
2065
+ `OIMSlotAtUnholdReleasePolicy`, `OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`, `OIMSlotEarliestReleasePolicy`,
2182
2066
  `OIMUnusedSlotCollectorService`, `TOIMUnusedSlotCollectorSlot`, `createOIMUnusedSlotCollectorSlot`,
2183
2067
  `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`.
2068
+ `OIMSlotWheel`, `TOIMSlotWheelOptions`, `OIMSlotReleaseWheel`, `TOIMSlotReleaseWheelOptions`,
2069
+ `oimMintAfterWriteSlotReleaseWheel`, `oimMintAfterIdleSlotReleaseWheel`, `oimMintAfterUnholdSlotReleaseWheel`,
2070
+ `OIMSlotRefetchWheel`, `TOIMSlotRefetchWheelOptions`, `IOIMSlotRefetchListener`,
2071
+ `oimSliceOfMs`, `OIM_SLICE_LENGTH_MS`.
2072
+
2073
+ **Slot services** — `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `TOIMSlotCollectionServiceOptions`, `createOIMSlotCollectionService`,
2074
+ `OIMSingleSlotService`, `OIMWritableSingleSlotService`, `TOIMSingleSlotServiceOptions`, `createOIMSingleSlotService`,
2075
+ `OIMColumnSlotService` (the column doors, `service.columnSlotService`), `OIMSlotOwnerService` (**type only**),
2076
+ `TOIMSlotOwnerServiceOptions`, `IOIMSlotContentService`, `IOIMSlotContentWriter`, `IOIMKeyedSlotCreator`, `IOIMSingleSlotCreator`,
2077
+ `IOIMSlotFilingService`, `TOIMSlotContentServiceKey`, `TOIMSlotContentServiceSlot`, `TOIMSlotContentServiceSingleSlot`,
2078
+ `TOIMSlotContentServiceContent`, `TOIMSlotContentServicePatch`,
2079
+ `OIMContentChangeListenerService`, `IOIMContentChangeListener`, `IOIMStoreSlotListener`, `OIMNoStoreSlotListener`,
2080
+ `IOIMSlotSubscriptionListener`, `OIMNoSlotSubscriptionListener`.
2192
2081
 
2193
2082
  **Slots and their owners** — `TOIMSlot`, `TOIMStoreSlot`, `TOIMSingleSlotStoreSlot`, `TOIMSlotEditable`, `TOIMKeyedSlot`,
2194
- `TOIMIndexableSlot`, `OIMSlotOwner`, `IOIMSlotOwner`, `IOIMSlotEmptiness`, `IOIMSlotStamp`, `TOIMSameValueGuard`,
2083
+ `TOIMIndexableSlot`, `OIMSlotOwner`, `IOIMSlotOwner`, `IOIMSlotEmptinessPolicy`, `IOIMSlotStamp`, `TOIMSameValueGuard`,
2195
2084
  `oimSameValueZeroGuard`, `oimIsSlotEmpty`, `oimIsSlotReleased`, `oimHoldSlots`, `oimUnholdSlotsIfAlive`,
2196
2085
  `OIM_FROZEN_EMPTY_SLOTS`.
2197
2086
 
2198
2087
  **Columns** — `TOIMColumnSlot`, `TOIMComputedColumnSlot`, `TOIMColumnGroup`, `TOIMColumnReaders`,
2199
- `OIMColumnOwner`, `OIMColumnOwnerPushDriven` (**type only**), `OIMColumnOwnerPullDriven`, `TOIMColumnOwnerOptions`.
2088
+ `OIMColumnSlotOwner`, `OIMPushColumnSlotOwner` (**type only**), `OIMPullColumnSlotOwner`, `TOIMColumnSlotOwnerOptions`.
2200
2089
 
2201
2090
  **Entities** — `OIMSlotContentEntityService`, `TOIMSlotContentEntityServiceOptions`, `TOIMEntitySlotOwnerService`,
2202
- `TOIMEntitySlot`, `createOIMSlotContentEntityService`, `TOIMSlotContentEntityServiceFactoryOptions`, and the convenience wrappers `createOIMEntityService` and
2203
- `createOIMSingleEntityService`; `IOIMEntityWriter`, `createOIMEntityWriterInPlace`,
2204
- `createOIMEntityWriterReplacing`, `TOIMEntityWriterOptions`, `TOIMEntityInsertStrategy`,
2091
+ `TOIMEntitySlot`, `TOIMEntityContentSlot`, `createOIMSlotContentEntityService`; `IOIMEntityWriter`, `createOIMEntityWriterInPlace`,
2092
+ `createOIMEntityWriterReplacing`, `TOIMEntityWriterOptions`, `TOIMEntityInserter`,
2205
2093
  `oimInsertEntityWithCopy`, `oimInsertEntityWithoutCopy`, `oimInsertEntityWithSanitizedCopy`, `oimPatchEntityColumns`,
2206
2094
  `oimPatchEntityColumn`, `oimPatchEntityColumnsGuarded`, `oimPatchEntityColumnGuarded`, `oimPutEntityColumns`,
2207
2095
  `oimPutEntityColumnsGuarded`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`.
2208
2096
 
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`,
2097
+ **Buckets** — `OIMSlotContentBucketService`, `OIMSlotContentArrayBucketService`,
2098
+ `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSetBucketService`,
2099
+ `OIMSlotContentSortedArrayBucketService`, `TOIMSlotContentBucketServiceOptions`,
2100
+ `TOIMSlotContentArrayBucketServiceOptions`, `TOIMSlotContentSortedArrayBucketServiceOptions`, `TOIMBucketSlot`,
2101
+ `TOIMBucketContentSlot`, `TOIMBucketSlotOwnerService`, `OIMBucketCommands`, `TOIMBucketCommandsOptions`, `IOIMBucketCommands`,
2102
+ `IOIMBucketMemberSlotsArrayService`, `OIMBucketMemberSlotsArrayService`, `OIMBucketUniqueMemberSlotsArrayService`,
2103
+ `TOIMBucketSlotsSetStore`, `TOIMBucketSlotsArrayStore`,
2217
2104
  `OIM_NOTHING_WAS_INSERTED`, `OIM_NOTHING_WAS_REMOVED`, `OIM_REMOVED_WITHOUT_A_POSITION`.
2218
2105
 
2219
- **Indexes** — `OIMIndexService`, `OIMIndexServiceSortedArrayDriven`, `TOIMIndexServiceOptions`, `TOIMIndexStoreSlot`,
2220
- `TOIMIndexServiceStoreSlot`, `TOIMIndexServiceFactoryOptions`, `createOIMIndexService`, `createOIMIndexServiceByCompositeKey`,
2221
- `createOIMIndexServiceSortedArrayDriven`, `TOIMIndexKeyOptions`, `TOIMIndexKey`,
2222
- `IOIMIndexKeysService`, `TOIMIndexKeysPositions`, `OIMIndexKeysService`, `OIMIndexKeysServiceAppending`,
2223
- `OIMIndexKeysServiceReclaiming`.
2106
+ **Indexes** — `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`, `TOIMSlotCollectionIndexServiceOptions`, `TOIMIndexStoreSlot`,
2107
+ `TOIMSlotCollectionIndexServiceStoreSlot`, `TOIMSlotCollectionIndexServiceFactoryOptions`, `createOIMSlotCollectionIndexService`, `createOIMSlotCollectionIndexService`,
2108
+ `createOIMSlotCollectionSortedArrayIndexService`, `TOIMIndexKeyOptions`, `TOIMIndexKey`,
2109
+ `IOIMIndexKeysService`, `TOIMIndexKeysPositions`, `OIMIndexKeysService`, `OIMAppendingIndexKeysService`,
2110
+ `OIMReclaimingIndexKeysService`.
2224
2111
 
2225
2112
  **The graph** — `OIMEffectService`, `IOIMEffectService`, `TOIMEffect`, `TOIMEffectBody`, `TOIMEffectBodyWithSourceTracking`,
2226
2113
  `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).
2114
+ `OIMComputedValueService`, `IOIMComputedValueService`, `TOIMComputedValueServiceOptions`, `TOIMComputedContentSlot`,
2115
+ `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
2116
 
2229
2117
  **Computed** — `OIMComputedService`, `TOIMComputedServiceOptions`, `createOIMComputedService`,
2230
- `OIMComputedServiceSorted`, `TOIMComputedServiceSortedOptions`, `createOIMComputedServiceSorted`, `TOIMSortedComputedSlot`,
2231
- `OIMComputedServiceBucketMemo`, `TOIMComputedServiceBucketMemoOptions`, `createOIMComputedServiceBucketMemo`,
2232
- `TOIMBucketMemoComputedSlot`, `TOIMSortColumn`, `TOIMSortableColumnName`, `oimColumnsComparator`.
2118
+ `OIMSortedComputedService`, `TOIMSortedComputedServiceOptions`, `createOIMSortedComputedService`, `TOIMSortedComputedSlot`,
2119
+ `OIMBucketMemoComputedService`, `TOIMBucketMemoComputedServiceOptions`, `createOIMBucketMemoComputedService`,
2120
+ `TOIMBucketMemoComputedSlot`, `OIMSingleComputedService`, `createOIMSingleComputedService`, `TOIMComputedDerivationOptions`,
2121
+ `TOIMSortColumn`, `TOIMSortableColumnName`, `oimColumnsComparator`.
2233
2122
 
2234
2123
  **Stores and keys** — `TOIMStoreKey`, `IOIMSlotStoreService`,
2235
- `OIMSlotStoreServiceMapDriven`, `OIMSlotStoreServiceTreeDriven`,
2236
- `OIMSlotStoreServiceSingleSlotDriven`, `TOIMSingleSlotStore`, `TOIMDefaultSlotStore`, `TOIMSlotStoreTree`, `IOIMKeyTreeWalk`, `TOIMCompositeKeyNode`,
2237
- `OIMKeyTreeWalk`, `OIMKeyTreeWalkComposite`, `OIMKeyTreeWalkScalar`.
2124
+ `OIMSlotMapStoreService`, `OIMSlotTreeStoreService`,
2125
+ `OIMSingleSlotStoreService`, `TOIMSingleSlotStore`, `TOIMDefaultSlotStore`, `TOIMSlotStoreTree`, `IOIMKeyTreeWalk`, `TOIMCompositeKeyNode`,
2126
+ `OIMKeyTreeWalk`, `OIMCompositeKeyTreeWalk`, `OIMScalarKeyTreeWalk`.
2238
2127
 
2239
2128
  **The root and its defaults** — `OIMDB`, `TOIMDBOptions`, `OIMDefaults`, `OIMDefaultValue`, `TOIMFactoryOptions`.
2240
2129