@oimdb/core 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,9 +60,9 @@ still on CommonJS, which is why both are here.
60
60
  throws.** `src/index.ts` registers its copy under `Symbol.for('@oimdb/core')` on
61
61
  `globalThis`, and a second copy that finds another one there throws
62
62
  `@oimdb/core is loaded twice in this process`. Without this check the split would be silent:
63
- each copy keeps its own `OIMDefaults`, so a default set through one
64
- (`OIMDefaults.ENTITY_WRITER.setValue(createOIMEntityWriterReplacing())`) never reaches a
65
- service built by the other, which keeps writing in place. Objects built by
63
+ each copy keeps its own built-in defaults and its own `OIMDefaults` class, so an `OIMDefaults` built
64
+ with one copy's class is a stranger to the other copy's factories, and two services built by the two copies
65
+ share no default instance. Objects built by
66
66
  either copy carry their own methods, so a channel, a service and an effect
67
67
  from different copies still deliver to each other; only the defaults diverge.
68
68
 
@@ -75,7 +75,7 @@ first sentence on its own points at the one thing that is not wrong.
75
75
 
76
76
  The mechanism is one line: a runtime loads a package once per format, so
77
77
  `import` in one file and `require` in another holds two complete copies of every
78
- class and every module-level value, such as the `OIMDefaults` statics or a React context.
78
+ class and every module-level value, such as `OIMDefaults.INSTANCE` or a React context.
79
79
 
80
80
  The rule is one line too: pick `import` or `require` for `@oimdb/core`, and use the
81
81
  same one everywhere in the process, including inside every `@oimdb` package
@@ -90,24 +90,36 @@ What each consumer ships, bundled with esbuild (minified, tree-shaken, ESM).
90
90
  <!-- bundle-size:begin -->
91
91
  | Consumer | Minified | Minified + gzip |
92
92
  |---|---:|---:|
93
- | Everything `@oimdb/core` exports | 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 |
93
+ | Everything `@oimdb/core` exports | 105563 B | 25051 B |
94
+ | One entity store slot: `OIMDB`, `createOIMSlotCollectionService`, `createOIMSlotContentEntityService`, `writeSlotContentByKey`, `getSlotByKey` | 36382 B | 9314 B |
95
+ | The same, plus one effect and `db.flush()` | 46546 B | 11711 B |
96
+ | The same, plus one index: `createOIMSlotCollectionIndexService` | 67554 B | 16458 B |
97
+ | An ordered bucket with commands: `OIMSlotContentPositionalArrayBucketService`, `OIMBucketCommands` | 49435 B | 12310 B |
98
98
  <!-- bundle-size:end -->
99
99
 
100
100
  ---
101
101
 
102
- ## The three layers
102
+ ## The domains
103
103
 
104
- `src/index.ts` is the entire public surface, and it is divided in three:
104
+ `src/` holds one folder per domain and `index.ts`, the entire public surface; a factory lives in
105
+ the domain of what it builds. Where a file goes is in `packages/core/src/README.md`.
105
106
 
106
- | Layer | What is in it |
107
+ | Domain | Contents |
107
108
  |---|---|
108
- | `src/engine/` | The primitives, each a stateless service over a plain struct: `OIMChannelService.INSTANCE` over a `TOIMChannelSlot` (frames, the revision), `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`. |
109
+ | `slot/` | `OIMSlotOwner`, `OIMSlotOwnerService` (type-only export), `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `OIMSingleSlotService`, `OIMWritableSingleSlotService`, `OIMSlotContentChangeListenerService`, `createOIMSlotCollectionService`, `createOIMSingleSlotService`, the store aggregates and their factories, `TOIMSlot`, `TOIMEditable` |
110
+ | `store/` | `TOIMStoreSlot`, the slot store services |
111
+ | `channel/` | `OIMChannelService`, `TOIMChannelSlot` |
112
+ | `effect/` | `OIMEffectService`, `TOIMEffect` |
113
+ | `computed/` | `OIMComputedValueService`, `OIMComputedService`, `OIMSingleComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService`, the source-tracking computed services, their factories, `TOIMComputedSlot` |
114
+ | `entity/` | `OIMSlotContentEntityService`, `createOIMSlotContentEntityService`, `createOIMInPlaceEntityWriter`, `createOIMReplacingEntityWriter` |
115
+ | `column/` | abstract `OIMColumnSlotOwner` with `OIMPushColumnSlotOwner` and `OIMPullColumnSlotOwner`, `OIMColumnSlotService` |
116
+ | `bucket/` | `OIMSlotContentBucketService` (abstract), `OIMSlotContentSetBucketService`, `OIMSlotContentArrayBucketService` (abstract), `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService`, `OIMBucketCommands` |
117
+ | `index/` | `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`, their factories, `OIMSlotCollectionIndexStoreAggregate`, the index keys services |
118
+ | `key/` | the key tree walks |
119
+ | `collector/` | the release policies, `OIMSlotReleaseWheel`, `OIMSlotRefetchWheel`, `OIMUnusedSlotCollectorService` |
120
+ | `schedule/` | `OIMFlushSchedulerService` |
121
+ | `error/` | the error policies |
122
+ | `db/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`, the default constants |
111
123
 
112
124
  **A service holds no data; a store slot is one instance.** A service is built once per
113
125
  type, and `cardService.createStoreSlot(db)` returns a `TOIMStoreSlot`, a plain object whose
@@ -127,14 +139,14 @@ release, subscribe), never through the service.
127
139
  ## The shortest thing that delivers
128
140
 
129
141
  ```typescript setup
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';
142
+ import { OIMBucketCommands, OIMChannelService, OIMDB, OIMDefaults, OIMEffectService, OIMLogAndContinueErrorPolicy, OIMSlotContentPositionalArrayBucketService, OIMBucketUniqueMemberSlotsArrayService, OIMSlotContentSetBucketService, OIMSlotAfterIdleReleasePolicy, OIMSlotAfterWriteReleasePolicy, OIMSlotEarliestReleasePolicy, createOIMSingleSlotService, createOIMSingleComputedService, OIMSlotMapStoreService, OIMSingleSlotStoreService, OIMSlotTreeStoreService, OIMUnusedSlotCollectorService, createOIMUnusedSlotCollectorSlot, oimInsertEntityWithCopy, oimIsSlotReleased, createOIMBucketMemoComputedService, createOIMComputedService, createOIMSortedComputedService, createOIMInPlaceEntityWriter, createOIMSlotCollectionIndexService, createOIMSlotCollectionSortedArrayIndexService, createOIMSlotCollectionService, createOIMSlotContentEntityService, oimOrderedListCommandsOfTransition, oimSameSlotSequence, oimSameValueZeroGuard, type TOIMComputedSlot, type TOIMBucketSlotsArrayStore, type TOIMBucketSlot, type TOIMBucketSlotsSetStore, type TOIMDisposer, type TOIMEntityColumnName, type TOIMEntitySlot, type TOIMOrderedListCommand, type TOIMSingleSlotStore, type TOIMSlotStoreTree, type TOIMStoreKey, createOIMReplacingEntityWriter, OIMScalarKeyTreeWalk, OIMCompositeKeyTreeWalk } from '@oimdb/core';
131
143
 
132
144
  type Card = { id: string; deck: string; title: string; rank: number };
133
145
  type CardSlot = TOIMEntitySlot<Card>;
134
146
 
135
147
  const db = new OIMDB();
136
148
  const effectService = OIMEffectService.INSTANCE;
137
- const cardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db) });
149
+ const cardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
138
150
  const cards = cardService.createStoreSlot(db);
139
151
  const slot = cardService.getSlotByKey(cards, 'c1');
140
152
  const byRank = (leftBucketMemberSlot: TOIMEntitySlot<Card>, rightBucketMemberSlot: TOIMEntitySlot<Card>): number =>
