foldkit-remote 0.8.0 → 0.10.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,12 +1,12 @@
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
4
  import { mapMessage } from "foldkit/command";
5
5
  import { defineMessageUnion } from "foldkit/message";
6
6
  import * as Subscription from "foldkit/subscription";
7
7
  import { Metadata } from "foldkit-surface";
8
- import { Rpc, RpcGroup } from "effect/unstable/rpc";
9
- import { KeyValueStore } from "effect/unstable/persistence";
8
+ import { Rpc, RpcGroup } from "effect/rpc";
9
+ import { KeyValueStore } from "effect/persistence";
10
10
  //#region src/requirement.ts
11
11
  /**
12
12
  * What a Remote Projection needs from the server: entity slices and query
@@ -110,7 +110,31 @@ const requirementsOf = (projection) => RemoteRequirements.get(projection.metadat
110
110
  /** The query connections a Projection carries. */
111
111
  const connectionsOf = (projection) => RemoteConnections.get(projection.metadata);
112
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
113
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
+ */
114
138
  /** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
115
139
  const RelationAnnotation = "foldkitRemoteRelation";
116
140
  /** The target entity's name on the same codec, so a nested selection of another entity is refused. */
@@ -176,8 +200,8 @@ const shapeOf = (ast, nullable) => {
176
200
  nullable,
177
201
  entity: String(ast.annotations?.[RelationEntityAnnotation])
178
202
  };
