foldkit-remote 0.7.0 → 0.9.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/dist/index.mjs CHANGED
@@ -1,6 +1,8 @@
1
- import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
1
+ import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaAST, SchemaGetter, Stream } from "effect";
2
2
  import * as Domain from "foldkit-entity";
3
- import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId, evaluate } from "foldkit-entity";
3
+ import { Entity as Entity$1, Expr, Query as Query$1, SchemaShape, SelectionTypeId, evaluate } from "foldkit-entity";
4
+ import { mapMessage } from "foldkit/command";
5
+ import { defineMessageUnion } from "foldkit/message";
4
6
  import * as Subscription from "foldkit/subscription";
5
7
  import { Metadata } from "foldkit-surface";
6
8
  import { Rpc, RpcGroup } from "effect/unstable/rpc";
@@ -108,7 +110,31 @@ const requirementsOf = (projection) => RemoteRequirements.get(projection.metadat
108
110
  /** The query connections a Projection carries. */
109
111
  const connectionsOf = (projection) => RemoteConnections.get(projection.metadata);
110
112
  //#endregion
113
+ //#region src/data.ts
114
+ /**
115
+ * Whether two values the server sent are the same data: equal primitives, or
116
+ * arrays and plain objects whose entries are. Anything else is compared by
117
+ * identity.
118
+ */
119
+ const sameData = (left, right) => {
120
+ if (Object.is(left, right)) return true;
121
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
122
+ if (Array.isArray(left)) return Array.isArray(right) && left.length === right.length && left.every((item, index) => sameData(item, right[index]));
123
+ if (!isPlain(left) || !isPlain(right)) return false;
124
+ const keys = Object.keys(left);
125
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameData(left[key], right[key]));
126
+ };
127
+ const isPlain = (value) => {
128
+ const prototype = Object.getPrototypeOf(value);
129
+ return prototype === Object.prototype || prototype === null;
130
+ };
131
+ //#endregion
111
132
  //#region src/relation.ts
133
+ /**
134
+ * Relation values on the wire and in the store: a ref key (`"Entity:id"`), a
135
+ * nullable ref key, an array of ref keys, or a page of ref keys. The planner
136
+ * follows them into the store; the server follows them into the next read.
137
+ */
112
138
  /** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
113
139
  const RelationAnnotation = "foldkitRemoteRelation";
114
140
  /** The target entity's name on the same codec, so a nested selection of another entity is refused. */
@@ -174,8 +200,8 @@ const shapeOf = (ast, nullable) => {
174
200
  nullable,
175
201
  entity: String(ast.annotations?.[RelationEntityAnnotation])
176
202
  };
