foldkit-remote 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -31
- package/dist/index.d.mts +301 -44
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +624 -218
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
|
|
1
|
+
import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaAST, SchemaGetter, Stream } from "effect";
|
|
2
2
|
import * as Domain from "foldkit-entity";
|
|
3
|
-
import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId, evaluate } from "foldkit-entity";
|
|
3
|
+
import { Entity as Entity$1, Expr, Query as Query$1, SchemaShape, SelectionTypeId, evaluate } from "foldkit-entity";
|
|
4
4
|
import { mapMessage } from "foldkit/command";
|
|
5
5
|
import { defineMessageUnion } from "foldkit/message";
|
|
6
6
|
import * as Subscription from "foldkit/subscription";
|
|
@@ -110,7 +110,31 @@ const requirementsOf = (projection) => RemoteRequirements.get(projection.metadat
|
|
|
110
110
|
/** The query connections a Projection carries. */
|
|
111
111
|
const connectionsOf = (projection) => RemoteConnections.get(projection.metadata);
|
|
112
112
|
//#endregion
|
|
113
|
+
//#region src/data.ts
|
|
114
|
+
/**
|
|
115
|
+
* Whether two values the server sent are the same data: equal primitives, or
|
|
116
|
+
* arrays and plain objects whose entries are. Anything else is compared by
|
|
117
|
+
* identity.
|
|
118
|
+
*/
|
|
119
|
+
const sameData = (left, right) => {
|
|
120
|
+
if (Object.is(left, right)) return true;
|
|
121
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
|
|
122
|
+
if (Array.isArray(left)) return Array.isArray(right) && left.length === right.length && left.every((item, index) => sameData(item, right[index]));
|
|
123
|
+
if (!isPlain(left) || !isPlain(right)) return false;
|
|
124
|
+
const keys = Object.keys(left);
|
|
125
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameData(left[key], right[key]));
|
|
126
|
+
};
|
|
127
|
+
const isPlain = (value) => {
|
|
128
|
+
const prototype = Object.getPrototypeOf(value);
|
|
129
|
+
return prototype === Object.prototype || prototype === null;
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
113
132
|
//#region src/relation.ts
|
|
133
|
+
/**
|
|
134
|
+
* Relation values on the wire and in the store: a ref key (`"Entity:id"`), a
|
|
135
|
+
* nullable ref key, an array of ref keys, or a page of ref keys. The planner
|
|
136
|
+
* follows them into the store; the server follows them into the next read.
|
|
137
|
+
*/
|
|
114
138
|
/** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
|
|
115
139
|
const RelationAnnotation = "foldkitRemoteRelation";
|
|
116
140
|
/** The target entity's name on the same codec, so a nested selection of another entity is refused. */
|
|
@@ -176,8 +200,8 @@ const shapeOf = (ast, nullable) => {
|
|
|
176
200
|
nullable,
|
|
177
201
|
entity: String(ast.annotations?.[RelationEntityAnnotation])
|
|
178
202
|
};
|
|
179
|
-
if (ast
|
|
180
|
-
const item = ast.rest
|
|
203
|
+
if (SchemaAST.isArrays(ast)) {
|
|
204
|
+
const item = ast.rest[0];
|
|
181
205
|
const inner = item === void 0 ? void 0 : shapeOf(item, false);
|
|
182
206
|
return inner?.kind === "one" ? {
|
|
183
207
|
kind: "many",
|
|
@@ -185,10 +209,9 @@ const shapeOf = (ast, nullable) => {
|
|
|
185
209
|
entity: inner.entity
|
|
186
210
|
} : void 0;
|
|
187
211
|
}
|
|
188
|
-
if (ast
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
for (const member of members) {
|
|
212
|
+
if (SchemaAST.isUnion(ast)) {
|
|
213
|
+
const optional = ast.types.some(SchemaShape.isNullish);
|
|
214
|
+
for (const member of ast.types) {
|
|
192
215
|
const shape = shapeOf(member, optional);
|
|
193
216
|
if (shape !== void 0) return shape;
|
|
194
217
|
}
|
|
@@ -201,8 +224,8 @@ const shapeOf = (ast, nullable) => {
|
|
|
201
224
|
*
|
|
202
225
|
* Presence is tracked **separately** from values, so `undefined`/`null`/absent/
|
|
203
226
|
* stale/not-found are distinct states and a missing field is never inferred from
|
|
204
|
-
* `value === undefined`. Nothing here performs I/O
|
|
205
|
-
*
|
|
227
|
+
* `value === undefined`. Nothing here performs I/O or mutates its input; an
|
|
228
|
+
* operation that changes nothing returns the store it was given.
|
|
206
229
|
*/
|
|
207
230
|
/** The separator keeps an untrusted id from colliding with a prototype key. */
|
|
208
231
|
const entityKey = (entity, id) => `${entity}:${id}`;
|
|
@@ -237,7 +260,7 @@ const written = (previous, values, now, windows) => {
|
|
|
237
260
|
}
|
|
238
261
|
}
|
|
239
262
|
return {
|
|
240
|
-
values: {
|
|
263
|
+
values: Object.keys(values).every((field) => Object.hasOwn(previous.values, field) && sameData(previous.values[field], values[field])) ? previous.values : {
|
|
241
264
|
...previous.values,
|
|
242
265
|
...values
|
|
243
266
|
},
|
|
@@ -257,7 +280,7 @@ const written = (previous, values, now, windows) => {
|
|
|
257
280
|
const writeEntities = (store, writes, now = 0) => {
|
|
258
281
|
if (writes.length === 0) return store;
|
|
259
282
|
const next = { ...store };
|
|
260
|
-
for (const write of writes) next[write.key] = written(next[write.key] ?? emptyEntry, write.values, now, write.windows);
|
|
283
|
+
for (const write of writes) next[write.key] = written(next[write.key] ?? emptyEntry, write.values, write.updatedAt ?? now, write.windows);
|
|
261
284
|
return next;
|
|
262
285
|
};
|
|
263
286
|
/** `writeEntities` for one entity. */
|
|
@@ -322,7 +345,7 @@ const isFieldUnavailable = (store, key, field) => {
|
|
|
322
345
|
return value !== void 0 && !value.tombstone && value.unavailable.has(field);
|
|
323
346
|
};
|
|
324
347
|
/** Records that the entity is known to be absent, so it is not refetched. */
|
|
325
|
-
const tombstone = (store, key) => replace(store, key, {
|
|
348
|
+
const tombstone = (store, key) => isTombstone(store, key) ? store : replace(store, key, {
|
|
326
349
|
values: {},
|
|
327
350
|
present: /* @__PURE__ */ new Set(),
|
|
328
351
|
stale: /* @__PURE__ */ new Set(),
|
|
@@ -445,8 +468,11 @@ const mutationStatus = (state, requestId) => {
|
|
|
445
468
|
* with the same `requestId` (a retry, or a live event describing the same change)
|
|
446
469
|
* does not re-apply the entities, but still clears `pending`.
|
|
447
470
|
*/
|
|
448
|
-
const reconcileMutation = (store, state, requestId,
|
|
449
|
-
const next = state.applied.has(requestId) ? store : deleted.reduce((current, gone) => tombstone(current, entityKey(gone.entity, gone.id)), entities.
|
|
471
|
+
const reconcileMutation = (store, state, requestId, answer) => {
|
|
472
|
+
const next = state.applied.has(requestId) ? store : (answer.deleted ?? []).reduce((current, gone) => tombstone(current, entityKey(gone.entity, gone.id)), writeEntities(store, answer.entities.map((entity) => ({
|
|
473
|
+
key: entityKey(entity.entity, entity.id),
|
|
474
|
+
values: entity.values
|
|
475
|
+
})), answer.now));
|
|
450
476
|
const applied = remember(state.applied, requestId);
|
|
451
477
|
const pending = new Set(state.pending);
|
|
452
478
|
pending.delete(requestId);
|
|
@@ -806,7 +832,13 @@ const WireBoundary = Schema.Union([
|
|
|
806
832
|
const QueryRequest = Schema.Struct({
|
|
807
833
|
query: Schema.String,
|
|
808
834
|
input: Schema.Unknown,
|
|
809
|
-
window: WindowSchema
|
|
835
|
+
window: WindowSchema,
|
|
836
|
+
/**
|
|
837
|
+
* What to read of each item, as the relation slice a read asks the server
|
|
838
|
+
* for. Absent for a query run without one: the response carries only edges,
|
|
839
|
+
* and the client's read entry fetches fields in a second round trip.
|
|
840
|
+
*/
|
|
841
|
+
select: Schema.optional(RelationRequest)
|
|
810
842
|
});
|
|
811
843
|
const QueryEdge = Schema.Struct({
|
|
812
844
|
entity: Schema.String,
|
|
@@ -816,7 +848,14 @@ const QueryEdge = Schema.Struct({
|
|
|
816
848
|
const QueryResult = Schema.Struct({
|
|
817
849
|
edges: Schema.Array(QueryEdge),
|
|
818
850
|
start: WireBoundary,
|
|
819
|
-
end: WireBoundary
|
|
851
|
+
end: WireBoundary,
|
|
852
|
+
/**
|
|
853
|
+
* The selected fields of the page's items, in the same shape a read batch
|
|
854
|
+
* returns. Absent from a server that predates payload planning: the client
|
|
855
|
+
* reads it as empty and fetches fields as before.
|
|
856
|
+
*/
|
|
857
|
+
entities: Schema.optional(Schema.Array(NormalizedEntity)),
|
|
858
|
+
settled: Schema.optional(Schema.Array(SettledFields))
|
|
820
859
|
});
|
|
821
860
|
const QueryRpc = Rpc.make("FoldkitRemoteQuery", {
|
|
822
861
|
payload: QueryRequest,
|
|
@@ -884,7 +923,8 @@ const coalesceQueries = (query, options = {}) => Effect.gen(function* () {
|
|
|
884
923
|
key: stableStringify([
|
|
885
924
|
request.query,
|
|
886
925
|
request.input,
|
|
887
|
-
request.window
|
|
926
|
+
request.window,
|
|
927
|
+
request.select ?? null
|
|
888
928
|
]),
|
|
889
929
|
request
|
|
890
930
|
}), resolver);
|
|
@@ -1694,12 +1734,13 @@ const ConnectionChange = {
|
|
|
1694
1734
|
const beginOptimistic = (optimistic, requestId, operations) => {
|
|
1695
1735
|
const patches = operations.filter((operation) => !isConnectionChange(operation));
|
|
1696
1736
|
const changes = operations.filter(isConnectionChange);
|
|
1737
|
+
if (patches.length === 0 && changes.length === 0) return optimistic;
|
|
1697
1738
|
return {
|
|
1698
1739
|
layers: patches.length === 0 ? optimistic.layers : [...optimistic.layers, {
|
|
1699
1740
|
id: requestId,
|
|
1700
1741
|
patches
|
|
1701
1742
|
}],
|
|
1702
|
-
overlays: [...optimistic.overlays, ...toOverlays(requestId, changes)]
|
|
1743
|
+
overlays: changes.length === 0 ? optimistic.overlays : [...optimistic.overlays, ...toOverlays(requestId, changes)]
|
|
1703
1744
|
};
|
|
1704
1745
|
};
|
|
1705
1746
|
/**
|
|
@@ -1708,8 +1749,9 @@ const beginOptimistic = (optimistic, requestId, operations) => {
|
|
|
1708
1749
|
* its place among other pending inserts.
|
|
1709
1750
|
*/
|
|
1710
1751
|
const confirm = (optimistic, requestId, id, changes) => {
|
|
1711
|
-
const confirmed = toOverlays(id, changes);
|
|
1712
1752
|
const at = optimistic.overlays.findIndex((overlay) => overlay.id === requestId);
|
|
1753
|
+
if (at === -1 && changes.length === 0) return optimistic;
|
|
1754
|
+
const confirmed = toOverlays(id, changes);
|
|
1713
1755
|
const others = optimistic.overlays.filter((overlay) => overlay.id !== requestId);
|
|
1714
1756
|
return {
|
|
1715
1757
|
...optimistic,
|
|
@@ -1728,37 +1770,62 @@ const addLayer = (optimistic, layer) => ({
|
|
|
1728
1770
|
...optimistic,
|
|
1729
1771
|
layers: [...optimistic.layers, layer]
|
|
1730
1772
|
});
|
|
1731
|
-
const removeLayer = (optimistic, id) =>
|
|
1732
|
-
|
|
1733
|
-
layers
|
|
1734
|
-
|
|
1773
|
+
const removeLayer = (optimistic, id) => {
|
|
1774
|
+
const layers = optimistic.layers.filter((layer) => layer.id !== id);
|
|
1775
|
+
return layers.length === optimistic.layers.length ? optimistic : {
|
|
1776
|
+
...optimistic,
|
|
1777
|
+
layers
|
|
1778
|
+
};
|
|
1779
|
+
};
|
|
1735
1780
|
const addOverlay = (optimistic, overlay) => ({
|
|
1736
1781
|
...optimistic,
|
|
1737
1782
|
overlays: [...optimistic.overlays, overlay]
|
|
1738
1783
|
});
|
|
1739
|
-
const removeOverlay = (optimistic, id) =>
|
|
1740
|
-
|
|
1741
|
-
overlays
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1784
|
+
const removeOverlay = (optimistic, id) => {
|
|
1785
|
+
const overlays = optimistic.overlays.filter((overlay) => overlay.id !== id);
|
|
1786
|
+
return overlays.length === optimistic.overlays.length ? optimistic : {
|
|
1787
|
+
...optimistic,
|
|
1788
|
+
overlays
|
|
1789
|
+
};
|
|
1790
|
+
};
|
|
1791
|
+
/**
|
|
1792
|
+
* Base store with every layer applied in order. Later layers win. A patch is
|
|
1793
|
+
* not news from the server, so it keeps the date of the value under it; an
|
|
1794
|
+
* entity only a request holds (a temporary id) is not the server's to age, and
|
|
1795
|
+
* is dated `Infinity`, so freshness never plans or times it.
|
|
1796
|
+
*/
|
|
1797
|
+
const visibleStore = (base, optimistic) => writeEntities(base, optimistic.layers.flatMap((layer) => layer.patches.map((patch) => {
|
|
1798
|
+
const key = entityKey(patch.entity, patch.id);
|
|
1799
|
+
const under = base[key];
|
|
1800
|
+
return {
|
|
1801
|
+
key,
|
|
1802
|
+
values: patch.values,
|
|
1803
|
+
updatedAt: under === void 0 || under.tombstone ? Infinity : under.updatedAt
|
|
1804
|
+
};
|
|
1805
|
+
})));
|
|
1745
1806
|
/**
|
|
1746
1807
|
* A fresh page is newer than any settled overlay it covers: an edge the page
|
|
1747
1808
|
* carries no longer needs an insert overlay, and a `remove` overlay hiding it
|
|
1748
1809
|
* is stale (the server says it is back). A pending request's overlays are
|
|
1749
1810
|
* left alone; they settle with the request.
|
|
1750
1811
|
*/
|
|
1751
|
-
const pruneOverlays = (optimistic, connection, covered, pending) =>
|
|
1752
|
-
|
|
1753
|
-
overlays
|
|
1812
|
+
const pruneOverlays = (optimistic, connection, covered, pending) => {
|
|
1813
|
+
let changed = false;
|
|
1814
|
+
const overlays = optimistic.overlays.flatMap((overlay) => {
|
|
1754
1815
|
if (overlay.connection !== connection || pending.has(overlay.id)) return [overlay];
|
|
1755
1816
|
const edges = overlay.edges.filter((edge) => !covered.has(edge.key));
|
|
1817
|
+
if (edges.length > 0 && edges.length === overlay.edges.length) return [overlay];
|
|
1818
|
+
changed = true;
|
|
1756
1819
|
return edges.length === 0 ? [] : [{
|
|
1757
1820
|
...overlay,
|
|
1758
1821
|
edges
|
|
1759
1822
|
}];
|
|
1760
|
-
})
|
|
1761
|
-
|
|
1823
|
+
});
|
|
1824
|
+
return changed ? {
|
|
1825
|
+
...optimistic,
|
|
1826
|
+
overlays
|
|
1827
|
+
} : optimistic;
|
|
1828
|
+
};
|
|
1762
1829
|
/** Everything a request owns: its layer and its overlays. */
|
|
1763
1830
|
const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic, requestId), requestId);
|
|
1764
1831
|
/**
|
|
@@ -1766,12 +1833,12 @@ const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic,
|
|
|
1766
1833
|
* a later layer re-wins on rebase. Confirmed connection changes are recorded
|
|
1767
1834
|
* once per request, as overlays the request no longer owns.
|
|
1768
1835
|
*/
|
|
1769
|
-
const settleSuccess = (base, optimistic, state, requestId,
|
|
1770
|
-
const reconciled = reconcileMutation(base, state, requestId,
|
|
1836
|
+
const settleSuccess = (base, optimistic, state, requestId, answer) => {
|
|
1837
|
+
const reconciled = reconcileMutation(base, state, requestId, answer);
|
|
1771
1838
|
return {
|
|
1772
1839
|
store: reconciled.store,
|
|
1773
1840
|
state: reconciled.state,
|
|
1774
|
-
optimistic: state.applied.has(requestId) ? release(optimistic, requestId) : confirm(removeLayer(optimistic, requestId), requestId, `confirmed:${requestId}`, connections)
|
|
1841
|
+
optimistic: state.applied.has(requestId) ? release(optimistic, requestId) : confirm(removeLayer(optimistic, requestId), requestId, `confirmed:${requestId}`, answer.connections ?? [])
|
|
1775
1842
|
};
|
|
1776
1843
|
};
|
|
1777
1844
|
/** A failed request's layer and overlays are dropped; the base was never mutated. */
|
|
@@ -1846,11 +1913,6 @@ const recordBoundary = (state, connection, position) => {
|
|
|
1846
1913
|
}
|
|
1847
1914
|
};
|
|
1848
1915
|
};
|
|
1849
|
-
/**
|
|
1850
|
-
* A subscriber is woken only if the event changed a field it selects. A
|
|
1851
|
-
* change to an unselected field is skipped entirely.
|
|
1852
|
-
*/
|
|
1853
|
-
const shouldWake = (changed, selected) => changed.some((field) => selected.includes(field));
|
|
1854
1916
|
const applyEntityEvent = (state, store, event, now = 0) => {
|
|
1855
1917
|
const outcome = classifyLive(state, event.cursor);
|
|
1856
1918
|
if (outcome !== "applied") return {
|
|
@@ -1868,9 +1930,13 @@ const applyEntityEvent = (state, store, event, now = 0) => {
|
|
|
1868
1930
|
};
|
|
1869
1931
|
const removeEdgeOverlays = (optimistic, connection, key) => ({
|
|
1870
1932
|
...optimistic,
|
|
1871
|
-
overlays: optimistic.overlays.map((overlay) =>
|
|
1872
|
-
|
|
1873
|
-
edges
|
|
1933
|
+
overlays: optimistic.overlays.map((overlay) => {
|
|
1934
|
+
if (overlay.connection !== connection || overlay.position === "remove") return overlay;
|
|
1935
|
+
const edges = overlay.edges.filter((edge) => edge.key !== key);
|
|
1936
|
+
return edges.length === overlay.edges.length ? overlay : {
|
|
1937
|
+
...overlay,
|
|
1938
|
+
edges
|
|
1939
|
+
};
|
|
1874
1940
|
})
|
|
1875
1941
|
});
|
|
1876
1942
|
const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
|
|
@@ -1960,10 +2026,13 @@ const followRelation = (value, relation) => targetsOf(value, relation).map((ref)
|
|
|
1960
2026
|
/**
|
|
1961
2027
|
* The earliest moment a value `requirements` read, following the relations the
|
|
1962
2028
|
* store holds as `plan` does, goes stale under `freshness`, with the
|
|
1963
|
-
* requirements of the entities that go stale then.
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
2029
|
+
* requirements of the entities that go stale then. A field the server settled
|
|
2030
|
+
* without a value ages with its entity too, since `plan` asks for it again once
|
|
2031
|
+
* the entity expires. Nothing when no such field is still fresh: what is
|
|
2032
|
+
* already expired or stale is planned, not awaited, which is what keeps a
|
|
2033
|
+
* timer from firing on what it already fired on. It is the next moment the
|
|
2034
|
+
* plan can change with the clock alone, which the read entry relies on to
|
|
2035
|
+
* reuse its plan until then.
|
|
1967
2036
|
*/
|
|
1968
2037
|
const deadlineOf = (store, requirements, freshness) => {
|
|
1969
2038
|
let at;
|
|
@@ -1972,9 +2041,9 @@ const deadlineOf = (store, requirements, freshness) => {
|
|
|
1972
2041
|
const key = entityKey(group.entity, group.id);
|
|
1973
2042
|
const entry = store[key];
|
|
1974
2043
|
if (entry === void 0 || entry.tombstone) return;
|
|
1975
|
-
if (group.fields.some((field) => entry.present.has(field) && !entry.stale.has(field))) {
|
|
2044
|
+
if (group.fields.some((field) => entry.present.has(field) && !entry.stale.has(field) || entry.unavailable.has(field))) {
|
|
1976
2045
|
const expires = entry.updatedAt + freshness.freshness;
|
|
1977
|
-
if (expires
|
|
2046
|
+
if (expires >= freshness.now && Number.isFinite(expires)) {
|
|
1978
2047
|
if (at === void 0 || expires < at) {
|
|
1979
2048
|
at = expires;
|
|
1980
2049
|
due = [group];
|
|
@@ -2076,17 +2145,25 @@ const reachable = (store, roots, connections, optimistic, pending) => {
|
|
|
2076
2145
|
const gc = (state, roots) => {
|
|
2077
2146
|
const pending = state.mutations.pending;
|
|
2078
2147
|
const kept = reachable(state.entities, roots, state.connections, state.optimistic, pending);
|
|
2079
|
-
const entities =
|
|
2148
|
+
const entities = keptOf(state.entities, (key) => kept.has(key));
|
|
2080
2149
|
const keptConnections = /* @__PURE__ */ new Set([...roots.connections.map((root) => root.identity), ...state.optimistic.overlays.filter((overlay) => pending.has(overlay.id)).map((overlay) => overlay.connection)]);
|
|
2150
|
+
const connections = keptOf(state.connections, (identity) => keptConnections.has(identity));
|
|
2151
|
+
const overlays = state.optimistic.overlays.filter((overlay) => pending.has(overlay.id) || keptConnections.has(overlay.connection));
|
|
2081
2152
|
return {
|
|
2082
2153
|
entities,
|
|
2083
|
-
connections
|
|
2084
|
-
optimistic: {
|
|
2154
|
+
connections,
|
|
2155
|
+
optimistic: overlays.length === state.optimistic.overlays.length ? state.optimistic : {
|
|
2085
2156
|
layers: state.optimistic.layers,
|
|
2086
|
-
overlays
|
|
2157
|
+
overlays
|
|
2087
2158
|
}
|
|
2088
2159
|
};
|
|
2089
2160
|
};
|
|
2161
|
+
/** The entries whose key is kept; `record` itself when that is all of them. */
|
|
2162
|
+
const keptOf = (record, keep) => {
|
|
2163
|
+
const entries = Object.entries(record);
|
|
2164
|
+
const kept = entries.filter(([key]) => keep(key));
|
|
2165
|
+
return kept.length === entries.length ? record : Object.fromEntries(kept);
|
|
2166
|
+
};
|
|
2090
2167
|
//#endregion
|
|
2091
2168
|
//#region src/persistence.ts
|
|
2092
2169
|
/**
|
|
@@ -2231,7 +2308,7 @@ const hydrate = (raw, options = {}) => {
|
|
|
2231
2308
|
* an entry the current store already has (it is at least as fresh as a
|
|
2232
2309
|
* snapshot taken earlier). Either way, keys only the current store has stay.
|
|
2233
2310
|
*/
|
|
2234
|
-
const mergeStores = (current, snapshot, policy) => policy === "replace" ? {
|
|
2311
|
+
const mergeStores = (current, snapshot, policy) => Object.keys(snapshot).length === 0 ? current : policy === "replace" ? {
|
|
2235
2312
|
...current,
|
|
2236
2313
|
...snapshot
|
|
2237
2314
|
} : {
|
|
@@ -2392,7 +2469,9 @@ const RemoteData = {
|
|
|
2392
2469
|
error: data.error,
|
|
2393
2470
|
previous: f(data.previous)
|
|
2394
2471
|
};
|
|
2395
|
-
|
|
2472
|
+
case "Initial":
|
|
2473
|
+
case "Loading":
|
|
2474
|
+
case "NotFound": return data;
|
|
2396
2475
|
}
|
|
2397
2476
|
}
|
|
2398
2477
|
};
|
|
@@ -2440,6 +2519,7 @@ const forgetRemote = (model) => {
|
|
|
2440
2519
|
const initialRemoteModel = {
|
|
2441
2520
|
entities: emptyStore,
|
|
2442
2521
|
connections: {},
|
|
2522
|
+
grown: {},
|
|
2443
2523
|
optimistic: emptyOptimistic,
|
|
2444
2524
|
live: {},
|
|
2445
2525
|
mutations: emptyMutationState,
|
|
@@ -2453,6 +2533,7 @@ const runtimeSchema = Schema.Unknown;
|
|
|
2453
2533
|
const remoteModelSchema = () => Schema.Struct({
|
|
2454
2534
|
entities: runtimeSchema,
|
|
2455
2535
|
connections: Schema.Record(Schema.String, runtimeSchema),
|
|
2536
|
+
grown: Schema.Record(Schema.String, Schema.Number),
|
|
2456
2537
|
optimistic: runtimeSchema,
|
|
2457
2538
|
live: Schema.Record(Schema.String, runtimeSchema),
|
|
2458
2539
|
mutations: runtimeSchema,
|
|
@@ -2509,7 +2590,8 @@ const remoteMessageCases = {
|
|
|
2509
2590
|
deleted: Schema.optional(Schema.Array(Schema.Struct({
|
|
2510
2591
|
entity: Schema.String,
|
|
2511
2592
|
id: Schema.String
|
|
2512
|
-
})))
|
|
2593
|
+
}))),
|
|
2594
|
+
now: Schema.Number
|
|
2513
2595
|
},
|
|
2514
2596
|
MutationFailed: {
|
|
2515
2597
|
requestId: Schema.String,
|
|
@@ -2520,6 +2602,10 @@ const remoteMessageCases = {
|
|
|
2520
2602
|
optimistic: Schema.Array(Schema.Unknown)
|
|
2521
2603
|
},
|
|
2522
2604
|
OverlayLifted: { id: Schema.String },
|
|
2605
|
+
WindowGrown: {
|
|
2606
|
+
window: Schema.String,
|
|
2607
|
+
size: Schema.Number
|
|
2608
|
+
},
|
|
2523
2609
|
LiveReceived: {
|
|
2524
2610
|
stream: Schema.String,
|
|
2525
2611
|
event: Schema.Unknown,
|
|
@@ -2530,7 +2616,11 @@ const remoteMessageCases = {
|
|
|
2530
2616
|
ConnectionMerged: {
|
|
2531
2617
|
connection: Schema.String,
|
|
2532
2618
|
page: Schema.Unknown,
|
|
2533
|
-
refreshes: Schema.optional(Schema.Boolean)
|
|
2619
|
+
refreshes: Schema.optional(Schema.Boolean),
|
|
2620
|
+
select: Schema.optional(RelationRequest),
|
|
2621
|
+
entities: Schema.optional(Schema.Array(NormalizedEntity)),
|
|
2622
|
+
settled: Schema.optional(Schema.Array(SettledFields)),
|
|
2623
|
+
now: Schema.optional(Schema.Number)
|
|
2534
2624
|
},
|
|
2535
2625
|
ConnectionInvalidated: { connection: Schema.String },
|
|
2536
2626
|
ConnectionRefreshed: { connection: Schema.String },
|
|
@@ -2550,6 +2640,18 @@ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(
|
|
|
2550
2640
|
})));
|
|
2551
2641
|
/** An overlay's layer, named apart from every request's: a request id is the application's to choose too. */
|
|
2552
2642
|
const overlayLayer = (id) => `overlay:${id}`;
|
|
2643
|
+
/** The key a grown window is kept under: its connection and the window first asked for. */
|
|
2644
|
+
const windowGrowthKey = (identity, window) => `${identity}\u0000${window}`;
|
|
2645
|
+
/**
|
|
2646
|
+
* Whether only overlays show this entity: the server has told the store
|
|
2647
|
+
* nothing of it and no request's layer writes it, so nothing will fetch what an
|
|
2648
|
+
* overlay leaves out.
|
|
2649
|
+
*/
|
|
2650
|
+
const onlyOverlaid = (model, entity, id) => {
|
|
2651
|
+
if (Object.hasOwn(model.entities, entityKey(entity, id))) return false;
|
|
2652
|
+
const writers = model.optimistic.layers.filter((layer) => layer.patches.some((patch) => patch.entity === entity && patch.id === id));
|
|
2653
|
+
return writers.length > 0 && writers.every((layer) => layer.id.startsWith("overlay:"));
|
|
2654
|
+
};
|
|
2553
2655
|
const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
|
|
2554
2656
|
/**
|
|
2555
2657
|
* One mark per requested field, `entity\0id\0field`. A read is identified by
|
|
@@ -2565,13 +2667,13 @@ const fieldMarks = function* (requests) {
|
|
|
2565
2667
|
const withLoading = (loading, requests) => {
|
|
2566
2668
|
const next = new Set(loading);
|
|
2567
2669
|
for (const mark of fieldMarks(requests)) next.add(mark);
|
|
2568
|
-
return next;
|
|
2670
|
+
return next.size === loading.size ? loading : next;
|
|
2569
2671
|
};
|
|
2570
2672
|
const withoutLoading = (loading, requests) => {
|
|
2571
2673
|
if (loading.size === 0) return loading;
|
|
2572
2674
|
const next = new Set(loading);
|
|
2573
2675
|
for (const mark of fieldMarks(requests)) next.delete(mark);
|
|
2574
|
-
return next;
|
|
2676
|
+
return next.size === loading.size ? loading : next;
|
|
2575
2677
|
};
|
|
2576
2678
|
/**
|
|
2577
2679
|
* A connection's mark. The leading separator keeps it out of the field marks'
|
|
@@ -2600,6 +2702,14 @@ const prunedLoading = (loading, roots, reached) => {
|
|
|
2600
2702
|
return kept.length === loading.size ? loading : new Set(kept);
|
|
2601
2703
|
};
|
|
2602
2704
|
/**
|
|
2705
|
+
* The windows grown on connections retention keeps; a dropped one starts over.
|
|
2706
|
+
* The same record when every one is kept.
|
|
2707
|
+
*/
|
|
2708
|
+
const keptWindows = (grown, connections) => {
|
|
2709
|
+
const kept = Object.entries(grown).filter(([key]) => Object.hasOwn(connections, key.slice(0, key.lastIndexOf("\0"))));
|
|
2710
|
+
return kept.length === Object.keys(grown).length ? grown : Object.fromEntries(kept);
|
|
2711
|
+
};
|
|
2712
|
+
/**
|
|
2603
2713
|
* Drops the refresh marks of entities and connections that were collected.
|
|
2604
2714
|
* Nothing observes a collected entity — the retention roots are the active
|
|
2605
2715
|
* Surfaces — so no read entry's generation can fall back when its mark goes.
|
|
@@ -2686,15 +2796,20 @@ const isLoading = (model, entity, id, fields) => {
|
|
|
2686
2796
|
return fields.some((field) => model.loading.has(fieldMark(entity, id, field)));
|
|
2687
2797
|
};
|
|
2688
2798
|
const setConnectionStale = (connections, connection, stale) => {
|
|
2689
|
-
const current = connections[connection]
|
|
2799
|
+
const current = connections[connection];
|
|
2800
|
+
if (current?.stale === stale) return connections;
|
|
2690
2801
|
return {
|
|
2691
2802
|
...connections,
|
|
2692
2803
|
[connection]: {
|
|
2693
|
-
...current,
|
|
2804
|
+
...current ?? emptyConnection,
|
|
2694
2805
|
stale
|
|
2695
2806
|
}
|
|
2696
2807
|
};
|
|
2697
2808
|
};
|
|
2809
|
+
const withLiveState = (live, stream, state) => live[stream] === state ? live : {
|
|
2810
|
+
...live,
|
|
2811
|
+
[stream]: state
|
|
2812
|
+
};
|
|
2698
2813
|
const markGap = (model, stream) => model.gaps.has(stream) ? model : {
|
|
2699
2814
|
...model,
|
|
2700
2815
|
gaps: /* @__PURE__ */ new Set([...model.gaps, stream])
|
|
@@ -2706,9 +2821,23 @@ const clearGap = (model, stream) => model.gaps.has(stream) ? {
|
|
|
2706
2821
|
/**
|
|
2707
2822
|
* The pure reducer all four producers share. A live event that arrives ahead of
|
|
2708
2823
|
* its cursor is a gap: it is not applied, and the stream is recorded so the host
|
|
2709
|
-
* can resubscribe rather than silently miss facts.
|
|
2824
|
+
* can resubscribe rather than silently miss facts. When every field a Message
|
|
2825
|
+
* touches keeps its identity, `model` itself is returned, so the application's
|
|
2826
|
+
* root keeps its identity and Foldkit does not render. The common no-ops do (a
|
|
2827
|
+
* duplicate live event, a repeated `ReadStarted`); a repeated failure, or a
|
|
2828
|
+
* `preserve-existing` restore of entities already held, still makes an equal
|
|
2829
|
+
* copy.
|
|
2710
2830
|
*/
|
|
2711
2831
|
const updateRemote = (model, message) => {
|
|
2832
|
+
const next = reduceRemote(model, message);
|
|
2833
|
+
return next === model || !sameFields(model, next) ? next : model;
|
|
2834
|
+
};
|
|
2835
|
+
/** Whether every field of `next` is the one `model` holds. */
|
|
2836
|
+
const sameFields = (model, next) => {
|
|
2837
|
+
const keys = Object.keys(next);
|
|
2838
|
+
return keys.length === Object.keys(model).length && keys.every((key) => Object.is(model[key], next[key]));
|
|
2839
|
+
};
|
|
2840
|
+
const reduceRemote = (model, message) => {
|
|
2712
2841
|
switch (message._tag) {
|
|
2713
2842
|
case "ReadReceived": return {
|
|
2714
2843
|
...model,
|
|
@@ -2731,6 +2860,7 @@ const updateRemote = (model, message) => {
|
|
|
2731
2860
|
return {
|
|
2732
2861
|
...model,
|
|
2733
2862
|
...retained,
|
|
2863
|
+
grown: keptWindows(model.grown, retained.connections),
|
|
2734
2864
|
refresh: prunedRefresh(model.refresh, retained),
|
|
2735
2865
|
failures: prunedFailures(model.failures, message.roots, reached),
|
|
2736
2866
|
loading: prunedLoading(model.loading, message.roots, reached)
|
|
@@ -2750,7 +2880,7 @@ const updateRemote = (model, message) => {
|
|
|
2750
2880
|
...model,
|
|
2751
2881
|
entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
|
|
2752
2882
|
failures: withoutFieldFailures(model.failures, written),
|
|
2753
|
-
connections: message.merge === "replace" ? {
|
|
2883
|
+
connections: restored.length === 0 ? model.connections : message.merge === "replace" ? {
|
|
2754
2884
|
...model.connections,
|
|
2755
2885
|
...Object.fromEntries(restored)
|
|
2756
2886
|
} : {
|
|
@@ -2764,7 +2894,7 @@ const updateRemote = (model, message) => {
|
|
|
2764
2894
|
const marks = marksOf(message.requests);
|
|
2765
2895
|
const entities = setUnavailable(setStale(asked, marks, true), marks, false);
|
|
2766
2896
|
const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
|
|
2767
|
-
return
|
|
2897
|
+
return {
|
|
2768
2898
|
...model,
|
|
2769
2899
|
entities,
|
|
2770
2900
|
failures
|
|
@@ -2789,7 +2919,7 @@ const updateRemote = (model, message) => {
|
|
|
2789
2919
|
optimistic: beginOptimistic(model.optimistic, message.requestId, message.optimistic ?? [])
|
|
2790
2920
|
};
|
|
2791
2921
|
case "MutationSucceeded": {
|
|
2792
|
-
const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message
|
|
2922
|
+
const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message);
|
|
2793
2923
|
return {
|
|
2794
2924
|
...model,
|
|
2795
2925
|
entities: settled.store,
|
|
@@ -2802,6 +2932,13 @@ const updateRemote = (model, message) => {
|
|
|
2802
2932
|
...model,
|
|
2803
2933
|
optimistic: beginOptimistic(settleFailure(model.optimistic, overlayLayer(message.id)), overlayLayer(message.id), message.optimistic)
|
|
2804
2934
|
};
|
|
2935
|
+
case "WindowGrown": return {
|
|
2936
|
+
...model,
|
|
2937
|
+
grown: {
|
|
2938
|
+
...model.grown,
|
|
2939
|
+
[message.window]: message.size
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2805
2942
|
case "OverlayLifted": {
|
|
2806
2943
|
const optimistic = settleFailure(model.optimistic, overlayLayer(message.id));
|
|
2807
2944
|
return optimistic.layers.length === model.optimistic.layers.length && optimistic.overlays.length === model.optimistic.overlays.length ? model : {
|
|
@@ -2827,10 +2964,7 @@ const updateRemote = (model, message) => {
|
|
|
2827
2964
|
return clearGap({
|
|
2828
2965
|
...model,
|
|
2829
2966
|
entities: applied.store,
|
|
2830
|
-
live:
|
|
2831
|
-
...model.live,
|
|
2832
|
-
[message.stream]: applied.state
|
|
2833
|
-
},
|
|
2967
|
+
live: withLiveState(model.live, message.stream, applied.state),
|
|
2834
2968
|
failures: withoutFieldFailures(model.failures, settled)
|
|
2835
2969
|
}, message.stream);
|
|
2836
2970
|
}
|
|
@@ -2839,10 +2973,7 @@ const updateRemote = (model, message) => {
|
|
|
2839
2973
|
const next = clearGap({
|
|
2840
2974
|
...model,
|
|
2841
2975
|
optimistic: applied.optimistic,
|
|
2842
|
-
live:
|
|
2843
|
-
...model.live,
|
|
2844
|
-
[message.stream]: applied.state
|
|
2845
|
-
}
|
|
2976
|
+
live: withLiveState(model.live, message.stream, applied.state)
|
|
2846
2977
|
}, message.stream);
|
|
2847
2978
|
return applied.invalidated === void 0 ? next : updateRemote(next, {
|
|
2848
2979
|
_tag: "ConnectionInvalidated",
|
|
@@ -2852,12 +2983,23 @@ const updateRemote = (model, message) => {
|
|
|
2852
2983
|
case "GapCleared": return clearGap(model, message.stream);
|
|
2853
2984
|
case "ConnectionMerged": {
|
|
2854
2985
|
const current = model.connections[message.connection] ?? emptyConnection;
|
|
2855
|
-
const failures = withoutConnectionFailure(model.failures, message.connection);
|
|
2856
2986
|
const loading = withoutQueryLoading(model.loading, message.connection);
|
|
2857
2987
|
const replaces = message.refreshes === true && current.stale;
|
|
2858
2988
|
const merged = merge(replaces ? emptyConnection : current, message.page);
|
|
2989
|
+
const payload = message.select === void 0 ? [] : message.entities ?? [];
|
|
2990
|
+
const settled = message.select === void 0 ? [] : message.settled ?? [];
|
|
2991
|
+
const requests = message.select === void 0 || payload.length === 0 && settled.length === 0 ? [] : message.page.edges.filter((edge) => edge.ref.entity === message.select.entity).map((edge) => ({
|
|
2992
|
+
...message.select,
|
|
2993
|
+
id: edge.ref.id
|
|
2994
|
+
}));
|
|
2995
|
+
const entities = requests.length === 0 ? model.entities : writeRead(model.entities, requests, {
|
|
2996
|
+
entities: [...payload],
|
|
2997
|
+
settled: [...settled]
|
|
2998
|
+
}, message.now ?? 0);
|
|
2999
|
+
const failures = withoutFieldFailures(withoutConnectionFailure(model.failures, message.connection), [...fieldMarks(requests), ...patchedMarks(payload)]);
|
|
2859
3000
|
return {
|
|
2860
3001
|
...model,
|
|
3002
|
+
entities,
|
|
2861
3003
|
connections: {
|
|
2862
3004
|
...model.connections,
|
|
2863
3005
|
[message.connection]: message.refreshes === true ? {
|
|
@@ -2870,13 +3012,11 @@ const updateRemote = (model, message) => {
|
|
|
2870
3012
|
loading
|
|
2871
3013
|
};
|
|
2872
3014
|
}
|
|
2873
|
-
case "ConnectionInvalidated":
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
failures: withoutConnectionFailure(model.failures, message.connection)
|
|
2879
|
-
};
|
|
3015
|
+
case "ConnectionInvalidated": return {
|
|
3016
|
+
...model,
|
|
3017
|
+
connections: setConnectionStale(model.connections, message.connection, true),
|
|
3018
|
+
failures: withoutConnectionFailure(model.failures, message.connection)
|
|
3019
|
+
};
|
|
2880
3020
|
case "QueryFailed": return {
|
|
2881
3021
|
...model,
|
|
2882
3022
|
failures: {
|
|
@@ -3381,6 +3521,67 @@ const resumePart = (domain, options = {}) => ({
|
|
|
3381
3521
|
*/
|
|
3382
3522
|
const descriptorOf = (entity) => Entity$1.is(entity) ? Entity.from(entity) : entity;
|
|
3383
3523
|
const selectionOf = (selection) => SelectionTypeId in selection ? Selection.from(selection) : selection;
|
|
3524
|
+
const readContract = (source, shape, window) => {
|
|
3525
|
+
const ref = {
|
|
3526
|
+
...source,
|
|
3527
|
+
window: pickWindow(window)
|
|
3528
|
+
};
|
|
3529
|
+
const relation = relationOf(shape);
|
|
3530
|
+
return {
|
|
3531
|
+
ref,
|
|
3532
|
+
relation,
|
|
3533
|
+
requirement: {
|
|
3534
|
+
identity: ref.identity,
|
|
3535
|
+
window: ref.window,
|
|
3536
|
+
select: relation,
|
|
3537
|
+
ref
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
};
|
|
3541
|
+
const metaOf = (remote, asked) => {
|
|
3542
|
+
const visible = visibleStoreOf(remote.entities, remote.optimistic);
|
|
3543
|
+
let updatedAt;
|
|
3544
|
+
let stale = false;
|
|
3545
|
+
let loading = false;
|
|
3546
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3547
|
+
const touch = (entity, id, requirement) => {
|
|
3548
|
+
const key = entityKey(entity, id);
|
|
3549
|
+
const memo = `${key}\u0000${stableStringify(requirement)}`;
|
|
3550
|
+
if (seen.has(memo)) return;
|
|
3551
|
+
seen.add(memo);
|
|
3552
|
+
const entry = visible[key];
|
|
3553
|
+
if (entry !== void 0 && !entry.tombstone) for (const field of requirement.fields) {
|
|
3554
|
+
if (!entry.present.has(field)) continue;
|
|
3555
|
+
if (Number.isFinite(entry.updatedAt)) updatedAt = updatedAt === void 0 ? entry.updatedAt : Math.max(updatedAt, entry.updatedAt);
|
|
3556
|
+
if (entry.stale.has(field)) stale = true;
|
|
3557
|
+
}
|
|
3558
|
+
if (isLoadingThrough(remote, entity, id, requirement)) loading = true;
|
|
3559
|
+
for (const [field, relation] of Object.entries(requirement.relations ?? {})) {
|
|
3560
|
+
const value = readField(visible, key, field);
|
|
3561
|
+
if (value._tag === "None" || value.value === null || value.value === void 0) continue;
|
|
3562
|
+
for (const ref of targetsOf(value.value, relation)) touch(ref.entity, ref.id, relation);
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
for (const requirement of asked.requirements) touch(requirement.entity, requirement.id, requirement);
|
|
3566
|
+
for (const connection of asked.connections) {
|
|
3567
|
+
const known = remote.connections[connection.identity];
|
|
3568
|
+
if (known === void 0) {
|
|
3569
|
+
if (isQueryLoading(remote, connection.identity)) loading = true;
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3572
|
+
if (known.stale) stale = true;
|
|
3573
|
+
if (isQueryLoading(remote, connection.identity)) loading = true;
|
|
3574
|
+
for (const edge of visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities)) {
|
|
3575
|
+
if (edge.ref.entity !== connection.select.entity) continue;
|
|
3576
|
+
touch(edge.ref.entity, edge.ref.id, connection.select);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return {
|
|
3580
|
+
updatedAt,
|
|
3581
|
+
stale,
|
|
3582
|
+
loading
|
|
3583
|
+
};
|
|
3584
|
+
};
|
|
3384
3585
|
/**
|
|
3385
3586
|
* The store a read or plan sees: the base with every pending optimistic layer
|
|
3386
3587
|
* applied, so a request's patches show until it settles and a temporary id is
|
|
@@ -3408,10 +3609,10 @@ const confirmed = (bound, projection) => ({
|
|
|
3408
3609
|
});
|
|
3409
3610
|
const visibleStores = /* @__PURE__ */ new WeakMap();
|
|
3410
3611
|
const visibleStoreOf = (entities, optimistic) => {
|
|
3411
|
-
let byStore = visibleStores.get(optimistic);
|
|
3612
|
+
let byStore = visibleStores.get(optimistic.layers);
|
|
3412
3613
|
if (byStore === void 0) {
|
|
3413
3614
|
byStore = /* @__PURE__ */ new WeakMap();
|
|
3414
|
-
visibleStores.set(optimistic, byStore);
|
|
3615
|
+
visibleStores.set(optimistic.layers, byStore);
|
|
3415
3616
|
}
|
|
3416
3617
|
let visible = byStore.get(entities);
|
|
3417
3618
|
if (visible === void 0) {
|
|
@@ -3436,25 +3637,71 @@ const unavailableFailure = (store, key, relation) => {
|
|
|
3436
3637
|
}
|
|
3437
3638
|
};
|
|
3438
3639
|
};
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3640
|
+
/**
|
|
3641
|
+
* A row's decoded value, kept while the data it was assembled from is the same,
|
|
3642
|
+
* so a refetch or live patch that brought equal data hands a view the object it
|
|
3643
|
+
* already rendered. Keyed on the row's own values object, which the store keeps
|
|
3644
|
+
* across equal writes; a relation's targets are covered by comparing what was
|
|
3645
|
+
* assembled.
|
|
3646
|
+
*/
|
|
3647
|
+
const decodedRows = /* @__PURE__ */ new WeakMap();
|
|
3648
|
+
const decodeRow = (schema, source, values) => {
|
|
3649
|
+
let bySchema = decodedRows.get(schema);
|
|
3650
|
+
if (bySchema === void 0) {
|
|
3651
|
+
bySchema = {
|
|
3652
|
+
decode: Schema.decodeUnknownResult(schema),
|
|
3653
|
+
rows: /* @__PURE__ */ new WeakMap()
|
|
3654
|
+
};
|
|
3655
|
+
decodedRows.set(schema, bySchema);
|
|
3445
3656
|
}
|
|
3446
|
-
|
|
3447
|
-
if (
|
|
3448
|
-
|
|
3449
|
-
|
|
3657
|
+
const cached = bySchema.rows.get(source);
|
|
3658
|
+
if (cached !== void 0 && sameData(cached.values, values)) return cached.decoded;
|
|
3659
|
+
const decoded = bySchema.decode(values);
|
|
3660
|
+
bySchema.rows.set(source, {
|
|
3661
|
+
values,
|
|
3662
|
+
decoded
|
|
3663
|
+
});
|
|
3664
|
+
return decoded;
|
|
3665
|
+
};
|
|
3666
|
+
const readResults = {
|
|
3667
|
+
inner: /* @__PURE__ */ new WeakMap(),
|
|
3668
|
+
results: /* @__PURE__ */ new Map()
|
|
3669
|
+
};
|
|
3670
|
+
const memoRead = (scopes, key, compute) => {
|
|
3671
|
+
let scope = readResults;
|
|
3672
|
+
for (const by of scopes) {
|
|
3673
|
+
let inner = scope.inner.get(by);
|
|
3674
|
+
if (inner === void 0) {
|
|
3675
|
+
inner = {
|
|
3676
|
+
inner: /* @__PURE__ */ new WeakMap(),
|
|
3677
|
+
results: /* @__PURE__ */ new Map()
|
|
3678
|
+
};
|
|
3679
|
+
scope.inner.set(by, inner);
|
|
3680
|
+
}
|
|
3681
|
+
scope = inner;
|
|
3450
3682
|
}
|
|
3451
|
-
if (results.has(key)) return results.get(key);
|
|
3683
|
+
if (scope.results.has(key)) return scope.results.get(key);
|
|
3452
3684
|
const value = compute();
|
|
3453
|
-
results.set(key, value);
|
|
3685
|
+
scope.results.set(key, value);
|
|
3454
3686
|
return value;
|
|
3455
3687
|
};
|
|
3688
|
+
/**
|
|
3689
|
+
* The default clock. It reads `Date.now` when called rather than holding the
|
|
3690
|
+
* function, so fake timers installed after the domain was made move it.
|
|
3691
|
+
*/
|
|
3692
|
+
const wallClock = () => Date.now();
|
|
3456
3693
|
/** The default `toMessage`: the application reduces `RemoteMessage` itself. */
|
|
3457
3694
|
const identityMessage = (message) => message;
|
|
3695
|
+
/**
|
|
3696
|
+
* `Data.satisfy` reached its bound with Surfaces still reading, such as a
|
|
3697
|
+
* chain whose every read reveals one more. It names them, so a page that
|
|
3698
|
+
* would have rendered them loading fails instead. (A read the server leaves
|
|
3699
|
+
* unanswered settles as missing and ends the loop.)
|
|
3700
|
+
*/
|
|
3701
|
+
var RemoteUnsatisfied = class extends Schema.TaggedError()("RemoteUnsatisfied", {
|
|
3702
|
+
surfaces: Schema.Array(Schema.String),
|
|
3703
|
+
passes: Schema.Number
|
|
3704
|
+
}) {};
|
|
3458
3705
|
/** A stable key for a live subscription's requirement set. */
|
|
3459
3706
|
const liveStreamKey = (requirements) => requirements.map((requirement) => `${entityKey(requirement.entity, requirement.id)}:${[...requirement.fields].sort().join(",")}`).sort().join("|");
|
|
3460
3707
|
/** Declares a Remote domain: its entities, queries, and mutations, plus the submodel. */
|
|
@@ -3498,10 +3745,15 @@ const nothingAsked = {
|
|
|
3498
3745
|
requirements: [],
|
|
3499
3746
|
connections: []
|
|
3500
3747
|
};
|
|
3501
|
-
const askedOf = (projection) =>
|
|
3748
|
+
const askedOf = (projection) => ({
|
|
3502
3749
|
requirements: requirementsOf(projection),
|
|
3503
3750
|
connections: connectionsOf(projection)
|
|
3504
|
-
};
|
|
3751
|
+
});
|
|
3752
|
+
/** What an active Surface asks for: nothing while it is inactive. */
|
|
3753
|
+
const askedWhile = (projection) => Option.match(projection, {
|
|
3754
|
+
onNone: () => nothingAsked,
|
|
3755
|
+
onSome: askedOf
|
|
3756
|
+
});
|
|
3505
3757
|
/**
|
|
3506
3758
|
* Whether `outer` asks for everything `inner` does of one entity: each field,
|
|
3507
3759
|
* each relation's own slice of its target, and each page window alike.
|
|
@@ -3532,17 +3784,78 @@ const covers = (outer, inner) => {
|
|
|
3532
3784
|
* failure is what its read shows, and retrying is `Remote.refresh`'s to ask
|
|
3533
3785
|
* for. The rows it already holds are still planned, since they are on screen.
|
|
3534
3786
|
*/
|
|
3787
|
+
/**
|
|
3788
|
+
* One requirement per connection and direction: of the sized windows reading
|
|
3789
|
+
* a connection from its start (or end), the widest, which covers the rest.
|
|
3790
|
+
* Windows with a cursor are pages of their own and stay as they are.
|
|
3791
|
+
*/
|
|
3792
|
+
const widest = (remote, connections) => {
|
|
3793
|
+
const kept = /* @__PURE__ */ new Map();
|
|
3794
|
+
const rest = [];
|
|
3795
|
+
for (const connection of connections) {
|
|
3796
|
+
const size = windowSize(remote, connection);
|
|
3797
|
+
if (size === void 0) {
|
|
3798
|
+
rest.push(connection);
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3801
|
+
const key = `${connection.identity}${String.fromCharCode(0)}${connection.window.first === void 0 ? "last" : "first"}`;
|
|
3802
|
+
const current = kept.get(key);
|
|
3803
|
+
if (current === void 0) kept.set(key, connection);
|
|
3804
|
+
else if (size > (windowSize(remote, current) ?? 0)) kept.set(key, {
|
|
3805
|
+
...connection,
|
|
3806
|
+
select: Requirement.mergeRelation(current.select, connection.select)
|
|
3807
|
+
});
|
|
3808
|
+
else kept.set(key, {
|
|
3809
|
+
...current,
|
|
3810
|
+
select: Requirement.mergeRelation(current.select, connection.select)
|
|
3811
|
+
});
|
|
3812
|
+
}
|
|
3813
|
+
return [...kept.values(), ...rest];
|
|
3814
|
+
};
|
|
3815
|
+
const withWindow = (connection, window) => ({
|
|
3816
|
+
...connection,
|
|
3817
|
+
window,
|
|
3818
|
+
ref: {
|
|
3819
|
+
...connection.ref,
|
|
3820
|
+
window
|
|
3821
|
+
}
|
|
3822
|
+
});
|
|
3823
|
+
/**
|
|
3824
|
+
* The page a connection lacks for a window of `size` from its start (or end):
|
|
3825
|
+
* the rows after its first segment's end cursor (or before its last's start).
|
|
3826
|
+
* Undefined when it holds enough, reaches the end, or does not know where it
|
|
3827
|
+
* starts.
|
|
3828
|
+
*/
|
|
3829
|
+
const missingRows = (known, window, size) => {
|
|
3830
|
+
const forward = window.first !== void 0;
|
|
3831
|
+
const segment = forward ? known.segments[0] : known.segments[known.segments.length - 1];
|
|
3832
|
+
if (segment === void 0) return void 0;
|
|
3833
|
+
const [from, to] = forward ? [segment.start, segment.end] : [segment.end, segment.start];
|
|
3834
|
+
if (from._tag !== "Terminal" || to._tag !== "Cursor" || segment.edges.length >= size) return void 0;
|
|
3835
|
+
const lacking = size - segment.edges.length;
|
|
3836
|
+
return forward ? {
|
|
3837
|
+
first: lacking,
|
|
3838
|
+
after: to.cursor
|
|
3839
|
+
} : {
|
|
3840
|
+
last: lacking,
|
|
3841
|
+
before: to.cursor
|
|
3842
|
+
};
|
|
3843
|
+
};
|
|
3535
3844
|
const planAsked = (remote, asked, options) => {
|
|
3536
3845
|
const queries = [];
|
|
3537
3846
|
const items = [];
|
|
3538
3847
|
const connections = Requirement.mergeConnections(asked.connections);
|
|
3539
|
-
for (const connection of connections) {
|
|
3848
|
+
for (const connection of widest(remote, connections)) {
|
|
3540
3849
|
const known = remote.connections[connection.identity];
|
|
3541
|
-
|
|
3542
|
-
|
|
3850
|
+
const failed = options.force !== true && connection.identity in remote.failures.connections;
|
|
3851
|
+
const size = windowSize(remote, connection);
|
|
3852
|
+
if (!failed && (known === void 0 || known.stale || options.force === true)) {
|
|
3853
|
+
queries.push(size === void 0 ? connection : withWindow(connection, connection.window.first === void 0 ? { last: size } : { first: size }));
|
|
3543
3854
|
continue;
|
|
3544
3855
|
}
|
|
3545
3856
|
if (known === void 0) continue;
|
|
3857
|
+
const tail = failed || size === void 0 ? void 0 : missingRows(known, connection.window, size);
|
|
3858
|
+
if (tail !== void 0) queries.push(withWindow(connection, tail));
|
|
3546
3859
|
const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
|
|
3547
3860
|
items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
|
|
3548
3861
|
}
|
|
@@ -3598,7 +3911,8 @@ const queryRequestOf = (query) => {
|
|
|
3598
3911
|
try: () => ({
|
|
3599
3912
|
query: query.identity.slice(0, separator),
|
|
3600
3913
|
input: JSON.parse(query.identity.slice(separator + 1)),
|
|
3601
|
-
window: query.window
|
|
3914
|
+
window: query.window,
|
|
3915
|
+
...query.select === void 0 ? {} : { select: query.select }
|
|
3602
3916
|
}),
|
|
3603
3917
|
catch: () => new RemoteQueryError({ message: `connection "${query.identity}" carries no encoded input` })
|
|
3604
3918
|
});
|
|
@@ -3627,7 +3941,7 @@ const overrun = (window, edges) => {
|
|
|
3627
3941
|
got: edges.length
|
|
3628
3942
|
} : void 0;
|
|
3629
3943
|
};
|
|
3630
|
-
const pageMessage = (connection, result, refreshes = false, window = {}) => {
|
|
3944
|
+
const pageMessage = (connection, result, refreshes = false, window = {}, select, now) => {
|
|
3631
3945
|
const over = overrun(window, result.edges);
|
|
3632
3946
|
if (over !== void 0) return {
|
|
3633
3947
|
_tag: "QueryFailed",
|
|
@@ -3637,6 +3951,7 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
|
|
|
3637
3951
|
message: `the server returned ${over.got} edges for a window of ${over.asked}`
|
|
3638
3952
|
}
|
|
3639
3953
|
};
|
|
3954
|
+
const hasPayload = select !== void 0 && (result.entities !== void 0 && result.entities.length > 0 || result.settled !== void 0 && result.settled.length > 0);
|
|
3640
3955
|
return {
|
|
3641
3956
|
_tag: "ConnectionMerged",
|
|
3642
3957
|
connection,
|
|
@@ -3651,7 +3966,13 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
|
|
|
3651
3966
|
start: result.start,
|
|
3652
3967
|
end: result.end
|
|
3653
3968
|
},
|
|
3654
|
-
...refreshes ? { refreshes } : {}
|
|
3969
|
+
...refreshes ? { refreshes } : {},
|
|
3970
|
+
...select === void 0 || !hasPayload ? {} : { select },
|
|
3971
|
+
...hasPayload ? {
|
|
3972
|
+
entities: [...result.entities ?? []],
|
|
3973
|
+
settled: [...result.settled ?? []],
|
|
3974
|
+
now: now ?? 0
|
|
3975
|
+
} : {}
|
|
3655
3976
|
};
|
|
3656
3977
|
};
|
|
3657
3978
|
/**
|
|
@@ -3694,14 +4015,14 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
|
|
|
3694
4015
|
};
|
|
3695
4016
|
});
|
|
3696
4017
|
/** Runs a query and reports the page that refreshes its connection, or the failure. Never fails. */
|
|
3697
|
-
const queryMessage = (query) => Effect.gen(function* () {
|
|
4018
|
+
const queryMessage = (query, now = wallClock) => Effect.gen(function* () {
|
|
3698
4019
|
const client = yield* RemoteClient;
|
|
3699
4020
|
const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
|
|
3700
4021
|
return Result.isFailure(result) ? {
|
|
3701
4022
|
_tag: "QueryFailed",
|
|
3702
4023
|
connection: query.identity,
|
|
3703
4024
|
error: remoteError(result.failure)
|
|
3704
|
-
} : pageMessage(query.identity, result.success, true, query.window);
|
|
4025
|
+
} : pageMessage(query.identity, result.success, true, query.window, query.select, now());
|
|
3705
4026
|
});
|
|
3706
4027
|
/** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
|
|
3707
4028
|
const PlannedQuery = Schema.Struct({
|
|
@@ -3719,9 +4040,10 @@ const PlannedQuery = Schema.Struct({
|
|
|
3719
4040
|
* to the restart; a page and its refresh are one `ConnectionMerged`.
|
|
3720
4041
|
*/
|
|
3721
4042
|
const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
3722
|
-
const { policy = RemotePolicy.cacheFirst, now =
|
|
4043
|
+
const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
|
|
3723
4044
|
const read = (requirements) => Effect.map(readMessage(requirements, now), toMessage);
|
|
3724
|
-
const run = (query) => Effect.map(queryMessage(query), toMessage);
|
|
4045
|
+
const run = (query) => Effect.map(queryMessage(query, now), toMessage);
|
|
4046
|
+
const planned = /* @__PURE__ */ new WeakMap();
|
|
3725
4047
|
return {
|
|
3726
4048
|
dependenciesSchema: Schema.Struct({
|
|
3727
4049
|
requirements: Schema.Array(ReadRequest),
|
|
@@ -3734,17 +4056,32 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3734
4056
|
}),
|
|
3735
4057
|
modelToDependencies: (model) => {
|
|
3736
4058
|
const remote = bound.store.get(model);
|
|
3737
|
-
const
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
4059
|
+
const asked = askedOf(model);
|
|
4060
|
+
const askedKey = stableStringify(asked);
|
|
4061
|
+
const at = now();
|
|
4062
|
+
let byAsked = planned.get(remote);
|
|
4063
|
+
const known = byAsked?.get(askedKey);
|
|
4064
|
+
if (known !== void 0 && at < known.until) return known.dependencies;
|
|
4065
|
+
const plan = planAsked(remote, asked, RemotePolicy.toPlan(policy, at));
|
|
4066
|
+
const dependencies = {
|
|
4067
|
+
refresh: refreshedAt(remote.refresh, plan.requirements, plan.queries.map((query) => query.identity)),
|
|
4068
|
+
requirements: plan.requirements,
|
|
4069
|
+
queries: plan.queries.map(({ identity, window, select }) => ({
|
|
3742
4070
|
identity,
|
|
3743
4071
|
window,
|
|
3744
4072
|
select
|
|
3745
4073
|
})),
|
|
3746
|
-
expires:
|
|
4074
|
+
expires: plan.expires
|
|
3747
4075
|
};
|
|
4076
|
+
if (byAsked === void 0) {
|
|
4077
|
+
byAsked = /* @__PURE__ */ new Map();
|
|
4078
|
+
planned.set(remote, byAsked);
|
|
4079
|
+
}
|
|
4080
|
+
byAsked.set(askedKey, {
|
|
4081
|
+
until: plan.expires?.at ?? Infinity,
|
|
4082
|
+
dependencies
|
|
4083
|
+
});
|
|
4084
|
+
return dependencies;
|
|
3748
4085
|
},
|
|
3749
4086
|
dependenciesToStream: ({ requirements, queries, refresh, expires }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
|
|
3750
4087
|
_tag: "ReadStarted",
|
|
@@ -3762,11 +4099,12 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3762
4099
|
})))))
|
|
3763
4100
|
};
|
|
3764
4101
|
};
|
|
4102
|
+
const resumeCursor = Schema.Number.pipe(Schema.overrideToEquivalence(() => () => true));
|
|
3765
4103
|
/** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
|
|
3766
4104
|
const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
3767
4105
|
dependenciesSchema: Schema.Struct({
|
|
3768
4106
|
requirements: Schema.Array(ReadRequest),
|
|
3769
|
-
cursor:
|
|
4107
|
+
cursor: resumeCursor,
|
|
3770
4108
|
floor: Schema.Number
|
|
3771
4109
|
}),
|
|
3772
4110
|
modelToDependencies: (model) => {
|
|
@@ -3788,7 +4126,7 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
|
3788
4126
|
_tag: "LiveReceived",
|
|
3789
4127
|
stream: liveStreamKey(requirements),
|
|
3790
4128
|
event,
|
|
3791
|
-
now: (options.now ??
|
|
4129
|
+
now: (options.now ?? wallClock)()
|
|
3792
4130
|
})), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
|
|
3793
4131
|
_tag: "ReadFailed",
|
|
3794
4132
|
requests: requirements,
|
|
@@ -3796,8 +4134,8 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
|
3796
4134
|
stream: liveStreamKey(requirements)
|
|
3797
4135
|
}))))
|
|
3798
4136
|
});
|
|
3799
|
-
/** The projection an active Surface has for this Model
|
|
3800
|
-
const projectionOf = (entry, model) => "projectionOf" in entry ? entry.projectionOf(model) : entry.projection();
|
|
4137
|
+
/** The projection an active Surface has for this Model; none while it is inactive. */
|
|
4138
|
+
const projectionOf = (entry, model) => "projectionOf" in entry ? entry.projectionOf(model) : Option.some(entry.projection());
|
|
3801
4139
|
/**
|
|
3802
4140
|
* `projectionOf` computed once per Model: the read, live, and retain entries
|
|
3803
4141
|
* all derive their dependencies from the same projection on the same Model
|
|
@@ -3809,7 +4147,8 @@ const memoizedProjectionOf = (entry) => {
|
|
|
3809
4147
|
return (model) => {
|
|
3810
4148
|
const key = model;
|
|
3811
4149
|
if (typeof key !== "object" || key === null) return projectionOf(entry, model);
|
|
3812
|
-
|
|
4150
|
+
const known = cache.get(key);
|
|
4151
|
+
if (known !== void 0) return known;
|
|
3813
4152
|
const projection = projectionOf(entry, model);
|
|
3814
4153
|
cache.set(key, projection);
|
|
3815
4154
|
return projection;
|
|
@@ -3860,7 +4199,6 @@ const Remote = {
|
|
|
3860
4199
|
fold: (domain, toParentMessage) => {
|
|
3861
4200
|
const fold = (model, message) => ({ model: domain.reduce(model, message) });
|
|
3862
4201
|
return Object.assign(fold, {
|
|
3863
|
-
fetch: (ref) => mapMessage(domain.fetch(ref), toParentMessage),
|
|
3864
4202
|
mutate: (...args) => {
|
|
3865
4203
|
const started = domain.mutate(...args);
|
|
3866
4204
|
return {
|
|
@@ -3925,6 +4263,7 @@ const Remote = {
|
|
|
3925
4263
|
const selection = selectionOf(given);
|
|
3926
4264
|
assertRegistered(bound, "Entity", bound.definition.registry.entities, selection.entity);
|
|
3927
4265
|
const relation = relationOf(selection);
|
|
4266
|
+
const relationKey = stableStringify(relation);
|
|
3928
4267
|
return (id) => ({
|
|
3929
4268
|
Model: remoteDataSchema(selection.schema),
|
|
3930
4269
|
dependencies: [],
|
|
@@ -3935,11 +4274,11 @@ const Remote = {
|
|
|
3935
4274
|
read: (root) => {
|
|
3936
4275
|
const store = storeOf(bound, root);
|
|
3937
4276
|
const key = entityKey(selection.entity, id);
|
|
3938
|
-
const present = memoRead(store,
|
|
4277
|
+
const present = memoRead([store], `${key}\u0000${relationKey}`, () => {
|
|
3939
4278
|
if (isTombstone(store, key)) return { _tag: "NotFound" };
|
|
3940
4279
|
const assembled = assemble(store, key, relation);
|
|
3941
4280
|
if (assembled === void 0) return unavailableFailure(store, key, relation);
|
|
3942
|
-
const decoded =
|
|
4281
|
+
const decoded = decodeRow(selection.schema, store[key].values, assembled.values);
|
|
3943
4282
|
return Result.isFailure(decoded) ? {
|
|
3944
4283
|
_tag: "Failed",
|
|
3945
4284
|
error: {
|
|
@@ -3962,14 +4301,36 @@ const Remote = {
|
|
|
3962
4301
|
previous: present.value
|
|
3963
4302
|
} : present;
|
|
3964
4303
|
if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
|
|
3965
|
-
|
|
4304
|
+
if (failure !== void 0) return {
|
|
3966
4305
|
_tag: "Failed",
|
|
3967
4306
|
error: failure
|
|
3968
4307
|
};
|
|
4308
|
+
if (onlyOverlaid(remote, selection.entity, id)) {
|
|
4309
|
+
const lacking = missingFields(store, key, relation.fields);
|
|
4310
|
+
return {
|
|
4311
|
+
_tag: "Failed",
|
|
4312
|
+
error: {
|
|
4313
|
+
_tag: "Overlaid",
|
|
4314
|
+
message: `${selection.entity} ${id} is shown only by an overlay, which does not hold ${lacking.length === 0 ? "what its relations select" : lacking.join(", ")}; the server has not seen it, so nothing will fetch them.`
|
|
4315
|
+
}
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
return { _tag: "Initial" };
|
|
3969
4319
|
}
|
|
3970
4320
|
});
|
|
3971
4321
|
},
|
|
3972
4322
|
/**
|
|
4323
|
+
* When what a projection shows was last received, and whether it is stale
|
|
4324
|
+
* or loading. Pure, so a view can show "updated 5s ago" without I/O.
|
|
4325
|
+
*
|
|
4326
|
+
* `updatedAt` is the newest server write among what is shown, as the
|
|
4327
|
+
* store dates it; `undefined` when nothing shown was received (nothing
|
|
4328
|
+
* loaded, or only an optimistic preview). `stale` is any shown field or
|
|
4329
|
+
* connection marked stale; `loading` is any of them in flight. Optimistic
|
|
4330
|
+
* previews do not date: showing only one reads `updatedAt: undefined`.
|
|
4331
|
+
*/
|
|
4332
|
+
meta: (bound, model, projection) => metaOf(bound.store.get(model), askedOf(projection)),
|
|
4333
|
+
/**
|
|
3973
4334
|
* The pure plan for a projection against a Model: the requirements its
|
|
3974
4335
|
* remote store does not satisfy, under `options` (freshness, force). A
|
|
3975
4336
|
* Surface's projection is `surface.projection(params)`.
|
|
@@ -4000,7 +4361,7 @@ const Remote = {
|
|
|
4000
4361
|
*/
|
|
4001
4362
|
prefetch: Effect.fn("Remote.prefetch")(function* (bound, model, projection, options = {}) {
|
|
4002
4363
|
const store = storeOf(bound, model);
|
|
4003
|
-
const { policy = RemotePolicy.cacheFirst, now =
|
|
4364
|
+
const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
|
|
4004
4365
|
const at = now();
|
|
4005
4366
|
const missing = plan(store, requirementsOf(projection), RemotePolicy.toPlan(policy, at));
|
|
4006
4367
|
if (missing.length === 0) return store;
|
|
@@ -4119,18 +4480,21 @@ const Remote = {
|
|
|
4119
4480
|
/**
|
|
4120
4481
|
* Runs a `Query` through `RemoteClient`, encoding its input from the ref.
|
|
4121
4482
|
* Pair the result with `Remote.queryMessage` to merge the page into the Model.
|
|
4483
|
+
* With `select`, the server also returns the selected fields of the page's
|
|
4484
|
+
* items, so one response populates both the connection and the store.
|
|
4122
4485
|
*/
|
|
4123
|
-
query: Effect.fn("Remote.query")(function* (ref) {
|
|
4486
|
+
query: Effect.fn("Remote.query")(function* (ref, select) {
|
|
4124
4487
|
const client = yield* RemoteClient;
|
|
4125
4488
|
const input = yield* Schema.encodeUnknownEffect(ref.Input)(ref.input).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new RemoteQueryError({ message: error.message }))));
|
|
4126
4489
|
return yield* client.query({
|
|
4127
4490
|
query: ref.query,
|
|
4128
4491
|
input,
|
|
4129
|
-
window: ref.window
|
|
4492
|
+
window: ref.window,
|
|
4493
|
+
...select === void 0 ? {} : { select }
|
|
4130
4494
|
});
|
|
4131
4495
|
}),
|
|
4132
4496
|
/** The `RemoteMessage` that merges a query page into its connection. */
|
|
4133
|
-
queryMessage: (ref, result) => pageMessage(ref.identity, result, false, ref.window),
|
|
4497
|
+
queryMessage: (ref, result, select, now) => pageMessage(ref.identity, result, false, ref.window, select, now),
|
|
4134
4498
|
/**
|
|
4135
4499
|
* The edges a connection shows: its server-known region with pending and
|
|
4136
4500
|
* confirmed overlays placed around it, minus removed edges and edges whose
|
|
@@ -4169,7 +4533,8 @@ const Remote = {
|
|
|
4169
4533
|
requestId,
|
|
4170
4534
|
entities: outcome.entities,
|
|
4171
4535
|
connections: outcome.connections,
|
|
4172
|
-
deleted: outcome.deleted
|
|
4536
|
+
deleted: outcome.deleted,
|
|
4537
|
+
now: (options.now ?? wallClock)()
|
|
4173
4538
|
});
|
|
4174
4539
|
return {
|
|
4175
4540
|
output: outcome.output,
|
|
@@ -4196,9 +4561,20 @@ const Remote = {
|
|
|
4196
4561
|
live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
|
|
4197
4562
|
};
|
|
4198
4563
|
const brandEntries = (entries) => Subscription.make()(() => entries);
|
|
4564
|
+
/** Why a read failed, in words: Remote's own failures say what to do, and a server's error says itself. */
|
|
4565
|
+
const failedReadWords = (error) => {
|
|
4566
|
+
switch (error._tag) {
|
|
4567
|
+
case "DecodeError": return `What the server sent does not decode against the Selection: ${error.message}`;
|
|
4568
|
+
case "Unavailable": return `${error.message} Select without it, or Data.refresh asks again.`;
|
|
4569
|
+
case "Overlaid": return `${error.message} Overlay every field the Selection reads, or select fewer.`;
|
|
4570
|
+
default: return `Its request failed: ${error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`;
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4199
4573
|
/** The bound domain: the descriptor, the binding, and the operations over them. */
|
|
4200
4574
|
const bindDomain = (definition, store) => {
|
|
4201
4575
|
const bound = bindRemote(definition, store);
|
|
4576
|
+
const readers = /* @__PURE__ */ new Map();
|
|
4577
|
+
const keptConnections = /* @__PURE__ */ new Set();
|
|
4202
4578
|
/**
|
|
4203
4579
|
* What to do with a row a live event says was inserted into a connection.
|
|
4204
4580
|
*
|
|
@@ -4239,7 +4615,12 @@ const bindDomain = (definition, store) => {
|
|
|
4239
4615
|
...live,
|
|
4240
4616
|
policy: livePolicyFor(model, live.event)
|
|
4241
4617
|
};
|
|
4242
|
-
|
|
4618
|
+
const remote = store.get(model);
|
|
4619
|
+
const next = updateRemote(remote, resolved);
|
|
4620
|
+
return next === remote ? model : store.set(model, next);
|
|
4621
|
+
};
|
|
4622
|
+
const assertOwned = (entry) => {
|
|
4623
|
+
if (bound.contract.owner !== void 0 && entry.owner !== bound.contract.owner) throw new Error(`Remote: Surface "${entry.name}" belongs to another application than domain "${bound.contract.name}"`);
|
|
4243
4624
|
};
|
|
4244
4625
|
const domain = {
|
|
4245
4626
|
...definition,
|
|
@@ -4255,23 +4636,30 @@ const bindDomain = (definition, store) => {
|
|
|
4255
4636
|
})))
|
|
4256
4637
|
};
|
|
4257
4638
|
},
|
|
4639
|
+
active: (name, projectionOf) => {
|
|
4640
|
+
const { owner } = bound.contract;
|
|
4641
|
+
if (owner === void 0) throw new Error(`Remote: "${name}" reads domain "${bound.contract.name}", whose Model field is a raw optic that names no application; make the domain from an application's field (App.model.remote) to give it reads`);
|
|
4642
|
+
return {
|
|
4643
|
+
name,
|
|
4644
|
+
owner,
|
|
4645
|
+
messages: [],
|
|
4646
|
+
projectionOf
|
|
4647
|
+
};
|
|
4648
|
+
},
|
|
4258
4649
|
subscriptions: (active, options = {}) => {
|
|
4259
4650
|
const entries = {};
|
|
4260
|
-
const
|
|
4651
|
+
for (const entry of Object.values(active)) assertOwned(entry);
|
|
4261
4652
|
for (const [key, entry] of Object.entries(active)) {
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
const asked = (model) => askedOf(projectionAt(model));
|
|
4653
|
+
const projectionAt = readers.get(entry) ?? memoizedProjectionOf(entry);
|
|
4654
|
+
readers.set(entry, projectionAt);
|
|
4655
|
+
const asked = (model) => askedWhile(projectionAt(model));
|
|
4266
4656
|
entries[`${key}.read`] = observeEntry(bound, asked, identityMessage, options);
|
|
4267
4657
|
entries[`${key}.live`] = liveEntry(bound, (model) => asked(model).requirements.filter((requirement) => requirement.live === true), identityMessage, options);
|
|
4268
4658
|
}
|
|
4659
|
+
for (const identity of options.connections ?? []) keptConnections.add(connectionIdentity(identity));
|
|
4269
4660
|
entries.retain = {
|
|
4270
4661
|
dependenciesSchema: retentionRootsSchema,
|
|
4271
|
-
modelToDependencies: (model) => rootsOf(
|
|
4272
|
-
const projection = projectionAt(model);
|
|
4273
|
-
return projection === void 0 ? [] : [projection];
|
|
4274
|
-
}), options),
|
|
4662
|
+
modelToDependencies: (model) => rootsOf([...readers.values()].flatMap((projectionAt) => Option.toArray(projectionAt(model))), { connections: [...keptConnections] }),
|
|
4275
4663
|
dependenciesToStream: (current) => Stream.fromEffect(Effect.succeed({
|
|
4276
4664
|
_tag: "RetentionChanged",
|
|
4277
4665
|
roots: current
|
|
@@ -4281,16 +4669,17 @@ const bindDomain = (definition, store) => {
|
|
|
4281
4669
|
return entries;
|
|
4282
4670
|
},
|
|
4283
4671
|
plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
|
|
4672
|
+
meta: (model, projection) => Remote.meta(bound, model, projection),
|
|
4284
4673
|
storeOf: (model) => storeOf(bound, model),
|
|
4285
4674
|
confirmed: (projection) => confirmed(bound, projection),
|
|
4286
4675
|
prefetch: (model, projection, options = {}) => Effect.gen(function* () {
|
|
4287
|
-
const { policy = RemotePolicy.cacheFirst, now =
|
|
4676
|
+
const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
|
|
4288
4677
|
const client = yield* RemoteClient;
|
|
4289
4678
|
const planOptions = RemotePolicy.toPlan(policy, now());
|
|
4290
4679
|
let current = model;
|
|
4291
4680
|
for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
|
|
4292
4681
|
const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
|
|
4293
|
-
current = reduce(current, pageMessage(query.identity, page, true, query.window));
|
|
4682
|
+
current = reduce(current, pageMessage(query.identity, page, true, query.window, query.select, now()));
|
|
4294
4683
|
}
|
|
4295
4684
|
const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
|
|
4296
4685
|
if (requirements.length === 0) return current;
|
|
@@ -4306,6 +4695,30 @@ const bindDomain = (definition, store) => {
|
|
|
4306
4695
|
now: at
|
|
4307
4696
|
});
|
|
4308
4697
|
}),
|
|
4698
|
+
satisfy: (model, active, options = {}) => Effect.gen(function* () {
|
|
4699
|
+
const { passes = 8, now = wallClock } = options;
|
|
4700
|
+
const entries = Object.values(active);
|
|
4701
|
+
entries.forEach(assertOwned);
|
|
4702
|
+
const planOptions = RemotePolicy.toPlan(RemotePolicy.cacheFirst, now());
|
|
4703
|
+
const plans = (current, entry) => {
|
|
4704
|
+
const planned = planAsked(store.get(current), askedWhile(projectionOf(entry, current)), planOptions);
|
|
4705
|
+
return planned.queries.length > 0 || planned.requirements.length > 0;
|
|
4706
|
+
};
|
|
4707
|
+
let current = model;
|
|
4708
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
4709
|
+
if (!entries.some((entry) => plans(current, entry))) return current;
|
|
4710
|
+
for (const entry of entries) {
|
|
4711
|
+
const projection = projectionOf(entry, current);
|
|
4712
|
+
if (Option.isSome(projection)) current = yield* domain.prefetch(current, projection.value, { now });
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
const reading = entries.filter((entry) => plans(current, entry));
|
|
4716
|
+
if (reading.length === 0) return current;
|
|
4717
|
+
return yield* new RemoteUnsatisfied({
|
|
4718
|
+
surfaces: reading.map((entry) => entry.name),
|
|
4719
|
+
passes
|
|
4720
|
+
});
|
|
4721
|
+
}),
|
|
4309
4722
|
query: (query, input, options) => {
|
|
4310
4723
|
assertRegistered(bound, "Query", definition.registry.queries, query.name);
|
|
4311
4724
|
const { select: given, ...window } = options;
|
|
@@ -4314,15 +4727,13 @@ const bindDomain = (definition, store) => {
|
|
|
4314
4727
|
const listed = query.Result.entity;
|
|
4315
4728
|
if (listed === void 0) throw new Error(`Remote: query "${query.name}" is not a connection over an entity, so it has no page to select`);
|
|
4316
4729
|
if (select.entity !== listed) throw new Error(`Remote: the selection is of "${select.entity}", but query "${query.name}" lists "${listed}"`);
|
|
4317
|
-
const ref =
|
|
4318
|
-
...query.ref(input),
|
|
4319
|
-
window: pickWindow(window)
|
|
4320
|
-
};
|
|
4321
|
-
const relation = relationOf(select);
|
|
4730
|
+
const { ref, relation, requirement } = readContract(query.ref(input), select, window);
|
|
4322
4731
|
const relationKey = stableStringify(relation);
|
|
4732
|
+
const sizeOf = (remote) => windowSize(remote, ref);
|
|
4733
|
+
const shown = (remote, connection) => cutToWindow(visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities), ref.window, sizeOf(remote));
|
|
4323
4734
|
const failedItem = (remote, connection) => {
|
|
4324
4735
|
if (Object.keys(remote.failures.fields).length === 0) return void 0;
|
|
4325
|
-
for (const edge of
|
|
4736
|
+
for (const edge of shown(remote, connection).edges) {
|
|
4326
4737
|
if (edge.ref.entity !== relation.entity) continue;
|
|
4327
4738
|
const error = failureOf(remote, edge.ref.entity, edge.ref.id, relation);
|
|
4328
4739
|
if (error !== void 0) return {
|
|
@@ -4333,22 +4744,26 @@ const bindDomain = (definition, store) => {
|
|
|
4333
4744
|
};
|
|
4334
4745
|
const loadingItem = (remote, connection) => {
|
|
4335
4746
|
if (remote.loading.size === 0) return void 0;
|
|
4336
|
-
for (const edge of
|
|
4747
|
+
for (const edge of shown(remote, connection).edges) {
|
|
4337
4748
|
if (edge.ref.entity !== relation.entity) continue;
|
|
4338
4749
|
if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
|
|
4339
4750
|
}
|
|
4340
4751
|
};
|
|
4341
4752
|
const readPage = (remote, connection) => {
|
|
4342
4753
|
const visible = visibleStoreOf(remote.entities, remote.optimistic);
|
|
4343
|
-
return memoRead(
|
|
4754
|
+
return memoRead([
|
|
4755
|
+
visible,
|
|
4756
|
+
connection,
|
|
4757
|
+
remote.optimistic.overlays
|
|
4758
|
+
], `${ref.identity}\u0000${relationKey}\u0000${String(sizeOf(remote))}`, () => {
|
|
4344
4759
|
const items = [];
|
|
4345
4760
|
let refreshing = connection.stale;
|
|
4346
|
-
const edges =
|
|
4761
|
+
const { edges, cutBefore, cutAfter } = shown(remote, connection);
|
|
4347
4762
|
for (const edge of edges) {
|
|
4348
4763
|
const key = entityKey(edge.ref.entity, edge.ref.id);
|
|
4349
4764
|
const assembled = assemble(visible, key, relation);
|
|
4350
4765
|
if (assembled === void 0) return unavailableFailure(visible, key, relation);
|
|
4351
|
-
const decoded =
|
|
4766
|
+
const decoded = decodeRow(select.schema, visible[key].values, assembled.values);
|
|
4352
4767
|
if (Result.isFailure(decoded)) return {
|
|
4353
4768
|
_tag: "Failed",
|
|
4354
4769
|
error: {
|
|
@@ -4361,8 +4776,8 @@ const bindDomain = (definition, store) => {
|
|
|
4361
4776
|
}
|
|
4362
4777
|
const page = {
|
|
4363
4778
|
items,
|
|
4364
|
-
hasNext: hasNext(connection),
|
|
4365
|
-
hasPrevious: hasPrevious(connection)
|
|
4779
|
+
hasNext: cutAfter || hasNext(connection),
|
|
4780
|
+
hasPrevious: cutBefore || hasPrevious(connection)
|
|
4366
4781
|
};
|
|
4367
4782
|
return refreshing ? {
|
|
4368
4783
|
_tag: "Refreshing",
|
|
@@ -4373,12 +4788,6 @@ const bindDomain = (definition, store) => {
|
|
|
4373
4788
|
};
|
|
4374
4789
|
});
|
|
4375
4790
|
};
|
|
4376
|
-
const requirement = {
|
|
4377
|
-
identity: ref.identity,
|
|
4378
|
-
window: ref.window,
|
|
4379
|
-
select: relation,
|
|
4380
|
-
ref
|
|
4381
|
-
};
|
|
4382
4791
|
return {
|
|
4383
4792
|
Model: remoteDataSchema(pageSchema(select.schema)),
|
|
4384
4793
|
dependencies: [],
|
|
@@ -4414,7 +4823,9 @@ const bindDomain = (definition, store) => {
|
|
|
4414
4823
|
previous: read.value
|
|
4415
4824
|
};
|
|
4416
4825
|
case "Failed": return read;
|
|
4417
|
-
|
|
4826
|
+
case "Initial":
|
|
4827
|
+
case "Loading":
|
|
4828
|
+
case "NotFound": return {
|
|
4418
4829
|
_tag: "Failed",
|
|
4419
4830
|
error: failure
|
|
4420
4831
|
};
|
|
@@ -4422,32 +4833,23 @@ const bindDomain = (definition, store) => {
|
|
|
4422
4833
|
}
|
|
4423
4834
|
};
|
|
4424
4835
|
},
|
|
4425
|
-
|
|
4426
|
-
const
|
|
4427
|
-
const
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
const start = store.get(model).connections[projection.ref.identity]?.segments[0]?.start;
|
|
4438
|
-
return start?._tag === "Cursor" ? {
|
|
4439
|
-
...projection.ref,
|
|
4440
|
-
window: {
|
|
4441
|
-
...pageSize(projection.ref.window, "last"),
|
|
4442
|
-
before: start.cursor
|
|
4443
|
-
}
|
|
4444
|
-
} : void 0;
|
|
4836
|
+
more: (model, projection) => {
|
|
4837
|
+
const remote = store.get(model);
|
|
4838
|
+
const { ref } = projection;
|
|
4839
|
+
const size = windowSize(remote, ref);
|
|
4840
|
+
const page = ref.window.first ?? ref.window.last;
|
|
4841
|
+
if (size === void 0 || page === void 0 || page === 0) return Option.none();
|
|
4842
|
+
const read = projection.read(model);
|
|
4843
|
+
return (read._tag === "Ready" || read._tag === "Refreshing") && (ref.window.first === void 0 ? read.value.hasPrevious : read.value.hasNext) ? Option.some(bound.store.set(model, updateRemote(remote, {
|
|
4844
|
+
_tag: "WindowGrown",
|
|
4845
|
+
window: windowGrowthKey(ref.identity, stableStringify(ref.window)),
|
|
4846
|
+
size: size + page
|
|
4847
|
+
}))) : Option.none();
|
|
4445
4848
|
},
|
|
4446
4849
|
explain: (model, projection, options) => {
|
|
4447
4850
|
const { ref } = projection;
|
|
4448
4851
|
const reading = Object.values(options?.surfaces ?? {}).filter((active) => {
|
|
4449
|
-
|
|
4450
|
-
return active_ !== void 0 && connectionsOf(active_).some((connection) => connection.identity === ref.identity);
|
|
4852
|
+
return Option.exists(active.projectionOf(model), (shown) => connectionsOf(shown).some((connection) => connection.identity === ref.identity));
|
|
4451
4853
|
});
|
|
4452
4854
|
const body = definition.registry.queries.get(ref.query)?.body;
|
|
4453
4855
|
const [connection] = RemoteConnections.get(projection.metadata);
|
|
@@ -4476,8 +4878,7 @@ const bindDomain = (definition, store) => {
|
|
|
4476
4878
|
const state = projection.read(model);
|
|
4477
4879
|
const asked = askedOf(projection);
|
|
4478
4880
|
const reading = options?.surfaces === void 0 ? void 0 : Object.values(options.surfaces).filter((active) => {
|
|
4479
|
-
|
|
4480
|
-
return shown !== void 0 && covers(askedOf(shown), asked);
|
|
4881
|
+
return Option.exists(active.projectionOf(model), (shown) => covers(askedOf(shown), asked));
|
|
4481
4882
|
}).map((active) => active.name);
|
|
4482
4883
|
const withSurfaces = reading === void 0 ? {} : { surfaces: reading };
|
|
4483
4884
|
switch (state._tag) {
|
|
@@ -4503,7 +4904,7 @@ const bindDomain = (definition, store) => {
|
|
|
4503
4904
|
};
|
|
4504
4905
|
case "Failed": return {
|
|
4505
4906
|
state: state._tag,
|
|
4506
|
-
message: state.error
|
|
4907
|
+
message: failedReadWords(state.error),
|
|
4507
4908
|
...withSurfaces
|
|
4508
4909
|
};
|
|
4509
4910
|
case "Initial":
|
|
@@ -4543,7 +4944,7 @@ const bindDomain = (definition, store) => {
|
|
|
4543
4944
|
assembledAll = false;
|
|
4544
4945
|
continue;
|
|
4545
4946
|
}
|
|
4546
|
-
const decoded =
|
|
4947
|
+
const decoded = decodeRow(over.selection.schema, visible[key].values, assembled.values);
|
|
4547
4948
|
if (Result.isFailure(decoded)) {
|
|
4548
4949
|
assembledAll = false;
|
|
4549
4950
|
continue;
|
|
@@ -4557,33 +4958,14 @@ const bindDomain = (definition, store) => {
|
|
|
4557
4958
|
},
|
|
4558
4959
|
refresh: (model, target) => Remote.refresh(bound, model, target),
|
|
4559
4960
|
forget: (model) => Remote.forget(bound, model),
|
|
4560
|
-
overlay: (model, id, optimistic) =>
|
|
4961
|
+
overlay: (model, id, optimistic) => reduce(model, {
|
|
4561
4962
|
_tag: "OverlayShown",
|
|
4562
4963
|
id,
|
|
4563
4964
|
optimistic
|
|
4564
|
-
})
|
|
4565
|
-
lift: (model, id) => {
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
_tag: "OverlayLifted",
|
|
4569
|
-
id
|
|
4570
|
-
});
|
|
4571
|
-
return lifted === remote ? model : bound.store.set(model, lifted);
|
|
4572
|
-
},
|
|
4573
|
-
fetch: (ref) => ({
|
|
4574
|
-
name: `Remote.query(${ref.query})`,
|
|
4575
|
-
args: {
|
|
4576
|
-
connection: ref.identity,
|
|
4577
|
-
window: ref.window
|
|
4578
|
-
},
|
|
4579
|
-
effect: Remote.query(ref).pipe(Effect.match({
|
|
4580
|
-
onFailure: (error) => ({
|
|
4581
|
-
_tag: "QueryFailed",
|
|
4582
|
-
connection: ref.identity,
|
|
4583
|
-
error: remoteError(error)
|
|
4584
|
-
}),
|
|
4585
|
-
onSuccess: (page) => pageMessage(ref.identity, page, false, ref.window)
|
|
4586
|
-
}))
|
|
4965
|
+
}),
|
|
4966
|
+
lift: (model, id) => reduce(model, {
|
|
4967
|
+
_tag: "OverlayLifted",
|
|
4968
|
+
id
|
|
4587
4969
|
}),
|
|
4588
4970
|
mutate: (model, mutation, input, options = {}) => {
|
|
4589
4971
|
assertRegistered(bound, "Mutation", definition.registry.mutations, mutation.name);
|
|
@@ -4617,7 +4999,8 @@ const bindDomain = (definition, store) => {
|
|
|
4617
4999
|
requestId,
|
|
4618
5000
|
entities: outcome.entities,
|
|
4619
5001
|
connections: outcome.connections,
|
|
4620
|
-
deleted: outcome.deleted
|
|
5002
|
+
deleted: outcome.deleted,
|
|
5003
|
+
now: (options.now ?? wallClock)()
|
|
4621
5004
|
})
|
|
4622
5005
|
}))
|
|
4623
5006
|
}
|
|
@@ -4637,18 +5020,41 @@ const bindDomain = (definition, store) => {
|
|
|
4637
5020
|
return domain;
|
|
4638
5021
|
};
|
|
4639
5022
|
/** The window keys of a `QueryOptions`, and only those, without the undefined ones. */
|
|
5023
|
+
/**
|
|
5024
|
+
* How many rows a read of `ref` shows: its window's `first` or `last`, as
|
|
5025
|
+
* `Data.more` has grown it. `undefined` for a window with a cursor or no size,
|
|
5026
|
+
* which shows whatever the connection holds.
|
|
5027
|
+
*/
|
|
5028
|
+
const windowSize = (remote, ref) => {
|
|
5029
|
+
const { first, last, after, before } = ref.window;
|
|
5030
|
+
if (after !== void 0 || before !== void 0) return void 0;
|
|
5031
|
+
const size = first ?? last;
|
|
5032
|
+
return size === void 0 ? void 0 : remote.grown[windowGrowthKey(ref.identity, stableStringify(ref.window))] ?? size;
|
|
5033
|
+
};
|
|
5034
|
+
/** The edges a window of `size` shows, from the start (`first`) or the end (`last`), and which end it cut. */
|
|
5035
|
+
const cutToWindow = (edges, window, size) => {
|
|
5036
|
+
if (size === void 0 || edges.length <= size) return {
|
|
5037
|
+
edges,
|
|
5038
|
+
cutBefore: false,
|
|
5039
|
+
cutAfter: false
|
|
5040
|
+
};
|
|
5041
|
+
return window.first === void 0 ? {
|
|
5042
|
+
edges: edges.slice(edges.length - size),
|
|
5043
|
+
cutBefore: true,
|
|
5044
|
+
cutAfter: false
|
|
5045
|
+
} : {
|
|
5046
|
+
edges: edges.slice(0, size),
|
|
5047
|
+
cutBefore: false,
|
|
5048
|
+
cutAfter: true
|
|
5049
|
+
};
|
|
5050
|
+
};
|
|
4640
5051
|
const pickWindow = (window) => ({
|
|
4641
5052
|
...window.first === void 0 ? {} : { first: window.first },
|
|
4642
5053
|
...window.last === void 0 ? {} : { last: window.last },
|
|
4643
5054
|
...window.after === void 0 ? {} : { after: window.after },
|
|
4644
5055
|
...window.before === void 0 ? {} : { before: window.before }
|
|
4645
5056
|
});
|
|
4646
|
-
/** The page size of a window, carried onto the next or previous page's window under `key`. */
|
|
4647
|
-
const pageSize = (window, key) => {
|
|
4648
|
-
const size = window.first ?? window.last;
|
|
4649
|
-
return size === void 0 ? {} : { [key]: size };
|
|
4650
|
-
};
|
|
4651
5057
|
//#endregion
|
|
4652
|
-
export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, SettledFields, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, deadlineOf, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, forgetRemote, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isFieldUnavailable, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, setUnavailable, settleFailure, settleSuccess,
|
|
5058
|
+
export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, RemoteUnsatisfied, Requirement, Selection, SettledFields, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, deadlineOf, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, forgetRemote, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isFieldUnavailable, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, onlyOverlaid, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, setUnavailable, settleFailure, settleSuccess, stableStringify, targetsOf, terminal, tombstone, unavailableOf, unknown, updateRemote, visibleItems, visibleStore, windowGrowthKey, windowKey, withRefreshRequested, writeEntities, writeEntity, writeRead };
|
|
4653
5059
|
|
|
4654
5060
|
//# sourceMappingURL=index.mjs.map
|