foldkit-remote 0.5.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/README.md +255 -70
- package/dist/index.d.mts +358 -28
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +701 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
|
|
2
2
|
import * as Domain from "foldkit-entity";
|
|
3
|
-
import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId } from "foldkit-entity";
|
|
3
|
+
import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId, evaluate } from "foldkit-entity";
|
|
4
4
|
import * as Subscription from "foldkit/subscription";
|
|
5
5
|
import { Metadata } from "foldkit-surface";
|
|
6
6
|
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
@@ -440,6 +440,16 @@ make: (name, config) => ({
|
|
|
440
440
|
* canonical encoded filter/sort input — **never** the pagination window, so
|
|
441
441
|
* `first(25)` and `after(cursor).first(25)` address the same logical connection.
|
|
442
442
|
*/
|
|
443
|
+
/**
|
|
444
|
+
* What separates a query's name from its canonical encoded input in a
|
|
445
|
+
* connection identity.
|
|
446
|
+
*
|
|
447
|
+
* NUL, because `JSON.stringify` escapes it inside a string (`\u0000`) and a
|
|
448
|
+
* query name is a TypeScript identifier — so the first occurrence is always the
|
|
449
|
+
* boundary, and a name or an input value cannot forge one. Anything recovering
|
|
450
|
+
* the query from an identity splits here.
|
|
451
|
+
*/
|
|
452
|
+
const IDENTITY_SEPARATOR = "\0";
|
|
443
453
|
/** Stable stringify: object keys sorted, undefined-valued keys dropped, so equal inputs encode equally. */
|
|
444
454
|
const stableStringify = (value) => {
|
|
445
455
|
if (value === void 0) return "null";
|
|
@@ -498,7 +508,7 @@ const Query = {
|
|
|
498
508
|
input,
|
|
499
509
|
Input,
|
|
500
510
|
window: {},
|
|
501
|
-
identity: `${name}
|
|
511
|
+
identity: `${name}${stableStringify(encode(input))}`
|
|
502
512
|
})
|
|
503
513
|
};
|
|
504
514
|
},
|
|
@@ -1424,6 +1434,118 @@ const Entity = {
|
|
|
1424
1434
|
})
|
|
1425
1435
|
};
|
|
1426
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
|
|
1427
1549
|
//#region src/inspect.ts
|
|
1428
1550
|
const inspectEntry = (key, entry) => ({
|
|
1429
1551
|
key,
|
|
@@ -1440,6 +1562,7 @@ const inspectRemote = (model) => ({
|
|
|
1440
1562
|
live: Object.keys(model.live),
|
|
1441
1563
|
gaps: [...model.gaps],
|
|
1442
1564
|
loading: [...model.loading],
|
|
1565
|
+
failures: model.failures,
|
|
1443
1566
|
mutations: {
|
|
1444
1567
|
pending: [...model.mutations.pending],
|
|
1445
1568
|
failed: [...model.mutations.failed],
|
|
@@ -1617,7 +1740,6 @@ const visibleItems = (connection, connectionId, overlays, store) => {
|
|
|
1617
1740
|
//#region src/live.ts
|
|
1618
1741
|
const emptyLiveState = {
|
|
1619
1742
|
cursor: 0,
|
|
1620
|
-
stale: /* @__PURE__ */ new Set(),
|
|
1621
1743
|
boundary: {}
|
|
1622
1744
|
};
|
|
1623
1745
|
/**
|
|
@@ -1651,10 +1773,6 @@ const recordBoundary = (state, connection, position) => {
|
|
|
1651
1773
|
}
|
|
1652
1774
|
};
|
|
1653
1775
|
};
|
|
1654
|
-
const invalidateConnection = (state, connection) => ({
|
|
1655
|
-
...state,
|
|
1656
|
-
stale: /* @__PURE__ */ new Set([...state.stale, connection])
|
|
1657
|
-
});
|
|
1658
1776
|
/**
|
|
1659
1777
|
* A subscriber is woken only if the event changed a field it selects. A
|
|
1660
1778
|
* change to an unselected field is skipped entirely.
|
|
@@ -1707,9 +1825,10 @@ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
|
|
|
1707
1825
|
outcome
|
|
1708
1826
|
};
|
|
1709
1827
|
case "invalidate": return {
|
|
1710
|
-
state:
|
|
1828
|
+
state: advance(state, event.cursor),
|
|
1711
1829
|
optimistic,
|
|
1712
|
-
outcome
|
|
1830
|
+
outcome,
|
|
1831
|
+
invalidated: event.connection
|
|
1713
1832
|
};
|
|
1714
1833
|
case "ignore": return {
|
|
1715
1834
|
state: advance(state, event.cursor),
|
|
@@ -1728,27 +1847,13 @@ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
|
|
|
1728
1847
|
outcome
|
|
1729
1848
|
};
|
|
1730
1849
|
case "ConnectionInvalidate": return {
|
|
1731
|
-
state:
|
|
1850
|
+
state: advance(state, event.cursor),
|
|
1851
|
+
invalidated: event.connection,
|
|
1732
1852
|
optimistic,
|
|
1733
1853
|
outcome
|
|
1734
1854
|
};
|
|
1735
1855
|
}
|
|
1736
1856
|
};
|
|
1737
|
-
const isStale = (state, connection) => state.stale.has(connection);
|
|
1738
|
-
/**
|
|
1739
|
-
* Clears a connection's stale mark once a fresh page has been adopted. An
|
|
1740
|
-
* invalidating event only records that a refetch is due; whoever merges the
|
|
1741
|
-
* refetched page calls this so `isStale` stops reporting.
|
|
1742
|
-
*/
|
|
1743
|
-
const refreshConnection = (state, connection) => {
|
|
1744
|
-
if (!state.stale.has(connection)) return state;
|
|
1745
|
-
const stale = new Set(state.stale);
|
|
1746
|
-
stale.delete(connection);
|
|
1747
|
-
return {
|
|
1748
|
-
...state,
|
|
1749
|
-
stale
|
|
1750
|
-
};
|
|
1751
|
-
};
|
|
1752
1857
|
/** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
|
|
1753
1858
|
const liveHasPrevious = (connection, state, connectionId) => hasPrevious(connection) || (state.boundary[connectionId]?.before ?? 0) > 0;
|
|
1754
1859
|
/** `hasNext` accounting for edges recorded outside the loaded boundary. */
|
|
@@ -1890,17 +1995,33 @@ const gc = (state, roots) => {
|
|
|
1890
1995
|
* application's choice.
|
|
1891
1996
|
*/
|
|
1892
1997
|
/** Bump when the serialized shape changes; a mismatch discards the cache. */
|
|
1893
|
-
const REMOTE_CACHE_VERSION =
|
|
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
|
+
});
|
|
1894
2014
|
const sorted = (values) => [...values].sort();
|
|
1895
2015
|
/**
|
|
1896
2016
|
* The snapshot's data shape. `stableStringify` sorts object keys, and the
|
|
1897
2017
|
* field sets are sorted here, so equal stores give byte-equal snapshots
|
|
1898
2018
|
* whatever order they were built in.
|
|
1899
2019
|
*/
|
|
1900
|
-
const serializeStore = (
|
|
1901
|
-
version:
|
|
2020
|
+
const serializeStore = (snapshot, scope) => ({
|
|
2021
|
+
version: 4,
|
|
1902
2022
|
scope: scope ?? null,
|
|
1903
|
-
|
|
2023
|
+
connections: snapshot.connections,
|
|
2024
|
+
entities: Object.fromEntries(Object.entries(snapshot.entities).map(([key, entry]) => [key, {
|
|
1904
2025
|
values: entry.values,
|
|
1905
2026
|
present: sorted(entry.present),
|
|
1906
2027
|
stale: sorted(entry.stale),
|
|
@@ -1930,9 +2051,30 @@ const parseEntry = (value) => {
|
|
|
1930
2051
|
windows: entry.windows
|
|
1931
2052
|
};
|
|
1932
2053
|
};
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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));
|
|
1936
2078
|
return exceeds(text, options.maxBytes) ? void 0 : text;
|
|
1937
2079
|
}
|
|
1938
2080
|
/**
|
|
@@ -1962,7 +2104,7 @@ const hydrate = (raw, options = {}) => {
|
|
|
1962
2104
|
}
|
|
1963
2105
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
1964
2106
|
const candidate = parsed;
|
|
1965
|
-
if (candidate.version !==
|
|
2107
|
+
if (candidate.version !== 4) return void 0;
|
|
1966
2108
|
if ((candidate.scope ?? null) !== (options.scope ?? null)) return void 0;
|
|
1967
2109
|
if (candidate.entities === null || typeof candidate.entities !== "object") return void 0;
|
|
1968
2110
|
if (Array.isArray(candidate.entities)) return void 0;
|
|
@@ -1985,37 +2127,49 @@ const mergeStores = (current, snapshot, policy) => policy === "replace" ? {
|
|
|
1985
2127
|
...snapshot,
|
|
1986
2128
|
...current
|
|
1987
2129
|
};
|
|
2130
|
+
/** An empty snapshot: the cache as a fresh session has it. */
|
|
2131
|
+
const emptySnapshot = {
|
|
2132
|
+
entities: emptyStore,
|
|
2133
|
+
connections: {}
|
|
2134
|
+
};
|
|
1988
2135
|
const RemotePersistence = {
|
|
1989
2136
|
/** The snapshot text, or `undefined` when it would exceed `maxBytes`. */
|
|
1990
2137
|
dehydrate,
|
|
1991
|
-
/**
|
|
2138
|
+
/** What a snapshot text holds, or `undefined` when it is refused. */
|
|
1992
2139
|
hydrate,
|
|
1993
|
-
/**
|
|
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. */
|
|
1994
2146
|
mergeStores,
|
|
2147
|
+
/** The cache as a fresh session has it; what a refused `restore` yields. */
|
|
2148
|
+
emptySnapshot,
|
|
1995
2149
|
/**
|
|
1996
2150
|
* Writes the snapshot under `key`, or removes the key when the store would
|
|
1997
2151
|
* exceed `maxBytes`, so a stale smaller snapshot cannot outlive a larger
|
|
1998
2152
|
* store it no longer describes.
|
|
1999
2153
|
*/
|
|
2000
|
-
save: (
|
|
2154
|
+
save: (snapshot, options) => Effect.gen(function* () {
|
|
2001
2155
|
const kv = yield* KeyValueStore.KeyValueStore;
|
|
2002
|
-
const text = dehydrate(
|
|
2156
|
+
const text = dehydrate(snapshot, options);
|
|
2003
2157
|
if (text === void 0) yield* kv.remove(options.key);
|
|
2004
2158
|
else yield* kv.set(options.key, text);
|
|
2005
2159
|
}),
|
|
2006
2160
|
/**
|
|
2007
|
-
* Reads the cache. A missing key yields `
|
|
2008
|
-
* refuses yields `
|
|
2009
|
-
* 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.
|
|
2010
2164
|
*/
|
|
2011
2165
|
restore: (options) => Effect.gen(function* () {
|
|
2012
2166
|
const kv = yield* KeyValueStore.KeyValueStore;
|
|
2013
2167
|
const raw = yield* kv.get(options.key);
|
|
2014
|
-
if (raw === void 0) return
|
|
2168
|
+
if (raw === void 0) return emptySnapshot;
|
|
2015
2169
|
const restored = hydrate(raw, options);
|
|
2016
2170
|
if (restored === void 0) {
|
|
2017
2171
|
yield* kv.remove(options.key);
|
|
2018
|
-
return
|
|
2172
|
+
return emptySnapshot;
|
|
2019
2173
|
}
|
|
2020
2174
|
return restored;
|
|
2021
2175
|
})
|
|
@@ -2138,6 +2292,10 @@ const RemoteData = {
|
|
|
2138
2292
|
* optimistic layers an application keeps in its Model, the Messages that
|
|
2139
2293
|
* change it, and the pure reducer over them.
|
|
2140
2294
|
*/
|
|
2295
|
+
const noFailures = {
|
|
2296
|
+
connections: {},
|
|
2297
|
+
fields: {}
|
|
2298
|
+
};
|
|
2141
2299
|
const emptyRefresh = {
|
|
2142
2300
|
generation: 0,
|
|
2143
2301
|
requested: /* @__PURE__ */ new Map(),
|
|
@@ -2151,7 +2309,8 @@ const initialRemoteModel = {
|
|
|
2151
2309
|
mutations: emptyMutationState,
|
|
2152
2310
|
gaps: /* @__PURE__ */ new Set(),
|
|
2153
2311
|
loading: /* @__PURE__ */ new Set(),
|
|
2154
|
-
refresh: emptyRefresh
|
|
2312
|
+
refresh: emptyRefresh,
|
|
2313
|
+
failures: noFailures
|
|
2155
2314
|
};
|
|
2156
2315
|
/** The entity store and the mutation ledger are runtime values, not wire shapes. */
|
|
2157
2316
|
const runtimeSchema = Schema.Unknown;
|
|
@@ -2163,7 +2322,11 @@ const remoteModelSchema = () => Schema.Struct({
|
|
|
2163
2322
|
mutations: runtimeSchema,
|
|
2164
2323
|
gaps: runtimeSchema,
|
|
2165
2324
|
loading: runtimeSchema,
|
|
2166
|
-
refresh: runtimeSchema
|
|
2325
|
+
refresh: runtimeSchema,
|
|
2326
|
+
failures: Schema.Struct({
|
|
2327
|
+
connections: Schema.Record(Schema.String, remoteErrorSchema),
|
|
2328
|
+
fields: Schema.Record(Schema.String, remoteErrorSchema)
|
|
2329
|
+
})
|
|
2167
2330
|
});
|
|
2168
2331
|
const retentionRootsSchema = Schema.Struct({
|
|
2169
2332
|
requirements: Schema.Array(ReadRequest),
|
|
@@ -2184,15 +2347,18 @@ const remoteMessageCases = {
|
|
|
2184
2347
|
},
|
|
2185
2348
|
ReadFailed: {
|
|
2186
2349
|
requests: Schema.Array(ReadRequest),
|
|
2187
|
-
error: remoteErrorSchema
|
|
2350
|
+
error: remoteErrorSchema,
|
|
2351
|
+
stream: Schema.optional(Schema.String)
|
|
2188
2352
|
},
|
|
2189
2353
|
RefreshStarted: { requests: Schema.Array(ReadRequest) },
|
|
2190
2354
|
ReadStarted: {
|
|
2191
2355
|
requests: Schema.Array(ReadRequest),
|
|
2192
2356
|
refresh: Schema.optional(Schema.Number)
|
|
2193
2357
|
},
|
|
2358
|
+
QueryStarted: { connections: Schema.Array(Schema.String) },
|
|
2194
2359
|
RetentionChanged: { roots: retentionRootsSchema },
|
|
2195
2360
|
Hydrated: {
|
|
2361
|
+
connections: Schema.optional(Schema.Unknown),
|
|
2196
2362
|
entities: runtimeSchema,
|
|
2197
2363
|
merge: Schema.Union([Schema.Literal("replace"), Schema.Literal("preserve-existing")])
|
|
2198
2364
|
},
|
|
@@ -2277,6 +2443,25 @@ const withoutLoading = (loading, requests) => {
|
|
|
2277
2443
|
*/
|
|
2278
2444
|
const connectionPrefix = "\0connection\0";
|
|
2279
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
|
+
};
|
|
2280
2465
|
/**
|
|
2281
2466
|
* Drops the refresh marks of entities and connections that were collected.
|
|
2282
2467
|
* Nothing observes a collected entity — the retention roots are the active
|
|
@@ -2391,31 +2576,64 @@ const updateRemote = (model, message) => {
|
|
|
2391
2576
|
case "ReadReceived": return {
|
|
2392
2577
|
...model,
|
|
2393
2578
|
entities: writeRead(model.entities, message.requests, message.result, message.now),
|
|
2394
|
-
loading: withoutLoading(model.loading, message.requests)
|
|
2395
|
-
|
|
2396
|
-
case "ReadFailed": return {
|
|
2397
|
-
...model,
|
|
2398
|
-
entities: setStale(model.entities, marksOf(message.requests), false),
|
|
2399
|
-
loading: withoutLoading(model.loading, message.requests)
|
|
2579
|
+
loading: withoutLoading(model.loading, message.requests),
|
|
2580
|
+
failures: withoutFieldFailures(model.failures, [...fieldMarks(message.requests), ...patchedMarks(message.result.entities)])
|
|
2400
2581
|
};
|
|
2582
|
+
case "ReadFailed":
|
|
2583
|
+
if (message.stream !== void 0) return markGap(model, message.stream);
|
|
2584
|
+
return {
|
|
2585
|
+
...model,
|
|
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
|
+
};
|
|
2401
2590
|
case "RetentionChanged": {
|
|
2402
2591
|
const retained = gc(model, message.roots);
|
|
2403
2592
|
return {
|
|
2404
2593
|
...model,
|
|
2405
2594
|
...retained,
|
|
2406
|
-
refresh: prunedRefresh(model.refresh, retained)
|
|
2595
|
+
refresh: prunedRefresh(model.refresh, retained),
|
|
2596
|
+
failures: prunedFailures(model, message.roots),
|
|
2597
|
+
loading: prunedQueryLoading(model.loading, message.roots)
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
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
|
+
}]);
|
|
2609
|
+
return {
|
|
2610
|
+
...model,
|
|
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
|
|
2618
|
+
}
|
|
2407
2619
|
};
|
|
2408
2620
|
}
|
|
2409
|
-
case "Hydrated": return {
|
|
2410
|
-
...model,
|
|
2411
|
-
entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge)
|
|
2412
|
-
};
|
|
2413
2621
|
case "RefreshStarted": {
|
|
2414
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);
|
|
2415
2623
|
const entities = setStale(asked, marksOf(message.requests), true);
|
|
2416
|
-
|
|
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 : {
|
|
2417
2635
|
...model,
|
|
2418
|
-
|
|
2636
|
+
loading
|
|
2419
2637
|
};
|
|
2420
2638
|
}
|
|
2421
2639
|
case "ReadStarted": return {
|
|
@@ -2434,7 +2652,8 @@ const updateRemote = (model, message) => {
|
|
|
2434
2652
|
...model,
|
|
2435
2653
|
entities: settled.store,
|
|
2436
2654
|
mutations: settled.state,
|
|
2437
|
-
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)))])
|
|
2438
2657
|
};
|
|
2439
2658
|
}
|
|
2440
2659
|
case "OverlayShown": return {
|
|
@@ -2456,18 +2675,26 @@ const updateRemote = (model, message) => {
|
|
|
2456
2675
|
case "LiveReceived": {
|
|
2457
2676
|
const state = model.live[message.stream] ?? emptyLiveState;
|
|
2458
2677
|
if (message.event._tag === "EntityPatched" || message.event._tag === "EntityDeleted") {
|
|
2459
|
-
const
|
|
2460
|
-
|
|
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({
|
|
2461
2686
|
...model,
|
|
2462
2687
|
entities: applied.store,
|
|
2463
2688
|
live: {
|
|
2464
2689
|
...model.live,
|
|
2465
2690
|
[message.stream]: applied.state
|
|
2466
|
-
}
|
|
2691
|
+
},
|
|
2692
|
+
failures: withoutFieldFailures(model.failures, settled)
|
|
2467
2693
|
}, message.stream);
|
|
2468
2694
|
}
|
|
2469
2695
|
const applied = applyConnectionEvent(state, model.optimistic, message.event, message.policy);
|
|
2470
|
-
|
|
2696
|
+
if (applied.outcome === "gap") return markGap(model, message.stream);
|
|
2697
|
+
const next = clearGap({
|
|
2471
2698
|
...model,
|
|
2472
2699
|
optimistic: applied.optimistic,
|
|
2473
2700
|
live: {
|
|
@@ -2475,10 +2702,16 @@ const updateRemote = (model, message) => {
|
|
|
2475
2702
|
[message.stream]: applied.state
|
|
2476
2703
|
}
|
|
2477
2704
|
}, message.stream);
|
|
2705
|
+
return applied.invalidated === void 0 ? next : updateRemote(next, {
|
|
2706
|
+
_tag: "ConnectionInvalidated",
|
|
2707
|
+
connection: applied.invalidated
|
|
2708
|
+
});
|
|
2478
2709
|
}
|
|
2479
2710
|
case "GapCleared": return clearGap(model, message.stream);
|
|
2480
2711
|
case "ConnectionMerged": {
|
|
2481
2712
|
const current = model.connections[message.connection] ?? emptyConnection;
|
|
2713
|
+
const failures = withoutConnectionFailure(model.failures, message.connection);
|
|
2714
|
+
const loading = withoutQueryLoading(model.loading, message.connection);
|
|
2482
2715
|
const replaces = message.refreshes === true && current.stale;
|
|
2483
2716
|
const merged = merge(replaces ? emptyConnection : current, message.page);
|
|
2484
2717
|
return {
|
|
@@ -2490,25 +2723,129 @@ const updateRemote = (model, message) => {
|
|
|
2490
2723
|
stale: false
|
|
2491
2724
|
} : merged
|
|
2492
2725
|
},
|
|
2493
|
-
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
|
|
2494
2729
|
};
|
|
2495
2730
|
}
|
|
2496
2731
|
case "ConnectionInvalidated":
|
|
2497
|
-
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;
|
|
2498
2733
|
return {
|
|
2499
2734
|
...model,
|
|
2500
|
-
connections: setConnectionStale(model.connections, message.connection, true)
|
|
2735
|
+
connections: setConnectionStale(model.connections, message.connection, true),
|
|
2736
|
+
failures: withoutConnectionFailure(model.failures, message.connection)
|
|
2501
2737
|
};
|
|
2502
|
-
case "QueryFailed": return
|
|
2738
|
+
case "QueryFailed": return {
|
|
2503
2739
|
...model,
|
|
2504
|
-
connections: setConnectionStale(model.connections, message.connection, false)
|
|
2505
|
-
|
|
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
|
+
};
|
|
2506
2750
|
case "ConnectionRefreshed": return {
|
|
2507
2751
|
...model,
|
|
2508
2752
|
connections: setConnectionStale(model.connections, message.connection, false)
|
|
2509
2753
|
};
|
|
2510
2754
|
}
|
|
2511
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
|
+
};
|
|
2512
2849
|
/** Appends (after) or prepends (before) an incoming page onto the stored one. */
|
|
2513
2850
|
const mergeWireRefPages = (current, next, direction) => {
|
|
2514
2851
|
const refs = direction === "after" ? [.../* @__PURE__ */ new Set([...current.refs, ...next.refs])] : [.../* @__PURE__ */ new Set([...next.refs, ...current.refs])];
|
|
@@ -2744,6 +3081,10 @@ const askedOf = (projection) => projection === void 0 ? nothingAsked : {
|
|
|
2744
3081
|
* fresh contributes its visible items' selected fields to the entity plan; one
|
|
2745
3082
|
* it does not hold, or holds stale, is a query to run (its items are planned
|
|
2746
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.
|
|
2747
3088
|
*/
|
|
2748
3089
|
const planAsked = (remote, asked, options) => {
|
|
2749
3090
|
const queries = [];
|
|
@@ -2751,18 +3092,46 @@ const planAsked = (remote, asked, options) => {
|
|
|
2751
3092
|
const connections = Requirement.mergeConnections(asked.connections);
|
|
2752
3093
|
for (const connection of connections) {
|
|
2753
3094
|
const known = remote.connections[connection.identity];
|
|
2754
|
-
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)) {
|
|
2755
3096
|
queries.push(connection);
|
|
2756
3097
|
continue;
|
|
2757
3098
|
}
|
|
3099
|
+
if (known === void 0) continue;
|
|
2758
3100
|
const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
|
|
2759
3101
|
items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
|
|
2760
3102
|
}
|
|
3103
|
+
const planned = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], options);
|
|
2761
3104
|
return {
|
|
2762
|
-
requirements:
|
|
3105
|
+
requirements: options.force === true ? planned : withoutFailedFields(remote, planned),
|
|
2763
3106
|
queries
|
|
2764
3107
|
};
|
|
2765
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
|
+
};
|
|
2766
3135
|
/** The entity requirements a page's edges add under a selection. */
|
|
2767
3136
|
const itemsOf = (edges, select) => edges.filter((edge) => edge.entity === select.entity).map((edge) => ({
|
|
2768
3137
|
...select,
|
|
@@ -2790,22 +3159,52 @@ const queryRequestOf = (query) => {
|
|
|
2790
3159
|
* `refreshes` marks the page as answering the connection's refresh, so one
|
|
2791
3160
|
* Message both merges it and clears `stale`.
|
|
2792
3161
|
*/
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
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
|
+
};
|
|
2809
3208
|
/**
|
|
2810
3209
|
* The retention roots of some projections: their requirements, their
|
|
2811
3210
|
* connections (each with the union of what the projections select of its
|
|
@@ -2853,7 +3252,7 @@ const queryMessage = (query) => Effect.gen(function* () {
|
|
|
2853
3252
|
_tag: "QueryFailed",
|
|
2854
3253
|
connection: query.identity,
|
|
2855
3254
|
error: remoteError(result.failure)
|
|
2856
|
-
} : pageMessage(query.identity, result.success, true);
|
|
3255
|
+
} : pageMessage(query.identity, result.success, true, query.window);
|
|
2857
3256
|
});
|
|
2858
3257
|
/** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
|
|
2859
3258
|
const PlannedQuery = Schema.Struct({
|
|
@@ -2893,14 +3292,17 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
2893
3292
|
}))
|
|
2894
3293
|
};
|
|
2895
3294
|
},
|
|
2896
|
-
dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(requirements.length === 0 ?
|
|
3295
|
+
dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
|
|
2897
3296
|
_tag: "ReadStarted",
|
|
2898
3297
|
requests: requirements,
|
|
2899
3298
|
refresh
|
|
2900
3299
|
}), ...RemotePolicy.refreshes(policy) ? [toMessage({
|
|
2901
3300
|
_tag: "RefreshStarted",
|
|
2902
3301
|
requests: requirements
|
|
2903
|
-
})] : []]
|
|
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" }))
|
|
2904
3306
|
};
|
|
2905
3307
|
};
|
|
2906
3308
|
/** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
|
|
@@ -2930,7 +3332,8 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
|
2930
3332
|
})), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
|
|
2931
3333
|
_tag: "ReadFailed",
|
|
2932
3334
|
requests: requirements,
|
|
2933
|
-
error: remoteError(error)
|
|
3335
|
+
error: remoteError(error),
|
|
3336
|
+
stream: liveStreamKey(requirements)
|
|
2934
3337
|
}))))
|
|
2935
3338
|
});
|
|
2936
3339
|
/** The projection an active Surface has for this Model, if it is active. */
|
|
@@ -3023,8 +3426,18 @@ const Remote = {
|
|
|
3023
3426
|
value: decoded.success
|
|
3024
3427
|
};
|
|
3025
3428
|
});
|
|
3026
|
-
|
|
3027
|
-
|
|
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
|
+
};
|
|
3028
3441
|
}
|
|
3029
3442
|
});
|
|
3030
3443
|
},
|
|
@@ -3083,6 +3496,9 @@ const Remote = {
|
|
|
3083
3496
|
* again with `next`/`previous`. The read entries restart, so a read or query
|
|
3084
3497
|
* sent before the refresh is not applied after it.
|
|
3085
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
|
+
*
|
|
3086
3502
|
* The projection must be observed, which it is while it is on screen. Live
|
|
3087
3503
|
* subscriptions are left as they are. For data nothing observes (SSR, tests),
|
|
3088
3504
|
* use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
|
|
@@ -3098,7 +3514,7 @@ const Remote = {
|
|
|
3098
3514
|
return known === void 0 ? [] : itemsOf(visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities).map((edge) => edge.ref), connection.select);
|
|
3099
3515
|
});
|
|
3100
3516
|
const requirements = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], { force: true });
|
|
3101
|
-
const invalidated = connections.filter((connection) => remote.connections[connection.identity] !== void 0).map((connection) => connection.identity);
|
|
3517
|
+
const invalidated = connections.filter((connection) => remote.connections[connection.identity] !== void 0 || connection.identity in remote.failures.connections).map((connection) => connection.identity);
|
|
3102
3518
|
const marked = [...requirements.length === 0 ? [] : [{
|
|
3103
3519
|
_tag: "RefreshStarted",
|
|
3104
3520
|
requests: requirements
|
|
@@ -3175,7 +3591,7 @@ const Remote = {
|
|
|
3175
3591
|
});
|
|
3176
3592
|
}),
|
|
3177
3593
|
/** The `RemoteMessage` that merges a query page into its connection. */
|
|
3178
|
-
queryMessage: (ref, result) => pageMessage(ref.identity, result),
|
|
3594
|
+
queryMessage: (ref, result) => pageMessage(ref.identity, result, false, ref.window),
|
|
3179
3595
|
/**
|
|
3180
3596
|
* The edges a connection shows: its server-known region with pending and
|
|
3181
3597
|
* confirmed overlays placed around it, minus removed edges and edges whose
|
|
@@ -3232,8 +3648,10 @@ const Remote = {
|
|
|
3232
3648
|
observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => askedOf(surface.projection(params)), toMessage, options),
|
|
3233
3649
|
/**
|
|
3234
3650
|
* A Foldkit Subscription entry that consumes the live stream for a Surface's
|
|
3235
|
-
* requirements, emitting a `LiveReceived` per event and a `ReadFailed`
|
|
3236
|
-
*
|
|
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
|
|
3237
3655
|
* from `RemoteModel.live`, so the application tracks no cursor of its own.
|
|
3238
3656
|
*/
|
|
3239
3657
|
live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
|
|
@@ -3242,7 +3660,48 @@ const brandEntries = (entries) => Subscription.make()(() => entries);
|
|
|
3242
3660
|
/** The bound domain: the descriptor, the binding, and the operations over them. */
|
|
3243
3661
|
const bindDomain = (definition, store) => {
|
|
3244
3662
|
const bound = bindRemote(definition, store);
|
|
3245
|
-
|
|
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
|
+
};
|
|
3246
3705
|
const domain = {
|
|
3247
3706
|
...definition,
|
|
3248
3707
|
...bound,
|
|
@@ -3291,7 +3750,7 @@ const bindDomain = (definition, store) => {
|
|
|
3291
3750
|
let current = model;
|
|
3292
3751
|
for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
|
|
3293
3752
|
const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
|
|
3294
|
-
current = reduce(current, pageMessage(query.identity, page, true));
|
|
3753
|
+
current = reduce(current, pageMessage(query.identity, page, true, query.window));
|
|
3295
3754
|
}
|
|
3296
3755
|
const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
|
|
3297
3756
|
if (requirements.length === 0) return current;
|
|
@@ -3321,6 +3780,58 @@ const bindDomain = (definition, store) => {
|
|
|
3321
3780
|
};
|
|
3322
3781
|
const relation = relationOf(select);
|
|
3323
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
|
+
};
|
|
3324
3835
|
const requirement = {
|
|
3325
3836
|
identity: ref.identity,
|
|
3326
3837
|
window: ref.window,
|
|
@@ -3332,42 +3843,41 @@ const bindDomain = (definition, store) => {
|
|
|
3332
3843
|
dependencies: [],
|
|
3333
3844
|
metadata: RemoteConnections.of(requirement),
|
|
3334
3845
|
ref,
|
|
3846
|
+
selection: select,
|
|
3335
3847
|
read: (root) => {
|
|
3336
3848
|
const remote = store.get(root);
|
|
3337
3849
|
const connection = remote.connections[ref.identity];
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
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
|
|
3856
|
+
};
|
|
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
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
switch (read._tag) {
|
|
3869
|
+
case "Ready":
|
|
3870
|
+
case "Refreshing": return {
|
|
3871
|
+
_tag: "Failed",
|
|
3872
|
+
error: failure,
|
|
3873
|
+
previous: read.value
|
|
3362
3874
|
};
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
_tag: "Ready",
|
|
3368
|
-
value: page
|
|
3875
|
+
case "Failed": return read;
|
|
3876
|
+
default: return {
|
|
3877
|
+
_tag: "Failed",
|
|
3878
|
+
error: failure
|
|
3369
3879
|
};
|
|
3370
|
-
}
|
|
3880
|
+
}
|
|
3371
3881
|
}
|
|
3372
3882
|
};
|
|
3373
3883
|
},
|
|
@@ -3392,6 +3902,65 @@ const bindDomain = (definition, store) => {
|
|
|
3392
3902
|
}
|
|
3393
3903
|
} : void 0;
|
|
3394
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
|
+
},
|
|
3395
3964
|
refresh: (model, target) => Remote.refresh(bound, model, target),
|
|
3396
3965
|
overlay: (model, id, optimistic) => bound.store.set(model, updateRemote(bound.store.get(model), {
|
|
3397
3966
|
_tag: "OverlayShown",
|
|
@@ -3418,7 +3987,7 @@ const bindDomain = (definition, store) => {
|
|
|
3418
3987
|
connection: ref.identity,
|
|
3419
3988
|
error: remoteError(error)
|
|
3420
3989
|
}),
|
|
3421
|
-
onSuccess: (page) => pageMessage(ref.identity, page)
|
|
3990
|
+
onSuccess: (page) => pageMessage(ref.identity, page, false, ref.window)
|
|
3422
3991
|
}))
|
|
3423
3992
|
}),
|
|
3424
3993
|
mutate: (model, mutation, input, options = {}) => {
|
|
@@ -3485,6 +4054,6 @@ const pageSize = (window, key) => {
|
|
|
3485
4054
|
return size === void 0 ? {} : { [key]: size };
|
|
3486
4055
|
};
|
|
3487
4056
|
//#endregion
|
|
3488
|
-
export { ConnectionChange, ConnectionChangeSchema, Entity, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote,
|
|
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 };
|
|
3489
4058
|
|
|
3490
4059
|
//# sourceMappingURL=index.mjs.map
|