@@ -145,9 +157,9 @@ const byRank = (leftBucketMemberSlot: TOIMEntitySlot<Card>, rightBucketMemberSlo
145
157
  cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', deck: 'spades', title: 'Ace', rank: 1 });
146
158
 
147
159
  let seen = '';
148
- const effect = effectService.createEffect(db, [slot], [], () => {
160
+ const effect = effectService.createEffect(db, () => {
149
161
  seen = slot.entity?.title ?? '';
150
- });
162
+ }, [slot]);
151
163
 
152
164
  cardService.slotContentService.updateEntity(slot, { title: 'Ace of Spades' });
153
165
  db.flush(); // seen === 'Ace of Spades'
@@ -157,9 +169,8 @@ effectService.disposeEffect(effect);
157
169
 
158
170
  `OIMDB` is the shared state and nothing else: one `clock` (`{ revision, epoch }`), one
159
171
  `defaultChannelSlot` (a `TOIMChannelSlot`, the struct a channel's state lives in), one
160
- `collectorSlot` (the unused-slot collector's slot and the release wheels, made on first use), and the
161
- `entityWriter`, `slotStamp`, `slotReleasePolicy` and `indexKeysService` its constructor is handed,
162
- which it keeps as the default that every door below reads. It is an `IOIMChannelMap`, so
172
+ `collectorSlot` (the unused-slot collector's slot and the release wheels, made on first use). It holds
173
+ no defaults: those are an `OIMDefaults` passed to each door as `defaults`. It is an `IOIMChannelMap`, so
163
174
  anything that takes a `channelMap` takes the `db`. `flush()` is
164
175
  `OIMChannelService.INSTANCE.flush(db.defaultChannelSlot)`: it runs the pending frame, then the
165
176
  unused-slot collector, then the after-each-flush handlers, in that order. It builds nothing.
@@ -183,8 +194,8 @@ Every mutation goes through a domain neuron. See
183
194
  [With cnstra](https://oimdb.org/docs/guides/with-cnstra).
184
195
 
185
196
  The doors are free functions, one module each, and each builds a **service**, once per
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
197
+ type; an instance is the store slot that service creates. Every door takes ONE options object and no
198
+ `db`; the `db` goes to `createStoreSlot(db)`. Every collection is `createOIMSlotCollectionService` over a content
188
199
  service, and every single is `createOIMSingleSlotService`; an array key needs a tree store
189
200
  (`slotStoreService: new OIMSlotTreeStoreService(…)`, or `bucketSlotStoreService` on an index), because a
190
201
  map-driven store compares arrays by reference. See [The six shapes](#the-six-shapes). The computed doors
@@ -192,15 +203,15 @@ take no `db`: a computed reaches its channel through the store slot it derives f
192
203
  the `db` handed to `createComputedStoreSlot`.
193
204
 
194
205
  ```typescript signature
195
- createOIMSlotContentEntityService<Card, TKey?, TColumnName?>(db, { entityWriter?, slotEmptinessPolicy? }?)
206
+ createOIMSlotContentEntityService<Card, TKey?, TColumnName?>({ entityWriter?, slotEmptinessPolicy? }?)
196
207
  new OIMSlotContentSetBucketService<TBucketMemberSlot, TKey?>({ bucketCommands? })
197
208
  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? })
202
- createOIMComputedService<TKey, TValue, TComputedStore?>({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? | columnNames? }) // a non-Map store: computedSlotStoreService required
203
- createOIMSingleComputedService<TValue>({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
209
+ createOIMSlotCollectionService({ slotContentService, slotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? | columnNames? })
210
+ createOIMSingleSlotService({ slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? | columnNames? })
211
+ createOIMSlotCollectionIndexService({ entityService, columns | keyOf, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
212
+ createOIMSlotCollectionSortedArrayIndexService({ entityService, sortColumns, columns | keyOf, bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
213
+ createOIMComputedService<TKey, TValue, TComputedStore?>({ createSources?, derive?, sameValueGuard?, releaseValue?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? | columnNames? }) // a non-Map store: computedSlotStoreService required
214
+ createOIMSingleComputedService<TValue>({ createSources?, derive?, sameValueGuard?, releaseValue?, initialValue?, slotReleasePolicy?, columnSlotStoreService? | columnNames? })
204
215
  createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
205
216
  createOIMBucketMemoComputedService({ bucketCollectionService, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService? })
206
217
 
@@ -213,27 +224,68 @@ computedService.createComputedStoreSlot(db, derive?, createSources?)
213
224
 
214
225
  **The content service is where everything per type is fixed, once:** the entity writer and the emptiness rule.
215
226
  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:
227
+ owner from them and `defaults.slotStamp`. Build the content service first when it is shared:
217
228
 
218
229
  ```typescript
219
- const cardContentService = createOIMSlotContentEntityService<Card>(db);
220
- const sameShapeCardService = createOIMSlotCollectionService(db, { slotContentService: cardContentService });
230
+ const cardContentService = createOIMSlotContentEntityService<Card>();
231
+ const sameShapeCardService = createOIMSlotCollectionService({ slotContentService: cardContentService });
221
232
  const sameShapeCards = sameShapeCardService.createStoreSlot(db);
222
233
  ```
223
234
 
235
+ ### A store aggregate: the store slot with its store methods
236
+
237
+ A **store aggregate** is a store slot together with the methods of its service that take that
238
+ store slot first, so a call no longer passes it. `service.createStoreAggregate(storeSlot)` wraps a
239
+ store slot you already have; `createOIMSlotCollectionStoreAggregate(db, options)` and
240
+ `createOIMSingleSlotStoreAggregate(db, options)` build the service, the store slot and the aggregate
241
+ in one call, with the options of `createOIMSlotCollectionService` / `createOIMSingleSlotService`.
242
+
243
+ ```typescript
244
+ import { createOIMSlotCollectionStoreAggregate, createOIMSingleSlotStoreAggregate } from '@oimdb/core';
245
+
246
+ const deckStoreAggregate = createOIMSlotCollectionStoreAggregate(db, {
247
+ slotContentService: createOIMSlotContentEntityService<Card>(),
248
+ });
249
+ const aceSlot = deckStoreAggregate.writeSlotContentByKey('c9', { id: 'c9', deck: 'hearts', title: 'Ace', rank: 1 });
250
+ deckStoreAggregate.slotContentService.updateEntity(aceSlot, { rank: 14 });
251
+ deckStoreAggregate.getSlotByKey('c9') === aceSlot; // true
252
+
253
+ const cardStoreAggregate = cardService.createStoreAggregate(cards);
254
+ cardStoreAggregate.storeSlot === cards; // true: hooks, indexes and computeds still take the store slot
255
+
256
+ const focusStoreAggregate = createOIMSingleSlotStoreAggregate(db, {
257
+ slotContentService: createOIMSlotContentEntityService<{ isFocused: boolean }>(),
258
+ });
259
+ focusStoreAggregate.writeSingleSlotContent({ isFocused: true });
260
+ ```
261
+
262
+ | Aggregate | Over | Methods |
263
+ |---|---|---|
264
+ | `OIMSlotCollectionStoreAggregate` | `OIMSlotCollectionService` | `findSlotByKey`, `hasSlotContentByKey`, `getSlotByKey`, `holdSlotByKey`, `getSlots`, `getSlotCount`, `openBatch`, `closeBatch`, `writeInBatch` |
265
+ | `OIMWritableSlotCollectionStoreAggregate` | `OIMWritableSlotCollectionService` | the above and `writeSlotContentByKey` |
266
+ | `OIMSingleSlotStoreAggregate` | `OIMSingleSlotService` | `findSingleSlot`, `hasSingleSlotContent`, `getSingleSlot`, `holdSingleSlot` |
267
+ | `OIMWritableSingleSlotStoreAggregate` | `OIMWritableSingleSlotService` | the above, `writeSingleSlotContent` and `clearSingleSlotContent` |
268
+ | `OIMSlotCollectionIndexStoreAggregate` | `OIMSlotCollectionIndexService` | the collection methods, `getEntitiesByKey`, `getSlotsByKeys`, `holdSlotsByKeys`, `getBucketColumnByKey`, `holdBucketColumnByKey`, `getAnyBucketColumnByKey`, `holdAnyBucketColumnByKey`, `disposeIndexStoreSlot` |
269
+
270
+ Every aggregate also carries `storeSlot`, `slotService` (typed as the service it came from) and
271
+ `slotContentService`. A method that takes a slot rather than the store slot stays on its service.
272
+ Each aggregate method calls the service with the store slot; the service and the store slot stay
273
+ usable directly. `createOIMSlotCollectionStoreAggregate` builds a new service per call, so for
274
+ many store slots of one type build the service once and call `createStoreAggregate` on it.
275
+
224
276
  The bucket doors take the type of the bucket member slots, `TBucketMemberSlot extends TOIMSlot`
225
277
  (default `TOIMEntitySlot`). A bucket holds bucket member slots of any slot type (entity slots,
226
278
  bucket slots, computeds), and an index's `getEntitiesByKey` is there because its members are
227
279
  entity slots.
228
280
 
229
- Each door reads the globals that the class it builds would otherwise fall back to and
230
- **always passes them**, so there is one answer to each question and no class
231
- reaches for a default of its own.
281
+ Each door resolves every optional dependency as its own option, then `defaults` (or
282
+ `OIMDefaults.INSTANCE`), then the built-in `OIMDefaultValue`, and **always passes the result**, so
283
+ there is one answer to each question and no class reaches for a default of its own.
232
284
 
233
285
  The doors are free functions, not methods on `OIMDB`, so a consumer who wires
234
286
  nothing does not import the bucket machinery. This is pinned by
235
287
  `tests/what-an-import-retains.test.ts` (the root retains no door) and by
236
- `src/factories/tests/which-answer-a-collection-the-root-mints-gets.test.ts`.
288
+ `src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts`.
237
289
 
238
290
  **A bucket's content service decides how it holds its slots.** `createOIMSlotCollectionService` takes it
239
291
  as `slotContentService`. With none, it builds an `OIMSlotContentPositionalArrayBucketService`
@@ -255,8 +307,8 @@ live** (`slotStoreService`, the strategy every store slot of the service files i
255
307
  with), **how a key comes out of an entity** (`keyOf`), **how a write lands** (`entityWriter`),
256
308
  **what counts as empty** (`slotEmptinessPolicy`), **when a slot goes** (`slotReleasePolicy`), and
257
309
  **where column slots live** (`columnSlotStoreService`, or `columnNames` for the ordinal store; the
258
- slot owner makes its own column owner over it). Everything but `keyOf` falls back to the `db` or
259
- to a lazy `OIMDefaultValue` on [`OIMDefaults`](#every-optional-dependency-has-a-default).
310
+ slot owner makes its own column owner over it). Everything but `keyOf` falls back to `defaults`, then
311
+ to a lazy `OIMDefaultValue` (see [`OIMDefaults`](#every-optional-dependency-has-a-default)).
260
312
 
261
313
  ---
262
314
 
@@ -267,7 +319,7 @@ There is no argument that turns delivery off. A store slot is created on a `db`
267
319
  unused-slot collector a read hands its mint to is that channel slot's `collectorSlot`.
268
320
 
269
321
  ```typescript
270
- const deliveringCardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db), slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, TOIMStoreKey>>(), columnSlotStoreService: OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue() });
322
+ const deliveringCardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>(), slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, TOIMStoreKey>>() });
271
323
  const deliveringCards = deliveringCardService.createStoreSlot(db);
272
324
  ```
273
325
 
@@ -277,11 +329,11 @@ and that constructor builds the type's `OIMSlotOwnerService`. The owner asks the
277
329
  through `IOIMSlotContentService` (`isSlotEmpty`, `releaseSlotContent`, `hasReference`); the collection
278
330
  service mints through the content service's `createSlot`.
279
331
 
280
- `keyOf` is the one required option. `entityWriter` falls back to `db.entityWriter`, then to
281
- `OIMDefaults.ENTITY_WRITER`; `slotEmptinessPolicy` to `OIMDefaults.ENTITY_SLOT_EMPTINESS`;
282
- `slotReleasePolicy` to `db.slotReleasePolicy`, then to nothing, which is the default of "a slot
283
- with a value stays for good" described under **The injected seams**; the column store to
284
- `OIMDefaults.COLUMN_SLOT_STORE_SERVICE`.
332
+ `keyOf` is the one required option. `entityWriter` falls back to `defaults.entityWriter`, then to
333
+ the in-place writer; `slotEmptinessPolicy` to `defaults.entitySlotEmptinessPolicy`, then to "the entity
334
+ is `undefined`"; `slotReleasePolicy` to `defaults.slotReleasePolicy`, then to nothing, which is the
335
+ default of "a slot with a value stays for good" described under **The injected seams**; the column store
336
+ to `defaults.columnSlotStoreService`, then to a shared map-driven store.
285
337
 
286
338
  A single entity is the same content service under a `void` key, built by
287
339
  `createOIMSingleSlotService`, and it reports the same way.
@@ -290,38 +342,34 @@ A single entity is the same content service under a `void` key, built by
290
342
 
291
343
  ## Every optional dependency has a default
292
344
 
293
- `OIMDefaults` is where a default that would otherwise be an object lives. It is a
294
- class of static `OIMDefaultValue` fields and nothing else; you never construct one.
345
+ `OIMDefaults` holds the strategies a factory falls back on when its own options leave them out. Build
346
+ one with the strategies you want shared and pass it as the `defaults` option of every factory that
347
+ should use them; a factory given none uses `OIMDefaults.INSTANCE`, which is empty. A field left
348
+ undefined falls back to the built-in default.
295
349
 
296
- | Accessor | Type | What it hands back |
350
+ | Field | Type | Built-in default |
297
351
  |---|---|---|
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` |
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` |
300
- | `OIMDefaults.KEY_TREE_WALK_SCALAR` | `OIMScalarKeyTreeWalk` | the chain-of-values walk |
301
- | `OIMDefaults.KEY_TREE_WALK_COMPOSITE` | `OIMCompositeKeyTreeWalk` | the chain-of-identities walk |
302
- | `OIMDefaults.STANDALONE_SLOT_OWNER` | `OIMSlotOwner` | the owner of a slot nobody filed |
303
- | `OIMDefaults.ENTITY_WRITER` | `IOIMEntityWriter` | `createOIMEntityWriterInPlace()` |
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 |
307
-
308
- **Nothing is constructed at module load.** Every entry is an `OIMDefaultValue`
309
- whose `getValue()` mints on its first call (`??=`) and hands back the same instance
310
- for the rest of the process, so a consumer that never reaches for a key tree walk
311
- never builds one.
312
-
313
- Each has a matching `setValue(value)`, which is how a process replaces a default
314
- everywhere at once. **`setValue` throws once `getValue` has handed the first one
315
- out**, naming the property: `OIMDefaults.ENTITY_WRITER has already been handed
316
- out`. Whoever took the default keeps it, so a late replacement would leave two
317
- implementations live in one process, with the older one inside every object built
318
- before the call.
319
-
320
- Setting a default before the first read works, and so does setting it twice before then. Each
321
- property is tracked on its own, so calling `OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()` does not close
322
- `OIMDefaults.KEY_TREE_WALK_SCALAR`. Set every default you mean to replace at startup, before
323
- any `OIMDB` or service exists.
324
- `src/factories/tests/what-a-default-replaced-too-late-does.test.ts` pins this.
352
+ | `slotStamp` | `IOIMSlotStamp<TOIMSlot>` | none: no stamp |
353
+ | `slotReleasePolicy` | `IOIMSlotReleasePolicy` | none: a slot with a value stays for good |
354
+ | `entityWriter` | `IOIMEntityWriter` | `createOIMInPlaceEntityWriter()` |
355
+ | `entitySlotEmptinessPolicy` | `IOIMSlotEmptinessPolicy<TOIMEntityContentSlot>` | "empty means `slot.entity === undefined`" |
356
+ | `indexKeysService` | `IOIMIndexKeysService` | an `OIMAppendingIndexKeysService`; see [Index keys strategy](#index-keys-strategy) |
357
+ | `columnSlotStoreService` | `IOIMSlotStoreService<string, TOIMColumnSlot>` | a shared map-driven store service for column slots |
358
+ | `computedColumnSlotStoreService` | `IOIMSlotStoreService<string, TOIMComputedColumnSlot>` | a shared map-driven store service for the column slots of computed services |
359
+ | `storeSlotListener` | `IOIMStoreSlotListener` | `OIMNoStoreSlotListener`, which hears nothing; a service calls `onStoreSlotCreated(storeSlot)` for each store slot it creates |
360
+
361
+ ```ts
362
+ const replacingDefaults = new OIMDefaults({ entityWriter: createOIMReplacingEntityWriter() });
363
+ const replacingCardService = createOIMSlotCollectionService({
364
+ defaults: replacingDefaults,
365
+ slotContentService: createOIMSlotContentEntityService<Card>({ defaults: replacingDefaults }),
366
+ });
367
+ ```
368
+
369
+ **The built-in defaults cost nothing until used.** Each is an `OIMDefaultValue` whose `getValue()`
370
+ builds it on its first call and hands back the same instance for the rest of the process, and
371
+ `OIMDefaults` itself imports none of them, so a bundle carries only the defaults its factories read.
372
+ `src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts` pins which door reads which.
325
373
 
326
374
  **One module names every default, so calling one `getValue()` retains all of them.** A
327
375
  service drags the map-driven store service and both key tree walks into a
@@ -346,7 +394,7 @@ const body = (): void => {};
346
394
  cardService.writeSlotContentByKey(cards, card.id, card); // write: the value is the reference
347
395
  cardService.writeSlotContentByKey(cards, key, card); // write: the same door, by key
348
396
  const entitySlot = cardService.holdSlotByKey(cards, key); // hold: a counted reference; give it back with entitySlot.owner.unholdSlot(entitySlot)
349
- effectService.createEffect(db, [entitySlot], [], body); // subscribe: the effect is the reference
397
+ effectService.createEffect(db, body, [entitySlot]); // subscribe: the effect is the reference
350
398
  const rankColumn = cardService.columnSlotService.holdColumn(entitySlot, 'rank'); // hold, on a column
351
399
  cardService.columnSlotService.unholdColumn(rankColumn); // give the column back
352
400
  cardService.getSlotByKey(cards, key); // read: the collector's hold is the reference
@@ -446,7 +494,7 @@ about a slot through `IOIMSlotContentService`, never through an override:
446
494
  |---|---|
447
495
  | `createSlot(key, storeSlot, slotOwner)` / `createSingleSlot(storeSlot, slotOwner)` | a key (or a single) is minted, by the collection service |
448
496
  | `isSlotEmpty(slot)` | emptiness is settled after a write |
449
- | `releaseSlotContent(slot, slotOwnerService)` | the identity is going. Give back what its content held |
497
+ | `releaseSlotContent(slot)` | the identity is going. Give back what its content held. Throws on a held slot |
450
498
  | `hasReference(slot)` | something outside `holdCount` and `effects` may keep it |
451
499
 
452
500
  The content service's write doors reach the owner through `slot.owner`; the owner files and unfiles
@@ -457,12 +505,12 @@ through the store service.
457
505
  ```typescript signature
458
506
  interface IOIMSlotEmptinessPolicy<TSlot> { isEmpty(slot: TSlot): boolean; }
459
507
 
460
- interface IOIMSlotStamp<TSlot> { stamp(slotEditable: TOIMSlotEditable<TSlot>): void; }
508
+ interface IOIMSlotStamp<TSlot> { stamp(editableSlot: TOIMEditable<TSlot>): void; }
461
509
  ```
462
510
 
463
511
  **`IOIMSlotEmptinessPolicy`** decides what counts as empty. The answer is written once
464
512
  per write into the EMPTY flag of `slot.slotFlags` and never recomputed at a read. An entity content
465
- service defaults to "the entity is `undefined`" (`OIMDefaults.ENTITY_SLOT_EMPTINESS`); a bucket
513
+ service defaults to "the entity is `undefined`" (or `defaults.entitySlotEmptinessPolicy`); a bucket
466
514
  content service answers "`getBucketMemberSlotCount(bucketSlot) === 0`"; a computed answers
467
515
  `false`.
468
516
 
@@ -476,8 +524,8 @@ settled, and it chains: each stamp takes the `next` one and calls it first. Use
476
524
  for stamps that have nothing to do with release; release has its own policy.
477
525
 
478
526
  **`IOIMSlotReleasePolicy`** decides whether and when a slot goes, and does no
479
- work itself. A door and `OIMDB` take ONE through `slotReleasePolicy`; a door given none takes
480
- `db.slotReleasePolicy`. With neither, the slot owner is built with `OIMSlotAtEmptyReleasePolicy`:
527
+ work itself. A door and an `OIMDefaults` take ONE through `slotReleasePolicy`; a door given none takes
528
+ `defaults.slotReleasePolicy`. With neither, the slot owner is built with `OIMSlotAtEmptyReleasePolicy`:
481
529
  a slot with a value stays for good. A computed service defaults to
482
530
  `OIMSlotAtUnholdReleasePolicy`. Two readonly flags on the policy, `releasesWrittenSlots` and
483
531
  `schedulesReleaseOnWrite`, are read once by the owner at construction, so a policy that never
@@ -496,7 +544,7 @@ A slot goes on the SOONEST of those that are set, and a hold outranks them all.
496
544
  **One number cannot carry both.** A write arriving after the subscriber left
497
545
  would overwrite the idle policy's verdict, and the last subscriber leaving after a
498
546
  write would overwrite the write policy's. Both directions are pinned by
499
- `src/engine/collector/tests/which-release-marker-each-collector-may-move.test.ts`.
547
+ `src/collector/tests/which-release-marker-each-collector-may-move.test.ts`.
500
548
 
501
549
  **The POLICY answers `isSlotReleasable(slot)`**, and only for a slot that is not
502
550
  empty and has nothing left keeping it (no holds, no references). `true` releases
@@ -554,7 +602,7 @@ leaves at the end of the frame. A window above zero is filed in that policy's ow
554
602
  queue.
555
603
 
556
604
  ```typescript
557
- const releasingDb = new OIMDB({
605
+ const releasingDefaults = new OIMDefaults({
558
606
  slotReleasePolicy: new OIMSlotEarliestReleasePolicy([
559
607
  new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: 60_000 }),
560
608
  new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
@@ -612,7 +660,7 @@ does not re-file it. Two consequences follow:
612
660
  that those entities' own expired windows already answer for, so the entities go in the
613
661
  same sweep.
614
662
 
615
- Pinned by `src/engine/collector/tests/what-a-stale-index-takes-with-it.test.ts`,
663
+ Pinned by `src/collector/tests/what-a-stale-index-takes-with-it.test.ts`,
616
664
  `…/what-an-identity-nobody-subscribed-to-costs-after-a-while.test.ts` and
617
665
  `…/what-a-subscribed-row-costs-the-release-clock.test.ts`.
618
666
 
@@ -703,7 +751,7 @@ gives them back when released, so the arena holds the top and the chain holds
703
751
  itself. Use one arena per screen, per component or per job: a scope's cache pins what
704
752
  that scope read and nothing else.
705
753
 
706
- Pinned by `src/engine/collector/tests/what-a-named-arena-keeps-cached.test.ts`.
754
+ Pinned by `src/collector/tests/what-a-named-arena-keeps-cached.test.ts`.
707
755
 
708
756
  Neither policy means *unread*: no read is reported to the clock. A subscriber
709
757
  never loses data mid-read, because an effect holds every root it reaches, so the
@@ -729,8 +777,8 @@ a store slot that holds one slot under no key, or many, a store slot that files
729
777
 
730
778
  | kind | single | collection |
731
779
  |---|---|---|
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)` |
780
+ | entity | `createOIMSingleSlotService<TEntity>(…)`, then `createStoreSlot(db)` | `createOIMSlotCollectionService<TEntity>({ keyOf, … })`, then `createStoreSlot(db)` |
781
+ | bucket | `createOIMSingleSlotService<TMember>({ slotContentService?, … })`, then `createStoreSlot(db)` | `createOIMSlotCollectionService<TMember>({ slotContentService?, … })`, then `createStoreSlot(db)` |
734
782
  | computed | `createOIMComputedService<void, TValue, TOIMSingleSlotStore<TOIMComputedSlot<TValue, void>>>({ computedSlotStoreService: new OIMSingleSlotStoreService<…>(), … })`, then `createComputedStoreSlot(db, derive?, createSources?)` | `createOIMComputedService<TKey, TValue>({ … })`, then `createComputedStoreSlot(db, derive?, createSources?)` |
735
783
 
736
784
  A single is not a second content API. Its content service is the same class with `TKey = void`,
@@ -794,10 +842,10 @@ argument, not a subclass.
794
842
  new OIMSlotMapStoreService<TOIMEntitySlot<Card, TOIMStoreKey>>()
795
843
 
796
844
  // A composite key of VALUES, descended one composite key part at a time.
797
- new OIMSlotTreeStoreService<TOIMStoreKey, TOIMKeyedSlot<readonly TOIMStoreKey[]>>(OIMDefaults.KEY_TREE_WALK_SCALAR.getValue())
845
+ new OIMSlotTreeStoreService<TOIMStoreKey, TOIMKeyedSlot<readonly TOIMStoreKey[]>>(OIMScalarKeyTreeWalk.INSTANCE)
798
846
 
799
847
  // A composite key of IDENTITIES, descended the same way — and holding each.
800
- new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMKeyedSlot<readonly TOIMEntitySlot[]>>(OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue())
848
+ new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMKeyedSlot<readonly TOIMEntitySlot[]>>(OIMCompositeKeyTreeWalk.INSTANCE)
801
849
  ```
802
850
 
803
851
  There are two stores, one per shape of address, and no store is ever asked at run time
@@ -819,8 +867,8 @@ type TScalarChain = readonly TOIMStoreKey[];
819
867
  type Tally = { deck: string; day: string; count: number };
820
868
  type TTallyStore = TOIMSlotStoreTree<TOIMEntitySlot<Tally, TScalarChain>, TOIMStoreKey>;
821
869
 
822
- const tallyService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(db), slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
823
- OIMDefaults.KEY_TREE_WALK_SCALAR.getValue(),
870
+ const tallyService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(), slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
871
+ OIMScalarKeyTreeWalk.INSTANCE,
824
872
  ) });
825
873
  const tallies = tallyService.createStoreSlot(db);
826
874
  tallyService.writeSlotContentByKey(tallies, ((t) => [t.deck, t.day])({ deck: 'spades', day: 'mon', count: 1 }), { deck: 'spades', day: 'mon', count: 1 });
@@ -842,17 +890,17 @@ type Enrolment = {
842
890
  };
843
891
  type TChain = readonly TOIMEntitySlot[];
844
892
 
845
- const userService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<User>(db) });
893
+ const userService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<User>() });
846
894
  const users = userService.createStoreSlot(db);
