foldkit-remote 0.5.0 → 0.7.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,6 @@
1
1
  import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
2
2
  import * as Domain from "foldkit-entity";
3
- import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId } from "foldkit-entity";
3
+ import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId, evaluate } from "foldkit-entity";
4
4
  import * as Subscription from "foldkit/subscription";
5
5
  import { Metadata } from "foldkit-surface";
6
6
  import { Rpc, RpcGroup } from "effect/unstable/rpc";
@@ -440,6 +440,16 @@ make: (name, config) => ({
440
440
  * canonical encoded filter/sort input — **never** the pagination window, so
441
441
  * `first(25)` and `after(cursor).first(25)` address the same logical connection.
442
442
  */
443
+ /**
444
+ * What separates a query's name from its canonical encoded input in a
445
+ * connection identity.
446
+ *
447
+ * NUL, because `JSON.stringify` escapes it inside a string (`\u0000`) and a
448
+ * query name is a TypeScript identifier — so the first occurrence is always the
449
+ * boundary, and a name or an input value cannot forge one. Anything recovering
450
+ * the query from an identity splits here.
451
+ */
452
+ const IDENTITY_SEPARATOR = "\0";
443
453
  /** Stable stringify: object keys sorted, undefined-valued keys dropped, so equal inputs encode equally. */
444
454
  const stableStringify = (value) => {
445
455
  if (value === void 0) return "null";
@@ -498,7 +508,7 @@ const Query = {
498
508
  input,
499
509
  Input,
500
510
  window: {},
501
- identity: `${name}\u0000${stableStringify(encode(input))}`
511
+ identity: `${name}${stableStringify(encode(input))}`
502
512
  })
503
513
  };
504
514
  },
