foldkit-remote 0.4.0 → 0.5.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 +173 -9
- package/dist/index.d.mts +232 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +384 -121
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -145,9 +145,9 @@ Start with one server entity and one selection:
|
|
|
145
145
|
|
|
146
146
|
```ts
|
|
147
147
|
import { Schema } from 'effect'
|
|
148
|
-
import { Entity } from 'foldkit-
|
|
148
|
+
import { Entity } from 'foldkit-entity'
|
|
149
149
|
|
|
150
|
-
const Project = Entity.
|
|
150
|
+
const Project = Entity.define(
|
|
151
151
|
'Project',
|
|
152
152
|
Schema.Struct({
|
|
153
153
|
id: Schema.String,
|
|
@@ -157,12 +157,20 @@ const Project = Entity.make(
|
|
|
157
157
|
)
|
|
158
158
|
|
|
159
159
|
// A Selection is the exact server-owned shape this consumer needs.
|
|
160
|
-
const ProjectSummary =
|
|
160
|
+
const ProjectSummary = Entity.select(Project, {
|
|
161
161
|
id: true,
|
|
162
162
|
name: true,
|
|
163
163
|
})
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
The entity comes from [`foldkit-entity`](../entity), which declares a domain
|
|
167
|
+
without Remote in it. Remote also has an `Entity.make` of its own, and accepts
|
|
168
|
+
either — but only a `foldkit-entity` entity has addressable `fields`, so only
|
|
169
|
+
that one can be given [relations](#entities-declared-with-foldkit-entity),
|
|
170
|
+
derived members, or a [query body](#queries-and-pagination). Reach for
|
|
171
|
+
`Entity.make` when a domain is small enough that Remote is all it will ever
|
|
172
|
+
need; reach for `Entity.define` otherwise, which is most of the time.
|
|
173
|
+
|
|
166
174
|
Embed Remote's Submodel in the application and bind the domain to that field:
|
|
167
175
|
|
|
168
176
|
```ts
|
|
@@ -371,6 +379,46 @@ state that actually means "wait for this request."
|
|
|
371
379
|
`RemoteData.match` is exhaustive, so adding or omitting a state is visible at
|
|
372
380
|
compile time.
|
|
373
381
|
|
|
382
|
+
### Drawing one: `RemoteData.render`
|
|
383
|
+
|
|
384
|
+
Most views draw these six states three ways, under one policy: useful data
|
|
385
|
+
stays on screen. `RemoteData.render` is that fold.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
RemoteData.render(project, {
|
|
389
|
+
loading: () => ProjectSkeleton(),
|
|
390
|
+
notFound: () => NoSuchProject(),
|
|
391
|
+
failed: error => ErrorView(error),
|
|
392
|
+
data: (value, freshness) =>
|
|
393
|
+
ProjectView({ project: value, dimmed: freshness._tag !== 'Fresh' }),
|
|
394
|
+
})
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`Initial` and `Loading` reach `loading`; `Ready` and `Refreshing` reach `data`;
|
|
398
|
+
a `Failed` that still carries the value it had reaches `data` too, so a read
|
|
399
|
+
that failed does not throw away what the reader was already looking at. Only a
|
|
400
|
+
`Failed` with nothing to show reaches `failed`.
|
|
401
|
+
|
|
402
|
+
The `data` branch is told which it got, as a `Freshness` — exported, so a view
|
|
403
|
+
helper that takes one can name it:
|
|
404
|
+
|
|
405
|
+
| `Freshness` | What it means |
|
|
406
|
+
| --- | --- |
|
|
407
|
+
| `Fresh` | This is the current answer. |
|
|
408
|
+
| `Refreshing` | A newer answer is on its way; this one is still good. |
|
|
409
|
+
| `Stale` | A read failed; this is what was last known good, with its `error`. |
|
|
410
|
+
|
|
411
|
+
It is one tag rather than a pair of booleans because a value cannot be both
|
|
412
|
+
refreshing and stale, and a type that can say so invites a view to handle a
|
|
413
|
+
state that never arrives.
|
|
414
|
+
|
|
415
|
+
`notFound` is its own branch and not optional. A row the server answered for
|
|
416
|
+
and does not have is neither loading nor a failure; drawing it as either is a
|
|
417
|
+
spinner that never ends or an error nobody can act on.
|
|
418
|
+
|
|
419
|
+
Reach for `match` instead when the six states really do draw differently — it
|
|
420
|
+
stays the exhaustive fold, and `render` does not replace it.
|
|
421
|
+
|
|
374
422
|
## Normalized entities and selections
|
|
375
423
|
|
|
376
424
|
Remote stores an entity once by identity, regardless of how many Surfaces read
|
|
@@ -444,7 +492,9 @@ const card = Data.get(ProjectCard, 'p1') // Projection<Model, RemoteData<{ name;
|
|
|
444
492
|
|
|
445
493
|
- A relation becomes a ref field: `one` a ref, an optional `one` a nullable ref,
|
|
446
494
|
`many` an array of refs. A derived member becomes a field the server supplies.
|
|
447
|
-
- The Entity needs an `id` field; Remote keys the store by it.
|
|
495
|
+
- The Entity needs an `id` field; Remote keys the store by it. When that field
|
|
496
|
+
is branded, `Data.get` and `Data.live` take that type for an Entity Selection:
|
|
497
|
+
another Entity's id, or plain text, does not compile.
|
|
448
498
|
- Register the Entities of one `Entity.relate` result and select from that same
|
|
449
499
|
result; `Object.values(Work)` registers them all.
|
|
450
500
|
- `Entity.from(entity)` and `Selection.from(selection)` are the compile steps,
|
|
@@ -452,9 +502,14 @@ const card = Data.get(ProjectCard, 'p1') // Projection<Model, RemoteData<{ name;
|
|
|
452
502
|
handler.
|
|
453
503
|
- `Entity.page(selection, window)` in an Entity Selection is Remote's
|
|
454
504
|
`Selection.connection`: the window travels with the read, and the server
|
|
455
|
-
answers with a page of refs.
|
|
456
|
-
|
|
457
|
-
same
|
|
505
|
+
answers with a page of refs. A page of a whole list is read under a name of
|
|
506
|
+
its own, `comments@first=10`, so one view can show every comment while another
|
|
507
|
+
shows the first ten of the same post: they are two fields to the store, fetched
|
|
508
|
+
in one batch, merged and refreshed each on its own. A write to the list (a
|
|
509
|
+
mutation's patch, a live change) marks its pages stale, so they are read again.
|
|
510
|
+
A cursor is not part of the name: a page read from a cursor continues its page.
|
|
511
|
+
The server refuses a request that pages one relation more than four ways, and
|
|
512
|
+
a field's own name may not contain `@`.
|
|
458
513
|
|
|
459
514
|
`Entity.make` with `Entity.ref` keeps working, and both kinds can share one
|
|
460
515
|
domain. Both packages export `Entity`; a module that needs `Entity.from` beside
|
|
@@ -509,6 +564,24 @@ Projection itself.
|
|
|
509
564
|
|
|
510
565
|
### Refreshing from `update`
|
|
511
566
|
|
|
567
|
+
### Showing a change nobody has made
|
|
568
|
+
|
|
569
|
+
A mutation's `optimistic` operations show over the store while it is in flight.
|
|
570
|
+
`Data.overlay` shows operations the same way with no request behind them, until
|
|
571
|
+
`Data.lift`: a preview, in every Selection and view, of something not yet sent.
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
const previewed = Data.overlay(model, 'post-preview', [Project.patch(id, { name: draft })])
|
|
575
|
+
const back = Data.lift(previewed, 'post-preview')
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
- Both are called from `update`, and neither performs I/O or touches what the
|
|
579
|
+
server said: the store beneath is as it was.
|
|
580
|
+
- Showing an id again replaces what it showed. Lifting what was never shown
|
|
581
|
+
returns the same Model.
|
|
582
|
+
- An overlay's id is apart from every request's, so a mutation that settles does
|
|
583
|
+
not take a preview with it.
|
|
584
|
+
|
|
512
585
|
To revalidate what a screen already declares — a refresh button, a focus
|
|
513
586
|
regained — hand its Projection (or a Surface without params) to `Data.refresh`:
|
|
514
587
|
|
|
@@ -521,8 +594,9 @@ case 'ClickedRefresh': {
|
|
|
521
594
|
```
|
|
522
595
|
|
|
523
596
|
`Data.refresh` performs no I/O and restates no request. It returns the Model
|
|
524
|
-
with every selected field the store holds reading `Refreshing
|
|
525
|
-
|
|
597
|
+
with every selected field the store holds reading `Refreshing`, every entity it
|
|
598
|
+
knew to be absent forgotten, so `NotFound` reads `Loading` and is asked for
|
|
599
|
+
again, and every loaded connection invalidated; the `Data.subscriptions` read entries then refetch it,
|
|
526
600
|
since stale data is planned again under every policy, so the page is requested
|
|
527
601
|
once. The Projection must be observed, as it is while it is on screen; for data
|
|
528
602
|
nothing observes, use `Data.prefetch` with `RemotePolicy.networkOnly`.
|
|
@@ -555,6 +629,52 @@ const Data = Remote.make({
|
|
|
555
629
|
})
|
|
556
630
|
```
|
|
557
631
|
|
|
632
|
+
`Query.make` names a query and says what it returns. Where it *means*
|
|
633
|
+
something the client should be able to state, declare it with a body instead:
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
import { Entity, Expr, Order, Query } from 'foldkit-entity'
|
|
637
|
+
|
|
638
|
+
const Task = Entity.define(
|
|
639
|
+
'Task',
|
|
640
|
+
Schema.Struct({ id: Schema.String, ownerId: Schema.String, updatedAt: Schema.Number }),
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
const TasksByOwner = Query.define(
|
|
644
|
+
'TasksByOwner',
|
|
645
|
+
{ ownerId: Schema.String },
|
|
646
|
+
({ input }) =>
|
|
647
|
+
Query.from(Task).pipe(
|
|
648
|
+
Query.where(Expr.eq(Task.fields.ownerId, input.ownerId)),
|
|
649
|
+
Query.orderBy(Order.desc(Task.fields.updatedAt), Order.asc(Task.fields.id)),
|
|
650
|
+
),
|
|
651
|
+
)
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
A body is written over a [`foldkit-entity`](../entity) Entity, because that is
|
|
655
|
+
what has addressable fields: `Task.fields.ownerId` is a reference an `Expr` can
|
|
656
|
+
be built from, where this package's own `Entity.make` describes a field as the
|
|
657
|
+
schema of its value and has nothing to point at. An entity declared either way
|
|
658
|
+
still reads, selects and normalizes the same; only a query body needs the
|
|
659
|
+
richer declaration.
|
|
660
|
+
|
|
661
|
+
What comes back is an ordinary descriptor — the same name, `Input`, `ref` and
|
|
662
|
+
connection identity — carrying its `body` besides, and the result is a
|
|
663
|
+
connection over the Entity the body reads, so it is not named twice. A server
|
|
664
|
+
can compile that body rather than be told the same thing again in its own
|
|
665
|
+
dialect; one that would rather answer the query its own way still can, and a
|
|
666
|
+
descriptor from `Query.make` has no body at all.
|
|
667
|
+
|
|
668
|
+
The body is built **once**, when the query is declared. Inside it `input.ownerId`
|
|
669
|
+
is a placeholder for the value the query will be given, not the value — there is
|
|
670
|
+
nothing there yet to branch on. A condition that depends on what was passed is a
|
|
671
|
+
comparison over the placeholder, never a `?:` around it.
|
|
672
|
+
|
|
673
|
+
`Input` must be fields or a plain `Schema.Struct`, because that is what the
|
|
674
|
+
placeholders are made from. A codec that exposes no keys is refused where the
|
|
675
|
+
query is declared rather than handing the body an empty object, which would
|
|
676
|
+
compare a column to nothing while every type agreed.
|
|
677
|
+
|
|
558
678
|
Then a query is still just a Projection:
|
|
559
679
|
|
|
560
680
|
```ts
|
|
@@ -663,6 +783,41 @@ const { model: started, command } = Data.mutate(model, AddComment, input, {
|
|
|
663
783
|
A successful server result can replace the request's temporary connection
|
|
664
784
|
overlays with confirmed ones in place.
|
|
665
785
|
|
|
786
|
+
### Reading past what is only pending
|
|
787
|
+
|
|
788
|
+
Every projection reads the **visible** cache: the server-derived store under
|
|
789
|
+
the layers still pending, which is why an optimistic change shows at once. A
|
|
790
|
+
reader that must not believe a change until the server has agreed to it asks
|
|
791
|
+
for the confirmed read instead:
|
|
792
|
+
|
|
793
|
+
```ts
|
|
794
|
+
Data.confirmed(Data.get(ProjectSummary, projectId))
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
It is the same projection — the same requirements, planned the same way, so
|
|
798
|
+
observing it fetches exactly what observing the original fetches — reading the
|
|
799
|
+
server-derived store alone, with the pending layers and connection overlays
|
|
800
|
+
left off. An optimistically inserted edge is not in its page; an
|
|
801
|
+
optimistically patched field reads as the server last said.
|
|
802
|
+
|
|
803
|
+
A view almost always wants the projection itself. This is for the reader that
|
|
804
|
+
reports on the world rather than drawing it, which in practice is an Agent
|
|
805
|
+
capability that must not claim success before the server reflects it:
|
|
806
|
+
|
|
807
|
+
```ts
|
|
808
|
+
Agent.when({
|
|
809
|
+
projection: Data.confirmed(Data.get(ProjectSummary, projectId)),
|
|
810
|
+
predicate: (project, request) => project.name === request.name,
|
|
811
|
+
})
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
There is deliberately no `Data.visible`: a projection already is the visible
|
|
815
|
+
read, and a second name for it would be a wrapper that only forwards.
|
|
816
|
+
|
|
817
|
+
Sync draws the same line over its replica, with the same words and a different
|
|
818
|
+
mechanism: see [what a reader sees while a change is in
|
|
819
|
+
flight](../../docs/state-model.md#what-a-reader-sees-while-a-change-is-in-flight).
|
|
820
|
+
|
|
666
821
|
### Remote mutation vs Sync operation
|
|
667
822
|
|
|
668
823
|
Do not use Remote mutation as a durable offline-write mechanism:
|
|
@@ -828,6 +983,15 @@ useful question is: **"Why is this Projection still Initial?"** `Data.plan`
|
|
|
828
983
|
shows whether Remote believes anything is actually missing; active Surface
|
|
829
984
|
wiring determines whether that plan is being executed.
|
|
830
985
|
|
|
986
|
+
`Data.inspect(model).loading` answers the other one — **"what is Remote doing
|
|
987
|
+
right now?"** — with the `entity\0id\0field` marks of the reads in flight,
|
|
988
|
+
beside `mutations.pending` for the writes. Both are read from the Model, not
|
|
989
|
+
from the fibers doing the work: a tool that shows them shows something a
|
|
990
|
+
recorded Model can be replayed to, and nothing that needs the runtime to be
|
|
991
|
+
asked. That is deliberate. Runtime activity is not a second source of truth
|
|
992
|
+
here, and a view that rendered from it would stop being reproducible from the
|
|
993
|
+
Model.
|
|
994
|
+
|
|
831
995
|
## Advanced: the kernel
|
|
832
996
|
|
|
833
997
|
Everything above compiles to the lower-level exports below; there is not a
|
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, 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,20 @@ 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]>; };
|
|
325
384
|
/** Stable stringify: object keys sorted, undefined-valued keys dropped, so equal inputs encode equally. */
|
|
326
385
|
export declare const stableStringify: (value: unknown) => string;
|
|
327
386
|
/**
|
|
@@ -332,6 +391,8 @@ export declare const stableStringify: (value: unknown) => string;
|
|
|
332
391
|
export type ResultOf<Result> = Result extends {
|
|
333
392
|
readonly name: infer Entity extends string;
|
|
334
393
|
} ? ConnectionSpec<Entity> : Result;
|
|
394
|
+
/** A relational `Query` over one Entity, as `Query.define`'s body returns. */
|
|
395
|
+
type RelationalQueryOf<E extends AnyEntity> = ReturnType<typeof Query$1.from<E>>;
|
|
335
396
|
export declare const Query: {
|
|
336
397
|
/** Declares the entity and options a query's result is a connection over. */
|
|
337
398
|
connection: <Entity extends string>(entity: {
|
|
@@ -349,10 +410,44 @@ export declare const Query: {
|
|
|
349
410
|
readonly Input: Input;
|
|
350
411
|
readonly Result: Result;
|
|
351
412
|
}) => QueryDescriptor<Name, TypeOf<Input>, ResultOf<Result>>;
|
|
413
|
+
/**
|
|
414
|
+
* Declares a query by what it means. The body is built once, here, and the
|
|
415
|
+
* `input` it is given holds placeholders rather than values — there is
|
|
416
|
+
* nothing yet to branch on, so a condition that depends on what was passed is
|
|
417
|
+
* a comparison over the placeholder:
|
|
418
|
+
*
|
|
419
|
+
* ```ts
|
|
420
|
+
* const PostsBySlug = Query.define('PostsBySlug', { slug: Schema.String }, ({ input }) =>
|
|
421
|
+
* Query.from(Post).pipe(
|
|
422
|
+
* Query.where(Expr.eq(Post.fields.slug, input.slug)),
|
|
423
|
+
* Query.orderBy(Order.asc(Post.fields.id)),
|
|
424
|
+
* ),
|
|
425
|
+
* )
|
|
426
|
+
* ```
|
|
427
|
+
*
|
|
428
|
+
* What comes back is an ordinary `QueryDescriptor` — the same name, `Input`,
|
|
429
|
+
* `ref` and connection identity `Query.make` gives — carrying its `body`
|
|
430
|
+
* besides. A server can compile that body instead of being told the same
|
|
431
|
+
* thing again in its own dialect, and one that would rather answer the query
|
|
432
|
+
* its own way still can.
|
|
433
|
+
*
|
|
434
|
+
* The result is a connection over the Entity the body reads, so it is not
|
|
435
|
+
* named twice.
|
|
436
|
+
*/
|
|
437
|
+
define: <const Name extends string, Input extends SchemaOrFields, E extends AnyEntity>(name: Name, Input: Input, body: (context: {
|
|
438
|
+
readonly input: QueryInputs<TypeOf<Input>>;
|
|
439
|
+
}) => RelationalQueryOf<E>, options?: {
|
|
440
|
+
readonly edgeKey?: Schema.Schema<unknown>;
|
|
441
|
+
readonly live?: LivePolicy;
|
|
442
|
+
}) => QueryDescriptor<Name, TypeOf<Input>, ConnectionSpec<E["name"]>>;
|
|
352
443
|
first: (count: number) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
353
444
|
last: (count: number) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
354
445
|
after: (cursor: Cursor) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
355
446
|
before: (cursor: Cursor) => <R extends QueryRef<string, unknown>>(ref: R) => R;
|
|
447
|
+
from: <E extends AnyEntity>(entity: E) => Query$1<E>;
|
|
448
|
+
where: (...predicates: ReadonlyArray<import("foldkit-entity").Predicate>) => <E extends AnyEntity>(self: Query$1<E>) => Query$1<E>;
|
|
449
|
+
orderBy: (...terms: ReadonlyArray<import("foldkit-entity").OrderTerm>) => <E extends AnyEntity>(self: Query$1<E>) => Query$1<E>;
|
|
450
|
+
dependencies: (self: AnyQuery) => import("foldkit-entity").Dependencies;
|
|
356
451
|
};
|
|
357
452
|
//#endregion
|
|
358
453
|
//#region src/requirement.d.ts
|
|
@@ -1160,15 +1255,25 @@ export interface RemoteModel {
|
|
|
1160
1255
|
*/
|
|
1161
1256
|
readonly loading: ReadonlySet<string>;
|
|
1162
1257
|
/**
|
|
1163
|
-
* `Remote.refresh` generations
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1258
|
+
* `Remote.refresh` generations, held per field rather than for the store as a
|
|
1259
|
+
* whole. A read entry restarts when the generation over the fields it plans
|
|
1260
|
+
* moves, so a read sent before a refresh of its own fields cannot land after
|
|
1261
|
+
* it, and a refresh of one field leaves every other entry's read alone.
|
|
1166
1262
|
*/
|
|
1167
|
-
readonly refresh:
|
|
1168
|
-
readonly requested: number;
|
|
1169
|
-
readonly started: number;
|
|
1170
|
-
};
|
|
1263
|
+
readonly refresh: RefreshState;
|
|
1171
1264
|
}
|
|
1265
|
+
/**
|
|
1266
|
+
* `Remote.refresh` generations, per field mark. `generation` is the last one
|
|
1267
|
+
* handed out; `requested` is when each field was last refreshed, and `started`
|
|
1268
|
+
* is the generation each field's read last began under. Holding them per field
|
|
1269
|
+
* is what keeps one Projection's refresh from restarting every read entry.
|
|
1270
|
+
*/
|
|
1271
|
+
export interface RefreshState {
|
|
1272
|
+
readonly generation: number;
|
|
1273
|
+
readonly requested: ReadonlyMap<string, number>;
|
|
1274
|
+
readonly started: ReadonlyMap<string, number>;
|
|
1275
|
+
}
|
|
1276
|
+
export declare const emptyRefresh: RefreshState;
|
|
1172
1277
|
export declare const initialRemoteModel: RemoteModel;
|
|
1173
1278
|
export declare const remoteModelSchema: () => Schema.Codec<RemoteModel, unknown>;
|
|
1174
1279
|
/** The submodel's Messages; each reduces to `RemoteModel` through `updateRemote`. */
|
|
@@ -1225,6 +1330,19 @@ export type RemoteMessage = {
|
|
|
1225
1330
|
readonly _tag: 'MutationFailed';
|
|
1226
1331
|
readonly requestId: string;
|
|
1227
1332
|
readonly error: RemoteError;
|
|
1333
|
+
} |
|
|
1334
|
+
/**
|
|
1335
|
+
* Operations shown over the store with no request behind them, until they are
|
|
1336
|
+
* lifted: a preview of a change nobody has made. Showing an id again replaces
|
|
1337
|
+
* what it showed.
|
|
1338
|
+
*/
|
|
1339
|
+
{
|
|
1340
|
+
readonly _tag: 'OverlayShown';
|
|
1341
|
+
readonly id: string;
|
|
1342
|
+
readonly optimistic: ReadonlyArray<OptimisticOperation>;
|
|
1343
|
+
} | {
|
|
1344
|
+
readonly _tag: 'OverlayLifted';
|
|
1345
|
+
readonly id: string;
|
|
1228
1346
|
} | {
|
|
1229
1347
|
readonly _tag: 'LiveReceived';
|
|
1230
1348
|
readonly stream: string;
|
|
@@ -1327,6 +1445,13 @@ export declare const remoteMessageCases: {
|
|
|
1327
1445
|
readonly message: Schema.String;
|
|
1328
1446
|
}>;
|
|
1329
1447
|
};
|
|
1448
|
+
OverlayShown: {
|
|
1449
|
+
id: Schema.String;
|
|
1450
|
+
optimistic: Schema.$Array<Schema.Unknown>;
|
|
1451
|
+
};
|
|
1452
|
+
OverlayLifted: {
|
|
1453
|
+
id: Schema.String;
|
|
1454
|
+
};
|
|
1330
1455
|
LiveReceived: {
|
|
1331
1456
|
stream: Schema.String;
|
|
1332
1457
|
event: Schema.Unknown;
|
|
@@ -1374,6 +1499,25 @@ export declare const isRemoteMessage: <M extends {
|
|
|
1374
1499
|
readonly _tag: RemoteMessageTag;
|
|
1375
1500
|
}>;
|
|
1376
1501
|
export declare const remoteMessageSchema: Schema.Schema<RemoteMessage>;
|
|
1502
|
+
/**
|
|
1503
|
+
* The generation a read is planned under: the highest any of the fields it
|
|
1504
|
+
* reads or connections it runs was refreshed at. A read entry carries this as a
|
|
1505
|
+
* dependency, so it restarts when what it observes is refreshed and not when
|
|
1506
|
+
* anything else is.
|
|
1507
|
+
*/
|
|
1508
|
+
export declare const refreshedAt: (refresh: RefreshState, requests: ReadonlyArray<Requirement>, connections?: ReadonlyArray<string>) => number;
|
|
1509
|
+
/**
|
|
1510
|
+
* Marks every field of `requests` and every connection in `connections`
|
|
1511
|
+
* refreshed at the next generation.
|
|
1512
|
+
*/
|
|
1513
|
+
export declare const withRefreshRequested: (refresh: RefreshState, requests: ReadonlyArray<Requirement>, connections?: ReadonlyArray<string>) => RefreshState;
|
|
1514
|
+
/**
|
|
1515
|
+
* Whether one field's read already began under the generation that field was
|
|
1516
|
+
* last refreshed at. Such a read is in flight and would otherwise outlive the
|
|
1517
|
+
* refresh, so the refresh takes a new generation to restart it even though the
|
|
1518
|
+
* field is already stale.
|
|
1519
|
+
*/
|
|
1520
|
+
export declare const refreshIsInFlight: (refresh: RefreshState, request: Requirement, field: string) => boolean;
|
|
1377
1521
|
/**
|
|
1378
1522
|
* Whether a read is fetching any of `fields` for this entity. Consulted only
|
|
1379
1523
|
* when the store lacks the value, which is what separates `Loading` from
|
|
@@ -1502,6 +1646,25 @@ export interface RelationShape {
|
|
|
1502
1646
|
readonly nullable: boolean;
|
|
1503
1647
|
readonly entity: string;
|
|
1504
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* A page of a relation that is a whole list is read under a name of its own, the
|
|
1651
|
+
* field's name and the page's size: `comments@first=10`. It is a field like any
|
|
1652
|
+
* other to the store, the planner, and the wire, so the whole list and a page of
|
|
1653
|
+
* it are held side by side, merged, marked stale, and persisted each on its own.
|
|
1654
|
+
* The server reads the name apart, reads the relation with the window, and
|
|
1655
|
+
* answers under the alias. A cursor is not part of the name: a page read from a
|
|
1656
|
+
* cursor merges onto the page it continues.
|
|
1657
|
+
*/
|
|
1658
|
+
export declare const RELATION_ALIAS = "@";
|
|
1659
|
+
/** The alias a relation field is read under, for a window of this size. */
|
|
1660
|
+
export declare const relationAlias: (field: string, window: {
|
|
1661
|
+
readonly first?: number | undefined;
|
|
1662
|
+
readonly last?: number | undefined;
|
|
1663
|
+
readonly after?: string | undefined;
|
|
1664
|
+
readonly before?: string | undefined;
|
|
1665
|
+
}) => string;
|
|
1666
|
+
/** The field an alias reads; a name that is no alias is its own field. */
|
|
1667
|
+
export declare const aliasedField: (name: string) => string;
|
|
1505
1668
|
/** Splits a ref key (`"Entity:id"`, the store key too) back into its parts. */
|
|
1506
1669
|
export declare const refParts: (encoded: string) => RefParts;
|
|
1507
1670
|
/**
|
|
@@ -1699,6 +1862,13 @@ export interface RemoteInspection {
|
|
|
1699
1862
|
readonly connections: ReadonlyArray<string>;
|
|
1700
1863
|
readonly live: ReadonlyArray<string>;
|
|
1701
1864
|
readonly gaps: ReadonlyArray<string>;
|
|
1865
|
+
/**
|
|
1866
|
+
* The `entity\0id\0field` marks of reads in flight. This is what "Remote is
|
|
1867
|
+
* working" means here, and it is read from the Model rather than from the
|
|
1868
|
+
* fibers doing the work: a tool that shows it shows something the Model can
|
|
1869
|
+
* be replayed to, and nothing that needs the runtime to be asked.
|
|
1870
|
+
*/
|
|
1871
|
+
readonly loading: ReadonlyArray<string>;
|
|
1702
1872
|
readonly mutations: {
|
|
1703
1873
|
readonly pending: ReadonlyArray<string>;
|
|
1704
1874
|
readonly failed: ReadonlyArray<string>;
|
|
@@ -1774,9 +1944,9 @@ export type EntityLike = EntityDescriptor<any, any> | Domain.AnyEntity;
|
|
|
1774
1944
|
* What a read selects: a Remote Selection, or a `foldkit-entity` Selection
|
|
1775
1945
|
* (read through `Selection.from`).
|
|
1776
1946
|
*/
|
|
1777
|
-
export type EntitySelection<Value, Name extends string> = Selection<Value, Name, 'entity'> | Domain.Selection<Name, unknown, Schema.Constraint & {
|
|
1947
|
+
export type EntitySelection<Value, Name extends string, Id extends string = string> = Selection<Value, Name, 'entity'> | Domain.Selection<Name, unknown, Schema.Constraint & {
|
|
1778
1948
|
readonly Type: Value;
|
|
1779
|
-
}>;
|
|
1949
|
+
}, Id>;
|
|
1780
1950
|
type EntityName<D> = D extends {
|
|
1781
1951
|
readonly name: infer Name extends string;
|
|
1782
1952
|
} ? Name : never;
|
|
@@ -1858,13 +2028,13 @@ export interface MutationStarted<AppModel> {
|
|
|
1858
2028
|
*/
|
|
1859
2029
|
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
2030
|
/** `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:
|
|
2031
|
+
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
2032
|
/**
|
|
1863
2033
|
* `get`, and the projection also subscribes to the entity's changes: its
|
|
1864
2034
|
* requirements are marked `live`, so `subscriptions` derives a live entry for
|
|
1865
2035
|
* the Surfaces that read it.
|
|
1866
2036
|
*/
|
|
1867
|
-
live<Value, Name extends string>(selection: EntitySelection<Value, Name> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id:
|
|
2037
|
+
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
2038
|
/**
|
|
1869
2039
|
* A query connection read as a `Page` of items selected of the query's
|
|
1870
2040
|
* entity: `Initial` until the page and every item's selected fields are
|
|
@@ -1879,6 +2049,15 @@ export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities exte
|
|
|
1879
2049
|
previous<Name extends string, Input>(model: AppModel, projection: QueryProjection<AppModel, any, Name, Input>): QueryRef<Name, Input> | undefined;
|
|
1880
2050
|
/** A Command that runs the query and yields the `ConnectionMerged` (or `QueryFailed`) that reduces it: "load more". */
|
|
1881
2051
|
fetch(ref: QueryRef<string, unknown>): Command<RemoteMessage, never, RemoteClient>;
|
|
2052
|
+
/**
|
|
2053
|
+
* Shows operations over the store with no request behind them, as a mutation's
|
|
2054
|
+
* `optimistic` ones show while it is in flight: every Selection and view draws
|
|
2055
|
+
* them. For a preview of a change nobody has made. They stay until `lift`;
|
|
2056
|
+
* showing an id again replaces what it showed. Called from `update`.
|
|
2057
|
+
*/
|
|
2058
|
+
overlay(model: AppModel, id: string, optimistic: ReadonlyArray<OptimisticOperation>): AppModel;
|
|
2059
|
+
/** Lifts what `overlay` showed under this id. Lifting nothing returns the same Model. */
|
|
2060
|
+
lift(model: AppModel, id: string): AppModel;
|
|
1882
2061
|
/** `Remote.refresh`: marks what a projection or a Surface requires as due, for its read entry to refetch. */
|
|
1883
2062
|
refresh(model: AppModel, target: Projection<AppModel, unknown> | Surface<AppModel, any, any, void>): AppModel;
|
|
1884
2063
|
/**
|
|
@@ -1894,6 +2073,30 @@ export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities exte
|
|
|
1894
2073
|
plan<Value>(model: AppModel, projection: Projection<AppModel, Value>, options?: PlanOptions): ReadonlyArray<Requirement>;
|
|
1895
2074
|
/** `Remote.storeOf`: the visible store, base under the pending optimistic layers. */
|
|
1896
2075
|
storeOf(model: AppModel): EntityStore;
|
|
2076
|
+
/**
|
|
2077
|
+
* `Remote.confirmed`: the same projection read against the server-derived
|
|
2078
|
+
* store alone, with every pending optimistic layer and connection overlay
|
|
2079
|
+
* left off. What it plans is unchanged — the requirements are the
|
|
2080
|
+
* projection's own — so observing it reads exactly what observing the
|
|
2081
|
+
* projection reads; only what it *shows* differs.
|
|
2082
|
+
*
|
|
2083
|
+
* A view usually wants the projection itself, which is the visible read:
|
|
2084
|
+
* the optimistic layers are there so a change shows before the server has
|
|
2085
|
+
* agreed to it. This is for the reader that must not believe a change until
|
|
2086
|
+
* the server has confirmed it, which in practice is an Agent capability
|
|
2087
|
+
* reporting that what it was asked to do is done:
|
|
2088
|
+
*
|
|
2089
|
+
* ```ts
|
|
2090
|
+
* Agent.when({
|
|
2091
|
+
* projection: Data.confirmed(ProjectPage.model.project),
|
|
2092
|
+
* predicate: (project, request) => project.name === request.name,
|
|
2093
|
+
* })
|
|
2094
|
+
* ```
|
|
2095
|
+
*
|
|
2096
|
+
* There is deliberately no `visible`: a projection is already the visible
|
|
2097
|
+
* read, and a wrapper that only forwards would be a second name for it.
|
|
2098
|
+
*/
|
|
2099
|
+
confirmed<P extends Projection<AppModel, any>>(projection: P): P;
|
|
1897
2100
|
/**
|
|
1898
2101
|
* The plan run through `RemoteClient` and reduced into the Model: the
|
|
1899
2102
|
* projection's pending queries first, then the fields it lacks (the pages'
|
|
@@ -1989,7 +2192,11 @@ declare const PlannedQuery: Schema.Struct<{
|
|
|
1989
2192
|
export interface ReadDependencies {
|
|
1990
2193
|
readonly requirements: ReadonlyArray<Requirement>;
|
|
1991
2194
|
readonly queries: ReadonlyArray<Schema.Schema.Type<typeof PlannedQuery>>;
|
|
1992
|
-
/**
|
|
2195
|
+
/**
|
|
2196
|
+
* The highest generation the fields this entry reads were refreshed at: a
|
|
2197
|
+
* refresh of any of them restarts the entry, and a refresh of anything else
|
|
2198
|
+
* leaves it running.
|
|
2199
|
+
*/
|
|
1993
2200
|
readonly refresh: number;
|
|
1994
2201
|
}
|
|
1995
2202
|
export declare const Remote: {
|
|
@@ -2065,6 +2272,13 @@ export declare const Remote: {
|
|
|
2065
2272
|
readonly message: Schema.String;
|
|
2066
2273
|
}>;
|
|
2067
2274
|
};
|
|
2275
|
+
OverlayShown: {
|
|
2276
|
+
id: Schema.String;
|
|
2277
|
+
optimistic: Schema.$Array<Schema.Unknown>;
|
|
2278
|
+
};
|
|
2279
|
+
OverlayLifted: {
|
|
2280
|
+
id: Schema.String;
|
|
2281
|
+
};
|
|
2068
2282
|
LiveReceived: {
|
|
2069
2283
|
stream: Schema.String;
|
|
2070
2284
|
event: Schema.Unknown;
|
|
@@ -2151,6 +2365,12 @@ export declare const Remote: {
|
|
|
2151
2365
|
* has not changed.
|
|
2152
2366
|
*/
|
|
2153
2367
|
storeOf: <AppModel, Store extends RemoteModel>(bound: BoundRemote<AppModel, Store>, model: AppModel) => EntityStore;
|
|
2368
|
+
/**
|
|
2369
|
+
* The projection read against the server-derived store alone: the same
|
|
2370
|
+
* requirements, planned the same way, with the pending optimistic layers and
|
|
2371
|
+
* connection overlays left off. See the bound `Remote.confirmed`.
|
|
2372
|
+
*/
|
|
2373
|
+
confirmed: <AppModel, Store extends RemoteModel, P extends Projection<AppModel, any>>(bound: BoundRemote<AppModel, Store>, projection: P) => P;
|
|
2154
2374
|
/**
|
|
2155
2375
|
* Executes the plan against the `RemoteClient` and returns a new store. Used
|
|
2156
2376
|
* for SSR route prefetch, hover prefetch, and tests. Never called during render.
|