foldkit-remote 0.4.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 +428 -79
- package/dist/index.d.mts +588 -38
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1074 -242
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +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, Dependencies, InputExpr, Query as Query$1 } from "foldkit-entity";
|
|
3
4
|
import { EntryWithoutKeepAlive } from "foldkit/subscription";
|
|
4
5
|
import { ActiveSurface, Contract, Invalid, Metadata, ModelRef, Projection, Surface, Wiring } from "foldkit-surface";
|
|
5
6
|
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
@@ -126,6 +127,21 @@ export type RemoteData<A> = {
|
|
|
126
127
|
} | {
|
|
127
128
|
readonly _tag: 'NotFound';
|
|
128
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* What `RemoteData.render` tells the `data` branch about the value it is
|
|
132
|
+
* given: whether a newer answer is on its way, and whether this one is what
|
|
133
|
+
* was last known good before a read failed. It is one tag rather than two
|
|
134
|
+
* booleans because a value cannot be both at once, and a type that can say so
|
|
135
|
+
* invites a view to handle a state that never arrives.
|
|
136
|
+
*/
|
|
137
|
+
export type Freshness = {
|
|
138
|
+
readonly _tag: 'Fresh';
|
|
139
|
+
} | {
|
|
140
|
+
readonly _tag: 'Refreshing';
|
|
141
|
+
} | {
|
|
142
|
+
readonly _tag: 'Stale';
|
|
143
|
+
readonly error: RemoteError;
|
|
144
|
+
};
|
|
129
145
|
export declare const remoteErrorSchema: Schema.Struct<{
|
|
130
146
|
readonly _tag: Schema.String;
|
|
131
147
|
readonly message: Schema.String;
|
|
@@ -146,6 +162,36 @@ export declare const RemoteData: {
|
|
|
146
162
|
readonly Failed: (error: RemoteError, previous: Option.Option<A>) => R;
|
|
147
163
|
readonly NotFound: () => R;
|
|
148
164
|
}) => R;
|
|
165
|
+
/**
|
|
166
|
+
* The view-oriented fold: the six states as the three things a view actually
|
|
167
|
+
* draws, under the policy that useful data stays on screen. A value that is
|
|
168
|
+
* being refetched, or that a later read failed to replace, still reaches
|
|
169
|
+
* `data` — with `freshness` saying which — instead of being replaced by a
|
|
170
|
+
* spinner or an error the reader cannot act on.
|
|
171
|
+
*
|
|
172
|
+
* ```ts
|
|
173
|
+
* RemoteData.render(model.user, {
|
|
174
|
+
* loading: () => UserSkeleton(),
|
|
175
|
+
* notFound: () => NoSuchUser(),
|
|
176
|
+
* failed: error => ErrorView(error),
|
|
177
|
+
* data: (user, freshness) => UserView({ user, dimmed: freshness._tag !== 'Fresh' }),
|
|
178
|
+
* })
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* `notFound` is its own branch and not optional: a row the server answered
|
|
182
|
+
* for and does not have is neither loading nor a failure, and drawing it as
|
|
183
|
+
* either is a spinner that never ends or an error nobody can fix.
|
|
184
|
+
*
|
|
185
|
+
* This does not replace `match`, which is the exhaustive fold over the states
|
|
186
|
+
* themselves. Reach for `match` when the six states really do draw
|
|
187
|
+
* differently, and for this when they draw the usual three ways.
|
|
188
|
+
*/
|
|
189
|
+
render: <A, R>(data: RemoteData<A>, cases: {
|
|
190
|
+
readonly loading: () => R;
|
|
191
|
+
readonly notFound: () => R;
|
|
192
|
+
readonly failed: (error: RemoteError) => R;
|
|
193
|
+
readonly data: (value: A, freshness: Freshness) => R;
|
|
194
|
+
}) => R;
|
|
149
195
|
map: <A, B>(data: RemoteData<A>, f: (value: A) => B) => RemoteData<B>;
|
|
150
196
|
};
|
|
151
197
|
//#endregion
|
|
@@ -321,7 +367,30 @@ export interface QueryDescriptor<Name extends string, Input, Result> {
|
|
|
321
367
|
readonly Input: Schema.Codec<Input>;
|
|
322
368
|
readonly Result: Result;
|
|
323
369
|
readonly ref: (input: Input) => QueryRef<Name, Input>;
|
|
370
|
+
/**
|
|
371
|
+
* What the query means, when it was declared with `Query.define`: the rows it
|
|
372
|
+
* is about, as a value an interpreter can compile. A descriptor from
|
|
373
|
+
* `Query.make` has none — its meaning lives in whatever the server registered
|
|
374
|
+
* to answer it — and a source is free to answer either.
|
|
375
|
+
*/
|
|
376
|
+
readonly body?: AnyQuery | undefined;
|
|
324
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* The inputs of a definition's body: one placeholder per key of `Input`, typed
|
|
380
|
+
* as that key's value. A body is built once, so these stand for what the query
|
|
381
|
+
* will be given rather than being it.
|
|
382
|
+
*/
|
|
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";
|
|
325
394
|
/** Stable stringify: object keys sorted, undefined-valued keys dropped, so equal inputs encode equally. */
|
|
326
395
|
export declare const stableStringify: (value: unknown) => string;
|
|
327
396
|
/**
|
|
@@ -332,6 +401,8 @@ export declare const stableStringify: (value: unknown) => string;
|
|
|
332
401
|
export type ResultOf<Result> = Result extends {
|
|
333
402
|
readonly name: infer Entity extends string;
|
|
334
403
|
} ? ConnectionSpec<Entity> : Result;
|
|
404
|
+
/** A relational `Query` over one Entity, as `Query.define`'s body returns. */
|
|
405
|
+
type RelationalQueryOf<E extends AnyEntity> = ReturnType<typeof Query$1.from<E>>;
|
|
335
406
|
export declare const Query: {
|
|
336
407
|
/** Declares the entity and options a query's result is a connection over. */
|
|
337
408
|
connection: <Entity extends string>(entity: {
|
|
@@ -349,10 +420,46 @@ export declare const Query: {
|
|
|
349
420
|
readonly Input: Input;
|
|
350
421
|
readonly Result: Result;
|
|
351
422
|
}) => QueryDescriptor<Name, TypeOf<Input>, ResultOf<Result>>;
|
|
423
|
+
/**
|
|
424
|
+
* Declares a query by what it means. The body is built once, here, and the
|
|
425
|
+
* `input` it is given holds placeholders rather than values — there is
|
|
426
|
+
* nothing yet to branch on, so a condition that depends on what was passed is
|
|
427
|
+
* a comparison over the placeholder:
|
|
428
|
+
*
|
|
429
|
+
* ```ts
|
|
430
|
+
* const PostsBySlug = Query.define('PostsBySlug', { slug: Schema.String }, ({ input }) =>
|
|
431
|
+
* Query.from(Post).pipe(
|
|
432
|
+
* Query.where(Expr.eq(Post.fields.slug, input.slug)),
|
|
433
|
+
* Query.orderBy(Order.asc(Post.fields.id)),
|
|
434
|
+
* ),
|
|
435
|
+
* )
|
|
436
|
+
* ```
|
|
437
|
+
*
|
|
438
|
+
* What comes back is an ordinary `QueryDescriptor` — the same name, `Input`,
|
|
439
|
+
* `ref` and connection identity `Query.make` gives — carrying its `body`
|
|
440
|
+
* besides. A server can compile that body instead of being told the same
|
|
441
|
+
* thing again in its own dialect, and one that would rather answer the query
|
|
442
|
+
* its own way still can.
|
|
443
|
+
*
|
|
444
|
+
* The result is a connection over the Entity the body reads, so it is not
|
|
445
|
+
* named twice.
|
|
446
|
+
*/
|
|
447
|
+
define: <const Name extends string, Input extends SchemaOrFields, E extends AnyEntity>(name: Name, Input: Input, body: (context: {
|
|
448
|
+
readonly input: QueryInputs<TypeOf<Input>>;
|
|
449
|
+
}) => RelationalQueryOf<E>, options?: {
|
|
450
|
+
readonly edgeKey?: Schema.Schema<unknown>;
|
|
451
|
+
readonly live?: LivePolicy;
|
|
452
|
+
}) => QueryDescriptor<Name, TypeOf<Input>, ConnectionSpec<E["name"]>>;
|
|
352
453
|
first: (count: number) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
353
454
|
last: (count: number) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
354
455
|
after: (cursor: Cursor) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
355
456
|
before: (cursor: Cursor) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
457
|
+
from: <E extends AnyEntity>(entity: E) => Query$1<E>;
|
|
458
|
+
where: (...predicates: ReadonlyArray<import("foldkit-entity").Predicate>) => <E extends AnyEntity>(self: Query$1<E>) => Query$1<E>;
|
|
459
|
+
orderBy: (...terms: ReadonlyArray<import("foldkit-entity").OrderTerm>) => <E extends AnyEntity>(self: Query$1<E>) => Query$1<E>;
|
|
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;
|
|
356
463
|
};
|
|
357
464
|
//#endregion
|
|
358
465
|
//#region src/requirement.d.ts
|
|
@@ -556,8 +663,6 @@ export interface BoundaryCounts {
|
|
|
556
663
|
export interface LiveState {
|
|
557
664
|
/** Last applied cursor for this stream. */
|
|
558
665
|
readonly cursor: LiveCursor;
|
|
559
|
-
/** Connections marked stale by an invalidating event. */
|
|
560
|
-
readonly stale: ReadonlySet<string>;
|
|
561
666
|
/** Edges recorded outside the loaded boundary, not visible in `items`. */
|
|
562
667
|
readonly boundary: Readonly<Record<string, BoundaryCounts>>;
|
|
563
668
|
}
|
|
@@ -570,7 +675,6 @@ export type LiveOutcome = 'applied' | 'duplicate' | 'gap';
|
|
|
570
675
|
* spurious gap.
|
|
571
676
|
*/
|
|
572
677
|
export declare const classifyLive: (state: LiveState, cursor: LiveCursor) => LiveOutcome;
|
|
573
|
-
export declare const invalidateConnection: (state: LiveState, connection: string) => LiveState;
|
|
574
678
|
/**
|
|
575
679
|
* A subscriber is woken only if the event changed a field it selects. A
|
|
576
680
|
* change to an unselected field is skipped entirely.
|
|
@@ -588,17 +692,17 @@ export interface ConnectionApplied {
|
|
|
588
692
|
readonly state: LiveState;
|
|
589
693
|
readonly optimistic: OptimisticState;
|
|
590
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;
|
|
591
702
|
}
|
|
592
703
|
export declare const applyConnectionEvent: (state: LiveState, optimistic: OptimisticState, event: Extract<LiveEvent, {
|
|
593
704
|
_tag: "ConnectionInsert" | "ConnectionRemove" | "ConnectionInvalidate";
|
|
594
705
|
}>, policy?: LivePolicy) => ConnectionApplied;
|
|
595
|
-
export declare const isStale: (state: LiveState, connection: string) => boolean;
|
|
596
|
-
/**
|
|
597
|
-
* Clears a connection's stale mark once a fresh page has been adopted. An
|
|
598
|
-
* invalidating event only records that a refetch is due; whoever merges the
|
|
599
|
-
* refetched page calls this so `isStale` stops reporting.
|
|
600
|
-
*/
|
|
601
|
-
export declare const refreshConnection: (state: LiveState, connection: string) => LiveState;
|
|
602
706
|
/** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
|
|
603
707
|
export declare const liveHasPrevious: (connection: Connection, state: LiveState, connectionId: string) => boolean;
|
|
604
708
|
/** `hasNext` accounting for edges recorded outside the loaded boundary. */
|
|
@@ -636,7 +740,33 @@ export declare const gc: (state: Retained & {
|
|
|
636
740
|
//#endregion
|
|
637
741
|
//#region src/persistence.d.ts
|
|
638
742
|
/** Bump when the serialized shape changes; a mismatch discards the cache. */
|
|
639
|
-
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
|
+
}
|
|
640
770
|
export interface SnapshotOptions {
|
|
641
771
|
/**
|
|
642
772
|
* Who or what the snapshot is for (a user id, a tenant, a build). A snapshot
|
|
@@ -650,36 +780,50 @@ export interface SnapshotOptions {
|
|
|
650
780
|
/** How a snapshot meets a store that already has entries. */
|
|
651
781
|
export type MergePolicy = 'replace' | 'preserve-existing';
|
|
652
782
|
/**
|
|
653
|
-
* The snapshot text, or `undefined` when it would exceed `maxBytes`.
|
|
654
|
-
*
|
|
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.
|
|
655
787
|
*/
|
|
656
|
-
declare function dehydrate(
|
|
788
|
+
declare function dehydrate(snapshot: Snapshot, options?: SnapshotOptions & {
|
|
657
789
|
readonly maxBytes?: undefined;
|
|
658
790
|
}): string;
|
|
659
|
-
declare function dehydrate(
|
|
791
|
+
declare function dehydrate(snapshot: Snapshot, options?: SnapshotOptions): string | undefined;
|
|
660
792
|
export declare const RemotePersistence: {
|
|
661
793
|
/** The snapshot text, or `undefined` when it would exceed `maxBytes`. */
|
|
662
794
|
dehydrate: typeof dehydrate;
|
|
663
|
-
/**
|
|
664
|
-
hydrate: (raw: string | undefined | null, options?: SnapshotOptions) =>
|
|
665
|
-
/**
|
|
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. */
|
|
666
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;
|
|
667
811
|
/**
|
|
668
812
|
* Writes the snapshot under `key`, or removes the key when the store would
|
|
669
813
|
* exceed `maxBytes`, so a stale smaller snapshot cannot outlive a larger
|
|
670
814
|
* store it no longer describes.
|
|
671
815
|
*/
|
|
672
|
-
save: (
|
|
816
|
+
save: (snapshot: Snapshot, options: {
|
|
673
817
|
readonly key: string;
|
|
674
818
|
} & SnapshotOptions) => Effect.Effect<void, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore>;
|
|
675
819
|
/**
|
|
676
|
-
* Reads the cache. A missing key yields `
|
|
677
|
-
* refuses yields `
|
|
678
|
-
* 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.
|
|
679
823
|
*/
|
|
680
824
|
restore: (options: {
|
|
681
825
|
readonly key: string;
|
|
682
|
-
} & SnapshotOptions) => Effect.Effect<
|
|
826
|
+
} & SnapshotOptions) => Effect.Effect<Snapshot, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore>;
|
|
683
827
|
};
|
|
684
828
|
//#endregion
|
|
685
829
|
//#region src/wire.d.ts
|
|
@@ -1160,15 +1304,52 @@ export interface RemoteModel {
|
|
|
1160
1304
|
*/
|
|
1161
1305
|
readonly loading: ReadonlySet<string>;
|
|
1162
1306
|
/**
|
|
1163
|
-
* `Remote.refresh` generations
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1307
|
+
* `Remote.refresh` generations, held per field rather than for the store as a
|
|
1308
|
+
* whole. A read entry restarts when the generation over the fields it plans
|
|
1309
|
+
* moves, so a read sent before a refresh of its own fields cannot land after
|
|
1310
|
+
* it, and a refresh of one field leaves every other entry's read alone.
|
|
1166
1311
|
*/
|
|
1167
|
-
readonly refresh:
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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;
|
|
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>>;
|
|
1171
1339
|
}
|
|
1340
|
+
export declare const noFailures: Failures;
|
|
1341
|
+
/**
|
|
1342
|
+
* `Remote.refresh` generations, per field mark. `generation` is the last one
|
|
1343
|
+
* handed out; `requested` is when each field was last refreshed, and `started`
|
|
1344
|
+
* is the generation each field's read last began under. Holding them per field
|
|
1345
|
+
* is what keeps one Projection's refresh from restarting every read entry.
|
|
1346
|
+
*/
|
|
1347
|
+
export interface RefreshState {
|
|
1348
|
+
readonly generation: number;
|
|
1349
|
+
readonly requested: ReadonlyMap<string, number>;
|
|
1350
|
+
readonly started: ReadonlyMap<string, number>;
|
|
1351
|
+
}
|
|
1352
|
+
export declare const emptyRefresh: RefreshState;
|
|
1172
1353
|
export declare const initialRemoteModel: RemoteModel;
|
|
1173
1354
|
export declare const remoteModelSchema: () => Schema.Codec<RemoteModel, unknown>;
|
|
1174
1355
|
/** The submodel's Messages; each reduces to `RemoteModel` through `updateRemote`. */
|
|
@@ -1181,6 +1362,13 @@ export type RemoteMessage = {
|
|
|
1181
1362
|
readonly _tag: 'ReadFailed';
|
|
1182
1363
|
readonly requests: readonly Requirement[];
|
|
1183
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;
|
|
1184
1372
|
} |
|
|
1185
1373
|
/** A refetch of present fields began; they read as `Refreshing` until it lands. */
|
|
1186
1374
|
{
|
|
@@ -1194,6 +1382,15 @@ export type RemoteMessage = {
|
|
|
1194
1382
|
/** The refresh generation the read was planned under. */
|
|
1195
1383
|
readonly refresh?: number | undefined;
|
|
1196
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
|
+
} |
|
|
1197
1394
|
/** The active Surfaces' roots changed; everything they do not reach is collected. */
|
|
1198
1395
|
{
|
|
1199
1396
|
readonly _tag: 'RetentionChanged';
|
|
@@ -1203,6 +1400,13 @@ export type RemoteMessage = {
|
|
|
1203
1400
|
{
|
|
1204
1401
|
readonly _tag: 'Hydrated';
|
|
1205
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;
|
|
1206
1410
|
readonly merge: MergePolicy;
|
|
1207
1411
|
} |
|
|
1208
1412
|
/** A mutation began; its optimistic operations show until it settles. */
|
|
@@ -1225,6 +1429,19 @@ export type RemoteMessage = {
|
|
|
1225
1429
|
readonly _tag: 'MutationFailed';
|
|
1226
1430
|
readonly requestId: string;
|
|
1227
1431
|
readonly error: RemoteError;
|
|
1432
|
+
} |
|
|
1433
|
+
/**
|
|
1434
|
+
* Operations shown over the store with no request behind them, until they are
|
|
1435
|
+
* lifted: a preview of a change nobody has made. Showing an id again replaces
|
|
1436
|
+
* what it showed.
|
|
1437
|
+
*/
|
|
1438
|
+
{
|
|
1439
|
+
readonly _tag: 'OverlayShown';
|
|
1440
|
+
readonly id: string;
|
|
1441
|
+
readonly optimistic: ReadonlyArray<OptimisticOperation>;
|
|
1442
|
+
} | {
|
|
1443
|
+
readonly _tag: 'OverlayLifted';
|
|
1444
|
+
readonly id: string;
|
|
1228
1445
|
} | {
|
|
1229
1446
|
readonly _tag: 'LiveReceived';
|
|
1230
1447
|
readonly stream: string;
|
|
@@ -1247,7 +1464,11 @@ export type RemoteMessage = {
|
|
|
1247
1464
|
readonly _tag: 'ConnectionRefreshed';
|
|
1248
1465
|
readonly connection: string;
|
|
1249
1466
|
} |
|
|
1250
|
-
/**
|
|
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
|
+
*/
|
|
1251
1472
|
{
|
|
1252
1473
|
readonly _tag: 'QueryFailed';
|
|
1253
1474
|
readonly connection: string;
|
|
@@ -1282,6 +1503,7 @@ export declare const remoteMessageCases: {
|
|
|
1282
1503
|
readonly _tag: Schema.String;
|
|
1283
1504
|
readonly message: Schema.String;
|
|
1284
1505
|
}>;
|
|
1506
|
+
stream: Schema.optional<Schema.String>;
|
|
1285
1507
|
};
|
|
1286
1508
|
RefreshStarted: {
|
|
1287
1509
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -1290,6 +1512,9 @@ export declare const remoteMessageCases: {
|
|
|
1290
1512
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
1291
1513
|
refresh: Schema.optional<Schema.Number>;
|
|
1292
1514
|
};
|
|
1515
|
+
QueryStarted: {
|
|
1516
|
+
connections: Schema.$Array<Schema.String>;
|
|
1517
|
+
};
|
|
1293
1518
|
RetentionChanged: {
|
|
1294
1519
|
roots: Schema.Struct<{
|
|
1295
1520
|
readonly requirements: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -1300,6 +1525,7 @@ export declare const remoteMessageCases: {
|
|
|
1300
1525
|
}>;
|
|
1301
1526
|
};
|
|
1302
1527
|
Hydrated: {
|
|
1528
|
+
connections: Schema.optional<Schema.Unknown>;
|
|
1303
1529
|
entities: Schema.Unknown;
|
|
1304
1530
|
merge: Schema.Union<readonly [Schema.Literal<"replace">, Schema.Literal<"preserve-existing">]>;
|
|
1305
1531
|
};
|
|
@@ -1327,6 +1553,13 @@ export declare const remoteMessageCases: {
|
|
|
1327
1553
|
readonly message: Schema.String;
|
|
1328
1554
|
}>;
|
|
1329
1555
|
};
|
|
1556
|
+
OverlayShown: {
|
|
1557
|
+
id: Schema.String;
|
|
1558
|
+
optimistic: Schema.$Array<Schema.Unknown>;
|
|
1559
|
+
};
|
|
1560
|
+
OverlayLifted: {
|
|
1561
|
+
id: Schema.String;
|
|
1562
|
+
};
|
|
1330
1563
|
LiveReceived: {
|
|
1331
1564
|
stream: Schema.String;
|
|
1332
1565
|
event: Schema.Unknown;
|
|
@@ -1374,6 +1607,27 @@ export declare const isRemoteMessage: <M extends {
|
|
|
1374
1607
|
readonly _tag: RemoteMessageTag;
|
|
1375
1608
|
}>;
|
|
1376
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;
|
|
1612
|
+
/**
|
|
1613
|
+
* The generation a read is planned under: the highest any of the fields it
|
|
1614
|
+
* reads or connections it runs was refreshed at. A read entry carries this as a
|
|
1615
|
+
* dependency, so it restarts when what it observes is refreshed and not when
|
|
1616
|
+
* anything else is.
|
|
1617
|
+
*/
|
|
1618
|
+
export declare const refreshedAt: (refresh: RefreshState, requests: ReadonlyArray<Requirement>, connections?: ReadonlyArray<string>) => number;
|
|
1619
|
+
/**
|
|
1620
|
+
* Marks every field of `requests` and every connection in `connections`
|
|
1621
|
+
* refreshed at the next generation.
|
|
1622
|
+
*/
|
|
1623
|
+
export declare const withRefreshRequested: (refresh: RefreshState, requests: ReadonlyArray<Requirement>, connections?: ReadonlyArray<string>) => RefreshState;
|
|
1624
|
+
/**
|
|
1625
|
+
* Whether one field's read already began under the generation that field was
|
|
1626
|
+
* last refreshed at. Such a read is in flight and would otherwise outlive the
|
|
1627
|
+
* refresh, so the refresh takes a new generation to restart it even though the
|
|
1628
|
+
* field is already stale.
|
|
1629
|
+
*/
|
|
1630
|
+
export declare const refreshIsInFlight: (refresh: RefreshState, request: Requirement, field: string) => boolean;
|
|
1377
1631
|
/**
|
|
1378
1632
|
* Whether a read is fetching any of `fields` for this entity. Consulted only
|
|
1379
1633
|
* when the store lacks the value, which is what separates `Loading` from
|
|
@@ -1386,6 +1640,15 @@ export declare const isLoading: (model: RemoteModel, entity: string, id: string,
|
|
|
1386
1640
|
* can resubscribe rather than silently miss facts.
|
|
1387
1641
|
*/
|
|
1388
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;
|
|
1389
1652
|
/**
|
|
1390
1653
|
* Writes a read result into the store, recording each field's applied window so
|
|
1391
1654
|
* a later request with a different window refetches. A relation page requested
|
|
@@ -1502,6 +1765,25 @@ export interface RelationShape {
|
|
|
1502
1765
|
readonly nullable: boolean;
|
|
1503
1766
|
readonly entity: string;
|
|
1504
1767
|
}
|
|
1768
|
+
/**
|
|
1769
|
+
* A page of a relation that is a whole list is read under a name of its own, the
|
|
1770
|
+
* field's name and the page's size: `comments@first=10`. It is a field like any
|
|
1771
|
+
* other to the store, the planner, and the wire, so the whole list and a page of
|
|
1772
|
+
* it are held side by side, merged, marked stale, and persisted each on its own.
|
|
1773
|
+
* The server reads the name apart, reads the relation with the window, and
|
|
1774
|
+
* answers under the alias. A cursor is not part of the name: a page read from a
|
|
1775
|
+
* cursor merges onto the page it continues.
|
|
1776
|
+
*/
|
|
1777
|
+
export declare const RELATION_ALIAS = "@";
|
|
1778
|
+
/** The alias a relation field is read under, for a window of this size. */
|
|
1779
|
+
export declare const relationAlias: (field: string, window: {
|
|
1780
|
+
readonly first?: number | undefined;
|
|
1781
|
+
readonly last?: number | undefined;
|
|
1782
|
+
readonly after?: string | undefined;
|
|
1783
|
+
readonly before?: string | undefined;
|
|
1784
|
+
}) => string;
|
|
1785
|
+
/** The field an alias reads; a name that is no alias is its own field. */
|
|
1786
|
+
export declare const aliasedField: (name: string) => string;
|
|
1505
1787
|
/** Splits a ref key (`"Entity:id"`, the store key too) back into its parts. */
|
|
1506
1788
|
export declare const refParts: (encoded: string) => RefParts;
|
|
1507
1789
|
/**
|
|
@@ -1685,6 +1967,92 @@ declare const Entity$1: {
|
|
|
1685
1967
|
patch: <Name extends string, F extends Schema.Struct.Fields>(ref: EntityRef<Name, F>, patch: Partial<Schema.Struct.Encoded<F>>) => EntityPatch<Name, F>;
|
|
1686
1968
|
};
|
|
1687
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
|
|
1688
2056
|
//#region src/inspect.d.ts
|
|
1689
2057
|
/** A serializable summary of a RemoteModel for DevTools and diagnostics. */
|
|
1690
2058
|
export interface RemoteInspection {
|
|
@@ -1699,6 +2067,19 @@ export interface RemoteInspection {
|
|
|
1699
2067
|
readonly connections: ReadonlyArray<string>;
|
|
1700
2068
|
readonly live: ReadonlyArray<string>;
|
|
1701
2069
|
readonly gaps: ReadonlyArray<string>;
|
|
2070
|
+
/**
|
|
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
|
|
2073
|
+
* working" means here, and it is read from the Model rather than from the
|
|
2074
|
+
* fibers doing the work: a tool that shows it shows something the Model can
|
|
2075
|
+
* be replayed to, and nothing that needs the runtime to be asked.
|
|
2076
|
+
*/
|
|
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;
|
|
1702
2083
|
readonly mutations: {
|
|
1703
2084
|
readonly pending: ReadonlyArray<string>;
|
|
1704
2085
|
readonly failed: ReadonlyArray<string>;
|
|
@@ -1709,6 +2090,75 @@ export interface RemoteInspection {
|
|
|
1709
2090
|
export declare const inspectRemote: (model: RemoteModel) => RemoteInspection;
|
|
1710
2091
|
/** A pure, serializable view of one entity, or `undefined` if unknown. */
|
|
1711
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
|
+
}
|
|
1712
2162
|
//#endregion
|
|
1713
2163
|
//#region src/plan.d.ts
|
|
1714
2164
|
type Window = NonNullable<Requirement['windows']>[string];
|
|
@@ -1774,9 +2224,9 @@ export type EntityLike = EntityDescriptor<any, any> | Domain.AnyEntity;
|
|
|
1774
2224
|
* What a read selects: a Remote Selection, or a `foldkit-entity` Selection
|
|
1775
2225
|
* (read through `Selection.from`).
|
|
1776
2226
|
*/
|
|
1777
|
-
export type EntitySelection<Value, Name extends string> = Selection<Value, Name, 'entity'> | Domain.Selection<Name, unknown, Schema.Constraint & {
|
|
2227
|
+
export type EntitySelection<Value, Name extends string, Id extends string = string> = Selection<Value, Name, 'entity'> | Domain.Selection<Name, unknown, Schema.Constraint & {
|
|
1778
2228
|
readonly Type: Value;
|
|
1779
|
-
}>;
|
|
2229
|
+
}, Id>;
|
|
1780
2230
|
type EntityName<D> = D extends {
|
|
1781
2231
|
readonly name: infer Name extends string;
|
|
1782
2232
|
} ? Name : never;
|
|
@@ -1858,13 +2308,13 @@ export interface MutationStarted<AppModel> {
|
|
|
1858
2308
|
*/
|
|
1859
2309
|
export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities extends readonly EntityLike[], Queries extends readonly QueryDescriptor<any, any, any>[], Mutations extends readonly MutationDescriptor<any, any, any>[]> extends BoundRemote<AppModel, Store, EntityName<Entities[number]>>, RemoteDescriptor<Entities, Queries, Mutations> {
|
|
1860
2310
|
/** `Remote.select`: a Projection reading one entity through a selection of a registered entity. */
|
|
1861
|
-
get<Value, Name extends string>(selection: EntitySelection<Value, Name> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id:
|
|
2311
|
+
get<Value, Name extends string, Id extends string = string>(selection: EntitySelection<Value, Name, Id> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id: NoInfer<Id>): Projection<AppModel, RemoteData<Value>>;
|
|
1862
2312
|
/**
|
|
1863
2313
|
* `get`, and the projection also subscribes to the entity's changes: its
|
|
1864
2314
|
* requirements are marked `live`, so `subscriptions` derives a live entry for
|
|
1865
2315
|
* the Surfaces that read it.
|
|
1866
2316
|
*/
|
|
1867
|
-
live<Value, Name extends string>(selection: EntitySelection<Value, Name> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id:
|
|
2317
|
+
live<Value, Name extends string, Id extends string = string>(selection: EntitySelection<Value, Name, Id> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id: NoInfer<Id>): Projection<AppModel, RemoteData<Value>>;
|
|
1868
2318
|
/**
|
|
1869
2319
|
* A query connection read as a `Page` of items selected of the query's
|
|
1870
2320
|
* entity: `Initial` until the page and every item's selected fields are
|
|
@@ -1877,8 +2327,55 @@ export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities exte
|
|
|
1877
2327
|
next<Name extends string, Input>(model: AppModel, projection: QueryProjection<AppModel, any, Name, Input>): QueryRef<Name, Input> | undefined;
|
|
1878
2328
|
/** The `QueryRef` for the page before the loaded start, or `undefined`. */
|
|
1879
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>;
|
|
1880
2368
|
/** A Command that runs the query and yields the `ConnectionMerged` (or `QueryFailed`) that reduces it: "load more". */
|
|
1881
2369
|
fetch(ref: QueryRef<string, unknown>): Command<RemoteMessage, never, RemoteClient>;
|
|
2370
|
+
/**
|
|
2371
|
+
* Shows operations over the store with no request behind them, as a mutation's
|
|
2372
|
+
* `optimistic` ones show while it is in flight: every Selection and view draws
|
|
2373
|
+
* them. For a preview of a change nobody has made. They stay until `lift`;
|
|
2374
|
+
* showing an id again replaces what it showed. Called from `update`.
|
|
2375
|
+
*/
|
|
2376
|
+
overlay(model: AppModel, id: string, optimistic: ReadonlyArray<OptimisticOperation>): AppModel;
|
|
2377
|
+
/** Lifts what `overlay` showed under this id. Lifting nothing returns the same Model. */
|
|
2378
|
+
lift(model: AppModel, id: string): AppModel;
|
|
1882
2379
|
/** `Remote.refresh`: marks what a projection or a Surface requires as due, for its read entry to refetch. */
|
|
1883
2380
|
refresh(model: AppModel, target: Projection<AppModel, unknown> | Surface<AppModel, any, any, void>): AppModel;
|
|
1884
2381
|
/**
|
|
@@ -1894,6 +2391,30 @@ export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities exte
|
|
|
1894
2391
|
plan<Value>(model: AppModel, projection: Projection<AppModel, Value>, options?: PlanOptions): ReadonlyArray<Requirement>;
|
|
1895
2392
|
/** `Remote.storeOf`: the visible store, base under the pending optimistic layers. */
|
|
1896
2393
|
storeOf(model: AppModel): EntityStore;
|
|
2394
|
+
/**
|
|
2395
|
+
* `Remote.confirmed`: the same projection read against the server-derived
|
|
2396
|
+
* store alone, with every pending optimistic layer and connection overlay
|
|
2397
|
+
* left off. What it plans is unchanged — the requirements are the
|
|
2398
|
+
* projection's own — so observing it reads exactly what observing the
|
|
2399
|
+
* projection reads; only what it *shows* differs.
|
|
2400
|
+
*
|
|
2401
|
+
* A view usually wants the projection itself, which is the visible read:
|
|
2402
|
+
* the optimistic layers are there so a change shows before the server has
|
|
2403
|
+
* agreed to it. This is for the reader that must not believe a change until
|
|
2404
|
+
* the server has confirmed it, which in practice is an Agent capability
|
|
2405
|
+
* reporting that what it was asked to do is done:
|
|
2406
|
+
*
|
|
2407
|
+
* ```ts
|
|
2408
|
+
* Agent.when({
|
|
2409
|
+
* projection: Data.confirmed(ProjectPage.model.project),
|
|
2410
|
+
* predicate: (project, request) => project.name === request.name,
|
|
2411
|
+
* })
|
|
2412
|
+
* ```
|
|
2413
|
+
*
|
|
2414
|
+
* There is deliberately no `visible`: a projection is already the visible
|
|
2415
|
+
* read, and a wrapper that only forwards would be a second name for it.
|
|
2416
|
+
*/
|
|
2417
|
+
confirmed<P extends Projection<AppModel, any>>(projection: P): P;
|
|
1897
2418
|
/**
|
|
1898
2419
|
* The plan run through `RemoteClient` and reduced into the Model: the
|
|
1899
2420
|
* projection's pending queries first, then the fields it lacks (the pages'
|
|
@@ -1948,6 +2469,8 @@ export type QueryOptions<Value, Entity extends string, Of extends string = Entit
|
|
|
1948
2469
|
/** A query connection read as a `Page` of selected items; `ref` is the connection with its first window. */
|
|
1949
2470
|
export interface QueryProjection<AppModel, Value, Name extends string, Input> extends Projection<AppModel, RemoteData<Page<Value>>> {
|
|
1950
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>;
|
|
1951
2474
|
}
|
|
1952
2475
|
/** The input of a query descriptor. */
|
|
1953
2476
|
export type QueryInput<Q> = Q extends QueryDescriptor<any, infer Input, any> ? Input : never;
|
|
@@ -1989,7 +2512,11 @@ declare const PlannedQuery: Schema.Struct<{
|
|
|
1989
2512
|
export interface ReadDependencies {
|
|
1990
2513
|
readonly requirements: ReadonlyArray<Requirement>;
|
|
1991
2514
|
readonly queries: ReadonlyArray<Schema.Schema.Type<typeof PlannedQuery>>;
|
|
1992
|
-
/**
|
|
2515
|
+
/**
|
|
2516
|
+
* The highest generation the fields this entry reads were refreshed at: a
|
|
2517
|
+
* refresh of any of them restarts the entry, and a refresh of anything else
|
|
2518
|
+
* leaves it running.
|
|
2519
|
+
*/
|
|
1993
2520
|
readonly refresh: number;
|
|
1994
2521
|
}
|
|
1995
2522
|
export declare const Remote: {
|
|
@@ -2020,6 +2547,7 @@ export declare const Remote: {
|
|
|
2020
2547
|
readonly _tag: Schema.String;
|
|
2021
2548
|
readonly message: Schema.String;
|
|
2022
2549
|
}>;
|
|
2550
|
+
stream: Schema.optional<Schema.String>;
|
|
2023
2551
|
};
|
|
2024
2552
|
RefreshStarted: {
|
|
2025
2553
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -2028,6 +2556,9 @@ export declare const Remote: {
|
|
|
2028
2556
|
requests: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
2029
2557
|
refresh: Schema.optional<Schema.Number>;
|
|
2030
2558
|
};
|
|
2559
|
+
QueryStarted: {
|
|
2560
|
+
connections: Schema.$Array<Schema.String>;
|
|
2561
|
+
};
|
|
2031
2562
|
RetentionChanged: {
|
|
2032
2563
|
roots: Schema.Struct<{
|
|
2033
2564
|
readonly requirements: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
|
|
@@ -2038,6 +2569,7 @@ export declare const Remote: {
|
|
|
2038
2569
|
}>;
|
|
2039
2570
|
};
|
|
2040
2571
|
Hydrated: {
|
|
2572
|
+
connections: Schema.optional<Schema.Unknown>;
|
|
2041
2573
|
entities: Schema.Unknown;
|
|
2042
2574
|
merge: Schema.Union<readonly [Schema.Literal<"replace">, Schema.Literal<"preserve-existing">]>;
|
|
2043
2575
|
};
|
|
@@ -2065,6 +2597,13 @@ export declare const Remote: {
|
|
|
2065
2597
|
readonly message: Schema.String;
|
|
2066
2598
|
}>;
|
|
2067
2599
|
};
|
|
2600
|
+
OverlayShown: {
|
|
2601
|
+
id: Schema.String;
|
|
2602
|
+
optimistic: Schema.$Array<Schema.Unknown>;
|
|
2603
|
+
};
|
|
2604
|
+
OverlayLifted: {
|
|
2605
|
+
id: Schema.String;
|
|
2606
|
+
};
|
|
2068
2607
|
LiveReceived: {
|
|
2069
2608
|
stream: Schema.String;
|
|
2070
2609
|
event: Schema.Unknown;
|
|
@@ -2151,6 +2690,12 @@ export declare const Remote: {
|
|
|
2151
2690
|
* has not changed.
|
|
2152
2691
|
*/
|
|
2153
2692
|
storeOf: <AppModel, Store extends RemoteModel>(bound: BoundRemote<AppModel, Store>, model: AppModel) => EntityStore;
|
|
2693
|
+
/**
|
|
2694
|
+
* The projection read against the server-derived store alone: the same
|
|
2695
|
+
* requirements, planned the same way, with the pending optimistic layers and
|
|
2696
|
+
* connection overlays left off. See the bound `Remote.confirmed`.
|
|
2697
|
+
*/
|
|
2698
|
+
confirmed: <AppModel, Store extends RemoteModel, P extends Projection<AppModel, any>>(bound: BoundRemote<AppModel, Store>, projection: P) => P;
|
|
2154
2699
|
/**
|
|
2155
2700
|
* Executes the plan against the `RemoteClient` and returns a new store. Used
|
|
2156
2701
|
* for SSR route prefetch, hover prefetch, and tests. Never called during render.
|
|
@@ -2171,6 +2716,9 @@ export declare const Remote: {
|
|
|
2171
2716
|
* again with `next`/`previous`. The read entries restart, so a read or query
|
|
2172
2717
|
* sent before the refresh is not applied after it.
|
|
2173
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
|
+
*
|
|
2174
2722
|
* The projection must be observed, which it is while it is on screen. Live
|
|
2175
2723
|
* subscriptions are left as they are. For data nothing observes (SSR, tests),
|
|
2176
2724
|
* use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
|
|
@@ -2283,8 +2831,10 @@ export declare const Remote: {
|
|
|
2283
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>;
|
|
2284
2832
|
/**
|
|
2285
2833
|
* A Foldkit Subscription entry that consumes the live stream for a Surface's
|
|
2286
|
-
* requirements, emitting a `LiveReceived` per event and a `ReadFailed`
|
|
2287
|
-
*
|
|
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
|
|
2288
2838
|
* from `RemoteModel.live`, so the application tracks no cursor of its own.
|
|
2289
2839
|
*/
|
|
2290
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, {
|