@oimdb/core 3.11.1 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/index.cjs +19 -0
- package/dist/index.d.cts +26 -5
- package/dist/index.d.ts +26 -5
- package/dist/index.js +19 -0
- package/llms.txt +36 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1331,6 +1331,24 @@ const unsubscribe = users.updateEventEmitter.subscribeOnKey('user1', () => {
|
|
|
1331
1331
|
unsubscribe();
|
|
1332
1332
|
```
|
|
1333
1333
|
|
|
1334
|
+
## 🕳️ Missing entities: the holes policy
|
|
1335
|
+
|
|
1336
|
+
A pk can point at nothing — never upserted, or removed while an index still holds it. One rule
|
|
1337
|
+
applies everywhere:
|
|
1338
|
+
|
|
1339
|
+
> **Reads are length-aligned and surface the gap as `undefined`. Dropping it is always a separate,
|
|
1340
|
+
> explicitly named call.**
|
|
1341
|
+
|
|
1342
|
+
```ts
|
|
1343
|
+
collection.getManyByPks(['u1', 'gone', 'u2']); // [User, undefined, User]
|
|
1344
|
+
collection.getManyByPksCompact(['u1', 'gone', 'u2']); // [User, User] — SHORTER, deliberate
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
`index.getEntitiesByKey`, `globalIndex.getEntities`, `stream.getEntitiesByKey` and every
|
|
1348
|
+
list-returning selector follow the same shape. A **derived** index is dense by construction, so the
|
|
1349
|
+
`| undefined` there is spurious; a **manual** index (`setPks`, composite) can hold a dangling pk, and
|
|
1350
|
+
compacting it away is how a torn state becomes invisible.
|
|
1351
|
+
|
|
1334
1352
|
## 📚 API Reference
|
|
1335
1353
|
|
|
1336
1354
|
### DX Factories
|
package/dist/index.cjs
CHANGED
|
@@ -1415,6 +1415,13 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
|
|
|
1415
1415
|
return this.slots.get(pk)?.item;
|
|
1416
1416
|
}
|
|
1417
1417
|
getManyByPks(pks) {
|
|
1418
|
+
const result = new Array(pks.length);
|
|
1419
|
+
for (let i = 0; i < pks.length; i++) {
|
|
1420
|
+
result[i] = this.getOneByPk(pks[i]);
|
|
1421
|
+
}
|
|
1422
|
+
return result;
|
|
1423
|
+
}
|
|
1424
|
+
getManyByPksCompact(pks) {
|
|
1418
1425
|
const result = [];
|
|
1419
1426
|
result.length = pks.length;
|
|
1420
1427
|
let writeIndex = 0;
|
|
@@ -1495,9 +1502,14 @@ var OIMCollection = class {
|
|
|
1495
1502
|
getOneByPk(pk) {
|
|
1496
1503
|
return this.store.getOneByPk(pk);
|
|
1497
1504
|
}
|
|
1505
|
+
/** Length-aligned with `pks`; `undefined` wherever a pk has no entity. */
|
|
1498
1506
|
getManyByPks(pks) {
|
|
1499
1507
|
return this.store.getManyByPks(pks);
|
|
1500
1508
|
}
|
|
1509
|
+
/** Holes filtered out — may be SHORTER than `pks`. Deliberate; see the store's docs. */
|
|
1510
|
+
getManyByPksCompact(pks) {
|
|
1511
|
+
return this.store.getManyByPksCompact(pks);
|
|
1512
|
+
}
|
|
1501
1513
|
getSlotByPk(pk) {
|
|
1502
1514
|
return this.store.getSlotByPk(pk);
|
|
1503
1515
|
}
|
|
@@ -4375,6 +4387,13 @@ var OIMCollectionStoreTrieDriven = class extends OIMCollectionStore {
|
|
|
4375
4387
|
return this.slots.get(pk)?.item;
|
|
4376
4388
|
}
|
|
4377
4389
|
getManyByPks(pks) {
|
|
4390
|
+
const result = new Array(pks.length);
|
|
4391
|
+
for (let i = 0; i < pks.length; i++) {
|
|
4392
|
+
result[i] = this.getOneByPk(pks[i]);
|
|
4393
|
+
}
|
|
4394
|
+
return result;
|
|
4395
|
+
}
|
|
4396
|
+
getManyByPksCompact(pks) {
|
|
4378
4397
|
const result = [];
|
|
4379
4398
|
result.length = pks.length;
|
|
4380
4399
|
let writeIndex = 0;
|
package/dist/index.d.cts
CHANGED
|
@@ -62,7 +62,18 @@ declare abstract class OIMCollectionStore<TEntity extends object, TPk extends TO
|
|
|
62
62
|
abstract removeOneByPk(pk: TPk): void;
|
|
63
63
|
abstract removeManyByPks(pks: readonly TPk[]): void;
|
|
64
64
|
abstract getOneByPk(pk: TPk): TEntity | undefined;
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Length-aligned with `pks`: `undefined` wherever a pk has no entity. A missing entity is a real
|
|
67
|
+
* state of the store, so the read surfaces it instead of deciding for the caller — the same
|
|
68
|
+
* contract as {@link OIMIndex.getEntitiesByKey} and every selector.
|
|
69
|
+
*/
|
|
70
|
+
abstract getManyByPks(pks: readonly TPk[]): (TEntity | undefined)[];
|
|
71
|
+
/**
|
|
72
|
+
* The same read with holes filtered out, so the result may be SHORTER than `pks`. Deliberate and
|
|
73
|
+
* separately named: silently swallowing a missing entity is exactly what makes torn state hard to
|
|
74
|
+
* find. Safe when the pks come from a derived index (dense by construction).
|
|
75
|
+
*/
|
|
76
|
+
abstract getManyByPksCompact(pks: readonly TPk[]): TEntity[];
|
|
66
77
|
abstract getAll(): TEntity[];
|
|
67
78
|
abstract getAllPks(): TPk[];
|
|
68
79
|
abstract countAll(): number;
|
|
@@ -785,7 +796,10 @@ declare class OIMCollection<TEntity extends object, TPk extends TOIMKey> {
|
|
|
785
796
|
protected readonly updateEntity: TOIMEntityUpdater<TEntity>;
|
|
786
797
|
constructor(opts?: TOIMCollectionOptions<TEntity, TPk>);
|
|
787
798
|
getOneByPk(pk: TPk): TEntity | undefined;
|
|
788
|
-
|
|
799
|
+
/** Length-aligned with `pks`; `undefined` wherever a pk has no entity. */
|
|
800
|
+
getManyByPks(pks: readonly TPk[]): (TEntity | undefined)[];
|
|
801
|
+
/** Holes filtered out — may be SHORTER than `pks`. Deliberate; see the store's docs. */
|
|
802
|
+
getManyByPksCompact(pks: readonly TPk[]): TEntity[];
|
|
789
803
|
getSlotByPk(pk: TPk): TOIMEntitySlot<TEntity, TPk> | undefined;
|
|
790
804
|
getOrReserveSlotByPk(pk: TPk): TOIMEntitySlot<TEntity, TPk>;
|
|
791
805
|
/**
|
|
@@ -1796,7 +1810,12 @@ type TOIMDerivedCollectionOptions<TDerived extends object, TPk extends TOIMKey>
|
|
|
1796
1810
|
* listens to the source and keeps itself in sync — add / update / remove.
|
|
1797
1811
|
*
|
|
1798
1812
|
* It is a first-class `OIMReactiveCollection`, so it can be indexed, selected,
|
|
1799
|
-
* and derived-from again
|
|
1813
|
+
* and derived-from again — a CHAIN of one-to-one projections, not a join. There
|
|
1814
|
+
* is deliberately no multi-source variant: a joined row would need a primary key
|
|
1815
|
+
* nobody references, and an identity nobody uses is not an identity. Combine
|
|
1816
|
+
* across collections with one `OIMComputed`, or, if something really does
|
|
1817
|
+
* reference the combined thing, give it its own collection and write to it.
|
|
1818
|
+
* Maintenance runs on the source's
|
|
1800
1819
|
* synchronous UPDATE channel, so a chain of derivations stays consistent within
|
|
1801
1820
|
* the same flush; delivery to keyed subscribers is batched on flush as usual.
|
|
1802
1821
|
*
|
|
@@ -1900,7 +1919,8 @@ declare class OIMCollectionStoreMapDriven<TEntity extends object, TPk extends TO
|
|
|
1900
1919
|
removeOneByPk(pk: TPk): void;
|
|
1901
1920
|
removeManyByPks(pks: readonly TPk[]): void;
|
|
1902
1921
|
getOneByPk(pk: TPk): TEntity | undefined;
|
|
1903
|
-
getManyByPks(pks: readonly TPk[]): TEntity[];
|
|
1922
|
+
getManyByPks(pks: readonly TPk[]): (TEntity | undefined)[];
|
|
1923
|
+
getManyByPksCompact(pks: readonly TPk[]): TEntity[];
|
|
1904
1924
|
getAll(): TEntity[];
|
|
1905
1925
|
countAll(): number;
|
|
1906
1926
|
clear(): void;
|
|
@@ -1951,7 +1971,8 @@ declare class OIMCollectionStoreTrieDriven<TEntity extends object> extends OIMCo
|
|
|
1951
1971
|
removeOneByPk(pk: TOIMKeyPath): void;
|
|
1952
1972
|
removeManyByPks(pks: readonly TOIMKeyPath[]): void;
|
|
1953
1973
|
getOneByPk(pk: TOIMKeyPath): TEntity | undefined;
|
|
1954
|
-
getManyByPks(pks: readonly TOIMKeyPath[]): TEntity[];
|
|
1974
|
+
getManyByPks(pks: readonly TOIMKeyPath[]): (TEntity | undefined)[];
|
|
1975
|
+
getManyByPksCompact(pks: readonly TOIMKeyPath[]): TEntity[];
|
|
1955
1976
|
getAll(): TEntity[];
|
|
1956
1977
|
countAll(): number;
|
|
1957
1978
|
clear(): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -62,7 +62,18 @@ declare abstract class OIMCollectionStore<TEntity extends object, TPk extends TO
|
|
|
62
62
|
abstract removeOneByPk(pk: TPk): void;
|
|
63
63
|
abstract removeManyByPks(pks: readonly TPk[]): void;
|
|
64
64
|
abstract getOneByPk(pk: TPk): TEntity | undefined;
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Length-aligned with `pks`: `undefined` wherever a pk has no entity. A missing entity is a real
|
|
67
|
+
* state of the store, so the read surfaces it instead of deciding for the caller — the same
|
|
68
|
+
* contract as {@link OIMIndex.getEntitiesByKey} and every selector.
|
|
69
|
+
*/
|
|
70
|
+
abstract getManyByPks(pks: readonly TPk[]): (TEntity | undefined)[];
|
|
71
|
+
/**
|
|
72
|
+
* The same read with holes filtered out, so the result may be SHORTER than `pks`. Deliberate and
|
|
73
|
+
* separately named: silently swallowing a missing entity is exactly what makes torn state hard to
|
|
74
|
+
* find. Safe when the pks come from a derived index (dense by construction).
|
|
75
|
+
*/
|
|
76
|
+
abstract getManyByPksCompact(pks: readonly TPk[]): TEntity[];
|
|
66
77
|
abstract getAll(): TEntity[];
|
|
67
78
|
abstract getAllPks(): TPk[];
|
|
68
79
|
abstract countAll(): number;
|
|
@@ -785,7 +796,10 @@ declare class OIMCollection<TEntity extends object, TPk extends TOIMKey> {
|
|
|
785
796
|
protected readonly updateEntity: TOIMEntityUpdater<TEntity>;
|
|
786
797
|
constructor(opts?: TOIMCollectionOptions<TEntity, TPk>);
|
|
787
798
|
getOneByPk(pk: TPk): TEntity | undefined;
|
|
788
|
-
|
|
799
|
+
/** Length-aligned with `pks`; `undefined` wherever a pk has no entity. */
|
|
800
|
+
getManyByPks(pks: readonly TPk[]): (TEntity | undefined)[];
|
|
801
|
+
/** Holes filtered out — may be SHORTER than `pks`. Deliberate; see the store's docs. */
|
|
802
|
+
getManyByPksCompact(pks: readonly TPk[]): TEntity[];
|
|
789
803
|
getSlotByPk(pk: TPk): TOIMEntitySlot<TEntity, TPk> | undefined;
|
|
790
804
|
getOrReserveSlotByPk(pk: TPk): TOIMEntitySlot<TEntity, TPk>;
|
|
791
805
|
/**
|
|
@@ -1796,7 +1810,12 @@ type TOIMDerivedCollectionOptions<TDerived extends object, TPk extends TOIMKey>
|
|
|
1796
1810
|
* listens to the source and keeps itself in sync — add / update / remove.
|
|
1797
1811
|
*
|
|
1798
1812
|
* It is a first-class `OIMReactiveCollection`, so it can be indexed, selected,
|
|
1799
|
-
* and derived-from again
|
|
1813
|
+
* and derived-from again — a CHAIN of one-to-one projections, not a join. There
|
|
1814
|
+
* is deliberately no multi-source variant: a joined row would need a primary key
|
|
1815
|
+
* nobody references, and an identity nobody uses is not an identity. Combine
|
|
1816
|
+
* across collections with one `OIMComputed`, or, if something really does
|
|
1817
|
+
* reference the combined thing, give it its own collection and write to it.
|
|
1818
|
+
* Maintenance runs on the source's
|
|
1800
1819
|
* synchronous UPDATE channel, so a chain of derivations stays consistent within
|
|
1801
1820
|
* the same flush; delivery to keyed subscribers is batched on flush as usual.
|
|
1802
1821
|
*
|
|
@@ -1900,7 +1919,8 @@ declare class OIMCollectionStoreMapDriven<TEntity extends object, TPk extends TO
|
|
|
1900
1919
|
removeOneByPk(pk: TPk): void;
|
|
1901
1920
|
removeManyByPks(pks: readonly TPk[]): void;
|
|
1902
1921
|
getOneByPk(pk: TPk): TEntity | undefined;
|
|
1903
|
-
getManyByPks(pks: readonly TPk[]): TEntity[];
|
|
1922
|
+
getManyByPks(pks: readonly TPk[]): (TEntity | undefined)[];
|
|
1923
|
+
getManyByPksCompact(pks: readonly TPk[]): TEntity[];
|
|
1904
1924
|
getAll(): TEntity[];
|
|
1905
1925
|
countAll(): number;
|
|
1906
1926
|
clear(): void;
|
|
@@ -1951,7 +1971,8 @@ declare class OIMCollectionStoreTrieDriven<TEntity extends object> extends OIMCo
|
|
|
1951
1971
|
removeOneByPk(pk: TOIMKeyPath): void;
|
|
1952
1972
|
removeManyByPks(pks: readonly TOIMKeyPath[]): void;
|
|
1953
1973
|
getOneByPk(pk: TOIMKeyPath): TEntity | undefined;
|
|
1954
|
-
getManyByPks(pks: readonly TOIMKeyPath[]): TEntity[];
|
|
1974
|
+
getManyByPks(pks: readonly TOIMKeyPath[]): (TEntity | undefined)[];
|
|
1975
|
+
getManyByPksCompact(pks: readonly TOIMKeyPath[]): TEntity[];
|
|
1955
1976
|
getAll(): TEntity[];
|
|
1956
1977
|
countAll(): number;
|
|
1957
1978
|
clear(): void;
|
package/dist/index.js
CHANGED
|
@@ -1265,6 +1265,13 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
|
|
|
1265
1265
|
return this.slots.get(pk)?.item;
|
|
1266
1266
|
}
|
|
1267
1267
|
getManyByPks(pks) {
|
|
1268
|
+
const result = new Array(pks.length);
|
|
1269
|
+
for (let i = 0; i < pks.length; i++) {
|
|
1270
|
+
result[i] = this.getOneByPk(pks[i]);
|
|
1271
|
+
}
|
|
1272
|
+
return result;
|
|
1273
|
+
}
|
|
1274
|
+
getManyByPksCompact(pks) {
|
|
1268
1275
|
const result = [];
|
|
1269
1276
|
result.length = pks.length;
|
|
1270
1277
|
let writeIndex = 0;
|
|
@@ -1345,9 +1352,14 @@ var OIMCollection = class {
|
|
|
1345
1352
|
getOneByPk(pk) {
|
|
1346
1353
|
return this.store.getOneByPk(pk);
|
|
1347
1354
|
}
|
|
1355
|
+
/** Length-aligned with `pks`; `undefined` wherever a pk has no entity. */
|
|
1348
1356
|
getManyByPks(pks) {
|
|
1349
1357
|
return this.store.getManyByPks(pks);
|
|
1350
1358
|
}
|
|
1359
|
+
/** Holes filtered out — may be SHORTER than `pks`. Deliberate; see the store's docs. */
|
|
1360
|
+
getManyByPksCompact(pks) {
|
|
1361
|
+
return this.store.getManyByPksCompact(pks);
|
|
1362
|
+
}
|
|
1351
1363
|
getSlotByPk(pk) {
|
|
1352
1364
|
return this.store.getSlotByPk(pk);
|
|
1353
1365
|
}
|
|
@@ -4225,6 +4237,13 @@ var OIMCollectionStoreTrieDriven = class extends OIMCollectionStore {
|
|
|
4225
4237
|
return this.slots.get(pk)?.item;
|
|
4226
4238
|
}
|
|
4227
4239
|
getManyByPks(pks) {
|
|
4240
|
+
const result = new Array(pks.length);
|
|
4241
|
+
for (let i = 0; i < pks.length; i++) {
|
|
4242
|
+
result[i] = this.getOneByPk(pks[i]);
|
|
4243
|
+
}
|
|
4244
|
+
return result;
|
|
4245
|
+
}
|
|
4246
|
+
getManyByPksCompact(pks) {
|
|
4228
4247
|
const result = [];
|
|
4229
4248
|
result.length = pks.length;
|
|
4230
4249
|
let writeIndex = 0;
|
package/llms.txt
CHANGED
|
@@ -203,7 +203,7 @@ rows.subscribeOnKey('team1', () => { for (const row of rows.getByKey('team1')) {
|
|
|
203
203
|
|
|
204
204
|
### Derived collections
|
|
205
205
|
|
|
206
|
-
`OIMDerivedCollection(queue, source, derive, opts?)` is a reactive collection whose entities are derived **one-to-one** from a source collection — `derived[pk] = derive(source[pk], pk)`, keyed by the SOURCE pk — kept in sync (add / update / remove) automatically. It is itself a first-class `OIMReactiveCollection`, so it can be indexed, selected, and derived-from again
|
|
206
|
+
`OIMDerivedCollection(queue, source, derive, opts?)` is a reactive collection whose entities are derived **one-to-one** from a source collection — `derived[pk] = derive(source[pk], pk)`, keyed by the SOURCE pk — kept in sync (add / update / remove) automatically. It is itself a first-class `OIMReactiveCollection`, so it can be indexed, selected, and derived-from again — a CHAIN of one-to-one projections, NOT a join. There is deliberately no multi-source derived collection: a joined row would need a pk nobody references, and an identity nobody uses is not an identity; combine across collections with one `OIMComputed`, or give the combined thing its own collection if something genuinely references it; maintenance runs on the source's **synchronous** update channel, so a chain of derivations stays consistent within a flush (delivery to keyed subscribers is batched on flush as usual). It parallels `OIMDerivedCollectionIndex*` and needs no compute runtime.
|
|
207
207
|
|
|
208
208
|
```ts
|
|
209
209
|
const nameView = new OIMDerivedCollection(queue, users,
|
|
@@ -395,41 +395,55 @@ Signal hooks: `useSelectEntityByPkSignal(collection, pk)`, `useSelectPksByIndexK
|
|
|
395
395
|
|
|
396
396
|
Trade-offs (use only when every reader is subscription-driven): NOT Concurrent-safe; the entity reference is stable across changes, so `React.memo` on entities, prev/next diffing, time-travel, and the default uSES hooks on the same collection won't see updates. Default to the standard immutable hooks; reach for this only when a profile shows the data layer is the bottleneck.
|
|
397
397
|
|
|
398
|
+
### Missing entities: the holes policy
|
|
399
|
+
|
|
400
|
+
A pk can point at nothing (never upserted, or removed while an index still holds it). ONE rule across the library: **reads are length-aligned and surface the gap as `undefined`; dropping it is always a separate, explicitly named call.** `collection.getManyByPks(pks)` → `(TEntity | undefined)[]`, aligned. `collection.getManyByPksCompact(pks)` → `TEntity[]`, filtered and possibly SHORTER. Same shape for `index.getEntitiesByKey`, `globalIndex.getEntities`, `stream.getEntitiesByKey` and every list-returning selector. A DERIVED index is dense by construction, so there the `| undefined` is spurious — but a MANUAL index (`setPks`, composite) can genuinely hold a dangling pk, and compacting it away is how torn state becomes invisible. `@oimdb/exodra` mirrors this: default index read keeps holes, `.compact(key)` filters, `.unsafeDense(key)` only retypes and checks nothing.
|
|
401
|
+
|
|
398
402
|
## Exodra (`@oimdb/exodra`)
|
|
399
403
|
|
|
400
404
|
```bash
|
|
401
405
|
npm install @oimdb/exodra @oimdb/core
|
|
402
406
|
```
|
|
403
407
|
|
|
404
|
-
|
|
408
|
+
The Exodra counterpart of `@oimdb/react`: instead of a hook + re-render, every OIMDB read becomes a read-only Exodra bindable (`TExoBindable<T> = { getValue(): T; subscribe(update): () => void }`). No React. Built on `@oimdb/core`'s `OIMSelector` (`{ getValue, watch }`) — the fine-grained subscription already lives there, so this is a thin adapter. Depends only on the type-only `@exodra/reactivity-types` (never the reactivity runtime); peer dep `@oimdb/core`. Everything produced is read-only, lazy + ref-counted (upstream subscribed only while a downstream subscriber exists → O(visible)) and SSR-safe (`getValue()` reads through, valid with no subscription — a reactive key resolves on every read, never stale).
|
|
405
409
|
|
|
406
410
|
```ts
|
|
407
|
-
import {
|
|
408
|
-
import { bindable, derive } from '@exodra/reactivity';
|
|
411
|
+
import { exoDb } from '@oimdb/exodra';
|
|
412
|
+
import { bindable, derive, h } from '@exodra/reactivity';
|
|
409
413
|
|
|
410
|
-
|
|
411
|
-
const
|
|
414
|
+
const byTeam = usersKit.indexFactory.derivedSetIndex<string>(u => u.teamId);
|
|
415
|
+
const byTeamOrdered = usersKit.indexFactory.derivedArrayIndex<string>(u => u.teamId, { orderBy: u => u.name });
|
|
416
|
+
const online = usersKit.indexFactory.derivedSetGlobalIndex({ filter: u => u.online });
|
|
417
|
+
|
|
418
|
+
const db = exoDb({
|
|
419
|
+
users: { kit: usersKit, indexes: { byTeam, byTeamOrdered, online } },
|
|
420
|
+
tags: { kit: tagsKit }, // no indexes → byPk / byPks only
|
|
421
|
+
});
|
|
412
422
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
423
|
+
const team = bindable('t1');
|
|
424
|
+
db.users.byPk('u1'); // TExoBindable<User | undefined>
|
|
425
|
+
db.users.byTeam(team); // set index — the kind is inferred from the index
|
|
426
|
+
db.users.online(); // global index — no key, and none is asked for
|
|
427
|
+
db.users.byTeamOrdered.rows(team, render); // → bindables.children (identity-stable)
|
|
428
|
+
db.users.byTeamOrdered.list('t1', render); // → bindableLists.children (O(delta), fixed key)
|
|
418
429
|
```
|
|
419
430
|
|
|
420
431
|
| Export | Purpose |
|
|
421
432
|
|---|---|
|
|
422
|
-
| `
|
|
423
|
-
| `
|
|
424
|
-
| `
|
|
425
|
-
| `
|
|
426
|
-
| `
|
|
427
|
-
| `
|
|
428
|
-
| `
|
|
429
|
-
| `
|
|
430
|
-
| `
|
|
431
|
-
|
|
432
|
-
|
|
433
|
+
| `exoDb(spec)` | whole system at once: `{ name: { kit, indexes? } }` → record of typed views |
|
|
434
|
+
| `exoCollection(kit, indexes?)` | one collection's Exodra view; `kit` stays reachable for writes |
|
|
435
|
+
| `<collection>.all()` | every entity, reactively — non-stale `<option>` lists, and the change signal for `exoCombine` when a row key depends on another collection |
|
|
436
|
+
| `exoSource(read, subscribe)` | base primitive: a raw pair → a lazy, ref-counted bindable |
|
|
437
|
+
| `exoSelector(selector)` / `exoComputed(computed)` | an `OIMSelector` / `OIMComputed` → a bindable |
|
|
438
|
+
| `exoKeyed(keyBindable, makeSelector)` | a MOVING key: repoints the selector as the key changes |
|
|
439
|
+
| `exoCombine(sources, fn)` | multi-source derive (Exodra's own `derive` is single-source) |
|
|
440
|
+
| `exoRows(keys, render)` | identity-stable children for any items |
|
|
441
|
+
| `exoList(stream, key, render)` | OIMDB ordered command stream → Exodra `bindableList` (commands map 1:1 to `TExoListOp`) |
|
|
442
|
+
|
|
443
|
+
Each named index becomes a CALLABLE member of that name carrying `pks`, `rows`, `subscribe` (and `list` on ordered ones), so the caller never picks a method to match the index kind and never passes the index object again. Any key accepts `TKey | TOIMExodraReadable<TKey>`. Dispatch is structural: array-based `getPksByKey` returns `TPk[]`, set-based returns `Set<TPk>`, keyless ones have none; composite indexes are the same classes with an array key, resolved per call via `Array.isArray(key)`. **`list` exists only on array-based indexes** — a stream is derived by diffing an order and a set has none; enforced in the types, and it builds/memoizes the stream itself. `indexes` is a second argument because a kit does not own its indexes (`kit.indexFactory` creates them, the app keeps them); if core ever lets a kit hold named indexes this becomes optional without a break.
|
|
444
|
+
|
|
445
|
+
**No function in this package takes an equality option, and there is no options type.** Equality belongs to whoever owns the value: `OIMSelector.areEqual` for selectors (override it), `OIMComputed`'s `compare` for computeds (pass at construction), your own `fn` for `exoCombine` (return a stable reference when content is unchanged), your own `subscribe` callback for `exoSource(read, subscribe)` (compare there and don't call `onChange` — that callback IS the extension point). Those owners run BELOW the bridge, so a filter here could only reject what they passed, never resurrect what they dropped; in-place entity updaters need no configuration anywhere. `exoCombine` drops recomputes landing on the same value, collapsing the N notifications N sources produce for one logical change — not glitch-free though, so put real fan-in in ONE `OIMComputed` forwarded via `exoComputed`. `exoRows` reads are idempotent (same array reference back, no render, no eviction, until the key sequence changes) and require UNIQUE keys — a duplicate throws rather than placing one schema object twice. `exoList` memoizes `render` per item, so repeated `snapshot()` calls return the same row objects (identity is what Exodra reconciles on); the memo is pruned against the live order on snapshot and amortized past 2x the live row count. Reads are length-aligned WITH HOLES by default (`readonly (T | undefined)[]`, matching `@oimdb/react`) — a missing entity is a real state of the store and hiding it is not the bridge's call. Dropping holes is explicit and never a flag: `<index>.compact(key)` filters (may be SHORTER — on MANUAL `setPks` pks that silently swallows a torn state), `<index>.unsafeDense(key)` filters and checks NOTHING and just retypes to `readonly T[]` — correct for a DERIVED index, which is dense by construction, and `unsafe` because the guarantee is the caller's. Pure aliases of a single core call (`getOneByPk`, `subscribeOnKey`, `getPksByKey`) are NOT exported — call core directly. Pair `exoDb` with Exodra's own DI: `createContextKey<typeof db>('db')`, created by the app since this package pulls types only. Exodra bucket bindings subscribe at *build* (not `onExoMount`); `<index>.subscribe(key, onChange)` covers `onExoMount`-scoped needs. No function takes more than one shape of argument: a fixed key and a moving key are different members (`<index>(key)` vs `<index>.for(keyBindable)`), never one member that inspects what it was given. `exoCombine`'s FIRST emit after subscribing always propagates — OIMDB sources fire synchronously on subscribe and a binding is built from `getValue()` then wired a moment later, so suppressing it leaves the region frozen at its build-time value. Sorting and filtering belong in the index (`derivedArrayIndex({ orderBy })`, or emit no key for excluded entities), not in `rows` — there they are maintained incrementally instead of recomputed per read. The PRIMITIVES are the load-bearing API: screens that sort/filter beyond an index, key rows off related data, or fan in across collections are built from `exoRows` over an `OIMComputed` forwarded by `exoComputed`, and that is the intended path, not a fallback. Patterns: ordered column → order in the index (`derivedArrayIndex({ orderBy })`), never a per-render sort; filtered list → emit no key for excluded entities; a row that follows another collection → bind INSIDE the row (`derive(exoSelector(other.select.byPk(id)), …)`) so only that node updates; a key is IDENTITY, never invalidation — `key: t => t.id` and nothing else; a composite key re-derives the whole key sequence per read and rebuilds rows whose markup did not change (2000 rows, one added comment: 2.44ms + 1 wasted rebuild vs 0.73ms + zero when the count is bound inside the row), so bind what varies WITHIN a row via `derive(db.comments.byTask.pks(pk), c => c.length)`; `<select>` options → `all()`, never a one-off `getAll()` at build; aggregate → ONE `OIMComputed` + `exoComputed`; moving selection → `<index>.for(keyBindable)`; `onExoMount` scope → `<index>.subscribe(key, onChange)`. Write-side is intentionally omitted (apps write via orchestration, e.g. cnstra, not from views).
|
|
446
|
+
|
|
433
447
|
|
|
434
448
|
## Persist (`@oimdb/persist`)
|
|
435
449
|
|
package/package.json
CHANGED