847
- const groupService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Group>(db) });
895
+ const groupService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Group>() });
848
896
  const groups = groupService.createStoreSlot(db);
849
897
  const alice = userService.writeSlotContentByKey(users, 'alice', { id: 'alice', name: 'Alice' });
850
898
  const admins = groupService.writeSlotContentByKey(groups, 'admins', { id: 'admins', name: 'Admins' });
851
899
 
852
900
  type TEnrolmentStore = TOIMSlotStoreTree<TOIMEntitySlot<Enrolment, TChain>, TOIMEntitySlot>;
853
901
 
854
- const enrolmentService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(db), slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
855
- OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue(),
902
+ const enrolmentService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(), slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
903
+ OIMCompositeKeyTreeWalk.INSTANCE,
856
904
  ) });
857
905
  const enrolments = enrolmentService.createStoreSlot(db);
858
906
  enrolmentService.writeSlotContentByKey(enrolments, ((m) => [m.user, m.group])({ user: alice, group: admins, since: 2026 }), { user: alice, group: admins, since: 2026 });
@@ -871,7 +919,7 @@ Both walks throw `an empty key is not a key` on `[]`.
871
919
  **A scalar is not a composite key part of a composite-walk key.** It is a compile
872
920
  error, and past a cast it throws inside the mint (`Cannot create property
873
921
  'holdCount' on string`) with the edge already in the tree. Hand the store
