@oimdb/react 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -31
- package/dist/index.cjs +49 -45
- package/dist/index.d.cts +17 -13
- package/dist/index.d.ts +17 -13
- package/dist/index.js +44 -40
- package/llms.txt +962 -1042
- package/package.json +2 -2
package/llms.txt
CHANGED
|
@@ -6,47 +6,51 @@
|
|
|
6
6
|
> written against the current source.
|
|
7
7
|
>
|
|
8
8
|
> **Services hold no data; state lives on slots.** An `OIMDB` owns the clock, the collector slot and
|
|
9
|
-
> its `defaultChannelSlot`.
|
|
9
|
+
> its `defaultChannelSlot`. One channel per database; for isolation, a separate `OIMDB` — a write is
|
|
10
|
+
> delivered at its own channel's flush to every effect on the slots it moved, so a second channel on
|
|
11
|
+
> the same clock isolates nothing. A collection service (`OIMSlotCollectionService`) or a single slot service
|
|
10
12
|
> (`OIMSingleSlotService`) is built once per collection type, and `service.createStoreSlot(db)` makes an
|
|
11
13
|
> instance, a `TOIMStoreSlot`, which is NOT a slot (no owner, holds or effects) and carries its
|
|
12
14
|
> `channelSlot`.
|
|
13
15
|
> READS and minting are on the collection service and take the store slot first
|
|
14
16
|
> (`cardService.getSlotByKey(cards, key)`, `holdSlotByKey`, `findSlotByKey`, `getSlots`, `getSlotCount`, the
|
|
15
|
-
> column doors).
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
> `
|
|
17
|
+
> column doors). One WRITE is on the collection and takes a key:
|
|
18
|
+
> `cardService.writeSlotContentByKey(cards, key, entity)`, which mints the slot on a miss and writes the whole
|
|
19
|
+
> value on a hit. Every other write is on `service.slotContentService` and takes a slot
|
|
20
|
+
> (`cardService.slotContentService.updateEntity(slot, patch)`, `writeSlotContent(slot, entity)`,
|
|
21
|
+
> `clearSlotContent(slot)`). There are NO bulk writes and no `keyOf`: the key is always the caller's. A load is a
|
|
22
|
+
> loop of `writeSlotContentByKey` inside `cardService.writeInBatch(cards, () => { … })`, so an index on the
|
|
23
|
+
> collection hears the whole loop in one call.
|
|
24
|
+
> A single — one slot under no key — is an `OIMSingleSlotService`: `writeSingleSlotContent(storeSlot, value)`,
|
|
25
|
+
> `getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`, `findSingleSlot(storeSlot)`,
|
|
26
|
+
> `clearSingleSlotContent(storeSlot)`. No key is `void` or `undefined`: `TKey extends NonNullable<unknown>`.
|
|
21
27
|
>
|
|
22
|
-
>
|
|
23
|
-
> `createOIMSingleSlotService({ slotContentService })
|
|
24
|
-
> `
|
|
25
|
-
>
|
|
26
|
-
> `
|
|
27
|
-
>
|
|
28
|
-
> `
|
|
29
|
-
> `createOIMIndexServiceByCompositeKey` is a convenience wrapper over it.
|
|
28
|
+
> Every collection is `createOIMSlotCollectionService({ slotContentService })` and every single is
|
|
29
|
+
> `createOIMSingleSlotService({ slotContentService })`. The content service decides what the slots hold:
|
|
30
|
+
> `createOIMSlotContentEntityService<TEntity>(…)` for entities, `new OIMSlotContentSetBucketService()`,
|
|
31
|
+
> `new OIMSlotContentPositionalArrayBucketService()` or `new OIMSlotContentSortedArrayBucketService({ … })` for
|
|
32
|
+
> buckets. An index is `createOIMSlotCollectionIndexService({ entityService, keyOf | columns, slotContentService? })`;
|
|
33
|
+
> its `keyOf` is the index's key of an entity, not the collection's. An array key needs a tree store:
|
|
34
|
+
> `slotStoreService` on the collection, `bucketSlotStoreService` on the index.
|
|
30
35
|
> Position reads and writes (`getBucketMemberSlotAt`, `getBucketMemberSlotPosition`, `insertBucketMemberSlotAt`,
|
|
31
36
|
> `removeBucketMemberSlotAt`, `removeBucketMemberSlotRange`, `moveBucketMemberSlotToPosition`,
|
|
32
|
-
> `moveBucketMemberSlotRangeToPosition`) are on `
|
|
37
|
+
> `moveBucketMemberSlotRangeToPosition`) are on `OIMSlotContentPositionalArrayBucketService`, the default bucket
|
|
33
38
|
> content service; the set-driven one has none. A bucket sorts only on the sorted-at-write content service
|
|
34
|
-
> (`
|
|
35
|
-
> over any index is `
|
|
39
|
+
> (`createOIMSlotCollectionSortedArrayIndexService`); on every other bucket its order is the caller's. A sorted list
|
|
40
|
+
> over any index is `createOIMSortedComputedService`. The render script is `OIMBucketCommands`.
|
|
36
41
|
> The channel has no class: its operations are on `OIMChannelService.INSTANCE` and take a channel slot.
|
|
37
|
-
> Effects are `OIMEffectService.INSTANCE.createEffect(db,
|
|
42
|
+
> Effects are `OIMEffectService.INSTANCE.createEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`.
|
|
38
43
|
> Every stateless singleton is a static `INSTANCE` on its class. Module-level defaults are on `OIMDefaults`.
|
|
39
44
|
>
|
|
40
45
|
> Check every symbol you write against [the full export list](#full-export-list).
|
|
41
46
|
>
|
|
42
|
-
>
|
|
43
|
-
> Backwards compatibility, semver and migration paths are not costs here.
|
|
47
|
+
> Install: `npm install @oimdb/core` (4.x). The 3.x line is the previous engine with a different API.
|
|
44
48
|
>
|
|
45
49
|
> `packages/core/src/index.ts` is the entire public surface.
|
|
46
50
|
|
|
47
51
|
## Contents
|
|
48
52
|
|
|
49
|
-
- [The
|
|
53
|
+
- [The domains](#the-domains)
|
|
50
54
|
- [The doctrine](#the-doctrine) — the four doors, the injected seams, the idle windows, the collectors, forks
|
|
51
55
|
- [The runtime: the channel slot](#the-runtime-the-channel-slot)
|
|
52
56
|
- [`OIMDB` and the three doors](#oimdb-and-the-three-doors)
|
|
@@ -77,47 +81,16 @@ block and the one under [One derivation per key](#one-derivation-per-key). Block
|
|
|
77
81
|
marked `ts signature` are listings of an API and are not compiled.
|
|
78
82
|
|
|
79
83
|
```ts setup
|
|
80
|
-
import {
|
|
81
|
-
OIMChannelService,
|
|
82
|
-
OIMContentChangeListenerService,
|
|
83
|
-
OIMDB,
|
|
84
|
-
OIMDefaults,
|
|
85
|
-
OIMEffectService,
|
|
86
|
-
OIMSlotContentBucketServiceArrayDriven,
|
|
87
|
-
OIMSlotContentBucketServiceSetDriven,
|
|
88
|
-
OIMSlotStoreServiceMapDriven,
|
|
89
|
-
OIMSlotStoreServiceSingleSlotDriven,
|
|
90
|
-
OIMSlotStoreServiceTreeDriven,
|
|
91
|
-
OIMUnusedSlotCollectorService,
|
|
92
|
-
createOIMBucketService,
|
|
93
|
-
createOIMComputedServiceBucketMemo,
|
|
94
|
-
createOIMComputedService,
|
|
95
|
-
createOIMComputedServiceSorted,
|
|
96
|
-
createOIMEntityService,
|
|
97
|
-
createOIMEntityWriterInPlace,
|
|
98
|
-
createOIMEntityWriterReplacing,
|
|
99
|
-
createOIMIndexService,
|
|
100
|
-
createOIMIndexServiceSortedArrayDriven,
|
|
101
|
-
createOIMSingleBucketService,
|
|
102
|
-
createOIMSingleEntityService,
|
|
103
|
-
createOIMSlotCollectionService,
|
|
104
|
-
createOIMSlotContentEntityService,
|
|
105
|
-
createOIMUnusedSlotCollectorSlot,
|
|
106
|
-
oimIsSlotEmpty,
|
|
107
|
-
oimIsSlotReleased,
|
|
108
|
-
oimPatchEntityColumn,
|
|
109
|
-
oimPatchEntityColumns,
|
|
110
|
-
oimPutEntityColumns,
|
|
111
|
-
oimSameValueZeroGuard,
|
|
112
|
-
} from '@oimdb/core';
|
|
84
|
+
import { OIMChannelService, OIMSlotContentChangeListenerService, OIMDB, OIMDefaults, OIMEffectService, OIMSlotContentPositionalArrayBucketService, OIMSlotContentSetBucketService, OIMSlotMapStoreService, OIMSingleSlotStoreService, OIMSlotTreeStoreService, OIMUnusedSlotCollectorService, createOIMBucketMemoComputedService, createOIMComputedService, createOIMSortedComputedService, createOIMInPlaceEntityWriter, createOIMReplacingEntityWriter, createOIMSlotCollectionIndexService, createOIMSlotCollectionSortedArrayIndexService, createOIMSlotCollectionService, createOIMSlotContentEntityService, createOIMSingleSlotService, createOIMSingleComputedService, createOIMUnusedSlotCollectorSlot, oimIsSlotEmpty, oimIsSlotReleased, oimPatchEntityColumn, oimPatchEntityColumns, oimPutEntityColumns, oimSameValueZeroGuard, OIMBucketCommands, OIMSlotEarliestReleasePolicy, OIMSlotAfterWriteReleasePolicy, OIMSlotAfterIdleReleasePolicy, OIMThrowAtOnceErrorPolicy, type TOIMSlotStoreTree, OIMScalarKeyTreeWalk, OIMCompositeKeyTreeWalk } from '@oimdb/core';
|
|
113
85
|
import type {
|
|
114
86
|
IOIMEntityWriter,
|
|
115
|
-
|
|
87
|
+
TOIMEditable,
|
|
116
88
|
TOIMBucketSlotsArrayStore,
|
|
117
89
|
TOIMBucketSlot,
|
|
118
90
|
TOIMBucketSlotsSetStore,
|
|
119
91
|
TOIMComputedSlot,
|
|
120
92
|
TOIMDisposer,
|
|
93
|
+
TOIMEntityContentSlot,
|
|
121
94
|
TOIMEntitySlot,
|
|
122
95
|
TOIMOrderedListCommand,
|
|
123
96
|
TOIMSingleSlotStore,
|
|
@@ -138,17 +111,15 @@ declare const payload: readonly Card[];
|
|
|
138
111
|
const db = new OIMDB();
|
|
139
112
|
const channelSlot = db.defaultChannelSlot;
|
|
140
113
|
const effectService = OIMEffectService.INSTANCE;
|
|
141
|
-
const cardService =
|
|
114
|
+
const cardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
|
|
142
115
|
const cards = cardService.createStoreSlot(db);
|
|
143
|
-
const slot = cardService.
|
|
116
|
+
const slot = cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
|
|
144
117
|
|
|
145
|
-
const orderedService =
|
|
146
|
-
slotContentService: new OIMSlotContentBucketServiceArrayDriven<TOIMEntitySlot<Card>>(),
|
|
147
|
-
});
|
|
118
|
+
const orderedService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>() });
|
|
148
119
|
const ordered = orderedService.createStoreSlot(db);
|
|
149
120
|
const orderedBucketSlot = orderedService.holdSlotByKey(ordered, 'top');
|
|
150
121
|
|
|
151
|
-
const pageService =
|
|
122
|
+
const pageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
152
123
|
const page = pageService.createStoreSlot(db);
|
|
153
124
|
const todo = pageService.holdSlotByKey(page, 'todo');
|
|
154
125
|
const done = pageService.holdSlotByKey(page, 'done');
|
|
@@ -156,30 +127,40 @@ const done = pageService.holdSlotByKey(page, 'done');
|
|
|
156
127
|
|
|
157
128
|
---
|
|
158
129
|
|
|
159
|
-
## The
|
|
130
|
+
## The domains
|
|
160
131
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
| `src/engine/<domain>/` | the channel (`OIMChannelService`, `TOIMChannelSlot`), the flush scheduler (`OIMFlushSchedulerService`), the slot owner (`OIMSlotOwner`, and `OIMSlotOwnerService`, a type-only export), `OIMContentChangeListenerService`, the entity content service `OIMSlotContentEntityService`, `OIMComputedValueService`, `OIMEffectService`, the column owner (abstract `OIMColumnOwner` with the strategies `OIMColumnOwnerPushDriven` and `OIMColumnOwnerPullDriven`), the key stores and key tree walks, the index keys services, `TOIMStoreSlot`, `TOIMComputedSlot`, `TOIMEffect`, the collector services and slots, and the release policies |
|
|
164
|
-
| `src/facade/<domain>/` | `facade/slot/`: `OIMSlotCollectionService`, `OIMSingleSlotService`; `facade/bucket/`: `OIMSlotContentBucketService` (abstract), `OIMSlotContentBucketServiceSetDriven`, `OIMSlotContentBucketServiceArrayBased` (abstract), `OIMSlotContentBucketServiceArrayDriven`, `OIMSlotContentBucketServiceSortedArrayDriven`, `OIMBucketCommands`; `facade/index/`: `OIMIndexService`, `OIMIndexServiceSortedArrayDriven`; `facade/computed/`: `OIMComputedService`, `OIMComputedServiceSorted`, `OIMComputedServiceBucketMemo` |
|
|
165
|
-
| `src/factories/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`; the base factories `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`; the convenience wrappers `createOIMEntityService`, `createOIMSingleEntityService`, `createOIMBucketService`, `createOIMSingleBucketService`, `createOIMIndexServiceByCompositeKey`; `createOIMIndexService`, `createOIMIndexServiceSortedArrayDriven`, `createOIMComputedService`, `createOIMComputedServiceSorted`, `createOIMComputedServiceBucketMemo`, `createOIMEntityWriterInPlace`, `createOIMEntityWriterReplacing` |
|
|
132
|
+
`src/` holds one folder per domain and `index.ts`, the entire public surface; a factory lives in
|
|
133
|
+
the domain of what it builds. Where a file goes is in `packages/core/src/README.md`.
|
|
166
134
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`
|
|
170
|
-
`
|
|
171
|
-
`
|
|
135
|
+
| Domain | Contents |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `slot/` | `OIMSlotOwner`, `OIMSlotOwnerService` (type-only export), `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `OIMSingleSlotService`, `OIMWritableSingleSlotService`, `OIMSlotContentChangeListenerService`, `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `TOIMSlot`, `TOIMEditable` |
|
|
138
|
+
| `store/` | `TOIMStoreSlot`, the slot store services |
|
|
139
|
+
| `channel/` | `OIMChannelService`, `TOIMChannelSlot` |
|
|
140
|
+
| `effect/` | `OIMEffectService`, `TOIMEffect` |
|
|
141
|
+
| `computed/` | `OIMComputedValueService`, `OIMComputedService`, `OIMSingleComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService`, the source-tracking computed services, their factories, `TOIMComputedSlot` |
|
|
142
|
+
| `entity/` | `OIMSlotContentEntityService`, `createOIMSlotContentEntityService`, `createOIMInPlaceEntityWriter`, `createOIMReplacingEntityWriter` |
|
|
143
|
+
| `column/` | abstract `OIMColumnSlotOwner` with `OIMPushColumnSlotOwner` and `OIMPullColumnSlotOwner`, `OIMColumnSlotService` |
|
|
144
|
+
| `bucket/` | `OIMSlotContentBucketService` (abstract), `OIMSlotContentSetBucketService`, `OIMSlotContentArrayBucketService` (abstract), `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService`, `OIMBucketCommands` |
|
|
145
|
+
| `index/` | `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`, their factories, the index keys services |
|
|
146
|
+
| `key/` | the key tree walks |
|
|
147
|
+
| `collector/` | the release policies, `OIMSlotReleaseWheel`, `OIMSlotRefetchWheel`, `OIMUnusedSlotCollectorService` |
|
|
148
|
+
| `schedule/` | `OIMFlushSchedulerService` |
|
|
149
|
+
| `error/` | the error policies |
|
|
150
|
+
| `db/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`, the default constants |
|
|
172
151
|
|
|
173
152
|
**A service holds no data; state lives on slots.** A collection service is built once per collection type
|
|
174
153
|
and holds only configuration and other services: its slot owner, its store service and its content
|
|
175
154
|
service (`service.slotContentService`). `service.createStoreSlot(db)` makes an instance, a `TOIMStoreSlot`,
|
|
176
155
|
which is NOT a slot (no owner, holds or effects): its `store` is what the store service built, its
|
|
177
|
-
`channelSlot` is the db's channel, and it carries the per-store
|
|
178
|
-
|
|
179
|
-
`slot.storeSlot`, which is why a slot door needs no store slot.
|
|
180
|
-
(`cardService.getSlotByKey(cards, pk)`,
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
`channelSlot` is the db's channel, its `slotService` is the service that made it, and it carries the per-store
|
|
157
|
+
state (content-change listeners, index key positions, the open batch and its queued changes, column readers).
|
|
158
|
+
Every slot filed in it points back to it through `slot.storeSlot`, which is why a slot door needs no store slot.
|
|
159
|
+
Reads and the keyed write are called on the service (`cardService.getSlotByKey(cards, pk)`,
|
|
160
|
+
`cardService.getSlots(cards)`, `cardService.writeSlotContentByKey(cards, pk, card)`, or through the store slot:
|
|
161
|
+
`cards.slotService.writeSlotContentByKey(cards, pk, card)`); writes on a slot go to its content service
|
|
162
|
+
(`cardService.slotContentService.updateEntity(slot, patch)`), and the column doors to
|
|
163
|
+
`cardService.columnSlotService`. A store slot's `store` is the store service's to read, never the caller's.
|
|
183
164
|
|
|
184
165
|
---
|
|
185
166
|
|
|
@@ -193,20 +174,20 @@ effect, a count, or a tracked mint — so nothing can unfile the identity under
|
|
|
193
174
|
the caller who just got it.
|
|
194
175
|
|
|
195
176
|
```ts
|
|
196
|
-
cardService.
|
|
197
|
-
effectService.createEffect(db,
|
|
177
|
+
cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'Two', rank: 2, deck: 'spades' }); // write
|
|
178
|
+
effectService.createEffect(db, () => { paint(slot.entity); }, [slot]); // subscribe
|
|
198
179
|
cardService.holdSlotByKey(cards, 'c1'); // hold, by key -> the slot, holdCount up by one
|
|
199
|
-
const rankColumn = cardService.holdColumn(slot, 'rank'); // hold, on a column
|
|
200
|
-
cardService.unholdColumn(rankColumn); // give the column back
|
|
180
|
+
const rankColumn = cardService.columnSlotService.holdColumn(slot, 'rank'); // hold, on a column
|
|
181
|
+
cardService.columnSlotService.unholdColumn(rankColumn); // give the column back
|
|
201
182
|
cardService.getSlotByKey(cards, 'c1'); // read, by key -> the slot, tracked by the collector
|
|
202
183
|
```
|
|
203
184
|
|
|
204
185
|
**The non-minting lookup leaves no reference.** `findSlotByKey(storeSlot, key)` (and
|
|
205
186
|
`findSingleSlot(storeSlot)` on a single) answers `undefined` on a miss and mints nothing, and the
|
|
206
|
-
slot it does hand back is protected by nothing the caller took
|
|
207
|
-
|
|
208
|
-
the slot first and write through it. Do not write a helper
|
|
209
|
-
second spelling of a mint, not a fifth door.
|
|
187
|
+
slot it does hand back is protected by nothing the caller took; the entity is
|
|
188
|
+
`findSlotByKey(storeSlot, key)?.entity`. The one by-key write is `writeSlotContentByKey`, the write door
|
|
189
|
+
above; to patch or clear an entity by key, find the slot first and write through it. Do not write a helper
|
|
190
|
+
over any of the four doors — it would be a second spelling of a mint, not a fifth door.
|
|
210
191
|
|
|
211
192
|
**Subscribing leaves BOTH kinds of reference.** `OIMEffectService.INSTANCE.createEffect(db, …)` files
|
|
212
193
|
the effect in `slot.effects` *and* holds each root it subscribed to
|
|
@@ -238,13 +219,13 @@ owned by the object asked, and out of grace. Terms, and who gives each back:
|
|
|
238
219
|
|
|
239
220
|
| Term | Given back by |
|
|
240
221
|
|---|---|
|
|
241
|
-
| `holdCount` | `slot.owner.unholdSlot(slot)`, or `service.unholdColumn(columnSlot)` for a column; the unused-slot collector's flush; a slot leaving a bucket; `effectService.disposeEffect(effect)`, for the hold the effect took |
|
|
222
|
+
| `holdCount` | `slot.owner.unholdSlot(slot)`, or `service.columnSlotService.unholdColumn(columnSlot)` for a column; the unused-slot collector's flush; a slot leaving a bucket; `effectService.disposeEffect(effect)`, for the hold the effect took |
|
|
242
223
|
| `effects` | `effectService.disposeEffect(effect)` |
|
|
243
224
|
| a held column | its last column effect being disposed, through the service's column owner (`unsubscribeFromSlot`) |
|
|
244
|
-
| the value | `
|
|
245
|
-
| a composite-key edge | `
|
|
225
|
+
| the value | `service.slotContentService.clearSlotContent(slot)`, on an entity slot or a bucket slot alike; `clearSingleSlotContent(storeSlot)` on a single |
|
|
226
|
+
| a composite-key edge | `OIMCompositeKeyTreeWalk.deleteKey` unlinking that composite key part |
|
|
246
227
|
|
|
247
|
-
`
|
|
228
|
+
`clearSlotContent(slot)` takes the **value**. Subscribers stay, holds
|
|
248
229
|
stay, and the identity stays with them. Slots you wrote into a bucket are held by
|
|
249
230
|
that bucket until you empty it.
|
|
250
231
|
|
|
@@ -256,27 +237,36 @@ built from three stateless parts, wired once at construction, plus the injected
|
|
|
256
237
|
| Part | Where | What it does |
|
|
257
238
|
|---|---|---|
|
|
258
239
|
| the slot owner, `OIMSlotOwnerService` (type-only export) | `slot.owner`, typed `IOIMSlotOwner` | holds, subscriptions, release, emptiness, `assertWritable`, the column owner; one per collection type |
|
|
259
|
-
| the store service, `IOIMSlotStoreService` | given to the factory (`
|
|
260
|
-
| the content service | `service.slotContentService` | the writes and reads of one kind of content (`OIMSlotContentEntityService`, `
|
|
240
|
+
| the store service, `IOIMSlotStoreService` | given to the factory (`slotStoreService`, `bucketSlotStoreService`, …) or the Map default | files and finds the slots of each store slot's `store` |
|
|
241
|
+
| the content service | `service.slotContentService` | the writes and reads of one kind of content (`OIMSlotContentEntityService`, the bucket content services, `OIMSlotContentComputedService`) and the answers the owner asks of that kind |
|
|
261
242
|
|
|
262
|
-
The
|
|
243
|
+
The factory reads what it needs off the content service it is handed, through four small interfaces, never
|
|
263
244
|
through a subclass of the service:
|
|
264
245
|
|
|
265
246
|
```ts signature
|
|
266
|
-
interface
|
|
267
|
-
createSlot(key: TKey, storeSlot: TOIMStoreSlot
|
|
247
|
+
interface IOIMKeyedSlotCreator<TKey extends NonNullable<unknown>, TSlot> {
|
|
248
|
+
createSlot(key: TKey, storeSlot: TOIMStoreSlot, slotOwner: IOIMSlotOwner): TSlot;
|
|
249
|
+
}
|
|
250
|
+
interface IOIMSingleSlotCreator<TSlot> {
|
|
251
|
+
createSingleSlot(storeSlot: TOIMStoreSlot, slotOwner: IOIMSlotOwner): TSlot;
|
|
252
|
+
}
|
|
253
|
+
interface IOIMSlotContentWriter<TSlot, TContent> {
|
|
254
|
+
insertSlotContent(slot: TSlot, content: TContent, changedSlots?: TSlot[]): void;
|
|
255
|
+
writeSlotContent(slot: TSlot, content: TContent): boolean;
|
|
256
|
+
clearSlotContent(slot: TSlot): void;
|
|
257
|
+
}
|
|
258
|
+
interface IOIMSlotContentService<TSlot> {
|
|
268
259
|
isSlotEmpty(slot: TSlot): boolean;
|
|
269
|
-
releaseSlotContent(slot: TSlot
|
|
260
|
+
releaseSlotContent(slot: TSlot): void;
|
|
270
261
|
hasReference(slot: TSlot): boolean;
|
|
271
262
|
}
|
|
272
|
-
interface IOIMSlotContentFlushStrategy<TSlot, TSlotOwnerService> {
|
|
273
|
-
onSubframeClosed(storeSlot: TOIMStoreSlot<unknown, TSlot>, slotOwnerService: TSlotOwnerService): void;
|
|
274
|
-
}
|
|
275
263
|
```
|
|
276
264
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
265
|
+
A content service with `createSlot` goes into `createOIMSlotCollectionService`, one with `createSingleSlot` into
|
|
266
|
+
`createOIMSingleSlotService`; the entity and bucket content services have both. `hasReference` answers whether
|
|
267
|
+
something outside `holdCount` and `effects` keeps the slot (a bucket's members). Every write door asserts the
|
|
268
|
+
identity is still filed (`assertWritable`). `releaseSlotContent` is what the slot owner calls when it releases a
|
|
269
|
+
slot nothing holds, and it throws on a slot that is still held.
|
|
280
270
|
|
|
281
271
|
### The injected seams
|
|
282
272
|
|
|
@@ -285,26 +275,26 @@ question that the engine asks but does not decide. There is no flag and no optio
|
|
|
285
275
|
object.
|
|
286
276
|
|
|
287
277
|
```ts signature
|
|
288
|
-
interface
|
|
278
|
+
interface IOIMSlotEmptinessPolicy<TSlot> { isEmpty(slot: TSlot): boolean; }
|
|
289
279
|
|
|
290
|
-
interface IOIMSlotStamp<TSlot> { stamp(
|
|
280
|
+
interface IOIMSlotStamp<TSlot> { stamp(editableSlot: TOIMEditable<TSlot>): void; }
|
|
291
281
|
```
|
|
292
282
|
|
|
293
283
|
**What a kind of content answers is not a seam.** Mint, write, clear and release
|
|
294
|
-
belong to the content service, which the
|
|
284
|
+
belong to the content service, which the collection service hands its owner as `slotContentService`.
|
|
295
285
|
"Does this still hold something" no part declares — the owner asks `oimIsSlotEmpty(slot)`,
|
|
296
286
|
`hasReference` and the release policy's `isSlotReleasable` separately.
|
|
297
287
|
|
|
298
|
-
**`
|
|
288
|
+
**`IOIMSlotEmptinessPolicy`** decides what counts as empty, and the answer is written once
|
|
299
289
|
per write into the EMPTY flag of `slot.slotFlags` — never recomputed at a read. The entity content
|
|
300
|
-
service defaults to "the entity is `undefined`" (the `
|
|
290
|
+
service defaults to "the entity is `undefined`" (the `slotEmptinessPolicy` option of
|
|
301
291
|
`createOIMSlotContentEntityService` replaces it); a bucket content service answers "no members"; a
|
|
302
292
|
computed answers `false`, because a derivation always has a value to produce.
|
|
303
293
|
|
|
304
|
-
**Emptiness decides lifetime, never the write path.**
|
|
305
|
-
still carries an entity
|
|
306
|
-
soft delete, a seed planted by `IOIMSlotStamp`): a column the new entity drops is
|
|
307
|
-
woken, an unchanged one is not, and
|
|
294
|
+
**Emptiness decides lifetime, never the write path.** A `writeSlotContent` (or `writeSlotContentByKey` on a
|
|
295
|
+
hit) onto an entity slot that still carries an entity is put over it even when emptiness calls the slot empty
|
|
296
|
+
(a soft delete, a seed planted by `IOIMSlotStamp`): a column the new entity drops is
|
|
297
|
+
woken, an unchanged one is not, and a write that repeats the entity exactly wakes
|
|
308
298
|
nothing. Only an entity slot with no entity at all is filled whole.
|
|
309
299
|
|
|
310
300
|
**`IOIMSlotStamp`** writes onto the slot at every write, just before emptiness is
|
|
@@ -325,53 +315,67 @@ interface IOIMSlotReleasePolicy {
|
|
|
325
315
|
cancelSlotRelease(slot: TOIMSlot): void;
|
|
326
316
|
}
|
|
327
317
|
|
|
328
|
-
new
|
|
329
|
-
|
|
330
|
-
new
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
318
|
+
new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: number, slotReleaseDueFramePolicy?: IOIMSlotReleaseDueFramePolicy })
|
|
319
|
+
OIMSlotReleaseAutoDueFramePolicy.INSTANCE
|
|
320
|
+
new OIMSlotReleaseManualDueFramePolicy() // openSlotReleaseDueFrame() · closeSlotReleaseDueFrame() · writeInSlotReleaseDueFrame(write)
|
|
321
|
+
new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: number })
|
|
322
|
+
new OIMSlotAfterUnholdReleasePolicy({ keepAfterUnholdMs: number })
|
|
323
|
+
OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
324
|
+
OIMSlotAtEmptyReleasePolicy.INSTANCE
|
|
325
|
+
OIMSlotNeverReleasePolicy.INSTANCE
|
|
326
|
+
new OIMSlotEarliestReleasePolicy(slotReleasePolicies: readonly IOIMSlotReleasePolicy[])
|
|
335
327
|
```
|
|
336
328
|
|
|
337
329
|
`releasesWrittenSlots` is whether the policy can ever release a slot that holds
|
|
338
330
|
data; a service reads it once at construction and skips the after-write
|
|
339
|
-
release check when it is false (`
|
|
340
|
-
`
|
|
331
|
+
release check when it is false (`OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`: false;
|
|
332
|
+
`OIMSlotAtUnholdReleasePolicy` and the timed policies: true; `OIMSlotEarliestReleasePolicy`: true
|
|
341
333
|
when any policy it combines is).
|
|
342
334
|
|
|
343
335
|
`schedulesReleaseOnWrite` is whether a write can make the policy schedule a release; a service
|
|
344
336
|
reads it once at construction and, when it is false and no `slotStamp` is set, skips `onSlotWritten`
|
|
345
|
-
and the first-fill `onSlotIdle` on every write (`
|
|
346
|
-
true; `
|
|
347
|
-
`
|
|
337
|
+
and the first-fill `onSlotIdle` on every write (`OIMSlotAfterWriteReleasePolicy`, `OIMSlotAfterIdleReleasePolicy`:
|
|
338
|
+
true; `OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`, `OIMSlotAtUnholdReleasePolicy`,
|
|
339
|
+
`OIMSlotAfterUnholdReleasePolicy`: false; `OIMSlotEarliestReleasePolicy`: true when any policy it combines is).
|
|
348
340
|
`onSlotIdle` is still called when the last subscriber leaves.
|
|
349
341
|
|
|
350
342
|
A policy decides whether and when a slot goes; it does no work itself. A
|
|
351
|
-
factory, and `
|
|
343
|
+
factory, and an `OIMDefaults` (whose `slotReleasePolicy` a factory given it as `defaults` reads as its default), takes ONE
|
|
352
344
|
through the option `slotReleasePolicy`. With none, the slot owner is built with
|
|
353
|
-
`
|
|
345
|
+
`OIMSlotAtEmptyReleasePolicy`, so a slot with a value stays for good and an empty
|
|
354
346
|
one with nothing holding it goes; there is no "no policy" branch on any path.
|
|
355
|
-
Computed services default to `
|
|
347
|
+
Computed services default to `OIMSlotAtUnholdReleasePolicy`.
|
|
356
348
|
|
|
357
349
|
| Policy | A slot goes |
|
|
358
350
|
|---|---|
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
-
| `
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
`
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
`
|
|
351
|
+
| `OIMSlotAfterWriteReleasePolicy` | once its last write is older than `keepAfterWriteMs` |
|
|
352
|
+
| `OIMSlotAfterIdleReleasePolicy` | once nothing has watched it for `keepAfterIdleMs` |
|
|
353
|
+
| `OIMSlotAfterUnholdReleasePolicy` | once `keepAfterUnholdMs` has passed since its LAST unhold; a hold taken again inside the window keeps it |
|
|
354
|
+
| `OIMSlotAtUnholdReleasePolicy` | at the last unhold, even with a value |
|
|
355
|
+
| `OIMSlotAtEmptyReleasePolicy` | once it has no value and nothing holds it; a computed, never empty, stays while its owner lives |
|
|
356
|
+
| `OIMSlotNeverReleasePolicy` | never, empty or not; a deleted key's slot stays filed |
|
|
357
|
+
| `OIMSlotEarliestReleasePolicy` | when the earliest deadline of its members falls due — a `min` over deadlines |
|
|
358
|
+
|
|
359
|
+
**WHICH WRITES MOVE THE WRITE DEADLINE is a strategy of `OIMSlotAfterWriteReleasePolicy`**,
|
|
360
|
+
`slotReleaseDueFramePolicy`, chosen at construction. `OIMSlotReleaseAutoDueFramePolicy` (the default)
|
|
361
|
+
makes every write its own frame, so every write moves the deadline. `OIMSlotReleaseManualDueFramePolicy`
|
|
362
|
+
moves it only for writes made while one of its frames is open: `openSlotReleaseDueFrame()` and
|
|
363
|
+
`closeSlotReleaseDueFrame()` (which throws by name when no frame is open), or
|
|
364
|
+
`writeInSlotReleaseDueFrame(write)`, which opens, calls `write` and closes in a `finally`. One Manual object
|
|
365
|
+
can be passed to several `OIMSlotAfterWriteReleasePolicy` policies with different windows: one frame then moves each slot's
|
|
366
|
+
deadline by its own collection's window. This is what keeps a local edit or a client-only field from
|
|
367
|
+
extending data that only a server response should refresh. `write` must be synchronous: a frame closes
|
|
368
|
+
when the function returns.
|
|
369
|
+
|
|
370
|
+
The release wheels live on the db's collector slot, at most one of each per `OIMDB`:
|
|
371
|
+
`db.collectorSlot.afterWriteSlotReleaseWheel`, `afterIdleSlotReleaseWheel` and
|
|
372
|
+
`afterUnholdSlotReleaseWheel`, each an `OIMSlotReleaseWheel` that holds its own slices, timer and clock.
|
|
373
|
+
A wheel is created by the first policy that needs it, through `oimMintAfterWriteSlotReleaseWheel(collectorSlot)`
|
|
374
|
+
(and its after-idle and after-unhold twins), which returns the existing wheel or creates it; until then the field is
|
|
375
|
+
`undefined`, and a db with no timed policy never pulls the wheel code into a bundle. The
|
|
372
376
|
clock and the timer are the db's: pass `new OIMDB({ getNowMs, releaseTimer })` to control time (tests),
|
|
373
|
-
and sweep with `
|
|
374
|
-
`
|
|
377
|
+
and sweep with `oimMintAfterIdleSlotReleaseWheel(db.collectorSlot).flush()`.
|
|
378
|
+
`OIMSlotAtUnholdReleasePolicy` is the zero window and has no collector.
|
|
375
379
|
|
|
376
380
|
A zero window never touches a release queue or a timer: when a slot goes idle with
|
|
377
381
|
`onSlotIdle` answering `true`, it is released at once if nothing holds it, and
|
|
@@ -379,17 +383,21 @@ a freshly minted slot that is already due goes to the unused-slot collector and
|
|
|
379
383
|
leaves at the end of the frame. A window above zero is filed in that policy's own
|
|
380
384
|
queue.
|
|
381
385
|
|
|
386
|
+
**Two writes keep their slot until the unused-slot collector flushes, and this is deliberate.**
|
|
387
|
+
(1) a value that the collection's emptiness policy calls empty, and (2) a write under a zero window
|
|
388
|
+
(`OIMSlotAfterWriteReleasePolicy` / `OIMSlotAfterIdleReleasePolicy` with `0`) on a slot with no subscriber.
|
|
389
|
+
Released at once, the slot `writeSlotContentByKey` returned would be dead on the next line: a second write
|
|
390
|
+
to it throws, and handing it to a bucket throws ("an identity handed to a bucket was released") — which is
|
|
391
|
+
what persist's restore does with the slots `writeInBatch` returns. The collector gives the hold back when it
|
|
392
|
+
flushes — at the end of `db.flush()`, or when a collector scope (`OIMUnusedSlotCollectorService.INSTANCE.run`)
|
|
393
|
+
closes — and the slot is released there.
|
|
394
|
+
|
|
382
395
|
```ts
|
|
383
|
-
import {
|
|
384
|
-
OIMSlotReleasePolicyAfterIdle,
|
|
385
|
-
OIMSlotReleasePolicyAfterWrite,
|
|
386
|
-
OIMSlotReleasePolicyEarliest,
|
|
387
|
-
} from '@oimdb/core';
|
|
388
396
|
|
|
389
|
-
const
|
|
390
|
-
slotReleasePolicy: new
|
|
391
|
-
new
|
|
392
|
-
new
|
|
397
|
+
const releasingDefaults = new OIMDefaults({
|
|
398
|
+
slotReleasePolicy: new OIMSlotEarliestReleasePolicy([
|
|
399
|
+
new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: 60_000 }),
|
|
400
|
+
new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
|
|
393
401
|
]),
|
|
394
402
|
});
|
|
395
403
|
```
|
|
@@ -401,9 +409,9 @@ no other:
|
|
|
401
409
|
|
|
402
410
|
| Marker | Moved by | Not moved by |
|
|
403
411
|
|---|---|---|
|
|
404
|
-
| `releaseDueAfterWriteMs` | every write, under `
|
|
405
|
-
| `releaseDueAfterIdleMs` | the first content of a slot with no subscriber, and the departure of the last subscriber, under `
|
|
406
|
-
| `releaseDueAfterUnholdMs` | every last unhold, under `
|
|
412
|
+
| `releaseDueAfterWriteMs` | every write, under `OIMSlotAfterWriteReleasePolicy` | a subscriber arriving or leaving |
|
|
413
|
+
| `releaseDueAfterIdleMs` | the first content of a slot with no subscriber, and the departure of the last subscriber, under `OIMSlotAfterIdleReleasePolicy` | a later write, and the give-back of a hold |
|
|
414
|
+
| `releaseDueAfterUnholdMs` | every last unhold, under `OIMSlotAfterUnholdReleasePolicy` | a write |
|
|
407
415
|
|
|
408
416
|
`-1` means unset. A slot goes on the SOONEST of those that are set — each
|
|
409
417
|
marker is an eviction deadline, so whichever says "out" first decides — and a
|
|
@@ -415,7 +423,7 @@ is allowed to move it: a write arriving after the subscriber left would push the
|
|
|
415
423
|
deadline out and destroy the idle policy's verdict, and the last subscriber
|
|
416
424
|
leaving after a write would destroy the write policy's. Both directions are
|
|
417
425
|
pinned by
|
|
418
|
-
`
|
|
426
|
+
`collector/tests/which-release-marker-each-collector-may-move.test.ts`;
|
|
419
427
|
collapse the two markers into one and six of its nine cases go red.
|
|
420
428
|
|
|
421
429
|
**The release policy's `isSlotReleasable(slot)`** is what the release guard asks
|
|
@@ -424,8 +432,8 @@ now; `false` keeps it, and a policy with a finite deadline has itself filed the
|
|
|
424
432
|
slot into its release queue (a marker of `-1` is kept with nothing scheduled).
|
|
425
433
|
An **empty** slot is released without asking, and a service given no release
|
|
426
434
|
policy keeps a non-empty slot. `OIMComputedService` defaults its policy to
|
|
427
|
-
`
|
|
428
|
-
nothing refers to goes at once. `
|
|
435
|
+
`OIMSlotAtUnholdReleasePolicy`, which always answers `true`, so a derivation
|
|
436
|
+
nothing refers to goes at once. `OIMSlotEarliestReleasePolicy` asks every member
|
|
429
437
|
and answers `true` if any does.
|
|
430
438
|
|
|
431
439
|
**AN IDLE WINDOW OPENS IN EXACTLY TWO PLACES, AND NOWHERE ELSE:**
|
|
@@ -436,7 +444,7 @@ and answers `true` if any does.
|
|
|
436
444
|
2. **when the LAST SUBSCRIBER leaves** — `OIMSlotOwner.unsubscribeFromSlot` empties
|
|
437
445
|
`slot.effects` and runs the idle step, which opens one.
|
|
438
446
|
|
|
439
|
-
Both ask the policy's `onSlotIdle(slot)`; under `
|
|
447
|
+
Both ask the policy's `onSlotIdle(slot)`; under `OIMSlotAfterIdleReleasePolicy`
|
|
440
448
|
it stamps `releaseDueAfterIdleMs` and files the slot into the policy's queue. An
|
|
441
449
|
**empty** slot gets no window, and the release guard takes it without asking a
|
|
442
450
|
clock at all.
|
|
@@ -450,60 +458,65 @@ therefore taken at the give-back and not one window later.
|
|
|
450
458
|
nothing — and the queue has taken the slot off its slice, so nothing re-files it.
|
|
451
459
|
The give-back is where the guard is asked again.
|
|
452
460
|
|
|
453
|
-
### The
|
|
461
|
+
### The release wheels
|
|
454
462
|
|
|
455
|
-
Each of `
|
|
456
|
-
`
|
|
457
|
-
|
|
458
|
-
|
|
463
|
+
Each of `OIMSlotAfterWriteReleasePolicy`, `OIMSlotAfterIdleReleasePolicy` and
|
|
464
|
+
`OIMSlotAfterUnholdReleasePolicy` files into its own release wheel, and the difference between the wheels
|
|
465
|
+
is WHICH MARKER each reads and writes — injected at construction, so there is no flag and no branch on the
|
|
466
|
+
sweep path. A wheel is an aggregate: its slices, its timer and its clock are fields of the object.
|
|
459
467
|
|
|
460
468
|
```ts signature
|
|
461
|
-
abstract class
|
|
469
|
+
abstract class OIMSlotWheel {
|
|
470
|
+
readonly getNowMs: () => number;
|
|
471
|
+
getNextSliceNumber(): number | undefined; // the nearest filed slice, or undefined
|
|
472
|
+
sweepUpToSliceNumber(upToSliceNumber: number): void;
|
|
473
|
+
flush(): void; // sweeps what is due now and re-arms the timer
|
|
474
|
+
cancelAllSlots(): void; // takes the timer down and drops every filing
|
|
475
|
+
}
|
|
476
|
+
class OIMSlotReleaseWheel extends OIMSlotWheel {
|
|
477
|
+
constructor(options: TOIMSlotReleaseWheelOptions); // { getNowMs, timer, errorPolicy, getReleaseDueMs, setReleaseDueMs, releaseScheduledSlotFlag }
|
|
462
478
|
readonly getReleaseDueMs: (slot: TOIMSlot) => number;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
isSlotReleasable(
|
|
467
|
-
cancelSlotRelease(
|
|
468
|
-
|
|
469
|
-
cancelAllSlotReleases(releaseSlotCollectorSlot): void;
|
|
479
|
+
markSlotReleaseDue(slot, keepMs): number; // stamps now + keepMs on this marker, returns now
|
|
480
|
+
markSlotReleaseDueNow(slot): void; // the marker is now; an unheld, unwatched slot goes at once
|
|
481
|
+
scheduleSlotRelease(slot, nowMs): void; // files the slot at its marker; -1 or already filed files nothing
|
|
482
|
+
isSlotReleasable(slot): boolean; // due now; if not yet, files it on the wheel
|
|
483
|
+
cancelSlotRelease(slot): void; // no-op for a slot whose marker is -1
|
|
484
|
+
isSlotReleaseScheduled(slot): boolean;
|
|
470
485
|
}
|
|
471
|
-
OIMReleaseSlotCollectorServiceAfterIdle.INSTANCE
|
|
472
|
-
OIMReleaseSlotCollectorServiceAfterWrite.INSTANCE
|
|
473
|
-
OIMReleaseSlotCollectorServiceAfterUnhold.INSTANCE
|
|
474
486
|
|
|
475
487
|
type TOIMCollectorSlot = {
|
|
476
488
|
readonly unusedSlotCollectorSlot: TOIMUnusedSlotCollectorSlot;
|
|
477
|
-
readonly
|
|
478
|
-
|
|
479
|
-
|
|
489
|
+
readonly slotWheelOptions: TOIMSlotWheelOptions; // the db's clock, timer and error policy
|
|
490
|
+
afterIdleSlotReleaseWheel: OIMSlotReleaseWheel | undefined; // created on first use
|
|
491
|
+
afterWriteSlotReleaseWheel: OIMSlotReleaseWheel | undefined;
|
|
492
|
+
afterUnholdSlotReleaseWheel: OIMSlotReleaseWheel | undefined;
|
|
480
493
|
};
|
|
494
|
+
oimMintAfterWriteSlotReleaseWheel(collectorSlot): OIMSlotReleaseWheel // and …AfterIdle…, …AfterUnhold…
|
|
481
495
|
```
|
|
482
496
|
|
|
483
497
|
The clock (`getNowMs`, default `Date.now`), the timer (`releaseTimer`, default an unref'd `setTimeout`)
|
|
484
|
-
and the `errorPolicy` (default `
|
|
498
|
+
and the `errorPolicy` (default `OIMLogAndContinueErrorPolicy.INSTANCE`) are `OIMDB` options, shared by the
|
|
485
499
|
channel and every release collector of that db. A release that throws inside a hand-called `flush` throws to
|
|
486
500
|
that caller; one that throws on the collector's own timer tick has no caller and goes to the db's
|
|
487
501
|
`errorPolicy`. So one error policy on the db routes every error through one place:
|
|
488
502
|
|
|
489
503
|
```ts
|
|
490
|
-
import { OIMErrorPolicyThrowAtOnce, OIMSlotReleasePolicyAfterIdle } from '@oimdb/core';
|
|
491
504
|
|
|
492
|
-
const oneSinkDb = new OIMDB({
|
|
493
|
-
|
|
494
|
-
slotReleasePolicy: new
|
|
505
|
+
const oneSinkDb = new OIMDB({ errorPolicy: new OIMThrowAtOnceErrorPolicy() });
|
|
506
|
+
const idleReleasingDefaults = new OIMDefaults({
|
|
507
|
+
slotReleasePolicy: new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
|
|
495
508
|
});
|
|
496
509
|
```
|
|
497
510
|
|
|
498
|
-
|
|
|
511
|
+
| Wheel | Reads | What renews it | What does not |
|
|
499
512
|
|---|---|---|---|
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
513
|
+
| `afterIdleSlotReleaseWheel` | `releaseDueAfterIdleMs` | the first content of a slot, and the give-back of the last hold | a later write |
|
|
514
|
+
| `afterWriteSlotReleaseWheel` | `releaseDueAfterWriteMs` | every write, under its policy | a subscriber arriving or leaving |
|
|
515
|
+
| `afterUnholdSlotReleaseWheel` | `releaseDueAfterUnholdMs` | every last unhold | a hold taken again inside the window |
|
|
503
516
|
|
|
504
|
-
**A CONSUMER MAY RUN BOTH, AND THEY SHARE NOTHING.** Each has its own
|
|
505
|
-
|
|
506
|
-
nothing has to be reconciled: `
|
|
517
|
+
**A CONSUMER MAY RUN BOTH, AND THEY SHARE NOTHING.** Each wheel has its own slices and its own
|
|
518
|
+
scheduled run. Whichever deadline falls due first takes the slot, and
|
|
519
|
+
nothing has to be reconciled: `OIMSlotEarliestReleasePolicy` answers a `min`
|
|
507
520
|
over its members' deadlines, so a sweep of either queue reaches the same verdict.
|
|
508
521
|
|
|
509
522
|
**NEITHER MEANS UNREAD.** No read is seen and no door reports one: an identity a
|
|
@@ -518,19 +531,16 @@ The slot owner's `releaseSlotIfUnheld` checks `holdCount > 0`, then
|
|
|
518
531
|
`oimIsSlotReleased(slot)`, then `slot.owner !== this`, then `hasReference(slot)`, and
|
|
519
532
|
only then — on a slot that is not empty — asks the policy's `isSlotReleasable`; an empty slot
|
|
520
533
|
asks `isEmptySlotReleasable` instead, which every policy but
|
|
521
|
-
`
|
|
534
|
+
`OIMSlotNeverReleasePolicy` answers `true`. So only `OIMSlotNeverReleasePolicy`
|
|
522
535
|
keeps an identity that holds nothing.
|
|
523
536
|
|
|
524
|
-
**A release that falls inside a flush
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
slot is handed to the unused-slot collector instead, which holds it and gives it
|
|
529
|
-
back once the flush is over; the release then runs. An empty slot needs no write
|
|
530
|
-
and goes at once.
|
|
537
|
+
**A release that falls inside a flush happens at once.** A slot at `holdCount` 0 has no subscriber
|
|
538
|
+
(a subscription is a hold), so clearing its value wakes nobody, and a release notifies no content-change
|
|
539
|
+
listener. When the last hold goes during a frame — an effect body disposing another effect, say — the slot is
|
|
540
|
+
released there, whatever its kind. A later `holdSlotByKey` for the same key in that flush mints a new slot.
|
|
531
541
|
|
|
532
542
|
*The `owner !== this` line costs no lookup. A KEYLESS slot — a single's, filed in an
|
|
533
|
-
`
|
|
543
|
+
`OIMSingleSlotStoreService` store — goes through the same guard under its service's policy: an
|
|
534
544
|
emptied single bucket nothing holds is released like a keyed one, and the single service's
|
|
535
545
|
`getSingleSlot(storeSlot)` mints a fresh slot on the next read.*
|
|
536
546
|
|
|
@@ -544,7 +554,7 @@ one whole window later. **A give-back is NOT that moment**: `unholdSlot` stamps
|
|
|
544
554
|
nothing, so a slot held through an expired sweep goes at the give-back.
|
|
545
555
|
|
|
546
556
|
```ts
|
|
547
|
-
const effect = effectService.createEffect(db,
|
|
557
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
548
558
|
// …the window passes, sweeps run, nothing happens to this slot…
|
|
549
559
|
effectService.disposeEffect(effect);
|
|
550
560
|
oimIsSlotReleased(slot); // false — a whole new window starts here
|
|
@@ -555,34 +565,41 @@ releases the stale index, which gives back one reference per entity slot it name
|
|
|
555
565
|
each entity slot whose own window already ran out is taken by the ordinary guard the
|
|
556
566
|
instant that reference departs.
|
|
557
567
|
|
|
558
|
-
Pinned by `
|
|
568
|
+
Pinned by `collector/tests/what-a-stale-index-takes-with-it.test.ts`,
|
|
559
569
|
`…/what-an-identity-nobody-subscribed-to-costs-after-a-while.test.ts` and
|
|
560
570
|
`…/what-a-subscribed-row-costs-the-release-clock.test.ts`.
|
|
561
571
|
|
|
562
572
|
### The release guard, and who fires it
|
|
563
573
|
|
|
564
|
-
**THE
|
|
565
|
-
`
|
|
574
|
+
**THE WHEEL DECIDES AT A DEADLINE, NOT THE OWNER.**
|
|
575
|
+
`OIMSlotReleaseWheel.sweepUpToSliceNumber`, per slot in a slice at or below the one due:
|
|
566
576
|
|
|
567
|
-
1. `oimIsSlotReleased(slot)` → skip it;
|
|
568
|
-
2. its own marker, read through `getReleaseDueMs(slot)` — the ONE field this
|
|
577
|
+
1. its bit is clear → a stale entry, skip it; `oimIsSlotReleased(slot)` → skip it;
|
|
578
|
+
2. its own marker, read through `getReleaseDueMs(slot)` — the ONE field this wheel
|
|
569
579
|
answers for;
|
|
570
|
-
3. `-1` → the slot is dropped from the
|
|
571
|
-
4. a moment still in the future → the
|
|
580
|
+
3. `-1` → the slot is dropped from the wheel: this policy does not claim it;
|
|
581
|
+
4. a moment still in the future → the wheel **re-files the slot itself**, into
|
|
572
582
|
`oimSliceOfMs(getReleaseDueMs)` through its own slice arithmetic;
|
|
573
583
|
5. a moment already passed → `slot.owner.releaseSlotIfUnheld(slot)`, which asks the
|
|
574
|
-
policy's `isSlotReleasable` and so declines when the other policy still keeps
|
|
575
|
-
the slot.
|
|
584
|
+
policy's `isSlotReleasable` and so declines when the other policy still keeps the
|
|
585
|
+
slot, or when the slot is held.
|
|
586
|
+
|
|
587
|
+
**INVALIDATING IS MARKING THE DEADLINE NOW.** `wheel.markSlotReleaseDueNow(slot)` sets this wheel's
|
|
588
|
+
marker to now and hands the slot to `slot.owner.releaseSlotIfUnheld(slot)` at once: a slot nothing
|
|
589
|
+
holds or watches goes with its data; a held or watched slot stays, keeps its data, and goes the moment
|
|
590
|
+
its last hold or subscriber leaves, because its deadline is already past. The next marked write moves
|
|
591
|
+
the marker a window ahead again. It writes no content, so it wakes no effect.
|
|
576
592
|
|
|
577
593
|
The owner files nothing from inside a sweep. It **opens** a window on its own
|
|
578
594
|
events, through the policy's `onSlotIdle` — when a slot first gets content, and
|
|
579
595
|
when the last subscriber leaves — and `onSlotUnheld` on the give-back of the last hold.
|
|
580
596
|
|
|
581
|
-
**A SLOT ALREADY ON A QUEUE STAYS WHERE IT IS.** `scheduleSlotRelease` of a slot
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
597
|
+
**A SLOT ALREADY ON A QUEUE STAYS WHERE IT IS.** `scheduleSlotRelease` of a slot that is
|
|
598
|
+
already filed moves nothing: the sweep re-reads the deadline when that slice comes
|
|
599
|
+
due and re-files the slot then (the timing-wheel lazy re-insertion). So a write to a
|
|
600
|
+
queued entity slot costs the queue one bit test, not an unfile and a refile. A
|
|
601
|
+
deadline only ever moves later — each policy adds a fixed window to a clock that
|
|
602
|
+
does not go back — so the slice a slot already sits in is never later than its deadline.
|
|
586
603
|
|
|
587
604
|
`releaseSlotIfUnheld(slot)` is the guard, and it releases only when all of
|
|
588
605
|
`holdCount === 0`, `oimIsSlotReleased(slot)` is false, `slot.owner === this`,
|
|
@@ -592,7 +609,7 @@ happens to it. On a slot that is not empty with time still owed, `isSlotReleasab
|
|
|
592
609
|
answers `false` and the policy files the slot into its queue at its own marker, so a
|
|
593
610
|
filing can neither shorten nor extend a deadline. That is the path from `unholdSlot` and from a write, never from a
|
|
594
611
|
sweep, which had already re-filed at step 4. Releasing hands the slot to the
|
|
595
|
-
content
|
|
612
|
+
content service's `releaseSlotContent` (which gives back what the content held),
|
|
596
613
|
marks the slot empty, calls the policy's `cancelSlotRelease(slot)`, unfiles the key
|
|
597
614
|
through the store service's `remove`, and sets the RELEASED flag in `slot.slotFlags`. The `owner` is NOT cleared — the slot keeps
|
|
598
615
|
pointing at the object that let it go, which is what makes `slot.owner !== this`
|
|
@@ -605,48 +622,48 @@ opposite:
|
|
|
605
622
|
`oimIsSlotReleased(slot)` and returns; it does not throw. A sweep meeting a slot
|
|
606
623
|
something else already let go must not raise.
|
|
607
624
|
- **Trying to WORK with a released slot throws by name.** `assertNotReleased`
|
|
608
|
-
guards the content services' write doors (`
|
|
609
|
-
`
|
|
625
|
+
guards the content services' write doors (`writeSlotContent`, `clearSlotContent`, `updateEntity`, `markEntityChanged`,
|
|
626
|
+
`addBucketMemberSlot`, `removeBucketMemberSlot`, `markContentChanged`) and the reference doors (`holdSlot`, `unholdSlot`, `subscribeToSlot`,
|
|
610
627
|
`unsubscribeFromSlot`). The refusal message starts `This identity was released:`.
|
|
611
628
|
|
|
612
|
-
### The
|
|
629
|
+
### The wheels and the unused-slot collector
|
|
613
630
|
|
|
614
631
|
```ts signature
|
|
615
|
-
|
|
616
|
-
|
|
632
|
+
oimMintAfterWriteSlotReleaseWheel(db.collectorSlot) // and …AfterIdle…, …AfterUnhold…
|
|
633
|
+
new OIMSlotRefetchWheel(options: TOIMSlotRefetchWheelOptions) // { slotRefetchListener, getNowMs?, timer?, errorPolicy? }
|
|
617
634
|
OIMUnusedSlotCollectorService.INSTANCE
|
|
618
635
|
createOIMUnusedSlotCollectorSlot(): TOIMUnusedSlotCollectorSlot
|
|
619
636
|
```
|
|
620
637
|
|
|
621
|
-
|
|
622
|
-
`
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
`
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
638
|
+
**A wheel is the clock.** `OIMSlotWheel` extends `OIMFlushSchedulerService` and is its own scheduled slot:
|
|
639
|
+
`flush()` sweeps what is due and re-arms the timer to the next deadline. `cancelAllSlots()` takes the timer
|
|
640
|
+
down and drops every filing; the next filing arms it again. The release wheels take the clock (`getNowMs`)
|
|
641
|
+
and the timer from the db, so a test drives them through `new OIMDB({ getNowMs, releaseTimer })`. The default
|
|
642
|
+
timer is a `setTimeout` that calls `unref` where the platform has it, so an idle Node process is not held open.
|
|
643
|
+
|
|
644
|
+
**A wheel files slots into time slices**, not one entry each. `oimSliceOfMs(ms)` is
|
|
645
|
+
`Math.floor(ms / OIM_SLICE_LENGTH_MS)` and `OIM_SLICE_LENGTH_MS` is `1024`, so every slot due inside the
|
|
646
|
+
same ~1s slice shares one array and one timer.
|
|
647
|
+
|
|
648
|
+
**WHETHER A SLOT IS ON A RELEASE WHEEL IS A BIT ON THE SLOT, NOT A MAP.** Each of the three release
|
|
649
|
+
wheels of a db owns one bit of `slot.slotFlags` (`releaseScheduledSlotFlag`, `4`, `8`, `16`), so a slot
|
|
650
|
+
can sit on all three at once, once on each. Filing pushes the slot onto its slice's array and sets the
|
|
651
|
+
bit; cancelling clears the bit and leaves the entry, which the sweep skips when its slice comes due. A
|
|
652
|
+
cancelled slot therefore keeps its slice named by `getNextSliceNumber` until that sweep.
|
|
653
|
+
|
|
654
|
+
A slot re-filed during a sweep cannot land in a slice the same sweep is still walking: the sweep raises a
|
|
655
|
+
floor for its duration, so a re-file — the wheel's own at step 4, or one a release makes — goes to the next
|
|
656
|
+
slice and is looked at next time, not in a loop.
|
|
657
|
+
|
|
658
|
+
**`OIMSlotRefetchWheel` is a wheel of slots due a refetch at an explicit time.** It computes no deadline,
|
|
659
|
+
keeps no bit and no field on the slot: `scheduleSlotRefetchAt(slot, atMs)` files an entry into the slice
|
|
660
|
+
of `atMs`, every call files one more. Its sweep takes only slices that have fully elapsed, skips released
|
|
661
|
+
slots, drops slots nobody subscribes to, and calls `slotRefetchListener.onSlotsRefetchDue(slots)` ONCE per
|
|
662
|
+
sweep with every watched slot that came due; the array is reused and emptied after the call, so a listener
|
|
663
|
+
does not keep it. Moving a refetch earlier is another `scheduleSlotRefetchAt`; the listener decides from
|
|
664
|
+
its own data whether an entry is current, so an early or duplicate entry is skipped there. The wheel is
|
|
665
|
+
created by whoever needs it, not by the db, and its clock, timer and error policy default to `Date.now`,
|
|
666
|
+
an unref'd `setTimeout` and `OIMLogAndContinueErrorPolicy.INSTANCE`.
|
|
650
667
|
|
|
651
668
|
**The unused-slot collector** has nothing to do with time. It is the one that makes
|
|
652
669
|
`getSlotByKey(storeSlot, key)` safe: `OIMUnusedSlotCollectorService.INSTANCE.holdSlotUntilFlush(unusedSlotCollectorSlot, slot)`
|
|
@@ -675,7 +692,7 @@ OIMUnusedSlotCollectorService.INSTANCE.flush(unusedSlotCollectorSlot); // now
|
|
|
675
692
|
get that.** A computed takes a hold on each of its own direct sources at the mint
|
|
676
693
|
and gives them back when it is released, so an arena holds the top and the chain holds itself.
|
|
677
694
|
|
|
678
|
-
Pinned by `
|
|
695
|
+
Pinned by `collector/tests/what-a-named-arena-keeps-cached.test.ts`: the
|
|
679
696
|
derivation survives a settle, is the SAME instance on the next read (built once),
|
|
680
697
|
is rebuilt only after the arena is flushed, and `open`/`close` nest so an inner
|
|
681
698
|
scope ending drops nothing an outer one stands on.
|
|
@@ -709,13 +726,13 @@ path. There is no `if (inPlace)` anywhere in the engine.
|
|
|
709
726
|
|
|
710
727
|
| Fork | Strategies |
|
|
711
728
|
|---|---|
|
|
712
|
-
| how a write lands | `
|
|
713
|
-
| how a bucket holds slots | `
|
|
714
|
-
| what naming a slot records | `
|
|
715
|
-
| which writes re-sort a sorted list | `sortColumns` (only a write changing one of them on a member) / `comparator` alone (any member write), the options of `
|
|
716
|
-
| how a key is addressed | `
|
|
717
|
-
| when a slot may go | `
|
|
718
|
-
| who decides a bucket's slots | a bucket collection (`
|
|
729
|
+
| how a write lands | `createOIMInPlaceEntityWriter()` / `createOIMReplacingEntityWriter()` |
|
|
730
|
+
| how a bucket holds slots | `OIMSlotContentSetBucketService` / `OIMSlotContentPositionalArrayBucketService` (the positions are the caller's) |
|
|
731
|
+
| what naming a slot records | `bucketMemberSlotsArrayService: OIMBucketMemberSlotsArrayService` (one per mention, the default) / `new OIMBucketUniqueMemberSlotsArrayService<TSlot>()` (one per slot), on either array bucket |
|
|
732
|
+
| which writes re-sort a sorted list | `sortColumns` (only a write changing one of them on a member) / `comparator` alone (any member write), the options of `createOIMSortedComputedService` |
|
|
733
|
+
| how a key is addressed | `OIMScalarKeyTreeWalk.INSTANCE` / `OIMCompositeKeyTreeWalk.INSTANCE` |
|
|
734
|
+
| when a slot may go | `OIMSlotAfterIdleReleasePolicy` / `OIMSlotAfterWriteReleasePolicy`, either or both |
|
|
735
|
+
| who decides a bucket's slots | a bucket collection (`createOIMSlotCollectionService` over a bucket content service, the caller) / an index (`createOIMSlotCollectionIndexService`, filed from the entity's own key) |
|
|
719
736
|
|
|
720
737
|
**One strategy per kind per app.** A V8 call site is shared by every service
|
|
721
738
|
that runs the same engine code, so mixing strategies of one kind in one process
|
|
@@ -724,8 +741,8 @@ V8 still caches each one and can still inline
|
|
|
724
741
|
(`--max-valid-polymorphic-map-count`, default 4). Five or more is megamorphic: no
|
|
725
742
|
per-site cache and no inlining through it. The kinds that count: release policies
|
|
726
743
|
(the `IOIMSlotReleasePolicy` strategies), set-driven vs array-driven buckets and indexes, entity
|
|
727
|
-
writers (`
|
|
728
|
-
`
|
|
744
|
+
writers (`createOIMInPlaceEntityWriter` / `createOIMReplacingEntityWriter`) and
|
|
745
|
+
`TOIMEntityInserter`, `TOIMSameValueGuard`, `IOIMSlotEmptinessPolicy`, and
|
|
729
746
|
`TOIMChannelScheduler`. Two or three of one kind is fine; five release policies in
|
|
730
747
|
one process is not. The pair
|
|
731
748
|
`alt.slot-shape.three-shapes-against-one-at-the-write` measures three slot shapes
|
|
@@ -736,27 +753,23 @@ Generate code with one strategy per kind unless the task names a second.
|
|
|
736
753
|
|
|
737
754
|
`createStoreSlot(db)` stamps the db's `defaultChannelSlot` on the store slot, and every write to its slots
|
|
738
755
|
is reported to that channel. No service holds a channel; there is no `channel` option and no
|
|
739
|
-
`unusedSlotCollector` option
|
|
756
|
+
`unusedSlotCollector` option:
|
|
740
757
|
|
|
741
758
|
```ts
|
|
742
|
-
const cardContentService = createOIMSlotContentEntityService<Card>(
|
|
759
|
+
const cardContentService = createOIMSlotContentEntityService<Card>();
|
|
743
760
|
const handBuiltCardService = createOIMSlotCollectionService({ slotContentService: cardContentService });
|
|
744
761
|
handBuiltCardService.createStoreSlot(db);
|
|
745
|
-
|
|
746
|
-
// THE CONVENIENCE WRAPPER — the same two calls in one:
|
|
747
|
-
createOIMEntityService<Card>(db, { keyOf: (c) => c.id }).createStoreSlot(db);
|
|
748
762
|
```
|
|
749
763
|
|
|
750
|
-
A single entity is `
|
|
751
|
-
`createOIMSlotContentEntityService(db, { slotStoreService: OIMSlotStoreServiceSingleSlotDriven.INSTANCE })`),
|
|
764
|
+
A single entity is `createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<T>() })`,
|
|
752
765
|
and its store slot takes the db's channel too and always reports.
|
|
753
766
|
|
|
754
767
|
### A bucket's slots are the caller's
|
|
755
768
|
|
|
756
769
|
A bucket's slots are a fact about the **response** being composed — a search result, a
|
|
757
770
|
page, a window — and **no column can express absence**. So a bucket collection
|
|
758
|
-
is manual, and `entityService.slotContentService.
|
|
759
|
-
bucket. Use an index (`
|
|
771
|
+
is manual, and `entityService.slotContentService.clearSlotContent(slot)` does not take a slot out of any
|
|
772
|
+
bucket. Use an index (`createOIMSlotCollectionIndexService`) when the slots are determined by the entity's key value,
|
|
760
773
|
and an index should maintain buckets only for keys being held.
|
|
761
774
|
|
|
762
775
|
---
|
|
@@ -773,7 +786,7 @@ new OIMDB(options?: TOIMDBOptions)
|
|
|
773
786
|
|
|
774
787
|
type TOIMDBOptions = {
|
|
775
788
|
channelScheduler?: TOIMChannelScheduler; // absent = you flush by hand
|
|
776
|
-
errorPolicy?: IOIMErrorPolicy; // absent =
|
|
789
|
+
errorPolicy?: IOIMErrorPolicy; // absent = OIMLogAndContinueErrorPolicy.INSTANCE
|
|
777
790
|
onBeforeFlush?: () => void; // start of the flush, before the frame; READ ONLY, a write is refused
|
|
778
791
|
getNowMs?: () => number; // the release collectors' clock; absent = Date.now
|
|
779
792
|
releaseTimer?: TOIMTimer; // the release collectors' timer; absent = an unref'd setTimeout
|
|
@@ -789,9 +802,9 @@ db.defaultChannelSlot // TOIMChannelSlot
|
|
|
789
802
|
db.flush() // OIMChannelService.INSTANCE.flush(db.defaultChannelSlot)
|
|
790
803
|
|
|
791
804
|
interface IOIMErrorPolicy { onErrorCaught(error: unknown): void }
|
|
792
|
-
|
|
793
|
-
new
|
|
794
|
-
new
|
|
805
|
+
OIMLogAndContinueErrorPolicy.INSTANCE // console.error; the flush goes on
|
|
806
|
+
new OIMLogAndContinueErrorPolicy({ onError?: (error: unknown) => void }) // console.error, then onError
|
|
807
|
+
new OIMThrowAtOnceErrorPolicy() // throws the first error out of the flush
|
|
795
808
|
```
|
|
796
809
|
|
|
797
810
|
**What the error policy covers** — every error with no synchronous caller to
|
|
@@ -802,7 +815,7 @@ hand-called collector `flush` — throws to it. `channelSlot.errorPolicy` is the
|
|
|
802
815
|
instance the channel reports through; it is the db's `errorPolicy`, which the release collectors share,
|
|
803
816
|
so one policy answers for the whole database. **The default digests and logs**: each throw goes to `console.error`
|
|
804
817
|
and then to `onError` (where Sentry or the devtools registry plugs in), and the
|
|
805
|
-
flush finishes every other effect and handler. `
|
|
818
|
+
flush finishes every other effect and handler. `OIMThrowAtOnceErrorPolicy` throws
|
|
806
819
|
the first error out of `flush()` at once; the effects the frame had not reached yet
|
|
807
820
|
stay queued and run at the next flush. **An effect whose body or cleanup threw
|
|
808
821
|
has not taken the change it woke for**: its `ranAtRevision`/`ranAtEpoch` are
|
|
@@ -853,17 +866,13 @@ Every door below is on `OIMChannelService.INSTANCE` and takes the channel slot f
|
|
|
853
866
|
| `assertNotFlushing(channelSlot)` | `void` — throws if the channel is flushing or its frame is running, i.e. for the whole of a flush |
|
|
854
867
|
| `write(channelSlot, slot)` | `void` — asks `assertNotFlushing`, then marks and queues |
|
|
855
868
|
| `writeWithoutFlushCheck(channelSlot, slot)` | `void` — the same marking and queueing WITHOUT the guard, for a door that already refused. Who may call it is pinned by `tests/who-may-write-without-asking-the-channel.test.ts`; a caller not on that list fails the suite |
|
|
856
|
-
| `openSubframe(channelSlot)` | `void` — a subframe opens, and the frame cannot close until it closes — `flush` throws meanwhile |
|
|
857
|
-
| `closeSubframe(channelSlot)` | `void` — it closes, delivering what was listed for its close and then asking for the frame it owes |
|
|
858
|
-
| `isSubframeOpen(channelSlot)` | `boolean` — whether a subframe is open right now, so a caller can defer work to its close instead of per write |
|
|
859
|
-
| `listChangedStoreSlot(channelSlot, storeSlot, subframeCloseListener)` | `void` — list a store slot whose changes are delivered when the OUTERMOST subframe closes, by calling `subframeCloseListener.onStoreSlotSubframeClosed(storeSlot)`; see below |
|
|
860
869
|
| `dropChannelSlot(channelSlot)` | `void` — kill the channel whole; see below |
|
|
861
870
|
| `isChangedInCurrentFrame(channelSlot, slot)` | `boolean` — whether the slot was written after the last frame ended, i.e. in the frame being collected now; compares `slot.changedAtRevision`/`changedAtEpoch` with the clock reading the last frame closed at. `OIMBucketCommands` keeps a bucket's render script across a flush only while this is true |
|
|
862
871
|
|
|
863
872
|
The channel slot's own fields that a caller reads: `frame` (`TOIMChannelFrame`, `{ isRunning, openedAtRevision,
|
|
864
873
|
openedAtEpoch, … }`; a frame carries no number, the clock reading it opened at is its boundary), `clock`
|
|
865
874
|
(`TOIMChannelClock`, the `{revision, epoch}` pair, shared by every channel of one db; an epoch holds
|
|
866
|
-
`OIM_REVISIONS_PER_EPOCH` (2^30) revisions, so the counter stays a V8 small integer under pointer compression),
|
|
875
|
+
`OIM_REVISIONS_PER_EPOCH` (2^30) revisions, so the counter stays a V8 small integer under pointer compression; the epoch is an int32 that wraps, compared as `(epoch − sinceEpoch) | 0`),
|
|
867
876
|
`collectorSlot`, `errorPolicy`, `isFlushing`. Everything else on it is engine state.
|
|
868
877
|
|
|
869
878
|
### A subscription hears only what happens after it
|
|
@@ -875,14 +884,14 @@ bucket, a computed — and a batch does not soften it: writes accumulate until
|
|
|
875
884
|
after it.
|
|
876
885
|
|
|
877
886
|
```ts
|
|
878
|
-
cardService.
|
|
887
|
+
cardService.writeSlotContentByKey(cards, 'a', { id: 'a', title: 'A', rank: 1, deck: 'spades' });
|
|
879
888
|
const entitySlot = cardService.getSlotByKey(cards, 'a');
|
|
880
889
|
|
|
881
|
-
effectService.createEffect(db,
|
|
890
|
+
effectService.createEffect(db, () => { /* NEVER runs for the upsert above */ }, [entitySlot]);
|
|
882
891
|
db.flush();
|
|
883
892
|
|
|
884
|
-
// A first value is `
|
|
885
|
-
effectService.
|
|
893
|
+
// A first value is `createAndRunEffect`'s job, not `createEffect`'s:
|
|
894
|
+
effectService.createAndRunEffect(db, () => { paint(entitySlot.entity); }, [entitySlot]); // runs now
|
|
886
895
|
```
|
|
887
896
|
|
|
888
897
|
A later write in that same batch IS delivered — "after I subscribed" is decided
|
|
@@ -895,11 +904,11 @@ Told after **one** frame — the first that ends after the call — and then gon
|
|
|
895
904
|
The place that asks decides which frame that is.
|
|
896
905
|
|
|
897
906
|
```ts
|
|
898
|
-
effectService.createEffect(db,
|
|
907
|
+
effectService.createEffect(db, () => {
|
|
899
908
|
paint(slot.entity);
|
|
900
909
|
// the tail of THIS frame, after every other effect of it
|
|
901
910
|
OIMChannelService.INSTANCE.subscribeOnceAfterFrame(channelSlot, () => { measureAndScroll(); });
|
|
902
|
-
});
|
|
911
|
+
}, [slot]);
|
|
903
912
|
```
|
|
904
913
|
|
|
905
914
|
- **Registered from inside an effect body it runs in the tail of that very
|
|
@@ -921,87 +930,27 @@ effectService.createEffect(db, [slot], [], () => {
|
|
|
921
930
|
- One list, not two: it goes into the same set at the same phase, so one-shot and
|
|
922
931
|
standing handlers are called in registration order among each other.
|
|
923
932
|
|
|
924
|
-
### `
|
|
925
|
-
|
|
926
|
-
A
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
bucket. Under a synchronous scheduler that gap used to flush — an auto-index
|
|
946
|
-
settled over a bucket the entity slot had not joined yet, stamped itself past the
|
|
947
|
-
entity slot's revision, and the entity was invisible to that index forever with nothing
|
|
948
|
-
thrown.
|
|
949
|
-
- `closeSubframe` is called from a `finally` by every opener, so a subframe that
|
|
950
|
-
threw part-way still closes and still delivers what it managed to write.
|
|
951
|
-
- **The span belongs to the DOOR, and exactly one door opens it.** The entity content service's
|
|
952
|
-
`writeEntity`, `deleteEntity`, `deleteEntities`, `fillEntities`, `updateEntity` and `markEntityChanged` each
|
|
953
|
-
open one — except a write that is exactly one channel write, which opens none — one write cannot be seen
|
|
954
|
-
half-done, and there is nobody to batch for: `updateEntity`/`updateEntityColumn` and `deleteEntity` when
|
|
955
|
-
the store slot has no content listener, no column of the entity is referenced and emptiness is not
|
|
956
|
-
recomputed on a column write; the entity writer's `fillEntity`, `putEntity`, `patchEntity`, `deleteColumns`
|
|
957
|
-
and `clearEntity` open none. One `upsertEntity` therefore opens one span, and one `insertEntities` opens one
|
|
958
|
-
however many entities it carries. The one exception is the content strategy's `releaseSlotContent`, which
|
|
959
|
-
keeps its own: `releaseSlotIfUnheld` reaches it when a release deadline has passed, and that call arrives
|
|
960
|
-
from the release queue through no door at all — so a removal opens two and an eviction opens one.
|
|
961
|
-
- Every write door of a content service is wrapped in the channel's `openSubframe` / `closeSubframe` on
|
|
962
|
-
`storeSlot.channelSlot`.
|
|
963
|
-
- **Every bucket door wakes the bucket the same way.** Each door of a bucket content service that changed
|
|
964
|
-
the bucket — `addBucketMemberSlot(s)`, `removeBucketMemberSlot(s)`,
|
|
965
|
-
`insertBucketMemberSlotAt`, `removeBucketMemberSlotAt` / `…Range`, both moves,
|
|
966
|
-
`writeBucketMemberSlots`, `clearBucketMemberSlots` — lists the bucket's store slot on the channel
|
|
967
|
-
(`listChangedStoreSlot`) and sets the bucket's EMPTY flag there and then; the outermost `closeSubframe`
|
|
968
|
-
stamps each changed bucket once, tells its release policy `onSlotWritten` once, and only then asks
|
|
969
|
-
whether it may go. So `hasSlotContentByKey` answers right inside an open batch, a
|
|
970
|
-
batch that mixes doors stamps the bucket once, a move renews a write deadline,
|
|
971
|
-
and a door that changed nothing stamps nothing and tells the policy nothing.
|
|
972
|
-
Commands are still recorded at the door, because a position is true only when it
|
|
973
|
-
is written. A release asked for while the bucket has a pending change waits for
|
|
974
|
-
the close.
|
|
975
|
-
- **A content listener that throws on one bucket does not strand the others.**
|
|
976
|
-
The close stamps every filed bucket and settles it before its listeners run,
|
|
977
|
-
goes on to the next bucket when one listener throws, and rethrows the first error
|
|
978
|
-
once every bucket is through — costing nothing when nothing throws.
|
|
979
|
-
- **A released identity is refused before the bucket changes.** Every door that
|
|
980
|
-
takes a member — the add doors, the doors by key, `insertBucketMemberSlotAt`,
|
|
981
|
-
a whole-list `writeBucketMemberSlots`, and the fill of a minted bucket — checks what it was
|
|
982
|
-
handed with `oimIsSlotReleased` first and throws, so a refused door leaves the members,
|
|
983
|
-
their holds and the EMPTY flag exactly as they were. It costs one field read per
|
|
984
|
-
member handed in.
|
|
985
|
-
|
|
986
|
-
### `isSubframeOpen(channelSlot)` / `listChangedStoreSlot(channelSlot, storeSlot, subframeCloseListener)`
|
|
987
|
-
|
|
988
|
-
**What a batch does once instead of per entity.** A subframe is the unit a
|
|
989
|
-
service can defer work to the end of: `listChangedStoreSlot` lists a store slot together with the
|
|
990
|
-
`IOIMSubframeCloseListener` (`onStoreSlotSubframeClosed(storeSlot)`) that delivers its changes, and the
|
|
991
|
-
outermost `closeSubframe` calls each listed listener with its store slot. `isSubframeOpen` says whether there
|
|
992
|
-
is a subframe to defer into at all — with none, the caller does the work immediately.
|
|
993
|
-
|
|
994
|
-
- **The drain runs before the count drops**, so a listener that writes is still inside the subframe and
|
|
995
|
-
still cannot close the frame. A store slot listed by a listener during the drain is picked up by the same
|
|
996
|
-
drain.
|
|
997
|
-
- **Every listed entry runs.** A listener that throws loses its own work for that batch and nothing retries
|
|
998
|
-
it; the entries after it still run, the list is emptied, the count still drops, the frame the subframe
|
|
999
|
-
owed is still asked for, and then the FIRST throw leaves `closeSubframe`.
|
|
1000
|
-
- **What uses it**: the bucket content services. A join or a leave records the bucket slot on its store
|
|
1001
|
-
slot's write queues and lists that store slot; the drain then writes the channel, settles emptiness and
|
|
1002
|
-
asks `releaseSlotIfUnheld` **once per bucket per batch** rather than once per member. Filing 10 000 entity
|
|
1003
|
-
slots into 100 buckets pays 100 of each tail instead of 10 000. Nothing is delivered before the flush either
|
|
1004
|
-
way, so the two are behaviourally the same.
|
|
933
|
+
### Batches: `openBatch` / `closeBatch` / `writeInBatch` on a collection
|
|
934
|
+
|
|
935
|
+
There are no bulk writes. A load or a sync is a loop of single writes, and a collection
|
|
936
|
+
service wraps it in a batch on its store slot:
|
|
937
|
+
|
|
938
|
+
```ts
|
|
939
|
+
cardService.writeInBatch(cards, () => {
|
|
940
|
+
for (const card of payload) cardService.writeSlotContentByKey(cards, card.id, card);
|
|
941
|
+
});
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
- `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `closeBatch` with no batch open
|
|
945
|
+
throws. `writeInBatch` is the `try/finally` form and returns what its function returns.
|
|
946
|
+
- **What the batch changes is who hears the writes, and when.** A content-change listener on the store slot (an
|
|
947
|
+
index on the collection) hears every write of the batch in ONE call, `onManyContentsChanged`, when the
|
|
948
|
+
outermost batch closes. Code inside the batch reads the index as it was before the batch.
|
|
949
|
+
- Effects are not batched by it: every write marks its slot on the channel as usual, and the frame delivers each
|
|
950
|
+
identity once whatever the batch did. A `flush()` inside a batch is allowed; the index catches up when the
|
|
951
|
+
batch closes.
|
|
952
|
+
- A single write outside a batch tells the index at once, and is not rolled back: the slot is filed first and the
|
|
953
|
+
index is told after, so an index that throws leaves the write in place and the throw reaches the caller.
|
|
1005
954
|
|
|
1006
955
|
### `dropChannelSlot(channelSlot)`
|
|
1007
956
|
|
|
@@ -1051,19 +1000,11 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
|
|
|
1051
1000
|
`finally`; `assertNotFlushing` throws on `isFlushing` or `frame.isRunning`, so
|
|
1052
1001
|
all three phases are refused. Refusal is per channel: a flush of one db's channel does not refuse a
|
|
1053
1002
|
write to another db's.
|
|
1054
|
-
- **A refused write keeps its change pending — it is never dropped.** A bucket
|
|
1055
|
-
carries a change `addBucketMemberSlots`/`removeBucketMemberSlots` recorded inside an outer open batch
|
|
1056
|
-
until that batch closes. If something delivers it mid-flush — a member's
|
|
1057
|
-
subscriber giving the list back, so `unholdSlot` reaches `releaseSlotIfUnheld` —
|
|
1058
|
-
the write is refused and the bucket's store slot stays listed, so the next
|
|
1059
|
-
subframe close delivers it.
|
|
1060
1003
|
- **Each door asks once, then writes through `writeWithoutFlushCheck`.** A content service
|
|
1061
1004
|
door opens with `assertWritable`, which asks the channel, and every write it
|
|
1062
|
-
makes after that asks nothing
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
removable and stays — user code runs inside a door (`keyOf`, `index.keyOf`,
|
|
1066
|
-
`slotEmptiness.isEmpty`, an `IOIMEntityWriter`) and can call
|
|
1005
|
+
makes after that asks nothing. The per-entity ask is NOT
|
|
1006
|
+
removable and stays — user code runs inside a door (`index.keyOf`,
|
|
1007
|
+
`slotEmptinessPolicy.isEmpty`, an `IOIMEntityWriter`) and can call
|
|
1067
1008
|
`flush()`, so each entity is a new question. What went is the SECOND and THIRD ask
|
|
1068
1009
|
of the same entity.
|
|
1069
1010
|
- **`flush` during a frame throws:** the message says a frame is already running, and a second one
|
|
@@ -1078,7 +1019,7 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
|
|
|
1078
1019
|
hands the work to a microtask or a later callback rather than writing itself.
|
|
1079
1020
|
- A refusal raised from a tail handler does NOT reach the caller at the call: it
|
|
1080
1021
|
goes to the error policy like any handler throw — logged by default, or thrown
|
|
1081
|
-
out of `flush()` under `
|
|
1022
|
+
out of `flush()` under `OIMThrowAtOnceErrorPolicy`.
|
|
1082
1023
|
- Effects are isolated under the default policy: a throw is logged and handed to
|
|
1083
1024
|
`onError`, and the frame finishes every other effect.
|
|
1084
1025
|
- **A derivation is asked, never woken.** No frame walks to a computed slot.
|
|
@@ -1086,7 +1027,7 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
|
|
|
1086
1027
|
### Subscribing
|
|
1087
1028
|
|
|
1088
1029
|
```ts
|
|
1089
|
-
const effect = effectService.createEffect(db,
|
|
1030
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
1090
1031
|
effectService.disposeEffect(effect);
|
|
1091
1032
|
```
|
|
1092
1033
|
|
|
@@ -1121,10 +1062,10 @@ function is the undo**: it is called before the next run of that body and by
|
|
|
1121
1062
|
`disposeEffect`.
|
|
1122
1063
|
|
|
1123
1064
|
```ts
|
|
1124
|
-
effectService.createEffect(db,
|
|
1065
|
+
effectService.createEffect(db, () => {
|
|
1125
1066
|
const timer = setInterval(poll, 1000);
|
|
1126
1067
|
return () => clearInterval(timer);
|
|
1127
|
-
});
|
|
1068
|
+
}, [slot]);
|
|
1128
1069
|
```
|
|
1129
1070
|
|
|
1130
1071
|
The return is checked with `typeof`, not for truthiness — `() => set.add(key)`
|
|
@@ -1132,24 +1073,38 @@ hands back the `Set` — so anything that is not a function is discarded. The un
|
|
|
1132
1073
|
is cleared before it is called, so one that throws is not called again by the
|
|
1133
1074
|
`disposeEffect` behind it.
|
|
1134
1075
|
|
|
1135
|
-
**
|
|
1136
|
-
|
|
1076
|
+
**A derivation that finds its sources as it reads:** `createOIMSourceTrackingComputedService<TKey, TValue>({ derive, … })` and
|
|
1077
|
+
`createOIMSingleSourceTrackingComputedService<TValue>({ derive, … })`. `derive: TOIMSourceTrackingComputedDerive<TValue, TSlot>` is
|
|
1078
|
+
`(computedSlot, sourceTracker: OIMSourceTracker) => TValue`; there is no `createSources`, and `createComputedStoreSlot(db)` takes no
|
|
1079
|
+
per-store `derive`. Every derive holds what it read and gives back what it no longer reads (new holds first). The slot
|
|
1080
|
+
(`TOIMSourceTrackingComputedSlot<TValue, TKey>` / `TOIMSourceTrackingComputedContentSlot<TValue>`, `sourceTracker` set) is read
|
|
1081
|
+
through `sourceTracker.readSourceComputedValue` (in `createSourceTrackingEffect` or another source-tracking derive) or through `createEffect`'s fifth list; `createEffect`'s `sourceComputeds` and
|
|
1082
|
+
a `createSources` list refuse it by type, because a declared effect subscribes once to the slots under a derivation. When a derive
|
|
1083
|
+
switches sources, a source-tracking effect or derivation over it re-subscribes even when the value held. A body that cannot read
|
|
1084
|
+
through a tracker may pass them as the fifth list, `createEffect(db, body, sourceSlots, sourceComputeds, sourceTrackingComputeds)`;
|
|
1085
|
+
such an effect derives them at creation and re-subscribes when their root slots are replaced. Prefer `createSourceTrackingEffect`: the
|
|
1086
|
+
root check runs after every run of such an effect. An effect with no fifth list pays nothing for it. Options otherwise as
|
|
1087
|
+
`createOIMComputedService` / `createOIMSingleComputedService` (`sameValueGuard`, `releaseValue`, `initialValue`, `slotReleasePolicy`,
|
|
1088
|
+
column options).
|
|
1089
|
+
|
|
1090
|
+
**Sources found at read: `createSourceTrackingEffect(db, body)`.** The second way to build
|
|
1091
|
+
an effect takes no source lists. The body receives an `OIMSourceTracker`, and what it
|
|
1137
1092
|
reads through the tracker is what that run depends on — signal-style tracking.
|
|
1138
1093
|
|
|
1139
1094
|
```ts signature
|
|
1140
|
-
|
|
1141
|
-
type
|
|
1142
|
-
class
|
|
1095
|
+
createSourceTrackingEffect(channelMap: IOIMChannelMap, body: TOIMSourceTrackingEffectBody): TOIMEffect;
|
|
1096
|
+
type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
|
|
1097
|
+
class OIMSourceTracker { // exported as a type; the service builds it
|
|
1143
1098
|
readSourceSlot<TSlot extends TOIMSlot>(slot: TSlot): TSlot; // files the slot, returns it
|
|
1144
1099
|
readSourceComputedValue<TValue>(computedSlot: TOIMComputedSlot<TValue, unknown>): TValue; // files it, returns its value
|
|
1145
1100
|
}
|
|
1146
1101
|
```
|
|
1147
1102
|
|
|
1148
1103
|
```ts
|
|
1149
|
-
const fallback = cardService.
|
|
1150
|
-
effectService.
|
|
1151
|
-
const isFirst =
|
|
1152
|
-
paint(
|
|
1104
|
+
const fallback = cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'King', rank: 13, deck: 'hearts' });
|
|
1105
|
+
effectService.createSourceTrackingEffect(db, sourceTracker => {
|
|
1106
|
+
const isFirst = sourceTracker.readSourceSlot(slot).entity?.rank === 1;
|
|
1107
|
+
paint(sourceTracker.readSourceSlot(isFirst ? slot : fallback).entity);
|
|
1153
1108
|
});
|
|
1154
1109
|
```
|
|
1155
1110
|
|
|
@@ -1162,7 +1117,7 @@ effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
|
|
|
1162
1117
|
creation. A returned function is the undo, exactly as for `TOIMEffectBody`.
|
|
1163
1118
|
|
|
1164
1119
|
```ts
|
|
1165
|
-
const onRank = effectService.createEffect(db,
|
|
1120
|
+
const onRank = effectService.createEffect(db, () => { paint(slot.entity); }, [cardService.columnSlotService.holdColumn(slot, 'rank')]);
|
|
1166
1121
|
```
|
|
1167
1122
|
|
|
1168
1123
|
**A column effect takes the `TOIMColumnSlot` as a root, not a slot and a name.** Minting the
|
|
@@ -1178,7 +1133,7 @@ its carrier. An index store slot's buckets have the same doors on the index serv
|
|
|
1178
1133
|
`createEffect` returns the **effect**, not the column: a column holds no value; read `slot.entity[name]`.
|
|
1179
1134
|
|
|
1180
1135
|
**The name is compile-time checked only as far as the service's column-name type goes** —
|
|
1181
|
-
`holdColumn` takes the service's `TColumnName`, which `
|
|
1136
|
+
`holdColumn` takes the service's `TColumnName`, which `createOIMSlotContentEntityService` sets to
|
|
1182
1137
|
`TOIMEntityColumnName<TEntity>`. `TOIMEntityColumnName<TEntity>` is the type that narrows a name to the
|
|
1183
1138
|
entity's own columns, and `slotContentService.deleteEntityColumns` is typed on it.
|
|
1184
1139
|
|
|
@@ -1188,13 +1143,13 @@ per call; get it once per view, not per entity. `service.holdColumnGroup(slot, c
|
|
|
1188
1143
|
the group's column slot on `slot`, and `service.unholdColumn(columnSlot)` gives it back. A write wakes it once
|
|
1189
1144
|
if it changes any of the group's columns and not at all if it changes only others.
|
|
1190
1145
|
|
|
1191
|
-
**`columnNames` picks the column slot store.** `
|
|
1146
|
+
**`columnNames` picks the column slot store.** `createOIMSlotCollectionService({ slotContentService, columnNames: ['rank', 'label'] })`
|
|
1192
1147
|
— and `columnNames` on any factory's options — files the service's column slots by ordinal: every named column
|
|
1193
1148
|
gets a number, a group gets one at its first slot, and an entity slot's column slots sit in an array by
|
|
1194
1149
|
number instead of a `Map` per entity slot, which is cheaper to create and give back. `holdColumn` and
|
|
1195
1150
|
`getColumnGroup` are typed on those names, and a slot for any other column throws. Without `columnNames` the
|
|
1196
1151
|
store is the `columnSlotStoreService` option, defaulting to the shared map-driven
|
|
1197
|
-
|
|
1152
|
+
a shared map-driven store, and any name is accepted.
|
|
1198
1153
|
|
|
1199
1154
|
---
|
|
1200
1155
|
|
|
@@ -1221,26 +1176,21 @@ frame, with no further code. Every mutation goes through a domain neuron — a w
|
|
|
1221
1176
|
from outside the orchestrator is the mistake, not a case to cover with a fallback.
|
|
1222
1177
|
Full page: `website/docs/guides/with-cnstra.md`.
|
|
1223
1178
|
|
|
1224
|
-
The doors are free functions, one module each, so a consumer ships only the one they import. The
|
|
1225
|
-
|
|
1226
|
-
|
|
1179
|
+
The doors are free functions, one module each, so a consumer ships only the one they import. The collection,
|
|
1180
|
+
single and index factories take `db` first and ONE options object second; the computed factories take only
|
|
1181
|
+
the options object. Each builds a SERVICE,
|
|
1227
1182
|
once per collection type; the store slot is made separately:
|
|
1228
1183
|
|
|
1229
1184
|
| Door | Builds | Store slot |
|
|
1230
1185
|
|---|---|---|
|
|
1231
|
-
| `createOIMSlotContentEntityService
|
|
1232
|
-
| `createOIMSlotCollectionService({ slotContentService })` | `
|
|
1233
|
-
| `createOIMSingleSlotService({ slotContentService })` | `
|
|
1234
|
-
| `
|
|
1235
|
-
| `
|
|
1236
|
-
| `
|
|
1237
|
-
| `
|
|
1238
|
-
| `
|
|
1239
|
-
| `createOIMIndexServiceSortedArrayDriven(db, { entityService, columns? \| keyOf?, sortColumns, … })` | `OIMIndexServiceSortedArrayDriven`: the index whose buckets are kept in the order of `sortColumns` (`readonly TOIMSortColumn<TEntity>[]`, required) through an `OIMSlotContentBucketServiceSortedArrayDriven`. See [Sorted at write](#sorted-at-write) | `createIndexStoreSlot(entityStoreSlot)` |
|
|
1240
|
-
| `createOIMIndexServiceByCompositeKey(db, { entityService, columns? \| keyOf?, … })` | convenience wrapper: the index keyed by an array, `TKey extends readonly TOIMStoreKey[]`, inferred as a tuple from two or more inline columns or from an array-returning `keyOf`, over a tree store | `createIndexStoreSlot(entityStoreSlot)` |
|
|
1241
|
-
| `createOIMComputedService({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService?, columnNames? })` | `OIMComputedService<TKey, TValue, TComputedStore>`; its reads are on `computedService.computedCollectionService`; `derive` and `createSources` may instead be given per store slot; `computedSlotStoreService` is REQUIRED for a non-Map store (a single: `OIMSlotStoreServiceSingleSlotDriven`); see [One derivation per key](#one-derivation-per-key) | `createComputedStoreSlot(db, derive?, createSources?)` |
|
|
1242
|
-
| `createOIMComputedServiceSorted({ indexService, sortColumns?, comparator?, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMComputedServiceSorted` — per key, the entity slots of the index bucket under that key, sorted; see [Sorted lists](#sorted-lists) | `createSortedStoreSlot(indexStoreSlot)` |
|
|
1243
|
-
| `createOIMComputedServiceBucketMemo({ bucketCollectionService, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMComputedServiceBucketMemo` — per key, the bucket's slots as one array; see [The order of a bucket](#the-order-of-a-bucket-as-one-derivation-per-key) | `createBucketMemoStoreSlot(bucketStoreSlot)` |
|
|
1186
|
+
| `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({ entityWriter?, slotEmptinessPolicy? }?)` | `OIMSlotContentEntityService<TEntity, TKey, TColumnName>`, the entity content service; it creates keyed and single entity slots alike | — |
|
|
1187
|
+
| `createOIMSlotCollectionService({ slotContentService, slotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSlotCollectionService` over any keyed content service (entity or bucket): the read doors, `writeSlotContentByKey`, the batch doors and `columnSlotService`. `slotStoreService` defaults to a Map store and is REQUIRED by type for an array key (`OIMSlotTreeStoreService`) | `createStoreSlot(db)` |
|
|
1188
|
+
| `createOIMSingleSlotService({ slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSingleSlotService` over any content service with `createSingleSlot` (entity or bucket) | `createStoreSlot(db)` |
|
|
1189
|
+
| `createOIMSlotCollectionIndexService({ entityService, columns? \| keyOf?, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMSlotCollectionIndexService`, over the entity collection service whose store slots it will index (it takes a `Pick` of `getSlots`); `slotContentService` defaults to an `OIMSlotContentPositionalArrayBucketService`, pass `new OIMSlotContentSetBucketService()` for an unordered one. A scalar key is inferred from ONE inline column or from `keyOf`'s return; an array key (two or more columns, or an array-returning `keyOf`) needs `bucketSlotStoreService: new OIMSlotTreeStoreService(…)` | `createIndexStoreSlot(entityStoreSlot)` |
|
|
1190
|
+
| `createOIMSlotCollectionSortedArrayIndexService({ entityService, columns? \| keyOf?, sortColumns, … })` | `OIMSlotCollectionSortedArrayIndexService`: the index whose buckets are kept in the order of `sortColumns` (`readonly TOIMSortColumn<TEntity>[]`, required) through an `OIMSlotContentSortedArrayBucketService`. See [Sorted at write](#sorted-at-write) | `createIndexStoreSlot(entityStoreSlot)` |
|
|
1191
|
+
| `createOIMComputedService({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService?, columnNames? })` | `OIMComputedService<TKey, TValue, TComputedStore>`; its reads are on `computedService.computedCollectionService`; `derive` and `createSources` may instead be given per store slot; `computedSlotStoreService` is REQUIRED for a non-Map store (a single: `OIMSingleSlotStoreService`); see [One derivation per key](#one-derivation-per-key) | `createComputedStoreSlot(db, derive?, createSources?)` |
|
|
1192
|
+
| `createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMSortedComputedService` — per key, the entity slots of the index bucket under that key, sorted; see [Sorted lists](#sorted-lists) | `createSortedStoreSlot(indexStoreSlot)` |
|
|
1193
|
+
| `createOIMBucketMemoComputedService({ bucketCollectionService, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMBucketMemoComputedService` — per key, the bucket's slots as one array; see [The order of a bucket](#the-order-of-a-bucket-as-one-derivation-per-key) | `createBucketMemoStoreSlot(bucketStoreSlot)` |
|
|
1244
1194
|
|
|
1245
1195
|
`entityWriter` and `slotReleasePolicy` on these doors default to the
|
|
1246
1196
|
db's, so naming one overrides it for that service alone.
|
|
@@ -1248,18 +1198,17 @@ db's, so naming one overrides it for that service alone.
|
|
|
1248
1198
|
**A DOOR ALWAYS PASSES; IT NEVER LETS A CLASS REACH FOR A GLOBAL.** Every option
|
|
1249
1199
|
a service would otherwise answer with `?? OIMDefaults.x` is in the object its
|
|
1250
1200
|
door hands it, so there is one answer to each question and not two. Pinned in
|
|
1251
|
-
`src/
|
|
1201
|
+
`src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts`,
|
|
1252
1202
|
which also fails when an option arrives on a service and is not forwarded,
|
|
1253
1203
|
when the root mints anything but the channel and the collector, or when a public
|
|
1254
1204
|
member appears on the root that is not one of the seven it owns.
|
|
1255
1205
|
|
|
1256
|
-
**`
|
|
1257
|
-
an `OIMSlotContentBucketServiceArrayDriven` — insertion order, never re-decided — that records an
|
|
1206
|
+
**`new OIMSlotContentPositionalArrayBucketService({ bucketCommands })` is the content service for a list whose order is yours.** It is — insertion order, never re-decided — that records an
|
|
1258
1207
|
`OIMBucketCommands` script per bucket. The recorder holds no subscription: it lists store slots on the channel
|
|
1259
1208
|
slot, and the script lives for one frame.
|
|
1260
1209
|
|
|
1261
1210
|
```ts
|
|
1262
|
-
const freshPageService =
|
|
1211
|
+
const freshPageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
1263
1212
|
const freshPage = freshPageService.createStoreSlot(db);
|
|
1264
1213
|
const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
|
|
1265
1214
|
// freshBucketSlot.commands and freshBucketSlot.isWholeListRecorded carry the script; read them in an effect on the bucket slot
|
|
@@ -1269,56 +1218,41 @@ const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
|
|
|
1269
1218
|
|
|
1270
1219
|
## `OIMDefaults` — where an optional dependency comes from
|
|
1271
1220
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1221
|
+
`OIMDefaults` is an instance holding the strategies factories fall back on when their own options leave
|
|
1222
|
+
them out. Build one with what you want shared and pass it as `defaults` to every factory that should use
|
|
1223
|
+
it; a factory given none uses `OIMDefaults.INSTANCE`, which is empty. A field left undefined falls back to
|
|
1224
|
+
the built-in default. Nothing is global and nothing can be replaced after the fact.
|
|
1275
1225
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
**NOTHING IS CONSTRUCTED AT MODULE LOAD.** Every entry is an `OIMDefaultValue`
|
|
1289
|
-
whose `getValue()` mints on its first call (`??=`) and hands the same instance back
|
|
1290
|
-
for the rest of the process. A consumer that never reaches for a key tree walk never
|
|
1291
|
-
builds one.
|
|
1292
|
-
|
|
1293
|
-
Each has a matching `setValue(value)`, which is how a process replaces a default
|
|
1294
|
-
everywhere at once:
|
|
1226
|
+
```ts signature
|
|
1227
|
+
new OIMDefaults({
|
|
1228
|
+
slotStamp?: IOIMSlotStamp<TOIMSlot>; // none
|
|
1229
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // none: a slot with a value stays
|
|
1230
|
+
entityWriter?: IOIMEntityWriter; // createOIMInPlaceEntityWriter()
|
|
1231
|
+
entitySlotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot>; // "empty means slot.entity === undefined"
|
|
1232
|
+
indexKeysService?: IOIMIndexKeysService; // OIMAppendingIndexKeysService
|
|
1233
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // a shared map-driven store
|
|
1234
|
+
computedColumnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // a shared map-driven store
|
|
1235
|
+
storeSlotListener?: IOIMStoreSlotListener; // OIMNoStoreSlotListener: hears nothing
|
|
1236
|
+
}?) // TOIMDefaultsOptions
|
|
1237
|
+
```
|
|
1295
1238
|
|
|
1296
1239
|
```ts
|
|
1297
|
-
OIMDefaults
|
|
1240
|
+
const replacingDefaults = new OIMDefaults({ entityWriter: createOIMReplacingEntityWriter() });
|
|
1241
|
+
const replacingCardService = createOIMSlotCollectionService({
|
|
1242
|
+
defaults: replacingDefaults,
|
|
1243
|
+
slotContentService: createOIMSlotContentEntityService<Card>({ defaults: replacingDefaults }),
|
|
1244
|
+
});
|
|
1298
1245
|
```
|
|
1299
1246
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
the first read. Each property is tracked on its own, so calling
|
|
1310
|
-
`OIMDefaults.COLUMN_SLOT_STORE_SERVICE.getValue()` does not close
|
|
1311
|
-
`OIMDefaults.KEY_TREE_WALK_SCALAR`. **Set every default you intend to replace at
|
|
1312
|
-
startup**, before you construct an `OIMDB` or a service. Pinned by
|
|
1313
|
-
`packages/core/src/factories/tests/what-a-default-replaced-too-late-does.test.ts`.
|
|
1314
|
-
|
|
1315
|
-
**Each default is its own module, so reaching one retains only its arm.** Every
|
|
1316
|
-
default lives in its own file under `packages/core/src/factories/defaults/` with its
|
|
1317
|
-
own `OIMDefaultValue`; a service reads the modules it needs and
|
|
1318
|
-
neither key tree walk enters its bundle. `OIMDefaults` itself delegates to all of
|
|
1319
|
-
them, so importing `OIMDefaults` retains every arm.
|
|
1320
|
-
`packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one
|
|
1321
|
-
door to its own arm, and the defaults class to all of them"*.
|
|
1247
|
+
An option given to a factory wins over `defaults`; `defaults` wins over the built-in default. A tool that
|
|
1248
|
+
wants to hear every store slot (devtools' `oimDevDefaults`) is a `defaults` with a `storeSlotListener`.
|
|
1249
|
+
|
|
1250
|
+
**Each built-in default is its own module, so reaching one retains only its arm.** Every built-in default
|
|
1251
|
+
lives in its own file under `packages/core/src/db/constants/` as an `OIMDefaultValue` whose `getValue()`
|
|
1252
|
+
builds it on the first call and hands the same instance back after; a factory reads the modules it needs.
|
|
1253
|
+
`OIMDefaults` imports none of them, so importing it retains no arm.
|
|
1254
|
+
`packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one door to its own arm, and
|
|
1255
|
+
the defaults class to none of them"*.
|
|
1322
1256
|
|
|
1323
1257
|
---
|
|
1324
1258
|
|
|
@@ -1330,66 +1264,65 @@ with its slot owner; the collection service (`OIMSlotCollectionService`) over it
|
|
|
1330
1264
|
Neither holds an entity: each `createStoreSlot(db)` is one instance of that type's store.
|
|
1331
1265
|
|
|
1332
1266
|
```ts signature
|
|
1333
|
-
createOIMSlotContentEntityService<TEntity,
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1267
|
+
createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({
|
|
1268
|
+
entityWriter?: IOIMEntityWriter; // defaults.entityWriter, then the in-place writer
|
|
1269
|
+
slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; // defaults.entitySlotEmptinessPolicy, then "entity is undefined"
|
|
1270
|
+
}?): OIMSlotContentEntityService<TEntity, TKey, TColumnName>
|
|
1271
|
+
|
|
1272
|
+
createOIMSlotCollectionService({
|
|
1273
|
+
slotContentService,
|
|
1274
|
+
slotStoreService?: IOIMSlotStoreService<TKey, TSlot, TStore>; // Map; REQUIRED for an array key
|
|
1275
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // defaults.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
|
|
1276
|
+
slotSubscriptionListener?: IOIMSlotSubscriptionListener; // OIMNoSlotSubscriptionListener
|
|
1277
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // defaults.columnSlotStoreService, then a shared map store
|
|
1341
1278
|
columnNames?: readonly TColumnName[]; // the ordinal column store over those names
|
|
1342
|
-
}):
|
|
1343
|
-
|
|
1344
|
-
createOIMSlotCollectionService({ slotContentService }): OIMSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, …>
|
|
1345
|
-
collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>>
|
|
1346
|
-
|
|
1347
|
-
createOIMEntityService<TEntity, TColumnName?, TKey?>(db, { keyOf, … }) // convenience: both calls, Map store
|
|
1279
|
+
}): OIMWritableSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, TEntity, …>
|
|
1280
|
+
collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>, typeof collectionService>
|
|
1348
1281
|
```
|
|
1349
1282
|
|
|
1350
1283
|
**Each collection type has its own column owner.** It is stateless configuration built with the slot owner
|
|
1351
1284
|
over `columnSlotStoreService` (or the ordinal store `columnNames` names); the column state — the column slots,
|
|
1352
1285
|
the column groups, and which columns are referenced — lives on the store slot. The doors are on the
|
|
1353
|
-
collection
|
|
1286
|
+
collection's `columnSlotService` (`getColumn`, `holdColumn`, `unholdColumn`, `getColumnGroup`, `holdColumnGroup`,
|
|
1354
1287
|
`getAnyColumn`, `holdAnyColumn`). The column owner has two strategies, chosen by the factory: write-driven
|
|
1355
|
-
column slots, woken by the marks a write makes (`
|
|
1356
|
-
derive-driven column slots, each a computed slot over its carrier (`
|
|
1288
|
+
column slots, woken by the marks a write makes (`OIMPushColumnSlotOwner`, entities and buckets), and
|
|
1289
|
+
derive-driven column slots, each a computed slot over its carrier (`OIMPullColumnSlotOwner`, computeds).
|
|
1357
1290
|
|
|
1358
1291
|
Example, over the `db`, `cardService`, `cards` and `slot` of the setup block:
|
|
1359
1292
|
|
|
1360
1293
|
```ts
|
|
1361
|
-
const effect = effectService.createEffect(db,
|
|
1294
|
+
const effect = effectService.createEffect(db, () => { console.log(slot.entity?.title); }, [slot]);
|
|
1362
1295
|
cardService.slotContentService.updateEntity(slot, { title: 'Ace of Spades' });
|
|
1363
1296
|
db.flush();
|
|
1364
1297
|
effectService.disposeEffect(effect);
|
|
1365
1298
|
```
|
|
1366
1299
|
|
|
1367
|
-
Assembled
|
|
1300
|
+
Assembled with a store service of your own:
|
|
1368
1301
|
|
|
1369
1302
|
```ts
|
|
1370
1303
|
const baseCardService = createOIMSlotCollectionService({
|
|
1371
|
-
slotContentService: createOIMSlotContentEntityService<Card
|
|
1304
|
+
slotContentService: createOIMSlotContentEntityService<Card, string>(),
|
|
1305
|
+
slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, string>>(),
|
|
1372
1306
|
});
|
|
1373
1307
|
const baseCards = baseCardService.createStoreSlot(db);
|
|
1374
1308
|
```
|
|
1375
1309
|
|
|
1376
1310
|
The writer is omitted because the db and `OIMDefaults` supply it, and so is the column store. The slot stamp
|
|
1377
|
-
is not an option of
|
|
1311
|
+
is not an option of any factory: it is `new OIMDefaults({ slotStamp })`, passed as `defaults`.
|
|
1378
1312
|
|
|
1379
1313
|
### Public properties
|
|
1380
1314
|
|
|
1381
1315
|
| Property | Of | Type |
|
|
1382
1316
|
|---|---|---|
|
|
1383
|
-
| `slotContentService` | the collection service | the content service: the write doors |
|
|
1384
|
-
| `
|
|
1385
|
-
| `
|
|
1386
|
-
| `slotStoreService` | the entity content service | the store service |
|
|
1317
|
+
| `slotContentService` | the collection service | the content service: the write doors on a slot |
|
|
1318
|
+
| `columnSlotService` | the collection service | `OIMColumnSlotService`: the column doors |
|
|
1319
|
+
| `slotService` | the store slot | the service that created it |
|
|
1387
1320
|
|
|
1388
1321
|
A store slot is not a slot, so nothing holds, subscribes or wakes it: there is no
|
|
1389
1322
|
`effectService.createEffect(db, [cards], …)` that hears the population. "Something in this population changed" is an
|
|
1390
1323
|
index over the store slot and a subscription to its bucket — the bucket's door
|
|
1391
1324
|
is written when an entity slot joins or leaves it — or a content-change listener on the store slot
|
|
1392
|
-
(`
|
|
1325
|
+
(`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames)`,
|
|
1393
1326
|
removed by the same listener object). The slot owner is what `slot.owner` points at for every slot the type
|
|
1394
1327
|
filed, in every one of its store slots; `slot.storeSlot` names which store slot.
|
|
1395
1328
|
|
|
@@ -1407,7 +1340,7 @@ for (const filedSlot of cardService.getSlots(cards)) {
|
|
|
1407
1340
|
```
|
|
1408
1341
|
|
|
1409
1342
|
To have that answer MAINTAINED and subscribable instead of walked, declare an
|
|
1410
|
-
index keyed on the entity's own id (`keyOf: card => card.id`) and hold the key
|
|
1343
|
+
index keyed on the entity's own id (`keyOf: (card: Card) => card.id`) and hold the key
|
|
1411
1344
|
you want. An index's bucket is an injected lifetime — a
|
|
1412
1345
|
`slotReleasePolicy` — so it is the consumer who says how long the
|
|
1413
1346
|
answer outlives its last reader.
|
|
@@ -1416,13 +1349,12 @@ An index maintains buckets only while they are held, filing entity slots from th
|
|
|
1416
1349
|
store slot on every write by the entity's key value. The bucket is created on first
|
|
1417
1350
|
`holdSlotByKey` by walking the source store slot's slots once, and released on the last give-back.
|
|
1418
1351
|
|
|
1419
|
-
**`holdColumn(slot, columnName)`** is on the collection
|
|
1420
|
-
`service.unholdColumn(columnSlot)` gives that hold back. **`service.getColumn(slot, columnName)`**
|
|
1352
|
+
**`holdColumn(slot, columnName)`** is on the collection's `columnSlotService` and takes a hold on a column slot;
|
|
1353
|
+
`service.columnSlotService.unholdColumn(columnSlot)` gives that hold back. **`service.columnSlotService.getColumn(slot, columnName)`**
|
|
1421
1354
|
returns the same column slot without a hold: a column slot it mints is held by the
|
|
1422
1355
|
unused-slot collector until the end of the next flush, so `getColumn` is
|
|
1423
|
-
the door that does not hold but tracks. `
|
|
1424
|
-
itself
|
|
1425
|
-
and leaves no slot behind.
|
|
1356
|
+
the door that does not hold but tracks. `findSlotByKey(storeSlot, key)?.entity` reads the entity
|
|
1357
|
+
itself, so it mints nothing: a miss answers `undefined` and leaves no slot behind.
|
|
1426
1358
|
|
|
1427
1359
|
### The doors every service has
|
|
1428
1360
|
|
|
@@ -1443,8 +1375,15 @@ class OIMSlotCollectionService<TKey, TSlot, TStore, TSlotContentService, TColumn
|
|
|
1443
1375
|
getSlots(storeSlot): IterableIterator<TSlot>;
|
|
1444
1376
|
getSlotCount(storeSlot): number;
|
|
1445
1377
|
isSameKey(key, otherKey): boolean;
|
|
1446
|
-
|
|
1378
|
+
openBatch(storeSlot): void;
|
|
1379
|
+
closeBatch(storeSlot): void;
|
|
1380
|
+
writeInBatch<TResult>(storeSlot, write: () => TResult): TResult;
|
|
1381
|
+
readonly columnSlotService: OIMColumnSlotService; // getColumn / holdColumn / getAnyColumn / holdAnyColumn / getColumnGroup / holdColumnGroup / unholdColumn
|
|
1447
1382
|
}
|
|
1383
|
+
// createOIMSlotCollectionService returns OIMWritableSlotCollectionService, which adds:
|
|
1384
|
+
// writeSlotContentByKey(storeSlot, key, content, changedSlots?): TSlot
|
|
1385
|
+
// createOIMSingleSlotService returns OIMWritableSingleSlotService, which adds:
|
|
1386
|
+
// writeSingleSlotContent(storeSlot, content, changedSlots?): TSlot; clearSingleSlotContent(storeSlot): void
|
|
1448
1387
|
```
|
|
1449
1388
|
|
|
1450
1389
|
| Door | Of | Signature | Mints? |
|
|
@@ -1456,13 +1395,10 @@ class OIMSlotCollectionService<TKey, TSlot, TStore, TSlotContentService, TColumn
|
|
|
1456
1395
|
| `holdSlotByKey(storeSlot, key)` | collection | `TSlot` — minted if missing, `holdCount` up by one | **yes** |
|
|
1457
1396
|
| `hasSlotContentByKey(storeSlot, key)` | collection | `boolean` — **does this key have content right now**, not whether an identity is filed under it: a slot a read or a hold minted and nobody wrote answers `false`. It hands back no slot, which is why it can answer without minting, and it is not a read: the answer is about the past the moment it returns | no |
|
|
1458
1397
|
| `isSameKey(key, otherKey)` | collection | `boolean` — whether two keys name one slot here, answered by the store service (SameValueZero for a Map store, part by part for a tree store) | no |
|
|
1459
|
-
| `
|
|
1460
|
-
| `
|
|
1461
|
-
| `
|
|
1462
|
-
| `deleteEntity(slot)` / `deleteEntities(storeSlot, slots)` / `clearBucketMemberSlots(bucketSlot)` | content | `void` — it takes the value, not the identity | no |
|
|
1463
|
-
| `clearAllEntities(storeSlot)` | entity content | `void` — clears every slot filed in that store slot | no |
|
|
1398
|
+
| `writeSlotContentByKey(storeSlot, key, content, changed?)` | collection | `TSlot` — **total; always the identity it resolved or minted**. A hit writes the whole value through `slotContentService.writeSlotContent`, a miss mints and `insertSlotContent`s | **yes** |
|
|
1399
|
+
| `writeSlotContent(slot, content)` | content | `boolean` — on a slot you already hold; the boolean is about the CONTENT | no |
|
|
1400
|
+
| `clearSlotContent(slot)` | content | `void` — it takes the value, not the identity | no |
|
|
1464
1401
|
| `markContentChanged(bucketSlot)` | bucket content | `void` — re-settle emptiness and re-ask the guard after a write you made another way; the entity counterpart is `markEntityChanged(slot, changedColumns?)` | no |
|
|
1465
|
-
| `fillEntities(storeSlot, keys, entities)` / `insertEntities(storeSlot, entities)` | entity content | `void` — a bulk load of new keys; hands out no reference. On a key that is already filed or named twice it throws and rolls back only the entities this call filed; `fillEntities` also requires an empty store slot, and throws on lists of different lengths | yes |
|
|
1466
1402
|
| `slot.owner.holdSlot(slot)` / `slot.owner.unholdSlot(slot)` | `void` — the `IOIMSlotOwner` pair; `unholdSlot` on an unheld slot throws by name | no |
|
|
1467
1403
|
| `slot.owner.releaseSlotIfUnheld(slot)` | `void` — run the release guard; it releases nothing unless the slot is unheld, unreferenced, still owned here and out of grace | no |
|
|
1468
1404
|
|
|
@@ -1471,10 +1407,10 @@ class OIMSlotCollectionService<TKey, TSlot, TStore, TSlotContentService, TColumn
|
|
|
1471
1407
|
owned or in-grace slot it returns having done nothing, and on an
|
|
1472
1408
|
already-released one it returns silently rather than throwing.
|
|
1473
1409
|
|
|
1474
|
-
**`
|
|
1410
|
+
**`writeSlotContentByKey` does not file a key whose content is refused.** On a MISS it
|
|
1475
1411
|
mints, writes inside a `try`, and on a throw runs the release guard on what it
|
|
1476
1412
|
just minted before re-raising — so the caller's error is the only thing that
|
|
1477
|
-
leaves.
|
|
1413
|
+
leaves. The HIT path looks
|
|
1478
1414
|
the key up first and never mints, so a refused write on an existing key leaves
|
|
1479
1415
|
that key exactly as it was.
|
|
1480
1416
|
|
|
@@ -1482,21 +1418,18 @@ that key exactly as it was.
|
|
|
1482
1418
|
|
|
1483
1419
|
| | What happened | What the caller gets |
|
|
1484
1420
|
|---|---|---|
|
|
1485
|
-
| **Refused** | The write threw; **no content ever arrived** | The throw. A key minted for it is unwound — `
|
|
1421
|
+
| **Refused** | The write threw; **no content ever arrived** | The throw. A key minted for it is unwound — `writeSlotContentByKey` files nothing |
|
|
1486
1422
|
| **Unchanged** | The write **did** happen and the content was already there | The identity, like any other write. Nothing is unwound |
|
|
1487
1423
|
|
|
1488
|
-
`
|
|
1424
|
+
`writeSlotContentByKey` is **total**: whatever it decides about the content, it hands back
|
|
1489
1425
|
the identity it resolved or minted. A door that mints must leave a reference
|
|
1490
1426
|
behind, which is the one test that admits a door at all.
|
|
1491
1427
|
|
|
1492
|
-
`fillEntities` is the companion and does not carry the rule: it returns `void` and hands
|
|
1493
|
-
out no reference at all.
|
|
1494
|
-
|
|
1495
1428
|
**`changed?` — the verdict, into an array you own.**
|
|
1496
1429
|
|
|
1497
1430
|
```ts
|
|
1498
1431
|
const changed: TOIMEntitySlot<Card, TOIMStoreKey>[] = [];
|
|
1499
|
-
cardService.
|
|
1432
|
+
cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' }, changed); // pushed exactly when the content differed
|
|
1500
1433
|
```
|
|
1501
1434
|
|
|
1502
1435
|
- **Optional, yours, and appended to** — never read by the engine, never cleared.
|
|
@@ -1514,49 +1447,45 @@ cardService.slotContentService.insertEntity(cards, 'c1', { id: 'c1', title: 'Ace
|
|
|
1514
1447
|
|
|
1515
1448
|
## Writing
|
|
1516
1449
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1450
|
+
The keyed write is on the collection; every other write is on its content service, `service.slotContentService`,
|
|
1451
|
+
and takes the slot alone. There is no `keyOf`: the caller names the key. There are no bulk writes: a load is a
|
|
1452
|
+
loop of single writes inside `service.writeInBatch(storeSlot, () => { … })`.
|
|
1520
1453
|
|
|
1521
1454
|
| Call | Signature |
|
|
1522
1455
|
|---|---|
|
|
1523
|
-
| `
|
|
1524
|
-
| `
|
|
1525
|
-
| `insertEntity(storeSlot, key, entity, changedEntitySlots?)` | `(storeSlot, TKey, TEntity, TOIMEntitySlot<TEntity, TKey>[]?) => TOIMEntitySlot<TEntity, TKey>` — **total**, under a key you name |
|
|
1526
|
-
| `insertEntities(storeSlot, entities)` | `(storeSlot, readonly TEntity[]) => void` — a bulk load of NEW keys. A key already filed, or named twice in the list, throws and rolls back only the entities this call filed; what was in the store slot before stays. No `changed` — a bulk load changes every entity |
|
|
1527
|
-
| `fillEntities(storeSlot, keys, entities)` | `(storeSlot, readonly TKey[], readonly TEntity[]) => void` — the same under keys you name, with the same rollback; it also **throws unless the store slot is empty** |
|
|
1456
|
+
| `service.writeSlotContentByKey(storeSlot, key, entity, changedEntitySlots?)` | `(storeSlot, TKey, TEntity, TOIMEntitySlot<TEntity, TKey>[]?) => TOIMEntitySlot<TEntity, TKey>` — **total**: a miss mints and fills, a hit writes the whole value |
|
|
1457
|
+
| `writeSlotContent(slot, entity)` | `(slot, TEntity) => boolean` — the whole value onto a slot you hold; columns it had and `entity` does not, go |
|
|
1528
1458
|
| `updateEntity(slot, patch)` | `(slot, Partial<TEntity>) => slot` |
|
|
1529
1459
|
| `updateEntityColumn(slot, columnName, value)` | `<C extends keyof TEntity & string>(slot, C, TEntity[C]) => slot` — one column, no patch object and no walk over it |
|
|
1530
|
-
| `
|
|
1531
|
-
| `
|
|
1532
|
-
| `clearAllEntities(storeSlot)` | `(storeSlot) => void` — clears every filed slot inside one subframe, so the whole clear is one frame, not one per slot |
|
|
1533
|
-
| `markEntityChanged(slot, changedColumns?)` | `(TOIMEntitySlot<TEntity, TKey>, readonly (keyof TEntity & string)[]?) => void` |
|
|
1460
|
+
| `clearSlotContent(slot)` | `(slot) => void` — the entity goes; the identity stays while anything refers to it |
|
|
1461
|
+
| `markEntityChanged(slot, changedColumns?)` | `(slot, readonly (keyof TEntity & string)[]?) => void` |
|
|
1534
1462
|
| `deleteEntityColumns(slot, columnNames)` | `(slot, readonly TOIMEntityColumnName<TEntity>[]) => slot` — the named columns go |
|
|
1535
|
-
| `
|
|
1536
|
-
| `
|
|
1463
|
+
| `service.writeInBatch(storeSlot, write)` | `<TResult>(storeSlot, () => TResult) => TResult` — an index on the collection hears the whole batch in one call |
|
|
1464
|
+
| `service.findSlotByKey(storeSlot, key)?.entity` | the entity alone; mints nothing |
|
|
1537
1465
|
|
|
1538
1466
|
- **The store keeps your object by default.** Under the default `oimInsertEntityWithoutCopy` the
|
|
1539
1467
|
slot holds the object you hand in, so with the in-place writer a later update edits that
|
|
1540
|
-
same object; pass `
|
|
1468
|
+
same object; pass `entityInserter: oimInsertEntityWithCopy` to store `{ ...entity }`
|
|
1541
1469
|
and leave your object untouched. After an insert, change the object only through the
|
|
1542
1470
|
content service (`updateEntity`, or `markEntityChanged` after a hand edit): editing it and calling
|
|
1543
|
-
`
|
|
1471
|
+
`writeSlotContent` with the same object again wakes nobody, because no column reads as changed. `oimInsertEntityWithSanitizedCopy` also removes an own
|
|
1544
1472
|
`__proto__` key: pick it for untrusted JSON.
|
|
1545
1473
|
- `updateEntity` takes a **patch** — named columns move, unnamed ones stay.
|
|
1546
|
-
`
|
|
1474
|
+
`writeSlotContentByKey` and `writeSlotContent` take a **whole value**, and a column the new value does not name
|
|
1547
1475
|
is removed from the entity.
|
|
1548
1476
|
- A write that moves no column wakes nobody.
|
|
1549
|
-
- **`
|
|
1550
|
-
|
|
1551
|
-
- **`changedEntitySlots?` is the same array on both**: optional, yours, appended with the
|
|
1477
|
+
- **`writeSlotContentByKey` always hands back the slot**, whatever the write changed.
|
|
1478
|
+
- **`changedEntitySlots?`**: optional, yours, appended with the
|
|
1552
1479
|
entity slots whose content was different from what they already held. One array reused
|
|
1553
|
-
across a whole
|
|
1480
|
+
across a whole batch is the intended use, so a
|
|
1554
1481
|
pass over its slots costs the entities that changed rather than the entities that
|
|
1555
1482
|
arrived. It is never cleared — you empty it.
|
|
1556
1483
|
|
|
1557
1484
|
```ts
|
|
1558
1485
|
const changed: TOIMEntitySlot<Card, TOIMStoreKey>[] = [];
|
|
1559
|
-
cardService.
|
|
1486
|
+
cardService.writeInBatch(cards, () => {
|
|
1487
|
+
for (const card of payload) cardService.writeSlotContentByKey(cards, card.id, card, changed);
|
|
1488
|
+
});
|
|
1560
1489
|
for (const slot of changed) reindex(slot); // the re-sent entities are not in here
|
|
1561
1490
|
changed.length = 0;
|
|
1562
1491
|
```
|
|
@@ -1584,7 +1513,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1584
1513
|
- **The hand mutation already compiles.** `TOIMEntitySlot` declares
|
|
1585
1514
|
`readonly entity: TEntity | undefined`, and `readonly` forbids REPLACING the
|
|
1586
1515
|
object, not writing its columns — `entity.title = 'two'` needs no cast, only
|
|
1587
|
-
the usual narrowing of `| undefined`. `
|
|
1516
|
+
the usual narrowing of `| undefined`. `TOIMEditable<TSlot>` is what strips
|
|
1588
1517
|
`readonly` from the field itself, which is what assigning `slot.entity` needs.
|
|
1589
1518
|
- **With a list, only the named columns wake. Without it, every watched column
|
|
1590
1519
|
does** — the column owner walks the slot's whole column store, and a
|
|
@@ -1593,13 +1522,13 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1593
1522
|
what the store holds. **Pass the list.** The no-argument form is the convenient
|
|
1594
1523
|
one and it gives back the per-column walk this door exists to skip.
|
|
1595
1524
|
- **An empty list wakes nothing at all** — not the columns, not the entity slot, not the
|
|
1596
|
-
store slot's listeners. `[]` is the caller saying nothing moved, so it returns before
|
|
1597
|
-
|
|
1525
|
+
store slot's listeners. `[]` is the caller saying nothing moved, so it returns before
|
|
1526
|
+
anything is marked: a write that changes nothing wakes nobody here too.
|
|
1598
1527
|
- **With either of the other two it wakes the entity slot and the store slot's content-change listeners**, so an
|
|
1599
1528
|
effect on the slot itself and an index over the store slot both see a hand mutation.
|
|
1600
1529
|
- **It re-stamps emptiness** whenever emptiness can depend on the mutation — with no column list,
|
|
1601
|
-
on a slot whose `entity` is `undefined`, and on a content service with a `
|
|
1602
|
-
`
|
|
1530
|
+
on a slot whose `entity` is `undefined`, and on a content service with a `slotEmptinessPolicy` other than
|
|
1531
|
+
"the entity is `undefined`", a release policy that releases written slots, or a db with a
|
|
1603
1532
|
`slotStamp` — so a hand mutation that emptied the entity leaves `oimIsSlotEmpty(slot)`
|
|
1604
1533
|
true rather than lying, and the release guard can take the identity when it should.
|
|
1605
1534
|
- **It refuses inside a frame**, through the same `assertWritable` →
|
|
@@ -1615,33 +1544,33 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1615
1544
|
A **writer** is the operations that move an entity's columns, and nothing
|
|
1616
1545
|
else: it reaches no channel, takes no holds and knows nothing about lifetime.
|
|
1617
1546
|
Both strategies ship as factories; the fork is chosen once, as the content service's `entityWriter`
|
|
1618
|
-
(or for every
|
|
1547
|
+
(or for every factory given it, `new OIMDefaults({ entityWriter })` as `defaults`).
|
|
1619
1548
|
|
|
1620
1549
|
```ts signature
|
|
1621
1550
|
interface IOIMEntityWriter {
|
|
1622
|
-
readonly fillEntity: (slot:
|
|
1623
|
-
readonly putEntity: (slot:
|
|
1624
|
-
readonly patchEntity: (slot:
|
|
1551
|
+
readonly fillEntity: (slot: TOIMEntityContentSlot, entity: object) => void;
|
|
1552
|
+
readonly putEntity: (slot: TOIMEntityContentSlot, entity: object, changedColumnNames: string[] | undefined) => boolean;
|
|
1553
|
+
readonly patchEntity: (slot: TOIMEntityContentSlot, patch: object, changedColumnNames: string[] | undefined) => boolean;
|
|
1625
1554
|
readonly patchColumn: (
|
|
1626
|
-
slot:
|
|
1555
|
+
slot: TOIMEntityContentSlot, columnName: string, value: unknown, changedColumnNames: string[] | undefined,
|
|
1627
1556
|
) => boolean;
|
|
1628
|
-
readonly deleteColumns: (slot:
|
|
1629
|
-
readonly clearEntity: (slot:
|
|
1557
|
+
readonly deleteColumns: (slot: TOIMEntityContentSlot, names: readonly string[]) => readonly string[];
|
|
1558
|
+
readonly clearEntity: (slot: TOIMEntityContentSlot) => void;
|
|
1630
1559
|
}
|
|
1631
1560
|
|
|
1632
1561
|
type TOIMEntityWriterOptions = {
|
|
1633
1562
|
sameValueGuard?: TOIMSameValueGuard;
|
|
1634
|
-
|
|
1635
|
-
putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns |
|
|
1636
|
-
patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns |
|
|
1637
|
-
patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn |
|
|
1563
|
+
entityInserter?: TOIMEntityInserter;
|
|
1564
|
+
putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimGuardedPutEntityColumns
|
|
1565
|
+
patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimGuardedPatchEntityColumns
|
|
1566
|
+
patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimGuardedPatchEntityColumn
|
|
1638
1567
|
};
|
|
1639
1568
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1569
|
+
createOIMInPlaceEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // stored object keeps its identity
|
|
1570
|
+
createOIMReplacingEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
|
|
1642
1571
|
```
|
|
1643
1572
|
|
|
1644
|
-
`
|
|
1573
|
+
`entityInserter` is the first-write strategy: `oimInsertEntityWithoutCopy` (the default) stores
|
|
1645
1574
|
your object itself, `oimInsertEntityWithCopy` stores `{ ...entity }`, `oimInsertEntityWithSanitizedCopy`
|
|
1646
1575
|
stores a copy with an own `__proto__` key removed (for untrusted JSON).
|
|
1647
1576
|
|
|
@@ -1661,14 +1590,14 @@ per-column comparison that decides "moved".
|
|
|
1661
1590
|
|
|
1662
1591
|
`putEntity` is PUT, `patchEntity` is PATCH (HTTP semantics). PUT makes the stored entity
|
|
1663
1592
|
the value handed in — changed columns written, columns the value lacks removed — in ONE
|
|
1664
|
-
call, returning both sets of names; PATCH writes only the columns named. `
|
|
1665
|
-
of a key that already has a value and `slotContentService.
|
|
1593
|
+
call, returning both sets of names; PATCH writes only the columns named. `writeSlotContentByKey`
|
|
1594
|
+
of a key that already has a value and `slotContentService.writeSlotContent(slot, entity)` are PUT: a whole-entity
|
|
1666
1595
|
write that drops a column is ONE channel write of the slot, and the replacing writer
|
|
1667
1596
|
builds ONE new object.
|
|
1668
1597
|
|
|
1669
|
-
Pick `
|
|
1598
|
+
Pick `createOIMReplacingEntityWriter` when something downstream compares by
|
|
1670
1599
|
reference — a React `useSyncExternalStore` snapshot, a memo, a shallow-equal
|
|
1671
|
-
selector. Under `
|
|
1600
|
+
selector. Under `createOIMInPlaceEntityWriter` the entity reference never moves,
|
|
1672
1601
|
so a value comparison never fires and a binding needs a counter bumped inside its
|
|
1673
1602
|
handler. Replacing allocates one object per changed write; in-place allocates
|
|
1674
1603
|
none.
|
|
@@ -1680,9 +1609,9 @@ replacing — so the entity keeps its hidden class, and the key stays in
|
|
|
1680
1609
|
leaves it out does not report it, and taking the entity away does not wake its
|
|
1681
1610
|
readers.
|
|
1682
1611
|
|
|
1683
|
-
**Neither strategy reports anything.** Reporting is `OIMSlotContentEntityService`'s
|
|
1684
|
-
|
|
1685
|
-
|
|
1612
|
+
**Neither strategy reports anything.** Reporting is `OIMSlotContentEntityService`'s:
|
|
1613
|
+
after the writer returns, it marks the slot on the channel and tells the store slot's content-change listeners
|
|
1614
|
+
once for the whole write.
|
|
1686
1615
|
|
|
1687
1616
|
**A writer of your own** reuses the column walks both shipped writers run:
|
|
1688
1617
|
|
|
@@ -1693,10 +1622,10 @@ oimPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, c
|
|
|
1693
1622
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1694
1623
|
oimPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
|
|
1695
1624
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1696
|
-
|
|
1625
|
+
oimGuardedPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
|
|
1697
1626
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1698
|
-
|
|
1699
|
-
|
|
1627
|
+
oimGuardedPatchEntityColumns(slot, patch: object, sameValueGuard, changedColumnNames, targetOf) // same signature as the unguarded walk
|
|
1628
|
+
oimGuardedPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, changedColumnNames, targetOf)
|
|
1700
1629
|
```
|
|
1701
1630
|
|
|
1702
1631
|
Each compares every column through `sameValueGuard`, writes the moved ones into
|
|
@@ -1709,26 +1638,26 @@ ships two arms:
|
|
|
1709
1638
|
|
|
1710
1639
|
| Option | Default (unguarded) | Guarded arm |
|
|
1711
1640
|
|---|---|---|
|
|
1712
|
-
| `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `
|
|
1713
|
-
| `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `
|
|
1714
|
-
| `patchEntityColumn` | `oimPatchEntityColumn` | `
|
|
1641
|
+
| `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `oimGuardedPutEntityColumns` — skips both |
|
|
1642
|
+
| `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `oimGuardedPatchEntityColumns` — skips a `__proto__` key and a key the patch does not own |
|
|
1643
|
+
| `patchEntityColumn` | `oimPatchEntityColumn` | `oimGuardedPatchEntityColumn` — refuses the column name `__proto__` |
|
|
1715
1644
|
|
|
1716
1645
|
Take the guarded arms, at a cost on every write, for values that can carry a `__proto__` key or are class
|
|
1717
|
-
instances — `
|
|
1718
|
-
patchEntityColumns:
|
|
1719
|
-
same options on `
|
|
1646
|
+
instances — `createOIMInPlaceEntityWriter({ putEntityColumns: oimGuardedPutEntityColumns,
|
|
1647
|
+
patchEntityColumns: oimGuardedPatchEntityColumns, patchEntityColumn: oimGuardedPatchEntityColumn })`, the
|
|
1648
|
+
same options on `createOIMReplacingEntityWriter`. The writer decides only where the result goes — this one
|
|
1720
1649
|
stores every value frozen:
|
|
1721
1650
|
|
|
1722
1651
|
```ts
|
|
1723
1652
|
const copyColumns = (entity: Record<string, unknown>): Record<string, unknown> => ({ ...entity });
|
|
1724
1653
|
|
|
1725
|
-
function storeFrozen(slot:
|
|
1654
|
+
function storeFrozen(slot: TOIMEntityContentSlot, next: Record<string, unknown> | undefined): boolean {
|
|
1726
1655
|
if (next === undefined) return false;
|
|
1727
|
-
(slot as
|
|
1656
|
+
(slot as TOIMEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
|
|
1728
1657
|
return true;
|
|
1729
1658
|
}
|
|
1730
1659
|
|
|
1731
|
-
const replacingWriter =
|
|
1660
|
+
const replacingWriter = createOIMReplacingEntityWriter();
|
|
1732
1661
|
|
|
1733
1662
|
const freezingWriter: IOIMEntityWriter = {
|
|
1734
1663
|
fillEntity: (slot, entity) => { storeFrozen(slot, copyColumns(entity as Record<string, unknown>)); },
|
|
@@ -1752,30 +1681,25 @@ const freezingWriter: IOIMEntityWriter = {
|
|
|
1752
1681
|
## Single entities
|
|
1753
1682
|
|
|
1754
1683
|
A single entity — one value under no key, such as settings or a session — is a store slot of an
|
|
1755
|
-
`OIMSingleSlotService` over an entity content service
|
|
1756
|
-
|
|
1757
|
-
by-key doors: its read doors name the one slot.
|
|
1684
|
+
`OIMSingleSlotService` over an entity content service; its store service is `OIMSingleSlotStoreService.INSTANCE`,
|
|
1685
|
+
which files at most one slot per store slot. It has no key and no by-key doors: its doors name the one slot.
|
|
1758
1686
|
|
|
1759
1687
|
```ts signature
|
|
1760
|
-
createOIMSingleEntityService<TEntity, TColumnName?>(db, {
|
|
1761
|
-
entityWriter?, slotEmptiness?, slotReleasePolicy?, columnSlotStoreService?, columnNames?,
|
|
1762
|
-
}?): OIMSingleSlotService<TOIMEntitySlot<TEntity, void>, OIMSlotContentEntityService<TEntity, void, TOIMSingleSlotStore<…>, TColumnName>, …>
|
|
1763
|
-
|
|
1764
|
-
// the base layer it assembles
|
|
1765
1688
|
createOIMSingleSlotService({
|
|
1766
|
-
slotContentService: createOIMSlotContentEntityService<TEntity>(
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
})
|
|
1689
|
+
slotContentService: createOIMSlotContentEntityService<TEntity>(),
|
|
1690
|
+
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
1691
|
+
}): OIMWritableSingleSlotService<TOIMEntityContentSlot<TEntity>, TEntity, …>
|
|
1770
1692
|
|
|
1771
|
-
class
|
|
1693
|
+
class OIMWritableSingleSlotService<TSlot, TContent, TSlotContentService, …> {
|
|
1772
1694
|
readonly slotContentService: TSlotContentService;
|
|
1773
|
-
|
|
1695
|
+
readonly columnSlotService: OIMColumnSlotService;
|
|
1696
|
+
createStoreSlot(channelMap: IOIMChannelMap): TOIMStoreSlot<TOIMSingleSlotStore<TSlot>, TSlot, TPatch, this>;
|
|
1697
|
+
writeSingleSlotContent(storeSlot, content, changedSlots?): TSlot; // minted if missing
|
|
1698
|
+
clearSingleSlotContent(storeSlot): void;
|
|
1774
1699
|
findSingleSlot(storeSlot): TSlot | undefined; // mints nothing
|
|
1775
1700
|
hasSingleSlotContent(storeSlot): boolean; // mints nothing
|
|
1776
1701
|
getSingleSlot(storeSlot): TSlot; // minted if missing, tracked until the next flush
|
|
1777
1702
|
holdSingleSlot(storeSlot): TSlot; // minted if missing, holdCount up by one
|
|
1778
|
-
getColumn / holdColumn / getAnyColumn / holdAnyColumn / getColumnGroup / holdColumnGroup / unholdColumn;
|
|
1779
1703
|
}
|
|
1780
1704
|
```
|
|
1781
1705
|
|
|
@@ -1786,33 +1710,32 @@ with no value until something writes it. Its store slot is a `TOIMSingleSlotStor
|
|
|
1786
1710
|
```ts
|
|
1787
1711
|
type Settings = { theme: 'light' | 'dark'; zoom: number };
|
|
1788
1712
|
|
|
1789
|
-
const settingsService =
|
|
1713
|
+
const settingsService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<Settings>() });
|
|
1790
1714
|
const settings = settingsService.createStoreSlot(db);
|
|
1791
|
-
settingsService.
|
|
1715
|
+
settingsService.writeSingleSlotContent(settings, { theme: 'light', zoom: 1 });
|
|
1792
1716
|
const settingsSlot = settingsService.holdSingleSlot(settings);
|
|
1793
|
-
const themeColumn = settingsService.holdColumn(settingsSlot, 'theme');
|
|
1794
|
-
const themeEffect = effectService.createEffect(db,
|
|
1717
|
+
const themeColumn = settingsService.columnSlotService.holdColumn(settingsSlot, 'theme');
|
|
1718
|
+
const themeEffect = effectService.createEffect(db, () => { console.log(settingsSlot.entity?.theme); }, [themeColumn]);
|
|
1795
1719
|
settingsService.slotContentService.updateEntity(settingsSlot, { theme: 'dark' });
|
|
1796
1720
|
db.flush();
|
|
1797
1721
|
effectService.disposeEffect(themeEffect);
|
|
1798
|
-
settingsService.unholdColumn(themeColumn);
|
|
1722
|
+
settingsService.columnSlotService.unholdColumn(themeColumn);
|
|
1799
1723
|
settingsSlot.owner.unholdSlot(settingsSlot);
|
|
1800
1724
|
```
|
|
1801
1725
|
|
|
1802
1726
|
**The slot is reached only through the service.** `getSingleSlot(storeSlot)` hands back the live slot, and
|
|
1803
1727
|
mints a fresh one — tracked by the unused-slot collector until the next flush — when there is none;
|
|
1804
|
-
`holdSingleSlot(storeSlot)` does the same and takes a hold.
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
`deleteEntity`) takes that slot.
|
|
1728
|
+
`holdSingleSlot(storeSlot)` does the same and takes a hold. `writeSingleSlotContent` and `clearSingleSlotContent`
|
|
1729
|
+
are the store doors, and every slot door of the content service (`updateEntity`, `updateEntityColumn`,
|
|
1730
|
+
`deleteEntityColumns`, `writeSlotContent`, `markEntityChanged`, `clearSlotContent`) takes that slot.
|
|
1808
1731
|
|
|
1809
|
-
The slot is of the keyed entity slot's literal shape
|
|
1810
|
-
`
|
|
1732
|
+
The slot is of the keyed entity slot's literal shape (with `key: undefined` and `indexKeys: undefined`), and its
|
|
1733
|
+
`storeSlot` is `settings` — so
|
|
1811
1734
|
`effectService.createEffect(db, [settingsSlot], [], …)` works and a column of it is a root like any other.
|
|
1812
1735
|
Its key is `undefined`, and its store is `{ slot }` (`TOIMSingleSlotStore`). Its lifetime is the
|
|
1813
|
-
`slotReleasePolicy` (
|
|
1736
|
+
`slotReleasePolicy` (`defaults.slotReleasePolicy`, then `OIMSlotAtEmptyReleasePolicy`), so a single entity that holds a value
|
|
1814
1737
|
stays; one emptied and unheld is released, and the slot `getSingleSlot` mints next holds no entity. The value
|
|
1815
|
-
is stored by the writer's `
|
|
1738
|
+
is stored by the writer's `entityInserter`, which by default keeps the object handed in rather than a copy.
|
|
1816
1739
|
|
|
1817
1740
|
---
|
|
1818
1741
|
|
|
@@ -1822,18 +1745,17 @@ A bucket collection is, like an entity collection, a content service and the col
|
|
|
1822
1745
|
The content service is the strategy — how a bucket keeps its member slots — and it is chosen at the factory:
|
|
1823
1746
|
|
|
1824
1747
|
```ts signature
|
|
1825
|
-
|
|
1826
|
-
slotContentService
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>;
|
|
1831
|
-
columnNames?: readonly string[];
|
|
1832
|
-
}?): OIMSlotCollectionService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore, TSlotContentService, …>
|
|
1748
|
+
createOIMSlotCollectionService({
|
|
1749
|
+
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or one of the others below
|
|
1750
|
+
slotStoreService?: IOIMSlotStoreService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore>; // Map; REQUIRED for an array key
|
|
1751
|
+
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
1752
|
+
}): OIMWritableSlotCollectionService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore, readonly TBucketMemberSlot[], …>
|
|
1833
1753
|
|
|
1834
|
-
new
|
|
1835
|
-
new
|
|
1836
|
-
new
|
|
1754
|
+
new OIMSlotContentSetBucketService<TBucketMemberSlot>({ bucketCommands? })
|
|
1755
|
+
new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>({ bucketCommands?, bucketMemberSlotsArrayService? })
|
|
1756
|
+
new OIMSlotContentSortedArrayBucketService<TBucketMemberSlot>({ compareBucketMemberSlots, bucketCommands?, bucketMemberSlotsArrayService? })
|
|
1757
|
+
// bucketMemberSlotsArrayService: OIMBucketMemberSlotsArrayService (every mention, the default)
|
|
1758
|
+
// or new OIMBucketUniqueMemberSlotsArrayService<TBucketMemberSlot>() (each slot once)
|
|
1837
1759
|
```
|
|
1838
1760
|
|
|
1839
1761
|
A bucket holds bucket member slots of any slot type — entity slots, bucket slots, computeds — and its
|
|
@@ -1843,16 +1765,13 @@ of the collection service take that store slot first, and the bucket doors of th
|
|
|
1843
1765
|
bucket slot.
|
|
1844
1766
|
|
|
1845
1767
|
```ts
|
|
1846
|
-
const colourService =
|
|
1847
|
-
OIMSlotContentBucketServiceSetDriven<TOIMEntitySlot<Card>>>(db, {
|
|
1848
|
-
slotContentService: new OIMSlotContentBucketServiceSetDriven<TOIMEntitySlot<Card>>(),
|
|
1849
|
-
});
|
|
1768
|
+
const colourService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
|
|
1850
1769
|
const colours = colourService.createStoreSlot(db);
|
|
1851
1770
|
const red = colourService.holdSlotByKey(colours, 'red');
|
|
1852
|
-
colourService.slotContentService.
|
|
1771
|
+
colourService.slotContentService.writeSlotContent(red, [slot, cardService.getSlotByKey(cards, 'c2')]);
|
|
1853
1772
|
db.flush();
|
|
1854
1773
|
for (const deckSlot of colourService.slotContentService.getBucketMemberSlots(red)) { paint(deckSlot.entity); }
|
|
1855
|
-
colourService.slotContentService.
|
|
1774
|
+
colourService.slotContentService.clearSlotContent(red);
|
|
1856
1775
|
red.owner.unholdSlot(red);
|
|
1857
1776
|
```
|
|
1858
1777
|
|
|
@@ -1860,23 +1779,24 @@ Every door below is the bucket content service's and takes the bucket slot:
|
|
|
1860
1779
|
|
|
1861
1780
|
| Door | Signature |
|
|
1862
1781
|
|---|---|
|
|
1863
|
-
| `
|
|
1782
|
+
| `writeSlotContent(bucketSlot, bucketMemberSlots)` | `boolean` — states the whole bucket; idempotent; true when anything arrived or departed |
|
|
1783
|
+
| `getBucketMemberSlotsMissingFrom(bucketSlot, bucketMemberSlots)` | `TBucketMemberSlot[]` — the members the given list leaves out, so a sync can write into them (clear a group key, set a deleted flag) instead of dropping them |
|
|
1864
1784
|
| `addBucketMemberSlot(bucketSlot, bucketMemberSlot)` / `addBucketMemberSlots(bucketSlot, bucketMemberSlots)` | joins the named bucket member slots and touches nothing else. REMOVES NOTHING |
|
|
1865
1785
|
| `removeBucketMemberSlot(bucketSlot, bucketMemberSlot)` / `removeBucketMemberSlots(bucketSlot, bucketMemberSlots)` | takes the named bucket member slots out and touches nothing else |
|
|
1866
|
-
| `
|
|
1786
|
+
| `clearSlotContent(bucketSlot)` | empties it |
|
|
1867
1787
|
| `getBucketMemberSlots(bucketSlot)` | `Iterable<TBucketMemberSlot>` |
|
|
1868
1788
|
| `getBucketMemberSlotCount(bucketSlot)` | `number` |
|
|
1869
|
-
| `hasBucketMemberSlot(bucketSlot, bucketMemberSlot)` | `boolean` — the content service's own membership test: one `Set.has` on a set
|
|
1789
|
+
| `hasBucketMemberSlot(bucketSlot, bucketMemberSlot)` | `boolean` — the content service's own membership test: one `Set.has` on a set bucket; on an array bucket its `bucketMemberSlotsArrayService.hasBucketMemberSlot` (a `Set.has` when unique, `indexOf` otherwise) |
|
|
1870
1790
|
| `markBucketColumnsChanged(bucketSlot, patch, changedColumns)` | `void` — tell the bucket's column slots which member columns moved |
|
|
1871
1791
|
| `markContentChanged(bucketSlot)` | `void` — re-settle emptiness and re-ask the release guard after a write made another way |
|
|
1872
|
-
| `fillBucketMemberSlots(bucketSlot, bucketMemberSlots)` | `void` — fills a bucket slot nobody was told about yet; it throws on one that was already announced, which `addBucketMemberSlots` / `
|
|
1792
|
+
| `fillBucketMemberSlots(bucketSlot, bucketMemberSlots)` | `void` — fills a bucket slot nobody was told about yet; it throws on one that was already announced, which `addBucketMemberSlots` / `writeSlotContent` are for |
|
|
1873
1793
|
|
|
1874
1794
|
The entities of a bucket by key are the index's door, `indexService.getEntitiesByKey(indexStoreSlot, key)`
|
|
1875
1795
|
([The index that fills itself](#the-index-that-fills-itself--oimindexservice)).
|
|
1876
1796
|
|
|
1877
1797
|
### Which door: you hold the answer, or you hold the delta
|
|
1878
1798
|
|
|
1879
|
-
`
|
|
1799
|
+
`writeSlotContent` is for a caller who has THE LIST THAT SHOULD BE THERE and does not
|
|
1880
1800
|
know what changed — a search result, a page from a server, a filter re-run. One
|
|
1881
1801
|
call, and the engine works out the arrivals and the departures. Its cost is
|
|
1882
1802
|
proportional to WHAT THE BUCKET HOLDS, not to the change: the difference builds a
|
|
@@ -1895,31 +1815,31 @@ the order you name them.
|
|
|
1895
1815
|
|
|
1896
1816
|
**`addBucketMemberSlots` removes nothing.** If the authoritative list SHRANK and you call
|
|
1897
1817
|
`addBucketMemberSlots`, the entity slots that left stay in the bucket, held. Not knowing what left is
|
|
1898
|
-
exactly when `
|
|
1818
|
+
exactly when `writeSlotContent` is the right door, and its cost is the price of not
|
|
1899
1819
|
knowing.
|
|
1900
1820
|
|
|
1901
1821
|
The keyed doors are the collection service's and take the store slot first —
|
|
1902
1822
|
`decksService.getSlotByKey(decks, key)`, `decksService.holdSlotByKey(decks, key)` — and a hold is given back
|
|
1903
|
-
with `bucketSlot.owner.unholdSlot(bucketSlot)`.
|
|
1904
|
-
`decksService.slotContentService.
|
|
1823
|
+
with `bucketSlot.owner.unholdSlot(bucketSlot)`. The by-key write is `decksService.writeSlotContentByKey(decks, key, members)`; on a bucket slot you hold it is
|
|
1824
|
+
`decksService.slotContentService.writeSlotContent(bucketSlot, members)`.
|
|
1905
1825
|
|
|
1906
1826
|
A bucket's emptiness is the content service's `isSlotEmpty(bucketSlot)`, a member count of zero, so
|
|
1907
1827
|
an emptied bucket that nothing holds and nothing watches is released by the
|
|
1908
1828
|
guard on the same call that emptied it.
|
|
1909
1829
|
|
|
1910
1830
|
**A bucket never sorts.** Its order is what the caller put in — `addBucketMemberSlots`
|
|
1911
|
-
appends, `insertBucketMemberSlotAt` inserts, a move moves, `
|
|
1831
|
+
appends, `insertBucketMemberSlotAt` inserts, a move moves, `writeSlotContent` takes the
|
|
1912
1832
|
sequence given. A sorted list is a computed over an index:
|
|
1913
|
-
[Sorted lists](#sorted-lists); or the bucket of a `
|
|
1833
|
+
[Sorted lists](#sorted-lists); or the bucket of a `createOIMSlotCollectionSortedArrayIndexService` index, which
|
|
1914
1834
|
orders itself: [Sorted at write](#sorted-at-write).
|
|
1915
1835
|
|
|
1916
|
-
`
|
|
1836
|
+
`writeSlotContent(bucketSlot, members)` runs `assertWritable` — the frame guard
|
|
1917
1837
|
(`assertNotFlushing`) and the released-identity check — then the reconcile (an empty `arrived` and
|
|
1918
1838
|
`departed` is an unchanged write, which wakes nobody), the holds taken on arrivals, the holds given back by
|
|
1919
1839
|
departures and the single channel write of the bucket slot. It is handed a slot, so it hands back a
|
|
1920
1840
|
`boolean`: true when anything arrived or departed.
|
|
1921
1841
|
|
|
1922
|
-
`
|
|
1842
|
+
`writeSlotContent` **joins before it evicts**, so a slot named by both the old and the
|
|
1923
1843
|
new list never reaches zero references. It empties with one `drainSlots` rather
|
|
1924
1844
|
than one removal at a time — per-slot eviction on an array-driven bucket is
|
|
1925
1845
|
n(n-1)/2 element moves.
|
|
@@ -1940,7 +1860,7 @@ empty means nothing changed. Each content service reconciles its own way:
|
|
|
1940
1860
|
`OIMBucketCommands` records onto the bucket slot (`bucket.commands`) as the writes land and
|
|
1941
1861
|
empties it once the flush has fully ended, so a consumer reads it INSIDE the
|
|
1942
1862
|
frame and what it reads can only describe the writes it was woken for.
|
|
1943
|
-
`
|
|
1863
|
+
`clearSlotContent` asks whether the bucket is empty **before** it
|
|
1944
1864
|
touches the store, so an already-empty bucket records nothing for a call that
|
|
1945
1865
|
reports nothing.
|
|
1946
1866
|
|
|
@@ -1948,33 +1868,20 @@ reports nothing.
|
|
|
1948
1868
|
|
|
1949
1869
|
| Class | Storage type | One add records |
|
|
1950
1870
|
|---|---|---|
|
|
1951
|
-
| `
|
|
1952
|
-
| `
|
|
1953
|
-
| `
|
|
1871
|
+
| `OIMSlotContentSetBucketService<TBucketMemberSlot>({ bucketCommands? })` | `TOIMBucketSlotsSetStore<TBucketMemberSlot>` = `Set<TBucketMemberSlot>` | one per slot |
|
|
1872
|
+
| `OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>({ bucketCommands?, bucketMemberSlotsArrayService? })` | `TOIMBucketSlotsArrayStore<TBucketMemberSlot>` = `{ bucketMemberSlots, uniqueBucketMemberSlots, sortedCount }` | one per **mention**; one per slot, order kept, with `new OIMBucketUniqueMemberSlotsArrayService()` |
|
|
1873
|
+
| `OIMSlotContentSortedArrayBucketService<TBucketMemberSlot>({ compareBucketMemberSlots, bucketMemberSlotsArrayService? })` | `TOIMBucketSlotsArrayStore<TBucketMemberSlot>` | one per mention or per slot, at its sorted place |
|
|
1954
1874
|
|
|
1955
|
-
The first two are the ones a caller picks between, and neither sorts: the array-driven
|
|
1956
|
-
caller's order. The
|
|
1875
|
+
The first two are the ones a caller picks between, and neither sorts: the array-driven services keep the
|
|
1876
|
+
caller's order. The last is the one the sorted-at-write index builds for itself
|
|
1957
1877
|
([Sorted at write](#sorted-at-write)); its `repositionBucketMemberSlot(bucketSlot, bucketMemberSlot)` moves one
|
|
1958
1878
|
member to its place after its sort columns changed.
|
|
1959
1879
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
>({
|
|
1966
|
-
bucketArrayMembership: new OIMBucketArrayMembershipUnique<TOIMEntitySlot<Card>>(),
|
|
1967
|
-
})
|
|
1968
|
-
|
|
1969
|
-
| Membership | Storage it builds | One add records |
|
|
1970
|
-
|---|---|---|
|
|
1971
|
-
| `OIMBucketArrayMembershipNonUnique.INSTANCE` (default) | `TOIMBucketSlotsArrayStore<TBucketMemberSlot>` | one per **mention** |
|
|
1972
|
-
| `new OIMBucketArrayMembershipUnique<TBucketMemberSlot>()` | `TOIMBucketSlotsArrayUniqueStore<TBucketMemberSlot>` | one per slot |
|
|
1973
|
-
|
|
1974
|
-
`IOIMBucketArrayMembership<TBucketMemberSlot, TSlots>`: `createStore`, `addSlot`, `removeSlot`,
|
|
1975
|
-
`hasSlot`, `clearSlots`, `containsTheSameSequence`. `hasBucketMemberSlot` asks the membership's `hasSlot`: the
|
|
1976
|
-
unique membership answers from its `Set` (no array walk), the non-unique one walks the
|
|
1977
|
-
array. Only the array-driven service takes one — a set-driven service keeps its slots IN a `Set`.
|
|
1880
|
+
Which slots an array bucket keeps is its `bucketMemberSlotsArrayService`, the only writer of the list and the
|
|
1881
|
+
`Set` beside it: `OIMBucketMemberSlotsArrayService` keeps every mention and no `Set`;
|
|
1882
|
+
`OIMBucketUniqueMemberSlotsArrayService` keeps each slot once and the list and its `Set` together, so the two never
|
|
1883
|
+
disagree. With the unique one `hasBucketMemberSlot` answers from the `Set` (no array walk); with the default it
|
|
1884
|
+
walks the array.
|
|
1978
1885
|
|
|
1979
1886
|
Nothing positional is on the abstract `OIMSlotContentBucketService`: the reads `getBucketMemberSlotAt` and
|
|
1980
1887
|
`getBucketMemberSlotPosition` are on the array-based services (array-driven and sorted), and the five
|
|
@@ -1994,8 +1901,8 @@ named constants:
|
|
|
1994
1901
|
A removal answering the third is recorded as `recordWholeList(bucketSlot)`: a set has no index to patch,
|
|
1995
1902
|
so the consumer is told to re-read the bucket rather than handed a fabricated one.
|
|
1996
1903
|
|
|
1997
|
-
`OIM_NOTHING_WAS_INSERTED` (`-1`) is the insert's answer when nothing was inserted, which is a repeat on an
|
|
1998
|
-
`
|
|
1904
|
+
`OIM_NOTHING_WAS_INSERTED` (`-1`) is the insert's answer when nothing was inserted, which is a repeat on an array bucket
|
|
1905
|
+
with `OIMBucketUniqueMemberSlotsArrayService`. The recorded insert names the index the slot LANDED on — `at` clamped to the
|
|
1999
1906
|
ends of the list — and not `getBucketMemberSlotPosition`, because the default ordered list keeps one place per
|
|
2000
1907
|
**mention**: inserting a slot the bucket already holds gives it a second position, and the position read
|
|
2001
1908
|
names the FIRST. A command script built from it would tell the consumer to patch at an index the slot is not
|
|
@@ -2007,9 +1914,8 @@ error. **One content service instance may serve two bucket collections** — it
|
|
|
2007
1914
|
|
|
2008
1915
|
### Positions the caller decides
|
|
2009
1916
|
|
|
2010
|
-
A bucket whose positions are the caller's is one over `
|
|
2011
|
-
|
|
2012
|
-
block. The two position reads and the five writes are that content service's, they take the bucket
|
|
1917
|
+
A bucket whose positions are the caller's is one over `OIMSlotContentPositionalArrayBucketService`, which is
|
|
1918
|
+
the `pageService` of the setup block. The two position reads and the five writes are that content service's, they take the bucket
|
|
2013
1919
|
slot and no store slot, and an index is valid only until the next write:
|
|
2014
1920
|
|
|
2015
1921
|
| Door | What it does |
|
|
@@ -2030,7 +1936,7 @@ like any other out-of-range position.
|
|
|
2030
1936
|
Each wraps a store change in the frame guard, the holds, the report and the
|
|
2031
1937
|
release question. The default ordered list records one place per MENTION, so
|
|
2032
1938
|
`insertBucketMemberSlotAt` of a slot the bucket already holds gives it a second position —
|
|
2033
|
-
|
|
1939
|
+
take `bucketMemberSlotsArrayService: new OIMBucketUniqueMemberSlotsArrayService()` when a position must name
|
|
2034
1940
|
exactly one entity slot.
|
|
2035
1941
|
|
|
2036
1942
|
Per strategy, generating code:
|
|
@@ -2053,7 +1959,7 @@ slotContentBucketService.moveBucketMemberSlotToPosition(
|
|
|
2053
1959
|
);
|
|
2054
1960
|
|
|
2055
1961
|
// a whole list at once, in the order the server sent
|
|
2056
|
-
slotContentBucketService.
|
|
1962
|
+
slotContentBucketService.writeSlotContent(todo, cardService.writeInBatch(cards, () => (payload).map(entity => cardService.writeSlotContentByKey(cards, entity.id, entity))));
|
|
2057
1963
|
```
|
|
2058
1964
|
|
|
2059
1965
|
### The render script: `OIMBucketCommands`
|
|
@@ -2062,14 +1968,13 @@ slotContentBucketService.writeBucketMemberSlots(todo, cardService.slotContentSer
|
|
|
2062
1968
|
new OIMBucketCommands(options: { maxCommandsPerSlot?: number }) // default OIM_MAX_COMMANDS_PER_SLOT
|
|
2063
1969
|
|
|
2064
1970
|
// a bucket content service takes it at construction, once:
|
|
2065
|
-
new
|
|
2066
|
-
// createOIMBucketService(db, { maxCommandsPerSlot }) builds the default content service with one
|
|
1971
|
+
new OIMSlotContentPositionalArrayBucketService({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: Infinity }) });
|
|
2067
1972
|
```
|
|
2068
1973
|
|
|
2069
1974
|
**Recording is a strategy chosen when the bucket content service is built.** `bucketCommands` is read once
|
|
2070
|
-
into a readonly field; left out of a content service built with `new`, it is `
|
|
1975
|
+
into a readonly field; left out of a content service built with `new`, it is `OIMNoBucketCommands`, whose
|
|
2071
1976
|
every method is empty, so a bucket nobody renders positionally pays one empty call per write and allocates no
|
|
2072
|
-
command.
|
|
1977
|
+
command. There is no setter: a content
|
|
2073
1978
|
service records into the one recorder it was built with. One recorder serves every bucket of every store slot
|
|
2074
1979
|
and of every db — it is stateless.
|
|
2075
1980
|
|
|
@@ -2078,10 +1983,10 @@ content service already writes the bucket slot on every mutation, so a consumer
|
|
|
2078
1983
|
reads the script inside the effect:
|
|
2079
1984
|
|
|
2080
1985
|
```ts
|
|
2081
|
-
effectService.createEffect(db,
|
|
1986
|
+
effectService.createEffect(db, () => {
|
|
2082
1987
|
if (todo.isWholeListRecorded) render(Array.from(pageService.slotContentService.getBucketMemberSlots(todo)));
|
|
2083
1988
|
else applyToTheRenderedList(todo.commands ?? []);
|
|
2084
|
-
});
|
|
1989
|
+
}, [todo]);
|
|
2085
1990
|
```
|
|
2086
1991
|
|
|
2087
1992
|
`TOIMBucketSlot` carries two fields for it, and a consumer reads both:
|
|
@@ -2104,8 +2009,8 @@ that lands after a flush but before that reset starts a new script instead of ex
|
|
|
2104
2009
|
read. The reset is subscribed with `subscribeAfterEachFlush` on the first script a channel slot ever sees, and
|
|
2105
2010
|
walks only the buckets that recorded.
|
|
2106
2011
|
|
|
2107
|
-
A positional write is always recorded as a position. `isWholeListRecorded` is set by a write of the whole bucket — `
|
|
2108
|
-
`
|
|
2012
|
+
A positional write is always recorded as a position. `isWholeListRecorded` is set by a write of the whole bucket — `writeSlotContent`,
|
|
2013
|
+
`clearSlotContent`, `moveBucketMemberSlotRangeToPosition` — by a removal from a bucket whose
|
|
2109
2014
|
content service keeps no positions, which has no index to patch, and by the script
|
|
2110
2015
|
outgrowing the list
|
|
2111
2016
|
it describes. `maxCommandsPerSlot` is that ratio; it defaults to
|
|
@@ -2124,43 +2029,43 @@ content service's own write doors do.
|
|
|
2124
2029
|
### A single bucket
|
|
2125
2030
|
|
|
2126
2031
|
One bucket under no key is a store slot of an `OIMSingleSlotService` over a bucket content service, with
|
|
2127
|
-
`
|
|
2032
|
+
`OIMSingleSlotStoreService.INSTANCE` as the store service:
|
|
2128
2033
|
|
|
2129
2034
|
```ts signature
|
|
2130
|
-
|
|
2131
|
-
slotContentService
|
|
2132
|
-
|
|
2035
|
+
createOIMSingleSlotService({
|
|
2036
|
+
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or the set one
|
|
2037
|
+
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
2038
|
+
}): OIMWritableSingleSlotService<TOIMBucketContentSlot<TSlots>, readonly TBucketMemberSlot[], …>
|
|
2133
2039
|
```
|
|
2134
2040
|
|
|
2135
2041
|
```ts
|
|
2136
|
-
const handService =
|
|
2042
|
+
const handService = createOIMSingleSlotService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
2137
2043
|
const hand = handService.createStoreSlot(db);
|
|
2138
2044
|
const handSlot = handService.holdSingleSlot(hand);
|
|
2139
|
-
handService.slotContentService.
|
|
2045
|
+
handService.slotContentService.writeSlotContent(handSlot, [slot]);
|
|
2140
2046
|
handService.slotContentService.insertBucketMemberSlotAt(handSlot, 0, cardService.getSlotByKey(cards, 'c2'));
|
|
2141
|
-
effectService.disposeEffect(effectService.createEffect(db,
|
|
2047
|
+
effectService.disposeEffect(effectService.createEffect(db, () => {
|
|
2142
2048
|
render(Array.from(handService.slotContentService.getBucketMemberSlots(handSlot)));
|
|
2143
|
-
}));
|
|
2049
|
+
}, [handSlot]));
|
|
2144
2050
|
handSlot.owner.unholdSlot(handSlot);
|
|
2145
2051
|
```
|
|
2146
2052
|
|
|
2147
|
-
One bucket, no key: the single-slot store files its one bucket slot
|
|
2148
|
-
|
|
2053
|
+
One bucket, no key: the single-slot store files its one bucket slot as the store's `slot`. The bucket slot is a
|
|
2054
|
+
`TOIMBucketContentSlot<TSlots>` of the keyed bucket slot's shape (`key: undefined`), what an effect subscribes to and what a reader is handed,
|
|
2149
2055
|
so the channel's write door still sees three slot shapes. Every door of the bucket content service takes it as
|
|
2150
2056
|
it takes a keyed bucket slot, and `markBucketColumnsChanged(bucketSlot, patch, changedColumns)` is how the
|
|
2151
2057
|
caller that changed members says which of their columns moved, as the index does for a keyed bucket. Each
|
|
2152
|
-
door
|
|
2153
|
-
subframe.
|
|
2058
|
+
door writes the bucket slot to the channel once.
|
|
2154
2059
|
|
|
2155
2060
|
**Its lifetime is the `slotReleasePolicy`.** With none named (here or on the db) it is
|
|
2156
|
-
`
|
|
2061
|
+
`OIMSlotAtEmptyReleasePolicy.INSTANCE`, so an emptied single bucket that nothing holds or watches is
|
|
2157
2062
|
released, and `getSingleSlot(storeSlot)` mints a fresh, empty one on the next read. Hold the
|
|
2158
|
-
slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `
|
|
2063
|
+
slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `OIMSlotNeverReleasePolicy.INSTANCE`.
|
|
2159
2064
|
|
|
2160
|
-
### The index that fills itself — `
|
|
2065
|
+
### The index that fills itself — `OIMSlotCollectionIndexService`
|
|
2161
2066
|
|
|
2162
2067
|
```ts signature
|
|
2163
|
-
|
|
2068
|
+
createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>({
|
|
2164
2069
|
entityService: Pick<OIMSlotCollectionService<TSourceKey, TOIMEntitySlot<TEntity, TSourceKey>, TSourceStore, unknown>, 'getSlots'>;
|
|
2165
2070
|
...TOIMIndexKeyOptions<TKey, TEntity, TColumns>; // { columns } | { keyOf } | { keyOf, columns }
|
|
2166
2071
|
slotContentService?: OIMSlotContentBucketService<TOIMEntitySlot<TEntity>, TSlots>; // default: array-driven, no commands
|
|
@@ -2168,9 +2073,9 @@ createOIMIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlo
|
|
|
2168
2073
|
slotReleasePolicy?: IOIMSlotReleasePolicy;
|
|
2169
2074
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>;
|
|
2170
2075
|
columnNames?: readonly string[];
|
|
2171
|
-
}):
|
|
2076
|
+
}): OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore, TSlotContentService, …>
|
|
2172
2077
|
|
|
2173
|
-
class
|
|
2078
|
+
class OIMSlotCollectionIndexService<…> extends OIMSlotCollectionService<…> {
|
|
2174
2079
|
createIndexStoreSlot(sourceStoreSlot: TOIMStoreSlot<TSourceStore, TOIMEntitySlot<TEntity, TSourceKey>, Partial<TEntity>>): TOIMIndexStoreSlot<…>;
|
|
2175
2080
|
disposeIndexStoreSlot(indexStoreSlot: TOIMIndexStoreSlot<…>): void;
|
|
2176
2081
|
getEntitiesByKey(indexStoreSlot, key): (TEntity | undefined)[];
|
|
@@ -2181,12 +2086,12 @@ class OIMIndexService<…> extends OIMSlotCollectionService<…> {
|
|
|
2181
2086
|
}
|
|
2182
2087
|
```
|
|
2183
2088
|
|
|
2184
|
-
`
|
|
2089
|
+
`OIMSlotCollectionIndexService` is a collection service of buckets, so it has every read door of one, and its
|
|
2185
2090
|
`slotContentService` is the bucket content service narrowed by type to its reads (`getBucketMemberSlots`,
|
|
2186
2091
|
`getBucketMemberSlotCount`, `hasBucketMemberSlot`, and the positional reads where the strategy has them) — the
|
|
2187
2092
|
index is the only writer of its buckets. The strategy is chosen at the factory:
|
|
2188
|
-
`
|
|
2189
|
-
`slotContentService`, and the sorted one built by `
|
|
2093
|
+
`OIMSlotContentPositionalArrayBucketService` by default, `OIMSlotContentSetBucketService` passed as
|
|
2094
|
+
`slotContentService`, and the sorted one built by `createOIMSlotCollectionSortedArrayIndexService`. Only the array-based
|
|
2190
2095
|
content services answer by position: `getBucketMemberSlotAt(bucketSlot, index)` (O(1), `undefined` past the
|
|
2191
2096
|
end) and `getBucketMemberSlotPosition(bucketSlot, entitySlot)` (walks the array, O(n), `-1` for a non-member).
|
|
2192
2097
|
The index's default content service records no commands; pass one built with `bucketCommands` for a render
|
|
@@ -2202,34 +2107,34 @@ read through the index service, passing that store slot; the index is what write
|
|
|
2202
2107
|
written before it, without a flush, so minting inside an effect body does not throw.
|
|
2203
2108
|
`getSlotsByKeys` / `holdSlotsByKeys` fill every new bucket in ONE pass over the source. How long it lives after that is **the
|
|
2204
2109
|
`slotReleasePolicy`'s ordinary business**, exactly as for a hand-held bucket; the index injects no lifetime of its
|
|
2205
|
-
own. With none named it is `
|
|
2110
|
+
own. With none named it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: the bucket stands while it has members and is
|
|
2206
2111
|
maintained on every write.
|
|
2207
2112
|
|
|
2208
2113
|
```ts
|
|
2209
|
-
const byDeckService =
|
|
2114
|
+
const byDeckService = createOIMSlotCollectionIndexService({
|
|
2210
2115
|
entityService: cardService,
|
|
2211
2116
|
keyOf: (card: Card) => card.deck,
|
|
2212
2117
|
});
|
|
2213
2118
|
const byDeck = byDeckService.createIndexStoreSlot(cards);
|
|
2214
2119
|
|
|
2215
2120
|
const spades = byDeckService.holdSlotByKey(byDeck, 'spades');
|
|
2216
|
-
effectService.createEffect(db,
|
|
2121
|
+
effectService.createEffect(db, () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))), [spades]);
|
|
2217
2122
|
```
|
|
2218
2123
|
|
|
2219
|
-
A set-driven index names its content service
|
|
2220
|
-
`new
|
|
2124
|
+
A set-driven index names its content service with the bucket member slot type and the index key type spelled
|
|
2125
|
+
out — `new OIMSlotContentSetBucketService()` infers `<TOIMSlot, TOIMStoreKey>` and fails to type-check:
|
|
2221
2126
|
|
|
2222
2127
|
```ts
|
|
2223
|
-
const bySuitService =
|
|
2128
|
+
const bySuitService = createOIMSlotCollectionIndexService({
|
|
2224
2129
|
entityService: cardService,
|
|
2225
2130
|
columns: ['deck'],
|
|
2226
|
-
slotContentService: new
|
|
2131
|
+
slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>, string>(),
|
|
2227
2132
|
});
|
|
2228
2133
|
const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
2229
2134
|
```
|
|
2230
2135
|
|
|
2231
2136
|
- **Passing the entity store slot to `createIndexStoreSlot` is the whole wiring.** It registers
|
|
2232
|
-
a content-change listener on that store slot (`
|
|
2137
|
+
a content-change listener on that store slot (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener`);
|
|
2233
2138
|
the entity services know nothing about indexes. Call `createIndexStoreSlot` twice over one entity
|
|
2234
2139
|
store slot and it files into both. `entityService` is used only for its `getSlots`, to fill a new bucket
|
|
2235
2140
|
from the source.
|
|
@@ -2241,17 +2146,15 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2241
2146
|
ones `keyOf` reads. With `columns`, a patch or put whose changed columns include none of
|
|
2242
2147
|
them keeps the entity's key and calls nothing; a write of the whole value (a fill, a
|
|
2243
2148
|
clear, `markEntityChanged(slot)` with no names) always reads the key again.
|
|
2244
|
-
- **A key of several columns
|
|
2245
|
-
|
|
2246
|
-
tuple.
|
|
2247
|
-
`
|
|
2248
|
-
`isSameKey`), so a fresh array with the same parts moves nothing. `createOIMIndexService` takes a scalar key
|
|
2149
|
+
- **A key of several columns**: `createOIMSlotCollectionIndexService({ entityService, columns: ['deck', 'suit'],
|
|
2150
|
+
bucketSlotStoreService: new OIMSlotTreeStoreService(OIMScalarKeyTreeWalk.INSTANCE) })` makes the key
|
|
2151
|
+
the array of those values in order, typed as a tuple. Only that store knows the key is an array (its
|
|
2152
|
+
`isSameKey`), so a fresh array with the same parts moves nothing. `createOIMSlotCollectionIndexService` takes a scalar key
|
|
2249
2153
|
unless it is handed a `bucketSlotStoreService`: two columns or an array `keyOf` without one do not compile.
|
|
2250
2154
|
`getSlotsByKeys` / `holdSlotsByKeys` fill array-keyed buckets one pass over the
|
|
2251
2155
|
source per key (the cost of `getSlotByKey` each); only a scalar key gets one pass for all.
|
|
2252
|
-
- **Entity slots are filed on write**, using the index's key.
|
|
2253
|
-
|
|
2254
|
-
batch; the index groups the entity slots by key and moves each group with one bucket write.
|
|
2156
|
+
- **Entity slots are filed on write**, using the index's key. Writes inside `writeInBatch` on the collection
|
|
2157
|
+
notify once per batch; the index groups the entity slots by key and moves each group with one bucket write.
|
|
2255
2158
|
- **Reading**, each taking the index store slot first: `getSlotByKey`, `holdSlotByKey`, `findSlotByKey`,
|
|
2256
2159
|
`hasSlotContentByKey`, `getSlotsByKeys`, `holdSlotsByKeys`, `getSlots`, `getSlotCount`, `getEntitiesByKey`, and the bucket column
|
|
2257
2160
|
slot doors `getAnyBucketColumnByKey`, `holdAnyBucketColumnByKey`,
|
|
@@ -2275,9 +2178,9 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2275
2178
|
is maintained whether anyone holds it or not.
|
|
2276
2179
|
- **Order is not the bucket's business, unless you pick the strategy that makes it so.**
|
|
2277
2180
|
A bucket keeps its arrival order; a sorted list is
|
|
2278
|
-
`
|
|
2181
|
+
`createOIMSortedComputedService(db, { indexService, sortColumns?, comparator? })`
|
|
2279
2182
|
— see [Sorted lists](#sorted-lists) — or the index is built with
|
|
2280
|
-
`
|
|
2183
|
+
`createOIMSlotCollectionSortedArrayIndexService`, whose buckets are already in order — see
|
|
2281
2184
|
[Sorted at write](#sorted-at-write). A second computed over the bucket handles
|
|
2282
2185
|
projection or filtering.
|
|
2283
2186
|
- **Members are read through the bucket content service**: `Array.from(indexService.slotContentService.getBucketMemberSlots(bucketSlot))`.
|
|
@@ -2287,9 +2190,9 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2287
2190
|
membership only**: an entity slot joining or leaving. A write to an entity that stays in the
|
|
2288
2191
|
bucket does not wake it — subscribe to that entity's own slot for that.
|
|
2289
2192
|
- **Bucket lifetime is the `slotReleasePolicy` option, not the index's.** Name nothing (here or on the db)
|
|
2290
|
-
and it is `
|
|
2291
|
-
once empty; `
|
|
2292
|
-
and it cools on its queue; pass `
|
|
2193
|
+
and it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: an unheld bucket stays while it has members and goes
|
|
2194
|
+
once empty; `OIMSlotNeverReleasePolicy.INSTANCE` keeps it for good; pass an `OIMSlotAfterIdleReleasePolicy`
|
|
2195
|
+
and it cools on its queue; pass `OIMSlotAtUnholdReleasePolicy.INSTANCE` and it dies at the last give-back,
|
|
2293
2196
|
and the next hold builds a fresh one by walking the source again.
|
|
2294
2197
|
|
|
2295
2198
|
**The bucket's any-column slot exists, but it is not the recommended path.**
|
|
@@ -2300,15 +2203,15 @@ the bucket slot (`getSlotByKey` / `holdSlotByKey`). A reader that wants both sub
|
|
|
2300
2203
|
to both, and the subscriber picks by which slots it puts in `createEffect`.
|
|
2301
2204
|
|
|
2302
2205
|
```ts
|
|
2303
|
-
const deckIndexService =
|
|
2206
|
+
const deckIndexService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2304
2207
|
const deckIndex = deckIndexService.createIndexStoreSlot(cards);
|
|
2305
2208
|
const spadesBucket = deckIndexService.holdSlotByKey(deckIndex, 'spades');
|
|
2306
2209
|
const spadesAnyColumn = deckIndexService.holdAnyBucketColumnByKey(deckIndex, 'spades');
|
|
2307
|
-
const entitiesEffect = effectService.createEffect(db,
|
|
2210
|
+
const entitiesEffect = effectService.createEffect(db, () => {
|
|
2308
2211
|
console.log(deckIndexService.getEntitiesByKey(deckIndex, 'spades'));
|
|
2309
|
-
});
|
|
2212
|
+
}, [spadesBucket, spadesAnyColumn]);
|
|
2310
2213
|
effectService.disposeEffect(entitiesEffect);
|
|
2311
|
-
deckIndexService.unholdColumn(spadesAnyColumn);
|
|
2214
|
+
deckIndexService.columnSlotService.unholdColumn(spadesAnyColumn);
|
|
2312
2215
|
spadesBucket.owner.unholdSlot(spadesBucket);
|
|
2313
2216
|
```
|
|
2314
2217
|
|
|
@@ -2325,7 +2228,7 @@ needs the whole list each time: an export, an aggregate over the entities, a log
|
|
|
2325
2228
|
It costs nothing until someone asks for one: a write is checked against it only after
|
|
2326
2229
|
the index has been asked for the first any-column slot.
|
|
2327
2230
|
|
|
2328
|
-
**The content-change hook.** `
|
|
2231
|
+
**The content-change hook.** `OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames): void`
|
|
2329
2232
|
and `removeContentChangeListener(storeSlot, listener): boolean` file and unfile a listener on that one store
|
|
2330
2233
|
slot, by the listener object. `addReadColumn` / `removeReadColumn` / `addReadAnyColumn` / `removeReadAnyColumn`
|
|
2331
2234
|
declare reads without a listener of their own, which is how an index declares its bucket columns.
|
|
@@ -2340,19 +2243,17 @@ no listener is called, nothing is buffered, and the names array is reused across
|
|
|
2340
2243
|
every column write is added with `readColumnNames` `undefined`. **The filter is the union, per store
|
|
2341
2244
|
slot, not per listener**: once a write touches a column ANY listener of that store reads, every
|
|
2342
2245
|
listener of that store is called, so a listener that declared a narrower set still checks
|
|
2343
|
-
`changedColumns` itself, as `
|
|
2344
|
-
`
|
|
2246
|
+
`changedColumns` itself, as `OIMSlotCollectionIndexService` does against its key columns.
|
|
2247
|
+
`IOIMSlotContentChangeListener<TSlot, TPatch>` has two methods:
|
|
2345
2248
|
`onContentChanged(slot, patch, changedColumns)` and
|
|
2346
|
-
`
|
|
2249
|
+
`onManyContentsChanged(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
|
|
2347
2250
|
parallel `OIMChannelQueue`s read with `getEntryCount()` / `getEntryAt(i)`, valid only during the
|
|
2348
2251
|
call, so a listener copies what it keeps — `patch` is what an update
|
|
2349
2252
|
wrote, `changedColumns` the names `markEntityChanged(slot, columns)` was given, either
|
|
2350
|
-
`undefined` when the write carries none. A
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
`onContentChangedMany` when the door returns. A subframe the caller opens on the channel slot
|
|
2355
|
-
does not hold them — an index is up to date as soon as the content service's own door returns.
|
|
2253
|
+
`undefined` when the write carries none. A single write outside a batch delivers one `onContentChanged` as soon as it is filed; inside
|
|
2254
|
+
`writeInBatch` (or `openBatch` / `closeBatch`) on the collection the writes are queued on the store slot and
|
|
2255
|
+
delivered as ONE `onManyContentsChanged` when the outermost batch closes. Code inside the batch reads the index
|
|
2256
|
+
as it was before the batch.
|
|
2356
2257
|
This hook is the only seam an index uses; an index of your own is a listener.
|
|
2357
2258
|
|
|
2358
2259
|
**Bucket column slots.** An index hands out one column slot per bucket and column:
|
|
@@ -2366,24 +2267,24 @@ return the same kind of slot, written when any column of any member changes. A g
|
|
|
2366
2267
|
`slot.owner.unholdSlot(slot)`.
|
|
2367
2268
|
|
|
2368
2269
|
```ts
|
|
2369
|
-
const decksService =
|
|
2270
|
+
const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2370
2271
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2371
2272
|
const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
|
|
2372
|
-
effectService.disposeEffect(effectService.createEffect(db,
|
|
2373
|
-
decksService.unholdColumn(rankOfSpades);
|
|
2273
|
+
effectService.disposeEffect(effectService.createEffect(db, () => {}, [rankOfSpades]));
|
|
2274
|
+
decksService.columnSlotService.unholdColumn(rankOfSpades);
|
|
2374
2275
|
```
|
|
2375
2276
|
|
|
2376
2277
|
### Sorted lists
|
|
2377
2278
|
|
|
2378
2279
|
```ts signature
|
|
2379
|
-
|
|
2380
|
-
indexService:
|
|
2381
|
-
sortColumns?: readonly TOIMSortColumn<TEntity>[]; // {
|
|
2280
|
+
createOIMSortedComputedService<TKey, TEntity extends object, TSlots, TSourceKey, TSourceStore, TComputedStore?>({
|
|
2281
|
+
indexService: OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore>;
|
|
2282
|
+
sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
|
|
2382
2283
|
comparator?: (left: TEntity, right: TEntity) => number;
|
|
2383
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; // default
|
|
2284
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2384
2285
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
|
|
2385
2286
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<…>, TComputedStore>; // Map; REQUIRED for any other store
|
|
2386
|
-
}):
|
|
2287
|
+
}): OIMSortedComputedService<TKey, TEntity, TSlots, TSourceKey, TSourceStore, TComputedStore>
|
|
2387
2288
|
|
|
2388
2289
|
createSortedStoreSlot(indexStoreSlot: TOIMIndexStoreSlot<…>): TOIMStoreSlot<TComputedStore, TOIMSortedComputedSlot<TEntity, TKey>>
|
|
2389
2290
|
readonly computedCollectionService: OIMSlotCollectionService<TKey, TOIMSortedComputedSlot<TEntity, TKey>, TComputedStore, …>
|
|
@@ -2391,11 +2292,11 @@ readonly computedCollectionService: OIMSlotCollectionService<TKey, TOIMSortedCom
|
|
|
2391
2292
|
|
|
2392
2293
|
`slotReleasePolicy` decides whether a key's sorted list outlives its last hold. The
|
|
2393
2294
|
default lets it go at the unhold, so holding the key again sorts from scratch;
|
|
2394
|
-
`
|
|
2295
|
+
`OIMSlotNeverReleasePolicy.INSTANCE` keeps every key that was ever read and re-sorts it
|
|
2395
2296
|
only when a read finds it changed — the arm for a view that switches between groups.
|
|
2396
2297
|
Pass the same policy to the index the list reads, or its bucket is rebuilt instead.
|
|
2397
2298
|
|
|
2398
|
-
An `
|
|
2299
|
+
An `OIMSortedComputedService` takes no db: its store slots run on the channel slot of the index store slot
|
|
2399
2300
|
they sort. One per index service. `createSortedStoreSlot(indexStoreSlot)` makes one sorted store slot over
|
|
2400
2301
|
one index store slot, and the read doors of `computedCollectionService` take that store slot first:
|
|
2401
2302
|
`getSlotByKey`, `holdSlotByKey`, `findSlotByKey`, `getSlots`, `getSlotCount`, and the column doors; per key a
|
|
@@ -2421,13 +2322,13 @@ slots sorted. A hold is given back with `slot.owner.unholdSlot(slot)`.
|
|
|
2421
2322
|
to a member as with `comparator` alone.
|
|
2422
2323
|
|
|
2423
2324
|
```ts
|
|
2424
|
-
const cardsInDeckService =
|
|
2325
|
+
const cardsInDeckService = createOIMSlotCollectionIndexService({
|
|
2425
2326
|
entityService: cardService,
|
|
2426
2327
|
keyOf: (card: Card) => card.deck,
|
|
2427
2328
|
});
|
|
2428
|
-
|
|
2329
|
+
createOIMSortedComputedService({
|
|
2429
2330
|
indexService: cardsInDeckService,
|
|
2430
|
-
sortColumns: [{
|
|
2331
|
+
sortColumns: [{ columnName: 'title', direction: 'asc' }, { columnName: 'rank', direction: 'asc' }],
|
|
2431
2332
|
comparator: (l, r) => l.title.localeCompare(r.title, 'de') || l.rank - r.rank,
|
|
2432
2333
|
}).createSortedStoreSlot(cardsInDeckService.createIndexStoreSlot(cards));
|
|
2433
2334
|
// a write to `title` or `rank` re-sorts; a write to `note` does not
|
|
@@ -2440,41 +2341,41 @@ SECOND array of `createEffect`. A re-sort that yields the same sequence
|
|
|
2440
2341
|
(`oimSameSlotSequence`) keeps the previous array, so its identity is stable.
|
|
2441
2342
|
|
|
2442
2343
|
```ts
|
|
2443
|
-
const decksService =
|
|
2344
|
+
const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2444
2345
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2445
|
-
const byRankInDeckService =
|
|
2346
|
+
const byRankInDeckService = createOIMSortedComputedService({
|
|
2446
2347
|
indexService: decksService,
|
|
2447
|
-
sortColumns: [{
|
|
2348
|
+
sortColumns: [{ columnName: 'rank', direction: 'asc' }],
|
|
2448
2349
|
});
|
|
2449
2350
|
const byRankInDeck = byRankInDeckService.createSortedStoreSlot(decks);
|
|
2450
2351
|
const spadesByRank = byRankInDeckService.computedCollectionService.holdSlotByKey(byRankInDeck, 'spades');
|
|
2451
|
-
const sortedEffect = effectService.createEffect(db,
|
|
2352
|
+
const sortedEffect = effectService.createEffect(db, () => {
|
|
2452
2353
|
render(spadesByRank.computedValueService.getValueForSlot(spadesByRank));
|
|
2453
|
-
});
|
|
2354
|
+
}, [], [spadesByRank]);
|
|
2454
2355
|
effectService.disposeEffect(sortedEffect);
|
|
2455
2356
|
spadesByRank.owner.unholdSlot(spadesByRank);
|
|
2456
2357
|
```
|
|
2457
2358
|
|
|
2458
2359
|
### Sorted at write
|
|
2459
2360
|
|
|
2460
|
-
A second way to get a sorted list. `
|
|
2461
|
-
index. `
|
|
2361
|
+
A second way to get a sorted list. `createOIMSortedComputedService` sorts at read, over any
|
|
2362
|
+
index. `createOIMSlotCollectionSortedArrayIndexService` keeps the index's own buckets sorted at write, and
|
|
2462
2363
|
the user picks one at construction.
|
|
2463
2364
|
|
|
2464
2365
|
```ts signature
|
|
2465
|
-
|
|
2466
|
-
entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as
|
|
2366
|
+
createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>({
|
|
2367
|
+
entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as createOIMSlotCollectionIndexService, scalar key
|
|
2467
2368
|
sortColumns: readonly TOIMSortColumn<TEntity>[]; // REQUIRED
|
|
2468
2369
|
bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService?, columnNames?,
|
|
2469
|
-
}):
|
|
2370
|
+
}): OIMSlotCollectionSortedArrayIndexService<…> // its slotContentService is an OIMSlotContentSortedArrayBucketService
|
|
2470
2371
|
```
|
|
2471
2372
|
|
|
2472
2373
|
```ts
|
|
2473
2374
|
declare const someCardSlot: TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
2474
|
-
const cardsByDeckService =
|
|
2375
|
+
const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService({
|
|
2475
2376
|
entityService: cardService,
|
|
2476
2377
|
columns: ['deck'],
|
|
2477
|
-
sortColumns: [{
|
|
2378
|
+
sortColumns: [{ columnName: 'rank', direction: 'asc' }, { columnName: 'id', direction: 'desc' }],
|
|
2478
2379
|
});
|
|
2479
2380
|
const cardsByDeck = cardsByDeckService.createIndexStoreSlot(cards);
|
|
2480
2381
|
const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
|
|
@@ -2484,26 +2385,26 @@ cardsByDeckService.slotContentService.getBucketMemberSlotPosition(spades, someCa
|
|
|
2484
2385
|
```
|
|
2485
2386
|
|
|
2486
2387
|
- **Reads come back in order**: `getBucketMemberSlots`, `getBucketMemberSlotAt` and
|
|
2487
|
-
`getBucketMemberSlotPosition` on `
|
|
2388
|
+
`getBucketMemberSlotPosition` on `OIMSlotContentSortedArrayBucketService` answer in `sortColumns` order,
|
|
2488
2389
|
so no sorted computed is needed.
|
|
2489
2390
|
- **A lazy insertion buffer.** A write appends the arrival to an unsorted tail of the bucket's
|
|
2490
|
-
array (`
|
|
2391
|
+
array (`TOIMBucketSlotsArrayStore`: the array store plus `sortedCount`). The first read
|
|
2491
2392
|
settles the tail: binary insertion when the tail is short, one sort of the array when it is
|
|
2492
2393
|
long. A bucket nobody reads is never sorted.
|
|
2493
2394
|
- **The index re-files a member when one of its sort columns changes, and only then.** A write
|
|
2494
2395
|
to a column that is neither the key nor a sort column neither reorders the bucket nor wakes it.
|
|
2495
2396
|
A re-file wakes the bucket once per frame.
|
|
2496
2397
|
- **There are no positional writes** on this bucket (`insertBucketMemberSlotAt` and the other
|
|
2497
|
-
position writes belong to `
|
|
2398
|
+
position writes belong to `OIMSlotContentPositionalArrayBucketService`): the order is the comparator's,
|
|
2498
2399
|
not the caller's.
|
|
2499
|
-
- **Bucket commands are always `
|
|
2400
|
+
- **Bucket commands are always `OIMNoBucketCommands`**: this strategy records no per-flush
|
|
2500
2401
|
script.
|
|
2501
|
-
- **Parts**: `
|
|
2502
|
-
`
|
|
2503
|
-
`repositionBucketMemberSlot(bucketSlot, bucketMemberSlot)`), `
|
|
2402
|
+
- **Parts**: `OIMSlotContentSortedArrayBucketService` (options
|
|
2403
|
+
`TOIMSlotContentSortedArrayBucketServiceOptions`: `compareBucketMemberSlots`; adds
|
|
2404
|
+
`repositionBucketMemberSlot(bucketSlot, bucketMemberSlot)`), `OIMSlotCollectionSortedArrayIndexService`.
|
|
2504
2405
|
|
|
2505
2406
|
**Choosing between the two arms.** Sort-at-write suits point updates: a change to a sort
|
|
2506
|
-
column re-files one member. Sort-at-read (`
|
|
2407
|
+
column re-files one member. Sort-at-read (`createOIMSortedComputedService` over any index)
|
|
2507
2408
|
suits bulk loads. Both are shipped; neither is a default.
|
|
2508
2409
|
|
|
2509
2410
|
**`indexKeys` on each entity slot.** Each indexable slot carries, per index store slot, the key the entity is
|
|
@@ -2511,15 +2412,15 @@ filed under now, so a moved entity slot leaves its old bucket in O(1), also unde
|
|
|
2511
2412
|
writer. Only the index writes it, through the `IOIMIndexKeysService` (`get(slot, position)`,
|
|
2512
2413
|
`set(slot, position, indexKey)`), at the position `createIndexStoreSlot` allocated
|
|
2513
2414
|
(`indexStoreSlot.indexKeysPosition`). Positions are allocated per entity store slot
|
|
2514
|
-
(`sourceStoreSlot.indexKeysPositions`). Pick the strategy
|
|
2515
|
-
(default `
|
|
2415
|
+
(`sourceStoreSlot.indexKeysPositions`). Pick the strategy with `new OIMDefaults({ indexKeysService })` passed as `defaults`
|
|
2416
|
+
(default `OIMAppendingIndexKeysService.INSTANCE`):
|
|
2516
2417
|
|
|
2517
2418
|
| strategy | on `disposeIndexStoreSlot` | cost |
|
|
2518
2419
|
|---|---|---|
|
|
2519
|
-
| `
|
|
2520
|
-
| `
|
|
2420
|
+
| `OIMAppendingIndexKeysService` (default) | the index's position is never reused; its stale entry stays on each entity slot | an entity slot's `indexKeys` grows to the highest position any index wrote on it (K indexes made and disposed → an array of K on an entity slot the last one filed), and a stale entry keeps its key reachable. Writes pay nothing for a disposed index: only live listeners are kept |
|
|
2421
|
+
| `OIMReclaimingIndexKeysService` | wipes that position on every entity slot of the source store slot and reuses it | O(N) per dispose |
|
|
2521
2422
|
|
|
2522
|
-
Switch to `
|
|
2423
|
+
Switch to `OIMReclaimingIndexKeysService` only if you create indexes dynamically and often. Creating
|
|
2523
2424
|
indexes dynamically and often is expensive, so avoid it.
|
|
2524
2425
|
|
|
2525
2426
|
### The channel of a bucket store slot
|
|
@@ -2541,7 +2442,7 @@ type TOIMComputedSources = {
|
|
|
2541
2442
|
In a `createSources` result (of `createOIMComputedService`'s options, or of `createStoreSlot`) both `sourceSlots` and `sourceComputeds` are optional; a missing one means none.
|
|
2542
2443
|
|
|
2543
2444
|
```ts signature
|
|
2544
|
-
type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<
|
|
2445
|
+
type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<TOIMComputedDerivation> & {
|
|
2545
2446
|
readonly owner: IOIMSlotOwner;
|
|
2546
2447
|
readonly computedValueService: IOIMComputedValueService<TValue>;
|
|
2547
2448
|
readonly value: TValue;
|
|
@@ -2582,22 +2483,30 @@ the next read or hold.
|
|
|
2582
2483
|
class OIMComputedService<TKey, TValue, TComputedStore> {
|
|
2583
2484
|
constructor(options: TOIMComputedServiceOptions<TKey, TValue, TComputedStore>);
|
|
2584
2485
|
readonly computedCollectionService: OIMSlotCollectionService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore,
|
|
2585
|
-
OIMComputedValueService<TKey, TValue>, Extract<keyof TValue, string>, TOIMComputedColumnSlot,
|
|
2486
|
+
OIMComputedValueService<TKey, TValue>, Extract<keyof TValue, string>, TOIMComputedColumnSlot, OIMPullColumnSlotOwner>;
|
|
2586
2487
|
createComputedStoreSlot(channelMap: IOIMChannelMap, derive?, createSources?): TOIMComputedStoreSlot<TComputedStore, TValue, TKey>;
|
|
2587
2488
|
}
|
|
2588
2489
|
|
|
2589
2490
|
createOIMComputedService<TKey, TValue, TComputedStore = Map<TKey, TOIMComputedSlot<TValue, TKey>>>({
|
|
2590
|
-
createSources?: (
|
|
2491
|
+
createSources?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>;
|
|
2591
2492
|
derive?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue;
|
|
2592
2493
|
sameValueGuard?: TOIMSameValueGuard<TValue>; // default Object.is
|
|
2494
|
+
releaseValue?: TOIMComputedValueReleaser<TValue>; // default: gives back nothing
|
|
2593
2495
|
initialValue?: TValue;
|
|
2594
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; // default
|
|
2595
|
-
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; //
|
|
2496
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2497
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // defaults.computedColumnSlotStoreService, then a shared map store
|
|
2596
2498
|
columnNames?: readonly Extract<keyof TValue, string>[];
|
|
2597
2499
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore>; // Map; REQUIRED for any other store
|
|
2598
2500
|
}): OIMComputedService<TKey, TValue, TComputedStore>
|
|
2599
2501
|
```
|
|
2600
2502
|
|
|
2503
|
+
**`releaseValue(value)` gives back what a derived value holds** (a request in flight, a timer). It is called
|
|
2504
|
+
with the previous value when `derive` produced one `sameValueGuard` calls different; with a new object the
|
|
2505
|
+
guard calls equal (the slot keeps the old one, the new one is dropped); with the current value when the
|
|
2506
|
+
computed slot is released; with the value a `derive` returned after its own slot was released (not stored).
|
|
2507
|
+
It is not called when `derive` returns the object the slot already holds, and never with `initialValue`.
|
|
2508
|
+
`createOIMSingleComputedService` takes it too.
|
|
2509
|
+
|
|
2601
2510
|
The factory takes no db: a computed service is configuration, and each store slot takes its channel slot at
|
|
2602
2511
|
`createComputedStoreSlot(db, …)`.
|
|
2603
2512
|
|
|
@@ -2611,15 +2520,13 @@ Doors of a keyed computed store slot are the read doors of `computedCollectionSe
|
|
|
2611
2520
|
`getSlots(storeSlot)`, `getSlotCount(storeSlot)`, and the column doors; the value is
|
|
2612
2521
|
`slot.computedValueService.getValueForSlot(slot)`, and a give-back is `slot.owner.unholdSlot(slot)`.
|
|
2613
2522
|
|
|
2614
|
-
**
|
|
2615
|
-
`
|
|
2616
|
-
|
|
2617
|
-
|
|
2523
|
+
**A single computed is its own service.** `createOIMSingleComputedService<TValue>({ … })` builds an
|
|
2524
|
+
`OIMSingleComputedService`, whose store slots each hold at most one computed slot, read through
|
|
2525
|
+
`computedSingleSlotService` (`getSingleSlot`, `holdSingleSlot`, `findSingleSlot`). Like a keyed one, a store slot
|
|
2526
|
+
outlives every computed slot it mints and mints a fresh one on the next read or hold once the previous one is gone.
|
|
2618
2527
|
|
|
2619
2528
|
```ts
|
|
2620
|
-
const topRankService =
|
|
2621
|
-
computedSlotStoreService:
|
|
2622
|
-
OIMSlotStoreServiceSingleSlotDriven.INSTANCE as unknown as OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<number, void>>,
|
|
2529
|
+
const topRankService = createOIMSingleComputedService<number>({
|
|
2623
2530
|
sameValueGuard: oimSameValueZeroGuard,
|
|
2624
2531
|
initialValue: -1,
|
|
2625
2532
|
});
|
|
@@ -2628,18 +2535,16 @@ const topRank = topRankService.createComputedStoreSlot(
|
|
|
2628
2535
|
() => cardService.getSlotByKey(cards, 'c1').entity?.rank ?? -1,
|
|
2629
2536
|
() => ({ sourceSlots: [cardService.getSlotByKey(cards, 'c1')] }),
|
|
2630
2537
|
);
|
|
2631
|
-
const heldTopRank = topRankService.
|
|
2538
|
+
const heldTopRank = topRankService.computedSingleSlotService.holdSingleSlot(topRank); // the same slot while anything holds it
|
|
2632
2539
|
heldTopRank.computedValueService.getValueForSlot(heldTopRank);
|
|
2633
|
-
heldTopRank.owner.unholdSlot(heldTopRank); // gone; the next
|
|
2540
|
+
heldTopRank.owner.unholdSlot(heldTopRank); // gone; the next holdSingleSlot(topRank) mints a new one
|
|
2634
2541
|
```
|
|
2635
2542
|
|
|
2636
|
-
A single computed store slot has
|
|
2637
|
-
undefined)` hands a slot without a hold — a fresh one goes to the unused-slot collector and is let go at the
|
|
2543
|
+
A single computed store slot has no key and a store of one slot. `getSingleSlot(storeSlot)` hands a slot without a hold — a fresh one goes to the unused-slot collector and is let go at the
|
|
2638
2544
|
next flush unless something holds it — and `holdSlotByKey(storeSlot, undefined)` holds the live slot, minting
|
|
2639
2545
|
one if the previous is gone. A computed slot goes at its last unhold once no effect reads it
|
|
2640
|
-
(`
|
|
2641
|
-
sources. A last unhold during a flush releases at
|
|
2642
|
-
db's unused-slot collector, as for every non-empty slot. The holds a computed takes on its sources and
|
|
2546
|
+
(`OIMSlotAtUnholdReleasePolicy`, the default), and gives back the holds it took on its
|
|
2547
|
+
sources. A last unhold during a flush releases it at once, as for every slot. The holds a computed takes on its sources and
|
|
2643
2548
|
gives back are one stateless service, shared by both shapes.
|
|
2644
2549
|
|
|
2645
2550
|
**`createSources` takes the key and the store slot, and nothing else. There is no `keep`.** A build that
|
|
@@ -2651,11 +2556,11 @@ list, and a source a build closes over without naming is a source nothing holds.
|
|
|
2651
2556
|
**A derivation is never empty.** `OIMComputedService` answers
|
|
2652
2557
|
"empty" with `false`, so a filed derivation is released only when nothing holds
|
|
2653
2558
|
it, nothing watches it, and the grace is `0` — which is what
|
|
2654
|
-
`
|
|
2559
|
+
`OIMSlotAtUnholdReleasePolicy` answers, and the default when no `slotReleasePolicy` is given.
|
|
2655
2560
|
|
|
2656
2561
|
```ts setup
|
|
2657
2562
|
const rankService = createOIMComputedService<TOIMStoreKey, number>({
|
|
2658
|
-
createSources: (
|
|
2563
|
+
createSources: ({ key }) => ({ sourceSlots: [cardService.getSlotByKey(cards, key)] }),
|
|
2659
2564
|
derive: (computedSlot) => cardService.getSlotByKey(cards, computedSlot.key).entity?.rank ?? -1,
|
|
2660
2565
|
sameValueGuard: oimSameValueZeroGuard,
|
|
2661
2566
|
initialValue: -1,
|
|
@@ -2682,41 +2587,38 @@ over the carrier computed, its value `carrier.value[column]` (a tuple of the gro
|
|
|
2682
2587
|
the whole value for the any-column), compared with `Object.is`. It is a `TOIMComputedColumnSlot`
|
|
2683
2588
|
(`TOIMComputedSlot<unknown, string>`), so it goes in the SECOND array of `createEffect`, and its value
|
|
2684
2589
|
is read off `columnSlot.value`. It wakes an effect only when that field of the derived value changes.
|
|
2685
|
-
Pinned by `
|
|
2590
|
+
Pinned by `computed/tests/what-a-computed-column-wakes-on.test.ts`.
|
|
2686
2591
|
|
|
2687
2592
|
```ts
|
|
2688
2593
|
type Summary = { top: number; count: number };
|
|
2689
|
-
const summaryService =
|
|
2690
|
-
computedSlotStoreService:
|
|
2691
|
-
OIMSlotStoreServiceSingleSlotDriven.INSTANCE as unknown as OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<Summary, void>>,
|
|
2692
|
-
});
|
|
2594
|
+
const summaryService = createOIMSingleComputedService<Summary>();
|
|
2693
2595
|
const summary = summaryService.createComputedStoreSlot(
|
|
2694
2596
|
db,
|
|
2695
2597
|
() => ({ top: slot.entity?.rank ?? -1, count: cardService.getSlotCount(cards) }),
|
|
2696
2598
|
() => ({ sourceSlots: [slot] }),
|
|
2697
2599
|
);
|
|
2698
|
-
const
|
|
2699
|
-
const summaryTop =
|
|
2700
|
-
const topEffect = effectService.createEffect(db,
|
|
2600
|
+
const summarySingleSlotService = summaryService.computedSingleSlotService;
|
|
2601
|
+
const summaryTop = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(summary), 'top');
|
|
2602
|
+
const topEffect = effectService.createEffect(db, () => { paint(slot.entity); }, [], [summaryTop]);
|
|
2701
2603
|
effectService.disposeEffect(topEffect);
|
|
2702
|
-
|
|
2604
|
+
summarySingleSlotService.columnSlotService.unholdColumn(summaryTop);
|
|
2703
2605
|
```
|
|
2704
2606
|
|
|
2705
2607
|
### The order of a bucket, as one derivation per key
|
|
2706
2608
|
|
|
2707
2609
|
```ts signature
|
|
2708
|
-
|
|
2610
|
+
createOIMBucketMemoComputedService<TKey, TBucketMemberSlot extends TOIMSlot, TSlots, TBucketStore, TComputedStore?>({
|
|
2709
2611
|
bucketCollectionService: OIMSlotCollectionService<TKey, TOIMBucketSlot<TSlots, TKey>, TBucketStore, OIMSlotContentBucketService<TBucketMemberSlot, TSlots>, …>;
|
|
2710
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; // default
|
|
2612
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2711
2613
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
|
|
2712
2614
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<readonly TBucketMemberSlot[], TKey>, TComputedStore>;
|
|
2713
|
-
}):
|
|
2615
|
+
}): OIMBucketMemoComputedService<TKey, TBucketMemberSlot, TSlots, TBucketStore, TComputedStore>
|
|
2714
2616
|
|
|
2715
2617
|
createBucketMemoStoreSlot(bucketStoreSlot: TOIMStoreSlot<TBucketStore, TOIMBucketSlot<TSlots, TKey>, readonly TBucketMemberSlot[]>): TOIMStoreSlot<TComputedStore, TOIMBucketMemoComputedSlot<TBucketMemberSlot, TKey>>
|
|
2716
2618
|
```
|
|
2717
2619
|
|
|
2718
2620
|
It takes no db: a memo store slot runs on the channel slot of the bucket store slot it reads. The class is
|
|
2719
|
-
`new
|
|
2621
|
+
`new OIMBucketMemoComputedService(options)` (options type `TOIMBucketMemoComputedServiceOptions`), whose
|
|
2720
2622
|
`computedCollectionService` reads the per-key derivations; `createBucketMemoStoreSlot(bucketStoreSlot)` makes
|
|
2721
2623
|
one memo store slot over one bucket store slot.
|
|
2722
2624
|
|
|
@@ -2726,18 +2628,18 @@ it is produced on the read that asks for it and never while nothing reads it, an
|
|
|
2726
2628
|
its `changedAtRevision` and `changedAtEpoch` move only when the list a reader would
|
|
2727
2629
|
see actually changed. The list is handed back by IDENTITY: it is the same array for
|
|
2728
2630
|
as long as the list holds, so a consumer memoising on identity needs no comparison
|
|
2729
|
-
of its own. For a SORTED list use [`
|
|
2631
|
+
of its own. For a SORTED list use [`createOIMSortedComputedService`](#sorted-lists).
|
|
2730
2632
|
|
|
2731
2633
|
The source list is ONE slot — the bucket — and it never grows: a bucket
|
|
2732
2634
|
change is a move of the bucket's value, not of the derivation's sources.
|
|
2733
2635
|
|
|
2734
2636
|
```ts
|
|
2735
|
-
const orderService =
|
|
2637
|
+
const orderService = createOIMBucketMemoComputedService({
|
|
2736
2638
|
bucketCollectionService: orderedService,
|
|
2737
2639
|
});
|
|
2738
2640
|
const order = orderService.createBucketMemoStoreSlot(ordered);
|
|
2739
2641
|
const deck = orderService.computedCollectionService.holdSlotByKey(order, 'spades'); // mints the bucket too, and holds it
|
|
2740
|
-
effectService.createEffect(db,
|
|
2642
|
+
effectService.createEffect(db, () => render(deck.computedValueService.getValueForSlot(deck)), [], [deck]);
|
|
2741
2643
|
```
|
|
2742
2644
|
|
|
2743
2645
|
---
|
|
@@ -2787,7 +2689,7 @@ type TOIMCompositeKeyNode<TAtKey = unknown, TCompositeKeyPart = TOIMEntitySlot>
|
|
|
2787
2689
|
};
|
|
2788
2690
|
```
|
|
2789
2691
|
|
|
2790
|
-
`TOIMSlotStoreTree` is what `
|
|
2692
|
+
`TOIMSlotStoreTree` is what `OIMSlotTreeStoreService.createStore()` builds:
|
|
2791
2693
|
the tree's root plus a flat `Set` of every filed slot, which makes `getSlots()` and
|
|
2792
2694
|
`getSlotCount()` O(1) rather than a walk, and keeps `getSlots()` in filing order like every other store.
|
|
2793
2695
|
`TOIMCompositeKeyNode` is one level of that tree — a `Map` from composite key part to the
|
|
@@ -2797,10 +2699,10 @@ are holding; neither is something you build by hand.
|
|
|
2797
2699
|
|
|
2798
2700
|
| Store | Key type |
|
|
2799
2701
|
|---|---|
|
|
2800
|
-
| `
|
|
2801
|
-
| `
|
|
2802
|
-
| `
|
|
2803
|
-
| `
|
|
2702
|
+
| `OIMSlotMapStoreService<TSlot>` | `TOIMStoreKey` = `string \| number` |
|
|
2703
|
+
| `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `OIMScalarKeyTreeWalk.INSTANCE` | `readonly TOIMStoreKey[]` — **a chain of VALUES** |
|
|
2704
|
+
| `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `OIMCompositeKeyTreeWalk.INSTANCE` | `readonly TOIMEntitySlot[]` — **a chain of entity SLOTS** |
|
|
2705
|
+
| `OIMSingleSlotStoreService.INSTANCE` | `void` — one slot per store slot, its store a `TOIMSingleSlotStore` (`{ slot }`); what every single is built on |
|
|
2804
2706
|
|
|
2805
2707
|
Two stores for keys, one per shape of address, and the single-slot store for no key. The chain store descends one `Map`
|
|
2806
2708
|
lookup per composite key part, keeps `[a]` and `[a, b]` apart, and has **no encoding step**:
|
|
@@ -2840,10 +2742,10 @@ deletes nothing and returns `undefined`.
|
|
|
2840
2742
|
|
|
2841
2743
|
| Walk | Class | Shared instance | What a composite key part is | Holds? |
|
|
2842
2744
|
|---|---|---|---|---|
|
|
2843
|
-
| scalar | `
|
|
2844
|
-
| composite | `
|
|
2745
|
+
| scalar | `OIMScalarKeyTreeWalk` | `OIMScalarKeyTreeWalk.INSTANCE` | `TOIMStoreKey` | no |
|
|
2746
|
+
| composite | `OIMCompositeKeyTreeWalk` | `OIMCompositeKeyTreeWalk.INSTANCE` | `TOIMEntitySlot` | **yes — one hold per edge** |
|
|
2845
2747
|
|
|
2846
|
-
**The hold is the whole difference.** `
|
|
2748
|
+
**The hold is the whole difference.** `OIMCompositeKeyTreeWalk.mintNodeAt` calls
|
|
2847
2749
|
`holdCompositeKeyPart` the first time it creates an edge, which runs
|
|
2848
2750
|
`entitySlot.owner.holdSlot(entitySlot)`; `deleteKey` calls `unholdCompositeKeyPart` on
|
|
2849
2751
|
exactly the edges it unlinked, deepest first and only after every unlink is done, which runs `entitySlot.owner.unholdSlot(entitySlot)` unless
|
|
@@ -2861,16 +2763,16 @@ type TScalarChain = readonly TOIMStoreKey[];
|
|
|
2861
2763
|
type Tally = { deck: string; day: string; count: number };
|
|
2862
2764
|
type TTallyStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Tally, TScalarChain>, TOIMStoreKey>;
|
|
2863
2765
|
|
|
2864
|
-
const tallyService = createOIMSlotCollectionService
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2766
|
+
const tallyService = createOIMSlotCollectionService<
|
|
2767
|
+
ReturnType<typeof createOIMSlotContentEntityService<Tally, TScalarChain>>, TTallyStore
|
|
2768
|
+
>({
|
|
2769
|
+
slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(),
|
|
2770
|
+
slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
|
|
2771
|
+
OIMScalarKeyTreeWalk.INSTANCE,
|
|
2772
|
+
),
|
|
2871
2773
|
});
|
|
2872
2774
|
const tallies = tallyService.createStoreSlot(db);
|
|
2873
|
-
tallyService.
|
|
2775
|
+
tallyService.writeSlotContentByKey(tallies, ['spades', 'mon'], { deck: 'spades', day: 'mon', count: 1 });
|
|
2874
2776
|
tallyService.getSlotByKey(tallies, ['spades', 'mon']);
|
|
2875
2777
|
```
|
|
2876
2778
|
|
|
@@ -2891,22 +2793,22 @@ type Enrolment = {
|
|
|
2891
2793
|
type TChain = readonly TOIMEntitySlot[];
|
|
2892
2794
|
type TEnrolmentStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Enrolment, TChain>, TOIMEntitySlot>;
|
|
2893
2795
|
|
|
2894
|
-
const enrolmentService = createOIMSlotCollectionService
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2796
|
+
const enrolmentService = createOIMSlotCollectionService<
|
|
2797
|
+
ReturnType<typeof createOIMSlotContentEntityService<Enrolment, TChain>>, TEnrolmentStore
|
|
2798
|
+
>({
|
|
2799
|
+
slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(),
|
|
2800
|
+
slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
|
|
2801
|
+
OIMCompositeKeyTreeWalk.INSTANCE,
|
|
2802
|
+
),
|
|
2901
2803
|
});
|
|
2902
2804
|
const enrolments = enrolmentService.createStoreSlot(db);
|
|
2903
|
-
enrolmentService.
|
|
2805
|
+
enrolmentService.writeSlotContentByKey(enrolments, [alice, admins], { user: alice, group: admins, since: 2026 });
|
|
2904
2806
|
enrolmentService.getSlotByKey(enrolments, [alice, admins]);
|
|
2905
2807
|
```
|
|
2906
2808
|
|
|
2907
2809
|
**Never put a scalar in a composite-walk key.** It is a type error, and
|
|
2908
2810
|
past a cast it throws a TypeError inside the mint, setting `holdCount` on a
|
|
2909
|
-
string, after the edge is already in the tree. `
|
|
2811
|
+
string, after the edge is already in the tree. `OIMScalarKeyTreeWalk.INSTANCE` is
|
|
2910
2812
|
the walk for that key — the same store class, the other strategy.
|
|
2911
2813
|
|
|
2912
2814
|
**Compare a chain with the strategy that matches it**: `oimSameSlotSequence` for identities
|
|
@@ -2919,8 +2821,7 @@ address" for the address the tree is looking at.
|
|
|
2919
2821
|
## Lifetime — the owner protocol
|
|
2920
2822
|
|
|
2921
2823
|
**`slot.owner` is the only reference a slot carries, and it is never
|
|
2922
|
-
`undefined`.** Every slot minted anywhere has one
|
|
2923
|
-
`OIMDefaults.STANDALONE_SLOT_OWNER.getValue()`, a slot a collection filed carries that collection type's slot owner (an `OIMSlotOwnerService`) — keyed or single alike — and a slot that has been released KEEPS its owner
|
|
2824
|
+
`undefined`.** Every slot minted anywhere has one: a slot a collection filed carries that collection type's slot owner (an `OIMSlotOwnerService`) — keyed or single alike — and a slot that has been released KEEPS its owner
|
|
2924
2825
|
and raises the RELEASED flag in `slot.slotFlags` instead, which the doors below read.
|
|
2925
2826
|
|
|
2926
2827
|
```ts signature
|
|
@@ -2945,26 +2846,27 @@ At a deadline the release queue reads its MARKER off the slot, decides, and call
|
|
|
2945
2846
|
| `unsubscribeFromSlot(slot, at, effect)` | swap the tail of both arrays into `at`, tell the moved effect its new position at its edge (one write, no search), and **if that emptied the list, `onSlotIdle(slot)` opens a fresh idle window**, then give the subscription's hold back through `unholdSlot`; hand back whether it unfiled. Unfiling by remembered position is why taking N subscribers off one identity is O(N) and not quadratic. **Throws by name** on a released slot |
|
|
2946
2847
|
|
|
2947
2848
|
**`OIMSlotOwner`** is the base class and it is concrete: it implements all five.
|
|
2948
|
-
Its `releaseSlotIfUnheld` does nothing,
|
|
2849
|
+
Its `releaseSlotIfUnheld` does nothing, its
|
|
2850
|
+
`unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMEditable` with no
|
|
2851
|
+
guard at all (it exists so a library that adds its own release wheel can keep its own bit in
|
|
2852
|
+
`slot.slotFlags`; the engine owns bits `1`, `2`, `4`, `8`, `16` and `32`, so a library starts at `64`;
|
|
2853
|
+
it is on the owner so a subclass can hook every such mutation), and a `protected
|
|
2949
2854
|
onSlotIdle` does nothing; a `protected assertNotReleased` is what every refusing
|
|
2950
2855
|
door above calls. `OIMSlotOwnerService` overrides `onSlotIdle` and
|
|
2951
|
-
`releaseSlotIfUnheld`, asking its release policy and its content
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
computed slot nobody has filed. It never releases anything, which is exactly what
|
|
2955
|
-
"filed nowhere, so nothing can unfile it" means. A single's slot is not such a slot: its
|
|
2956
|
-
owner is its collection type's slot owner, and that release policy applies to it.
|
|
2856
|
+
`releaseSlotIfUnheld`, asking its release policy and its content service; the clock is the slot's channel slot's.
|
|
2857
|
+
A single's slot is owned the same way: its owner is its collection type's slot owner, and that release policy
|
|
2858
|
+
applies to it.
|
|
2957
2859
|
|
|
2958
2860
|
### The column owner
|
|
2959
2861
|
|
|
2960
2862
|
**Columns are a module of every collection.** The column slots of a carrier slot live
|
|
2961
2863
|
in `carrierSlot.columnSlotStore`, a field of the base `TOIMSlot`, so any slot can carry
|
|
2962
|
-
them — an entity slot, a bucket slot, a computed slot. `
|
|
2864
|
+
them — an entity slot, a bucket slot, a computed slot. `OIMColumnSlotOwner` is abstract, an
|
|
2963
2865
|
`OIMSlotOwner` subclass, with two strategies:
|
|
2964
|
-
`
|
|
2965
|
-
keyed and single) and `
|
|
2866
|
+
`OIMPushColumnSlotOwner` (plain column slots woken by marks on a write; entity and bucket collections,
|
|
2867
|
+
keyed and single) and `OIMPullColumnSlotOwner` (each column slot a computed slot over its carrier;
|
|
2966
2868
|
computed collections, keyed and single). The factory hands the strategy's factory to the slot owner as
|
|
2967
|
-
`
|
|
2869
|
+
`createColumnSlotOwner`. Each collection type has its own column owner, and every column slot it mints names that
|
|
2968
2870
|
owner as its `owner`. The owner holds only the column slot store service; the column state — the column
|
|
2969
2871
|
groups and which columns are referenced — lives on the store slot. The collection exposes it only
|
|
2970
2872
|
through its column doors: `getColumn(slot, columnName)`, `holdColumn(slot, columnName)`,
|
|
@@ -2991,12 +2893,12 @@ so a slot with any live column slot is never released.
|
|
|
2991
2893
|
### Disposers
|
|
2992
2894
|
|
|
2993
2895
|
The disposers this package hands back are the unsubscribes `OIMChannelService.INSTANCE.subscribeAfterEachFrame`
|
|
2994
|
-
and its siblings return, and the one `effectService.subscribeEffect(db,
|
|
2896
|
+
and its siblings return, and the one `effectService.subscribeEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`
|
|
2995
2897
|
returns. An effect from `createEffect` has none, so the caller writes
|
|
2996
2898
|
`() => effectService.disposeEffect(effect)`. A hold is given back through its owner, not through a disposer.
|
|
2997
2899
|
|
|
2998
2900
|
```ts
|
|
2999
|
-
const effect = effectService.createEffect(db,
|
|
2901
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
3000
2902
|
const disposers: TOIMDisposer[] = [];
|
|
3001
2903
|
disposers.push(() => { effectService.disposeEffect(effect); });
|
|
3002
2904
|
disposers.push(() => { slot.owner.unholdSlot(slot); });
|
|
@@ -3027,12 +2929,11 @@ content service hands back the LIVE list — declared `Iterable`, not an array,
|
|
|
3027
2929
|
`Array.from(bucketService.slotContentService.getBucketMemberSlots(bucketSlot))` is required and is the copy. A `prev` kept
|
|
3028
2930
|
live is the same array as `next` on the following frame: the diff is empty,
|
|
3029
2931
|
no reorder is ever drawn, and nothing throws. The other safe `prev` is
|
|
3030
|
-
`
|
|
2932
|
+
`createOIMBucketMemoComputedService`'s value — a fresh array on every write that
|
|
3031
2933
|
changed the list, the same array while it did not.
|
|
3032
2934
|
|
|
3033
2935
|
A LIST MAY NAME ONE SLOT TWICE, and then it holds that slot twice.
|
|
3034
|
-
`
|
|
3035
|
-
records every mention, so an ordered bucket's list is exactly such a list. The
|
|
2936
|
+
`OIMSlotContentPositionalArrayBucketService` records every mention, so an ordered bucket's list is exactly such a list. The
|
|
3036
2937
|
mentions of one identity are matched in the order they appear, and the script
|
|
3037
2938
|
reproduces `next` mention for mention. The minimal move count (`common − LIS`)
|
|
3038
2939
|
is claimed for lists whose identities are distinct: where the two lists name one
|
|
@@ -3048,15 +2949,15 @@ leave the longest.
|
|
|
3048
2949
|
type TOIMStoreKey = string | number;
|
|
3049
2950
|
type TOIMDisposer = () => void;
|
|
3050
2951
|
type TOIMEffectBody = () => (() => void) | void; // a returned function is the undo
|
|
3051
|
-
type
|
|
2952
|
+
type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
|
|
3052
2953
|
type TOIMSameValueGuard<TValue = unknown> = (prev: TValue, next: TValue) => boolean;
|
|
3053
2954
|
type TOIMChannelScheduler = (flush: () => void) => void;
|
|
3054
|
-
type TOIMSortColumn<TEntity extends object> = {
|
|
2955
|
+
type TOIMSortColumn<TEntity extends object> = { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' };
|
|
3055
2956
|
type TOIMSortableColumnName<TEntity extends object> = // a column whose values are string | number | bigint
|
|
3056
2957
|
{ [TColumn in keyof TEntity]-?: NonNullable<TEntity[TColumn]> extends string | number | bigint ? TColumn : never }[keyof TEntity] & string;
|
|
3057
2958
|
|
|
3058
2959
|
// EVERY property is `readonly`. A slot is written through its owner and its
|
|
3059
|
-
// service, never by assigning to it; `
|
|
2960
|
+
// service, never by assigning to it; `TOIMEditable` is how the engine
|
|
3060
2961
|
// itself drops that guard where it is the one mutator.
|
|
3061
2962
|
type TOIMSlot = {
|
|
3062
2963
|
readonly effects: TOIMEffect[] | undefined;
|
|
@@ -3067,9 +2968,9 @@ type TOIMSlot = {
|
|
|
3067
2968
|
readonly releaseDueAfterWriteMs: number; // -1 until a write stamps it
|
|
3068
2969
|
readonly releaseDueAfterIdleMs: number; // -1 until idleness stamps it
|
|
3069
2970
|
readonly releaseDueAfterUnholdMs: number; // -1 until the last unhold stamps it
|
|
3070
|
-
readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2
|
|
2971
|
+
readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2; bits up to 32 are the engine's
|
|
3071
2972
|
readonly columnSlotStore: unknown; // the column slots this slot carries; undefined while none
|
|
3072
|
-
readonly storeSlot: TOIMStoreSlot
|
|
2973
|
+
readonly storeSlot: TOIMStoreSlot; // the store slot it belongs to; a column slot's is its carrier's
|
|
3073
2974
|
readonly owner: IOIMSlotOwner; // never undefined, never cleared
|
|
3074
2975
|
};
|
|
3075
2976
|
|
|
@@ -3079,12 +2980,14 @@ type TOIMSlot = {
|
|
|
3079
2980
|
type TOIMStoreSlot<TStore = unknown, TSlot extends TOIMSlot = TOIMSlot, TPatch = unknown> = {
|
|
3080
2981
|
readonly store: TStore;
|
|
3081
2982
|
readonly channelSlot: TOIMChannelSlot;
|
|
3082
|
-
readonly contentChangeListeners:
|
|
2983
|
+
readonly contentChangeListeners: IOIMSlotContentChangeListener<TSlot, TPatch>[] | undefined;
|
|
3083
2984
|
readonly contentChangeListenerReadColumnNames: (readonly string[] | undefined)[] | undefined;
|
|
3084
2985
|
readonly indexKeysPositions: TOIMIndexKeysPositions | undefined;
|
|
3085
2986
|
readonly sourceStoreSlot: TOIMStoreSlot | undefined; // an index store slot's source
|
|
3086
2987
|
readonly isDisposed: boolean; // an index store slot after disposeIndexStoreSlot
|
|
3087
|
-
|
|
2988
|
+
readonly slotService: TSlotService; // the service that created the store slot
|
|
2989
|
+
readonly batchDepth: number; // openBatch / closeBatch nesting
|
|
2990
|
+
// … column-reader counts, column groups, batch queues, bucket write queues: the services' own
|
|
3088
2991
|
};
|
|
3089
2992
|
|
|
3090
2993
|
type TOIMSingleSlotStore<TSlot> = { slot: TSlot | undefined };
|
|
@@ -3094,7 +2997,7 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
|
|
|
3094
2997
|
TOIMStoreSlot<TStore, TBucketSlot, TBucketPatch> & {
|
|
3095
2998
|
readonly sourceStoreSlot: TOIMStoreSlot<TSourceStore, TSourceSlot, TSourcePatch>;
|
|
3096
2999
|
readonly indexKeysPosition: number;
|
|
3097
|
-
readonly contentChangeListener:
|
|
3000
|
+
readonly contentChangeListener: IOIMSlotContentChangeListener<TSourceSlot, TSourcePatch>;
|
|
3098
3001
|
readonly requestedKeys: readonly TKey[] | undefined;
|
|
3099
3002
|
readonly bucketColumnNames: Set<string>;
|
|
3100
3003
|
readonly isAnyBucketColumnRead: boolean;
|
|
@@ -3104,10 +3007,10 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
|
|
|
3104
3007
|
type TOIMComputedStoreSlot<TStore = unknown, TValue = unknown, TKey = unknown> =
|
|
3105
3008
|
TOIMStoreSlot<TStore, TOIMComputedSlot<TValue, TKey>> & {
|
|
3106
3009
|
readonly derive: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue) | undefined;
|
|
3107
|
-
readonly createSources: ((
|
|
3010
|
+
readonly createSources: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>) | undefined;
|
|
3108
3011
|
};
|
|
3109
3012
|
|
|
3110
|
-
type
|
|
3013
|
+
type TOIMEditable<TSlot> = { -readonly [K in keyof TSlot]: TSlot[K] };
|
|
3111
3014
|
|
|
3112
3015
|
type TOIMKeyedSlot<TKey = unknown> = TOIMSlot & { readonly key: TKey };
|
|
3113
3016
|
type TOIMIndexableSlot<TKey = unknown> = TOIMKeyedSlot<TKey> & { readonly indexKeys: unknown[] | undefined };
|
|
@@ -3118,8 +3021,8 @@ type TOIMEntitySlot<TEntity extends object = object, TKey = unknown> = TOIMIndex
|
|
|
3118
3021
|
};
|
|
3119
3022
|
|
|
3120
3023
|
// `data` belongs to the consumer: the core never reads or writes it, and clearing or
|
|
3121
|
-
// removing the slot (`slotContentService.
|
|
3122
|
-
// last key for the entity slot in `indexKeys`, through the
|
|
3024
|
+
// removing the slot (`slotContentService.clearSlotContent(slot)`) does not reset it. Each index keeps its
|
|
3025
|
+
// last key for the entity slot in `indexKeys`, through the index's `IOIMIndexKeysService`.
|
|
3123
3026
|
|
|
3124
3027
|
type TOIMColumnSlot = TOIMSlot & {
|
|
3125
3028
|
readonly carrierSlot: TOIMSlot; // the entity slot or bucket slot it hangs off
|
|
@@ -3128,39 +3031,39 @@ type TOIMColumnSlot = TOIMSlot & {
|
|
|
3128
3031
|
|
|
3129
3032
|
type TOIMComputedColumnSlot = TOIMComputedSlot<unknown, string>; // a computed's getColumn / holdColumn return it; it goes in createEffect's second list
|
|
3130
3033
|
|
|
3131
|
-
type
|
|
3034
|
+
type TOIMBucketContentSlot<TSlots = unknown> = TOIMSlot & {
|
|
3132
3035
|
readonly owner: IOIMSlotOwner;
|
|
3133
|
-
readonly storeSlot: TOIMStoreSlot<unknown,
|
|
3036
|
+
readonly storeSlot: TOIMStoreSlot<unknown, TOIMBucketContentSlot<TSlots>>;
|
|
3134
3037
|
readonly bucketSlotsStore: TSlots;
|
|
3135
3038
|
readonly commands: readonly TOIMOrderedListCommand[] | undefined; // emptied when the flush has ended
|
|
3136
3039
|
readonly isWholeListRecorded: boolean; // reset to false with it
|
|
3137
3040
|
readonly commandsStartedAtRevision: number; // the clock the script started at
|
|
3138
3041
|
readonly commandsStartedAtEpoch: number;
|
|
3139
|
-
readonly wasEmptyBeforeChange: boolean | undefined;
|
|
3140
|
-
readonly isChangedAgainInSubframe: boolean;
|
|
3141
3042
|
};
|
|
3043
|
+
type TOIMBucketSlot<TSlots = unknown, TKey extends NonNullable<unknown> = NonNullable<unknown>> =
|
|
3044
|
+
TOIMBucketContentSlot<TSlots> & TOIMIndexableSlot<TKey>; // a keyed bucket slot; a single bucket is the content slot alone
|
|
3142
3045
|
|
|
3143
3046
|
// `holdSlotByKey(storeSlot, key)` hands back the slot itself; give the hold back with `slot.owner.unholdSlot(slot)`.
|
|
3144
3047
|
|
|
3145
3048
|
type TOIMBucketSlotsSetStore<TBucketMemberSlot extends TOIMSlot = TOIMSlot> = Set<TBucketMemberSlot>;
|
|
3146
3049
|
type TOIMBucketSlotsArrayStore<TBucketMemberSlot extends TOIMSlot = TOIMSlot> = {
|
|
3147
3050
|
bucketMemberSlots: TBucketMemberSlot[];
|
|
3051
|
+
uniqueBucketMemberSlots: Set<TBucketMemberSlot> | undefined; // kept by OIMBucketUniqueMemberSlotsArrayService
|
|
3052
|
+
sortedCount: number;
|
|
3148
3053
|
};
|
|
3149
|
-
type TOIMBucketSlotsArrayUniqueStore<TBucketMemberSlot extends TOIMSlot = TOIMSlot> =
|
|
3150
|
-
TOIMBucketSlotsArrayStore<TBucketMemberSlot> & { uniqueBucketMemberSlots: Set<TBucketMemberSlot> };
|
|
3151
3054
|
|
|
3152
3055
|
type TOIMEntityColumnName<TEntity extends object> =
|
|
3153
3056
|
[TEntity] extends [never] ? never
|
|
3154
3057
|
: object extends TEntity ? string : keyof TEntity & string;
|
|
3155
3058
|
|
|
3156
|
-
type
|
|
3059
|
+
type TOIMComputedDerivation = TOIMComputedSources & { // what a computed slot and a TOIMEffect share
|
|
3157
3060
|
ranAtRevision: number; ranAtEpoch: number;
|
|
3158
3061
|
};
|
|
3159
3062
|
|
|
3160
3063
|
type TOIMTimer = (run: () => void, inMs: number) => TOIMDisposer;
|
|
3161
3064
|
```
|
|
3162
3065
|
|
|
3163
|
-
**`
|
|
3066
|
+
**`TOIMEditable<TSlot>` strips `readonly` from every property.** It is exported
|
|
3164
3067
|
because the rule "data has one mutator" is enforced by the slot types being
|
|
3165
3068
|
readonly everywhere else: a class that IS the sole mutator of a slot's property
|
|
3166
3069
|
casts through this type at the one place it writes, and every other reader is
|
|
@@ -3182,103 +3085,103 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3182
3085
|
|
|
3183
3086
|
## Full export list
|
|
3184
3087
|
|
|
3185
|
-
**
|
|
3088
|
+
**230 names, every export of `packages/core/src/index.ts`, grouped by topic.**
|
|
3186
3089
|
|
|
3187
3090
|
*Channel and database slots* — `OIMChannelService` · `TOIMChannelSlot` · `TOIMChannelSlotOptions` · `createOIMChannelSlot` ·
|
|
3188
|
-
`IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot`
|
|
3091
|
+
`IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot` · `IOIMFlushSchedulerListener`
|
|
3189
3092
|
|
|
3190
3093
|
*Collector services and slots* — `OIMUnusedSlotCollectorService` · `TOIMUnusedSlotCollectorSlot` ·
|
|
3191
3094
|
`createOIMUnusedSlotCollectorSlot` · `TOIMCollectorSlot` · `TOIMCollectorSlotOptions` · `createOIMCollectorSlot` ·
|
|
3192
|
-
`
|
|
3193
|
-
`
|
|
3194
|
-
`
|
|
3195
|
-
|
|
3196
|
-
*Slot services* — `
|
|
3197
|
-
`
|
|
3198
|
-
`
|
|
3199
|
-
`
|
|
3200
|
-
`
|
|
3095
|
+
`OIMSlotWheel` · `TOIMSlotWheelOptions` · `OIMSlotReleaseWheel` · `TOIMSlotReleaseWheelOptions` ·
|
|
3096
|
+
`oimMintAfterWriteSlotReleaseWheel` · `oimMintAfterIdleSlotReleaseWheel` · `oimMintAfterUnholdSlotReleaseWheel` ·
|
|
3097
|
+
`OIMSlotRefetchWheel` · `TOIMSlotRefetchWheelOptions` · `IOIMSlotRefetchListener`
|
|
3098
|
+
|
|
3099
|
+
*Slot services* — `OIMSlotCollectionService` · `OIMWritableSlotCollectionService` · `TOIMSlotCollectionServiceOptions` ·
|
|
3100
|
+
`createOIMSlotCollectionService` · `OIMSingleSlotService` · `OIMWritableSingleSlotService` · `TOIMSingleSlotServiceOptions` ·
|
|
3101
|
+
`createOIMSingleSlotService` · `OIMColumnSlotService` (the column doors, `service.columnSlotService`) ·
|
|
3102
|
+
`OIMSlotOwnerService` (**type only**) · `TOIMSlotOwnerServiceOptions` · `IOIMSlotContentService` · `IOIMSlotContentWriter` ·
|
|
3103
|
+
`IOIMKeyedSlotCreator` · `IOIMSingleSlotCreator` · `IOIMSlotFilingService` · `TOIMSlotContentServiceKey` ·
|
|
3104
|
+
`TOIMSlotContentServiceSlot` · `TOIMSlotContentServiceSingleSlot` · `TOIMSlotContentServiceContent` ·
|
|
3105
|
+
`TOIMSlotContentServicePatch` · `OIMSlotContentChangeListenerService`
|
|
3201
3106
|
|
|
3202
3107
|
*Content services* — `OIMSlotContentEntityService` · `TOIMSlotContentEntityServiceOptions` ·
|
|
3203
|
-
`createOIMSlotContentEntityService` · `
|
|
3204
|
-
`
|
|
3205
|
-
`
|
|
3206
|
-
`
|
|
3207
|
-
`
|
|
3208
|
-
`
|
|
3108
|
+
`createOIMSlotContentEntityService` · `TOIMEntitySlotOwnerService` · `TOIMEntityContentSlot` · `OIMSlotContentBucketService` ·
|
|
3109
|
+
`OIMSlotContentArrayBucketService` · `OIMSlotContentPositionalArrayBucketService` · `OIMSlotContentSetBucketService` ·
|
|
3110
|
+
`OIMSlotContentSortedArrayBucketService` · `TOIMSlotContentBucketServiceOptions` ·
|
|
3111
|
+
`TOIMSlotContentArrayBucketServiceOptions` · `TOIMSlotContentSortedArrayBucketServiceOptions` · `TOIMBucketSlotOwnerService` ·
|
|
3112
|
+
`TOIMBucketContentSlot` · `OIMSlotContentComputedService` (**type only**) · `TOIMSlotContentComputedServiceOptions` ·
|
|
3113
|
+
`TOIMComputedContentSlot`
|
|
3209
3114
|
|
|
3210
|
-
*Index and computed store slots* — `
|
|
3211
|
-
`
|
|
3212
|
-
`TOIMIndexServiceStoreSlot` · `TOIMSortedComputedSlot` · `TOIMBucketMemoComputedSlot`
|
|
3115
|
+
*Index and computed store slots* — `createOIMSlotCollectionIndexService` · `TOIMSlotCollectionIndexServiceFactoryOptions` ·
|
|
3116
|
+
`TOIMSlotCollectionIndexServiceStoreSlot` · `TOIMSortedComputedSlot` · `TOIMBucketMemoComputedSlot`
|
|
3213
3117
|
|
|
3214
3118
|
*Runtime* — `TOIMChannelFrame` · `TOIMChannelClock` ·
|
|
3215
|
-
`TOIMChannelScheduler` · `TOIMDisposer` · `IOIMErrorPolicy` · `
|
|
3216
|
-
`
|
|
3119
|
+
`TOIMChannelScheduler` · `TOIMDisposer` · `IOIMErrorPolicy` · `OIMLogAndContinueErrorPolicy` ·
|
|
3120
|
+
`TOIMLogAndContinueErrorPolicyOptions` · `OIMThrowAtOnceErrorPolicy` · `OIMChannelQueue` (**type only**) ·
|
|
3217
3121
|
`oimIsAnySlotChangedAfter`
|
|
3218
3122
|
|
|
3219
3123
|
`oimIsAnySlotChangedAfter(slots, revision, epoch): boolean` answers whether any of the slots was
|
|
3220
3124
|
written after that clock reading — `channelSlot.clock` read earlier.
|
|
3221
3125
|
|
|
3222
3126
|
*Derivations* — `OIMEffectService` · `IOIMEffectService` · `TOIMEffect` (**type only** —
|
|
3223
|
-
`OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `
|
|
3224
|
-
`
|
|
3225
|
-
`OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `
|
|
3127
|
+
`OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `TOIMSourceTrackingEffectBody` ·
|
|
3128
|
+
`OIMSourceTracker` (**type only** — the service builds it) · `TOIMComputedSlot` · `TOIMComputedSources` · `TOIMComputedValueReleaser` · `OIMSourceTrackingComputedService` · `OIMSingleSourceTrackingComputedService` · `createOIMSourceTrackingComputedService` · `createOIMSingleSourceTrackingComputedService` · `TOIMSourceTrackingComputedSlot` · `TOIMSourceTrackingComputedContentSlot` · `TOIMSourceTrackingComputedDerive` · `OIMSlotContentSourceTrackingComputedService` (**type only**) · `TOIMSourceTrackingComputedDerivationOptions` · `TOIMComputedServiceFactoryOptions` ·
|
|
3129
|
+
`OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `TOIMComputedDerivation`
|
|
3226
3130
|
|
|
3227
3131
|
`oimRefreshSourceComputeds(sourceComputeds)` brings every computed in the list current. A binding
|
|
3228
3132
|
read outside an effect (`useOIMValue` in `@oimdb/react`, `exoBinding` in `@oimdb/exodra`) calls it
|
|
3229
3133
|
on its `sourceComputeds` before `derive`, so the derive never reads a stale computed.
|
|
3230
3134
|
|
|
3231
|
-
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `
|
|
3232
|
-
`TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `
|
|
3233
|
-
`
|
|
3135
|
+
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
|
|
3136
|
+
`TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `OIMColumnSlotOwner` ·
|
|
3137
|
+
`OIMPushColumnSlotOwner` · `OIMPullColumnSlotOwner` · `TOIMColumnSlotOwnerOptions` · `TOIMBucketSlot` ·
|
|
3234
3138
|
`TOIMSameValueGuard` · `OIMSlotOwner` ·
|
|
3235
|
-
`IOIMSlotOwner` · `
|
|
3236
|
-
`IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `
|
|
3139
|
+
`IOIMSlotOwner` · `IOIMSlotEmptinessPolicy` ·
|
|
3140
|
+
`IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMSlotContentChangeListener`
|
|
3237
3141
|
|
|
3238
3142
|
`oimIsSlotEmpty(slot): boolean` reads the EMPTY bit of `slot.slotFlags`.
|
|
3239
3143
|
`oimIsSlotReleased(slot): boolean` reads its RELEASED bit.
|
|
3240
3144
|
|
|
3241
3145
|
*Release policies* — `IOIMSlotReleasePolicy` ·
|
|
3242
|
-
`
|
|
3243
|
-
`
|
|
3244
|
-
`
|
|
3245
|
-
`
|
|
3146
|
+
`OIMSlotAfterWriteReleasePolicy` · `TOIMSlotAfterWriteReleasePolicyOptions` ·
|
|
3147
|
+
`IOIMSlotReleaseDueFramePolicy` · `OIMSlotReleaseAutoDueFramePolicy` · `OIMSlotReleaseManualDueFramePolicy` ·
|
|
3148
|
+
`OIMSlotAfterIdleReleasePolicy` · `TOIMSlotAfterIdleReleasePolicyOptions` ·
|
|
3149
|
+
`OIMSlotAfterUnholdReleasePolicy` · `TOIMSlotAfterUnholdReleasePolicyOptions` ·
|
|
3150
|
+
`OIMSlotAtUnholdReleasePolicy` · `OIMSlotAtEmptyReleasePolicy` · `OIMSlotNeverReleasePolicy` · `OIMSlotEarliestReleasePolicy` ·
|
|
3246
3151
|
`TOIMTimer` · `oimSliceOfMs` · `OIM_SLICE_LENGTH_MS`
|
|
3247
3152
|
|
|
3248
|
-
*Entity writers* — `IOIMEntityWriter` · `
|
|
3249
|
-
`
|
|
3250
|
-
`oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `
|
|
3153
|
+
*Entity writers* — `IOIMEntityWriter` · `createOIMInPlaceEntityWriter` ·
|
|
3154
|
+
`createOIMReplacingEntityWriter` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
|
|
3155
|
+
`oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimGuardedPatchEntityColumns` · `oimGuardedPatchEntityColumn` · `oimPutEntityColumns` · `oimGuardedPutEntityColumns` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
|
|
3251
3156
|
|
|
3252
|
-
*Buckets* — `
|
|
3253
|
-
`
|
|
3254
|
-
`
|
|
3255
|
-
`OIMBucketArrayMembershipUnique` · `TOIMBucketSlotsSetStore` · `TOIMBucketSlotsArrayStore` ·
|
|
3256
|
-
`TOIMBucketSlotsSortedArrayStore` · `TOIMBucketSlotsArrayUniqueStore` · `OIM_NOTHING_WAS_INSERTED` ·
|
|
3157
|
+
*Buckets* — `OIMBucketCommands` · `TOIMBucketCommandsOptions` · `IOIMBucketCommands` ·
|
|
3158
|
+
`IOIMBucketMemberSlotsArrayService` · `OIMBucketMemberSlotsArrayService` · `OIMBucketUniqueMemberSlotsArrayService` ·
|
|
3159
|
+
`TOIMBucketSlotsSetStore` · `TOIMBucketSlotsArrayStore` · `OIM_NOTHING_WAS_INSERTED` ·
|
|
3257
3160
|
`OIM_NOTHING_WAS_REMOVED` · `OIM_FROZEN_EMPTY_SLOTS` · `OIM_REMOVED_WITHOUT_A_POSITION`
|
|
3258
3161
|
|
|
3259
|
-
*Index services* — `
|
|
3260
|
-
`
|
|
3162
|
+
*Index services* — `OIMSlotCollectionIndexService` · `OIMSlotCollectionSortedArrayIndexService` · `TOIMSlotCollectionIndexServiceOptions` · `TOIMIndexStoreSlot` ·
|
|
3163
|
+
`createOIMSlotCollectionSortedArrayIndexService` · `TOIMIndexKeyOptions` · `TOIMIndexKey` ·
|
|
3261
3164
|
`IOIMIndexKeysService` · `TOIMIndexKeysPositions` · `OIMIndexKeysService` ·
|
|
3262
|
-
`
|
|
3165
|
+
`OIMAppendingIndexKeysService` · `OIMReclaimingIndexKeysService`
|
|
3263
3166
|
|
|
3264
3167
|
*Computed services* — `OIMComputedService` · `TOIMComputedServiceOptions` ·
|
|
3265
|
-
`createOIMComputedService` · `TOIMComputedStoreSlot` · `
|
|
3266
|
-
`
|
|
3267
|
-
`
|
|
3168
|
+
`createOIMComputedService` · `TOIMComputedStoreSlot` · `OIMSortedComputedService` · `TOIMSortedComputedServiceOptions` ·
|
|
3169
|
+
`createOIMSortedComputedService` · `OIMBucketMemoComputedService` · `TOIMBucketMemoComputedServiceOptions` ·
|
|
3170
|
+
`createOIMBucketMemoComputedService` · `OIMSingleComputedService` · `createOIMSingleComputedService` ·
|
|
3171
|
+
`TOIMComputedDerivationOptions` · `TOIMSortColumn` · `TOIMSortableColumnName` · `oimColumnsComparator`
|
|
3268
3172
|
|
|
3269
3173
|
*Stores and keys* — `TOIMStoreKey` · `IOIMSlotStoreService` ·
|
|
3270
|
-
`
|
|
3271
|
-
`
|
|
3174
|
+
`OIMSlotMapStoreService` · `OIMSlotTreeStoreService` ·
|
|
3175
|
+
`OIMSingleSlotStoreService` · `TOIMSingleSlotStore` · `TOIMDefaultSlotStore` ·
|
|
3272
3176
|
`TOIMSlotStoreTree` · `IOIMKeyTreeWalk` · `TOIMCompositeKeyNode` ·
|
|
3273
|
-
`OIMKeyTreeWalk` · `
|
|
3177
|
+
`OIMKeyTreeWalk` · `OIMCompositeKeyTreeWalk` · `OIMScalarKeyTreeWalk`
|
|
3274
3178
|
|
|
3275
|
-
*Assemblies* — `OIMDB` · `OIMDefaults` · `OIMDefaultValue` · `TOIMFactoryOptions`
|
|
3179
|
+
*Assemblies* — `OIMDB` · `OIMDefaults` · `TOIMDefaultsOptions` · `OIMDefaultValue` · `TOIMFactoryOptions`
|
|
3276
3180
|
|
|
3277
3181
|
*Comparison and diffing* — `oimSameValueZeroGuard` ·
|
|
3278
3182
|
`oimSameSlotSequence` · `oimSameKeySequence` · `oimOrderedListCommandsOfTransition` · `TOIMOrderedListCommand`
|
|
3279
3183
|
|
|
3280
|
-
*Shared defaults* — `
|
|
3281
|
-
`OIMDefaults.KEY_TREE_WALK_SCALAR` and `OIMDefaults.KEY_TREE_WALK_COMPOSITE`.
|
|
3184
|
+
*Shared defaults* — `OIMScalarKeyTreeWalk.INSTANCE` and `OIMCompositeKeyTreeWalk.INSTANCE`.
|
|
3282
3185
|
|
|
3283
3186
|
**Nothing else is exported.** If a symbol is not on this list, it does not exist.
|
|
3284
3187
|
|
|
@@ -3286,18 +3189,18 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3286
3189
|
|
|
3287
3190
|
## Known defects
|
|
3288
3191
|
|
|
3289
|
-
### A scalar in a
|
|
3192
|
+
### A scalar in a key of the composite key tree walk throws
|
|
3290
3193
|
|
|
3291
|
-
`
|
|
3194
|
+
`OIMSlotTreeStoreService` built with `OIMCompositeKeyTreeWalk.INSTANCE` keys on
|
|
3292
3195
|
`readonly TOIMEntitySlot[]`, so a string or a number in the key throws
|
|
3293
3196
|
a TypeError past a cast, setting `holdCount` on a string. The store for a tuple
|
|
3294
|
-
of scalars is `
|
|
3295
|
-
`
|
|
3197
|
+
of scalars is `OIMSlotTreeStoreService` built with
|
|
3198
|
+
`OIMScalarKeyTreeWalk.INSTANCE` —
|
|
3296
3199
|
see **Key stores** above. The fix is the other walk, never a cast.
|
|
3297
3200
|
|
|
3298
3201
|
### `TOIMEffect` is a type-only export
|
|
3299
3202
|
|
|
3300
|
-
`effectService.createEffect(...)` and `effectService.
|
|
3203
|
+
`effectService.createEffect(...)` and `effectService.createAndRunEffect(...)` are the whole of
|
|
3301
3204
|
legitimate construction. An object literal typed `TOIMEffect` subscribes to nothing and
|
|
3302
3205
|
schedules nothing: when one of its sources already moved in the open batch, a
|
|
3303
3206
|
later write to that source is swallowed by the coalescing guard and the effect
|
|
@@ -3311,8 +3214,8 @@ Every `@oimdb` package ships both formats — `dist/index.js` for `import`,
|
|
|
3311
3214
|
condition so a CommonJS consumer's declarations match its runtime), both named
|
|
3312
3215
|
in the `exports` map. A runtime loads a package once per format, so a process
|
|
3313
3216
|
that reaches the core both ways would hold two copies of every class and every
|
|
3314
|
-
module-level value — two
|
|
3315
|
-
|
|
3217
|
+
module-level value — two sets of built-in defaults and two `OIMDefaults` classes,
|
|
3218
|
+
so services built by the two copies share nothing.
|
|
3316
3219
|
|
|
3317
3220
|
So `src/index.ts` registers its copy under `Symbol.for('@oimdb/core')` on
|
|
3318
3221
|
`globalThis`, and the second copy to load throws
|
|
@@ -3330,8 +3233,8 @@ core objects around — through both spellings in one process. Pick one.
|
|
|
3330
3233
|
|
|
3331
3234
|
## Other packages
|
|
3332
3235
|
|
|
3333
|
-
`@oimdb/react`, `@oimdb/persist` (with its five backends) and `@oimdb/
|
|
3334
|
-
on the current core — persist and
|
|
3236
|
+
`@oimdb/react`, `@oimdb/persist` (with its five backends), `@oimdb/exodra` and `@oimdb/browser` build
|
|
3237
|
+
on the current core — persist, exodra and browser have sections of their own below. Every
|
|
3335
3238
|
remaining package imports only names the core exports. Read a package's own
|
|
3336
3239
|
source before generating code against it.
|
|
3337
3240
|
|
|
@@ -3358,14 +3261,14 @@ The computed SERVICE is the caller's, built once at module scope with `createOIM
|
|
|
3358
3261
|
slot per mount — `computedService.createComputedStoreSlot(db, derive, createSources)` in a `useMemo` on `deps`;
|
|
3359
3262
|
`derive` / `createSources` may be `undefined` when the service has them.
|
|
3360
3263
|
`useOIMSingleComputedValue(computedService, derive, createSources, deps): TValue` is the same for a void-keyed
|
|
3361
|
-
service whose `computedSlotStoreService` is `
|
|
3264
|
+
service whose `computedSlotStoreService` is `OIMSingleSlotStoreService.INSTANCE`, read through
|
|
3362
3265
|
`useOIMComputedValueByKey(computedService.computedCollectionService, computedStoreSlot, undefined)`, so a flush
|
|
3363
3266
|
between render and subscribe is safe.
|
|
3364
3267
|
`useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)` holds the identities its keys name for as long as the
|
|
3365
3268
|
component is mounted (a `holdSlotByKey` per key on commit, the same holds given back on unmount or when the list
|
|
3366
3269
|
moves) and subscribes to nothing; it needs no provider.
|
|
3367
|
-
Singles are not keyed collections: `
|
|
3368
|
-
`
|
|
3270
|
+
Singles are not keyed collections: `createOIMSingleSlotService({ slotContentService })` returns an
|
|
3271
|
+
`OIMWritableSingleSlotService` (`getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`), read by
|
|
3369
3272
|
`useOIMSingleEntity(singleSlotService, singleSlotStoreSlot): TEntity | undefined` and
|
|
3370
3273
|
`useOIMSingleBucketSlots(singleSlotService, singleSlotStoreSlot): readonly TOIMEntitySlot<TEntity>[]` — the by-key
|
|
3371
3274
|
mechanics without a key (getSingleSlot on render, holdSingleSlot on commit, resolve-hold given back once the
|
|
@@ -3376,11 +3279,12 @@ re-read the imports rather than trusting this table.
|
|
|
3376
3279
|
|
|
3377
3280
|
| Package | Imports from the core | Imports a name the core does not export? |
|
|
3378
3281
|
|---|---|---|
|
|
3379
|
-
| `@oimdb/persist` | `IOIMChannelMap`, `
|
|
3380
|
-
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `
|
|
3282
|
+
| `@oimdb/persist` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `IOIMEntityWriter`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMChannelClock`, `TOIMChannelSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMSingleSlotStore`, `TOIMSingleSlotStoreSlot`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimIsAnySlotChangedAfter`, `oimIsSlotReleased`, `oimSameValueZeroGuard`, `oimUnholdSlotsIfAlive` | no |
|
|
3283
|
+
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntityColumnName`, `TOIMEntitySlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
3381
3284
|
| `@oimdb/exodra` | `IOIMChannelMap`, `OIMChannelService`, `OIMEffectService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMComputedSlot`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMOrderedListCommand`, `TOIMSlot`, `TOIMStoreKey`, `oimOrderedListCommandsOfTransition`, `oimRefreshSourceComputeds`, `oimSameKeySequence` | no |
|
|
3382
3285
|
| `@oimdb/react` | `IOIMChannelMap`, `OIMComputedService`, `OIMDB`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMComputedSlot`, `TOIMComputedSources`, `TOIMComputedStoreSlot`, `TOIMDisposer`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`, `TOIMEntitySlot`, `TOIMIndexableSlot`, `TOIMKeyedSlot`, `TOIMSingleSlotStore`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimRefreshSourceComputeds`, `oimSameKeySequence`, `oimUnholdSlotsIfAlive` | no |
|
|
3383
3286
|
| `@oimdb/devtools` | `IOIMChannelMap`, `IOIMStoreSlotListener`, `OIMChannelService`, `OIMComputedService`, `OIMDefaults`, `OIMSlotCollectionService`, `TOIMBucketSlot`, `TOIMComputedStoreSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEntitySlot`, `TOIMKeyedSlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
3287
|
+
| `@oimdb/browser` | `IOIMSlotSubscriptionListener`, `OIMChannelService`, `OIMDefaults`, `OIMSingleSlotService`, `OIMSlotContentEntityService`, `TOIMDisposer`, `TOIMEntityContentSlot`, `TOIMSlot`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`, `oimIsSlotReleased` | no |
|
|
3384
3288
|
| `@oimdb/async` | — | deprecated stub, `export {}` |
|
|
3385
3289
|
| `@oimdb/snapshot-manager` | — | empty stub |
|
|
3386
3290
|
|
|
@@ -3398,10 +3302,10 @@ Every constructor in the family takes ONE options object.
|
|
|
3398
3302
|
| package | exports |
|
|
3399
3303
|
|---|---|
|
|
3400
3304
|
| `@oimdb/persist` | the engine, plus the key-value backend `OIMPersistKeyValueBackend` and its strategies; error types `TOIMPersistErrorHandler`, `TOIMPersistErrorContext`, `TOIMPersistOperation` and `TOIMPersistErrorReport` (`{ error, persistErrorContext }`, one failure as `onPersistError` receives it or as it lands in the `AggregateError`) |
|
|
3401
|
-
| `@oimdb/persist-memory` | `OIMPersistMemoryBackend`, `OIMPersistMemoryWriteBatch`, `
|
|
3402
|
-
| `@oimdb/persist-json` | `
|
|
3403
|
-
| `@oimdb/persist-idb` | `OIMPersistIndexedDbBackend`, `OIMPersistIndexedDbWriteBatch`, `
|
|
3404
|
-
| `@oimdb/persist-localstorage` | `
|
|
3305
|
+
| `@oimdb/persist-memory` | `OIMPersistMemoryBackend`, `OIMPersistMemoryWriteBatch`, `OIMPersistMemoryRecordsService`, `OIMPersistMemorySnapshotService`, `TOIMPersistMemoryServiceOptions` |
|
|
3306
|
+
| `@oimdb/persist-json` | `OIMPersistJsonMemoryBackend`, `TOIMPersistJsonMemoryBackendOptions`, `TOIMPersistJsonDump`; depends on `@oimdb/persist-memory` |
|
|
3307
|
+
| `@oimdb/persist-idb` | `OIMPersistIndexedDbBackend`, `OIMPersistIndexedDbWriteBatch`, `OIMPersistIndexedDbRecordsService`, `OIMPersistIndexedDbSnapshotService`, `TOIMPersistIndexedDbBackendOptions`, `TOIMPersistIndexedDbRecordsServiceOptions`, `TOIMPersistIndexedDbSnapshotServiceOptions`, `TOIMPersistIndexedDbPrimaryKey`, `TOIMPersistIndexedDbTableRecord` |
|
|
3308
|
+
| `@oimdb/persist-localstorage` | `OIMPersistLocalStorageKeyValueBackend`, `TOIMPersistLocalStorageKeyValueBackendOptions` |
|
|
3405
3309
|
| `@oimdb/persist-async-kv` | `OIMPersistAsyncKVStorage`, `TOIMPersistAsyncKVStorageOptions`, `TOIMPersistAsyncKVMultiStorage` |
|
|
3406
3310
|
|
|
3407
3311
|
### The wiring
|
|
@@ -3412,42 +3316,37 @@ lives in the backend) and what to do on hydrate.
|
|
|
3412
3316
|
|
|
3413
3317
|
```ts
|
|
3414
3318
|
import {
|
|
3415
|
-
OIMPersistEntityWriter,
|
|
3416
|
-
|
|
3319
|
+
OIMPersistEntityWriter, OIMPersistCollectionWriteRecordSource, OIMPersistRecordsResource,
|
|
3320
|
+
OIMPersistMicrotaskScheduler, OIMPersistor,
|
|
3417
3321
|
} from '@oimdb/persist';
|
|
3418
|
-
import { OIMPersistMemoryBackend,
|
|
3322
|
+
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3419
3323
|
|
|
3420
3324
|
const persistEntityWriter = new OIMPersistEntityWriter<Card, TOIMStoreKey>({
|
|
3421
|
-
entityWriter:
|
|
3422
|
-
});
|
|
3423
|
-
const persistedCardCollectionService = createOIMEntityService<Card>(db, {
|
|
3424
|
-
keyOf: card => card.id,
|
|
3425
|
-
entityWriter: persistEntityWriter,
|
|
3325
|
+
entityWriter: createOIMInPlaceEntityWriter(),
|
|
3426
3326
|
});
|
|
3327
|
+
const persistedCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>({ entityWriter: persistEntityWriter }) });
|
|
3427
3328
|
const persistedCardEntityStoreSlot = persistedCardCollectionService.createStoreSlot(db);
|
|
3428
3329
|
|
|
3429
3330
|
const persistMemoryBackend = new OIMPersistMemoryBackend();
|
|
3430
3331
|
const persistor = new OIMPersistor<OIMPersistMemoryBackend, OIMPersistMemoryWriteBatch>({
|
|
3431
3332
|
persistBackend: persistMemoryBackend,
|
|
3432
|
-
persistScheduler: new
|
|
3333
|
+
persistScheduler: new OIMPersistMicrotaskScheduler(),
|
|
3433
3334
|
onPersistError: (error, persistErrorContext) => {
|
|
3434
3335
|
console.error(persistErrorContext.persistOperation, error);
|
|
3435
3336
|
},
|
|
3436
3337
|
});
|
|
3437
|
-
const cardPersistResourceRecords = persistor.addPersistResource(new
|
|
3438
|
-
persistRecordSource: new
|
|
3338
|
+
const cardPersistResourceRecords = persistor.addPersistResource(new OIMPersistRecordsResource({
|
|
3339
|
+
persistRecordSource: new OIMPersistCollectionWriteRecordSource({
|
|
3439
3340
|
entityCollectionService: persistedCardCollectionService,
|
|
3440
3341
|
entityStoreSlot: persistedCardEntityStoreSlot,
|
|
3441
3342
|
persistEntityWriter,
|
|
3442
3343
|
}),
|
|
3443
|
-
|
|
3344
|
+
persistRecordsService: new OIMPersistMemoryRecordsService<TOIMStoreKey, Card>({ storageKey: 'cards' }),
|
|
3444
3345
|
}));
|
|
3445
3346
|
|
|
3446
3347
|
await persistor.hydrate();
|
|
3447
3348
|
persistor.start();
|
|
3448
|
-
persistedCardCollectionService.
|
|
3449
|
-
persistedCardEntityStoreSlot, { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' },
|
|
3450
|
-
);
|
|
3349
|
+
persistedCardCollectionService.writeSlotContentByKey(persistedCardEntityStoreSlot, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
|
|
3451
3350
|
await persistor.persist();
|
|
3452
3351
|
await persistor.unload(cardPersistResourceRecords, ['c1']);
|
|
3453
3352
|
await persistor.load(cardPersistResourceRecords, ['c1']);
|
|
@@ -3457,8 +3356,8 @@ Rules a generator must keep:
|
|
|
3457
3356
|
|
|
3458
3357
|
- Pass both generics of `OIMPersistor<TPersistBackend, TWriteBatch>`: the backend
|
|
3459
3358
|
class and its write batch class (`OIMPersistMemoryWriteBatch`, `OIMPersistIndexedDbWriteBatch`,
|
|
3460
|
-
`OIMPersistKeyValueWriteBatch`; `
|
|
3461
|
-
`
|
|
3359
|
+
`OIMPersistKeyValueWriteBatch`; `OIMPersistJsonMemoryBackend` uses `OIMPersistMemoryWriteBatch`,
|
|
3360
|
+
`OIMPersistLocalStorageKeyValueBackend` uses `OIMPersistKeyValueWriteBatch`).
|
|
3462
3361
|
- Pass the generics of strategies, codecs and `OIMPersistEntityWriter<TEntity, TKey>`
|
|
3463
3362
|
explicitly; their options carry no type to infer them from.
|
|
3464
3363
|
- An entity collection persisted through a collection source is built with the
|
|
@@ -3470,9 +3369,9 @@ Rules a generator must keep:
|
|
|
3470
3369
|
`new OIMPersistEntityWriter({ entityWriter })` wraps the `IOIMEntityWriter` that
|
|
3471
3370
|
really writes, and reports the entity slot of every fill, put, patch, column patch,
|
|
3472
3371
|
column delete and clear that changed something (`subscribeEntitySlotWritten`).
|
|
3473
|
-
- The entity collection service a source takes is any collection service with `getSlots`, `findSlotByKey
|
|
3474
|
-
`
|
|
3475
|
-
(`TOIMPersistEntityCollectionService`)
|
|
3372
|
+
- The entity collection service a source takes is any collection service with `getSlots`, `findSlotByKey`,
|
|
3373
|
+
`writeSlotContentByKey`, `writeInBatch` and a `slotContentService` with `clearSlotContent`
|
|
3374
|
+
(`TOIMPersistEntityCollectionService`): what `createOIMSlotCollectionService` over an entity content service builds.
|
|
3476
3375
|
- A source refuses, by name, a store slot on another channel slot than the one it was given (the bucket source
|
|
3477
3376
|
checks both the entity and the bucket store slot).
|
|
3478
3377
|
- Order: `addPersistResource` every resource, `await persistor.hydrate()`, then
|
|
@@ -3483,29 +3382,29 @@ Rules a generator must keep:
|
|
|
3483
3382
|
| constructor | options |
|
|
3484
3383
|
|---|---|
|
|
3485
3384
|
| `OIMPersistor` | `{ persistBackend, persistScheduler, onPersistError? }` |
|
|
3486
|
-
| `
|
|
3487
|
-
| `
|
|
3488
|
-
| `
|
|
3489
|
-
| `
|
|
3490
|
-
| `
|
|
3385
|
+
| `OIMPersistRecordsResource` | `{ persistRecordSource, persistRecordsService, persistHydrateReconcile? }` |
|
|
3386
|
+
| `OIMPersistSnapshotResource` | `{ persistSnapshotSource, persistSnapshotService, persistCodec, persistHydrateReconcile? }` |
|
|
3387
|
+
| `OIMPersistCollectionWriteRecordSource`, `OIMPersistCollectionChangeWriteRecordSource` | `{ entityCollectionService, entityStoreSlot, persistEntityWriter }` (`TOIMPersistRecordSourceCollectionOptions`) |
|
|
3388
|
+
| `OIMPersistBucketRecordSource` | `{ channelMap, entityCollectionService, entityStoreSlot, slotContentBucketService, bucketSlot }` — `channelMap` is the db, `slotContentBucketService` the bucket collection's `slotContentService` |
|
|
3389
|
+
| `OIMPersistSingleEntitySnapshotSource` | `{ channelMap, singleSlotService, entityStoreSlot }` — a single entity service (`createOIMSingleSlotService` over an entity content service) and one of its store slots (`TOIMSingleSlotStoreSlot`) |
|
|
3491
3390
|
| `OIMPersistEntityWriter` | `{ entityWriter }` |
|
|
3492
|
-
| `
|
|
3493
|
-
| `
|
|
3494
|
-
| `
|
|
3495
|
-
| `
|
|
3391
|
+
| `OIMPersistMicrotaskScheduler` | none |
|
|
3392
|
+
| `OIMPersistFrameScheduler` | `{ channelSlot }` — `db.defaultChannelSlot` |
|
|
3393
|
+
| `OIMPersistIdentityCodec` | none |
|
|
3394
|
+
| `OIMPersistVersionedCodec` | `{ version, migrations, persistCodec }` |
|
|
3496
3395
|
| `OIMPersistMemoryBackend` | none |
|
|
3497
|
-
| `
|
|
3498
|
-
| `
|
|
3396
|
+
| `OIMPersistMemoryRecordsService`, `OIMPersistMemorySnapshotService` | `{ storageKey }` |
|
|
3397
|
+
| `OIMPersistJsonMemoryBackend` | `{ initialPersistJsonDump? }`, the object itself optional |
|
|
3499
3398
|
| `OIMPersistIndexedDbBackend` | `{ databaseName, databaseVersion?, indexedDb? }` |
|
|
3500
|
-
| `
|
|
3501
|
-
| `
|
|
3399
|
+
| `OIMPersistIndexedDbRecordsService` | `{ tableName }` |
|
|
3400
|
+
| `OIMPersistIndexedDbSnapshotService` | `{ tableName, primaryKey }` |
|
|
3502
3401
|
| `OIMPersistKeyValueBackend` | `{ persistKeyValueStorage, persistKeyValueSerializer? }` |
|
|
3503
|
-
| `
|
|
3504
|
-
| `
|
|
3505
|
-
| `
|
|
3506
|
-
| `
|
|
3507
|
-
| `
|
|
3508
|
-
| `
|
|
3402
|
+
| `OIMPersistItemKeyValueStorage` | `{ persistKeyValueItemStorage }` |
|
|
3403
|
+
| `OIMPersistKeyValueEntrySnapshotService` | `{ storageKey }` |
|
|
3404
|
+
| `OIMPersistKeyValuePathSnapshotService` | `{ storageKey, path }` |
|
|
3405
|
+
| `OIMPersistLocalStorageKeyValueBackend` | `{ persistKeyValueItemStorage?, persistKeyValueSerializer? }`, the object itself optional |
|
|
3406
|
+
| `OIMPersistSlotSourceChangeMark` | `{ channelClock, slots }` |
|
|
3407
|
+
| `OIMPersistSubscriptionSourceChangeMark` | `{ subscribeSourceChanged }` |
|
|
3509
3408
|
| `OIMPersistAsyncKVStorage` | `{ persistAsyncKVMultiStorage }` |
|
|
3510
3409
|
|
|
3511
3410
|
`onPersistError(error, persistErrorContext)` receives a `TOIMPersistErrorContext`,
|
|
@@ -3516,10 +3415,10 @@ or collected into the `AggregateError`.
|
|
|
3516
3415
|
|
|
3517
3416
|
### Two resource kinds, and when to pick each
|
|
3518
3417
|
|
|
3519
|
-
| | `
|
|
3418
|
+
| | `OIMPersistRecordsResource` | `OIMPersistSnapshotResource` |
|
|
3520
3419
|
|---|---|---|
|
|
3521
3420
|
| source | a record source: entity store slot or bucket | any snapshot source: entity store slot, bucket or single entity |
|
|
3522
|
-
| strategy | `
|
|
3421
|
+
| strategy | `OIMPersistMemoryRecordsService`, `OIMPersistIndexedDbRecordsService` | `OIMPersistMemorySnapshotService`, `OIMPersistIndexedDbSnapshotService`, `OIMPersistKeyValueEntrySnapshotService`, `OIMPersistKeyValuePathSnapshotService` |
|
|
3523
3422
|
| autosave writes | the delta: one upsert or one deletion per entity slot written since the last drain | the whole snapshot, read from the source and encoded |
|
|
3524
3423
|
| `persist()` writes | every record, replacing what is stored; while any key is unloaded, every in-memory record as an upsert over what is stored, plus the deletions the resource already heard | the whole snapshot |
|
|
3525
3424
|
| codec | none | `persistCodec`, required |
|
|
@@ -3545,10 +3444,10 @@ slot wins: an upsert, not a deletion.
|
|
|
3545
3444
|
|
|
3546
3445
|
| source | reports | scheduler |
|
|
3547
3446
|
|---|---|---|
|
|
3548
|
-
| `
|
|
3549
|
-
| `
|
|
3550
|
-
| `
|
|
3551
|
-
| `
|
|
3447
|
+
| `OIMPersistCollectionWriteRecordSource` | synchronously, at the write, from `OIMPersistEntityWriter` only | `OIMPersistMicrotaskScheduler` |
|
|
3448
|
+
| `OIMPersistCollectionChangeWriteRecordSource` | synchronously, at the write, from `OIMPersistEntityWriter` and from a content-change listener on the entity store slot | `OIMPersistMicrotaskScheduler` |
|
|
3449
|
+
| `OIMPersistBucketRecordSource` | from effects (`OIMEffectService`), when the channel runs its frame | `OIMPersistFrameScheduler({ channelSlot })` |
|
|
3450
|
+
| `OIMPersistSingleEntitySnapshotSource` | from an effect, when the channel runs its frame | `OIMPersistFrameScheduler({ channelSlot })` |
|
|
3552
3451
|
|
|
3553
3452
|
- **Collection** (an entity collection service and one of its store slots): every entity slot of that store slot that holds an entity. Starting
|
|
3554
3453
|
reports nothing; content present before `start()` is written by `persist()` or by
|
|
@@ -3559,8 +3458,8 @@ slot wins: an upsert, not a deletion.
|
|
|
3559
3458
|
|
|
3560
3459
|
| collection source | in-place edit announced with `slotContentService.markEntityChanged(entitySlot, changedColumns?)` | cost while a resource listens | pick it when |
|
|
3561
3460
|
|---|---|---|---|
|
|
3562
|
-
| `
|
|
3563
|
-
| `
|
|
3461
|
+
| `OIMPersistCollectionWriteRecordSource` | NOT persisted: it hears only the writer | none: every write stays on the core fast path | every change goes through `writeSlotContentByKey`, `writeSlotContent`, `updateEntity` and `clearSlotContent` |
|
|
3462
|
+
| `OIMPersistCollectionChangeWriteRecordSource` | persisted: while subscribed it adds a content-change listener of every column to the store slot | EVERY write to that store slot leaves the core fast path, not only `markEntityChanged`: changed-column bookkeeping and the listener loop per write | the app edits entities in place |
|
|
3564
3463
|
- **Bucket**: the entities one bucket slot holds. Get the slot with
|
|
3565
3464
|
`bucketCollectionService.holdSlotByKey(bucketStoreSlot, key)`. The source watches the
|
|
3566
3465
|
bucket slot with one effect and each member entity slot with one effect. An entity
|
|
@@ -3570,7 +3469,7 @@ slot wins: an upsert, not a deletion.
|
|
|
3570
3469
|
hydrate restored.
|
|
3571
3470
|
- **Single entity**: the snapshot is the entity itself (a store slot of a single entity service, read through
|
|
3572
3471
|
`findSingleSlot(entityStoreSlot)`), so it
|
|
3573
|
-
goes only into `
|
|
3472
|
+
goes only into `OIMPersistSnapshotResource`. Its first subscription reports the
|
|
3574
3473
|
current value unless it is the value a hydrate restored. `readSnapshot` throws when
|
|
3575
3474
|
the entity slot holds no value.
|
|
3576
3475
|
|
|
@@ -3583,38 +3482,36 @@ frame, are both written.
|
|
|
3583
3482
|
|
|
3584
3483
|
```ts
|
|
3585
3484
|
import {
|
|
3586
|
-
|
|
3485
|
+
OIMPersistBucketRecordSource, OIMPersistRecordsResource, OIMPersistFrameScheduler, OIMPersistor,
|
|
3587
3486
|
} from '@oimdb/persist';
|
|
3588
|
-
import { OIMPersistMemoryBackend,
|
|
3487
|
+
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3589
3488
|
|
|
3590
3489
|
type TDeckCardSlot = TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
3591
|
-
const deckCardCollectionService =
|
|
3490
|
+
const deckCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
|
|
3592
3491
|
const deckCardEntityStoreSlot = deckCardCollectionService.createStoreSlot(db);
|
|
3593
|
-
const deckBucketService =
|
|
3492
|
+
const deckBucketService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
3594
3493
|
const deckBucketStoreSlot = deckBucketService.createStoreSlot(db);
|
|
3595
3494
|
const spadesBucketSlot = deckBucketService.holdSlotByKey(deckBucketStoreSlot, 'spades');
|
|
3596
3495
|
|
|
3597
3496
|
const deckPersistor = new OIMPersistor<OIMPersistMemoryBackend, OIMPersistMemoryWriteBatch>({
|
|
3598
3497
|
persistBackend: new OIMPersistMemoryBackend(),
|
|
3599
|
-
persistScheduler: new
|
|
3498
|
+
persistScheduler: new OIMPersistFrameScheduler({ channelSlot: db.defaultChannelSlot }),
|
|
3600
3499
|
});
|
|
3601
|
-
deckPersistor.addPersistResource(new
|
|
3602
|
-
persistRecordSource: new
|
|
3500
|
+
deckPersistor.addPersistResource(new OIMPersistRecordsResource({
|
|
3501
|
+
persistRecordSource: new OIMPersistBucketRecordSource({
|
|
3603
3502
|
channelMap: db,
|
|
3604
3503
|
entityCollectionService: deckCardCollectionService,
|
|
3605
3504
|
entityStoreSlot: deckCardEntityStoreSlot,
|
|
3606
3505
|
slotContentBucketService: deckBucketService.slotContentService,
|
|
3607
3506
|
bucketSlot: spadesBucketSlot,
|
|
3608
3507
|
}),
|
|
3609
|
-
|
|
3508
|
+
persistRecordsService: new OIMPersistMemoryRecordsService<TOIMStoreKey, Card>({ storageKey: 'spades' }),
|
|
3610
3509
|
}));
|
|
3611
3510
|
await deckPersistor.hydrate();
|
|
3612
3511
|
deckPersistor.start();
|
|
3613
3512
|
|
|
3614
|
-
const aceSlot = deckCardCollectionService.
|
|
3615
|
-
|
|
3616
|
-
);
|
|
3617
|
-
deckBucketService.slotContentService.writeBucketMemberSlots(spadesBucketSlot, [aceSlot]);
|
|
3513
|
+
const aceSlot = deckCardCollectionService.writeSlotContentByKey(deckCardEntityStoreSlot, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
|
|
3514
|
+
deckBucketService.slotContentService.writeSlotContent(spadesBucketSlot, [aceSlot]);
|
|
3618
3515
|
db.flush();
|
|
3619
3516
|
await deckPersistor.whenIdle();
|
|
3620
3517
|
```
|
|
@@ -3625,10 +3522,10 @@ await deckPersistor.whenIdle();
|
|
|
3625
3522
|
The persistor calls `attachDrain` from its constructor; both shipped schedulers throw
|
|
3626
3523
|
when a second persistor attaches, so construct one scheduler per persistor.
|
|
3627
3524
|
|
|
3628
|
-
- `
|
|
3525
|
+
- `OIMPersistMicrotaskScheduler`: `requestDrain()` queues one microtask; the
|
|
3629
3526
|
requests that follow before it runs are absorbed by it. `stop()` makes a queued
|
|
3630
3527
|
microtask do nothing.
|
|
3631
|
-
- `
|
|
3528
|
+
- `OIMPersistFrameScheduler({ channelSlot })`: while started it subscribes with
|
|
3632
3529
|
`OIMChannelService.INSTANCE.subscribeAfterEachFrame(channelSlot, …)` and drains after a frame only when a drain was
|
|
3633
3530
|
requested since the last one; `stop()` unsubscribes and forgets the request. It
|
|
3634
3531
|
holds no slot and never asks the channel for a frame: it drains only after a frame
|
|
@@ -3637,11 +3534,11 @@ when a second persistor attaches, so construct one scheduler per persistor.
|
|
|
3637
3534
|
**The pairing is a rule.** A write to an entity slot that nobody holds causes no frame
|
|
3638
3535
|
(`OIMChannelService.INSTANCE.writeWithoutFlushCheck` returns at once when `holdCount` is `0`), so a
|
|
3639
3536
|
collection source on the frame scheduler can report changes that no frame ever
|
|
3640
|
-
drains. Pair `
|
|
3641
|
-
`
|
|
3642
|
-
`
|
|
3537
|
+
drains. Pair `OIMPersistCollectionWriteRecordSource` and
|
|
3538
|
+
`OIMPersistCollectionChangeWriteRecordSource` with
|
|
3539
|
+
`OIMPersistMicrotaskScheduler`. The bucket and single-entity sources report from
|
|
3643
3540
|
effects, which cause the frames they run in, so pair them with
|
|
3644
|
-
`
|
|
3541
|
+
`OIMPersistFrameScheduler`. Resources of both groups in one persistor share its
|
|
3645
3542
|
one scheduler; give them two persistors when they need different schedulers.
|
|
3646
3543
|
|
|
3647
3544
|
`whenIdle()` resolves when the work queued at the moment of the call has settled. On
|
|
@@ -3661,8 +3558,8 @@ is queued on the microtask after the write: let one microtask pass before callin
|
|
|
3661
3558
|
| `hydrate()` | reads every resource and writes it into its source; ends the upsert-only mode of an unloaded resource |
|
|
3662
3559
|
| `persist()` | writes every resource in full, started or not, in one write batch; a records resource with unloaded keys writes its in-memory records as upserts over the stored table instead of replacing it, plus the deletions it already heard |
|
|
3663
3560
|
| `clearPersisted()` | deletes every resource's stored data in one write batch; memory is untouched; ends the upsert-only mode of an unloaded resource |
|
|
3664
|
-
| `unload(
|
|
3665
|
-
| `load(
|
|
3561
|
+
| `unload(persistRecordsResource, keys)` | writes the keys' current values plus the resource's pending change in one write batch, repeated until the keys have no unwritten change (unload holds the keys' entity slots while it writes and compares their revision stamps, a slot's stamp moving only on a real write; it also checks the resource's pending set and whether a key's slot was replaced), then removes the keys from memory only |
|
|
3562
|
+
| `load(persistRecordsResource, keys)` | the pair of `unload`: queued after the pending writes, reads only those keys from the backend (`readRecordsOfKeys`) and puts the stored entities back without writing them back; skips a key that holds a value in memory or has a change the resource has not written yet; the keys stop counting as unloaded |
|
|
3666
3563
|
| `whenIdle()` | resolves when the queued work settles; never rejects |
|
|
3667
3564
|
|
|
3668
3565
|
Every promise-returning call and every autosave runs on one ordered chain, one at a
|
|
@@ -3713,7 +3610,7 @@ What the source does with it:
|
|
|
3713
3610
|
name; the store slot becomes exactly the stored content;
|
|
3714
3611
|
- bucket: upserts every record into the entity store slot and sets the bucket's
|
|
3715
3612
|
members to exactly the restored entities;
|
|
3716
|
-
- single entity: `singleSlotService.
|
|
3613
|
+
- single entity: `singleSlotService.writeSingleSlotContent(entityStoreSlot, entity)`.
|
|
3717
3614
|
|
|
3718
3615
|
**Refusal.** If the source changes while the backend read is in flight and the
|
|
3719
3616
|
resource has no `persistHydrateReconcile`, hydrate throws instead of applying, because
|
|
@@ -3721,10 +3618,10 @@ applying would revert that change. The source keeps its newer state; the error g
|
|
|
3721
3618
|
to `onPersistError` with `'hydrate'`, or into the `AggregateError`. Before the read
|
|
3722
3619
|
the resource calls `persistSnapshotSource.markSourceChanges()`, which returns an
|
|
3723
3620
|
`IOIMPersistSourceChangeMark` (`isSourceChangedSinceMark()`, `dispose()`): the
|
|
3724
|
-
single-entity and bucket sources return an `
|
|
3621
|
+
single-entity and bucket sources return an `OIMPersistSlotSourceChangeMark`
|
|
3725
3622
|
that holds the slots and compares their revision stamps (`changedAtRevision`, `changedAtEpoch`) with the
|
|
3726
3623
|
channel clock reading taken at the mark (`oimIsAnySlotChangedAfter`), the
|
|
3727
|
-
collection sources an `
|
|
3624
|
+
collection sources an `OIMPersistSubscriptionSourceChangeMark` whose flag their
|
|
3728
3625
|
own report sets. A change made during the read is seen even when no frame runs, and
|
|
3729
3626
|
the guard runs whether or not the persistor is started.
|
|
3730
3627
|
|
|
@@ -3748,7 +3645,7 @@ const answerReconcile = oimReconcileByKey<TOIMStoreKey, Card>(
|
|
|
3748
3645
|
|
|
3749
3646
|
### unload, load and clearPersisted
|
|
3750
3647
|
|
|
3751
|
-
`persistor.unload(
|
|
3648
|
+
`persistor.unload(persistRecordsResource, keys)` takes an `OIMPersistRecordsResource`
|
|
3752
3649
|
only. It first drains the persistor's dirty resources onto the chain, then, in its
|
|
3753
3650
|
own turn, writes in one write batch that resource's pending change and the current
|
|
3754
3651
|
value of every unloaded key (`persistRecordSource.readRecordsDelta(persistRecordSource.findEntitySlotsByKeys(keys))`), so
|
|
@@ -3776,11 +3673,11 @@ stay on the backend. A deletion it never heard is not written in that mode.
|
|
|
3776
3673
|
`hydrate()` and `clearPersisted()` end that mode, and so does a `load` of every
|
|
3777
3674
|
unloaded key.
|
|
3778
3675
|
|
|
3779
|
-
`persistor.load(
|
|
3780
|
-
`
|
|
3676
|
+
`persistor.load(persistRecordsResource, keys)` is the pair of `unload` and takes an
|
|
3677
|
+
`OIMPersistRecordsResource` only (a key-value backend has no records strategy, so a
|
|
3781
3678
|
snapshot resource has no `load`). It drains the dirty resources onto the chain and
|
|
3782
3679
|
runs after those writes. In its turn it reads only those keys through
|
|
3783
|
-
`
|
|
3680
|
+
`persistRecordsService.readRecordsOfKeys(persistBackend, keys)`: on IndexedDB one
|
|
3784
3681
|
`readonly` transaction with one `get` per key, on memory a lookup of each key in the
|
|
3785
3682
|
stored map; a key the backend does not hold is left out. It skips every key that holds
|
|
3786
3683
|
a value in memory or has a change the resource has not written yet (pending or in a
|
|
@@ -3820,10 +3717,10 @@ required `persistCodec`, an `IOIMPersistCodec<TSourceSnapshot, TPersistedSnapsho
|
|
|
3820
3717
|
(`encode`, `decode`). A records resource has no codec: it stores entities as the
|
|
3821
3718
|
source holds them.
|
|
3822
3719
|
|
|
3823
|
-
- `new
|
|
3824
|
-
- `new
|
|
3720
|
+
- `new OIMPersistIdentityCodec<TSnapshot>()`: passes the snapshot through.
|
|
3721
|
+
- `new OIMPersistVersionedCodec<TSourceSnapshot, TPersistedSnapshot>({ version, migrations, persistCodec })`:
|
|
3825
3722
|
stores a `TOIMPersistVersionedShape`, `{ version, data }`, where `data` is the
|
|
3826
|
-
inner `persistCodec`'s encoding (required; use `
|
|
3723
|
+
inner `persistCodec`'s encoding (required; use `OIMPersistIdentityCodec` when
|
|
3827
3724
|
nothing transforms). `migrations` is a `Record<number, (data: unknown) => unknown>`
|
|
3828
3725
|
keyed by the version each migration produces. On decode it runs, ascending, every
|
|
3829
3726
|
migration whose key is above the stored `version` and at most the current one,
|
|
@@ -3833,21 +3730,21 @@ source holds them.
|
|
|
3833
3730
|
|
|
3834
3731
|
```ts
|
|
3835
3732
|
import {
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3733
|
+
OIMPersistIdentityCodec, OIMPersistVersionedCodec, OIMPersistKeyValueBackend, OIMPersistKeyValueEntrySnapshotService,
|
|
3734
|
+
OIMPersistItemKeyValueStorage, OIMPersistKeyValueWriteBatch, OIMPersistSnapshotResource,
|
|
3735
|
+
OIMPersistFrameScheduler, OIMPersistSingleEntitySnapshotSource, OIMPersistor,
|
|
3839
3736
|
} from '@oimdb/persist';
|
|
3840
3737
|
import type { TOIMPersistVersionedShape } from '@oimdb/persist';
|
|
3841
3738
|
|
|
3842
3739
|
type TSettings = { theme: 'light' | 'dark'; density: 'compact' | 'comfortable' };
|
|
3843
3740
|
|
|
3844
|
-
const settingsSingleEntityService =
|
|
3741
|
+
const settingsSingleEntityService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<TSettings>() });
|
|
3845
3742
|
const settingsEntityStoreSlot = settingsSingleEntityService.createStoreSlot(db);
|
|
3846
|
-
settingsSingleEntityService.
|
|
3743
|
+
settingsSingleEntityService.writeSingleSlotContent(settingsEntityStoreSlot, { theme: 'light', density: 'compact' });
|
|
3847
3744
|
const itemByStorageKey = new Map<string, string>();
|
|
3848
3745
|
const settingsPersistor = new OIMPersistor<OIMPersistKeyValueBackend, OIMPersistKeyValueWriteBatch>({
|
|
3849
3746
|
persistBackend: new OIMPersistKeyValueBackend({
|
|
3850
|
-
persistKeyValueStorage: new
|
|
3747
|
+
persistKeyValueStorage: new OIMPersistItemKeyValueStorage({
|
|
3851
3748
|
persistKeyValueItemStorage: {
|
|
3852
3749
|
getItem: storageKey => itemByStorageKey.get(storageKey) ?? null,
|
|
3853
3750
|
setItem: (storageKey, item) => { itemByStorageKey.set(storageKey, item); },
|
|
@@ -3855,17 +3752,17 @@ const settingsPersistor = new OIMPersistor<OIMPersistKeyValueBackend, OIMPersist
|
|
|
3855
3752
|
},
|
|
3856
3753
|
}),
|
|
3857
3754
|
}),
|
|
3858
|
-
persistScheduler: new
|
|
3755
|
+
persistScheduler: new OIMPersistFrameScheduler({ channelSlot: db.defaultChannelSlot }),
|
|
3859
3756
|
});
|
|
3860
|
-
settingsPersistor.addPersistResource(new
|
|
3861
|
-
persistSnapshotSource: new
|
|
3757
|
+
settingsPersistor.addPersistResource(new OIMPersistSnapshotResource({
|
|
3758
|
+
persistSnapshotSource: new OIMPersistSingleEntitySnapshotSource({
|
|
3862
3759
|
channelMap: db, singleSlotService: settingsSingleEntityService, entityStoreSlot: settingsEntityStoreSlot,
|
|
3863
3760
|
}),
|
|
3864
|
-
|
|
3865
|
-
persistCodec: new
|
|
3761
|
+
persistSnapshotService: new OIMPersistKeyValueEntrySnapshotService<TOIMPersistVersionedShape>({ storageKey: 'settings' }),
|
|
3762
|
+
persistCodec: new OIMPersistVersionedCodec<TSettings, TSettings>({
|
|
3866
3763
|
version: 2,
|
|
3867
3764
|
migrations: { 2: data => ({ density: 'comfortable', ...(data as object) }) },
|
|
3868
|
-
persistCodec: new
|
|
3765
|
+
persistCodec: new OIMPersistIdentityCodec<TSettings>(),
|
|
3869
3766
|
}),
|
|
3870
3767
|
}));
|
|
3871
3768
|
await settingsPersistor.hydrate();
|
|
@@ -3882,34 +3779,34 @@ await settingsPersistor.whenIdle();
|
|
|
3882
3779
|
|
|
3883
3780
|
**Memory** (`@oimdb/persist-memory`). `new OIMPersistMemoryBackend()`. Snapshot values
|
|
3884
3781
|
and records live in two separate maps, so a snapshot and a records resource may share
|
|
3885
|
-
a `storageKey`. `
|
|
3782
|
+
a `storageKey`. `OIMPersistMemoryRecordsService<TKey, TEntity>({ storageKey })` keeps one
|
|
3886
3783
|
`Map` of key to entity per `storageKey`: autosave sets and deletes keys, `persist()`
|
|
3887
|
-
replaces the map (while the resource has unloaded keys, sets keys and deletes only the deletions the resource already heard), `clearPersisted()` deletes it, and a `load` looks each of its keys up in the map through `readRecordsOfKeys`. `
|
|
3784
|
+
replaces the map (while the resource has unloaded keys, sets keys and deletes only the deletions the resource already heard), `clearPersisted()` deletes it, and a `load` looks each of its keys up in the map through `readRecordsOfKeys`. `OIMPersistMemorySnapshotService<TSnapshot>({ storageKey })`
|
|
3888
3785
|
keeps one value. `readRecords(storageKey)` and `readSnapshotValue(storageKey)` read
|
|
3889
3786
|
what was committed. `OIMPersistMemoryWriteBatch` queues every change and applies them in
|
|
3890
3787
|
order on `commit()`.
|
|
3891
3788
|
|
|
3892
3789
|
**JSON** (`@oimdb/persist-json`, depends on `@oimdb/persist-memory`).
|
|
3893
|
-
`
|
|
3790
|
+
`OIMPersistJsonMemoryBackend` extends `OIMPersistMemoryBackend` and takes the memory
|
|
3894
3791
|
strategies. `dehydrate()` returns a `TOIMPersistJsonDump`,
|
|
3895
3792
|
`{ snapshotValues: Record<storageKey, value>, recordsByStorageKey: Record<storageKey, { key, entity }[]> }`;
|
|
3896
|
-
`new
|
|
3793
|
+
`new OIMPersistJsonMemoryBackend({ initialPersistJsonDump })` writes such a dump straight into
|
|
3897
3794
|
its maps. The SSR flow: on the server, `await persistor.persist()` and then
|
|
3898
|
-
`JSON.stringify(
|
|
3899
|
-
`new
|
|
3795
|
+
`JSON.stringify(persistJsonMemoryBackend.dehydrate())`; on the client,
|
|
3796
|
+
`new OIMPersistJsonMemoryBackend({ initialPersistJsonDump: JSON.parse(jsonText) })`, register
|
|
3900
3797
|
the same resources under the same `storageKey`s, `await persistor.hydrate()`. To merge
|
|
3901
3798
|
a durable cache over the server's content, hydrate the JSON persistor first, then the
|
|
3902
3799
|
durable persistor with `persistHydrateReconcile`, then `start()` the durable one.
|
|
3903
3800
|
|
|
3904
3801
|
**IndexedDB** (`@oimdb/persist-idb`). `new OIMPersistIndexedDbBackend({ databaseName, databaseVersion?, indexedDb? })`;
|
|
3905
3802
|
`indexedDb` defaults to `globalThis.indexedDB` and the constructor throws without
|
|
3906
|
-
either. `
|
|
3803
|
+
either. `OIMPersistIndexedDbRecordsService<TKey, TEntity>({ tableName })` stores one value
|
|
3907
3804
|
per entity key in an object store: autosave puts and deletes per key, `persist()`
|
|
3908
3805
|
clears the table and puts every entity (while the resource has unloaded keys, only
|
|
3909
3806
|
puts plus a delete per deletion the resource already heard), `clearPersisted()` clears it,
|
|
3910
3807
|
and a `load` reads its keys through `readRecordsOfKeys` in one `readonly` transaction,
|
|
3911
3808
|
one `get` per key.
|
|
3912
|
-
`
|
|
3809
|
+
`OIMPersistIndexedDbSnapshotService<TSnapshot>({ tableName, primaryKey })` stores one value
|
|
3913
3810
|
at `primaryKey`. One commit is one `readwrite` transaction over every table the batch
|
|
3914
3811
|
touches. A commit opens the database and creates missing tables by a version upgrade
|
|
3915
3812
|
(retried when another connection upgrades first, up to five attempts; an upgrade
|
|
@@ -3921,7 +3818,7 @@ sorted property names, and is read back as that string.
|
|
|
3921
3818
|
**Key-value** (`@oimdb/persist`). `new OIMPersistKeyValueBackend({ persistKeyValueStorage, persistKeyValueSerializer? })`
|
|
3922
3819
|
stores serialized strings under string keys. `persistKeyValueStorage` is an
|
|
3923
3820
|
`IOIMPersistKeyValueStorage` (`readItems(storageKeys)`, `writeItems(itemByStorageKey)`,
|
|
3924
|
-
`removeItems(storageKeys)`); `
|
|
3821
|
+
`removeItems(storageKeys)`); `OIMPersistItemKeyValueStorage({ persistKeyValueItemStorage })`
|
|
3925
3822
|
adapts any `TOIMPersistKeyValueItemStorage` (`getItem`, `setItem`, `removeItem`,
|
|
3926
3823
|
synchronous or returning promises) with one call per key. The serializer is a
|
|
3927
3824
|
`TOIMPersistKeyValueSerializer` (`serialize`, `deserialize`) and defaults to
|
|
@@ -3929,15 +3826,15 @@ synchronous or returning promises) with one call per key. The serializer is a
|
|
|
3929
3826
|
|
|
3930
3827
|
| strategy | stored as |
|
|
3931
3828
|
|---|---|
|
|
3932
|
-
| `
|
|
3933
|
-
| `
|
|
3829
|
+
| `OIMPersistKeyValueEntrySnapshotService<TSnapshot>({ storageKey })` | the whole snapshot under `storageKey` |
|
|
3830
|
+
| `OIMPersistKeyValuePathSnapshotService<TSnapshot>({ storageKey, path })` | the snapshot at `path` (a `string[]`) inside an object under `storageKey`; several resources may share the key; the constructor throws on an empty `path` |
|
|
3934
3831
|
|
|
3935
3832
|
`OIMPersistKeyValueWriteBatch` commits with at most one `readItems` (keys with path
|
|
3936
3833
|
writes or path deletions and no whole-value write in the batch), one `writeItems` and
|
|
3937
3834
|
one `removeItems`; it serializes every value before writing any. Path segments are
|
|
3938
3835
|
own properties, so a stored `__proto__` is data.
|
|
3939
3836
|
|
|
3940
|
-
- **localStorage** (`@oimdb/persist-localstorage`): `
|
|
3837
|
+
- **localStorage** (`@oimdb/persist-localstorage`): `OIMPersistLocalStorageKeyValueBackend`
|
|
3941
3838
|
extends `OIMPersistKeyValueBackend` over a `TOIMPersistKeyValueItemStorage`
|
|
3942
3839
|
(`getItem`, `setItem`, `removeItem`) given as `persistKeyValueItemStorage`, which
|
|
3943
3840
|
defaults to `globalThis.localStorage`; the constructor throws without either. One `setItem` per written key.
|
|
@@ -3956,10 +3853,10 @@ own properties, so a stored `__proto__` is data.
|
|
|
3956
3853
|
batch keeps them in: `addWriteBatchChange(change)`, `hasWriteBatchChanges`,
|
|
3957
3854
|
`applyWriteBatchChanges(target)`. `oimWritePersistRecordsDelta(delta, putRecord, deleteRecord)`
|
|
3958
3855
|
walks a `TOIMPersistRecordsDelta` in the one order every backend uses: upserts, then deletions.
|
|
3959
|
-
- Snapshot strategy: `
|
|
3856
|
+
- Snapshot strategy: `IOIMPersistSnapshotService<TPersistBackend, TWriteBatch, TSnapshot>`
|
|
3960
3857
|
with `readSnapshot(persistBackend)`, `writeSnapshot(writeBatch, snapshot)`,
|
|
3961
3858
|
`deleteSnapshot(writeBatch)`. Records strategy:
|
|
3962
|
-
`
|
|
3859
|
+
`IOIMPersistRecordsService<TPersistBackend, TWriteBatch, TKey, TEntity>` with
|
|
3963
3860
|
`readRecords(persistBackend)`, `readRecordsOfKeys(persistBackend, keys)`,
|
|
3964
3861
|
`writeRecords(writeBatch, persistRecords)`,
|
|
3965
3862
|
`writeRecordsDelta(writeBatch, persistRecordsDelta)` (a `TOIMPersistRecordsDelta`,
|
|
@@ -3973,25 +3870,25 @@ own properties, so a stored `__proto__` is data.
|
|
|
3973
3870
|
`loadRecords(persistRecords)`, `unloadKeys(keys)`.
|
|
3974
3871
|
`markSourceChanges()` is required and returns an `IOIMPersistSourceChangeMark`
|
|
3975
3872
|
(`isSourceChangedSinceMark()`, `dispose()`); build it with
|
|
3976
|
-
`new
|
|
3977
|
-
`new
|
|
3873
|
+
`new OIMPersistSlotSourceChangeMark({ channelClock, slots })` or
|
|
3874
|
+
`new OIMPersistSubscriptionSourceChangeMark({ subscribeSourceChanged })`.
|
|
3978
3875
|
It must accept more than one subscription, each with its own `TOIMDisposer`.
|
|
3979
3876
|
- Scheduler: `IOIMPersistScheduler`, above.
|
|
3980
3877
|
|
|
3981
3878
|
### Limits
|
|
3982
3879
|
|
|
3983
3880
|
- **No key-value records strategy.** Over `OIMPersistKeyValueBackend` (localStorage,
|
|
3984
|
-
async KV) an entity store slot or a bucket goes through `
|
|
3881
|
+
async KV) an entity store slot or a bucket goes through `OIMPersistSnapshotResource`: every
|
|
3985
3882
|
autosave walks every entity slot of the source and serializes the whole snapshot
|
|
3986
3883
|
under one key.
|
|
3987
3884
|
- **A bucket membership change walks the bucket.** Each change of the bucket slot
|
|
3988
3885
|
re-reads every member and every watched slot, and the source keeps one effect per
|
|
3989
3886
|
member entity slot.
|
|
3990
3887
|
- **The memory and JSON backends keep values by reference.** With
|
|
3991
|
-
`
|
|
3888
|
+
`createOIMInPlaceEntityWriter()` the stored entity is the live entity object, so an
|
|
3992
3889
|
in-place write moves the stored value before any commit, and `dehydrate()` returns
|
|
3993
|
-
the live objects. Use `
|
|
3994
|
-
`
|
|
3890
|
+
the live objects. Use `createOIMReplacingEntityWriter()` with
|
|
3891
|
+
`entityInserter: oimInsertEntityWithCopy` where the stored value must stay
|
|
3995
3892
|
what was committed, and serialize a dump at once.
|
|
3996
3893
|
- **Hydrate without a reconcile replaces.** The collection source deletes every
|
|
3997
3894
|
entity the stored records do not name, and an existing but empty records table
|
|
@@ -4011,7 +3908,7 @@ only on the type-only `@exodra/reactivity-types`.
|
|
|
4011
3908
|
|
|
4012
3909
|
### One door: `exoBinding(channelMap, binding)`
|
|
4013
3910
|
|
|
4014
|
-
A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, sourceSlots, sourceComputeds
|
|
3911
|
+
A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, onChange, sourceSlots, sourceComputeds)` —
|
|
4015
3912
|
and the value is read off the slots afterwards. So one adapter covers an entity, a
|
|
4016
3913
|
column, a bucket's slots and a derivation:
|
|
4017
3914
|
|
|
@@ -4067,7 +3964,7 @@ resolved before its entity arrived, an entity since removed.
|
|
|
4067
3964
|
|
|
4068
3965
|
- `exoSource(read, subscribe)` — the lazy, ref-counted base. For sources that are
|
|
4069
3966
|
**not** this engine's.
|
|
4070
|
-
- `exoCombine(
|
|
3967
|
+
- `exoCombine(fn, sources)` — fan-in across any `TOIMExoReadable`. Not glitch-free;
|
|
4071
3968
|
keep heavy fan-in inside one computed.
|
|
4072
3969
|
- `exoFollowKey(key, makeBindable)` — repoints as a selection moves. The factory
|
|
4073
3970
|
must return a `TOIMExoDisposable`.
|
|
@@ -4080,7 +3977,7 @@ Who decided the order is the difference, and the core already forks on it.
|
|
|
4080
3977
|
|
|
4081
3978
|
| | `exoDerivedList(channelMap, computedSlot, render, maxCommandsPerSlot?)` | `exoPositionalList(channelMap, slotContentBucketService, bucketSlot, render)` |
|
|
4082
3979
|
|---|---|---|
|
|
4083
|
-
| takes | a `TOIMComputedSlot<readonly TOIMEntitySlot[]>` | a bucket content service built with an `OIMBucketCommands`
|
|
3980
|
+
| takes | a `TOIMComputedSlot<readonly TOIMEntitySlot[]>` | a bucket content service built with an `OIMBucketCommands` and one of its bucket slots, from any of its store slots |
|
|
4084
3981
|
| the script | **derived**, by `oimOrderedListCommandsOfTransition` | **recorded**, by `OIMBucketCommands`, read inside the frame |
|
|
4085
3982
|
| per changed frame | one diff: two passes plus an LIS pass, `common − LIS` moves | one pass over the frame's own script |
|
|
4086
3983
|
|
|
@@ -4101,6 +3998,29 @@ memoised per list, so two lists showing one entity slot render it twice.
|
|
|
4101
3998
|
|
|
4102
3999
|
---
|
|
4103
4000
|
|
|
4001
|
+
## `@oimdb/browser`
|
|
4002
|
+
|
|
4003
|
+
Browser state as single slots. Each factory returns an `OIMSingleSlotService` (`TOIMBrowserSingleSlotService<TEntity>`)
|
|
4004
|
+
built with a slot subscription listener: the browser listener is added when the slot gets its first
|
|
4005
|
+
subscriber (directly or through a computed that reads it) and removed when the last one goes; the current
|
|
4006
|
+
value is written at the first subscription, and a write that would land during a flush is made right after it.
|
|
4007
|
+
|
|
4008
|
+
```ts signature
|
|
4009
|
+
createOIMBrowserOnlineSingleSlotService(options?: { defaults?: OIMDefaults }): TOIMBrowserSingleSlotService<TOIMBrowserOnlineEntity> // { isOnline }
|
|
4010
|
+
createOIMBrowserFocusSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserFocusEntity> // { isFocused }: document.hasFocus()
|
|
4011
|
+
createOIMBrowserVisibilitySingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserVisibilityEntity> // { isVisible }
|
|
4012
|
+
createOIMBrowserLocationSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserLocationEntity> // { href, pathname, search, hash }
|
|
4013
|
+
```
|
|
4014
|
+
|
|
4015
|
+
Read it like any single: `const onlineSlot = onlineService.getSingleSlot(onlineService.createStoreSlot(db))`, then
|
|
4016
|
+
an effect over `[onlineSlot]`, or `useOIMSingleEntity(onlineService, online)` in React. `isOnline` is
|
|
4017
|
+
`navigator.onLine` (a network interface exists, not a reachable internet); the location follows the
|
|
4018
|
+
Navigation API's `currententrychange` where it exists and `popstate` + `hashchange` otherwise, so without it a
|
|
4019
|
+
`history.pushState` is not seen until the next of those events. Without the API a sensor needs, the slot holds
|
|
4020
|
+
`true` (or an empty location). `OIMBrowserSlotSubscriptionListener` is the base class of the four listeners.
|
|
4021
|
+
|
|
4022
|
+
---
|
|
4023
|
+
|
|
4104
4024
|
## Performance
|
|
4105
4025
|
|
|
4106
4026
|
**No figure may be quoted for this engine, and none is quoted anywhere in this
|
|
@@ -4131,9 +4051,9 @@ them.
|
|
|
4131
4051
|
|
|
4132
4052
|
The one quantity that needs no measurement, because it is arithmetic rather than
|
|
4133
4053
|
a reading: emptying a bucket one slot at a time on an array-driven bucket costs
|
|
4134
|
-
n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `
|
|
4135
|
-
and `
|
|
4136
|
-
`packages/core/src/
|
|
4054
|
+
n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `writeSlotContent`
|
|
4055
|
+
and `clearSlotContent` go through one `drainSlots`. Counted, not timed, in
|
|
4056
|
+
`packages/core/src/bucket/array/tests/everyone-out-at-once-and-not-one-at-a-time.test.ts`, which
|
|
4137
4057
|
also pins `drainSlots` at zero element writes.
|
|
4138
4058
|
|
|
4139
4059
|
Fast properties are checked, not timed. Every slot kind is one object literal with
|