@@ -1334,12 +1344,6 @@ const assembleRelation = (store, value, relation) => {
1334
1344
  * Entities: the descriptor an application declares, refs, ref pages, and
1335
1345
  * patches, with the codecs that carry refs on the wire.
1336
1346
  */
1337
- /**
1338
- * Relations are **references**, never inline target schemas: a ref cannot
1339
- * reconstruct a full entity, and dereferencing is a store concern. Because the
1340
- * target schema is not inlined, recursive relations cannot arise through the
1341
- * schema graph.
1342
- */
1343
1347
  const refCodec = (entity) => Schema.Struct({
1344
1348
  entity: Schema.String,
1345
1349
  id: Schema.String
@@ -1424,6 +1428,118 @@ const Entity = {
1424
1428
  })
1425
1429
  };
1426
1430
  //#endregion
1431
+ //#region src/matching.ts
1432
+ /**
1433
+ * Running a query body against the rows the client already holds.
1434
+ *
1435
+ * local-execution-DESIGN phase 3. A connection's rows are edges the server
1436
+ * delivered, and until now nothing on the client ever evaluated a body — which
1437
+ * is why an optimistic insert has to guess between `prepend` and `append`, and
1438
+ * why `LivePolicy` exists to be told in advance what to do with a row nobody
1439
+ * could judge.
1440
+ *
1441
+ * This judges them. It is pure, it performs no I/O, and it reuses the reference
1442
+ * interpreter in `foldkit-entity` rather than reimplementing the semantics —
1443
+ * so a body means here exactly what it means on the server, checked by the same
1444
+ * conformance suite.
1445
+ *
1446
+ * **It answers "which of the rows I hold match", never "which rows match".**
1447
+ * Those are different questions and the difference is not recoverable from the
1448
+ * result, so it is in the shape: `matched` is what satisfied the body, and
1449
+ * `skipped` is what was held but could not be judged. A caller that knows it
1450
+ * holds the whole population — a connection terminal at both ends — is the one
1451
+ * that can turn this into a complete answer. Nothing here can.
1452
+ *
1453
+ * **It is not authorization.** On the server a compiled `where` is conjoined
1454
+ * with the binding's `visible` rule. There is no `visible` here, and this is
1455
+ * safe only because the client holds only rows the server already released to
1456
+ * it. A local filter is not an access decision.
1457
+ */
1458
+ /**
1459
+ * The fields of the body's own Entity that a row must have for the body to be
1460
+ * decidable about it — its predicates and its ordering terms.
1461
+ *
1462
+ * `id` is never among them: it is the key, so it is known for every row in the
1463
+ * store whether or not anything fetched it as a field.
1464
+ */
1465
+ const decidableOn = (body) => Query$1.dependencies(body).fields.filter((field) => field.entity === body.entity.name && field.key !== "id").map((field) => field.key);
1466
+ /**
1467
+ * The rows of `descriptor`'s Entity that the store holds and the body matches.
1468
+ *
1469
+ * The input is given **decoded**, as an application writes it, and encoded here
1470
+ * through the descriptor's own `Input` codec. That is deliberate and is the
1471
+ * whole reason this takes a descriptor rather than a bare body: a store holds
1472
+ * wire values, so an input has to be in that space to be compared against them,
1473
+ * and handing an interpreter a decoded value is a mistake **no runtime error
1474
+ * catches** — the reference interpreter answers "no rows", which reads exactly
1475
+ * like a correct answer. Doing it here means a caller cannot get it wrong.
1476
+ *
1477
+ * Literals in the body are deliberately *not* encoded. A literal written in
1478
+ * domain terms is already broken against the server, which binds it to a column
1479
+ * the same way; accepting it here would make the client quietly more permissive
1480
+ * than the thing it has to agree with.
1481
+ */
1482
+ const matching = (store, descriptor, input, options = {}) => matchingEncoded(store, descriptor, Schema.encodeSync(descriptor.Input)(input), options);
1483
+ /**
1484
+ * As `matching`, for the callers in this module that already hold the input
1485
+ * **encoded**. Not exported: `matching` is the way in, and it cannot be given
1486
+ * the wrong space.
1487
+ */
1488
+ const matchingEncoded = (store, descriptor, encoded, options = {}) => {
1489
+ const body = descriptor.body;
1490
+ if (body === void 0) throw new Error(`[foldkit-remote] Remote.matching: query "${descriptor.name}" carries no body, so there is nothing to judge rows against. Declare it with Query.define.`);
1491
+ const prefix = `${body.entity.name}:`;
1492
+ const needed = decidableOn(body);
1493
+ const candidates = options.among ?? Object.keys(store).filter((key) => key.startsWith(prefix));
1494
+ const rows = [];
1495
+ const skipped = [];
1496
+ for (const key of candidates) {
1497
+ if (!key.startsWith(prefix)) continue;
1498
+ const entry = store[key];
1499
+ if (entry === void 0) {
1500
+ skipped.push(key);
1501
+ continue;
1502
+ }
1503
+ if (entry.tombstone) continue;
1504
+ if (needed.some((field) => !entry.present.has(field))) {
1505
+ skipped.push(key);
1506
+ continue;
1507
+ }
1508
+ rows.push("id" in entry.values ? {
1509
+ ...entry.values,
1510
+ __key: key
1511
+ } : {
1512
+ ...entry.values,
1513
+ id: key.slice(prefix.length),
1514
+ __key: key
1515
+ });
1516
+ }
1517
+ return {
1518
+ matched: evaluate(body, encoded, rows).map((row) => row.__key),
1519
+ skipped
1520
+ };
1521
+ };
1522
+ /**
1523
+ * Takes the input **encoded**, because its one caller holds it that way: a
1524
+ * connection identity carries the canonical encoded input, and making it decode
1525
+ * only for this to encode again would be a round trip that can fail.
1526
+ *
1527
+ * A decoded-input sibling was written first and deleted: nothing called it. The
1528
+ * asymmetry with `matching`, which takes a decoded input, is the shape of the
1529
+ * two real callers rather than an oversight.
1530
+ */
1531
+ const belongsEncoded = (store, descriptor, encoded, key) => {
1532
+ const entry = store[key];
1533
+ if (entry === void 0) return "unknown";
1534
+ if (entry.tombstone) return "no";
1535
+ const body = descriptor.body;
1536
+ if (body !== void 0 && decidableOn(body).some((field) => entry.stale.has(field))) return "unknown";
1537
+ const judged = matchingEncoded(store, descriptor, encoded, { among: [key] });
1538
+ if (judged.matched.includes(key)) return "yes";
1539
+ if (judged.skipped.includes(key)) return "unknown";
1540
+ return "no";
1541
+ };
1542
+ //#endregion
1427
1543
  //#region src/inspect.ts
1428
1544
  const inspectEntry = (key, entry) => ({
1429
1545
  key,
@@ -1440,6 +1556,7 @@ const inspectRemote = (model) => ({
1440
1556
  live: Object.keys(model.live),
1441
1557
  gaps: [...model.gaps],
1442
1558
  loading: [...model.loading],
1559
+ failures: model.failures,
1443
1560
  mutations: {
1444
1561
  pending: [...model.mutations.pending],
1445
1562
  failed: [...model.mutations.failed],
@@ -1617,7 +1734,6 @@ const visibleItems = (connection, connectionId, overlays, store) => {
1617
1734
  //#region src/live.ts
1618
1735
  const emptyLiveState = {
1619
1736
  cursor: 0,
1620
- stale: /* @__PURE__ */ new Set(),
1621
1737
  boundary: {}
1622
1738
  };
1623
1739
  /**
@@ -1651,10 +1767,6 @@ const recordBoundary = (state, connection, position) => {
1651
1767
  }
1652
1768
  };
1653
1769
  };
1654
- const invalidateConnection = (state, connection) => ({
1655
- ...state,
1656
- stale: /* @__PURE__ */ new Set([...state.stale, connection])
1657
- });
1658
1770
  /**
1659
1771
  * A subscriber is woken only if the event changed a field it selects. A
1660
1772
  * change to an unselected field is skipped entirely.
@@ -1707,9 +1819,10 @@ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
1707
1819
  outcome
1708
1820
  };
1709
1821
  case "invalidate": return {
1710
- state: invalidateConnection(advance(state, event.cursor), event.connection),
1822
+ state: advance(state, event.cursor),
1711
1823
  optimistic,
1712
- outcome
1824
+ outcome,
1825
+ invalidated: event.connection
1713
1826
  };
1714
1827
  case "ignore": return {
1715
1828
  state: advance(state, event.cursor),
@@ -1728,27 +1841,13 @@ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
1728
1841
  outcome
1729
1842
  };
1730
1843
  case "ConnectionInvalidate": return {
1731
- state: invalidateConnection(advance(state, event.cursor), event.connection),
1844
+ state: advance(state, event.cursor),
1845
+ invalidated: event.connection,
1732
1846
  optimistic,
1733
1847
  outcome
1734
1848
  };
1735
1849
  }
1736
1850
  };
1737
- const isStale = (state, connection) => state.stale.has(connection);
1738
- /**
1739
- * Clears a connection's stale mark once a fresh page has been adopted. An
1740
- * invalidating event only records that a refetch is due; whoever merges the
1741
- * refetched page calls this so `isStale` stops reporting.
1742
- */
1743
- const refreshConnection = (state, connection) => {
1744
- if (!state.stale.has(connection)) return state;
1745
- const stale = new Set(state.stale);
1746
- stale.delete(connection);
1747
- return {
1748
- ...state,
1749
- stale
1750
- };
1751
- };
1752
1851
  /** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
1753
1852
  const liveHasPrevious = (connection, state, connectionId) => hasPrevious(connection) || (state.boundary[connectionId]?.before ?? 0) > 0;
1754
1853
  /** `hasNext` accounting for edges recorded outside the loaded boundary. */
@@ -1890,17 +1989,33 @@ const gc = (state, roots) => {
1890
1989
  * application's choice.
1891
1990
  */
1892
1991
  /** Bump when the serialized shape changes; a mismatch discards the cache. */
1893
- const REMOTE_CACHE_VERSION = 3;
1992
+ const REMOTE_CACHE_VERSION = 4;
1993
+ /**
1994
+ * A snapshot of a Model, keeping the entity cache and only the connections
1995
+ * named.
1996
+ *
1997
+ * Naming none — the default — is exactly what Remote did before connections
1998
+ * could be kept at all: a disposable, server-derived cache.
1999
+ */
2000
+ const snapshotOf = (model, options = {}) => ({
2001
+ entities: model.entities,
2002
+ connections: Object.fromEntries((options.connections ?? []).flatMap((connection) => {
2003
+ const identity = connectionIdentity(connection);
2004
+ const held = model.connections[identity];
2005
+ return held === void 0 ? [] : [[identity, held.segments.map((segment) => segment.edges)]];
2006
+ }))
2007
+ });
1894
2008
  const sorted = (values) => [...values].sort();
1895
2009
  /**
1896
2010
  * The snapshot's data shape. `stableStringify` sorts object keys, and the
1897
2011
  * field sets are sorted here, so equal stores give byte-equal snapshots
1898
2012
  * whatever order they were built in.
1899
2013
  */
1900
- const serializeStore = (store, scope) => ({
1901
- version: 3,
2014
+ const serializeStore = (snapshot, scope) => ({
2015
+ version: 4,
1902
2016
  scope: scope ?? null,
1903
- entities: Object.fromEntries(Object.entries(store).map(([key, entry]) => [key, {
2017
+ connections: snapshot.connections,
2018
+ entities: Object.fromEntries(Object.entries(snapshot.entities).map(([key, entry]) => [key, {
1904
2019
  values: entry.values,
1905
2020
  present: sorted(entry.present),
1906
2021
  stale: sorted(entry.stale),
@@ -1930,9 +2045,30 @@ const parseEntry = (value) => {
1930
2045
  windows: entry.windows
1931
2046
  };
1932
2047
  };
1933
- const deserializeStore = (serialized) => Object.fromEntries(Object.entries(serialized.entities).map(([key, entry]) => [key, parseEntry(entry)]));
1934
- function dehydrate(store, options = {}) {
1935
- const text = stableStringify(serializeStore(store, options.scope));
2048
+ /** Throws on a malformed edge so the whole snapshot is discarded. */
2049
+ const parseEdges = (value) => {
2050
+ if (!Array.isArray(value)) throw new Error("segment is not an array of edges");
2051
+ return value.map((edge) => {
2052
+ const candidate = edge;
2053
+ if (typeof candidate.key !== "string" || typeof candidate.ref?.entity !== "string" || typeof candidate.ref?.id !== "string") throw new Error("edge is not { key, ref: { entity, id } }");
2054
+ return {
2055
+ key: candidate.key,
2056
+ ref: {
2057
+ entity: candidate.ref.entity,
2058
+ id: candidate.ref.id
2059
+ }
2060
+ };
2061
+ });
2062
+ };
2063
+ const deserializeStore = (serialized) => ({
2064
+ entities: Object.fromEntries(Object.entries(serialized.entities).map(([key, entry]) => [key, parseEntry(entry)])),
2065
+ connections: Object.fromEntries(Object.entries(serialized.connections ?? {}).map(([key, segments]) => {
2066
+ if (!Array.isArray(segments)) throw new Error("connection is not an array of segments");
2067
+ return [key, segments.map(parseEdges)];
2068
+ }))
2069
+ });
2070
+ function dehydrate(snapshot, options = {}) {
2071
+ const text = stableStringify(serializeStore(snapshot, options.scope));
1936
2072
  return exceeds(text, options.maxBytes) ? void 0 : text;
1937
2073
  }
1938
2074
  /**
@@ -1962,7 +2098,7 @@ const hydrate = (raw, options = {}) => {
1962
2098
  }
1963
2099
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
1964
2100
  const candidate = parsed;
1965
- if (candidate.version !== 3) return void 0;
2101
+ if (candidate.version !== 4) return void 0;
1966
2102
  if ((candidate.scope ?? null) !== (options.scope ?? null)) return void 0;
1967
2103
  if (candidate.entities === null || typeof candidate.entities !== "object") return void 0;
1968
2104
  if (Array.isArray(candidate.entities)) return void 0;
@@ -1985,37 +2121,49 @@ const mergeStores = (current, snapshot, policy) => policy === "replace" ? {
1985
2121
  ...snapshot,
1986
2122
  ...current
1987
2123
  };
2124
+ /** An empty snapshot: the cache as a fresh session has it. */
2125
+ const emptySnapshot = {
2126
+ entities: emptyStore,
2127
+ connections: {}
2128
+ };
1988
2129
  const RemotePersistence = {
1989
2130
  /** The snapshot text, or `undefined` when it would exceed `maxBytes`. */
1990
2131
  dehydrate,
1991
- /** The store a snapshot text holds, or `undefined` when it is refused. */
2132
+ /** What a snapshot text holds, or `undefined` when it is refused. */
1992
2133
  hydrate,
1993
- /** A snapshot brought into a store by policy. */
2134
+ /**
2135
+ * What to keep of a Model, naming the connections that should survive a
2136
+ * reload. Naming none is the default and is what Remote always did.
2137
+ */
2138
+ snapshotOf,
2139
+ /** A snapshot's entities brought into a store by policy. */
1994
2140
  mergeStores,
2141
+ /** The cache as a fresh session has it; what a refused `restore` yields. */
2142
+ emptySnapshot,
1995
2143
  /**
1996
2144
  * Writes the snapshot under `key`, or removes the key when the store would
1997
2145
  * exceed `maxBytes`, so a stale smaller snapshot cannot outlive a larger
1998
2146
  * store it no longer describes.
1999
2147
  */
2000
- save: (store, options) => Effect.gen(function* () {
2148
+ save: (snapshot, options) => Effect.gen(function* () {
2001
2149
  const kv = yield* KeyValueStore.KeyValueStore;
2002
- const text = dehydrate(store, options);
2150
+ const text = dehydrate(snapshot, options);
2003
2151
  if (text === void 0) yield* kv.remove(options.key);
2004
2152
  else yield* kv.set(options.key, text);
2005
2153
  }),
2006
2154
  /**
2007
- * Reads the cache. A missing key yields `emptyStore`; anything `hydrate`
2008
- * refuses yields `emptyStore` and removes the key, so the next `restore` is
2009
- * clean and the planner refetches.
2155
+ * Reads the cache. A missing key yields `emptySnapshot`; anything `hydrate`
2156
+ * refuses yields `emptySnapshot` and removes the key, so the next `restore`
2157
+ * is clean and the planner refetches.
2010
2158
  */
2011
2159
  restore: (options) => Effect.gen(function* () {
2012
2160
  const kv = yield* KeyValueStore.KeyValueStore;
2013
2161
  const raw = yield* kv.get(options.key);
2014
- if (raw === void 0) return emptyStore;
2162
+ if (raw === void 0) return emptySnapshot;
2015
2163
  const restored = hydrate(raw, options);
2016
2164
  if (restored === void 0) {
2017
2165
  yield* kv.remove(options.key);
2018
- return emptyStore;
2166
+ return emptySnapshot;
2019
2167
  }
2020
2168
  return restored;
2021
2169
  })
@@ -2138,6 +2286,10 @@ const RemoteData = {
2138
2286
  * optimistic layers an application keeps in its Model, the Messages that
2139
2287
  * change it, and the pure reducer over them.
2140
2288
  */
2289
+ const noFailures = {
2290
+ connections: {},
2291
+ fields: {}
2292
+ };
2141
2293
  const emptyRefresh = {
2142
2294
  generation: 0,
2143
2295
  requested: /* @__PURE__ */ new Map(),
@@ -2151,7 +2303,8 @@ const initialRemoteModel = {
2151
2303
  mutations: emptyMutationState,
2152
2304
  gaps: /* @__PURE__ */ new Set(),
2153
2305
  loading: /* @__PURE__ */ new Set(),
2154
- refresh: emptyRefresh
2306
+ refresh: emptyRefresh,
2307
+ failures: noFailures
2155
2308
  };
2156
2309
  /** The entity store and the mutation ledger are runtime values, not wire shapes. */
2157
2310
  const runtimeSchema = Schema.Unknown;
@@ -2163,7 +2316,11 @@ const remoteModelSchema = () => Schema.Struct({
2163
2316
  mutations: runtimeSchema,
2164
2317
  gaps: runtimeSchema,
2165
2318
  loading: runtimeSchema,
2166
- refresh: runtimeSchema
2319
+ refresh: runtimeSchema,
2320
+ failures: Schema.Struct({
2321
+ connections: Schema.Record(Schema.String, remoteErrorSchema),
2322
+ fields: Schema.Record(Schema.String, remoteErrorSchema)
2323
+ })
2167
2324
  });
2168
2325
  const retentionRootsSchema = Schema.Struct({
2169
2326
  requirements: Schema.Array(ReadRequest),
@@ -2184,15 +2341,18 @@ const remoteMessageCases = {
2184
2341
  },
2185
2342
  ReadFailed: {
2186
2343
  requests: Schema.Array(ReadRequest),
2187
- error: remoteErrorSchema
2344
+ error: remoteErrorSchema,
2345
+ stream: Schema.optional(Schema.String)
2188
2346
  },
2189
2347
  RefreshStarted: { requests: Schema.Array(ReadRequest) },
2190
2348
  ReadStarted: {
2191
2349
  requests: Schema.Array(ReadRequest),
2192
2350
  refresh: Schema.optional(Schema.Number)
2193
2351
  },
2352
+ QueryStarted: { connections: Schema.Array(Schema.String) },
2194
2353
  RetentionChanged: { roots: retentionRootsSchema },
2195
2354
  Hydrated: {
2355
+ connections: Schema.optional(Schema.Unknown),
2196
2356
  entities: runtimeSchema,
2197
2357
  merge: Schema.Union([Schema.Literal("replace"), Schema.Literal("preserve-existing")])
2198
2358
  },
@@ -2277,6 +2437,26 @@ const withoutLoading = (loading, requests) => {
2277
2437
  */
2278
2438
  const connectionPrefix = "\0connection\0";
2279
2439
  const connectionMark = (identity) => `${connectionPrefix}${identity}`;
2440
+ /** Whether a query for this connection is in flight. */
2441
+ const isQueryLoading = (model, identity) => model.loading.size !== 0 && model.loading.has(connectionMark(identity));
2442
+ const withoutQueryLoading = (loading, identity) => {
2443
+ if (!loading.has(connectionMark(identity))) return loading;
2444
+ const next = new Set(loading);
2445
+ next.delete(connectionMark(identity));
2446
+ return next;
2447
+ };
2448
+ /**
2449
+ * The in-flight marks of connections the roots still name. A query the entry
2450
+ * stopped waiting for — it was restarted or deactivated before an answer came —
2451
+ * leaves its mark behind, and a connection released with that mark would read
2452
+ * `Loading` forever the next time something asked for it before sending.
2453
+ */
2454
+ const prunedLoading = (loading, roots, reached) => {
2455
+ if (loading.size === 0) return loading;
2456
+ const named = new Set(roots.connections.map((root) => connectionMark(root.identity)));
2457
+ const kept = [...loading].filter((mark) => mark.startsWith(connectionPrefix) ? named.has(mark) : reached().has(markEntity(mark)));
2458
+ return kept.length === loading.size ? loading : new Set(kept);
2459
+ };
2280
2460
  /**
2281
2461
  * Drops the refresh marks of entities and connections that were collected.
2282
2462
  * Nothing observes a collected entity — the retention roots are the active
@@ -2391,31 +2571,68 @@ const updateRemote = (model, message) => {
2391
2571
  case "ReadReceived": return {
2392
2572
  ...model,
2393
2573
  entities: writeRead(model.entities, message.requests, message.result, message.now),
2394
- loading: withoutLoading(model.loading, message.requests)
2395
- };
2396
- case "ReadFailed": return {
2397
- ...model,
2398
- entities: setStale(model.entities, marksOf(message.requests), false),
2399
- loading: withoutLoading(model.loading, message.requests)
2574
+ loading: withoutLoading(model.loading, message.requests),
2575
+ failures: withoutFieldFailures(model.failures, [...fieldMarks(message.requests), ...patchedMarks(message.result.entities)])
2400
2576
  };
2577
+ case "ReadFailed":
2578
+ if (message.stream !== void 0) return markGap(model, message.stream);
2579
+ return {
2580
+ ...model,
2581
+ entities: setStale(model.entities, marksOf(message.requests), false),
2582
+ loading: withoutLoading(model.loading, message.requests),
2583
+ failures: withFieldFailures(model.failures, message.requests, message.error)
2584
+ };
2401
2585
  case "RetentionChanged": {
2402
2586
  const retained = gc(model, message.roots);
2587
+ let reachedKeys;
2588
+ const reached = () => reachedKeys ??= reachable(model.entities, message.roots, model.connections, model.optimistic, model.mutations.pending);
2403
2589
  return {
2404
2590
  ...model,
2405
2591
  ...retained,
2406
- refresh: prunedRefresh(model.refresh, retained)
2592
+ refresh: prunedRefresh(model.refresh, retained),
2593
+ failures: prunedFailures(model.failures, message.roots, reached),
2594
+ loading: prunedLoading(model.loading, message.roots, reached)
2595
+ };
2596
+ }
2597
+ case "Hydrated": {
2598
+ const restored = Object.entries(message.connections ?? {}).map(([identity, segments]) => [identity, {
2599
+ segments: segments.map((edges) => ({
2600
+ edges: [...edges],
2601
+ start: unknown,
2602
+ end: unknown
2603
+ })),
2604
+ stale: true
2605
+ }]);
2606
+ const written = Object.entries(message.entities).flatMap(([key, entry]) => message.merge === "replace" || !(key in model.entities) ? [...entry.present].map((field) => `${key}\u0000${field}`) : []);
2607
+ return {
2608
+ ...model,
2609
+ entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
2610
+ failures: withoutFieldFailures(model.failures, written),
2611
+ connections: message.merge === "replace" ? {
2612
+ ...model.connections,
2613
+ ...Object.fromEntries(restored)
2614
+ } : {
2615
+ ...Object.fromEntries(restored),
2616
+ ...model.connections
2617
+ }
2407
2618
  };
2408
2619
  }
2409
- case "Hydrated": return {
2410
- ...model,
2411
- entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge)
2412
- };
2413
2620
  case "RefreshStarted": {
2414
2621
  const asked = message.requests.reduce((store, request) => isTombstone(store, entityKey(request.entity, request.id)) ? remove(store, entityKey(request.entity, request.id)) : store, model.entities);
2415
2622
  const entities = setStale(asked, marksOf(message.requests), true);
2416
- return entities === model.entities ? model : {
2623
+ const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
2624
+ return entities === model.entities && failures === model.failures ? model : {
2625
+ ...model,
2626
+ entities,
2627
+ failures
2628
+ };
2629
+ }
2630
+ case "QueryStarted": {
2631
+ const loading = new Set(model.loading);
2632
+ for (const identity of message.connections) loading.add(connectionMark(identity));
2633
+ return loading.size === model.loading.size ? model : {
2417
2634
  ...model,
2418
- entities
2635
+ loading
2419
2636
  };
2420
2637
  }
2421
2638
  case "ReadStarted": return {
@@ -2434,7 +2651,8 @@ const updateRemote = (model, message) => {
2434
2651
  ...model,
2435
2652
  entities: settled.store,
2436
2653
  mutations: settled.state,
2437
- optimistic: settled.optimistic
2654
+ optimistic: settled.optimistic,
2655
+ failures: withoutFieldFailures(model.failures, [...patchedMarks(message.entities), ...(message.deleted ?? []).flatMap((ref) => entityFailureMarks(model.failures, entityKey(ref.entity, ref.id)))])
2438
2656
  };
2439
2657
  }
2440
2658
  case "OverlayShown": return {
@@ -2456,18 +2674,26 @@ const updateRemote = (model, message) => {
2456
2674
  case "LiveReceived": {
2457
2675
  const state = model.live[message.stream] ?? emptyLiveState;
2458
2676
  if (message.event._tag === "EntityPatched" || message.event._tag === "EntityDeleted") {
2459
- const applied = applyEntityEvent(state, model.entities, message.event, message.now);
2460
- return applied.outcome === "gap" ? markGap(model, message.stream) : clearGap({
2677
+ const event = message.event;
2678
+ const applied = applyEntityEvent(state, model.entities, event, message.now);
2679
+ if (applied.outcome === "gap") return markGap(model, message.stream);
2680
+ const settled = applied.outcome !== "applied" ? [] : event._tag === "EntityPatched" ? [...patchedMarks([{
2681
+ ...event.ref,
2682
+ values: event.values
2683
+ }])] : entityFailureMarks(model.failures, entityKey(event.ref.entity, event.ref.id));
2684
+ return clearGap({
2461
2685
  ...model,
2462
2686
  entities: applied.store,
2463
2687
  live: {
2464
2688
  ...model.live,
2465
2689
  [message.stream]: applied.state
2466
- }
2690
+ },
2691
+ failures: withoutFieldFailures(model.failures, settled)
2467
2692
  }, message.stream);
2468
2693
  }
2469
2694
  const applied = applyConnectionEvent(state, model.optimistic, message.event, message.policy);
2470
- return applied.outcome === "gap" ? markGap(model, message.stream) : clearGap({
2695
+ if (applied.outcome === "gap") return markGap(model, message.stream);
2696
+ const next = clearGap({
2471
2697
  ...model,
2472
2698
  optimistic: applied.optimistic,
2473
2699
  live: {
@@ -2475,10 +2701,16 @@ const updateRemote = (model, message) => {
2475
2701
  [message.stream]: applied.state
2476
2702
  }
2477
2703
  }, message.stream);
2704
+ return applied.invalidated === void 0 ? next : updateRemote(next, {
2705
+ _tag: "ConnectionInvalidated",
2706
+ connection: applied.invalidated
2707
+ });
2478
2708
  }
2479
2709
  case "GapCleared": return clearGap(model, message.stream);
2480
2710
  case "ConnectionMerged": {
2481
2711
  const current = model.connections[message.connection] ?? emptyConnection;
2712
+ const failures = withoutConnectionFailure(model.failures, message.connection);
2713
+ const loading = withoutQueryLoading(model.loading, message.connection);
2482
2714
  const replaces = message.refreshes === true && current.stale;
2483
2715
  const merged = merge(replaces ? emptyConnection : current, message.page);
2484
2716
  return {
@@ -2490,25 +2722,181 @@ const updateRemote = (model, message) => {
2490
2722
  stale: false
2491
2723
  } : merged
2492
2724
  },
2493
- optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending)
2725
+ optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending),
2726
+ failures,
2727
+ loading
2494
2728
  };
2495
2729
  }
2496
2730
  case "ConnectionInvalidated":
2497
- if (model.connections[message.connection]?.stale === true) return model;
2731
+ if (model.connections[message.connection]?.stale === true && !(message.connection in model.failures.connections)) return model;
2498
2732
  return {
2499
2733
  ...model,
2500
- connections: setConnectionStale(model.connections, message.connection, true)
2734
+ connections: setConnectionStale(model.connections, message.connection, true),
2735
+ failures: withoutConnectionFailure(model.failures, message.connection)
2501
2736
  };
2502
- case "QueryFailed": return message.connection in model.connections ? {
2737
+ case "QueryFailed": return {
2503
2738
  ...model,
2504
- connections: setConnectionStale(model.connections, message.connection, false)
2505
- } : model;
2739
+ failures: {
2740
+ ...model.failures,
2741
+ connections: {
2742
+ ...model.failures.connections,
2743
+ [message.connection]: message.error
2744
+ }
2745
+ },
2746
+ loading: withoutQueryLoading(model.loading, message.connection)
2747
+ };
2506
2748
  case "ConnectionRefreshed": return {
2507
2749
  ...model,
2508
2750
  connections: setConnectionStale(model.connections, message.connection, false)
2509
2751
  };
2510
2752
  }
2511
2753
  };
2754
+ /** The failures without one connection's, keeping identity when it had none. */
2755
+ const withoutConnectionFailure = (failures, connection) => {
2756
+ if (!(connection in failures.connections)) return failures;
2757
+ const { [connection]: _settled, ...connections } = failures.connections;
2758
+ return {
2759
+ ...failures,
2760
+ connections
2761
+ };
2762
+ };
2763
+ /** The failures with every field a read asked for failed with `error`. */
2764
+ const withFieldFailures = (failures, requests, error) => {
2765
+ const fields = { ...failures.fields };
2766
+ for (const mark of fieldMarks(requests)) fields[mark] = error;
2767
+ return {
2768
+ ...failures,
2769
+ fields
2770
+ };
2771
+ };
2772
+ /**
2773
+ * The failures without the fields `settled` names, keeping identity when none
2774
+ * of them had failed, which is nearly always: this runs on every write.
2775
+ */
2776
+ const withoutFieldFailures = (failures, settled) => {
2777
+ if (Object.keys(failures.fields).length === 0) return failures;
2778
+ const marks = [...settled].filter((mark) => mark in failures.fields);
2779
+ if (marks.length === 0) return failures;
2780
+ const fields = { ...failures.fields };
2781
+ for (const mark of marks) delete fields[mark];
2782
+ return {
2783
+ ...failures,
2784
+ fields
2785
+ };
2786
+ };
2787
+ /** Every failed field of one entity: what deleting the entity settles. */
2788
+ const entityFailureMarks = (failures, key) => Object.keys(failures.fields).filter((mark) => mark.startsWith(`${key}\u0000`));
2789
+ /** The field marks of written values: each patch settles the fields it carries. */
2790
+ const patchedMarks = function* (patches) {
2791
+ for (const patch of patches) for (const field of Object.keys(patch.values)) yield fieldMark(patch.entity, patch.id, field);
2792
+ };
2793
+ /**
2794
+ * Every field a read's requests reach: their own, and through each relation
2795
+ * the store already holds, the fields they ask of its targets. A request with
2796
+ * a relation riding on it names no target id; the store does, and a failure
2797
+ * recorded on a target is reached only this way.
2798
+ */
2799
+ const reachedMarks = (store, requests) => {
2800
+ const marks = [];
2801
+ const walked = /* @__PURE__ */ new Map();
2802
+ const walk = (entity, id, requirement) => {
2803
+ const key = entityKey(entity, id);
2804
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
2805
+ if (seen.has(requirement)) return;
2806
+ seen.add(requirement);
2807
+ walked.set(key, seen);
2808
+ for (const field of requirement.fields) {
2809
+ marks.push(fieldMark(entity, id, field));
2810
+ const nested = requirement.relations?.[field];
2811
+ if (nested === void 0) continue;
2812
+ const value = readField(store, key, field);
2813
+ if (value._tag === "None") continue;
2814
+ for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
2815
+ }
2816
+ };
2817
+ for (const request of requests) walk(request.entity, request.id, request);
2818
+ return marks;
2819
+ };
2820
+ /** The entity key a field mark belongs to. */
2821
+ const markEntity = (mark) => mark.slice(0, mark.indexOf("\0"));
2822
+ /**
2823
+ * The failures of what the roots still reach. A released connection's or
2824
+ * entity's failure goes with it, so asking for it again later asks the server
2825
+ * again rather than repeating an error that may no longer hold.
2826
+ *
2827
+ * Reached is retention's own walk, taken over the Model before collection. It
2828
+ * counts a relation's target and a list's row that the store does not hold,
2829
+ * which is exactly a value whose first read failed: it is still on screen,
2830
+ * behind the parent that refers to it.
2831
+ */
2832
+ const prunedFailures = (failures, roots, reached) => {
2833
+ const named = new Set(roots.connections.map((root) => root.identity));
2834
+ const connections = Object.entries(failures.connections).filter(([identity]) => named.has(identity));
2835
+ const fields = Object.keys(failures.fields).length === 0 ? [] : Object.entries(failures.fields).filter(([mark]) => reached().has(markEntity(mark)));
2836
+ return connections.length === Object.keys(failures.connections).length && fields.length === Object.keys(failures.fields).length ? failures : {
2837
+ connections: Object.fromEntries(connections),
2838
+ fields: Object.fromEntries(fields)
2839
+ };
2840
+ };
2841
+ /** Whether this field's last read failed and nothing has settled it since. */
2842
+ const isFieldFailed = (model, entity, id, field) => fieldMark(entity, id, field) in model.failures.fields;
2843
+ /**
2844
+ * Whether a read is fetching any field `relation` reads of an entity, its own
2845
+ * or, through each relation the store holds, its targets'. A value whose owner
2846
+ * is being fetched is loading, not `Initial`: nothing is missing a fetch.
2847
+ */
2848
+ const isLoadingThrough = (model, entity, id, relation) => {
2849
+ if (model.loading.size === 0) return false;
2850
+ const walked = /* @__PURE__ */ new Map();
2851
+ const walk = (target, targetId, requirement) => {
2852
+ const key = entityKey(target, targetId);
2853
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
2854
+ if (seen.has(requirement)) return false;
2855
+ seen.add(requirement);
2856
+ walked.set(key, seen);
2857
+ for (const field of requirement.fields) {
2858
+ if (model.loading.has(fieldMark(target, targetId, field))) return true;
2859
+ const nested = requirement.relations?.[field];
2860
+ if (nested === void 0) continue;
2861
+ const value = readField(model.entities, key, field);
2862
+ if (value._tag === "None") continue;
2863
+ for (const ref of targetsOf(value.value, nested)) if (walk(ref.entity, ref.id, nested)) return true;
2864
+ }
2865
+ return false;
2866
+ };
2867
+ return walk(entity, id, relation);
2868
+ };
2869
+ /**
2870
+ * The first failure among the fields `relation` reads of an entity, following
2871
+ * each relation the store holds into its targets, as `assemble` does. A read
2872
+ * that consults it shows `Failed` for a related entity's failure too, rather
2873
+ * than waiting on a field nothing will fetch.
2874
+ */
2875
+ const failureOf = (model, entity, id, relation) => {
2876
+ const failed = model.failures.fields;
2877
+ if (Object.keys(failed).length === 0) return void 0;
2878
+ const walked = /* @__PURE__ */ new Map();
2879
+ const walk = (target, targetId, requirement) => {
2880
+ const key = entityKey(target, targetId);
2881
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
2882
+ if (seen.has(requirement)) return void 0;
2883
+ seen.add(requirement);
2884
+ walked.set(key, seen);
2885
+ for (const field of requirement.fields) {
2886
+ const error = failed[fieldMark(target, targetId, field)];
2887
+ if (error !== void 0) return error;
2888
+ const nested = requirement.relations?.[field];
2889
+ if (nested === void 0) continue;
2890
+ const value = readField(model.entities, key, field);
2891
+ if (value._tag === "None") continue;
2892
+ for (const ref of targetsOf(value.value, nested)) {
2893
+ const found = walk(ref.entity, ref.id, nested);
2894
+ if (found !== void 0) return found;
2895
+ }
2896
+ }
2897
+ };
2898
+ return walk(entity, id, relation);
2899
+ };
2512
2900
  /** Appends (after) or prepends (before) an incoming page onto the stored one. */
2513
2901
  const mergeWireRefPages = (current, next, direction) => {
2514
2902
  const refs = direction === "after" ? [.../* @__PURE__ */ new Set([...current.refs, ...next.refs])] : [.../* @__PURE__ */ new Set([...next.refs, ...current.refs])];
@@ -2740,10 +3128,34 @@ const askedOf = (projection) => projection === void 0 ? nothingAsked : {
2740
3128
  connections: connectionsOf(projection)
2741
3129
  };
2742
3130
  /**
3131
+ * Whether `outer` asks for everything `inner` does of one entity: each field,
3132
+ * each relation's own slice of its target, and each page window alike.
3133
+ */
3134
+ const sliceCovers = (outer, inner) => outer.entity === inner.entity && inner.fields.every((field) => outer.fields.includes(field)) && Object.entries(inner.relations ?? {}).every(([field, relation]) => {
3135
+ const wider = outer.relations?.[field];
3136
+ return wider !== void 0 && sliceCovers(wider, relation);
3137
+ }) && Object.entries(inner.windows ?? {}).every(([field, window]) => outer.windows?.[field] !== void 0 && stableStringify(outer.windows[field]) === stableStringify(window));
3138
+ /**
3139
+ * Whether what `outer` asks includes everything `inner` asks: each list with
3140
+ * the slice it selects of its rows, and each entity with the slice it reads,
3141
+ * relations included. A Surface that shows a read is one whose projection
3142
+ * asks for all of it; one that reads part of it cannot be why it is fetched.
3143
+ * A Surface that asks for one entity in several pieces is already one
3144
+ * requirement here: projection metadata unions them per entity and id.
3145
+ */
3146
+ const covers = (outer, inner) => {
3147
+ const { connections, requirements } = outer;
3148
+ return inner.connections.every((connection) => connections.some((wider) => wider.identity === connection.identity && (connection.select === void 0 || wider.select !== void 0 && sliceCovers(wider.select, connection.select)))) && inner.requirements.every((requirement) => requirements.some((wider) => wider.id === requirement.id && sliceCovers(wider, requirement)));
3149
+ };
3150
+ /**
2743
3151
  * Plans what a projection asks against the Model. A connection the Model holds
2744
3152
  * fresh contributes its visible items' selected fields to the entity plan; one
2745
3153
  * it does not hold, or holds stale, is a query to run (its items are planned
2746
3154
  * once the page arrives).
3155
+ *
3156
+ * A connection whose query failed is not run again here, short of `force`: the
3157
+ * failure is what its read shows, and retrying is `Remote.refresh`'s to ask
3158
+ * for. The rows it already holds are still planned, since they are on screen.
2747
3159
  */
2748
3160
  const planAsked = (remote, asked, options) => {
2749
3161
  const queries = [];
@@ -2751,18 +3163,46 @@ const planAsked = (remote, asked, options) => {
2751
3163
  const connections = Requirement.mergeConnections(asked.connections);
2752
3164
  for (const connection of connections) {
2753
3165
  const known = remote.connections[connection.identity];
2754
- if (known === void 0 || known.stale || options.force === true) {
3166
+ if (!(options.force !== true && connection.identity in remote.failures.connections) && (known === void 0 || known.stale || options.force === true)) {
2755
3167
  queries.push(connection);
2756
3168
  continue;
2757
3169
  }
3170
+ if (known === void 0) continue;
2758
3171
  const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
2759
3172
  items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
2760
3173
  }
3174
+ const planned = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], options);
2761
3175
  return {
2762
- requirements: plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], options),
3176
+ requirements: options.force === true ? planned : withoutFailedFields(remote, planned),
2763
3177
  queries
2764
3178
  };
2765
3179
  };
3180
+ /**
3181
+ * The plan without the fields whose last read failed. Like a failed query,
3182
+ * a failed field is shown rather than retried on its own, so a persistent
3183
+ * error is not asked again on every unrelated restart of the read entry. A
3184
+ * field leaving the request takes its window and its relation with it.
3185
+ */
3186
+ const withoutFailedFields = (remote, planned) => {
3187
+ if (Object.keys(remote.failures.fields).length === 0) return planned;
3188
+ return planned.flatMap((requirement) => {
3189
+ const fields = requirement.fields.filter((field) => !isFieldFailed(remote, requirement.entity, requirement.id, field));
3190
+ if (fields.length === requirement.fields.length) return [requirement];
3191
+ if (fields.length === 0) return [];
3192
+ const kept = new Set(fields);
3193
+ const only = (record) => record === void 0 ? void 0 : Object.fromEntries(Object.entries(record).filter(([field]) => kept.has(field)));
3194
+ const windows = only(requirement.windows);
3195
+ const relations = only(requirement.relations);
3196
+ return [{
3197
+ entity: requirement.entity,
3198
+ id: requirement.id,
3199
+ fields,
3200
+ ...windows === void 0 || Object.keys(windows).length === 0 ? {} : { windows },
3201
+ ...relations === void 0 || Object.keys(relations).length === 0 ? {} : { relations },
3202
+ ...requirement.live === void 0 ? {} : { live: requirement.live }
3203
+ }];
3204
+ });
3205
+ };
2766
3206
  /** The entity requirements a page's edges add under a selection. */
2767
3207
  const itemsOf = (edges, select) => edges.filter((edge) => edge.entity === select.entity).map((edge) => ({
2768
3208
  ...select,
@@ -2790,22 +3230,52 @@ const queryRequestOf = (query) => {
2790
3230
  * `refreshes` marks the page as answering the connection's refresh, so one
2791
3231
  * Message both merges it and clears `stale`.
2792
3232
  */
2793
- const pageMessage = (connection, result, refreshes = false) => ({
2794
- _tag: "ConnectionMerged",
2795
- connection,
2796
- page: {
2797
- edges: result.edges.map((edge) => ({
2798
- key: edge.key,
2799
- ref: {
2800
- entity: edge.entity,
2801
- id: edge.id
2802
- }
2803
- })),
2804
- start: result.start,
2805
- end: result.end
2806
- },
2807
- ...refreshes ? { refreshes } : {}
2808
- });
3233
+ /**
3234
+ * The page a window asked for, or the number of edges that came back instead.
3235
+ *
3236
+ * A window is a request the client made and the server answered, so more edges
3237
+ * than were asked for is a protocol disagreement rather than a windfall: the
3238
+ * client cannot tell which of them the window meant, the connection's
3239
+ * boundaries stop describing what it holds, and a `first: 25` that quietly
3240
+ * becomes a thousand is a memory event with no error attached to it.
3241
+ *
3242
+ * `undefined` when the page is within its window, or when no size was asked
3243
+ * for — `after`/`before` with no `first`/`last` bounds nothing.
3244
+ */
3245
+ const overrun = (window, edges) => {
3246
+ const asked = window.first ?? window.last;
3247
+ return asked !== void 0 && edges.length > asked ? {
3248
+ asked,
3249
+ got: edges.length
3250
+ } : void 0;
3251
+ };
3252
+ const pageMessage = (connection, result, refreshes = false, window = {}) => {
3253
+ const over = overrun(window, result.edges);
3254
+ if (over !== void 0) return {
3255
+ _tag: "QueryFailed",
3256
+ connection,
3257
+ error: {
3258
+ _tag: "RemoteProtocolError",
3259
+ message: `the server returned ${over.got} edges for a window of ${over.asked}`
3260
+ }
3261
+ };
3262
+ return {
3263
+ _tag: "ConnectionMerged",
3264
+ connection,
3265
+ page: {
3266
+ edges: result.edges.map((edge) => ({
3267
+ key: edge.key,
3268
+ ref: {
3269
+ entity: edge.entity,
3270
+ id: edge.id
3271
+ }
3272
+ })),
3273
+ start: result.start,
3274
+ end: result.end
3275
+ },
3276
+ ...refreshes ? { refreshes } : {}
3277
+ };
3278
+ };
2809
3279
  /**
2810
3280
  * The retention roots of some projections: their requirements, their
2811
3281
  * connections (each with the union of what the projections select of its
@@ -2853,7 +3323,7 @@ const queryMessage = (query) => Effect.gen(function* () {
2853
3323
  _tag: "QueryFailed",
2854
3324
  connection: query.identity,
2855
3325
  error: remoteError(result.failure)
2856
- } : pageMessage(query.identity, result.success, true);
3326
+ } : pageMessage(query.identity, result.success, true, query.window);
2857
3327
  });
2858
3328
  /** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
2859
3329
  const PlannedQuery = Schema.Struct({
@@ -2893,14 +3363,17 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
2893
3363
  }))
2894
3364
  };
2895
3365
  },
2896
- dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(requirements.length === 0 ? Stream.empty : Stream.fromIterable([toMessage({
3366
+ dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
2897
3367
  _tag: "ReadStarted",
2898
3368
  requests: requirements,
2899
3369
  refresh
2900
3370
  }), ...RemotePolicy.refreshes(policy) ? [toMessage({
2901
3371
  _tag: "RefreshStarted",
2902
3372
  requests: requirements
2903
- })] : []]), Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }))
3373
+ })] : []], ...queries.length === 0 ? [] : [toMessage({
3374
+ _tag: "QueryStarted",
3375
+ 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" }))
2904
3377
  };
2905
3378
  };
2906
3379
  /** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
@@ -2930,7 +3403,8 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
2930
3403
  })), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
2931
3404
  _tag: "ReadFailed",
2932
3405
  requests: requirements,
2933
- error: remoteError(error)
3406
+ error: remoteError(error),
3407
+ stream: liveStreamKey(requirements)
2934
3408
  }))))
2935
3409
  });
2936
3410
  /** The projection an active Surface has for this Model, if it is active. */
@@ -2966,6 +3440,21 @@ const Remote = {
2966
3440
  /** Whether a Message is one of Remote's, by tag. */
2967
3441
  reduces: isRemoteMessage,
2968
3442
  /**
3443
+ * A patch of one entity's values, for a mutation's `optimistic` list or an
3444
+ * overlay. Takes a `foldkit-entity` Entity or a Remote descriptor alike, so
3445
+ * the domain you declared once is the one you patch:
3446
+ * `Remote.patch(Project, 'p1', { name: 'Apollo II' })`. Values are the
3447
+ * fields' wire shape, as the server writes them; a relation is its ref key
3448
+ * (`'User:u1'`).
3449
+ */
3450
+ patch: (entity, id, values) => descriptorOf(entity).patch(id, values),
3451
+ /**
3452
+ * A reference to one entity, for `ConnectionChange` and anything else that
3453
+ * names a row: `Remote.ref(Project, 'p3')`. Either kind of entity, as with
3454
+ * `Remote.patch`.
3455
+ */
3456
+ ref: (entity, id) => descriptorOf(entity).ref(id),
3457
+ /**
2969
3458
  * Declares a Remote domain and binds it to its place in the application Model
2970
3459
  * in one step; the result carries the application-facing operations. The
2971
3460
  * descriptor alone is `Remote.define`, the binding alone `Remote.at`.
@@ -3023,8 +3512,18 @@ const Remote = {
3023
3512
  value: decoded.success
3024
3513
  };
3025
3514
  });
3026
- if (present !== void 0) return present;
3027
- return isLoading(bound.store.get(root), selection.entity, id, relation.fields) ? { _tag: "Loading" } : { _tag: "Initial" };
3515
+ const remote = bound.store.get(root);
3516
+ const failure = failureOf(remote, selection.entity, id, relation);
3517
+ if (present !== void 0) return failure !== void 0 && (present._tag === "Ready" || present._tag === "Refreshing") ? {
3518
+ _tag: "Failed",
3519
+ error: failure,
3520
+ previous: present.value
3521
+ } : present;
3522
+ if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
3523
+ return failure === void 0 ? { _tag: "Initial" } : {
3524
+ _tag: "Failed",
3525
+ error: failure
3526
+ };
3028
3527
  }
3029
3528
  });
3030
3529
  },
@@ -3083,6 +3582,9 @@ const Remote = {
3083
3582
  * again with `next`/`previous`. The read entries restart, so a read or query
3084
3583
  * sent before the refresh is not applied after it.
3085
3584
  *
3585
+ * It is also how a failed query is retried: a connection whose query failed
3586
+ * is not run again on its own, so its read stays `Failed` until this asks.
3587
+ *
3086
3588
  * The projection must be observed, which it is while it is on screen. Live
3087
3589
  * subscriptions are left as they are. For data nothing observes (SSR, tests),
3088
3590
  * use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
@@ -3098,7 +3600,7 @@ const Remote = {
3098
3600
  return known === void 0 ? [] : itemsOf(visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities).map((edge) => edge.ref), connection.select);
3099
3601
  });
3100
3602
  const requirements = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], { force: true });
3101
- const invalidated = connections.filter((connection) => remote.connections[connection.identity] !== void 0).map((connection) => connection.identity);
3603
+ const invalidated = connections.filter((connection) => remote.connections[connection.identity] !== void 0 || connection.identity in remote.failures.connections).map((connection) => connection.identity);
3102
3604
  const marked = [...requirements.length === 0 ? [] : [{
3103
3605
  _tag: "RefreshStarted",
3104
3606
  requests: requirements
@@ -3175,7 +3677,7 @@ const Remote = {
3175
3677
  });
3176
3678
  }),
3177
3679
  /** The `RemoteMessage` that merges a query page into its connection. */
3178
- queryMessage: (ref, result) => pageMessage(ref.identity, result),
3680
+ queryMessage: (ref, result) => pageMessage(ref.identity, result, false, ref.window),
3179
3681
  /**
3180
3682
  * The edges a connection shows: its server-known region with pending and
3181
3683
  * confirmed overlays placed around it, minus removed edges and edges whose
@@ -3232,8 +3734,10 @@ const Remote = {
3232
3734
  observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => askedOf(surface.projection(params)), toMessage, options),
3233
3735
  /**
3234
3736
  * A Foldkit Subscription entry that consumes the live stream for a Surface's
3235
- * requirements, emitting a `LiveReceived` per event and a `ReadFailed` when
3236
- * the stream breaks (including `ResumeUnavailable`). The resume cursor is read
3737
+ * requirements, emitting a `LiveReceived` per event and a `ReadFailed`
3738
+ * carrying its `stream` when the stream breaks (including
3739
+ * `ResumeUnavailable`). That records a gap on the stream rather than a failed
3740
+ * read, since nothing was being read. The resume cursor is read
3237
3741
  * from `RemoteModel.live`, so the application tracks no cursor of its own.
3238
3742
  */
3239
3743
  live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
@@ -3242,7 +3746,48 @@ const brandEntries = (entries) => Subscription.make()(() => entries);
3242
3746
  /** The bound domain: the descriptor, the binding, and the operations over them. */
3243
3747
  const bindDomain = (definition, store) => {
3244
3748
  const bound = bindRemote(definition, store);
3245
- const reduce = (model, message) => store.set(model, updateRemote(store.get(model), message));
3749
+ /**
3750
+ * What to do with a row a live event says was inserted into a connection.
3751
+ *
3752
+ * Resolved here rather than in the pure reducer because this is the only
3753
+ * place with both the Model and the registry, and two separate things needed
3754
+ * one or the other.
3755
+ *
3756
+ * **The declared policy reaches the decision.** `Query.connection(E, { live })`
3757
+ * has always been typed, documented, carried on the descriptor and encoded in
3758
+ * the Message schema — and nothing ever put it on a Message, so every live
3759
+ * insert took the default whatever an application asked for. It does now.
3760
+ *
3761
+ * **A row the client can judge does not need a policy.** The policy exists
3762
+ * because nothing could tell whether an inserted row belonged to the query.
3763
+ * Where the body says it does not, `ignore` is not a guess. Where the body
3764
+ * says it does, or the client cannot tell — a row it never fetched, or holds
3765
+ * without a field the body reads — the declared policy is the answer, exactly
3766
+ * as it was meant to be.
3767
+ *
3768
+ * Only membership is decided here. *Where* a row sorts needs text collation,
3769
+ * which is the backend's, so the position the event carries stands.
3770
+ */
3771
+ const livePolicyFor = (model, event) => {
3772
+ if (event._tag !== "ConnectionInsert") return void 0;
3773
+ const identity = event.connection;
3774
+ const descriptor = definition.registry.queries.get(identity.slice(0, identity.indexOf("\0")));
3775
+ const declared = (descriptor?.Result)?.live;
3776
+ if (descriptor?.body === void 0) return declared;
3777
+ const encoded = JSON.parse(identity.slice(identity.indexOf("\0") + 1));
3778
+ return belongsEncoded(storeOf(bound, model), descriptor, encoded, entityKey(event.edge.ref.entity, event.edge.ref.id)) === "no" ? {
3779
+ prepend: "ignore",
3780
+ append: "ignore"
3781
+ } : declared;
3782
+ };
3783
+ const reduce = (model, message) => {
3784
+ const live = message._tag === "LiveReceived" ? message : void 0;
3785
+ const resolved = live === void 0 || live.policy !== void 0 ? message : {
3786
+ ...live,
3787
+ policy: livePolicyFor(model, live.event)
3788
+ };
3789
+ return store.set(model, updateRemote(store.get(model), resolved));
3790
+ };
3246
3791
  const domain = {
3247
3792
  ...definition,
3248
3793
  ...bound,
@@ -3291,7 +3836,7 @@ const bindDomain = (definition, store) => {
3291
3836
  let current = model;
3292
3837
  for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
3293
3838
  const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
3294
- current = reduce(current, pageMessage(query.identity, page, true));
3839
+ current = reduce(current, pageMessage(query.identity, page, true, query.window));
3295
3840
  }
3296
3841
  const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
3297
3842
  if (requirements.length === 0) return current;
@@ -3321,6 +3866,58 @@ const bindDomain = (definition, store) => {
3321
3866
  };
3322
3867
  const relation = relationOf(select);
3323
3868
  const relationKey = stableStringify(relation);
3869
+ const failedItem = (remote, connection) => {
3870
+ 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)) {
3872
+ if (edge.ref.entity !== relation.entity) continue;
3873
+ const error = failureOf(remote, edge.ref.entity, edge.ref.id, relation);
3874
+ if (error !== void 0) return {
3875
+ _tag: "Failed",
3876
+ error
3877
+ };
3878
+ }
3879
+ };
3880
+ const loadingItem = (remote, connection) => {
3881
+ if (remote.loading.size === 0) return void 0;
3882
+ for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
3883
+ if (edge.ref.entity !== relation.entity) continue;
3884
+ if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
3885
+ }
3886
+ };
3887
+ const readPage = (remote, connection) => {
3888
+ const visible = visibleStoreOf(remote.entities, remote.optimistic);
3889
+ return memoRead(visible, connection, `${ref.identity}\u0000${relationKey}`, () => {
3890
+ const items = [];
3891
+ let refreshing = connection.stale;
3892
+ const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
3893
+ 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);
3897
+ if (Result.isFailure(decoded)) return {
3898
+ _tag: "Failed",
3899
+ error: {
3900
+ _tag: "DecodeError",
3901
+ message: decoded.failure.message
3902
+ }
3903
+ };
3904
+ refreshing ||= assembled.refreshing;
3905
+ items.push(decoded.success);
3906
+ }
3907
+ const page = {
3908
+ items,
3909
+ hasNext: hasNext(connection),
3910
+ hasPrevious: hasPrevious(connection)
3911
+ };
3912
+ return refreshing ? {
3913
+ _tag: "Refreshing",
3914
+ value: page
3915
+ } : {
3916
+ _tag: "Ready",
3917
+ value: page
3918
+ };
3919
+ });
3920
+ };
3324
3921
  const requirement = {
3325
3922
  identity: ref.identity,
3326
3923
  window: ref.window,
@@ -3332,42 +3929,41 @@ const bindDomain = (definition, store) => {
3332
3929
  dependencies: [],
3333
3930
  metadata: RemoteConnections.of(requirement),
3334
3931
  ref,
3932
+ selection: select,
3335
3933
  read: (root) => {
3336
3934
  const remote = store.get(root);
3337
3935
  const connection = remote.connections[ref.identity];
3338
- if (connection === void 0 || connection.stale && connection.segments.length === 0) return { _tag: "Initial" };
3339
- const visible = visibleStoreOf(remote.entities, remote.optimistic);
3340
- return memoRead(visible, connection, `${ref.identity}\u0000${relationKey}`, () => {
3341
- const items = [];
3342
- let refreshing = connection.stale;
3343
- const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
3344
- for (const edge of edges) {
3345
- const assembled = assemble(visible, entityKey(edge.ref.entity, edge.ref.id), relation);
3346
- if (assembled === void 0) return { _tag: "Initial" };
3347
- const decoded = Schema.decodeUnknownResult(select.schema)(assembled.values);
3348
- if (Result.isFailure(decoded)) return {
3349
- _tag: "Failed",
3350
- error: {
3351
- _tag: "DecodeError",
3352
- message: decoded.failure.message
3353
- }
3354
- };
3355
- refreshing ||= assembled.refreshing;
3356
- items.push(decoded.success);
3357
- }
3358
- const page = {
3359
- items,
3360
- hasNext: hasNext(connection),
3361
- hasPrevious: hasPrevious(connection)
3936
+ const failure = remote.failures.connections[ref.identity];
3937
+ if (connection === void 0 || connection.stale && connection.segments.length === 0) {
3938
+ if (isQueryLoading(remote, ref.identity)) return { _tag: "Loading" };
3939
+ return failure === void 0 ? { _tag: "Initial" } : {
3940
+ _tag: "Failed",
3941
+ error: failure
3942
+ };
3943
+ }
3944
+ const read = readPage(remote, connection) ?? failedItem(remote, connection) ?? loadingItem(remote, connection) ?? { _tag: "Initial" };
3945
+ if (failure === void 0) {
3946
+ if (read._tag !== "Ready" && read._tag !== "Refreshing") return read;
3947
+ const item = failedItem(remote, connection);
3948
+ return item === void 0 ? read : {
3949
+ _tag: "Failed",
3950
+ error: item.error,
3951
+ previous: read.value
3952
+ };
3953
+ }
3954
+ switch (read._tag) {
3955
+ case "Ready":
3956
+ case "Refreshing": return {
3957
+ _tag: "Failed",
3958
+ error: failure,
3959
+ previous: read.value
3362
3960
  };
3363
- return refreshing ? {
3364
- _tag: "Refreshing",
3365
- value: page
3366
- } : {
3367
- _tag: "Ready",
3368
- value: page
3961
+ case "Failed": return read;
3962
+ default: return {
3963
+ _tag: "Failed",
3964
+ error: failure
3369
3965
  };
3370
- });
3966
+ }
3371
3967
  }
3372
3968
  };
3373
3969
  },
@@ -3392,6 +3988,118 @@ const bindDomain = (definition, store) => {
3392
3988
  }
3393
3989
  } : void 0;
3394
3990
  },
3991
+ explain: (model, projection, options) => {
3992
+ const { ref } = projection;
3993
+ 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);
3996
+ });
3997
+ const body = definition.registry.queries.get(ref.query)?.body;
3998
+ const [connection] = RemoteConnections.get(projection.metadata);
3999
+ return {
4000
+ domain: bound.contract.name,
4001
+ query: ref.query,
4002
+ input: Schema.encodeSync(ref.Input)(ref.input),
4003
+ identity: ref.identity,
4004
+ window: ref.window,
4005
+ select: connection.select,
4006
+ ...body === void 0 ? {} : {
4007
+ body: Query$1.show(body),
4008
+ dependencies: Query$1.dependencies(body)
4009
+ },
4010
+ state: projection.read(model)._tag,
4011
+ ...options?.surfaces === void 0 ? {} : {
4012
+ surfaces: reading.map((active) => active.name),
4013
+ activation: reading.flatMap((active) => active.activation === void 0 ? [] : [{
4014
+ surface: active.name,
4015
+ ...active.activation
4016
+ }])
4017
+ }
4018
+ };
4019
+ },
4020
+ why: (model, projection, options) => {
4021
+ const state = projection.read(model);
4022
+ const asked = askedOf(projection);
4023
+ 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);
4026
+ }).map((active) => active.name);
4027
+ const withSurfaces = reading === void 0 ? {} : { surfaces: reading };
4028
+ switch (state._tag) {
4029
+ case "Ready": return {
4030
+ state: state._tag,
4031
+ message: "Everything it selects is here.",
4032
+ ...withSurfaces
4033
+ };
4034
+ case "Refreshing": return {
4035
+ state: state._tag,
4036
+ message: "It is shown while a newer value is fetched.",
4037
+ ...withSurfaces
4038
+ };
4039
+ case "Loading": return {
4040
+ state: state._tag,
4041
+ message: "A request for it is in flight.",
4042
+ ...withSurfaces
4043
+ };
4044
+ case "NotFound": return {
4045
+ state: state._tag,
4046
+ message: "The server answered without it: it does not exist, or this principal may not see it.",
4047
+ ...withSurfaces
4048
+ };
4049
+ case "Failed": return {
4050
+ 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.`,
4052
+ ...withSurfaces
4053
+ };
4054
+ case "Initial":
4055
+ if (reading === void 0) return {
4056
+ state: state._tag,
4057
+ reason: "Unknown",
4058
+ message: "Nothing is fetching it. Pass the active record you give Data.subscriptions as `surfaces` to tell whether an active Surface reads it."
4059
+ };
4060
+ return reading.length === 0 ? {
4061
+ state: state._tag,
4062
+ reason: "NotObserved",
4063
+ message: "No active Surface reads it, so nothing fetches it. Include it in a Surface that is active for this Model, in the record given to Data.subscriptions.",
4064
+ surfaces: reading
4065
+ } : {
4066
+ state: state._tag,
4067
+ reason: "NotFetching",
4068
+ message: `${reading.join(", ")} ${reading.length === 1 ? "reads it and is" : "read it and are"} active, yet nothing is fetching it. Remote's Subscriptions are most likely not installed: give the runtime Data.subscriptions (or Data.wiring) and a RemoteClient.`,
4069
+ surfaces: reading
4070
+ };
4071
+ }
4072
+ },
4073
+ filtered: (model, over, by, input) => {
4074
+ assertRegistered(bound, "Query", definition.registry.queries, by.name);
4075
+ const filters = by.body?.entity.name;
4076
+ if (filters !== void 0 && filters !== over.selection.entity) throw new Error(`Remote: query "${by.name}" is over "${filters}", but the list is of "${over.selection.entity}", so it cannot filter it`);
4077
+ const remote = store.get(model);
4078
+ const connection = remote.connections[over.ref.identity];
4079
+ const visible = visibleStoreOf(remote.entities, remote.optimistic);
4080
+ const among = (connection === void 0 ? [] : visibleItems(connection, over.ref.identity, remote.optimistic.overlays, remote.entities)).map((edge) => entityKey(edge.ref.entity, edge.ref.id));
4081
+ const judged = matching(visible, by, input, { among });
4082
+ const relation = relationOf(over.selection);
4083
+ const items = [];
4084
+ let assembledAll = true;
4085
+ for (const key of judged.matched) {
4086
+ const assembled = assemble(visible, key, relation);
4087
+ if (assembled === void 0) {
4088
+ assembledAll = false;
4089
+ continue;
4090
+ }
4091
+ const decoded = Schema.decodeUnknownResult(over.selection.schema)(assembled.values);
4092
+ if (Result.isFailure(decoded)) {
4093
+ assembledAll = false;
4094
+ continue;
4095
+ }
4096
+ items.push(decoded.success);
4097
+ }
4098
+ return {
4099
+ items,
4100
+ complete: judged.skipped.length === 0 && assembledAll && connection !== void 0 && !isGapped(connection) && !hasNext(connection) && !hasPrevious(connection)
4101
+ };
4102
+ },
3395
4103
  refresh: (model, target) => Remote.refresh(bound, model, target),
3396
4104
  overlay: (model, id, optimistic) => bound.store.set(model, updateRemote(bound.store.get(model), {
3397
4105
  _tag: "OverlayShown",
@@ -3418,7 +4126,7 @@ const bindDomain = (definition, store) => {
3418
4126
  connection: ref.identity,
3419
4127
  error: remoteError(error)
3420
4128
  }),
3421
- onSuccess: (page) => pageMessage(ref.identity, page)
4129
+ onSuccess: (page) => pageMessage(ref.identity, page, false, ref.window)
3422
4130
  }))
3423
4131
  }),
3424
4132
  mutate: (model, mutation, input, options = {}) => {
@@ -3485,6 +4193,6 @@ const pageSize = (window, key) => {
3485
4193
  return size === void 0 ? {} : { [key]: size };
3486
4194
  };
3487
4195
  //#endregion
3488
- export { ConnectionChange, ConnectionChangeSchema, Entity, 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, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnection, isFieldStale, isGapped, isLoading, isRefPage, isRemoteMessage, isStale, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, merge, missingFields, mutateRemote, mutationStatus, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshConnection, 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 };
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 };
3489
4197
 
3490
4198
  //# sourceMappingURL=index.mjs.map