foldkit-remote 0.4.0 → 0.6.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, 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";
@@ -108,6 +108,91 @@ const requirementsOf = (projection) => RemoteRequirements.get(projection.metadat
108
108
  /** The query connections a Projection carries. */
109
109
  const connectionsOf = (projection) => RemoteConnections.get(projection.metadata);
110
110
  //#endregion
111
+ //#region src/relation.ts
112
+ /** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
113
+ const RelationAnnotation = "foldkitRemoteRelation";
114
+ /** The target entity's name on the same codec, so a nested selection of another entity is refused. */
115
+ const RelationEntityAnnotation = "foldkitRemoteRelationEntity";
116
+ /**
117
+ * A page of a relation that is a whole list is read under a name of its own, the
118
+ * field's name and the page's size: `comments@first=10`. It is a field like any
119
+ * other to the store, the planner, and the wire, so the whole list and a page of
120
+ * it are held side by side, merged, marked stale, and persisted each on its own.
121
+ * The server reads the name apart, reads the relation with the window, and
122
+ * answers under the alias. A cursor is not part of the name: a page read from a
123
+ * cursor merges onto the page it continues.
124
+ */
125
+ const RELATION_ALIAS = "@";
126
+ /** The alias a relation field is read under, for a window of this size. */
127
+ const relationAlias = (field, window) => `${field}@${window.last !== void 0 ? `last=${window.last}` : `first=${window.first ?? ""}`}`;
128
+ /** The field an alias reads; a name that is no alias is its own field. */
129
+ const aliasedField = (name) => {
130
+ const at = name.indexOf("@");
131
+ return at === -1 ? name : name.slice(0, at);
132
+ };
133
+ /** Splits a ref key (`"Entity:id"`, the store key too) back into its parts. */
134
+ const refParts = (encoded) => {
135
+ const separator = encoded.indexOf(":");
136
+ return separator === -1 ? {
137
+ entity: encoded,
138
+ id: ""
139
+ } : {
140
+ entity: encoded.slice(0, separator),
141
+ id: encoded.slice(separator + 1)
142
+ };
143
+ };
144
+ /**
145
+ * The refs a relation value carries, in order. Anything that is not a ref key,
146
+ * an array of them, or a page of them contributes nothing.
147
+ */
148
+ const refsIn = (value) => {
149
+ if (typeof value === "string") return [refParts(value)];
150
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string").map(refParts);
151
+ if (value !== null && typeof value === "object") {
152
+ const refs = value.refs;
153
+ if (Array.isArray(refs)) return refs.filter((item) => typeof item === "string").map(refParts);
154
+ }
155
+ return [];
156
+ };
157
+ /** The refs in a relation value that name the relation's target entity, and no other. */
158
+ const targetsOf = (value, relation) => refsIn(value).filter((ref) => ref.entity === relation.entity);
159
+ const isRefPage = (value) => {
160
+ if (value === null || typeof value !== "object") return false;
161
+ const page = value;
162
+ return Array.isArray(page.refs) && page.refs.every((ref) => typeof ref === "string") && typeof page.hasNext === "boolean" && typeof page.hasPrevious === "boolean";
163
+ };
164
+ /**
165
+ * The relation shape an entity field schema declares: a ref (`one`), a nullable
166
+ * ref, an array of refs (`many`), or a page of refs (`page`). `undefined` for a
167
+ * scalar field, which cannot take a nested selection.
168
+ */
169
+ const relationShape = (schema) => shapeOf(schema.ast, false);
170
+ const shapeOf = (ast, nullable) => {
171
+ const annotated = ast.annotations?.[RelationAnnotation];
172
+ if (annotated === "one" || annotated === "page") return {
173
+ kind: annotated,
174
+ nullable,
175
+ entity: String(ast.annotations?.[RelationEntityAnnotation])
176
+ };
177
+ if (ast._tag === "Arrays") {
178
+ const item = ast.rest?.[0];
179
+ const inner = item === void 0 ? void 0 : shapeOf(item, false);
180
+ return inner?.kind === "one" ? {
181
+ kind: "many",
182
+ nullable,
183
+ entity: inner.entity
184
+ } : void 0;
185
+ }
186
+ if (ast._tag === "Union") {
187
+ const members = ast.types ?? [];
188
+ const optional = members.some((member) => member._tag === "Null" || member._tag === "Undefined");
189
+ for (const member of members) {
190
+ const shape = shapeOf(member, optional);
191
+ if (shape !== void 0) return shape;
192
+ }
193
+ }
194
+ };
195
+ //#endregion
111
196
  //#region src/store.ts
112
197
  /**
113
198
  * The pure entity store: values, per-field presence, staleness, and tombstones.
@@ -142,6 +227,9 @@ const written = (previous, values, now, windows) => {
142
227
  const requested = windows?.[field];
143
228
  if (requested !== void 0 && requested !== "") nextWindows[field] = requested;
144
229
  else delete nextWindows[field];
230
+ if (!field.includes("@")) {
231
+ for (const held of present) if (held.startsWith(`${field}@`) && !(held in values)) stale.add(held);
232
+ }
145
233
  }
146
234
  return {
147
235
  values: {
@@ -352,6 +440,16 @@ make: (name, config) => ({
352
440
  * canonical encoded filter/sort input — **never** the pagination window, so
353
441
  * `first(25)` and `after(cursor).first(25)` address the same logical connection.
354
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";
355
453
  /** Stable stringify: object keys sorted, undefined-valued keys dropped, so equal inputs encode equally. */
356
454
  const stableStringify = (value) => {
357
455
  if (value === void 0) return "null";
@@ -361,7 +459,32 @@ const stableStringify = (value) => {
361
459
  return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
362
460
  };
363
461
  const isEntityName = (result) => typeof result === "object" && result !== null && typeof result.name === "string";
462
+ /**
463
+ * The relational half comes from `foldkit-entity`, so one `Query` namespace
464
+ * holds both what a query means (`from`, `where`, `orderBy`) and how this
465
+ * client addresses it (`make`, `connection`, the window steps). They are one
466
+ * vocabulary in use — a definition's body is composed and then named — and two
467
+ * namespaces of the same name in one file would be a trap.
468
+ */
469
+ /**
470
+ * One placeholder per key of the input's schema, each carrying that key's own
471
+ * codec so a comparison against it is checked.
472
+ *
473
+ * A codec that is not a `Schema.Struct` exposes no keys to stand for, while its
474
+ * *type* may well have them — so the body would be handed an empty object,
475
+ * `input.slug` would be `undefined`, and the comparison would silently be
476
+ * against nothing while the types all agreed. That is a wrong query rather than
477
+ * a missing one, so it is refused where the query is declared.
478
+ */
479
+ const inputsOf = (name, schema) => {
480
+ const fields = schema.fields;
481
+ if (fields === void 0) throw new Error(`[foldkit-remote] Query.define: the Input of "${name}" is not a Schema.Struct, so its body has no inputs to read. Declare it as fields (\`{ slug: Schema.String }\`) or a plain \`Schema.Struct\`.`);
482
+ const inputs = {};
483
+ for (const key of Object.keys(fields)) inputs[key] = Expr.input(key, fields[key]);
484
+ return Object.freeze(inputs);
485
+ };
364
486
  const Query = {
487
+ ...Query$1,
365
488
  /** Declares the entity and options a query's result is a connection over. */
366
489
  connection: (entity, options) => ({
367
490
  entity: entity.name,
@@ -385,10 +508,45 @@ const Query = {
385
508
  input,
386
509
  Input,
387
510
  window: {},
388
- identity: `${name}\u0000${stableStringify(encode(input))}`
511
+ identity: `${name}${stableStringify(encode(input))}`
389
512
  })
390
513
  };
391
514
  },
515
+ /**
516
+ * Declares a query by what it means. The body is built once, here, and the
517
+ * `input` it is given holds placeholders rather than values — there is
518
+ * nothing yet to branch on, so a condition that depends on what was passed is
519
+ * a comparison over the placeholder:
520
+ *
521
+ * ```ts
522
+ * const PostsBySlug = Query.define('PostsBySlug', { slug: Schema.String }, ({ input }) =>
523
+ * Query.from(Post).pipe(
524
+ * Query.where(Expr.eq(Post.fields.slug, input.slug)),
525
+ * Query.orderBy(Order.asc(Post.fields.id)),
526
+ * ),
527
+ * )
528
+ * ```
529
+ *
530
+ * What comes back is an ordinary `QueryDescriptor` — the same name, `Input`,
531
+ * `ref` and connection identity `Query.make` gives — carrying its `body`
532
+ * besides. A server can compile that body instead of being told the same
533
+ * thing again in its own dialect, and one that would rather answer the query
534
+ * its own way still can.
535
+ *
536
+ * The result is a connection over the Entity the body reads, so it is not
537
+ * named twice.
538
+ */
539
+ define: (name, Input, body, options) => {
540
+ const schema = schemaOf(Input);
541
+ const built = body({ input: inputsOf(name, schema) });
542
+ return {
543
+ ...Query.make(name, {
544
+ Input,
545
+ Result: Query.connection(built.entity, options)
546
+ }),
547
+ body: built
548
+ };
549
+ },
392
550
  first: (count) => (ref) => ({
393
551
  ...ref,
394
552
  window: {
@@ -1016,74 +1174,6 @@ const hasNext = (connection) => {
1016
1174
  /** A connection with more than one segment has an explicit unknown gap. */
1017
1175
  const isGapped = (connection) => connection.segments.length > 1;
1018
1176
  //#endregion
1019
- //#region src/relation.ts
1020
- /** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
1021
- const RelationAnnotation = "foldkitRemoteRelation";
1022
- /** The target entity's name on the same codec, so a nested selection of another entity is refused. */
1023
- const RelationEntityAnnotation = "foldkitRemoteRelationEntity";
1024
- /** Splits a ref key (`"Entity:id"`, the store key too) back into its parts. */
1025
- const refParts = (encoded) => {
1026
- const separator = encoded.indexOf(":");
1027
- return separator === -1 ? {
1028
- entity: encoded,
1029
- id: ""
1030
- } : {
1031
- entity: encoded.slice(0, separator),
1032
- id: encoded.slice(separator + 1)
1033
- };
1034
- };
1035
- /**
1036
- * The refs a relation value carries, in order. Anything that is not a ref key,
1037
- * an array of them, or a page of them contributes nothing.
1038
- */
1039
- const refsIn = (value) => {
1040
- if (typeof value === "string") return [refParts(value)];
1041
- if (Array.isArray(value)) return value.filter((item) => typeof item === "string").map(refParts);
1042
- if (value !== null && typeof value === "object") {
1043
- const refs = value.refs;
1044
- if (Array.isArray(refs)) return refs.filter((item) => typeof item === "string").map(refParts);
1045
- }
1046
- return [];
1047
- };
1048
- /** The refs in a relation value that name the relation's target entity, and no other. */
1049
- const targetsOf = (value, relation) => refsIn(value).filter((ref) => ref.entity === relation.entity);
1050
- const isRefPage = (value) => {
1051
- if (value === null || typeof value !== "object") return false;
1052
- const page = value;
1053
- return Array.isArray(page.refs) && page.refs.every((ref) => typeof ref === "string") && typeof page.hasNext === "boolean" && typeof page.hasPrevious === "boolean";
1054
- };
1055
- /**
1056
- * The relation shape an entity field schema declares: a ref (`one`), a nullable
1057
- * ref, an array of refs (`many`), or a page of refs (`page`). `undefined` for a
1058
- * scalar field, which cannot take a nested selection.
1059
- */
1060
- const relationShape = (schema) => shapeOf(schema.ast, false);
1061
- const shapeOf = (ast, nullable) => {
1062
- const annotated = ast.annotations?.[RelationAnnotation];
1063
- if (annotated === "one" || annotated === "page") return {
1064
- kind: annotated,
1065
- nullable,
1066
- entity: String(ast.annotations?.[RelationEntityAnnotation])
1067
- };
1068
- if (ast._tag === "Arrays") {
1069
- const item = ast.rest?.[0];
1070
- const inner = item === void 0 ? void 0 : shapeOf(item, false);
1071
- return inner?.kind === "one" ? {
1072
- kind: "many",
1073
- nullable,
1074
- entity: inner.entity
1075
- } : void 0;
1076
- }
1077
- if (ast._tag === "Union") {
1078
- const members = ast.types ?? [];
1079
- const optional = members.some((member) => member._tag === "Null" || member._tag === "Undefined");
1080
- for (const member of members) {
1081
- const shape = shapeOf(member, optional);
1082
- if (shape !== void 0) return shape;
1083
- }
1084
- }
1085
- };
1086
- //#endregion
1087
1177
  //#region src/selection.ts
1088
1178
  /**
1089
1179
  * Selections: the fields a Surface picks of an entity (nested through
@@ -1109,15 +1199,20 @@ const relationOf = (selection) => ({
1109
1199
  ...selection.connections === void 0 ? {} : { windows: selection.connections },
1110
1200
  ...selection.relations === void 0 ? {} : { relations: selection.relations }
1111
1201
  });
1202
+ const fromEntitySelections = /* @__PURE__ */ new WeakMap();
1112
1203
  const Selection = {
1113
1204
  /**
1114
1205
  * The Remote Selection of a `foldkit-entity` Selection: the same members, read
1115
1206
  * from Remote's store. `true` on a relation reads its refs, as it does here.
1116
1207
  */
1117
1208
  from: (selection) => {
1209
+ const compiled = fromEntitySelections.get(selection);
1210
+ if (compiled !== void 0) return compiled;
1118
1211
  const spec = {};
1119
1212
  for (const [key, selected] of Object.entries(selection.members)) spec[key] = selected === true ? true : Domain.SelectionPageTypeId in selected ? Selection.connection(Entity.from(selected.selection.entity), selected.window, Selection.from(selected.selection)) : Selection.from(selected);
1120
- return Selection.make(Entity.from(selection.entity), spec);
1213
+ const made = Selection.make(Entity.from(selection.entity), spec);
1214
+ fromEntitySelections.set(selection, made);
1215
+ return made;
1121
1216
  },
1122
1217
  /**
1123
1218
  * The fields to read of `entity`. A nested `Selection` on a relation field
@@ -1127,12 +1222,14 @@ const Selection = {
1127
1222
  make: (entity, selection) => {
1128
1223
  if (Object.keys(selection).length === 0) throw new Error(`Selection.make: a selection of "${entity.name}" picks at least one field`);
1129
1224
  const picked = {};
1225
+ const names = [];
1130
1226
  const connections = {};
1131
1227
  const relations = {};
1132
1228
  for (const key of Object.keys(selection)) {
1133
1229
  const choice = selection[key];
1134
1230
  if (choice === true) {
1135
1231
  picked[key] = entity.fields[key];
1232
+ names.push(key);
1136
1233
  continue;
1137
1234
  }
1138
1235
  const nested = choice;
@@ -1141,18 +1238,21 @@ const Selection = {
1141
1238
  if (nested.entity !== shape.entity) throw new Error(`Selection.make: "${key}" on "${entity.name}" refers to "${shape.entity}", not "${nested.entity}"`);
1142
1239
  const nullable = (codec) => shape.nullable ? Schema.NullOr(codec) : codec;
1143
1240
  if (nested.window !== void 0) {
1144
- connections[key] = nested.window;
1241
+ const name = shape.kind === "many" ? relationAlias(key, nested.window) : key;
1242
+ names.push(name);
1243
+ connections[name] = nested.window;
1145
1244
  picked[key] = nullable(nested.schema);
1146
- if (nested.fields.length > 0) relations[key] = relationOf(nested);
1245
+ if (nested.fields.length > 0) relations[name] = relationOf(nested);
1147
1246
  continue;
1148
1247
  }
1248
+ names.push(key);
1149
1249
  const item = nested.schema;
1150
1250
  picked[key] = nullable(shape.kind === "many" ? Schema.Array(item) : shape.kind === "page" ? pageSchema(item) : item);
1151
1251
  relations[key] = relationOf(nested);
1152
1252
  }
1153
1253
  return {
1154
1254
  entity: entity.name,
1155
- fields: Object.keys(selection),
1255
+ fields: names,
1156
1256
  schema: Schema.Struct(picked),
1157
1257
  ...Object.keys(connections).length === 0 ? {} : { connections },
1158
1258
  ...Object.keys(relations).length === 0 ? {} : { relations }
@@ -1189,12 +1289,12 @@ const assemble = (store, key, requirement) => {
1189
1289
  refreshing ||= isFieldStale(store, key, field);
1190
1290
  const relation = requirement.relations?.[field];
1191
1291
  if (relation === void 0) {
1192
- values[field] = value.value;
1292
+ values[aliasedField(field)] = value.value;
1193
1293
  continue;
1194
1294
  }
1195
1295
  const nested = assembleRelation(store, value.value, relation);
1196
1296
  if (nested === void 0) return void 0;
1197
- values[field] = nested.values;
1297
+ values[aliasedField(field)] = nested.values;
1198
1298
  refreshing ||= nested.refreshing;
1199
1299
  }
1200
1300
  return {
@@ -1291,6 +1391,8 @@ const Entity = {
1291
1391
  return descriptor;
1292
1392
  },
1293
1393
  make: (name, schema) => {
1394
+ const reserved = Object.keys(schema.fields).find((field) => field.includes("@"));
1395
+ if (reserved !== void 0) throw new Error(`Entity.make: field "${reserved}" of "${name}" contains "@", which names a page of a relation`);
1294
1396
  const descriptor = {
1295
1397
  name,
1296
1398
  schema,
@@ -1332,6 +1434,118 @@ const Entity = {
1332
1434
  })
1333
1435
  };
1334
1436
  //#endregion
1437
+ //#region src/matching.ts
1438
+ /**
1439
+ * Running a query body against the rows the client already holds.
1440
+ *
1441
+ * local-execution-DESIGN phase 3. A connection's rows are edges the server
1442
+ * delivered, and until now nothing on the client ever evaluated a body — which
1443
+ * is why an optimistic insert has to guess between `prepend` and `append`, and
1444
+ * why `LivePolicy` exists to be told in advance what to do with a row nobody
1445
+ * could judge.
1446
+ *
1447
+ * This judges them. It is pure, it performs no I/O, and it reuses the reference
1448
+ * interpreter in `foldkit-entity` rather than reimplementing the semantics —
1449
+ * so a body means here exactly what it means on the server, checked by the same
1450
+ * conformance suite.
1451
+ *
1452
+ * **It answers "which of the rows I hold match", never "which rows match".**
1453
+ * Those are different questions and the difference is not recoverable from the
1454
+ * result, so it is in the shape: `matched` is what satisfied the body, and
1455
+ * `skipped` is what was held but could not be judged. A caller that knows it
1456
+ * holds the whole population — a connection terminal at both ends — is the one
1457
+ * that can turn this into a complete answer. Nothing here can.
1458
+ *
1459
+ * **It is not authorization.** On the server a compiled `where` is conjoined
1460
+ * with the binding's `visible` rule. There is no `visible` here, and this is
1461
+ * safe only because the client holds only rows the server already released to
1462
+ * it. A local filter is not an access decision.
1463
+ */
1464
+ /**
1465
+ * The fields of the body's own Entity that a row must have for the body to be
1466
+ * decidable about it — its predicates and its ordering terms.
1467
+ *
1468
+ * `id` is never among them: it is the key, so it is known for every row in the
1469
+ * store whether or not anything fetched it as a field.
1470
+ */
1471
+ const decidableOn = (body) => Query$1.dependencies(body).fields.filter((field) => field.entity === body.entity.name && field.key !== "id").map((field) => field.key);
1472
+ /**
1473
+ * The rows of `descriptor`'s Entity that the store holds and the body matches.
1474
+ *
1475
+ * The input is given **decoded**, as an application writes it, and encoded here
1476
+ * through the descriptor's own `Input` codec. That is deliberate and is the
1477
+ * whole reason this takes a descriptor rather than a bare body: a store holds
1478
+ * wire values, so an input has to be in that space to be compared against them,
1479
+ * and handing an interpreter a decoded value is a mistake **no runtime error
1480
+ * catches** — the reference interpreter answers "no rows", which reads exactly
1481
+ * like a correct answer. Doing it here means a caller cannot get it wrong.
1482
+ *
1483
+ * Literals in the body are deliberately *not* encoded. A literal written in
1484
+ * domain terms is already broken against the server, which binds it to a column
1485
+ * the same way; accepting it here would make the client quietly more permissive
1486
+ * than the thing it has to agree with.
1487
+ */
1488
+ const matching = (store, descriptor, input, options = {}) => matchingEncoded(store, descriptor, Schema.encodeSync(descriptor.Input)(input), options);
1489
+ /**
1490
+ * As `matching`, for the callers in this module that already hold the input
1491
+ * **encoded**. Not exported: `matching` is the way in, and it cannot be given
1492
+ * the wrong space.
1493
+ */
1494
+ const matchingEncoded = (store, descriptor, encoded, options = {}) => {
1495
+ const body = descriptor.body;
1496
+ 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.`);
1497
+ const prefix = `${body.entity.name}:`;
1498
+ const needed = decidableOn(body);
1499
+ const candidates = options.among ?? Object.keys(store).filter((key) => key.startsWith(prefix));
1500
+ const rows = [];
1501
+ const skipped = [];
1502
+ for (const key of candidates) {
1503
+ if (!key.startsWith(prefix)) continue;
1504
+ const entry = store[key];
1505
+ if (entry === void 0) {
1506
+ skipped.push(key);
1507
+ continue;
1508
+ }
1509
+ if (entry.tombstone) continue;
1510
+ if (needed.some((field) => !entry.present.has(field))) {
1511
+ skipped.push(key);
1512
+ continue;
1513
+ }
1514
+ rows.push("id" in entry.values ? {
1515
+ ...entry.values,
1516
+ __key: key
1517
+ } : {
1518
+ ...entry.values,
1519
+ id: key.slice(prefix.length),
1520
+ __key: key
1521
+ });
1522
+ }
1523
+ return {
1524
+ matched: evaluate(body, encoded, rows).map((row) => row.__key),
1525
+ skipped
1526
+ };
1527
+ };
1528
+ /**
1529
+ * Takes the input **encoded**, because its one caller holds it that way: a
1530
+ * connection identity carries the canonical encoded input, and making it decode
1531
+ * only for this to encode again would be a round trip that can fail.
1532
+ *
1533
+ * A decoded-input sibling was written first and deleted: nothing called it. The
1534
+ * asymmetry with `matching`, which takes a decoded input, is the shape of the
1535
+ * two real callers rather than an oversight.
1536
+ */
1537
+ const belongsEncoded = (store, descriptor, encoded, key) => {
1538
+ const entry = store[key];
1539
+ if (entry === void 0) return "unknown";
1540
+ if (entry.tombstone) return "no";
1541
+ const body = descriptor.body;
1542
+ if (body !== void 0 && decidableOn(body).some((field) => entry.stale.has(field))) return "unknown";
1543
+ const judged = matchingEncoded(store, descriptor, encoded, { among: [key] });
1544
+ if (judged.matched.includes(key)) return "yes";
1545
+ if (judged.skipped.includes(key)) return "unknown";
1546
+ return "no";
1547
+ };
1548
+ //#endregion
1335
1549
  //#region src/inspect.ts
1336
1550
  const inspectEntry = (key, entry) => ({
1337
1551
  key,
@@ -1347,6 +1561,8 @@ const inspectRemote = (model) => ({
1347
1561
  connections: Object.keys(model.connections),
1348
1562
  live: Object.keys(model.live),
1349
1563
  gaps: [...model.gaps],
1564
+ loading: [...model.loading],
1565
+ failures: model.failures,
1350
1566
  mutations: {
1351
1567
  pending: [...model.mutations.pending],
1352
1568
  failed: [...model.mutations.failed],
@@ -1524,7 +1740,6 @@ const visibleItems = (connection, connectionId, overlays, store) => {
1524
1740
  //#region src/live.ts
1525
1741
  const emptyLiveState = {
1526
1742
  cursor: 0,
1527
- stale: /* @__PURE__ */ new Set(),
1528
1743
  boundary: {}
1529
1744
  };
1530
1745
  /**
@@ -1558,10 +1773,6 @@ const recordBoundary = (state, connection, position) => {
1558
1773
  }
1559
1774
  };
1560
1775
  };
1561
- const invalidateConnection = (state, connection) => ({
1562
- ...state,
1563
- stale: /* @__PURE__ */ new Set([...state.stale, connection])
1564
- });
1565
1776
  /**
1566
1777
  * A subscriber is woken only if the event changed a field it selects. A
1567
1778
  * change to an unselected field is skipped entirely.
@@ -1614,9 +1825,10 @@ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
1614
1825
  outcome
1615
1826
  };
1616
1827
  case "invalidate": return {
1617
- state: invalidateConnection(advance(state, event.cursor), event.connection),
1828
+ state: advance(state, event.cursor),
1618
1829
  optimistic,
1619
- outcome
1830
+ outcome,
1831
+ invalidated: event.connection
1620
1832
  };
1621
1833
  case "ignore": return {
1622
1834
  state: advance(state, event.cursor),
@@ -1635,27 +1847,13 @@ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
1635
1847
  outcome
1636
1848
  };
1637
1849
  case "ConnectionInvalidate": return {
1638
- state: invalidateConnection(advance(state, event.cursor), event.connection),
1850
+ state: advance(state, event.cursor),
1851
+ invalidated: event.connection,
1639
1852
  optimistic,
1640
1853
  outcome
1641
1854
  };
1642
1855
  }
1643
1856
  };
1644
- const isStale = (state, connection) => state.stale.has(connection);
1645
- /**
1646
- * Clears a connection's stale mark once a fresh page has been adopted. An
1647
- * invalidating event only records that a refetch is due; whoever merges the
1648
- * refetched page calls this so `isStale` stops reporting.
1649
- */
1650
- const refreshConnection = (state, connection) => {
1651
- if (!state.stale.has(connection)) return state;
1652
- const stale = new Set(state.stale);
1653
- stale.delete(connection);
1654
- return {
1655
- ...state,
1656
- stale
1657
- };
1658
- };
1659
1857
  /** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
1660
1858
  const liveHasPrevious = (connection, state, connectionId) => hasPrevious(connection) || (state.boundary[connectionId]?.before ?? 0) > 0;
1661
1859
  /** `hasNext` accounting for edges recorded outside the loaded boundary. */
@@ -1797,17 +1995,33 @@ const gc = (state, roots) => {
1797
1995
  * application's choice.
1798
1996
  */
1799
1997
  /** Bump when the serialized shape changes; a mismatch discards the cache. */
1800
- const REMOTE_CACHE_VERSION = 3;
1998
+ const REMOTE_CACHE_VERSION = 4;
1999
+ /**
2000
+ * A snapshot of a Model, keeping the entity cache and only the connections
2001
+ * named.
2002
+ *
2003
+ * Naming none — the default — is exactly what Remote did before connections
2004
+ * could be kept at all: a disposable, server-derived cache.
2005
+ */
2006
+ const snapshotOf = (model, options = {}) => ({
2007
+ entities: model.entities,
2008
+ connections: Object.fromEntries((options.connections ?? []).flatMap((connection) => {
2009
+ const identity = connectionIdentity(connection);
2010
+ const held = model.connections[identity];
2011
+ return held === void 0 ? [] : [[identity, held.segments.map((segment) => segment.edges)]];
2012
+ }))
2013
+ });
1801
2014
  const sorted = (values) => [...values].sort();
1802
2015
  /**
1803
2016
  * The snapshot's data shape. `stableStringify` sorts object keys, and the
1804
2017
  * field sets are sorted here, so equal stores give byte-equal snapshots
1805
2018
  * whatever order they were built in.
1806
2019
  */
1807
- const serializeStore = (store, scope) => ({
1808
- version: 3,
2020
+ const serializeStore = (snapshot, scope) => ({
2021
+ version: 4,
1809
2022
  scope: scope ?? null,
1810
- entities: Object.fromEntries(Object.entries(store).map(([key, entry]) => [key, {
2023
+ connections: snapshot.connections,
2024
+ entities: Object.fromEntries(Object.entries(snapshot.entities).map(([key, entry]) => [key, {
1811
2025
  values: entry.values,
1812
2026
  present: sorted(entry.present),
1813
2027
  stale: sorted(entry.stale),
@@ -1837,9 +2051,30 @@ const parseEntry = (value) => {
1837
2051
  windows: entry.windows
1838
2052
  };
1839
2053
  };
1840
- const deserializeStore = (serialized) => Object.fromEntries(Object.entries(serialized.entities).map(([key, entry]) => [key, parseEntry(entry)]));
1841
- function dehydrate(store, options = {}) {
1842
- const text = stableStringify(serializeStore(store, options.scope));
2054
+ /** Throws on a malformed edge so the whole snapshot is discarded. */
2055
+ const parseEdges = (value) => {
2056
+ if (!Array.isArray(value)) throw new Error("segment is not an array of edges");
2057
+ return value.map((edge) => {
2058
+ const candidate = edge;
2059
+ 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 } }");
2060
+ return {
2061
+ key: candidate.key,
2062
+ ref: {
2063
+ entity: candidate.ref.entity,
2064
+ id: candidate.ref.id
2065
+ }
2066
+ };
2067
+ });
2068
+ };
2069
+ const deserializeStore = (serialized) => ({
2070
+ entities: Object.fromEntries(Object.entries(serialized.entities).map(([key, entry]) => [key, parseEntry(entry)])),
2071
+ connections: Object.fromEntries(Object.entries(serialized.connections ?? {}).map(([key, segments]) => {
2072
+ if (!Array.isArray(segments)) throw new Error("connection is not an array of segments");
2073
+ return [key, segments.map(parseEdges)];
2074
+ }))
2075
+ });
2076
+ function dehydrate(snapshot, options = {}) {
2077
+ const text = stableStringify(serializeStore(snapshot, options.scope));
1843
2078
  return exceeds(text, options.maxBytes) ? void 0 : text;
1844
2079
  }
1845
2080
  /**
@@ -1869,7 +2104,7 @@ const hydrate = (raw, options = {}) => {
1869
2104
  }
1870
2105
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
1871
2106
  const candidate = parsed;
1872
- if (candidate.version !== 3) return void 0;
2107
+ if (candidate.version !== 4) return void 0;
1873
2108
  if ((candidate.scope ?? null) !== (options.scope ?? null)) return void 0;
1874
2109
  if (candidate.entities === null || typeof candidate.entities !== "object") return void 0;
1875
2110
  if (Array.isArray(candidate.entities)) return void 0;
@@ -1892,37 +2127,49 @@ const mergeStores = (current, snapshot, policy) => policy === "replace" ? {
1892
2127
  ...snapshot,
1893
2128
  ...current
1894
2129
  };
2130
+ /** An empty snapshot: the cache as a fresh session has it. */
2131
+ const emptySnapshot = {
2132
+ entities: emptyStore,
2133
+ connections: {}
2134
+ };
1895
2135
  const RemotePersistence = {
1896
2136
  /** The snapshot text, or `undefined` when it would exceed `maxBytes`. */
1897
2137
  dehydrate,
1898
- /** The store a snapshot text holds, or `undefined` when it is refused. */
2138
+ /** What a snapshot text holds, or `undefined` when it is refused. */
1899
2139
  hydrate,
1900
- /** A snapshot brought into a store by policy. */
2140
+ /**
2141
+ * What to keep of a Model, naming the connections that should survive a
2142
+ * reload. Naming none is the default and is what Remote always did.
2143
+ */
2144
+ snapshotOf,
2145
+ /** A snapshot's entities brought into a store by policy. */
1901
2146
  mergeStores,
2147
+ /** The cache as a fresh session has it; what a refused `restore` yields. */
2148
+ emptySnapshot,
1902
2149
  /**
1903
2150
  * Writes the snapshot under `key`, or removes the key when the store would
1904
2151
  * exceed `maxBytes`, so a stale smaller snapshot cannot outlive a larger
1905
2152
  * store it no longer describes.
1906
2153
  */
1907
- save: (store, options) => Effect.gen(function* () {
2154
+ save: (snapshot, options) => Effect.gen(function* () {
1908
2155
  const kv = yield* KeyValueStore.KeyValueStore;
1909
- const text = dehydrate(store, options);
2156
+ const text = dehydrate(snapshot, options);
1910
2157
  if (text === void 0) yield* kv.remove(options.key);
1911
2158
  else yield* kv.set(options.key, text);
1912
2159
  }),
1913
2160
  /**
1914
- * Reads the cache. A missing key yields `emptyStore`; anything `hydrate`
1915
- * refuses yields `emptyStore` and removes the key, so the next `restore` is
1916
- * clean and the planner refetches.
2161
+ * Reads the cache. A missing key yields `emptySnapshot`; anything `hydrate`
2162
+ * refuses yields `emptySnapshot` and removes the key, so the next `restore`
2163
+ * is clean and the planner refetches.
1917
2164
  */
1918
2165
  restore: (options) => Effect.gen(function* () {
1919
2166
  const kv = yield* KeyValueStore.KeyValueStore;
1920
2167
  const raw = yield* kv.get(options.key);
1921
- if (raw === void 0) return emptyStore;
2168
+ if (raw === void 0) return emptySnapshot;
1922
2169
  const restored = hydrate(raw, options);
1923
2170
  if (restored === void 0) {
1924
2171
  yield* kv.remove(options.key);
1925
- return emptyStore;
2172
+ return emptySnapshot;
1926
2173
  }
1927
2174
  return restored;
1928
2175
  })
@@ -1930,6 +2177,8 @@ const RemotePersistence = {
1930
2177
  //#endregion
1931
2178
  //#region src/remoteData.ts
1932
2179
  /** `RemoteData`: the state of a value the store may not hold yet. */
2180
+ const fresh = { _tag: "Fresh" };
2181
+ const refreshing = { _tag: "Refreshing" };
1933
2182
  const remoteErrorSchema = Schema.Struct({
1934
2183
  _tag: Schema.String,
1935
2184
  message: Schema.String
@@ -1977,6 +2226,43 @@ const RemoteData = {
1977
2226
  case "NotFound": return cases.NotFound();
1978
2227
  }
1979
2228
  },
2229
+ /**
2230
+ * The view-oriented fold: the six states as the three things a view actually
2231
+ * draws, under the policy that useful data stays on screen. A value that is
2232
+ * being refetched, or that a later read failed to replace, still reaches
2233
+ * `data` — with `freshness` saying which — instead of being replaced by a
2234
+ * spinner or an error the reader cannot act on.
2235
+ *
2236
+ * ```ts
2237
+ * RemoteData.render(model.user, {
2238
+ * loading: () => UserSkeleton(),
2239
+ * notFound: () => NoSuchUser(),
2240
+ * failed: error => ErrorView(error),
2241
+ * data: (user, freshness) => UserView({ user, dimmed: freshness._tag !== 'Fresh' }),
2242
+ * })
2243
+ * ```
2244
+ *
2245
+ * `notFound` is its own branch and not optional: a row the server answered
2246
+ * for and does not have is neither loading nor a failure, and drawing it as
2247
+ * either is a spinner that never ends or an error nobody can fix.
2248
+ *
2249
+ * This does not replace `match`, which is the exhaustive fold over the states
2250
+ * themselves. Reach for `match` when the six states really do draw
2251
+ * differently, and for this when they draw the usual three ways.
2252
+ */
2253
+ render: (data, cases) => {
2254
+ switch (data._tag) {
2255
+ case "Initial":
2256
+ case "Loading": return cases.loading();
2257
+ case "NotFound": return cases.notFound();
2258
+ case "Ready": return cases.data(data.value, fresh);
2259
+ case "Refreshing": return cases.data(data.value, refreshing);
2260
+ case "Failed": return data.previous === void 0 ? cases.failed(data.error) : cases.data(data.previous, {
2261
+ _tag: "Stale",
2262
+ error: data.error
2263
+ });
2264
+ }
2265
+ },
1980
2266
  map: (data, f) => {
1981
2267
  switch (data._tag) {
1982
2268
  case "Ready": return {
@@ -2006,6 +2292,15 @@ const RemoteData = {
2006
2292
  * optimistic layers an application keeps in its Model, the Messages that
2007
2293
  * change it, and the pure reducer over them.
2008
2294
  */
2295
+ const noFailures = {
2296
+ connections: {},
2297
+ fields: {}
2298
+ };
2299
+ const emptyRefresh = {
2300
+ generation: 0,
2301
+ requested: /* @__PURE__ */ new Map(),
2302
+ started: /* @__PURE__ */ new Map()
2303
+ };
2009
2304
  const initialRemoteModel = {
2010
2305
  entities: emptyStore,
2011
2306
  connections: {},
@@ -2014,10 +2309,8 @@ const initialRemoteModel = {
2014
2309
  mutations: emptyMutationState,
2015
2310
  gaps: /* @__PURE__ */ new Set(),
2016
2311
  loading: /* @__PURE__ */ new Set(),
2017
- refresh: {
2018
- requested: 0,
2019
- started: 0
2020
- }
2312
+ refresh: emptyRefresh,
2313
+ failures: noFailures
2021
2314
  };
2022
2315
  /** The entity store and the mutation ledger are runtime values, not wire shapes. */
2023
2316
  const runtimeSchema = Schema.Unknown;
@@ -2029,9 +2322,10 @@ const remoteModelSchema = () => Schema.Struct({
2029
2322
  mutations: runtimeSchema,
2030
2323
  gaps: runtimeSchema,
2031
2324
  loading: runtimeSchema,
2032
- refresh: Schema.Struct({
2033
- requested: Schema.Number,
2034
- started: Schema.Number
2325
+ refresh: runtimeSchema,
2326
+ failures: Schema.Struct({
2327
+ connections: Schema.Record(Schema.String, remoteErrorSchema),
2328
+ fields: Schema.Record(Schema.String, remoteErrorSchema)
2035
2329
  })
2036
2330
  });
2037
2331
  const retentionRootsSchema = Schema.Struct({
@@ -2053,15 +2347,18 @@ const remoteMessageCases = {
2053
2347
  },
2054
2348
  ReadFailed: {
2055
2349
  requests: Schema.Array(ReadRequest),
2056
- error: remoteErrorSchema
2350
+ error: remoteErrorSchema,
2351
+ stream: Schema.optional(Schema.String)
2057
2352
  },
2058
2353
  RefreshStarted: { requests: Schema.Array(ReadRequest) },
2059
2354
  ReadStarted: {
2060
2355
  requests: Schema.Array(ReadRequest),
2061
2356
  refresh: Schema.optional(Schema.Number)
2062
2357
  },
2358
+ QueryStarted: { connections: Schema.Array(Schema.String) },
2063
2359
  RetentionChanged: { roots: retentionRootsSchema },
2064
2360
  Hydrated: {
2361
+ connections: Schema.optional(Schema.Unknown),
2065
2362
  entities: runtimeSchema,
2066
2363
  merge: Schema.Union([Schema.Literal("replace"), Schema.Literal("preserve-existing")])
2067
2364
  },
@@ -2082,6 +2379,11 @@ const remoteMessageCases = {
2082
2379
  requestId: Schema.String,
2083
2380
  error: remoteErrorSchema
2084
2381
  },
2382
+ OverlayShown: {
2383
+ id: Schema.String,
2384
+ optimistic: Schema.Array(Schema.Unknown)
2385
+ },
2386
+ OverlayLifted: { id: Schema.String },
2085
2387
  LiveReceived: {
2086
2388
  stream: Schema.String,
2087
2389
  event: Schema.Unknown,
@@ -2110,39 +2412,141 @@ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(
2110
2412
  _tag: Schema.Literal(tag),
2111
2413
  ...fields
2112
2414
  })));
2415
+ /** An overlay's layer, named apart from every request's: a request id is the application's to choose too. */
2416
+ const overlayLayer = (id) => `overlay:${id}`;
2113
2417
  const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
2114
2418
  /**
2115
- * One mark per requested field. A read is identified by what it asks for, not by
2116
- * a request id: two reads asking for one field share its mark, so the first
2117
- * answer clears it. Over-clearing shows `Initial` rather than a spinner, which
2118
- * is the safer way to be wrong.
2419
+ * One mark per requested field, `entity\0id\0field`. A read is identified by
2420
+ * what it asks for, not by a request id: two reads asking for one field share
2421
+ * its mark, so the first answer clears it. Over-clearing shows `Initial` rather
2422
+ * than a spinner, which is the safer way to be wrong. The refresh generations
2423
+ * below key off the same marks, so "what a read asks for" means one thing.
2119
2424
  */
2120
- const loadingMarks = function* (requests) {
2121
- for (const request of requests) {
2122
- const key = entityKey(request.entity, request.id);
2123
- for (const field of request.fields) yield `${key}\u0000${field}`;
2124
- }
2425
+ const fieldMark = (entity, id, field) => `${entityKey(entity, id)}\u0000${field}`;
2426
+ const fieldMarks = function* (requests) {
2427
+ for (const request of requests) for (const field of request.fields) yield fieldMark(request.entity, request.id, field);
2125
2428
  };
2126
2429
  const withLoading = (loading, requests) => {
2127
2430
  const next = new Set(loading);
2128
- for (const mark of loadingMarks(requests)) next.add(mark);
2431
+ for (const mark of fieldMarks(requests)) next.add(mark);
2129
2432
  return next;
2130
2433
  };
2131
2434
  const withoutLoading = (loading, requests) => {
2132
2435
  if (loading.size === 0) return loading;
2133
2436
  const next = new Set(loading);
2134
- for (const mark of loadingMarks(requests)) next.delete(mark);
2437
+ for (const mark of fieldMarks(requests)) next.delete(mark);
2135
2438
  return next;
2136
2439
  };
2137
2440
  /**
2441
+ * A connection's mark. The leading separator keeps it out of the field marks'
2442
+ * space, whatever a connection identity happens to spell.
2443
+ */
2444
+ const connectionPrefix = "\0connection\0";
2445
+ const connectionMark = (identity) => `${connectionPrefix}${identity}`;
2446
+ /** Whether a query for this connection is in flight. */
2447
+ const isQueryLoading = (model, identity) => model.loading.size !== 0 && model.loading.has(connectionMark(identity));
2448
+ const withoutQueryLoading = (loading, identity) => {
2449
+ if (!loading.has(connectionMark(identity))) return loading;
2450
+ const next = new Set(loading);
2451
+ next.delete(connectionMark(identity));
2452
+ return next;
2453
+ };
2454
+ /**
2455
+ * The in-flight marks of connections the roots still name. A query the entry
2456
+ * stopped waiting for — it was restarted or deactivated before an answer came —
2457
+ * leaves its mark behind, and a connection released with that mark would read
2458
+ * `Loading` forever the next time something asked for it before sending.
2459
+ */
2460
+ const prunedQueryLoading = (loading, roots) => {
2461
+ const named = new Set(roots.connections.map((root) => connectionMark(root.identity)));
2462
+ const kept = [...loading].filter((mark) => !mark.startsWith(connectionPrefix) || named.has(mark));
2463
+ return kept.length === loading.size ? loading : new Set(kept);
2464
+ };
2465
+ /**
2466
+ * Drops the refresh marks of entities and connections that were collected.
2467
+ * Nothing observes a collected entity — the retention roots are the active
2468
+ * Surfaces — so no read entry's generation can fall back when its mark goes.
2469
+ * Without this the generations would be bounded by how many things the
2470
+ * application has ever refreshed rather than by what it currently holds, which
2471
+ * for a list refreshed on a timer is the same as not bounded at all.
2472
+ */
2473
+ const prunedRefresh = (refresh, retained) => {
2474
+ const live = (mark) => mark.startsWith(connectionPrefix) ? retained.connections[mark.slice(12)] !== void 0 : retained.entities[mark.slice(0, mark.indexOf("\0"))] !== void 0;
2475
+ const keep = (marks) => {
2476
+ const next = /* @__PURE__ */ new Map();
2477
+ for (const [mark, generation] of marks) if (live(mark)) next.set(mark, generation);
2478
+ return next.size === marks.size ? marks : next;
2479
+ };
2480
+ const requested = keep(refresh.requested);
2481
+ const started = keep(refresh.started);
2482
+ return requested === refresh.requested && started === refresh.started ? refresh : {
2483
+ ...refresh,
2484
+ requested,
2485
+ started
2486
+ };
2487
+ };
2488
+ /**
2489
+ * The generation a read is planned under: the highest any of the fields it
2490
+ * reads or connections it runs was refreshed at. A read entry carries this as a
2491
+ * dependency, so it restarts when what it observes is refreshed and not when
2492
+ * anything else is.
2493
+ */
2494
+ const refreshedAt = (refresh, requests, connections = []) => {
2495
+ let at = 0;
2496
+ for (const mark of fieldMarks(requests)) at = Math.max(at, refresh.requested.get(mark) ?? 0);
2497
+ for (const identity of connections) at = Math.max(at, refresh.requested.get(connectionMark(identity)) ?? 0);
2498
+ return at;
2499
+ };
2500
+ /**
2501
+ * Marks every field of `requests` and every connection in `connections`
2502
+ * refreshed at the next generation.
2503
+ */
2504
+ const withRefreshRequested = (refresh, requests, connections = []) => {
2505
+ const generation = refresh.generation + 1;
2506
+ const requested = new Map(refresh.requested);
2507
+ for (const mark of fieldMarks(requests)) requested.set(mark, generation);
2508
+ for (const identity of connections) requested.set(connectionMark(identity), generation);
2509
+ return {
2510
+ ...refresh,
2511
+ generation,
2512
+ requested
2513
+ };
2514
+ };
2515
+ /**
2516
+ * Records that a read of `requests` began under `generation`. A read that
2517
+ * carries no generation began under none, and leaves the marks as they are.
2518
+ */
2519
+ const withRefreshStarted = (refresh, requests, generation) => {
2520
+ if (generation === void 0 || generation === 0) return refresh;
2521
+ let started;
2522
+ for (const mark of fieldMarks(requests)) {
2523
+ if ((refresh.started.get(mark) ?? 0) >= generation) continue;
2524
+ started ??= new Map(refresh.started);
2525
+ started.set(mark, generation);
2526
+ }
2527
+ return started === void 0 ? refresh : {
2528
+ ...refresh,
2529
+ started
2530
+ };
2531
+ };
2532
+ /**
2533
+ * Whether one field's read already began under the generation that field was
2534
+ * last refreshed at. Such a read is in flight and would otherwise outlive the
2535
+ * refresh, so the refresh takes a new generation to restart it even though the
2536
+ * field is already stale.
2537
+ */
2538
+ const refreshIsInFlight = (refresh, request, field) => {
2539
+ const mark = fieldMark(request.entity, request.id, field);
2540
+ return (refresh.started.get(mark) ?? 0) === (refresh.requested.get(mark) ?? 0);
2541
+ };
2542
+ /**
2138
2543
  * Whether a read is fetching any of `fields` for this entity. Consulted only
2139
2544
  * when the store lacks the value, which is what separates `Loading` from
2140
2545
  * `Initial`.
2141
2546
  */
2142
2547
  const isLoading = (model, entity, id, fields) => {
2143
2548
  if (model.loading.size === 0) return false;
2144
- const key = entityKey(entity, id);
2145
- return fields.some((field) => model.loading.has(`${key}\u0000${field}`));
2549
+ return fields.some((field) => model.loading.has(fieldMark(entity, id, field)));
2146
2550
  };
2147
2551
  const setConnectionStale = (connections, connection, stale) => {
2148
2552
  const current = connections[connection] ?? emptyConnection;
@@ -2172,39 +2576,71 @@ const updateRemote = (model, message) => {
2172
2576
  case "ReadReceived": return {
2173
2577
  ...model,
2174
2578
  entities: writeRead(model.entities, message.requests, message.result, message.now),
2175
- loading: withoutLoading(model.loading, message.requests)
2176
- };
2177
- case "ReadFailed": return {
2178
- ...model,
2179
- entities: setStale(model.entities, marksOf(message.requests), false),
2180
- loading: withoutLoading(model.loading, message.requests)
2181
- };
2182
- case "RetentionChanged": return {
2183
- ...model,
2184
- ...gc(model, message.roots)
2185
- };
2186
- case "Hydrated": return {
2187
- ...model,
2188
- entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge)
2579
+ loading: withoutLoading(model.loading, message.requests),
2580
+ failures: withoutFieldFailures(model.failures, [...fieldMarks(message.requests), ...patchedMarks(message.result.entities)])
2189
2581
  };
2190
- case "RefreshStarted": {
2191
- const entities = setStale(model.entities, marksOf(message.requests), true);
2192
- return entities === model.entities ? model : {
2582
+ case "ReadFailed":
2583
+ if (message.stream !== void 0) return markGap(model, message.stream);
2584
+ return {
2193
2585
  ...model,
2194
- entities
2586
+ entities: setStale(model.entities, marksOf(message.requests), false),
2587
+ loading: withoutLoading(model.loading, message.requests),
2588
+ failures: withFieldFailures(model.failures, message.requests, message.error)
2589
+ };
2590
+ case "RetentionChanged": {
2591
+ const retained = gc(model, message.roots);
2592
+ return {
2593
+ ...model,
2594
+ ...retained,
2595
+ refresh: prunedRefresh(model.refresh, retained),
2596
+ failures: prunedFailures(model, message.roots),
2597
+ loading: prunedQueryLoading(model.loading, message.roots)
2195
2598
  };
2196
2599
  }
2197
- case "ReadStarted": {
2198
- const started = Math.max(model.refresh.started, message.refresh ?? 0);
2600
+ case "Hydrated": {
2601
+ const restored = Object.entries(message.connections ?? {}).map(([identity, segments]) => [identity, {
2602
+ segments: segments.map((edges) => ({
2603
+ edges: [...edges],
2604
+ start: unknown,
2605
+ end: unknown
2606
+ })),
2607
+ stale: true
2608
+ }]);
2199
2609
  return {
2200
2610
  ...model,
2201
- loading: withLoading(model.loading, message.requests),
2202
- refresh: started === model.refresh.started ? model.refresh : {
2203
- ...model.refresh,
2204
- started
2611
+ entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
2612
+ connections: message.merge === "replace" ? {
2613
+ ...model.connections,
2614
+ ...Object.fromEntries(restored)
2615
+ } : {
2616
+ ...Object.fromEntries(restored),
2617
+ ...model.connections
2205
2618
  }
2206
2619
  };
2207
2620
  }
2621
+ case "RefreshStarted": {
2622
+ const asked = message.requests.reduce((store, request) => isTombstone(store, entityKey(request.entity, request.id)) ? remove(store, entityKey(request.entity, request.id)) : store, model.entities);
2623
+ const entities = setStale(asked, marksOf(message.requests), true);
2624
+ const failures = withoutFieldFailures(model.failures, fieldMarks(message.requests));
2625
+ return entities === model.entities && failures === model.failures ? model : {
2626
+ ...model,
2627
+ entities,
2628
+ failures
2629
+ };
2630
+ }
2631
+ case "QueryStarted": {
2632
+ const loading = new Set(model.loading);
2633
+ for (const identity of message.connections) loading.add(connectionMark(identity));
2634
+ return loading.size === model.loading.size ? model : {
2635
+ ...model,
2636
+ loading
2637
+ };
2638
+ }
2639
+ case "ReadStarted": return {
2640
+ ...model,
2641
+ loading: withLoading(model.loading, message.requests),
2642
+ refresh: withRefreshStarted(model.refresh, message.requests, message.refresh)
2643
+ };
2208
2644
  case "MutationStarted": return {
2209
2645
  ...model,
2210
2646
  mutations: beginMutation(model.mutations, message.requestId),
@@ -2216,7 +2652,19 @@ const updateRemote = (model, message) => {
2216
2652
  ...model,
2217
2653
  entities: settled.store,
2218
2654
  mutations: settled.state,
2219
- optimistic: settled.optimistic
2655
+ optimistic: settled.optimistic,
2656
+ failures: withoutFieldFailures(model.failures, [...patchedMarks(message.entities), ...(message.deleted ?? []).flatMap((ref) => entityFailureMarks(model.failures, entityKey(ref.entity, ref.id)))])
2657
+ };
2658
+ }
2659
+ case "OverlayShown": return {
2660
+ ...model,
2661
+ optimistic: beginOptimistic(settleFailure(model.optimistic, overlayLayer(message.id)), overlayLayer(message.id), message.optimistic)
2662
+ };
2663
+ case "OverlayLifted": {
2664
+ const optimistic = settleFailure(model.optimistic, overlayLayer(message.id));
2665
+ return optimistic.layers.length === model.optimistic.layers.length && optimistic.overlays.length === model.optimistic.overlays.length ? model : {
2666
+ ...model,
2667
+ optimistic
2220
2668
  };
2221
2669
  }
2222
2670
  case "MutationFailed": return {
@@ -2227,18 +2675,26 @@ const updateRemote = (model, message) => {
2227
2675
  case "LiveReceived": {
2228
2676
  const state = model.live[message.stream] ?? emptyLiveState;
2229
2677
  if (message.event._tag === "EntityPatched" || message.event._tag === "EntityDeleted") {
2230
- const applied = applyEntityEvent(state, model.entities, message.event, message.now);
2231
- return applied.outcome === "gap" ? markGap(model, message.stream) : clearGap({
2678
+ const event = message.event;
2679
+ const applied = applyEntityEvent(state, model.entities, event, message.now);
2680
+ if (applied.outcome === "gap") return markGap(model, message.stream);
2681
+ const settled = applied.outcome !== "applied" ? [] : event._tag === "EntityPatched" ? [...patchedMarks([{
2682
+ ...event.ref,
2683
+ values: event.values
2684
+ }])] : entityFailureMarks(model.failures, entityKey(event.ref.entity, event.ref.id));
2685
+ return clearGap({
2232
2686
  ...model,
2233
2687
  entities: applied.store,
2234
2688
  live: {
2235
2689
  ...model.live,
2236
2690
  [message.stream]: applied.state
2237
- }
2691
+ },
2692
+ failures: withoutFieldFailures(model.failures, settled)
2238
2693
  }, message.stream);
2239
2694
  }
2240
2695
  const applied = applyConnectionEvent(state, model.optimistic, message.event, message.policy);
2241
- return applied.outcome === "gap" ? markGap(model, message.stream) : clearGap({
2696
+ if (applied.outcome === "gap") return markGap(model, message.stream);
2697
+ const next = clearGap({
2242
2698
  ...model,
2243
2699
  optimistic: applied.optimistic,
2244
2700
  live: {
@@ -2246,10 +2702,16 @@ const updateRemote = (model, message) => {
2246
2702
  [message.stream]: applied.state
2247
2703
  }
2248
2704
  }, message.stream);
2705
+ return applied.invalidated === void 0 ? next : updateRemote(next, {
2706
+ _tag: "ConnectionInvalidated",
2707
+ connection: applied.invalidated
2708
+ });
2249
2709
  }
2250
2710
  case "GapCleared": return clearGap(model, message.stream);
2251
2711
  case "ConnectionMerged": {
2252
2712
  const current = model.connections[message.connection] ?? emptyConnection;
2713
+ const failures = withoutConnectionFailure(model.failures, message.connection);
2714
+ const loading = withoutQueryLoading(model.loading, message.connection);
2253
2715
  const replaces = message.refreshes === true && current.stale;
2254
2716
  const merged = merge(replaces ? emptyConnection : current, message.page);
2255
2717
  return {
@@ -2261,25 +2723,129 @@ const updateRemote = (model, message) => {
2261
2723
  stale: false
2262
2724
  } : merged
2263
2725
  },
2264
- optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending)
2726
+ optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending),
2727
+ failures,
2728
+ loading
2265
2729
  };
2266
2730
  }
2267
2731
  case "ConnectionInvalidated":
2268
- if (model.connections[message.connection]?.stale === true) return model;
2732
+ if (model.connections[message.connection]?.stale === true && !(message.connection in model.failures.connections)) return model;
2269
2733
  return {
2270
2734
  ...model,
2271
- connections: setConnectionStale(model.connections, message.connection, true)
2735
+ connections: setConnectionStale(model.connections, message.connection, true),
2736
+ failures: withoutConnectionFailure(model.failures, message.connection)
2272
2737
  };
2273
- case "QueryFailed": return message.connection in model.connections ? {
2738
+ case "QueryFailed": return {
2274
2739
  ...model,
2275
- connections: setConnectionStale(model.connections, message.connection, false)
2276
- } : model;
2740
+ connections: message.connection in model.connections ? setConnectionStale(model.connections, message.connection, false) : model.connections,
2741
+ failures: {
2742
+ ...model.failures,
2743
+ connections: {
2744
+ ...model.failures.connections,
2745
+ [message.connection]: message.error
2746
+ }
2747
+ },
2748
+ loading: withoutQueryLoading(model.loading, message.connection)
2749
+ };
2277
2750
  case "ConnectionRefreshed": return {
2278
2751
  ...model,
2279
2752
  connections: setConnectionStale(model.connections, message.connection, false)
2280
2753
  };
2281
2754
  }
2282
2755
  };
2756
+ /** The failures without one connection's, keeping identity when it had none. */
2757
+ const withoutConnectionFailure = (failures, connection) => {
2758
+ if (!(connection in failures.connections)) return failures;
2759
+ const { [connection]: _settled, ...connections } = failures.connections;
2760
+ return {
2761
+ ...failures,
2762
+ connections
2763
+ };
2764
+ };
2765
+ /** The failures with every field a read asked for failed with `error`. */
2766
+ const withFieldFailures = (failures, requests, error) => {
2767
+ const fields = { ...failures.fields };
2768
+ for (const mark of fieldMarks(requests)) fields[mark] = error;
2769
+ return {
2770
+ ...failures,
2771
+ fields
2772
+ };
2773
+ };
2774
+ /**
2775
+ * The failures without the fields `settled` names, keeping identity when none
2776
+ * of them had failed, which is nearly always: this runs on every write.
2777
+ */
2778
+ const withoutFieldFailures = (failures, settled) => {
2779
+ if (Object.keys(failures.fields).length === 0) return failures;
2780
+ const marks = [...settled].filter((mark) => mark in failures.fields);
2781
+ if (marks.length === 0) return failures;
2782
+ const fields = { ...failures.fields };
2783
+ for (const mark of marks) delete fields[mark];
2784
+ return {
2785
+ ...failures,
2786
+ fields
2787
+ };
2788
+ };
2789
+ /** Every failed field of one entity: what deleting the entity settles. */
2790
+ const entityFailureMarks = (failures, key) => Object.keys(failures.fields).filter((mark) => mark.startsWith(`${key}\u0000`));
2791
+ /** The field marks of written values: each patch settles the fields it carries. */
2792
+ const patchedMarks = function* (patches) {
2793
+ for (const patch of patches) for (const field of Object.keys(patch.values)) yield fieldMark(patch.entity, patch.id, field);
2794
+ };
2795
+ /**
2796
+ * The failures of what the roots still reach. A released connection's or
2797
+ * entity's failure goes with it, so asking for it again later asks the server
2798
+ * again rather than repeating an error that may no longer hold.
2799
+ *
2800
+ * Reached is retention's own walk, taken over the Model before collection. It
2801
+ * counts a relation's target and a list's row that the store does not hold,
2802
+ * which is exactly a value whose first read failed: it is still on screen,
2803
+ * behind the parent that refers to it.
2804
+ */
2805
+ const prunedFailures = (model, roots) => {
2806
+ const { failures } = model;
2807
+ const named = new Set(roots.connections.map((root) => root.identity));
2808
+ const connections = Object.entries(failures.connections).filter(([identity]) => named.has(identity));
2809
+ const kept = Object.keys(failures.fields).length === 0 ? /* @__PURE__ */ new Set() : reachable(model.entities, roots, model.connections, model.optimistic, model.mutations.pending);
2810
+ const fields = Object.entries(failures.fields).filter(([mark]) => kept.has(mark.slice(0, mark.indexOf("\0"))));
2811
+ return connections.length === Object.keys(failures.connections).length && fields.length === Object.keys(failures.fields).length ? failures : {
2812
+ connections: Object.fromEntries(connections),
2813
+ fields: Object.fromEntries(fields)
2814
+ };
2815
+ };
2816
+ /** Whether this field's last read failed and nothing has settled it since. */
2817
+ const isFieldFailed = (model, entity, id, field) => fieldMark(entity, id, field) in model.failures.fields;
2818
+ /**
2819
+ * The first failure among the fields `relation` reads of an entity, following
2820
+ * each relation the store holds into its targets, as `assemble` does. A read
2821
+ * that consults it shows `Failed` for a related entity's failure too, rather
2822
+ * than waiting on a field nothing will fetch.
2823
+ */
2824
+ const failureOf = (model, entity, id, relation) => {
2825
+ const failed = model.failures.fields;
2826
+ if (Object.keys(failed).length === 0) return void 0;
2827
+ const walked = /* @__PURE__ */ new Map();
2828
+ const walk = (target, targetId, requirement) => {
2829
+ const key = entityKey(target, targetId);
2830
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
2831
+ if (seen.has(requirement)) return void 0;
2832
+ seen.add(requirement);
2833
+ walked.set(key, seen);
2834
+ for (const field of requirement.fields) {
2835
+ const error = failed[fieldMark(target, targetId, field)];
2836
+ if (error !== void 0) return error;
2837
+ const nested = requirement.relations?.[field];
2838
+ if (nested === void 0) continue;
2839
+ const value = readField(model.entities, key, field);
2840
+ if (value._tag === "None") continue;
2841
+ for (const ref of targetsOf(value.value, nested)) {
2842
+ const found = walk(ref.entity, ref.id, nested);
2843
+ if (found !== void 0) return found;
2844
+ }
2845
+ }
2846
+ };
2847
+ return walk(entity, id, relation);
2848
+ };
2283
2849
  /** Appends (after) or prepends (before) an incoming page onto the stored one. */
2284
2850
  const mergeWireRefPages = (current, next, direction) => {
2285
2851
  const refs = direction === "after" ? [.../* @__PURE__ */ new Set([...current.refs, ...next.refs])] : [.../* @__PURE__ */ new Set([...next.refs, ...current.refs])];
@@ -2414,6 +2980,22 @@ const storeOf = (bound, model) => {
2414
2980
  const remote = bound.store.get(model);
2415
2981
  return visibleStoreOf(remote.entities, remote.optimistic);
2416
2982
  };
2983
+ /**
2984
+ * The Model as it would be with nothing optimistic pending. Every read reaches
2985
+ * the layers through the Model it is given, so dropping them here is what makes
2986
+ * a read confirmed — no read has to know it is being read confirmed.
2987
+ */
2988
+ const withoutOptimistic = (bound, model) => {
2989
+ const remote = bound.store.get(model);
2990
+ return remote.optimistic.layers.length === 0 && remote.optimistic.overlays.length === 0 ? model : bound.store.set(model, {
2991
+ ...remote,
2992
+ optimistic: emptyOptimistic
2993
+ });
2994
+ };
2995
+ const confirmed = (bound, projection) => ({
2996
+ ...projection,
2997
+ read: (root) => projection.read(withoutOptimistic(bound, root))
2998
+ });
2417
2999
  const visibleStores = /* @__PURE__ */ new WeakMap();
2418
3000
  const visibleStoreOf = (entities, optimistic) => {
2419
3001
  let byStore = visibleStores.get(optimistic);
@@ -2499,6 +3081,10 @@ const askedOf = (projection) => projection === void 0 ? nothingAsked : {
2499
3081
  * fresh contributes its visible items' selected fields to the entity plan; one
2500
3082
  * it does not hold, or holds stale, is a query to run (its items are planned
2501
3083
  * once the page arrives).
3084
+ *
3085
+ * A connection whose query failed is not run again here, short of `force`: the
3086
+ * failure is what its read shows, and retrying is `Remote.refresh`'s to ask
3087
+ * for. The rows it already holds are still planned, since they are on screen.
2502
3088
  */
2503
3089
  const planAsked = (remote, asked, options) => {
2504
3090
  const queries = [];
@@ -2506,18 +3092,46 @@ const planAsked = (remote, asked, options) => {
2506
3092
  const connections = Requirement.mergeConnections(asked.connections);
2507
3093
  for (const connection of connections) {
2508
3094
  const known = remote.connections[connection.identity];
2509
- if (known === void 0 || known.stale || options.force === true) {
3095
+ if (!(options.force !== true && connection.identity in remote.failures.connections) && (known === void 0 || known.stale || options.force === true)) {
2510
3096
  queries.push(connection);
2511
3097
  continue;
2512
3098
  }
3099
+ if (known === void 0) continue;
2513
3100
  const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
2514
3101
  items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
2515
3102
  }
3103
+ const planned = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], options);
2516
3104
  return {
2517
- requirements: plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], options),
3105
+ requirements: options.force === true ? planned : withoutFailedFields(remote, planned),
2518
3106
  queries
2519
3107
  };
2520
3108
  };
3109
+ /**
3110
+ * The plan without the fields whose last read failed. Like a failed query,
3111
+ * a failed field is shown rather than retried on its own, so a persistent
3112
+ * error is not asked again on every unrelated restart of the read entry. A
3113
+ * field leaving the request takes its window and its relation with it.
3114
+ */
3115
+ const withoutFailedFields = (remote, planned) => {
3116
+ if (Object.keys(remote.failures.fields).length === 0) return planned;
3117
+ return planned.flatMap((requirement) => {
3118
+ const fields = requirement.fields.filter((field) => !isFieldFailed(remote, requirement.entity, requirement.id, field));
3119
+ if (fields.length === requirement.fields.length) return [requirement];
3120
+ if (fields.length === 0) return [];
3121
+ const kept = new Set(fields);
3122
+ const only = (record) => record === void 0 ? void 0 : Object.fromEntries(Object.entries(record).filter(([field]) => kept.has(field)));
3123
+ const windows = only(requirement.windows);
3124
+ const relations = only(requirement.relations);
3125
+ return [{
3126
+ entity: requirement.entity,
3127
+ id: requirement.id,
3128
+ fields,
3129
+ ...windows === void 0 || Object.keys(windows).length === 0 ? {} : { windows },
3130
+ ...relations === void 0 || Object.keys(relations).length === 0 ? {} : { relations },
3131
+ ...requirement.live === void 0 ? {} : { live: requirement.live }
3132
+ }];
3133
+ });
3134
+ };
2521
3135
  /** The entity requirements a page's edges add under a selection. */
2522
3136
  const itemsOf = (edges, select) => edges.filter((edge) => edge.entity === select.entity).map((edge) => ({
2523
3137
  ...select,
@@ -2545,22 +3159,52 @@ const queryRequestOf = (query) => {
2545
3159
  * `refreshes` marks the page as answering the connection's refresh, so one
2546
3160
  * Message both merges it and clears `stale`.
2547
3161
  */
2548
- const pageMessage = (connection, result, refreshes = false) => ({
2549
- _tag: "ConnectionMerged",
2550
- connection,
2551
- page: {
2552
- edges: result.edges.map((edge) => ({
2553
- key: edge.key,
2554
- ref: {
2555
- entity: edge.entity,
2556
- id: edge.id
2557
- }
2558
- })),
2559
- start: result.start,
2560
- end: result.end
2561
- },
2562
- ...refreshes ? { refreshes } : {}
2563
- });
3162
+ /**
3163
+ * The page a window asked for, or the number of edges that came back instead.
3164
+ *
3165
+ * A window is a request the client made and the server answered, so more edges
3166
+ * than were asked for is a protocol disagreement rather than a windfall: the
3167
+ * client cannot tell which of them the window meant, the connection's
3168
+ * boundaries stop describing what it holds, and a `first: 25` that quietly
3169
+ * becomes a thousand is a memory event with no error attached to it.
3170
+ *
3171
+ * `undefined` when the page is within its window, or when no size was asked
3172
+ * for — `after`/`before` with no `first`/`last` bounds nothing.
3173
+ */
3174
+ const overrun = (window, edges) => {
3175
+ const asked = window.first ?? window.last;
3176
+ return asked !== void 0 && edges.length > asked ? {
3177
+ asked,
3178
+ got: edges.length
3179
+ } : void 0;
3180
+ };
3181
+ const pageMessage = (connection, result, refreshes = false, window = {}) => {
3182
+ const over = overrun(window, result.edges);
3183
+ if (over !== void 0) return {
3184
+ _tag: "QueryFailed",
3185
+ connection,
3186
+ error: {
3187
+ _tag: "RemoteProtocolError",
3188
+ message: `the server returned ${over.got} edges for a window of ${over.asked}`
3189
+ }
3190
+ };
3191
+ return {
3192
+ _tag: "ConnectionMerged",
3193
+ connection,
3194
+ page: {
3195
+ edges: result.edges.map((edge) => ({
3196
+ key: edge.key,
3197
+ ref: {
3198
+ entity: edge.entity,
3199
+ id: edge.id
3200
+ }
3201
+ })),
3202
+ start: result.start,
3203
+ end: result.end
3204
+ },
3205
+ ...refreshes ? { refreshes } : {}
3206
+ };
3207
+ };
2564
3208
  /**
2565
3209
  * The retention roots of some projections: their requirements, their
2566
3210
  * connections (each with the union of what the projections select of its
@@ -2608,7 +3252,7 @@ const queryMessage = (query) => Effect.gen(function* () {
2608
3252
  _tag: "QueryFailed",
2609
3253
  connection: query.identity,
2610
3254
  error: remoteError(result.failure)
2611
- } : pageMessage(query.identity, result.success, true);
3255
+ } : pageMessage(query.identity, result.success, true, query.window);
2612
3256
  });
2613
3257
  /** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
2614
3258
  const PlannedQuery = Schema.Struct({
@@ -2639,7 +3283,7 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
2639
3283
  const remote = bound.store.get(model);
2640
3284
  const planned = planAsked(remote, askedOf(model), RemotePolicy.toPlan(policy, now()));
2641
3285
  return {
2642
- refresh: remote.refresh.requested,
3286
+ refresh: refreshedAt(remote.refresh, planned.requirements, planned.queries.map((query) => query.identity)),
2643
3287
  requirements: planned.requirements,
2644
3288
  queries: planned.queries.map(({ identity, window, select }) => ({
2645
3289
  identity,
@@ -2648,14 +3292,17 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
2648
3292
  }))
2649
3293
  };
2650
3294
  },
2651
- dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(requirements.length === 0 ? Stream.empty : Stream.fromIterable([toMessage({
3295
+ dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
2652
3296
  _tag: "ReadStarted",
2653
3297
  requests: requirements,
2654
3298
  refresh
2655
3299
  }), ...RemotePolicy.refreshes(policy) ? [toMessage({
2656
3300
  _tag: "RefreshStarted",
2657
3301
  requests: requirements
2658
- })] : []]), Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }))
3302
+ })] : []], ...queries.length === 0 ? [] : [toMessage({
3303
+ _tag: "QueryStarted",
3304
+ connections: queries.map((query) => query.identity)
3305
+ })]]), Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }))
2659
3306
  };
2660
3307
  };
2661
3308
  /** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
@@ -2685,7 +3332,8 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
2685
3332
  })), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
2686
3333
  _tag: "ReadFailed",
2687
3334
  requests: requirements,
2688
- error: remoteError(error)
3335
+ error: remoteError(error),
3336
+ stream: liveStreamKey(requirements)
2689
3337
  }))))
2690
3338
  });
2691
3339
  /** The projection an active Surface has for this Model, if it is active. */
@@ -2778,8 +3426,18 @@ const Remote = {
2778
3426
  value: decoded.success
2779
3427
  };
2780
3428
  });
2781
- if (present !== void 0) return present;
2782
- return isLoading(bound.store.get(root), selection.entity, id, relation.fields) ? { _tag: "Loading" } : { _tag: "Initial" };
3429
+ const remote = bound.store.get(root);
3430
+ const failure = failureOf(remote, selection.entity, id, relation);
3431
+ if (present !== void 0) return failure !== void 0 && (present._tag === "Ready" || present._tag === "Refreshing") ? {
3432
+ _tag: "Failed",
3433
+ error: failure,
3434
+ previous: present.value
3435
+ } : present;
3436
+ if (isLoading(remote, selection.entity, id, relation.fields)) return { _tag: "Loading" };
3437
+ return failure === void 0 ? { _tag: "Initial" } : {
3438
+ _tag: "Failed",
3439
+ error: failure
3440
+ };
2783
3441
  }
2784
3442
  });
2785
3443
  },
@@ -2801,6 +3459,12 @@ const Remote = {
2801
3459
  */
2802
3460
  storeOf: (bound, model) => storeOf(bound, model),
2803
3461
  /**
3462
+ * The projection read against the server-derived store alone: the same
3463
+ * requirements, planned the same way, with the pending optimistic layers and
3464
+ * connection overlays left off. See the bound `Remote.confirmed`.
3465
+ */
3466
+ confirmed: (bound, projection) => confirmed(bound, projection),
3467
+ /**
2804
3468
  * Executes the plan against the `RemoteClient` and returns a new store. Used
2805
3469
  * for SSR route prefetch, hover prefetch, and tests. Never called during render.
2806
3470
  * `policy` decides what a present field means (default cache-first); `now`
@@ -2832,6 +3496,9 @@ const Remote = {
2832
3496
  * again with `next`/`previous`. The read entries restart, so a read or query
2833
3497
  * sent before the refresh is not applied after it.
2834
3498
  *
3499
+ * It is also how a failed query is retried: a connection whose query failed
3500
+ * is not run again on its own, so its read stays `Failed` until this asks.
3501
+ *
2835
3502
  * The projection must be observed, which it is while it is on screen. Live
2836
3503
  * subscriptions are left as they are. For data nothing observes (SSR, tests),
2837
3504
  * use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
@@ -2847,19 +3514,17 @@ const Remote = {
2847
3514
  return known === void 0 ? [] : itemsOf(visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities).map((edge) => edge.ref), connection.select);
2848
3515
  });
2849
3516
  const requirements = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], { force: true });
3517
+ const invalidated = connections.filter((connection) => remote.connections[connection.identity] !== void 0 || connection.identity in remote.failures.connections).map((connection) => connection.identity);
2850
3518
  const marked = [...requirements.length === 0 ? [] : [{
2851
3519
  _tag: "RefreshStarted",
2852
3520
  requests: requirements
2853
- }], ...connections.filter((connection) => remote.connections[connection.identity] !== void 0).map((connection) => ({
3521
+ }], ...invalidated.map((connection) => ({
2854
3522
  _tag: "ConnectionInvalidated",
2855
- connection: connection.identity
3523
+ connection
2856
3524
  }))].reduce(updateRemote, remote);
2857
- const inFlight = remote.refresh.started === remote.refresh.requested && requirements.some((requirement) => requirement.fields.some((field) => remote.entities[entityKey(requirement.entity, requirement.id)]?.stale.has(field) === true));
3525
+ const inFlight = requirements.some((requirement) => requirement.fields.some((field) => remote.entities[entityKey(requirement.entity, requirement.id)]?.stale.has(field) === true && refreshIsInFlight(remote.refresh, requirement, field)));
2858
3526
  if (marked === remote && !inFlight) return model;
2859
- const refresh = {
2860
- ...marked.refresh,
2861
- requested: marked.refresh.requested + 1
2862
- };
3527
+ const refresh = withRefreshRequested(marked.refresh, requirements, invalidated);
2863
3528
  return bound.store.set(model, {
2864
3529
  ...marked,
2865
3530
  refresh
@@ -2926,7 +3591,7 @@ const Remote = {
2926
3591
  });
2927
3592
  }),
2928
3593
  /** The `RemoteMessage` that merges a query page into its connection. */
2929
- queryMessage: (ref, result) => pageMessage(ref.identity, result),
3594
+ queryMessage: (ref, result) => pageMessage(ref.identity, result, false, ref.window),
2930
3595
  /**
2931
3596
  * The edges a connection shows: its server-known region with pending and
2932
3597
  * confirmed overlays placed around it, minus removed edges and edges whose
@@ -2983,8 +3648,10 @@ const Remote = {
2983
3648
  observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => askedOf(surface.projection(params)), toMessage, options),
2984
3649
  /**
2985
3650
  * A Foldkit Subscription entry that consumes the live stream for a Surface's
2986
- * requirements, emitting a `LiveReceived` per event and a `ReadFailed` when
2987
- * the stream breaks (including `ResumeUnavailable`). The resume cursor is read
3651
+ * requirements, emitting a `LiveReceived` per event and a `ReadFailed`
3652
+ * carrying its `stream` when the stream breaks (including
3653
+ * `ResumeUnavailable`). That records a gap on the stream rather than a failed
3654
+ * read, since nothing was being read. The resume cursor is read
2988
3655
  * from `RemoteModel.live`, so the application tracks no cursor of its own.
2989
3656
  */
2990
3657
  live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
@@ -2993,7 +3660,48 @@ const brandEntries = (entries) => Subscription.make()(() => entries);
2993
3660
  /** The bound domain: the descriptor, the binding, and the operations over them. */
2994
3661
  const bindDomain = (definition, store) => {
2995
3662
  const bound = bindRemote(definition, store);
2996
- const reduce = (model, message) => store.set(model, updateRemote(store.get(model), message));
3663
+ /**
3664
+ * What to do with a row a live event says was inserted into a connection.
3665
+ *
3666
+ * Resolved here rather than in the pure reducer because this is the only
3667
+ * place with both the Model and the registry, and two separate things needed
3668
+ * one or the other.
3669
+ *
3670
+ * **The declared policy reaches the decision.** `Query.connection(E, { live })`
3671
+ * has always been typed, documented, carried on the descriptor and encoded in
3672
+ * the Message schema — and nothing ever put it on a Message, so every live
3673
+ * insert took the default whatever an application asked for. It does now.
3674
+ *
3675
+ * **A row the client can judge does not need a policy.** The policy exists
3676
+ * because nothing could tell whether an inserted row belonged to the query.
3677
+ * Where the body says it does not, `ignore` is not a guess. Where the body
3678
+ * says it does, or the client cannot tell — a row it never fetched, or holds
3679
+ * without a field the body reads — the declared policy is the answer, exactly
3680
+ * as it was meant to be.
3681
+ *
3682
+ * Only membership is decided here. *Where* a row sorts needs text collation,
3683
+ * which is the backend's, so the position the event carries stands.
3684
+ */
3685
+ const livePolicyFor = (model, event) => {
3686
+ if (event._tag !== "ConnectionInsert") return void 0;
3687
+ const identity = event.connection;
3688
+ const descriptor = definition.registry.queries.get(identity.slice(0, identity.indexOf("\0")));
3689
+ const declared = (descriptor?.Result)?.live;
3690
+ if (descriptor?.body === void 0) return declared;
3691
+ const encoded = JSON.parse(identity.slice(identity.indexOf("\0") + 1));
3692
+ return belongsEncoded(storeOf(bound, model), descriptor, encoded, entityKey(event.edge.ref.entity, event.edge.ref.id)) === "no" ? {
3693
+ prepend: "ignore",
3694
+ append: "ignore"
3695
+ } : declared;
3696
+ };
3697
+ const reduce = (model, message) => {
3698
+ const live = message._tag === "LiveReceived" ? message : void 0;
3699
+ const resolved = live === void 0 || live.policy !== void 0 ? message : {
3700
+ ...live,
3701
+ policy: livePolicyFor(model, live.event)
3702
+ };
3703
+ return store.set(model, updateRemote(store.get(model), resolved));
3704
+ };
2997
3705
  const domain = {
2998
3706
  ...definition,
2999
3707
  ...bound,
@@ -3034,6 +3742,7 @@ const bindDomain = (definition, store) => {
3034
3742
  },
3035
3743
  plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
3036
3744
  storeOf: (model) => storeOf(bound, model),
3745
+ confirmed: (projection) => confirmed(bound, projection),
3037
3746
  prefetch: (model, projection, options = {}) => Effect.gen(function* () {
3038
3747
  const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
3039
3748
  const client = yield* RemoteClient;
@@ -3041,7 +3750,7 @@ const bindDomain = (definition, store) => {
3041
3750
  let current = model;
3042
3751
  for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
3043
3752
  const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
3044
- current = reduce(current, pageMessage(query.identity, page, true));
3753
+ current = reduce(current, pageMessage(query.identity, page, true, query.window));
3045
3754
  }
3046
3755
  const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
3047
3756
  if (requirements.length === 0) return current;
@@ -3071,6 +3780,58 @@ const bindDomain = (definition, store) => {
3071
3780
  };
3072
3781
  const relation = relationOf(select);
3073
3782
  const relationKey = stableStringify(relation);
3783
+ const failedItem = (remote, connection) => {
3784
+ if (Object.keys(remote.failures.fields).length === 0) return void 0;
3785
+ for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
3786
+ if (edge.ref.entity !== relation.entity) continue;
3787
+ const error = failureOf(remote, edge.ref.entity, edge.ref.id, relation);
3788
+ if (error !== void 0) return {
3789
+ _tag: "Failed",
3790
+ error
3791
+ };
3792
+ }
3793
+ };
3794
+ const loadingItem = (remote, connection) => {
3795
+ if (remote.loading.size === 0) return void 0;
3796
+ for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
3797
+ if (edge.ref.entity !== relation.entity) continue;
3798
+ if (isLoading(remote, edge.ref.entity, edge.ref.id, relation.fields)) return { _tag: "Loading" };
3799
+ }
3800
+ };
3801
+ const readPage = (remote, connection) => {
3802
+ const visible = visibleStoreOf(remote.entities, remote.optimistic);
3803
+ return memoRead(visible, connection, `${ref.identity}\u0000${relationKey}`, () => {
3804
+ const items = [];
3805
+ let refreshing = connection.stale;
3806
+ const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
3807
+ for (const edge of edges) {
3808
+ const assembled = assemble(visible, entityKey(edge.ref.entity, edge.ref.id), relation);
3809
+ if (assembled === void 0) return void 0;
3810
+ const decoded = Schema.decodeUnknownResult(select.schema)(assembled.values);
3811
+ if (Result.isFailure(decoded)) return {
3812
+ _tag: "Failed",
3813
+ error: {
3814
+ _tag: "DecodeError",
3815
+ message: decoded.failure.message
3816
+ }
3817
+ };
3818
+ refreshing ||= assembled.refreshing;
3819
+ items.push(decoded.success);
3820
+ }
3821
+ const page = {
3822
+ items,
3823
+ hasNext: hasNext(connection),
3824
+ hasPrevious: hasPrevious(connection)
3825
+ };
3826
+ return refreshing ? {
3827
+ _tag: "Refreshing",
3828
+ value: page
3829
+ } : {
3830
+ _tag: "Ready",
3831
+ value: page
3832
+ };
3833
+ });
3834
+ };
3074
3835
  const requirement = {
3075
3836
  identity: ref.identity,
3076
3837
  window: ref.window,
@@ -3082,42 +3843,41 @@ const bindDomain = (definition, store) => {
3082
3843
  dependencies: [],
3083
3844
  metadata: RemoteConnections.of(requirement),
3084
3845
  ref,
3846
+ selection: select,
3085
3847
  read: (root) => {
3086
3848
  const remote = store.get(root);
3087
3849
  const connection = remote.connections[ref.identity];
3088
- if (connection === void 0 || connection.stale && connection.segments.length === 0) return { _tag: "Initial" };
3089
- const visible = visibleStoreOf(remote.entities, remote.optimistic);
3090
- return memoRead(visible, connection, `${ref.identity}\u0000${relationKey}`, () => {
3091
- const items = [];
3092
- let refreshing = connection.stale;
3093
- const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
3094
- for (const edge of edges) {
3095
- const assembled = assemble(visible, entityKey(edge.ref.entity, edge.ref.id), relation);
3096
- if (assembled === void 0) return { _tag: "Initial" };
3097
- const decoded = Schema.decodeUnknownResult(select.schema)(assembled.values);
3098
- if (Result.isFailure(decoded)) return {
3099
- _tag: "Failed",
3100
- error: {
3101
- _tag: "DecodeError",
3102
- message: decoded.failure.message
3103
- }
3104
- };
3105
- refreshing ||= assembled.refreshing;
3106
- items.push(decoded.success);
3107
- }
3108
- const page = {
3109
- items,
3110
- hasNext: hasNext(connection),
3111
- hasPrevious: hasPrevious(connection)
3850
+ const failure = remote.failures.connections[ref.identity];
3851
+ if (connection === void 0 || connection.stale && connection.segments.length === 0) {
3852
+ if (isQueryLoading(remote, ref.identity)) return { _tag: "Loading" };
3853
+ return failure === void 0 ? { _tag: "Initial" } : {
3854
+ _tag: "Failed",
3855
+ error: failure
3112
3856
  };
3113
- return refreshing ? {
3114
- _tag: "Refreshing",
3115
- value: page
3116
- } : {
3117
- _tag: "Ready",
3118
- value: page
3857
+ }
3858
+ const read = readPage(remote, connection) ?? failedItem(remote, connection) ?? loadingItem(remote, connection) ?? { _tag: "Initial" };
3859
+ if (failure === void 0) {
3860
+ if (read._tag !== "Ready" && read._tag !== "Refreshing") return read;
3861
+ const item = failedItem(remote, connection);
3862
+ return item === void 0 ? read : {
3863
+ _tag: "Failed",
3864
+ error: item.error,
3865
+ previous: read.value
3119
3866
  };
3120
- });
3867
+ }
3868
+ switch (read._tag) {
3869
+ case "Ready":
3870
+ case "Refreshing": return {
3871
+ _tag: "Failed",
3872
+ error: failure,
3873
+ previous: read.value
3874
+ };
3875
+ case "Failed": return read;
3876
+ default: return {
3877
+ _tag: "Failed",
3878
+ error: failure
3879
+ };
3880
+ }
3121
3881
  }
3122
3882
  };
3123
3883
  },
@@ -3142,7 +3902,79 @@ const bindDomain = (definition, store) => {
3142
3902
  }
3143
3903
  } : void 0;
3144
3904
  },
3905
+ explain: (model, projection, options) => {
3906
+ const { ref } = projection;
3907
+ const reading = Object.values(options?.surfaces ?? {}).filter((active) => {
3908
+ const active_ = active.projectionOf(model);
3909
+ return active_ !== void 0 && connectionsOf(active_).some((connection) => connection.identity === ref.identity);
3910
+ });
3911
+ const body = definition.registry.queries.get(ref.query)?.body;
3912
+ const [connection] = RemoteConnections.get(projection.metadata);
3913
+ return {
3914
+ domain: bound.contract.name,
3915
+ query: ref.query,
3916
+ input: Schema.encodeSync(ref.Input)(ref.input),
3917
+ identity: ref.identity,
3918
+ window: ref.window,
3919
+ select: connection.select,
3920
+ ...body === void 0 ? {} : {
3921
+ body: Query$1.show(body),
3922
+ dependencies: Query$1.dependencies(body)
3923
+ },
3924
+ state: projection.read(model)._tag,
3925
+ ...options?.surfaces === void 0 ? {} : {
3926
+ surfaces: reading.map((active) => active.name),
3927
+ activation: reading.flatMap((active) => active.activation === void 0 ? [] : [{
3928
+ surface: active.name,
3929
+ ...active.activation
3930
+ }])
3931
+ }
3932
+ };
3933
+ },
3934
+ filtered: (model, over, by, input) => {
3935
+ assertRegistered(bound, "Query", definition.registry.queries, by.name);
3936
+ const filters = by.body?.entity.name;
3937
+ 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`);
3938
+ const remote = store.get(model);
3939
+ const connection = remote.connections[over.ref.identity];
3940
+ const visible = visibleStoreOf(remote.entities, remote.optimistic);
3941
+ const among = (connection === void 0 ? [] : visibleItems(connection, over.ref.identity, remote.optimistic.overlays, remote.entities)).map((edge) => entityKey(edge.ref.entity, edge.ref.id));
3942
+ const judged = matching(visible, by, input, { among });
3943
+ const relation = relationOf(over.selection);
3944
+ const items = [];
3945
+ let assembledAll = true;
3946
+ for (const key of judged.matched) {
3947
+ const assembled = assemble(visible, key, relation);
3948
+ if (assembled === void 0) {
3949
+ assembledAll = false;
3950
+ continue;
3951
+ }
3952
+ const decoded = Schema.decodeUnknownResult(over.selection.schema)(assembled.values);
3953
+ if (Result.isFailure(decoded)) {
3954
+ assembledAll = false;
3955
+ continue;
3956
+ }
3957
+ items.push(decoded.success);
3958
+ }
3959
+ return {
3960
+ items,
3961
+ complete: judged.skipped.length === 0 && assembledAll && connection !== void 0 && !isGapped(connection) && !hasNext(connection) && !hasPrevious(connection)
3962
+ };
3963
+ },
3145
3964
  refresh: (model, target) => Remote.refresh(bound, model, target),
3965
+ overlay: (model, id, optimistic) => bound.store.set(model, updateRemote(bound.store.get(model), {
3966
+ _tag: "OverlayShown",
3967
+ id,
3968
+ optimistic
3969
+ })),
3970
+ lift: (model, id) => {
3971
+ const remote = bound.store.get(model);
3972
+ const lifted = updateRemote(remote, {
3973
+ _tag: "OverlayLifted",
3974
+ id
3975
+ });
3976
+ return lifted === remote ? model : bound.store.set(model, lifted);
3977
+ },
3146
3978
  fetch: (ref) => ({
3147
3979
  name: `Remote.query(${ref.query})`,
3148
3980
  args: {
@@ -3155,7 +3987,7 @@ const bindDomain = (definition, store) => {
3155
3987
  connection: ref.identity,
3156
3988
  error: remoteError(error)
3157
3989
  }),
3158
- onSuccess: (page) => pageMessage(ref.identity, page)
3990
+ onSuccess: (page) => pageMessage(ref.identity, page, false, ref.window)
3159
3991
  }))
3160
3992
  }),
3161
3993
  mutate: (model, mutation, input, options = {}) => {
@@ -3222,6 +4054,6 @@ const pageSize = (window, key) => {
3222
4054
  return size === void 0 ? {} : { [key]: size };
3223
4055
  };
3224
4056
  //#endregion
3225
- 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, 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, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, 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, refsIn, 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, writeEntities, writeEntity, writeRead };
4057
+ 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, 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 };
3226
4058
 
3227
4059
  //# sourceMappingURL=index.mjs.map