874
- `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` instead of using a cast.
922
+ `OIMScalarKeyTreeWalk.INSTANCE` instead of using a cast.
875
923
 
876
924
  **Compare a chain with the strategy that matches it.** `oimSameSlotSequence` walks identities by
877
925
  reference. Using it on a chain of values is the `NaN` defect: it answers
@@ -913,7 +961,7 @@ made once at construction and costs nothing per write:
913
961
  | `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) |
914
962
 
915
963
  ```typescript
916
- const copiedCardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db, { entityWriter: createOIMEntityWriterInPlace({ entityInserter: oimInsertEntityWithCopy }) }) });
964
+ const copiedCardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>({ entityWriter: createOIMInPlaceEntityWriter({ entityInserter: oimInsertEntityWithCopy }) }) });
917
965
  ```
918
966
 
919
967
  **A copy is one level deep, and no deeper.** A nested object or
@@ -948,7 +996,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
948
996
  **The hand mutation needs no cast.** `TOIMEntitySlot` declares
949
997
  `readonly entity: TEntity | undefined`, and `readonly` forbids replacing the
950
998
  object, not writing its columns. You still have to narrow the `| undefined`.
951
- `TOIMSlotEditable<TSlot>` is the type that strips `readonly` from the field
999
+ `TOIMEditable<TSlot>` is the type that strips `readonly` from the field
952
1000
  itself, which is what assigning `slot.entity` needs.
953
1001
 
954
1002
  **The list is the whole point, and leaving it out gives it back.** Named columns
@@ -1024,8 +1072,8 @@ interface IOIMEntityWriter {
1024
1072
  readonly clearEntity: (slot: TOIMEntityContentSlot) => void;
1025
1073
  }
1026
1074
 
1027
- createOIMEntityWriterInPlace(options?) // the stored object keeps its identity
1028
- createOIMEntityWriterReplacing(options?) // every write that changed something produces a new object
1075
+ createOIMInPlaceEntityWriter(options?) // the stored object keeps its identity
1076
+ createOIMReplacingEntityWriter(options?) // every write that changed something produces a new object
1029
1077
  // TOIMEntityWriterOptions = { sameValueGuard?: TOIMSameValueGuard; entityInserter?: TOIMEntityInserter; putEntityColumns?: typeof oimPutEntityColumns }
1030
1078
  ```
1031
1079
 
