foldkit-remote 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -70
- package/dist/index.d.mts +358 -28
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +701 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Duration, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
2
2
|
import * as Domain from "foldkit-entity";
|
|
3
|
-
import { AnyEntity, AnyQuery, InputExpr, Query as Query$1 } from "foldkit-entity";
|
|
3
|
+
import { AnyEntity, AnyQuery, Dependencies, InputExpr, Query as Query$1 } from "foldkit-entity";
|
|
4
4
|
import { EntryWithoutKeepAlive } from "foldkit/subscription";
|
|
5
5
|
import { ActiveSurface, Contract, Invalid, Metadata, ModelRef, Projection, Surface, Wiring } from "foldkit-surface";
|
|
6
6
|
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
@@ -381,6 +381,16 @@ export interface QueryDescriptor<Name extends string, Input, Result> {
|
|
|
381
381
|
* will be given rather than being it.
|
|
382
382
|
*/
|
|
383
383
|
export type QueryInputs<Input> = { readonly [K in keyof Input & string]: InputExpr<Input[K]>; };
|
|
384
|
+
/**
|
|
385
|
+
* What separates a query's name from its canonical encoded input in a
|
|
386
|
+
* connection identity.
|
|
387
|
+
*
|
|
388
|
+
* NUL, because `JSON.stringify` escapes it inside a string (`\u0000`) and a
|
|
389
|
+
* query name is a TypeScript identifier — so the first occurrence is always the
|
|
390
|
+
* boundary, and a name or an input value cannot forge one. Anything recovering
|
|
391
|
+
* the query from an identity splits here.
|
|
392
|
+
*/
|
|
393
|
+
export declare const IDENTITY_SEPARATOR = "\0";
|
|
384
394
|
/** Stable stringify: object keys sorted, undefined-valued keys dropped, so equal inputs encode equally. */
|
|
385
395
|
export declare const stableStringify: (value: unknown) => string;
|
|
386
396
|
/**
|
|
@@ -448,6 +458,8 @@ export declare const Query: {
|
|
|
448
458
|
where: (...predicates: ReadonlyArray<import("foldkit-entity").Predicate>) => <E extends AnyEntity>(self: Query$1<E>) => Query$1<E>;
|
|
449
459
|
orderBy: (...terms: ReadonlyArray<import("foldkit-entity").OrderTerm>) => <E extends AnyEntity>(self: Query$1<E>) => Query$1<E>;
|
|
450
460
|
dependencies: (self: AnyQuery) => import("foldkit-entity").Dependencies;
|
|
461
|
+
unsupported: (self: AnyQuery, supported: ReadonlyArray<import("foldkit-entity").Operation>) => ReadonlyArray<import("foldkit-entity").Operation>;
|
|
462
|
+
show: (self: AnyQuery) => string;
|
|
451
463
|
};
|
|
452
464
|
//#endregion
|
|
453
465
|
//#region src/requirement.d.ts
|
|
@@ -651,8 +663,6 @@ export interface BoundaryCounts {
|
|
|
651
663
|
export interface LiveState {
|
|
652
664
|
/** Last applied cursor for this stream. */
|
|
653
665
|
readonly cursor: LiveCursor;
|
|
654
|
-
/** Connections marked stale by an invalidating event. */
|
|
655
|
-
readonly stale: ReadonlySet<string>;
|
|
656
666
|
/** Edges recorded outside the loaded boundary, not visible in `items`. */
|
|
657
667
|
readonly boundary: Readonly<Record<string, BoundaryCounts>>;
|
|
658
668
|
}
|
|
@@ -665,7 +675,6 @@ export type LiveOutcome = 'applied' | 'duplicate' | 'gap';
|
|
|
665
675
|
* spurious gap.
|
|
666
676
|
*/
|
|
667
677
|
export declare const classifyLive: (state: LiveState, cursor: LiveCursor) => LiveOutcome;
|
|
668
|
-
export declare const invalidateConnection: (state: LiveState, connection: string) => LiveState;
|
|
669
678
|
/**
|
|
670
679
|
* A subscriber is woken only if the event changed a field it selects. A
|
|
671
680
|
* change to an unselected field is skipped entirely.
|
|
@@ -683,17 +692,17 @@ export interface ConnectionApplied {
|
|
|
683
692
|
readonly state: LiveState;
|
|
684
693
|
readonly optimistic: OptimisticState;
|
|
685
694
|
readonly outcome: LiveOutcome;
|
|
695
|
+
/**
|
|
696
|
+
* A connection the event says to refetch. Reported rather than recorded here,
|
|
697
|
+
* because staleness lives on the connection in the Model — which is what the
|
|
698
|
+
* planner and every read consult. It used to be written into a set on this
|
|
699
|
+
* live state that nothing read, so an invalidating event did nothing at all.
|
|
700
|
+
*/
|
|
701
|
+
readonly invalidated?: string | undefined;
|
|
686
702
|
}
|
|
687
703
|
export declare const applyConnectionEvent: (state: LiveState, optimistic: OptimisticState, event: Extract<LiveEvent, {
|
|
688
704
|
_tag: "ConnectionInsert" | "ConnectionRemove" | "ConnectionInvalidate";
|
|
689
705
|
}>, policy?: LivePolicy) => ConnectionApplied;
|
|
690
|
-
export declare const isStale: (state: LiveState, connection: string) => boolean;
|
|
691
|
-
/**
|
|
692
|
-
* Clears a connection's stale mark once a fresh page has been adopted. An
|
|
693
|
-
* invalidating event only records that a refetch is due; whoever merges the
|
|
694
|
-
* refetched page calls this so `isStale` stops reporting.
|
|
695
|
-
*/
|
|
696
|
-
export declare const refreshConnection: (state: LiveState, connection: string) => LiveState;
|
|
697
706
|
/** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
|
|
698
707
|
export declare const liveHasPrevious: (connection: Connection, state: LiveState, connectionId: string) => boolean;
|
|
699
708
|
/** `hasNext` accounting for edges recorded outside the loaded boundary. */
|
|
@@ -731,7 +740,33 @@ export declare const gc: (state: Retained & {
|
|
|
731
740
|
//#endregion
|
|
732
741
|
//#region src/persistence.d.ts
|
|
733
742
|
/** Bump when the serialized shape changes; a mismatch discards the cache. */
|
|
734
|
-
export declare const REMOTE_CACHE_VERSION =
|
|
743
|
+
export declare const REMOTE_CACHE_VERSION = 4;
|
|
744
|
+
/**
|
|
745
|
+
* What survives a reload: the entity cache, and the connections an application
|
|
746
|
+
* **declared** should come back.
|
|
747
|
+
*
|
|
748
|
+
* Connections are session state by default and stay that way — `snapshotOf`
|
|
749
|
+
* takes none unless asked. What a declared one keeps is its **edges and
|
|
750
|
+
* nothing else**, which is not a simplification but the rule: a connection's
|
|
751
|
+
* boundaries are cursors the server minted, and a cursor may name server state
|
|
752
|
+
* that no longer exists. Restoring one would be claiming to know where the
|
|
753
|
+
* page ends. The shape has nowhere to put a cursor, so that cannot be got
|
|
754
|
+
* wrong later.
|
|
755
|
+
*/
|
|
756
|
+
export interface Snapshot {
|
|
757
|
+
readonly entities: EntityStore;
|
|
758
|
+
/**
|
|
759
|
+
* By connection identity: its **segments**, each a run of edges. No
|
|
760
|
+
* boundaries.
|
|
761
|
+
*
|
|
762
|
+
* Segments rather than one flat list, because the gaps between them are
|
|
763
|
+
* knowledge too. A connection paged from both ends holds a head and a tail
|
|
764
|
+
* with an unfetched middle; flattening those into one run would claim the
|
|
765
|
+
* tail's first row follows the head's last, which is a thing the client was
|
|
766
|
+
* never told.
|
|
767
|
+
*/
|
|
768
|
+
readonly connections: Readonly<Record<string, ReadonlyArray<ReadonlyArray<Edge>>>>;
|
|
769
|
+
}
|
|
735
770
|
export interface SnapshotOptions {
|
|
736
771
|
/**
|
|
737
772
|
* Who or what the snapshot is for (a user id, a tenant, a build). A snapshot
|
|
@@ -745,36 +780,50 @@ export interface SnapshotOptions {
|
|
|
745
780
|
/** How a snapshot meets a store that already has entries. */
|
|
746
781
|
export type MergePolicy = 'replace' | 'preserve-existing';
|
|
747
782
|
/**
|
|
748
|
-
* The snapshot text, or `undefined` when it would exceed `maxBytes`.
|
|
749
|
-
*
|
|
783
|
+
* The snapshot text, or `undefined` when it would exceed `maxBytes`. Build the
|
|
784
|
+
* snapshot with `snapshotOf`, which is where the connections that survive are
|
|
785
|
+
* named; runtime state — optimistic layers, live cursors, gaps, the mutation
|
|
786
|
+
* ledger — is never in one.
|
|
750
787
|
*/
|
|
751
|
-
declare function dehydrate(
|
|
788
|
+
declare function dehydrate(snapshot: Snapshot, options?: SnapshotOptions & {
|
|
752
789
|
readonly maxBytes?: undefined;
|
|
753
790
|
}): string;
|
|
754
|
-
declare function dehydrate(
|
|
791
|
+
declare function dehydrate(snapshot: Snapshot, options?: SnapshotOptions): string | undefined;
|
|
755
792
|
export declare const RemotePersistence: {
|
|
756
793
|
/** The snapshot text, or `undefined` when it would exceed `maxBytes`. */
|
|
757
794
|
dehydrate: typeof dehydrate;
|
|
758
|
-
/**
|
|
759
|
-
hydrate: (raw: string | undefined | null, options?: SnapshotOptions) =>
|
|
760
|
-
/**
|
|
795
|
+
/** What a snapshot text holds, or `undefined` when it is refused. */
|
|
796
|
+
hydrate: (raw: string | undefined | null, options?: SnapshotOptions) => Snapshot | undefined;
|
|
797
|
+
/**
|
|
798
|
+
* What to keep of a Model, naming the connections that should survive a
|
|
799
|
+
* reload. Naming none is the default and is what Remote always did.
|
|
800
|
+
*/
|
|
801
|
+
snapshotOf: (model: {
|
|
802
|
+
readonly entities: EntityStore;
|
|
803
|
+
readonly connections: Readonly<Record<string, Connection>>;
|
|
804
|
+
}, options?: {
|
|
805
|
+
readonly connections?: ReadonlyArray<ConnectionIdentity> | undefined;
|
|
806
|
+
}) => Snapshot;
|
|
807
|
+
/** A snapshot's entities brought into a store by policy. */
|
|
761
808
|
mergeStores: (current: EntityStore, snapshot: EntityStore, policy: MergePolicy) => EntityStore;
|
|
809
|
+
/** The cache as a fresh session has it; what a refused `restore` yields. */
|
|
810
|
+
emptySnapshot: Snapshot;
|
|
762
811
|
/**
|
|
763
812
|
* Writes the snapshot under `key`, or removes the key when the store would
|
|
764
813
|
* exceed `maxBytes`, so a stale smaller snapshot cannot outlive a larger
|
|
765
814
|
* store it no longer describes.
|
|
766
815
|
*/
|
|
767
|
-
save: (
|
|
816
|
+
save: (snapshot: Snapshot, options: {
|
|
768
817
|
readonly key: string;
|
|
769
818
|
} & SnapshotOptions) => Effect.Effect<void, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore>;
|
|
770
819
|
/**
|
|
771
|
-
* Reads the cache. A missing key yields `
|
|
772
|
-
* refuses yields `
|
|
773
|
-
* clean and the planner refetches.
|
|
820
|
+
* Reads the cache. A missing key yields `emptySnapshot`; anything `hydrate`
|
|
821
|
+
* refuses yields `emptySnapshot` and removes the key, so the next `restore`
|
|
822
|
+
* is clean and the planner refetches.
|
|
774
823
|
*/
|
|
775
824
|
restore: (options: {
|
|
776
825
|
readonly key: string;
|
|
777
|
-
} & SnapshotOptions) => Effect.Effect<
|
|
826
|
+
} & SnapshotOptions) => Effect.Effect<Snapshot, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore>;
|
|
778
827
|
};
|
|
779
828
|
//#endregion
|
|
780
829
|
//#region src/wire.d.ts
|
|
@@ -1261,7 +1310,34 @@ export interface RemoteModel {
|
|
|
1261
1310
|
* it, and a refresh of one field leaves every other entry's read alone.
|
|
1262
1311
|
*/
|
|
1263
1312
|
readonly refresh: RefreshState;
|
|
1313
|
+
/**
|
|
1314
|
+
* The reads that failed and have not been settled since, so a read can say
|
|
1315
|
+
* `Failed` rather than `Initial`.
|
|
1316
|
+
*
|
|
1317
|
+
* Without them a read that failed before anything loaded read exactly like
|
|
1318
|
+
* one nobody had asked for, and nothing asked again: a read entry restarts
|
|
1319
|
+
* only when what it plans changes, and a failure changed nothing. What failed
|
|
1320
|
+
* is not planned again on its own either, so a persistent error is not
|
|
1321
|
+
* retried every time an unrelated read restarts the entry. `Remote.refresh`
|
|
1322
|
+
* is how a view retries.
|
|
1323
|
+
*/
|
|
1324
|
+
readonly failures: Failures;
|
|
1264
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Failed reads, as the errors they failed with.
|
|
1328
|
+
*
|
|
1329
|
+
* `connections` is by connection identity, and is settled by a page arriving,
|
|
1330
|
+
* an invalidation, or retention dropping the connection. `fields` is by field
|
|
1331
|
+
* mark (`entity\0id\0field`, the marks `loading` uses), and is settled by the
|
|
1332
|
+
* field being written again, a refresh asking for it, or retention dropping the
|
|
1333
|
+
* entity. A read of it in flight reads `Loading` over the failure without
|
|
1334
|
+
* settling it: if that read fails too, the failure is simply recorded again.
|
|
1335
|
+
*/
|
|
1336
|
+
export interface Failures {
|
|
1337
|
+
readonly connections: Readonly<Record<string, RemoteError>>;
|
|
1338
|
+
readonly fields: Readonly<Record<string, RemoteError>>;
|
|
1339
|
+
}
|
|
1340
|
+
export declare const noFailures: Failures;
|
|
1265
1341
|
/**
|
|
1266
1342
|
* `Remote.refresh` generations, per field mark. `generation` is the last one
|
|
1267
1343
|
* handed out; `requested` is when each field was last refreshed, and `started`
|
|
@@ -1286,6 +1362,13 @@ export type RemoteMessage = {
|
|
|
1286
1362
|
readonly _tag: 'ReadFailed';
|
|
1287
1363
|
readonly requests: readonly Requirement[];
|
|
1288
1364
|
readonly error: RemoteError;
|
|
1365
|
+
/**
|
|
1366
|
+
* The live stream whose subscription broke, when this is that rather
|
|
1367
|
+
* than a read. Nothing was being read, so no field failed: the stream is
|
|
1368
|
+
* recorded as a gap, since changes after it are missed until the host
|
|
1369
|
+
* resubscribes.
|
|
1370
|
+
*/
|
|
1371
|
+
readonly stream?: string | undefined;
|
|
1289
1372
|
} |
|
|
1290
1373
|
/** A refetch of present fields began; they read as `Refreshing` until it lands. */
|
|
1291
1374
|
{
|
|
@@ -1299,6 +1382,15 @@ export type RemoteMessage = {
|
|
|
1299
1382
|
/** The refresh generation the read was planned under. */
|
|
1300
1383
|
readonly refresh?: number | undefined;
|
|
1301
1384
|
} |
|
|
1385
|
+
/**
|
|
1386
|
+
* Queries for these connections were sent. One with nothing to show reads
|
|
1387
|
+
* `Loading` until its page or its failure arrives, which is what separates
|
|
1388
|
+
* "being fetched" from `Initial`, "nothing is fetching this".
|
|
1389
|
+
*/
|
|
1390
|
+
{
|
|
1391
|
+
readonly _tag: 'QueryStarted';
|
|
1392
|
+
readonly connections: readonly string[];
|
|
1393
|
+
} |
|
|
1302
1394
|
/** The active Surfaces' roots changed; everything they do not reach is collected. */
|
|
1303
1395
|
{
|
|
1304
1396
|
readonly _tag: 'RetentionChanged';
|
|
@@ -1308,6 +1400,13 @@ export type RemoteMessage = {
|
|
|
1308
1400
|
{
|
|
1309
1401
|
readonly _tag: 'Hydrated';
|
|
1310
1402
|
readonly entities: EntityStore;
|
|
1403
|
+
/**
|
|
1404
|
+
* The connections a snapshot declared should survive, as their edges.
|
|
1405
|
+
* Restored with **unknown** boundaries and marked stale: the rows show
|
|
1406
|
+
* at once and the connection is refetched, because a persisted cursor
|
|
1407
|
+
* may name server state that is gone.
|
|
1408
|
+
*/
|
|
1409
|
+
readonly connections?: Readonly<Record<string, ReadonlyArray<ReadonlyArray<Edge>>>> | undefined;
|
|
1311
1410
|
readonly merge: MergePolicy;
|
|
1312
1411
|
} |
|
|
1313
1412
|
/** A mutation began; its optimistic operations show until it settles. */
|
|
@@ -1365,7 +1464,11 @@ export type RemoteMessage = {
|
|
|
1365
1464
|
readonly _tag: 'ConnectionRefreshed';
|
|
1366
1465
|
readonly connection: string;
|
|
1367
1466
|
} |
|
|
1368
|
-
/**
|
|
1467
|
+
/**
|
|
1468
|
+
* A query for the connection failed. Its pages stay as they were, and it
|
|
1469
|
+
* reads `Failed` — with them as `previous`, when it has any — until a page
|
|
1470
|
+
* arrives, it is invalidated, or retention drops it.
|
|
1471
|
+
*/
|
|
1369
1472
|
{
|
|
1370
1473
|
readonly _tag: 'QueryFailed';
|
|
1371
1474
|
readonly connection: string;
|
|
@@ -1400,6 +1503,7 @@ export declare const remoteMessageCases: {
|
|
|
1400
1503
|
readonly _tag: Schema.String;
|
|
1401
1504
|
readonly message: Schema.String;
|
|
1402
1505
|
}>;
|
|
1506
|
+
stream: Schema.optional<Schema.String>;
|
|
1403
1507
|
};
|
|
1404
1508
|
RefreshStarted: {
|
|
1405
1509
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -1408,6 +1512,9 @@ export declare const remoteMessageCases: {
|
|
|
1408
1512
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
1409
1513
|
refresh: Schema.optional<Schema.Number>;
|
|
1410
1514
|
};
|
|
1515
|
+
QueryStarted: {
|
|
1516
|
+
connections: Schema.$Array<Schema.String>;
|
|
1517
|
+
};
|
|
1411
1518
|
RetentionChanged: {
|
|
1412
1519
|
roots: Schema.Struct<{
|
|
1413
1520
|
readonly requirements: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -1418,6 +1525,7 @@ export declare const remoteMessageCases: {
|
|
|
1418
1525
|
}>;
|
|
1419
1526
|
};
|
|
1420
1527
|
Hydrated: {
|
|
1528
|
+
connections: Schema.optional<Schema.Unknown>;
|
|
1421
1529
|
entities: Schema.Unknown;
|
|
1422
1530
|
merge: Schema.Union<readonly [Schema.Literal<"replace">, Schema.Literal<"preserve-existing">]>;
|
|
1423
1531
|
};
|
|
@@ -1499,6 +1607,8 @@ export declare const isRemoteMessage: <M extends {
|
|
|
1499
1607
|
readonly _tag: RemoteMessageTag;
|
|
1500
1608
|
}>;
|
|
1501
1609
|
export declare const remoteMessageSchema: Schema.Schema<RemoteMessage>;
|
|
1610
|
+
/** Whether a query for this connection is in flight. */
|
|
1611
|
+
export declare const isQueryLoading: (model: RemoteModel, identity: string) => boolean;
|
|
1502
1612
|
/**
|
|
1503
1613
|
* The generation a read is planned under: the highest any of the fields it
|
|
1504
1614
|
* reads or connections it runs was refreshed at. A read entry carries this as a
|
|
@@ -1530,6 +1640,15 @@ export declare const isLoading: (model: RemoteModel, entity: string, id: string,
|
|
|
1530
1640
|
* can resubscribe rather than silently miss facts.
|
|
1531
1641
|
*/
|
|
1532
1642
|
export declare const updateRemote: (model: RemoteModel, message: RemoteMessage) => RemoteModel;
|
|
1643
|
+
/** Whether this field's last read failed and nothing has settled it since. */
|
|
1644
|
+
export declare const isFieldFailed: (model: RemoteModel, entity: string, id: string, field: string) => boolean;
|
|
1645
|
+
/**
|
|
1646
|
+
* The first failure among the fields `relation` reads of an entity, following
|
|
1647
|
+
* each relation the store holds into its targets, as `assemble` does. A read
|
|
1648
|
+
* that consults it shows `Failed` for a related entity's failure too, rather
|
|
1649
|
+
* than waiting on a field nothing will fetch.
|
|
1650
|
+
*/
|
|
1651
|
+
export declare const failureOf: (model: RemoteModel, entity: string, id: string, relation: RelationRequirement) => RemoteError | undefined;
|
|
1533
1652
|
/**
|
|
1534
1653
|
* Writes a read result into the store, recording each field's applied window so
|
|
1535
1654
|
* a later request with a different window refetches. A relation page requested
|
|
@@ -1848,6 +1967,92 @@ declare const Entity$1: {
|
|
|
1848
1967
|
patch: <Name extends string, F extends Schema.Struct.Fields>(ref: EntityRef<Name, F>, patch: Partial<Schema.Struct.Encoded<F>>) => EntityPatch<Name, F>;
|
|
1849
1968
|
};
|
|
1850
1969
|
//#endregion
|
|
1970
|
+
//#region src/matching.d.ts
|
|
1971
|
+
/**
|
|
1972
|
+
* An answer over the rows the client holds — never over the rows that exist.
|
|
1973
|
+
*
|
|
1974
|
+
* `complete` is the whole reason this is its own type rather than a `Page`. A
|
|
1975
|
+
* `Page` carries `hasNext`/`hasPrevious`, which are facts the *server* stated
|
|
1976
|
+
* about rows beyond the ones it delivered; a local answer has no such facts and
|
|
1977
|
+
* cannot invent them. What it can say is whether the population it judged was
|
|
1978
|
+
* all of it, which is decidable: a connection terminal at both ends is wholly
|
|
1979
|
+
* held, and nothing was skipped for a missing field.
|
|
1980
|
+
*
|
|
1981
|
+
* An incomplete answer is not wrong — it is an answer about less than the
|
|
1982
|
+
* caller may have meant, and the flag is what lets a view say "3 so far" rather
|
|
1983
|
+
* than "3", or a caller decide to ask the server.
|
|
1984
|
+
*/
|
|
1985
|
+
export interface Matched<A> {
|
|
1986
|
+
readonly items: ReadonlyArray<A>;
|
|
1987
|
+
/** Whether every row the answer is about was judged. */
|
|
1988
|
+
readonly complete: boolean;
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* What the rows the client holds had to say about a body.
|
|
1992
|
+
*
|
|
1993
|
+
* `skipped` is the honest half. A row missing a field the body reads cannot be
|
|
1994
|
+
* judged — it is neither a match nor a non-match — and dropping it silently
|
|
1995
|
+
* would turn "I could not tell" into "no". Naming it lets a caller decide:
|
|
1996
|
+
* fetch the field, ask the server, or say the answer is partial.
|
|
1997
|
+
*/
|
|
1998
|
+
export interface Judged {
|
|
1999
|
+
/** Keys of the rows that satisfy the body, in the order the body asks for. */
|
|
2000
|
+
readonly matched: ReadonlyArray<EntityKey>;
|
|
2001
|
+
/** Keys of rows held but missing a field the body reads, so not judged either way. */
|
|
2002
|
+
readonly skipped: ReadonlyArray<EntityKey>;
|
|
2003
|
+
}
|
|
2004
|
+
export interface MatchingOptions {
|
|
2005
|
+
/**
|
|
2006
|
+
* Judge only these keys. Without it every row of the body's Entity in the
|
|
2007
|
+
* store is a candidate, which is what a filter over a loaded list wants; with
|
|
2008
|
+
* it, a caller narrows to a connection's own edges.
|
|
2009
|
+
*/
|
|
2010
|
+
readonly among?: ReadonlyArray<EntityKey> | undefined;
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* The rows of `descriptor`'s Entity that the store holds and the body matches.
|
|
2014
|
+
*
|
|
2015
|
+
* The input is given **decoded**, as an application writes it, and encoded here
|
|
2016
|
+
* through the descriptor's own `Input` codec. That is deliberate and is the
|
|
2017
|
+
* whole reason this takes a descriptor rather than a bare body: a store holds
|
|
2018
|
+
* wire values, so an input has to be in that space to be compared against them,
|
|
2019
|
+
* and handing an interpreter a decoded value is a mistake **no runtime error
|
|
2020
|
+
* catches** — the reference interpreter answers "no rows", which reads exactly
|
|
2021
|
+
* like a correct answer. Doing it here means a caller cannot get it wrong.
|
|
2022
|
+
*
|
|
2023
|
+
* Literals in the body are deliberately *not* encoded. A literal written in
|
|
2024
|
+
* domain terms is already broken against the server, which binds it to a column
|
|
2025
|
+
* the same way; accepting it here would make the client quietly more permissive
|
|
2026
|
+
* than the thing it has to agree with.
|
|
2027
|
+
*/
|
|
2028
|
+
export declare const matching: <Name extends string, Input>(store: EntityStore, descriptor: QueryDescriptor<Name, Input, unknown>, input: Input, options?: MatchingOptions) => Judged;
|
|
2029
|
+
/**
|
|
2030
|
+
* Whether one row belongs to a query, as far as the client can tell.
|
|
2031
|
+
*
|
|
2032
|
+
* Three answers, and the third is the one that makes this usable. `'unknown'`
|
|
2033
|
+
* is a row the store never fetched, or holds without a field the body reads —
|
|
2034
|
+
* a caller that collapses it into `'no'` has turned "I could not tell" into an
|
|
2035
|
+
* answer, which is the mistake this whole module is shaped against.
|
|
2036
|
+
*
|
|
2037
|
+
* **Membership is decidable where position is not.** Whether a row satisfies
|
|
2038
|
+
* `eq`, `isNull` or `contains` needs no collation: equality on text is exact
|
|
2039
|
+
* and `contains` folds ASCII, both stated in §6.0.1. *Where* a row sorts needs
|
|
2040
|
+
* text collation, which is the backend's and deliberately outside the
|
|
2041
|
+
* conformant subset — so this answers the half that is always answerable, and
|
|
2042
|
+
* ordering stays the server's until a query can declare its collation.
|
|
2043
|
+
*/
|
|
2044
|
+
export type Belongs = 'yes' | 'no' | 'unknown';
|
|
2045
|
+
/**
|
|
2046
|
+
* Takes the input **encoded**, because its one caller holds it that way: a
|
|
2047
|
+
* connection identity carries the canonical encoded input, and making it decode
|
|
2048
|
+
* only for this to encode again would be a round trip that can fail.
|
|
2049
|
+
*
|
|
2050
|
+
* A decoded-input sibling was written first and deleted: nothing called it. The
|
|
2051
|
+
* asymmetry with `matching`, which takes a decoded input, is the shape of the
|
|
2052
|
+
* two real callers rather than an oversight.
|
|
2053
|
+
*/
|
|
2054
|
+
export declare const belongsEncoded: <Name extends string, Input>(store: EntityStore, descriptor: QueryDescriptor<Name, Input, unknown>, encoded: Readonly<Record<string, unknown>>, key: EntityKey) => Belongs;
|
|
2055
|
+
//#endregion
|
|
1851
2056
|
//#region src/inspect.d.ts
|
|
1852
2057
|
/** A serializable summary of a RemoteModel for DevTools and diagnostics. */
|
|
1853
2058
|
export interface RemoteInspection {
|
|
@@ -1863,12 +2068,18 @@ export interface RemoteInspection {
|
|
|
1863
2068
|
readonly live: ReadonlyArray<string>;
|
|
1864
2069
|
readonly gaps: ReadonlyArray<string>;
|
|
1865
2070
|
/**
|
|
1866
|
-
* The `entity\0id\0field` marks of reads in flight
|
|
2071
|
+
* The `entity\0id\0field` marks of reads in flight, and a
|
|
2072
|
+
* `\0connection\0identity` mark for each query in flight. This is what "Remote is
|
|
1867
2073
|
* working" means here, and it is read from the Model rather than from the
|
|
1868
2074
|
* fibers doing the work: a tool that shows it shows something the Model can
|
|
1869
2075
|
* be replayed to, and nothing that needs the runtime to be asked.
|
|
1870
2076
|
*/
|
|
1871
2077
|
readonly loading: ReadonlyArray<string>;
|
|
2078
|
+
/**
|
|
2079
|
+
* The reads that failed and are not yet settled: queries by connection
|
|
2080
|
+
* identity, entity fields by `entity\0id\0field` mark, each with its error.
|
|
2081
|
+
*/
|
|
2082
|
+
readonly failures: Failures;
|
|
1872
2083
|
readonly mutations: {
|
|
1873
2084
|
readonly pending: ReadonlyArray<string>;
|
|
1874
2085
|
readonly failed: ReadonlyArray<string>;
|
|
@@ -1879,6 +2090,75 @@ export interface RemoteInspection {
|
|
|
1879
2090
|
export declare const inspectRemote: (model: RemoteModel) => RemoteInspection;
|
|
1880
2091
|
/** A pure, serializable view of one entity, or `undefined` if unknown. */
|
|
1881
2092
|
export declare const inspectEntity: (model: RemoteModel, key: string) => RemoteInspection["entities"][number] | undefined;
|
|
2093
|
+
/**
|
|
2094
|
+
* What one query read is, and what it currently is — data-query-DESIGN §29.1.
|
|
2095
|
+
*
|
|
2096
|
+
* Every member is gathered from something that already existed: the read was
|
|
2097
|
+
* always this many pieces, and nothing had ever been asked to put them in one
|
|
2098
|
+
* place. It is plain serializable data, so a DevTools panel, a log line and a
|
|
2099
|
+
* test assert on the same value.
|
|
2100
|
+
*
|
|
2101
|
+
* Two members of §29.1's sketch are **not** here, and their absence is the
|
|
2102
|
+
* finding rather than an omission:
|
|
2103
|
+
*
|
|
2104
|
+
* - **Surface.** A Projection does not know which Surface reads it, and often
|
|
2105
|
+
* several do — so it is not a property of the read, and `explain` cannot
|
|
2106
|
+
* recover it alone. Given the active Surfaces (the same record
|
|
2107
|
+
* `subscriptions` takes), it reports **every** Surface reading the connection
|
|
2108
|
+
* rather than guessing one, and omits the member entirely when it was not
|
|
2109
|
+
* given them.
|
|
2110
|
+
* - **Executor.** The thing that answers a query is a `RemoteClient` Layer in
|
|
2111
|
+
* the runtime, not a value in the Model, and a pure read of the Model cannot
|
|
2112
|
+
* see it. That is the same boundary that makes this function pure and
|
|
2113
|
+
* replayable, so it is worth more than the line of text.
|
|
2114
|
+
*
|
|
2115
|
+
* And one that the design proposed and this does not want: *expectation*
|
|
2116
|
+
* (required versus optional). §11 left it unbuilt for want of a consumer, and
|
|
2117
|
+
* the explanation was the likeliest consumer. It turns out not to need it — a
|
|
2118
|
+
* query's result is a connection, so the shape is a `Page`, decided by the
|
|
2119
|
+
* definition rather than by the read. Nothing here has an opinion about whether
|
|
2120
|
+
* an empty one is an error, because nothing here has to have one.
|
|
2121
|
+
*/
|
|
2122
|
+
export interface QueryExplanation {
|
|
2123
|
+
/** The bound Remote domain that answers it. */
|
|
2124
|
+
readonly domain: string;
|
|
2125
|
+
/** The definition's name. */
|
|
2126
|
+
readonly query: string;
|
|
2127
|
+
/** The input as encoded, which is what the identity is built from. */
|
|
2128
|
+
readonly input: unknown;
|
|
2129
|
+
/** The connection identity: definition plus canonical input, excluding the window. */
|
|
2130
|
+
readonly identity: string;
|
|
2131
|
+
/** How much of the connection this read asks for. */
|
|
2132
|
+
readonly window: QueryWindow;
|
|
2133
|
+
/** What it reads of each item, as the slice asked of the server. */
|
|
2134
|
+
readonly select: RelationRequirement;
|
|
2135
|
+
/**
|
|
2136
|
+
* What the query means, as readable text — `Query.show`, and so neither SQL
|
|
2137
|
+
* nor whatever the backend compiled. Absent for a definition made with
|
|
2138
|
+
* `Query.make`, whose meaning lives on the server that answers it.
|
|
2139
|
+
*/
|
|
2140
|
+
readonly body?: string | undefined;
|
|
2141
|
+
/** The fields, inputs and operations the body reads; absent with the body. */
|
|
2142
|
+
readonly dependencies?: Dependencies | undefined;
|
|
2143
|
+
/** What the read answers from this Model right now. */
|
|
2144
|
+
readonly state: RemoteData<unknown>['_tag'];
|
|
2145
|
+
/**
|
|
2146
|
+
* The active Surfaces reading this connection at this Model, if the active
|
|
2147
|
+
* record was supplied. Plural because several may, which is why a Projection
|
|
2148
|
+
* cannot carry the answer itself.
|
|
2149
|
+
*/
|
|
2150
|
+
readonly surfaces?: ReadonlyArray<string> | undefined;
|
|
2151
|
+
/**
|
|
2152
|
+
* Why each of those Surfaces is active, where it was placed with
|
|
2153
|
+
* `Surface.when` — the Model path and the tag, without running anything. A
|
|
2154
|
+
* Surface placed with `Surface.at` has no entry: a callback cannot be read.
|
|
2155
|
+
*/
|
|
2156
|
+
readonly activation?: ReadonlyArray<{
|
|
2157
|
+
readonly surface: string;
|
|
2158
|
+
readonly path: readonly string[];
|
|
2159
|
+
readonly tag: string;
|
|
2160
|
+
}> | undefined;
|
|
2161
|
+
}
|
|
1882
2162
|
//#endregion
|
|
1883
2163
|
//#region src/plan.d.ts
|
|
1884
2164
|
type Window = NonNullable<Requirement['windows']>[string];
|
|
@@ -2047,6 +2327,44 @@ export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities exte
|
|
|
2047
2327
|
next<Name extends string, Input>(model: AppModel, projection: QueryProjection<AppModel, any, Name, Input>): QueryRef<Name, Input> | undefined;
|
|
2048
2328
|
/** The `QueryRef` for the page before the loaded start, or `undefined`. */
|
|
2049
2329
|
previous<Name extends string, Input>(model: AppModel, projection: QueryProjection<AppModel, any, Name, Input>): QueryRef<Name, Input> | undefined;
|
|
2330
|
+
/**
|
|
2331
|
+
* What a query read is and what it currently is, as one serializable value —
|
|
2332
|
+
* data-query-DESIGN §29.1: the domain, the definition and its input, the
|
|
2333
|
+
* connection identity, the window, the Selection, the body as readable text
|
|
2334
|
+
* with what it depends on, and what the read answers from this Model.
|
|
2335
|
+
*
|
|
2336
|
+
* Pure, so a DevTools panel showing it shows something the Model can be
|
|
2337
|
+
* replayed to.
|
|
2338
|
+
*
|
|
2339
|
+
* Given `surfaces` — the same active record `subscriptions` takes — it also
|
|
2340
|
+
* reports which of them read this connection, and why each is active where
|
|
2341
|
+
* that is a readable fact (`Surface.when`) rather than a callback
|
|
2342
|
+
* (`Surface.at`). A Projection cannot carry this itself: several Surfaces may
|
|
2343
|
+
* read one connection, so it is not a property of the read.
|
|
2344
|
+
*/
|
|
2345
|
+
explain<Name extends string, Input>(model: AppModel, projection: QueryProjection<AppModel, any, Name, Input>, options?: {
|
|
2346
|
+
readonly surfaces?: Readonly<Record<string, ActiveSurface<AppModel>>> | undefined;
|
|
2347
|
+
}): QueryExplanation;
|
|
2348
|
+
/**
|
|
2349
|
+
* The rows of a loaded list that a body matches, decoded as the list decodes
|
|
2350
|
+
* them — a filter that asks the server nothing.
|
|
2351
|
+
*
|
|
2352
|
+
* **It filters a list; it does not run a query.** That distinction is what
|
|
2353
|
+
* keeps it honest. "Which rows match" would need to know that the list holds
|
|
2354
|
+
* every row the body could match, which is predicate containment and is
|
|
2355
|
+
* deliberately not built. "Which rows *of this list* match" is decidable from
|
|
2356
|
+
* what is already here, and is what a search box over a loaded page actually
|
|
2357
|
+
* wants.
|
|
2358
|
+
*
|
|
2359
|
+
* `complete` says whether the answer is about the whole list: every edge
|
|
2360
|
+
* judged, and the connection terminal at both ends. An incomplete answer is
|
|
2361
|
+
* not wrong — it is about less than the caller may have meant, which is why
|
|
2362
|
+
* it is said rather than left for a view to assume.
|
|
2363
|
+
*
|
|
2364
|
+
* Creates no connection, so there is nothing new to retain and nothing new to
|
|
2365
|
+
* fetch. The server stays authoritative for which rows exist.
|
|
2366
|
+
*/
|
|
2367
|
+
filtered<Value, Name extends string, Input, Q extends QueryDescriptor<any, any, any>>(model: AppModel, over: QueryProjection<AppModel, Value, Name, Input>, by: Q & Registered<Q['name'], QueryName<Queries[number]>, 'Query'>, input: QueryInput<Q>): Matched<Value>;
|
|
2050
2368
|
/** A Command that runs the query and yields the `ConnectionMerged` (or `QueryFailed`) that reduces it: "load more". */
|
|
2051
2369
|
fetch(ref: QueryRef<string, unknown>): Command<RemoteMessage, never, RemoteClient>;
|
|
2052
2370
|
/**
|
|
@@ -2151,6 +2469,8 @@ export type QueryOptions<Value, Entity extends string, Of extends string = Entit
|
|
|
2151
2469
|
/** A query connection read as a `Page` of selected items; `ref` is the connection with its first window. */
|
|
2152
2470
|
export interface QueryProjection<AppModel, Value, Name extends string, Input> extends Projection<AppModel, RemoteData<Page<Value>>> {
|
|
2153
2471
|
readonly ref: QueryRef<Name, Input>;
|
|
2472
|
+
/** What it reads of each item, so a filter over the same list decodes identically. */
|
|
2473
|
+
readonly selection: Selection<Value, string>;
|
|
2154
2474
|
}
|
|
2155
2475
|
/** The input of a query descriptor. */
|
|
2156
2476
|
export type QueryInput<Q> = Q extends QueryDescriptor<any, infer Input, any> ? Input : never;
|
|
@@ -2227,6 +2547,7 @@ export declare const Remote: {
|
|
|
2227
2547
|
readonly _tag: Schema.String;
|
|
2228
2548
|
readonly message: Schema.String;
|
|
2229
2549
|
}>;
|
|
2550
|
+
stream: Schema.optional<Schema.String>;
|
|
2230
2551
|
};
|
|
2231
2552
|
RefreshStarted: {
|
|
2232
2553
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -2235,6 +2556,9 @@ export declare const Remote: {
|
|
|
2235
2556
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
2236
2557
|
refresh: Schema.optional<Schema.Number>;
|
|
2237
2558
|
};
|
|
2559
|
+
QueryStarted: {
|
|
2560
|
+
connections: Schema.$Array<Schema.String>;
|
|
2561
|
+
};
|
|
2238
2562
|
RetentionChanged: {
|
|
2239
2563
|
roots: Schema.Struct<{
|
|
2240
2564
|
readonly requirements: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -2245,6 +2569,7 @@ export declare const Remote: {
|
|
|
2245
2569
|
}>;
|
|
2246
2570
|
};
|
|
2247
2571
|
Hydrated: {
|
|
2572
|
+
connections: Schema.optional<Schema.Unknown>;
|
|
2248
2573
|
entities: Schema.Unknown;
|
|
2249
2574
|
merge: Schema.Union<readonly [Schema.Literal<"replace">, Schema.Literal<"preserve-existing">]>;
|
|
2250
2575
|
};
|
|
@@ -2391,6 +2716,9 @@ export declare const Remote: {
|
|
|
2391
2716
|
* again with `next`/`previous`. The read entries restart, so a read or query
|
|
2392
2717
|
* sent before the refresh is not applied after it.
|
|
2393
2718
|
*
|
|
2719
|
+
* It is also how a failed query is retried: a connection whose query failed
|
|
2720
|
+
* is not run again on its own, so its read stays `Failed` until this asks.
|
|
2721
|
+
*
|
|
2394
2722
|
* The projection must be observed, which it is while it is on screen. Live
|
|
2395
2723
|
* subscriptions are left as they are. For data nothing observes (SSR, tests),
|
|
2396
2724
|
* use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
|
|
@@ -2503,8 +2831,10 @@ export declare const Remote: {
|
|
|
2503
2831
|
observe: <AppModel, Store extends RemoteModel, Names extends string, Model, SurfaceMessage, Params, Message = RemoteMessage>(bound: BoundRemote<AppModel, Store, Names>, surface: Surface<AppModel, Model, SurfaceMessage, Params>, params: Params, toMessage?: (message: RemoteMessage) => Message, options?: ObserveOptions) => EntryWithoutKeepAlive<AppModel, Message, ReadDependencies, RemoteClient>;
|
|
2504
2832
|
/**
|
|
2505
2833
|
* A Foldkit Subscription entry that consumes the live stream for a Surface's
|
|
2506
|
-
* requirements, emitting a `LiveReceived` per event and a `ReadFailed`
|
|
2507
|
-
*
|
|
2834
|
+
* requirements, emitting a `LiveReceived` per event and a `ReadFailed`
|
|
2835
|
+
* carrying its `stream` when the stream breaks (including
|
|
2836
|
+
* `ResumeUnavailable`). That records a gap on the stream rather than a failed
|
|
2837
|
+
* read, since nothing was being read. The resume cursor is read
|
|
2508
2838
|
* from `RemoteModel.live`, so the application tracks no cursor of its own.
|
|
2509
2839
|
*/
|
|
2510
2840
|
live: <AppModel, Store extends RemoteModel, Names extends string, Model, SurfaceMessage, Params, Message = RemoteMessage>(bound: BoundRemote<AppModel, Store, Names>, surface: Surface<AppModel, Model, SurfaceMessage, Params>, params: Params, toMessage?: (message: RemoteMessage) => Message, options?: LiveOptions) => EntryWithoutKeepAlive<AppModel, Message, {
|