179
- if (ast._tag === "Arrays") {
180
- const item = ast.rest?.[0];
203
+ if (SchemaAST.isArrays(ast)) {
204
+ const item = ast.rest[0];
181
205
  const inner = item === void 0 ? void 0 : shapeOf(item, false);
182
206
  return inner?.kind === "one" ? {
183
207
  kind: "many",
@@ -185,10 +209,9 @@ const shapeOf = (ast, nullable) => {
185
209
  entity: inner.entity
186
210
  } : void 0;
187
211
  }
188
- if (ast._tag === "Union") {
189
- const members = ast.types ?? [];
190
- const optional = members.some((member) => member._tag === "Null" || member._tag === "Undefined");
191
- 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) {
192
215
  const shape = shapeOf(member, optional);
193
216
  if (shape !== void 0) return shape;
194
217
  }
@@ -201,8 +224,8 @@ const shapeOf = (ast, nullable) => {
201
224
  *
202
225
  * Presence is tracked **separately** from values, so `undefined`/`null`/absent/
203
226
  * stale/not-found are distinct states and a missing field is never inferred from
204
- * `value === undefined`. Nothing here performs I/O; every operation returns a
205
- * 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.
206
229
  */
207
230
  /** The separator keeps an untrusted id from colliding with a prototype key. */
208
231
  const entityKey = (entity, id) => `${entity}:${id}`;
@@ -237,7 +260,7 @@ const written = (previous, values, now, windows) => {
237
260
  }
238
261
  }
239
262
  return {
240
- values: {
263
+ values: Object.keys(values).every((field) => Object.hasOwn(previous.values, field) && sameData(previous.values[field], values[field])) ? previous.values : {
241
264
  ...previous.values,
242
265
  ...values
243
266
  },
@@ -257,7 +280,7 @@ const written = (previous, values, now, windows) => {
257
280
  const writeEntities = (store, writes, now = 0) => {
258
281
  if (writes.length === 0) return store;
259
282
  const next = { ...store };
260
- 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);
261
284
  return next;
262
285
  };
263
286
  /** `writeEntities` for one entity. */
@@ -322,7 +345,7 @@ const isFieldUnavailable = (store, key, field) => {
322
345
  return value !== void 0 && !value.tombstone && value.unavailable.has(field);
323
346
  };
324
347
  /** Records that the entity is known to be absent, so it is not refetched. */
325
- const tombstone = (store, key) => replace(store, key, {
348
+ const tombstone = (store, key) => isTombstone(store, key) ? store : replace(store, key, {
326
349
  values: {},
327
350
  present: /* @__PURE__ */ new Set(),
328
351
  stale: /* @__PURE__ */ new Set(),
@@ -445,8 +468,11 @@ const mutationStatus = (state, requestId) => {
445
468
  * with the same `requestId` (a retry, or a live event describing the same change)
446
469
  * does not re-apply the entities, but still clears `pending`.
447
470
  */
448
- const reconcileMutation = (store, state, requestId, entities, deleted = []) => {
449
- 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));
450
476
  const applied = remember(state.applied, requestId);
451
477
  const pending = new Set(state.pending);
452
478
  pending.delete(requestId);
@@ -617,7 +643,7 @@ const Query = {
617
643
  //#endregion
618
644
  //#region src/wire.ts
619
645
  /**
620
- * The Remote wire: Effect RPC semantics over `effect/unstable/rpc`. Remote owns
646
+ * The Remote wire: Effect RPC semantics over `effect/rpc`. Remote owns
621
647
  * the message shapes; Effect owns the transport. `Make`ing reads batch, mutations
622
648
  * preserve order, and live data is a stream.
623
649
  */
@@ -806,7 +832,13 @@ const WireBoundary = Schema.Union([
806
832
  const QueryRequest = Schema.Struct({
807
833
  query: Schema.String,
808
834
  input: Schema.Unknown,
809
- 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)
810
842
  });
811
843
  const QueryEdge = Schema.Struct({
812
844
  entity: Schema.String,
@@ -816,7 +848,14 @@ const QueryEdge = Schema.Struct({
816
848
  const QueryResult = Schema.Struct({
817
849
  edges: Schema.Array(QueryEdge),
818
850
  start: WireBoundary,
819
- 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))
820
859
  });
821
860
  const QueryRpc = Rpc.make("FoldkitRemoteQuery", {
822
861
  payload: QueryRequest,
@@ -884,7 +923,8 @@ const coalesceQueries = (query, options = {}) => Effect.gen(function* () {
884
923
  key: stableStringify([
885
924
  request.query,
886
925
  request.input,
887
- request.window
926
+ request.window,
927
+ request.select ?? null
888
928
  ]),
889
929
  request
890
930
  }), resolver);
@@ -1694,12 +1734,13 @@ const ConnectionChange = {
1694
1734
  const beginOptimistic = (optimistic, requestId, operations) => {
1695
1735
  const patches = operations.filter((operation) => !isConnectionChange(operation));
1696
1736
  const changes = operations.filter(isConnectionChange);
1737
+ if (patches.length === 0 && changes.length === 0) return optimistic;
1697
1738
  return {
1698
1739
  layers: patches.length === 0 ? optimistic.layers : [...optimistic.layers, {
1699
1740
  id: requestId,
1700
1741
  patches
1701
1742
  }],
1702
- overlays: [...optimistic.overlays, ...toOverlays(requestId, changes)]
1743
+ overlays: changes.length === 0 ? optimistic.overlays : [...optimistic.overlays, ...toOverlays(requestId, changes)]
1703
1744
  };
1704
1745
  };
1705
1746
  /**
@@ -1708,8 +1749,9 @@ const beginOptimistic = (optimistic, requestId, operations) => {
1708
1749
  * its place among other pending inserts.
1709
1750
  */
1710
1751
  const confirm = (optimistic, requestId, id, changes) => {
1711
- const confirmed = toOverlays(id, changes);
1712
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);
1713
1755
  const others = optimistic.overlays.filter((overlay) => overlay.id !== requestId);
1714
1756
  return {
1715
1757
  ...optimistic,
@@ -1728,37 +1770,62 @@ const addLayer = (optimistic, layer) => ({
1728
1770
  ...optimistic,
1729
1771
  layers: [...optimistic.layers, layer]
1730
1772
  });
1731
- const removeLayer = (optimistic, id) => ({
1732
- ...optimistic,
1733
- layers: optimistic.layers.filter((layer) => layer.id !== id)
1734
- });
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
+ };
1735
1780
  const addOverlay = (optimistic, overlay) => ({
1736
1781
  ...optimistic,
1737
1782
  overlays: [...optimistic.overlays, overlay]
1738
1783
  });
1739
- const removeOverlay = (optimistic, id) => ({
1740
- ...optimistic,
1741
- overlays: optimistic.overlays.filter((overlay) => overlay.id !== id)
1742
- });
1743
- /** Base store with every layer applied in order. Later layers win. */
1744
- 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
+ })));
1745
1806
  /**
1746
1807
  * A fresh page is newer than any settled overlay it covers: an edge the page
1747
1808
  * carries no longer needs an insert overlay, and a `remove` overlay hiding it
1748
1809
  * is stale (the server says it is back). A pending request's overlays are
1749
1810
  * left alone; they settle with the request.
1750
1811
  */
1751
- const pruneOverlays = (optimistic, connection, covered, pending) => ({
1752
- ...optimistic,
1753
- overlays: optimistic.overlays.flatMap((overlay) => {
1812
+ const pruneOverlays = (optimistic, connection, covered, pending) => {
1813
+ let changed = false;
1814
+ const overlays = optimistic.overlays.flatMap((overlay) => {
1754
1815
  if (overlay.connection !== connection || pending.has(overlay.id)) return [overlay];
1755
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;
1756
1819
  return edges.length === 0 ? [] : [{
1757
1820
  ...overlay,
1758
1821
  edges
1759
1822
  }];
1760
- })
1761
- });
1823
+ });
1824
+ return changed ? {
1825
+ ...optimistic,
1826
+ overlays
1827
+ } : optimistic;
1828
+ };
1762
1829
  /** Everything a request owns: its layer and its overlays. */
1763
1830
  const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic, requestId), requestId);
1764
1831
  /**
@@ -1766,12 +1833,12 @@ const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic,
1766
1833
  * a later layer re-wins on rebase. Confirmed connection changes are recorded
1767
1834
  * once per request, as overlays the request no longer owns.
1768
1835
  */
1769
- const settleSuccess = (base, optimistic, state, requestId, entities, connections = [], deleted = []) => {
1770
- const reconciled = reconcileMutation(base, state, requestId, entities, deleted);
1836
+ const settleSuccess = (base, optimistic, state, requestId, answer) => {
1837
+ const reconciled = reconcileMutation(base, state, requestId, answer);
1771
1838
  return {
1772
1839
  store: reconciled.store,
1773
1840
  state: reconciled.state,
1774
- 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 ?? [])
1775
1842
  };
1776
1843
  };
1777
1844
  /** A failed request's layer and overlays are dropped; the base was never mutated. */
@@ -1846,11 +1913,6 @@ const recordBoundary = (state, connection, position) => {
1846
1913
  }
1847
1914
  };
1848
1915
  };
1849
- /**
1850
- * A subscriber is woken only if the event changed a field it selects. A
1851
- * change to an unselected field is skipped entirely.
1852
- */
1853
- const shouldWake = (changed, selected) => changed.some((field) => selected.includes(field));
1854
1916
  const applyEntityEvent = (state, store, event, now = 0) => {
1855
1917
  const outcome = classifyLive(state, event.cursor);
1856
1918
  if (outcome !== "applied") return {
@@ -1868,9 +1930,13 @@ const applyEntityEvent = (state, store, event, now = 0) => {
1868
1930
  };
1869
1931
  const removeEdgeOverlays = (optimistic, connection, key) => ({
1870
1932
  ...optimistic,
1871
- overlays: optimistic.overlays.map((overlay) => overlay.connection !== connection || overlay.position === "remove" ? overlay : {
1872
- ...overlay,
1873
- 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
+ };
1874
1940
  })
1875
1941
  });
1876
1942
  const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
@@ -1960,10 +2026,13 @@ const followRelation = (value, relation) => targetsOf(value, relation).map((ref)
1960
2026
  /**
1961
2027
  * The earliest moment a value `requirements` read, following the relations the
1962
2028
  * store holds as `plan` does, goes stale under `freshness`, with the
1963
- * requirements of the entities that go stale then. Nothing when no value is
1964
- * both present and still fresh: what is already expired or stale is planned,
1965
- * not awaited, which is what keeps a timer from firing on what it already
1966
- * fired on.
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.
1967
2036
  */
1968
2037
  const deadlineOf = (store, requirements, freshness) => {
1969
2038
  let at;
@@ -1972,9 +2041,9 @@ const deadlineOf = (store, requirements, freshness) => {
1972
2041
  const key = entityKey(group.entity, group.id);
1973
2042
  const entry = store[key];
1974
2043
  if (entry === void 0 || entry.tombstone) return;
1975
- if (group.fields.some((field) => entry.present.has(field) && !entry.stale.has(field))) {
2044
+ if (group.fields.some((field) => entry.present.has(field) && !entry.stale.has(field) || entry.unavailable.has(field))) {
1976
2045
  const expires = entry.updatedAt + freshness.freshness;
1977
- if (expires > freshness.now) {
2046
+ if (expires >= freshness.now && Number.isFinite(expires)) {
1978
2047
  if (at === void 0 || expires < at) {
1979
2048
  at = expires;
1980
2049
  due = [group];
@@ -2076,17 +2145,25 @@ const reachable = (store, roots, connections, optimistic, pending) => {
2076
2145
  const gc = (state, roots) => {
2077
2146
  const pending = state.mutations.pending;
2078
2147
  const kept = reachable(state.entities, roots, state.connections, state.optimistic, pending);
2079
- const entities = Object.fromEntries(Object.entries(state.entities).filter(([key]) => kept.has(key)));
2148
+ const entities = keptOf(state.entities, (key) => kept.has(key));
2080
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));
2081
2152
  return {
2082
2153
  entities,
2083
- connections: Object.fromEntries(Object.entries(state.connections).filter(([identity]) => keptConnections.has(identity))),
2084
- optimistic: {
2154
+ connections,
2155
+ optimistic: overlays.length === state.optimistic.overlays.length ? state.optimistic : {
2085
2156
  layers: state.optimistic.layers,
2086
- overlays: state.optimistic.overlays.filter((overlay) => pending.has(overlay.id) || keptConnections.has(overlay.connection))
2157
+ overlays
2087
2158
  }
2088
2159
  };
2089
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
+ };
2090
2167
  //#endregion
2091
2168
  //#region src/persistence.ts
2092
2169
  /**
@@ -2231,7 +2308,7 @@ const hydrate = (raw, options = {}) => {
2231
2308
  * an entry the current store already has (it is at least as fresh as a
2232
2309
  * snapshot taken earlier). Either way, keys only the current store has stay.
2233
2310
  */
2234
- const mergeStores = (current, snapshot, policy) => policy === "replace" ? {
2311
+ const mergeStores = (current, snapshot, policy) => Object.keys(snapshot).length === 0 ? current : policy === "replace" ? {
2235
2312
  ...current,
2236
2313
  ...snapshot
2237
2314
  } : {
@@ -2392,7 +2469,9 @@ const RemoteData = {
2392
2469
  error: data.error,
2393
2470
  previous: f(data.previous)
2394
2471
  };
2395
- default: return data;
2472
+ case "Initial":
2473
+ case "Loading":
2474
+ case "NotFound": return data;
2396
2475
  }
2397
2476
  }
2398
2477
  };
@@ -2440,6 +2519,7 @@ const forgetRemote = (model) => {
2440
2519
  const initialRemoteModel = {
2441
2520
  entities: emptyStore,
2442
2521
  connections: {},
2522
+ grown: {},
2443
2523
  optimistic: emptyOptimistic,
2444
2524
  live: {},
2445
2525
  mutations: emptyMutationState,
@@ -2453,6 +2533,7 @@ const runtimeSchema = Schema.Unknown;
2453
2533
  const remoteModelSchema = () => Schema.Struct({
2454
2534
  entities: runtimeSchema,
2455
2535
  connections: Schema.Record(Schema.String, runtimeSchema),
2536
+ grown: Schema.Record(Schema.String, Schema.Number),
2456
2537
  optimistic: runtimeSchema,
2457
2538
  live: Schema.Record(Schema.String, runtimeSchema),
2458
2539
  mutations: runtimeSchema,
@@ -2509,7 +2590,8 @@ const remoteMessageCases = {
2509
2590
  deleted: Schema.optional(Schema.Array(Schema.Struct({
2510
2591
  entity: Schema.String,
2511
2592
  id: Schema.String
2512
- })))
2593
+ }))),
2594
+ now: Schema.Number
2513
2595
  },
2514
2596
  MutationFailed: {
2515
2597
  requestId: Schema.String,
@@ -2520,6 +2602,10 @@ const remoteMessageCases = {
2520
2602
  optimistic: Schema.Array(Schema.Unknown)
2521
2603
  },
2522
2604
  OverlayLifted: { id: Schema.String },
2605
+ WindowGrown: {
2606
+ window: Schema.String,
2607
+ size: Schema.Number
2608
+ },
2523
2609
  LiveReceived: {
2524
2610
  stream: Schema.String,
2525
2611
  event: Schema.Unknown,
@@ -2530,7 +2616,11 @@ const remoteMessageCases = {
2530
2616
  ConnectionMerged: {
2531
2617
  connection: Schema.String,
2532
2618
  page: Schema.Unknown,
2533
- 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)
2534
2624
  },
2535
2625
  ConnectionInvalidated: { connection: Schema.String },
2536
2626
  ConnectionRefreshed: { connection: Schema.String },
@@ -2550,6 +2640,18 @@ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(
2550
2640
  })));
2551
2641
  /** An overlay's layer, named apart from every request's: a request id is the application's to choose too. */
2552
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
+ };
2553
2655
  const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
2554
2656
  /**
2555
2657
  * One mark per requested field, `entity\0id\0field`. A read is identified by
@@ -2565,13 +2667,13 @@ const fieldMarks = function* (requests) {
2565
2667
  const withLoading = (loading, requests) => {
2566
2668
  const next = new Set(loading);
2567
2669
  for (const mark of fieldMarks(requests)) next.add(mark);
2568
- return next;
2670
+ return next.size === loading.size ? loading : next;
2569
2671
  };
2570
2672
  const withoutLoading = (loading, requests) => {
2571
2673
  if (loading.size === 0) return loading;
2572
2674
  const next = new Set(loading);
2573
2675
  for (const mark of fieldMarks(requests)) next.delete(mark);
2574
- return next;
2676
+ return next.size === loading.size ? loading : next;
2575
2677
  };
2576
2678
  /**
2577
2679
  * A connection's mark. The leading separator keeps it out of the field marks'
@@ -2600,6 +2702,14 @@ const prunedLoading = (loading, roots, reached) => {
2600
2702
  return kept.length === loading.size ? loading : new Set(kept);
2601
2703
  };
2602
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
+ /**
2603
2713
  * Drops the refresh marks of entities and connections that were collected.
2604
2714
  * Nothing observes a collected entity — the retention roots are the active
2605
2715
  * Surfaces — so no read entry's generation can fall back when its mark goes.
@@ -2686,15 +2796,20 @@ const isLoading = (model, entity, id, fields) => {
2686
2796
  return fields.some((field) => model.loading.has(fieldMark(entity, id, field)));
2687
2797
  };
2688
2798
  const setConnectionStale = (connections, connection, stale) => {
2689
- const current = connections[connection] ?? emptyConnection;
2799
+ const current = connections[connection];
2800
+ if (current?.stale === stale) return connections;
2690
2801
  return {
2691
2802
  ...connections,
2692
2803
  [connection]: {
2693
- ...current,
2804
+ ...current ?? emptyConnection,
2694
2805
  stale
2695
2806
  }
2696
2807
  };
2697
2808
  };
2809
+ const withLiveState = (live, stream, state) => live[stream] === state ? live : {
2810
+ ...live,
2811
+ [stream]: state
2812
+ };
2698
2813
  const markGap = (model, stream) => model.gaps.has(stream) ? model : {
2699
2814
  ...model,
2700
2815
  gaps: /* @__PURE__ */ new Set([...model.gaps, stream])
@@ -2706,9 +2821,23 @@ const clearGap = (model, stream) => model.gaps.has(stream) ? {
2706
2821
  /**
2707
2822
  * The pure reducer all four producers share. A live event that arrives ahead of
2708
2823
  * its cursor is a gap: it is not applied, and the stream is recorded so the host
2709
- * 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.
2710
2830
  */
2711
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) => {
2712
2841
  switch (message._tag) {
2713
2842
  case "ReadReceived": return {
2714
2843
  ...model,
@@ -2731,6 +2860,7 @@ const updateRemote = (model, message) => {
2731
2860
  return {
2732
2861
  ...model,
2733
2862
  ...retained,
2863
+ grown: keptWindows(model.grown, retained.connections),
2734
2864
  refresh: prunedRefresh(model.refresh, retained),
2735
2865
  failures: prunedFailures(model.failures, message.roots, reached),
2736
2866
  loading: prunedLoading(model.loading, message.roots, reached)
@@ -2750,7 +2880,7 @@ const updateRemote = (model, message) => {
2750
2880
  ...model,
2751
2881
  entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
2752
2882
  failures: withoutFieldFailures(model.failures, written),
2753
- connections: message.merge === "replace" ? {
2883
+ connections: restored.length === 0 ? model.connections : message.merge === "replace" ? {
2754
2884
  ...model.connections,
2755
2885
  ...Object.fromEntries(restored)
2756
2886
  } : {
@@ -2764,7 +2894,7 @@ const updateRemote = (model, message) => {
2764
2894
  const marks = marksOf(message.requests);
2765
2895
  const entities = setUnavailable(setStale(asked, marks, true), marks, false);
2766
2896
  const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
2767
- return entities === model.entities && failures === model.failures ? model : {
2897
+ return {
2768
2898
  ...model,
2769
2899
  entities,
2770
2900
  failures
@@ -2789,7 +2919,7 @@ const updateRemote = (model, message) => {
2789
2919
  optimistic: beginOptimistic(model.optimistic, message.requestId, message.optimistic ?? [])
2790
2920
  };
2791
2921
  case "MutationSucceeded": {
2792
- 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);
2793
2923
  return {
2794
2924
  ...model,
2795
2925
  entities: settled.store,
@@ -2802,6 +2932,13 @@ const updateRemote = (model, message) => {
2802
2932
  ...model,
2803
2933
  optimistic: beginOptimistic(settleFailure(model.optimistic, overlayLayer(message.id)), overlayLayer(message.id), message.optimistic)
2804
2934
  };
2935
+ case "WindowGrown": return {
2936
+ ...model,
2937
+ grown: {
2938
+ ...model.grown,
2939
+ [message.window]: message.size
2940
+ }
2941
+ };
2805
2942
  case "OverlayLifted": {
2806
2943
  const optimistic = settleFailure(model.optimistic, overlayLayer(message.id));
2807
2944
  return optimistic.layers.length === model.optimistic.layers.length && optimistic.overlays.length === model.optimistic.overlays.length ? model : {
@@ -2827,10 +2964,7 @@ const updateRemote = (model, message) => {
2827
2964
  return clearGap({
2828
2965
  ...model,
2829
2966
  entities: applied.store,
2830
- live: {
2831
- ...model.live,
2832
- [message.stream]: applied.state
2833
- },
2967
+ live: withLiveState(model.live, message.stream, applied.state),
2834
2968
  failures: withoutFieldFailures(model.failures, settled)
2835
2969
  }, message.stream);
2836
2970
  }
@@ -2839,10 +2973,7 @@ const updateRemote = (model, message) => {
2839
2973
  const next = clearGap({
2840
2974
  ...model,
2841
2975
  optimistic: applied.optimistic,
2842
- live: {
2843
- ...model.live,
2844
- [message.stream]: applied.state
2845
- }
2976
+ live: withLiveState(model.live, message.stream, applied.state)
2846
2977
  }, message.stream);
2847
2978
  return applied.invalidated === void 0 ? next : updateRemote(next, {
2848
2979
  _tag: "ConnectionInvalidated",
@@ -2852,12 +2983,23 @@ const updateRemote = (model, message) => {
2852
2983
  case "GapCleared": return clearGap(model, message.stream);
2853
2984
  case "ConnectionMerged": {
2854
2985
  const current = model.connections[message.connection] ?? emptyConnection;
2855
- const failures = withoutConnectionFailure(model.failures, message.connection);
2856
2986
  const loading = withoutQueryLoading(model.loading, message.connection);
2857
2987
  const replaces = message.refreshes === true && current.stale;
2858
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)]);
2859
3000
  return {
2860
3001
  ...model,
3002
+ entities,
2861
3003
  connections: {
2862
3004
  ...model.connections,
2863
3005
  [message.connection]: message.refreshes === true ? {
@@ -2870,13 +3012,11 @@ const updateRemote = (model, message) => {
2870
3012
  loading
2871
3013
  };
2872
3014
  }
2873
- case "ConnectionInvalidated":
2874
- if (model.connections[message.connection]?.stale === true && !(message.connection in model.failures.connections)) return model;
2875
- return {
2876
- ...model,
2877
- connections: setConnectionStale(model.connections, message.connection, true),
2878
- failures: withoutConnectionFailure(model.failures, message.connection)
2879
- };
3015
+ case "ConnectionInvalidated": return {
3016
+ ...model,
3017
+ connections: setConnectionStale(model.connections, message.connection, true),
3018
+ failures: withoutConnectionFailure(model.failures, message.connection)
3019
+ };
2880
3020
  case "QueryFailed": return {
2881
3021
  ...model,
2882
3022
  failures: {
@@ -3381,6 +3521,67 @@ const resumePart = (domain, options = {}) => ({
3381
3521
  */
3382
3522
  const descriptorOf = (entity) => Entity$1.is(entity) ? Entity.from(entity) : entity;
3383
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
+ };
3384
3585
  /**
3385
3586
  * The store a read or plan sees: the base with every pending optimistic layer
3386
3587
  * applied, so a request's patches show until it settles and a temporary id is
@@ -3408,10 +3609,10 @@ const confirmed = (bound, projection) => ({
3408
3609
  });
3409
3610
  const visibleStores = /* @__PURE__ */ new WeakMap();
3410
3611
  const visibleStoreOf = (entities, optimistic) => {
3411
- let byStore = visibleStores.get(optimistic);
3612
+ let byStore = visibleStores.get(optimistic.layers);
3412
3613
  if (byStore === void 0) {
3413
3614
  byStore = /* @__PURE__ */ new WeakMap();
3414
- visibleStores.set(optimistic, byStore);
3615
+ visibleStores.set(optimistic.layers, byStore);
3415
3616
  }
3416
3617
  let visible = byStore.get(entities);
3417
3618
  if (visible === void 0) {
@@ -3436,25 +3637,71 @@ const unavailableFailure = (store, key, relation) => {
3436
3637
  }
3437
3638
  };
3438
3639
  };
3439
- const readResults = /* @__PURE__ */ new WeakMap();
3440
- const memoRead = (snapshot, by, key, compute) => {
3441
- let byScope = readResults.get(snapshot);
3442
- if (byScope === void 0) {
3443
- byScope = /* @__PURE__ */ new WeakMap();
3444
- readResults.set(snapshot, byScope);
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);
3445
3656
  }
3446
- let results = byScope.get(by);
3447
- if (results === void 0) {
3448
- results = /* @__PURE__ */ new Map();
3449
- 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;
3450
3682
  }
3451
- if (results.has(key)) return results.get(key);
3683
+ if (scope.results.has(key)) return scope.results.get(key);
3452
3684
  const value = compute();
3453
- results.set(key, value);
3685
+ scope.results.set(key, value);
3454
3686
  return value;
3455
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();
3456
3693
  /** The default `toMessage`: the application reduces `RemoteMessage` itself. */
3457
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
+ }) {};
3458
3705
  /** A stable key for a live subscription's requirement set. */
3459
3706
  const liveStreamKey = (requirements) => requirements.map((requirement) => `${entityKey(requirement.entity, requirement.id)}:${[...requirement.fields].sort().join(",")}`).sort().join("|");
3460
3707
  /** Declares a Remote domain: its entities, queries, and mutations, plus the submodel. */
@@ -3498,10 +3745,15 @@ const nothingAsked = {
3498
3745
  requirements: [],
3499
3746
  connections: []
3500
3747
  };
3501
- const askedOf = (projection) => projection === void 0 ? nothingAsked : {
3748
+ const askedOf = (projection) => ({
3502
3749
  requirements: requirementsOf(projection),
3503
3750
  connections: connectionsOf(projection)
3504
- };
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
+ });
3505
3757
  /**
3506
3758
  * Whether `outer` asks for everything `inner` does of one entity: each field,
3507
3759
  * each relation's own slice of its target, and each page window alike.
@@ -3532,17 +3784,78 @@ const covers = (outer, inner) => {
3532
3784
  * failure is what its read shows, and retrying is `Remote.refresh`'s to ask
3533
3785
  * for. The rows it already holds are still planned, since they are on screen.
3534
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
+ };
3535
3844
  const planAsked = (remote, asked, options) => {
3536
3845
  const queries = [];
3537
3846
  const items = [];
3538
3847
  const connections = Requirement.mergeConnections(asked.connections);
3539
- for (const connection of connections) {
3848
+ for (const connection of widest(remote, connections)) {
3540
3849
  const known = remote.connections[connection.identity];
3541
- if (!(options.force !== true && connection.identity in remote.failures.connections) && (known === void 0 || known.stale || options.force === true)) {
3542
- 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 }));
3543
3854
  continue;
3544
3855
  }
3545
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));
3546
3859
  const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
3547
3860
  items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
3548
3861
  }
@@ -3598,7 +3911,8 @@ const queryRequestOf = (query) => {
3598
3911
  try: () => ({
3599
3912
  query: query.identity.slice(0, separator),
3600
3913
  input: JSON.parse(query.identity.slice(separator + 1)),
3601
- window: query.window
3914
+ window: query.window,
3915
+ ...query.select === void 0 ? {} : { select: query.select }
3602
3916
  }),
3603
3917
  catch: () => new RemoteQueryError({ message: `connection "${query.identity}" carries no encoded input` })
3604
3918
  });
@@ -3627,7 +3941,7 @@ const overrun = (window, edges) => {
3627
3941
  got: edges.length
3628
3942
  } : void 0;
3629
3943
  };
3630
- const pageMessage = (connection, result, refreshes = false, window = {}) => {
3944
+ const pageMessage = (connection, result, refreshes = false, window = {}, select, now) => {
3631
3945
  const over = overrun(window, result.edges);
3632
3946
  if (over !== void 0) return {
3633
3947
  _tag: "QueryFailed",
@@ -3637,6 +3951,7 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
3637
3951
  message: `the server returned ${over.got} edges for a window of ${over.asked}`
3638
3952
  }
3639
3953
  };
3954
+ const hasPayload = select !== void 0 && (result.entities !== void 0 && result.entities.length > 0 || result.settled !== void 0 && result.settled.length > 0);
3640
3955
  return {
3641
3956
  _tag: "ConnectionMerged",
3642
3957
  connection,
@@ -3651,7 +3966,13 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
3651
3966
  start: result.start,
3652
3967
  end: result.end
3653
3968
  },
3654
- ...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
+ } : {}
3655
3976
  };
3656
3977
  };
3657
3978
  /**
@@ -3694,14 +4015,14 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
3694
4015
  };
3695
4016
  });
3696
4017
  /** Runs a query and reports the page that refreshes its connection, or the failure. Never fails. */
3697
- const queryMessage = (query) => Effect.gen(function* () {
4018
+ const queryMessage = (query, now = wallClock) => Effect.gen(function* () {
3698
4019
  const client = yield* RemoteClient;
3699
4020
  const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
3700
4021
  return Result.isFailure(result) ? {
3701
4022
  _tag: "QueryFailed",
3702
4023
  connection: query.identity,
3703
4024
  error: remoteError(result.failure)
3704
- } : pageMessage(query.identity, result.success, true, query.window);
4025
+ } : pageMessage(query.identity, result.success, true, query.window, query.select, now());
3705
4026
  });
3706
4027
  /** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
3707
4028
  const PlannedQuery = Schema.Struct({
@@ -3719,9 +4040,10 @@ const PlannedQuery = Schema.Struct({
3719
4040
  * to the restart; a page and its refresh are one `ConnectionMerged`.
3720
4041
  */
3721
4042
  const observeEntry = (bound, askedOf, toMessage, options) => {
3722
- const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
4043
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
3723
4044
  const read = (requirements) => Effect.map(readMessage(requirements, now), toMessage);
3724
- const run = (query) => Effect.map(queryMessage(query), toMessage);
4045
+ const run = (query) => Effect.map(queryMessage(query, now), toMessage);
4046
+ const planned = /* @__PURE__ */ new WeakMap();
3725
4047
  return {
3726
4048
  dependenciesSchema: Schema.Struct({
3727
4049
  requirements: Schema.Array(ReadRequest),
@@ -3734,17 +4056,32 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
3734
4056
  }),
3735
4057
  modelToDependencies: (model) => {
3736
4058
  const remote = bound.store.get(model);
3737
- const planned = planAsked(remote, askedOf(model), RemotePolicy.toPlan(policy, now()));
3738
- return {
3739
- refresh: refreshedAt(remote.refresh, planned.requirements, planned.queries.map((query) => query.identity)),
3740
- requirements: planned.requirements,
3741
- 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 }) => ({
3742
4070
  identity,
3743
4071
  window,
3744
4072
  select
3745
4073
  })),
3746
- expires: planned.expires
4074
+ expires: plan.expires
3747
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;
3748
4085
  },
3749
4086
  dependenciesToStream: ({ requirements, queries, refresh, expires }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
3750
4087
  _tag: "ReadStarted",
@@ -3762,11 +4099,12 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
3762
4099
  })))))
3763
4100
  };
3764
4101
  };
4102
+ const resumeCursor = Schema.Number.pipe(Schema.overrideToEquivalence(() => () => true));
3765
4103
  /** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
3766
4104
  const liveEntry = (bound, requirementsOf, toMessage, options) => ({
3767
4105
  dependenciesSchema: Schema.Struct({
3768
4106
  requirements: Schema.Array(ReadRequest),
3769
- cursor: Schema.Number,
4107
+ cursor: resumeCursor,
3770
4108
  floor: Schema.Number
3771
4109
  }),
3772
4110
  modelToDependencies: (model) => {
@@ -3788,7 +4126,7 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
3788
4126
  _tag: "LiveReceived",
3789
4127
  stream: liveStreamKey(requirements),
3790
4128
  event,
3791
- now: (options.now ?? Date.now)()
4129
+ now: (options.now ?? wallClock)()
3792
4130
  })), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
3793
4131
  _tag: "ReadFailed",
3794
4132
  requests: requirements,
@@ -3796,8 +4134,8 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
3796
4134
  stream: liveStreamKey(requirements)
3797
4135
  }))))
3798
4136
  });
3799
- /** The projection an active Surface has for this Model, if it is active. */
3800
- 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());
3801
4139
  /**
3802
4140
  * `projectionOf` computed once per Model: the read, live, and retain entries
3803
4141
  * all derive their dependencies from the same projection on the same Model
@@ -3809,7 +4147,8 @@ const memoizedProjectionOf = (entry) => {
3809
4147
  return (model) => {
3810
4148
  const key = model;
3811
4149
  if (typeof key !== "object" || key === null) return projectionOf(entry, model);
3812
- if (cache.has(key)) return cache.get(key);
4150
+ const known = cache.get(key);
4151
+ if (known !== void 0) return known;
3813
4152
  const projection = projectionOf(entry, model);
3814
4153
  cache.set(key, projection);
3815
4154
  return projection;
@@ -3860,7 +4199,6 @@ const Remote = {
3860
4199
  fold: (domain, toParentMessage) => {
3861
4200
  const fold = (model, message) => ({ model: domain.reduce(model, message) });
3862
4201
  return Object.assign(fold, {
3863
- fetch: (ref) => mapMessage(domain.fetch(ref), toParentMessage),
3864
4202
  mutate: (...args) => {
3865
4203
  const started = domain.mutate(...args);
3866
4204
  return {
@@ -3925,6 +4263,7 @@ const Remote = {
3925
4263
  const selection = selectionOf(given);
3926
4264
  assertRegistered(bound, "Entity", bound.definition.registry.entities, selection.entity);
3927
4265
  const relation = relationOf(selection);
4266
+ const relationKey = stableStringify(relation);
3928
4267
  return (id) => ({
3929
4268
  Model: remoteDataSchema(selection.schema),
3930
4269
  dependencies: [],
@@ -3935,11 +4274,11 @@ const Remote = {
3935
4274
  read: (root) => {
3936
4275
  const store = storeOf(bound, root);
3937
4276
  const key = entityKey(selection.entity, id);
3938
- const present = memoRead(store, store, `${key}\u0000${stableStringify(relation)}`, () => {
4277
+ const present = memoRead([store], `${key}\u0000${relationKey}`, () => {
3939
4278
  if (isTombstone(store, key)) return { _tag: "NotFound" };
3940
4279
  const assembled = assemble(store, key, relation);
3941
4280
  if (assembled === void 0) return unavailableFailure(store, key, relation);
3942
- const decoded = Schema.decodeUnknownResult(selection.schema)(assembled.values);
4281
+ const decoded = decodeRow(selection.schema, store[key].values, assembled.values);
3943
4282
  return Result.isFailure(decoded) ? {
3944
4283
  _tag: "Failed",
3945
4284
  error: {
@@ -3962,14 +4301,36 @@ const Remote = {
3962
4301
  previous: present.value
3963
4302
  } : present;
3964
4303
  if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
3965
- return failure === void 0 ? { _tag: "Initial" } : {
4304
+ if (failure !== void 0) return {
3966
4305
  _tag: "Failed",
3967
4306
  error: failure
3968
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" };
3969
4319
  }
3970
4320
  });
3971
4321
  },
3972
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
+ /**
3973
4334
  * The pure plan for a projection against a Model: the requirements its
3974
4335
  * remote store does not satisfy, under `options` (freshness, force). A
3975
4336
  * Surface's projection is `surface.projection(params)`.
@@ -4000,7 +4361,7 @@ const Remote = {
4000
4361
  */
4001
4362
  prefetch: Effect.fn("Remote.prefetch")(function* (bound, model, projection, options = {}) {
4002
4363
  const store = storeOf(bound, model);
4003
- const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
4364
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
4004
4365
  const at = now();
4005
4366
  const missing = plan(store, requirementsOf(projection), RemotePolicy.toPlan(policy, at));
4006
4367
  if (missing.length === 0) return store;
@@ -4119,18 +4480,21 @@ const Remote = {
4119
4480
  /**
4120
4481
  * Runs a `Query` through `RemoteClient`, encoding its input from the ref.
4121
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.
4122
4485
  */
4123
- query: Effect.fn("Remote.query")(function* (ref) {
4486
+ query: Effect.fn("Remote.query")(function* (ref, select) {
4124
4487
  const client = yield* RemoteClient;
4125
4488
  const input = yield* Schema.encodeUnknownEffect(ref.Input)(ref.input).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new RemoteQueryError({ message: error.message }))));
4126
4489
  return yield* client.query({
4127
4490
  query: ref.query,
4128
4491
  input,
4129
- window: ref.window
4492
+ window: ref.window,
4493
+ ...select === void 0 ? {} : { select }
4130
4494
  });
4131
4495
  }),
4132
4496
  /** The `RemoteMessage` that merges a query page into its connection. */
4133
- 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),
4134
4498
  /**
4135
4499
  * The edges a connection shows: its server-known region with pending and
4136
4500
  * confirmed overlays placed around it, minus removed edges and edges whose
@@ -4169,7 +4533,8 @@ const Remote = {
4169
4533
  requestId,
4170
4534
  entities: outcome.entities,
4171
4535
  connections: outcome.connections,
4172
- deleted: outcome.deleted
4536
+ deleted: outcome.deleted,
4537
+ now: (options.now ?? wallClock)()
4173
4538
  });
4174
4539
  return {
4175
4540
  output: outcome.output,
@@ -4196,9 +4561,20 @@ const Remote = {
4196
4561
  live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
4197
4562
  };
4198
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
+ };
4199
4573
  /** The bound domain: the descriptor, the binding, and the operations over them. */
4200
4574
  const bindDomain = (definition, store) => {
4201
4575
  const bound = bindRemote(definition, store);
4576
+ const readers = /* @__PURE__ */ new Map();
4577
+ const keptConnections = /* @__PURE__ */ new Set();
4202
4578
  /**
4203
4579
  * What to do with a row a live event says was inserted into a connection.
4204
4580
  *
@@ -4239,7 +4615,12 @@ const bindDomain = (definition, store) => {
4239
4615
  ...live,
4240
4616
  policy: livePolicyFor(model, live.event)
4241
4617
  };
4242
- 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}"`);
4243
4624
  };
4244
4625
  const domain = {
4245
4626
  ...definition,
@@ -4255,23 +4636,30 @@ const bindDomain = (definition, store) => {
4255
4636
  })))
4256
4637
  };
4257
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
+ },
4258
4649
  subscriptions: (active, options = {}) => {
4259
4650
  const entries = {};
4260
- const projections = [];
4651
+ for (const entry of Object.values(active)) assertOwned(entry);
4261
4652
  for (const [key, entry] of Object.entries(active)) {
4262
- 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}"`);
4263
- const projectionAt = memoizedProjectionOf(entry);
4264
- projections.push(projectionAt);
4265
- 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));
4266
4656
  entries[`${key}.read`] = observeEntry(bound, asked, identityMessage, options);
4267
4657
  entries[`${key}.live`] = liveEntry(bound, (model) => asked(model).requirements.filter((requirement) => requirement.live === true), identityMessage, options);
4268
4658
  }
4659
+ for (const identity of options.connections ?? []) keptConnections.add(connectionIdentity(identity));
4269
4660
  entries.retain = {
4270
4661
  dependenciesSchema: retentionRootsSchema,
4271
- modelToDependencies: (model) => rootsOf(projections.flatMap((projectionAt) => {
4272
- const projection = projectionAt(model);
4273
- return projection === void 0 ? [] : [projection];
4274
- }), options),
4662
+ modelToDependencies: (model) => rootsOf([...readers.values()].flatMap((projectionAt) => Option.toArray(projectionAt(model))), { connections: [...keptConnections] }),
4275
4663
  dependenciesToStream: (current) => Stream.fromEffect(Effect.succeed({
4276
4664
  _tag: "RetentionChanged",
4277
4665
  roots: current
@@ -4281,16 +4669,17 @@ const bindDomain = (definition, store) => {
4281
4669
  return entries;
4282
4670
  },
4283
4671
  plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
4672
+ meta: (model, projection) => Remote.meta(bound, model, projection),
4284
4673
  storeOf: (model) => storeOf(bound, model),
4285
4674
  confirmed: (projection) => confirmed(bound, projection),
4286
4675
  prefetch: (model, projection, options = {}) => Effect.gen(function* () {
4287
- const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
4676
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
4288
4677
  const client = yield* RemoteClient;
4289
4678
  const planOptions = RemotePolicy.toPlan(policy, now());
4290
4679
  let current = model;
4291
4680
  for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
4292
4681
  const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
4293
- current = reduce(current, pageMessage(query.identity, page, true, query.window));
4682
+ current = reduce(current, pageMessage(query.identity, page, true, query.window, query.select, now()));
4294
4683
  }
4295
4684
  const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
4296
4685
  if (requirements.length === 0) return current;
@@ -4306,6 +4695,30 @@ const bindDomain = (definition, store) => {
4306
4695
  now: at
4307
4696
  });
4308
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
+ }),
4309
4722
  query: (query, input, options) => {
4310
4723
  assertRegistered(bound, "Query", definition.registry.queries, query.name);
4311
4724
  const { select: given, ...window } = options;
@@ -4314,15 +4727,13 @@ const bindDomain = (definition, store) => {
4314
4727
  const listed = query.Result.entity;
4315
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`);
4316
4729
  if (select.entity !== listed) throw new Error(`Remote: the selection is of "${select.entity}", but query "${query.name}" lists "${listed}"`);
4317
- const ref = {
4318
- ...query.ref(input),
4319
- window: pickWindow(window)
4320
- };
4321
- const relation = relationOf(select);
4730
+ const { ref, relation, requirement } = readContract(query.ref(input), select, window);
4322
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));
4323
4734
  const failedItem = (remote, connection) => {
4324
4735
  if (Object.keys(remote.failures.fields).length === 0) return void 0;
4325
- for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
4736
+ for (const edge of shown(remote, connection).edges) {
4326
4737
  if (edge.ref.entity !== relation.entity) continue;
4327
4738
  const error = failureOf(remote, edge.ref.entity, edge.ref.id, relation);
4328
4739
  if (error !== void 0) return {
@@ -4333,22 +4744,26 @@ const bindDomain = (definition, store) => {
4333
4744
  };
4334
4745
  const loadingItem = (remote, connection) => {
4335
4746
  if (remote.loading.size === 0) return void 0;
4336
- for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
4747
+ for (const edge of shown(remote, connection).edges) {
4337
4748
  if (edge.ref.entity !== relation.entity) continue;
4338
4749
  if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
4339
4750
  }
4340
4751
  };
4341
4752
  const readPage = (remote, connection) => {
4342
4753
  const visible = visibleStoreOf(remote.entities, remote.optimistic);
4343
- 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))}`, () => {
4344
4759
  const items = [];
4345
4760
  let refreshing = connection.stale;
4346
- const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
4761
+ const { edges, cutBefore, cutAfter } = shown(remote, connection);
4347
4762
  for (const edge of edges) {
4348
4763
  const key = entityKey(edge.ref.entity, edge.ref.id);
4349
4764
  const assembled = assemble(visible, key, relation);
4350
4765
  if (assembled === void 0) return unavailableFailure(visible, key, relation);
4351
- const decoded = Schema.decodeUnknownResult(select.schema)(assembled.values);
4766
+ const decoded = decodeRow(select.schema, visible[key].values, assembled.values);
4352
4767
  if (Result.isFailure(decoded)) return {
4353
4768
  _tag: "Failed",
4354
4769
  error: {
@@ -4361,8 +4776,8 @@ const bindDomain = (definition, store) => {
4361
4776
  }
4362
4777
  const page = {
4363
4778
  items,
4364
- hasNext: hasNext(connection),
4365
- hasPrevious: hasPrevious(connection)
4779
+ hasNext: cutAfter || hasNext(connection),
4780
+ hasPrevious: cutBefore || hasPrevious(connection)
4366
4781
  };
4367
4782
  return refreshing ? {
4368
4783
  _tag: "Refreshing",
@@ -4373,12 +4788,6 @@ const bindDomain = (definition, store) => {
4373
4788
  };
4374
4789
  });
4375
4790
  };
4376
- const requirement = {
4377
- identity: ref.identity,
4378
- window: ref.window,
4379
- select: relation,
4380
- ref
4381
- };
4382
4791
  return {
4383
4792
  Model: remoteDataSchema(pageSchema(select.schema)),
4384
4793
  dependencies: [],
@@ -4414,7 +4823,9 @@ const bindDomain = (definition, store) => {
4414
4823
  previous: read.value
4415
4824
  };
4416
4825
  case "Failed": return read;
4417
- default: return {
4826
+ case "Initial":
4827
+ case "Loading":
4828
+ case "NotFound": return {
4418
4829
  _tag: "Failed",
4419
4830
  error: failure
4420
4831
  };
@@ -4422,32 +4833,23 @@ const bindDomain = (definition, store) => {
4422
4833
  }
4423
4834
  };
4424
4835
  },
4425
- next: (model, projection) => {
4426
- const segments = store.get(model).connections[projection.ref.identity]?.segments ?? [];
4427
- const end = segments[segments.length - 1]?.end;
4428
- return end?._tag === "Cursor" ? {
4429
- ...projection.ref,
4430
- window: {
4431
- ...pageSize(projection.ref.window, "first"),
4432
- after: end.cursor
4433
- }
4434
- } : void 0;
4435
- },
4436
- previous: (model, projection) => {
4437
- const start = store.get(model).connections[projection.ref.identity]?.segments[0]?.start;
4438
- return start?._tag === "Cursor" ? {
4439
- ...projection.ref,
4440
- window: {
4441
- ...pageSize(projection.ref.window, "last"),
4442
- before: start.cursor
4443
- }
4444
- } : 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();
4445
4848
  },
4446
4849
  explain: (model, projection, options) => {
4447
4850
  const { ref } = projection;
4448
4851
  const reading = Object.values(options?.surfaces ?? {}).filter((active) => {
4449
- const active_ = active.projectionOf(model);
4450
- 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));
4451
4853
  });
4452
4854
  const body = definition.registry.queries.get(ref.query)?.body;
4453
4855
  const [connection] = RemoteConnections.get(projection.metadata);
@@ -4476,8 +4878,7 @@ const bindDomain = (definition, store) => {
4476
4878
  const state = projection.read(model);
4477
4879
  const asked = askedOf(projection);
4478
4880
  const reading = options?.surfaces === void 0 ? void 0 : Object.values(options.surfaces).filter((active) => {
4479
- const shown = active.projectionOf(model);
4480
- return shown !== void 0 && covers(askedOf(shown), asked);
4881
+ return Option.exists(active.projectionOf(model), (shown) => covers(askedOf(shown), asked));
4481
4882
  }).map((active) => active.name);
4482
4883
  const withSurfaces = reading === void 0 ? {} : { surfaces: reading };
4483
4884
  switch (state._tag) {
@@ -4503,7 +4904,7 @@ const bindDomain = (definition, store) => {
4503
4904
  };
4504
4905
  case "Failed": return {
4505
4906
  state: state._tag,
4506
- message: state.error._tag === "DecodeError" ? `What the server sent does not decode against the Selection: ${state.error.message}` : state.error._tag === "Unavailable" ? `${state.error.message} Select without it, or Data.refresh asks again.` : `Its request failed: ${state.error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`,
4907
+ message: failedReadWords(state.error),
4507
4908
  ...withSurfaces
4508
4909
  };
4509
4910
  case "Initial":
@@ -4543,7 +4944,7 @@ const bindDomain = (definition, store) => {
4543
4944
  assembledAll = false;
4544
4945
  continue;
4545
4946
  }
4546
- const decoded = Schema.decodeUnknownResult(over.selection.schema)(assembled.values);
4947
+ const decoded = decodeRow(over.selection.schema, visible[key].values, assembled.values);
4547
4948
  if (Result.isFailure(decoded)) {
4548
4949
  assembledAll = false;
4549
4950
  continue;
@@ -4557,33 +4958,14 @@ const bindDomain = (definition, store) => {
4557
4958
  },
4558
4959
  refresh: (model, target) => Remote.refresh(bound, model, target),
4559
4960
  forget: (model) => Remote.forget(bound, model),
4560
- overlay: (model, id, optimistic) => bound.store.set(model, updateRemote(bound.store.get(model), {
4961
+ overlay: (model, id, optimistic) => reduce(model, {
4561
4962
  _tag: "OverlayShown",
4562
4963
  id,
4563
4964
  optimistic
4564
- })),
4565
- lift: (model, id) => {
4566
- const remote = bound.store.get(model);
4567
- const lifted = updateRemote(remote, {
4568
- _tag: "OverlayLifted",
4569
- id
4570
- });
4571
- return lifted === remote ? model : bound.store.set(model, lifted);
4572
- },
4573
- fetch: (ref) => ({
4574
- name: `Remote.query(${ref.query})`,
4575
- args: {
4576
- connection: ref.identity,
4577
- window: ref.window
4578
- },
4579
- effect: Remote.query(ref).pipe(Effect.match({
4580
- onFailure: (error) => ({
4581
- _tag: "QueryFailed",
4582
- connection: ref.identity,
4583
- error: remoteError(error)
4584
- }),
4585
- onSuccess: (page) => pageMessage(ref.identity, page, false, ref.window)
4586
- }))
4965
+ }),
4966
+ lift: (model, id) => reduce(model, {
4967
+ _tag: "OverlayLifted",
4968
+ id
4587
4969
  }),
4588
4970
  mutate: (model, mutation, input, options = {}) => {
4589
4971
  assertRegistered(bound, "Mutation", definition.registry.mutations, mutation.name);
@@ -4617,7 +4999,8 @@ const bindDomain = (definition, store) => {
4617
4999
  requestId,
4618
5000
  entities: outcome.entities,
4619
5001
  connections: outcome.connections,
4620
- deleted: outcome.deleted
5002
+ deleted: outcome.deleted,
5003
+ now: (options.now ?? wallClock)()
4621
5004
  })
4622
5005
  }))
4623
5006
  }
@@ -4637,18 +5020,41 @@ const bindDomain = (definition, store) => {
4637
5020
  return domain;
4638
5021
  };
4639
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
+ };
4640
5051
  const pickWindow = (window) => ({
4641
5052
  ...window.first === void 0 ? {} : { first: window.first },
4642
5053
  ...window.last === void 0 ? {} : { last: window.last },
4643
5054
  ...window.after === void 0 ? {} : { after: window.after },
4644
5055
  ...window.before === void 0 ? {} : { before: window.before }
4645
5056
  });
4646
- /** The page size of a window, carried onto the next or previous page's window under `key`. */
4647
- const pageSize = (window, key) => {
4648
- const size = window.first ?? window.last;
4649
- return size === void 0 ? {} : { [key]: size };
4650
- };
4651
5057
  //#endregion
4652
- 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, 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, 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, shouldWake, stableStringify, targetsOf, terminal, tombstone, unavailableOf, 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 };
4653
5059
 
4654
5060
  //# sourceMappingURL=index.mjs.map