@@ -1044,9 +1092,9 @@ A writer of your own can reuse the column walks that both shipped writers run,
1044
1092
  `oimPatchEntityColumns`, `oimPatchEntityColumn` (one column, behind `patchColumn`) and
1045
1093
  `oimPutEntityColumns`. The whole-entity put is itself the writers' `putEntityColumns` option:
1046
1094
  `oimPutEntityColumns` is the default and skips neither `__proto__` keys nor inherited stored
1047
- columns; `oimPutEntityColumnsGuarded` keeps both guards, at a measured cost on every put, for
1095
+ columns; `oimGuardedPutEntityColumns` keeps both guards, at a measured cost on every put, for
1048
1096
  values that can carry a `__proto__` key or are class instances
1049
- (`createOIMEntityWriterInPlace({ putEntityColumns: oimPutEntityColumnsGuarded })`). Each returns the object
1097
+ (`createOIMInPlaceEntityWriter({ putEntityColumns: oimGuardedPutEntityColumns })`). Each returns the object
1050
1098
  `targetOf` handed back with the moved columns written into it, or `undefined`
1051
1099
  when nothing moved, so the writer decides only where the result goes. The
1052
1100
  [docs](https://oimdb.org/docs/core/writing#writing-your-own-writer) build one that
@@ -1058,7 +1106,7 @@ does not have are cleared to `undefined`, in one call, and the names it returns
1058
1106
  only the columns the patch names. `writeSlotContentByKey` of a key that has a value and
1059
1107
  `writeSlotContent(slot, entity)` are PUT, so a whole-entity write that drops a column writes
1060
1108
  the slot to the channel once, and the replacing writer builds one new object, not
1061
- two. This is counted in `src/facade/entity/tests/what-a-put-that-drops-a-column-costs.test.ts`.
1109
+ two. This is counted in `src/entity/tests/what-a-put-that-drops-a-column-costs.test.ts`.
1062
1110
 
1063
1111
  Neither writer `delete`s. A column that a PUT drops or `deleteColumns` takes away is
1064
1112
  written `undefined`, into the stored object in place or into a spread copy when
@@ -1068,8 +1116,8 @@ replacing, so the entity keeps its hidden class: the key stays in
1068
1116
  does not report it, and taking the entity away does not wake its readers.
1069
1117
 
1070
1118
  The choice is made once, when the entity content service is built (`entityWriter`, else
1071
- `db.entityWriter`), so there is no `if (inPlace)` anywhere on any write path; the object already
1072
- is the answer. The default is `createOIMEntityWriterInPlace()`. Pass `createOIMEntityWriterReplacing()` when
1119
+ `defaults.entityWriter`), so there is no `if (inPlace)` anywhere on any write path; the object already
1120
+ is the answer. The default is `createOIMInPlaceEntityWriter()`. Pass `createOIMReplacingEntityWriter()` when
1073
1121
  something downstream compares by reference.
1074
1122
 
1075
1123
  ---
@@ -1107,8 +1155,7 @@ the next read; a computed with no sources derives on its first read. A throw nev
1107
1155
  for frames: the next write after an aborted frame asks for one. A throw in one
1108
1156
  release of a sweep leaves the rest of that slice on the release clock for the next
