@oimdb/core 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +164 -161
- package/dist/index.cjs +926 -722
- package/dist/index.d.cts +206 -137
- package/dist/index.d.ts +206 -137
- package/dist/index.js +916 -716
- package/llms.txt +314 -275
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
> `getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`, `findSingleSlot(storeSlot)`,
|
|
26
26
|
> `clearSingleSlotContent(storeSlot)`. No key is `void` or `undefined`: `TKey extends NonNullable<unknown>`.
|
|
27
27
|
>
|
|
28
|
-
> Every collection is `createOIMSlotCollectionService(
|
|
29
|
-
> `createOIMSingleSlotService(
|
|
30
|
-
> `createOIMSlotContentEntityService<TEntity>(
|
|
28
|
+
> Every collection is `createOIMSlotCollectionService({ slotContentService })` and every single is
|
|
29
|
+
> `createOIMSingleSlotService({ slotContentService })`. The content service decides what the slots hold:
|
|
30
|
+
> `createOIMSlotContentEntityService<TEntity>(…)` for entities, `new OIMSlotContentSetBucketService()`,
|
|
31
31
|
> `new OIMSlotContentPositionalArrayBucketService()` or `new OIMSlotContentSortedArrayBucketService({ … })` for
|
|
32
|
-
> buckets. An index is `createOIMSlotCollectionIndexService(
|
|
32
|
+
> buckets. An index is `createOIMSlotCollectionIndexService({ entityService, keyOf | columns, slotContentService? })`;
|
|
33
33
|
> its `keyOf` is the index's key of an entity, not the collection's. An array key needs a tree store:
|
|
34
34
|
> `slotStoreService` on the collection, `bucketSlotStoreService` on the index.
|
|
35
35
|
> Position reads and writes (`getBucketMemberSlotAt`, `getBucketMemberSlotPosition`, `insertBucketMemberSlotAt`,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
> (`createOIMSlotCollectionSortedArrayIndexService`); on every other bucket its order is the caller's. A sorted list
|
|
40
40
|
> over any index is `createOIMSortedComputedService`. The render script is `OIMBucketCommands`.
|
|
41
41
|
> The channel has no class: its operations are on `OIMChannelService.INSTANCE` and take a channel slot.
|
|
42
|
-
> Effects are `OIMEffectService.INSTANCE.createEffect(db,
|
|
42
|
+
> Effects are `OIMEffectService.INSTANCE.createEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`.
|
|
43
43
|
> Every stateless singleton is a static `INSTANCE` on its class. Module-level defaults are on `OIMDefaults`.
|
|
44
44
|
>
|
|
45
45
|
> Check every symbol you write against [the full export list](#full-export-list).
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
## Contents
|
|
52
52
|
|
|
53
|
-
- [The
|
|
53
|
+
- [The domains](#the-domains)
|
|
54
54
|
- [The doctrine](#the-doctrine) — the four doors, the injected seams, the idle windows, the collectors, forks
|
|
55
55
|
- [The runtime: the channel slot](#the-runtime-the-channel-slot)
|
|
56
56
|
- [`OIMDB` and the three doors](#oimdb-and-the-three-doors)
|
|
@@ -81,10 +81,10 @@ block and the one under [One derivation per key](#one-derivation-per-key). Block
|
|
|
81
81
|
marked `ts signature` are listings of an API and are not compiled.
|
|
82
82
|
|
|
83
83
|
```ts setup
|
|
84
|
-
import { OIMChannelService,
|
|
84
|
+
import { OIMChannelService, OIMSlotContentChangeListenerService, OIMDB, OIMDefaults, OIMEffectService, OIMSlotContentPositionalArrayBucketService, OIMSlotContentSetBucketService, OIMSlotMapStoreService, OIMSingleSlotStoreService, OIMSlotTreeStoreService, OIMUnusedSlotCollectorService, createOIMBucketMemoComputedService, createOIMComputedService, createOIMSortedComputedService, createOIMInPlaceEntityWriter, createOIMReplacingEntityWriter, createOIMSlotCollectionIndexService, createOIMSlotCollectionSortedArrayIndexService, createOIMSlotCollectionService, createOIMSlotContentEntityService, createOIMSingleSlotService, createOIMSingleComputedService, createOIMUnusedSlotCollectorSlot, oimIsSlotEmpty, oimIsSlotReleased, oimPatchEntityColumn, oimPatchEntityColumns, oimPutEntityColumns, oimSameValueZeroGuard, OIMBucketCommands, OIMSlotEarliestReleasePolicy, OIMSlotAfterWriteReleasePolicy, OIMSlotAfterIdleReleasePolicy, OIMThrowAtOnceErrorPolicy, type TOIMSlotStoreTree, OIMScalarKeyTreeWalk, OIMCompositeKeyTreeWalk } from '@oimdb/core';
|
|
85
85
|
import type {
|
|
86
86
|
IOIMEntityWriter,
|
|
87
|
-
|
|
87
|
+
TOIMEditable,
|
|
88
88
|
TOIMBucketSlotsArrayStore,
|
|
89
89
|
TOIMBucketSlot,
|
|
90
90
|
TOIMBucketSlotsSetStore,
|
|
@@ -111,15 +111,15 @@ declare const payload: readonly Card[];
|
|
|
111
111
|
const db = new OIMDB();
|
|
112
112
|
const channelSlot = db.defaultChannelSlot;
|
|
113
113
|
const effectService = OIMEffectService.INSTANCE;
|
|
114
|
-
const cardService = createOIMSlotCollectionService(
|
|
114
|
+
const cardService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
|
|
115
115
|
const cards = cardService.createStoreSlot(db);
|
|
116
116
|
const slot = cardService.writeSlotContentByKey(cards, 'c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' });
|
|
117
117
|
|
|
118
|
-
const orderedService = createOIMSlotCollectionService(
|
|
118
|
+
const orderedService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>() });
|
|
119
119
|
const ordered = orderedService.createStoreSlot(db);
|
|
120
120
|
const orderedBucketSlot = orderedService.holdSlotByKey(ordered, 'top');
|
|
121
121
|
|
|
122
|
-
const pageService = createOIMSlotCollectionService(
|
|
122
|
+
const pageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
123
123
|
const page = pageService.createStoreSlot(db);
|
|
124
124
|
const todo = pageService.holdSlotByKey(page, 'todo');
|
|
125
125
|
const done = pageService.holdSlotByKey(page, 'done');
|
|
@@ -127,19 +127,27 @@ const done = pageService.holdSlotByKey(page, 'done');
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
## The
|
|
130
|
+
## The domains
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
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` |
|
|
132
|
+
`src/` holds one folder per domain and `index.ts`, the entire public surface; a factory lives in
|
|
133
|
+
the domain of what it builds. Where a file goes is in `packages/core/src/README.md`.
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
`
|
|
141
|
-
`
|
|
142
|
-
|
|
135
|
+
| Domain | Contents |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `slot/` | `OIMSlotOwner`, `OIMSlotOwnerService` (type-only export), `OIMSlotCollectionService`, `OIMWritableSlotCollectionService`, `OIMSingleSlotService`, `OIMWritableSingleSlotService`, `OIMSlotContentChangeListenerService`, `createOIMSlotCollectionService`, `createOIMSingleSlotService`, `TOIMSlot`, `TOIMEditable` |
|
|
138
|
+
| `store/` | `TOIMStoreSlot`, the slot store services |
|
|
139
|
+
| `channel/` | `OIMChannelService`, `TOIMChannelSlot` |
|
|
140
|
+
| `effect/` | `OIMEffectService`, `TOIMEffect` |
|
|
141
|
+
| `computed/` | `OIMComputedValueService`, `OIMComputedService`, `OIMSingleComputedService`, `OIMSortedComputedService`, `OIMBucketMemoComputedService`, the source-tracking computed services, their factories, `TOIMComputedSlot` |
|
|
142
|
+
| `entity/` | `OIMSlotContentEntityService`, `createOIMSlotContentEntityService`, `createOIMInPlaceEntityWriter`, `createOIMReplacingEntityWriter` |
|
|
143
|
+
| `column/` | abstract `OIMColumnSlotOwner` with `OIMPushColumnSlotOwner` and `OIMPullColumnSlotOwner`, `OIMColumnSlotService` |
|
|
144
|
+
| `bucket/` | `OIMSlotContentBucketService` (abstract), `OIMSlotContentSetBucketService`, `OIMSlotContentArrayBucketService` (abstract), `OIMSlotContentPositionalArrayBucketService`, `OIMSlotContentSortedArrayBucketService`, `OIMBucketCommands` |
|
|
145
|
+
| `index/` | `OIMSlotCollectionIndexService`, `OIMSlotCollectionSortedArrayIndexService`, their factories, the index keys services |
|
|
146
|
+
| `key/` | the key tree walks |
|
|
147
|
+
| `collector/` | the release policies, `OIMSlotReleaseWheel`, `OIMSlotRefetchWheel`, `OIMUnusedSlotCollectorService` |
|
|
148
|
+
| `schedule/` | `OIMFlushSchedulerService` |
|
|
149
|
+
| `error/` | the error policies |
|
|
150
|
+
| `db/` | `OIMDB`, `OIMDefaults`, `OIMDefaultValue`, the default constants |
|
|
143
151
|
|
|
144
152
|
**A service holds no data; state lives on slots.** A collection service is built once per collection type
|
|
145
153
|
and holds only configuration and other services: its slot owner, its store service and its content
|
|
@@ -167,7 +175,7 @@ the caller who just got it.
|
|
|
167
175
|
|
|
168
176
|
```ts
|
|
169
177
|
cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'Two', rank: 2, deck: 'spades' }); // write
|
|
170
|
-
effectService.createEffect(db,
|
|
178
|
+
effectService.createEffect(db, () => { paint(slot.entity); }, [slot]); // subscribe
|
|
171
179
|
cardService.holdSlotByKey(cards, 'c1'); // hold, by key -> the slot, holdCount up by one
|
|
172
180
|
const rankColumn = cardService.columnSlotService.holdColumn(slot, 'rank'); // hold, on a column
|
|
173
181
|
cardService.columnSlotService.unholdColumn(rankColumn); // give the column back
|
|
@@ -247,9 +255,9 @@ interface IOIMSlotContentWriter<TSlot, TContent> {
|
|
|
247
255
|
writeSlotContent(slot: TSlot, content: TContent): boolean;
|
|
248
256
|
clearSlotContent(slot: TSlot): void;
|
|
249
257
|
}
|
|
250
|
-
interface IOIMSlotContentService<TSlot
|
|
258
|
+
interface IOIMSlotContentService<TSlot> {
|
|
251
259
|
isSlotEmpty(slot: TSlot): boolean;
|
|
252
|
-
releaseSlotContent(slot: TSlot
|
|
260
|
+
releaseSlotContent(slot: TSlot): void;
|
|
253
261
|
hasReference(slot: TSlot): boolean;
|
|
254
262
|
}
|
|
255
263
|
```
|
|
@@ -257,8 +265,8 @@ interface IOIMSlotContentService<TSlot, TSlotOwnerService extends IOIMSlotOwner
|
|
|
257
265
|
A content service with `createSlot` goes into `createOIMSlotCollectionService`, one with `createSingleSlot` into
|
|
258
266
|
`createOIMSingleSlotService`; the entity and bucket content services have both. `hasReference` answers whether
|
|
259
267
|
something outside `holdCount` and `effects` keeps the slot (a bucket's members). Every write door asserts the
|
|
260
|
-
identity is still filed (`assertWritable`)
|
|
261
|
-
|
|
268
|
+
identity is still filed (`assertWritable`). `releaseSlotContent` is what the slot owner calls when it releases a
|
|
269
|
+
slot nothing holds, and it throws on a slot that is still held.
|
|
262
270
|
|
|
263
271
|
### The injected seams
|
|
264
272
|
|
|
@@ -269,7 +277,7 @@ object.
|
|
|
269
277
|
```ts signature
|
|
270
278
|
interface IOIMSlotEmptinessPolicy<TSlot> { isEmpty(slot: TSlot): boolean; }
|
|
271
279
|
|
|
272
|
-
interface IOIMSlotStamp<TSlot> { stamp(
|
|
280
|
+
interface IOIMSlotStamp<TSlot> { stamp(editableSlot: TOIMEditable<TSlot>): void; }
|
|
273
281
|
```
|
|
274
282
|
|
|
275
283
|
**What a kind of content answers is not a seam.** Mint, write, clear and release
|
|
@@ -332,7 +340,7 @@ true; `OIMSlotAtEmptyReleasePolicy`, `OIMSlotNeverReleasePolicy`, `OIMSlotAtUnho
|
|
|
332
340
|
`onSlotIdle` is still called when the last subscriber leaves.
|
|
333
341
|
|
|
334
342
|
A policy decides whether and when a slot goes; it does no work itself. A
|
|
335
|
-
factory, and `
|
|
343
|
+
factory, and an `OIMDefaults` (whose `slotReleasePolicy` a factory given it as `defaults` reads as its default), takes ONE
|
|
336
344
|
through the option `slotReleasePolicy`. With none, the slot owner is built with
|
|
337
345
|
`OIMSlotAtEmptyReleasePolicy`, so a slot with a value stays for good and an empty
|
|
338
346
|
one with nothing holding it goes; there is no "no policy" branch on any path.
|
|
@@ -375,9 +383,18 @@ a freshly minted slot that is already due goes to the unused-slot collector and
|
|
|
375
383
|
leaves at the end of the frame. A window above zero is filed in that policy's own
|
|
376
384
|
queue.
|
|
377
385
|
|
|
386
|
+
**Two writes keep their slot until the unused-slot collector flushes, and this is deliberate.**
|
|
387
|
+
(1) a value that the collection's emptiness policy calls empty, and (2) a write under a zero window
|
|
388
|
+
(`OIMSlotAfterWriteReleasePolicy` / `OIMSlotAfterIdleReleasePolicy` with `0`) on a slot with no subscriber.
|
|
389
|
+
Released at once, the slot `writeSlotContentByKey` returned would be dead on the next line: a second write
|
|
390
|
+
to it throws, and handing it to a bucket throws ("an identity handed to a bucket was released") — which is
|
|
391
|
+
what persist's restore does with the slots `writeInBatch` returns. The collector gives the hold back when it
|
|
392
|
+
flushes — at the end of `db.flush()`, or when a collector scope (`OIMUnusedSlotCollectorService.INSTANCE.run`)
|
|
393
|
+
closes — and the slot is released there.
|
|
394
|
+
|
|
378
395
|
```ts
|
|
379
396
|
|
|
380
|
-
const
|
|
397
|
+
const releasingDefaults = new OIMDefaults({
|
|
381
398
|
slotReleasePolicy: new OIMSlotEarliestReleasePolicy([
|
|
382
399
|
new OIMSlotAfterWriteReleasePolicy({ keepAfterWriteMs: 60_000 }),
|
|
383
400
|
new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
|
|
@@ -406,7 +423,7 @@ is allowed to move it: a write arriving after the subscriber left would push the
|
|
|
406
423
|
deadline out and destroy the idle policy's verdict, and the last subscriber
|
|
407
424
|
leaving after a write would destroy the write policy's. Both directions are
|
|
408
425
|
pinned by
|
|
409
|
-
`
|
|
426
|
+
`collector/tests/which-release-marker-each-collector-may-move.test.ts`;
|
|
410
427
|
collapse the two markers into one and six of its nine cases go red.
|
|
411
428
|
|
|
412
429
|
**The release policy's `isSlotReleasable(slot)`** is what the release guard asks
|
|
@@ -485,8 +502,8 @@ that caller; one that throws on the collector's own timer tick has no caller and
|
|
|
485
502
|
|
|
486
503
|
```ts
|
|
487
504
|
|
|
488
|
-
const oneSinkDb = new OIMDB({
|
|
489
|
-
|
|
505
|
+
const oneSinkDb = new OIMDB({ errorPolicy: new OIMThrowAtOnceErrorPolicy() });
|
|
506
|
+
const idleReleasingDefaults = new OIMDefaults({
|
|
490
507
|
slotReleasePolicy: new OIMSlotAfterIdleReleasePolicy({ keepAfterIdleMs: 30_000 }),
|
|
491
508
|
});
|
|
492
509
|
```
|
|
@@ -517,13 +534,10 @@ asks `isEmptySlotReleasable` instead, which every policy but
|
|
|
517
534
|
`OIMSlotNeverReleasePolicy` answers `true`. So only `OIMSlotNeverReleasePolicy`
|
|
518
535
|
keeps an identity that holds nothing.
|
|
519
536
|
|
|
520
|
-
**A release that falls inside a flush
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
slot is handed to the unused-slot collector instead, which holds it and gives it
|
|
525
|
-
back once the flush is over; the release then runs. An empty slot needs no write
|
|
526
|
-
and goes at once.
|
|
537
|
+
**A release that falls inside a flush happens at once.** A slot at `holdCount` 0 has no subscriber
|
|
538
|
+
(a subscription is a hold), so clearing its value wakes nobody, and a release notifies no content-change
|
|
539
|
+
listener. When the last hold goes during a frame — an effect body disposing another effect, say — the slot is
|
|
540
|
+
released there, whatever its kind. A later `holdSlotByKey` for the same key in that flush mints a new slot.
|
|
527
541
|
|
|
528
542
|
*The `owner !== this` line costs no lookup. A KEYLESS slot — a single's, filed in an
|
|
529
543
|
`OIMSingleSlotStoreService` store — goes through the same guard under its service's policy: an
|
|
@@ -540,7 +554,7 @@ one whole window later. **A give-back is NOT that moment**: `unholdSlot` stamps
|
|
|
540
554
|
nothing, so a slot held through an expired sweep goes at the give-back.
|
|
541
555
|
|
|
542
556
|
```ts
|
|
543
|
-
const effect = effectService.createEffect(db,
|
|
557
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
544
558
|
// …the window passes, sweeps run, nothing happens to this slot…
|
|
545
559
|
effectService.disposeEffect(effect);
|
|
546
560
|
oimIsSlotReleased(slot); // false — a whole new window starts here
|
|
@@ -551,7 +565,7 @@ releases the stale index, which gives back one reference per entity slot it name
|
|
|
551
565
|
each entity slot whose own window already ran out is taken by the ordinary guard the
|
|
552
566
|
instant that reference departs.
|
|
553
567
|
|
|
554
|
-
Pinned by `
|
|
568
|
+
Pinned by `collector/tests/what-a-stale-index-takes-with-it.test.ts`,
|
|
555
569
|
`…/what-an-identity-nobody-subscribed-to-costs-after-a-while.test.ts` and
|
|
556
570
|
`…/what-a-subscribed-row-costs-the-release-clock.test.ts`.
|
|
557
571
|
|
|
@@ -678,7 +692,7 @@ OIMUnusedSlotCollectorService.INSTANCE.flush(unusedSlotCollectorSlot); // now
|
|
|
678
692
|
get that.** A computed takes a hold on each of its own direct sources at the mint
|
|
679
693
|
and gives them back when it is released, so an arena holds the top and the chain holds itself.
|
|
680
694
|
|
|
681
|
-
Pinned by `
|
|
695
|
+
Pinned by `collector/tests/what-a-named-arena-keeps-cached.test.ts`: the
|
|
682
696
|
derivation survives a settle, is the SAME instance on the next read (built once),
|
|
683
697
|
is rebuilt only after the arena is flushed, and `open`/`close` nest so an inner
|
|
684
698
|
scope ending drops nothing an outer one stands on.
|
|
@@ -712,11 +726,11 @@ path. There is no `if (inPlace)` anywhere in the engine.
|
|
|
712
726
|
|
|
713
727
|
| Fork | Strategies |
|
|
714
728
|
|---|---|
|
|
715
|
-
| how a write lands | `
|
|
729
|
+
| how a write lands | `createOIMInPlaceEntityWriter()` / `createOIMReplacingEntityWriter()` |
|
|
716
730
|
| how a bucket holds slots | `OIMSlotContentSetBucketService` / `OIMSlotContentPositionalArrayBucketService` (the positions are the caller's) |
|
|
717
731
|
| what naming a slot records | `bucketMemberSlotsArrayService: OIMBucketMemberSlotsArrayService` (one per mention, the default) / `new OIMBucketUniqueMemberSlotsArrayService<TSlot>()` (one per slot), on either array bucket |
|
|
718
732
|
| which writes re-sort a sorted list | `sortColumns` (only a write changing one of them on a member) / `comparator` alone (any member write), the options of `createOIMSortedComputedService` |
|
|
719
|
-
| how a key is addressed | `
|
|
733
|
+
| how a key is addressed | `OIMScalarKeyTreeWalk.INSTANCE` / `OIMCompositeKeyTreeWalk.INSTANCE` |
|
|
720
734
|
| when a slot may go | `OIMSlotAfterIdleReleasePolicy` / `OIMSlotAfterWriteReleasePolicy`, either or both |
|
|
721
735
|
| who decides a bucket's slots | a bucket collection (`createOIMSlotCollectionService` over a bucket content service, the caller) / an index (`createOIMSlotCollectionIndexService`, filed from the entity's own key) |
|
|
722
736
|
|
|
@@ -727,7 +741,7 @@ V8 still caches each one and can still inline
|
|
|
727
741
|
(`--max-valid-polymorphic-map-count`, default 4). Five or more is megamorphic: no
|
|
728
742
|
per-site cache and no inlining through it. The kinds that count: release policies
|
|
729
743
|
(the `IOIMSlotReleasePolicy` strategies), set-driven vs array-driven buckets and indexes, entity
|
|
730
|
-
writers (`
|
|
744
|
+
writers (`createOIMInPlaceEntityWriter` / `createOIMReplacingEntityWriter`) and
|
|
731
745
|
`TOIMEntityInserter`, `TOIMSameValueGuard`, `IOIMSlotEmptinessPolicy`, and
|
|
732
746
|
`TOIMChannelScheduler`. Two or three of one kind is fine; five release policies in
|
|
733
747
|
one process is not. The pair
|
|
@@ -742,12 +756,12 @@ is reported to that channel. No service holds a channel; there is no `channel` o
|
|
|
742
756
|
`unusedSlotCollector` option:
|
|
743
757
|
|
|
744
758
|
```ts
|
|
745
|
-
const cardContentService = createOIMSlotContentEntityService<Card>(
|
|
746
|
-
const handBuiltCardService = createOIMSlotCollectionService(
|
|
759
|
+
const cardContentService = createOIMSlotContentEntityService<Card>();
|
|
760
|
+
const handBuiltCardService = createOIMSlotCollectionService({ slotContentService: cardContentService });
|
|
747
761
|
handBuiltCardService.createStoreSlot(db);
|
|
748
762
|
```
|
|
749
763
|
|
|
750
|
-
A single entity is `createOIMSingleSlotService(
|
|
764
|
+
A single entity is `createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<T>() })`,
|
|
751
765
|
and its store slot takes the db's channel too and always reports.
|
|
752
766
|
|
|
753
767
|
### A bucket's slots are the caller's
|
|
@@ -858,7 +872,7 @@ Every door below is on `OIMChannelService.INSTANCE` and takes the channel slot f
|
|
|
858
872
|
The channel slot's own fields that a caller reads: `frame` (`TOIMChannelFrame`, `{ isRunning, openedAtRevision,
|
|
859
873
|
openedAtEpoch, … }`; a frame carries no number, the clock reading it opened at is its boundary), `clock`
|
|
860
874
|
(`TOIMChannelClock`, the `{revision, epoch}` pair, shared by every channel of one db; an epoch holds
|
|
861
|
-
`OIM_REVISIONS_PER_EPOCH` (2^30) revisions, so the counter stays a V8 small integer under pointer compression),
|
|
875
|
+
`OIM_REVISIONS_PER_EPOCH` (2^30) revisions, so the counter stays a V8 small integer under pointer compression; the epoch is an int32 that wraps, compared as `(epoch − sinceEpoch) | 0`),
|
|
862
876
|
`collectorSlot`, `errorPolicy`, `isFlushing`. Everything else on it is engine state.
|
|
863
877
|
|
|
864
878
|
### A subscription hears only what happens after it
|
|
@@ -873,11 +887,11 @@ after it.
|
|
|
873
887
|
cardService.writeSlotContentByKey(cards, 'a', { id: 'a', title: 'A', rank: 1, deck: 'spades' });
|
|
874
888
|
const entitySlot = cardService.getSlotByKey(cards, 'a');
|
|
875
889
|
|
|
876
|
-
effectService.createEffect(db,
|
|
890
|
+
effectService.createEffect(db, () => { /* NEVER runs for the upsert above */ }, [entitySlot]);
|
|
877
891
|
db.flush();
|
|
878
892
|
|
|
879
|
-
// A first value is `
|
|
880
|
-
effectService.
|
|
893
|
+
// A first value is `createAndRunEffect`'s job, not `createEffect`'s:
|
|
894
|
+
effectService.createAndRunEffect(db, () => { paint(entitySlot.entity); }, [entitySlot]); // runs now
|
|
881
895
|
```
|
|
882
896
|
|
|
883
897
|
A later write in that same batch IS delivered — "after I subscribed" is decided
|
|
@@ -890,11 +904,11 @@ Told after **one** frame — the first that ends after the call — and then gon
|
|
|
890
904
|
The place that asks decides which frame that is.
|
|
891
905
|
|
|
892
906
|
```ts
|
|
893
|
-
effectService.createEffect(db,
|
|
907
|
+
effectService.createEffect(db, () => {
|
|
894
908
|
paint(slot.entity);
|
|
895
909
|
// the tail of THIS frame, after every other effect of it
|
|
896
910
|
OIMChannelService.INSTANCE.subscribeOnceAfterFrame(channelSlot, () => { measureAndScroll(); });
|
|
897
|
-
});
|
|
911
|
+
}, [slot]);
|
|
898
912
|
```
|
|
899
913
|
|
|
900
914
|
- **Registered from inside an effect body it runs in the tail of that very
|
|
@@ -927,10 +941,10 @@ cardService.writeInBatch(cards, () => {
|
|
|
927
941
|
});
|
|
928
942
|
```
|
|
929
943
|
|
|
930
|
-
- `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `
|
|
931
|
-
`try/finally` form and returns what its function returns.
|
|
944
|
+
- `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `closeBatch` with no batch open
|
|
945
|
+
throws. `writeInBatch` is the `try/finally` form and returns what its function returns.
|
|
932
946
|
- **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, `
|
|
947
|
+
index on the collection) hears every write of the batch in ONE call, `onManyContentsChanged`, when the
|
|
934
948
|
outermost batch closes. Code inside the batch reads the index as it was before the batch.
|
|
935
949
|
- Effects are not batched by it: every write marks its slot on the channel as usual, and the frame delivers each
|
|
936
950
|
identity once whatever the batch did. A `flush()` inside a batch is allowed; the index catches up when the
|
|
@@ -1013,7 +1027,7 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
|
|
|
1013
1027
|
### Subscribing
|
|
1014
1028
|
|
|
1015
1029
|
```ts
|
|
1016
|
-
const effect = effectService.createEffect(db,
|
|
1030
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
1017
1031
|
effectService.disposeEffect(effect);
|
|
1018
1032
|
```
|
|
1019
1033
|
|
|
@@ -1048,10 +1062,10 @@ function is the undo**: it is called before the next run of that body and by
|
|
|
1048
1062
|
`disposeEffect`.
|
|
1049
1063
|
|
|
1050
1064
|
```ts
|
|
1051
|
-
effectService.createEffect(db,
|
|
1065
|
+
effectService.createEffect(db, () => {
|
|
1052
1066
|
const timer = setInterval(poll, 1000);
|
|
1053
1067
|
return () => clearInterval(timer);
|
|
1054
|
-
});
|
|
1068
|
+
}, [slot]);
|
|
1055
1069
|
```
|
|
1056
1070
|
|
|
1057
1071
|
The return is checked with `typeof`, not for truthiness — `() => set.add(key)`
|
|
@@ -1059,14 +1073,28 @@ hands back the `Set` — so anything that is not a function is discarded. The un
|
|
|
1059
1073
|
is cleared before it is called, so one that throws is not called again by the
|
|
1060
1074
|
`disposeEffect` behind it.
|
|
1061
1075
|
|
|
1062
|
-
**
|
|
1063
|
-
|
|
1076
|
+
**A derivation that finds its sources as it reads:** `createOIMSourceTrackingComputedService<TKey, TValue>({ derive, … })` and
|
|
1077
|
+
`createOIMSingleSourceTrackingComputedService<TValue>({ derive, … })`. `derive: TOIMSourceTrackingComputedDerive<TValue, TSlot>` is
|
|
1078
|
+
`(computedSlot, sourceTracker: OIMSourceTracker) => TValue`; there is no `createSources`, and `createComputedStoreSlot(db)` takes no
|
|
1079
|
+
per-store `derive`. Every derive holds what it read and gives back what it no longer reads (new holds first). The slot
|
|
1080
|
+
(`TOIMSourceTrackingComputedSlot<TValue, TKey>` / `TOIMSourceTrackingComputedContentSlot<TValue>`, `sourceTracker` set) is read
|
|
1081
|
+
through `sourceTracker.readSourceComputedValue` (in `createSourceTrackingEffect` or another source-tracking derive) or through `createEffect`'s fifth list; `createEffect`'s `sourceComputeds` and
|
|
1082
|
+
a `createSources` list refuse it by type, because a declared effect subscribes once to the slots under a derivation. When a derive
|
|
1083
|
+
switches sources, a source-tracking effect or derivation over it re-subscribes even when the value held. A body that cannot read
|
|
1084
|
+
through a tracker may pass them as the fifth list, `createEffect(db, body, sourceSlots, sourceComputeds, sourceTrackingComputeds)`;
|
|
1085
|
+
such an effect derives them at creation and re-subscribes when their root slots are replaced. Prefer `createSourceTrackingEffect`: the
|
|
1086
|
+
root check runs after every run of such an effect. An effect with no fifth list pays nothing for it. Options otherwise as
|
|
1087
|
+
`createOIMComputedService` / `createOIMSingleComputedService` (`sameValueGuard`, `releaseValue`, `initialValue`, `slotReleasePolicy`,
|
|
1088
|
+
column options).
|
|
1089
|
+
|
|
1090
|
+
**Sources found at read: `createSourceTrackingEffect(db, body)`.** The second way to build
|
|
1091
|
+
an effect takes no source lists. The body receives an `OIMSourceTracker`, and what it
|
|
1064
1092
|
reads through the tracker is what that run depends on — signal-style tracking.
|
|
1065
1093
|
|
|
1066
1094
|
```ts signature
|
|
1067
|
-
|
|
1068
|
-
type
|
|
1069
|
-
class
|
|
1095
|
+
createSourceTrackingEffect(channelMap: IOIMChannelMap, body: TOIMSourceTrackingEffectBody): TOIMEffect;
|
|
1096
|
+
type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
|
|
1097
|
+
class OIMSourceTracker { // exported as a type; the service builds it
|
|
1070
1098
|
readSourceSlot<TSlot extends TOIMSlot>(slot: TSlot): TSlot; // files the slot, returns it
|
|
1071
1099
|
readSourceComputedValue<TValue>(computedSlot: TOIMComputedSlot<TValue, unknown>): TValue; // files it, returns its value
|
|
1072
1100
|
}
|
|
@@ -1074,9 +1102,9 @@ class OIMEffectSourceTracker { // exported as a type; the service b
|
|
|
1074
1102
|
|
|
1075
1103
|
```ts
|
|
1076
1104
|
const fallback = cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'King', rank: 13, deck: 'hearts' });
|
|
1077
|
-
effectService.
|
|
1078
|
-
const isFirst =
|
|
1079
|
-
paint(
|
|
1105
|
+
effectService.createSourceTrackingEffect(db, sourceTracker => {
|
|
1106
|
+
const isFirst = sourceTracker.readSourceSlot(slot).entity?.rank === 1;
|
|
1107
|
+
paint(sourceTracker.readSourceSlot(isFirst ? slot : fallback).entity);
|
|
1080
1108
|
});
|
|
1081
1109
|
```
|
|
1082
1110
|
|
|
@@ -1089,7 +1117,7 @@ effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
|
|
|
1089
1117
|
creation. A returned function is the undo, exactly as for `TOIMEffectBody`.
|
|
1090
1118
|
|
|
1091
1119
|
```ts
|
|
1092
|
-
const onRank = effectService.createEffect(db,
|
|
1120
|
+
const onRank = effectService.createEffect(db, () => { paint(slot.entity); }, [cardService.columnSlotService.holdColumn(slot, 'rank')]);
|
|
1093
1121
|
```
|
|
1094
1122
|
|
|
1095
1123
|
**A column effect takes the `TOIMColumnSlot` as a root, not a slot and a name.** Minting the
|
|
@@ -1115,13 +1143,13 @@ per call; get it once per view, not per entity. `service.holdColumnGroup(slot, c
|
|
|
1115
1143
|
the group's column slot on `slot`, and `service.unholdColumn(columnSlot)` gives it back. A write wakes it once
|
|
1116
1144
|
if it changes any of the group's columns and not at all if it changes only others.
|
|
1117
1145
|
|
|
1118
|
-
**`columnNames` picks the column slot store.** `createOIMSlotCollectionService(
|
|
1146
|
+
**`columnNames` picks the column slot store.** `createOIMSlotCollectionService({ slotContentService, columnNames: ['rank', 'label'] })`
|
|
1119
1147
|
— and `columnNames` on any factory's options — files the service's column slots by ordinal: every named column
|
|
1120
1148
|
gets a number, a group gets one at its first slot, and an entity slot's column slots sit in an array by
|
|
1121
1149
|
number instead of a `Map` per entity slot, which is cheaper to create and give back. `holdColumn` and
|
|
1122
1150
|
`getColumnGroup` are typed on those names, and a slot for any other column throws. Without `columnNames` the
|
|
1123
1151
|
store is the `columnSlotStoreService` option, defaulting to the shared map-driven
|
|
1124
|
-
|
|
1152
|
+
a shared map-driven store, and any name is accepted.
|
|
1125
1153
|
|
|
1126
1154
|
---
|
|
1127
1155
|
|
|
@@ -1155,11 +1183,11 @@ once per collection type; the store slot is made separately:
|
|
|
1155
1183
|
|
|
1156
1184
|
| Door | Builds | Store slot |
|
|
1157
1185
|
|---|---|---|
|
|
1158
|
-
| `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(
|
|
1159
|
-
| `createOIMSlotCollectionService(
|
|
1160
|
-
| `createOIMSingleSlotService(
|
|
1161
|
-
| `createOIMSlotCollectionIndexService(
|
|
1162
|
-
| `createOIMSlotCollectionSortedArrayIndexService(
|
|
1186
|
+
| `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({ entityWriter?, slotEmptinessPolicy? }?)` | `OIMSlotContentEntityService<TEntity, TKey, TColumnName>`, the entity content service; it creates keyed and single entity slots alike | — |
|
|
1187
|
+
| `createOIMSlotCollectionService({ slotContentService, slotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSlotCollectionService` over any keyed content service (entity or bucket): the read doors, `writeSlotContentByKey`, the batch doors and `columnSlotService`. `slotStoreService` defaults to a Map store and is REQUIRED by type for an array key (`OIMSlotTreeStoreService`) | `createStoreSlot(db)` |
|
|
1188
|
+
| `createOIMSingleSlotService({ slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSingleSlotService` over any content service with `createSingleSlot` (entity or bucket) | `createStoreSlot(db)` |
|
|
1189
|
+
| `createOIMSlotCollectionIndexService({ entityService, columns? \| keyOf?, slotContentService?, bucketSlotStoreService?, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMSlotCollectionIndexService`, over the entity collection service whose store slots it will index (it takes a `Pick` of `getSlots`); `slotContentService` defaults to an `OIMSlotContentPositionalArrayBucketService`, pass `new OIMSlotContentSetBucketService()` for an unordered one. A scalar key is inferred from ONE inline column or from `keyOf`'s return; an array key (two or more columns, or an array-returning `keyOf`) needs `bucketSlotStoreService: new OIMSlotTreeStoreService(…)` | `createIndexStoreSlot(entityStoreSlot)` |
|
|
1190
|
+
| `createOIMSlotCollectionSortedArrayIndexService({ entityService, columns? \| keyOf?, sortColumns, … })` | `OIMSlotCollectionSortedArrayIndexService`: the index whose buckets are kept in the order of `sortColumns` (`readonly TOIMSortColumn<TEntity>[]`, required) through an `OIMSlotContentSortedArrayBucketService`. See [Sorted at write](#sorted-at-write) | `createIndexStoreSlot(entityStoreSlot)` |
|
|
1163
1191
|
| `createOIMComputedService({ createSources?, derive?, sameValueGuard?, initialValue?, slotReleasePolicy?, computedSlotStoreService?, columnSlotStoreService?, columnNames? })` | `OIMComputedService<TKey, TValue, TComputedStore>`; its reads are on `computedService.computedCollectionService`; `derive` and `createSources` may instead be given per store slot; `computedSlotStoreService` is REQUIRED for a non-Map store (a single: `OIMSingleSlotStoreService`); see [One derivation per key](#one-derivation-per-key) | `createComputedStoreSlot(db, derive?, createSources?)` |
|
|
1164
1192
|
| `createOIMSortedComputedService({ indexService, sortColumns?, comparator?, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMSortedComputedService` — per key, the entity slots of the index bucket under that key, sorted; see [Sorted lists](#sorted-lists) | `createSortedStoreSlot(indexStoreSlot)` |
|
|
1165
1193
|
| `createOIMBucketMemoComputedService({ bucketCollectionService, slotReleasePolicy?, columnSlotStoreService?, computedSlotStoreService? })` | `OIMBucketMemoComputedService` — per key, the bucket's slots as one array; see [The order of a bucket](#the-order-of-a-bucket-as-one-derivation-per-key) | `createBucketMemoStoreSlot(bucketStoreSlot)` |
|
|
@@ -1170,7 +1198,7 @@ db's, so naming one overrides it for that service alone.
|
|
|
1170
1198
|
**A DOOR ALWAYS PASSES; IT NEVER LETS A CLASS REACH FOR A GLOBAL.** Every option
|
|
1171
1199
|
a service would otherwise answer with `?? OIMDefaults.x` is in the object its
|
|
1172
1200
|
door hands it, so there is one answer to each question and not two. Pinned in
|
|
1173
|
-
`src/
|
|
1201
|
+
`src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts`,
|
|
1174
1202
|
which also fails when an option arrives on a service and is not forwarded,
|
|
1175
1203
|
when the root mints anything but the channel and the collector, or when a public
|
|
1176
1204
|
member appears on the root that is not one of the seven it owns.
|
|
@@ -1180,7 +1208,7 @@ member appears on the root that is not one of the seven it owns.
|
|
|
1180
1208
|
slot, and the script lives for one frame.
|
|
1181
1209
|
|
|
1182
1210
|
```ts
|
|
1183
|
-
const freshPageService = createOIMSlotCollectionService(
|
|
1211
|
+
const freshPageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
1184
1212
|
const freshPage = freshPageService.createStoreSlot(db);
|
|
1185
1213
|
const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
|
|
1186
1214
|
// freshBucketSlot.commands and freshBucketSlot.isWholeListRecorded carry the script; read them in an effect on the bucket slot
|
|
@@ -1190,56 +1218,41 @@ const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
|
|
|
1190
1218
|
|
|
1191
1219
|
## `OIMDefaults` — where an optional dependency comes from
|
|
1192
1220
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1221
|
+
`OIMDefaults` is an instance holding the strategies factories fall back on when their own options leave
|
|
1222
|
+
them out. Build one with what you want shared and pass it as `defaults` to every factory that should use
|
|
1223
|
+
it; a factory given none uses `OIMDefaults.INSTANCE`, which is empty. A field left undefined falls back to
|
|
1224
|
+
the built-in default. Nothing is global and nothing can be replaced after the fact.
|
|
1196
1225
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
**NOTHING IS CONSTRUCTED AT MODULE LOAD.** Every entry is an `OIMDefaultValue`
|
|
1210
|
-
whose `getValue()` mints on its first call (`??=`) and hands the same instance back
|
|
1211
|
-
for the rest of the process. A consumer that never reaches for a key tree walk never
|
|
1212
|
-
builds one.
|
|
1213
|
-
|
|
1214
|
-
Each has a matching `setValue(value)`, which is how a process replaces a default
|
|
1215
|
-
everywhere at once:
|
|
1226
|
+
```ts signature
|
|
1227
|
+
new OIMDefaults({
|
|
1228
|
+
slotStamp?: IOIMSlotStamp<TOIMSlot>; // none
|
|
1229
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // none: a slot with a value stays
|
|
1230
|
+
entityWriter?: IOIMEntityWriter; // createOIMInPlaceEntityWriter()
|
|
1231
|
+
entitySlotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot>; // "empty means slot.entity === undefined"
|
|
1232
|
+
indexKeysService?: IOIMIndexKeysService; // OIMAppendingIndexKeysService
|
|
1233
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // a shared map-driven store
|
|
1234
|
+
computedColumnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // a shared map-driven store
|
|
1235
|
+
storeSlotListener?: IOIMStoreSlotListener; // OIMNoStoreSlotListener: hears nothing
|
|
1236
|
+
}?) // TOIMDefaultsOptions
|
|
1237
|
+
```
|
|
1216
1238
|
|
|
1217
1239
|
```ts
|
|
1218
|
-
OIMDefaults
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
the first
|
|
1231
|
-
`OIMDefaults
|
|
1232
|
-
`
|
|
1233
|
-
|
|
1234
|
-
`packages/core/src/factories/tests/what-a-default-replaced-too-late-does.test.ts`.
|
|
1235
|
-
|
|
1236
|
-
**Each default is its own module, so reaching one retains only its arm.** Every
|
|
1237
|
-
default lives in its own file under `packages/core/src/factories/defaults/` with its
|
|
1238
|
-
own `OIMDefaultValue`; a service reads the modules it needs and
|
|
1239
|
-
neither key tree walk enters its bundle. `OIMDefaults` itself delegates to all of
|
|
1240
|
-
them, so importing `OIMDefaults` retains every arm.
|
|
1241
|
-
`packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one
|
|
1242
|
-
door to its own arm, and the defaults class to all of them"*.
|
|
1240
|
+
const replacingDefaults = new OIMDefaults({ entityWriter: createOIMReplacingEntityWriter() });
|
|
1241
|
+
const replacingCardService = createOIMSlotCollectionService({
|
|
1242
|
+
defaults: replacingDefaults,
|
|
1243
|
+
slotContentService: createOIMSlotContentEntityService<Card>({ defaults: replacingDefaults }),
|
|
1244
|
+
});
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
An option given to a factory wins over `defaults`; `defaults` wins over the built-in default. A tool that
|
|
1248
|
+
wants to hear every store slot (devtools' `oimDevDefaults`) is a `defaults` with a `storeSlotListener`.
|
|
1249
|
+
|
|
1250
|
+
**Each built-in default is its own module, so reaching one retains only its arm.** Every built-in default
|
|
1251
|
+
lives in its own file under `packages/core/src/db/constants/` as an `OIMDefaultValue` whose `getValue()`
|
|
1252
|
+
builds it on the first call and hands the same instance back after; a factory reads the modules it needs.
|
|
1253
|
+
`OIMDefaults` imports none of them, so importing it retains no arm.
|
|
1254
|
+
`packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one door to its own arm, and
|
|
1255
|
+
the defaults class to none of them"*.
|
|
1243
1256
|
|
|
1244
1257
|
---
|
|
1245
1258
|
|
|
@@ -1251,17 +1264,17 @@ with its slot owner; the collection service (`OIMSlotCollectionService`) over it
|
|
|
1251
1264
|
Neither holds an entity: each `createStoreSlot(db)` is one instance of that type's store.
|
|
1252
1265
|
|
|
1253
1266
|
```ts signature
|
|
1254
|
-
createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(
|
|
1255
|
-
entityWriter?: IOIMEntityWriter; //
|
|
1256
|
-
slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; //
|
|
1267
|
+
createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({
|
|
1268
|
+
entityWriter?: IOIMEntityWriter; // defaults.entityWriter, then the in-place writer
|
|
1269
|
+
slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; // defaults.entitySlotEmptinessPolicy, then "entity is undefined"
|
|
1257
1270
|
}?): OIMSlotContentEntityService<TEntity, TKey, TColumnName>
|
|
1258
1271
|
|
|
1259
|
-
createOIMSlotCollectionService(
|
|
1272
|
+
createOIMSlotCollectionService({
|
|
1260
1273
|
slotContentService,
|
|
1261
1274
|
slotStoreService?: IOIMSlotStoreService<TKey, TSlot, TStore>; // Map; REQUIRED for an array key
|
|
1262
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; //
|
|
1275
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // defaults.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
|
|
1263
1276
|
slotSubscriptionListener?: IOIMSlotSubscriptionListener; // OIMNoSlotSubscriptionListener
|
|
1264
|
-
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; //
|
|
1277
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // defaults.columnSlotStoreService, then a shared map store
|
|
1265
1278
|
columnNames?: readonly TColumnName[]; // the ordinal column store over those names
|
|
1266
1279
|
}): OIMWritableSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, TEntity, …>
|
|
1267
1280
|
collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>, typeof collectionService>
|
|
@@ -1278,7 +1291,7 @@ derive-driven column slots, each a computed slot over its carrier (`OIMPullColum
|
|
|
1278
1291
|
Example, over the `db`, `cardService`, `cards` and `slot` of the setup block:
|
|
1279
1292
|
|
|
1280
1293
|
```ts
|
|
1281
|
-
const effect = effectService.createEffect(db,
|
|
1294
|
+
const effect = effectService.createEffect(db, () => { console.log(slot.entity?.title); }, [slot]);
|
|
1282
1295
|
cardService.slotContentService.updateEntity(slot, { title: 'Ace of Spades' });
|
|
1283
1296
|
db.flush();
|
|
1284
1297
|
effectService.disposeEffect(effect);
|
|
@@ -1287,15 +1300,15 @@ effectService.disposeEffect(effect);
|
|
|
1287
1300
|
Assembled with a store service of your own:
|
|
1288
1301
|
|
|
1289
1302
|
```ts
|
|
1290
|
-
const baseCardService = createOIMSlotCollectionService(
|
|
1291
|
-
slotContentService: createOIMSlotContentEntityService<Card, string>(
|
|
1303
|
+
const baseCardService = createOIMSlotCollectionService({
|
|
1304
|
+
slotContentService: createOIMSlotContentEntityService<Card, string>(),
|
|
1292
1305
|
slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, string>>(),
|
|
1293
1306
|
});
|
|
1294
1307
|
const baseCards = baseCardService.createStoreSlot(db);
|
|
1295
1308
|
```
|
|
1296
1309
|
|
|
1297
1310
|
The writer is omitted because the db and `OIMDefaults` supply it, and so is the column store. The slot stamp
|
|
1298
|
-
is not an option of any factory: it is `new
|
|
1311
|
+
is not an option of any factory: it is `new OIMDefaults({ slotStamp })`, passed as `defaults`.
|
|
1299
1312
|
|
|
1300
1313
|
### Public properties
|
|
1301
1314
|
|
|
@@ -1309,7 +1322,7 @@ A store slot is not a slot, so nothing holds, subscribes or wakes it: there is n
|
|
|
1309
1322
|
`effectService.createEffect(db, [cards], …)` that hears the population. "Something in this population changed" is an
|
|
1310
1323
|
index over the store slot and a subscription to its bucket — the bucket's door
|
|
1311
1324
|
is written when an entity slot joins or leaves it — or a content-change listener on the store slot
|
|
1312
|
-
(`
|
|
1325
|
+
(`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames)`,
|
|
1313
1326
|
removed by the same listener object). The slot owner is what `slot.owner` points at for every slot the type
|
|
1314
1327
|
filed, in every one of its store slots; `slot.storeSlot` names which store slot.
|
|
1315
1328
|
|
|
@@ -1500,7 +1513,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1500
1513
|
- **The hand mutation already compiles.** `TOIMEntitySlot` declares
|
|
1501
1514
|
`readonly entity: TEntity | undefined`, and `readonly` forbids REPLACING the
|
|
1502
1515
|
object, not writing its columns — `entity.title = 'two'` needs no cast, only
|
|
1503
|
-
the usual narrowing of `| undefined`. `
|
|
1516
|
+
the usual narrowing of `| undefined`. `TOIMEditable<TSlot>` is what strips
|
|
1504
1517
|
`readonly` from the field itself, which is what assigning `slot.entity` needs.
|
|
1505
1518
|
- **With a list, only the named columns wake. Without it, every watched column
|
|
1506
1519
|
does** — the column owner walks the slot's whole column store, and a
|
|
@@ -1515,7 +1528,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1515
1528
|
effect on the slot itself and an index over the store slot both see a hand mutation.
|
|
1516
1529
|
- **It re-stamps emptiness** whenever emptiness can depend on the mutation — with no column list,
|
|
1517
1530
|
on a slot whose `entity` is `undefined`, and on a content service with a `slotEmptinessPolicy` other than
|
|
1518
|
-
`
|
|
1531
|
+
"the entity is `undefined`", a release policy that releases written slots, or a db with a
|
|
1519
1532
|
`slotStamp` — so a hand mutation that emptied the entity leaves `oimIsSlotEmpty(slot)`
|
|
1520
1533
|
true rather than lying, and the release guard can take the identity when it should.
|
|
1521
1534
|
- **It refuses inside a frame**, through the same `assertWritable` →
|
|
@@ -1531,7 +1544,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1531
1544
|
A **writer** is the operations that move an entity's columns, and nothing
|
|
1532
1545
|
else: it reaches no channel, takes no holds and knows nothing about lifetime.
|
|
1533
1546
|
Both strategies ship as factories; the fork is chosen once, as the content service's `entityWriter`
|
|
1534
|
-
(or for every
|
|
1547
|
+
(or for every factory given it, `new OIMDefaults({ entityWriter })` as `defaults`).
|
|
1535
1548
|
|
|
1536
1549
|
```ts signature
|
|
1537
1550
|
interface IOIMEntityWriter {
|
|
@@ -1548,13 +1561,13 @@ interface IOIMEntityWriter {
|
|
|
1548
1561
|
type TOIMEntityWriterOptions = {
|
|
1549
1562
|
sameValueGuard?: TOIMSameValueGuard;
|
|
1550
1563
|
entityInserter?: TOIMEntityInserter;
|
|
1551
|
-
putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns |
|
|
1552
|
-
patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns |
|
|
1553
|
-
patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn |
|
|
1564
|
+
putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimGuardedPutEntityColumns
|
|
1565
|
+
patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimGuardedPatchEntityColumns
|
|
1566
|
+
patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimGuardedPatchEntityColumn
|
|
1554
1567
|
};
|
|
1555
1568
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1569
|
+
createOIMInPlaceEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // stored object keeps its identity
|
|
1570
|
+
createOIMReplacingEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
|
|
1558
1571
|
```
|
|
1559
1572
|
|
|
1560
1573
|
`entityInserter` is the first-write strategy: `oimInsertEntityWithoutCopy` (the default) stores
|
|
@@ -1582,9 +1595,9 @@ of a key that already has a value and `slotContentService.writeSlotContent(slot,
|
|
|
1582
1595
|
write that drops a column is ONE channel write of the slot, and the replacing writer
|
|
1583
1596
|
builds ONE new object.
|
|
1584
1597
|
|
|
1585
|
-
Pick `
|
|
1598
|
+
Pick `createOIMReplacingEntityWriter` when something downstream compares by
|
|
1586
1599
|
reference — a React `useSyncExternalStore` snapshot, a memo, a shallow-equal
|
|
1587
|
-
selector. Under `
|
|
1600
|
+
selector. Under `createOIMInPlaceEntityWriter` the entity reference never moves,
|
|
1588
1601
|
so a value comparison never fires and a binding needs a counter bumped inside its
|
|
1589
1602
|
handler. Replacing allocates one object per changed write; in-place allocates
|
|
1590
1603
|
none.
|
|
@@ -1609,10 +1622,10 @@ oimPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, c
|
|
|
1609
1622
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1610
1623
|
oimPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
|
|
1611
1624
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1612
|
-
|
|
1625
|
+
oimGuardedPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
|
|
1613
1626
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1614
|
-
|
|
1615
|
-
|
|
1627
|
+
oimGuardedPatchEntityColumns(slot, patch: object, sameValueGuard, changedColumnNames, targetOf) // same signature as the unguarded walk
|
|
1628
|
+
oimGuardedPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, changedColumnNames, targetOf)
|
|
1616
1629
|
```
|
|
1617
1630
|
|
|
1618
1631
|
Each compares every column through `sameValueGuard`, writes the moved ones into
|
|
@@ -1625,14 +1638,14 @@ ships two arms:
|
|
|
1625
1638
|
|
|
1626
1639
|
| Option | Default (unguarded) | Guarded arm |
|
|
1627
1640
|
|---|---|---|
|
|
1628
|
-
| `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `
|
|
1629
|
-
| `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `
|
|
1630
|
-
| `patchEntityColumn` | `oimPatchEntityColumn` | `
|
|
1641
|
+
| `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `oimGuardedPutEntityColumns` — skips both |
|
|
1642
|
+
| `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `oimGuardedPatchEntityColumns` — skips a `__proto__` key and a key the patch does not own |
|
|
1643
|
+
| `patchEntityColumn` | `oimPatchEntityColumn` | `oimGuardedPatchEntityColumn` — refuses the column name `__proto__` |
|
|
1631
1644
|
|
|
1632
1645
|
Take the guarded arms, at a cost on every write, for values that can carry a `__proto__` key or are class
|
|
1633
|
-
instances — `
|
|
1634
|
-
patchEntityColumns:
|
|
1635
|
-
same options on `
|
|
1646
|
+
instances — `createOIMInPlaceEntityWriter({ putEntityColumns: oimGuardedPutEntityColumns,
|
|
1647
|
+
patchEntityColumns: oimGuardedPatchEntityColumns, patchEntityColumn: oimGuardedPatchEntityColumn })`, the
|
|
1648
|
+
same options on `createOIMReplacingEntityWriter`. The writer decides only where the result goes — this one
|
|
1636
1649
|
stores every value frozen:
|
|
1637
1650
|
|
|
1638
1651
|
```ts
|
|
@@ -1640,11 +1653,11 @@ const copyColumns = (entity: Record<string, unknown>): Record<string, unknown> =
|
|
|
1640
1653
|
|
|
1641
1654
|
function storeFrozen(slot: TOIMEntityContentSlot, next: Record<string, unknown> | undefined): boolean {
|
|
1642
1655
|
if (next === undefined) return false;
|
|
1643
|
-
(slot as
|
|
1656
|
+
(slot as TOIMEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
|
|
1644
1657
|
return true;
|
|
1645
1658
|
}
|
|
1646
1659
|
|
|
1647
|
-
const replacingWriter =
|
|
1660
|
+
const replacingWriter = createOIMReplacingEntityWriter();
|
|
1648
1661
|
|
|
1649
1662
|
const freezingWriter: IOIMEntityWriter = {
|
|
1650
1663
|
fillEntity: (slot, entity) => { storeFrozen(slot, copyColumns(entity as Record<string, unknown>)); },
|
|
@@ -1672,8 +1685,8 @@ A single entity — one value under no key, such as settings or a session — is
|
|
|
1672
1685
|
which files at most one slot per store slot. It has no key and no by-key doors: its doors name the one slot.
|
|
1673
1686
|
|
|
1674
1687
|
```ts signature
|
|
1675
|
-
createOIMSingleSlotService(
|
|
1676
|
-
slotContentService: createOIMSlotContentEntityService<TEntity>(
|
|
1688
|
+
createOIMSingleSlotService({
|
|
1689
|
+
slotContentService: createOIMSlotContentEntityService<TEntity>(),
|
|
1677
1690
|
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
1678
1691
|
}): OIMWritableSingleSlotService<TOIMEntityContentSlot<TEntity>, TEntity, …>
|
|
1679
1692
|
|
|
@@ -1697,12 +1710,12 @@ with no value until something writes it. Its store slot is a `TOIMSingleSlotStor
|
|
|
1697
1710
|
```ts
|
|
1698
1711
|
type Settings = { theme: 'light' | 'dark'; zoom: number };
|
|
1699
1712
|
|
|
1700
|
-
const settingsService = createOIMSingleSlotService(
|
|
1713
|
+
const settingsService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<Settings>() });
|
|
1701
1714
|
const settings = settingsService.createStoreSlot(db);
|
|
1702
1715
|
settingsService.writeSingleSlotContent(settings, { theme: 'light', zoom: 1 });
|
|
1703
1716
|
const settingsSlot = settingsService.holdSingleSlot(settings);
|
|
1704
1717
|
const themeColumn = settingsService.columnSlotService.holdColumn(settingsSlot, 'theme');
|
|
1705
|
-
const themeEffect = effectService.createEffect(db,
|
|
1718
|
+
const themeEffect = effectService.createEffect(db, () => { console.log(settingsSlot.entity?.theme); }, [themeColumn]);
|
|
1706
1719
|
settingsService.slotContentService.updateEntity(settingsSlot, { theme: 'dark' });
|
|
1707
1720
|
db.flush();
|
|
1708
1721
|
effectService.disposeEffect(themeEffect);
|
|
@@ -1720,7 +1733,7 @@ The slot is of the keyed entity slot's literal shape (with `key: undefined` and
|
|
|
1720
1733
|
`storeSlot` is `settings` — so
|
|
1721
1734
|
`effectService.createEffect(db, [settingsSlot], [], …)` works and a column of it is a root like any other.
|
|
1722
1735
|
Its key is `undefined`, and its store is `{ slot }` (`TOIMSingleSlotStore`). Its lifetime is the
|
|
1723
|
-
`slotReleasePolicy` (
|
|
1736
|
+
`slotReleasePolicy` (`defaults.slotReleasePolicy`, then `OIMSlotAtEmptyReleasePolicy`), so a single entity that holds a value
|
|
1724
1737
|
stays; one emptied and unheld is released, and the slot `getSingleSlot` mints next holds no entity. The value
|
|
1725
1738
|
is stored by the writer's `entityInserter`, which by default keeps the object handed in rather than a copy.
|
|
1726
1739
|
|
|
@@ -1732,7 +1745,7 @@ A bucket collection is, like an entity collection, a content service and the col
|
|
|
1732
1745
|
The content service is the strategy — how a bucket keeps its member slots — and it is chosen at the factory:
|
|
1733
1746
|
|
|
1734
1747
|
```ts signature
|
|
1735
|
-
createOIMSlotCollectionService(
|
|
1748
|
+
createOIMSlotCollectionService({
|
|
1736
1749
|
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or one of the others below
|
|
1737
1750
|
slotStoreService?: IOIMSlotStoreService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore>; // Map; REQUIRED for an array key
|
|
1738
1751
|
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
@@ -1752,7 +1765,7 @@ of the collection service take that store slot first, and the bucket doors of th
|
|
|
1752
1765
|
bucket slot.
|
|
1753
1766
|
|
|
1754
1767
|
```ts
|
|
1755
|
-
const colourService = createOIMSlotCollectionService(
|
|
1768
|
+
const colourService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
|
|
1756
1769
|
const colours = colourService.createStoreSlot(db);
|
|
1757
1770
|
const red = colourService.holdSlotByKey(colours, 'red');
|
|
1758
1771
|
colourService.slotContentService.writeSlotContent(red, [slot, cardService.getSlotByKey(cards, 'c2')]);
|
|
@@ -1970,10 +1983,10 @@ content service already writes the bucket slot on every mutation, so a consumer
|
|
|
1970
1983
|
reads the script inside the effect:
|
|
1971
1984
|
|
|
1972
1985
|
```ts
|
|
1973
|
-
effectService.createEffect(db,
|
|
1986
|
+
effectService.createEffect(db, () => {
|
|
1974
1987
|
if (todo.isWholeListRecorded) render(Array.from(pageService.slotContentService.getBucketMemberSlots(todo)));
|
|
1975
1988
|
else applyToTheRenderedList(todo.commands ?? []);
|
|
1976
|
-
});
|
|
1989
|
+
}, [todo]);
|
|
1977
1990
|
```
|
|
1978
1991
|
|
|
1979
1992
|
`TOIMBucketSlot` carries two fields for it, and a consumer reads both:
|
|
@@ -2019,21 +2032,21 @@ One bucket under no key is a store slot of an `OIMSingleSlotService` over a buck
|
|
|
2019
2032
|
`OIMSingleSlotStoreService.INSTANCE` as the store service:
|
|
2020
2033
|
|
|
2021
2034
|
```ts signature
|
|
2022
|
-
createOIMSingleSlotService(
|
|
2035
|
+
createOIMSingleSlotService({
|
|
2023
2036
|
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or the set one
|
|
2024
2037
|
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
2025
2038
|
}): OIMWritableSingleSlotService<TOIMBucketContentSlot<TSlots>, readonly TBucketMemberSlot[], …>
|
|
2026
2039
|
```
|
|
2027
2040
|
|
|
2028
2041
|
```ts
|
|
2029
|
-
const handService = createOIMSingleSlotService(
|
|
2042
|
+
const handService = createOIMSingleSlotService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
2030
2043
|
const hand = handService.createStoreSlot(db);
|
|
2031
2044
|
const handSlot = handService.holdSingleSlot(hand);
|
|
2032
2045
|
handService.slotContentService.writeSlotContent(handSlot, [slot]);
|
|
2033
2046
|
handService.slotContentService.insertBucketMemberSlotAt(handSlot, 0, cardService.getSlotByKey(cards, 'c2'));
|
|
2034
|
-
effectService.disposeEffect(effectService.createEffect(db,
|
|
2047
|
+
effectService.disposeEffect(effectService.createEffect(db, () => {
|
|
2035
2048
|
render(Array.from(handService.slotContentService.getBucketMemberSlots(handSlot)));
|
|
2036
|
-
}));
|
|
2049
|
+
}, [handSlot]));
|
|
2037
2050
|
handSlot.owner.unholdSlot(handSlot);
|
|
2038
2051
|
```
|
|
2039
2052
|
|
|
@@ -2052,7 +2065,7 @@ slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `OI
|
|
|
2052
2065
|
### The index that fills itself — `OIMSlotCollectionIndexService`
|
|
2053
2066
|
|
|
2054
2067
|
```ts signature
|
|
2055
|
-
createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>(
|
|
2068
|
+
createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>({
|
|
2056
2069
|
entityService: Pick<OIMSlotCollectionService<TSourceKey, TOIMEntitySlot<TEntity, TSourceKey>, TSourceStore, unknown>, 'getSlots'>;
|
|
2057
2070
|
...TOIMIndexKeyOptions<TKey, TEntity, TColumns>; // { columns } | { keyOf } | { keyOf, columns }
|
|
2058
2071
|
slotContentService?: OIMSlotContentBucketService<TOIMEntitySlot<TEntity>, TSlots>; // default: array-driven, no commands
|
|
@@ -2098,21 +2111,21 @@ own. With none named it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: the bucket st
|
|
|
2098
2111
|
maintained on every write.
|
|
2099
2112
|
|
|
2100
2113
|
```ts
|
|
2101
|
-
const byDeckService = createOIMSlotCollectionIndexService(
|
|
2114
|
+
const byDeckService = createOIMSlotCollectionIndexService({
|
|
2102
2115
|
entityService: cardService,
|
|
2103
2116
|
keyOf: (card: Card) => card.deck,
|
|
2104
2117
|
});
|
|
2105
2118
|
const byDeck = byDeckService.createIndexStoreSlot(cards);
|
|
2106
2119
|
|
|
2107
2120
|
const spades = byDeckService.holdSlotByKey(byDeck, 'spades');
|
|
2108
|
-
effectService.createEffect(db,
|
|
2121
|
+
effectService.createEffect(db, () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))), [spades]);
|
|
2109
2122
|
```
|
|
2110
2123
|
|
|
2111
2124
|
A set-driven index names its content service with the bucket member slot type and the index key type spelled
|
|
2112
2125
|
out — `new OIMSlotContentSetBucketService()` infers `<TOIMSlot, TOIMStoreKey>` and fails to type-check:
|
|
2113
2126
|
|
|
2114
2127
|
```ts
|
|
2115
|
-
const bySuitService = createOIMSlotCollectionIndexService(
|
|
2128
|
+
const bySuitService = createOIMSlotCollectionIndexService({
|
|
2116
2129
|
entityService: cardService,
|
|
2117
2130
|
columns: ['deck'],
|
|
2118
2131
|
slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>, string>(),
|
|
@@ -2121,7 +2134,7 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2121
2134
|
```
|
|
2122
2135
|
|
|
2123
2136
|
- **Passing the entity store slot to `createIndexStoreSlot` is the whole wiring.** It registers
|
|
2124
|
-
a content-change listener on that store slot (`
|
|
2137
|
+
a content-change listener on that store slot (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener`);
|
|
2125
2138
|
the entity services know nothing about indexes. Call `createIndexStoreSlot` twice over one entity
|
|
2126
2139
|
store slot and it files into both. `entityService` is used only for its `getSlots`, to fill a new bucket
|
|
2127
2140
|
from the source.
|
|
@@ -2133,8 +2146,8 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2133
2146
|
ones `keyOf` reads. With `columns`, a patch or put whose changed columns include none of
|
|
2134
2147
|
them keeps the entity's key and calls nothing; a write of the whole value (a fill, a
|
|
2135
2148
|
clear, `markEntityChanged(slot)` with no names) always reads the key again.
|
|
2136
|
-
- **A key of several columns**: `createOIMSlotCollectionIndexService(
|
|
2137
|
-
bucketSlotStoreService: new OIMSlotTreeStoreService(
|
|
2149
|
+
- **A key of several columns**: `createOIMSlotCollectionIndexService({ entityService, columns: ['deck', 'suit'],
|
|
2150
|
+
bucketSlotStoreService: new OIMSlotTreeStoreService(OIMScalarKeyTreeWalk.INSTANCE) })` makes the key
|
|
2138
2151
|
the array of those values in order, typed as a tuple. Only that store knows the key is an array (its
|
|
2139
2152
|
`isSameKey`), so a fresh array with the same parts moves nothing. `createOIMSlotCollectionIndexService` takes a scalar key
|
|
2140
2153
|
unless it is handed a `bucketSlotStoreService`: two columns or an array `keyOf` without one do not compile.
|
|
@@ -2190,13 +2203,13 @@ the bucket slot (`getSlotByKey` / `holdSlotByKey`). A reader that wants both sub
|
|
|
2190
2203
|
to both, and the subscriber picks by which slots it puts in `createEffect`.
|
|
2191
2204
|
|
|
2192
2205
|
```ts
|
|
2193
|
-
const deckIndexService = createOIMSlotCollectionIndexService(
|
|
2206
|
+
const deckIndexService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2194
2207
|
const deckIndex = deckIndexService.createIndexStoreSlot(cards);
|
|
2195
2208
|
const spadesBucket = deckIndexService.holdSlotByKey(deckIndex, 'spades');
|
|
2196
2209
|
const spadesAnyColumn = deckIndexService.holdAnyBucketColumnByKey(deckIndex, 'spades');
|
|
2197
|
-
const entitiesEffect = effectService.createEffect(db,
|
|
2210
|
+
const entitiesEffect = effectService.createEffect(db, () => {
|
|
2198
2211
|
console.log(deckIndexService.getEntitiesByKey(deckIndex, 'spades'));
|
|
2199
|
-
});
|
|
2212
|
+
}, [spadesBucket, spadesAnyColumn]);
|
|
2200
2213
|
effectService.disposeEffect(entitiesEffect);
|
|
2201
2214
|
deckIndexService.columnSlotService.unholdColumn(spadesAnyColumn);
|
|
2202
2215
|
spadesBucket.owner.unholdSlot(spadesBucket);
|
|
@@ -2215,7 +2228,7 @@ needs the whole list each time: an export, an aggregate over the entities, a log
|
|
|
2215
2228
|
It costs nothing until someone asks for one: a write is checked against it only after
|
|
2216
2229
|
the index has been asked for the first any-column slot.
|
|
2217
2230
|
|
|
2218
|
-
**The content-change hook.** `
|
|
2231
|
+
**The content-change hook.** `OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames): void`
|
|
2219
2232
|
and `removeContentChangeListener(storeSlot, listener): boolean` file and unfile a listener on that one store
|
|
2220
2233
|
slot, by the listener object. `addReadColumn` / `removeReadColumn` / `addReadAnyColumn` / `removeReadAnyColumn`
|
|
2221
2234
|
declare reads without a listener of their own, which is how an index declares its bucket columns.
|
|
@@ -2231,15 +2244,15 @@ every column write is added with `readColumnNames` `undefined`. **The filter is
|
|
|
2231
2244
|
slot, not per listener**: once a write touches a column ANY listener of that store reads, every
|
|
2232
2245
|
listener of that store is called, so a listener that declared a narrower set still checks
|
|
2233
2246
|
`changedColumns` itself, as `OIMSlotCollectionIndexService` does against its key columns.
|
|
2234
|
-
`
|
|
2247
|
+
`IOIMSlotContentChangeListener<TSlot, TPatch>` has two methods:
|
|
2235
2248
|
`onContentChanged(slot, patch, changedColumns)` and
|
|
2236
|
-
`
|
|
2249
|
+
`onManyContentsChanged(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
|
|
2237
2250
|
parallel `OIMChannelQueue`s read with `getEntryCount()` / `getEntryAt(i)`, valid only during the
|
|
2238
2251
|
call, so a listener copies what it keeps — `patch` is what an update
|
|
2239
2252
|
wrote, `changedColumns` the names `markEntityChanged(slot, columns)` was given, either
|
|
2240
2253
|
`undefined` when the write carries none. A single write outside a batch delivers one `onContentChanged` as soon as it is filed; inside
|
|
2241
2254
|
`writeInBatch` (or `openBatch` / `closeBatch`) on the collection the writes are queued on the store slot and
|
|
2242
|
-
delivered as ONE `
|
|
2255
|
+
delivered as ONE `onManyContentsChanged` when the outermost batch closes. Code inside the batch reads the index
|
|
2243
2256
|
as it was before the batch.
|
|
2244
2257
|
This hook is the only seam an index uses; an index of your own is a listener.
|
|
2245
2258
|
|
|
@@ -2254,10 +2267,10 @@ return the same kind of slot, written when any column of any member changes. A g
|
|
|
2254
2267
|
`slot.owner.unholdSlot(slot)`.
|
|
2255
2268
|
|
|
2256
2269
|
```ts
|
|
2257
|
-
const decksService = createOIMSlotCollectionIndexService(
|
|
2270
|
+
const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2258
2271
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2259
2272
|
const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
|
|
2260
|
-
effectService.disposeEffect(effectService.createEffect(db,
|
|
2273
|
+
effectService.disposeEffect(effectService.createEffect(db, () => {}, [rankOfSpades]));
|
|
2261
2274
|
decksService.columnSlotService.unholdColumn(rankOfSpades);
|
|
2262
2275
|
```
|
|
2263
2276
|
|
|
@@ -2266,7 +2279,7 @@ decksService.columnSlotService.unholdColumn(rankOfSpades);
|
|
|
2266
2279
|
```ts signature
|
|
2267
2280
|
createOIMSortedComputedService<TKey, TEntity extends object, TSlots, TSourceKey, TSourceStore, TComputedStore?>({
|
|
2268
2281
|
indexService: OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore>;
|
|
2269
|
-
sortColumns?: readonly TOIMSortColumn<TEntity>[]; // {
|
|
2282
|
+
sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
|
|
2270
2283
|
comparator?: (left: TEntity, right: TEntity) => number;
|
|
2271
2284
|
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2272
2285
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
|
|
@@ -2309,13 +2322,13 @@ slots sorted. A hold is given back with `slot.owner.unholdSlot(slot)`.
|
|
|
2309
2322
|
to a member as with `comparator` alone.
|
|
2310
2323
|
|
|
2311
2324
|
```ts
|
|
2312
|
-
const cardsInDeckService = createOIMSlotCollectionIndexService(
|
|
2325
|
+
const cardsInDeckService = createOIMSlotCollectionIndexService({
|
|
2313
2326
|
entityService: cardService,
|
|
2314
2327
|
keyOf: (card: Card) => card.deck,
|
|
2315
2328
|
});
|
|
2316
2329
|
createOIMSortedComputedService({
|
|
2317
2330
|
indexService: cardsInDeckService,
|
|
2318
|
-
sortColumns: [{
|
|
2331
|
+
sortColumns: [{ columnName: 'title', direction: 'asc' }, { columnName: 'rank', direction: 'asc' }],
|
|
2319
2332
|
comparator: (l, r) => l.title.localeCompare(r.title, 'de') || l.rank - r.rank,
|
|
2320
2333
|
}).createSortedStoreSlot(cardsInDeckService.createIndexStoreSlot(cards));
|
|
2321
2334
|
// a write to `title` or `rank` re-sorts; a write to `note` does not
|
|
@@ -2328,17 +2341,17 @@ SECOND array of `createEffect`. A re-sort that yields the same sequence
|
|
|
2328
2341
|
(`oimSameSlotSequence`) keeps the previous array, so its identity is stable.
|
|
2329
2342
|
|
|
2330
2343
|
```ts
|
|
2331
|
-
const decksService = createOIMSlotCollectionIndexService(
|
|
2344
|
+
const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2332
2345
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2333
2346
|
const byRankInDeckService = createOIMSortedComputedService({
|
|
2334
2347
|
indexService: decksService,
|
|
2335
|
-
sortColumns: [{
|
|
2348
|
+
sortColumns: [{ columnName: 'rank', direction: 'asc' }],
|
|
2336
2349
|
});
|
|
2337
2350
|
const byRankInDeck = byRankInDeckService.createSortedStoreSlot(decks);
|
|
2338
2351
|
const spadesByRank = byRankInDeckService.computedCollectionService.holdSlotByKey(byRankInDeck, 'spades');
|
|
2339
|
-
const sortedEffect = effectService.createEffect(db,
|
|
2352
|
+
const sortedEffect = effectService.createEffect(db, () => {
|
|
2340
2353
|
render(spadesByRank.computedValueService.getValueForSlot(spadesByRank));
|
|
2341
|
-
});
|
|
2354
|
+
}, [], [spadesByRank]);
|
|
2342
2355
|
effectService.disposeEffect(sortedEffect);
|
|
2343
2356
|
spadesByRank.owner.unholdSlot(spadesByRank);
|
|
2344
2357
|
```
|
|
@@ -2350,7 +2363,7 @@ index. `createOIMSlotCollectionSortedArrayIndexService` keeps the index's own bu
|
|
|
2350
2363
|
the user picks one at construction.
|
|
2351
2364
|
|
|
2352
2365
|
```ts signature
|
|
2353
|
-
createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>(
|
|
2366
|
+
createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>({
|
|
2354
2367
|
entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as createOIMSlotCollectionIndexService, scalar key
|
|
2355
2368
|
sortColumns: readonly TOIMSortColumn<TEntity>[]; // REQUIRED
|
|
2356
2369
|
bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService?, columnNames?,
|
|
@@ -2359,10 +2372,10 @@ createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKe
|
|
|
2359
2372
|
|
|
2360
2373
|
```ts
|
|
2361
2374
|
declare const someCardSlot: TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
2362
|
-
const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService(
|
|
2375
|
+
const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService({
|
|
2363
2376
|
entityService: cardService,
|
|
2364
2377
|
columns: ['deck'],
|
|
2365
|
-
sortColumns: [{
|
|
2378
|
+
sortColumns: [{ columnName: 'rank', direction: 'asc' }, { columnName: 'id', direction: 'desc' }],
|
|
2366
2379
|
});
|
|
2367
2380
|
const cardsByDeck = cardsByDeckService.createIndexStoreSlot(cards);
|
|
2368
2381
|
const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
|
|
@@ -2399,8 +2412,8 @@ filed under now, so a moved entity slot leaves its old bucket in O(1), also unde
|
|
|
2399
2412
|
writer. Only the index writes it, through the `IOIMIndexKeysService` (`get(slot, position)`,
|
|
2400
2413
|
`set(slot, position, indexKey)`), at the position `createIndexStoreSlot` allocated
|
|
2401
2414
|
(`indexStoreSlot.indexKeysPosition`). Positions are allocated per entity store slot
|
|
2402
|
-
(`sourceStoreSlot.indexKeysPositions`). Pick the strategy
|
|
2403
|
-
(default `
|
|
2415
|
+
(`sourceStoreSlot.indexKeysPositions`). Pick the strategy with `new OIMDefaults({ indexKeysService })` passed as `defaults`
|
|
2416
|
+
(default `OIMAppendingIndexKeysService.INSTANCE`):
|
|
2404
2417
|
|
|
2405
2418
|
| strategy | on `disposeIndexStoreSlot` | cost |
|
|
2406
2419
|
|---|---|---|
|
|
@@ -2429,7 +2442,7 @@ type TOIMComputedSources = {
|
|
|
2429
2442
|
In a `createSources` result (of `createOIMComputedService`'s options, or of `createStoreSlot`) both `sourceSlots` and `sourceComputeds` are optional; a missing one means none.
|
|
2430
2443
|
|
|
2431
2444
|
```ts signature
|
|
2432
|
-
type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<
|
|
2445
|
+
type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<TOIMComputedDerivation> & {
|
|
2433
2446
|
readonly owner: IOIMSlotOwner;
|
|
2434
2447
|
readonly computedValueService: IOIMComputedValueService<TValue>;
|
|
2435
2448
|
readonly value: TValue;
|
|
@@ -2475,17 +2488,25 @@ class OIMComputedService<TKey, TValue, TComputedStore> {
|
|
|
2475
2488
|
}
|
|
2476
2489
|
|
|
2477
2490
|
createOIMComputedService<TKey, TValue, TComputedStore = Map<TKey, TOIMComputedSlot<TValue, TKey>>>({
|
|
2478
|
-
createSources?: (
|
|
2491
|
+
createSources?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>;
|
|
2479
2492
|
derive?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue;
|
|
2480
2493
|
sameValueGuard?: TOIMSameValueGuard<TValue>; // default Object.is
|
|
2494
|
+
releaseValue?: TOIMComputedValueReleaser<TValue>; // default: gives back nothing
|
|
2481
2495
|
initialValue?: TValue;
|
|
2482
2496
|
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2483
|
-
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; //
|
|
2497
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // defaults.computedColumnSlotStoreService, then a shared map store
|
|
2484
2498
|
columnNames?: readonly Extract<keyof TValue, string>[];
|
|
2485
2499
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore>; // Map; REQUIRED for any other store
|
|
2486
2500
|
}): OIMComputedService<TKey, TValue, TComputedStore>
|
|
2487
2501
|
```
|
|
2488
2502
|
|
|
2503
|
+
**`releaseValue(value)` gives back what a derived value holds** (a request in flight, a timer). It is called
|
|
2504
|
+
with the previous value when `derive` produced one `sameValueGuard` calls different; with a new object the
|
|
2505
|
+
guard calls equal (the slot keeps the old one, the new one is dropped); with the current value when the
|
|
2506
|
+
computed slot is released; with the value a `derive` returned after its own slot was released (not stored).
|
|
2507
|
+
It is not called when `derive` returns the object the slot already holds, and never with `initialValue`.
|
|
2508
|
+
`createOIMSingleComputedService` takes it too.
|
|
2509
|
+
|
|
2489
2510
|
The factory takes no db: a computed service is configuration, and each store slot takes its channel slot at
|
|
2490
2511
|
`createComputedStoreSlot(db, …)`.
|
|
2491
2512
|
|
|
@@ -2523,8 +2544,7 @@ A single computed store slot has no key and a store of one slot. `getSingleSlot(
|
|
|
2523
2544
|
next flush unless something holds it — and `holdSlotByKey(storeSlot, undefined)` holds the live slot, minting
|
|
2524
2545
|
one if the previous is gone. A computed slot goes at its last unhold once no effect reads it
|
|
2525
2546
|
(`OIMSlotAtUnholdReleasePolicy`, the default), and gives back the holds it took on its
|
|
2526
|
-
sources. A last unhold during a flush releases at
|
|
2527
|
-
db's unused-slot collector, as for every non-empty slot. The holds a computed takes on its sources and
|
|
2547
|
+
sources. A last unhold during a flush releases it at once, as for every slot. The holds a computed takes on its sources and
|
|
2528
2548
|
gives back are one stateless service, shared by both shapes.
|
|
2529
2549
|
|
|
2530
2550
|
**`createSources` takes the key and the store slot, and nothing else. There is no `keep`.** A build that
|
|
@@ -2567,7 +2587,7 @@ over the carrier computed, its value `carrier.value[column]` (a tuple of the gro
|
|
|
2567
2587
|
the whole value for the any-column), compared with `Object.is`. It is a `TOIMComputedColumnSlot`
|
|
2568
2588
|
(`TOIMComputedSlot<unknown, string>`), so it goes in the SECOND array of `createEffect`, and its value
|
|
2569
2589
|
is read off `columnSlot.value`. It wakes an effect only when that field of the derived value changes.
|
|
2570
|
-
Pinned by `
|
|
2590
|
+
Pinned by `computed/tests/what-a-computed-column-wakes-on.test.ts`.
|
|
2571
2591
|
|
|
2572
2592
|
```ts
|
|
2573
2593
|
type Summary = { top: number; count: number };
|
|
@@ -2579,7 +2599,7 @@ const summary = summaryService.createComputedStoreSlot(
|
|
|
2579
2599
|
);
|
|
2580
2600
|
const summarySingleSlotService = summaryService.computedSingleSlotService;
|
|
2581
2601
|
const summaryTop = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(summary), 'top');
|
|
2582
|
-
const topEffect = effectService.createEffect(db,
|
|
2602
|
+
const topEffect = effectService.createEffect(db, () => { paint(slot.entity); }, [], [summaryTop]);
|
|
2583
2603
|
effectService.disposeEffect(topEffect);
|
|
2584
2604
|
summarySingleSlotService.columnSlotService.unholdColumn(summaryTop);
|
|
2585
2605
|
```
|
|
@@ -2619,7 +2639,7 @@ const orderService = createOIMBucketMemoComputedService({
|
|
|
2619
2639
|
});
|
|
2620
2640
|
const order = orderService.createBucketMemoStoreSlot(ordered);
|
|
2621
2641
|
const deck = orderService.computedCollectionService.holdSlotByKey(order, 'spades'); // mints the bucket too, and holds it
|
|
2622
|
-
effectService.createEffect(db,
|
|
2642
|
+
effectService.createEffect(db, () => render(deck.computedValueService.getValueForSlot(deck)), [], [deck]);
|
|
2623
2643
|
```
|
|
2624
2644
|
|
|
2625
2645
|
---
|
|
@@ -2680,8 +2700,8 @@ are holding; neither is something you build by hand.
|
|
|
2680
2700
|
| Store | Key type |
|
|
2681
2701
|
|---|---|
|
|
2682
2702
|
| `OIMSlotMapStoreService<TSlot>` | `TOIMStoreKey` = `string \| number` |
|
|
2683
|
-
| `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `
|
|
2684
|
-
| `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `
|
|
2703
|
+
| `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `OIMScalarKeyTreeWalk.INSTANCE` | `readonly TOIMStoreKey[]` — **a chain of VALUES** |
|
|
2704
|
+
| `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `OIMCompositeKeyTreeWalk.INSTANCE` | `readonly TOIMEntitySlot[]` — **a chain of entity SLOTS** |
|
|
2685
2705
|
| `OIMSingleSlotStoreService.INSTANCE` | `void` — one slot per store slot, its store a `TOIMSingleSlotStore` (`{ slot }`); what every single is built on |
|
|
2686
2706
|
|
|
2687
2707
|
Two stores for keys, one per shape of address, and the single-slot store for no key. The chain store descends one `Map`
|
|
@@ -2722,8 +2742,8 @@ deletes nothing and returns `undefined`.
|
|
|
2722
2742
|
|
|
2723
2743
|
| Walk | Class | Shared instance | What a composite key part is | Holds? |
|
|
2724
2744
|
|---|---|---|---|---|
|
|
2725
|
-
| scalar | `OIMScalarKeyTreeWalk` | `
|
|
2726
|
-
| composite | `OIMCompositeKeyTreeWalk` | `
|
|
2745
|
+
| scalar | `OIMScalarKeyTreeWalk` | `OIMScalarKeyTreeWalk.INSTANCE` | `TOIMStoreKey` | no |
|
|
2746
|
+
| composite | `OIMCompositeKeyTreeWalk` | `OIMCompositeKeyTreeWalk.INSTANCE` | `TOIMEntitySlot` | **yes — one hold per edge** |
|
|
2727
2747
|
|
|
2728
2748
|
**The hold is the whole difference.** `OIMCompositeKeyTreeWalk.mintNodeAt` calls
|
|
2729
2749
|
`holdCompositeKeyPart` the first time it creates an edge, which runs
|
|
@@ -2745,10 +2765,10 @@ type TTallyStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Tally,
|
|
|
2745
2765
|
|
|
2746
2766
|
const tallyService = createOIMSlotCollectionService<
|
|
2747
2767
|
ReturnType<typeof createOIMSlotContentEntityService<Tally, TScalarChain>>, TTallyStore
|
|
2748
|
-
>(
|
|
2749
|
-
slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(
|
|
2768
|
+
>({
|
|
2769
|
+
slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(),
|
|
2750
2770
|
slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
|
|
2751
|
-
|
|
2771
|
+
OIMScalarKeyTreeWalk.INSTANCE,
|
|
2752
2772
|
),
|
|
2753
2773
|
});
|
|
2754
2774
|
const tallies = tallyService.createStoreSlot(db);
|
|
@@ -2775,10 +2795,10 @@ type TEnrolmentStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<En
|
|
|
2775
2795
|
|
|
2776
2796
|
const enrolmentService = createOIMSlotCollectionService<
|
|
2777
2797
|
ReturnType<typeof createOIMSlotContentEntityService<Enrolment, TChain>>, TEnrolmentStore
|
|
2778
|
-
>(
|
|
2779
|
-
slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(
|
|
2798
|
+
>({
|
|
2799
|
+
slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(),
|
|
2780
2800
|
slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
|
|
2781
|
-
|
|
2801
|
+
OIMCompositeKeyTreeWalk.INSTANCE,
|
|
2782
2802
|
),
|
|
2783
2803
|
});
|
|
2784
2804
|
const enrolments = enrolmentService.createStoreSlot(db);
|
|
@@ -2788,7 +2808,7 @@ enrolmentService.getSlotByKey(enrolments, [alice, admins]);
|
|
|
2788
2808
|
|
|
2789
2809
|
**Never put a scalar in a composite-walk key.** It is a type error, and
|
|
2790
2810
|
past a cast it throws a TypeError inside the mint, setting `holdCount` on a
|
|
2791
|
-
string, after the edge is already in the tree. `
|
|
2811
|
+
string, after the edge is already in the tree. `OIMScalarKeyTreeWalk.INSTANCE` is
|
|
2792
2812
|
the walk for that key — the same store class, the other strategy.
|
|
2793
2813
|
|
|
2794
2814
|
**Compare a chain with the strategy that matches it**: `oimSameSlotSequence` for identities
|
|
@@ -2801,8 +2821,7 @@ address" for the address the tree is looking at.
|
|
|
2801
2821
|
## Lifetime — the owner protocol
|
|
2802
2822
|
|
|
2803
2823
|
**`slot.owner` is the only reference a slot carries, and it is never
|
|
2804
|
-
`undefined`.** Every slot minted anywhere has one
|
|
2805
|
-
`OIMDefaults.STANDALONE_SLOT_OWNER.getValue()`, a slot a collection filed carries that collection type's slot owner (an `OIMSlotOwnerService`) — keyed or single alike — and a slot that has been released KEEPS its owner
|
|
2824
|
+
`undefined`.** Every slot minted anywhere has one: a slot a collection filed carries that collection type's slot owner (an `OIMSlotOwnerService`) — keyed or single alike — and a slot that has been released KEEPS its owner
|
|
2806
2825
|
and raises the RELEASED flag in `slot.slotFlags` instead, which the doors below read.
|
|
2807
2826
|
|
|
2808
2827
|
```ts signature
|
|
@@ -2828,18 +2847,15 @@ At a deadline the release queue reads its MARKER off the slot, decides, and call
|
|
|
2828
2847
|
|
|
2829
2848
|
**`OIMSlotOwner`** is the base class and it is concrete: it implements all five.
|
|
2830
2849
|
Its `releaseSlotIfUnheld` does nothing, its
|
|
2831
|
-
`unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `
|
|
2850
|
+
`unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMEditable` with no
|
|
2832
2851
|
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 `
|
|
2852
|
+
`slot.slotFlags`; the engine owns bits `1`, `2`, `4`, `8`, `16` and `32`, so a library starts at `64`;
|
|
2834
2853
|
it is on the owner so a subclass can hook every such mutation), and a `protected
|
|
2835
2854
|
onSlotIdle` does nothing; a `protected assertNotReleased` is what every refusing
|
|
2836
2855
|
door above calls. `OIMSlotOwnerService` overrides `onSlotIdle` and
|
|
2837
2856
|
`releaseSlotIfUnheld`, asking its release policy and its content service; the clock is the slot's channel slot's.
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
computed slot nobody has filed. It never releases anything, which is exactly what
|
|
2841
|
-
"filed nowhere, so nothing can unfile it" means. A single's slot is not such a slot: its
|
|
2842
|
-
owner is its collection type's slot owner, and that release policy applies to it.
|
|
2857
|
+
A single's slot is owned the same way: its owner is its collection type's slot owner, and that release policy
|
|
2858
|
+
applies to it.
|
|
2843
2859
|
|
|
2844
2860
|
### The column owner
|
|
2845
2861
|
|
|
@@ -2877,12 +2893,12 @@ so a slot with any live column slot is never released.
|
|
|
2877
2893
|
### Disposers
|
|
2878
2894
|
|
|
2879
2895
|
The disposers this package hands back are the unsubscribes `OIMChannelService.INSTANCE.subscribeAfterEachFrame`
|
|
2880
|
-
and its siblings return, and the one `effectService.subscribeEffect(db,
|
|
2896
|
+
and its siblings return, and the one `effectService.subscribeEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`
|
|
2881
2897
|
returns. An effect from `createEffect` has none, so the caller writes
|
|
2882
2898
|
`() => effectService.disposeEffect(effect)`. A hold is given back through its owner, not through a disposer.
|
|
2883
2899
|
|
|
2884
2900
|
```ts
|
|
2885
|
-
const effect = effectService.createEffect(db,
|
|
2901
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
2886
2902
|
const disposers: TOIMDisposer[] = [];
|
|
2887
2903
|
disposers.push(() => { effectService.disposeEffect(effect); });
|
|
2888
2904
|
disposers.push(() => { slot.owner.unholdSlot(slot); });
|
|
@@ -2933,15 +2949,15 @@ leave the longest.
|
|
|
2933
2949
|
type TOIMStoreKey = string | number;
|
|
2934
2950
|
type TOIMDisposer = () => void;
|
|
2935
2951
|
type TOIMEffectBody = () => (() => void) | void; // a returned function is the undo
|
|
2936
|
-
type
|
|
2952
|
+
type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
|
|
2937
2953
|
type TOIMSameValueGuard<TValue = unknown> = (prev: TValue, next: TValue) => boolean;
|
|
2938
2954
|
type TOIMChannelScheduler = (flush: () => void) => void;
|
|
2939
|
-
type TOIMSortColumn<TEntity extends object> = {
|
|
2955
|
+
type TOIMSortColumn<TEntity extends object> = { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' };
|
|
2940
2956
|
type TOIMSortableColumnName<TEntity extends object> = // a column whose values are string | number | bigint
|
|
2941
2957
|
{ [TColumn in keyof TEntity]-?: NonNullable<TEntity[TColumn]> extends string | number | bigint ? TColumn : never }[keyof TEntity] & string;
|
|
2942
2958
|
|
|
2943
2959
|
// EVERY property is `readonly`. A slot is written through its owner and its
|
|
2944
|
-
// service, never by assigning to it; `
|
|
2960
|
+
// service, never by assigning to it; `TOIMEditable` is how the engine
|
|
2945
2961
|
// itself drops that guard where it is the one mutator.
|
|
2946
2962
|
type TOIMSlot = {
|
|
2947
2963
|
readonly effects: TOIMEffect[] | undefined;
|
|
@@ -2952,9 +2968,9 @@ type TOIMSlot = {
|
|
|
2952
2968
|
readonly releaseDueAfterWriteMs: number; // -1 until a write stamps it
|
|
2953
2969
|
readonly releaseDueAfterIdleMs: number; // -1 until idleness stamps it
|
|
2954
2970
|
readonly releaseDueAfterUnholdMs: number; // -1 until the last unhold stamps it
|
|
2955
|
-
readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2
|
|
2971
|
+
readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2; bits up to 32 are the engine's
|
|
2956
2972
|
readonly columnSlotStore: unknown; // the column slots this slot carries; undefined while none
|
|
2957
|
-
readonly storeSlot: TOIMStoreSlot
|
|
2973
|
+
readonly storeSlot: TOIMStoreSlot; // the store slot it belongs to; a column slot's is its carrier's
|
|
2958
2974
|
readonly owner: IOIMSlotOwner; // never undefined, never cleared
|
|
2959
2975
|
};
|
|
2960
2976
|
|
|
@@ -2964,7 +2980,7 @@ type TOIMSlot = {
|
|
|
2964
2980
|
type TOIMStoreSlot<TStore = unknown, TSlot extends TOIMSlot = TOIMSlot, TPatch = unknown> = {
|
|
2965
2981
|
readonly store: TStore;
|
|
2966
2982
|
readonly channelSlot: TOIMChannelSlot;
|
|
2967
|
-
readonly contentChangeListeners:
|
|
2983
|
+
readonly contentChangeListeners: IOIMSlotContentChangeListener<TSlot, TPatch>[] | undefined;
|
|
2968
2984
|
readonly contentChangeListenerReadColumnNames: (readonly string[] | undefined)[] | undefined;
|
|
2969
2985
|
readonly indexKeysPositions: TOIMIndexKeysPositions | undefined;
|
|
2970
2986
|
readonly sourceStoreSlot: TOIMStoreSlot | undefined; // an index store slot's source
|
|
@@ -2981,7 +2997,7 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
|
|
|
2981
2997
|
TOIMStoreSlot<TStore, TBucketSlot, TBucketPatch> & {
|
|
2982
2998
|
readonly sourceStoreSlot: TOIMStoreSlot<TSourceStore, TSourceSlot, TSourcePatch>;
|
|
2983
2999
|
readonly indexKeysPosition: number;
|
|
2984
|
-
readonly contentChangeListener:
|
|
3000
|
+
readonly contentChangeListener: IOIMSlotContentChangeListener<TSourceSlot, TSourcePatch>;
|
|
2985
3001
|
readonly requestedKeys: readonly TKey[] | undefined;
|
|
2986
3002
|
readonly bucketColumnNames: Set<string>;
|
|
2987
3003
|
readonly isAnyBucketColumnRead: boolean;
|
|
@@ -2991,10 +3007,10 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
|
|
|
2991
3007
|
type TOIMComputedStoreSlot<TStore = unknown, TValue = unknown, TKey = unknown> =
|
|
2992
3008
|
TOIMStoreSlot<TStore, TOIMComputedSlot<TValue, TKey>> & {
|
|
2993
3009
|
readonly derive: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue) | undefined;
|
|
2994
|
-
readonly createSources: ((
|
|
3010
|
+
readonly createSources: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>) | undefined;
|
|
2995
3011
|
};
|
|
2996
3012
|
|
|
2997
|
-
type
|
|
3013
|
+
type TOIMEditable<TSlot> = { -readonly [K in keyof TSlot]: TSlot[K] };
|
|
2998
3014
|
|
|
2999
3015
|
type TOIMKeyedSlot<TKey = unknown> = TOIMSlot & { readonly key: TKey };
|
|
3000
3016
|
type TOIMIndexableSlot<TKey = unknown> = TOIMKeyedSlot<TKey> & { readonly indexKeys: unknown[] | undefined };
|
|
@@ -3006,7 +3022,7 @@ type TOIMEntitySlot<TEntity extends object = object, TKey = unknown> = TOIMIndex
|
|
|
3006
3022
|
|
|
3007
3023
|
// `data` belongs to the consumer: the core never reads or writes it, and clearing or
|
|
3008
3024
|
// removing the slot (`slotContentService.clearSlotContent(slot)`) does not reset it. Each index keeps its
|
|
3009
|
-
// last key for the entity slot in `indexKeys`, through the
|
|
3025
|
+
// last key for the entity slot in `indexKeys`, through the index's `IOIMIndexKeysService`.
|
|
3010
3026
|
|
|
3011
3027
|
type TOIMColumnSlot = TOIMSlot & {
|
|
3012
3028
|
readonly carrierSlot: TOIMSlot; // the entity slot or bucket slot it hangs off
|
|
@@ -3040,14 +3056,14 @@ type TOIMEntityColumnName<TEntity extends object> =
|
|
|
3040
3056
|
[TEntity] extends [never] ? never
|
|
3041
3057
|
: object extends TEntity ? string : keyof TEntity & string;
|
|
3042
3058
|
|
|
3043
|
-
type
|
|
3059
|
+
type TOIMComputedDerivation = TOIMComputedSources & { // what a computed slot and a TOIMEffect share
|
|
3044
3060
|
ranAtRevision: number; ranAtEpoch: number;
|
|
3045
3061
|
};
|
|
3046
3062
|
|
|
3047
3063
|
type TOIMTimer = (run: () => void, inMs: number) => TOIMDisposer;
|
|
3048
3064
|
```
|
|
3049
3065
|
|
|
3050
|
-
**`
|
|
3066
|
+
**`TOIMEditable<TSlot>` strips `readonly` from every property.** It is exported
|
|
3051
3067
|
because the rule "data has one mutator" is enforced by the slot types being
|
|
3052
3068
|
readonly everywhere else: a class that IS the sole mutator of a slot's property
|
|
3053
3069
|
casts through this type at the one place it writes, and every other reader is
|
|
@@ -3069,7 +3085,7 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3069
3085
|
|
|
3070
3086
|
## Full export list
|
|
3071
3087
|
|
|
3072
|
-
**
|
|
3088
|
+
**230 names, every export of `packages/core/src/index.ts`, grouped by topic.**
|
|
3073
3089
|
|
|
3074
3090
|
*Channel and database slots* — `OIMChannelService` · `TOIMChannelSlot` · `TOIMChannelSlotOptions` · `createOIMChannelSlot` ·
|
|
3075
3091
|
`IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot` · `IOIMFlushSchedulerListener`
|
|
@@ -3086,7 +3102,7 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3086
3102
|
`OIMSlotOwnerService` (**type only**) · `TOIMSlotOwnerServiceOptions` · `IOIMSlotContentService` · `IOIMSlotContentWriter` ·
|
|
3087
3103
|
`IOIMKeyedSlotCreator` · `IOIMSingleSlotCreator` · `IOIMSlotFilingService` · `TOIMSlotContentServiceKey` ·
|
|
3088
3104
|
`TOIMSlotContentServiceSlot` · `TOIMSlotContentServiceSingleSlot` · `TOIMSlotContentServiceContent` ·
|
|
3089
|
-
`TOIMSlotContentServicePatch` · `
|
|
3105
|
+
`TOIMSlotContentServicePatch` · `OIMSlotContentChangeListenerService`
|
|
3090
3106
|
|
|
3091
3107
|
*Content services* — `OIMSlotContentEntityService` · `TOIMSlotContentEntityServiceOptions` ·
|
|
3092
3108
|
`createOIMSlotContentEntityService` · `TOIMEntitySlotOwnerService` · `TOIMEntityContentSlot` · `OIMSlotContentBucketService` ·
|
|
@@ -3108,20 +3124,20 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3108
3124
|
written after that clock reading — `channelSlot.clock` read earlier.
|
|
3109
3125
|
|
|
3110
3126
|
*Derivations* — `OIMEffectService` · `IOIMEffectService` · `TOIMEffect` (**type only** —
|
|
3111
|
-
`OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `
|
|
3112
|
-
`
|
|
3113
|
-
`OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `
|
|
3127
|
+
`OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `TOIMSourceTrackingEffectBody` ·
|
|
3128
|
+
`OIMSourceTracker` (**type only** — the service builds it) · `TOIMComputedSlot` · `TOIMComputedSources` · `TOIMComputedValueReleaser` · `OIMSourceTrackingComputedService` · `OIMSingleSourceTrackingComputedService` · `createOIMSourceTrackingComputedService` · `createOIMSingleSourceTrackingComputedService` · `TOIMSourceTrackingComputedSlot` · `TOIMSourceTrackingComputedContentSlot` · `TOIMSourceTrackingComputedDerive` · `OIMSlotContentSourceTrackingComputedService` (**type only**) · `TOIMSourceTrackingComputedDerivationOptions` · `TOIMComputedServiceFactoryOptions` ·
|
|
3129
|
+
`OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `TOIMComputedDerivation`
|
|
3114
3130
|
|
|
3115
3131
|
`oimRefreshSourceComputeds(sourceComputeds)` brings every computed in the list current. A binding
|
|
3116
3132
|
read outside an effect (`useOIMValue` in `@oimdb/react`, `exoBinding` in `@oimdb/exodra`) calls it
|
|
3117
3133
|
on its `sourceComputeds` before `derive`, so the derive never reads a stale computed.
|
|
3118
3134
|
|
|
3119
|
-
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `
|
|
3135
|
+
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
|
|
3120
3136
|
`TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `OIMColumnSlotOwner` ·
|
|
3121
3137
|
`OIMPushColumnSlotOwner` · `OIMPullColumnSlotOwner` · `TOIMColumnSlotOwnerOptions` · `TOIMBucketSlot` ·
|
|
3122
3138
|
`TOIMSameValueGuard` · `OIMSlotOwner` ·
|
|
3123
3139
|
`IOIMSlotOwner` · `IOIMSlotEmptinessPolicy` ·
|
|
3124
|
-
`IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `
|
|
3140
|
+
`IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMSlotContentChangeListener`
|
|
3125
3141
|
|
|
3126
3142
|
`oimIsSlotEmpty(slot): boolean` reads the EMPTY bit of `slot.slotFlags`.
|
|
3127
3143
|
`oimIsSlotReleased(slot): boolean` reads its RELEASED bit.
|
|
@@ -3134,9 +3150,9 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3134
3150
|
`OIMSlotAtUnholdReleasePolicy` · `OIMSlotAtEmptyReleasePolicy` · `OIMSlotNeverReleasePolicy` · `OIMSlotEarliestReleasePolicy` ·
|
|
3135
3151
|
`TOIMTimer` · `oimSliceOfMs` · `OIM_SLICE_LENGTH_MS`
|
|
3136
3152
|
|
|
3137
|
-
*Entity writers* — `IOIMEntityWriter` · `
|
|
3138
|
-
`
|
|
3139
|
-
`oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `
|
|
3153
|
+
*Entity writers* — `IOIMEntityWriter` · `createOIMInPlaceEntityWriter` ·
|
|
3154
|
+
`createOIMReplacingEntityWriter` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
|
|
3155
|
+
`oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimGuardedPatchEntityColumns` · `oimGuardedPatchEntityColumn` · `oimPutEntityColumns` · `oimGuardedPutEntityColumns` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
|
|
3140
3156
|
|
|
3141
3157
|
*Buckets* — `OIMBucketCommands` · `TOIMBucketCommandsOptions` · `IOIMBucketCommands` ·
|
|
3142
3158
|
`IOIMBucketMemberSlotsArrayService` · `OIMBucketMemberSlotsArrayService` · `OIMBucketUniqueMemberSlotsArrayService` ·
|
|
@@ -3160,13 +3176,12 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3160
3176
|
`TOIMSlotStoreTree` · `IOIMKeyTreeWalk` · `TOIMCompositeKeyNode` ·
|
|
3161
3177
|
`OIMKeyTreeWalk` · `OIMCompositeKeyTreeWalk` · `OIMScalarKeyTreeWalk`
|
|
3162
3178
|
|
|
3163
|
-
*Assemblies* — `OIMDB` · `OIMDefaults` · `OIMDefaultValue` · `TOIMFactoryOptions`
|
|
3179
|
+
*Assemblies* — `OIMDB` · `OIMDefaults` · `TOIMDefaultsOptions` · `OIMDefaultValue` · `TOIMFactoryOptions`
|
|
3164
3180
|
|
|
3165
3181
|
*Comparison and diffing* — `oimSameValueZeroGuard` ·
|
|
3166
3182
|
`oimSameSlotSequence` · `oimSameKeySequence` · `oimOrderedListCommandsOfTransition` · `TOIMOrderedListCommand`
|
|
3167
3183
|
|
|
3168
|
-
*Shared defaults* — `
|
|
3169
|
-
`OIMDefaults.KEY_TREE_WALK_SCALAR` and `OIMDefaults.KEY_TREE_WALK_COMPOSITE`.
|
|
3184
|
+
*Shared defaults* — `OIMScalarKeyTreeWalk.INSTANCE` and `OIMCompositeKeyTreeWalk.INSTANCE`.
|
|
3170
3185
|
|
|
3171
3186
|
**Nothing else is exported.** If a symbol is not on this list, it does not exist.
|
|
3172
3187
|
|
|
@@ -3174,18 +3189,18 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3174
3189
|
|
|
3175
3190
|
## Known defects
|
|
3176
3191
|
|
|
3177
|
-
### A scalar in a
|
|
3192
|
+
### A scalar in a key of the composite key tree walk throws
|
|
3178
3193
|
|
|
3179
|
-
`OIMSlotTreeStoreService` built with `
|
|
3194
|
+
`OIMSlotTreeStoreService` built with `OIMCompositeKeyTreeWalk.INSTANCE` keys on
|
|
3180
3195
|
`readonly TOIMEntitySlot[]`, so a string or a number in the key throws
|
|
3181
3196
|
a TypeError past a cast, setting `holdCount` on a string. The store for a tuple
|
|
3182
3197
|
of scalars is `OIMSlotTreeStoreService` built with
|
|
3183
|
-
`
|
|
3198
|
+
`OIMScalarKeyTreeWalk.INSTANCE` —
|
|
3184
3199
|
see **Key stores** above. The fix is the other walk, never a cast.
|
|
3185
3200
|
|
|
3186
3201
|
### `TOIMEffect` is a type-only export
|
|
3187
3202
|
|
|
3188
|
-
`effectService.createEffect(...)` and `effectService.
|
|
3203
|
+
`effectService.createEffect(...)` and `effectService.createAndRunEffect(...)` are the whole of
|
|
3189
3204
|
legitimate construction. An object literal typed `TOIMEffect` subscribes to nothing and
|
|
3190
3205
|
schedules nothing: when one of its sources already moved in the open batch, a
|
|
3191
3206
|
later write to that source is swallowed by the coalescing guard and the effect
|
|
@@ -3199,8 +3214,8 @@ Every `@oimdb` package ships both formats — `dist/index.js` for `import`,
|
|
|
3199
3214
|
condition so a CommonJS consumer's declarations match its runtime), both named
|
|
3200
3215
|
in the `exports` map. A runtime loads a package once per format, so a process
|
|
3201
3216
|
that reaches the core both ways would hold two copies of every class and every
|
|
3202
|
-
module-level value — two
|
|
3203
|
-
|
|
3217
|
+
module-level value — two sets of built-in defaults and two `OIMDefaults` classes,
|
|
3218
|
+
so services built by the two copies share nothing.
|
|
3204
3219
|
|
|
3205
3220
|
So `src/index.ts` registers its copy under `Symbol.for('@oimdb/core')` on
|
|
3206
3221
|
`globalThis`, and the second copy to load throws
|
|
@@ -3218,8 +3233,8 @@ core objects around — through both spellings in one process. Pick one.
|
|
|
3218
3233
|
|
|
3219
3234
|
## Other packages
|
|
3220
3235
|
|
|
3221
|
-
`@oimdb/react`, `@oimdb/persist` (with its five backends) and `@oimdb/
|
|
3222
|
-
on the current core — persist and
|
|
3236
|
+
`@oimdb/react`, `@oimdb/persist` (with its five backends), `@oimdb/exodra` and `@oimdb/browser` build
|
|
3237
|
+
on the current core — persist, exodra and browser have sections of their own below. Every
|
|
3223
3238
|
remaining package imports only names the core exports. Read a package's own
|
|
3224
3239
|
source before generating code against it.
|
|
3225
3240
|
|
|
@@ -3252,7 +3267,7 @@ between render and subscribe is safe.
|
|
|
3252
3267
|
`useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)` holds the identities its keys name for as long as the
|
|
3253
3268
|
component is mounted (a `holdSlotByKey` per key on commit, the same holds given back on unmount or when the list
|
|
3254
3269
|
moves) and subscribes to nothing; it needs no provider.
|
|
3255
|
-
Singles are not keyed collections: `createOIMSingleSlotService(
|
|
3270
|
+
Singles are not keyed collections: `createOIMSingleSlotService({ slotContentService })` returns an
|
|
3256
3271
|
`OIMWritableSingleSlotService` (`getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`), read by
|
|
3257
3272
|
`useOIMSingleEntity(singleSlotService, singleSlotStoreSlot): TEntity | undefined` and
|
|
3258
3273
|
`useOIMSingleBucketSlots(singleSlotService, singleSlotStoreSlot): readonly TOIMEntitySlot<TEntity>[]` — the by-key
|
|
@@ -3264,11 +3279,12 @@ re-read the imports rather than trusting this table.
|
|
|
3264
3279
|
|
|
3265
3280
|
| Package | Imports from the core | Imports a name the core does not export? |
|
|
3266
3281
|
|---|---|---|
|
|
3267
|
-
| `@oimdb/persist` | `IOIMChannelMap`, `
|
|
3268
|
-
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `
|
|
3282
|
+
| `@oimdb/persist` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `IOIMEntityWriter`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMChannelClock`, `TOIMChannelSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMSingleSlotStore`, `TOIMSingleSlotStoreSlot`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimIsAnySlotChangedAfter`, `oimIsSlotReleased`, `oimSameValueZeroGuard`, `oimUnholdSlotsIfAlive` | no |
|
|
3283
|
+
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntityColumnName`, `TOIMEntitySlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
3269
3284
|
| `@oimdb/exodra` | `IOIMChannelMap`, `OIMChannelService`, `OIMEffectService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMComputedSlot`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMOrderedListCommand`, `TOIMSlot`, `TOIMStoreKey`, `oimOrderedListCommandsOfTransition`, `oimRefreshSourceComputeds`, `oimSameKeySequence` | no |
|
|
3270
3285
|
| `@oimdb/react` | `IOIMChannelMap`, `OIMComputedService`, `OIMDB`, `OIMEffectService`, `OIMSingleSlotService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMComputedSlot`, `TOIMComputedSources`, `TOIMComputedStoreSlot`, `TOIMDisposer`, `TOIMEntityColumnName`, `TOIMEntityColumnValue`, `TOIMEntitySlot`, `TOIMIndexableSlot`, `TOIMKeyedSlot`, `TOIMSingleSlotStore`, `TOIMSlot`, `TOIMStoreKey`, `TOIMStoreSlot`, `oimHoldSlots`, `oimRefreshSourceComputeds`, `oimSameKeySequence`, `oimUnholdSlotsIfAlive` | no |
|
|
3271
3286
|
| `@oimdb/devtools` | `IOIMChannelMap`, `IOIMStoreSlotListener`, `OIMChannelService`, `OIMComputedService`, `OIMDefaults`, `OIMSlotCollectionService`, `TOIMBucketSlot`, `TOIMComputedStoreSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEntitySlot`, `TOIMKeyedSlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
3287
|
+
| `@oimdb/browser` | `IOIMSlotSubscriptionListener`, `OIMChannelService`, `OIMDefaults`, `OIMSingleSlotService`, `OIMSlotContentEntityService`, `TOIMDisposer`, `TOIMEntityContentSlot`, `TOIMSlot`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`, `oimIsSlotReleased` | no |
|
|
3272
3288
|
| `@oimdb/async` | — | deprecated stub, `export {}` |
|
|
3273
3289
|
| `@oimdb/snapshot-manager` | — | empty stub |
|
|
3274
3290
|
|
|
@@ -3306,9 +3322,9 @@ import {
|
|
|
3306
3322
|
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3307
3323
|
|
|
3308
3324
|
const persistEntityWriter = new OIMPersistEntityWriter<Card, TOIMStoreKey>({
|
|
3309
|
-
entityWriter:
|
|
3325
|
+
entityWriter: createOIMInPlaceEntityWriter(),
|
|
3310
3326
|
});
|
|
3311
|
-
const persistedCardCollectionService = createOIMSlotCollectionService(
|
|
3327
|
+
const persistedCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>({ entityWriter: persistEntityWriter }) });
|
|
3312
3328
|
const persistedCardEntityStoreSlot = persistedCardCollectionService.createStoreSlot(db);
|
|
3313
3329
|
|
|
3314
3330
|
const persistMemoryBackend = new OIMPersistMemoryBackend();
|
|
@@ -3471,9 +3487,9 @@ import {
|
|
|
3471
3487
|
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3472
3488
|
|
|
3473
3489
|
type TDeckCardSlot = TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
3474
|
-
const deckCardCollectionService = createOIMSlotCollectionService(
|
|
3490
|
+
const deckCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
|
|
3475
3491
|
const deckCardEntityStoreSlot = deckCardCollectionService.createStoreSlot(db);
|
|
3476
|
-
const deckBucketService = createOIMSlotCollectionService(
|
|
3492
|
+
const deckBucketService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
3477
3493
|
const deckBucketStoreSlot = deckBucketService.createStoreSlot(db);
|
|
3478
3494
|
const spadesBucketSlot = deckBucketService.holdSlotByKey(deckBucketStoreSlot, 'spades');
|
|
3479
3495
|
|
|
@@ -3722,7 +3738,7 @@ import type { TOIMPersistVersionedShape } from '@oimdb/persist';
|
|
|
3722
3738
|
|
|
3723
3739
|
type TSettings = { theme: 'light' | 'dark'; density: 'compact' | 'comfortable' };
|
|
3724
3740
|
|
|
3725
|
-
const settingsSingleEntityService = createOIMSingleSlotService(
|
|
3741
|
+
const settingsSingleEntityService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<TSettings>() });
|
|
3726
3742
|
const settingsEntityStoreSlot = settingsSingleEntityService.createStoreSlot(db);
|
|
3727
3743
|
settingsSingleEntityService.writeSingleSlotContent(settingsEntityStoreSlot, { theme: 'light', density: 'compact' });
|
|
3728
3744
|
const itemByStorageKey = new Map<string, string>();
|
|
@@ -3869,9 +3885,9 @@ own properties, so a stored `__proto__` is data.
|
|
|
3869
3885
|
re-reads every member and every watched slot, and the source keeps one effect per
|
|
3870
3886
|
member entity slot.
|
|
3871
3887
|
- **The memory and JSON backends keep values by reference.** With
|
|
3872
|
-
`
|
|
3888
|
+
`createOIMInPlaceEntityWriter()` the stored entity is the live entity object, so an
|
|
3873
3889
|
in-place write moves the stored value before any commit, and `dehydrate()` returns
|
|
3874
|
-
the live objects. Use `
|
|
3890
|
+
the live objects. Use `createOIMReplacingEntityWriter()` with
|
|
3875
3891
|
`entityInserter: oimInsertEntityWithCopy` where the stored value must stay
|
|
3876
3892
|
what was committed, and serialize a dump at once.
|
|
3877
3893
|
- **Hydrate without a reconcile replaces.** The collection source deletes every
|
|
@@ -3892,7 +3908,7 @@ only on the type-only `@exodra/reactivity-types`.
|
|
|
3892
3908
|
|
|
3893
3909
|
### One door: `exoBinding(channelMap, binding)`
|
|
3894
3910
|
|
|
3895
|
-
A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, sourceSlots, sourceComputeds
|
|
3911
|
+
A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, onChange, sourceSlots, sourceComputeds)` —
|
|
3896
3912
|
and the value is read off the slots afterwards. So one adapter covers an entity, a
|
|
3897
3913
|
column, a bucket's slots and a derivation:
|
|
3898
3914
|
|
|
@@ -3948,7 +3964,7 @@ resolved before its entity arrived, an entity since removed.
|
|
|
3948
3964
|
|
|
3949
3965
|
- `exoSource(read, subscribe)` — the lazy, ref-counted base. For sources that are
|
|
3950
3966
|
**not** this engine's.
|
|
3951
|
-
- `exoCombine(
|
|
3967
|
+
- `exoCombine(fn, sources)` — fan-in across any `TOIMExoReadable`. Not glitch-free;
|
|
3952
3968
|
keep heavy fan-in inside one computed.
|
|
3953
3969
|
- `exoFollowKey(key, makeBindable)` — repoints as a selection moves. The factory
|
|
3954
3970
|
must return a `TOIMExoDisposable`.
|
|
@@ -3982,6 +3998,29 @@ memoised per list, so two lists showing one entity slot render it twice.
|
|
|
3982
3998
|
|
|
3983
3999
|
---
|
|
3984
4000
|
|
|
4001
|
+
## `@oimdb/browser`
|
|
4002
|
+
|
|
4003
|
+
Browser state as single slots. Each factory returns an `OIMSingleSlotService` (`TOIMBrowserSingleSlotService<TEntity>`)
|
|
4004
|
+
built with a slot subscription listener: the browser listener is added when the slot gets its first
|
|
4005
|
+
subscriber (directly or through a computed that reads it) and removed when the last one goes; the current
|
|
4006
|
+
value is written at the first subscription, and a write that would land during a flush is made right after it.
|
|
4007
|
+
|
|
4008
|
+
```ts signature
|
|
4009
|
+
createOIMBrowserOnlineSingleSlotService(options?: { defaults?: OIMDefaults }): TOIMBrowserSingleSlotService<TOIMBrowserOnlineEntity> // { isOnline }
|
|
4010
|
+
createOIMBrowserFocusSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserFocusEntity> // { isFocused }: document.hasFocus()
|
|
4011
|
+
createOIMBrowserVisibilitySingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserVisibilityEntity> // { isVisible }
|
|
4012
|
+
createOIMBrowserLocationSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserLocationEntity> // { href, pathname, search, hash }
|
|
4013
|
+
```
|
|
4014
|
+
|
|
4015
|
+
Read it like any single: `const onlineSlot = onlineService.getSingleSlot(onlineService.createStoreSlot(db))`, then
|
|
4016
|
+
an effect over `[onlineSlot]`, or `useOIMSingleEntity(onlineService, online)` in React. `isOnline` is
|
|
4017
|
+
`navigator.onLine` (a network interface exists, not a reachable internet); the location follows the
|
|
4018
|
+
Navigation API's `currententrychange` where it exists and `popstate` + `hashchange` otherwise, so without it a
|
|
4019
|
+
`history.pushState` is not seen until the next of those events. Without the API a sensor needs, the slot holds
|
|
4020
|
+
`true` (or an empty location). `OIMBrowserSlotSubscriptionListener` is the base class of the four listeners.
|
|
4021
|
+
|
|
4022
|
+
---
|
|
4023
|
+
|
|
3985
4024
|
## Performance
|
|
3986
4025
|
|
|
3987
4026
|
**No figure may be quoted for this engine, and none is quoted anywhere in this
|
|
@@ -4014,7 +4053,7 @@ The one quantity that needs no measurement, because it is arithmetic rather than
|
|
|
4014
4053
|
a reading: emptying a bucket one slot at a time on an array-driven bucket costs
|
|
4015
4054
|
n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `writeSlotContent`
|
|
4016
4055
|
and `clearSlotContent` go through one `drainSlots`. Counted, not timed, in
|
|
4017
|
-
`packages/core/src/
|
|
4056
|
+
`packages/core/src/bucket/array/tests/everyone-out-at-once-and-not-one-at-a-time.test.ts`, which
|
|
4018
4057
|
also pins `drainSlots` at zero element writes.
|
|
4019
4058
|
|
|
4020
4059
|
Fast properties are checked, not timed. Every slot kind is one object literal with
|