@oimdb/core 4.0.0 → 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/CHANGELOG.md +154 -32
- package/README.md +342 -453
- package/dist/index.cjs +1825 -1990
- package/dist/index.d.cts +619 -543
- package/dist/index.d.ts +619 -543
- package/dist/index.js +1778 -1945
- package/llms.txt +741 -860
- package/package.json +2 -2
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 |
|
|
94
|
-
| One entity store slot: `OIMDB`, `
|
|
95
|
-
| The same, plus one effect and `db.flush()` |
|
|
96
|
-
| The same, plus one index: `
|
|
97
|
-
| An ordered bucket with commands: `
|
|
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
|
|
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 `
|
|
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, `
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
228
|
-
`db` first and ONE options object second.
|
|
229
|
-
|
|
230
|
-
`
|
|
231
|
-
arrays by reference
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
and `
|
|
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
|
|
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.** `
|
|
288
|
-
as `slotContentService`. With none, it builds an `
|
|
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
|
|
292
|
-
and `new
|
|
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.
|
|
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** (`
|
|
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** (`
|
|
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 =
|
|
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
|
|
329
|
-
|
|
330
|
-
`
|
|
331
|
-
|
|
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`; `
|
|
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
|
-
`
|
|
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` | `
|
|
355
|
-
| `OIMDefaults.KEY_TREE_WALK_COMPOSITE` | `
|
|
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` | `
|
|
359
|
-
| `OIMDefaults.INDEX_KEYS_SERVICE` | `IOIMIndexKeysService` | an `
|
|
360
|
-
| `OIMDefaults.STORE_SLOT_LISTENER` | `IOIMStoreSlotListener` | `
|
|
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.
|
|
401
|
-
cardService.
|
|
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.
|
|
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.
|
|
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. `
|
|
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
|
-
`
|
|
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 `
|
|
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,
|
|
434
|
-
|
|
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 `
|
|
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.
|
|
469
|
-
| a slot in a bucket | `bucketService.slotContentService.
|
|
470
|
-
| a composite-key edge | `
|
|
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; `
|
|
496
|
-
`
|
|
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
|
|
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`)
|
|
502
|
-
about a slot through
|
|
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
|
-
|
|
|
445
|
+
| Member | Called when |
|
|
505
446
|
|---|---|
|
|
506
|
-
| `createSlot(key, storeSlot,
|
|
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
|
|
512
|
-
|
|
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
|
|
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
|
-
**`
|
|
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 `
|
|
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 `
|
|
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
|
-
`
|
|
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 `
|
|
557
|
-
| `releaseDueAfterIdleMs` | the first content of a slot with no subscriber, and the departure of the last subscriber, under `
|
|
558
|
-
| `releaseDueAfterUnholdMs` | every last unhold, under `
|
|
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. `
|
|
571
|
-
`
|
|
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 `
|
|
574
|
-
policy is built with `
|
|
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 `
|
|
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
|
|
586
|
-
| `new
|
|
587
|
-
| `new
|
|
588
|
-
| `
|
|
589
|
-
| `
|
|
590
|
-
| `
|
|
591
|
-
| `new
|
|
592
|
-
|
|
593
|
-
A policy holds only its window. The clock, the timer and the
|
|
594
|
-
has a `collectorSlot` with one release
|
|
595
|
-
(`
|
|
596
|
-
`
|
|
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`. `
|
|
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
|
|
624
|
-
new
|
|
625
|
-
new
|
|
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. `
|
|
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 `
|
|
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.
|
|
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
|
|
700
|
-
db.collectorSlot
|
|
701
|
-
db.collectorSlot
|
|
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(
|
|
705
|
-
|
|
706
|
-
|
|
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
|
|
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
|
-
**`
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
`
|
|
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
|
-
**`
|
|
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
|
|
729
|
-
timer.
|
|
730
|
-
|
|
731
|
-
`isSlotReleaseScheduled`, `
|
|
732
|
-
`
|
|
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
|
-
|
|
736
|
-
|
|
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.
|
|
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 | `
|
|
799
|
-
| bucket | `
|
|
800
|
-
| computed | `createOIMComputedService<void, TValue, TOIMSingleSlotStore<TOIMComputedSlot<TValue, void>>>({ computedSlotStoreService: new
|
|
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 `
|
|
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.
|
|
811
|
-
`singleService.slotContentService.
|
|
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 `
|
|
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 `
|
|
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
|
-
`
|
|
839
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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 =
|
|
845
|
+
const userService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<User>(db) });
|
|
916
846
|
const users = userService.createStoreSlot(db);
|
|
917
|
-
const groupService =
|
|
847
|
+
const groupService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Group>(db) });
|
|
918
848
|
const groups = groupService.createStoreSlot(db);
|
|
919
|
-
const alice = userService.
|
|
920
|
-
const admins = groupService.
|
|
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 =
|
|
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.
|
|
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 `
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
to a slot once, with `findSlotByKey` (mints nothing) or `getSlotByKey` / `holdSlotByKey
|
|
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
|
|
889
|
+
| Call | What it does |
|
|
966
890
|
|---|---|
|
|
967
|
-
| `
|
|
968
|
-
| `
|
|
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
|
-
| `
|
|
976
|
-
| `
|
|
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 `
|
|
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 =
|
|
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
|
-
`
|
|
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
|
|
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 `
|
|
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.
|
|
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.
|
|
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 (`
|
|
1090
|
-
`
|
|
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:
|
|
1099
|
-
readonly putEntity: (slot:
|
|
1100
|
-
readonly patchEntity: (slot:
|
|
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:
|
|
1021
|
+
slot: TOIMEntityContentSlot, columnName: string, value: unknown, changedColumnNames: string[] | undefined,
|
|
1103
1022
|
) => boolean;
|
|
1104
|
-
readonly deleteColumns: (slot:
|
|
1105
|
-
readonly clearEntity: (slot:
|
|
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;
|
|
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. `
|
|
1140
|
-
`
|
|
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
|
|
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
|
-
`
|
|
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. `
|
|
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.
|
|
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.
|
|
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 (`
|
|
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
|
-
(`
|
|
1291
|
-
`
|
|
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.** `
|
|
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`, `
|
|
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
|
-
- **
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
the
|
|
1355
|
-
|
|
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
|
-
`
|
|
1366
|
-
|
|
1367
|
-
|
|
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** (`
|
|
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 =
|
|
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.
|
|
1487
|
-
const kingSlot = cardService.
|
|
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.
|
|
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.
|
|
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; `
|
|
1398
|
+
its buckets go; `OIMSlotAtUnholdReleasePolicy.INSTANCE` drops a bucket at the last give-back.
|
|
1505
1399
|
|
|
1506
|
-
`
|
|
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
|
-
`
|
|
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 `
|
|
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. `
|
|
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
|
|
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
|
-
| `
|
|
1568
|
-
| `
|
|
1569
|
-
| `
|
|
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
|
|
1468
|
+
### And what naming a slot RECORDS is a member slots service
|
|
1575
1469
|
|
|
1576
|
-
|
|
|
1577
|
-
|
|
1578
|
-
| `
|
|
1579
|
-
| `new
|
|
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
|
|
1583
|
-
|
|
1584
|
-
>({
|
|
1585
|
-
bucketArrayMembership: new OIMBucketArrayMembershipUnique<CardSlot>(),
|
|
1476
|
+
const dedupedContentService = new OIMSlotContentPositionalArrayBucketService<CardSlot>({
|
|
1477
|
+
bucketMemberSlotsArrayService: new OIMBucketUniqueMemberSlotsArrayService<CardSlot>(),
|
|
1586
1478
|
});
|
|
1587
1479
|
```
|
|
1588
1480
|
|
|
1589
|
-
|
|
1590
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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.
|
|
1614
|
-
|
|
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 =
|
|
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 (`
|
|
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 `
|
|
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
|
-
`
|
|
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 =
|
|
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 (`
|
|
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
|
-
`
|
|
1719
|
-
new
|
|
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
|
-
|
|
1740
|
-
|
|
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. `
|
|
1751
|
-
cools it on the queue, and `
|
|
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
|
-
`
|
|
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
|
-
(`
|
|
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 =
|
|
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 =
|
|
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
|
-
`
|
|
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 `
|
|
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 `
|
|
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 `
|
|
1817
|
-
list outlives its last hold. `
|
|
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 =
|
|
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
|
-
`
|
|
1846
|
-
returns an `
|
|
1847
|
-
`
|
|
1848
|
-
that `
|
|
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 `
|
|
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
|
-
(`
|
|
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
|
-
`
|
|
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 =
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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 `
|
|
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 =
|
|
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
|
-
`
|
|
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: `
|
|
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 =
|
|
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.
|
|
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: (
|
|
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: `
|
|
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
|
-
`
|
|
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 (`
|
|
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 `
|
|
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 =
|
|
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
|
|
2058
|
-
const topColumn =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
[`
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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: `
|
|
1999
|
+
A standalone record is a single service: `createOIMSingleSlotService` is
|
|
2117
2000
|
`createOIMSingleSlotService` over `createOIMSlotContentEntityService`, with the store fixed to
|
|
2118
|
-
`
|
|
2119
|
-
`entityWriter` and `
|
|
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.
|
|
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
|
|
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
|
-
**
|
|
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`, `
|
|
2057
|
+
**Scheduling and errors** — `OIMFlushSchedulerService`, `TOIMFlushSchedulerSlot`, `IOIMFlushSchedulerListener`, `TOIMTimer`, `TOIMDisposer`,
|
|
2058
|
+
`IOIMErrorPolicy`, `OIMLogAndContinueErrorPolicy`, `TOIMLogAndContinueErrorPolicyOptions`, `OIMThrowAtOnceErrorPolicy`.
|
|
2176
2059
|
|
|
2177
2060
|
**Release policies and collectors** — `IOIMSlotReleasePolicy`,
|
|
2178
|
-
`
|
|
2179
|
-
`
|
|
2180
|
-
`
|
|
2181
|
-
`
|
|
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
|
-
`
|
|
2185
|
-
`
|
|
2186
|
-
`
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
`
|
|
2190
|
-
`
|
|
2191
|
-
`
|
|
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`, `
|
|
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
|
-
`
|
|
2088
|
+
`OIMColumnSlotOwner`, `OIMPushColumnSlotOwner` (**type only**), `OIMPullColumnSlotOwner`, `TOIMColumnSlotOwnerOptions`.
|
|
2200
2089
|
|
|
2201
2090
|
**Entities** — `OIMSlotContentEntityService`, `TOIMSlotContentEntityServiceOptions`, `TOIMEntitySlotOwnerService`,
|
|
2202
|
-
`TOIMEntitySlot`, `
|
|
2203
|
-
`
|
|
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`, `
|
|
2210
|
-
`
|
|
2211
|
-
`
|
|
2212
|
-
`
|
|
2213
|
-
|
|
2214
|
-
`
|
|
2215
|
-
`
|
|
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** — `
|
|
2220
|
-
`
|
|
2221
|
-
`
|
|
2222
|
-
`IOIMIndexKeysService`, `TOIMIndexKeysPositions`, `OIMIndexKeysService`, `
|
|
2223
|
-
`
|
|
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`, `
|
|
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
|
-
`
|
|
2231
|
-
`
|
|
2232
|
-
`TOIMBucketMemoComputedSlot`, `
|
|
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
|
-
`
|
|
2236
|
-
`
|
|
2237
|
-
`OIMKeyTreeWalk`, `
|
|
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
|
|