1109
1157
  sweep. A release that falls inside a flush (an effect body disposing another
1110
- effect) waits: the slot goes to the unused-slot collector and is released once the
1111
- flush is over. Code that keeps
1158
+ effect) happens at once: nothing holds the slot, so nothing hears it go. Code that keeps
1112
1159
  derived state (an index's `keyOf`, an emptiness check) is outside the
1113
1160
  policy: a throw there leaves state consistent and is thrown in either mode. An
1114
1161
  index whose `keyOf` throws on an entity files that entity under no key, finishes the rest
@@ -1117,7 +1164,7 @@ of the write or batch, then rethrows; the next write it accepts files the entity
1117
1164
  **Subscribing is building an effect.** There is no `subscribe` helper over this,
1118
1165
  and there must not be one. An effect is stamped with the channel clock at birth,
1119
1166
  so `effectService.createEffect` hears only what moves after it and never delivers a first
1120
- value. `effectService.createEffectImmediate(db, roots, computeds, body)` is the door that runs
1167
+ value. `effectService.createAndRunEffect(db, roots, computeds, body)` is the door that runs
1121
1168
  the body once at creation. An effect is a plain `TOIMEffect` object with no methods (its
1122
1169
  channel is `effect.channelSlot`); `OIMEffectService.INSTANCE` does everything to it. There is
1123
1170
  one effect service for the whole process, and every door is handed the `db` (any
@@ -1125,14 +1172,14 @@ one effect service for the whole process, and every door is handed the `db` (any
1125
1172
 
1126
1173
  ```typescript
1127
1174
  // Sources are named in two lists: root slots, and derivations.
1128
- const effect = effectService.createEffect(db, [slot], [totalService.computedSingleSlotService.getSingleSlot(total)], () => {});
1175
+ const effect = effectService.createEffect(db, () => {}, [slot], [totalService.computedSingleSlotService.getSingleSlot(total)]);
1129
1176
  effectService.disposeEffect(effect);
1130
1177
  // The second list is typed `readonly TOIMComputedSlot[]` — pass slots taken from
1131
1178
  // a computed service's `computedCollectionService` straight in.
1132
1179
 
1133
1180
  // One column of one slot, woken when that named column moves and by nothing else.
1134
1181
  // It takes the COLUMN SLOT, which the column owner mints and holds.
1135
- const onRank = effectService.createEffect(db, [cardService.columnSlotService.holdColumn(slot, 'rank')], [], () => {});
1182
+ const onRank = effectService.createEffect(db, () => {}, [cardService.columnSlotService.holdColumn(slot, 'rank')]);
1136
1183
  ```
1137
1184
 
1138
1185
  The body's type is `TOIMEffectBody`, which is `() => (() => void) | void`. **A returned
@@ -1142,10 +1189,10 @@ so a body that opened something on each run has somewhere to close it.
1142
1189
  ```typescript
1143
1190
  const poll = (): void => {};
1144
1191
 
1145
- effectService.createEffect(db, [slot], [], () => {
1192
+ effectService.createEffect(db, () => {
1146
1193
  const timer = setInterval(poll, 1000);
1147
1194
  return () => clearInterval(timer);
1148
- });
1195
+ }, [slot]);
1149
1196
  ```
1150
1197
 
1151
1198
  The return is checked with `typeof` rather than for truthiness, because an
@@ -1153,16 +1200,16 @@ expression-bodied arrow hands back whatever the expression evaluated to
1153
1200
  (`() => set.add(key)` returns the `Set`), and anything that is not a function is
1154
1201
  discarded.
1155
1202
 
1156
- **An effect can also find its sources as it reads.** `createEffectWithSourceTracking(db, body)`
1157
- takes no source lists: the body gets an `OIMEffectSourceTracker`, and what it reads through
1203
+ **An effect can also find its sources as it reads.** `createSourceTrackingEffect(db, body)`
1204
+ takes no source lists: the body gets an `OIMSourceTracker`, and what it reads through
1158
1205
  `readSourceSlot(slot)` (which files the slot and returns it) and
1159
1206
  `readSourceComputedValue(computedSlot)` (which files the derivation and returns its value)
1160
- is what that run depends on. The body's type is `TOIMEffectBodyWithSourceTracking`.
1207
+ is what that run depends on. The body's type is `TOIMSourceTrackingEffectBody`.
1161
1208
 
1162
1209
  ```typescript
1163
- effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
1164
- const isFirst = effectSourceTracker.readSourceSlot(slot).entity?.rank === 1;
1165
- console.log(isFirst ? effectSourceTracker.readSourceSlot(slot).entity?.title : 'not first');
1210
+ effectService.createSourceTrackingEffect(db, sourceTracker => {
1211
+ const isFirst = sourceTracker.readSourceSlot(slot).entity?.rank === 1;
1212
+ console.log(isFirst ? sourceTracker.readSourceSlot(slot).entity?.title : 'not first');
1166
1213
  });
1167
1214
  ```
1168
1215
 
@@ -1226,13 +1273,13 @@ frozen object per set of names on that service; get it once per view, not per en
1226
1273
  and `unholdColumn(columnSlot)` gives it back. A write wakes it once if it changes any of the
1227
1274
  group's columns and not at all if it changes only others.
1228
1275
 
1229
- **`columnNames` picks the column slot store.** `createOIMSlotCollectionService<Card, 'rank' | 'title'>(db, { keyOf, columnNames: ['rank', 'title'] })`
1276
+ **`columnNames` picks the column slot store.** `createOIMSlotCollectionService<Card, 'rank' | 'title'>({ keyOf, columnNames: ['rank', 'title'] })`
1230
1277
  keeps each entity's column slots in an array by column number instead of a `Map` per entity,
1231
1278
  which makes an entity's column store cheaper to create and give back; that is what a scrolling view pays.
1232
1279
  `holdColumn` and `getColumnGroup` are typed on the named columns, and a slot for any other
1233
1280
  column throws. `columnNames` is on the options of every door — entity, bucket, index, computed —
1234
1281
  keyed or single. Without `columnNames` the store is the `columnSlotStoreService` option, and the
1235
- doors default it to the shared map-driven `OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()`.
1282
+ doors default it to the shared map-driven a shared map-driven store.
1236
1283
 
1237
1284
  ### Frame rules
1238
1285
 
@@ -1287,10 +1334,10 @@ doors default it to the shared map-driven `OIMDefaults.COLUMN_SLOT_STORE_SERVICE
1287
1334
  const paint = (card: Card | undefined): void => {};
1288
1335
  const measureAndScroll = (): void => {};
1289
1336
 
1290
- effectService.createEffect(db, [slot], [], () => {
1337
+ effectService.createEffect(db, () => {
1291
1338
  paint(slot.entity);
1292
1339
  OIMChannelService.INSTANCE.subscribeOnceAfterFrame(db.defaultChannelSlot, () => { measureAndScroll(); });
1293
- });
1340
+ }, [slot]);
1294
1341
  ```
1295
1342
 
1296
1343
  The handler is told after **one** frame, the first that ends after the call, and is then gone.
@@ -1303,7 +1350,7 @@ inside its effect and is answered at the end of that same frame.
1303
1350
 
1304
1351
  It is **not** `subscribeAfterEachFrame` plus a disposal. The effect runs again
1305
1352
  next frame and registers again, so a standing handler accumulates one more per run:
1306
- `src/engine/channel/tests/a-handler-that-wants-one-frame-and-not-every-frame.test.ts` measures the
1353
+ `src/channel/tests/a-handler-that-wants-one-frame-and-not-every-frame.test.ts` measures the
1307
1354
  one-shot at 1, 2, 3 calls over three frames against the standing handler's 1, 3,
1308
1355
  6. The one-shot handler also comes off *before* its body runs, so a handler that throws is not left
1309
1356
  standing for the next frame.
@@ -1374,7 +1421,7 @@ computeds), and the bucket's type parameter is that slot type,
1374
1421
  `getEntitiesByKey` is there because its members are entity slots.
1375
1422
 
1376
1423
  ```typescript
1377
- const byDeckService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentSetBucketService<CardSlot>() });
1424
+ const byDeckService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentSetBucketService<CardSlot>() });
1378
1425
  const byDeck = byDeckService.createStoreSlot(db);
1379
1426
 
1380
1427
  const aceSlot = cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', deck: 'red', title: 'Ace', rank: 1 });
@@ -1483,7 +1530,7 @@ disagree. With the unique one `hasBucketMemberSlot` answers from the `Set`; with
1483
1530
 
1484
1531
  ### Positions the caller decides, and the commands that record them
1485
1532
 
1486
- `createOIMSlotCollectionService<CardSlot>(db)` with no `slotContentService` is the bucket collection to
1533
+ `createOIMSlotCollectionService<CardSlot>()` with no `slotContentService` is the bucket collection to
1487
1534
  take when the positions are yours: its default content service is
1488
1535
  `OIMSlotContentPositionalArrayBucketService` with an `OIMBucketCommands` recording over it.
1489
1536
 
@@ -1529,17 +1576,17 @@ from the bucket's for good.
1529
1576
  consumer patches instead of rebuilding:
1530
1577
 
1531
1578
  ```typescript
1532
- const pageService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1579
+ const pageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1533
1580
  const page = pageService.createStoreSlot(db);
1534
1581
  const bucket = pageService.holdSlotByKey(page, 'p1');
1535
1582
 
1536
1583
  const rebuildFrom = (slots: Iterable<CardSlot>): void => {};
1537
1584
  const applyToTheRenderedList = (commands: readonly TOIMOrderedListCommand[]): void => {};
1538
1585
 
1539
- effectService.createEffect(db, [bucket], [], () => {
1586
+ effectService.createEffect(db, () => {
1540
1587
  if (bucket.isWholeListRecorded) rebuildFrom(pageService.slotContentService.getBucketMemberSlots(bucket));
1541
1588
  else applyToTheRenderedList(bucket.commands ?? []);
1542
- });
1589
+ }, [bucket]);
1543
1590
  ```
1544
1591
 
1545
1592
  The content service already writes the bucket slot on every mutation, so the consumer
@@ -1584,7 +1631,7 @@ per-bucket state, so **one instance may serve two bucket collections**.
1584
1631
  ### The other strategy: an index that fills itself
1585
1632
 
1586
1633
  ```typescript
1587
- const indexService = createOIMSlotCollectionIndexService(db, {
1634
+ const indexService = createOIMSlotCollectionIndexService({
1588
1635
  entityService: cardService,
1589
1636
  keyOf: (card: Card) => card.deck, // which key an entity belongs to
1590
1637
  });
@@ -1593,7 +1640,7 @@ const index = indexService.createIndexStoreSlot(cards);
1593
1640
  const render = (slots: unknown[]): void => {};
1594
1641
 
1595
1642
  const spades = indexService.holdSlotByKey(index, 'spades'); // the hold mints the bucket and fills it
1596
- effectService.createEffect(db, [spades], [], () => render(Array.from(indexService.slotContentService.getBucketMemberSlots(spades))));
1643
+ effectService.createEffect(db, () => render(Array.from(indexService.slotContentService.getBucketMemberSlots(spades))), [spades]);
1597
1644
  ```
1598
1645
 
1599
1646
  It **is** a bucket collection (`OIMSlotCollectionIndexService` extends `OIMSlotCollectionService`), and
@@ -1609,7 +1656,7 @@ index's buckets cannot be written from outside. The default content service is
1609
1656
  new OIMSlotContentSetBucketService()` for buckets that keep no positions. Passing
1610
1657
  `entityService` and then the entity store slot is the whole wiring: `createIndexStoreSlot(cards)`
1611
1658
  registers a content-change listener on that store slot
1612
- (`OIMContentChangeListenerService.INSTANCE.addContentChangeListener(cards, …)`), and the entity
1659
+ (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(cards, …)`), and the entity
1613
1660
  service knows nothing about indexes.
1614
1661
  `disposeIndexStoreSlot(index)` takes that listener off, empties the buckets and gives back every
1615
1662
  entity held. After that, `getSlotByKey` and `holdSlotByKey` throw, but the two give-backs
@@ -1635,14 +1682,14 @@ Know these four things before choosing it:
1635
1682
  door (`getSlotByKey`, `holdSlotByKey`, `getSlotsByKeys`, `holdSlotsByKeys`) is filled from the source
1636
1683
  entities written before it, without a flush, so minting inside an effect body does not
1637
1684
  throw. `getSlotsByKeys` / `holdSlotsByKeys` fill all new buckets in ONE pass over the source. How long the bucket lives then is the
1638
- collection's ordinary business: the `slotReleasePolicy` option, or `db.slotReleasePolicy`
1685
+ collection's ordinary business: the `slotReleasePolicy` option, or `defaults.slotReleasePolicy`
1639
1686
  when you name none; with neither, the bucket is kept for good. `OIMSlotAfterIdleReleasePolicy`
1640
1687
  cools it on the queue, and `OIMSlotAtUnholdReleasePolicy.INSTANCE` ends it at
1641
1688
  the last give-back. The index injects no lifetime of its own.
1642
1689
  - **The key comes from `columns`, `keyOf`, or both**, the same pair as the sorter's
1643
1690
  `sortColumns` / `comparator`. The key type is inferred from the columns (`TOIMIndexKey`):
1644
1691
  `{ columns: ['deck'] }` is keyed by that one column's value and refuses an array;
1645
- `createOIMSlotCollectionIndexService(db, { entityService, columns: ['deck', 'suit'] })` is keyed by the
1692
+ `createOIMSlotCollectionIndexService({ entityService, columns: ['deck', 'suit'] })` is keyed by the
1646
1693
  tuple of their values and builds the bucket collection on a tree-driven store
1647
1694
  (`OIMSlotTreeStoreService`) by itself, while `createOIMSlotCollectionIndexService` without a
1648
1695
  `bucketSlotStoreService` refuses two columns. When the index asks
@@ -1662,7 +1709,7 @@ Know these four things before choosing it:
1662
1709
  ### A sorted list is a computed over an index
1663
1710
 
1664
1711
  ```typescript
1665
- const indexService = createOIMSlotCollectionIndexService(db, {
1712
+ const indexService = createOIMSlotCollectionIndexService({
1666
1713
  entityService: cardService,
1667
1714
  keyOf: (card: Card) => card.deck,
1668
1715
  });
@@ -1670,12 +1717,12 @@ const index = indexService.createIndexStoreSlot(cards);
1670
1717
 
1671
1718
  const byRankInDeckService = createOIMSortedComputedService({
1672
1719
  indexService,
1673
- sortColumns: [{ column: 'rank', direction: 'asc' }],
1720
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }],
1674
1721
  });
1675
1722
  const byRankInDeck = byRankInDeckService.createSortedStoreSlot(index);
1676
1723
 
1677
1724
  const spades = byRankInDeckService.computedCollectionService.holdSlotByKey(byRankInDeck, 'spades');
1678
- effectService.createEffect(db, [], [spades], () => { console.log(spades.computedValueService.getValueForSlot(spades).map((entitySlot) => entitySlot.entity?.title)); });
1725
+ effectService.createEffect(db, () => { console.log(spades.computedValueService.getValueForSlot(spades).map((entitySlot) => entitySlot.entity?.title)); }, [], [spades]);
1679
1726
  spades.owner.unholdSlot(spades);
1680
1727
  ```
1681
1728
 
@@ -1688,7 +1735,7 @@ with that store slot first. A key's value, read with
1688
1735
  `computedSlot.computedValueService.getValueForSlot(computedSlot)`, is that key's entity slots,
1689
1736
  sorted. `indexService` is the `OIMSlotCollectionIndexService` whose buckets it sorts.
1690
1737
 
1691
- - `sortColumns` is `readonly TOIMSortColumn<TEntity>[]`, each `{ column, direction: 'asc' | 'desc' }`.
1738
+ - `sortColumns` is `readonly TOIMSortColumn<TEntity>[]`, each `{ columnName, direction: 'asc' | 'desc' }`.
1692
1739
  `column` is a `TOIMSortableColumnName<TEntity>`, meaning a column whose values are strings,
1693
1740
  numbers or bigints. **Columns decide which writes re-sort.**
1694
1741
  - `comparator` is `(left: TEntity, right: TEntity) => number`. **It decides the order.**
@@ -1721,17 +1768,17 @@ The other strategy keeps the index's own buckets in order, so a read needs no co
1721
1768
  The consumer picks one at construction.
1722
1769
 
1723
1770
  ```typescript
1724
- const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService(db, {
1771
+ const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService({
1725
1772
  entityService: cardService,
1726
1773
  columns: ['deck'],
1727
- sortColumns: [{ column: 'rank', direction: 'asc' }, { column: 'id', direction: 'desc' }],
1774
+ sortColumns: [{ columnName: 'rank', direction: 'asc' }, { columnName: 'id', direction: 'desc' }],
1728
1775
  });
1729
1776
  const cardsByDeck = cardsByDeckService.createIndexStoreSlot(cards);
1730
1777
  const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
1731
1778
  Array.from(cardsByDeckService.slotContentService.getBucketMemberSlots(spades));
1732
1779
  ```
1733
1780
 
1734
- `createOIMSlotCollectionSortedArrayIndexService(db, { entityService, columns? | keyOf?, sortColumns, slotReleasePolicy?, bucketSlotStoreService? })`
1781
+ `createOIMSlotCollectionSortedArrayIndexService({ entityService, columns? | keyOf?, sortColumns, slotReleasePolicy?, bucketSlotStoreService? })`
1735
1782
  returns an `OIMSlotCollectionSortedArrayIndexService`, whose content service is an
1736
1783
  `OIMSlotContentSortedArrayBucketService`. `sortColumns` is the same `readonly TOIMSortColumn<TEntity>[]`
1737
1784
  that `createOIMSortedComputedService` takes.
@@ -1760,14 +1807,14 @@ bucket any-column slot doors (`getAnyBucketColumnByKey(indexStoreSlot, key)`,
1760
1807
  the positional ones of the array-based kinds included.
1761
1808
 
1762
1809
  ```typescript
1763
- const decksService = createOIMSlotCollectionIndexService(db, {
1810
+ const decksService = createOIMSlotCollectionIndexService({
1764
1811
  entityService: cardService,
1765
1812
  keyOf: (card: Card) => card.deck,
1766
1813
  });
1767
1814
  const decks = decksService.createIndexStoreSlot(cards);
1768
1815
 
1769
1816
  const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
1770
- const onRankInSpades = effectService.createEffect(db, [rankOfSpades], [], () => {});
1817
+ const onRankInSpades = effectService.createEffect(db, () => {}, [rankOfSpades]);
1771
1818
  effectService.disposeEffect(onRankInSpades);
1772
1819
  decksService.columnSlotService.unholdColumn(rankOfSpades);
1773
1820
  ```
@@ -1779,7 +1826,7 @@ leaves its old bucket in O(1). Only an index writes it, through its `IOIMIndexKe
1779
1826
  (`get(slot, position)`, `set(slot, position, indexKey)`), at the position its index store
1780
1827
  slot was given (`TOIMIndexStoreSlot.indexKeysPosition`, allocated from the source store slot's
1781
1828
  `indexKeysPositions`). Choose the service with the `indexKeysService` option of `OIMDB`; every
1782
- index the factories build on that `db` takes it (default `OIMDefaults.INDEX_KEYS_SERVICE.getValue()`).
1829
+ index the factories build on that `db` takes it (default `OIMAppendingIndexKeysService.INSTANCE`).
1783
1830
 
1784
1831
  - `OIMAppendingIndexKeysService` (default): a disposed index's position is never
1785
1832
  reused, and its stale entry stays on each entity slot. A slot's `indexKeys` grows to the highest
@@ -1796,7 +1843,7 @@ Creating indexes that way is expensive; better not to.
1796
1843
  ### Rendering an ordered bucket
1797
1844
 
1798
1845
  ```typescript setup
1799
- const orderedBucketsService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1846
+ const orderedBucketsService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<CardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
1800
1847
  const orderedBuckets = orderedBucketsService.createStoreSlot(db);
1801
1848
  ```
1802
1849
 
@@ -1875,7 +1922,7 @@ this engine (`createEffect`'s second argument, and `sourceComputeds` in
1875
1922
  it is. Inside `derive`, `computedSlot.value` is the previous value:
1876
1923
 
1877
1924
  ```typescript
1878
- effectService.createEffect(db, [], [totalSlot], () => { console.log(totalSlot.computedValueService.getValueForSlot(totalSlot)); });
1925
+ effectService.createEffect(db, () => { console.log(totalSlot.computedValueService.getValueForSlot(totalSlot)); }, [], [totalSlot]);
1879
1926
  ```
1880
1927
 
1881
1928
  ### One derivation per key
@@ -1908,9 +1955,7 @@ A derivation is never empty: `OIMSlotContentComputedService.isSlotEmpty` answers
1908
1955
  so a filed derivation goes only when nothing holds it,
1909
1956
  nothing watches it, and the grace is `0`. That is what
1910
1957
  `OIMSlotAtUnholdReleasePolicy.INSTANCE` answers, and it is the default when no `slotReleasePolicy` is given.
1911
- A derivation let go of while the channel is flushing is not released on the spot. It is
1912
- handed to the channel's unused-slot collector and released when the flush ends, the same
1913
- deferral that every other non-empty slot gets.
1958
+ A derivation let go of while the channel is flushing is released on the spot, like every other slot.
1914
1959
  `createOIMComputedService` takes a `slotReleasePolicy` as an optional key of its options object.
1915
1960
 
1916
1961
  `createOIMComputedService` is `OIMComputedService` with its generics
@@ -1941,7 +1986,7 @@ const cardSummary = cardSummaryService.createComputedStoreSlot(
1941
1986
  );
1942
1987
  const summarySingleSlotService = cardSummaryService.computedSingleSlotService;
1943
1988
  const topColumn = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(cardSummary), 'top');
1944
- const onTop = effectService.createEffect(db, [], [topColumn], () => { console.log(topColumn.value); });
1989
+ const onTop = effectService.createEffect(db, () => { console.log(topColumn.value); }, [], [topColumn]);
1945
1990
  effectService.disposeEffect(onTop);
1946
1991
  summarySingleSlotService.columnSlotService.unholdColumn(topColumn);
1947
1992
  ```
@@ -1959,7 +2004,7 @@ const order = orderService.createBucketMemoStoreSlot(orderedBuckets);
1959
2004
  const render = (slots: readonly CardSlot[]): void => {};
1960
2005
 
1961
2006
  const deck = orderService.computedCollectionService.holdSlotByKey(order, 'spades'); // mints and holds the bucket too
1962
- effectService.createEffect(db, [], [deck], () => render(deck.computedValueService.getValueForSlot(deck)));
2007
+ effectService.createEffect(db, () => render(deck.computedValueService.getValueForSlot(deck)), [], [deck]);
1963
2008
  ```
1964
2009
 
1965
2010
  Its value is the bucket's slots in the bucket's order, produced on the read that
@@ -1981,14 +2026,14 @@ A sorted list is the other derivation of this kind:
1981
2026
  ```typescript
1982
2027
  type Settings = { theme: string; fontSize: number };
1983
2028
 
1984
- const settingsService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<Settings>(db), columnNames: ['theme', 'fontSize'] });
2029
+ const settingsService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<Settings>(), columnNames: ['theme', 'fontSize'] });
1985
2030
  const settings = settingsService.createStoreSlot(db);
1986
2031
  const settingsContentService = settingsService.slotContentService;
1987
2032
  settingsService.writeSingleSlotContent(settings, { theme: 'dark', fontSize: 14 });
1988
2033
  const settingsSlot = settingsService.holdSingleSlot(settings);
1989
2034
 
1990
- effectService.createEffect(db, [settingsSlot], [], () => { console.log(settingsSlot.entity?.theme); });
1991
- effectService.createEffect(db, [settingsService.columnSlotService.holdColumn(settingsSlot, 'theme')], [], () => {});
2035
+ effectService.createEffect(db, () => { console.log(settingsSlot.entity?.theme); }, [settingsSlot]);
2036
+ effectService.createEffect(db, () => {}, [settingsService.columnSlotService.holdColumn(settingsSlot, 'theme')]);
1992
2037
 
1993
2038
  settingsContentService.updateEntity(settingsSlot, { theme: 'light' });
1994
2039
  settingsContentService.deleteEntityColumns(settingsSlot, ['fontSize']); // the thing a patch cannot say
@@ -1999,7 +2044,7 @@ db.flush();
1999
2044
  A standalone record is a single service: `createOIMSingleSlotService` is
2000
2045
  `createOIMSingleSlotService` over `createOIMSlotContentEntityService`, with the store fixed to
2001
2046
  `OIMSingleSlotStoreService` and no `keyOf`. Every option is optional:
2002
- `entityWriter` and `slotEmptinessPolicy` fall back to `OIMDefaults`, and `columnNames` names the
2047
+ `entityWriter` and `slotEmptinessPolicy` fall back to `defaults`, then to the built-in defaults, and `columnNames` names the
2003
2048
  columns and files their slots in the ordinal store. The entity is
2004
2049
  written through `slotContentService.writeSlotContentByKey(settings, entity)`, an ordinary write announced
2005
2050
  to whoever subscribed, not handed to the options or to `createStoreSlot(db)`, which mints
@@ -2036,7 +2081,7 @@ const body = (): void => {};
2036
2081
  const entitySlot = cardService.holdSlotByKey(cards, 'c1');
2037
2082
 
2038
2083
  const disposers: TOIMDisposer[] = [];
2039
- disposers.push(effectService.subscribeEffect(db, [entitySlot], [], body));
2084
+ disposers.push(effectService.subscribeEffect(db, body, [entitySlot]));
2040
2085
  disposers.push(() => { entitySlot.owner.unholdSlot(entitySlot); });
2041
2086
  for (let i = disposers.length - 1; i >= 0; i--) disposers[i]();
2042
2087
  ```
@@ -2048,7 +2093,7 @@ a throwing disposer does not stop the rest. The engine ships no scope to do that
2048
2093
 
2049
2094
  ## Export reference
2050
2095
 
2051
- **218 names. This list is `src/index.ts`; nothing else is exported.**
2096
+ **237 names. This list is `src/index.ts`; nothing else is exported.**
2052
2097
 
2053
2098
  **The channel slot** — `OIMChannelService`, `TOIMChannelSlot`, `TOIMChannelSlotOptions`, `createOIMChannelSlot`,
2054
2099
  `IOIMChannelMap`, `TOIMChannelFrame`, `TOIMChannelClock`, `TOIMChannelScheduler`, `OIMChannelQueue` (**type only**),
@@ -2076,10 +2121,14 @@ a throwing disposer does not stop the rest. The engine ships no scope to do that
2076
2121
  `TOIMSlotOwnerServiceOptions`, `IOIMSlotContentService`, `IOIMSlotContentWriter`, `IOIMKeyedSlotCreator`, `IOIMSingleSlotCreator`,
2077
2122
  `IOIMSlotFilingService`, `TOIMSlotContentServiceKey`, `TOIMSlotContentServiceSlot`, `TOIMSlotContentServiceSingleSlot`,
2078
2123
  `TOIMSlotContentServiceContent`, `TOIMSlotContentServicePatch`,
2079
- `OIMContentChangeListenerService`, `IOIMContentChangeListener`, `IOIMStoreSlotListener`, `OIMNoStoreSlotListener`,
2124
+ `OIMSlotContentChangeListenerService`, `IOIMSlotContentChangeListener`, `IOIMStoreSlotListener`, `OIMNoStoreSlotListener`,
2080
2125
  `IOIMSlotSubscriptionListener`, `OIMNoSlotSubscriptionListener`.
2081
2126
 
2082
- **Slots and their owners** — `TOIMSlot`, `TOIMStoreSlot`, `TOIMSingleSlotStoreSlot`, `TOIMSlotEditable`, `TOIMKeyedSlot`,
2127
+ **Store aggregates** — `OIMSlotCollectionStoreAggregate`, `OIMWritableSlotCollectionStoreAggregate`,
2128
+ `OIMSingleSlotStoreAggregate`, `OIMWritableSingleSlotStoreAggregate`, `createOIMSlotCollectionStoreAggregate`,
2129
+ `createOIMSingleSlotStoreAggregate`, `OIMSlotCollectionIndexStoreAggregate`.
2130
+
2131
+ **Slots and their owners** — `TOIMSlot`, `TOIMStoreSlot`, `TOIMSingleSlotStoreSlot`, `TOIMEditable`, `TOIMKeyedSlot`,
2083
2132
  `TOIMIndexableSlot`, `OIMSlotOwner`, `IOIMSlotOwner`, `IOIMSlotEmptinessPolicy`, `IOIMSlotStamp`, `TOIMSameValueGuard`,
2084
2133
  `oimSameValueZeroGuard`, `oimIsSlotEmpty`, `oimIsSlotReleased`, `oimHoldSlots`, `oimUnholdSlotsIfAlive`,
2085
2134
  `OIM_FROZEN_EMPTY_SLOTS`.
@@ -2088,11 +2137,11 @@ a throwing disposer does not stop the rest. The engine ships no scope to do that
2088
2137
  `OIMColumnSlotOwner`, `OIMPushColumnSlotOwner` (**type only**), `OIMPullColumnSlotOwner`, `TOIMColumnSlotOwnerOptions`.
2089
2138
 
2090
2139
  **Entities** — `OIMSlotContentEntityService`, `TOIMSlotContentEntityServiceOptions`, `TOIMEntitySlotOwnerService`,
2091
- `TOIMEntitySlot`, `TOIMEntityContentSlot`, `createOIMSlotContentEntityService`; `IOIMEntityWriter`, `createOIMEntityWriterInPlace`,
2092
- `createOIMEntityWriterReplacing`, `TOIMEntityWriterOptions`, `TOIMEntityInserter`,
2140
+ `TOIMEntitySlot`, `TOIMEntityContentSlot`, `createOIMSlotContentEntityService`; `IOIMEntityWriter`, `createOIMInPlaceEntityWriter`,
2141
+ `createOIMReplacingEntityWriter`, `TOIMEntityWriterOptions`, `TOIMEntityInserter`,
2093
2142
  `oimInsertEntityWithCopy`, `oimInsertEntityWithoutCopy`, `oimInsertEntityWithSanitizedCopy`, `oimPatchEntityColumns`,
2094
- `oimPatchEntityColumn`, `oimPatchEntityColumnsGuarded`, `oimPatchEntityColumnGuarded`, `oimPutEntityColumns`,
2095
- `oimPutEntityColumnsGuarded`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`.
2143
+ `oimPatchEntityColumn`, `oimGuardedPatchEntityColumns`, `oimGuardedPatchEntityColumn`, `oimPutEntityColumns`,
2144
+ `oimGuardedPutEntityColumns`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`.
2096
2145
 
2097
2146
  **Buckets** — `OIMSlotContentBucketService`, `OIMSlotContentArrayBucketService`,
2098
2147
  `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSetBucketService`,
@@ -2109,8 +2158,8 @@ a throwing disposer does not stop the rest. The engine ships no scope to do that
2109
2158
  `IOIMIndexKeysService`, `TOIMIndexKeysPositions`, `OIMIndexKeysService`, `OIMAppendingIndexKeysService`,
2110
2159
  `OIMReclaimingIndexKeysService`.
2111
2160
 
2112
- **The graph** — `OIMEffectService`, `IOIMEffectService`, `TOIMEffect`, `TOIMEffectBody`, `TOIMEffectBodyWithSourceTracking`,
2113
- `OIMEffectSourceTracker` (**type only**), `TOIMComputedSlot`, `TOIMComputedSources`, `TOIMDerivation`, `TOIMComputedStoreSlot`,
2161
+ **The graph** — `OIMEffectService`, `IOIMEffectService`, `TOIMEffect`, `TOIMEffectBody`, `TOIMSourceTrackingEffectBody`,
2162
+ `OIMSourceTracker` (**type only**), `TOIMComputedSlot`, `TOIMComputedSources`, `TOIMComputedValueReleaser`, `OIMSourceTrackingComputedService`, `OIMSingleSourceTrackingComputedService`, `createOIMSourceTrackingComputedService`, `createOIMSingleSourceTrackingComputedService`, `TOIMSourceTrackingComputedSlot`, `TOIMSourceTrackingComputedContentSlot`, `TOIMSourceTrackingComputedDerive`, `OIMSlotContentSourceTrackingComputedService` (**type only**), `TOIMSourceTrackingComputedDerivationOptions`, `TOIMComputedServiceFactoryOptions`, `TOIMComputedDerivation`, `TOIMComputedStoreSlot`,
2114
2163
  `OIMComputedValueService`, `IOIMComputedValueService`, `TOIMComputedValueServiceOptions`, `TOIMComputedContentSlot`,
2115
2164
  `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).
2116
2165
 
@@ -2125,16 +2174,14 @@ a throwing disposer does not stop the rest. The engine ships no scope to do that
2125
2174
  `OIMSingleSlotStoreService`, `TOIMSingleSlotStore`, `TOIMDefaultSlotStore`, `TOIMSlotStoreTree`, `IOIMKeyTreeWalk`, `TOIMCompositeKeyNode`,
2126
2175
  `OIMKeyTreeWalk`, `OIMCompositeKeyTreeWalk`, `OIMScalarKeyTreeWalk`.
2127
2176
 
2128
- **The root and its defaults** — `OIMDB`, `TOIMDBOptions`, `OIMDefaults`, `OIMDefaultValue`, `TOIMFactoryOptions`.
2177
+ **The root and its defaults** — `OIMDB`, `TOIMDBOptions`, `OIMDefaults`, `TOIMDefaultsOptions`, `OIMDefaultValue`, `TOIMFactoryOptions`.
2129
2178
 
2130
2179
  **Comparison and diffing** — `oimSameSlotSequence`, `oimSameKeySequence`, `oimOrderedListCommandsOfTransition`,
2131
2180
  `TOIMOrderedListCommand`.
2132
2181
 
2133
- **Shared stateless defaults** — the statics of `OIMDefaults`: `COLUMN_SLOT_STORE_SERVICE`,
2134
- `COMPUTED_COLUMN_SLOT_STORE_SERVICE`, `KEY_TREE_WALK_SCALAR`, `KEY_TREE_WALK_COMPOSITE`,
2135
- `STANDALONE_SLOT_OWNER`, `ENTITY_WRITER`, `ENTITY_SLOT_EMPTINESS`, `INDEX_KEYS_SERVICE`,
2136
- `STORE_SLOT_LISTENER`. Each is minted on its first `getValue()` instead of at module load, so a
2137
- consumer that names none of them constructs none of them. The cost of the single door is described under **Every optional
2182
+ **Shared stateless defaults** — `OIMDefaults` (an instance passed as `defaults`, `OIMDefaults.INSTANCE`
2183
+ the empty one) and `OIMDefaultValue`, the lazy holder of each built-in default, which is built on its
2184
+ first `getValue()` instead of at module load, so a consumer that reaches none of them constructs none of them. The cost of the single door is described under **Every optional
2138
2185
  dependency has a default** above.
2139
2186
 
2140
2187
  That is the whole of `src/index.ts`. If a name is not on this list, it is not