@oimdb/core 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -32
- package/README.md +342 -453
- package/dist/index.cjs +1783 -1997
- package/dist/index.d.cts +618 -544
- package/dist/index.d.ts +618 -544
- package/dist/index.js +1736 -1952
- package/llms.txt +740 -858
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -6,33 +6,38 @@
|
|
|
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(db, { slotContentService })` and every single is
|
|
29
|
+
> `createOIMSingleSlotService(db, { slotContentService })`. The content service decides what the slots hold:
|
|
30
|
+
> `createOIMSlotContentEntityService<TEntity>(db, …)` for entities, `new OIMSlotContentSetBucketService()`,
|
|
31
|
+
> `new OIMSlotContentPositionalArrayBucketService()` or `new OIMSlotContentSortedArrayBucketService({ … })` for
|
|
32
|
+
> buckets. An index is `createOIMSlotCollectionIndexService(db, { 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
42
|
> Effects are `OIMEffectService.INSTANCE.createEffect(db, sourceSlots, sourceComputeds, body)`.
|
|
38
43
|
> Every stateless singleton is a static `INSTANCE` on its class. Module-level defaults are on `OIMDefaults`.
|
|
@@ -76,39 +81,7 @@ block and the one under [One derivation per key](#one-derivation-per-key). Block
|
|
|
76
81
|
marked `ts signature` are listings of an API and are not compiled.
|
|
77
82
|
|
|
78
83
|
```ts setup
|
|
79
|
-
import {
|
|
80
|
-
OIMChannelService,
|
|
81
|
-
OIMContentChangeListenerService,
|
|
82
|
-
OIMDB,
|
|
83
|
-
OIMDefaults,
|
|
84
|
-
OIMEffectService,
|
|
85
|
-
OIMSlotContentBucketServiceArrayDriven,
|
|
86
|
-
OIMSlotContentBucketServiceSetDriven,
|
|
87
|
-
OIMSlotStoreServiceMapDriven,
|
|
88
|
-
OIMSlotStoreServiceSingleSlotDriven,
|
|
89
|
-
OIMSlotStoreServiceTreeDriven,
|
|
90
|
-
OIMUnusedSlotCollectorService,
|
|
91
|
-
createOIMBucketService,
|
|
92
|
-
createOIMComputedServiceBucketMemo,
|
|
93
|
-
createOIMComputedService,
|
|
94
|
-
createOIMComputedServiceSorted,
|
|
95
|
-
createOIMEntityService,
|
|
96
|
-
createOIMEntityWriterInPlace,
|
|
97
|
-
createOIMEntityWriterReplacing,
|
|
98
|
-
createOIMIndexService,
|
|
99
|
-
createOIMIndexServiceSortedArrayDriven,
|
|
100
|
-
createOIMSingleBucketService,
|
|
101
|
-
createOIMSingleEntityService,
|
|
102
|
-
createOIMSlotCollectionService,
|
|
103
|
-
createOIMSlotContentEntityService,
|
|
104
|
-
createOIMUnusedSlotCollectorSlot,
|
|
105
|
-
oimIsSlotEmpty,
|
|
106
|
-
oimIsSlotReleased,
|
|
107
|
-
oimPatchEntityColumn,
|
|
108
|
-
oimPatchEntityColumns,
|
|
109
|
-
oimPutEntityColumns,
|
|
110
|
-
oimSameValueZeroGuard,
|
|
111
|
-
} from '@oimdb/core';
|
|
84
|
+
import { OIMChannelService, OIMContentChangeListenerService, OIMDB, OIMDefaults, OIMEffectService, OIMSlotContentPositionalArrayBucketService, OIMSlotContentSetBucketService, OIMSlotMapStoreService, OIMSingleSlotStoreService, OIMSlotTreeStoreService, OIMUnusedSlotCollectorService, createOIMBucketMemoComputedService, createOIMComputedService, createOIMSortedComputedService, createOIMEntityWriterInPlace, createOIMEntityWriterReplacing, createOIMSlotCollectionIndexService, createOIMSlotCollectionSortedArrayIndexService, createOIMSlotCollectionService, createOIMSlotContentEntityService, createOIMSingleSlotService, createOIMSingleComputedService, createOIMUnusedSlotCollectorSlot, oimIsSlotEmpty, oimIsSlotReleased, oimPatchEntityColumn, oimPatchEntityColumns, oimPutEntityColumns, oimSameValueZeroGuard, OIMBucketCommands, OIMSlotEarliestReleasePolicy, OIMSlotAfterWriteReleasePolicy, OIMSlotAfterIdleReleasePolicy, OIMThrowAtOnceErrorPolicy, type TOIMSlotStoreTree } from '@oimdb/core';
|
|
112
85
|
import type {
|
|
113
86
|
IOIMEntityWriter,
|
|
114
87
|
TOIMSlotEditable,
|
|
@@ -117,6 +90,7 @@ import type {
|
|
|
117
90
|
TOIMBucketSlotsSetStore,
|
|
118
91
|
TOIMComputedSlot,
|
|
119
92
|
TOIMDisposer,
|
|
93
|
+
TOIMEntityContentSlot,
|
|
120
94
|
TOIMEntitySlot,
|
|
121
95
|
TOIMOrderedListCommand,
|
|
122
96
|
TOIMSingleSlotStore,
|
|
@@ -137,17 +111,15 @@ declare const payload: readonly Card[];
|
|
|
137
111
|
const db = new OIMDB();
|
|
138
112
|
const channelSlot = db.defaultChannelSlot;
|
|
139
113
|
const effectService = OIMEffectService.INSTANCE;
|
|
140
|
-
const cardService =
|
|
114
|
+
const cardService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db) });
|
|
141
115
|
const cards = cardService.createStoreSlot(db);
|
|
142
|
-
const slot = cardService.
|
|
116
|
+
const slot = cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
|
|
143
117
|
|
|
144
|
-
const orderedService =
|
|
145
|
-
slotContentService: new OIMSlotContentBucketServiceArrayDriven<TOIMEntitySlot<Card>>(),
|
|
146
|
-
});
|
|
118
|
+
const orderedService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>() });
|
|
147
119
|
const ordered = orderedService.createStoreSlot(db);
|
|
148
120
|
const orderedBucketSlot = orderedService.holdSlotByKey(ordered, 'top');
|
|
149
121
|
|
|
150
|
-
const pageService =
|
|
122
|
+
const pageService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
151
123
|
const page = pageService.createStoreSlot(db);
|
|
152
124
|
const todo = pageService.holdSlotByKey(page, 'todo');
|
|
153
125
|
const done = pageService.holdSlotByKey(page, 'done');
|
|
@@ -159,26 +131,28 @@ const done = pageService.holdSlotByKey(page, 'done');
|
|
|
159
131
|
|
|
160
132
|
| Layer | Contents |
|
|
161
133
|
|---|---|
|
|
162
|
-
| `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 `
|
|
163
|
-
| `src/facade/<domain>/` | `facade/slot/`: `OIMSlotCollectionService`, `OIMSingleSlotService`; `facade/bucket/`: `OIMSlotContentBucketService` (abstract), `
|
|
164
|
-
| `src/factories/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`;
|
|
134
|
+
| `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 `OIMColumnSlotOwner` with the strategies `OIMPushColumnSlotOwner` and `OIMPullColumnSlotOwner`), the key stores and key tree walks, the index keys services, `TOIMStoreSlot`, `TOIMComputedSlot`, `TOIMEffect`, the collector services and slots, and the release policies |
|
|
135
|
+
| `src/facade/<domain>/` | `facade/slot/`: `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `OIMSingleSlotService`, `OIMWritableSingleSlotService`; `facade/column/`: `OIMColumnSlotService`; `facade/bucket/`: `OIMSlotContentBucketService` (abstract), `OIMSlotContentSetBucketService`, `OIMSlotContentArrayBucketService` (abstract), `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService`, `OIMBucketCommands`; `facade/index/`: `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`; `facade/computed/`: `OIMComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService` |
|
|
136
|
+
| `src/factories/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`; `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`, `createOIMSlotCollectionIndexService`, `createOIMSlotCollectionSortedArrayIndexService`, `createOIMComputedService`, `createOIMSingleComputedService`, `createOIMSortedComputedService`, `createOIMBucketMemoComputedService`, `createOIMEntityWriterInPlace`, `createOIMEntityWriterReplacing` |
|
|
165
137
|
|
|
166
138
|
**No class sits directly in `engine/` or `facade/`.** The directory is the
|
|
167
139
|
domain: `engine/bucket/`, `engine/channel/`, `engine/collector/`, `engine/column/`, `engine/computed/`,
|
|
168
140
|
`engine/entity/`, `engine/error/`, `engine/index/`, `engine/key/`, `engine/schedule/`, `engine/slot/`,
|
|
169
141
|
`engine/store/`, each with its own `constants/`, `interfaces/`, `types/` and `utils/` beside its classes; and
|
|
170
|
-
`facade/bucket/`, `facade/computed/`, `facade/index/`, `facade/slot/`.
|
|
142
|
+
`facade/bucket/`, `facade/column/`, `facade/computed/`, `facade/index/`, `facade/slot/`.
|
|
171
143
|
|
|
172
144
|
**A service holds no data; state lives on slots.** A collection service is built once per collection type
|
|
173
145
|
and holds only configuration and other services: its slot owner, its store service and its content
|
|
174
146
|
service (`service.slotContentService`). `service.createStoreSlot(db)` makes an instance, a `TOIMStoreSlot`,
|
|
175
147
|
which is NOT a slot (no owner, holds or effects): its `store` is what the store service built, its
|
|
176
|
-
`channelSlot` is the db's channel, and it carries the per-store
|
|
177
|
-
|
|
178
|
-
`slot.storeSlot`, which is why a slot door needs no store slot.
|
|
179
|
-
(`cardService.getSlotByKey(cards, pk)`,
|
|
180
|
-
|
|
181
|
-
|
|
148
|
+
`channelSlot` is the db's channel, its `slotService` is the service that made it, and it carries the per-store
|
|
149
|
+
state (content-change listeners, index key positions, the open batch and its queued changes, column readers).
|
|
150
|
+
Every slot filed in it points back to it through `slot.storeSlot`, which is why a slot door needs no store slot.
|
|
151
|
+
Reads and the keyed write are called on the service (`cardService.getSlotByKey(cards, pk)`,
|
|
152
|
+
`cardService.getSlots(cards)`, `cardService.writeSlotContentByKey(cards, pk, card)`, or through the store slot:
|
|
153
|
+
`cards.slotService.writeSlotContentByKey(cards, pk, card)`); writes on a slot go to its content service
|
|
154
|
+
(`cardService.slotContentService.updateEntity(slot, patch)`), and the column doors to
|
|
155
|
+
`cardService.columnSlotService`. A store slot's `store` is the store service's to read, never the caller's.
|
|
182
156
|
|
|
183
157
|
---
|
|
184
158
|
|
|
@@ -192,20 +166,20 @@ effect, a count, or a tracked mint — so nothing can unfile the identity under
|
|
|
192
166
|
the caller who just got it.
|
|
193
167
|
|
|
194
168
|
```ts
|
|
195
|
-
cardService.
|
|
169
|
+
cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'Two', rank: 2, deck: 'spades' }); // write
|
|
196
170
|
effectService.createEffect(db, [slot], [], () => { paint(slot.entity); }); // subscribe
|
|
197
171
|
cardService.holdSlotByKey(cards, 'c1'); // hold, by key -> the slot, holdCount up by one
|
|
198
|
-
const rankColumn = cardService.holdColumn(slot, 'rank'); // hold, on a column
|
|
199
|
-
cardService.unholdColumn(rankColumn); // give the column back
|
|
172
|
+
const rankColumn = cardService.columnSlotService.holdColumn(slot, 'rank'); // hold, on a column
|
|
173
|
+
cardService.columnSlotService.unholdColumn(rankColumn); // give the column back
|
|
200
174
|
cardService.getSlotByKey(cards, 'c1'); // read, by key -> the slot, tracked by the collector
|
|
201
175
|
```
|
|
202
176
|
|
|
203
177
|
**The non-minting lookup leaves no reference.** `findSlotByKey(storeSlot, key)` (and
|
|
204
178
|
`findSingleSlot(storeSlot)` on a single) answers `undefined` on a miss and mints nothing, and the
|
|
205
|
-
slot it does hand back is protected by nothing the caller took
|
|
206
|
-
|
|
207
|
-
the slot first and write through it. Do not write a helper
|
|
208
|
-
second spelling of a mint, not a fifth door.
|
|
179
|
+
slot it does hand back is protected by nothing the caller took; the entity is
|
|
180
|
+
`findSlotByKey(storeSlot, key)?.entity`. The one by-key write is `writeSlotContentByKey`, the write door
|
|
181
|
+
above; to patch or clear an entity by key, find the slot first and write through it. Do not write a helper
|
|
182
|
+
over any of the four doors — it would be a second spelling of a mint, not a fifth door.
|
|
209
183
|
|
|
210
184
|
**Subscribing leaves BOTH kinds of reference.** `OIMEffectService.INSTANCE.createEffect(db, …)` files
|
|
211
185
|
the effect in `slot.effects` *and* holds each root it subscribed to
|
|
@@ -237,13 +211,13 @@ owned by the object asked, and out of grace. Terms, and who gives each back:
|
|
|
237
211
|
|
|
238
212
|
| Term | Given back by |
|
|
239
213
|
|---|---|
|
|
240
|
-
| `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 |
|
|
214
|
+
| `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 |
|
|
241
215
|
| `effects` | `effectService.disposeEffect(effect)` |
|
|
242
216
|
| a held column | its last column effect being disposed, through the service's column owner (`unsubscribeFromSlot`) |
|
|
243
|
-
| the value | `
|
|
244
|
-
| a composite-key edge | `
|
|
217
|
+
| the value | `service.slotContentService.clearSlotContent(slot)`, on an entity slot or a bucket slot alike; `clearSingleSlotContent(storeSlot)` on a single |
|
|
218
|
+
| a composite-key edge | `OIMCompositeKeyTreeWalk.deleteKey` unlinking that composite key part |
|
|
245
219
|
|
|
246
|
-
`
|
|
220
|
+
`clearSlotContent(slot)` takes the **value**. Subscribers stay, holds
|
|
247
221
|
stay, and the identity stays with them. Slots you wrote into a bucket are held by
|
|
248
222
|
that bucket until you empty it.
|
|
249
223
|
|
|
@@ -255,27 +229,36 @@ built from three stateless parts, wired once at construction, plus the injected
|
|
|
255
229
|
| Part | Where | What it does |
|
|
256
230
|
|---|---|---|
|
|
257
231
|
| the slot owner, `OIMSlotOwnerService` (type-only export) | `slot.owner`, typed `IOIMSlotOwner` | holds, subscriptions, release, emptiness, `assertWritable`, the column owner; one per collection type |
|
|
258
|
-
| the store service, `IOIMSlotStoreService` | given to the factory (`
|
|
259
|
-
| the content service | `service.slotContentService` | the writes and reads of one kind of content (`OIMSlotContentEntityService`, `
|
|
232
|
+
| 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` |
|
|
233
|
+
| 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 |
|
|
260
234
|
|
|
261
|
-
The
|
|
235
|
+
The factory reads what it needs off the content service it is handed, through four small interfaces, never
|
|
262
236
|
through a subclass of the service:
|
|
263
237
|
|
|
264
238
|
```ts signature
|
|
265
|
-
interface
|
|
266
|
-
createSlot(key: TKey, storeSlot: TOIMStoreSlot
|
|
239
|
+
interface IOIMKeyedSlotCreator<TKey extends NonNullable<unknown>, TSlot> {
|
|
240
|
+
createSlot(key: TKey, storeSlot: TOIMStoreSlot, slotOwner: IOIMSlotOwner): TSlot;
|
|
241
|
+
}
|
|
242
|
+
interface IOIMSingleSlotCreator<TSlot> {
|
|
243
|
+
createSingleSlot(storeSlot: TOIMStoreSlot, slotOwner: IOIMSlotOwner): TSlot;
|
|
244
|
+
}
|
|
245
|
+
interface IOIMSlotContentWriter<TSlot, TContent> {
|
|
246
|
+
insertSlotContent(slot: TSlot, content: TContent, changedSlots?: TSlot[]): void;
|
|
247
|
+
writeSlotContent(slot: TSlot, content: TContent): boolean;
|
|
248
|
+
clearSlotContent(slot: TSlot): void;
|
|
249
|
+
}
|
|
250
|
+
interface IOIMSlotContentService<TSlot, TSlotOwnerService extends IOIMSlotOwner = IOIMSlotOwner> {
|
|
267
251
|
isSlotEmpty(slot: TSlot): boolean;
|
|
268
252
|
releaseSlotContent(slot: TSlot, slotOwnerService: TSlotOwnerService): void;
|
|
269
253
|
hasReference(slot: TSlot): boolean;
|
|
270
254
|
}
|
|
271
|
-
interface IOIMSlotContentFlushStrategy<TSlot, TSlotOwnerService> {
|
|
272
|
-
onSubframeClosed(storeSlot: TOIMStoreSlot<unknown, TSlot>, slotOwnerService: TSlotOwnerService): void;
|
|
273
|
-
}
|
|
274
255
|
```
|
|
275
256
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
257
|
+
A content service with `createSlot` goes into `createOIMSlotCollectionService`, one with `createSingleSlot` into
|
|
258
|
+
`createOIMSingleSlotService`; the entity and bucket content services have both. `hasReference` answers whether
|
|
259
|
+
something outside `holdCount` and `effects` keeps the slot (a bucket's members). Every write door asserts the
|
|
260
|
+
identity is still filed (`assertWritable`), and `releaseSlotContent` refuses inside a flush when the slot still
|
|
261
|
+
has content.
|
|
279
262
|
|
|
280
263
|
### The injected seams
|
|
281
264
|
|
|
@@ -284,26 +267,26 @@ question that the engine asks but does not decide. There is no flag and no optio
|
|
|
284
267
|
object.
|
|
285
268
|
|
|
286
269
|
```ts signature
|
|
287
|
-
interface
|
|
270
|
+
interface IOIMSlotEmptinessPolicy<TSlot> { isEmpty(slot: TSlot): boolean; }
|
|
288
271
|
|
|
289
272
|
interface IOIMSlotStamp<TSlot> { stamp(slotEditable: TOIMSlotEditable<TSlot>): void; }
|
|
290
273
|
```
|
|
291
274
|
|
|
292
275
|
**What a kind of content answers is not a seam.** Mint, write, clear and release
|
|
293
|
-
belong to the content service, which the
|
|
276
|
+
belong to the content service, which the collection service hands its owner as `slotContentService`.
|
|
294
277
|
"Does this still hold something" no part declares — the owner asks `oimIsSlotEmpty(slot)`,
|
|
295
278
|
`hasReference` and the release policy's `isSlotReleasable` separately.
|
|
296
279
|
|
|
297
|
-
**`
|
|
280
|
+
**`IOIMSlotEmptinessPolicy`** decides what counts as empty, and the answer is written once
|
|
298
281
|
per write into the EMPTY flag of `slot.slotFlags` — never recomputed at a read. The entity content
|
|
299
|
-
service defaults to "the entity is `undefined`" (the `
|
|
282
|
+
service defaults to "the entity is `undefined`" (the `slotEmptinessPolicy` option of
|
|
300
283
|
`createOIMSlotContentEntityService` replaces it); a bucket content service answers "no members"; a
|
|
301
284
|
computed answers `false`, because a derivation always has a value to produce.
|
|
302
285
|
|
|
303
|
-
**Emptiness decides lifetime, never the write path.**
|
|
304
|
-
still carries an entity
|
|
305
|
-
soft delete, a seed planted by `IOIMSlotStamp`): a column the new entity drops is
|
|
306
|
-
woken, an unchanged one is not, and
|
|
286
|
+
**Emptiness decides lifetime, never the write path.** A `writeSlotContent` (or `writeSlotContentByKey` on a
|
|
287
|
+
hit) onto an entity slot that still carries an entity is put over it even when emptiness calls the slot empty
|
|
288
|
+
(a soft delete, a seed planted by `IOIMSlotStamp`): a column the new entity drops is
|
|
289
|
+
woken, an unchanged one is not, and a write that repeats the entity exactly wakes
|
|
307
290
|
nothing. Only an entity slot with no entity at all is filled whole.
|
|
308
291
|
|
|
309
292
|
**`IOIMSlotStamp`** writes onto the slot at every write, just before emptiness is
|
|
@@ -324,53 +307,67 @@ interface IOIMSlotReleasePolicy {
|
|
|
324
307
|
cancelSlotRelease(slot: TOIMSlot): void;
|
|
325
308
|
}
|
|
326
309
|
|
|
327
|
-
new
|
|
328
|
-
|
|
329
|
-
new
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
310
|
+
new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: number, slotReleaseDueFramePolicy?: IOIMSlotReleaseDueFramePolicy })
|
|
311
|
+
OIMSlotReleaseAutoDueFramePolicy.INSTANCE
|
|
312
|
+
new OIMSlotReleaseManualDueFramePolicy() // openSlotReleaseDueFrame() · closeSlotReleaseDueFrame() · writeInSlotReleaseDueFrame(write)
|
|
313
|
+
new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: number })
|
|
314
|
+
new OIMSlotAfterUnholdReleasePolicy({ keepAfterUnholdMs: number })
|
|
315
|
+
OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
316
|
+
OIMSlotAtEmptyReleasePolicy.INSTANCE
|
|
317
|
+
OIMSlotNeverReleasePolicy.INSTANCE
|
|
318
|
+
new OIMSlotEarliestReleasePolicy(slotReleasePolicies: readonly IOIMSlotReleasePolicy[])
|
|
334
319
|
```
|
|
335
320
|
|
|
336
321
|
`releasesWrittenSlots` is whether the policy can ever release a slot that holds
|
|
337
322
|
data; a service reads it once at construction and skips the after-write
|
|
338
|
-
release check when it is false (`
|
|
339
|
-
`
|
|
323
|
+
release check when it is false (`OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`: false;
|
|
324
|
+
`OIMSlotAtUnholdReleasePolicy` and the timed policies: true; `OIMSlotEarliestReleasePolicy`: true
|
|
340
325
|
when any policy it combines is).
|
|
341
326
|
|
|
342
327
|
`schedulesReleaseOnWrite` is whether a write can make the policy schedule a release; a service
|
|
343
328
|
reads it once at construction and, when it is false and no `slotStamp` is set, skips `onSlotWritten`
|
|
344
|
-
and the first-fill `onSlotIdle` on every write (`
|
|
345
|
-
true; `
|
|
346
|
-
`
|
|
329
|
+
and the first-fill `onSlotIdle` on every write (`OIMSlotAfterWriteReleasePolicy`, `OIMSlotAfterIdleReleasePolicy`:
|
|
330
|
+
true; `OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`, `OIMSlotAtUnholdReleasePolicy`,
|
|
331
|
+
`OIMSlotAfterUnholdReleasePolicy`: false; `OIMSlotEarliestReleasePolicy`: true when any policy it combines is).
|
|
347
332
|
`onSlotIdle` is still called when the last subscriber leaves.
|
|
348
333
|
|
|
349
334
|
A policy decides whether and when a slot goes; it does no work itself. A
|
|
350
335
|
factory, and `OIMDB` (whose `slotReleasePolicy` option every factory reads as its default), takes ONE
|
|
351
336
|
through the option `slotReleasePolicy`. With none, the slot owner is built with
|
|
352
|
-
`
|
|
337
|
+
`OIMSlotAtEmptyReleasePolicy`, so a slot with a value stays for good and an empty
|
|
353
338
|
one with nothing holding it goes; there is no "no policy" branch on any path.
|
|
354
|
-
Computed services default to `
|
|
339
|
+
Computed services default to `OIMSlotAtUnholdReleasePolicy`.
|
|
355
340
|
|
|
356
341
|
| Policy | A slot goes |
|
|
357
342
|
|---|---|
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
-
| `
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
`
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
`
|
|
343
|
+
| `OIMSlotAfterWriteReleasePolicy` | once its last write is older than `keepAfterWriteMs` |
|
|
344
|
+
| `OIMSlotAfterIdleReleasePolicy` | once nothing has watched it for `keepAfterIdleMs` |
|
|
345
|
+
| `OIMSlotAfterUnholdReleasePolicy` | once `keepAfterUnholdMs` has passed since its LAST unhold; a hold taken again inside the window keeps it |
|
|
346
|
+
| `OIMSlotAtUnholdReleasePolicy` | at the last unhold, even with a value |
|
|
347
|
+
| `OIMSlotAtEmptyReleasePolicy` | once it has no value and nothing holds it; a computed, never empty, stays while its owner lives |
|
|
348
|
+
| `OIMSlotNeverReleasePolicy` | never, empty or not; a deleted key's slot stays filed |
|
|
349
|
+
| `OIMSlotEarliestReleasePolicy` | when the earliest deadline of its members falls due — a `min` over deadlines |
|
|
350
|
+
|
|
351
|
+
**WHICH WRITES MOVE THE WRITE DEADLINE is a strategy of `OIMSlotAfterWriteReleasePolicy`**,
|
|
352
|
+
`slotReleaseDueFramePolicy`, chosen at construction. `OIMSlotReleaseAutoDueFramePolicy` (the default)
|
|
353
|
+
makes every write its own frame, so every write moves the deadline. `OIMSlotReleaseManualDueFramePolicy`
|
|
354
|
+
moves it only for writes made while one of its frames is open: `openSlotReleaseDueFrame()` and
|
|
355
|
+
`closeSlotReleaseDueFrame()` (which throws by name when no frame is open), or
|
|
356
|
+
`writeInSlotReleaseDueFrame(write)`, which opens, calls `write` and closes in a `finally`. One Manual object
|
|
357
|
+
can be passed to several `OIMSlotAfterWriteReleasePolicy` policies with different windows: one frame then moves each slot's
|
|
358
|
+
deadline by its own collection's window. This is what keeps a local edit or a client-only field from
|
|
359
|
+
extending data that only a server response should refresh. `write` must be synchronous: a frame closes
|
|
360
|
+
when the function returns.
|
|
361
|
+
|
|
362
|
+
The release wheels live on the db's collector slot, at most one of each per `OIMDB`:
|
|
363
|
+
`db.collectorSlot.afterWriteSlotReleaseWheel`, `afterIdleSlotReleaseWheel` and
|
|
364
|
+
`afterUnholdSlotReleaseWheel`, each an `OIMSlotReleaseWheel` that holds its own slices, timer and clock.
|
|
365
|
+
A wheel is created by the first policy that needs it, through `oimMintAfterWriteSlotReleaseWheel(collectorSlot)`
|
|
366
|
+
(and its after-idle and after-unhold twins), which returns the existing wheel or creates it; until then the field is
|
|
367
|
+
`undefined`, and a db with no timed policy never pulls the wheel code into a bundle. The
|
|
371
368
|
clock and the timer are the db's: pass `new OIMDB({ getNowMs, releaseTimer })` to control time (tests),
|
|
372
|
-
and sweep with `
|
|
373
|
-
`
|
|
369
|
+
and sweep with `oimMintAfterIdleSlotReleaseWheel(db.collectorSlot).flush()`.
|
|
370
|
+
`OIMSlotAtUnholdReleasePolicy` is the zero window and has no collector.
|
|
374
371
|
|
|
375
372
|
A zero window never touches a release queue or a timer: when a slot goes idle with
|
|
376
373
|
`onSlotIdle` answering `true`, it is released at once if nothing holds it, and
|
|
@@ -379,16 +376,11 @@ leaves at the end of the frame. A window above zero is filed in that policy's ow
|
|
|
379
376
|
queue.
|
|
380
377
|
|
|
381
378
|
```ts
|
|
382
|
-
import {
|
|
383
|
-
OIMSlotReleasePolicyAfterIdle,
|
|
384
|
-
OIMSlotReleasePolicyAfterWrite,
|
|
385
|
-
OIMSlotReleasePolicyEarliest,
|
|
386
|
-
} from '@oimdb/core';
|
|
387
379
|
|
|
388
380
|
const releasingDb = new OIMDB({
|
|
389
|
-
slotReleasePolicy: new
|
|
390
|
-
new
|
|
391
|
-
new
|
|
381
|
+
slotReleasePolicy: new OIMSlotEarliestReleasePolicy([
|
|
382
|
+
new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: 60_000 }),
|
|
383
|
+
new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
|
|
392
384
|
]),
|
|
393
385
|
});
|
|
394
386
|
```
|
|
@@ -400,9 +392,9 @@ no other:
|
|
|
400
392
|
|
|
401
393
|
| Marker | Moved by | Not moved by |
|
|
402
394
|
|---|---|---|
|
|
403
|
-
| `releaseDueAfterWriteMs` | every write, under `
|
|
404
|
-
| `releaseDueAfterIdleMs` | the first content of a slot with no subscriber, and the departure of the last subscriber, under `
|
|
405
|
-
| `releaseDueAfterUnholdMs` | every last unhold, under `
|
|
395
|
+
| `releaseDueAfterWriteMs` | every write, under `OIMSlotAfterWriteReleasePolicy` | a subscriber arriving or leaving |
|
|
396
|
+
| `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 |
|
|
397
|
+
| `releaseDueAfterUnholdMs` | every last unhold, under `OIMSlotAfterUnholdReleasePolicy` | a write |
|
|
406
398
|
|
|
407
399
|
`-1` means unset. A slot goes on the SOONEST of those that are set — each
|
|
408
400
|
marker is an eviction deadline, so whichever says "out" first decides — and a
|
|
@@ -423,8 +415,8 @@ now; `false` keeps it, and a policy with a finite deadline has itself filed the
|
|
|
423
415
|
slot into its release queue (a marker of `-1` is kept with nothing scheduled).
|
|
424
416
|
An **empty** slot is released without asking, and a service given no release
|
|
425
417
|
policy keeps a non-empty slot. `OIMComputedService` defaults its policy to
|
|
426
|
-
`
|
|
427
|
-
nothing refers to goes at once. `
|
|
418
|
+
`OIMSlotAtUnholdReleasePolicy`, which always answers `true`, so a derivation
|
|
419
|
+
nothing refers to goes at once. `OIMSlotEarliestReleasePolicy` asks every member
|
|
428
420
|
and answers `true` if any does.
|
|
429
421
|
|
|
430
422
|
**AN IDLE WINDOW OPENS IN EXACTLY TWO PLACES, AND NOWHERE ELSE:**
|
|
@@ -435,7 +427,7 @@ and answers `true` if any does.
|
|
|
435
427
|
2. **when the LAST SUBSCRIBER leaves** — `OIMSlotOwner.unsubscribeFromSlot` empties
|
|
436
428
|
`slot.effects` and runs the idle step, which opens one.
|
|
437
429
|
|
|
438
|
-
Both ask the policy's `onSlotIdle(slot)`; under `
|
|
430
|
+
Both ask the policy's `onSlotIdle(slot)`; under `OIMSlotAfterIdleReleasePolicy`
|
|
439
431
|
it stamps `releaseDueAfterIdleMs` and files the slot into the policy's queue. An
|
|
440
432
|
**empty** slot gets no window, and the release guard takes it without asking a
|
|
441
433
|
clock at all.
|
|
@@ -449,60 +441,65 @@ therefore taken at the give-back and not one window later.
|
|
|
449
441
|
nothing — and the queue has taken the slot off its slice, so nothing re-files it.
|
|
450
442
|
The give-back is where the guard is asked again.
|
|
451
443
|
|
|
452
|
-
### The
|
|
444
|
+
### The release wheels
|
|
453
445
|
|
|
454
|
-
Each of `
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
|
|
446
|
+
Each of `OIMSlotAfterWriteReleasePolicy`, `OIMSlotAfterIdleReleasePolicy` and
|
|
447
|
+
`OIMSlotAfterUnholdReleasePolicy` files into its own release wheel, and the difference between the wheels
|
|
448
|
+
is WHICH MARKER each reads and writes — injected at construction, so there is no flag and no branch on the
|
|
449
|
+
sweep path. A wheel is an aggregate: its slices, its timer and its clock are fields of the object.
|
|
458
450
|
|
|
459
451
|
```ts signature
|
|
460
|
-
abstract class
|
|
452
|
+
abstract class OIMSlotWheel {
|
|
453
|
+
readonly getNowMs: () => number;
|
|
454
|
+
getNextSliceNumber(): number | undefined; // the nearest filed slice, or undefined
|
|
455
|
+
sweepUpToSliceNumber(upToSliceNumber: number): void;
|
|
456
|
+
flush(): void; // sweeps what is due now and re-arms the timer
|
|
457
|
+
cancelAllSlots(): void; // takes the timer down and drops every filing
|
|
458
|
+
}
|
|
459
|
+
class OIMSlotReleaseWheel extends OIMSlotWheel {
|
|
460
|
+
constructor(options: TOIMSlotReleaseWheelOptions); // { getNowMs, timer, errorPolicy, getReleaseDueMs, setReleaseDueMs, releaseScheduledSlotFlag }
|
|
461
461
|
readonly getReleaseDueMs: (slot: TOIMSlot) => number;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
isSlotReleasable(
|
|
466
|
-
cancelSlotRelease(
|
|
467
|
-
|
|
468
|
-
cancelAllSlotReleases(releaseSlotCollectorSlot): void;
|
|
462
|
+
markSlotReleaseDue(slot, keepMs): number; // stamps now + keepMs on this marker, returns now
|
|
463
|
+
markSlotReleaseDueNow(slot): void; // the marker is now; an unheld, unwatched slot goes at once
|
|
464
|
+
scheduleSlotRelease(slot, nowMs): void; // files the slot at its marker; -1 or already filed files nothing
|
|
465
|
+
isSlotReleasable(slot): boolean; // due now; if not yet, files it on the wheel
|
|
466
|
+
cancelSlotRelease(slot): void; // no-op for a slot whose marker is -1
|
|
467
|
+
isSlotReleaseScheduled(slot): boolean;
|
|
469
468
|
}
|
|
470
|
-
OIMReleaseSlotCollectorServiceAfterIdle.INSTANCE
|
|
471
|
-
OIMReleaseSlotCollectorServiceAfterWrite.INSTANCE
|
|
472
|
-
OIMReleaseSlotCollectorServiceAfterUnhold.INSTANCE
|
|
473
469
|
|
|
474
470
|
type TOIMCollectorSlot = {
|
|
475
471
|
readonly unusedSlotCollectorSlot: TOIMUnusedSlotCollectorSlot;
|
|
476
|
-
readonly
|
|
477
|
-
|
|
478
|
-
|
|
472
|
+
readonly slotWheelOptions: TOIMSlotWheelOptions; // the db's clock, timer and error policy
|
|
473
|
+
afterIdleSlotReleaseWheel: OIMSlotReleaseWheel | undefined; // created on first use
|
|
474
|
+
afterWriteSlotReleaseWheel: OIMSlotReleaseWheel | undefined;
|
|
475
|
+
afterUnholdSlotReleaseWheel: OIMSlotReleaseWheel | undefined;
|
|
479
476
|
};
|
|
477
|
+
oimMintAfterWriteSlotReleaseWheel(collectorSlot): OIMSlotReleaseWheel // and …AfterIdle…, …AfterUnhold…
|
|
480
478
|
```
|
|
481
479
|
|
|
482
480
|
The clock (`getNowMs`, default `Date.now`), the timer (`releaseTimer`, default an unref'd `setTimeout`)
|
|
483
|
-
and the `errorPolicy` (default `
|
|
481
|
+
and the `errorPolicy` (default `OIMLogAndContinueErrorPolicy.INSTANCE`) are `OIMDB` options, shared by the
|
|
484
482
|
channel and every release collector of that db. A release that throws inside a hand-called `flush` throws to
|
|
485
483
|
that caller; one that throws on the collector's own timer tick has no caller and goes to the db's
|
|
486
484
|
`errorPolicy`. So one error policy on the db routes every error through one place:
|
|
487
485
|
|
|
488
486
|
```ts
|
|
489
|
-
import { OIMErrorPolicyThrowAtOnce, OIMSlotReleasePolicyAfterIdle } from '@oimdb/core';
|
|
490
487
|
|
|
491
488
|
const oneSinkDb = new OIMDB({
|
|
492
|
-
errorPolicy: new
|
|
493
|
-
slotReleasePolicy: new
|
|
489
|
+
errorPolicy: new OIMThrowAtOnceErrorPolicy(),
|
|
490
|
+
slotReleasePolicy: new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
|
|
494
491
|
});
|
|
495
492
|
```
|
|
496
493
|
|
|
497
|
-
|
|
|
494
|
+
| Wheel | Reads | What renews it | What does not |
|
|
498
495
|
|---|---|---|---|
|
|
499
|
-
| `
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
496
|
+
| `afterIdleSlotReleaseWheel` | `releaseDueAfterIdleMs` | the first content of a slot, and the give-back of the last hold | a later write |
|
|
497
|
+
| `afterWriteSlotReleaseWheel` | `releaseDueAfterWriteMs` | every write, under its policy | a subscriber arriving or leaving |
|
|
498
|
+
| `afterUnholdSlotReleaseWheel` | `releaseDueAfterUnholdMs` | every last unhold | a hold taken again inside the window |
|
|
502
499
|
|
|
503
|
-
**A CONSUMER MAY RUN BOTH, AND THEY SHARE NOTHING.** Each has its own
|
|
504
|
-
|
|
505
|
-
nothing has to be reconciled: `
|
|
500
|
+
**A CONSUMER MAY RUN BOTH, AND THEY SHARE NOTHING.** Each wheel has its own slices and its own
|
|
501
|
+
scheduled run. Whichever deadline falls due first takes the slot, and
|
|
502
|
+
nothing has to be reconciled: `OIMSlotEarliestReleasePolicy` answers a `min`
|
|
506
503
|
over its members' deadlines, so a sweep of either queue reaches the same verdict.
|
|
507
504
|
|
|
508
505
|
**NEITHER MEANS UNREAD.** No read is seen and no door reports one: an identity a
|
|
@@ -517,7 +514,7 @@ The slot owner's `releaseSlotIfUnheld` checks `holdCount > 0`, then
|
|
|
517
514
|
`oimIsSlotReleased(slot)`, then `slot.owner !== this`, then `hasReference(slot)`, and
|
|
518
515
|
only then — on a slot that is not empty — asks the policy's `isSlotReleasable`; an empty slot
|
|
519
516
|
asks `isEmptySlotReleasable` instead, which every policy but
|
|
520
|
-
`
|
|
517
|
+
`OIMSlotNeverReleasePolicy` answers `true`. So only `OIMSlotNeverReleasePolicy`
|
|
521
518
|
keeps an identity that holds nothing.
|
|
522
519
|
|
|
523
520
|
**A release that falls inside a flush waits for it to end.** Releasing a slot
|
|
@@ -529,7 +526,7 @@ back once the flush is over; the release then runs. An empty slot needs no write
|
|
|
529
526
|
and goes at once.
|
|
530
527
|
|
|
531
528
|
*The `owner !== this` line costs no lookup. A KEYLESS slot — a single's, filed in an
|
|
532
|
-
`
|
|
529
|
+
`OIMSingleSlotStoreService` store — goes through the same guard under its service's policy: an
|
|
533
530
|
emptied single bucket nothing holds is released like a keyed one, and the single service's
|
|
534
531
|
`getSingleSlot(storeSlot)` mints a fresh slot on the next read.*
|
|
535
532
|
|
|
@@ -560,28 +557,35 @@ Pinned by `engine/collector/tests/what-a-stale-index-takes-with-it.test.ts`,
|
|
|
560
557
|
|
|
561
558
|
### The release guard, and who fires it
|
|
562
559
|
|
|
563
|
-
**THE
|
|
564
|
-
`
|
|
560
|
+
**THE WHEEL DECIDES AT A DEADLINE, NOT THE OWNER.**
|
|
561
|
+
`OIMSlotReleaseWheel.sweepUpToSliceNumber`, per slot in a slice at or below the one due:
|
|
565
562
|
|
|
566
|
-
1. `oimIsSlotReleased(slot)` → skip it;
|
|
567
|
-
2. its own marker, read through `getReleaseDueMs(slot)` — the ONE field this
|
|
563
|
+
1. its bit is clear → a stale entry, skip it; `oimIsSlotReleased(slot)` → skip it;
|
|
564
|
+
2. its own marker, read through `getReleaseDueMs(slot)` — the ONE field this wheel
|
|
568
565
|
answers for;
|
|
569
|
-
3. `-1` → the slot is dropped from the
|
|
570
|
-
4. a moment still in the future → the
|
|
566
|
+
3. `-1` → the slot is dropped from the wheel: this policy does not claim it;
|
|
567
|
+
4. a moment still in the future → the wheel **re-files the slot itself**, into
|
|
571
568
|
`oimSliceOfMs(getReleaseDueMs)` through its own slice arithmetic;
|
|
572
569
|
5. a moment already passed → `slot.owner.releaseSlotIfUnheld(slot)`, which asks the
|
|
573
|
-
policy's `isSlotReleasable` and so declines when the other policy still keeps
|
|
574
|
-
the slot.
|
|
570
|
+
policy's `isSlotReleasable` and so declines when the other policy still keeps the
|
|
571
|
+
slot, or when the slot is held.
|
|
572
|
+
|
|
573
|
+
**INVALIDATING IS MARKING THE DEADLINE NOW.** `wheel.markSlotReleaseDueNow(slot)` sets this wheel's
|
|
574
|
+
marker to now and hands the slot to `slot.owner.releaseSlotIfUnheld(slot)` at once: a slot nothing
|
|
575
|
+
holds or watches goes with its data; a held or watched slot stays, keeps its data, and goes the moment
|
|
576
|
+
its last hold or subscriber leaves, because its deadline is already past. The next marked write moves
|
|
577
|
+
the marker a window ahead again. It writes no content, so it wakes no effect.
|
|
575
578
|
|
|
576
579
|
The owner files nothing from inside a sweep. It **opens** a window on its own
|
|
577
580
|
events, through the policy's `onSlotIdle` — when a slot first gets content, and
|
|
578
581
|
when the last subscriber leaves — and `onSlotUnheld` on the give-back of the last hold.
|
|
579
582
|
|
|
580
|
-
**A SLOT ALREADY ON A QUEUE STAYS WHERE IT IS.** `scheduleSlotRelease` of a slot
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
583
|
+
**A SLOT ALREADY ON A QUEUE STAYS WHERE IT IS.** `scheduleSlotRelease` of a slot that is
|
|
584
|
+
already filed moves nothing: the sweep re-reads the deadline when that slice comes
|
|
585
|
+
due and re-files the slot then (the timing-wheel lazy re-insertion). So a write to a
|
|
586
|
+
queued entity slot costs the queue one bit test, not an unfile and a refile. A
|
|
587
|
+
deadline only ever moves later — each policy adds a fixed window to a clock that
|
|
588
|
+
does not go back — so the slice a slot already sits in is never later than its deadline.
|
|
585
589
|
|
|
586
590
|
`releaseSlotIfUnheld(slot)` is the guard, and it releases only when all of
|
|
587
591
|
`holdCount === 0`, `oimIsSlotReleased(slot)` is false, `slot.owner === this`,
|
|
@@ -591,7 +595,7 @@ happens to it. On a slot that is not empty with time still owed, `isSlotReleasab
|
|
|
591
595
|
answers `false` and the policy files the slot into its queue at its own marker, so a
|
|
592
596
|
filing can neither shorten nor extend a deadline. That is the path from `unholdSlot` and from a write, never from a
|
|
593
597
|
sweep, which had already re-filed at step 4. Releasing hands the slot to the
|
|
594
|
-
content
|
|
598
|
+
content service's `releaseSlotContent` (which gives back what the content held),
|
|
595
599
|
marks the slot empty, calls the policy's `cancelSlotRelease(slot)`, unfiles the key
|
|
596
600
|
through the store service's `remove`, and sets the RELEASED flag in `slot.slotFlags`. The `owner` is NOT cleared — the slot keeps
|
|
597
601
|
pointing at the object that let it go, which is what makes `slot.owner !== this`
|
|
@@ -604,48 +608,48 @@ opposite:
|
|
|
604
608
|
`oimIsSlotReleased(slot)` and returns; it does not throw. A sweep meeting a slot
|
|
605
609
|
something else already let go must not raise.
|
|
606
610
|
- **Trying to WORK with a released slot throws by name.** `assertNotReleased`
|
|
607
|
-
guards the content services' write doors (`
|
|
608
|
-
`
|
|
611
|
+
guards the content services' write doors (`writeSlotContent`, `clearSlotContent`, `updateEntity`, `markEntityChanged`,
|
|
612
|
+
`addBucketMemberSlot`, `removeBucketMemberSlot`, `markContentChanged`) and the reference doors (`holdSlot`, `unholdSlot`, `subscribeToSlot`,
|
|
609
613
|
`unsubscribeFromSlot`). The refusal message starts `This identity was released:`.
|
|
610
614
|
|
|
611
|
-
### The
|
|
615
|
+
### The wheels and the unused-slot collector
|
|
612
616
|
|
|
613
617
|
```ts signature
|
|
614
|
-
|
|
615
|
-
|
|
618
|
+
oimMintAfterWriteSlotReleaseWheel(db.collectorSlot) // and …AfterIdle…, …AfterUnhold…
|
|
619
|
+
new OIMSlotRefetchWheel(options: TOIMSlotRefetchWheelOptions) // { slotRefetchListener, getNowMs?, timer?, errorPolicy? }
|
|
616
620
|
OIMUnusedSlotCollectorService.INSTANCE
|
|
617
621
|
createOIMUnusedSlotCollectorSlot(): TOIMUnusedSlotCollectorSlot
|
|
618
622
|
```
|
|
619
623
|
|
|
620
|
-
|
|
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
|
-
|
|
624
|
+
**A wheel is the clock.** `OIMSlotWheel` extends `OIMFlushSchedulerService` and is its own scheduled slot:
|
|
625
|
+
`flush()` sweeps what is due and re-arms the timer to the next deadline. `cancelAllSlots()` takes the timer
|
|
626
|
+
down and drops every filing; the next filing arms it again. The release wheels take the clock (`getNowMs`)
|
|
627
|
+
and the timer from the db, so a test drives them through `new OIMDB({ getNowMs, releaseTimer })`. The default
|
|
628
|
+
timer is a `setTimeout` that calls `unref` where the platform has it, so an idle Node process is not held open.
|
|
629
|
+
|
|
630
|
+
**A wheel files slots into time slices**, not one entry each. `oimSliceOfMs(ms)` is
|
|
631
|
+
`Math.floor(ms / OIM_SLICE_LENGTH_MS)` and `OIM_SLICE_LENGTH_MS` is `1024`, so every slot due inside the
|
|
632
|
+
same ~1s slice shares one array and one timer.
|
|
633
|
+
|
|
634
|
+
**WHETHER A SLOT IS ON A RELEASE WHEEL IS A BIT ON THE SLOT, NOT A MAP.** Each of the three release
|
|
635
|
+
wheels of a db owns one bit of `slot.slotFlags` (`releaseScheduledSlotFlag`, `4`, `8`, `16`), so a slot
|
|
636
|
+
can sit on all three at once, once on each. Filing pushes the slot onto its slice's array and sets the
|
|
637
|
+
bit; cancelling clears the bit and leaves the entry, which the sweep skips when its slice comes due. A
|
|
638
|
+
cancelled slot therefore keeps its slice named by `getNextSliceNumber` until that sweep.
|
|
639
|
+
|
|
640
|
+
A slot re-filed during a sweep cannot land in a slice the same sweep is still walking: the sweep raises a
|
|
641
|
+
floor for its duration, so a re-file — the wheel's own at step 4, or one a release makes — goes to the next
|
|
642
|
+
slice and is looked at next time, not in a loop.
|
|
643
|
+
|
|
644
|
+
**`OIMSlotRefetchWheel` is a wheel of slots due a refetch at an explicit time.** It computes no deadline,
|
|
645
|
+
keeps no bit and no field on the slot: `scheduleSlotRefetchAt(slot, atMs)` files an entry into the slice
|
|
646
|
+
of `atMs`, every call files one more. Its sweep takes only slices that have fully elapsed, skips released
|
|
647
|
+
slots, drops slots nobody subscribes to, and calls `slotRefetchListener.onSlotsRefetchDue(slots)` ONCE per
|
|
648
|
+
sweep with every watched slot that came due; the array is reused and emptied after the call, so a listener
|
|
649
|
+
does not keep it. Moving a refetch earlier is another `scheduleSlotRefetchAt`; the listener decides from
|
|
650
|
+
its own data whether an entry is current, so an early or duplicate entry is skipped there. The wheel is
|
|
651
|
+
created by whoever needs it, not by the db, and its clock, timer and error policy default to `Date.now`,
|
|
652
|
+
an unref'd `setTimeout` and `OIMLogAndContinueErrorPolicy.INSTANCE`.
|
|
649
653
|
|
|
650
654
|
**The unused-slot collector** has nothing to do with time. It is the one that makes
|
|
651
655
|
`getSlotByKey(storeSlot, key)` safe: `OIMUnusedSlotCollectorService.INSTANCE.holdSlotUntilFlush(unusedSlotCollectorSlot, slot)`
|
|
@@ -709,12 +713,12 @@ path. There is no `if (inPlace)` anywhere in the engine.
|
|
|
709
713
|
| Fork | Strategies |
|
|
710
714
|
|---|---|
|
|
711
715
|
| how a write lands | `createOIMEntityWriterInPlace()` / `createOIMEntityWriterReplacing()` |
|
|
712
|
-
| how a bucket holds slots | `
|
|
713
|
-
| what naming a slot records | `
|
|
714
|
-
| 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 bucket holds slots | `OIMSlotContentSetBucketService` / `OIMSlotContentPositionalArrayBucketService` (the positions are the caller's) |
|
|
717
|
+
| what naming a slot records | `bucketMemberSlotsArrayService: OIMBucketMemberSlotsArrayService` (one per mention, the default) / `new OIMBucketUniqueMemberSlotsArrayService<TSlot>()` (one per slot), on either array bucket |
|
|
718
|
+
| 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` |
|
|
715
719
|
| how a key is addressed | `OIMDefaults.KEY_TREE_WALK_SCALAR` / `OIMDefaults.KEY_TREE_WALK_COMPOSITE` |
|
|
716
|
-
| when a slot may go | `
|
|
717
|
-
| who decides a bucket's slots | a bucket collection (`
|
|
720
|
+
| when a slot may go | `OIMSlotAfterIdleReleasePolicy` / `OIMSlotAfterWriteReleasePolicy`, either or both |
|
|
721
|
+
| 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) |
|
|
718
722
|
|
|
719
723
|
**One strategy per kind per app.** A V8 call site is shared by every service
|
|
720
724
|
that runs the same engine code, so mixing strategies of one kind in one process
|
|
@@ -724,7 +728,7 @@ V8 still caches each one and can still inline
|
|
|
724
728
|
per-site cache and no inlining through it. The kinds that count: release policies
|
|
725
729
|
(the `IOIMSlotReleasePolicy` strategies), set-driven vs array-driven buckets and indexes, entity
|
|
726
730
|
writers (`createOIMEntityWriterInPlace` / `createOIMEntityWriterReplacing`) and
|
|
727
|
-
`
|
|
731
|
+
`TOIMEntityInserter`, `TOIMSameValueGuard`, `IOIMSlotEmptinessPolicy`, and
|
|
728
732
|
`TOIMChannelScheduler`. Two or three of one kind is fine; five release policies in
|
|
729
733
|
one process is not. The pair
|
|
730
734
|
`alt.slot-shape.three-shapes-against-one-at-the-write` measures three slot shapes
|
|
@@ -735,27 +739,23 @@ Generate code with one strategy per kind unless the task names a second.
|
|
|
735
739
|
|
|
736
740
|
`createStoreSlot(db)` stamps the db's `defaultChannelSlot` on the store slot, and every write to its slots
|
|
737
741
|
is reported to that channel. No service holds a channel; there is no `channel` option and no
|
|
738
|
-
`unusedSlotCollector` option
|
|
742
|
+
`unusedSlotCollector` option:
|
|
739
743
|
|
|
740
744
|
```ts
|
|
741
|
-
const cardContentService = createOIMSlotContentEntityService<Card>(db
|
|
742
|
-
const handBuiltCardService = createOIMSlotCollectionService({ slotContentService: cardContentService });
|
|
745
|
+
const cardContentService = createOIMSlotContentEntityService<Card>(db);
|
|
746
|
+
const handBuiltCardService = createOIMSlotCollectionService(db, { slotContentService: cardContentService });
|
|
743
747
|
handBuiltCardService.createStoreSlot(db);
|
|
744
|
-
|
|
745
|
-
// THE CONVENIENCE WRAPPER — the same two calls in one:
|
|
746
|
-
createOIMEntityService<Card>(db, { keyOf: (c) => c.id }).createStoreSlot(db);
|
|
747
748
|
```
|
|
748
749
|
|
|
749
|
-
A single entity is `
|
|
750
|
-
`createOIMSlotContentEntityService(db, { slotStoreService: OIMSlotStoreServiceSingleSlotDriven.INSTANCE })`),
|
|
750
|
+
A single entity is `createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<T>(db) })`,
|
|
751
751
|
and its store slot takes the db's channel too and always reports.
|
|
752
752
|
|
|
753
753
|
### A bucket's slots are the caller's
|
|
754
754
|
|
|
755
755
|
A bucket's slots are a fact about the **response** being composed — a search result, a
|
|
756
756
|
page, a window — and **no column can express absence**. So a bucket collection
|
|
757
|
-
is manual, and `entityService.slotContentService.
|
|
758
|
-
bucket. Use an index (`
|
|
757
|
+
is manual, and `entityService.slotContentService.clearSlotContent(slot)` does not take a slot out of any
|
|
758
|
+
bucket. Use an index (`createOIMSlotCollectionIndexService`) when the slots are determined by the entity's key value,
|
|
759
759
|
and an index should maintain buckets only for keys being held.
|
|
760
760
|
|
|
761
761
|
---
|
|
@@ -772,7 +772,7 @@ new OIMDB(options?: TOIMDBOptions)
|
|
|
772
772
|
|
|
773
773
|
type TOIMDBOptions = {
|
|
774
774
|
channelScheduler?: TOIMChannelScheduler; // absent = you flush by hand
|
|
775
|
-
errorPolicy?: IOIMErrorPolicy; // absent =
|
|
775
|
+
errorPolicy?: IOIMErrorPolicy; // absent = OIMLogAndContinueErrorPolicy.INSTANCE
|
|
776
776
|
onBeforeFlush?: () => void; // start of the flush, before the frame; READ ONLY, a write is refused
|
|
777
777
|
getNowMs?: () => number; // the release collectors' clock; absent = Date.now
|
|
778
778
|
releaseTimer?: TOIMTimer; // the release collectors' timer; absent = an unref'd setTimeout
|
|
@@ -788,9 +788,9 @@ db.defaultChannelSlot // TOIMChannelSlot
|
|
|
788
788
|
db.flush() // OIMChannelService.INSTANCE.flush(db.defaultChannelSlot)
|
|
789
789
|
|
|
790
790
|
interface IOIMErrorPolicy { onErrorCaught(error: unknown): void }
|
|
791
|
-
|
|
792
|
-
new
|
|
793
|
-
new
|
|
791
|
+
OIMLogAndContinueErrorPolicy.INSTANCE // console.error; the flush goes on
|
|
792
|
+
new OIMLogAndContinueErrorPolicy({ onError?: (error: unknown) => void }) // console.error, then onError
|
|
793
|
+
new OIMThrowAtOnceErrorPolicy() // throws the first error out of the flush
|
|
794
794
|
```
|
|
795
795
|
|
|
796
796
|
**What the error policy covers** — every error with no synchronous caller to
|
|
@@ -801,7 +801,7 @@ hand-called collector `flush` — throws to it. `channelSlot.errorPolicy` is the
|
|
|
801
801
|
instance the channel reports through; it is the db's `errorPolicy`, which the release collectors share,
|
|
802
802
|
so one policy answers for the whole database. **The default digests and logs**: each throw goes to `console.error`
|
|
803
803
|
and then to `onError` (where Sentry or the devtools registry plugs in), and the
|
|
804
|
-
flush finishes every other effect and handler. `
|
|
804
|
+
flush finishes every other effect and handler. `OIMThrowAtOnceErrorPolicy` throws
|
|
805
805
|
the first error out of `flush()` at once; the effects the frame had not reached yet
|
|
806
806
|
stay queued and run at the next flush. **An effect whose body or cleanup threw
|
|
807
807
|
has not taken the change it woke for**: its `ranAtRevision`/`ranAtEpoch` are
|
|
@@ -852,10 +852,6 @@ Every door below is on `OIMChannelService.INSTANCE` and takes the channel slot f
|
|
|
852
852
|
| `assertNotFlushing(channelSlot)` | `void` — throws if the channel is flushing or its frame is running, i.e. for the whole of a flush |
|
|
853
853
|
| `write(channelSlot, slot)` | `void` — asks `assertNotFlushing`, then marks and queues |
|
|
854
854
|
| `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 |
|
|
855
|
-
| `openSubframe(channelSlot)` | `void` — a subframe opens, and the frame cannot close until it closes — `flush` throws meanwhile |
|
|
856
|
-
| `closeSubframe(channelSlot)` | `void` — it closes, delivering what was listed for its close and then asking for the frame it owes |
|
|
857
|
-
| `isSubframeOpen(channelSlot)` | `boolean` — whether a subframe is open right now, so a caller can defer work to its close instead of per write |
|
|
858
|
-
| `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 |
|
|
859
855
|
| `dropChannelSlot(channelSlot)` | `void` — kill the channel whole; see below |
|
|
860
856
|
| `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 |
|
|
861
857
|
|
|
@@ -874,7 +870,7 @@ bucket, a computed — and a batch does not soften it: writes accumulate until
|
|
|
874
870
|
after it.
|
|
875
871
|
|
|
876
872
|
```ts
|
|
877
|
-
cardService.
|
|
873
|
+
cardService.writeSlotContentByKey(cards, 'a', { id: 'a', title: 'A', rank: 1, deck: 'spades' });
|
|
878
874
|
const entitySlot = cardService.getSlotByKey(cards, 'a');
|
|
879
875
|
|
|
880
876
|
effectService.createEffect(db, [entitySlot], [], () => { /* NEVER runs for the upsert above */ });
|
|
@@ -920,87 +916,27 @@ effectService.createEffect(db, [slot], [], () => {
|
|
|
920
916
|
- One list, not two: it goes into the same set at the same phase, so one-shot and
|
|
921
917
|
standing handlers are called in registration order among each other.
|
|
922
918
|
|
|
923
|
-
### `
|
|
924
|
-
|
|
925
|
-
A
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
bucket. Under a synchronous scheduler that gap used to flush — an auto-index
|
|
945
|
-
settled over a bucket the entity slot had not joined yet, stamped itself past the
|
|
946
|
-
entity slot's revision, and the entity was invisible to that index forever with nothing
|
|
947
|
-
thrown.
|
|
948
|
-
- `closeSubframe` is called from a `finally` by every opener, so a subframe that
|
|
949
|
-
threw part-way still closes and still delivers what it managed to write.
|
|
950
|
-
- **The span belongs to the DOOR, and exactly one door opens it.** The entity content service's
|
|
951
|
-
`writeEntity`, `deleteEntity`, `deleteEntities`, `fillEntities`, `updateEntity` and `markEntityChanged` each
|
|
952
|
-
open one — except a write that is exactly one channel write, which opens none — one write cannot be seen
|
|
953
|
-
half-done, and there is nobody to batch for: `updateEntity`/`updateEntityColumn` and `deleteEntity` when
|
|
954
|
-
the store slot has no content listener, no column of the entity is referenced and emptiness is not
|
|
955
|
-
recomputed on a column write; the entity writer's `fillEntity`, `putEntity`, `patchEntity`, `deleteColumns`
|
|
956
|
-
and `clearEntity` open none. One `upsertEntity` therefore opens one span, and one `insertEntities` opens one
|
|
957
|
-
however many entities it carries. The one exception is the content strategy's `releaseSlotContent`, which
|
|
958
|
-
keeps its own: `releaseSlotIfUnheld` reaches it when a release deadline has passed, and that call arrives
|
|
959
|
-
from the release queue through no door at all — so a removal opens two and an eviction opens one.
|
|
960
|
-
- Every write door of a content service is wrapped in the channel's `openSubframe` / `closeSubframe` on
|
|
961
|
-
`storeSlot.channelSlot`.
|
|
962
|
-
- **Every bucket door wakes the bucket the same way.** Each door of a bucket content service that changed
|
|
963
|
-
the bucket — `addBucketMemberSlot(s)`, `removeBucketMemberSlot(s)`,
|
|
964
|
-
`insertBucketMemberSlotAt`, `removeBucketMemberSlotAt` / `…Range`, both moves,
|
|
965
|
-
`writeBucketMemberSlots`, `clearBucketMemberSlots` — lists the bucket's store slot on the channel
|
|
966
|
-
(`listChangedStoreSlot`) and sets the bucket's EMPTY flag there and then; the outermost `closeSubframe`
|
|
967
|
-
stamps each changed bucket once, tells its release policy `onSlotWritten` once, and only then asks
|
|
968
|
-
whether it may go. So `hasSlotContentByKey` answers right inside an open batch, a
|
|
969
|
-
batch that mixes doors stamps the bucket once, a move renews a write deadline,
|
|
970
|
-
and a door that changed nothing stamps nothing and tells the policy nothing.
|
|
971
|
-
Commands are still recorded at the door, because a position is true only when it
|
|
972
|
-
is written. A release asked for while the bucket has a pending change waits for
|
|
973
|
-
the close.
|
|
974
|
-
- **A content listener that throws on one bucket does not strand the others.**
|
|
975
|
-
The close stamps every filed bucket and settles it before its listeners run,
|
|
976
|
-
goes on to the next bucket when one listener throws, and rethrows the first error
|
|
977
|
-
once every bucket is through — costing nothing when nothing throws.
|
|
978
|
-
- **A released identity is refused before the bucket changes.** Every door that
|
|
979
|
-
takes a member — the add doors, the doors by key, `insertBucketMemberSlotAt`,
|
|
980
|
-
a whole-list `writeBucketMemberSlots`, and the fill of a minted bucket — checks what it was
|
|
981
|
-
handed with `oimIsSlotReleased` first and throws, so a refused door leaves the members,
|
|
982
|
-
their holds and the EMPTY flag exactly as they were. It costs one field read per
|
|
983
|
-
member handed in.
|
|
984
|
-
|
|
985
|
-
### `isSubframeOpen(channelSlot)` / `listChangedStoreSlot(channelSlot, storeSlot, subframeCloseListener)`
|
|
986
|
-
|
|
987
|
-
**What a batch does once instead of per entity.** A subframe is the unit a
|
|
988
|
-
service can defer work to the end of: `listChangedStoreSlot` lists a store slot together with the
|
|
989
|
-
`IOIMSubframeCloseListener` (`onStoreSlotSubframeClosed(storeSlot)`) that delivers its changes, and the
|
|
990
|
-
outermost `closeSubframe` calls each listed listener with its store slot. `isSubframeOpen` says whether there
|
|
991
|
-
is a subframe to defer into at all — with none, the caller does the work immediately.
|
|
992
|
-
|
|
993
|
-
- **The drain runs before the count drops**, so a listener that writes is still inside the subframe and
|
|
994
|
-
still cannot close the frame. A store slot listed by a listener during the drain is picked up by the same
|
|
995
|
-
drain.
|
|
996
|
-
- **Every listed entry runs.** A listener that throws loses its own work for that batch and nothing retries
|
|
997
|
-
it; the entries after it still run, the list is emptied, the count still drops, the frame the subframe
|
|
998
|
-
owed is still asked for, and then the FIRST throw leaves `closeSubframe`.
|
|
999
|
-
- **What uses it**: the bucket content services. A join or a leave records the bucket slot on its store
|
|
1000
|
-
slot's write queues and lists that store slot; the drain then writes the channel, settles emptiness and
|
|
1001
|
-
asks `releaseSlotIfUnheld` **once per bucket per batch** rather than once per member. Filing 10 000 entity
|
|
1002
|
-
slots into 100 buckets pays 100 of each tail instead of 10 000. Nothing is delivered before the flush either
|
|
1003
|
-
way, so the two are behaviourally the same.
|
|
919
|
+
### Batches: `openBatch` / `closeBatch` / `writeInBatch` on a collection
|
|
920
|
+
|
|
921
|
+
There are no bulk writes. A load or a sync is a loop of single writes, and a collection
|
|
922
|
+
service wraps it in a batch on its store slot:
|
|
923
|
+
|
|
924
|
+
```ts
|
|
925
|
+
cardService.writeInBatch(cards, () => {
|
|
926
|
+
for (const card of payload) cardService.writeSlotContentByKey(cards, card.id, card);
|
|
927
|
+
});
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
- `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `writeInBatch` is the
|
|
931
|
+
`try/finally` form and returns what its function returns.
|
|
932
|
+
- **What the batch changes is who hears the writes, and when.** A content-change listener on the store slot (an
|
|
933
|
+
index on the collection) hears every write of the batch in ONE call, `onContentChangedMany`, when the
|
|
934
|
+
outermost batch closes. Code inside the batch reads the index as it was before the batch.
|
|
935
|
+
- Effects are not batched by it: every write marks its slot on the channel as usual, and the frame delivers each
|
|
936
|
+
identity once whatever the batch did. A `flush()` inside a batch is allowed; the index catches up when the
|
|
937
|
+
batch closes.
|
|
938
|
+
- A single write outside a batch tells the index at once, and is not rolled back: the slot is filed first and the
|
|
939
|
+
index is told after, so an index that throws leaves the write in place and the throw reaches the caller.
|
|
1004
940
|
|
|
1005
941
|
### `dropChannelSlot(channelSlot)`
|
|
1006
942
|
|
|
@@ -1050,19 +986,11 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
|
|
|
1050
986
|
`finally`; `assertNotFlushing` throws on `isFlushing` or `frame.isRunning`, so
|
|
1051
987
|
all three phases are refused. Refusal is per channel: a flush of one db's channel does not refuse a
|
|
1052
988
|
write to another db's.
|
|
1053
|
-
- **A refused write keeps its change pending — it is never dropped.** A bucket
|
|
1054
|
-
carries a change `addBucketMemberSlots`/`removeBucketMemberSlots` recorded inside an outer open batch
|
|
1055
|
-
until that batch closes. If something delivers it mid-flush — a member's
|
|
1056
|
-
subscriber giving the list back, so `unholdSlot` reaches `releaseSlotIfUnheld` —
|
|
1057
|
-
the write is refused and the bucket's store slot stays listed, so the next
|
|
1058
|
-
subframe close delivers it.
|
|
1059
989
|
- **Each door asks once, then writes through `writeWithoutFlushCheck`.** A content service
|
|
1060
990
|
door opens with `assertWritable`, which asks the channel, and every write it
|
|
1061
|
-
makes after that asks nothing
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
removable and stays — user code runs inside a door (`keyOf`, `index.keyOf`,
|
|
1065
|
-
`slotEmptiness.isEmpty`, an `IOIMEntityWriter`) and can call
|
|
991
|
+
makes after that asks nothing. The per-entity ask is NOT
|
|
992
|
+
removable and stays — user code runs inside a door (`index.keyOf`,
|
|
993
|
+
`slotEmptinessPolicy.isEmpty`, an `IOIMEntityWriter`) and can call
|
|
1066
994
|
`flush()`, so each entity is a new question. What went is the SECOND and THIRD ask
|
|
1067
995
|
of the same entity.
|
|
1068
996
|
- **`flush` during a frame throws:** the message says a frame is already running, and a second one
|
|
@@ -1077,7 +1005,7 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
|
|
|
1077
1005
|
hands the work to a microtask or a later callback rather than writing itself.
|
|
1078
1006
|
- A refusal raised from a tail handler does NOT reach the caller at the call: it
|
|
1079
1007
|
goes to the error policy like any handler throw — logged by default, or thrown
|
|
1080
|
-
out of `flush()` under `
|
|
1008
|
+
out of `flush()` under `OIMThrowAtOnceErrorPolicy`.
|
|
1081
1009
|
- Effects are isolated under the default policy: a throw is logged and handed to
|
|
1082
1010
|
`onError`, and the frame finishes every other effect.
|
|
1083
1011
|
- **A derivation is asked, never woken.** No frame walks to a computed slot.
|
|
@@ -1145,7 +1073,7 @@ class OIMEffectSourceTracker { // exported as a type; the service b
|
|
|
1145
1073
|
```
|
|
1146
1074
|
|
|
1147
1075
|
```ts
|
|
1148
|
-
const fallback = cardService.
|
|
1076
|
+
const fallback = cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'King', rank: 13, deck: 'hearts' });
|
|
1149
1077
|
effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
|
|
1150
1078
|
const isFirst = effectSourceTracker.readSourceSlot(slot).entity?.rank === 1;
|
|
1151
1079
|
paint(effectSourceTracker.readSourceSlot(isFirst ? slot : fallback).entity);
|
|
@@ -1161,7 +1089,7 @@ effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
|
|
|
1161
1089
|
creation. A returned function is the undo, exactly as for `TOIMEffectBody`.
|
|
1162
1090
|
|
|
1163
1091
|
```ts
|
|
1164
|
-
const onRank = effectService.createEffect(db, [cardService.holdColumn(slot, 'rank')], [], () => { paint(slot.entity); });
|
|
1092
|
+
const onRank = effectService.createEffect(db, [cardService.columnSlotService.holdColumn(slot, 'rank')], [], () => { paint(slot.entity); });
|
|
1165
1093
|
```
|
|
1166
1094
|
|
|
1167
1095
|
**A column effect takes the `TOIMColumnSlot` as a root, not a slot and a name.** Minting the
|
|
@@ -1177,7 +1105,7 @@ its carrier. An index store slot's buckets have the same doors on the index serv
|
|
|
1177
1105
|
`createEffect` returns the **effect**, not the column: a column holds no value; read `slot.entity[name]`.
|
|
1178
1106
|
|
|
1179
1107
|
**The name is compile-time checked only as far as the service's column-name type goes** —
|
|
1180
|
-
`holdColumn` takes the service's `TColumnName`, which `
|
|
1108
|
+
`holdColumn` takes the service's `TColumnName`, which `createOIMSlotContentEntityService` sets to
|
|
1181
1109
|
`TOIMEntityColumnName<TEntity>`. `TOIMEntityColumnName<TEntity>` is the type that narrows a name to the
|
|
1182
1110
|
entity's own columns, and `slotContentService.deleteEntityColumns` is typed on it.
|
|
1183
1111
|
|
|
@@ -1187,7 +1115,7 @@ per call; get it once per view, not per entity. `service.holdColumnGroup(slot, c
|
|
|
1187
1115
|
the group's column slot on `slot`, and `service.unholdColumn(columnSlot)` gives it back. A write wakes it once
|
|
1188
1116
|
if it changes any of the group's columns and not at all if it changes only others.
|
|
1189
1117
|
|
|
1190
|
-
**`columnNames` picks the column slot store.** `
|
|
1118
|
+
**`columnNames` picks the column slot store.** `createOIMSlotCollectionService(db, { slotContentService, columnNames: ['rank', 'label'] })`
|
|
1191
1119
|
— and `columnNames` on any factory's options — files the service's column slots by ordinal: every named column
|
|
1192
1120
|
gets a number, a group gets one at its first slot, and an entity slot's column slots sit in an array by
|
|
1193
1121
|
number instead of a `Map` per entity slot, which is cheaper to create and give back. `holdColumn` and
|
|
@@ -1220,26 +1148,21 @@ frame, with no further code. Every mutation goes through a domain neuron — a w
|
|
|
1220
1148
|
from outside the orchestrator is the mistake, not a case to cover with a fallback.
|
|
1221
1149
|
Full page: `website/docs/guides/with-cnstra.md`.
|
|
1222
1150
|
|
|
1223
|
-
The doors are free functions, one module each, so a consumer ships only the one they import. The
|
|
1224
|
-
|
|
1225
|
-
|
|
1151
|
+
The doors are free functions, one module each, so a consumer ships only the one they import. The collection,
|
|
1152
|
+
single and index factories take `db` first and ONE options object second; the computed factories take only
|
|
1153
|
+
the options object. Each builds a SERVICE,
|
|
1226
1154
|
once per collection type; the store slot is made separately:
|
|
1227
1155
|
|
|
1228
1156
|
| Door | Builds | Store slot |
|
|
1229
1157
|
|---|---|---|
|
|
1230
|
-
| `createOIMSlotContentEntityService(db, {
|
|
1231
|
-
| `createOIMSlotCollectionService({ slotContentService })` | `
|
|
1232
|
-
| `createOIMSingleSlotService({ slotContentService })` | `
|
|
1233
|
-
| `
|
|
1234
|
-
| `
|
|
1235
|
-
| `
|
|
1236
|
-
| `
|
|
1237
|
-
| `
|
|
1238
|
-
| `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)` |
|
|
1239
|
-
| `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)` |
|
|
1240
|
-
| `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?)` |
|
|
1241
|
-
| `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)` |
|
|
1242
|
-
| `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)` |
|
|
1158
|
+
| `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(db, { entityWriter?, slotEmptinessPolicy? }?)` | `OIMSlotContentEntityService<TEntity, TKey, TColumnName>`, the entity content service; it creates keyed and single entity slots alike | — |
|
|
1159
|
+
| `createOIMSlotCollectionService(db, { 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)` |
|
|
1160
|
+
| `createOIMSingleSlotService(db, { slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSingleSlotService` over any content service with `createSingleSlot` (entity or bucket) | `createStoreSlot(db)` |
|
|
1161
|
+
| `createOIMSlotCollectionIndexService(db, { 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)` |
|
|
1162
|
+
| `createOIMSlotCollectionSortedArrayIndexService(db, { 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)` |
|
|
1163
|
+
| `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?)` |
|
|
1164
|
+
| `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)` |
|
|
1165
|
+
| `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)` |
|
|
1243
1166
|
|
|
1244
1167
|
`entityWriter` and `slotReleasePolicy` on these doors default to the
|
|
1245
1168
|
db's, so naming one overrides it for that service alone.
|
|
@@ -1252,13 +1175,12 @@ which also fails when an option arrives on a service and is not forwarded,
|
|
|
1252
1175
|
when the root mints anything but the channel and the collector, or when a public
|
|
1253
1176
|
member appears on the root that is not one of the seven it owns.
|
|
1254
1177
|
|
|
1255
|
-
**`
|
|
1256
|
-
an `OIMSlotContentBucketServiceArrayDriven` — insertion order, never re-decided — that records an
|
|
1178
|
+
**`new OIMSlotContentPositionalArrayBucketService({ bucketCommands })` is the content service for a list whose order is yours.** It is — insertion order, never re-decided — that records an
|
|
1257
1179
|
`OIMBucketCommands` script per bucket. The recorder holds no subscription: it lists store slots on the channel
|
|
1258
1180
|
slot, and the script lives for one frame.
|
|
1259
1181
|
|
|
1260
1182
|
```ts
|
|
1261
|
-
const freshPageService =
|
|
1183
|
+
const freshPageService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
1262
1184
|
const freshPage = freshPageService.createStoreSlot(db);
|
|
1263
1185
|
const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
|
|
1264
1186
|
// freshBucketSlot.commands and freshBucketSlot.isWholeListRecorded carry the script; read them in an effect on the bucket slot
|
|
@@ -1276,13 +1198,13 @@ construct one, never subclass it.
|
|
|
1276
1198
|
|---|---|---|
|
|
1277
1199
|
| `OIMDefaults.COLUMN_SLOT_STORE_SERVICE` | `IOIMSlotStoreService<string, TOIMColumnSlot>` | a map-driven store service for column slots; the factories pass it to every service they build without `columnNames` |
|
|
1278
1200
|
| `OIMDefaults.COMPUTED_COLUMN_SLOT_STORE_SERVICE` | `IOIMSlotStoreService<string, TOIMComputedColumnSlot>` | a map-driven store service for the column slots of computed services, keyed or single; the computed factories pass it to every one they build without `columnNames` |
|
|
1279
|
-
| `OIMDefaults.INDEX_KEYS_SERVICE` | `IOIMIndexKeysService` | `
|
|
1280
|
-
| `OIMDefaults.KEY_TREE_WALK_SCALAR` | `
|
|
1281
|
-
| `OIMDefaults.KEY_TREE_WALK_COMPOSITE` | `
|
|
1201
|
+
| `OIMDefaults.INDEX_KEYS_SERVICE` | `IOIMIndexKeysService` | `OIMAppendingIndexKeysService`; the factories pass it when `db.indexKeysService` is not set |
|
|
1202
|
+
| `OIMDefaults.KEY_TREE_WALK_SCALAR` | `OIMScalarKeyTreeWalk` | the chain-of-values walk |
|
|
1203
|
+
| `OIMDefaults.KEY_TREE_WALK_COMPOSITE` | `OIMCompositeKeyTreeWalk` | the chain-of-identities walk |
|
|
1282
1204
|
| `OIMDefaults.STANDALONE_SLOT_OWNER` | `OIMSlotOwner` | the owner of a slot nobody filed |
|
|
1283
1205
|
| `OIMDefaults.ENTITY_WRITER` | `IOIMEntityWriter` | `createOIMEntityWriterInPlace()` |
|
|
1284
|
-
| `OIMDefaults.ENTITY_SLOT_EMPTINESS` | `
|
|
1285
|
-
| `OIMDefaults.STORE_SLOT_LISTENER` | `IOIMStoreSlotListener` | `
|
|
1206
|
+
| `OIMDefaults.ENTITY_SLOT_EMPTINESS` | `IOIMSlotEmptinessPolicy<TOIMEntitySlot>` | "empty means `slot.entity === undefined`" |
|
|
1207
|
+
| `OIMDefaults.STORE_SLOT_LISTENER` | `IOIMStoreSlotListener` | `OIMNoStoreSlotListener`: hears nothing. Not a constructor option: every service reads it when it is built and calls `onStoreSlotCreated(storeSlot)` for each store slot it creates. A tool that wants to see every store slot sets its own with `setValue` before the first service is built; once one has been built, `setValue` throws |
|
|
1286
1208
|
|
|
1287
1209
|
**NOTHING IS CONSTRUCTED AT MODULE LOAD.** Every entry is an `OIMDefaultValue`
|
|
1288
1210
|
whose `getValue()` mints on its first call (`??=`) and hands the same instance back
|
|
@@ -1329,30 +1251,29 @@ with its slot owner; the collection service (`OIMSlotCollectionService`) over it
|
|
|
1329
1251
|
Neither holds an entity: each `createStoreSlot(db)` is one instance of that type's store.
|
|
1330
1252
|
|
|
1331
1253
|
```ts signature
|
|
1332
|
-
createOIMSlotContentEntityService<TEntity,
|
|
1333
|
-
keyOf: (entity: TEntity) => TKey; // REQUIRED for a key; absent for a single
|
|
1254
|
+
createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(db, {
|
|
1334
1255
|
entityWriter?: IOIMEntityWriter; // db.entityWriter, then OIMDefaults.ENTITY_WRITER
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1256
|
+
slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; // OIMDefaults.ENTITY_SLOT_EMPTINESS
|
|
1257
|
+
}?): OIMSlotContentEntityService<TEntity, TKey, TColumnName>
|
|
1258
|
+
|
|
1259
|
+
createOIMSlotCollectionService(db, {
|
|
1260
|
+
slotContentService,
|
|
1261
|
+
slotStoreService?: IOIMSlotStoreService<TKey, TSlot, TStore>; // Map; REQUIRED for an array key
|
|
1262
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // db.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
|
|
1263
|
+
slotSubscriptionListener?: IOIMSlotSubscriptionListener; // OIMNoSlotSubscriptionListener
|
|
1339
1264
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // OIMDefaults.COLUMN_SLOT_STORE_SERVICE
|
|
1340
1265
|
columnNames?: readonly TColumnName[]; // the ordinal column store over those names
|
|
1341
|
-
}):
|
|
1342
|
-
|
|
1343
|
-
createOIMSlotCollectionService({ slotContentService }): OIMSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, …>
|
|
1344
|
-
collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>>
|
|
1345
|
-
|
|
1346
|
-
createOIMEntityService<TEntity, TColumnName?, TKey?>(db, { keyOf, … }) // convenience: both calls, Map store
|
|
1266
|
+
}): OIMWritableSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, TEntity, …>
|
|
1267
|
+
collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>, typeof collectionService>
|
|
1347
1268
|
```
|
|
1348
1269
|
|
|
1349
1270
|
**Each collection type has its own column owner.** It is stateless configuration built with the slot owner
|
|
1350
1271
|
over `columnSlotStoreService` (or the ordinal store `columnNames` names); the column state — the column slots,
|
|
1351
1272
|
the column groups, and which columns are referenced — lives on the store slot. The doors are on the
|
|
1352
|
-
collection
|
|
1273
|
+
collection's `columnSlotService` (`getColumn`, `holdColumn`, `unholdColumn`, `getColumnGroup`, `holdColumnGroup`,
|
|
1353
1274
|
`getAnyColumn`, `holdAnyColumn`). The column owner has two strategies, chosen by the factory: write-driven
|
|
1354
|
-
column slots, woken by the marks a write makes (`
|
|
1355
|
-
derive-driven column slots, each a computed slot over its carrier (`
|
|
1275
|
+
column slots, woken by the marks a write makes (`OIMPushColumnSlotOwner`, entities and buckets), and
|
|
1276
|
+
derive-driven column slots, each a computed slot over its carrier (`OIMPullColumnSlotOwner`, computeds).
|
|
1356
1277
|
|
|
1357
1278
|
Example, over the `db`, `cardService`, `cards` and `slot` of the setup block:
|
|
1358
1279
|
|
|
@@ -1363,26 +1284,26 @@ db.flush();
|
|
|
1363
1284
|
effectService.disposeEffect(effect);
|
|
1364
1285
|
```
|
|
1365
1286
|
|
|
1366
|
-
Assembled
|
|
1287
|
+
Assembled with a store service of your own:
|
|
1367
1288
|
|
|
1368
1289
|
```ts
|
|
1369
|
-
const baseCardService = createOIMSlotCollectionService({
|
|
1370
|
-
slotContentService: createOIMSlotContentEntityService<Card>(db
|
|
1290
|
+
const baseCardService = createOIMSlotCollectionService(db, {
|
|
1291
|
+
slotContentService: createOIMSlotContentEntityService<Card, string>(db),
|
|
1292
|
+
slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, string>>(),
|
|
1371
1293
|
});
|
|
1372
1294
|
const baseCards = baseCardService.createStoreSlot(db);
|
|
1373
1295
|
```
|
|
1374
1296
|
|
|
1375
1297
|
The writer is omitted because the db and `OIMDefaults` supply it, and so is the column store. The slot stamp
|
|
1376
|
-
is not an option of
|
|
1298
|
+
is not an option of any factory: it is `new OIMDB({ slotStamp })`, for every collection of that db.
|
|
1377
1299
|
|
|
1378
1300
|
### Public properties
|
|
1379
1301
|
|
|
1380
1302
|
| Property | Of | Type |
|
|
1381
1303
|
|---|---|---|
|
|
1382
|
-
| `slotContentService` | the collection service | the content service: the write doors |
|
|
1383
|
-
| `
|
|
1384
|
-
| `
|
|
1385
|
-
| `slotStoreService` | the entity content service | the store service |
|
|
1304
|
+
| `slotContentService` | the collection service | the content service: the write doors on a slot |
|
|
1305
|
+
| `columnSlotService` | the collection service | `OIMColumnSlotService`: the column doors |
|
|
1306
|
+
| `slotService` | the store slot | the service that created it |
|
|
1386
1307
|
|
|
1387
1308
|
A store slot is not a slot, so nothing holds, subscribes or wakes it: there is no
|
|
1388
1309
|
`effectService.createEffect(db, [cards], …)` that hears the population. "Something in this population changed" is an
|
|
@@ -1406,7 +1327,7 @@ for (const filedSlot of cardService.getSlots(cards)) {
|
|
|
1406
1327
|
```
|
|
1407
1328
|
|
|
1408
1329
|
To have that answer MAINTAINED and subscribable instead of walked, declare an
|
|
1409
|
-
index keyed on the entity's own id (`keyOf: card => card.id`) and hold the key
|
|
1330
|
+
index keyed on the entity's own id (`keyOf: (card: Card) => card.id`) and hold the key
|
|
1410
1331
|
you want. An index's bucket is an injected lifetime — a
|
|
1411
1332
|
`slotReleasePolicy` — so it is the consumer who says how long the
|
|
1412
1333
|
answer outlives its last reader.
|
|
@@ -1415,13 +1336,12 @@ An index maintains buckets only while they are held, filing entity slots from th
|
|
|
1415
1336
|
store slot on every write by the entity's key value. The bucket is created on first
|
|
1416
1337
|
`holdSlotByKey` by walking the source store slot's slots once, and released on the last give-back.
|
|
1417
1338
|
|
|
1418
|
-
**`holdColumn(slot, columnName)`** is on the collection
|
|
1419
|
-
`service.unholdColumn(columnSlot)` gives that hold back. **`service.getColumn(slot, columnName)`**
|
|
1339
|
+
**`holdColumn(slot, columnName)`** is on the collection's `columnSlotService` and takes a hold on a column slot;
|
|
1340
|
+
`service.columnSlotService.unholdColumn(columnSlot)` gives that hold back. **`service.columnSlotService.getColumn(slot, columnName)`**
|
|
1420
1341
|
returns the same column slot without a hold: a column slot it mints is held by the
|
|
1421
1342
|
unused-slot collector until the end of the next flush, so `getColumn` is
|
|
1422
|
-
the door that does not hold but tracks. `
|
|
1423
|
-
itself
|
|
1424
|
-
and leaves no slot behind.
|
|
1343
|
+
the door that does not hold but tracks. `findSlotByKey(storeSlot, key)?.entity` reads the entity
|
|
1344
|
+
itself, so it mints nothing: a miss answers `undefined` and leaves no slot behind.
|
|
1425
1345
|
|
|
1426
1346
|
### The doors every service has
|
|
1427
1347
|
|
|
@@ -1442,8 +1362,15 @@ class OIMSlotCollectionService<TKey, TSlot, TStore, TSlotContentService, TColumn
|
|
|
1442
1362
|
getSlots(storeSlot): IterableIterator<TSlot>;
|
|
1443
1363
|
getSlotCount(storeSlot): number;
|
|
1444
1364
|
isSameKey(key, otherKey): boolean;
|
|
1445
|
-
|
|
1365
|
+
openBatch(storeSlot): void;
|
|
1366
|
+
closeBatch(storeSlot): void;
|
|
1367
|
+
writeInBatch<TResult>(storeSlot, write: () => TResult): TResult;
|
|
1368
|
+
readonly columnSlotService: OIMColumnSlotService; // getColumn / holdColumn / getAnyColumn / holdAnyColumn / getColumnGroup / holdColumnGroup / unholdColumn
|
|
1446
1369
|
}
|
|
1370
|
+
// createOIMSlotCollectionService returns OIMWritableSlotCollectionService, which adds:
|
|
1371
|
+
// writeSlotContentByKey(storeSlot, key, content, changedSlots?): TSlot
|
|
1372
|
+
// createOIMSingleSlotService returns OIMWritableSingleSlotService, which adds:
|
|
1373
|
+
// writeSingleSlotContent(storeSlot, content, changedSlots?): TSlot; clearSingleSlotContent(storeSlot): void
|
|
1447
1374
|
```
|
|
1448
1375
|
|
|
1449
1376
|
| Door | Of | Signature | Mints? |
|
|
@@ -1455,13 +1382,10 @@ class OIMSlotCollectionService<TKey, TSlot, TStore, TSlotContentService, TColumn
|
|
|
1455
1382
|
| `holdSlotByKey(storeSlot, key)` | collection | `TSlot` — minted if missing, `holdCount` up by one | **yes** |
|
|
1456
1383
|
| `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 |
|
|
1457
1384
|
| `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 |
|
|
1458
|
-
| `
|
|
1459
|
-
| `
|
|
1460
|
-
| `
|
|
1461
|
-
| `deleteEntity(slot)` / `deleteEntities(storeSlot, slots)` / `clearBucketMemberSlots(bucketSlot)` | content | `void` — it takes the value, not the identity | no |
|
|
1462
|
-
| `clearAllEntities(storeSlot)` | entity content | `void` — clears every slot filed in that store slot | no |
|
|
1385
|
+
| `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** |
|
|
1386
|
+
| `writeSlotContent(slot, content)` | content | `boolean` — on a slot you already hold; the boolean is about the CONTENT | no |
|
|
1387
|
+
| `clearSlotContent(slot)` | content | `void` — it takes the value, not the identity | no |
|
|
1463
1388
|
| `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 |
|
|
1464
|
-
| `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 |
|
|
1465
1389
|
| `slot.owner.holdSlot(slot)` / `slot.owner.unholdSlot(slot)` | `void` — the `IOIMSlotOwner` pair; `unholdSlot` on an unheld slot throws by name | no |
|
|
1466
1390
|
| `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 |
|
|
1467
1391
|
|
|
@@ -1470,10 +1394,10 @@ class OIMSlotCollectionService<TKey, TSlot, TStore, TSlotContentService, TColumn
|
|
|
1470
1394
|
owned or in-grace slot it returns having done nothing, and on an
|
|
1471
1395
|
already-released one it returns silently rather than throwing.
|
|
1472
1396
|
|
|
1473
|
-
**`
|
|
1397
|
+
**`writeSlotContentByKey` does not file a key whose content is refused.** On a MISS it
|
|
1474
1398
|
mints, writes inside a `try`, and on a throw runs the release guard on what it
|
|
1475
1399
|
just minted before re-raising — so the caller's error is the only thing that
|
|
1476
|
-
leaves.
|
|
1400
|
+
leaves. The HIT path looks
|
|
1477
1401
|
the key up first and never mints, so a refused write on an existing key leaves
|
|
1478
1402
|
that key exactly as it was.
|
|
1479
1403
|
|
|
@@ -1481,21 +1405,18 @@ that key exactly as it was.
|
|
|
1481
1405
|
|
|
1482
1406
|
| | What happened | What the caller gets |
|
|
1483
1407
|
|---|---|---|
|
|
1484
|
-
| **Refused** | The write threw; **no content ever arrived** | The throw. A key minted for it is unwound — `
|
|
1408
|
+
| **Refused** | The write threw; **no content ever arrived** | The throw. A key minted for it is unwound — `writeSlotContentByKey` files nothing |
|
|
1485
1409
|
| **Unchanged** | The write **did** happen and the content was already there | The identity, like any other write. Nothing is unwound |
|
|
1486
1410
|
|
|
1487
|
-
`
|
|
1411
|
+
`writeSlotContentByKey` is **total**: whatever it decides about the content, it hands back
|
|
1488
1412
|
the identity it resolved or minted. A door that mints must leave a reference
|
|
1489
1413
|
behind, which is the one test that admits a door at all.
|
|
1490
1414
|
|
|
1491
|
-
`fillEntities` is the companion and does not carry the rule: it returns `void` and hands
|
|
1492
|
-
out no reference at all.
|
|
1493
|
-
|
|
1494
1415
|
**`changed?` — the verdict, into an array you own.**
|
|
1495
1416
|
|
|
1496
1417
|
```ts
|
|
1497
1418
|
const changed: TOIMEntitySlot<Card, TOIMStoreKey>[] = [];
|
|
1498
|
-
cardService.
|
|
1419
|
+
cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' }, changed); // pushed exactly when the content differed
|
|
1499
1420
|
```
|
|
1500
1421
|
|
|
1501
1422
|
- **Optional, yours, and appended to** — never read by the engine, never cleared.
|
|
@@ -1513,49 +1434,45 @@ cardService.slotContentService.insertEntity(cards, 'c1', { id: 'c1', title: 'Ace
|
|
|
1513
1434
|
|
|
1514
1435
|
## Writing
|
|
1515
1436
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1437
|
+
The keyed write is on the collection; every other write is on its content service, `service.slotContentService`,
|
|
1438
|
+
and takes the slot alone. There is no `keyOf`: the caller names the key. There are no bulk writes: a load is a
|
|
1439
|
+
loop of single writes inside `service.writeInBatch(storeSlot, () => { … })`.
|
|
1519
1440
|
|
|
1520
1441
|
| Call | Signature |
|
|
1521
1442
|
|---|---|
|
|
1522
|
-
| `
|
|
1523
|
-
| `
|
|
1524
|
-
| `insertEntity(storeSlot, key, entity, changedEntitySlots?)` | `(storeSlot, TKey, TEntity, TOIMEntitySlot<TEntity, TKey>[]?) => TOIMEntitySlot<TEntity, TKey>` — **total**, under a key you name |
|
|
1525
|
-
| `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 |
|
|
1526
|
-
| `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** |
|
|
1443
|
+
| `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 |
|
|
1444
|
+
| `writeSlotContent(slot, entity)` | `(slot, TEntity) => boolean` — the whole value onto a slot you hold; columns it had and `entity` does not, go |
|
|
1527
1445
|
| `updateEntity(slot, patch)` | `(slot, Partial<TEntity>) => slot` |
|
|
1528
1446
|
| `updateEntityColumn(slot, columnName, value)` | `<C extends keyof TEntity & string>(slot, C, TEntity[C]) => slot` — one column, no patch object and no walk over it |
|
|
1529
|
-
| `
|
|
1530
|
-
| `
|
|
1531
|
-
| `clearAllEntities(storeSlot)` | `(storeSlot) => void` — clears every filed slot inside one subframe, so the whole clear is one frame, not one per slot |
|
|
1532
|
-
| `markEntityChanged(slot, changedColumns?)` | `(TOIMEntitySlot<TEntity, TKey>, readonly (keyof TEntity & string)[]?) => void` |
|
|
1447
|
+
| `clearSlotContent(slot)` | `(slot) => void` — the entity goes; the identity stays while anything refers to it |
|
|
1448
|
+
| `markEntityChanged(slot, changedColumns?)` | `(slot, readonly (keyof TEntity & string)[]?) => void` |
|
|
1533
1449
|
| `deleteEntityColumns(slot, columnNames)` | `(slot, readonly TOIMEntityColumnName<TEntity>[]) => slot` — the named columns go |
|
|
1534
|
-
| `
|
|
1535
|
-
| `
|
|
1450
|
+
| `service.writeInBatch(storeSlot, write)` | `<TResult>(storeSlot, () => TResult) => TResult` — an index on the collection hears the whole batch in one call |
|
|
1451
|
+
| `service.findSlotByKey(storeSlot, key)?.entity` | the entity alone; mints nothing |
|
|
1536
1452
|
|
|
1537
1453
|
- **The store keeps your object by default.** Under the default `oimInsertEntityWithoutCopy` the
|
|
1538
1454
|
slot holds the object you hand in, so with the in-place writer a later update edits that
|
|
1539
|
-
same object; pass `
|
|
1455
|
+
same object; pass `entityInserter: oimInsertEntityWithCopy` to store `{ ...entity }`
|
|
1540
1456
|
and leave your object untouched. After an insert, change the object only through the
|
|
1541
1457
|
content service (`updateEntity`, or `markEntityChanged` after a hand edit): editing it and calling
|
|
1542
|
-
`
|
|
1458
|
+
`writeSlotContent` with the same object again wakes nobody, because no column reads as changed. `oimInsertEntityWithSanitizedCopy` also removes an own
|
|
1543
1459
|
`__proto__` key: pick it for untrusted JSON.
|
|
1544
1460
|
- `updateEntity` takes a **patch** — named columns move, unnamed ones stay.
|
|
1545
|
-
`
|
|
1461
|
+
`writeSlotContentByKey` and `writeSlotContent` take a **whole value**, and a column the new value does not name
|
|
1546
1462
|
is removed from the entity.
|
|
1547
1463
|
- A write that moves no column wakes nobody.
|
|
1548
|
-
- **`
|
|
1549
|
-
|
|
1550
|
-
- **`changedEntitySlots?` is the same array on both**: optional, yours, appended with the
|
|
1464
|
+
- **`writeSlotContentByKey` always hands back the slot**, whatever the write changed.
|
|
1465
|
+
- **`changedEntitySlots?`**: optional, yours, appended with the
|
|
1551
1466
|
entity slots whose content was different from what they already held. One array reused
|
|
1552
|
-
across a whole
|
|
1467
|
+
across a whole batch is the intended use, so a
|
|
1553
1468
|
pass over its slots costs the entities that changed rather than the entities that
|
|
1554
1469
|
arrived. It is never cleared — you empty it.
|
|
1555
1470
|
|
|
1556
1471
|
```ts
|
|
1557
1472
|
const changed: TOIMEntitySlot<Card, TOIMStoreKey>[] = [];
|
|
1558
|
-
cardService.
|
|
1473
|
+
cardService.writeInBatch(cards, () => {
|
|
1474
|
+
for (const card of payload) cardService.writeSlotContentByKey(cards, card.id, card, changed);
|
|
1475
|
+
});
|
|
1559
1476
|
for (const slot of changed) reindex(slot); // the re-sent entities are not in here
|
|
1560
1477
|
changed.length = 0;
|
|
1561
1478
|
```
|
|
@@ -1592,13 +1509,13 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1592
1509
|
what the store holds. **Pass the list.** The no-argument form is the convenient
|
|
1593
1510
|
one and it gives back the per-column walk this door exists to skip.
|
|
1594
1511
|
- **An empty list wakes nothing at all** — not the columns, not the entity slot, not the
|
|
1595
|
-
store slot's listeners. `[]` is the caller saying nothing moved, so it returns before
|
|
1596
|
-
|
|
1512
|
+
store slot's listeners. `[]` is the caller saying nothing moved, so it returns before
|
|
1513
|
+
anything is marked: a write that changes nothing wakes nobody here too.
|
|
1597
1514
|
- **With either of the other two it wakes the entity slot and the store slot's content-change listeners**, so an
|
|
1598
1515
|
effect on the slot itself and an index over the store slot both see a hand mutation.
|
|
1599
1516
|
- **It re-stamps emptiness** whenever emptiness can depend on the mutation — with no column list,
|
|
1600
|
-
on a slot whose `entity` is `undefined`, and on a content service with a `
|
|
1601
|
-
`
|
|
1517
|
+
on a slot whose `entity` is `undefined`, and on a content service with a `slotEmptinessPolicy` other than
|
|
1518
|
+
`OIMSlotEntityPresenceEmptinessPolicy.INSTANCE`, a release policy that releases written slots, or a db with a
|
|
1602
1519
|
`slotStamp` — so a hand mutation that emptied the entity leaves `oimIsSlotEmpty(slot)`
|
|
1603
1520
|
true rather than lying, and the release guard can take the identity when it should.
|
|
1604
1521
|
- **It refuses inside a frame**, through the same `assertWritable` →
|
|
@@ -1618,19 +1535,19 @@ Both strategies ship as factories; the fork is chosen once, as the content servi
|
|
|
1618
1535
|
|
|
1619
1536
|
```ts signature
|
|
1620
1537
|
interface IOIMEntityWriter {
|
|
1621
|
-
readonly fillEntity: (slot:
|
|
1622
|
-
readonly putEntity: (slot:
|
|
1623
|
-
readonly patchEntity: (slot:
|
|
1538
|
+
readonly fillEntity: (slot: TOIMEntityContentSlot, entity: object) => void;
|
|
1539
|
+
readonly putEntity: (slot: TOIMEntityContentSlot, entity: object, changedColumnNames: string[] | undefined) => boolean;
|
|
1540
|
+
readonly patchEntity: (slot: TOIMEntityContentSlot, patch: object, changedColumnNames: string[] | undefined) => boolean;
|
|
1624
1541
|
readonly patchColumn: (
|
|
1625
|
-
slot:
|
|
1542
|
+
slot: TOIMEntityContentSlot, columnName: string, value: unknown, changedColumnNames: string[] | undefined,
|
|
1626
1543
|
) => boolean;
|
|
1627
|
-
readonly deleteColumns: (slot:
|
|
1628
|
-
readonly clearEntity: (slot:
|
|
1544
|
+
readonly deleteColumns: (slot: TOIMEntityContentSlot, names: readonly string[]) => readonly string[];
|
|
1545
|
+
readonly clearEntity: (slot: TOIMEntityContentSlot) => void;
|
|
1629
1546
|
}
|
|
1630
1547
|
|
|
1631
1548
|
type TOIMEntityWriterOptions = {
|
|
1632
1549
|
sameValueGuard?: TOIMSameValueGuard;
|
|
1633
|
-
|
|
1550
|
+
entityInserter?: TOIMEntityInserter;
|
|
1634
1551
|
putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimPutEntityColumnsGuarded
|
|
1635
1552
|
patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimPatchEntityColumnsGuarded
|
|
1636
1553
|
patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimPatchEntityColumnGuarded
|
|
@@ -1640,7 +1557,7 @@ createOIMEntityWriterInPlace(options?: TOIMEntityWriterOptions): IOIMEntityWrite
|
|
|
1640
1557
|
createOIMEntityWriterReplacing(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
|
|
1641
1558
|
```
|
|
1642
1559
|
|
|
1643
|
-
`
|
|
1560
|
+
`entityInserter` is the first-write strategy: `oimInsertEntityWithoutCopy` (the default) stores
|
|
1644
1561
|
your object itself, `oimInsertEntityWithCopy` stores `{ ...entity }`, `oimInsertEntityWithSanitizedCopy`
|
|
1645
1562
|
stores a copy with an own `__proto__` key removed (for untrusted JSON).
|
|
1646
1563
|
|
|
@@ -1660,8 +1577,8 @@ per-column comparison that decides "moved".
|
|
|
1660
1577
|
|
|
1661
1578
|
`putEntity` is PUT, `patchEntity` is PATCH (HTTP semantics). PUT makes the stored entity
|
|
1662
1579
|
the value handed in — changed columns written, columns the value lacks removed — in ONE
|
|
1663
|
-
call, returning both sets of names; PATCH writes only the columns named. `
|
|
1664
|
-
of a key that already has a value and `slotContentService.
|
|
1580
|
+
call, returning both sets of names; PATCH writes only the columns named. `writeSlotContentByKey`
|
|
1581
|
+
of a key that already has a value and `slotContentService.writeSlotContent(slot, entity)` are PUT: a whole-entity
|
|
1665
1582
|
write that drops a column is ONE channel write of the slot, and the replacing writer
|
|
1666
1583
|
builds ONE new object.
|
|
1667
1584
|
|
|
@@ -1679,9 +1596,9 @@ replacing — so the entity keeps its hidden class, and the key stays in
|
|
|
1679
1596
|
leaves it out does not report it, and taking the entity away does not wake its
|
|
1680
1597
|
readers.
|
|
1681
1598
|
|
|
1682
|
-
**Neither strategy reports anything.** Reporting is `OIMSlotContentEntityService`'s
|
|
1683
|
-
|
|
1684
|
-
|
|
1599
|
+
**Neither strategy reports anything.** Reporting is `OIMSlotContentEntityService`'s:
|
|
1600
|
+
after the writer returns, it marks the slot on the channel and tells the store slot's content-change listeners
|
|
1601
|
+
once for the whole write.
|
|
1685
1602
|
|
|
1686
1603
|
**A writer of your own** reuses the column walks both shipped writers run:
|
|
1687
1604
|
|
|
@@ -1721,9 +1638,9 @@ stores every value frozen:
|
|
|
1721
1638
|
```ts
|
|
1722
1639
|
const copyColumns = (entity: Record<string, unknown>): Record<string, unknown> => ({ ...entity });
|
|
1723
1640
|
|
|
1724
|
-
function storeFrozen(slot:
|
|
1641
|
+
function storeFrozen(slot: TOIMEntityContentSlot, next: Record<string, unknown> | undefined): boolean {
|
|
1725
1642
|
if (next === undefined) return false;
|
|
1726
|
-
(slot as TOIMSlotEditable<
|
|
1643
|
+
(slot as TOIMSlotEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
|
|
1727
1644
|
return true;
|
|
1728
1645
|
}
|
|
1729
1646
|
|
|
@@ -1751,30 +1668,25 @@ const freezingWriter: IOIMEntityWriter = {
|
|
|
1751
1668
|
## Single entities
|
|
1752
1669
|
|
|
1753
1670
|
A single entity — one value under no key, such as settings or a session — is a store slot of an
|
|
1754
|
-
`OIMSingleSlotService` over an entity content service
|
|
1755
|
-
|
|
1756
|
-
by-key doors: its read doors name the one slot.
|
|
1671
|
+
`OIMSingleSlotService` over an entity content service; its store service is `OIMSingleSlotStoreService.INSTANCE`,
|
|
1672
|
+
which files at most one slot per store slot. It has no key and no by-key doors: its doors name the one slot.
|
|
1757
1673
|
|
|
1758
1674
|
```ts signature
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
// the base layer it assembles
|
|
1764
|
-
createOIMSingleSlotService({
|
|
1765
|
-
slotContentService: createOIMSlotContentEntityService<TEntity>(db, {
|
|
1766
|
-
slotStoreService: OIMSlotStoreServiceSingleSlotDriven.INSTANCE, // no keyOf
|
|
1767
|
-
}),
|
|
1768
|
-
})
|
|
1675
|
+
createOIMSingleSlotService(db, {
|
|
1676
|
+
slotContentService: createOIMSlotContentEntityService<TEntity>(db),
|
|
1677
|
+
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
1678
|
+
}): OIMWritableSingleSlotService<TOIMEntityContentSlot<TEntity>, TEntity, …>
|
|
1769
1679
|
|
|
1770
|
-
class
|
|
1680
|
+
class OIMWritableSingleSlotService<TSlot, TContent, TSlotContentService, …> {
|
|
1771
1681
|
readonly slotContentService: TSlotContentService;
|
|
1772
|
-
|
|
1682
|
+
readonly columnSlotService: OIMColumnSlotService;
|
|
1683
|
+
createStoreSlot(channelMap: IOIMChannelMap): TOIMStoreSlot<TOIMSingleSlotStore<TSlot>, TSlot, TPatch, this>;
|
|
1684
|
+
writeSingleSlotContent(storeSlot, content, changedSlots?): TSlot; // minted if missing
|
|
1685
|
+
clearSingleSlotContent(storeSlot): void;
|
|
1773
1686
|
findSingleSlot(storeSlot): TSlot | undefined; // mints nothing
|
|
1774
1687
|
hasSingleSlotContent(storeSlot): boolean; // mints nothing
|
|
1775
1688
|
getSingleSlot(storeSlot): TSlot; // minted if missing, tracked until the next flush
|
|
1776
1689
|
holdSingleSlot(storeSlot): TSlot; // minted if missing, holdCount up by one
|
|
1777
|
-
getColumn / holdColumn / getAnyColumn / holdAnyColumn / getColumnGroup / holdColumnGroup / unholdColumn;
|
|
1778
1690
|
}
|
|
1779
1691
|
```
|
|
1780
1692
|
|
|
@@ -1785,33 +1697,32 @@ with no value until something writes it. Its store slot is a `TOIMSingleSlotStor
|
|
|
1785
1697
|
```ts
|
|
1786
1698
|
type Settings = { theme: 'light' | 'dark'; zoom: number };
|
|
1787
1699
|
|
|
1788
|
-
const settingsService =
|
|
1700
|
+
const settingsService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<Settings>(db) });
|
|
1789
1701
|
const settings = settingsService.createStoreSlot(db);
|
|
1790
|
-
settingsService.
|
|
1702
|
+
settingsService.writeSingleSlotContent(settings, { theme: 'light', zoom: 1 });
|
|
1791
1703
|
const settingsSlot = settingsService.holdSingleSlot(settings);
|
|
1792
|
-
const themeColumn = settingsService.holdColumn(settingsSlot, 'theme');
|
|
1704
|
+
const themeColumn = settingsService.columnSlotService.holdColumn(settingsSlot, 'theme');
|
|
1793
1705
|
const themeEffect = effectService.createEffect(db, [themeColumn], [], () => { console.log(settingsSlot.entity?.theme); });
|
|
1794
1706
|
settingsService.slotContentService.updateEntity(settingsSlot, { theme: 'dark' });
|
|
1795
1707
|
db.flush();
|
|
1796
1708
|
effectService.disposeEffect(themeEffect);
|
|
1797
|
-
settingsService.unholdColumn(themeColumn);
|
|
1709
|
+
settingsService.columnSlotService.unholdColumn(themeColumn);
|
|
1798
1710
|
settingsSlot.owner.unholdSlot(settingsSlot);
|
|
1799
1711
|
```
|
|
1800
1712
|
|
|
1801
1713
|
**The slot is reached only through the service.** `getSingleSlot(storeSlot)` hands back the live slot, and
|
|
1802
1714
|
mints a fresh one — tracked by the unused-slot collector until the next flush — when there is none;
|
|
1803
|
-
`holdSingleSlot(storeSlot)` does the same and takes a hold.
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
`deleteEntity`) takes that slot.
|
|
1715
|
+
`holdSingleSlot(storeSlot)` does the same and takes a hold. `writeSingleSlotContent` and `clearSingleSlotContent`
|
|
1716
|
+
are the store doors, and every slot door of the content service (`updateEntity`, `updateEntityColumn`,
|
|
1717
|
+
`deleteEntityColumns`, `writeSlotContent`, `markEntityChanged`, `clearSlotContent`) takes that slot.
|
|
1807
1718
|
|
|
1808
|
-
The slot is of the keyed entity slot's literal shape
|
|
1809
|
-
`
|
|
1719
|
+
The slot is of the keyed entity slot's literal shape (with `key: undefined` and `indexKeys: undefined`), and its
|
|
1720
|
+
`storeSlot` is `settings` — so
|
|
1810
1721
|
`effectService.createEffect(db, [settingsSlot], [], …)` works and a column of it is a root like any other.
|
|
1811
1722
|
Its key is `undefined`, and its store is `{ slot }` (`TOIMSingleSlotStore`). Its lifetime is the
|
|
1812
|
-
`slotReleasePolicy` (the db's, then `
|
|
1723
|
+
`slotReleasePolicy` (the db's, then `OIMSlotAtEmptyReleasePolicy`), so a single entity that holds a value
|
|
1813
1724
|
stays; one emptied and unheld is released, and the slot `getSingleSlot` mints next holds no entity. The value
|
|
1814
|
-
is stored by the writer's `
|
|
1725
|
+
is stored by the writer's `entityInserter`, which by default keeps the object handed in rather than a copy.
|
|
1815
1726
|
|
|
1816
1727
|
---
|
|
1817
1728
|
|
|
@@ -1821,18 +1732,17 @@ A bucket collection is, like an entity collection, a content service and the col
|
|
|
1821
1732
|
The content service is the strategy — how a bucket keeps its member slots — and it is chosen at the factory:
|
|
1822
1733
|
|
|
1823
1734
|
```ts signature
|
|
1824
|
-
|
|
1825
|
-
slotContentService
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>;
|
|
1830
|
-
columnNames?: readonly string[];
|
|
1831
|
-
}?): OIMSlotCollectionService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore, TSlotContentService, …>
|
|
1735
|
+
createOIMSlotCollectionService(db, {
|
|
1736
|
+
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or one of the others below
|
|
1737
|
+
slotStoreService?: IOIMSlotStoreService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore>; // Map; REQUIRED for an array key
|
|
1738
|
+
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
1739
|
+
}): OIMWritableSlotCollectionService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore, readonly TBucketMemberSlot[], …>
|
|
1832
1740
|
|
|
1833
|
-
new
|
|
1834
|
-
new
|
|
1835
|
-
new
|
|
1741
|
+
new OIMSlotContentSetBucketService<TBucketMemberSlot>({ bucketCommands? })
|
|
1742
|
+
new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>({ bucketCommands?, bucketMemberSlotsArrayService? })
|
|
1743
|
+
new OIMSlotContentSortedArrayBucketService<TBucketMemberSlot>({ compareBucketMemberSlots, bucketCommands?, bucketMemberSlotsArrayService? })
|
|
1744
|
+
// bucketMemberSlotsArrayService: OIMBucketMemberSlotsArrayService (every mention, the default)
|
|
1745
|
+
// or new OIMBucketUniqueMemberSlotsArrayService<TBucketMemberSlot>() (each slot once)
|
|
1836
1746
|
```
|
|
1837
1747
|
|
|
1838
1748
|
A bucket holds bucket member slots of any slot type — entity slots, bucket slots, computeds — and its
|
|
@@ -1842,16 +1752,13 @@ of the collection service take that store slot first, and the bucket doors of th
|
|
|
1842
1752
|
bucket slot.
|
|
1843
1753
|
|
|
1844
1754
|
```ts
|
|
1845
|
-
const colourService =
|
|
1846
|
-
OIMSlotContentBucketServiceSetDriven<TOIMEntitySlot<Card>>>(db, {
|
|
1847
|
-
slotContentService: new OIMSlotContentBucketServiceSetDriven<TOIMEntitySlot<Card>>(),
|
|
1848
|
-
});
|
|
1755
|
+
const colourService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
|
|
1849
1756
|
const colours = colourService.createStoreSlot(db);
|
|
1850
1757
|
const red = colourService.holdSlotByKey(colours, 'red');
|
|
1851
|
-
colourService.slotContentService.
|
|
1758
|
+
colourService.slotContentService.writeSlotContent(red, [slot, cardService.getSlotByKey(cards, 'c2')]);
|
|
1852
1759
|
db.flush();
|
|
1853
1760
|
for (const deckSlot of colourService.slotContentService.getBucketMemberSlots(red)) { paint(deckSlot.entity); }
|
|
1854
|
-
colourService.slotContentService.
|
|
1761
|
+
colourService.slotContentService.clearSlotContent(red);
|
|
1855
1762
|
red.owner.unholdSlot(red);
|
|
1856
1763
|
```
|
|
1857
1764
|
|
|
@@ -1859,23 +1766,24 @@ Every door below is the bucket content service's and takes the bucket slot:
|
|
|
1859
1766
|
|
|
1860
1767
|
| Door | Signature |
|
|
1861
1768
|
|---|---|
|
|
1862
|
-
| `
|
|
1769
|
+
| `writeSlotContent(bucketSlot, bucketMemberSlots)` | `boolean` — states the whole bucket; idempotent; true when anything arrived or departed |
|
|
1770
|
+
| `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 |
|
|
1863
1771
|
| `addBucketMemberSlot(bucketSlot, bucketMemberSlot)` / `addBucketMemberSlots(bucketSlot, bucketMemberSlots)` | joins the named bucket member slots and touches nothing else. REMOVES NOTHING |
|
|
1864
1772
|
| `removeBucketMemberSlot(bucketSlot, bucketMemberSlot)` / `removeBucketMemberSlots(bucketSlot, bucketMemberSlots)` | takes the named bucket member slots out and touches nothing else |
|
|
1865
|
-
| `
|
|
1773
|
+
| `clearSlotContent(bucketSlot)` | empties it |
|
|
1866
1774
|
| `getBucketMemberSlots(bucketSlot)` | `Iterable<TBucketMemberSlot>` |
|
|
1867
1775
|
| `getBucketMemberSlotCount(bucketSlot)` | `number` |
|
|
1868
|
-
| `hasBucketMemberSlot(bucketSlot, bucketMemberSlot)` | `boolean` — the content service's own membership test: one `Set.has` on a set
|
|
1776
|
+
| `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) |
|
|
1869
1777
|
| `markBucketColumnsChanged(bucketSlot, patch, changedColumns)` | `void` — tell the bucket's column slots which member columns moved |
|
|
1870
1778
|
| `markContentChanged(bucketSlot)` | `void` — re-settle emptiness and re-ask the release guard after a write made another way |
|
|
1871
|
-
| `fillBucketMemberSlots(bucketSlot, bucketMemberSlots)` | `void` — fills a bucket slot nobody was told about yet; it throws on one that was already announced, which `addBucketMemberSlots` / `
|
|
1779
|
+
| `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 |
|
|
1872
1780
|
|
|
1873
1781
|
The entities of a bucket by key are the index's door, `indexService.getEntitiesByKey(indexStoreSlot, key)`
|
|
1874
1782
|
([The index that fills itself](#the-index-that-fills-itself--oimindexservice)).
|
|
1875
1783
|
|
|
1876
1784
|
### Which door: you hold the answer, or you hold the delta
|
|
1877
1785
|
|
|
1878
|
-
`
|
|
1786
|
+
`writeSlotContent` is for a caller who has THE LIST THAT SHOULD BE THERE and does not
|
|
1879
1787
|
know what changed — a search result, a page from a server, a filter re-run. One
|
|
1880
1788
|
call, and the engine works out the arrivals and the departures. Its cost is
|
|
1881
1789
|
proportional to WHAT THE BUCKET HOLDS, not to the change: the difference builds a
|
|
@@ -1894,31 +1802,31 @@ the order you name them.
|
|
|
1894
1802
|
|
|
1895
1803
|
**`addBucketMemberSlots` removes nothing.** If the authoritative list SHRANK and you call
|
|
1896
1804
|
`addBucketMemberSlots`, the entity slots that left stay in the bucket, held. Not knowing what left is
|
|
1897
|
-
exactly when `
|
|
1805
|
+
exactly when `writeSlotContent` is the right door, and its cost is the price of not
|
|
1898
1806
|
knowing.
|
|
1899
1807
|
|
|
1900
1808
|
The keyed doors are the collection service's and take the store slot first —
|
|
1901
1809
|
`decksService.getSlotByKey(decks, key)`, `decksService.holdSlotByKey(decks, key)` — and a hold is given back
|
|
1902
|
-
with `bucketSlot.owner.unholdSlot(bucketSlot)`.
|
|
1903
|
-
`decksService.slotContentService.
|
|
1810
|
+
with `bucketSlot.owner.unholdSlot(bucketSlot)`. The by-key write is `decksService.writeSlotContentByKey(decks, key, members)`; on a bucket slot you hold it is
|
|
1811
|
+
`decksService.slotContentService.writeSlotContent(bucketSlot, members)`.
|
|
1904
1812
|
|
|
1905
1813
|
A bucket's emptiness is the content service's `isSlotEmpty(bucketSlot)`, a member count of zero, so
|
|
1906
1814
|
an emptied bucket that nothing holds and nothing watches is released by the
|
|
1907
1815
|
guard on the same call that emptied it.
|
|
1908
1816
|
|
|
1909
1817
|
**A bucket never sorts.** Its order is what the caller put in — `addBucketMemberSlots`
|
|
1910
|
-
appends, `insertBucketMemberSlotAt` inserts, a move moves, `
|
|
1818
|
+
appends, `insertBucketMemberSlotAt` inserts, a move moves, `writeSlotContent` takes the
|
|
1911
1819
|
sequence given. A sorted list is a computed over an index:
|
|
1912
|
-
[Sorted lists](#sorted-lists); or the bucket of a `
|
|
1820
|
+
[Sorted lists](#sorted-lists); or the bucket of a `createOIMSlotCollectionSortedArrayIndexService` index, which
|
|
1913
1821
|
orders itself: [Sorted at write](#sorted-at-write).
|
|
1914
1822
|
|
|
1915
|
-
`
|
|
1823
|
+
`writeSlotContent(bucketSlot, members)` runs `assertWritable` — the frame guard
|
|
1916
1824
|
(`assertNotFlushing`) and the released-identity check — then the reconcile (an empty `arrived` and
|
|
1917
1825
|
`departed` is an unchanged write, which wakes nobody), the holds taken on arrivals, the holds given back by
|
|
1918
1826
|
departures and the single channel write of the bucket slot. It is handed a slot, so it hands back a
|
|
1919
1827
|
`boolean`: true when anything arrived or departed.
|
|
1920
1828
|
|
|
1921
|
-
`
|
|
1829
|
+
`writeSlotContent` **joins before it evicts**, so a slot named by both the old and the
|
|
1922
1830
|
new list never reaches zero references. It empties with one `drainSlots` rather
|
|
1923
1831
|
than one removal at a time — per-slot eviction on an array-driven bucket is
|
|
1924
1832
|
n(n-1)/2 element moves.
|
|
@@ -1939,7 +1847,7 @@ empty means nothing changed. Each content service reconciles its own way:
|
|
|
1939
1847
|
`OIMBucketCommands` records onto the bucket slot (`bucket.commands`) as the writes land and
|
|
1940
1848
|
empties it once the flush has fully ended, so a consumer reads it INSIDE the
|
|
1941
1849
|
frame and what it reads can only describe the writes it was woken for.
|
|
1942
|
-
`
|
|
1850
|
+
`clearSlotContent` asks whether the bucket is empty **before** it
|
|
1943
1851
|
touches the store, so an already-empty bucket records nothing for a call that
|
|
1944
1852
|
reports nothing.
|
|
1945
1853
|
|
|
@@ -1947,33 +1855,20 @@ reports nothing.
|
|
|
1947
1855
|
|
|
1948
1856
|
| Class | Storage type | One add records |
|
|
1949
1857
|
|---|---|---|
|
|
1950
|
-
| `
|
|
1951
|
-
| `
|
|
1952
|
-
| `
|
|
1858
|
+
| `OIMSlotContentSetBucketService<TBucketMemberSlot>({ bucketCommands? })` | `TOIMBucketSlotsSetStore<TBucketMemberSlot>` = `Set<TBucketMemberSlot>` | one per slot |
|
|
1859
|
+
| `OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>({ bucketCommands?, bucketMemberSlotsArrayService? })` | `TOIMBucketSlotsArrayStore<TBucketMemberSlot>` = `{ bucketMemberSlots, uniqueBucketMemberSlots, sortedCount }` | one per **mention**; one per slot, order kept, with `new OIMBucketUniqueMemberSlotsArrayService()` |
|
|
1860
|
+
| `OIMSlotContentSortedArrayBucketService<TBucketMemberSlot>({ compareBucketMemberSlots, bucketMemberSlotsArrayService? })` | `TOIMBucketSlotsArrayStore<TBucketMemberSlot>` | one per mention or per slot, at its sorted place |
|
|
1953
1861
|
|
|
1954
|
-
The first two are the ones a caller picks between, and neither sorts: the array-driven
|
|
1955
|
-
caller's order. The
|
|
1862
|
+
The first two are the ones a caller picks between, and neither sorts: the array-driven services keep the
|
|
1863
|
+
caller's order. The last is the one the sorted-at-write index builds for itself
|
|
1956
1864
|
([Sorted at write](#sorted-at-write)); its `repositionBucketMemberSlot(bucketSlot, bucketMemberSlot)` moves one
|
|
1957
1865
|
member to its place after its sort columns changed.
|
|
1958
1866
|
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
>({
|
|
1965
|
-
bucketArrayMembership: new OIMBucketArrayMembershipUnique<TOIMEntitySlot<Card>>(),
|
|
1966
|
-
})
|
|
1967
|
-
|
|
1968
|
-
| Membership | Storage it builds | One add records |
|
|
1969
|
-
|---|---|---|
|
|
1970
|
-
| `OIMBucketArrayMembershipNonUnique.INSTANCE` (default) | `TOIMBucketSlotsArrayStore<TBucketMemberSlot>` | one per **mention** |
|
|
1971
|
-
| `new OIMBucketArrayMembershipUnique<TBucketMemberSlot>()` | `TOIMBucketSlotsArrayUniqueStore<TBucketMemberSlot>` | one per slot |
|
|
1972
|
-
|
|
1973
|
-
`IOIMBucketArrayMembership<TBucketMemberSlot, TSlots>`: `createStore`, `addSlot`, `removeSlot`,
|
|
1974
|
-
`hasSlot`, `clearSlots`, `containsTheSameSequence`. `hasBucketMemberSlot` asks the membership's `hasSlot`: the
|
|
1975
|
-
unique membership answers from its `Set` (no array walk), the non-unique one walks the
|
|
1976
|
-
array. Only the array-driven service takes one — a set-driven service keeps its slots IN a `Set`.
|
|
1867
|
+
Which slots an array bucket keeps is its `bucketMemberSlotsArrayService`, the only writer of the list and the
|
|
1868
|
+
`Set` beside it: `OIMBucketMemberSlotsArrayService` keeps every mention and no `Set`;
|
|
1869
|
+
`OIMBucketUniqueMemberSlotsArrayService` keeps each slot once and the list and its `Set` together, so the two never
|
|
1870
|
+
disagree. With the unique one `hasBucketMemberSlot` answers from the `Set` (no array walk); with the default it
|
|
1871
|
+
walks the array.
|
|
1977
1872
|
|
|
1978
1873
|
Nothing positional is on the abstract `OIMSlotContentBucketService`: the reads `getBucketMemberSlotAt` and
|
|
1979
1874
|
`getBucketMemberSlotPosition` are on the array-based services (array-driven and sorted), and the five
|
|
@@ -1993,8 +1888,8 @@ named constants:
|
|
|
1993
1888
|
A removal answering the third is recorded as `recordWholeList(bucketSlot)`: a set has no index to patch,
|
|
1994
1889
|
so the consumer is told to re-read the bucket rather than handed a fabricated one.
|
|
1995
1890
|
|
|
1996
|
-
`OIM_NOTHING_WAS_INSERTED` (`-1`) is the insert's answer when nothing was inserted, which is a repeat on an
|
|
1997
|
-
`
|
|
1891
|
+
`OIM_NOTHING_WAS_INSERTED` (`-1`) is the insert's answer when nothing was inserted, which is a repeat on an array bucket
|
|
1892
|
+
with `OIMBucketUniqueMemberSlotsArrayService`. The recorded insert names the index the slot LANDED on — `at` clamped to the
|
|
1998
1893
|
ends of the list — and not `getBucketMemberSlotPosition`, because the default ordered list keeps one place per
|
|
1999
1894
|
**mention**: inserting a slot the bucket already holds gives it a second position, and the position read
|
|
2000
1895
|
names the FIRST. A command script built from it would tell the consumer to patch at an index the slot is not
|
|
@@ -2006,9 +1901,8 @@ error. **One content service instance may serve two bucket collections** — it
|
|
|
2006
1901
|
|
|
2007
1902
|
### Positions the caller decides
|
|
2008
1903
|
|
|
2009
|
-
A bucket whose positions are the caller's is one over `
|
|
2010
|
-
|
|
2011
|
-
block. The two position reads and the five writes are that content service's, they take the bucket
|
|
1904
|
+
A bucket whose positions are the caller's is one over `OIMSlotContentPositionalArrayBucketService`, which is
|
|
1905
|
+
the `pageService` of the setup block. The two position reads and the five writes are that content service's, they take the bucket
|
|
2012
1906
|
slot and no store slot, and an index is valid only until the next write:
|
|
2013
1907
|
|
|
2014
1908
|
| Door | What it does |
|
|
@@ -2029,7 +1923,7 @@ like any other out-of-range position.
|
|
|
2029
1923
|
Each wraps a store change in the frame guard, the holds, the report and the
|
|
2030
1924
|
release question. The default ordered list records one place per MENTION, so
|
|
2031
1925
|
`insertBucketMemberSlotAt` of a slot the bucket already holds gives it a second position —
|
|
2032
|
-
|
|
1926
|
+
take `bucketMemberSlotsArrayService: new OIMBucketUniqueMemberSlotsArrayService()` when a position must name
|
|
2033
1927
|
exactly one entity slot.
|
|
2034
1928
|
|
|
2035
1929
|
Per strategy, generating code:
|
|
@@ -2052,7 +1946,7 @@ slotContentBucketService.moveBucketMemberSlotToPosition(
|
|
|
2052
1946
|
);
|
|
2053
1947
|
|
|
2054
1948
|
// a whole list at once, in the order the server sent
|
|
2055
|
-
slotContentBucketService.
|
|
1949
|
+
slotContentBucketService.writeSlotContent(todo, cardService.writeInBatch(cards, () => (payload).map(entity => cardService.writeSlotContentByKey(cards, entity.id, entity))));
|
|
2056
1950
|
```
|
|
2057
1951
|
|
|
2058
1952
|
### The render script: `OIMBucketCommands`
|
|
@@ -2061,14 +1955,13 @@ slotContentBucketService.writeBucketMemberSlots(todo, cardService.slotContentSer
|
|
|
2061
1955
|
new OIMBucketCommands(options: { maxCommandsPerSlot?: number }) // default OIM_MAX_COMMANDS_PER_SLOT
|
|
2062
1956
|
|
|
2063
1957
|
// a bucket content service takes it at construction, once:
|
|
2064
|
-
new
|
|
2065
|
-
// createOIMBucketService(db, { maxCommandsPerSlot }) builds the default content service with one
|
|
1958
|
+
new OIMSlotContentPositionalArrayBucketService({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: Infinity }) });
|
|
2066
1959
|
```
|
|
2067
1960
|
|
|
2068
1961
|
**Recording is a strategy chosen when the bucket content service is built.** `bucketCommands` is read once
|
|
2069
|
-
into a readonly field; left out of a content service built with `new`, it is `
|
|
1962
|
+
into a readonly field; left out of a content service built with `new`, it is `OIMNoBucketCommands`, whose
|
|
2070
1963
|
every method is empty, so a bucket nobody renders positionally pays one empty call per write and allocates no
|
|
2071
|
-
command.
|
|
1964
|
+
command. There is no setter: a content
|
|
2072
1965
|
service records into the one recorder it was built with. One recorder serves every bucket of every store slot
|
|
2073
1966
|
and of every db — it is stateless.
|
|
2074
1967
|
|
|
@@ -2103,8 +1996,8 @@ that lands after a flush but before that reset starts a new script instead of ex
|
|
|
2103
1996
|
read. The reset is subscribed with `subscribeAfterEachFlush` on the first script a channel slot ever sees, and
|
|
2104
1997
|
walks only the buckets that recorded.
|
|
2105
1998
|
|
|
2106
|
-
A positional write is always recorded as a position. `isWholeListRecorded` is set by a write of the whole bucket — `
|
|
2107
|
-
`
|
|
1999
|
+
A positional write is always recorded as a position. `isWholeListRecorded` is set by a write of the whole bucket — `writeSlotContent`,
|
|
2000
|
+
`clearSlotContent`, `moveBucketMemberSlotRangeToPosition` — by a removal from a bucket whose
|
|
2108
2001
|
content service keeps no positions, which has no index to patch, and by the script
|
|
2109
2002
|
outgrowing the list
|
|
2110
2003
|
it describes. `maxCommandsPerSlot` is that ratio; it defaults to
|
|
@@ -2123,19 +2016,20 @@ content service's own write doors do.
|
|
|
2123
2016
|
### A single bucket
|
|
2124
2017
|
|
|
2125
2018
|
One bucket under no key is a store slot of an `OIMSingleSlotService` over a bucket content service, with
|
|
2126
|
-
`
|
|
2019
|
+
`OIMSingleSlotStoreService.INSTANCE` as the store service:
|
|
2127
2020
|
|
|
2128
2021
|
```ts signature
|
|
2129
|
-
|
|
2130
|
-
slotContentService
|
|
2131
|
-
|
|
2022
|
+
createOIMSingleSlotService(db, {
|
|
2023
|
+
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or the set one
|
|
2024
|
+
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
2025
|
+
}): OIMWritableSingleSlotService<TOIMBucketContentSlot<TSlots>, readonly TBucketMemberSlot[], …>
|
|
2132
2026
|
```
|
|
2133
2027
|
|
|
2134
2028
|
```ts
|
|
2135
|
-
const handService =
|
|
2029
|
+
const handService = createOIMSingleSlotService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
2136
2030
|
const hand = handService.createStoreSlot(db);
|
|
2137
2031
|
const handSlot = handService.holdSingleSlot(hand);
|
|
2138
|
-
handService.slotContentService.
|
|
2032
|
+
handService.slotContentService.writeSlotContent(handSlot, [slot]);
|
|
2139
2033
|
handService.slotContentService.insertBucketMemberSlotAt(handSlot, 0, cardService.getSlotByKey(cards, 'c2'));
|
|
2140
2034
|
effectService.disposeEffect(effectService.createEffect(db, [handSlot], [], () => {
|
|
2141
2035
|
render(Array.from(handService.slotContentService.getBucketMemberSlots(handSlot)));
|
|
@@ -2143,23 +2037,22 @@ effectService.disposeEffect(effectService.createEffect(db, [handSlot], [], () =>
|
|
|
2143
2037
|
handSlot.owner.unholdSlot(handSlot);
|
|
2144
2038
|
```
|
|
2145
2039
|
|
|
2146
|
-
One bucket, no key: the single-slot store files its one bucket slot
|
|
2147
|
-
|
|
2040
|
+
One bucket, no key: the single-slot store files its one bucket slot as the store's `slot`. The bucket slot is a
|
|
2041
|
+
`TOIMBucketContentSlot<TSlots>` of the keyed bucket slot's shape (`key: undefined`), what an effect subscribes to and what a reader is handed,
|
|
2148
2042
|
so the channel's write door still sees three slot shapes. Every door of the bucket content service takes it as
|
|
2149
2043
|
it takes a keyed bucket slot, and `markBucketColumnsChanged(bucketSlot, patch, changedColumns)` is how the
|
|
2150
2044
|
caller that changed members says which of their columns moved, as the index does for a keyed bucket. Each
|
|
2151
|
-
door
|
|
2152
|
-
subframe.
|
|
2045
|
+
door writes the bucket slot to the channel once.
|
|
2153
2046
|
|
|
2154
2047
|
**Its lifetime is the `slotReleasePolicy`.** With none named (here or on the db) it is
|
|
2155
|
-
`
|
|
2048
|
+
`OIMSlotAtEmptyReleasePolicy.INSTANCE`, so an emptied single bucket that nothing holds or watches is
|
|
2156
2049
|
released, and `getSingleSlot(storeSlot)` mints a fresh, empty one on the next read. Hold the
|
|
2157
|
-
slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `
|
|
2050
|
+
slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `OIMSlotNeverReleasePolicy.INSTANCE`.
|
|
2158
2051
|
|
|
2159
|
-
### The index that fills itself — `
|
|
2052
|
+
### The index that fills itself — `OIMSlotCollectionIndexService`
|
|
2160
2053
|
|
|
2161
2054
|
```ts signature
|
|
2162
|
-
|
|
2055
|
+
createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>(db, {
|
|
2163
2056
|
entityService: Pick<OIMSlotCollectionService<TSourceKey, TOIMEntitySlot<TEntity, TSourceKey>, TSourceStore, unknown>, 'getSlots'>;
|
|
2164
2057
|
...TOIMIndexKeyOptions<TKey, TEntity, TColumns>; // { columns } | { keyOf } | { keyOf, columns }
|
|
2165
2058
|
slotContentService?: OIMSlotContentBucketService<TOIMEntitySlot<TEntity>, TSlots>; // default: array-driven, no commands
|
|
@@ -2167,9 +2060,9 @@ createOIMIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlo
|
|
|
2167
2060
|
slotReleasePolicy?: IOIMSlotReleasePolicy;
|
|
2168
2061
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>;
|
|
2169
2062
|
columnNames?: readonly string[];
|
|
2170
|
-
}):
|
|
2063
|
+
}): OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore, TSlotContentService, …>
|
|
2171
2064
|
|
|
2172
|
-
class
|
|
2065
|
+
class OIMSlotCollectionIndexService<…> extends OIMSlotCollectionService<…> {
|
|
2173
2066
|
createIndexStoreSlot(sourceStoreSlot: TOIMStoreSlot<TSourceStore, TOIMEntitySlot<TEntity, TSourceKey>, Partial<TEntity>>): TOIMIndexStoreSlot<…>;
|
|
2174
2067
|
disposeIndexStoreSlot(indexStoreSlot: TOIMIndexStoreSlot<…>): void;
|
|
2175
2068
|
getEntitiesByKey(indexStoreSlot, key): (TEntity | undefined)[];
|
|
@@ -2180,12 +2073,12 @@ class OIMIndexService<…> extends OIMSlotCollectionService<…> {
|
|
|
2180
2073
|
}
|
|
2181
2074
|
```
|
|
2182
2075
|
|
|
2183
|
-
`
|
|
2076
|
+
`OIMSlotCollectionIndexService` is a collection service of buckets, so it has every read door of one, and its
|
|
2184
2077
|
`slotContentService` is the bucket content service narrowed by type to its reads (`getBucketMemberSlots`,
|
|
2185
2078
|
`getBucketMemberSlotCount`, `hasBucketMemberSlot`, and the positional reads where the strategy has them) — the
|
|
2186
2079
|
index is the only writer of its buckets. The strategy is chosen at the factory:
|
|
2187
|
-
`
|
|
2188
|
-
`slotContentService`, and the sorted one built by `
|
|
2080
|
+
`OIMSlotContentPositionalArrayBucketService` by default, `OIMSlotContentSetBucketService` passed as
|
|
2081
|
+
`slotContentService`, and the sorted one built by `createOIMSlotCollectionSortedArrayIndexService`. Only the array-based
|
|
2189
2082
|
content services answer by position: `getBucketMemberSlotAt(bucketSlot, index)` (O(1), `undefined` past the
|
|
2190
2083
|
end) and `getBucketMemberSlotPosition(bucketSlot, entitySlot)` (walks the array, O(n), `-1` for a non-member).
|
|
2191
2084
|
The index's default content service records no commands; pass one built with `bucketCommands` for a render
|
|
@@ -2201,11 +2094,11 @@ read through the index service, passing that store slot; the index is what write
|
|
|
2201
2094
|
written before it, without a flush, so minting inside an effect body does not throw.
|
|
2202
2095
|
`getSlotsByKeys` / `holdSlotsByKeys` fill every new bucket in ONE pass over the source. How long it lives after that is **the
|
|
2203
2096
|
`slotReleasePolicy`'s ordinary business**, exactly as for a hand-held bucket; the index injects no lifetime of its
|
|
2204
|
-
own. With none named it is `
|
|
2097
|
+
own. With none named it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: the bucket stands while it has members and is
|
|
2205
2098
|
maintained on every write.
|
|
2206
2099
|
|
|
2207
2100
|
```ts
|
|
2208
|
-
const byDeckService =
|
|
2101
|
+
const byDeckService = createOIMSlotCollectionIndexService(db, {
|
|
2209
2102
|
entityService: cardService,
|
|
2210
2103
|
keyOf: (card: Card) => card.deck,
|
|
2211
2104
|
});
|
|
@@ -2215,14 +2108,14 @@ const spades = byDeckService.holdSlotByKey(byDeck, 'spades');
|
|
|
2215
2108
|
effectService.createEffect(db, [spades], [], () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))));
|
|
2216
2109
|
```
|
|
2217
2110
|
|
|
2218
|
-
A set-driven index names its content service
|
|
2219
|
-
`new
|
|
2111
|
+
A set-driven index names its content service with the bucket member slot type and the index key type spelled
|
|
2112
|
+
out — `new OIMSlotContentSetBucketService()` infers `<TOIMSlot, TOIMStoreKey>` and fails to type-check:
|
|
2220
2113
|
|
|
2221
2114
|
```ts
|
|
2222
|
-
const bySuitService =
|
|
2115
|
+
const bySuitService = createOIMSlotCollectionIndexService(db, {
|
|
2223
2116
|
entityService: cardService,
|
|
2224
2117
|
columns: ['deck'],
|
|
2225
|
-
slotContentService: new
|
|
2118
|
+
slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>, string>(),
|
|
2226
2119
|
});
|
|
2227
2120
|
const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
2228
2121
|
```
|
|
@@ -2240,17 +2133,15 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2240
2133
|
ones `keyOf` reads. With `columns`, a patch or put whose changed columns include none of
|
|
2241
2134
|
them keeps the entity's key and calls nothing; a write of the whole value (a fill, a
|
|
2242
2135
|
clear, `markEntityChanged(slot)` with no names) always reads the key again.
|
|
2243
|
-
- **A key of several columns
|
|
2244
|
-
|
|
2245
|
-
tuple.
|
|
2246
|
-
`
|
|
2247
|
-
`isSameKey`), so a fresh array with the same parts moves nothing. `createOIMIndexService` takes a scalar key
|
|
2136
|
+
- **A key of several columns**: `createOIMSlotCollectionIndexService(db, { entityService, columns: ['deck', 'suit'],
|
|
2137
|
+
bucketSlotStoreService: new OIMSlotTreeStoreService(OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()) })` makes the key
|
|
2138
|
+
the array of those values in order, typed as a tuple. Only that store knows the key is an array (its
|
|
2139
|
+
`isSameKey`), so a fresh array with the same parts moves nothing. `createOIMSlotCollectionIndexService` takes a scalar key
|
|
2248
2140
|
unless it is handed a `bucketSlotStoreService`: two columns or an array `keyOf` without one do not compile.
|
|
2249
2141
|
`getSlotsByKeys` / `holdSlotsByKeys` fill array-keyed buckets one pass over the
|
|
2250
2142
|
source per key (the cost of `getSlotByKey` each); only a scalar key gets one pass for all.
|
|
2251
|
-
- **Entity slots are filed on write**, using the index's key.
|
|
2252
|
-
|
|
2253
|
-
batch; the index groups the entity slots by key and moves each group with one bucket write.
|
|
2143
|
+
- **Entity slots are filed on write**, using the index's key. Writes inside `writeInBatch` on the collection
|
|
2144
|
+
notify once per batch; the index groups the entity slots by key and moves each group with one bucket write.
|
|
2254
2145
|
- **Reading**, each taking the index store slot first: `getSlotByKey`, `holdSlotByKey`, `findSlotByKey`,
|
|
2255
2146
|
`hasSlotContentByKey`, `getSlotsByKeys`, `holdSlotsByKeys`, `getSlots`, `getSlotCount`, `getEntitiesByKey`, and the bucket column
|
|
2256
2147
|
slot doors `getAnyBucketColumnByKey`, `holdAnyBucketColumnByKey`,
|
|
@@ -2274,9 +2165,9 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2274
2165
|
is maintained whether anyone holds it or not.
|
|
2275
2166
|
- **Order is not the bucket's business, unless you pick the strategy that makes it so.**
|
|
2276
2167
|
A bucket keeps its arrival order; a sorted list is
|
|
2277
|
-
`
|
|
2168
|
+
`createOIMSortedComputedService(db, { indexService, sortColumns?, comparator? })`
|
|
2278
2169
|
— see [Sorted lists](#sorted-lists) — or the index is built with
|
|
2279
|
-
`
|
|
2170
|
+
`createOIMSlotCollectionSortedArrayIndexService`, whose buckets are already in order — see
|
|
2280
2171
|
[Sorted at write](#sorted-at-write). A second computed over the bucket handles
|
|
2281
2172
|
projection or filtering.
|
|
2282
2173
|
- **Members are read through the bucket content service**: `Array.from(indexService.slotContentService.getBucketMemberSlots(bucketSlot))`.
|
|
@@ -2286,9 +2177,9 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2286
2177
|
membership only**: an entity slot joining or leaving. A write to an entity that stays in the
|
|
2287
2178
|
bucket does not wake it — subscribe to that entity's own slot for that.
|
|
2288
2179
|
- **Bucket lifetime is the `slotReleasePolicy` option, not the index's.** Name nothing (here or on the db)
|
|
2289
|
-
and it is `
|
|
2290
|
-
once empty; `
|
|
2291
|
-
and it cools on its queue; pass `
|
|
2180
|
+
and it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: an unheld bucket stays while it has members and goes
|
|
2181
|
+
once empty; `OIMSlotNeverReleasePolicy.INSTANCE` keeps it for good; pass an `OIMSlotAfterIdleReleasePolicy`
|
|
2182
|
+
and it cools on its queue; pass `OIMSlotAtUnholdReleasePolicy.INSTANCE` and it dies at the last give-back,
|
|
2292
2183
|
and the next hold builds a fresh one by walking the source again.
|
|
2293
2184
|
|
|
2294
2185
|
**The bucket's any-column slot exists, but it is not the recommended path.**
|
|
@@ -2299,7 +2190,7 @@ the bucket slot (`getSlotByKey` / `holdSlotByKey`). A reader that wants both sub
|
|
|
2299
2190
|
to both, and the subscriber picks by which slots it puts in `createEffect`.
|
|
2300
2191
|
|
|
2301
2192
|
```ts
|
|
2302
|
-
const deckIndexService =
|
|
2193
|
+
const deckIndexService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2303
2194
|
const deckIndex = deckIndexService.createIndexStoreSlot(cards);
|
|
2304
2195
|
const spadesBucket = deckIndexService.holdSlotByKey(deckIndex, 'spades');
|
|
2305
2196
|
const spadesAnyColumn = deckIndexService.holdAnyBucketColumnByKey(deckIndex, 'spades');
|
|
@@ -2307,7 +2198,7 @@ const entitiesEffect = effectService.createEffect(db, [spadesBucket, spadesAnyCo
|
|
|
2307
2198
|
console.log(deckIndexService.getEntitiesByKey(deckIndex, 'spades'));
|
|
2308
2199
|
});
|
|
2309
2200
|
effectService.disposeEffect(entitiesEffect);
|
|
2310
|
-
deckIndexService.unholdColumn(spadesAnyColumn);
|
|
2201
|
+
deckIndexService.columnSlotService.unholdColumn(spadesAnyColumn);
|
|
2311
2202
|
spadesBucket.owner.unholdSlot(spadesBucket);
|
|
2312
2203
|
```
|
|
2313
2204
|
|
|
@@ -2339,19 +2230,17 @@ no listener is called, nothing is buffered, and the names array is reused across
|
|
|
2339
2230
|
every column write is added with `readColumnNames` `undefined`. **The filter is the union, per store
|
|
2340
2231
|
slot, not per listener**: once a write touches a column ANY listener of that store reads, every
|
|
2341
2232
|
listener of that store is called, so a listener that declared a narrower set still checks
|
|
2342
|
-
`changedColumns` itself, as `
|
|
2233
|
+
`changedColumns` itself, as `OIMSlotCollectionIndexService` does against its key columns.
|
|
2343
2234
|
`IOIMContentChangeListener<TSlot, TPatch>` has two methods:
|
|
2344
2235
|
`onContentChanged(slot, patch, changedColumns)` and
|
|
2345
2236
|
`onContentChangedMany(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
|
|
2346
2237
|
parallel `OIMChannelQueue`s read with `getEntryCount()` / `getEntryAt(i)`, valid only during the
|
|
2347
2238
|
call, so a listener copies what it keeps — `patch` is what an update
|
|
2348
2239
|
wrote, `changedColumns` the names `markEntityChanged(slot, columns)` was given, either
|
|
2349
|
-
`undefined` when the write carries none. A
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
`onContentChangedMany` when the door returns. A subframe the caller opens on the channel slot
|
|
2354
|
-
does not hold them — an index is up to date as soon as the content service's own door returns.
|
|
2240
|
+
`undefined` when the write carries none. A single write outside a batch delivers one `onContentChanged` as soon as it is filed; inside
|
|
2241
|
+
`writeInBatch` (or `openBatch` / `closeBatch`) on the collection the writes are queued on the store slot and
|
|
2242
|
+
delivered as ONE `onContentChangedMany` when the outermost batch closes. Code inside the batch reads the index
|
|
2243
|
+
as it was before the batch.
|
|
2355
2244
|
This hook is the only seam an index uses; an index of your own is a listener.
|
|
2356
2245
|
|
|
2357
2246
|
**Bucket column slots.** An index hands out one column slot per bucket and column:
|
|
@@ -2365,24 +2254,24 @@ return the same kind of slot, written when any column of any member changes. A g
|
|
|
2365
2254
|
`slot.owner.unholdSlot(slot)`.
|
|
2366
2255
|
|
|
2367
2256
|
```ts
|
|
2368
|
-
const decksService =
|
|
2257
|
+
const decksService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2369
2258
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2370
2259
|
const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
|
|
2371
2260
|
effectService.disposeEffect(effectService.createEffect(db, [rankOfSpades], [], () => {}));
|
|
2372
|
-
decksService.unholdColumn(rankOfSpades);
|
|
2261
|
+
decksService.columnSlotService.unholdColumn(rankOfSpades);
|
|
2373
2262
|
```
|
|
2374
2263
|
|
|
2375
2264
|
### Sorted lists
|
|
2376
2265
|
|
|
2377
2266
|
```ts signature
|
|
2378
|
-
|
|
2379
|
-
indexService:
|
|
2267
|
+
createOIMSortedComputedService<TKey, TEntity extends object, TSlots, TSourceKey, TSourceStore, TComputedStore?>({
|
|
2268
|
+
indexService: OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore>;
|
|
2380
2269
|
sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { column: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
|
|
2381
2270
|
comparator?: (left: TEntity, right: TEntity) => number;
|
|
2382
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; // default
|
|
2271
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2383
2272
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
|
|
2384
2273
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<…>, TComputedStore>; // Map; REQUIRED for any other store
|
|
2385
|
-
}):
|
|
2274
|
+
}): OIMSortedComputedService<TKey, TEntity, TSlots, TSourceKey, TSourceStore, TComputedStore>
|
|
2386
2275
|
|
|
2387
2276
|
createSortedStoreSlot(indexStoreSlot: TOIMIndexStoreSlot<…>): TOIMStoreSlot<TComputedStore, TOIMSortedComputedSlot<TEntity, TKey>>
|
|
2388
2277
|
readonly computedCollectionService: OIMSlotCollectionService<TKey, TOIMSortedComputedSlot<TEntity, TKey>, TComputedStore, …>
|
|
@@ -2390,11 +2279,11 @@ readonly computedCollectionService: OIMSlotCollectionService<TKey, TOIMSortedCom
|
|
|
2390
2279
|
|
|
2391
2280
|
`slotReleasePolicy` decides whether a key's sorted list outlives its last hold. The
|
|
2392
2281
|
default lets it go at the unhold, so holding the key again sorts from scratch;
|
|
2393
|
-
`
|
|
2282
|
+
`OIMSlotNeverReleasePolicy.INSTANCE` keeps every key that was ever read and re-sorts it
|
|
2394
2283
|
only when a read finds it changed — the arm for a view that switches between groups.
|
|
2395
2284
|
Pass the same policy to the index the list reads, or its bucket is rebuilt instead.
|
|
2396
2285
|
|
|
2397
|
-
An `
|
|
2286
|
+
An `OIMSortedComputedService` takes no db: its store slots run on the channel slot of the index store slot
|
|
2398
2287
|
they sort. One per index service. `createSortedStoreSlot(indexStoreSlot)` makes one sorted store slot over
|
|
2399
2288
|
one index store slot, and the read doors of `computedCollectionService` take that store slot first:
|
|
2400
2289
|
`getSlotByKey`, `holdSlotByKey`, `findSlotByKey`, `getSlots`, `getSlotCount`, and the column doors; per key a
|
|
@@ -2420,11 +2309,11 @@ slots sorted. A hold is given back with `slot.owner.unholdSlot(slot)`.
|
|
|
2420
2309
|
to a member as with `comparator` alone.
|
|
2421
2310
|
|
|
2422
2311
|
```ts
|
|
2423
|
-
const cardsInDeckService =
|
|
2312
|
+
const cardsInDeckService = createOIMSlotCollectionIndexService(db, {
|
|
2424
2313
|
entityService: cardService,
|
|
2425
2314
|
keyOf: (card: Card) => card.deck,
|
|
2426
2315
|
});
|
|
2427
|
-
|
|
2316
|
+
createOIMSortedComputedService({
|
|
2428
2317
|
indexService: cardsInDeckService,
|
|
2429
2318
|
sortColumns: [{ column: 'title', direction: 'asc' }, { column: 'rank', direction: 'asc' }],
|
|
2430
2319
|
comparator: (l, r) => l.title.localeCompare(r.title, 'de') || l.rank - r.rank,
|
|
@@ -2439,9 +2328,9 @@ SECOND array of `createEffect`. A re-sort that yields the same sequence
|
|
|
2439
2328
|
(`oimSameSlotSequence`) keeps the previous array, so its identity is stable.
|
|
2440
2329
|
|
|
2441
2330
|
```ts
|
|
2442
|
-
const decksService =
|
|
2331
|
+
const decksService = createOIMSlotCollectionIndexService(db, { entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2443
2332
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2444
|
-
const byRankInDeckService =
|
|
2333
|
+
const byRankInDeckService = createOIMSortedComputedService({
|
|
2445
2334
|
indexService: decksService,
|
|
2446
2335
|
sortColumns: [{ column: 'rank', direction: 'asc' }],
|
|
2447
2336
|
});
|
|
@@ -2456,21 +2345,21 @@ spadesByRank.owner.unholdSlot(spadesByRank);
|
|
|
2456
2345
|
|
|
2457
2346
|
### Sorted at write
|
|
2458
2347
|
|
|
2459
|
-
A second way to get a sorted list. `
|
|
2460
|
-
index. `
|
|
2348
|
+
A second way to get a sorted list. `createOIMSortedComputedService` sorts at read, over any
|
|
2349
|
+
index. `createOIMSlotCollectionSortedArrayIndexService` keeps the index's own buckets sorted at write, and
|
|
2461
2350
|
the user picks one at construction.
|
|
2462
2351
|
|
|
2463
2352
|
```ts signature
|
|
2464
|
-
|
|
2465
|
-
entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as
|
|
2353
|
+
createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>(db, {
|
|
2354
|
+
entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as createOIMSlotCollectionIndexService, scalar key
|
|
2466
2355
|
sortColumns: readonly TOIMSortColumn<TEntity>[]; // REQUIRED
|
|
2467
2356
|
bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService?, columnNames?,
|
|
2468
|
-
}):
|
|
2357
|
+
}): OIMSlotCollectionSortedArrayIndexService<…> // its slotContentService is an OIMSlotContentSortedArrayBucketService
|
|
2469
2358
|
```
|
|
2470
2359
|
|
|
2471
2360
|
```ts
|
|
2472
2361
|
declare const someCardSlot: TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
2473
|
-
const cardsByDeckService =
|
|
2362
|
+
const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService(db, {
|
|
2474
2363
|
entityService: cardService,
|
|
2475
2364
|
columns: ['deck'],
|
|
2476
2365
|
sortColumns: [{ column: 'rank', direction: 'asc' }, { column: 'id', direction: 'desc' }],
|
|
@@ -2483,26 +2372,26 @@ cardsByDeckService.slotContentService.getBucketMemberSlotPosition(spades, someCa
|
|
|
2483
2372
|
```
|
|
2484
2373
|
|
|
2485
2374
|
- **Reads come back in order**: `getBucketMemberSlots`, `getBucketMemberSlotAt` and
|
|
2486
|
-
`getBucketMemberSlotPosition` on `
|
|
2375
|
+
`getBucketMemberSlotPosition` on `OIMSlotContentSortedArrayBucketService` answer in `sortColumns` order,
|
|
2487
2376
|
so no sorted computed is needed.
|
|
2488
2377
|
- **A lazy insertion buffer.** A write appends the arrival to an unsorted tail of the bucket's
|
|
2489
|
-
array (`
|
|
2378
|
+
array (`TOIMBucketSlotsArrayStore`: the array store plus `sortedCount`). The first read
|
|
2490
2379
|
settles the tail: binary insertion when the tail is short, one sort of the array when it is
|
|
2491
2380
|
long. A bucket nobody reads is never sorted.
|
|
2492
2381
|
- **The index re-files a member when one of its sort columns changes, and only then.** A write
|
|
2493
2382
|
to a column that is neither the key nor a sort column neither reorders the bucket nor wakes it.
|
|
2494
2383
|
A re-file wakes the bucket once per frame.
|
|
2495
2384
|
- **There are no positional writes** on this bucket (`insertBucketMemberSlotAt` and the other
|
|
2496
|
-
position writes belong to `
|
|
2385
|
+
position writes belong to `OIMSlotContentPositionalArrayBucketService`): the order is the comparator's,
|
|
2497
2386
|
not the caller's.
|
|
2498
|
-
- **Bucket commands are always `
|
|
2387
|
+
- **Bucket commands are always `OIMNoBucketCommands`**: this strategy records no per-flush
|
|
2499
2388
|
script.
|
|
2500
|
-
- **Parts**: `
|
|
2501
|
-
`
|
|
2502
|
-
`repositionBucketMemberSlot(bucketSlot, bucketMemberSlot)`), `
|
|
2389
|
+
- **Parts**: `OIMSlotContentSortedArrayBucketService` (options
|
|
2390
|
+
`TOIMSlotContentSortedArrayBucketServiceOptions`: `compareBucketMemberSlots`; adds
|
|
2391
|
+
`repositionBucketMemberSlot(bucketSlot, bucketMemberSlot)`), `OIMSlotCollectionSortedArrayIndexService`.
|
|
2503
2392
|
|
|
2504
2393
|
**Choosing between the two arms.** Sort-at-write suits point updates: a change to a sort
|
|
2505
|
-
column re-files one member. Sort-at-read (`
|
|
2394
|
+
column re-files one member. Sort-at-read (`createOIMSortedComputedService` over any index)
|
|
2506
2395
|
suits bulk loads. Both are shipped; neither is a default.
|
|
2507
2396
|
|
|
2508
2397
|
**`indexKeys` on each entity slot.** Each indexable slot carries, per index store slot, the key the entity is
|
|
@@ -2515,10 +2404,10 @@ writer. Only the index writes it, through the `IOIMIndexKeysService` (`get(slot,
|
|
|
2515
2404
|
|
|
2516
2405
|
| strategy | on `disposeIndexStoreSlot` | cost |
|
|
2517
2406
|
|---|---|---|
|
|
2518
|
-
| `
|
|
2519
|
-
| `
|
|
2407
|
+
| `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 |
|
|
2408
|
+
| `OIMReclaimingIndexKeysService` | wipes that position on every entity slot of the source store slot and reuses it | O(N) per dispose |
|
|
2520
2409
|
|
|
2521
|
-
Switch to `
|
|
2410
|
+
Switch to `OIMReclaimingIndexKeysService` only if you create indexes dynamically and often. Creating
|
|
2522
2411
|
indexes dynamically and often is expensive, so avoid it.
|
|
2523
2412
|
|
|
2524
2413
|
### The channel of a bucket store slot
|
|
@@ -2581,7 +2470,7 @@ the next read or hold.
|
|
|
2581
2470
|
class OIMComputedService<TKey, TValue, TComputedStore> {
|
|
2582
2471
|
constructor(options: TOIMComputedServiceOptions<TKey, TValue, TComputedStore>);
|
|
2583
2472
|
readonly computedCollectionService: OIMSlotCollectionService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore,
|
|
2584
|
-
OIMComputedValueService<TKey, TValue>, Extract<keyof TValue, string>, TOIMComputedColumnSlot,
|
|
2473
|
+
OIMComputedValueService<TKey, TValue>, Extract<keyof TValue, string>, TOIMComputedColumnSlot, OIMPullColumnSlotOwner>;
|
|
2585
2474
|
createComputedStoreSlot(channelMap: IOIMChannelMap, derive?, createSources?): TOIMComputedStoreSlot<TComputedStore, TValue, TKey>;
|
|
2586
2475
|
}
|
|
2587
2476
|
|
|
@@ -2590,7 +2479,7 @@ createOIMComputedService<TKey, TValue, TComputedStore = Map<TKey, TOIMComputedSl
|
|
|
2590
2479
|
derive?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue;
|
|
2591
2480
|
sameValueGuard?: TOIMSameValueGuard<TValue>; // default Object.is
|
|
2592
2481
|
initialValue?: TValue;
|
|
2593
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; // default
|
|
2482
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2594
2483
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // OIMDefaults.COMPUTED_COLUMN_SLOT_STORE_SERVICE
|
|
2595
2484
|
columnNames?: readonly Extract<keyof TValue, string>[];
|
|
2596
2485
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore>; // Map; REQUIRED for any other store
|
|
@@ -2610,15 +2499,13 @@ Doors of a keyed computed store slot are the read doors of `computedCollectionSe
|
|
|
2610
2499
|
`getSlots(storeSlot)`, `getSlotCount(storeSlot)`, and the column doors; the value is
|
|
2611
2500
|
`slot.computedValueService.getValueForSlot(slot)`, and a give-back is `slot.owner.unholdSlot(slot)`.
|
|
2612
2501
|
|
|
2613
|
-
**
|
|
2614
|
-
`
|
|
2615
|
-
|
|
2616
|
-
|
|
2502
|
+
**A single computed is its own service.** `createOIMSingleComputedService<TValue>({ … })` builds an
|
|
2503
|
+
`OIMSingleComputedService`, whose store slots each hold at most one computed slot, read through
|
|
2504
|
+
`computedSingleSlotService` (`getSingleSlot`, `holdSingleSlot`, `findSingleSlot`). Like a keyed one, a store slot
|
|
2505
|
+
outlives every computed slot it mints and mints a fresh one on the next read or hold once the previous one is gone.
|
|
2617
2506
|
|
|
2618
2507
|
```ts
|
|
2619
|
-
const topRankService =
|
|
2620
|
-
computedSlotStoreService:
|
|
2621
|
-
OIMSlotStoreServiceSingleSlotDriven.INSTANCE as unknown as OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<number, void>>,
|
|
2508
|
+
const topRankService = createOIMSingleComputedService<number>({
|
|
2622
2509
|
sameValueGuard: oimSameValueZeroGuard,
|
|
2623
2510
|
initialValue: -1,
|
|
2624
2511
|
});
|
|
@@ -2627,16 +2514,15 @@ const topRank = topRankService.createComputedStoreSlot(
|
|
|
2627
2514
|
() => cardService.getSlotByKey(cards, 'c1').entity?.rank ?? -1,
|
|
2628
2515
|
() => ({ sourceSlots: [cardService.getSlotByKey(cards, 'c1')] }),
|
|
2629
2516
|
);
|
|
2630
|
-
const heldTopRank = topRankService.
|
|
2517
|
+
const heldTopRank = topRankService.computedSingleSlotService.holdSingleSlot(topRank); // the same slot while anything holds it
|
|
2631
2518
|
heldTopRank.computedValueService.getValueForSlot(heldTopRank);
|
|
2632
|
-
heldTopRank.owner.unholdSlot(heldTopRank); // gone; the next
|
|
2519
|
+
heldTopRank.owner.unholdSlot(heldTopRank); // gone; the next holdSingleSlot(topRank) mints a new one
|
|
2633
2520
|
```
|
|
2634
2521
|
|
|
2635
|
-
A single computed store slot has
|
|
2636
|
-
undefined)` hands a slot without a hold — a fresh one goes to the unused-slot collector and is let go at the
|
|
2522
|
+
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
|
|
2637
2523
|
next flush unless something holds it — and `holdSlotByKey(storeSlot, undefined)` holds the live slot, minting
|
|
2638
2524
|
one if the previous is gone. A computed slot goes at its last unhold once no effect reads it
|
|
2639
|
-
(`
|
|
2525
|
+
(`OIMSlotAtUnholdReleasePolicy`, the default), and gives back the holds it took on its
|
|
2640
2526
|
sources. A last unhold during a flush releases at the end of that flush, through the
|
|
2641
2527
|
db's unused-slot collector, as for every non-empty slot. The holds a computed takes on its sources and
|
|
2642
2528
|
gives back are one stateless service, shared by both shapes.
|
|
@@ -2650,11 +2536,11 @@ list, and a source a build closes over without naming is a source nothing holds.
|
|
|
2650
2536
|
**A derivation is never empty.** `OIMComputedService` answers
|
|
2651
2537
|
"empty" with `false`, so a filed derivation is released only when nothing holds
|
|
2652
2538
|
it, nothing watches it, and the grace is `0` — which is what
|
|
2653
|
-
`
|
|
2539
|
+
`OIMSlotAtUnholdReleasePolicy` answers, and the default when no `slotReleasePolicy` is given.
|
|
2654
2540
|
|
|
2655
2541
|
```ts setup
|
|
2656
2542
|
const rankService = createOIMComputedService<TOIMStoreKey, number>({
|
|
2657
|
-
createSources: (
|
|
2543
|
+
createSources: ({ key }) => ({ sourceSlots: [cardService.getSlotByKey(cards, key)] }),
|
|
2658
2544
|
derive: (computedSlot) => cardService.getSlotByKey(cards, computedSlot.key).entity?.rank ?? -1,
|
|
2659
2545
|
sameValueGuard: oimSameValueZeroGuard,
|
|
2660
2546
|
initialValue: -1,
|
|
@@ -2685,37 +2571,34 @@ Pinned by `facade/computed/tests/what-a-computed-column-wakes-on.test.ts`.
|
|
|
2685
2571
|
|
|
2686
2572
|
```ts
|
|
2687
2573
|
type Summary = { top: number; count: number };
|
|
2688
|
-
const summaryService =
|
|
2689
|
-
computedSlotStoreService:
|
|
2690
|
-
OIMSlotStoreServiceSingleSlotDriven.INSTANCE as unknown as OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<Summary, void>>,
|
|
2691
|
-
});
|
|
2574
|
+
const summaryService = createOIMSingleComputedService<Summary>();
|
|
2692
2575
|
const summary = summaryService.createComputedStoreSlot(
|
|
2693
2576
|
db,
|
|
2694
2577
|
() => ({ top: slot.entity?.rank ?? -1, count: cardService.getSlotCount(cards) }),
|
|
2695
2578
|
() => ({ sourceSlots: [slot] }),
|
|
2696
2579
|
);
|
|
2697
|
-
const
|
|
2698
|
-
const summaryTop =
|
|
2580
|
+
const summarySingleSlotService = summaryService.computedSingleSlotService;
|
|
2581
|
+
const summaryTop = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(summary), 'top');
|
|
2699
2582
|
const topEffect = effectService.createEffect(db, [], [summaryTop], () => { paint(slot.entity); });
|
|
2700
2583
|
effectService.disposeEffect(topEffect);
|
|
2701
|
-
|
|
2584
|
+
summarySingleSlotService.columnSlotService.unholdColumn(summaryTop);
|
|
2702
2585
|
```
|
|
2703
2586
|
|
|
2704
2587
|
### The order of a bucket, as one derivation per key
|
|
2705
2588
|
|
|
2706
2589
|
```ts signature
|
|
2707
|
-
|
|
2590
|
+
createOIMBucketMemoComputedService<TKey, TBucketMemberSlot extends TOIMSlot, TSlots, TBucketStore, TComputedStore?>({
|
|
2708
2591
|
bucketCollectionService: OIMSlotCollectionService<TKey, TOIMBucketSlot<TSlots, TKey>, TBucketStore, OIMSlotContentBucketService<TBucketMemberSlot, TSlots>, …>;
|
|
2709
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; // default
|
|
2592
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2710
2593
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
|
|
2711
2594
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<readonly TBucketMemberSlot[], TKey>, TComputedStore>;
|
|
2712
|
-
}):
|
|
2595
|
+
}): OIMBucketMemoComputedService<TKey, TBucketMemberSlot, TSlots, TBucketStore, TComputedStore>
|
|
2713
2596
|
|
|
2714
2597
|
createBucketMemoStoreSlot(bucketStoreSlot: TOIMStoreSlot<TBucketStore, TOIMBucketSlot<TSlots, TKey>, readonly TBucketMemberSlot[]>): TOIMStoreSlot<TComputedStore, TOIMBucketMemoComputedSlot<TBucketMemberSlot, TKey>>
|
|
2715
2598
|
```
|
|
2716
2599
|
|
|
2717
2600
|
It takes no db: a memo store slot runs on the channel slot of the bucket store slot it reads. The class is
|
|
2718
|
-
`new
|
|
2601
|
+
`new OIMBucketMemoComputedService(options)` (options type `TOIMBucketMemoComputedServiceOptions`), whose
|
|
2719
2602
|
`computedCollectionService` reads the per-key derivations; `createBucketMemoStoreSlot(bucketStoreSlot)` makes
|
|
2720
2603
|
one memo store slot over one bucket store slot.
|
|
2721
2604
|
|
|
@@ -2725,13 +2608,13 @@ it is produced on the read that asks for it and never while nothing reads it, an
|
|
|
2725
2608
|
its `changedAtRevision` and `changedAtEpoch` move only when the list a reader would
|
|
2726
2609
|
see actually changed. The list is handed back by IDENTITY: it is the same array for
|
|
2727
2610
|
as long as the list holds, so a consumer memoising on identity needs no comparison
|
|
2728
|
-
of its own. For a SORTED list use [`
|
|
2611
|
+
of its own. For a SORTED list use [`createOIMSortedComputedService`](#sorted-lists).
|
|
2729
2612
|
|
|
2730
2613
|
The source list is ONE slot — the bucket — and it never grows: a bucket
|
|
2731
2614
|
change is a move of the bucket's value, not of the derivation's sources.
|
|
2732
2615
|
|
|
2733
2616
|
```ts
|
|
2734
|
-
const orderService =
|
|
2617
|
+
const orderService = createOIMBucketMemoComputedService({
|
|
2735
2618
|
bucketCollectionService: orderedService,
|
|
2736
2619
|
});
|
|
2737
2620
|
const order = orderService.createBucketMemoStoreSlot(ordered);
|
|
@@ -2786,7 +2669,7 @@ type TOIMCompositeKeyNode<TAtKey = unknown, TCompositeKeyPart = TOIMEntitySlot>
|
|
|
2786
2669
|
};
|
|
2787
2670
|
```
|
|
2788
2671
|
|
|
2789
|
-
`TOIMSlotStoreTree` is what `
|
|
2672
|
+
`TOIMSlotStoreTree` is what `OIMSlotTreeStoreService.createStore()` builds:
|
|
2790
2673
|
the tree's root plus a flat `Set` of every filed slot, which makes `getSlots()` and
|
|
2791
2674
|
`getSlotCount()` O(1) rather than a walk, and keeps `getSlots()` in filing order like every other store.
|
|
2792
2675
|
`TOIMCompositeKeyNode` is one level of that tree — a `Map` from composite key part to the
|
|
@@ -2796,10 +2679,10 @@ are holding; neither is something you build by hand.
|
|
|
2796
2679
|
|
|
2797
2680
|
| Store | Key type |
|
|
2798
2681
|
|---|---|
|
|
2799
|
-
| `
|
|
2800
|
-
| `
|
|
2801
|
-
| `
|
|
2802
|
-
| `
|
|
2682
|
+
| `OIMSlotMapStoreService<TSlot>` | `TOIMStoreKey` = `string \| number` |
|
|
2683
|
+
| `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` | `readonly TOIMStoreKey[]` — **a chain of VALUES** |
|
|
2684
|
+
| `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue()` | `readonly TOIMEntitySlot[]` — **a chain of entity SLOTS** |
|
|
2685
|
+
| `OIMSingleSlotStoreService.INSTANCE` | `void` — one slot per store slot, its store a `TOIMSingleSlotStore` (`{ slot }`); what every single is built on |
|
|
2803
2686
|
|
|
2804
2687
|
Two stores for keys, one per shape of address, and the single-slot store for no key. The chain store descends one `Map`
|
|
2805
2688
|
lookup per composite key part, keeps `[a]` and `[a, b]` apart, and has **no encoding step**:
|
|
@@ -2839,10 +2722,10 @@ deletes nothing and returns `undefined`.
|
|
|
2839
2722
|
|
|
2840
2723
|
| Walk | Class | Shared instance | What a composite key part is | Holds? |
|
|
2841
2724
|
|---|---|---|---|---|
|
|
2842
|
-
| scalar | `
|
|
2843
|
-
| composite | `
|
|
2725
|
+
| scalar | `OIMScalarKeyTreeWalk` | `OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` | `TOIMStoreKey` | no |
|
|
2726
|
+
| composite | `OIMCompositeKeyTreeWalk` | `OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue()` | `TOIMEntitySlot` | **yes — one hold per edge** |
|
|
2844
2727
|
|
|
2845
|
-
**The hold is the whole difference.** `
|
|
2728
|
+
**The hold is the whole difference.** `OIMCompositeKeyTreeWalk.mintNodeAt` calls
|
|
2846
2729
|
`holdCompositeKeyPart` the first time it creates an edge, which runs
|
|
2847
2730
|
`entitySlot.owner.holdSlot(entitySlot)`; `deleteKey` calls `unholdCompositeKeyPart` on
|
|
2848
2731
|
exactly the edges it unlinked, deepest first and only after every unlink is done, which runs `entitySlot.owner.unholdSlot(entitySlot)` unless
|
|
@@ -2860,16 +2743,16 @@ type TScalarChain = readonly TOIMStoreKey[];
|
|
|
2860
2743
|
type Tally = { deck: string; day: string; count: number };
|
|
2861
2744
|
type TTallyStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Tally, TScalarChain>, TOIMStoreKey>;
|
|
2862
2745
|
|
|
2863
|
-
const tallyService = createOIMSlotCollectionService
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2746
|
+
const tallyService = createOIMSlotCollectionService<
|
|
2747
|
+
ReturnType<typeof createOIMSlotContentEntityService<Tally, TScalarChain>>, TTallyStore
|
|
2748
|
+
>(db, {
|
|
2749
|
+
slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(db),
|
|
2750
|
+
slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
|
|
2751
|
+
OIMDefaults.KEY_TREE_WALK_SCALAR.getValue(),
|
|
2752
|
+
),
|
|
2870
2753
|
});
|
|
2871
2754
|
const tallies = tallyService.createStoreSlot(db);
|
|
2872
|
-
tallyService.
|
|
2755
|
+
tallyService.writeSlotContentByKey(tallies, ['spades', 'mon'], { deck: 'spades', day: 'mon', count: 1 });
|
|
2873
2756
|
tallyService.getSlotByKey(tallies, ['spades', 'mon']);
|
|
2874
2757
|
```
|
|
2875
2758
|
|
|
@@ -2890,16 +2773,16 @@ type Enrolment = {
|
|
|
2890
2773
|
type TChain = readonly TOIMEntitySlot[];
|
|
2891
2774
|
type TEnrolmentStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Enrolment, TChain>, TOIMEntitySlot>;
|
|
2892
2775
|
|
|
2893
|
-
const enrolmentService = createOIMSlotCollectionService
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2776
|
+
const enrolmentService = createOIMSlotCollectionService<
|
|
2777
|
+
ReturnType<typeof createOIMSlotContentEntityService<Enrolment, TChain>>, TEnrolmentStore
|
|
2778
|
+
>(db, {
|
|
2779
|
+
slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(db),
|
|
2780
|
+
slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
|
|
2781
|
+
OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue(),
|
|
2782
|
+
),
|
|
2900
2783
|
});
|
|
2901
2784
|
const enrolments = enrolmentService.createStoreSlot(db);
|
|
2902
|
-
enrolmentService.
|
|
2785
|
+
enrolmentService.writeSlotContentByKey(enrolments, [alice, admins], { user: alice, group: admins, since: 2026 });
|
|
2903
2786
|
enrolmentService.getSlotByKey(enrolments, [alice, admins]);
|
|
2904
2787
|
```
|
|
2905
2788
|
|
|
@@ -2944,10 +2827,14 @@ At a deadline the release queue reads its MARKER off the slot, decides, and call
|
|
|
2944
2827
|
| `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 |
|
|
2945
2828
|
|
|
2946
2829
|
**`OIMSlotOwner`** is the base class and it is concrete: it implements all five.
|
|
2947
|
-
Its `releaseSlotIfUnheld` does nothing,
|
|
2830
|
+
Its `releaseSlotIfUnheld` does nothing, its
|
|
2831
|
+
`unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMSlotEditable` with no
|
|
2832
|
+
guard at all (it exists so a library that adds its own release wheel can keep its own bit in
|
|
2833
|
+
`slot.slotFlags`; the engine owns bits `1`, `2`, `4`, `8` and `16`, so a library starts at `32`;
|
|
2834
|
+
it is on the owner so a subclass can hook every such mutation), and a `protected
|
|
2948
2835
|
onSlotIdle` does nothing; a `protected assertNotReleased` is what every refusing
|
|
2949
2836
|
door above calls. `OIMSlotOwnerService` overrides `onSlotIdle` and
|
|
2950
|
-
`releaseSlotIfUnheld`, asking its release policy and its content
|
|
2837
|
+
`releaseSlotIfUnheld`, asking its release policy and its content service; the clock is the slot's channel slot's.
|
|
2951
2838
|
Extend `OIMSlotOwner` when you own slots that no collection files.
|
|
2952
2839
|
**`OIMDefaults.STANDALONE_SLOT_OWNER`** is one shared instance of it — the owner of a
|
|
2953
2840
|
computed slot nobody has filed. It never releases anything, which is exactly what
|
|
@@ -2958,12 +2845,12 @@ owner is its collection type's slot owner, and that release policy applies to it
|
|
|
2958
2845
|
|
|
2959
2846
|
**Columns are a module of every collection.** The column slots of a carrier slot live
|
|
2960
2847
|
in `carrierSlot.columnSlotStore`, a field of the base `TOIMSlot`, so any slot can carry
|
|
2961
|
-
them — an entity slot, a bucket slot, a computed slot. `
|
|
2848
|
+
them — an entity slot, a bucket slot, a computed slot. `OIMColumnSlotOwner` is abstract, an
|
|
2962
2849
|
`OIMSlotOwner` subclass, with two strategies:
|
|
2963
|
-
`
|
|
2964
|
-
keyed and single) and `
|
|
2850
|
+
`OIMPushColumnSlotOwner` (plain column slots woken by marks on a write; entity and bucket collections,
|
|
2851
|
+
keyed and single) and `OIMPullColumnSlotOwner` (each column slot a computed slot over its carrier;
|
|
2965
2852
|
computed collections, keyed and single). The factory hands the strategy's factory to the slot owner as
|
|
2966
|
-
`
|
|
2853
|
+
`createColumnSlotOwner`. Each collection type has its own column owner, and every column slot it mints names that
|
|
2967
2854
|
owner as its `owner`. The owner holds only the column slot store service; the column state — the column
|
|
2968
2855
|
groups and which columns are referenced — lives on the store slot. The collection exposes it only
|
|
2969
2856
|
through its column doors: `getColumn(slot, columnName)`, `holdColumn(slot, columnName)`,
|
|
@@ -3026,12 +2913,11 @@ content service hands back the LIVE list — declared `Iterable`, not an array,
|
|
|
3026
2913
|
`Array.from(bucketService.slotContentService.getBucketMemberSlots(bucketSlot))` is required and is the copy. A `prev` kept
|
|
3027
2914
|
live is the same array as `next` on the following frame: the diff is empty,
|
|
3028
2915
|
no reorder is ever drawn, and nothing throws. The other safe `prev` is
|
|
3029
|
-
`
|
|
2916
|
+
`createOIMBucketMemoComputedService`'s value — a fresh array on every write that
|
|
3030
2917
|
changed the list, the same array while it did not.
|
|
3031
2918
|
|
|
3032
2919
|
A LIST MAY NAME ONE SLOT TWICE, and then it holds that slot twice.
|
|
3033
|
-
`
|
|
3034
|
-
records every mention, so an ordered bucket's list is exactly such a list. The
|
|
2920
|
+
`OIMSlotContentPositionalArrayBucketService` records every mention, so an ordered bucket's list is exactly such a list. The
|
|
3035
2921
|
mentions of one identity are matched in the order they appear, and the script
|
|
3036
2922
|
reproduces `next` mention for mention. The minimal move count (`common − LIS`)
|
|
3037
2923
|
is claimed for lists whose identities are distinct: where the two lists name one
|
|
@@ -3083,7 +2969,9 @@ type TOIMStoreSlot<TStore = unknown, TSlot extends TOIMSlot = TOIMSlot, TPatch =
|
|
|
3083
2969
|
readonly indexKeysPositions: TOIMIndexKeysPositions | undefined;
|
|
3084
2970
|
readonly sourceStoreSlot: TOIMStoreSlot | undefined; // an index store slot's source
|
|
3085
2971
|
readonly isDisposed: boolean; // an index store slot after disposeIndexStoreSlot
|
|
3086
|
-
|
|
2972
|
+
readonly slotService: TSlotService; // the service that created the store slot
|
|
2973
|
+
readonly batchDepth: number; // openBatch / closeBatch nesting
|
|
2974
|
+
// … column-reader counts, column groups, batch queues, bucket write queues: the services' own
|
|
3087
2975
|
};
|
|
3088
2976
|
|
|
3089
2977
|
type TOIMSingleSlotStore<TSlot> = { slot: TSlot | undefined };
|
|
@@ -3117,7 +3005,7 @@ type TOIMEntitySlot<TEntity extends object = object, TKey = unknown> = TOIMIndex
|
|
|
3117
3005
|
};
|
|
3118
3006
|
|
|
3119
3007
|
// `data` belongs to the consumer: the core never reads or writes it, and clearing or
|
|
3120
|
-
// removing the slot (`slotContentService.
|
|
3008
|
+
// removing the slot (`slotContentService.clearSlotContent(slot)`) does not reset it. Each index keeps its
|
|
3121
3009
|
// last key for the entity slot in `indexKeys`, through the db's `IOIMIndexKeysService`.
|
|
3122
3010
|
|
|
3123
3011
|
type TOIMColumnSlot = TOIMSlot & {
|
|
@@ -3127,26 +3015,26 @@ type TOIMColumnSlot = TOIMSlot & {
|
|
|
3127
3015
|
|
|
3128
3016
|
type TOIMComputedColumnSlot = TOIMComputedSlot<unknown, string>; // a computed's getColumn / holdColumn return it; it goes in createEffect's second list
|
|
3129
3017
|
|
|
3130
|
-
type
|
|
3018
|
+
type TOIMBucketContentSlot<TSlots = unknown> = TOIMSlot & {
|
|
3131
3019
|
readonly owner: IOIMSlotOwner;
|
|
3132
|
-
readonly storeSlot: TOIMStoreSlot<unknown,
|
|
3020
|
+
readonly storeSlot: TOIMStoreSlot<unknown, TOIMBucketContentSlot<TSlots>>;
|
|
3133
3021
|
readonly bucketSlotsStore: TSlots;
|
|
3134
3022
|
readonly commands: readonly TOIMOrderedListCommand[] | undefined; // emptied when the flush has ended
|
|
3135
3023
|
readonly isWholeListRecorded: boolean; // reset to false with it
|
|
3136
3024
|
readonly commandsStartedAtRevision: number; // the clock the script started at
|
|
3137
3025
|
readonly commandsStartedAtEpoch: number;
|
|
3138
|
-
readonly wasEmptyBeforeChange: boolean | undefined;
|
|
3139
|
-
readonly isChangedAgainInSubframe: boolean;
|
|
3140
3026
|
};
|
|
3027
|
+
type TOIMBucketSlot<TSlots = unknown, TKey extends NonNullable<unknown> = NonNullable<unknown>> =
|
|
3028
|
+
TOIMBucketContentSlot<TSlots> & TOIMIndexableSlot<TKey>; // a keyed bucket slot; a single bucket is the content slot alone
|
|
3141
3029
|
|
|
3142
3030
|
// `holdSlotByKey(storeSlot, key)` hands back the slot itself; give the hold back with `slot.owner.unholdSlot(slot)`.
|
|
3143
3031
|
|
|
3144
3032
|
type TOIMBucketSlotsSetStore<TBucketMemberSlot extends TOIMSlot = TOIMSlot> = Set<TBucketMemberSlot>;
|
|
3145
3033
|
type TOIMBucketSlotsArrayStore<TBucketMemberSlot extends TOIMSlot = TOIMSlot> = {
|
|
3146
3034
|
bucketMemberSlots: TBucketMemberSlot[];
|
|
3035
|
+
uniqueBucketMemberSlots: Set<TBucketMemberSlot> | undefined; // kept by OIMBucketUniqueMemberSlotsArrayService
|
|
3036
|
+
sortedCount: number;
|
|
3147
3037
|
};
|
|
3148
|
-
type TOIMBucketSlotsArrayUniqueStore<TBucketMemberSlot extends TOIMSlot = TOIMSlot> =
|
|
3149
|
-
TOIMBucketSlotsArrayStore<TBucketMemberSlot> & { uniqueBucketMemberSlots: Set<TBucketMemberSlot> };
|
|
3150
3038
|
|
|
3151
3039
|
type TOIMEntityColumnName<TEntity extends object> =
|
|
3152
3040
|
[TEntity] extends [never] ? never
|
|
@@ -3181,38 +3069,39 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3181
3069
|
|
|
3182
3070
|
## Full export list
|
|
3183
3071
|
|
|
3184
|
-
**
|
|
3072
|
+
**218 names, every export of `packages/core/src/index.ts`, grouped by topic.**
|
|
3185
3073
|
|
|
3186
3074
|
*Channel and database slots* — `OIMChannelService` · `TOIMChannelSlot` · `TOIMChannelSlotOptions` · `createOIMChannelSlot` ·
|
|
3187
|
-
`IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot`
|
|
3075
|
+
`IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot` · `IOIMFlushSchedulerListener`
|
|
3188
3076
|
|
|
3189
3077
|
*Collector services and slots* — `OIMUnusedSlotCollectorService` · `TOIMUnusedSlotCollectorSlot` ·
|
|
3190
3078
|
`createOIMUnusedSlotCollectorSlot` · `TOIMCollectorSlot` · `TOIMCollectorSlotOptions` · `createOIMCollectorSlot` ·
|
|
3191
|
-
`
|
|
3192
|
-
`
|
|
3193
|
-
`
|
|
3194
|
-
|
|
3195
|
-
*Slot services* — `
|
|
3196
|
-
`
|
|
3197
|
-
`
|
|
3198
|
-
`
|
|
3199
|
-
`
|
|
3079
|
+
`OIMSlotWheel` · `TOIMSlotWheelOptions` · `OIMSlotReleaseWheel` · `TOIMSlotReleaseWheelOptions` ·
|
|
3080
|
+
`oimMintAfterWriteSlotReleaseWheel` · `oimMintAfterIdleSlotReleaseWheel` · `oimMintAfterUnholdSlotReleaseWheel` ·
|
|
3081
|
+
`OIMSlotRefetchWheel` · `TOIMSlotRefetchWheelOptions` · `IOIMSlotRefetchListener`
|
|
3082
|
+
|
|
3083
|
+
*Slot services* — `OIMSlotCollectionService` · `OIMWritableSlotCollectionService` · `TOIMSlotCollectionServiceOptions` ·
|
|
3084
|
+
`createOIMSlotCollectionService` · `OIMSingleSlotService` · `OIMWritableSingleSlotService` · `TOIMSingleSlotServiceOptions` ·
|
|
3085
|
+
`createOIMSingleSlotService` · `OIMColumnSlotService` (the column doors, `service.columnSlotService`) ·
|
|
3086
|
+
`OIMSlotOwnerService` (**type only**) · `TOIMSlotOwnerServiceOptions` · `IOIMSlotContentService` · `IOIMSlotContentWriter` ·
|
|
3087
|
+
`IOIMKeyedSlotCreator` · `IOIMSingleSlotCreator` · `IOIMSlotFilingService` · `TOIMSlotContentServiceKey` ·
|
|
3088
|
+
`TOIMSlotContentServiceSlot` · `TOIMSlotContentServiceSingleSlot` · `TOIMSlotContentServiceContent` ·
|
|
3089
|
+
`TOIMSlotContentServicePatch` · `OIMContentChangeListenerService`
|
|
3200
3090
|
|
|
3201
3091
|
*Content services* — `OIMSlotContentEntityService` · `TOIMSlotContentEntityServiceOptions` ·
|
|
3202
|
-
`createOIMSlotContentEntityService` · `
|
|
3203
|
-
`
|
|
3204
|
-
`
|
|
3205
|
-
`
|
|
3206
|
-
`
|
|
3207
|
-
`
|
|
3092
|
+
`createOIMSlotContentEntityService` · `TOIMEntitySlotOwnerService` · `TOIMEntityContentSlot` · `OIMSlotContentBucketService` ·
|
|
3093
|
+
`OIMSlotContentArrayBucketService` · `OIMSlotContentPositionalArrayBucketService` · `OIMSlotContentSetBucketService` ·
|
|
3094
|
+
`OIMSlotContentSortedArrayBucketService` · `TOIMSlotContentBucketServiceOptions` ·
|
|
3095
|
+
`TOIMSlotContentArrayBucketServiceOptions` · `TOIMSlotContentSortedArrayBucketServiceOptions` · `TOIMBucketSlotOwnerService` ·
|
|
3096
|
+
`TOIMBucketContentSlot` · `OIMSlotContentComputedService` (**type only**) · `TOIMSlotContentComputedServiceOptions` ·
|
|
3097
|
+
`TOIMComputedContentSlot`
|
|
3208
3098
|
|
|
3209
|
-
*Index and computed store slots* — `
|
|
3210
|
-
`
|
|
3211
|
-
`TOIMIndexServiceStoreSlot` · `TOIMSortedComputedSlot` · `TOIMBucketMemoComputedSlot`
|
|
3099
|
+
*Index and computed store slots* — `createOIMSlotCollectionIndexService` · `TOIMSlotCollectionIndexServiceFactoryOptions` ·
|
|
3100
|
+
`TOIMSlotCollectionIndexServiceStoreSlot` · `TOIMSortedComputedSlot` · `TOIMBucketMemoComputedSlot`
|
|
3212
3101
|
|
|
3213
3102
|
*Runtime* — `TOIMChannelFrame` · `TOIMChannelClock` ·
|
|
3214
|
-
`TOIMChannelScheduler` · `TOIMDisposer` · `IOIMErrorPolicy` · `
|
|
3215
|
-
`
|
|
3103
|
+
`TOIMChannelScheduler` · `TOIMDisposer` · `IOIMErrorPolicy` · `OIMLogAndContinueErrorPolicy` ·
|
|
3104
|
+
`TOIMLogAndContinueErrorPolicyOptions` · `OIMThrowAtOnceErrorPolicy` · `OIMChannelQueue` (**type only**) ·
|
|
3216
3105
|
`oimIsAnySlotChangedAfter`
|
|
3217
3106
|
|
|
3218
3107
|
`oimIsAnySlotChangedAfter(slots, revision, epoch): boolean` answers whether any of the slots was
|
|
@@ -3227,49 +3116,49 @@ written after that clock reading — `channelSlot.clock` read earlier.
|
|
|
3227
3116
|
read outside an effect (`useOIMValue` in `@oimdb/react`, `exoBinding` in `@oimdb/exodra`) calls it
|
|
3228
3117
|
on its `sourceComputeds` before `derive`, so the derive never reads a stale computed.
|
|
3229
3118
|
|
|
3230
|
-
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `
|
|
3231
|
-
`TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `
|
|
3232
|
-
`
|
|
3119
|
+
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMSlotEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
|
|
3120
|
+
`TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `OIMColumnSlotOwner` ·
|
|
3121
|
+
`OIMPushColumnSlotOwner` · `OIMPullColumnSlotOwner` · `TOIMColumnSlotOwnerOptions` · `TOIMBucketSlot` ·
|
|
3233
3122
|
`TOIMSameValueGuard` · `OIMSlotOwner` ·
|
|
3234
|
-
`IOIMSlotOwner` · `
|
|
3123
|
+
`IOIMSlotOwner` · `IOIMSlotEmptinessPolicy` ·
|
|
3235
3124
|
`IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMContentChangeListener`
|
|
3236
3125
|
|
|
3237
3126
|
`oimIsSlotEmpty(slot): boolean` reads the EMPTY bit of `slot.slotFlags`.
|
|
3238
3127
|
`oimIsSlotReleased(slot): boolean` reads its RELEASED bit.
|
|
3239
3128
|
|
|
3240
3129
|
*Release policies* — `IOIMSlotReleasePolicy` ·
|
|
3241
|
-
`
|
|
3242
|
-
`
|
|
3243
|
-
`
|
|
3244
|
-
`
|
|
3130
|
+
`OIMSlotAfterWriteReleasePolicy` · `TOIMSlotAfterWriteReleasePolicyOptions` ·
|
|
3131
|
+
`IOIMSlotReleaseDueFramePolicy` · `OIMSlotReleaseAutoDueFramePolicy` · `OIMSlotReleaseManualDueFramePolicy` ·
|
|
3132
|
+
`OIMSlotAfterIdleReleasePolicy` · `TOIMSlotAfterIdleReleasePolicyOptions` ·
|
|
3133
|
+
`OIMSlotAfterUnholdReleasePolicy` · `TOIMSlotAfterUnholdReleasePolicyOptions` ·
|
|
3134
|
+
`OIMSlotAtUnholdReleasePolicy` · `OIMSlotAtEmptyReleasePolicy` · `OIMSlotNeverReleasePolicy` · `OIMSlotEarliestReleasePolicy` ·
|
|
3245
3135
|
`TOIMTimer` · `oimSliceOfMs` · `OIM_SLICE_LENGTH_MS`
|
|
3246
3136
|
|
|
3247
3137
|
*Entity writers* — `IOIMEntityWriter` · `createOIMEntityWriterInPlace` ·
|
|
3248
|
-
`createOIMEntityWriterReplacing` · `TOIMEntityWriterOptions` · `
|
|
3138
|
+
`createOIMEntityWriterReplacing` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
|
|
3249
3139
|
`oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimPatchEntityColumnsGuarded` · `oimPatchEntityColumnGuarded` · `oimPutEntityColumns` · `oimPutEntityColumnsGuarded` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
|
|
3250
3140
|
|
|
3251
|
-
*Buckets* — `
|
|
3252
|
-
`
|
|
3253
|
-
`
|
|
3254
|
-
`OIMBucketArrayMembershipUnique` · `TOIMBucketSlotsSetStore` · `TOIMBucketSlotsArrayStore` ·
|
|
3255
|
-
`TOIMBucketSlotsSortedArrayStore` · `TOIMBucketSlotsArrayUniqueStore` · `OIM_NOTHING_WAS_INSERTED` ·
|
|
3141
|
+
*Buckets* — `OIMBucketCommands` · `TOIMBucketCommandsOptions` · `IOIMBucketCommands` ·
|
|
3142
|
+
`IOIMBucketMemberSlotsArrayService` · `OIMBucketMemberSlotsArrayService` · `OIMBucketUniqueMemberSlotsArrayService` ·
|
|
3143
|
+
`TOIMBucketSlotsSetStore` · `TOIMBucketSlotsArrayStore` · `OIM_NOTHING_WAS_INSERTED` ·
|
|
3256
3144
|
`OIM_NOTHING_WAS_REMOVED` · `OIM_FROZEN_EMPTY_SLOTS` · `OIM_REMOVED_WITHOUT_A_POSITION`
|
|
3257
3145
|
|
|
3258
|
-
*Index services* — `
|
|
3259
|
-
`
|
|
3146
|
+
*Index services* — `OIMSlotCollectionIndexService` · `OIMSlotCollectionSortedArrayIndexService` · `TOIMSlotCollectionIndexServiceOptions` · `TOIMIndexStoreSlot` ·
|
|
3147
|
+
`createOIMSlotCollectionSortedArrayIndexService` · `TOIMIndexKeyOptions` · `TOIMIndexKey` ·
|
|
3260
3148
|
`IOIMIndexKeysService` · `TOIMIndexKeysPositions` · `OIMIndexKeysService` ·
|
|
3261
|
-
`
|
|
3149
|
+
`OIMAppendingIndexKeysService` · `OIMReclaimingIndexKeysService`
|
|
3262
3150
|
|
|
3263
3151
|
*Computed services* — `OIMComputedService` · `TOIMComputedServiceOptions` ·
|
|
3264
|
-
`createOIMComputedService` · `TOIMComputedStoreSlot` · `
|
|
3265
|
-
`
|
|
3266
|
-
`
|
|
3152
|
+
`createOIMComputedService` · `TOIMComputedStoreSlot` · `OIMSortedComputedService` · `TOIMSortedComputedServiceOptions` ·
|
|
3153
|
+
`createOIMSortedComputedService` · `OIMBucketMemoComputedService` · `TOIMBucketMemoComputedServiceOptions` ·
|
|
3154
|
+
`createOIMBucketMemoComputedService` · `OIMSingleComputedService` · `createOIMSingleComputedService` ·
|
|
3155
|
+
`TOIMComputedDerivationOptions` · `TOIMSortColumn` · `TOIMSortableColumnName` · `oimColumnsComparator`
|
|
3267
3156
|
|
|
3268
3157
|
*Stores and keys* — `TOIMStoreKey` · `IOIMSlotStoreService` ·
|
|
3269
|
-
`
|
|
3270
|
-
`
|
|
3158
|
+
`OIMSlotMapStoreService` · `OIMSlotTreeStoreService` ·
|
|
3159
|
+
`OIMSingleSlotStoreService` · `TOIMSingleSlotStore` · `TOIMDefaultSlotStore` ·
|
|
3271
3160
|
`TOIMSlotStoreTree` · `IOIMKeyTreeWalk` · `TOIMCompositeKeyNode` ·
|
|
3272
|
-
`OIMKeyTreeWalk` · `
|
|
3161
|
+
`OIMKeyTreeWalk` · `OIMCompositeKeyTreeWalk` · `OIMScalarKeyTreeWalk`
|
|
3273
3162
|
|
|
3274
3163
|
*Assemblies* — `OIMDB` · `OIMDefaults` · `OIMDefaultValue` · `TOIMFactoryOptions`
|
|
3275
3164
|
|
|
@@ -3287,10 +3176,10 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3287
3176
|
|
|
3288
3177
|
### A scalar in a `KEY_TREE_WALK_COMPOSITE` key throws
|
|
3289
3178
|
|
|
3290
|
-
`
|
|
3179
|
+
`OIMSlotTreeStoreService` built with `OIMDefaults.KEY_TREE_WALK_COMPOSITE.getValue()` keys on
|
|
3291
3180
|
`readonly TOIMEntitySlot[]`, so a string or a number in the key throws
|
|
3292
3181
|
a TypeError past a cast, setting `holdCount` on a string. The store for a tuple
|
|
3293
|
-
of scalars is `
|
|
3182
|
+
of scalars is `OIMSlotTreeStoreService` built with
|
|
3294
3183
|
`OIMDefaults.KEY_TREE_WALK_SCALAR.getValue()` —
|
|
3295
3184
|
see **Key stores** above. The fix is the other walk, never a cast.
|
|
3296
3185
|
|
|
@@ -3357,14 +3246,14 @@ The computed SERVICE is the caller's, built once at module scope with `createOIM
|
|
|
3357
3246
|
slot per mount — `computedService.createComputedStoreSlot(db, derive, createSources)` in a `useMemo` on `deps`;
|
|
3358
3247
|
`derive` / `createSources` may be `undefined` when the service has them.
|
|
3359
3248
|
`useOIMSingleComputedValue(computedService, derive, createSources, deps): TValue` is the same for a void-keyed
|
|
3360
|
-
service whose `computedSlotStoreService` is `
|
|
3249
|
+
service whose `computedSlotStoreService` is `OIMSingleSlotStoreService.INSTANCE`, read through
|
|
3361
3250
|
`useOIMComputedValueByKey(computedService.computedCollectionService, computedStoreSlot, undefined)`, so a flush
|
|
3362
3251
|
between render and subscribe is safe.
|
|
3363
3252
|
`useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)` holds the identities its keys name for as long as the
|
|
3364
3253
|
component is mounted (a `holdSlotByKey` per key on commit, the same holds given back on unmount or when the list
|
|
3365
3254
|
moves) and subscribes to nothing; it needs no provider.
|
|
3366
|
-
Singles are not keyed collections: `
|
|
3367
|
-
`
|
|
3255
|
+
Singles are not keyed collections: `createOIMSingleSlotService(db, { slotContentService })` returns an
|
|
3256
|
+
`OIMWritableSingleSlotService` (`getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`), read by
|
|
3368
3257
|
`useOIMSingleEntity(singleSlotService, singleSlotStoreSlot): TEntity | undefined` and
|
|
3369
3258
|
`useOIMSingleBucketSlots(singleSlotService, singleSlotStoreSlot): readonly TOIMEntitySlot<TEntity>[]` — the by-key
|
|
3370
3259
|
mechanics without a key (getSingleSlot on render, holdSingleSlot on commit, resolve-hold given back once the
|
|
@@ -3375,8 +3264,8 @@ re-read the imports rather than trusting this table.
|
|
|
3375
3264
|
|
|
3376
3265
|
| Package | Imports from the core | Imports a name the core does not export? |
|
|
3377
3266
|
|---|---|---|
|
|
3378
|
-
| `@oimdb/persist` | `IOIMChannelMap`, `IOIMContentChangeListener`, `IOIMEntityWriter`, `OIMChannelService`, `
|
|
3379
|
-
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMContentChangeListener`, `OIMChannelService`, `
|
|
3267
|
+
| `@oimdb/persist` | `IOIMChannelMap`, `IOIMContentChangeListener`, `IOIMEntityWriter`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMContentChangeListenerService`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMChannelClock`, `TOIMChannelSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMSingleSlotStore`, `TOIMSingleSlotStoreSlot`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimIsAnySlotChangedAfter`, `oimIsSlotReleased`, `oimSameValueZeroGuard`, `oimUnholdSlotsIfAlive` | no |
|
|
3268
|
+
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMContentChangeListener`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMContentChangeListenerService`, `OIMEffectService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntityColumnName`, `TOIMEntitySlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
3380
3269
|
| `@oimdb/exodra` | `IOIMChannelMap`, `OIMChannelService`, `OIMEffectService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMComputedSlot`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMOrderedListCommand`, `TOIMSlot`, `TOIMStoreKey`, `oimOrderedListCommandsOfTransition`, `oimRefreshSourceComputeds`, `oimSameKeySequence` | no |
|
|
3381
3270
|
| `@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 |
|
|
3382
3271
|
| `@oimdb/devtools` | `IOIMChannelMap`, `IOIMStoreSlotListener`, `OIMChannelService`, `OIMComputedService`, `OIMDefaults`, `OIMSlotCollectionService`, `TOIMBucketSlot`, `TOIMComputedStoreSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEntitySlot`, `TOIMKeyedSlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
@@ -3397,10 +3286,10 @@ Every constructor in the family takes ONE options object.
|
|
|
3397
3286
|
| package | exports |
|
|
3398
3287
|
|---|---|
|
|
3399
3288
|
| `@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`) |
|
|
3400
|
-
| `@oimdb/persist-memory` | `OIMPersistMemoryBackend`, `OIMPersistMemoryWriteBatch`, `
|
|
3401
|
-
| `@oimdb/persist-json` | `
|
|
3402
|
-
| `@oimdb/persist-idb` | `OIMPersistIndexedDbBackend`, `OIMPersistIndexedDbWriteBatch`, `
|
|
3403
|
-
| `@oimdb/persist-localstorage` | `
|
|
3289
|
+
| `@oimdb/persist-memory` | `OIMPersistMemoryBackend`, `OIMPersistMemoryWriteBatch`, `OIMPersistMemoryRecordsService`, `OIMPersistMemorySnapshotService`, `TOIMPersistMemoryServiceOptions` |
|
|
3290
|
+
| `@oimdb/persist-json` | `OIMPersistJsonMemoryBackend`, `TOIMPersistJsonMemoryBackendOptions`, `TOIMPersistJsonDump`; depends on `@oimdb/persist-memory` |
|
|
3291
|
+
| `@oimdb/persist-idb` | `OIMPersistIndexedDbBackend`, `OIMPersistIndexedDbWriteBatch`, `OIMPersistIndexedDbRecordsService`, `OIMPersistIndexedDbSnapshotService`, `TOIMPersistIndexedDbBackendOptions`, `TOIMPersistIndexedDbRecordsServiceOptions`, `TOIMPersistIndexedDbSnapshotServiceOptions`, `TOIMPersistIndexedDbPrimaryKey`, `TOIMPersistIndexedDbTableRecord` |
|
|
3292
|
+
| `@oimdb/persist-localstorage` | `OIMPersistLocalStorageKeyValueBackend`, `TOIMPersistLocalStorageKeyValueBackendOptions` |
|
|
3404
3293
|
| `@oimdb/persist-async-kv` | `OIMPersistAsyncKVStorage`, `TOIMPersistAsyncKVStorageOptions`, `TOIMPersistAsyncKVMultiStorage` |
|
|
3405
3294
|
|
|
3406
3295
|
### The wiring
|
|
@@ -3411,42 +3300,37 @@ lives in the backend) and what to do on hydrate.
|
|
|
3411
3300
|
|
|
3412
3301
|
```ts
|
|
3413
3302
|
import {
|
|
3414
|
-
OIMPersistEntityWriter,
|
|
3415
|
-
|
|
3303
|
+
OIMPersistEntityWriter, OIMPersistCollectionWriteRecordSource, OIMPersistRecordsResource,
|
|
3304
|
+
OIMPersistMicrotaskScheduler, OIMPersistor,
|
|
3416
3305
|
} from '@oimdb/persist';
|
|
3417
|
-
import { OIMPersistMemoryBackend,
|
|
3306
|
+
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3418
3307
|
|
|
3419
3308
|
const persistEntityWriter = new OIMPersistEntityWriter<Card, TOIMStoreKey>({
|
|
3420
3309
|
entityWriter: createOIMEntityWriterInPlace(),
|
|
3421
3310
|
});
|
|
3422
|
-
const persistedCardCollectionService =
|
|
3423
|
-
keyOf: card => card.id,
|
|
3424
|
-
entityWriter: persistEntityWriter,
|
|
3425
|
-
});
|
|
3311
|
+
const persistedCardCollectionService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db, { entityWriter: persistEntityWriter }) });
|
|
3426
3312
|
const persistedCardEntityStoreSlot = persistedCardCollectionService.createStoreSlot(db);
|
|
3427
3313
|
|
|
3428
3314
|
const persistMemoryBackend = new OIMPersistMemoryBackend();
|
|
3429
3315
|
const persistor = new OIMPersistor<OIMPersistMemoryBackend, OIMPersistMemoryWriteBatch>({
|
|
3430
3316
|
persistBackend: persistMemoryBackend,
|
|
3431
|
-
persistScheduler: new
|
|
3317
|
+
persistScheduler: new OIMPersistMicrotaskScheduler(),
|
|
3432
3318
|
onPersistError: (error, persistErrorContext) => {
|
|
3433
3319
|
console.error(persistErrorContext.persistOperation, error);
|
|
3434
3320
|
},
|
|
3435
3321
|
});
|
|
3436
|
-
const cardPersistResourceRecords = persistor.addPersistResource(new
|
|
3437
|
-
persistRecordSource: new
|
|
3322
|
+
const cardPersistResourceRecords = persistor.addPersistResource(new OIMPersistRecordsResource({
|
|
3323
|
+
persistRecordSource: new OIMPersistCollectionWriteRecordSource({
|
|
3438
3324
|
entityCollectionService: persistedCardCollectionService,
|
|
3439
3325
|
entityStoreSlot: persistedCardEntityStoreSlot,
|
|
3440
3326
|
persistEntityWriter,
|
|
3441
3327
|
}),
|
|
3442
|
-
|
|
3328
|
+
persistRecordsService: new OIMPersistMemoryRecordsService<TOIMStoreKey, Card>({ storageKey: 'cards' }),
|
|
3443
3329
|
}));
|
|
3444
3330
|
|
|
3445
3331
|
await persistor.hydrate();
|
|
3446
3332
|
persistor.start();
|
|
3447
|
-
persistedCardCollectionService.
|
|
3448
|
-
persistedCardEntityStoreSlot, { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' },
|
|
3449
|
-
);
|
|
3333
|
+
persistedCardCollectionService.writeSlotContentByKey(persistedCardEntityStoreSlot, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
|
|
3450
3334
|
await persistor.persist();
|
|
3451
3335
|
await persistor.unload(cardPersistResourceRecords, ['c1']);
|
|
3452
3336
|
await persistor.load(cardPersistResourceRecords, ['c1']);
|
|
@@ -3456,8 +3340,8 @@ Rules a generator must keep:
|
|
|
3456
3340
|
|
|
3457
3341
|
- Pass both generics of `OIMPersistor<TPersistBackend, TWriteBatch>`: the backend
|
|
3458
3342
|
class and its write batch class (`OIMPersistMemoryWriteBatch`, `OIMPersistIndexedDbWriteBatch`,
|
|
3459
|
-
`OIMPersistKeyValueWriteBatch`; `
|
|
3460
|
-
`
|
|
3343
|
+
`OIMPersistKeyValueWriteBatch`; `OIMPersistJsonMemoryBackend` uses `OIMPersistMemoryWriteBatch`,
|
|
3344
|
+
`OIMPersistLocalStorageKeyValueBackend` uses `OIMPersistKeyValueWriteBatch`).
|
|
3461
3345
|
- Pass the generics of strategies, codecs and `OIMPersistEntityWriter<TEntity, TKey>`
|
|
3462
3346
|
explicitly; their options carry no type to infer them from.
|
|
3463
3347
|
- An entity collection persisted through a collection source is built with the
|
|
@@ -3469,9 +3353,9 @@ Rules a generator must keep:
|
|
|
3469
3353
|
`new OIMPersistEntityWriter({ entityWriter })` wraps the `IOIMEntityWriter` that
|
|
3470
3354
|
really writes, and reports the entity slot of every fill, put, patch, column patch,
|
|
3471
3355
|
column delete and clear that changed something (`subscribeEntitySlotWritten`).
|
|
3472
|
-
- The entity collection service a source takes is any collection service with `getSlots`, `findSlotByKey
|
|
3473
|
-
`
|
|
3474
|
-
(`TOIMPersistEntityCollectionService`)
|
|
3356
|
+
- The entity collection service a source takes is any collection service with `getSlots`, `findSlotByKey`,
|
|
3357
|
+
`writeSlotContentByKey`, `writeInBatch` and a `slotContentService` with `clearSlotContent`
|
|
3358
|
+
(`TOIMPersistEntityCollectionService`): what `createOIMSlotCollectionService` over an entity content service builds.
|
|
3475
3359
|
- A source refuses, by name, a store slot on another channel slot than the one it was given (the bucket source
|
|
3476
3360
|
checks both the entity and the bucket store slot).
|
|
3477
3361
|
- Order: `addPersistResource` every resource, `await persistor.hydrate()`, then
|
|
@@ -3482,29 +3366,29 @@ Rules a generator must keep:
|
|
|
3482
3366
|
| constructor | options |
|
|
3483
3367
|
|---|---|
|
|
3484
3368
|
| `OIMPersistor` | `{ persistBackend, persistScheduler, onPersistError? }` |
|
|
3485
|
-
| `
|
|
3486
|
-
| `
|
|
3487
|
-
| `
|
|
3488
|
-
| `
|
|
3489
|
-
| `
|
|
3369
|
+
| `OIMPersistRecordsResource` | `{ persistRecordSource, persistRecordsService, persistHydrateReconcile? }` |
|
|
3370
|
+
| `OIMPersistSnapshotResource` | `{ persistSnapshotSource, persistSnapshotService, persistCodec, persistHydrateReconcile? }` |
|
|
3371
|
+
| `OIMPersistCollectionWriteRecordSource`, `OIMPersistCollectionChangeWriteRecordSource` | `{ entityCollectionService, entityStoreSlot, persistEntityWriter }` (`TOIMPersistRecordSourceCollectionOptions`) |
|
|
3372
|
+
| `OIMPersistBucketRecordSource` | `{ channelMap, entityCollectionService, entityStoreSlot, slotContentBucketService, bucketSlot }` — `channelMap` is the db, `slotContentBucketService` the bucket collection's `slotContentService` |
|
|
3373
|
+
| `OIMPersistSingleEntitySnapshotSource` | `{ channelMap, singleSlotService, entityStoreSlot }` — a single entity service (`createOIMSingleSlotService` over an entity content service) and one of its store slots (`TOIMSingleSlotStoreSlot`) |
|
|
3490
3374
|
| `OIMPersistEntityWriter` | `{ entityWriter }` |
|
|
3491
|
-
| `
|
|
3492
|
-
| `
|
|
3493
|
-
| `
|
|
3494
|
-
| `
|
|
3375
|
+
| `OIMPersistMicrotaskScheduler` | none |
|
|
3376
|
+
| `OIMPersistFrameScheduler` | `{ channelSlot }` — `db.defaultChannelSlot` |
|
|
3377
|
+
| `OIMPersistIdentityCodec` | none |
|
|
3378
|
+
| `OIMPersistVersionedCodec` | `{ version, migrations, persistCodec }` |
|
|
3495
3379
|
| `OIMPersistMemoryBackend` | none |
|
|
3496
|
-
| `
|
|
3497
|
-
| `
|
|
3380
|
+
| `OIMPersistMemoryRecordsService`, `OIMPersistMemorySnapshotService` | `{ storageKey }` |
|
|
3381
|
+
| `OIMPersistJsonMemoryBackend` | `{ initialPersistJsonDump? }`, the object itself optional |
|
|
3498
3382
|
| `OIMPersistIndexedDbBackend` | `{ databaseName, databaseVersion?, indexedDb? }` |
|
|
3499
|
-
| `
|
|
3500
|
-
| `
|
|
3383
|
+
| `OIMPersistIndexedDbRecordsService` | `{ tableName }` |
|
|
3384
|
+
| `OIMPersistIndexedDbSnapshotService` | `{ tableName, primaryKey }` |
|
|
3501
3385
|
| `OIMPersistKeyValueBackend` | `{ persistKeyValueStorage, persistKeyValueSerializer? }` |
|
|
3502
|
-
| `
|
|
3503
|
-
| `
|
|
3504
|
-
| `
|
|
3505
|
-
| `
|
|
3506
|
-
| `
|
|
3507
|
-
| `
|
|
3386
|
+
| `OIMPersistItemKeyValueStorage` | `{ persistKeyValueItemStorage }` |
|
|
3387
|
+
| `OIMPersistKeyValueEntrySnapshotService` | `{ storageKey }` |
|
|
3388
|
+
| `OIMPersistKeyValuePathSnapshotService` | `{ storageKey, path }` |
|
|
3389
|
+
| `OIMPersistLocalStorageKeyValueBackend` | `{ persistKeyValueItemStorage?, persistKeyValueSerializer? }`, the object itself optional |
|
|
3390
|
+
| `OIMPersistSlotSourceChangeMark` | `{ channelClock, slots }` |
|
|
3391
|
+
| `OIMPersistSubscriptionSourceChangeMark` | `{ subscribeSourceChanged }` |
|
|
3508
3392
|
| `OIMPersistAsyncKVStorage` | `{ persistAsyncKVMultiStorage }` |
|
|
3509
3393
|
|
|
3510
3394
|
`onPersistError(error, persistErrorContext)` receives a `TOIMPersistErrorContext`,
|
|
@@ -3515,10 +3399,10 @@ or collected into the `AggregateError`.
|
|
|
3515
3399
|
|
|
3516
3400
|
### Two resource kinds, and when to pick each
|
|
3517
3401
|
|
|
3518
|
-
| | `
|
|
3402
|
+
| | `OIMPersistRecordsResource` | `OIMPersistSnapshotResource` |
|
|
3519
3403
|
|---|---|---|
|
|
3520
3404
|
| source | a record source: entity store slot or bucket | any snapshot source: entity store slot, bucket or single entity |
|
|
3521
|
-
| strategy | `
|
|
3405
|
+
| strategy | `OIMPersistMemoryRecordsService`, `OIMPersistIndexedDbRecordsService` | `OIMPersistMemorySnapshotService`, `OIMPersistIndexedDbSnapshotService`, `OIMPersistKeyValueEntrySnapshotService`, `OIMPersistKeyValuePathSnapshotService` |
|
|
3522
3406
|
| 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 |
|
|
3523
3407
|
| `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 |
|
|
3524
3408
|
| codec | none | `persistCodec`, required |
|
|
@@ -3544,10 +3428,10 @@ slot wins: an upsert, not a deletion.
|
|
|
3544
3428
|
|
|
3545
3429
|
| source | reports | scheduler |
|
|
3546
3430
|
|---|---|---|
|
|
3547
|
-
| `
|
|
3548
|
-
| `
|
|
3549
|
-
| `
|
|
3550
|
-
| `
|
|
3431
|
+
| `OIMPersistCollectionWriteRecordSource` | synchronously, at the write, from `OIMPersistEntityWriter` only | `OIMPersistMicrotaskScheduler` |
|
|
3432
|
+
| `OIMPersistCollectionChangeWriteRecordSource` | synchronously, at the write, from `OIMPersistEntityWriter` and from a content-change listener on the entity store slot | `OIMPersistMicrotaskScheduler` |
|
|
3433
|
+
| `OIMPersistBucketRecordSource` | from effects (`OIMEffectService`), when the channel runs its frame | `OIMPersistFrameScheduler({ channelSlot })` |
|
|
3434
|
+
| `OIMPersistSingleEntitySnapshotSource` | from an effect, when the channel runs its frame | `OIMPersistFrameScheduler({ channelSlot })` |
|
|
3551
3435
|
|
|
3552
3436
|
- **Collection** (an entity collection service and one of its store slots): every entity slot of that store slot that holds an entity. Starting
|
|
3553
3437
|
reports nothing; content present before `start()` is written by `persist()` or by
|
|
@@ -3558,8 +3442,8 @@ slot wins: an upsert, not a deletion.
|
|
|
3558
3442
|
|
|
3559
3443
|
| collection source | in-place edit announced with `slotContentService.markEntityChanged(entitySlot, changedColumns?)` | cost while a resource listens | pick it when |
|
|
3560
3444
|
|---|---|---|---|
|
|
3561
|
-
| `
|
|
3562
|
-
| `
|
|
3445
|
+
| `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` |
|
|
3446
|
+
| `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 |
|
|
3563
3447
|
- **Bucket**: the entities one bucket slot holds. Get the slot with
|
|
3564
3448
|
`bucketCollectionService.holdSlotByKey(bucketStoreSlot, key)`. The source watches the
|
|
3565
3449
|
bucket slot with one effect and each member entity slot with one effect. An entity
|
|
@@ -3569,7 +3453,7 @@ slot wins: an upsert, not a deletion.
|
|
|
3569
3453
|
hydrate restored.
|
|
3570
3454
|
- **Single entity**: the snapshot is the entity itself (a store slot of a single entity service, read through
|
|
3571
3455
|
`findSingleSlot(entityStoreSlot)`), so it
|
|
3572
|
-
goes only into `
|
|
3456
|
+
goes only into `OIMPersistSnapshotResource`. Its first subscription reports the
|
|
3573
3457
|
current value unless it is the value a hydrate restored. `readSnapshot` throws when
|
|
3574
3458
|
the entity slot holds no value.
|
|
3575
3459
|
|
|
@@ -3582,38 +3466,36 @@ frame, are both written.
|
|
|
3582
3466
|
|
|
3583
3467
|
```ts
|
|
3584
3468
|
import {
|
|
3585
|
-
|
|
3469
|
+
OIMPersistBucketRecordSource, OIMPersistRecordsResource, OIMPersistFrameScheduler, OIMPersistor,
|
|
3586
3470
|
} from '@oimdb/persist';
|
|
3587
|
-
import { OIMPersistMemoryBackend,
|
|
3471
|
+
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3588
3472
|
|
|
3589
3473
|
type TDeckCardSlot = TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
3590
|
-
const deckCardCollectionService =
|
|
3474
|
+
const deckCardCollectionService = createOIMSlotCollectionService(db, { slotContentService: createOIMSlotContentEntityService<Card>(db) });
|
|
3591
3475
|
const deckCardEntityStoreSlot = deckCardCollectionService.createStoreSlot(db);
|
|
3592
|
-
const deckBucketService =
|
|
3476
|
+
const deckBucketService = createOIMSlotCollectionService(db, { slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
3593
3477
|
const deckBucketStoreSlot = deckBucketService.createStoreSlot(db);
|
|
3594
3478
|
const spadesBucketSlot = deckBucketService.holdSlotByKey(deckBucketStoreSlot, 'spades');
|
|
3595
3479
|
|
|
3596
3480
|
const deckPersistor = new OIMPersistor<OIMPersistMemoryBackend, OIMPersistMemoryWriteBatch>({
|
|
3597
3481
|
persistBackend: new OIMPersistMemoryBackend(),
|
|
3598
|
-
persistScheduler: new
|
|
3482
|
+
persistScheduler: new OIMPersistFrameScheduler({ channelSlot: db.defaultChannelSlot }),
|
|
3599
3483
|
});
|
|
3600
|
-
deckPersistor.addPersistResource(new
|
|
3601
|
-
persistRecordSource: new
|
|
3484
|
+
deckPersistor.addPersistResource(new OIMPersistRecordsResource({
|
|
3485
|
+
persistRecordSource: new OIMPersistBucketRecordSource({
|
|
3602
3486
|
channelMap: db,
|
|
3603
3487
|
entityCollectionService: deckCardCollectionService,
|
|
3604
3488
|
entityStoreSlot: deckCardEntityStoreSlot,
|
|
3605
3489
|
slotContentBucketService: deckBucketService.slotContentService,
|
|
3606
3490
|
bucketSlot: spadesBucketSlot,
|
|
3607
3491
|
}),
|
|
3608
|
-
|
|
3492
|
+
persistRecordsService: new OIMPersistMemoryRecordsService<TOIMStoreKey, Card>({ storageKey: 'spades' }),
|
|
3609
3493
|
}));
|
|
3610
3494
|
await deckPersistor.hydrate();
|
|
3611
3495
|
deckPersistor.start();
|
|
3612
3496
|
|
|
3613
|
-
const aceSlot = deckCardCollectionService.
|
|
3614
|
-
|
|
3615
|
-
);
|
|
3616
|
-
deckBucketService.slotContentService.writeBucketMemberSlots(spadesBucketSlot, [aceSlot]);
|
|
3497
|
+
const aceSlot = deckCardCollectionService.writeSlotContentByKey(deckCardEntityStoreSlot, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
|
|
3498
|
+
deckBucketService.slotContentService.writeSlotContent(spadesBucketSlot, [aceSlot]);
|
|
3617
3499
|
db.flush();
|
|
3618
3500
|
await deckPersistor.whenIdle();
|
|
3619
3501
|
```
|
|
@@ -3624,10 +3506,10 @@ await deckPersistor.whenIdle();
|
|
|
3624
3506
|
The persistor calls `attachDrain` from its constructor; both shipped schedulers throw
|
|
3625
3507
|
when a second persistor attaches, so construct one scheduler per persistor.
|
|
3626
3508
|
|
|
3627
|
-
- `
|
|
3509
|
+
- `OIMPersistMicrotaskScheduler`: `requestDrain()` queues one microtask; the
|
|
3628
3510
|
requests that follow before it runs are absorbed by it. `stop()` makes a queued
|
|
3629
3511
|
microtask do nothing.
|
|
3630
|
-
- `
|
|
3512
|
+
- `OIMPersistFrameScheduler({ channelSlot })`: while started it subscribes with
|
|
3631
3513
|
`OIMChannelService.INSTANCE.subscribeAfterEachFrame(channelSlot, …)` and drains after a frame only when a drain was
|
|
3632
3514
|
requested since the last one; `stop()` unsubscribes and forgets the request. It
|
|
3633
3515
|
holds no slot and never asks the channel for a frame: it drains only after a frame
|
|
@@ -3636,11 +3518,11 @@ when a second persistor attaches, so construct one scheduler per persistor.
|
|
|
3636
3518
|
**The pairing is a rule.** A write to an entity slot that nobody holds causes no frame
|
|
3637
3519
|
(`OIMChannelService.INSTANCE.writeWithoutFlushCheck` returns at once when `holdCount` is `0`), so a
|
|
3638
3520
|
collection source on the frame scheduler can report changes that no frame ever
|
|
3639
|
-
drains. Pair `
|
|
3640
|
-
`
|
|
3641
|
-
`
|
|
3521
|
+
drains. Pair `OIMPersistCollectionWriteRecordSource` and
|
|
3522
|
+
`OIMPersistCollectionChangeWriteRecordSource` with
|
|
3523
|
+
`OIMPersistMicrotaskScheduler`. The bucket and single-entity sources report from
|
|
3642
3524
|
effects, which cause the frames they run in, so pair them with
|
|
3643
|
-
`
|
|
3525
|
+
`OIMPersistFrameScheduler`. Resources of both groups in one persistor share its
|
|
3644
3526
|
one scheduler; give them two persistors when they need different schedulers.
|
|
3645
3527
|
|
|
3646
3528
|
`whenIdle()` resolves when the work queued at the moment of the call has settled. On
|
|
@@ -3660,8 +3542,8 @@ is queued on the microtask after the write: let one microtask pass before callin
|
|
|
3660
3542
|
| `hydrate()` | reads every resource and writes it into its source; ends the upsert-only mode of an unloaded resource |
|
|
3661
3543
|
| `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 |
|
|
3662
3544
|
| `clearPersisted()` | deletes every resource's stored data in one write batch; memory is untouched; ends the upsert-only mode of an unloaded resource |
|
|
3663
|
-
| `unload(
|
|
3664
|
-
| `load(
|
|
3545
|
+
| `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 |
|
|
3546
|
+
| `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 |
|
|
3665
3547
|
| `whenIdle()` | resolves when the queued work settles; never rejects |
|
|
3666
3548
|
|
|
3667
3549
|
Every promise-returning call and every autosave runs on one ordered chain, one at a
|
|
@@ -3712,7 +3594,7 @@ What the source does with it:
|
|
|
3712
3594
|
name; the store slot becomes exactly the stored content;
|
|
3713
3595
|
- bucket: upserts every record into the entity store slot and sets the bucket's
|
|
3714
3596
|
members to exactly the restored entities;
|
|
3715
|
-
- single entity: `singleSlotService.
|
|
3597
|
+
- single entity: `singleSlotService.writeSingleSlotContent(entityStoreSlot, entity)`.
|
|
3716
3598
|
|
|
3717
3599
|
**Refusal.** If the source changes while the backend read is in flight and the
|
|
3718
3600
|
resource has no `persistHydrateReconcile`, hydrate throws instead of applying, because
|
|
@@ -3720,10 +3602,10 @@ applying would revert that change. The source keeps its newer state; the error g
|
|
|
3720
3602
|
to `onPersistError` with `'hydrate'`, or into the `AggregateError`. Before the read
|
|
3721
3603
|
the resource calls `persistSnapshotSource.markSourceChanges()`, which returns an
|
|
3722
3604
|
`IOIMPersistSourceChangeMark` (`isSourceChangedSinceMark()`, `dispose()`): the
|
|
3723
|
-
single-entity and bucket sources return an `
|
|
3605
|
+
single-entity and bucket sources return an `OIMPersistSlotSourceChangeMark`
|
|
3724
3606
|
that holds the slots and compares their revision stamps (`changedAtRevision`, `changedAtEpoch`) with the
|
|
3725
3607
|
channel clock reading taken at the mark (`oimIsAnySlotChangedAfter`), the
|
|
3726
|
-
collection sources an `
|
|
3608
|
+
collection sources an `OIMPersistSubscriptionSourceChangeMark` whose flag their
|
|
3727
3609
|
own report sets. A change made during the read is seen even when no frame runs, and
|
|
3728
3610
|
the guard runs whether or not the persistor is started.
|
|
3729
3611
|
|
|
@@ -3747,7 +3629,7 @@ const answerReconcile = oimReconcileByKey<TOIMStoreKey, Card>(
|
|
|
3747
3629
|
|
|
3748
3630
|
### unload, load and clearPersisted
|
|
3749
3631
|
|
|
3750
|
-
`persistor.unload(
|
|
3632
|
+
`persistor.unload(persistRecordsResource, keys)` takes an `OIMPersistRecordsResource`
|
|
3751
3633
|
only. It first drains the persistor's dirty resources onto the chain, then, in its
|
|
3752
3634
|
own turn, writes in one write batch that resource's pending change and the current
|
|
3753
3635
|
value of every unloaded key (`persistRecordSource.readRecordsDelta(persistRecordSource.findEntitySlotsByKeys(keys))`), so
|
|
@@ -3775,11 +3657,11 @@ stay on the backend. A deletion it never heard is not written in that mode.
|
|
|
3775
3657
|
`hydrate()` and `clearPersisted()` end that mode, and so does a `load` of every
|
|
3776
3658
|
unloaded key.
|
|
3777
3659
|
|
|
3778
|
-
`persistor.load(
|
|
3779
|
-
`
|
|
3660
|
+
`persistor.load(persistRecordsResource, keys)` is the pair of `unload` and takes an
|
|
3661
|
+
`OIMPersistRecordsResource` only (a key-value backend has no records strategy, so a
|
|
3780
3662
|
snapshot resource has no `load`). It drains the dirty resources onto the chain and
|
|
3781
3663
|
runs after those writes. In its turn it reads only those keys through
|
|
3782
|
-
`
|
|
3664
|
+
`persistRecordsService.readRecordsOfKeys(persistBackend, keys)`: on IndexedDB one
|
|
3783
3665
|
`readonly` transaction with one `get` per key, on memory a lookup of each key in the
|
|
3784
3666
|
stored map; a key the backend does not hold is left out. It skips every key that holds
|
|
3785
3667
|
a value in memory or has a change the resource has not written yet (pending or in a
|
|
@@ -3819,10 +3701,10 @@ required `persistCodec`, an `IOIMPersistCodec<TSourceSnapshot, TPersistedSnapsho
|
|
|
3819
3701
|
(`encode`, `decode`). A records resource has no codec: it stores entities as the
|
|
3820
3702
|
source holds them.
|
|
3821
3703
|
|
|
3822
|
-
- `new
|
|
3823
|
-
- `new
|
|
3704
|
+
- `new OIMPersistIdentityCodec<TSnapshot>()`: passes the snapshot through.
|
|
3705
|
+
- `new OIMPersistVersionedCodec<TSourceSnapshot, TPersistedSnapshot>({ version, migrations, persistCodec })`:
|
|
3824
3706
|
stores a `TOIMPersistVersionedShape`, `{ version, data }`, where `data` is the
|
|
3825
|
-
inner `persistCodec`'s encoding (required; use `
|
|
3707
|
+
inner `persistCodec`'s encoding (required; use `OIMPersistIdentityCodec` when
|
|
3826
3708
|
nothing transforms). `migrations` is a `Record<number, (data: unknown) => unknown>`
|
|
3827
3709
|
keyed by the version each migration produces. On decode it runs, ascending, every
|
|
3828
3710
|
migration whose key is above the stored `version` and at most the current one,
|
|
@@ -3832,21 +3714,21 @@ source holds them.
|
|
|
3832
3714
|
|
|
3833
3715
|
```ts
|
|
3834
3716
|
import {
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3717
|
+
OIMPersistIdentityCodec, OIMPersistVersionedCodec, OIMPersistKeyValueBackend, OIMPersistKeyValueEntrySnapshotService,
|
|
3718
|
+
OIMPersistItemKeyValueStorage, OIMPersistKeyValueWriteBatch, OIMPersistSnapshotResource,
|
|
3719
|
+
OIMPersistFrameScheduler, OIMPersistSingleEntitySnapshotSource, OIMPersistor,
|
|
3838
3720
|
} from '@oimdb/persist';
|
|
3839
3721
|
import type { TOIMPersistVersionedShape } from '@oimdb/persist';
|
|
3840
3722
|
|
|
3841
3723
|
type TSettings = { theme: 'light' | 'dark'; density: 'compact' | 'comfortable' };
|
|
3842
3724
|
|
|
3843
|
-
const settingsSingleEntityService =
|
|
3725
|
+
const settingsSingleEntityService = createOIMSingleSlotService(db, { slotContentService: createOIMSlotContentEntityService<TSettings>(db) });
|
|
3844
3726
|
const settingsEntityStoreSlot = settingsSingleEntityService.createStoreSlot(db);
|
|
3845
|
-
settingsSingleEntityService.
|
|
3727
|
+
settingsSingleEntityService.writeSingleSlotContent(settingsEntityStoreSlot, { theme: 'light', density: 'compact' });
|
|
3846
3728
|
const itemByStorageKey = new Map<string, string>();
|
|
3847
3729
|
const settingsPersistor = new OIMPersistor<OIMPersistKeyValueBackend, OIMPersistKeyValueWriteBatch>({
|
|
3848
3730
|
persistBackend: new OIMPersistKeyValueBackend({
|
|
3849
|
-
persistKeyValueStorage: new
|
|
3731
|
+
persistKeyValueStorage: new OIMPersistItemKeyValueStorage({
|
|
3850
3732
|
persistKeyValueItemStorage: {
|
|
3851
3733
|
getItem: storageKey => itemByStorageKey.get(storageKey) ?? null,
|
|
3852
3734
|
setItem: (storageKey, item) => { itemByStorageKey.set(storageKey, item); },
|
|
@@ -3854,17 +3736,17 @@ const settingsPersistor = new OIMPersistor<OIMPersistKeyValueBackend, OIMPersist
|
|
|
3854
3736
|
},
|
|
3855
3737
|
}),
|
|
3856
3738
|
}),
|
|
3857
|
-
persistScheduler: new
|
|
3739
|
+
persistScheduler: new OIMPersistFrameScheduler({ channelSlot: db.defaultChannelSlot }),
|
|
3858
3740
|
});
|
|
3859
|
-
settingsPersistor.addPersistResource(new
|
|
3860
|
-
persistSnapshotSource: new
|
|
3741
|
+
settingsPersistor.addPersistResource(new OIMPersistSnapshotResource({
|
|
3742
|
+
persistSnapshotSource: new OIMPersistSingleEntitySnapshotSource({
|
|
3861
3743
|
channelMap: db, singleSlotService: settingsSingleEntityService, entityStoreSlot: settingsEntityStoreSlot,
|
|
3862
3744
|
}),
|
|
3863
|
-
|
|
3864
|
-
persistCodec: new
|
|
3745
|
+
persistSnapshotService: new OIMPersistKeyValueEntrySnapshotService<TOIMPersistVersionedShape>({ storageKey: 'settings' }),
|
|
3746
|
+
persistCodec: new OIMPersistVersionedCodec<TSettings, TSettings>({
|
|
3865
3747
|
version: 2,
|
|
3866
3748
|
migrations: { 2: data => ({ density: 'comfortable', ...(data as object) }) },
|
|
3867
|
-
persistCodec: new
|
|
3749
|
+
persistCodec: new OIMPersistIdentityCodec<TSettings>(),
|
|
3868
3750
|
}),
|
|
3869
3751
|
}));
|
|
3870
3752
|
await settingsPersistor.hydrate();
|
|
@@ -3881,34 +3763,34 @@ await settingsPersistor.whenIdle();
|
|
|
3881
3763
|
|
|
3882
3764
|
**Memory** (`@oimdb/persist-memory`). `new OIMPersistMemoryBackend()`. Snapshot values
|
|
3883
3765
|
and records live in two separate maps, so a snapshot and a records resource may share
|
|
3884
|
-
a `storageKey`. `
|
|
3766
|
+
a `storageKey`. `OIMPersistMemoryRecordsService<TKey, TEntity>({ storageKey })` keeps one
|
|
3885
3767
|
`Map` of key to entity per `storageKey`: autosave sets and deletes keys, `persist()`
|
|
3886
|
-
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`. `
|
|
3768
|
+
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 })`
|
|
3887
3769
|
keeps one value. `readRecords(storageKey)` and `readSnapshotValue(storageKey)` read
|
|
3888
3770
|
what was committed. `OIMPersistMemoryWriteBatch` queues every change and applies them in
|
|
3889
3771
|
order on `commit()`.
|
|
3890
3772
|
|
|
3891
3773
|
**JSON** (`@oimdb/persist-json`, depends on `@oimdb/persist-memory`).
|
|
3892
|
-
`
|
|
3774
|
+
`OIMPersistJsonMemoryBackend` extends `OIMPersistMemoryBackend` and takes the memory
|
|
3893
3775
|
strategies. `dehydrate()` returns a `TOIMPersistJsonDump`,
|
|
3894
3776
|
`{ snapshotValues: Record<storageKey, value>, recordsByStorageKey: Record<storageKey, { key, entity }[]> }`;
|
|
3895
|
-
`new
|
|
3777
|
+
`new OIMPersistJsonMemoryBackend({ initialPersistJsonDump })` writes such a dump straight into
|
|
3896
3778
|
its maps. The SSR flow: on the server, `await persistor.persist()` and then
|
|
3897
|
-
`JSON.stringify(
|
|
3898
|
-
`new
|
|
3779
|
+
`JSON.stringify(persistJsonMemoryBackend.dehydrate())`; on the client,
|
|
3780
|
+
`new OIMPersistJsonMemoryBackend({ initialPersistJsonDump: JSON.parse(jsonText) })`, register
|
|
3899
3781
|
the same resources under the same `storageKey`s, `await persistor.hydrate()`. To merge
|
|
3900
3782
|
a durable cache over the server's content, hydrate the JSON persistor first, then the
|
|
3901
3783
|
durable persistor with `persistHydrateReconcile`, then `start()` the durable one.
|
|
3902
3784
|
|
|
3903
3785
|
**IndexedDB** (`@oimdb/persist-idb`). `new OIMPersistIndexedDbBackend({ databaseName, databaseVersion?, indexedDb? })`;
|
|
3904
3786
|
`indexedDb` defaults to `globalThis.indexedDB` and the constructor throws without
|
|
3905
|
-
either. `
|
|
3787
|
+
either. `OIMPersistIndexedDbRecordsService<TKey, TEntity>({ tableName })` stores one value
|
|
3906
3788
|
per entity key in an object store: autosave puts and deletes per key, `persist()`
|
|
3907
3789
|
clears the table and puts every entity (while the resource has unloaded keys, only
|
|
3908
3790
|
puts plus a delete per deletion the resource already heard), `clearPersisted()` clears it,
|
|
3909
3791
|
and a `load` reads its keys through `readRecordsOfKeys` in one `readonly` transaction,
|
|
3910
3792
|
one `get` per key.
|
|
3911
|
-
`
|
|
3793
|
+
`OIMPersistIndexedDbSnapshotService<TSnapshot>({ tableName, primaryKey })` stores one value
|
|
3912
3794
|
at `primaryKey`. One commit is one `readwrite` transaction over every table the batch
|
|
3913
3795
|
touches. A commit opens the database and creates missing tables by a version upgrade
|
|
3914
3796
|
(retried when another connection upgrades first, up to five attempts; an upgrade
|
|
@@ -3920,7 +3802,7 @@ sorted property names, and is read back as that string.
|
|
|
3920
3802
|
**Key-value** (`@oimdb/persist`). `new OIMPersistKeyValueBackend({ persistKeyValueStorage, persistKeyValueSerializer? })`
|
|
3921
3803
|
stores serialized strings under string keys. `persistKeyValueStorage` is an
|
|
3922
3804
|
`IOIMPersistKeyValueStorage` (`readItems(storageKeys)`, `writeItems(itemByStorageKey)`,
|
|
3923
|
-
`removeItems(storageKeys)`); `
|
|
3805
|
+
`removeItems(storageKeys)`); `OIMPersistItemKeyValueStorage({ persistKeyValueItemStorage })`
|
|
3924
3806
|
adapts any `TOIMPersistKeyValueItemStorage` (`getItem`, `setItem`, `removeItem`,
|
|
3925
3807
|
synchronous or returning promises) with one call per key. The serializer is a
|
|
3926
3808
|
`TOIMPersistKeyValueSerializer` (`serialize`, `deserialize`) and defaults to
|
|
@@ -3928,15 +3810,15 @@ synchronous or returning promises) with one call per key. The serializer is a
|
|
|
3928
3810
|
|
|
3929
3811
|
| strategy | stored as |
|
|
3930
3812
|
|---|---|
|
|
3931
|
-
| `
|
|
3932
|
-
| `
|
|
3813
|
+
| `OIMPersistKeyValueEntrySnapshotService<TSnapshot>({ storageKey })` | the whole snapshot under `storageKey` |
|
|
3814
|
+
| `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` |
|
|
3933
3815
|
|
|
3934
3816
|
`OIMPersistKeyValueWriteBatch` commits with at most one `readItems` (keys with path
|
|
3935
3817
|
writes or path deletions and no whole-value write in the batch), one `writeItems` and
|
|
3936
3818
|
one `removeItems`; it serializes every value before writing any. Path segments are
|
|
3937
3819
|
own properties, so a stored `__proto__` is data.
|
|
3938
3820
|
|
|
3939
|
-
- **localStorage** (`@oimdb/persist-localstorage`): `
|
|
3821
|
+
- **localStorage** (`@oimdb/persist-localstorage`): `OIMPersistLocalStorageKeyValueBackend`
|
|
3940
3822
|
extends `OIMPersistKeyValueBackend` over a `TOIMPersistKeyValueItemStorage`
|
|
3941
3823
|
(`getItem`, `setItem`, `removeItem`) given as `persistKeyValueItemStorage`, which
|
|
3942
3824
|
defaults to `globalThis.localStorage`; the constructor throws without either. One `setItem` per written key.
|
|
@@ -3955,10 +3837,10 @@ own properties, so a stored `__proto__` is data.
|
|
|
3955
3837
|
batch keeps them in: `addWriteBatchChange(change)`, `hasWriteBatchChanges`,
|
|
3956
3838
|
`applyWriteBatchChanges(target)`. `oimWritePersistRecordsDelta(delta, putRecord, deleteRecord)`
|
|
3957
3839
|
walks a `TOIMPersistRecordsDelta` in the one order every backend uses: upserts, then deletions.
|
|
3958
|
-
- Snapshot strategy: `
|
|
3840
|
+
- Snapshot strategy: `IOIMPersistSnapshotService<TPersistBackend, TWriteBatch, TSnapshot>`
|
|
3959
3841
|
with `readSnapshot(persistBackend)`, `writeSnapshot(writeBatch, snapshot)`,
|
|
3960
3842
|
`deleteSnapshot(writeBatch)`. Records strategy:
|
|
3961
|
-
`
|
|
3843
|
+
`IOIMPersistRecordsService<TPersistBackend, TWriteBatch, TKey, TEntity>` with
|
|
3962
3844
|
`readRecords(persistBackend)`, `readRecordsOfKeys(persistBackend, keys)`,
|
|
3963
3845
|
`writeRecords(writeBatch, persistRecords)`,
|
|
3964
3846
|
`writeRecordsDelta(writeBatch, persistRecordsDelta)` (a `TOIMPersistRecordsDelta`,
|
|
@@ -3972,15 +3854,15 @@ own properties, so a stored `__proto__` is data.
|
|
|
3972
3854
|
`loadRecords(persistRecords)`, `unloadKeys(keys)`.
|
|
3973
3855
|
`markSourceChanges()` is required and returns an `IOIMPersistSourceChangeMark`
|
|
3974
3856
|
(`isSourceChangedSinceMark()`, `dispose()`); build it with
|
|
3975
|
-
`new
|
|
3976
|
-
`new
|
|
3857
|
+
`new OIMPersistSlotSourceChangeMark({ channelClock, slots })` or
|
|
3858
|
+
`new OIMPersistSubscriptionSourceChangeMark({ subscribeSourceChanged })`.
|
|
3977
3859
|
It must accept more than one subscription, each with its own `TOIMDisposer`.
|
|
3978
3860
|
- Scheduler: `IOIMPersistScheduler`, above.
|
|
3979
3861
|
|
|
3980
3862
|
### Limits
|
|
3981
3863
|
|
|
3982
3864
|
- **No key-value records strategy.** Over `OIMPersistKeyValueBackend` (localStorage,
|
|
3983
|
-
async KV) an entity store slot or a bucket goes through `
|
|
3865
|
+
async KV) an entity store slot or a bucket goes through `OIMPersistSnapshotResource`: every
|
|
3984
3866
|
autosave walks every entity slot of the source and serializes the whole snapshot
|
|
3985
3867
|
under one key.
|
|
3986
3868
|
- **A bucket membership change walks the bucket.** Each change of the bucket slot
|
|
@@ -3990,7 +3872,7 @@ own properties, so a stored `__proto__` is data.
|
|
|
3990
3872
|
`createOIMEntityWriterInPlace()` the stored entity is the live entity object, so an
|
|
3991
3873
|
in-place write moves the stored value before any commit, and `dehydrate()` returns
|
|
3992
3874
|
the live objects. Use `createOIMEntityWriterReplacing()` with
|
|
3993
|
-
`
|
|
3875
|
+
`entityInserter: oimInsertEntityWithCopy` where the stored value must stay
|
|
3994
3876
|
what was committed, and serialize a dump at once.
|
|
3995
3877
|
- **Hydrate without a reconcile replaces.** The collection source deletes every
|
|
3996
3878
|
entity the stored records do not name, and an existing but empty records table
|
|
@@ -4079,7 +3961,7 @@ Who decided the order is the difference, and the core already forks on it.
|
|
|
4079
3961
|
|
|
4080
3962
|
| | `exoDerivedList(channelMap, computedSlot, render, maxCommandsPerSlot?)` | `exoPositionalList(channelMap, slotContentBucketService, bucketSlot, render)` |
|
|
4081
3963
|
|---|---|---|
|
|
4082
|
-
| takes | a `TOIMComputedSlot<readonly TOIMEntitySlot[]>` | a bucket content service built with an `OIMBucketCommands`
|
|
3964
|
+
| 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 |
|
|
4083
3965
|
| the script | **derived**, by `oimOrderedListCommandsOfTransition` | **recorded**, by `OIMBucketCommands`, read inside the frame |
|
|
4084
3966
|
| per changed frame | one diff: two passes plus an LIS pass, `common − LIS` moves | one pass over the frame's own script |
|
|
4085
3967
|
|
|
@@ -4130,8 +4012,8 @@ them.
|
|
|
4130
4012
|
|
|
4131
4013
|
The one quantity that needs no measurement, because it is arithmetic rather than
|
|
4132
4014
|
a reading: emptying a bucket one slot at a time on an array-driven bucket costs
|
|
4133
|
-
n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `
|
|
4134
|
-
and `
|
|
4015
|
+
n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `writeSlotContent`
|
|
4016
|
+
and `clearSlotContent` go through one `drainSlots`. Counted, not timed, in
|
|
4135
4017
|
`packages/core/src/engine/bucket/array/tests/everyone-out-at-once-and-not-one-at-a-time.test.ts`, which
|
|
4136
4018
|
also pins `drainSlots` at zero element writes.
|
|
4137
4019
|
|