177
- if (ast._tag === "Arrays") {
178
- const item = ast.rest?.[0];
203
+ if (SchemaAST.isArrays(ast)) {
204
+ const item = ast.rest[0];
179
205
  const inner = item === void 0 ? void 0 : shapeOf(item, false);
180
206
  return inner?.kind === "one" ? {
181
207
  kind: "many",
@@ -183,10 +209,9 @@ const shapeOf = (ast, nullable) => {
183
209
  entity: inner.entity
184
210
  } : void 0;
185
211
  }
186
- if (ast._tag === "Union") {
187
- const members = ast.types ?? [];
188
- const optional = members.some((member) => member._tag === "Null" || member._tag === "Undefined");
189
- for (const member of members) {
212
+ if (SchemaAST.isUnion(ast)) {
213
+ const optional = ast.types.some(SchemaShape.isNullish);
214
+ for (const member of ast.types) {
190
215
  const shape = shapeOf(member, optional);
191
216
  if (shape !== void 0) return shape;
192
217
  }
@@ -199,8 +224,8 @@ const shapeOf = (ast, nullable) => {
199
224
  *
200
225
  * Presence is tracked **separately** from values, so `undefined`/`null`/absent/
201
226
  * stale/not-found are distinct states and a missing field is never inferred from
202
- * `value === undefined`. Nothing here performs I/O; every operation returns a
203
- * new store.
227
+ * `value === undefined`. Nothing here performs I/O or mutates its input; an
228
+ * operation that changes nothing returns the store it was given.
204
229
  */
205
230
  /** The separator keeps an untrusted id from colliding with a prototype key. */
206
231
  const entityKey = (entity, id) => `${entity}:${id}`;
@@ -208,6 +233,7 @@ const emptyEntry = {
208
233
  values: {},
209
234
  present: /* @__PURE__ */ new Set(),
210
235
  stale: /* @__PURE__ */ new Set(),
236
+ unavailable: /* @__PURE__ */ new Set(),
211
237
  tombstone: false,
212
238
  updatedAt: 0,
213
239
  windows: {}
@@ -220,10 +246,12 @@ const replace = (store, key, entry) => ({
220
246
  const written = (previous, values, now, windows) => {
221
247
  const present = new Set(previous.present);
222
248
  const stale = new Set(previous.stale);
249
+ const unavailable = new Set(previous.unavailable);
223
250
  const nextWindows = { ...previous.windows };
224
251
  for (const field of Object.keys(values)) {
225
252
  present.add(field);
226
253
  stale.delete(field);
254
+ unavailable.delete(field);
227
255
  const requested = windows?.[field];
228
256
  if (requested !== void 0 && requested !== "") nextWindows[field] = requested;
229
257
  else delete nextWindows[field];
@@ -232,12 +260,13 @@ const written = (previous, values, now, windows) => {
232
260
  }
233
261
  }
234
262
  return {
235
- values: {
263
+ values: Object.keys(values).every((field) => Object.hasOwn(previous.values, field) && sameData(previous.values[field], values[field])) ? previous.values : {
236
264
  ...previous.values,
237
265
  ...values
238
266
  },
239
267
  present,
240
268
  stale,
269
+ unavailable,
241
270
  tombstone: false,
242
271
  updatedAt: now,
243
272
  windows: nextWindows
@@ -251,7 +280,7 @@ const written = (previous, values, now, windows) => {
251
280
  const writeEntities = (store, writes, now = 0) => {
252
281
  if (writes.length === 0) return store;
253
282
  const next = { ...store };
254
- for (const write of writes) next[write.key] = written(next[write.key] ?? emptyEntry, write.values, now, write.windows);
283
+ for (const write of writes) next[write.key] = written(next[write.key] ?? emptyEntry, write.values, write.updatedAt ?? now, write.windows);
255
284
  return next;
256
285
  };
257
286
  /** `writeEntities` for one entity. */
@@ -287,11 +316,40 @@ const setStale = (store, marks, stale) => {
287
316
  const markStale = (store, key, fields) => setStale(store, [[key, fields]], true);
288
317
  /** Ends a refresh that did not land: the fields are present again as they were. */
289
318
  const clearStale = (store, key, fields) => setStale(store, [[key, fields]], false);
319
+ /**
320
+ * Records fields the server settled without a value. A present field is left
321
+ * as it is: the answer that settled it was not about the value it holds.
322
+ * `false` forgets the marks, which is how a refresh asks again.
323
+ */
324
+ const setUnavailable = (store, marks, unavailable) => {
325
+ let next;
326
+ for (const [key, fields] of marks) {
327
+ const previous = (next ?? store)[key] ?? emptyEntry;
328
+ if (previous.tombstone) continue;
329
+ const marked = new Set(previous.unavailable);
330
+ for (const field of fields) if (unavailable) {
331
+ if (!previous.present.has(field)) marked.add(field);
332
+ } else marked.delete(field);
333
+ if (marked.size === previous.unavailable.size) continue;
334
+ next ??= { ...store };
335
+ next[key] = {
336
+ ...previous,
337
+ unavailable: marked
338
+ };
339
+ }
340
+ return next ?? store;
341
+ };
342
+ /** Whether the server settled this field without a value, and nothing has written it since. */
343
+ const isFieldUnavailable = (store, key, field) => {
344
+ const value = store[key];
345
+ return value !== void 0 && !value.tombstone && value.unavailable.has(field);
346
+ };
290
347
  /** Records that the entity is known to be absent, so it is not refetched. */
291
- const tombstone = (store, key) => replace(store, key, {
348
+ const tombstone = (store, key) => isTombstone(store, key) ? store : replace(store, key, {
292
349
  values: {},
293
350
  present: /* @__PURE__ */ new Set(),
294
351
  stale: /* @__PURE__ */ new Set(),
352
+ unavailable: /* @__PURE__ */ new Set(),
295
353
  tombstone: true,
296
354
  updatedAt: 0,
297
355
  windows: {}
@@ -329,7 +387,8 @@ const readField = (store, key, field) => {
329
387
  };
330
388
  /**
331
389
  * The fields the planner must fetch. A tombstone makes every field known
332
- * (absent), so it returns an empty list and the entity is not refetched. A
390
+ * (absent), so it returns an empty list and the entity is not refetched, and a
391
+ * field the server settled without a value is known the same way. A
333
392
  * requested window that differs from the one a field was fetched with also
334
393
  * marks it missing, but only when the stored window is known (never `""`), so a
335
394
  * writer that does not record windows cannot cause a refetch loop.
@@ -338,7 +397,9 @@ const missingFields = (store, key, fields, windows) => {
338
397
  const value = store[key];
339
398
  if (value !== void 0 && value.tombstone) return [];
340
399
  return [...fields].filter((field) => {
341
- if (value === void 0 || !value.present.has(field) || value.stale.has(field)) return true;
400
+ if (value === void 0) return true;
401
+ if (!value.present.has(field)) return !value.unavailable.has(field);
402
+ if (value.stale.has(field)) return true;
342
403
  const requested = windows?.[field];
343
404
  const stored = value.windows[field];
344
405
  return requested !== void 0 && stored !== void 0 && stored !== "" && stored !== requested;
@@ -407,8 +468,11 @@ const mutationStatus = (state, requestId) => {
407
468
  * with the same `requestId` (a retry, or a live event describing the same change)
408
469
  * does not re-apply the entities, but still clears `pending`.
409
470
  */
410
- const reconcileMutation = (store, state, requestId, entities, deleted = []) => {
411
- const next = state.applied.has(requestId) ? store : deleted.reduce((current, gone) => tombstone(current, entityKey(gone.entity, gone.id)), entities.reduce((current, entity) => writeEntity(current, entityKey(entity.entity, entity.id), entity.values), store));
471
+ const reconcileMutation = (store, state, requestId, answer) => {
472
+ const next = state.applied.has(requestId) ? store : (answer.deleted ?? []).reduce((current, gone) => tombstone(current, entityKey(gone.entity, gone.id)), writeEntities(store, answer.entities.map((entity) => ({
473
+ key: entityKey(entity.entity, entity.id),
474
+ values: entity.values
475
+ })), answer.now));
412
476
  const applied = remember(state.applied, requestId);
413
477
  const pending = new Set(state.pending);
414
478
  pending.delete(requestId);
@@ -588,7 +652,7 @@ const Query = {
588
652
  * server refuses a mismatch with `RemoteProtocolError`, so a shape change never
589
653
  * drifts silently: bump it whenever `ReadRequest` or `LiveRequirement` change.
590
654
  */
591
- const REMOTE_PROTOCOL_VERSION = 3;
655
+ const REMOTE_PROTOCOL_VERSION = 4;
592
656
  var RemoteReadError = class extends Schema.TaggedError()("RemoteReadError", { message: Schema.String }) {};
593
657
  var RemoteMutationError = class extends Schema.TaggedError()("RemoteMutationError", { message: Schema.String }) {};
594
658
  var RemoteLiveError = class extends Schema.TaggedError()("RemoteLiveError", { message: Schema.String }) {};
@@ -649,7 +713,20 @@ const NormalizedEntity = Schema.Struct({
649
713
  id: Schema.String,
650
714
  values: Schema.Record(Schema.String, Schema.Unknown)
651
715
  });
652
- const ReadBatchResult = Schema.Struct({ entities: Schema.Array(NormalizedEntity) });
716
+ /**
717
+ * Fields a read asked for that the server answered without, and will not
718
+ * answer with: the client stops asking. It says nothing about why, so a rule
719
+ * that withholds a field is not itself on the wire.
720
+ */
721
+ const SettledFields = Schema.Struct({
722
+ entity: Schema.String,
723
+ id: Schema.String,
724
+ fields: Schema.Array(Schema.String)
725
+ });
726
+ const ReadBatchResult = Schema.Struct({
727
+ entities: Schema.Array(NormalizedEntity),
728
+ settled: Schema.Array(SettledFields)
729
+ });
653
730
  const MutationRequest = Schema.Struct({
654
731
  /** Stable across transport retries so a mutation is not applied twice. */
655
732
  requestId: Schema.String,
@@ -755,7 +832,13 @@ const WireBoundary = Schema.Union([
755
832
  const QueryRequest = Schema.Struct({
756
833
  query: Schema.String,
757
834
  input: Schema.Unknown,
758
- window: WindowSchema
835
+ window: WindowSchema,
836
+ /**
837
+ * What to read of each item, as the relation slice a read asks the server
838
+ * for. Absent for a query run without one: the response carries only edges,
839
+ * and the client's read entry fetches fields in a second round trip.
840
+ */
841
+ select: Schema.optional(RelationRequest)
759
842
  });
760
843
  const QueryEdge = Schema.Struct({
761
844
  entity: Schema.String,
@@ -765,7 +848,14 @@ const QueryEdge = Schema.Struct({
765
848
  const QueryResult = Schema.Struct({
766
849
  edges: Schema.Array(QueryEdge),
767
850
  start: WireBoundary,
768
- end: WireBoundary
851
+ end: WireBoundary,
852
+ /**
853
+ * The selected fields of the page's items, in the same shape a read batch
854
+ * returns. Absent from a server that predates payload planning: the client
855
+ * reads it as empty and fetches fields as before.
856
+ */
857
+ entities: Schema.optional(Schema.Array(NormalizedEntity)),
858
+ settled: Schema.optional(Schema.Array(SettledFields))
769
859
  });
770
860
  const QueryRpc = Rpc.make("FoldkitRemoteQuery", {
771
861
  payload: QueryRequest,
@@ -833,7 +923,8 @@ const coalesceQueries = (query, options = {}) => Effect.gen(function* () {
833
923
  key: stableStringify([
834
924
  request.query,
835
925
  request.input,
836
- request.window
926
+ request.window,
927
+ request.select ?? null
837
928
  ]),
838
929
  request
839
930
  }), resolver);
@@ -867,7 +958,7 @@ const coalesceReads = (read, options = {}) => Effect.gen(function* () {
867
958
  for (const entry of entries) if (own.has(entry.request.key)) owned.set(entry.request.key, entry.request.requirement);
868
959
  yield* Effect.forEach(readsOf(owned), ({ keys, requests }) => Effect.gen(function* () {
869
960
  const exit = yield* Effect.exit(read({
870
- version: 3,
961
+ version: 4,
871
962
  requests
872
963
  }));
873
964
  for (const key of keys) {
@@ -883,10 +974,19 @@ const coalesceReads = (read, options = {}) => Effect.gen(function* () {
883
974
  });
884
975
  let resolver = RequestResolver.make(runAll);
885
976
  if (options.window !== void 0) resolver = RequestResolver.setDelay(resolver, Duration.fromInputUnsafe(options.window));
886
- return (batch) => batch.requests.length === 0 ? Effect.succeed({ entities: [] }) : Effect.forEach(batch.requests, (requirement) => Effect.request(new ReadRequirement({
977
+ return (batch) => batch.requests.length === 0 ? Effect.succeed({
978
+ entities: [],
979
+ settled: []
980
+ }) : Effect.forEach(batch.requests, (requirement) => Effect.request(new ReadRequirement({
887
981
  key: requirementKey(requirement),
888
982
  requirement
889
- }), resolver), { concurrency: "unbounded" }).pipe(Effect.map((results) => ({ entities: [...new Set(results)].flatMap((result) => result.entities) })));
983
+ }), resolver), { concurrency: "unbounded" }).pipe(Effect.map((results) => {
984
+ const distinct = [...new Set(results)];
985
+ return {
986
+ entities: distinct.flatMap((result) => result.entities),
987
+ settled: distinct.flatMap((result) => result.settled)
988
+ };
989
+ }));
890
990
  });
891
991
  //#endregion
892
992
  //#region src/client.ts
@@ -1302,6 +1402,24 @@ const assemble = (store, key, requirement) => {
1302
1402
  refreshing
1303
1403
  };
1304
1404
  };
1405
+ /**
1406
+ * The first field `requirement` reads, at any depth, that the server settled
1407
+ * without a value, as `Entity:id.field`. A read that consults it says why it
1408
+ * cannot assemble rather than waiting on a field nothing will fetch.
1409
+ */
1410
+ const unavailableOf = (store, key, requirement) => {
1411
+ for (const field of requirement.fields) {
1412
+ if (isFieldUnavailable(store, key, field)) return `${key}.${field}`;
1413
+ const relation = requirement.relations?.[field];
1414
+ if (relation === void 0) continue;
1415
+ const value = readField(store, key, field);
1416
+ if (Option.isNone(value) || value.value === null || value.value === void 0) continue;
1417
+ for (const ref of targetsOf(value.value, relation)) {
1418
+ const found = unavailableOf(store, entityKey(ref.entity, ref.id), relation);
1419
+ if (found !== void 0) return found;
1420
+ }
1421
+ }
1422
+ };
1305
1423
  const assembleTarget = (store, ref, relation) => {
1306
1424
  const key = entityKey(ref.entity, ref.id);
1307
1425
  return isTombstone(store, key) ? "absent" : assemble(store, key, relation);
@@ -1545,6 +1663,7 @@ const inspectEntry = (key, entry) => ({
1545
1663
  key,
1546
1664
  present: [...entry.present],
1547
1665
  stale: [...entry.stale],
1666
+ unavailable: [...entry.unavailable],
1548
1667
  tombstone: entry.tombstone,
1549
1668
  updatedAt: entry.updatedAt,
1550
1669
  windows: { ...entry.windows }
@@ -1615,12 +1734,13 @@ const ConnectionChange = {
1615
1734
  const beginOptimistic = (optimistic, requestId, operations) => {
1616
1735
  const patches = operations.filter((operation) => !isConnectionChange(operation));
1617
1736
  const changes = operations.filter(isConnectionChange);
1737
+ if (patches.length === 0 && changes.length === 0) return optimistic;
1618
1738
  return {
1619
1739
  layers: patches.length === 0 ? optimistic.layers : [...optimistic.layers, {
1620
1740
  id: requestId,
1621
1741
  patches
1622
1742
  }],
1623
- overlays: [...optimistic.overlays, ...toOverlays(requestId, changes)]
1743
+ overlays: changes.length === 0 ? optimistic.overlays : [...optimistic.overlays, ...toOverlays(requestId, changes)]
1624
1744
  };
1625
1745
  };
1626
1746
  /**
@@ -1629,8 +1749,9 @@ const beginOptimistic = (optimistic, requestId, operations) => {
1629
1749
  * its place among other pending inserts.
1630
1750
  */
1631
1751
  const confirm = (optimistic, requestId, id, changes) => {
1632
- const confirmed = toOverlays(id, changes);
1633
1752
  const at = optimistic.overlays.findIndex((overlay) => overlay.id === requestId);
1753
+ if (at === -1 && changes.length === 0) return optimistic;
1754
+ const confirmed = toOverlays(id, changes);
1634
1755
  const others = optimistic.overlays.filter((overlay) => overlay.id !== requestId);
1635
1756
  return {
1636
1757
  ...optimistic,
@@ -1649,37 +1770,62 @@ const addLayer = (optimistic, layer) => ({
1649
1770
  ...optimistic,
1650
1771
  layers: [...optimistic.layers, layer]
1651
1772
  });
1652
- const removeLayer = (optimistic, id) => ({
1653
- ...optimistic,
1654
- layers: optimistic.layers.filter((layer) => layer.id !== id)
1655
- });
1773
+ const removeLayer = (optimistic, id) => {
1774
+ const layers = optimistic.layers.filter((layer) => layer.id !== id);
1775
+ return layers.length === optimistic.layers.length ? optimistic : {
1776
+ ...optimistic,
1777
+ layers
1778
+ };
1779
+ };
1656
1780
  const addOverlay = (optimistic, overlay) => ({
1657
1781
  ...optimistic,
1658
1782
  overlays: [...optimistic.overlays, overlay]
1659
1783
  });
1660
- const removeOverlay = (optimistic, id) => ({
1661
- ...optimistic,
1662
- overlays: optimistic.overlays.filter((overlay) => overlay.id !== id)
1663
- });
1664
- /** Base store with every layer applied in order. Later layers win. */
1665
- const visibleStore = (base, optimistic) => optimistic.layers.reduce((store, layer) => layer.patches.reduce((current, patch) => writeEntity(current, entityKey(patch.entity, patch.id), patch.values), store), base);
1784
+ const removeOverlay = (optimistic, id) => {
1785
+ const overlays = optimistic.overlays.filter((overlay) => overlay.id !== id);
1786
+ return overlays.length === optimistic.overlays.length ? optimistic : {
1787
+ ...optimistic,
1788
+ overlays
1789
+ };
1790
+ };
1791
+ /**
1792
+ * Base store with every layer applied in order. Later layers win. A patch is
1793
+ * not news from the server, so it keeps the date of the value under it; an
1794
+ * entity only a request holds (a temporary id) is not the server's to age, and
1795
+ * is dated `Infinity`, so freshness never plans or times it.
1796
+ */
1797
+ const visibleStore = (base, optimistic) => writeEntities(base, optimistic.layers.flatMap((layer) => layer.patches.map((patch) => {
1798
+ const key = entityKey(patch.entity, patch.id);
1799
+ const under = base[key];
1800
+ return {
1801
+ key,
1802
+ values: patch.values,
1803
+ updatedAt: under === void 0 || under.tombstone ? Infinity : under.updatedAt
1804
+ };
1805
+ })));
1666
1806
  /**
1667
1807
  * A fresh page is newer than any settled overlay it covers: an edge the page
1668
1808
  * carries no longer needs an insert overlay, and a `remove` overlay hiding it
1669
1809
  * is stale (the server says it is back). A pending request's overlays are
1670
1810
  * left alone; they settle with the request.
1671
1811
  */
1672
- const pruneOverlays = (optimistic, connection, covered, pending) => ({
1673
- ...optimistic,
1674
- overlays: optimistic.overlays.flatMap((overlay) => {
1812
+ const pruneOverlays = (optimistic, connection, covered, pending) => {
1813
+ let changed = false;
1814
+ const overlays = optimistic.overlays.flatMap((overlay) => {
1675
1815
  if (overlay.connection !== connection || pending.has(overlay.id)) return [overlay];
1676
1816
  const edges = overlay.edges.filter((edge) => !covered.has(edge.key));
1817
+ if (edges.length > 0 && edges.length === overlay.edges.length) return [overlay];
1818
+ changed = true;
1677
1819
  return edges.length === 0 ? [] : [{
1678
1820
  ...overlay,
1679
1821
  edges
1680
1822
  }];
1681
- })
1682
- });
1823
+ });
1824
+ return changed ? {
1825
+ ...optimistic,
1826
+ overlays
1827
+ } : optimistic;
1828
+ };
1683
1829
  /** Everything a request owns: its layer and its overlays. */
1684
1830
  const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic, requestId), requestId);
1685
1831
  /**
@@ -1687,12 +1833,12 @@ const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic,
1687
1833
  * a later layer re-wins on rebase. Confirmed connection changes are recorded
1688
1834
  * once per request, as overlays the request no longer owns.
1689
1835
  */
1690
- const settleSuccess = (base, optimistic, state, requestId, entities, connections = [], deleted = []) => {
1691
- const reconciled = reconcileMutation(base, state, requestId, entities, deleted);
1836
+ const settleSuccess = (base, optimistic, state, requestId, answer) => {
1837
+ const reconciled = reconcileMutation(base, state, requestId, answer);
1692
1838
  return {
1693
1839
  store: reconciled.store,
1694
1840
  state: reconciled.state,
1695
- optimistic: state.applied.has(requestId) ? release(optimistic, requestId) : confirm(removeLayer(optimistic, requestId), requestId, `confirmed:${requestId}`, connections)
1841
+ optimistic: state.applied.has(requestId) ? release(optimistic, requestId) : confirm(removeLayer(optimistic, requestId), requestId, `confirmed:${requestId}`, answer.connections ?? [])
1696
1842
  };
1697
1843
  };
1698
1844
  /** A failed request's layer and overlays are dropped; the base was never mutated. */
@@ -1767,11 +1913,6 @@ const recordBoundary = (state, connection, position) => {
1767
1913
  }
1768
1914
  };
1769
1915
  };
1770
- /**
1771
- * A subscriber is woken only if the event changed a field it selects. A
1772
- * change to an unselected field is skipped entirely.
1773
- */
1774
- const shouldWake = (changed, selected) => changed.some((field) => selected.includes(field));
1775
1916
  const applyEntityEvent = (state, store, event, now = 0) => {
1776
1917
  const outcome = classifyLive(state, event.cursor);
1777
1918
  if (outcome !== "applied") return {
@@ -1789,9 +1930,13 @@ const applyEntityEvent = (state, store, event, now = 0) => {
1789
1930
  };
1790
1931
  const removeEdgeOverlays = (optimistic, connection, key) => ({
1791
1932
  ...optimistic,
1792
- overlays: optimistic.overlays.map((overlay) => overlay.connection !== connection || overlay.position === "remove" ? overlay : {
1793
- ...overlay,
1794
- edges: overlay.edges.filter((edge) => edge.key !== key)
1933
+ overlays: optimistic.overlays.map((overlay) => {
1934
+ if (overlay.connection !== connection || overlay.position === "remove") return overlay;
1935
+ const edges = overlay.edges.filter((edge) => edge.key !== key);
1936
+ return edges.length === overlay.edges.length ? overlay : {
1937
+ ...overlay,
1938
+ edges
1939
+ };
1795
1940
  })
1796
1941
  });
1797
1942
  const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
@@ -1878,6 +2023,44 @@ const followRelation = (value, relation) => targetsOf(value, relation).map((ref)
1878
2023
  ...relation,
1879
2024
  id: ref.id
1880
2025
  }));
2026
+ /**
2027
+ * The earliest moment a value `requirements` read, following the relations the
2028
+ * store holds as `plan` does, goes stale under `freshness`, with the
2029
+ * requirements of the entities that go stale then. A field the server settled
2030
+ * without a value ages with its entity too, since `plan` asks for it again once
2031
+ * the entity expires. Nothing when no such field is still fresh: what is
2032
+ * already expired or stale is planned, not awaited, which is what keeps a
2033
+ * timer from firing on what it already fired on. It is the next moment the
2034
+ * plan can change with the clock alone, which the read entry relies on to
2035
+ * reuse its plan until then.
2036
+ */
2037
+ const deadlineOf = (store, requirements, freshness) => {
2038
+ let at;
2039
+ let due = [];
2040
+ const walk = (group) => {
2041
+ const key = entityKey(group.entity, group.id);
2042
+ const entry = store[key];
2043
+ if (entry === void 0 || entry.tombstone) return;
2044
+ if (group.fields.some((field) => entry.present.has(field) && !entry.stale.has(field) || entry.unavailable.has(field))) {
2045
+ const expires = entry.updatedAt + freshness.freshness;
2046
+ if (expires >= freshness.now && Number.isFinite(expires)) {
2047
+ if (at === void 0 || expires < at) {
2048
+ at = expires;
2049
+ due = [group];
2050
+ } else if (expires === at) due.push(group);
2051
+ }
2052
+ }
2053
+ for (const [field, relation] of Object.entries(group.relations ?? {})) {
2054
+ const value = readField(store, key, field);
2055
+ if (value._tag === "Some") for (const next of followRelation(value.value, relation)) walk(next);
2056
+ }
2057
+ };
2058
+ for (const group of Requirement.merge(requirements)) walk(group);
2059
+ return at === void 0 ? void 0 : {
2060
+ at,
2061
+ due: Requirement.merge(due)
2062
+ };
2063
+ };
1881
2064
  const plan = (store, requirements, options = {}) => {
1882
2065
  const { freshness, force = false } = options;
1883
2066
  const planned = [];
@@ -1962,17 +2145,25 @@ const reachable = (store, roots, connections, optimistic, pending) => {
1962
2145
  const gc = (state, roots) => {
1963
2146
  const pending = state.mutations.pending;
1964
2147
  const kept = reachable(state.entities, roots, state.connections, state.optimistic, pending);
1965
- const entities = Object.fromEntries(Object.entries(state.entities).filter(([key]) => kept.has(key)));
2148
+ const entities = keptOf(state.entities, (key) => kept.has(key));
1966
2149
  const keptConnections = /* @__PURE__ */ new Set([...roots.connections.map((root) => root.identity), ...state.optimistic.overlays.filter((overlay) => pending.has(overlay.id)).map((overlay) => overlay.connection)]);
2150
+ const connections = keptOf(state.connections, (identity) => keptConnections.has(identity));
2151
+ const overlays = state.optimistic.overlays.filter((overlay) => pending.has(overlay.id) || keptConnections.has(overlay.connection));
1967
2152
  return {
1968
2153
  entities,
1969
- connections: Object.fromEntries(Object.entries(state.connections).filter(([identity]) => keptConnections.has(identity))),
1970
- optimistic: {
2154
+ connections,
2155
+ optimistic: overlays.length === state.optimistic.overlays.length ? state.optimistic : {
1971
2156
  layers: state.optimistic.layers,
1972
- overlays: state.optimistic.overlays.filter((overlay) => pending.has(overlay.id) || keptConnections.has(overlay.connection))
2157
+ overlays
1973
2158
  }
1974
2159
  };
1975
2160
  };
2161
+ /** The entries whose key is kept; `record` itself when that is all of them. */
2162
+ const keptOf = (record, keep) => {
2163
+ const entries = Object.entries(record);
2164
+ const kept = entries.filter(([key]) => keep(key));
2165
+ return kept.length === entries.length ? record : Object.fromEntries(kept);
2166
+ };
1976
2167
  //#endregion
1977
2168
  //#region src/persistence.ts
1978
2169
  /**
@@ -1989,7 +2180,7 @@ const gc = (state, roots) => {
1989
2180
  * application's choice.
1990
2181
  */
1991
2182
  /** Bump when the serialized shape changes; a mismatch discards the cache. */
1992
- const REMOTE_CACHE_VERSION = 4;
2183
+ const REMOTE_CACHE_VERSION = 5;
1993
2184
  /**
1994
2185
  * A snapshot of a Model, keeping the entity cache and only the connections
1995
2186
  * named.
@@ -2012,13 +2203,14 @@ const sorted = (values) => [...values].sort();
2012
2203
  * whatever order they were built in.
2013
2204
  */
2014
2205
  const serializeStore = (snapshot, scope) => ({
2015
- version: 4,
2206
+ version: 5,
2016
2207
  scope: scope ?? null,
2017
2208
  connections: snapshot.connections,
2018
2209
  entities: Object.fromEntries(Object.entries(snapshot.entities).map(([key, entry]) => [key, {
2019
2210
  values: entry.values,
2020
2211
  present: sorted(entry.present),
2021
2212
  stale: sorted(entry.stale),
2213
+ unavailable: sorted(entry.unavailable),
2022
2214
  tombstone: entry.tombstone,
2023
2215
  updatedAt: entry.updatedAt,
2024
2216
  windows: entry.windows
@@ -2033,6 +2225,7 @@ const parseEntry = (value) => {
2033
2225
  if (entry.values === null || typeof entry.values !== "object" || Array.isArray(entry.values)) throw new Error("entry.values is not a record");
2034
2226
  if (!isStringArray(entry.present)) throw new Error("entry.present is not a string array");
2035
2227
  if (!isStringArray(entry.stale)) throw new Error("entry.stale is not a string array");
2228
+ if (!isStringArray(entry.unavailable)) throw new Error("entry.unavailable is not a string array");
2036
2229
  if (typeof entry.tombstone !== "boolean") throw new Error("entry.tombstone is not a boolean");
2037
2230
  if (typeof entry.updatedAt !== "number") throw new Error("entry.updatedAt is not a number");
2038
2231
  if (!isStringRecord(entry.windows)) throw new Error("entry.windows is not a string record");
@@ -2040,6 +2233,7 @@ const parseEntry = (value) => {
2040
2233
  values: entry.values,
2041
2234
  present: new Set(entry.present),
2042
2235
  stale: new Set(entry.stale),
2236
+ unavailable: new Set(entry.unavailable),
2043
2237
  tombstone: entry.tombstone,
2044
2238
  updatedAt: entry.updatedAt,
2045
2239
  windows: entry.windows
@@ -2098,7 +2292,7 @@ const hydrate = (raw, options = {}) => {
2098
2292
  }
2099
2293
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
2100
2294
  const candidate = parsed;
2101
- if (candidate.version !== 4) return void 0;
2295
+ if (candidate.version !== 5) return void 0;
2102
2296
  if ((candidate.scope ?? null) !== (options.scope ?? null)) return void 0;
2103
2297
  if (candidate.entities === null || typeof candidate.entities !== "object") return void 0;
2104
2298
  if (Array.isArray(candidate.entities)) return void 0;
@@ -2114,7 +2308,7 @@ const hydrate = (raw, options = {}) => {
2114
2308
  * an entry the current store already has (it is at least as fresh as a
2115
2309
  * snapshot taken earlier). Either way, keys only the current store has stay.
2116
2310
  */
2117
- const mergeStores = (current, snapshot, policy) => policy === "replace" ? {
2311
+ const mergeStores = (current, snapshot, policy) => Object.keys(snapshot).length === 0 ? current : policy === "replace" ? {
2118
2312
  ...current,
2119
2313
  ...snapshot
2120
2314
  } : {
@@ -2275,7 +2469,9 @@ const RemoteData = {
2275
2469
  error: data.error,
2276
2470
  previous: f(data.previous)
2277
2471
  };
2278
- default: return data;
2472
+ case "Initial":
2473
+ case "Loading":
2474
+ case "NotFound": return data;
2279
2475
  }
2280
2476
  }
2281
2477
  };
@@ -2293,11 +2489,37 @@ const noFailures = {
2293
2489
  const emptyRefresh = {
2294
2490
  generation: 0,
2295
2491
  requested: /* @__PURE__ */ new Map(),
2296
- started: /* @__PURE__ */ new Map()
2492
+ started: /* @__PURE__ */ new Map(),
2493
+ floor: 0
2494
+ };
2495
+ /**
2496
+ * The Model with everything the server told it forgotten: values, tombstones,
2497
+ * unavailable fields, connections, live cursors, failures, and what is in
2498
+ * flight. For a change of principal, whose facts these were. The mutation
2499
+ * sequence is kept, so request ids stay unique, and a mutation in flight is
2500
+ * treated as applied: its answer, when it comes, writes nothing here.
2501
+ */
2502
+ const forgetRemote = (model) => {
2503
+ const generation = model.refresh.generation + 1;
2504
+ return {
2505
+ ...initialRemoteModel,
2506
+ refresh: {
2507
+ generation,
2508
+ requested: /* @__PURE__ */ new Map(),
2509
+ started: /* @__PURE__ */ new Map(),
2510
+ floor: generation
2511
+ },
2512
+ mutations: {
2513
+ ...emptyMutationState,
2514
+ sequence: model.mutations.sequence,
2515
+ applied: /* @__PURE__ */ new Set([...model.mutations.applied, ...model.mutations.pending])
2516
+ }
2517
+ };
2297
2518
  };
2298
2519
  const initialRemoteModel = {
2299
2520
  entities: emptyStore,
2300
2521
  connections: {},
2522
+ grown: {},
2301
2523
  optimistic: emptyOptimistic,
2302
2524
  live: {},
2303
2525
  mutations: emptyMutationState,
@@ -2311,6 +2533,7 @@ const runtimeSchema = Schema.Unknown;
2311
2533
  const remoteModelSchema = () => Schema.Struct({
2312
2534
  entities: runtimeSchema,
2313
2535
  connections: Schema.Record(Schema.String, runtimeSchema),
2536
+ grown: Schema.Record(Schema.String, Schema.Number),
2314
2537
  optimistic: runtimeSchema,
2315
2538
  live: Schema.Record(Schema.String, runtimeSchema),
2316
2539
  mutations: runtimeSchema,
@@ -2367,7 +2590,8 @@ const remoteMessageCases = {
2367
2590
  deleted: Schema.optional(Schema.Array(Schema.Struct({
2368
2591
  entity: Schema.String,
2369
2592
  id: Schema.String
2370
- })))
2593
+ }))),
2594
+ now: Schema.Number
2371
2595
  },
2372
2596
  MutationFailed: {
2373
2597
  requestId: Schema.String,
@@ -2378,6 +2602,10 @@ const remoteMessageCases = {
2378
2602
  optimistic: Schema.Array(Schema.Unknown)
2379
2603
  },
2380
2604
  OverlayLifted: { id: Schema.String },
2605
+ WindowGrown: {
2606
+ window: Schema.String,
2607
+ size: Schema.Number
2608
+ },
2381
2609
  LiveReceived: {
2382
2610
  stream: Schema.String,
2383
2611
  event: Schema.Unknown,
@@ -2388,7 +2616,11 @@ const remoteMessageCases = {
2388
2616
  ConnectionMerged: {
2389
2617
  connection: Schema.String,
2390
2618
  page: Schema.Unknown,
2391
- refreshes: Schema.optional(Schema.Boolean)
2619
+ refreshes: Schema.optional(Schema.Boolean),
2620
+ select: Schema.optional(RelationRequest),
2621
+ entities: Schema.optional(Schema.Array(NormalizedEntity)),
2622
+ settled: Schema.optional(Schema.Array(SettledFields)),
2623
+ now: Schema.optional(Schema.Number)
2392
2624
  },
2393
2625
  ConnectionInvalidated: { connection: Schema.String },
2394
2626
  ConnectionRefreshed: { connection: Schema.String },
@@ -2408,6 +2640,18 @@ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(
2408
2640
  })));
2409
2641
  /** An overlay's layer, named apart from every request's: a request id is the application's to choose too. */
2410
2642
  const overlayLayer = (id) => `overlay:${id}`;
2643
+ /** The key a grown window is kept under: its connection and the window first asked for. */
2644
+ const windowGrowthKey = (identity, window) => `${identity}\u0000${window}`;
2645
+ /**
2646
+ * Whether only overlays show this entity: the server has told the store
2647
+ * nothing of it and no request's layer writes it, so nothing will fetch what an
2648
+ * overlay leaves out.
2649
+ */
2650
+ const onlyOverlaid = (model, entity, id) => {
2651
+ if (Object.hasOwn(model.entities, entityKey(entity, id))) return false;
2652
+ const writers = model.optimistic.layers.filter((layer) => layer.patches.some((patch) => patch.entity === entity && patch.id === id));
2653
+ return writers.length > 0 && writers.every((layer) => layer.id.startsWith("overlay:"));
2654
+ };
2411
2655
  const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
2412
2656
  /**
2413
2657
  * One mark per requested field, `entity\0id\0field`. A read is identified by
@@ -2423,13 +2667,13 @@ const fieldMarks = function* (requests) {
2423
2667
  const withLoading = (loading, requests) => {
2424
2668
  const next = new Set(loading);
2425
2669
  for (const mark of fieldMarks(requests)) next.add(mark);
2426
- return next;
2670
+ return next.size === loading.size ? loading : next;
2427
2671
  };
2428
2672
  const withoutLoading = (loading, requests) => {
2429
2673
  if (loading.size === 0) return loading;
2430
2674
  const next = new Set(loading);
2431
2675
  for (const mark of fieldMarks(requests)) next.delete(mark);
2432
- return next;
2676
+ return next.size === loading.size ? loading : next;
2433
2677
  };
2434
2678
  /**
2435
2679
  * A connection's mark. The leading separator keeps it out of the field marks'
@@ -2458,6 +2702,14 @@ const prunedLoading = (loading, roots, reached) => {
2458
2702
  return kept.length === loading.size ? loading : new Set(kept);
2459
2703
  };
2460
2704
  /**
2705
+ * The windows grown on connections retention keeps; a dropped one starts over.
2706
+ * The same record when every one is kept.
2707
+ */
2708
+ const keptWindows = (grown, connections) => {
2709
+ const kept = Object.entries(grown).filter(([key]) => Object.hasOwn(connections, key.slice(0, key.lastIndexOf("\0"))));
2710
+ return kept.length === Object.keys(grown).length ? grown : Object.fromEntries(kept);
2711
+ };
2712
+ /**
2461
2713
  * Drops the refresh marks of entities and connections that were collected.
2462
2714
  * Nothing observes a collected entity — the retention roots are the active
2463
2715
  * Surfaces — so no read entry's generation can fall back when its mark goes.
@@ -2487,7 +2739,7 @@ const prunedRefresh = (refresh, retained) => {
2487
2739
  * anything else is.
2488
2740
  */
2489
2741
  const refreshedAt = (refresh, requests, connections = []) => {
2490
- let at = 0;
2742
+ let at = refresh.floor;
2491
2743
  for (const mark of fieldMarks(requests)) at = Math.max(at, refresh.requested.get(mark) ?? 0);
2492
2744
  for (const identity of connections) at = Math.max(at, refresh.requested.get(connectionMark(identity)) ?? 0);
2493
2745
  return at;
@@ -2544,15 +2796,20 @@ const isLoading = (model, entity, id, fields) => {
2544
2796
  return fields.some((field) => model.loading.has(fieldMark(entity, id, field)));
2545
2797
  };
2546
2798
  const setConnectionStale = (connections, connection, stale) => {
2547
- const current = connections[connection] ?? emptyConnection;
2799
+ const current = connections[connection];
2800
+ if (current?.stale === stale) return connections;
2548
2801
  return {
2549
2802
  ...connections,
2550
2803
  [connection]: {
2551
- ...current,
2804
+ ...current ?? emptyConnection,
2552
2805
  stale
2553
2806
  }
2554
2807
  };
2555
2808
  };
2809
+ const withLiveState = (live, stream, state) => live[stream] === state ? live : {
2810
+ ...live,
2811
+ [stream]: state
2812
+ };
2556
2813
  const markGap = (model, stream) => model.gaps.has(stream) ? model : {
2557
2814
  ...model,
2558
2815
  gaps: /* @__PURE__ */ new Set([...model.gaps, stream])
@@ -2564,9 +2821,23 @@ const clearGap = (model, stream) => model.gaps.has(stream) ? {
2564
2821
  /**
2565
2822
  * The pure reducer all four producers share. A live event that arrives ahead of
2566
2823
  * its cursor is a gap: it is not applied, and the stream is recorded so the host
2567
- * can resubscribe rather than silently miss facts.
2824
+ * can resubscribe rather than silently miss facts. When every field a Message
2825
+ * touches keeps its identity, `model` itself is returned, so the application's
2826
+ * root keeps its identity and Foldkit does not render. The common no-ops do (a
2827
+ * duplicate live event, a repeated `ReadStarted`); a repeated failure, or a
2828
+ * `preserve-existing` restore of entities already held, still makes an equal
2829
+ * copy.
2568
2830
  */
2569
2831
  const updateRemote = (model, message) => {
2832
+ const next = reduceRemote(model, message);
2833
+ return next === model || !sameFields(model, next) ? next : model;
2834
+ };
2835
+ /** Whether every field of `next` is the one `model` holds. */
2836
+ const sameFields = (model, next) => {
2837
+ const keys = Object.keys(next);
2838
+ return keys.length === Object.keys(model).length && keys.every((key) => Object.is(model[key], next[key]));
2839
+ };
2840
+ const reduceRemote = (model, message) => {
2570
2841
  switch (message._tag) {
2571
2842
  case "ReadReceived": return {
2572
2843
  ...model,
@@ -2589,6 +2860,7 @@ const updateRemote = (model, message) => {
2589
2860
  return {
2590
2861
  ...model,
2591
2862
  ...retained,
2863
+ grown: keptWindows(model.grown, retained.connections),
2592
2864
  refresh: prunedRefresh(model.refresh, retained),
2593
2865
  failures: prunedFailures(model.failures, message.roots, reached),
2594
2866
  loading: prunedLoading(model.loading, message.roots, reached)
@@ -2608,7 +2880,7 @@ const updateRemote = (model, message) => {
2608
2880
  ...model,
2609
2881
  entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
2610
2882
  failures: withoutFieldFailures(model.failures, written),
2611
- connections: message.merge === "replace" ? {
2883
+ connections: restored.length === 0 ? model.connections : message.merge === "replace" ? {
2612
2884
  ...model.connections,
2613
2885
  ...Object.fromEntries(restored)
2614
2886
  } : {
@@ -2619,9 +2891,10 @@ const updateRemote = (model, message) => {
2619
2891
  }
2620
2892
  case "RefreshStarted": {
2621
2893
  const asked = message.requests.reduce((store, request) => isTombstone(store, entityKey(request.entity, request.id)) ? remove(store, entityKey(request.entity, request.id)) : store, model.entities);
2622
- const entities = setStale(asked, marksOf(message.requests), true);
2894
+ const marks = marksOf(message.requests);
2895
+ const entities = setUnavailable(setStale(asked, marks, true), marks, false);
2623
2896
  const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
2624
- return entities === model.entities && failures === model.failures ? model : {
2897
+ return {
2625
2898
  ...model,
2626
2899
  entities,
2627
2900
  failures
@@ -2646,7 +2919,7 @@ const updateRemote = (model, message) => {
2646
2919
  optimistic: beginOptimistic(model.optimistic, message.requestId, message.optimistic ?? [])
2647
2920
  };
2648
2921
  case "MutationSucceeded": {
2649
- const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message.entities, message.connections ?? [], message.deleted ?? []);
2922
+ const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message);
2650
2923
  return {
2651
2924
  ...model,
2652
2925
  entities: settled.store,
@@ -2659,6 +2932,13 @@ const updateRemote = (model, message) => {
2659
2932
  ...model,
2660
2933
  optimistic: beginOptimistic(settleFailure(model.optimistic, overlayLayer(message.id)), overlayLayer(message.id), message.optimistic)
2661
2934
  };
2935
+ case "WindowGrown": return {
2936
+ ...model,
2937
+ grown: {
2938
+ ...model.grown,
2939
+ [message.window]: message.size
2940
+ }
2941
+ };
2662
2942
  case "OverlayLifted": {
2663
2943
  const optimistic = settleFailure(model.optimistic, overlayLayer(message.id));
2664
2944
  return optimistic.layers.length === model.optimistic.layers.length && optimistic.overlays.length === model.optimistic.overlays.length ? model : {
@@ -2684,10 +2964,7 @@ const updateRemote = (model, message) => {
2684
2964
  return clearGap({
2685
2965
  ...model,
2686
2966
  entities: applied.store,
2687
- live: {
2688
- ...model.live,
2689
- [message.stream]: applied.state
2690
- },
2967
+ live: withLiveState(model.live, message.stream, applied.state),
2691
2968
  failures: withoutFieldFailures(model.failures, settled)
2692
2969
  }, message.stream);
2693
2970
  }
@@ -2696,10 +2973,7 @@ const updateRemote = (model, message) => {
2696
2973
  const next = clearGap({
2697
2974
  ...model,
2698
2975
  optimistic: applied.optimistic,
2699
- live: {
2700
- ...model.live,
2701
- [message.stream]: applied.state
2702
- }
2976
+ live: withLiveState(model.live, message.stream, applied.state)
2703
2977
  }, message.stream);
2704
2978
  return applied.invalidated === void 0 ? next : updateRemote(next, {
2705
2979
  _tag: "ConnectionInvalidated",
@@ -2709,12 +2983,23 @@ const updateRemote = (model, message) => {
2709
2983
  case "GapCleared": return clearGap(model, message.stream);
2710
2984
  case "ConnectionMerged": {
2711
2985
  const current = model.connections[message.connection] ?? emptyConnection;
2712
- const failures = withoutConnectionFailure(model.failures, message.connection);
2713
2986
  const loading = withoutQueryLoading(model.loading, message.connection);
2714
2987
  const replaces = message.refreshes === true && current.stale;
2715
2988
  const merged = merge(replaces ? emptyConnection : current, message.page);
2989
+ const payload = message.select === void 0 ? [] : message.entities ?? [];
2990
+ const settled = message.select === void 0 ? [] : message.settled ?? [];
2991
+ const requests = message.select === void 0 || payload.length === 0 && settled.length === 0 ? [] : message.page.edges.filter((edge) => edge.ref.entity === message.select.entity).map((edge) => ({
2992
+ ...message.select,
2993
+ id: edge.ref.id
2994
+ }));
2995
+ const entities = requests.length === 0 ? model.entities : writeRead(model.entities, requests, {
2996
+ entities: [...payload],
2997
+ settled: [...settled]
2998
+ }, message.now ?? 0);
2999
+ const failures = withoutFieldFailures(withoutConnectionFailure(model.failures, message.connection), [...fieldMarks(requests), ...patchedMarks(payload)]);
2716
3000
  return {
2717
3001
  ...model,
3002
+ entities,
2718
3003
  connections: {
2719
3004
  ...model.connections,
2720
3005
  [message.connection]: message.refreshes === true ? {
@@ -2727,13 +3012,11 @@ const updateRemote = (model, message) => {
2727
3012
  loading
2728
3013
  };
2729
3014
  }
2730
- case "ConnectionInvalidated":
2731
- if (model.connections[message.connection]?.stale === true && !(message.connection in model.failures.connections)) return model;
2732
- return {
2733
- ...model,
2734
- connections: setConnectionStale(model.connections, message.connection, true),
2735
- failures: withoutConnectionFailure(model.failures, message.connection)
2736
- };
3015
+ case "ConnectionInvalidated": return {
3016
+ ...model,
3017
+ connections: setConnectionStale(model.connections, message.connection, true),
3018
+ failures: withoutConnectionFailure(model.failures, message.connection)
3019
+ };
2737
3020
  case "QueryFailed": return {
2738
3021
  ...model,
2739
3022
  failures: {
@@ -2974,12 +3257,13 @@ const writeRead = (store, requests, result, now = 0) => {
2974
3257
  windows: entry?.windows
2975
3258
  });
2976
3259
  }
3260
+ const settled = new Set(result.settled.map((entry) => entityKey(entry.entity, entry.id)));
2977
3261
  let answered = store;
2978
3262
  for (const request of requests) {
2979
3263
  const key = entityKey(request.entity, request.id);
2980
- if (!returned.has(key)) answered = tombstone(answered, key);
3264
+ if (!returned.has(key) && !settled.has(key)) answered = tombstone(answered, key);
2981
3265
  }
2982
- return writeEntities(answered, writes, now);
3266
+ return setUnavailable(writeEntities(answered, writes, now), result.settled.map((entry) => [entityKey(entry.entity, entry.id), entry.fields]), true);
2983
3267
  };
2984
3268
  //#endregion
2985
3269
  //#region src/policy.ts
@@ -3012,6 +3296,221 @@ const RemotePolicy = {
3012
3296
  refreshes: (policy) => policy._tag !== "CacheFirst"
3013
3297
  };
3014
3298
  //#endregion
3299
+ //#region src/resume.ts
3300
+ /**
3301
+ * What of Remote's state crosses from a server render to the browser.
3302
+ *
3303
+ * A page rendered a moment ago sends exactly what its active Surfaces read:
3304
+ * for each requirement the fields it names, following relations through the
3305
+ * refs the store holds, and each connection whole, with its boundaries, so
3306
+ * "load more" knows where the server stopped. A live subscription's cursor
3307
+ * crosses with the entities it covers, so a subscription started in the
3308
+ * browser resumes where the server's left off.
3309
+ *
3310
+ * This is not a `Snapshot`, which is built for a cache that survived a reload
3311
+ * and deliberately keeps no cursors. Nothing else of `RemoteModel` crosses:
3312
+ * loading marks, failures, the mutation ledger, optimistic layers, gaps and
3313
+ * retention start from their initial values in the browser.
3314
+ */
3315
+ const CapturedEntry = Schema.Struct({
3316
+ values: Schema.Record(Schema.String, Schema.Unknown),
3317
+ present: Schema.Array(Schema.String),
3318
+ stale: Schema.Array(Schema.String),
3319
+ unavailable: Schema.Array(Schema.String),
3320
+ windows: Schema.Record(Schema.String, Schema.String),
3321
+ tombstone: Schema.Boolean,
3322
+ updatedAt: Schema.Number
3323
+ });
3324
+ const CapturedBoundary = Schema.Union([
3325
+ Schema.TaggedStruct("Terminal", {}),
3326
+ Schema.TaggedStruct("Cursor", { cursor: Schema.String }),
3327
+ Schema.TaggedStruct("Unknown", {})
3328
+ ]);
3329
+ const CapturedConnection = Schema.Struct({
3330
+ segments: Schema.Array(Schema.Struct({
3331
+ edges: Schema.Array(Schema.Struct({
3332
+ key: Schema.String,
3333
+ ref: Schema.Struct({
3334
+ entity: Schema.String,
3335
+ id: Schema.String
3336
+ })
3337
+ })),
3338
+ start: CapturedBoundary,
3339
+ end: CapturedBoundary
3340
+ })),
3341
+ stale: Schema.Boolean
3342
+ });
3343
+ const CapturedLive = Schema.Struct({
3344
+ cursor: Schema.Number,
3345
+ boundary: Schema.Record(Schema.String, Schema.Struct({
3346
+ before: Schema.Number,
3347
+ after: Schema.Number
3348
+ }))
3349
+ });
3350
+ /**
3351
+ * Marks a Subscription entry Remote built as one that may start late under
3352
+ * `foldkit-ssr`'s deferred boot (its decision 10): a read with resumed data
3353
+ * plans nothing, a live subscription resumes from the cursor the envelope
3354
+ * carries, and retention running late collects late. Non-enumerable, so the
3355
+ * runtime's iteration of an entry never sees it.
3356
+ */
3357
+ const DEFERRABLE = Symbol.for("foldkit-remote/deferrable");
3358
+ /** Marks every entry of a record Remote built, lifted or not. */
3359
+ const markAll = (entries) => {
3360
+ for (const entry of Object.values(entries)) Object.defineProperty(entry, DEFERRABLE, {
3361
+ value: true,
3362
+ enumerable: false
3363
+ });
3364
+ return entries;
3365
+ };
3366
+ /** The captured part of a Remote store, as JSON. */
3367
+ const RemoteCapture = Schema.Struct({
3368
+ entities: Schema.Record(Schema.String, CapturedEntry),
3369
+ connections: Schema.Record(Schema.String, CapturedConnection),
3370
+ live: Schema.Record(Schema.String, CapturedLive)
3371
+ });
3372
+ /** What some projections read, from their metadata. */
3373
+ const askedOf$1 = (projections) => ({
3374
+ requirements: projections.flatMap((projection) => RemoteRequirements.get(projection.metadata)),
3375
+ connections: projections.flatMap((projection) => RemoteConnections.get(projection.metadata))
3376
+ });
3377
+ /** Whether every `entity:id:fields` part of a live stream key was captured with those fields. */
3378
+ const liveCovered = (stream, entries) => stream.split("|").every((part) => {
3379
+ const split = part.lastIndexOf(":");
3380
+ const entry = entries.get(part.slice(0, split));
3381
+ const fields = part.slice(split + 1).split(",").filter((field) => field !== "");
3382
+ return entry !== void 0 && fields.every((field) => entry.present.has(field));
3383
+ });
3384
+ /**
3385
+ * Captures what some projections read from a store: the fields each
3386
+ * requirement names, through its relations; each connection whole, and the
3387
+ * fields its selection reads of each item; and each live cursor whose
3388
+ * entities were all captured with the fields it follows.
3389
+ */
3390
+ const captureRemote = (remote, projections) => {
3391
+ const store = remote.entities;
3392
+ const entries = /* @__PURE__ */ new Map();
3393
+ const walked = /* @__PURE__ */ new Map();
3394
+ const walk = (entity, id, requirement) => {
3395
+ const key = entityKey(entity, id);
3396
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
3397
+ if (seen.has(requirement)) return;
3398
+ seen.add(requirement);
3399
+ walked.set(key, seen);
3400
+ const stored = store[key];
3401
+ if (stored === void 0) return;
3402
+ const entry = entries.get(key) ?? {
3403
+ values: {},
3404
+ present: /* @__PURE__ */ new Set(),
3405
+ stale: /* @__PURE__ */ new Set(),
3406
+ unavailable: /* @__PURE__ */ new Set(),
3407
+ windows: {},
3408
+ tombstone: stored.tombstone,
3409
+ updatedAt: stored.updatedAt
3410
+ };
3411
+ entries.set(key, entry);
3412
+ for (const field of requirement.fields) {
3413
+ if (stored.unavailable.has(field)) entry.unavailable.add(field);
3414
+ if (!stored.present.has(field)) continue;
3415
+ entry.values[field] = stored.values[field];
3416
+ entry.present.add(field);
3417
+ if (stored.stale.has(field)) entry.stale.add(field);
3418
+ const window = stored.windows[field];
3419
+ if (window !== void 0) entry.windows[field] = window;
3420
+ const nested = requirement.relations?.[field];
3421
+ if (nested === void 0) continue;
3422
+ const value = readField(store, key, field);
3423
+ if (value._tag === "None") continue;
3424
+ for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
3425
+ }
3426
+ };
3427
+ const asked = askedOf$1(projections);
3428
+ for (const requirement of asked.requirements) walk(requirement.entity, requirement.id, requirement);
3429
+ const connections = {};
3430
+ for (const { identity, select } of asked.connections) {
3431
+ const connection = remote.connections[identity];
3432
+ if (connection === void 0) continue;
3433
+ connections[identity] = connection;
3434
+ for (const segment of connection.segments) for (const edge of segment.edges) walk(edge.ref.entity, edge.ref.id, select);
3435
+ }
3436
+ const live = {};
3437
+ for (const [stream, state] of Object.entries(remote.live)) if (liveCovered(stream, entries)) live[stream] = state;
3438
+ return {
3439
+ entities: Object.fromEntries([...entries].map(([key, entry]) => [key, {
3440
+ values: entry.values,
3441
+ present: [...entry.present],
3442
+ stale: [...entry.stale],
3443
+ unavailable: [...entry.unavailable],
3444
+ windows: entry.windows,
3445
+ tombstone: entry.tombstone,
3446
+ updatedAt: entry.updatedAt
3447
+ }])),
3448
+ connections: Object.fromEntries(Object.entries(connections).map(([identity, connection]) => [identity, {
3449
+ segments: connection.segments.map((segment) => ({
3450
+ edges: segment.edges.map((edge) => ({
3451
+ key: edge.key,
3452
+ ref: edge.ref
3453
+ })),
3454
+ start: segment.start,
3455
+ end: segment.end
3456
+ })),
3457
+ stale: connection.stale
3458
+ }])),
3459
+ live
3460
+ };
3461
+ };
3462
+ /**
3463
+ * Sets a capture onto a store: its entities, connections and live cursors
3464
+ * replace any held under the same keys, and nothing else of the store changes.
3465
+ */
3466
+ const restoreRemote = (remote, capture) => ({
3467
+ ...remote,
3468
+ entities: {
3469
+ ...remote.entities,
3470
+ ...Object.fromEntries(Object.entries(capture.entities).map(([key, entry]) => [key, {
3471
+ values: entry.values,
3472
+ present: new Set(entry.present),
3473
+ stale: new Set(entry.stale),
3474
+ unavailable: new Set(entry.unavailable),
3475
+ windows: entry.windows,
3476
+ tombstone: entry.tombstone,
3477
+ updatedAt: entry.updatedAt
3478
+ }]))
3479
+ },
3480
+ connections: {
3481
+ ...remote.connections,
3482
+ ...Object.fromEntries(Object.entries(capture.connections).map(([identity, connection]) => [identity, {
3483
+ segments: connection.segments.map((segment) => ({
3484
+ edges: segment.edges,
3485
+ start: segment.start,
3486
+ end: segment.end
3487
+ })),
3488
+ stale: connection.stale
3489
+ }]))
3490
+ },
3491
+ live: {
3492
+ ...remote.live,
3493
+ ...capture.live
3494
+ }
3495
+ });
3496
+ const encodeCapture = Schema.encodeSync(RemoteCapture);
3497
+ const decodeCapture = Schema.decodeUnknownResult(RemoteCapture);
3498
+ /**
3499
+ * The resume part for a Remote domain, for `SSR.plan`'s `parts`: it sends
3500
+ * what the plan's active Surfaces read from this domain's store, and nothing
3501
+ * else of it. `id` tells two domains in one application apart.
3502
+ */
3503
+ const resumePart = (domain, options = {}) => ({
3504
+ id: options.id ?? "remote",
3505
+ covers: [RemoteRequirements.name, RemoteConnections.name],
3506
+ deferrable: (_key, entry) => typeof entry === "object" && entry !== null && DEFERRABLE in entry,
3507
+ capture: (model, projections) => encodeCapture(captureRemote(domain.store.get(model), projections)),
3508
+ restore: (model, value) => Result.match(decodeCapture(value), {
3509
+ onFailure: (error) => Result.fail(`Remote's state does not decode: ${error.message}`),
3510
+ onSuccess: (capture) => Result.succeed(domain.store.set(model, restoreRemote(domain.store.get(model), capture)))
3511
+ })
3512
+ });
3513
+ //#endregion
3015
3514
  //#region src/index.ts
3016
3515
  /**
3017
3516
  * `foldkit-remote` — normalized application-facing server state.
@@ -3022,6 +3521,67 @@ const RemotePolicy = {
3022
3521
  */
3023
3522
  const descriptorOf = (entity) => Entity$1.is(entity) ? Entity.from(entity) : entity;
3024
3523
  const selectionOf = (selection) => SelectionTypeId in selection ? Selection.from(selection) : selection;
3524
+ const readContract = (source, shape, window) => {
3525
+ const ref = {
3526
+ ...source,
3527
+ window: pickWindow(window)
3528
+ };
3529
+ const relation = relationOf(shape);
3530
+ return {
3531
+ ref,
3532
+ relation,
3533
+ requirement: {
3534
+ identity: ref.identity,
3535
+ window: ref.window,
3536
+ select: relation,
3537
+ ref
3538
+ }
3539
+ };
3540
+ };
3541
+ const metaOf = (remote, asked) => {
3542
+ const visible = visibleStoreOf(remote.entities, remote.optimistic);
3543
+ let updatedAt;
3544
+ let stale = false;
3545
+ let loading = false;
3546
+ const seen = /* @__PURE__ */ new Set();
3547
+ const touch = (entity, id, requirement) => {
3548
+ const key = entityKey(entity, id);
3549
+ const memo = `${key}\u0000${stableStringify(requirement)}`;
3550
+ if (seen.has(memo)) return;
3551
+ seen.add(memo);
3552
+ const entry = visible[key];
3553
+ if (entry !== void 0 && !entry.tombstone) for (const field of requirement.fields) {
3554
+ if (!entry.present.has(field)) continue;
3555
+ if (Number.isFinite(entry.updatedAt)) updatedAt = updatedAt === void 0 ? entry.updatedAt : Math.max(updatedAt, entry.updatedAt);
3556
+ if (entry.stale.has(field)) stale = true;
3557
+ }
3558
+ if (isLoadingThrough(remote, entity, id, requirement)) loading = true;
3559
+ for (const [field, relation] of Object.entries(requirement.relations ?? {})) {
3560
+ const value = readField(visible, key, field);
3561
+ if (value._tag === "None" || value.value === null || value.value === void 0) continue;
3562
+ for (const ref of targetsOf(value.value, relation)) touch(ref.entity, ref.id, relation);
3563
+ }
3564
+ };
3565
+ for (const requirement of asked.requirements) touch(requirement.entity, requirement.id, requirement);
3566
+ for (const connection of asked.connections) {
3567
+ const known = remote.connections[connection.identity];
3568
+ if (known === void 0) {
3569
+ if (isQueryLoading(remote, connection.identity)) loading = true;
3570
+ continue;
3571
+ }
3572
+ if (known.stale) stale = true;
3573
+ if (isQueryLoading(remote, connection.identity)) loading = true;
3574
+ for (const edge of visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities)) {
3575
+ if (edge.ref.entity !== connection.select.entity) continue;
3576
+ touch(edge.ref.entity, edge.ref.id, connection.select);
3577
+ }
3578
+ }
3579
+ return {
3580
+ updatedAt,
3581
+ stale,
3582
+ loading
3583
+ };
3584
+ };
3025
3585
  /**
3026
3586
  * The store a read or plan sees: the base with every pending optimistic layer
3027
3587
  * applied, so a request's patches show until it settles and a temporary id is
@@ -3049,10 +3609,10 @@ const confirmed = (bound, projection) => ({
3049
3609
  });
3050
3610
  const visibleStores = /* @__PURE__ */ new WeakMap();
3051
3611
  const visibleStoreOf = (entities, optimistic) => {
3052
- let byStore = visibleStores.get(optimistic);
3612
+ let byStore = visibleStores.get(optimistic.layers);
3053
3613
  if (byStore === void 0) {
3054
3614
  byStore = /* @__PURE__ */ new WeakMap();
3055
- visibleStores.set(optimistic, byStore);
3615
+ visibleStores.set(optimistic.layers, byStore);
3056
3616
  }
3057
3617
  let visible = byStore.get(entities);
3058
3618
  if (visible === void 0) {
@@ -3061,25 +3621,87 @@ const visibleStoreOf = (entities, optimistic) => {
3061
3621
  }
3062
3622
  return visible;
3063
3623
  };
3064
- const readResults = /* @__PURE__ */ new WeakMap();
3065
- const memoRead = (snapshot, by, key, compute) => {
3066
- let byScope = readResults.get(snapshot);
3067
- if (byScope === void 0) {
3068
- byScope = /* @__PURE__ */ new WeakMap();
3069
- readResults.set(snapshot, byScope);
3624
+ /**
3625
+ * `Failed` naming a field the server settled without a value, when one is
3626
+ * among what `relation` reads of the entity; otherwise nothing, and the store
3627
+ * merely lacks the value. The reason stays with the server, so a view shows
3628
+ * the field it cannot have, not why.
3629
+ */
3630
+ const unavailableFailure = (store, key, relation) => {
3631
+ const field = unavailableOf(store, key, relation);
3632
+ return field === void 0 ? void 0 : {
3633
+ _tag: "Failed",
3634
+ error: {
3635
+ _tag: "Unavailable",
3636
+ message: `The server answered without ${field}, and will not answer with it: the field is not available to this client.`
3637
+ }
3638
+ };
3639
+ };
3640
+ /**
3641
+ * A row's decoded value, kept while the data it was assembled from is the same,
3642
+ * so a refetch or live patch that brought equal data hands a view the object it
3643
+ * already rendered. Keyed on the row's own values object, which the store keeps
3644
+ * across equal writes; a relation's targets are covered by comparing what was
3645
+ * assembled.
3646
+ */
3647
+ const decodedRows = /* @__PURE__ */ new WeakMap();
3648
+ const decodeRow = (schema, source, values) => {
3649
+ let bySchema = decodedRows.get(schema);
3650
+ if (bySchema === void 0) {
3651
+ bySchema = {
3652
+ decode: Schema.decodeUnknownResult(schema),
3653
+ rows: /* @__PURE__ */ new WeakMap()
3654
+ };
3655
+ decodedRows.set(schema, bySchema);
3070
3656
  }
3071
- let results = byScope.get(by);
3072
- if (results === void 0) {
3073
- results = /* @__PURE__ */ new Map();
3074
- byScope.set(by, results);
3657
+ const cached = bySchema.rows.get(source);
3658
+ if (cached !== void 0 && sameData(cached.values, values)) return cached.decoded;
3659
+ const decoded = bySchema.decode(values);
3660
+ bySchema.rows.set(source, {
3661
+ values,
3662
+ decoded
3663
+ });
3664
+ return decoded;
3665
+ };
3666
+ const readResults = {
3667
+ inner: /* @__PURE__ */ new WeakMap(),
3668
+ results: /* @__PURE__ */ new Map()
3669
+ };
3670
+ const memoRead = (scopes, key, compute) => {
3671
+ let scope = readResults;
3672
+ for (const by of scopes) {
3673
+ let inner = scope.inner.get(by);
3674
+ if (inner === void 0) {
3675
+ inner = {
3676
+ inner: /* @__PURE__ */ new WeakMap(),
3677
+ results: /* @__PURE__ */ new Map()
3678
+ };
3679
+ scope.inner.set(by, inner);
3680
+ }
3681
+ scope = inner;
3075
3682
  }
3076
- if (results.has(key)) return results.get(key);
3683
+ if (scope.results.has(key)) return scope.results.get(key);
3077
3684
  const value = compute();
3078
- results.set(key, value);
3685
+ scope.results.set(key, value);
3079
3686
  return value;
3080
3687
  };
3688
+ /**
3689
+ * The default clock. It reads `Date.now` when called rather than holding the
3690
+ * function, so fake timers installed after the domain was made move it.
3691
+ */
3692
+ const wallClock = () => Date.now();
3081
3693
  /** The default `toMessage`: the application reduces `RemoteMessage` itself. */
3082
3694
  const identityMessage = (message) => message;
3695
+ /**
3696
+ * `Data.satisfy` reached its bound with Surfaces still reading, such as a
3697
+ * chain whose every read reveals one more. It names them, so a page that
3698
+ * would have rendered them loading fails instead. (A read the server leaves
3699
+ * unanswered settles as missing and ends the loop.)
3700
+ */
3701
+ var RemoteUnsatisfied = class extends Schema.TaggedError()("RemoteUnsatisfied", {
3702
+ surfaces: Schema.Array(Schema.String),
3703
+ passes: Schema.Number
3704
+ }) {};
3083
3705
  /** A stable key for a live subscription's requirement set. */
3084
3706
  const liveStreamKey = (requirements) => requirements.map((requirement) => `${entityKey(requirement.entity, requirement.id)}:${[...requirement.fields].sort().join(",")}`).sort().join("|");
3085
3707
  /** Declares a Remote domain: its entities, queries, and mutations, plus the submodel. */
@@ -3123,10 +3745,15 @@ const nothingAsked = {
3123
3745
  requirements: [],
3124
3746
  connections: []
3125
3747
  };
3126
- const askedOf = (projection) => projection === void 0 ? nothingAsked : {
3748
+ const askedOf = (projection) => ({
3127
3749
  requirements: requirementsOf(projection),
3128
3750
  connections: connectionsOf(projection)
3129
- };
3751
+ });
3752
+ /** What an active Surface asks for: nothing while it is inactive. */
3753
+ const askedWhile = (projection) => Option.match(projection, {
3754
+ onNone: () => nothingAsked,
3755
+ onSome: askedOf
3756
+ });
3130
3757
  /**
3131
3758
  * Whether `outer` asks for everything `inner` does of one entity: each field,
3132
3759
  * each relation's own slice of its target, and each page window alike.
@@ -3157,24 +3784,88 @@ const covers = (outer, inner) => {
3157
3784
  * failure is what its read shows, and retrying is `Remote.refresh`'s to ask
3158
3785
  * for. The rows it already holds are still planned, since they are on screen.
3159
3786
  */
3787
+ /**
3788
+ * One requirement per connection and direction: of the sized windows reading
3789
+ * a connection from its start (or end), the widest, which covers the rest.
3790
+ * Windows with a cursor are pages of their own and stay as they are.
3791
+ */
3792
+ const widest = (remote, connections) => {
3793
+ const kept = /* @__PURE__ */ new Map();
3794
+ const rest = [];
3795
+ for (const connection of connections) {
3796
+ const size = windowSize(remote, connection);
3797
+ if (size === void 0) {
3798
+ rest.push(connection);
3799
+ continue;
3800
+ }
3801
+ const key = `${connection.identity}${String.fromCharCode(0)}${connection.window.first === void 0 ? "last" : "first"}`;
3802
+ const current = kept.get(key);
3803
+ if (current === void 0) kept.set(key, connection);
3804
+ else if (size > (windowSize(remote, current) ?? 0)) kept.set(key, {
3805
+ ...connection,
3806
+ select: Requirement.mergeRelation(current.select, connection.select)
3807
+ });
3808
+ else kept.set(key, {
3809
+ ...current,
3810
+ select: Requirement.mergeRelation(current.select, connection.select)
3811
+ });
3812
+ }
3813
+ return [...kept.values(), ...rest];
3814
+ };
3815
+ const withWindow = (connection, window) => ({
3816
+ ...connection,
3817
+ window,
3818
+ ref: {
3819
+ ...connection.ref,
3820
+ window
3821
+ }
3822
+ });
3823
+ /**
3824
+ * The page a connection lacks for a window of `size` from its start (or end):
3825
+ * the rows after its first segment's end cursor (or before its last's start).
3826
+ * Undefined when it holds enough, reaches the end, or does not know where it
3827
+ * starts.
3828
+ */
3829
+ const missingRows = (known, window, size) => {
3830
+ const forward = window.first !== void 0;
3831
+ const segment = forward ? known.segments[0] : known.segments[known.segments.length - 1];
3832
+ if (segment === void 0) return void 0;
3833
+ const [from, to] = forward ? [segment.start, segment.end] : [segment.end, segment.start];
3834
+ if (from._tag !== "Terminal" || to._tag !== "Cursor" || segment.edges.length >= size) return void 0;
3835
+ const lacking = size - segment.edges.length;
3836
+ return forward ? {
3837
+ first: lacking,
3838
+ after: to.cursor
3839
+ } : {
3840
+ last: lacking,
3841
+ before: to.cursor
3842
+ };
3843
+ };
3160
3844
  const planAsked = (remote, asked, options) => {
3161
3845
  const queries = [];
3162
3846
  const items = [];
3163
3847
  const connections = Requirement.mergeConnections(asked.connections);
3164
- for (const connection of connections) {
3848
+ for (const connection of widest(remote, connections)) {
3165
3849
  const known = remote.connections[connection.identity];
3166
- if (!(options.force !== true && connection.identity in remote.failures.connections) && (known === void 0 || known.stale || options.force === true)) {
3167
- queries.push(connection);
3850
+ const failed = options.force !== true && connection.identity in remote.failures.connections;
3851
+ const size = windowSize(remote, connection);
3852
+ if (!failed && (known === void 0 || known.stale || options.force === true)) {
3853
+ queries.push(size === void 0 ? connection : withWindow(connection, connection.window.first === void 0 ? { last: size } : { first: size }));
3168
3854
  continue;
3169
3855
  }
3170
3856
  if (known === void 0) continue;
3857
+ const tail = failed || size === void 0 ? void 0 : missingRows(known, connection.window, size);
3858
+ if (tail !== void 0) queries.push(withWindow(connection, tail));
3171
3859
  const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
3172
3860
  items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
3173
3861
  }
3174
- const planned = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], options);
3862
+ const visible = visibleStoreOf(remote.entities, remote.optimistic);
3863
+ const read = [...asked.requirements, ...items];
3864
+ const planned = plan(visible, read, options);
3175
3865
  return {
3176
3866
  requirements: options.force === true ? planned : withoutFailedFields(remote, planned),
3177
- queries
3867
+ queries,
3868
+ expires: options.freshness === void 0 ? null : deadlineOf(visible, read, options.freshness) ?? null
3178
3869
  };
3179
3870
  };
3180
3871
  /**
@@ -3220,7 +3911,8 @@ const queryRequestOf = (query) => {
3220
3911
  try: () => ({
3221
3912
  query: query.identity.slice(0, separator),
3222
3913
  input: JSON.parse(query.identity.slice(separator + 1)),
3223
- window: query.window
3914
+ window: query.window,
3915
+ ...query.select === void 0 ? {} : { select: query.select }
3224
3916
  }),
3225
3917
  catch: () => new RemoteQueryError({ message: `connection "${query.identity}" carries no encoded input` })
3226
3918
  });
@@ -3249,7 +3941,7 @@ const overrun = (window, edges) => {
3249
3941
  got: edges.length
3250
3942
  } : void 0;
3251
3943
  };
3252
- const pageMessage = (connection, result, refreshes = false, window = {}) => {
3944
+ const pageMessage = (connection, result, refreshes = false, window = {}, select, now) => {
3253
3945
  const over = overrun(window, result.edges);
3254
3946
  if (over !== void 0) return {
3255
3947
  _tag: "QueryFailed",
@@ -3259,6 +3951,7 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
3259
3951
  message: `the server returned ${over.got} edges for a window of ${over.asked}`
3260
3952
  }
3261
3953
  };
3954
+ const hasPayload = select !== void 0 && (result.entities !== void 0 && result.entities.length > 0 || result.settled !== void 0 && result.settled.length > 0);
3262
3955
  return {
3263
3956
  _tag: "ConnectionMerged",
3264
3957
  connection,
@@ -3273,7 +3966,13 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
3273
3966
  start: result.start,
3274
3967
  end: result.end
3275
3968
  },
3276
- ...refreshes ? { refreshes } : {}
3969
+ ...refreshes ? { refreshes } : {},
3970
+ ...select === void 0 || !hasPayload ? {} : { select },
3971
+ ...hasPayload ? {
3972
+ entities: [...result.entities ?? []],
3973
+ settled: [...result.settled ?? []],
3974
+ now: now ?? 0
3975
+ } : {}
3277
3976
  };
3278
3977
  };
3279
3978
  /**
@@ -3301,7 +4000,7 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
3301
4000
  const client = yield* RemoteClient;
3302
4001
  const at = now();
3303
4002
  const result = yield* Effect.result(client.read({
3304
- version: 3,
4003
+ version: 4,
3305
4004
  requests: requirements
3306
4005
  }));
3307
4006
  return Result.isFailure(result) ? {
@@ -3316,14 +4015,14 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
3316
4015
  };
3317
4016
  });
3318
4017
  /** Runs a query and reports the page that refreshes its connection, or the failure. Never fails. */
3319
- const queryMessage = (query) => Effect.gen(function* () {
4018
+ const queryMessage = (query, now = wallClock) => Effect.gen(function* () {
3320
4019
  const client = yield* RemoteClient;
3321
4020
  const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
3322
4021
  return Result.isFailure(result) ? {
3323
4022
  _tag: "QueryFailed",
3324
4023
  connection: query.identity,
3325
4024
  error: remoteError(result.failure)
3326
- } : pageMessage(query.identity, result.success, true, query.window);
4025
+ } : pageMessage(query.identity, result.success, true, query.window, query.select, now());
3327
4026
  });
3328
4027
  /** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
3329
4028
  const PlannedQuery = Schema.Struct({
@@ -3341,29 +4040,50 @@ const PlannedQuery = Schema.Struct({
3341
4040
  * to the restart; a page and its refresh are one `ConnectionMerged`.
3342
4041
  */
3343
4042
  const observeEntry = (bound, askedOf, toMessage, options) => {
3344
- const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
4043
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
3345
4044
  const read = (requirements) => Effect.map(readMessage(requirements, now), toMessage);
3346
- const run = (query) => Effect.map(queryMessage(query), toMessage);
4045
+ const run = (query) => Effect.map(queryMessage(query, now), toMessage);
4046
+ const planned = /* @__PURE__ */ new WeakMap();
3347
4047
  return {
3348
4048
  dependenciesSchema: Schema.Struct({
3349
4049
  requirements: Schema.Array(ReadRequest),
3350
4050
  queries: Schema.Array(PlannedQuery),
3351
- refresh: Schema.Number
4051
+ refresh: Schema.Number,
4052
+ expires: Schema.NullOr(Schema.Struct({
4053
+ at: Schema.Number,
4054
+ due: Schema.Array(ReadRequest)
4055
+ }))
3352
4056
  }),
3353
4057
  modelToDependencies: (model) => {
3354
4058
  const remote = bound.store.get(model);
3355
- const planned = planAsked(remote, askedOf(model), RemotePolicy.toPlan(policy, now()));
3356
- return {
3357
- refresh: refreshedAt(remote.refresh, planned.requirements, planned.queries.map((query) => query.identity)),
3358
- requirements: planned.requirements,
3359
- queries: planned.queries.map(({ identity, window, select }) => ({
4059
+ const asked = askedOf(model);
4060
+ const askedKey = stableStringify(asked);
4061
+ const at = now();
4062
+ let byAsked = planned.get(remote);
4063
+ const known = byAsked?.get(askedKey);
4064
+ if (known !== void 0 && at < known.until) return known.dependencies;
4065
+ const plan = planAsked(remote, asked, RemotePolicy.toPlan(policy, at));
4066
+ const dependencies = {
4067
+ refresh: refreshedAt(remote.refresh, plan.requirements, plan.queries.map((query) => query.identity)),
4068
+ requirements: plan.requirements,
4069
+ queries: plan.queries.map(({ identity, window, select }) => ({
3360
4070
  identity,
3361
4071
  window,
3362
4072
  select
3363
- }))
4073
+ })),
4074
+ expires: plan.expires
3364
4075
  };
4076
+ if (byAsked === void 0) {
4077
+ byAsked = /* @__PURE__ */ new Map();
4078
+ planned.set(remote, byAsked);
4079
+ }
4080
+ byAsked.set(askedKey, {
4081
+ until: plan.expires?.at ?? Infinity,
4082
+ dependencies
4083
+ });
4084
+ return dependencies;
3365
4085
  },
3366
- dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
4086
+ dependenciesToStream: ({ requirements, queries, refresh, expires }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
3367
4087
  _tag: "ReadStarted",
3368
4088
  requests: requirements,
3369
4089
  refresh
@@ -3373,21 +4093,28 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
3373
4093
  })] : []], ...queries.length === 0 ? [] : [toMessage({
3374
4094
  _tag: "QueryStarted",
3375
4095
  connections: queries.map((query) => query.identity)
3376
- })]]), Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }))
4096
+ })]]), Stream.merge(Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }), expires === null ? Stream.empty : Stream.fromEffect(Effect.as(Effect.sleep(Math.max(0, expires.at - now())), toMessage({
4097
+ _tag: "RefreshStarted",
4098
+ requests: expires.due
4099
+ })))))
3377
4100
  };
3378
4101
  };
4102
+ const resumeCursor = Schema.Number.pipe(Schema.overrideToEquivalence(() => () => true));
3379
4103
  /** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
3380
4104
  const liveEntry = (bound, requirementsOf, toMessage, options) => ({
3381
4105
  dependenciesSchema: Schema.Struct({
3382
4106
  requirements: Schema.Array(ReadRequest),
3383
- cursor: Schema.Number
4107
+ cursor: resumeCursor,
4108
+ floor: Schema.Number
3384
4109
  }),
3385
4110
  modelToDependencies: (model) => {
3386
4111
  const requirements = requirementsOf(model);
3387
4112
  const stream = liveStreamKey(requirements);
4113
+ const remote = bound.store.get(model);
3388
4114
  return {
3389
4115
  requirements,
3390
- cursor: bound.store.get(model).live[stream]?.cursor ?? 0
4116
+ cursor: remote.live[stream]?.cursor ?? 0,
4117
+ floor: remote.refresh.floor
3391
4118
  };
3392
4119
  },
3393
4120
  dependenciesToStream: ({ requirements, cursor }) => requirements.length === 0 ? Stream.empty : Stream.unwrap(Effect.gen(function* () {
@@ -3399,7 +4126,7 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
3399
4126
  _tag: "LiveReceived",
3400
4127
  stream: liveStreamKey(requirements),
3401
4128
  event,
3402
- now: (options.now ?? Date.now)()
4129
+ now: (options.now ?? wallClock)()
3403
4130
  })), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
3404
4131
  _tag: "ReadFailed",
3405
4132
  requests: requirements,
@@ -3407,8 +4134,8 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
3407
4134
  stream: liveStreamKey(requirements)
3408
4135
  }))))
3409
4136
  });
3410
- /** The projection an active Surface has for this Model, if it is active. */
3411
- const projectionOf = (entry, model) => "projectionOf" in entry ? entry.projectionOf(model) : entry.projection();
4137
+ /** The projection an active Surface has for this Model; none while it is inactive. */
4138
+ const projectionOf = (entry, model) => "projectionOf" in entry ? entry.projectionOf(model) : Option.some(entry.projection());
3412
4139
  /**
3413
4140
  * `projectionOf` computed once per Model: the read, live, and retain entries
3414
4141
  * all derive their dependencies from the same projection on the same Model
@@ -3420,7 +4147,8 @@ const memoizedProjectionOf = (entry) => {
3420
4147
  return (model) => {
3421
4148
  const key = model;
3422
4149
  if (typeof key !== "object" || key === null) return projectionOf(entry, model);
3423
- if (cache.has(key)) return cache.get(key);
4150
+ const known = cache.get(key);
4151
+ if (known !== void 0) return known;
3424
4152
  const projection = projectionOf(entry, model);
3425
4153
  cache.set(key, projection);
3426
4154
  return projection;
@@ -3440,6 +4168,51 @@ const Remote = {
3440
4168
  /** Whether a Message is one of Remote's, by tag. */
3441
4169
  reduces: isRemoteMessage,
3442
4170
  /**
4171
+ * Remote's Messages as one Schema, for a wrapper variant in the
4172
+ * application's union: `GotRemoteMessage: { message: Remote.Message }`. With
4173
+ * a wrapper, `update` matches the application's union exhaustively and
4174
+ * `Remote.fold` reduces what arrives inside it.
4175
+ */
4176
+ Message: defineMessageUnion(remoteMessageCases),
4177
+ /**
4178
+ * A bound domain under one wrapper variant of the application's union, the
4179
+ * same shape Foldkit gives a Submodel: `toParentMessage` wraps Remote's
4180
+ * Messages, the fold reduces them, and its `fetch`, `mutate`, and
4181
+ * `subscriptions` yield the wrapper. Spreading `Remote.messages` and
4182
+ * narrowing with `Remote.reduces` remains the shorter path for an
4183
+ * application whose `update` only reduces.
4184
+ *
4185
+ * @example
4186
+ * ```ts
4187
+ * const Message = defineMessageUnion({ GotRemoteMessage: { message: Remote.Message } })
4188
+ * const foldData = Remote.fold(Data, message => Message.GotRemoteMessage({ message }))
4189
+ *
4190
+ * const update = (model: Model, message: Message): Return =>
4191
+ * Message.match(message, {
4192
+ * GotRemoteMessage: ({ message }) => foldData(model, message),
4193
+ * })
4194
+ * const subscriptions = Subscription.make<Model, Message, RemoteClient>()(() =>
4195
+ * foldData.subscriptions({ page: Surface.at(ProjectPage, …) }),
4196
+ * )
4197
+ * ```
4198
+ */
4199
+ fold: (domain, toParentMessage) => {
4200
+ const fold = (model, message) => ({ model: domain.reduce(model, message) });
4201
+ return Object.assign(fold, {
4202
+ mutate: (...args) => {
4203
+ const started = domain.mutate(...args);
4204
+ return {
4205
+ ...started,
4206
+ command: mapMessage(started.command, toParentMessage)
4207
+ };
4208
+ },
4209
+ subscriptions: (active, options) => markAll(Subscription.lift(brandEntries(domain.subscriptions(active, options)))({
4210
+ toChildModel: (model) => model,
4211
+ toParentMessage
4212
+ }))
4213
+ });
4214
+ },
4215
+ /**
3443
4216
  * A patch of one entity's values, for a mutation's `optimistic` list or an
3444
4217
  * overlay. Takes a `foldkit-entity` Entity or a Remote descriptor alike, so
3445
4218
  * the domain you declared once is the one you patch:
@@ -3473,6 +4246,13 @@ const Remote = {
3473
4246
  */
3474
4247
  at: bindRemote,
3475
4248
  /**
4249
+ * The resume part for this domain, for `foldkit-ssr`'s `SSR.plan({ parts })`:
4250
+ * a server render sends what the plan's active Surfaces read from the store,
4251
+ * each connection with its boundaries and each live cursor with it, and
4252
+ * nothing else. `id` tells two domains in one application apart.
4253
+ */
4254
+ resume: resumePart,
4255
+ /**
3476
4256
  * A Projection node that reads a `RemoteData` value out of the store. The id
3477
4257
  * is supplied by the caller, usually from a Surface's params. The assembled
3478
4258
  * value is decoded against the Selection, so malformed server data surfaces
@@ -3483,6 +4263,7 @@ const Remote = {
3483
4263
  const selection = selectionOf(given);
3484
4264
  assertRegistered(bound, "Entity", bound.definition.registry.entities, selection.entity);
3485
4265
  const relation = relationOf(selection);
4266
+ const relationKey = stableStringify(relation);
3486
4267
  return (id) => ({
3487
4268
  Model: remoteDataSchema(selection.schema),
3488
4269
  dependencies: [],
@@ -3493,11 +4274,11 @@ const Remote = {
3493
4274
  read: (root) => {
3494
4275
  const store = storeOf(bound, root);
3495
4276
  const key = entityKey(selection.entity, id);
3496
- const present = memoRead(store, store, `${key}\u0000${stableStringify(relation)}`, () => {
4277
+ const present = memoRead([store], `${key}\u0000${relationKey}`, () => {
3497
4278
  if (isTombstone(store, key)) return { _tag: "NotFound" };
3498
4279
  const assembled = assemble(store, key, relation);
3499
- if (assembled === void 0) return void 0;
3500
- const decoded = Schema.decodeUnknownResult(selection.schema)(assembled.values);
4280
+ if (assembled === void 0) return unavailableFailure(store, key, relation);
4281
+ const decoded = decodeRow(selection.schema, store[key].values, assembled.values);
3501
4282
  return Result.isFailure(decoded) ? {
3502
4283
  _tag: "Failed",
3503
4284
  error: {
@@ -3520,14 +4301,36 @@ const Remote = {
3520
4301
  previous: present.value
3521
4302
  } : present;
3522
4303
  if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
3523
- return failure === void 0 ? { _tag: "Initial" } : {
4304
+ if (failure !== void 0) return {
3524
4305
  _tag: "Failed",
3525
4306
  error: failure
3526
4307
  };
4308
+ if (onlyOverlaid(remote, selection.entity, id)) {
4309
+ const lacking = missingFields(store, key, relation.fields);
4310
+ return {
4311
+ _tag: "Failed",
4312
+ error: {
4313
+ _tag: "Overlaid",
4314
+ message: `${selection.entity} ${id} is shown only by an overlay, which does not hold ${lacking.length === 0 ? "what its relations select" : lacking.join(", ")}; the server has not seen it, so nothing will fetch them.`
4315
+ }
4316
+ };
4317
+ }
4318
+ return { _tag: "Initial" };
3527
4319
  }
3528
4320
  });
3529
4321
  },
3530
4322
  /**
4323
+ * When what a projection shows was last received, and whether it is stale
4324
+ * or loading. Pure, so a view can show "updated 5s ago" without I/O.
4325
+ *
4326
+ * `updatedAt` is the newest server write among what is shown, as the
4327
+ * store dates it; `undefined` when nothing shown was received (nothing
4328
+ * loaded, or only an optimistic preview). `stale` is any shown field or
4329
+ * connection marked stale; `loading` is any of them in flight. Optimistic
4330
+ * previews do not date: showing only one reads `updatedAt: undefined`.
4331
+ */
4332
+ meta: (bound, model, projection) => metaOf(bound.store.get(model), askedOf(projection)),
4333
+ /**
3531
4334
  * The pure plan for a projection against a Model: the requirements its
3532
4335
  * remote store does not satisfy, under `options` (freshness, force). A
3533
4336
  * Surface's projection is `surface.projection(params)`.
@@ -3558,13 +4361,13 @@ const Remote = {
3558
4361
  */
3559
4362
  prefetch: Effect.fn("Remote.prefetch")(function* (bound, model, projection, options = {}) {
3560
4363
  const store = storeOf(bound, model);
3561
- const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
4364
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
3562
4365
  const at = now();
3563
4366
  const missing = plan(store, requirementsOf(projection), RemotePolicy.toPlan(policy, at));
3564
4367
  if (missing.length === 0) return store;
3565
4368
  yield* Effect.annotateCurrentSpan("requirementCount", missing.length);
3566
4369
  const result = yield* (yield* RemoteClient).read({
3567
- version: 3,
4370
+ version: 4,
3568
4371
  requests: missing
3569
4372
  });
3570
4373
  return writeRead(store, missing, result, at);
@@ -3617,6 +4420,17 @@ const Remote = {
3617
4420
  });
3618
4421
  },
3619
4422
  /**
4423
+ * Forgets everything the server told this Model: values, tombstones,
4424
+ * unavailable fields, connections, live cursors, failures. What is known is
4425
+ * known for a principal, and this is the one boundary for a login, a logout
4426
+ * or a switch of organization: called from `update`, it performs no I/O, and
4427
+ * every active Surface's read and live entries restart, so the screen asks
4428
+ * again as whoever the client now is. A read or stream begun before is
4429
+ * interrupted rather than landing after. A mutation in flight is treated as
4430
+ * applied: its answer writes nothing here.
4431
+ */
4432
+ forget: (bound, model) => bound.store.set(model, forgetRemote(bound.store.get(model))),
4433
+ /**
3620
4434
  * Writes a read result into the store, recording each field's applied window.
3621
4435
  * `requests` are the planned requirements the result answers.
3622
4436
  */
@@ -3633,7 +4447,7 @@ const Remote = {
3633
4447
  query: (request) => client.FoldkitRemoteQuery(request).pipe(Effect.provideContext(context)),
3634
4448
  mutate: (request) => client.FoldkitRemoteMutate(request).pipe(Effect.provideContext(context)),
3635
4449
  live: ({ requirements, after }) => client.FoldkitRemoteLive({
3636
- version: 3,
4450
+ version: 4,
3637
4451
  requirements,
3638
4452
  after
3639
4453
  }).pipe(Stream.map(liveEventOf), Stream.provideContext(context))
@@ -3666,18 +4480,21 @@ const Remote = {
3666
4480
  /**
3667
4481
  * Runs a `Query` through `RemoteClient`, encoding its input from the ref.
3668
4482
  * Pair the result with `Remote.queryMessage` to merge the page into the Model.
4483
+ * With `select`, the server also returns the selected fields of the page's
4484
+ * items, so one response populates both the connection and the store.
3669
4485
  */
3670
- query: Effect.fn("Remote.query")(function* (ref) {
4486
+ query: Effect.fn("Remote.query")(function* (ref, select) {
3671
4487
  const client = yield* RemoteClient;
3672
4488
  const input = yield* Schema.encodeUnknownEffect(ref.Input)(ref.input).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new RemoteQueryError({ message: error.message }))));
3673
4489
  return yield* client.query({
3674
4490
  query: ref.query,
3675
4491
  input,
3676
- window: ref.window
4492
+ window: ref.window,
4493
+ ...select === void 0 ? {} : { select }
3677
4494
  });
3678
4495
  }),
3679
4496
  /** The `RemoteMessage` that merges a query page into its connection. */
3680
- queryMessage: (ref, result) => pageMessage(ref.identity, result, false, ref.window),
4497
+ queryMessage: (ref, result, select, now) => pageMessage(ref.identity, result, false, ref.window, select, now),
3681
4498
  /**
3682
4499
  * The edges a connection shows: its server-known region with pending and
3683
4500
  * confirmed overlays placed around it, minus removed edges and edges whose
@@ -3716,7 +4533,8 @@ const Remote = {
3716
4533
  requestId,
3717
4534
  entities: outcome.entities,
3718
4535
  connections: outcome.connections,
3719
- deleted: outcome.deleted
4536
+ deleted: outcome.deleted,
4537
+ now: (options.now ?? wallClock)()
3720
4538
  });
3721
4539
  return {
3722
4540
  output: outcome.output,
@@ -3743,9 +4561,20 @@ const Remote = {
3743
4561
  live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
3744
4562
  };
3745
4563
  const brandEntries = (entries) => Subscription.make()(() => entries);
4564
+ /** Why a read failed, in words: Remote's own failures say what to do, and a server's error says itself. */
4565
+ const failedReadWords = (error) => {
4566
+ switch (error._tag) {
4567
+ case "DecodeError": return `What the server sent does not decode against the Selection: ${error.message}`;
4568
+ case "Unavailable": return `${error.message} Select without it, or Data.refresh asks again.`;
4569
+ case "Overlaid": return `${error.message} Overlay every field the Selection reads, or select fewer.`;
4570
+ default: return `Its request failed: ${error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`;
4571
+ }
4572
+ };
3746
4573
  /** The bound domain: the descriptor, the binding, and the operations over them. */
3747
4574
  const bindDomain = (definition, store) => {
3748
4575
  const bound = bindRemote(definition, store);
4576
+ const readers = /* @__PURE__ */ new Map();
4577
+ const keptConnections = /* @__PURE__ */ new Set();
3749
4578
  /**
3750
4579
  * What to do with a row a live event says was inserted into a connection.
3751
4580
  *
@@ -3786,7 +4615,12 @@ const bindDomain = (definition, store) => {
3786
4615
  ...live,
3787
4616
  policy: livePolicyFor(model, live.event)
3788
4617
  };
3789
- return store.set(model, updateRemote(store.get(model), resolved));
4618
+ const remote = store.get(model);
4619
+ const next = updateRemote(remote, resolved);
4620
+ return next === remote ? model : store.set(model, next);
4621
+ };
4622
+ const assertOwned = (entry) => {
4623
+ if (bound.contract.owner !== void 0 && entry.owner !== bound.contract.owner) throw new Error(`Remote: Surface "${entry.name}" belongs to another application than domain "${bound.contract.name}"`);
3790
4624
  };
3791
4625
  const domain = {
3792
4626
  ...definition,
@@ -3802,47 +4636,56 @@ const bindDomain = (definition, store) => {
3802
4636
  })))
3803
4637
  };
3804
4638
  },
4639
+ active: (name, projectionOf) => {
4640
+ const { owner } = bound.contract;
4641
+ if (owner === void 0) throw new Error(`Remote: "${name}" reads domain "${bound.contract.name}", whose Model field is a raw optic that names no application; make the domain from an application's field (App.model.remote) to give it reads`);
4642
+ return {
4643
+ name,
4644
+ owner,
4645
+ messages: [],
4646
+ projectionOf
4647
+ };
4648
+ },
3805
4649
  subscriptions: (active, options = {}) => {
3806
4650
  const entries = {};
3807
- const projections = [];
4651
+ for (const entry of Object.values(active)) assertOwned(entry);
3808
4652
  for (const [key, entry] of Object.entries(active)) {
3809
- if (bound.contract.owner !== void 0 && entry.owner !== bound.contract.owner) throw new Error(`Remote: Surface "${entry.name}" belongs to another application than domain "${bound.contract.name}"`);
3810
- const projectionAt = memoizedProjectionOf(entry);
3811
- projections.push(projectionAt);
3812
- const asked = (model) => askedOf(projectionAt(model));
4653
+ const projectionAt = readers.get(entry) ?? memoizedProjectionOf(entry);
4654
+ readers.set(entry, projectionAt);
4655
+ const asked = (model) => askedWhile(projectionAt(model));
3813
4656
  entries[`${key}.read`] = observeEntry(bound, asked, identityMessage, options);
3814
4657
  entries[`${key}.live`] = liveEntry(bound, (model) => asked(model).requirements.filter((requirement) => requirement.live === true), identityMessage, options);
3815
4658
  }
4659
+ for (const identity of options.connections ?? []) keptConnections.add(connectionIdentity(identity));
3816
4660
  entries.retain = {
3817
4661
  dependenciesSchema: retentionRootsSchema,
3818
- modelToDependencies: (model) => rootsOf(projections.flatMap((projectionAt) => {
3819
- const projection = projectionAt(model);
3820
- return projection === void 0 ? [] : [projection];
3821
- }), options),
4662
+ modelToDependencies: (model) => rootsOf([...readers.values()].flatMap((projectionAt) => Option.toArray(projectionAt(model))), { connections: [...keptConnections] }),
3822
4663
  dependenciesToStream: (current) => Stream.fromEffect(Effect.succeed({
3823
4664
  _tag: "RetentionChanged",
3824
4665
  roots: current
3825
4666
  }).pipe(Effect.delay(options.grace ?? 0)))
3826
4667
  };
4668
+ markAll(entries);
3827
4669
  return entries;
3828
4670
  },
3829
4671
  plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
4672
+ meta: (model, projection) => Remote.meta(bound, model, projection),
3830
4673
  storeOf: (model) => storeOf(bound, model),
3831
4674
  confirmed: (projection) => confirmed(bound, projection),
3832
4675
  prefetch: (model, projection, options = {}) => Effect.gen(function* () {
3833
- const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
4676
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
3834
4677
  const client = yield* RemoteClient;
3835
4678
  const planOptions = RemotePolicy.toPlan(policy, now());
3836
4679
  let current = model;
3837
4680
  for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
3838
4681
  const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
3839
- current = reduce(current, pageMessage(query.identity, page, true, query.window));
4682
+ current = reduce(current, pageMessage(query.identity, page, true, query.window, query.select, now()));
3840
4683
  }
3841
4684
  const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
3842
4685
  if (requirements.length === 0) return current;
3843
4686
  const at = now();
3844
4687
  const result = yield* client.read({
3845
- version: 3,
4688
+ version: 4,
3846
4689
  requests: requirements
3847
4690
  });
3848
4691
  return reduce(current, {
@@ -3852,6 +4695,30 @@ const bindDomain = (definition, store) => {
3852
4695
  now: at
3853
4696
  });
3854
4697
  }),
4698
+ satisfy: (model, active, options = {}) => Effect.gen(function* () {
4699
+ const { passes = 8, now = wallClock } = options;
4700
+ const entries = Object.values(active);
4701
+ entries.forEach(assertOwned);
4702
+ const planOptions = RemotePolicy.toPlan(RemotePolicy.cacheFirst, now());
4703
+ const plans = (current, entry) => {
4704
+ const planned = planAsked(store.get(current), askedWhile(projectionOf(entry, current)), planOptions);
4705
+ return planned.queries.length > 0 || planned.requirements.length > 0;
4706
+ };
4707
+ let current = model;
4708
+ for (let pass = 0; pass < passes; pass++) {
4709
+ if (!entries.some((entry) => plans(current, entry))) return current;
4710
+ for (const entry of entries) {
4711
+ const projection = projectionOf(entry, current);
4712
+ if (Option.isSome(projection)) current = yield* domain.prefetch(current, projection.value, { now });
4713
+ }
4714
+ }
4715
+ const reading = entries.filter((entry) => plans(current, entry));
4716
+ if (reading.length === 0) return current;
4717
+ return yield* new RemoteUnsatisfied({
4718
+ surfaces: reading.map((entry) => entry.name),
4719
+ passes
4720
+ });
4721
+ }),
3855
4722
  query: (query, input, options) => {
3856
4723
  assertRegistered(bound, "Query", definition.registry.queries, query.name);
3857
4724
  const { select: given, ...window } = options;
@@ -3860,15 +4727,13 @@ const bindDomain = (definition, store) => {
3860
4727
  const listed = query.Result.entity;
3861
4728
  if (listed === void 0) throw new Error(`Remote: query "${query.name}" is not a connection over an entity, so it has no page to select`);
3862
4729
  if (select.entity !== listed) throw new Error(`Remote: the selection is of "${select.entity}", but query "${query.name}" lists "${listed}"`);
3863
- const ref = {
3864
- ...query.ref(input),
3865
- window: pickWindow(window)
3866
- };
3867
- const relation = relationOf(select);
4730
+ const { ref, relation, requirement } = readContract(query.ref(input), select, window);
3868
4731
  const relationKey = stableStringify(relation);
4732
+ const sizeOf = (remote) => windowSize(remote, ref);
4733
+ const shown = (remote, connection) => cutToWindow(visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities), ref.window, sizeOf(remote));
3869
4734
  const failedItem = (remote, connection) => {
3870
4735
  if (Object.keys(remote.failures.fields).length === 0) return void 0;
3871
- for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
4736
+ for (const edge of shown(remote, connection).edges) {
3872
4737
  if (edge.ref.entity !== relation.entity) continue;
3873
4738
  const error = failureOf(remote, edge.ref.entity, edge.ref.id, relation);
3874
4739
  if (error !== void 0) return {
@@ -3879,21 +4744,26 @@ const bindDomain = (definition, store) => {
3879
4744
  };
3880
4745
  const loadingItem = (remote, connection) => {
3881
4746
  if (remote.loading.size === 0) return void 0;
3882
- for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
4747
+ for (const edge of shown(remote, connection).edges) {
3883
4748
  if (edge.ref.entity !== relation.entity) continue;
3884
4749
  if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
3885
4750
  }
3886
4751
  };
3887
4752
  const readPage = (remote, connection) => {
3888
4753
  const visible = visibleStoreOf(remote.entities, remote.optimistic);
3889
- return memoRead(visible, connection, `${ref.identity}\u0000${relationKey}`, () => {
4754
+ return memoRead([
4755
+ visible,
4756
+ connection,
4757
+ remote.optimistic.overlays
4758
+ ], `${ref.identity}\u0000${relationKey}\u0000${String(sizeOf(remote))}`, () => {
3890
4759
  const items = [];
3891
4760
  let refreshing = connection.stale;
3892
- const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
4761
+ const { edges, cutBefore, cutAfter } = shown(remote, connection);
3893
4762
  for (const edge of edges) {
3894
- const assembled = assemble(visible, entityKey(edge.ref.entity, edge.ref.id), relation);
3895
- if (assembled === void 0) return void 0;
3896
- const decoded = Schema.decodeUnknownResult(select.schema)(assembled.values);
4763
+ const key = entityKey(edge.ref.entity, edge.ref.id);
4764
+ const assembled = assemble(visible, key, relation);
4765
+ if (assembled === void 0) return unavailableFailure(visible, key, relation);
4766
+ const decoded = decodeRow(select.schema, visible[key].values, assembled.values);
3897
4767
  if (Result.isFailure(decoded)) return {
3898
4768
  _tag: "Failed",
3899
4769
  error: {
@@ -3906,8 +4776,8 @@ const bindDomain = (definition, store) => {
3906
4776
  }
3907
4777
  const page = {
3908
4778
  items,
3909
- hasNext: hasNext(connection),
3910
- hasPrevious: hasPrevious(connection)
4779
+ hasNext: cutAfter || hasNext(connection),
4780
+ hasPrevious: cutBefore || hasPrevious(connection)
3911
4781
  };
3912
4782
  return refreshing ? {
3913
4783
  _tag: "Refreshing",
@@ -3918,12 +4788,6 @@ const bindDomain = (definition, store) => {
3918
4788
  };
3919
4789
  });
3920
4790
  };
3921
- const requirement = {
3922
- identity: ref.identity,
3923
- window: ref.window,
3924
- select: relation,
3925
- ref
3926
- };
3927
4791
  return {
3928
4792
  Model: remoteDataSchema(pageSchema(select.schema)),
3929
4793
  dependencies: [],
@@ -3959,7 +4823,9 @@ const bindDomain = (definition, store) => {
3959
4823
  previous: read.value
3960
4824
  };
3961
4825
  case "Failed": return read;
3962
- default: return {
4826
+ case "Initial":
4827
+ case "Loading":
4828
+ case "NotFound": return {
3963
4829
  _tag: "Failed",
3964
4830
  error: failure
3965
4831
  };
@@ -3967,32 +4833,23 @@ const bindDomain = (definition, store) => {
3967
4833
  }
3968
4834
  };
3969
4835
  },
3970
- next: (model, projection) => {
3971
- const segments = store.get(model).connections[projection.ref.identity]?.segments ?? [];
3972
- const end = segments[segments.length - 1]?.end;
3973
- return end?._tag === "Cursor" ? {
3974
- ...projection.ref,
3975
- window: {
3976
- ...pageSize(projection.ref.window, "first"),
3977
- after: end.cursor
3978
- }
3979
- } : void 0;
3980
- },
3981
- previous: (model, projection) => {
3982
- const start = store.get(model).connections[projection.ref.identity]?.segments[0]?.start;
3983
- return start?._tag === "Cursor" ? {
3984
- ...projection.ref,
3985
- window: {
3986
- ...pageSize(projection.ref.window, "last"),
3987
- before: start.cursor
3988
- }
3989
- } : void 0;
4836
+ more: (model, projection) => {
4837
+ const remote = store.get(model);
4838
+ const { ref } = projection;
4839
+ const size = windowSize(remote, ref);
4840
+ const page = ref.window.first ?? ref.window.last;
4841
+ if (size === void 0 || page === void 0 || page === 0) return Option.none();
4842
+ const read = projection.read(model);
4843
+ return (read._tag === "Ready" || read._tag === "Refreshing") && (ref.window.first === void 0 ? read.value.hasPrevious : read.value.hasNext) ? Option.some(bound.store.set(model, updateRemote(remote, {
4844
+ _tag: "WindowGrown",
4845
+ window: windowGrowthKey(ref.identity, stableStringify(ref.window)),
4846
+ size: size + page
4847
+ }))) : Option.none();
3990
4848
  },
3991
4849
  explain: (model, projection, options) => {
3992
4850
  const { ref } = projection;
3993
4851
  const reading = Object.values(options?.surfaces ?? {}).filter((active) => {
3994
- const active_ = active.projectionOf(model);
3995
- return active_ !== void 0 && connectionsOf(active_).some((connection) => connection.identity === ref.identity);
4852
+ return Option.exists(active.projectionOf(model), (shown) => connectionsOf(shown).some((connection) => connection.identity === ref.identity));
3996
4853
  });
3997
4854
  const body = definition.registry.queries.get(ref.query)?.body;
3998
4855
  const [connection] = RemoteConnections.get(projection.metadata);
@@ -4021,8 +4878,7 @@ const bindDomain = (definition, store) => {
4021
4878
  const state = projection.read(model);
4022
4879
  const asked = askedOf(projection);
4023
4880
  const reading = options?.surfaces === void 0 ? void 0 : Object.values(options.surfaces).filter((active) => {
4024
- const shown = active.projectionOf(model);
4025
- return shown !== void 0 && covers(askedOf(shown), asked);
4881
+ return Option.exists(active.projectionOf(model), (shown) => covers(askedOf(shown), asked));
4026
4882
  }).map((active) => active.name);
4027
4883
  const withSurfaces = reading === void 0 ? {} : { surfaces: reading };
4028
4884
  switch (state._tag) {
@@ -4048,7 +4904,7 @@ const bindDomain = (definition, store) => {
4048
4904
  };
4049
4905
  case "Failed": return {
4050
4906
  state: state._tag,
4051
- message: state.error._tag === "DecodeError" ? `What the server sent does not decode against the Selection: ${state.error.message}` : `Its request failed: ${state.error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`,
4907
+ message: failedReadWords(state.error),
4052
4908
  ...withSurfaces
4053
4909
  };
4054
4910
  case "Initial":
@@ -4088,7 +4944,7 @@ const bindDomain = (definition, store) => {
4088
4944
  assembledAll = false;
4089
4945
  continue;
4090
4946
  }
4091
- const decoded = Schema.decodeUnknownResult(over.selection.schema)(assembled.values);
4947
+ const decoded = decodeRow(over.selection.schema, visible[key].values, assembled.values);
4092
4948
  if (Result.isFailure(decoded)) {
4093
4949
  assembledAll = false;
4094
4950
  continue;
@@ -4101,33 +4957,15 @@ const bindDomain = (definition, store) => {
4101
4957
  };
4102
4958
  },
4103
4959
  refresh: (model, target) => Remote.refresh(bound, model, target),
4104
- overlay: (model, id, optimistic) => bound.store.set(model, updateRemote(bound.store.get(model), {
4960
+ forget: (model) => Remote.forget(bound, model),
4961
+ overlay: (model, id, optimistic) => reduce(model, {
4105
4962
  _tag: "OverlayShown",
4106
4963
  id,
4107
4964
  optimistic
4108
- })),
4109
- lift: (model, id) => {
4110
- const remote = bound.store.get(model);
4111
- const lifted = updateRemote(remote, {
4112
- _tag: "OverlayLifted",
4113
- id
4114
- });
4115
- return lifted === remote ? model : bound.store.set(model, lifted);
4116
- },
4117
- fetch: (ref) => ({
4118
- name: `Remote.query(${ref.query})`,
4119
- args: {
4120
- connection: ref.identity,
4121
- window: ref.window
4122
- },
4123
- effect: Remote.query(ref).pipe(Effect.match({
4124
- onFailure: (error) => ({
4125
- _tag: "QueryFailed",
4126
- connection: ref.identity,
4127
- error: remoteError(error)
4128
- }),
4129
- onSuccess: (page) => pageMessage(ref.identity, page, false, ref.window)
4130
- }))
4965
+ }),
4966
+ lift: (model, id) => reduce(model, {
4967
+ _tag: "OverlayLifted",
4968
+ id
4131
4969
  }),
4132
4970
  mutate: (model, mutation, input, options = {}) => {
4133
4971
  assertRegistered(bound, "Mutation", definition.registry.mutations, mutation.name);
@@ -4161,7 +4999,8 @@ const bindDomain = (definition, store) => {
4161
4999
  requestId,
4162
5000
  entities: outcome.entities,
4163
5001
  connections: outcome.connections,
4164
- deleted: outcome.deleted
5002
+ deleted: outcome.deleted,
5003
+ now: (options.now ?? wallClock)()
4165
5004
  })
4166
5005
  }))
4167
5006
  }
@@ -4181,18 +5020,41 @@ const bindDomain = (definition, store) => {
4181
5020
  return domain;
4182
5021
  };
4183
5022
  /** The window keys of a `QueryOptions`, and only those, without the undefined ones. */
5023
+ /**
5024
+ * How many rows a read of `ref` shows: its window's `first` or `last`, as
5025
+ * `Data.more` has grown it. `undefined` for a window with a cursor or no size,
5026
+ * which shows whatever the connection holds.
5027
+ */
5028
+ const windowSize = (remote, ref) => {
5029
+ const { first, last, after, before } = ref.window;
5030
+ if (after !== void 0 || before !== void 0) return void 0;
5031
+ const size = first ?? last;
5032
+ return size === void 0 ? void 0 : remote.grown[windowGrowthKey(ref.identity, stableStringify(ref.window))] ?? size;
5033
+ };
5034
+ /** The edges a window of `size` shows, from the start (`first`) or the end (`last`), and which end it cut. */
5035
+ const cutToWindow = (edges, window, size) => {
5036
+ if (size === void 0 || edges.length <= size) return {
5037
+ edges,
5038
+ cutBefore: false,
5039
+ cutAfter: false
5040
+ };
5041
+ return window.first === void 0 ? {
5042
+ edges: edges.slice(edges.length - size),
5043
+ cutBefore: true,
5044
+ cutAfter: false
5045
+ } : {
5046
+ edges: edges.slice(0, size),
5047
+ cutBefore: false,
5048
+ cutAfter: true
5049
+ };
5050
+ };
4184
5051
  const pickWindow = (window) => ({
4185
5052
  ...window.first === void 0 ? {} : { first: window.first },
4186
5053
  ...window.last === void 0 ? {} : { last: window.last },
4187
5054
  ...window.after === void 0 ? {} : { after: window.after },
4188
5055
  ...window.before === void 0 ? {} : { before: window.before }
4189
5056
  });
4190
- /** The page size of a window, carried onto the next or previous page's window under `key`. */
4191
- const pageSize = (window, key) => {
4192
- const size = window.first ?? window.last;
4193
- return size === void 0 ? {} : { [key]: size };
4194
- };
4195
5057
  //#endregion
4196
- export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unknown, updateRemote, visibleItems, visibleStore, windowKey, withRefreshRequested, writeEntities, writeEntity, writeRead };
5058
+ export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, RemoteUnsatisfied, Requirement, Selection, SettledFields, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, deadlineOf, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, forgetRemote, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isFieldUnavailable, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, onlyOverlaid, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, setUnavailable, settleFailure, settleSuccess, stableStringify, targetsOf, terminal, tombstone, unavailableOf, unknown, updateRemote, visibleItems, visibleStore, windowGrowthKey, windowKey, withRefreshRequested, writeEntities, writeEntity, writeRead };
4197
5059
 
4198
5060
  //# sourceMappingURL=index.mjs.map