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 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-remote'
148
+ import { Entity } from 'foldkit-entity'
149
149
 
150
- const Project = Entity.make(
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 = Project.select({
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. One relation of one entity is held whole or as
456
- one window at a time, so two views of the same entity should not read the
457
- same relation both ways at once.
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` and every loaded
525
- connection invalidated; the `Data.subscriptions` read entries then refetch it,
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. The read entries restart when `requested`
1164
- * moves, so a read sent before a refresh cannot land after it; `started` is
1165
- * the latest generation a read began under.
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: string): Projection<AppModel, RemoteData<Value>>;
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: string): Projection<AppModel, RemoteData<Value>>;
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
- /** The Model's requested refresh generation: a refresh restarts the entry. */
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.