@oimdb/core 4.1.0 → 4.3.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 +59 -0
- package/README.md +218 -171
- package/dist/index.cjs +1073 -722
- package/dist/index.d.cts +267 -141
- package/dist/index.d.ts +267 -141
- package/dist/index.js +1056 -716
- package/llms.txt +365 -299
- 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,12 +692,12 @@ 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.
|
|
685
699
|
|
|
686
|
-
`@oimdb/react`'s `useOIMSlotsByKeys(
|
|
700
|
+
`@oimdb/react`'s `useOIMSlotsByKeys(storeSlot, keys)` does not use an arena: it takes a
|
|
687
701
|
plain `holdSlotByKey` per key on commit and gives the same holds back on unmount or when the key list moves.
|
|
688
702
|
|
|
689
703
|
```ts signature
|
|
@@ -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
|
|
@@ -776,10 +790,6 @@ type TOIMDBOptions = {
|
|
|
776
790
|
onBeforeFlush?: () => void; // start of the flush, before the frame; READ ONLY, a write is refused
|
|
777
791
|
getNowMs?: () => number; // the release collectors' clock; absent = Date.now
|
|
778
792
|
releaseTimer?: TOIMTimer; // the release collectors' timer; absent = an unref'd setTimeout
|
|
779
|
-
entityWriter?: IOIMEntityWriter; // default entity writer for every entity factory
|
|
780
|
-
indexKeysService?: IOIMIndexKeysService;
|
|
781
|
-
slotStamp?: IOIMSlotStamp<TOIMSlot>;
|
|
782
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; // default release policy for every factory
|
|
783
793
|
};
|
|
784
794
|
|
|
785
795
|
db.clock // TOIMChannelClock, the same object as db.defaultChannelSlot.clock
|
|
@@ -858,7 +868,7 @@ Every door below is on `OIMChannelService.INSTANCE` and takes the channel slot f
|
|
|
858
868
|
The channel slot's own fields that a caller reads: `frame` (`TOIMChannelFrame`, `{ isRunning, openedAtRevision,
|
|
859
869
|
openedAtEpoch, … }`; a frame carries no number, the clock reading it opened at is its boundary), `clock`
|
|
860
870
|
(`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),
|
|
871
|
+
`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
872
|
`collectorSlot`, `errorPolicy`, `isFlushing`. Everything else on it is engine state.
|
|
863
873
|
|
|
864
874
|
### A subscription hears only what happens after it
|
|
@@ -873,11 +883,11 @@ after it.
|
|
|
873
883
|
cardService.writeSlotContentByKey(cards, 'a', { id: 'a', title: 'A', rank: 1, deck: 'spades' });
|
|
874
884
|
const entitySlot = cardService.getSlotByKey(cards, 'a');
|
|
875
885
|
|
|
876
|
-
effectService.createEffect(db,
|
|
886
|
+
effectService.createEffect(db, () => { /* NEVER runs for the upsert above */ }, [entitySlot]);
|
|
877
887
|
db.flush();
|
|
878
888
|
|
|
879
|
-
// A first value is `
|
|
880
|
-
effectService.
|
|
889
|
+
// A first value is `createAndRunEffect`'s job, not `createEffect`'s:
|
|
890
|
+
effectService.createAndRunEffect(db, () => { paint(entitySlot.entity); }, [entitySlot]); // runs now
|
|
881
891
|
```
|
|
882
892
|
|
|
883
893
|
A later write in that same batch IS delivered — "after I subscribed" is decided
|
|
@@ -890,11 +900,11 @@ Told after **one** frame — the first that ends after the call — and then gon
|
|
|
890
900
|
The place that asks decides which frame that is.
|
|
891
901
|
|
|
892
902
|
```ts
|
|
893
|
-
effectService.createEffect(db,
|
|
903
|
+
effectService.createEffect(db, () => {
|
|
894
904
|
paint(slot.entity);
|
|
895
905
|
// the tail of THIS frame, after every other effect of it
|
|
896
906
|
OIMChannelService.INSTANCE.subscribeOnceAfterFrame(channelSlot, () => { measureAndScroll(); });
|
|
897
|
-
});
|
|
907
|
+
}, [slot]);
|
|
898
908
|
```
|
|
899
909
|
|
|
900
910
|
- **Registered from inside an effect body it runs in the tail of that very
|
|
@@ -927,10 +937,10 @@ cardService.writeInBatch(cards, () => {
|
|
|
927
937
|
});
|
|
928
938
|
```
|
|
929
939
|
|
|
930
|
-
- `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `
|
|
931
|
-
`try/finally` form and returns what its function returns.
|
|
940
|
+
- `openBatch(storeSlot)` / `closeBatch(storeSlot)` nest by a count on the store slot; `closeBatch` with no batch open
|
|
941
|
+
throws. `writeInBatch` is the `try/finally` form and returns what its function returns.
|
|
932
942
|
- **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, `
|
|
943
|
+
index on the collection) hears every write of the batch in ONE call, `onManyContentsChanged`, when the
|
|
934
944
|
outermost batch closes. Code inside the batch reads the index as it was before the batch.
|
|
935
945
|
- Effects are not batched by it: every write marks its slot on the channel as usual, and the frame delivers each
|
|
936
946
|
identity once whatever the batch did. A `flush()` inside a batch is allowed; the index catches up when the
|
|
@@ -1013,7 +1023,7 @@ OIMChannelService.INSTANCE.dropChannelSlot(db.defaultChannelSlot);
|
|
|
1013
1023
|
### Subscribing
|
|
1014
1024
|
|
|
1015
1025
|
```ts
|
|
1016
|
-
const effect = effectService.createEffect(db,
|
|
1026
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
1017
1027
|
effectService.disposeEffect(effect);
|
|
1018
1028
|
```
|
|
1019
1029
|
|
|
@@ -1048,10 +1058,10 @@ function is the undo**: it is called before the next run of that body and by
|
|
|
1048
1058
|
`disposeEffect`.
|
|
1049
1059
|
|
|
1050
1060
|
```ts
|
|
1051
|
-
effectService.createEffect(db,
|
|
1061
|
+
effectService.createEffect(db, () => {
|
|
1052
1062
|
const timer = setInterval(poll, 1000);
|
|
1053
1063
|
return () => clearInterval(timer);
|
|
1054
|
-
});
|
|
1064
|
+
}, [slot]);
|
|
1055
1065
|
```
|
|
1056
1066
|
|
|
1057
1067
|
The return is checked with `typeof`, not for truthiness — `() => set.add(key)`
|
|
@@ -1059,14 +1069,28 @@ hands back the `Set` — so anything that is not a function is discarded. The un
|
|
|
1059
1069
|
is cleared before it is called, so one that throws is not called again by the
|
|
1060
1070
|
`disposeEffect` behind it.
|
|
1061
1071
|
|
|
1062
|
-
**
|
|
1063
|
-
|
|
1072
|
+
**A derivation that finds its sources as it reads:** `createOIMSourceTrackingComputedService<TKey, TValue>({ derive, … })` and
|
|
1073
|
+
`createOIMSingleSourceTrackingComputedService<TValue>({ derive, … })`. `derive: TOIMSourceTrackingComputedDerive<TValue, TSlot>` is
|
|
1074
|
+
`(computedSlot, sourceTracker: OIMSourceTracker) => TValue`; there is no `createSources`, and `createComputedStoreSlot(db)` takes no
|
|
1075
|
+
per-store `derive`. Every derive holds what it read and gives back what it no longer reads (new holds first). The slot
|
|
1076
|
+
(`TOIMSourceTrackingComputedSlot<TValue, TKey>` / `TOIMSourceTrackingComputedContentSlot<TValue>`, `sourceTracker` set) is read
|
|
1077
|
+
through `sourceTracker.readSourceComputedValue` (in `createSourceTrackingEffect` or another source-tracking derive) or through `createEffect`'s fifth list; `createEffect`'s `sourceComputeds` and
|
|
1078
|
+
a `createSources` list refuse it by type, because a declared effect subscribes once to the slots under a derivation. When a derive
|
|
1079
|
+
switches sources, a source-tracking effect or derivation over it re-subscribes even when the value held. A body that cannot read
|
|
1080
|
+
through a tracker may pass them as the fifth list, `createEffect(db, body, sourceSlots, sourceComputeds, sourceTrackingComputeds)`;
|
|
1081
|
+
such an effect derives them at creation and re-subscribes when their root slots are replaced. Prefer `createSourceTrackingEffect`: the
|
|
1082
|
+
root check runs after every run of such an effect. An effect with no fifth list pays nothing for it. Options otherwise as
|
|
1083
|
+
`createOIMComputedService` / `createOIMSingleComputedService` (`sameValueGuard`, `releaseValue`, `initialValue`, `slotReleasePolicy`,
|
|
1084
|
+
column options).
|
|
1085
|
+
|
|
1086
|
+
**Sources found at read: `createSourceTrackingEffect(db, body)`.** The second way to build
|
|
1087
|
+
an effect takes no source lists. The body receives an `OIMSourceTracker`, and what it
|
|
1064
1088
|
reads through the tracker is what that run depends on — signal-style tracking.
|
|
1065
1089
|
|
|
1066
1090
|
```ts signature
|
|
1067
|
-
|
|
1068
|
-
type
|
|
1069
|
-
class
|
|
1091
|
+
createSourceTrackingEffect(channelMap: IOIMChannelMap, body: TOIMSourceTrackingEffectBody): TOIMEffect;
|
|
1092
|
+
type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
|
|
1093
|
+
class OIMSourceTracker { // exported as a type; the service builds it
|
|
1070
1094
|
readSourceSlot<TSlot extends TOIMSlot>(slot: TSlot): TSlot; // files the slot, returns it
|
|
1071
1095
|
readSourceComputedValue<TValue>(computedSlot: TOIMComputedSlot<TValue, unknown>): TValue; // files it, returns its value
|
|
1072
1096
|
}
|
|
@@ -1074,9 +1098,9 @@ class OIMEffectSourceTracker { // exported as a type; the service b
|
|
|
1074
1098
|
|
|
1075
1099
|
```ts
|
|
1076
1100
|
const fallback = cardService.writeSlotContentByKey(cards, 'c2', { id: 'c2', title: 'King', rank: 13, deck: 'hearts' });
|
|
1077
|
-
effectService.
|
|
1078
|
-
const isFirst =
|
|
1079
|
-
paint(
|
|
1101
|
+
effectService.createSourceTrackingEffect(db, sourceTracker => {
|
|
1102
|
+
const isFirst = sourceTracker.readSourceSlot(slot).entity?.rank === 1;
|
|
1103
|
+
paint(sourceTracker.readSourceSlot(isFirst ? slot : fallback).entity);
|
|
1080
1104
|
});
|
|
1081
1105
|
```
|
|
1082
1106
|
|
|
@@ -1089,7 +1113,7 @@ effectService.createEffectWithSourceTracking(db, effectSourceTracker => {
|
|
|
1089
1113
|
creation. A returned function is the undo, exactly as for `TOIMEffectBody`.
|
|
1090
1114
|
|
|
1091
1115
|
```ts
|
|
1092
|
-
const onRank = effectService.createEffect(db,
|
|
1116
|
+
const onRank = effectService.createEffect(db, () => { paint(slot.entity); }, [cardService.columnSlotService.holdColumn(slot, 'rank')]);
|
|
1093
1117
|
```
|
|
1094
1118
|
|
|
1095
1119
|
**A column effect takes the `TOIMColumnSlot` as a root, not a slot and a name.** Minting the
|
|
@@ -1115,13 +1139,13 @@ per call; get it once per view, not per entity. `service.holdColumnGroup(slot, c
|
|
|
1115
1139
|
the group's column slot on `slot`, and `service.unholdColumn(columnSlot)` gives it back. A write wakes it once
|
|
1116
1140
|
if it changes any of the group's columns and not at all if it changes only others.
|
|
1117
1141
|
|
|
1118
|
-
**`columnNames` picks the column slot store.** `createOIMSlotCollectionService(
|
|
1142
|
+
**`columnNames` picks the column slot store.** `createOIMSlotCollectionService({ slotContentService, columnNames: ['rank', 'label'] })`
|
|
1119
1143
|
— and `columnNames` on any factory's options — files the service's column slots by ordinal: every named column
|
|
1120
1144
|
gets a number, a group gets one at its first slot, and an entity slot's column slots sit in an array by
|
|
1121
1145
|
number instead of a `Map` per entity slot, which is cheaper to create and give back. `holdColumn` and
|
|
1122
1146
|
`getColumnGroup` are typed on those names, and a slot for any other column throws. Without `columnNames` the
|
|
1123
1147
|
store is the `columnSlotStoreService` option, defaulting to the shared map-driven
|
|
1124
|
-
|
|
1148
|
+
a shared map-driven store, and any name is accepted.
|
|
1125
1149
|
|
|
1126
1150
|
---
|
|
1127
1151
|
|
|
@@ -1148,29 +1172,56 @@ frame, with no further code. Every mutation goes through a domain neuron — a w
|
|
|
1148
1172
|
from outside the orchestrator is the mistake, not a case to cover with a fallback.
|
|
1149
1173
|
Full page: `website/docs/guides/with-cnstra.md`.
|
|
1150
1174
|
|
|
1151
|
-
The doors are free functions, one module each, so a consumer ships only the one they import.
|
|
1152
|
-
|
|
1153
|
-
the options object. Each builds a SERVICE,
|
|
1175
|
+
The doors are free functions, one module each, so a consumer ships only the one they import. Every factory
|
|
1176
|
+
takes ONE options object and no `db`; the `db` goes to `createStoreSlot(db)`. Each builds a SERVICE,
|
|
1154
1177
|
once per collection type; the store slot is made separately:
|
|
1155
1178
|
|
|
1156
1179
|
| Door | Builds | Store slot |
|
|
1157
1180
|
|---|---|---|
|
|
1158
|
-
| `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(
|
|
1159
|
-
| `createOIMSlotCollectionService(
|
|
1160
|
-
| `createOIMSingleSlotService(
|
|
1161
|
-
| `createOIMSlotCollectionIndexService(
|
|
1162
|
-
| `createOIMSlotCollectionSortedArrayIndexService(
|
|
1181
|
+
| `createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({ entityWriter?, slotEmptinessPolicy? }?)` | `OIMSlotContentEntityService<TEntity, TKey, TColumnName>`, the entity content service; it creates keyed and single entity slots alike | — |
|
|
1182
|
+
| `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)` |
|
|
1183
|
+
| `createOIMSingleSlotService({ slotContentService, slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService? \| columnNames? })` | `OIMWritableSingleSlotService` over any content service with `createSingleSlot` (entity or bucket) | `createStoreSlot(db)` |
|
|
1184
|
+
| `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)` |
|
|
1185
|
+
| `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
1186
|
| `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
1187
|
| `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
1188
|
| `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)` |
|
|
1166
1189
|
|
|
1167
|
-
`entityWriter` and `slotReleasePolicy` on these doors default to the
|
|
1168
|
-
|
|
1190
|
+
`entityWriter` and `slotReleasePolicy` on these doors default to those of the
|
|
1191
|
+
`defaults` option, so naming one overrides it for that service alone.
|
|
1192
|
+
|
|
1193
|
+
**A store aggregate is the store slot with its store methods, so a call does not pass the store slot.**
|
|
1194
|
+
`service.createStoreAggregate(storeSlot)` wraps a store slot you have (the collection, single and index
|
|
1195
|
+
services each return their own aggregate); `createOIMSlotCollectionStoreAggregate(db, options)` and
|
|
1196
|
+
`createOIMSingleSlotStoreAggregate(db, options)` build service + store slot + aggregate in one call, with the
|
|
1197
|
+
options of `createOIMSlotCollectionService` / `createOIMSingleSlotService` (a new service per call: for many
|
|
1198
|
+
store slots of one type, build the service once).
|
|
1199
|
+
|
|
1200
|
+
```ts
|
|
1201
|
+
import { createOIMSlotCollectionStoreAggregate } from '@oimdb/core';
|
|
1202
|
+
|
|
1203
|
+
const deckStoreAggregate = createOIMSlotCollectionStoreAggregate(db, { slotContentService: createOIMSlotContentEntityService<Card>() });
|
|
1204
|
+
const aceSlot = deckStoreAggregate.writeSlotContentByKey('c1', { id: 'c1', title: 'Ace', rank: 1, deck: 'spades' }); // = service.writeSlotContentByKey(storeSlot, 'c1', …)
|
|
1205
|
+
deckStoreAggregate.slotContentService.updateEntity(aceSlot, { rank: 14 });
|
|
1206
|
+
deckStoreAggregate.storeSlot; // for hooks, indexes and computeds
|
|
1207
|
+
deckStoreAggregate.slotService; // the service itself
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
| Aggregate | Methods (the service's, minus the store slot argument) |
|
|
1211
|
+
|---|---|
|
|
1212
|
+
| `OIMSlotCollectionStoreAggregate` | `findSlotByKey`, `hasSlotContentByKey`, `getSlotByKey`, `holdSlotByKey`, `getSlots`, `getSlotCount`, `openBatch`, `closeBatch`, `writeInBatch` |
|
|
1213
|
+
| `OIMWritableSlotCollectionStoreAggregate` | + `writeSlotContentByKey` |
|
|
1214
|
+
| `OIMSingleSlotStoreAggregate` | `findSingleSlot`, `hasSingleSlotContent`, `getSingleSlot`, `holdSingleSlot` |
|
|
1215
|
+
| `OIMWritableSingleSlotStoreAggregate` | + `writeSingleSlotContent`, `clearSingleSlotContent` |
|
|
1216
|
+
| `OIMSlotCollectionIndexStoreAggregate` | the collection methods + `getEntitiesByKey`, `getSlotsByKeys`, `holdSlotsByKeys`, `get/holdBucketColumnByKey`, `get/holdAnyBucketColumnByKey`, `disposeIndexStoreSlot` |
|
|
1217
|
+
|
|
1218
|
+
Every aggregate carries `storeSlot`, `slotService` (typed as the service it came from), `slotContentService`.
|
|
1219
|
+
Methods that take a slot stay on their service.
|
|
1169
1220
|
|
|
1170
1221
|
**A DOOR ALWAYS PASSES; IT NEVER LETS A CLASS REACH FOR A GLOBAL.** Every option
|
|
1171
1222
|
a service would otherwise answer with `?? OIMDefaults.x` is in the object its
|
|
1172
1223
|
door hands it, so there is one answer to each question and not two. Pinned in
|
|
1173
|
-
`src/
|
|
1224
|
+
`src/db/tests/which-answer-a-collection-the-root-mints-gets.test.ts`,
|
|
1174
1225
|
which also fails when an option arrives on a service and is not forwarded,
|
|
1175
1226
|
when the root mints anything but the channel and the collector, or when a public
|
|
1176
1227
|
member appears on the root that is not one of the seven it owns.
|
|
@@ -1180,7 +1231,7 @@ member appears on the root that is not one of the seven it owns.
|
|
|
1180
1231
|
slot, and the script lives for one frame.
|
|
1181
1232
|
|
|
1182
1233
|
```ts
|
|
1183
|
-
const freshPageService = createOIMSlotCollectionService(
|
|
1234
|
+
const freshPageService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
1184
1235
|
const freshPage = freshPageService.createStoreSlot(db);
|
|
1185
1236
|
const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
|
|
1186
1237
|
// freshBucketSlot.commands and freshBucketSlot.isWholeListRecorded carry the script; read them in an effect on the bucket slot
|
|
@@ -1190,56 +1241,41 @@ const freshBucketSlot = freshPageService.holdSlotByKey(freshPage, 'page-1');
|
|
|
1190
1241
|
|
|
1191
1242
|
## `OIMDefaults` — where an optional dependency comes from
|
|
1192
1243
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1244
|
+
`OIMDefaults` is an instance holding the strategies factories fall back on when their own options leave
|
|
1245
|
+
them out. Build one with what you want shared and pass it as `defaults` to every factory that should use
|
|
1246
|
+
it; a factory given none uses `OIMDefaults.INSTANCE`, which is empty. A field left undefined falls back to
|
|
1247
|
+
the built-in default. Nothing is global and nothing can be replaced after the fact.
|
|
1196
1248
|
|
|
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:
|
|
1249
|
+
```ts signature
|
|
1250
|
+
new OIMDefaults({
|
|
1251
|
+
slotStamp?: IOIMSlotStamp<TOIMSlot>; // none
|
|
1252
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // none: a slot with a value stays
|
|
1253
|
+
entityWriter?: IOIMEntityWriter; // createOIMInPlaceEntityWriter()
|
|
1254
|
+
entitySlotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot>; // "empty means slot.entity === undefined"
|
|
1255
|
+
indexKeysService?: IOIMIndexKeysService; // OIMAppendingIndexKeysService
|
|
1256
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // a shared map-driven store
|
|
1257
|
+
computedColumnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // a shared map-driven store
|
|
1258
|
+
storeSlotListener?: IOIMStoreSlotListener; // OIMNoStoreSlotListener: hears nothing
|
|
1259
|
+
}?) // TOIMDefaultsOptions
|
|
1260
|
+
```
|
|
1216
1261
|
|
|
1217
1262
|
```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"*.
|
|
1263
|
+
const replacingDefaults = new OIMDefaults({ entityWriter: createOIMReplacingEntityWriter() });
|
|
1264
|
+
const replacingCardService = createOIMSlotCollectionService({
|
|
1265
|
+
defaults: replacingDefaults,
|
|
1266
|
+
slotContentService: createOIMSlotContentEntityService<Card>({ defaults: replacingDefaults }),
|
|
1267
|
+
});
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
An option given to a factory wins over `defaults`; `defaults` wins over the built-in default. A tool that
|
|
1271
|
+
wants to hear every store slot (devtools' `oimDevDefaults`) is a `defaults` with a `storeSlotListener`.
|
|
1272
|
+
|
|
1273
|
+
**Each built-in default is its own module, so reaching one retains only its arm.** Every built-in default
|
|
1274
|
+
lives in its own file under `packages/core/src/db/constants/` as an `OIMDefaultValue` whose `getValue()`
|
|
1275
|
+
builds it on the first call and hands the same instance back after; a factory reads the modules it needs.
|
|
1276
|
+
`OIMDefaults` imports none of them, so importing it retains no arm.
|
|
1277
|
+
`packages/core/tests/what-an-import-retains.test.ts` pins it — *"a default is one door to its own arm, and
|
|
1278
|
+
the defaults class to none of them"*.
|
|
1243
1279
|
|
|
1244
1280
|
---
|
|
1245
1281
|
|
|
@@ -1251,17 +1287,17 @@ with its slot owner; the collection service (`OIMSlotCollectionService`) over it
|
|
|
1251
1287
|
Neither holds an entity: each `createStoreSlot(db)` is one instance of that type's store.
|
|
1252
1288
|
|
|
1253
1289
|
```ts signature
|
|
1254
|
-
createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>(
|
|
1255
|
-
entityWriter?: IOIMEntityWriter; //
|
|
1256
|
-
slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; //
|
|
1290
|
+
createOIMSlotContentEntityService<TEntity, TKey?, TColumnName?>({
|
|
1291
|
+
entityWriter?: IOIMEntityWriter; // defaults.entityWriter, then the in-place writer
|
|
1292
|
+
slotEmptinessPolicy?: IOIMSlotEmptinessPolicy<TOIMEntityContentSlot<TEntity>>; // defaults.entitySlotEmptinessPolicy, then "entity is undefined"
|
|
1257
1293
|
}?): OIMSlotContentEntityService<TEntity, TKey, TColumnName>
|
|
1258
1294
|
|
|
1259
|
-
createOIMSlotCollectionService(
|
|
1295
|
+
createOIMSlotCollectionService({
|
|
1260
1296
|
slotContentService,
|
|
1261
1297
|
slotStoreService?: IOIMSlotStoreService<TKey, TSlot, TStore>; // Map; REQUIRED for an array key
|
|
1262
|
-
slotReleasePolicy?: IOIMSlotReleasePolicy; //
|
|
1298
|
+
slotReleasePolicy?: IOIMSlotReleasePolicy; // defaults.slotReleasePolicy, then OIMSlotAtEmptyReleasePolicy
|
|
1263
1299
|
slotSubscriptionListener?: IOIMSlotSubscriptionListener; // OIMNoSlotSubscriptionListener
|
|
1264
|
-
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; //
|
|
1300
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMColumnSlot>; // defaults.columnSlotStoreService, then a shared map store
|
|
1265
1301
|
columnNames?: readonly TColumnName[]; // the ordinal column store over those names
|
|
1266
1302
|
}): OIMWritableSlotCollectionService<TKey, TOIMEntitySlot<TEntity, TKey>, TStore, TEntity, …>
|
|
1267
1303
|
collectionService.createStoreSlot(db): TOIMStoreSlot<TStore, TOIMEntitySlot<TEntity, TKey>, Partial<TEntity>, typeof collectionService>
|
|
@@ -1278,7 +1314,7 @@ derive-driven column slots, each a computed slot over its carrier (`OIMPullColum
|
|
|
1278
1314
|
Example, over the `db`, `cardService`, `cards` and `slot` of the setup block:
|
|
1279
1315
|
|
|
1280
1316
|
```ts
|
|
1281
|
-
const effect = effectService.createEffect(db,
|
|
1317
|
+
const effect = effectService.createEffect(db, () => { console.log(slot.entity?.title); }, [slot]);
|
|
1282
1318
|
cardService.slotContentService.updateEntity(slot, { title: 'Ace of Spades' });
|
|
1283
1319
|
db.flush();
|
|
1284
1320
|
effectService.disposeEffect(effect);
|
|
@@ -1287,15 +1323,15 @@ effectService.disposeEffect(effect);
|
|
|
1287
1323
|
Assembled with a store service of your own:
|
|
1288
1324
|
|
|
1289
1325
|
```ts
|
|
1290
|
-
const baseCardService = createOIMSlotCollectionService(
|
|
1291
|
-
slotContentService: createOIMSlotContentEntityService<Card, string>(
|
|
1326
|
+
const baseCardService = createOIMSlotCollectionService({
|
|
1327
|
+
slotContentService: createOIMSlotContentEntityService<Card, string>(),
|
|
1292
1328
|
slotStoreService: new OIMSlotMapStoreService<TOIMEntitySlot<Card, string>>(),
|
|
1293
1329
|
});
|
|
1294
1330
|
const baseCards = baseCardService.createStoreSlot(db);
|
|
1295
1331
|
```
|
|
1296
1332
|
|
|
1297
1333
|
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
|
|
1334
|
+
is not an option of any factory: it is `new OIMDefaults({ slotStamp })`, passed as `defaults`.
|
|
1299
1335
|
|
|
1300
1336
|
### Public properties
|
|
1301
1337
|
|
|
@@ -1309,7 +1345,7 @@ A store slot is not a slot, so nothing holds, subscribes or wakes it: there is n
|
|
|
1309
1345
|
`effectService.createEffect(db, [cards], …)` that hears the population. "Something in this population changed" is an
|
|
1310
1346
|
index over the store slot and a subscription to its bucket — the bucket's door
|
|
1311
1347
|
is written when an entity slot joins or leaves it — or a content-change listener on the store slot
|
|
1312
|
-
(`
|
|
1348
|
+
(`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames)`,
|
|
1313
1349
|
removed by the same listener object). The slot owner is what `slot.owner` points at for every slot the type
|
|
1314
1350
|
filed, in every one of its store slots; `slot.storeSlot` names which store slot.
|
|
1315
1351
|
|
|
@@ -1500,7 +1536,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1500
1536
|
- **The hand mutation already compiles.** `TOIMEntitySlot` declares
|
|
1501
1537
|
`readonly entity: TEntity | undefined`, and `readonly` forbids REPLACING the
|
|
1502
1538
|
object, not writing its columns — `entity.title = 'two'` needs no cast, only
|
|
1503
|
-
the usual narrowing of `| undefined`. `
|
|
1539
|
+
the usual narrowing of `| undefined`. `TOIMEditable<TSlot>` is what strips
|
|
1504
1540
|
`readonly` from the field itself, which is what assigning `slot.entity` needs.
|
|
1505
1541
|
- **With a list, only the named columns wake. Without it, every watched column
|
|
1506
1542
|
does** — the column owner walks the slot's whole column store, and a
|
|
@@ -1515,7 +1551,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1515
1551
|
effect on the slot itself and an index over the store slot both see a hand mutation.
|
|
1516
1552
|
- **It re-stamps emptiness** whenever emptiness can depend on the mutation — with no column list,
|
|
1517
1553
|
on a slot whose `entity` is `undefined`, and on a content service with a `slotEmptinessPolicy` other than
|
|
1518
|
-
`
|
|
1554
|
+
"the entity is `undefined`", a release policy that releases written slots, or a db with a
|
|
1519
1555
|
`slotStamp` — so a hand mutation that emptied the entity leaves `oimIsSlotEmpty(slot)`
|
|
1520
1556
|
true rather than lying, and the release guard can take the identity when it should.
|
|
1521
1557
|
- **It refuses inside a frame**, through the same `assertWritable` →
|
|
@@ -1531,7 +1567,7 @@ cardService.slotContentService.markEntityChanged(slot, ['title']);
|
|
|
1531
1567
|
A **writer** is the operations that move an entity's columns, and nothing
|
|
1532
1568
|
else: it reaches no channel, takes no holds and knows nothing about lifetime.
|
|
1533
1569
|
Both strategies ship as factories; the fork is chosen once, as the content service's `entityWriter`
|
|
1534
|
-
(or for every
|
|
1570
|
+
(or for every factory given it, `new OIMDefaults({ entityWriter })` as `defaults`).
|
|
1535
1571
|
|
|
1536
1572
|
```ts signature
|
|
1537
1573
|
interface IOIMEntityWriter {
|
|
@@ -1548,13 +1584,13 @@ interface IOIMEntityWriter {
|
|
|
1548
1584
|
type TOIMEntityWriterOptions = {
|
|
1549
1585
|
sameValueGuard?: TOIMSameValueGuard;
|
|
1550
1586
|
entityInserter?: TOIMEntityInserter;
|
|
1551
|
-
putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns |
|
|
1552
|
-
patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns |
|
|
1553
|
-
patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn |
|
|
1587
|
+
putEntityColumns?: typeof oimPutEntityColumns; // oimPutEntityColumns | oimGuardedPutEntityColumns
|
|
1588
|
+
patchEntityColumns?: typeof oimPatchEntityColumns; // oimPatchEntityColumns | oimGuardedPatchEntityColumns
|
|
1589
|
+
patchEntityColumn?: typeof oimPatchEntityColumn; // oimPatchEntityColumn | oimGuardedPatchEntityColumn
|
|
1554
1590
|
};
|
|
1555
1591
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1592
|
+
createOIMInPlaceEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // stored object keeps its identity
|
|
1593
|
+
createOIMReplacingEntityWriter(options?: TOIMEntityWriterOptions): IOIMEntityWriter // a new object per write that changed something
|
|
1558
1594
|
```
|
|
1559
1595
|
|
|
1560
1596
|
`entityInserter` is the first-write strategy: `oimInsertEntityWithoutCopy` (the default) stores
|
|
@@ -1582,9 +1618,9 @@ of a key that already has a value and `slotContentService.writeSlotContent(slot,
|
|
|
1582
1618
|
write that drops a column is ONE channel write of the slot, and the replacing writer
|
|
1583
1619
|
builds ONE new object.
|
|
1584
1620
|
|
|
1585
|
-
Pick `
|
|
1621
|
+
Pick `createOIMReplacingEntityWriter` when something downstream compares by
|
|
1586
1622
|
reference — a React `useSyncExternalStore` snapshot, a memo, a shallow-equal
|
|
1587
|
-
selector. Under `
|
|
1623
|
+
selector. Under `createOIMInPlaceEntityWriter` the entity reference never moves,
|
|
1588
1624
|
so a value comparison never fires and a binding needs a counter bumped inside its
|
|
1589
1625
|
handler. Replacing allocates one object per changed write; in-place allocates
|
|
1590
1626
|
none.
|
|
@@ -1609,10 +1645,10 @@ oimPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, c
|
|
|
1609
1645
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1610
1646
|
oimPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
|
|
1611
1647
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1612
|
-
|
|
1648
|
+
oimGuardedPutEntityColumns(slot, entity: object, sameValueGuard, changedColumnNames: string[] | undefined,
|
|
1613
1649
|
targetOf: (entity: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown> | undefined
|
|
1614
|
-
|
|
1615
|
-
|
|
1650
|
+
oimGuardedPatchEntityColumns(slot, patch: object, sameValueGuard, changedColumnNames, targetOf) // same signature as the unguarded walk
|
|
1651
|
+
oimGuardedPatchEntityColumn(slot, columnName: string, value: unknown, sameValueGuard, changedColumnNames, targetOf)
|
|
1616
1652
|
```
|
|
1617
1653
|
|
|
1618
1654
|
Each compares every column through `sameValueGuard`, writes the moved ones into
|
|
@@ -1625,14 +1661,14 @@ ships two arms:
|
|
|
1625
1661
|
|
|
1626
1662
|
| Option | Default (unguarded) | Guarded arm |
|
|
1627
1663
|
|---|---|---|
|
|
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` | `
|
|
1664
|
+
| `putEntityColumns` | `oimPutEntityColumns` — skips neither `__proto__` keys nor inherited stored columns | `oimGuardedPutEntityColumns` — skips both |
|
|
1665
|
+
| `patchEntityColumns` | `oimPatchEntityColumns` — every enumerable key of the patch, `__proto__` included | `oimGuardedPatchEntityColumns` — skips a `__proto__` key and a key the patch does not own |
|
|
1666
|
+
| `patchEntityColumn` | `oimPatchEntityColumn` | `oimGuardedPatchEntityColumn` — refuses the column name `__proto__` |
|
|
1631
1667
|
|
|
1632
1668
|
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 `
|
|
1669
|
+
instances — `createOIMInPlaceEntityWriter({ putEntityColumns: oimGuardedPutEntityColumns,
|
|
1670
|
+
patchEntityColumns: oimGuardedPatchEntityColumns, patchEntityColumn: oimGuardedPatchEntityColumn })`, the
|
|
1671
|
+
same options on `createOIMReplacingEntityWriter`. The writer decides only where the result goes — this one
|
|
1636
1672
|
stores every value frozen:
|
|
1637
1673
|
|
|
1638
1674
|
```ts
|
|
@@ -1640,11 +1676,11 @@ const copyColumns = (entity: Record<string, unknown>): Record<string, unknown> =
|
|
|
1640
1676
|
|
|
1641
1677
|
function storeFrozen(slot: TOIMEntityContentSlot, next: Record<string, unknown> | undefined): boolean {
|
|
1642
1678
|
if (next === undefined) return false;
|
|
1643
|
-
(slot as
|
|
1679
|
+
(slot as TOIMEditable<TOIMEntityContentSlot>).entity = Object.freeze(next);
|
|
1644
1680
|
return true;
|
|
1645
1681
|
}
|
|
1646
1682
|
|
|
1647
|
-
const replacingWriter =
|
|
1683
|
+
const replacingWriter = createOIMReplacingEntityWriter();
|
|
1648
1684
|
|
|
1649
1685
|
const freezingWriter: IOIMEntityWriter = {
|
|
1650
1686
|
fillEntity: (slot, entity) => { storeFrozen(slot, copyColumns(entity as Record<string, unknown>)); },
|
|
@@ -1672,8 +1708,8 @@ A single entity — one value under no key, such as settings or a session — is
|
|
|
1672
1708
|
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
1709
|
|
|
1674
1710
|
```ts signature
|
|
1675
|
-
createOIMSingleSlotService(
|
|
1676
|
-
slotContentService: createOIMSlotContentEntityService<TEntity>(
|
|
1711
|
+
createOIMSingleSlotService({
|
|
1712
|
+
slotContentService: createOIMSlotContentEntityService<TEntity>(),
|
|
1677
1713
|
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
1678
1714
|
}): OIMWritableSingleSlotService<TOIMEntityContentSlot<TEntity>, TEntity, …>
|
|
1679
1715
|
|
|
@@ -1697,12 +1733,12 @@ with no value until something writes it. Its store slot is a `TOIMSingleSlotStor
|
|
|
1697
1733
|
```ts
|
|
1698
1734
|
type Settings = { theme: 'light' | 'dark'; zoom: number };
|
|
1699
1735
|
|
|
1700
|
-
const settingsService = createOIMSingleSlotService(
|
|
1736
|
+
const settingsService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<Settings>() });
|
|
1701
1737
|
const settings = settingsService.createStoreSlot(db);
|
|
1702
1738
|
settingsService.writeSingleSlotContent(settings, { theme: 'light', zoom: 1 });
|
|
1703
1739
|
const settingsSlot = settingsService.holdSingleSlot(settings);
|
|
1704
1740
|
const themeColumn = settingsService.columnSlotService.holdColumn(settingsSlot, 'theme');
|
|
1705
|
-
const themeEffect = effectService.createEffect(db,
|
|
1741
|
+
const themeEffect = effectService.createEffect(db, () => { console.log(settingsSlot.entity?.theme); }, [themeColumn]);
|
|
1706
1742
|
settingsService.slotContentService.updateEntity(settingsSlot, { theme: 'dark' });
|
|
1707
1743
|
db.flush();
|
|
1708
1744
|
effectService.disposeEffect(themeEffect);
|
|
@@ -1720,7 +1756,7 @@ The slot is of the keyed entity slot's literal shape (with `key: undefined` and
|
|
|
1720
1756
|
`storeSlot` is `settings` — so
|
|
1721
1757
|
`effectService.createEffect(db, [settingsSlot], [], …)` works and a column of it is a root like any other.
|
|
1722
1758
|
Its key is `undefined`, and its store is `{ slot }` (`TOIMSingleSlotStore`). Its lifetime is the
|
|
1723
|
-
`slotReleasePolicy` (
|
|
1759
|
+
`slotReleasePolicy` (`defaults.slotReleasePolicy`, then `OIMSlotAtEmptyReleasePolicy`), so a single entity that holds a value
|
|
1724
1760
|
stays; one emptied and unheld is released, and the slot `getSingleSlot` mints next holds no entity. The value
|
|
1725
1761
|
is stored by the writer's `entityInserter`, which by default keeps the object handed in rather than a copy.
|
|
1726
1762
|
|
|
@@ -1732,7 +1768,7 @@ A bucket collection is, like an entity collection, a content service and the col
|
|
|
1732
1768
|
The content service is the strategy — how a bucket keeps its member slots — and it is chosen at the factory:
|
|
1733
1769
|
|
|
1734
1770
|
```ts signature
|
|
1735
|
-
createOIMSlotCollectionService(
|
|
1771
|
+
createOIMSlotCollectionService({
|
|
1736
1772
|
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or one of the others below
|
|
1737
1773
|
slotStoreService?: IOIMSlotStoreService<TKey, TOIMBucketSlot<TSlots, TKey>, TStore>; // Map; REQUIRED for an array key
|
|
1738
1774
|
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
@@ -1752,7 +1788,7 @@ of the collection service take that store slot first, and the bucket doors of th
|
|
|
1752
1788
|
bucket slot.
|
|
1753
1789
|
|
|
1754
1790
|
```ts
|
|
1755
|
-
const colourService = createOIMSlotCollectionService(
|
|
1791
|
+
const colourService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>>() });
|
|
1756
1792
|
const colours = colourService.createStoreSlot(db);
|
|
1757
1793
|
const red = colourService.holdSlotByKey(colours, 'red');
|
|
1758
1794
|
colourService.slotContentService.writeSlotContent(red, [slot, cardService.getSlotByKey(cards, 'c2')]);
|
|
@@ -1970,10 +2006,10 @@ content service already writes the bucket slot on every mutation, so a consumer
|
|
|
1970
2006
|
reads the script inside the effect:
|
|
1971
2007
|
|
|
1972
2008
|
```ts
|
|
1973
|
-
effectService.createEffect(db,
|
|
2009
|
+
effectService.createEffect(db, () => {
|
|
1974
2010
|
if (todo.isWholeListRecorded) render(Array.from(pageService.slotContentService.getBucketMemberSlots(todo)));
|
|
1975
2011
|
else applyToTheRenderedList(todo.commands ?? []);
|
|
1976
|
-
});
|
|
2012
|
+
}, [todo]);
|
|
1977
2013
|
```
|
|
1978
2014
|
|
|
1979
2015
|
`TOIMBucketSlot` carries two fields for it, and a consumer reads both:
|
|
@@ -2019,21 +2055,21 @@ One bucket under no key is a store slot of an `OIMSingleSlotService` over a buck
|
|
|
2019
2055
|
`OIMSingleSlotStoreService.INSTANCE` as the store service:
|
|
2020
2056
|
|
|
2021
2057
|
```ts signature
|
|
2022
|
-
createOIMSingleSlotService(
|
|
2058
|
+
createOIMSingleSlotService({
|
|
2023
2059
|
slotContentService: new OIMSlotContentPositionalArrayBucketService<TBucketMemberSlot>(), // or the set one
|
|
2024
2060
|
slotReleasePolicy?, slotSubscriptionListener?, columnSlotStoreService?, columnNames?,
|
|
2025
2061
|
}): OIMWritableSingleSlotService<TOIMBucketContentSlot<TSlots>, readonly TBucketMemberSlot[], …>
|
|
2026
2062
|
```
|
|
2027
2063
|
|
|
2028
2064
|
```ts
|
|
2029
|
-
const handService = createOIMSingleSlotService(
|
|
2065
|
+
const handService = createOIMSingleSlotService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TOIMEntitySlot<Card>>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
2030
2066
|
const hand = handService.createStoreSlot(db);
|
|
2031
2067
|
const handSlot = handService.holdSingleSlot(hand);
|
|
2032
2068
|
handService.slotContentService.writeSlotContent(handSlot, [slot]);
|
|
2033
2069
|
handService.slotContentService.insertBucketMemberSlotAt(handSlot, 0, cardService.getSlotByKey(cards, 'c2'));
|
|
2034
|
-
effectService.disposeEffect(effectService.createEffect(db,
|
|
2070
|
+
effectService.disposeEffect(effectService.createEffect(db, () => {
|
|
2035
2071
|
render(Array.from(handService.slotContentService.getBucketMemberSlots(handSlot)));
|
|
2036
|
-
}));
|
|
2072
|
+
}, [handSlot]));
|
|
2037
2073
|
handSlot.owner.unholdSlot(handSlot);
|
|
2038
2074
|
```
|
|
2039
2075
|
|
|
@@ -2052,7 +2088,7 @@ slot (`holdSingleSlot(storeSlot)`) for as long as you write into it, or name `OI
|
|
|
2052
2088
|
### The index that fills itself — `OIMSlotCollectionIndexService`
|
|
2053
2089
|
|
|
2054
2090
|
```ts signature
|
|
2055
|
-
createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>(
|
|
2091
|
+
createOIMSlotCollectionIndexService<TEntity, TSourceKey, TColumns?, TKey?, TSourceStore?, TSlots?, TSlotContentService?>({
|
|
2056
2092
|
entityService: Pick<OIMSlotCollectionService<TSourceKey, TOIMEntitySlot<TEntity, TSourceKey>, TSourceStore, unknown>, 'getSlots'>;
|
|
2057
2093
|
...TOIMIndexKeyOptions<TKey, TEntity, TColumns>; // { columns } | { keyOf } | { keyOf, columns }
|
|
2058
2094
|
slotContentService?: OIMSlotContentBucketService<TOIMEntitySlot<TEntity>, TSlots>; // default: array-driven, no commands
|
|
@@ -2098,21 +2134,21 @@ own. With none named it is `OIMSlotAtEmptyReleasePolicy.INSTANCE`: the bucket st
|
|
|
2098
2134
|
maintained on every write.
|
|
2099
2135
|
|
|
2100
2136
|
```ts
|
|
2101
|
-
const byDeckService = createOIMSlotCollectionIndexService(
|
|
2137
|
+
const byDeckService = createOIMSlotCollectionIndexService({
|
|
2102
2138
|
entityService: cardService,
|
|
2103
2139
|
keyOf: (card: Card) => card.deck,
|
|
2104
2140
|
});
|
|
2105
2141
|
const byDeck = byDeckService.createIndexStoreSlot(cards);
|
|
2106
2142
|
|
|
2107
2143
|
const spades = byDeckService.holdSlotByKey(byDeck, 'spades');
|
|
2108
|
-
effectService.createEffect(db,
|
|
2144
|
+
effectService.createEffect(db, () => render(Array.from(byDeckService.slotContentService.getBucketMemberSlots(spades))), [spades]);
|
|
2109
2145
|
```
|
|
2110
2146
|
|
|
2111
2147
|
A set-driven index names its content service with the bucket member slot type and the index key type spelled
|
|
2112
2148
|
out — `new OIMSlotContentSetBucketService()` infers `<TOIMSlot, TOIMStoreKey>` and fails to type-check:
|
|
2113
2149
|
|
|
2114
2150
|
```ts
|
|
2115
|
-
const bySuitService = createOIMSlotCollectionIndexService(
|
|
2151
|
+
const bySuitService = createOIMSlotCollectionIndexService({
|
|
2116
2152
|
entityService: cardService,
|
|
2117
2153
|
columns: ['deck'],
|
|
2118
2154
|
slotContentService: new OIMSlotContentSetBucketService<TOIMEntitySlot<Card>, string>(),
|
|
@@ -2121,7 +2157,7 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2121
2157
|
```
|
|
2122
2158
|
|
|
2123
2159
|
- **Passing the entity store slot to `createIndexStoreSlot` is the whole wiring.** It registers
|
|
2124
|
-
a content-change listener on that store slot (`
|
|
2160
|
+
a content-change listener on that store slot (`OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener`);
|
|
2125
2161
|
the entity services know nothing about indexes. Call `createIndexStoreSlot` twice over one entity
|
|
2126
2162
|
store slot and it files into both. `entityService` is used only for its `getSlots`, to fill a new bucket
|
|
2127
2163
|
from the source.
|
|
@@ -2133,8 +2169,8 @@ const bySuit = bySuitService.createIndexStoreSlot(cards);
|
|
|
2133
2169
|
ones `keyOf` reads. With `columns`, a patch or put whose changed columns include none of
|
|
2134
2170
|
them keeps the entity's key and calls nothing; a write of the whole value (a fill, a
|
|
2135
2171
|
clear, `markEntityChanged(slot)` with no names) always reads the key again.
|
|
2136
|
-
- **A key of several columns**: `createOIMSlotCollectionIndexService(
|
|
2137
|
-
bucketSlotStoreService: new OIMSlotTreeStoreService(
|
|
2172
|
+
- **A key of several columns**: `createOIMSlotCollectionIndexService({ entityService, columns: ['deck', 'suit'],
|
|
2173
|
+
bucketSlotStoreService: new OIMSlotTreeStoreService(OIMScalarKeyTreeWalk.INSTANCE) })` makes the key
|
|
2138
2174
|
the array of those values in order, typed as a tuple. Only that store knows the key is an array (its
|
|
2139
2175
|
`isSameKey`), so a fresh array with the same parts moves nothing. `createOIMSlotCollectionIndexService` takes a scalar key
|
|
2140
2176
|
unless it is handed a `bucketSlotStoreService`: two columns or an array `keyOf` without one do not compile.
|
|
@@ -2190,13 +2226,13 @@ the bucket slot (`getSlotByKey` / `holdSlotByKey`). A reader that wants both sub
|
|
|
2190
2226
|
to both, and the subscriber picks by which slots it puts in `createEffect`.
|
|
2191
2227
|
|
|
2192
2228
|
```ts
|
|
2193
|
-
const deckIndexService = createOIMSlotCollectionIndexService(
|
|
2229
|
+
const deckIndexService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2194
2230
|
const deckIndex = deckIndexService.createIndexStoreSlot(cards);
|
|
2195
2231
|
const spadesBucket = deckIndexService.holdSlotByKey(deckIndex, 'spades');
|
|
2196
2232
|
const spadesAnyColumn = deckIndexService.holdAnyBucketColumnByKey(deckIndex, 'spades');
|
|
2197
|
-
const entitiesEffect = effectService.createEffect(db,
|
|
2233
|
+
const entitiesEffect = effectService.createEffect(db, () => {
|
|
2198
2234
|
console.log(deckIndexService.getEntitiesByKey(deckIndex, 'spades'));
|
|
2199
|
-
});
|
|
2235
|
+
}, [spadesBucket, spadesAnyColumn]);
|
|
2200
2236
|
effectService.disposeEffect(entitiesEffect);
|
|
2201
2237
|
deckIndexService.columnSlotService.unholdColumn(spadesAnyColumn);
|
|
2202
2238
|
spadesBucket.owner.unholdSlot(spadesBucket);
|
|
@@ -2215,7 +2251,7 @@ needs the whole list each time: an export, an aggregate over the entities, a log
|
|
|
2215
2251
|
It costs nothing until someone asks for one: a write is checked against it only after
|
|
2216
2252
|
the index has been asked for the first any-column slot.
|
|
2217
2253
|
|
|
2218
|
-
**The content-change hook.** `
|
|
2254
|
+
**The content-change hook.** `OIMSlotContentChangeListenerService.INSTANCE.addContentChangeListener(storeSlot, listener, readColumnNames): void`
|
|
2219
2255
|
and `removeContentChangeListener(storeSlot, listener): boolean` file and unfile a listener on that one store
|
|
2220
2256
|
slot, by the listener object. `addReadColumn` / `removeReadColumn` / `addReadAnyColumn` / `removeReadAnyColumn`
|
|
2221
2257
|
declare reads without a listener of their own, which is how an index declares its bucket columns.
|
|
@@ -2231,15 +2267,15 @@ every column write is added with `readColumnNames` `undefined`. **The filter is
|
|
|
2231
2267
|
slot, not per listener**: once a write touches a column ANY listener of that store reads, every
|
|
2232
2268
|
listener of that store is called, so a listener that declared a narrower set still checks
|
|
2233
2269
|
`changedColumns` itself, as `OIMSlotCollectionIndexService` does against its key columns.
|
|
2234
|
-
`
|
|
2270
|
+
`IOIMSlotContentChangeListener<TSlot, TPatch>` has two methods:
|
|
2235
2271
|
`onContentChanged(slot, patch, changedColumns)` and
|
|
2236
|
-
`
|
|
2272
|
+
`onManyContentsChanged(slotChannelQueue, patchChannelQueue, changedColumnsChannelQueue)` — three
|
|
2237
2273
|
parallel `OIMChannelQueue`s read with `getEntryCount()` / `getEntryAt(i)`, valid only during the
|
|
2238
2274
|
call, so a listener copies what it keeps — `patch` is what an update
|
|
2239
2275
|
wrote, `changedColumns` the names `markEntityChanged(slot, columns)` was given, either
|
|
2240
2276
|
`undefined` when the write carries none. A single write outside a batch delivers one `onContentChanged` as soon as it is filed; inside
|
|
2241
2277
|
`writeInBatch` (or `openBatch` / `closeBatch`) on the collection the writes are queued on the store slot and
|
|
2242
|
-
delivered as ONE `
|
|
2278
|
+
delivered as ONE `onManyContentsChanged` when the outermost batch closes. Code inside the batch reads the index
|
|
2243
2279
|
as it was before the batch.
|
|
2244
2280
|
This hook is the only seam an index uses; an index of your own is a listener.
|
|
2245
2281
|
|
|
@@ -2254,10 +2290,10 @@ return the same kind of slot, written when any column of any member changes. A g
|
|
|
2254
2290
|
`slot.owner.unholdSlot(slot)`.
|
|
2255
2291
|
|
|
2256
2292
|
```ts
|
|
2257
|
-
const decksService = createOIMSlotCollectionIndexService(
|
|
2293
|
+
const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2258
2294
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2259
2295
|
const rankOfSpades = decksService.holdBucketColumnByKey(decks, 'spades', 'rank');
|
|
2260
|
-
effectService.disposeEffect(effectService.createEffect(db,
|
|
2296
|
+
effectService.disposeEffect(effectService.createEffect(db, () => {}, [rankOfSpades]));
|
|
2261
2297
|
decksService.columnSlotService.unholdColumn(rankOfSpades);
|
|
2262
2298
|
```
|
|
2263
2299
|
|
|
@@ -2266,7 +2302,7 @@ decksService.columnSlotService.unholdColumn(rankOfSpades);
|
|
|
2266
2302
|
```ts signature
|
|
2267
2303
|
createOIMSortedComputedService<TKey, TEntity extends object, TSlots, TSourceKey, TSourceStore, TComputedStore?>({
|
|
2268
2304
|
indexService: OIMSlotCollectionIndexService<TKey, TEntity, TSlots, TSourceKey, TSourceStore>;
|
|
2269
|
-
sortColumns?: readonly TOIMSortColumn<TEntity>[]; // {
|
|
2305
|
+
sortColumns?: readonly TOIMSortColumn<TEntity>[]; // { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' }
|
|
2270
2306
|
comparator?: (left: TEntity, right: TEntity) => number;
|
|
2271
2307
|
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2272
2308
|
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>;
|
|
@@ -2309,13 +2345,13 @@ slots sorted. A hold is given back with `slot.owner.unholdSlot(slot)`.
|
|
|
2309
2345
|
to a member as with `comparator` alone.
|
|
2310
2346
|
|
|
2311
2347
|
```ts
|
|
2312
|
-
const cardsInDeckService = createOIMSlotCollectionIndexService(
|
|
2348
|
+
const cardsInDeckService = createOIMSlotCollectionIndexService({
|
|
2313
2349
|
entityService: cardService,
|
|
2314
2350
|
keyOf: (card: Card) => card.deck,
|
|
2315
2351
|
});
|
|
2316
2352
|
createOIMSortedComputedService({
|
|
2317
2353
|
indexService: cardsInDeckService,
|
|
2318
|
-
sortColumns: [{
|
|
2354
|
+
sortColumns: [{ columnName: 'title', direction: 'asc' }, { columnName: 'rank', direction: 'asc' }],
|
|
2319
2355
|
comparator: (l, r) => l.title.localeCompare(r.title, 'de') || l.rank - r.rank,
|
|
2320
2356
|
}).createSortedStoreSlot(cardsInDeckService.createIndexStoreSlot(cards));
|
|
2321
2357
|
// a write to `title` or `rank` re-sorts; a write to `note` does not
|
|
@@ -2328,17 +2364,17 @@ SECOND array of `createEffect`. A re-sort that yields the same sequence
|
|
|
2328
2364
|
(`oimSameSlotSequence`) keeps the previous array, so its identity is stable.
|
|
2329
2365
|
|
|
2330
2366
|
```ts
|
|
2331
|
-
const decksService = createOIMSlotCollectionIndexService(
|
|
2367
|
+
const decksService = createOIMSlotCollectionIndexService({ entityService: cardService, keyOf: (card: Card) => card.deck });
|
|
2332
2368
|
const decks = decksService.createIndexStoreSlot(cards);
|
|
2333
2369
|
const byRankInDeckService = createOIMSortedComputedService({
|
|
2334
2370
|
indexService: decksService,
|
|
2335
|
-
sortColumns: [{
|
|
2371
|
+
sortColumns: [{ columnName: 'rank', direction: 'asc' }],
|
|
2336
2372
|
});
|
|
2337
2373
|
const byRankInDeck = byRankInDeckService.createSortedStoreSlot(decks);
|
|
2338
2374
|
const spadesByRank = byRankInDeckService.computedCollectionService.holdSlotByKey(byRankInDeck, 'spades');
|
|
2339
|
-
const sortedEffect = effectService.createEffect(db,
|
|
2375
|
+
const sortedEffect = effectService.createEffect(db, () => {
|
|
2340
2376
|
render(spadesByRank.computedValueService.getValueForSlot(spadesByRank));
|
|
2341
|
-
});
|
|
2377
|
+
}, [], [spadesByRank]);
|
|
2342
2378
|
effectService.disposeEffect(sortedEffect);
|
|
2343
2379
|
spadesByRank.owner.unholdSlot(spadesByRank);
|
|
2344
2380
|
```
|
|
@@ -2350,7 +2386,7 @@ index. `createOIMSlotCollectionSortedArrayIndexService` keeps the index's own bu
|
|
|
2350
2386
|
the user picks one at construction.
|
|
2351
2387
|
|
|
2352
2388
|
```ts signature
|
|
2353
|
-
createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>(
|
|
2389
|
+
createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKey, TColumns?, TKey extends TOIMStoreKey?, TSourceStore?, TStore?>({
|
|
2354
2390
|
entityService, ...TOIMIndexKeyOptions<TKey, TEntity, TColumns>, // as createOIMSlotCollectionIndexService, scalar key
|
|
2355
2391
|
sortColumns: readonly TOIMSortColumn<TEntity>[]; // REQUIRED
|
|
2356
2392
|
bucketSlotStoreService?, slotReleasePolicy?, columnSlotStoreService?, columnNames?,
|
|
@@ -2359,10 +2395,10 @@ createOIMSlotCollectionSortedArrayIndexService<TEntity extends object, TSourceKe
|
|
|
2359
2395
|
|
|
2360
2396
|
```ts
|
|
2361
2397
|
declare const someCardSlot: TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
2362
|
-
const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService(
|
|
2398
|
+
const cardsByDeckService = createOIMSlotCollectionSortedArrayIndexService({
|
|
2363
2399
|
entityService: cardService,
|
|
2364
2400
|
columns: ['deck'],
|
|
2365
|
-
sortColumns: [{
|
|
2401
|
+
sortColumns: [{ columnName: 'rank', direction: 'asc' }, { columnName: 'id', direction: 'desc' }],
|
|
2366
2402
|
});
|
|
2367
2403
|
const cardsByDeck = cardsByDeckService.createIndexStoreSlot(cards);
|
|
2368
2404
|
const spades = cardsByDeckService.holdSlotByKey(cardsByDeck, 'spades');
|
|
@@ -2399,8 +2435,8 @@ filed under now, so a moved entity slot leaves its old bucket in O(1), also unde
|
|
|
2399
2435
|
writer. Only the index writes it, through the `IOIMIndexKeysService` (`get(slot, position)`,
|
|
2400
2436
|
`set(slot, position, indexKey)`), at the position `createIndexStoreSlot` allocated
|
|
2401
2437
|
(`indexStoreSlot.indexKeysPosition`). Positions are allocated per entity store slot
|
|
2402
|
-
(`sourceStoreSlot.indexKeysPositions`). Pick the strategy
|
|
2403
|
-
(default `
|
|
2438
|
+
(`sourceStoreSlot.indexKeysPositions`). Pick the strategy with `new OIMDefaults({ indexKeysService })` passed as `defaults`
|
|
2439
|
+
(default `OIMAppendingIndexKeysService.INSTANCE`):
|
|
2404
2440
|
|
|
2405
2441
|
| strategy | on `disposeIndexStoreSlot` | cost |
|
|
2406
2442
|
|---|---|---|
|
|
@@ -2429,7 +2465,7 @@ type TOIMComputedSources = {
|
|
|
2429
2465
|
In a `createSources` result (of `createOIMComputedService`'s options, or of `createStoreSlot`) both `sourceSlots` and `sourceComputeds` are optional; a missing one means none.
|
|
2430
2466
|
|
|
2431
2467
|
```ts signature
|
|
2432
|
-
type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<
|
|
2468
|
+
type TOIMComputedSlot<TValue = unknown, TKey = unknown> = TOIMIndexableSlot<TKey> & Readonly<TOIMComputedDerivation> & {
|
|
2433
2469
|
readonly owner: IOIMSlotOwner;
|
|
2434
2470
|
readonly computedValueService: IOIMComputedValueService<TValue>;
|
|
2435
2471
|
readonly value: TValue;
|
|
@@ -2475,17 +2511,25 @@ class OIMComputedService<TKey, TValue, TComputedStore> {
|
|
|
2475
2511
|
}
|
|
2476
2512
|
|
|
2477
2513
|
createOIMComputedService<TKey, TValue, TComputedStore = Map<TKey, TOIMComputedSlot<TValue, TKey>>>({
|
|
2478
|
-
createSources?: (
|
|
2514
|
+
createSources?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>;
|
|
2479
2515
|
derive?: (computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue;
|
|
2480
2516
|
sameValueGuard?: TOIMSameValueGuard<TValue>; // default Object.is
|
|
2517
|
+
releaseValue?: TOIMComputedValueReleaser<TValue>; // default: gives back nothing
|
|
2481
2518
|
initialValue?: TValue;
|
|
2482
2519
|
slotReleasePolicy?: IOIMSlotReleasePolicy; // default OIMSlotAtUnholdReleasePolicy.INSTANCE
|
|
2483
|
-
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; //
|
|
2520
|
+
columnSlotStoreService?: IOIMSlotStoreService<string, TOIMComputedColumnSlot>; // defaults.computedColumnSlotStoreService, then a shared map store
|
|
2484
2521
|
columnNames?: readonly Extract<keyof TValue, string>[];
|
|
2485
2522
|
computedSlotStoreService?: IOIMSlotStoreService<TKey, TOIMComputedSlot<TValue, TKey>, TComputedStore>; // Map; REQUIRED for any other store
|
|
2486
2523
|
}): OIMComputedService<TKey, TValue, TComputedStore>
|
|
2487
2524
|
```
|
|
2488
2525
|
|
|
2526
|
+
**`releaseValue(value)` gives back what a derived value holds** (a request in flight, a timer). It is called
|
|
2527
|
+
with the previous value when `derive` produced one `sameValueGuard` calls different; with a new object the
|
|
2528
|
+
guard calls equal (the slot keeps the old one, the new one is dropped); with the current value when the
|
|
2529
|
+
computed slot is released; with the value a `derive` returned after its own slot was released (not stored).
|
|
2530
|
+
It is not called when `derive` returns the object the slot already holds, and never with `initialValue`.
|
|
2531
|
+
`createOIMSingleComputedService` takes it too.
|
|
2532
|
+
|
|
2489
2533
|
The factory takes no db: a computed service is configuration, and each store slot takes its channel slot at
|
|
2490
2534
|
`createComputedStoreSlot(db, …)`.
|
|
2491
2535
|
|
|
@@ -2523,8 +2567,7 @@ A single computed store slot has no key and a store of one slot. `getSingleSlot(
|
|
|
2523
2567
|
next flush unless something holds it — and `holdSlotByKey(storeSlot, undefined)` holds the live slot, minting
|
|
2524
2568
|
one if the previous is gone. A computed slot goes at its last unhold once no effect reads it
|
|
2525
2569
|
(`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
|
|
2570
|
+
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
2571
|
gives back are one stateless service, shared by both shapes.
|
|
2529
2572
|
|
|
2530
2573
|
**`createSources` takes the key and the store slot, and nothing else. There is no `keep`.** A build that
|
|
@@ -2567,7 +2610,7 @@ over the carrier computed, its value `carrier.value[column]` (a tuple of the gro
|
|
|
2567
2610
|
the whole value for the any-column), compared with `Object.is`. It is a `TOIMComputedColumnSlot`
|
|
2568
2611
|
(`TOIMComputedSlot<unknown, string>`), so it goes in the SECOND array of `createEffect`, and its value
|
|
2569
2612
|
is read off `columnSlot.value`. It wakes an effect only when that field of the derived value changes.
|
|
2570
|
-
Pinned by `
|
|
2613
|
+
Pinned by `computed/tests/what-a-computed-column-wakes-on.test.ts`.
|
|
2571
2614
|
|
|
2572
2615
|
```ts
|
|
2573
2616
|
type Summary = { top: number; count: number };
|
|
@@ -2579,7 +2622,7 @@ const summary = summaryService.createComputedStoreSlot(
|
|
|
2579
2622
|
);
|
|
2580
2623
|
const summarySingleSlotService = summaryService.computedSingleSlotService;
|
|
2581
2624
|
const summaryTop = summarySingleSlotService.columnSlotService.holdColumn(summarySingleSlotService.getSingleSlot(summary), 'top');
|
|
2582
|
-
const topEffect = effectService.createEffect(db,
|
|
2625
|
+
const topEffect = effectService.createEffect(db, () => { paint(slot.entity); }, [], [summaryTop]);
|
|
2583
2626
|
effectService.disposeEffect(topEffect);
|
|
2584
2627
|
summarySingleSlotService.columnSlotService.unholdColumn(summaryTop);
|
|
2585
2628
|
```
|
|
@@ -2619,7 +2662,7 @@ const orderService = createOIMBucketMemoComputedService({
|
|
|
2619
2662
|
});
|
|
2620
2663
|
const order = orderService.createBucketMemoStoreSlot(ordered);
|
|
2621
2664
|
const deck = orderService.computedCollectionService.holdSlotByKey(order, 'spades'); // mints the bucket too, and holds it
|
|
2622
|
-
effectService.createEffect(db,
|
|
2665
|
+
effectService.createEffect(db, () => render(deck.computedValueService.getValueForSlot(deck)), [], [deck]);
|
|
2623
2666
|
```
|
|
2624
2667
|
|
|
2625
2668
|
---
|
|
@@ -2680,8 +2723,8 @@ are holding; neither is something you build by hand.
|
|
|
2680
2723
|
| Store | Key type |
|
|
2681
2724
|
|---|---|
|
|
2682
2725
|
| `OIMSlotMapStoreService<TSlot>` | `TOIMStoreKey` = `string \| number` |
|
|
2683
|
-
| `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `
|
|
2684
|
-
| `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `
|
|
2726
|
+
| `OIMSlotTreeStoreService<TOIMStoreKey, TSlot>` built with `OIMScalarKeyTreeWalk.INSTANCE` | `readonly TOIMStoreKey[]` — **a chain of VALUES** |
|
|
2727
|
+
| `OIMSlotTreeStoreService<TOIMEntitySlot, TSlot>` built with `OIMCompositeKeyTreeWalk.INSTANCE` | `readonly TOIMEntitySlot[]` — **a chain of entity SLOTS** |
|
|
2685
2728
|
| `OIMSingleSlotStoreService.INSTANCE` | `void` — one slot per store slot, its store a `TOIMSingleSlotStore` (`{ slot }`); what every single is built on |
|
|
2686
2729
|
|
|
2687
2730
|
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 +2765,8 @@ deletes nothing and returns `undefined`.
|
|
|
2722
2765
|
|
|
2723
2766
|
| Walk | Class | Shared instance | What a composite key part is | Holds? |
|
|
2724
2767
|
|---|---|---|---|---|
|
|
2725
|
-
| scalar | `OIMScalarKeyTreeWalk` | `
|
|
2726
|
-
| composite | `OIMCompositeKeyTreeWalk` | `
|
|
2768
|
+
| scalar | `OIMScalarKeyTreeWalk` | `OIMScalarKeyTreeWalk.INSTANCE` | `TOIMStoreKey` | no |
|
|
2769
|
+
| composite | `OIMCompositeKeyTreeWalk` | `OIMCompositeKeyTreeWalk.INSTANCE` | `TOIMEntitySlot` | **yes — one hold per edge** |
|
|
2727
2770
|
|
|
2728
2771
|
**The hold is the whole difference.** `OIMCompositeKeyTreeWalk.mintNodeAt` calls
|
|
2729
2772
|
`holdCompositeKeyPart` the first time it creates an edge, which runs
|
|
@@ -2745,10 +2788,10 @@ type TTallyStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<Tally,
|
|
|
2745
2788
|
|
|
2746
2789
|
const tallyService = createOIMSlotCollectionService<
|
|
2747
2790
|
ReturnType<typeof createOIMSlotContentEntityService<Tally, TScalarChain>>, TTallyStore
|
|
2748
|
-
>(
|
|
2749
|
-
slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(
|
|
2791
|
+
>({
|
|
2792
|
+
slotContentService: createOIMSlotContentEntityService<Tally, TScalarChain>(),
|
|
2750
2793
|
slotStoreService: new OIMSlotTreeStoreService<TOIMStoreKey, TOIMEntitySlot<Tally, TScalarChain>>(
|
|
2751
|
-
|
|
2794
|
+
OIMScalarKeyTreeWalk.INSTANCE,
|
|
2752
2795
|
),
|
|
2753
2796
|
});
|
|
2754
2797
|
const tallies = tallyService.createStoreSlot(db);
|
|
@@ -2775,10 +2818,10 @@ type TEnrolmentStore = import('@oimdb/core').TOIMSlotStoreTree<TOIMEntitySlot<En
|
|
|
2775
2818
|
|
|
2776
2819
|
const enrolmentService = createOIMSlotCollectionService<
|
|
2777
2820
|
ReturnType<typeof createOIMSlotContentEntityService<Enrolment, TChain>>, TEnrolmentStore
|
|
2778
|
-
>(
|
|
2779
|
-
slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(
|
|
2821
|
+
>({
|
|
2822
|
+
slotContentService: createOIMSlotContentEntityService<Enrolment, TChain>(),
|
|
2780
2823
|
slotStoreService: new OIMSlotTreeStoreService<TOIMEntitySlot, TOIMEntitySlot<Enrolment, TChain>>(
|
|
2781
|
-
|
|
2824
|
+
OIMCompositeKeyTreeWalk.INSTANCE,
|
|
2782
2825
|
),
|
|
2783
2826
|
});
|
|
2784
2827
|
const enrolments = enrolmentService.createStoreSlot(db);
|
|
@@ -2788,7 +2831,7 @@ enrolmentService.getSlotByKey(enrolments, [alice, admins]);
|
|
|
2788
2831
|
|
|
2789
2832
|
**Never put a scalar in a composite-walk key.** It is a type error, and
|
|
2790
2833
|
past a cast it throws a TypeError inside the mint, setting `holdCount` on a
|
|
2791
|
-
string, after the edge is already in the tree. `
|
|
2834
|
+
string, after the edge is already in the tree. `OIMScalarKeyTreeWalk.INSTANCE` is
|
|
2792
2835
|
the walk for that key — the same store class, the other strategy.
|
|
2793
2836
|
|
|
2794
2837
|
**Compare a chain with the strategy that matches it**: `oimSameSlotSequence` for identities
|
|
@@ -2801,8 +2844,7 @@ address" for the address the tree is looking at.
|
|
|
2801
2844
|
## Lifetime — the owner protocol
|
|
2802
2845
|
|
|
2803
2846
|
**`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
|
|
2847
|
+
`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
2848
|
and raises the RELEASED flag in `slot.slotFlags` instead, which the doors below read.
|
|
2807
2849
|
|
|
2808
2850
|
```ts signature
|
|
@@ -2828,18 +2870,15 @@ At a deadline the release queue reads its MARKER off the slot, decides, and call
|
|
|
2828
2870
|
|
|
2829
2871
|
**`OIMSlotOwner`** is the base class and it is concrete: it implements all five.
|
|
2830
2872
|
Its `releaseSlotIfUnheld` does nothing, its
|
|
2831
|
-
`unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `
|
|
2873
|
+
`unsafeMutateSlot(slot, mutateSlot)` hands `mutateSlot` the slot as `TOIMEditable` with no
|
|
2832
2874
|
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 `
|
|
2875
|
+
`slot.slotFlags`; the engine owns bits `1`, `2`, `4`, `8`, `16` and `32`, so a library starts at `64`;
|
|
2834
2876
|
it is on the owner so a subclass can hook every such mutation), and a `protected
|
|
2835
2877
|
onSlotIdle` does nothing; a `protected assertNotReleased` is what every refusing
|
|
2836
2878
|
door above calls. `OIMSlotOwnerService` overrides `onSlotIdle` and
|
|
2837
2879
|
`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.
|
|
2880
|
+
A single's slot is owned the same way: its owner is its collection type's slot owner, and that release policy
|
|
2881
|
+
applies to it.
|
|
2843
2882
|
|
|
2844
2883
|
### The column owner
|
|
2845
2884
|
|
|
@@ -2877,12 +2916,12 @@ so a slot with any live column slot is never released.
|
|
|
2877
2916
|
### Disposers
|
|
2878
2917
|
|
|
2879
2918
|
The disposers this package hands back are the unsubscribes `OIMChannelService.INSTANCE.subscribeAfterEachFrame`
|
|
2880
|
-
and its siblings return, and the one `effectService.subscribeEffect(db,
|
|
2919
|
+
and its siblings return, and the one `effectService.subscribeEffect(db, body, sourceSlots?, sourceComputeds?, sourceTrackingComputeds?)`
|
|
2881
2920
|
returns. An effect from `createEffect` has none, so the caller writes
|
|
2882
2921
|
`() => effectService.disposeEffect(effect)`. A hold is given back through its owner, not through a disposer.
|
|
2883
2922
|
|
|
2884
2923
|
```ts
|
|
2885
|
-
const effect = effectService.createEffect(db,
|
|
2924
|
+
const effect = effectService.createEffect(db, () => { paint(slot.entity); }, [slot]);
|
|
2886
2925
|
const disposers: TOIMDisposer[] = [];
|
|
2887
2926
|
disposers.push(() => { effectService.disposeEffect(effect); });
|
|
2888
2927
|
disposers.push(() => { slot.owner.unholdSlot(slot); });
|
|
@@ -2933,15 +2972,15 @@ leave the longest.
|
|
|
2933
2972
|
type TOIMStoreKey = string | number;
|
|
2934
2973
|
type TOIMDisposer = () => void;
|
|
2935
2974
|
type TOIMEffectBody = () => (() => void) | void; // a returned function is the undo
|
|
2936
|
-
type
|
|
2975
|
+
type TOIMSourceTrackingEffectBody = (sourceTracker: OIMSourceTracker) => (() => void) | void;
|
|
2937
2976
|
type TOIMSameValueGuard<TValue = unknown> = (prev: TValue, next: TValue) => boolean;
|
|
2938
2977
|
type TOIMChannelScheduler = (flush: () => void) => void;
|
|
2939
|
-
type TOIMSortColumn<TEntity extends object> = {
|
|
2978
|
+
type TOIMSortColumn<TEntity extends object> = { columnName: TOIMSortableColumnName<TEntity>; direction: 'asc' | 'desc' };
|
|
2940
2979
|
type TOIMSortableColumnName<TEntity extends object> = // a column whose values are string | number | bigint
|
|
2941
2980
|
{ [TColumn in keyof TEntity]-?: NonNullable<TEntity[TColumn]> extends string | number | bigint ? TColumn : never }[keyof TEntity] & string;
|
|
2942
2981
|
|
|
2943
2982
|
// EVERY property is `readonly`. A slot is written through its owner and its
|
|
2944
|
-
// service, never by assigning to it; `
|
|
2983
|
+
// service, never by assigning to it; `TOIMEditable` is how the engine
|
|
2945
2984
|
// itself drops that guard where it is the one mutator.
|
|
2946
2985
|
type TOIMSlot = {
|
|
2947
2986
|
readonly effects: TOIMEffect[] | undefined;
|
|
@@ -2952,9 +2991,9 @@ type TOIMSlot = {
|
|
|
2952
2991
|
readonly releaseDueAfterWriteMs: number; // -1 until a write stamps it
|
|
2953
2992
|
readonly releaseDueAfterIdleMs: number; // -1 until idleness stamps it
|
|
2954
2993
|
readonly releaseDueAfterUnholdMs: number; // -1 until the last unhold stamps it
|
|
2955
|
-
readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2
|
|
2994
|
+
readonly slotFlags: number; // OIM_SLOT_FLAG_EMPTY = 1, OIM_SLOT_FLAG_RELEASED = 2; bits up to 32 are the engine's
|
|
2956
2995
|
readonly columnSlotStore: unknown; // the column slots this slot carries; undefined while none
|
|
2957
|
-
readonly storeSlot: TOIMStoreSlot
|
|
2996
|
+
readonly storeSlot: TOIMStoreSlot; // the store slot it belongs to; a column slot's is its carrier's
|
|
2958
2997
|
readonly owner: IOIMSlotOwner; // never undefined, never cleared
|
|
2959
2998
|
};
|
|
2960
2999
|
|
|
@@ -2964,7 +3003,7 @@ type TOIMSlot = {
|
|
|
2964
3003
|
type TOIMStoreSlot<TStore = unknown, TSlot extends TOIMSlot = TOIMSlot, TPatch = unknown> = {
|
|
2965
3004
|
readonly store: TStore;
|
|
2966
3005
|
readonly channelSlot: TOIMChannelSlot;
|
|
2967
|
-
readonly contentChangeListeners:
|
|
3006
|
+
readonly contentChangeListeners: IOIMSlotContentChangeListener<TSlot, TPatch>[] | undefined;
|
|
2968
3007
|
readonly contentChangeListenerReadColumnNames: (readonly string[] | undefined)[] | undefined;
|
|
2969
3008
|
readonly indexKeysPositions: TOIMIndexKeysPositions | undefined;
|
|
2970
3009
|
readonly sourceStoreSlot: TOIMStoreSlot | undefined; // an index store slot's source
|
|
@@ -2981,7 +3020,7 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
|
|
|
2981
3020
|
TOIMStoreSlot<TStore, TBucketSlot, TBucketPatch> & {
|
|
2982
3021
|
readonly sourceStoreSlot: TOIMStoreSlot<TSourceStore, TSourceSlot, TSourcePatch>;
|
|
2983
3022
|
readonly indexKeysPosition: number;
|
|
2984
|
-
readonly contentChangeListener:
|
|
3023
|
+
readonly contentChangeListener: IOIMSlotContentChangeListener<TSourceSlot, TSourcePatch>;
|
|
2985
3024
|
readonly requestedKeys: readonly TKey[] | undefined;
|
|
2986
3025
|
readonly bucketColumnNames: Set<string>;
|
|
2987
3026
|
readonly isAnyBucketColumnRead: boolean;
|
|
@@ -2991,10 +3030,10 @@ type TOIMIndexStoreSlot<TStore, TBucketSlot, TBucketPatch, TSourceSlot, TSourceP
|
|
|
2991
3030
|
type TOIMComputedStoreSlot<TStore = unknown, TValue = unknown, TKey = unknown> =
|
|
2992
3031
|
TOIMStoreSlot<TStore, TOIMComputedSlot<TValue, TKey>> & {
|
|
2993
3032
|
readonly derive: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => TValue) | undefined;
|
|
2994
|
-
readonly createSources: ((
|
|
3033
|
+
readonly createSources: ((computedSlot: TOIMComputedSlot<TValue, TKey>) => Partial<TOIMComputedSources>) | undefined;
|
|
2995
3034
|
};
|
|
2996
3035
|
|
|
2997
|
-
type
|
|
3036
|
+
type TOIMEditable<TSlot> = { -readonly [K in keyof TSlot]: TSlot[K] };
|
|
2998
3037
|
|
|
2999
3038
|
type TOIMKeyedSlot<TKey = unknown> = TOIMSlot & { readonly key: TKey };
|
|
3000
3039
|
type TOIMIndexableSlot<TKey = unknown> = TOIMKeyedSlot<TKey> & { readonly indexKeys: unknown[] | undefined };
|
|
@@ -3006,7 +3045,7 @@ type TOIMEntitySlot<TEntity extends object = object, TKey = unknown> = TOIMIndex
|
|
|
3006
3045
|
|
|
3007
3046
|
// `data` belongs to the consumer: the core never reads or writes it, and clearing or
|
|
3008
3047
|
// 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
|
|
3048
|
+
// last key for the entity slot in `indexKeys`, through the index's `IOIMIndexKeysService`.
|
|
3010
3049
|
|
|
3011
3050
|
type TOIMColumnSlot = TOIMSlot & {
|
|
3012
3051
|
readonly carrierSlot: TOIMSlot; // the entity slot or bucket slot it hangs off
|
|
@@ -3040,14 +3079,14 @@ type TOIMEntityColumnName<TEntity extends object> =
|
|
|
3040
3079
|
[TEntity] extends [never] ? never
|
|
3041
3080
|
: object extends TEntity ? string : keyof TEntity & string;
|
|
3042
3081
|
|
|
3043
|
-
type
|
|
3082
|
+
type TOIMComputedDerivation = TOIMComputedSources & { // what a computed slot and a TOIMEffect share
|
|
3044
3083
|
ranAtRevision: number; ranAtEpoch: number;
|
|
3045
3084
|
};
|
|
3046
3085
|
|
|
3047
3086
|
type TOIMTimer = (run: () => void, inMs: number) => TOIMDisposer;
|
|
3048
3087
|
```
|
|
3049
3088
|
|
|
3050
|
-
**`
|
|
3089
|
+
**`TOIMEditable<TSlot>` strips `readonly` from every property.** It is exported
|
|
3051
3090
|
because the rule "data has one mutator" is enforced by the slot types being
|
|
3052
3091
|
readonly everywhere else: a class that IS the sole mutator of a slot's property
|
|
3053
3092
|
casts through this type at the one place it writes, and every other reader is
|
|
@@ -3069,7 +3108,11 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3069
3108
|
|
|
3070
3109
|
## Full export list
|
|
3071
3110
|
|
|
3072
|
-
**
|
|
3111
|
+
**237 names, every export of `packages/core/src/index.ts`, grouped by topic.**
|
|
3112
|
+
|
|
3113
|
+
*Store aggregates* — `OIMSlotCollectionStoreAggregate` · `OIMWritableSlotCollectionStoreAggregate` ·
|
|
3114
|
+
`OIMSingleSlotStoreAggregate` · `OIMWritableSingleSlotStoreAggregate` · `OIMSlotCollectionIndexStoreAggregate` ·
|
|
3115
|
+
`createOIMSlotCollectionStoreAggregate` · `createOIMSingleSlotStoreAggregate`
|
|
3073
3116
|
|
|
3074
3117
|
*Channel and database slots* — `OIMChannelService` · `TOIMChannelSlot` · `TOIMChannelSlotOptions` · `createOIMChannelSlot` ·
|
|
3075
3118
|
`IOIMChannelMap` · `TOIMDBOptions` · `OIMFlushSchedulerService` · `TOIMFlushSchedulerSlot` · `IOIMFlushSchedulerListener`
|
|
@@ -3086,7 +3129,7 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3086
3129
|
`OIMSlotOwnerService` (**type only**) · `TOIMSlotOwnerServiceOptions` · `IOIMSlotContentService` · `IOIMSlotContentWriter` ·
|
|
3087
3130
|
`IOIMKeyedSlotCreator` · `IOIMSingleSlotCreator` · `IOIMSlotFilingService` · `TOIMSlotContentServiceKey` ·
|
|
3088
3131
|
`TOIMSlotContentServiceSlot` · `TOIMSlotContentServiceSingleSlot` · `TOIMSlotContentServiceContent` ·
|
|
3089
|
-
`TOIMSlotContentServicePatch` · `
|
|
3132
|
+
`TOIMSlotContentServicePatch` · `OIMSlotContentChangeListenerService`
|
|
3090
3133
|
|
|
3091
3134
|
*Content services* — `OIMSlotContentEntityService` · `TOIMSlotContentEntityServiceOptions` ·
|
|
3092
3135
|
`createOIMSlotContentEntityService` · `TOIMEntitySlotOwnerService` · `TOIMEntityContentSlot` · `OIMSlotContentBucketService` ·
|
|
@@ -3108,20 +3151,20 @@ that matches what you are comparing; they are not interchangeable.
|
|
|
3108
3151
|
written after that clock reading — `channelSlot.clock` read earlier.
|
|
3109
3152
|
|
|
3110
3153
|
*Derivations* — `OIMEffectService` · `IOIMEffectService` · `TOIMEffect` (**type only** —
|
|
3111
|
-
`OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `
|
|
3112
|
-
`
|
|
3113
|
-
`OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `
|
|
3154
|
+
`OIMEffectService.createEffect` is the constructor) · `TOIMEffectBody` · `TOIMSourceTrackingEffectBody` ·
|
|
3155
|
+
`OIMSourceTracker` (**type only** — the service builds it) · `TOIMComputedSlot` · `TOIMComputedSources` · `TOIMComputedValueReleaser` · `OIMSourceTrackingComputedService` · `OIMSingleSourceTrackingComputedService` · `createOIMSourceTrackingComputedService` · `createOIMSingleSourceTrackingComputedService` · `TOIMSourceTrackingComputedSlot` · `TOIMSourceTrackingComputedContentSlot` · `TOIMSourceTrackingComputedDerive` · `OIMSlotContentSourceTrackingComputedService` (**type only**) · `TOIMSourceTrackingComputedDerivationOptions` · `TOIMComputedServiceFactoryOptions` ·
|
|
3156
|
+
`OIMComputedValueService` · `IOIMComputedValueService` · `TOIMComputedValueServiceOptions` · `oimRefreshSourceComputeds` · `TOIMComputedDerivation`
|
|
3114
3157
|
|
|
3115
3158
|
`oimRefreshSourceComputeds(sourceComputeds)` brings every computed in the list current. A binding
|
|
3116
3159
|
read outside an effect (`useOIMValue` in `@oimdb/react`, `exoBinding` in `@oimdb/exodra`) calls it
|
|
3117
3160
|
on its `sourceComputeds` before `derive`, so the derive never reads a stale computed.
|
|
3118
3161
|
|
|
3119
|
-
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `
|
|
3162
|
+
*Slots and their owners* — `TOIMSlot` · `TOIMStoreSlot` · `TOIMSingleSlotStoreSlot` ·`IOIMStoreSlotListener` · `OIMNoStoreSlotListener` · `IOIMSlotSubscriptionListener` · `OIMNoSlotSubscriptionListener` ·`oimIsSlotEmpty` · `oimIsSlotReleased` · `TOIMEditable` · `TOIMKeyedSlot` · `TOIMIndexableSlot` ·
|
|
3120
3163
|
`TOIMEntitySlot` · `TOIMColumnSlot` · `TOIMComputedColumnSlot` · `TOIMColumnGroup` · `TOIMColumnReaders` · `OIMColumnSlotOwner` ·
|
|
3121
3164
|
`OIMPushColumnSlotOwner` · `OIMPullColumnSlotOwner` · `TOIMColumnSlotOwnerOptions` · `TOIMBucketSlot` ·
|
|
3122
3165
|
`TOIMSameValueGuard` · `OIMSlotOwner` ·
|
|
3123
3166
|
`IOIMSlotOwner` · `IOIMSlotEmptinessPolicy` ·
|
|
3124
|
-
`IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `
|
|
3167
|
+
`IOIMSlotStamp` · `oimHoldSlots` · `oimUnholdSlotsIfAlive` · `IOIMSlotContentChangeListener`
|
|
3125
3168
|
|
|
3126
3169
|
`oimIsSlotEmpty(slot): boolean` reads the EMPTY bit of `slot.slotFlags`.
|
|
3127
3170
|
`oimIsSlotReleased(slot): boolean` reads its RELEASED bit.
|
|
@@ -3134,9 +3177,9 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3134
3177
|
`OIMSlotAtUnholdReleasePolicy` · `OIMSlotAtEmptyReleasePolicy` · `OIMSlotNeverReleasePolicy` · `OIMSlotEarliestReleasePolicy` ·
|
|
3135
3178
|
`TOIMTimer` · `oimSliceOfMs` · `OIM_SLICE_LENGTH_MS`
|
|
3136
3179
|
|
|
3137
|
-
*Entity writers* — `IOIMEntityWriter` · `
|
|
3138
|
-
`
|
|
3139
|
-
`oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `
|
|
3180
|
+
*Entity writers* — `IOIMEntityWriter` · `createOIMInPlaceEntityWriter` ·
|
|
3181
|
+
`createOIMReplacingEntityWriter` · `TOIMEntityWriterOptions` · `TOIMEntityInserter` ·
|
|
3182
|
+
`oimInsertEntityWithCopy` · `oimInsertEntityWithoutCopy` · `oimInsertEntityWithSanitizedCopy` · `oimPatchEntityColumns` · `oimPatchEntityColumn` · `oimGuardedPatchEntityColumns` · `oimGuardedPatchEntityColumn` · `oimPutEntityColumns` · `oimGuardedPutEntityColumns` · `TOIMEntityColumnName` · `TOIMEntityColumnValue`
|
|
3140
3183
|
|
|
3141
3184
|
*Buckets* — `OIMBucketCommands` · `TOIMBucketCommandsOptions` · `IOIMBucketCommands` ·
|
|
3142
3185
|
`IOIMBucketMemberSlotsArrayService` · `OIMBucketMemberSlotsArrayService` · `OIMBucketUniqueMemberSlotsArrayService` ·
|
|
@@ -3160,13 +3203,12 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3160
3203
|
`TOIMSlotStoreTree` · `IOIMKeyTreeWalk` · `TOIMCompositeKeyNode` ·
|
|
3161
3204
|
`OIMKeyTreeWalk` · `OIMCompositeKeyTreeWalk` · `OIMScalarKeyTreeWalk`
|
|
3162
3205
|
|
|
3163
|
-
*Assemblies* — `OIMDB` · `OIMDefaults` · `OIMDefaultValue` · `TOIMFactoryOptions`
|
|
3206
|
+
*Assemblies* — `OIMDB` · `OIMDefaults` · `TOIMDefaultsOptions` · `OIMDefaultValue` · `TOIMFactoryOptions`
|
|
3164
3207
|
|
|
3165
3208
|
*Comparison and diffing* — `oimSameValueZeroGuard` ·
|
|
3166
3209
|
`oimSameSlotSequence` · `oimSameKeySequence` · `oimOrderedListCommandsOfTransition` · `TOIMOrderedListCommand`
|
|
3167
3210
|
|
|
3168
|
-
*Shared defaults* — `
|
|
3169
|
-
`OIMDefaults.KEY_TREE_WALK_SCALAR` and `OIMDefaults.KEY_TREE_WALK_COMPOSITE`.
|
|
3211
|
+
*Shared defaults* — `OIMScalarKeyTreeWalk.INSTANCE` and `OIMCompositeKeyTreeWalk.INSTANCE`.
|
|
3170
3212
|
|
|
3171
3213
|
**Nothing else is exported.** If a symbol is not on this list, it does not exist.
|
|
3172
3214
|
|
|
@@ -3174,18 +3216,18 @@ on its `sourceComputeds` before `derive`, so the derive never reads a stale comp
|
|
|
3174
3216
|
|
|
3175
3217
|
## Known defects
|
|
3176
3218
|
|
|
3177
|
-
### A scalar in a
|
|
3219
|
+
### A scalar in a key of the composite key tree walk throws
|
|
3178
3220
|
|
|
3179
|
-
`OIMSlotTreeStoreService` built with `
|
|
3221
|
+
`OIMSlotTreeStoreService` built with `OIMCompositeKeyTreeWalk.INSTANCE` keys on
|
|
3180
3222
|
`readonly TOIMEntitySlot[]`, so a string or a number in the key throws
|
|
3181
3223
|
a TypeError past a cast, setting `holdCount` on a string. The store for a tuple
|
|
3182
3224
|
of scalars is `OIMSlotTreeStoreService` built with
|
|
3183
|
-
`
|
|
3225
|
+
`OIMScalarKeyTreeWalk.INSTANCE` —
|
|
3184
3226
|
see **Key stores** above. The fix is the other walk, never a cast.
|
|
3185
3227
|
|
|
3186
3228
|
### `TOIMEffect` is a type-only export
|
|
3187
3229
|
|
|
3188
|
-
`effectService.createEffect(...)` and `effectService.
|
|
3230
|
+
`effectService.createEffect(...)` and `effectService.createAndRunEffect(...)` are the whole of
|
|
3189
3231
|
legitimate construction. An object literal typed `TOIMEffect` subscribes to nothing and
|
|
3190
3232
|
schedules nothing: when one of its sources already moved in the open batch, a
|
|
3191
3233
|
later write to that source is swallowed by the coalescing guard and the effect
|
|
@@ -3199,8 +3241,8 @@ Every `@oimdb` package ships both formats — `dist/index.js` for `import`,
|
|
|
3199
3241
|
condition so a CommonJS consumer's declarations match its runtime), both named
|
|
3200
3242
|
in the `exports` map. A runtime loads a package once per format, so a process
|
|
3201
3243
|
that reaches the core both ways would hold two copies of every class and every
|
|
3202
|
-
module-level value — two
|
|
3203
|
-
|
|
3244
|
+
module-level value — two sets of built-in defaults and two `OIMDefaults` classes,
|
|
3245
|
+
so services built by the two copies share nothing.
|
|
3204
3246
|
|
|
3205
3247
|
So `src/index.ts` registers its copy under `Symbol.for('@oimdb/core')` on
|
|
3206
3248
|
`globalThis`, and the second copy to load throws
|
|
@@ -3218,26 +3260,27 @@ core objects around — through both spellings in one process. Pick one.
|
|
|
3218
3260
|
|
|
3219
3261
|
## Other packages
|
|
3220
3262
|
|
|
3221
|
-
`@oimdb/react`, `@oimdb/persist` (with its five backends) and `@oimdb/
|
|
3222
|
-
on the current core — persist and
|
|
3263
|
+
`@oimdb/react`, `@oimdb/persist` (with its five backends), `@oimdb/exodra` and `@oimdb/browser` build
|
|
3264
|
+
on the current core — persist, exodra and browser have sections of their own below. Every
|
|
3223
3265
|
remaining package imports only names the core exports. Read a package's own
|
|
3224
3266
|
source before generating code against it.
|
|
3225
3267
|
|
|
3226
3268
|
`@oimdb/react` — `<OIMDBProvider db={db}>` carries the `OIMDB` (`useOIMDB()`) and nothing else: the effect
|
|
3227
3269
|
service is stateless (`OIMEffectService.INSTANCE`) and every hook subscribes with
|
|
3228
3270
|
`OIMEffectService.INSTANCE.subscribeEffect(db, …)`. A hook that resolves or takes a slot filed on another `OIMDB`
|
|
3229
|
-
throws, since nothing below would wake. Every hook that reads a collection takes the
|
|
3230
|
-
`Pick` of `OIMSlotCollectionService` with only
|
|
3231
|
-
|
|
3232
|
-
`
|
|
3233
|
-
`
|
|
3234
|
-
|
|
3235
|
-
`
|
|
3271
|
+
throws, since nothing below would wake. Every hook that reads a collection by key takes the store slot alone and
|
|
3272
|
+
calls the service it carries, `storeSlot.slotService`, typed as a `Pick` of `OIMSlotCollectionService` with only
|
|
3273
|
+
the members it calls (a store aggregate passes its `storeSlot`):
|
|
3274
|
+
`useOIMEntity(entityStoreSlot, key)` and
|
|
3275
|
+
`useOIMBucketSlotsByKey(bucketStoreSlot, key)` (reads `bucketStoreSlot.slotService.slotContentService.getBucketMemberSlots`;
|
|
3276
|
+
an index store slot fits too). The hooks over a slot you already hold take the service and the slot:
|
|
3277
|
+
`useOIMField(entityCollectionService, slot, name)` (calls `getColumn(slot, name)`),
|
|
3278
|
+
`useOIMBucketSlots(bucketCollectionService, bucketSlot)`, and
|
|
3236
3279
|
`useOIMValue(binding)`, where a binding is `{ sourceSlots?, sourceComputeds?, derive }` (a missing list means none)
|
|
3237
3280
|
and must keep its identity across renders.
|
|
3238
3281
|
`useOIMComputedValue(computedSlot: TOIMComputedSlot<TValue>): TValue` is the one-computed case: it subscribes to
|
|
3239
3282
|
the slot, settles it and reads `computedSlot.value`, with no binding to memoise.
|
|
3240
|
-
`useOIMComputedValueByKey(
|
|
3283
|
+
`useOIMComputedValueByKey(computedStoreSlot, key)` reads one key of a
|
|
3241
3284
|
computed store slot (plain, sorted, bucket memo) with the same by-key body as `useOIMEntity` /
|
|
3242
3285
|
`useOIMBucketSlotsByKey`: resolve on commit with `holdSlotByKey`, effect over that slot, resolve-hold given back,
|
|
3243
3286
|
a key built inline settled by meaning.
|
|
@@ -3246,16 +3289,15 @@ The computed SERVICE is the caller's, built once at module scope with `createOIM
|
|
|
3246
3289
|
slot per mount — `computedService.createComputedStoreSlot(db, derive, createSources)` in a `useMemo` on `deps`;
|
|
3247
3290
|
`derive` / `createSources` may be `undefined` when the service has them.
|
|
3248
3291
|
`useOIMSingleComputedValue(computedService, derive, createSources, deps): TValue` is the same for a void-keyed
|
|
3249
|
-
service
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
`useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)` holds the identities its keys name for as long as the
|
|
3292
|
+
service built with `createOIMSingleComputedService`, its slot read through the store slot's single slot service,
|
|
3293
|
+
so a flush between render and subscribe is safe.
|
|
3294
|
+
`useOIMSlotsByKeys(storeSlot, keys)` holds the identities its keys name for as long as the
|
|
3253
3295
|
component is mounted (a `holdSlotByKey` per key on commit, the same holds given back on unmount or when the list
|
|
3254
3296
|
moves) and subscribes to nothing; it needs no provider.
|
|
3255
|
-
Singles are not keyed collections: `createOIMSingleSlotService(
|
|
3297
|
+
Singles are not keyed collections: `createOIMSingleSlotService({ slotContentService })` returns an
|
|
3256
3298
|
`OIMWritableSingleSlotService` (`getSingleSlot(storeSlot)`, `holdSingleSlot(storeSlot)`), read by
|
|
3257
|
-
`useOIMSingleEntity(
|
|
3258
|
-
`useOIMSingleBucketSlots(
|
|
3299
|
+
`useOIMSingleEntity(singleSlotStoreSlot): TEntity | undefined` and
|
|
3300
|
+
`useOIMSingleBucketSlots(singleSlotStoreSlot): readonly TOIMEntitySlot<TEntity>[]` — the by-key
|
|
3259
3301
|
mechanics without a key (getSingleSlot on render, holdSingleSlot on commit, resolve-hold given back once the
|
|
3260
3302
|
effect exists; the bucket members are copied per render). See `packages/react/README.md`.
|
|
3261
3303
|
|
|
@@ -3264,11 +3306,12 @@ re-read the imports rather than trusting this table.
|
|
|
3264
3306
|
|
|
3265
3307
|
| Package | Imports from the core | Imports a name the core does not export? |
|
|
3266
3308
|
|---|---|---|
|
|
3267
|
-
| `@oimdb/persist` | `IOIMChannelMap`, `
|
|
3268
|
-
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `
|
|
3309
|
+
| `@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 |
|
|
3310
|
+
| `@oimdb/redux-adapter` | `IOIMChannelMap`, `IOIMSlotContentChangeListener`, `OIMChannelService`, `OIMPushColumnSlotOwner`, `OIMSlotContentChangeListenerService`, `OIMEffectService`, `OIMSlotCollectionService`, `OIMSlotContentBucketService`, `OIMSlotContentEntityService`, `TOIMBucketSlot`, `TOIMColumnSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntityColumnName`, `TOIMEntitySlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
3269
3311
|
| `@oimdb/exodra` | `IOIMChannelMap`, `OIMChannelService`, `OIMEffectService`, `OIMSlotContentBucketService`, `OIM_FROZEN_EMPTY_SLOTS`, `TOIMBucketSlot`, `TOIMComputedSlot`, `TOIMDisposer`, `TOIMEffect`, `TOIMEntitySlot`, `TOIMOrderedListCommand`, `TOIMSlot`, `TOIMStoreKey`, `oimOrderedListCommandsOfTransition`, `oimRefreshSourceComputeds`, `oimSameKeySequence` | no |
|
|
3270
3312
|
| `@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
3313
|
| `@oimdb/devtools` | `IOIMChannelMap`, `IOIMStoreSlotListener`, `OIMChannelService`, `OIMComputedService`, `OIMDefaults`, `OIMSlotCollectionService`, `TOIMBucketSlot`, `TOIMComputedStoreSlot`, `TOIMDefaultSlotStore`, `TOIMDisposer`, `TOIMEntitySlot`, `TOIMKeyedSlot`, `TOIMStoreKey`, `TOIMStoreSlot` | no |
|
|
3314
|
+
| `@oimdb/browser` | `IOIMSlotSubscriptionListener`, `OIMChannelService`, `OIMDefaults`, `OIMSingleSlotService`, `OIMSlotContentEntityService`, `TOIMDisposer`, `TOIMEntityContentSlot`, `TOIMSlot`, `createOIMSingleSlotService`, `createOIMSlotContentEntityService`, `oimIsSlotReleased` | no |
|
|
3272
3315
|
| `@oimdb/async` | — | deprecated stub, `export {}` |
|
|
3273
3316
|
| `@oimdb/snapshot-manager` | — | empty stub |
|
|
3274
3317
|
|
|
@@ -3306,9 +3349,9 @@ import {
|
|
|
3306
3349
|
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3307
3350
|
|
|
3308
3351
|
const persistEntityWriter = new OIMPersistEntityWriter<Card, TOIMStoreKey>({
|
|
3309
|
-
entityWriter:
|
|
3352
|
+
entityWriter: createOIMInPlaceEntityWriter(),
|
|
3310
3353
|
});
|
|
3311
|
-
const persistedCardCollectionService = createOIMSlotCollectionService(
|
|
3354
|
+
const persistedCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>({ entityWriter: persistEntityWriter }) });
|
|
3312
3355
|
const persistedCardEntityStoreSlot = persistedCardCollectionService.createStoreSlot(db);
|
|
3313
3356
|
|
|
3314
3357
|
const persistMemoryBackend = new OIMPersistMemoryBackend();
|
|
@@ -3471,9 +3514,9 @@ import {
|
|
|
3471
3514
|
import { OIMPersistMemoryBackend, OIMPersistMemoryRecordsService, OIMPersistMemoryWriteBatch } from '@oimdb/persist-memory';
|
|
3472
3515
|
|
|
3473
3516
|
type TDeckCardSlot = TOIMEntitySlot<Card, TOIMStoreKey>;
|
|
3474
|
-
const deckCardCollectionService = createOIMSlotCollectionService(
|
|
3517
|
+
const deckCardCollectionService = createOIMSlotCollectionService({ slotContentService: createOIMSlotContentEntityService<Card>() });
|
|
3475
3518
|
const deckCardEntityStoreSlot = deckCardCollectionService.createStoreSlot(db);
|
|
3476
|
-
const deckBucketService = createOIMSlotCollectionService(
|
|
3519
|
+
const deckBucketService = createOIMSlotCollectionService({ slotContentService: new OIMSlotContentPositionalArrayBucketService<TDeckCardSlot>({ bucketCommands: new OIMBucketCommands({ maxCommandsPerSlot: undefined }) }) });
|
|
3477
3520
|
const deckBucketStoreSlot = deckBucketService.createStoreSlot(db);
|
|
3478
3521
|
const spadesBucketSlot = deckBucketService.holdSlotByKey(deckBucketStoreSlot, 'spades');
|
|
3479
3522
|
|
|
@@ -3722,7 +3765,7 @@ import type { TOIMPersistVersionedShape } from '@oimdb/persist';
|
|
|
3722
3765
|
|
|
3723
3766
|
type TSettings = { theme: 'light' | 'dark'; density: 'compact' | 'comfortable' };
|
|
3724
3767
|
|
|
3725
|
-
const settingsSingleEntityService = createOIMSingleSlotService(
|
|
3768
|
+
const settingsSingleEntityService = createOIMSingleSlotService({ slotContentService: createOIMSlotContentEntityService<TSettings>() });
|
|
3726
3769
|
const settingsEntityStoreSlot = settingsSingleEntityService.createStoreSlot(db);
|
|
3727
3770
|
settingsSingleEntityService.writeSingleSlotContent(settingsEntityStoreSlot, { theme: 'light', density: 'compact' });
|
|
3728
3771
|
const itemByStorageKey = new Map<string, string>();
|
|
@@ -3869,9 +3912,9 @@ own properties, so a stored `__proto__` is data.
|
|
|
3869
3912
|
re-reads every member and every watched slot, and the source keeps one effect per
|
|
3870
3913
|
member entity slot.
|
|
3871
3914
|
- **The memory and JSON backends keep values by reference.** With
|
|
3872
|
-
`
|
|
3915
|
+
`createOIMInPlaceEntityWriter()` the stored entity is the live entity object, so an
|
|
3873
3916
|
in-place write moves the stored value before any commit, and `dehydrate()` returns
|
|
3874
|
-
the live objects. Use `
|
|
3917
|
+
the live objects. Use `createOIMReplacingEntityWriter()` with
|
|
3875
3918
|
`entityInserter: oimInsertEntityWithCopy` where the stored value must stay
|
|
3876
3919
|
what was committed, and serialize a dump at once.
|
|
3877
3920
|
- **Hydrate without a reconcile replaces.** The collection source deletes every
|
|
@@ -3892,7 +3935,7 @@ only on the type-only `@exodra/reactivity-types`.
|
|
|
3892
3935
|
|
|
3893
3936
|
### One door: `exoBinding(channelMap, binding)`
|
|
3894
3937
|
|
|
3895
|
-
A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, sourceSlots, sourceComputeds
|
|
3938
|
+
A subscription here is one call — `OIMEffectService.INSTANCE.subscribeEffect(channelMap, onChange, sourceSlots, sourceComputeds)` —
|
|
3896
3939
|
and the value is read off the slots afterwards. So one adapter covers an entity, a
|
|
3897
3940
|
column, a bucket's slots and a derivation:
|
|
3898
3941
|
|
|
@@ -3948,7 +3991,7 @@ resolved before its entity arrived, an entity since removed.
|
|
|
3948
3991
|
|
|
3949
3992
|
- `exoSource(read, subscribe)` — the lazy, ref-counted base. For sources that are
|
|
3950
3993
|
**not** this engine's.
|
|
3951
|
-
- `exoCombine(
|
|
3994
|
+
- `exoCombine(fn, sources)` — fan-in across any `TOIMExoReadable`. Not glitch-free;
|
|
3952
3995
|
keep heavy fan-in inside one computed.
|
|
3953
3996
|
- `exoFollowKey(key, makeBindable)` — repoints as a selection moves. The factory
|
|
3954
3997
|
must return a `TOIMExoDisposable`.
|
|
@@ -3982,6 +4025,29 @@ memoised per list, so two lists showing one entity slot render it twice.
|
|
|
3982
4025
|
|
|
3983
4026
|
---
|
|
3984
4027
|
|
|
4028
|
+
## `@oimdb/browser`
|
|
4029
|
+
|
|
4030
|
+
Browser state as single slots. Each factory returns an `OIMSingleSlotService` (`TOIMBrowserSingleSlotService<TEntity>`)
|
|
4031
|
+
built with a slot subscription listener: the browser listener is added when the slot gets its first
|
|
4032
|
+
subscriber (directly or through a computed that reads it) and removed when the last one goes; the current
|
|
4033
|
+
value is written at the first subscription, and a write that would land during a flush is made right after it.
|
|
4034
|
+
|
|
4035
|
+
```ts signature
|
|
4036
|
+
createOIMBrowserOnlineSingleSlotService(options?: { defaults?: OIMDefaults }): TOIMBrowserSingleSlotService<TOIMBrowserOnlineEntity> // { isOnline }
|
|
4037
|
+
createOIMBrowserFocusSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserFocusEntity> // { isFocused }: document.hasFocus()
|
|
4038
|
+
createOIMBrowserVisibilitySingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserVisibilityEntity> // { isVisible }
|
|
4039
|
+
createOIMBrowserLocationSingleSlotService(options?): TOIMBrowserSingleSlotService<TOIMBrowserLocationEntity> // { href, pathname, search, hash }
|
|
4040
|
+
```
|
|
4041
|
+
|
|
4042
|
+
Read it like any single: `const onlineSlot = onlineService.getSingleSlot(onlineService.createStoreSlot(db))`, then
|
|
4043
|
+
an effect over `[onlineSlot]`, or `useOIMSingleEntity(online)` in React. `isOnline` is
|
|
4044
|
+
`navigator.onLine` (a network interface exists, not a reachable internet); the location follows the
|
|
4045
|
+
Navigation API's `currententrychange` where it exists and `popstate` + `hashchange` otherwise, so without it a
|
|
4046
|
+
`history.pushState` is not seen until the next of those events. Without the API a sensor needs, the slot holds
|
|
4047
|
+
`true` (or an empty location). `OIMBrowserSlotSubscriptionListener` is the base class of the four listeners.
|
|
4048
|
+
|
|
4049
|
+
---
|
|
4050
|
+
|
|
3985
4051
|
## Performance
|
|
3986
4052
|
|
|
3987
4053
|
**No figure may be quoted for this engine, and none is quoted anywhere in this
|
|
@@ -4014,7 +4080,7 @@ The one quantity that needs no measurement, because it is arithmetic rather than
|
|
|
4014
4080
|
a reading: emptying a bucket one slot at a time on an array-driven bucket costs
|
|
4015
4081
|
n(n-1)/2 element moves — **499 500** for a thousand — which is why a bucket content service's `writeSlotContent`
|
|
4016
4082
|
and `clearSlotContent` go through one `drainSlots`. Counted, not timed, in
|
|
4017
|
-
`packages/core/src/
|
|
4083
|
+
`packages/core/src/bucket/array/tests/everyone-out-at-once-and-not-one-at-a-time.test.ts`, which
|
|
4018
4084
|
also pins `drainSlots` at zero element writes.
|
|
4019
4085
|
|
|
4020
4086
|
Fast properties are checked, not timed. Every slot kind is one object literal with
|