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