foldkit-remote 0.1.0 → 0.3.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 +805 -421
- package/dist/index.d.mts +251 -116
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +300 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -2
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,110 @@
|
|
|
1
1
|
import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { Metadata } from "foldkit-surface";
|
|
3
3
|
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
4
4
|
import { KeyValueStore } from "effect/unstable/persistence";
|
|
5
|
+
//#region src/requirement.ts
|
|
6
|
+
/**
|
|
7
|
+
* What a Remote Projection needs from the server: entity slices and query
|
|
8
|
+
* connections. They ride on `Projection.metadata` under Remote's own keys, so
|
|
9
|
+
* Surface composes them without knowing their shape.
|
|
10
|
+
*/
|
|
11
|
+
const windowKey$1 = (window) => JSON.stringify([
|
|
12
|
+
window.first ?? null,
|
|
13
|
+
window.last ?? null,
|
|
14
|
+
window.after ?? null,
|
|
15
|
+
window.before ?? null
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Merges connection requirements for the same connection and window into one,
|
|
19
|
+
* unioning what they select of each item; the first keeps its other properties.
|
|
20
|
+
*/
|
|
21
|
+
function mergeConnections(connections) {
|
|
22
|
+
const merged = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const connection of connections) {
|
|
24
|
+
const key = `${connection.identity}\u0000${windowKey$1(connection.window)}`;
|
|
25
|
+
const current = merged.get(key);
|
|
26
|
+
merged.set(key, current === void 0 ? connection : {
|
|
27
|
+
...current,
|
|
28
|
+
select: mergeRelation(current.select, connection.select)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return [...merged.values()];
|
|
32
|
+
}
|
|
33
|
+
/** Unions two relation slices for the same target: fields, windows, and nested relations. */
|
|
34
|
+
function mergeRelation(current, next) {
|
|
35
|
+
const fields = [...current.fields];
|
|
36
|
+
const seen = new Set(fields);
|
|
37
|
+
for (const field of next.fields) {
|
|
38
|
+
if (seen.has(field)) continue;
|
|
39
|
+
seen.add(field);
|
|
40
|
+
fields.push(field);
|
|
41
|
+
}
|
|
42
|
+
const windows = {
|
|
43
|
+
...current.windows,
|
|
44
|
+
...next.windows
|
|
45
|
+
};
|
|
46
|
+
const relations = mergeRelations(current.relations, next.relations);
|
|
47
|
+
return {
|
|
48
|
+
entity: current.entity,
|
|
49
|
+
fields,
|
|
50
|
+
...Object.keys(windows).length === 0 ? {} : { windows },
|
|
51
|
+
...relations === void 0 ? {} : { relations }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Unions two relation maps field by field; a field in both merges recursively. */
|
|
55
|
+
function mergeRelations(current, next) {
|
|
56
|
+
if (current === void 0) return next;
|
|
57
|
+
if (next === void 0) return current;
|
|
58
|
+
const merged = { ...current };
|
|
59
|
+
for (const [field, relation] of Object.entries(next)) {
|
|
60
|
+
const existing = merged[field];
|
|
61
|
+
merged[field] = existing === void 0 ? relation : mergeRelation(existing, relation);
|
|
62
|
+
}
|
|
63
|
+
return merged;
|
|
64
|
+
}
|
|
65
|
+
/** Unions requirements for the same entity + id, dropping duplicate fields. */
|
|
66
|
+
function mergeRequirements(requirements) {
|
|
67
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const requirement of requirements) {
|
|
69
|
+
const key = `${requirement.entity}\u0000${requirement.id}`;
|
|
70
|
+
const group = grouped.get(key);
|
|
71
|
+
const live = group?.live === true || requirement.live === true;
|
|
72
|
+
grouped.set(key, {
|
|
73
|
+
...mergeRelation(group ?? {
|
|
74
|
+
entity: requirement.entity,
|
|
75
|
+
fields: []
|
|
76
|
+
}, requirement),
|
|
77
|
+
id: requirement.id,
|
|
78
|
+
...live ? { live } : {}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return [...grouped.values()];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Requirement helpers, for the packages that plan and serve requirements.
|
|
85
|
+
* `merge` unions same-entity+id requirements; `mergeRelation` unions two
|
|
86
|
+
* slices of one target (fields, windows, nested relations).
|
|
87
|
+
*/
|
|
88
|
+
const Requirement = {
|
|
89
|
+
merge: mergeRequirements,
|
|
90
|
+
mergeRelation,
|
|
91
|
+
mergeConnections
|
|
92
|
+
};
|
|
93
|
+
/** Remote's entity requirements on a Projection, unioned per entity and id as nodes compose. */
|
|
94
|
+
const RemoteRequirements = Metadata.key("remote", {
|
|
95
|
+
merge: mergeRequirements,
|
|
96
|
+
summarize: (requirement) => `${requirement.entity}:${requirement.id}`
|
|
97
|
+
});
|
|
98
|
+
/** Remote's query connections on a Projection, unioned per connection and window. */
|
|
99
|
+
const RemoteConnections = Metadata.key("remote.connection", {
|
|
100
|
+
merge: mergeConnections,
|
|
101
|
+
summarize: (connection) => connection.identity.split("\0").join(" ")
|
|
102
|
+
});
|
|
103
|
+
/** The entity requirements a Projection carries. */
|
|
104
|
+
const requirementsOf = (projection) => RemoteRequirements.get(projection.metadata);
|
|
105
|
+
/** The query connections a Projection carries. */
|
|
106
|
+
const connectionsOf = (projection) => RemoteConnections.get(projection.metadata);
|
|
107
|
+
//#endregion
|
|
5
108
|
//#region src/store.ts
|
|
6
109
|
/**
|
|
7
110
|
* The pure entity store: values, per-field presence, staleness, and tombstones.
|
|
@@ -80,6 +183,7 @@ const setStale = (store, marks, stale) => {
|
|
|
80
183
|
for (const field of fields) if (stale) {
|
|
81
184
|
if (previous.present.has(field)) staleFields.add(field);
|
|
82
185
|
} else staleFields.delete(field);
|
|
186
|
+
if (staleFields.size === previous.stale.size) continue;
|
|
83
187
|
next ??= { ...store };
|
|
84
188
|
next[key] = {
|
|
85
189
|
...previous,
|
|
@@ -1463,14 +1567,10 @@ const parseEntry = (value) => {
|
|
|
1463
1567
|
};
|
|
1464
1568
|
};
|
|
1465
1569
|
const deserializeStore = (serialized) => Object.fromEntries(Object.entries(serialized.entities).map(([key, entry]) => [key, parseEntry(entry)]));
|
|
1466
|
-
|
|
1467
|
-
* The snapshot text, or `undefined` when it would exceed `maxBytes`. Only the
|
|
1468
|
-
* entity store goes in; pass `model.entities`, never the whole `RemoteModel`.
|
|
1469
|
-
*/
|
|
1470
|
-
const dehydrate = (store, options = {}) => {
|
|
1570
|
+
function dehydrate(store, options = {}) {
|
|
1471
1571
|
const text = stableStringify(serializeStore(store, options.scope));
|
|
1472
1572
|
return exceeds(text, options.maxBytes) ? void 0 : text;
|
|
1473
|
-
}
|
|
1573
|
+
}
|
|
1474
1574
|
/**
|
|
1475
1575
|
* Whether the UTF-8 size of `text` exceeds `maxBytes`. A character is one to
|
|
1476
1576
|
* three bytes, so most texts are decided from their length alone; only the
|
|
@@ -1641,7 +1741,12 @@ const initialRemoteModel = {
|
|
|
1641
1741
|
optimistic: emptyOptimistic,
|
|
1642
1742
|
live: {},
|
|
1643
1743
|
mutations: emptyMutationState,
|
|
1644
|
-
gaps: /* @__PURE__ */ new Set()
|
|
1744
|
+
gaps: /* @__PURE__ */ new Set(),
|
|
1745
|
+
loading: /* @__PURE__ */ new Set(),
|
|
1746
|
+
refresh: {
|
|
1747
|
+
requested: 0,
|
|
1748
|
+
started: 0
|
|
1749
|
+
}
|
|
1645
1750
|
};
|
|
1646
1751
|
/** The entity store and the mutation ledger are runtime values, not wire shapes. */
|
|
1647
1752
|
const runtimeSchema = Schema.Unknown;
|
|
@@ -1651,7 +1756,12 @@ const remoteModelSchema = () => Schema.Struct({
|
|
|
1651
1756
|
optimistic: runtimeSchema,
|
|
1652
1757
|
live: Schema.Record(Schema.String, runtimeSchema),
|
|
1653
1758
|
mutations: runtimeSchema,
|
|
1654
|
-
gaps: runtimeSchema
|
|
1759
|
+
gaps: runtimeSchema,
|
|
1760
|
+
loading: runtimeSchema,
|
|
1761
|
+
refresh: Schema.Struct({
|
|
1762
|
+
requested: Schema.Number,
|
|
1763
|
+
started: Schema.Number
|
|
1764
|
+
})
|
|
1655
1765
|
});
|
|
1656
1766
|
const retentionRootsSchema = Schema.Struct({
|
|
1657
1767
|
requirements: Schema.Array(ReadRequest),
|
|
@@ -1675,6 +1785,10 @@ const remoteMessageCases = {
|
|
|
1675
1785
|
error: remoteErrorSchema
|
|
1676
1786
|
},
|
|
1677
1787
|
RefreshStarted: { requests: Schema.Array(ReadRequest) },
|
|
1788
|
+
ReadStarted: {
|
|
1789
|
+
requests: Schema.Array(ReadRequest),
|
|
1790
|
+
refresh: Schema.optional(Schema.Number)
|
|
1791
|
+
},
|
|
1678
1792
|
RetentionChanged: { roots: retentionRootsSchema },
|
|
1679
1793
|
Hydrated: {
|
|
1680
1794
|
entities: runtimeSchema,
|
|
@@ -1722,6 +1836,39 @@ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(
|
|
|
1722
1836
|
...fields
|
|
1723
1837
|
})));
|
|
1724
1838
|
const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
|
|
1839
|
+
/**
|
|
1840
|
+
* One mark per requested field. A read is identified by what it asks for, not by
|
|
1841
|
+
* a request id: two reads asking for one field share its mark, so the first
|
|
1842
|
+
* answer clears it. Over-clearing shows `Initial` rather than a spinner, which
|
|
1843
|
+
* is the safer way to be wrong.
|
|
1844
|
+
*/
|
|
1845
|
+
const loadingMarks = function* (requests) {
|
|
1846
|
+
for (const request of requests) {
|
|
1847
|
+
const key = entityKey(request.entity, request.id);
|
|
1848
|
+
for (const field of request.fields) yield `${key}\u0000${field}`;
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
const withLoading = (loading, requests) => {
|
|
1852
|
+
const next = new Set(loading);
|
|
1853
|
+
for (const mark of loadingMarks(requests)) next.add(mark);
|
|
1854
|
+
return next;
|
|
1855
|
+
};
|
|
1856
|
+
const withoutLoading = (loading, requests) => {
|
|
1857
|
+
if (loading.size === 0) return loading;
|
|
1858
|
+
const next = new Set(loading);
|
|
1859
|
+
for (const mark of loadingMarks(requests)) next.delete(mark);
|
|
1860
|
+
return next;
|
|
1861
|
+
};
|
|
1862
|
+
/**
|
|
1863
|
+
* Whether a read is fetching any of `fields` for this entity. Consulted only
|
|
1864
|
+
* when the store lacks the value, which is what separates `Loading` from
|
|
1865
|
+
* `Initial`.
|
|
1866
|
+
*/
|
|
1867
|
+
const isLoading = (model, entity, id, fields) => {
|
|
1868
|
+
if (model.loading.size === 0) return false;
|
|
1869
|
+
const key = entityKey(entity, id);
|
|
1870
|
+
return fields.some((field) => model.loading.has(`${key}\u0000${field}`));
|
|
1871
|
+
};
|
|
1725
1872
|
const setConnectionStale = (connections, connection, stale) => {
|
|
1726
1873
|
const current = connections[connection] ?? emptyConnection;
|
|
1727
1874
|
return {
|
|
@@ -1749,11 +1896,13 @@ const updateRemote = (model, message) => {
|
|
|
1749
1896
|
switch (message._tag) {
|
|
1750
1897
|
case "ReadReceived": return {
|
|
1751
1898
|
...model,
|
|
1752
|
-
entities: writeRead(model.entities, message.requests, message.result, message.now)
|
|
1899
|
+
entities: writeRead(model.entities, message.requests, message.result, message.now),
|
|
1900
|
+
loading: withoutLoading(model.loading, message.requests)
|
|
1753
1901
|
};
|
|
1754
1902
|
case "ReadFailed": return {
|
|
1755
1903
|
...model,
|
|
1756
|
-
entities: setStale(model.entities, marksOf(message.requests), false)
|
|
1904
|
+
entities: setStale(model.entities, marksOf(message.requests), false),
|
|
1905
|
+
loading: withoutLoading(model.loading, message.requests)
|
|
1757
1906
|
};
|
|
1758
1907
|
case "RetentionChanged": return {
|
|
1759
1908
|
...model,
|
|
@@ -1763,10 +1912,24 @@ const updateRemote = (model, message) => {
|
|
|
1763
1912
|
...model,
|
|
1764
1913
|
entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge)
|
|
1765
1914
|
};
|
|
1766
|
-
case "RefreshStarted":
|
|
1767
|
-
|
|
1768
|
-
entities
|
|
1769
|
-
|
|
1915
|
+
case "RefreshStarted": {
|
|
1916
|
+
const entities = setStale(model.entities, marksOf(message.requests), true);
|
|
1917
|
+
return entities === model.entities ? model : {
|
|
1918
|
+
...model,
|
|
1919
|
+
entities
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
case "ReadStarted": {
|
|
1923
|
+
const started = Math.max(model.refresh.started, message.refresh ?? 0);
|
|
1924
|
+
return {
|
|
1925
|
+
...model,
|
|
1926
|
+
loading: withLoading(model.loading, message.requests),
|
|
1927
|
+
refresh: started === model.refresh.started ? model.refresh : {
|
|
1928
|
+
...model.refresh,
|
|
1929
|
+
started
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1770
1933
|
case "MutationStarted": return {
|
|
1771
1934
|
...model,
|
|
1772
1935
|
mutations: beginMutation(model.mutations, message.requestId),
|
|
@@ -1812,7 +1975,8 @@ const updateRemote = (model, message) => {
|
|
|
1812
1975
|
case "GapCleared": return clearGap(model, message.stream);
|
|
1813
1976
|
case "ConnectionMerged": {
|
|
1814
1977
|
const current = model.connections[message.connection] ?? emptyConnection;
|
|
1815
|
-
const
|
|
1978
|
+
const replaces = message.refreshes === true && current.stale;
|
|
1979
|
+
const merged = merge(replaces ? emptyConnection : current, message.page);
|
|
1816
1980
|
return {
|
|
1817
1981
|
...model,
|
|
1818
1982
|
connections: {
|
|
@@ -1825,10 +1989,12 @@ const updateRemote = (model, message) => {
|
|
|
1825
1989
|
optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending)
|
|
1826
1990
|
};
|
|
1827
1991
|
}
|
|
1828
|
-
case "ConnectionInvalidated":
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1992
|
+
case "ConnectionInvalidated":
|
|
1993
|
+
if (model.connections[message.connection]?.stale === true) return model;
|
|
1994
|
+
return {
|
|
1995
|
+
...model,
|
|
1996
|
+
connections: setConnectionStale(model.connections, message.connection, true)
|
|
1997
|
+
};
|
|
1832
1998
|
case "QueryFailed": return message.connection in model.connections ? {
|
|
1833
1999
|
...model,
|
|
1834
2000
|
connections: setConnectionStale(model.connections, message.connection, false)
|
|
@@ -2246,7 +2412,7 @@ const bindRemote = (definition, store) => ({
|
|
|
2246
2412
|
owns: store.dependency.length === 0 ? [] : [store.dependency],
|
|
2247
2413
|
observes: store.dependency.length === 0 ? [] : [store.dependency],
|
|
2248
2414
|
messages: [],
|
|
2249
|
-
|
|
2415
|
+
metadata: []
|
|
2250
2416
|
}
|
|
2251
2417
|
});
|
|
2252
2418
|
/** The runtime half of `Registered`: a descriptor the domain never declared is an error naming both. */
|
|
@@ -2257,7 +2423,10 @@ const nothingAsked = {
|
|
|
2257
2423
|
requirements: [],
|
|
2258
2424
|
connections: []
|
|
2259
2425
|
};
|
|
2260
|
-
const askedOf = (projection) => projection === void 0 ? nothingAsked :
|
|
2426
|
+
const askedOf = (projection) => projection === void 0 ? nothingAsked : {
|
|
2427
|
+
requirements: requirementsOf(projection),
|
|
2428
|
+
connections: connectionsOf(projection)
|
|
2429
|
+
};
|
|
2261
2430
|
/**
|
|
2262
2431
|
* Plans what a projection asks against the Model. A connection the Model holds
|
|
2263
2432
|
* fresh contributes its visible items' selected fields to the entity plan; one
|
|
@@ -2333,7 +2502,7 @@ const pageMessage = (connection, result, refreshes = false) => ({
|
|
|
2333
2502
|
const rootsOf = (projections, options) => {
|
|
2334
2503
|
const connections = /* @__PURE__ */ new Map();
|
|
2335
2504
|
for (const identity of (options.connections ?? []).map(connectionIdentity)) connections.set(identity, { identity });
|
|
2336
|
-
for (const { identity, select } of projections.flatMap(
|
|
2505
|
+
for (const { identity, select } of projections.flatMap(connectionsOf)) {
|
|
2337
2506
|
const current = connections.get(identity)?.select;
|
|
2338
2507
|
connections.set(identity, {
|
|
2339
2508
|
identity,
|
|
@@ -2341,10 +2510,39 @@ const rootsOf = (projections, options) => {
|
|
|
2341
2510
|
});
|
|
2342
2511
|
}
|
|
2343
2512
|
return {
|
|
2344
|
-
requirements: Requirement.merge(projections.flatMap(
|
|
2513
|
+
requirements: Requirement.merge(projections.flatMap(requirementsOf)),
|
|
2345
2514
|
connections: [...connections.values()].sort((a, b) => a.identity < b.identity ? -1 : 1)
|
|
2346
2515
|
};
|
|
2347
2516
|
};
|
|
2517
|
+
/** Reads requirements and reports the Message that settles them. Never fails. */
|
|
2518
|
+
const readMessage = (requirements, now) => Effect.gen(function* () {
|
|
2519
|
+
const client = yield* RemoteClient;
|
|
2520
|
+
const at = now();
|
|
2521
|
+
const result = yield* Effect.result(client.read({
|
|
2522
|
+
version: 3,
|
|
2523
|
+
requests: requirements
|
|
2524
|
+
}));
|
|
2525
|
+
return Result.isFailure(result) ? {
|
|
2526
|
+
_tag: "ReadFailed",
|
|
2527
|
+
requests: requirements,
|
|
2528
|
+
error: remoteError(result.failure)
|
|
2529
|
+
} : {
|
|
2530
|
+
_tag: "ReadReceived",
|
|
2531
|
+
requests: requirements,
|
|
2532
|
+
result: result.success,
|
|
2533
|
+
now: at
|
|
2534
|
+
};
|
|
2535
|
+
});
|
|
2536
|
+
/** Runs a query and reports the page that refreshes its connection, or the failure. Never fails. */
|
|
2537
|
+
const queryMessage = (query) => Effect.gen(function* () {
|
|
2538
|
+
const client = yield* RemoteClient;
|
|
2539
|
+
const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
|
|
2540
|
+
return Result.isFailure(result) ? {
|
|
2541
|
+
_tag: "QueryFailed",
|
|
2542
|
+
connection: query.identity,
|
|
2543
|
+
error: remoteError(result.failure)
|
|
2544
|
+
} : pageMessage(query.identity, result.success, true);
|
|
2545
|
+
});
|
|
2348
2546
|
/** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
|
|
2349
2547
|
const PlannedQuery = Schema.Struct({
|
|
2350
2548
|
identity: Schema.String,
|
|
@@ -2362,41 +2560,19 @@ const PlannedQuery = Schema.Struct({
|
|
|
2362
2560
|
*/
|
|
2363
2561
|
const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
2364
2562
|
const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
|
|
2365
|
-
const read = (requirements) => Effect.
|
|
2366
|
-
|
|
2367
|
-
const at = now();
|
|
2368
|
-
const result = yield* Effect.result(client.read({
|
|
2369
|
-
version: 3,
|
|
2370
|
-
requests: requirements
|
|
2371
|
-
}));
|
|
2372
|
-
return Result.isFailure(result) ? toMessage({
|
|
2373
|
-
_tag: "ReadFailed",
|
|
2374
|
-
requests: requirements,
|
|
2375
|
-
error: remoteError(result.failure)
|
|
2376
|
-
}) : toMessage({
|
|
2377
|
-
_tag: "ReadReceived",
|
|
2378
|
-
requests: requirements,
|
|
2379
|
-
result: result.success,
|
|
2380
|
-
now: at
|
|
2381
|
-
});
|
|
2382
|
-
});
|
|
2383
|
-
const run = (query) => Effect.gen(function* () {
|
|
2384
|
-
const client = yield* RemoteClient;
|
|
2385
|
-
const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
|
|
2386
|
-
return Result.isFailure(result) ? toMessage({
|
|
2387
|
-
_tag: "QueryFailed",
|
|
2388
|
-
connection: query.identity,
|
|
2389
|
-
error: remoteError(result.failure)
|
|
2390
|
-
}) : toMessage(pageMessage(query.identity, result.success, true));
|
|
2391
|
-
});
|
|
2563
|
+
const read = (requirements) => Effect.map(readMessage(requirements, now), toMessage);
|
|
2564
|
+
const run = (query) => Effect.map(queryMessage(query), toMessage);
|
|
2392
2565
|
return {
|
|
2393
2566
|
dependenciesSchema: Schema.Struct({
|
|
2394
2567
|
requirements: Schema.Array(ReadRequest),
|
|
2395
|
-
queries: Schema.Array(PlannedQuery)
|
|
2568
|
+
queries: Schema.Array(PlannedQuery),
|
|
2569
|
+
refresh: Schema.Number
|
|
2396
2570
|
}),
|
|
2397
2571
|
modelToDependencies: (model) => {
|
|
2398
|
-
const
|
|
2572
|
+
const remote = bound.store.get(model);
|
|
2573
|
+
const planned = planAsked(remote, askedOf(model), RemotePolicy.toPlan(policy, now()));
|
|
2399
2574
|
return {
|
|
2575
|
+
refresh: remote.refresh.requested,
|
|
2400
2576
|
requirements: planned.requirements,
|
|
2401
2577
|
queries: planned.queries.map(({ identity, window, select }) => ({
|
|
2402
2578
|
identity,
|
|
@@ -2405,10 +2581,14 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
2405
2581
|
}))
|
|
2406
2582
|
};
|
|
2407
2583
|
},
|
|
2408
|
-
dependenciesToStream: ({ requirements, queries }) => Stream.concat(
|
|
2584
|
+
dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(requirements.length === 0 ? Stream.empty : Stream.fromIterable([toMessage({
|
|
2585
|
+
_tag: "ReadStarted",
|
|
2586
|
+
requests: requirements,
|
|
2587
|
+
refresh
|
|
2588
|
+
}), ...RemotePolicy.refreshes(policy) ? [toMessage({
|
|
2409
2589
|
_tag: "RefreshStarted",
|
|
2410
2590
|
requests: requirements
|
|
2411
|
-
})
|
|
2591
|
+
})] : []]), Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }))
|
|
2412
2592
|
};
|
|
2413
2593
|
};
|
|
2414
2594
|
/** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
|
|
@@ -2504,18 +2684,17 @@ const Remote = {
|
|
|
2504
2684
|
return (id) => ({
|
|
2505
2685
|
Model: remoteDataSchema(selection.schema),
|
|
2506
2686
|
dependencies: [],
|
|
2507
|
-
|
|
2687
|
+
metadata: RemoteRequirements.of({
|
|
2508
2688
|
...relation,
|
|
2509
2689
|
id
|
|
2510
|
-
}
|
|
2511
|
-
connections: [],
|
|
2690
|
+
}),
|
|
2512
2691
|
read: (root) => {
|
|
2513
2692
|
const store = storeOf(bound, root);
|
|
2514
2693
|
const key = entityKey(selection.entity, id);
|
|
2515
|
-
|
|
2694
|
+
const present = memoRead(store, store, `${key}\u0000${stableStringify(relation)}`, () => {
|
|
2516
2695
|
if (isTombstone(store, key)) return { _tag: "NotFound" };
|
|
2517
2696
|
const assembled = assemble(store, key, relation);
|
|
2518
|
-
if (assembled === void 0) return
|
|
2697
|
+
if (assembled === void 0) return void 0;
|
|
2519
2698
|
const decoded = Schema.decodeUnknownResult(selection.schema)(assembled.values);
|
|
2520
2699
|
return Result.isFailure(decoded) ? {
|
|
2521
2700
|
_tag: "Failed",
|
|
@@ -2531,6 +2710,8 @@ const Remote = {
|
|
|
2531
2710
|
value: decoded.success
|
|
2532
2711
|
};
|
|
2533
2712
|
});
|
|
2713
|
+
if (present !== void 0) return present;
|
|
2714
|
+
return isLoading(bound.store.get(root), selection.entity, id, relation.fields) ? { _tag: "Loading" } : { _tag: "Initial" };
|
|
2534
2715
|
}
|
|
2535
2716
|
});
|
|
2536
2717
|
},
|
|
@@ -2539,12 +2720,12 @@ const Remote = {
|
|
|
2539
2720
|
* remote store does not satisfy, under `options` (freshness, force). A
|
|
2540
2721
|
* Surface's projection is `surface.projection(params)`.
|
|
2541
2722
|
*/
|
|
2542
|
-
plan: (bound, model, projection, options) => planAsked(bound.store.get(model), projection, options ?? {}).requirements,
|
|
2723
|
+
plan: (bound, model, projection, options) => planAsked(bound.store.get(model), askedOf(projection), options ?? {}).requirements,
|
|
2543
2724
|
/**
|
|
2544
2725
|
* The queries a projection needs run before its connections read: those the
|
|
2545
2726
|
* Model does not hold, or holds stale (every one under `force`).
|
|
2546
2727
|
*/
|
|
2547
|
-
planQueries: (bound, model, projection, options) => planAsked(bound.store.get(model), projection, options ?? {}).queries.flatMap((query) => query.ref === void 0 ? [] : [query.ref]),
|
|
2728
|
+
planQueries: (bound, model, projection, options) => planAsked(bound.store.get(model), askedOf(projection), options ?? {}).queries.flatMap((query) => query.ref === void 0 ? [] : [query.ref]),
|
|
2548
2729
|
/**
|
|
2549
2730
|
* The store reads see: the base store under the pending optimistic layers.
|
|
2550
2731
|
* One store is shared by every read and plan of a Model whose remote state
|
|
@@ -2561,7 +2742,7 @@ const Remote = {
|
|
|
2561
2742
|
const store = storeOf(bound, model);
|
|
2562
2743
|
const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
|
|
2563
2744
|
const at = now();
|
|
2564
|
-
const missing = plan(store, projection
|
|
2745
|
+
const missing = plan(store, requirementsOf(projection), RemotePolicy.toPlan(policy, at));
|
|
2565
2746
|
if (missing.length === 0) return store;
|
|
2566
2747
|
yield* Effect.annotateCurrentSpan("requirementCount", missing.length);
|
|
2567
2748
|
const result = yield* (yield* RemoteClient).read({
|
|
@@ -2571,6 +2752,52 @@ const Remote = {
|
|
|
2571
2752
|
return writeRead(store, missing, result, at);
|
|
2572
2753
|
}),
|
|
2573
2754
|
/**
|
|
2755
|
+
* Marks what a projection, or a Surface without params, requires as due again.
|
|
2756
|
+
* Called from `update`: every selected field the store holds reads `Refreshing`,
|
|
2757
|
+
* and every loaded query connection is invalidated. Nothing is fetched here.
|
|
2758
|
+
* The read entries `Data.subscriptions` derives refetch it, because a stale
|
|
2759
|
+
* field or connection is planned again under every policy, so a refresh never
|
|
2760
|
+
* sends a request beside the entry already observing the same data.
|
|
2761
|
+
*
|
|
2762
|
+
* A refreshed connection's page replaces its pages, so items the server removed
|
|
2763
|
+
* or reordered follow it; pages loaded past the first are dropped and paged
|
|
2764
|
+
* again with `next`/`previous`. The read entries restart, so a read or query
|
|
2765
|
+
* sent before the refresh is not applied after it.
|
|
2766
|
+
*
|
|
2767
|
+
* The projection must be observed, which it is while it is on screen. Live
|
|
2768
|
+
* subscriptions are left as they are. For data nothing observes (SSR, tests),
|
|
2769
|
+
* use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
|
|
2770
|
+
* already being refreshed returns the same Model.
|
|
2771
|
+
*/
|
|
2772
|
+
refresh: (bound, model, target) => {
|
|
2773
|
+
const projection = "read" in target ? target : target.projection(void 0);
|
|
2774
|
+
const remote = bound.store.get(model);
|
|
2775
|
+
const asked = askedOf(projection);
|
|
2776
|
+
const connections = Requirement.mergeConnections(asked.connections);
|
|
2777
|
+
const items = connections.flatMap((connection) => {
|
|
2778
|
+
const known = remote.connections[connection.identity];
|
|
2779
|
+
return known === void 0 ? [] : itemsOf(visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities).map((edge) => edge.ref), connection.select);
|
|
2780
|
+
});
|
|
2781
|
+
const requirements = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], { force: true });
|
|
2782
|
+
const marked = [...requirements.length === 0 ? [] : [{
|
|
2783
|
+
_tag: "RefreshStarted",
|
|
2784
|
+
requests: requirements
|
|
2785
|
+
}], ...connections.filter((connection) => remote.connections[connection.identity] !== void 0).map((connection) => ({
|
|
2786
|
+
_tag: "ConnectionInvalidated",
|
|
2787
|
+
connection: connection.identity
|
|
2788
|
+
}))].reduce(updateRemote, remote);
|
|
2789
|
+
const inFlight = remote.refresh.started === remote.refresh.requested && requirements.some((requirement) => requirement.fields.some((field) => remote.entities[entityKey(requirement.entity, requirement.id)]?.stale.has(field) === true));
|
|
2790
|
+
if (marked === remote && !inFlight) return model;
|
|
2791
|
+
const refresh = {
|
|
2792
|
+
...marked.refresh,
|
|
2793
|
+
requested: marked.refresh.requested + 1
|
|
2794
|
+
};
|
|
2795
|
+
return bound.store.set(model, {
|
|
2796
|
+
...marked,
|
|
2797
|
+
refresh
|
|
2798
|
+
});
|
|
2799
|
+
},
|
|
2800
|
+
/**
|
|
2574
2801
|
* Writes a read result into the store, recording each field's applied window.
|
|
2575
2802
|
* `requests` are the planned requirements the result answers.
|
|
2576
2803
|
*/
|
|
@@ -2648,7 +2875,7 @@ const Remote = {
|
|
|
2648
2875
|
/**
|
|
2649
2876
|
* Runs a mutation through `RemoteClient`, decoding its typed Output and
|
|
2650
2877
|
* returning the result's normalized patches so the caller can reconcile them
|
|
2651
|
-
* through `
|
|
2878
|
+
* through `updateRemote`'s `MutationSucceeded` (or `Remote.mutateInto`).
|
|
2652
2879
|
*/
|
|
2653
2880
|
mutate: mutateRemote,
|
|
2654
2881
|
/**
|
|
@@ -2684,14 +2911,14 @@ const Remote = {
|
|
|
2684
2911
|
* emits `RefreshStarted`, so the fields being refetched read as `Refreshing`
|
|
2685
2912
|
* while the request is pending.
|
|
2686
2913
|
*/
|
|
2687
|
-
observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => surface.projection(params), toMessage, options),
|
|
2914
|
+
observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => askedOf(surface.projection(params)), toMessage, options),
|
|
2688
2915
|
/**
|
|
2689
2916
|
* A Foldkit Subscription entry that consumes the live stream for a Surface's
|
|
2690
2917
|
* requirements, emitting a `LiveReceived` per event and a `ReadFailed` when
|
|
2691
2918
|
* the stream breaks (including `ResumeUnavailable`). The resume cursor is read
|
|
2692
2919
|
* from `RemoteModel.live`, so the application tracks no cursor of its own.
|
|
2693
2920
|
*/
|
|
2694
|
-
live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => surface.projection(params)
|
|
2921
|
+
live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
|
|
2695
2922
|
};
|
|
2696
2923
|
/** The bound domain: the descriptor, the binding, and the operations over them. */
|
|
2697
2924
|
const bindDomain = (definition, store) => {
|
|
@@ -2705,10 +2932,10 @@ const bindDomain = (definition, store) => {
|
|
|
2705
2932
|
const projection = Remote.select(bound, selection)(id);
|
|
2706
2933
|
return {
|
|
2707
2934
|
...projection,
|
|
2708
|
-
|
|
2935
|
+
metadata: RemoteRequirements.of(...requirementsOf(projection).map((requirement) => ({
|
|
2709
2936
|
...requirement,
|
|
2710
2937
|
live: true
|
|
2711
|
-
}))
|
|
2938
|
+
})))
|
|
2712
2939
|
};
|
|
2713
2940
|
},
|
|
2714
2941
|
subscriptions: (active, options = {}) => {
|
|
@@ -2742,11 +2969,11 @@ const bindDomain = (definition, store) => {
|
|
|
2742
2969
|
const client = yield* RemoteClient;
|
|
2743
2970
|
const planOptions = RemotePolicy.toPlan(policy, now());
|
|
2744
2971
|
let current = model;
|
|
2745
|
-
for (const query of planAsked(store.get(current), projection, planOptions).queries) {
|
|
2972
|
+
for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
|
|
2746
2973
|
const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
|
|
2747
2974
|
current = reduce(current, pageMessage(query.identity, page, true));
|
|
2748
2975
|
}
|
|
2749
|
-
const requirements = planAsked(store.get(current), projection, planOptions).requirements;
|
|
2976
|
+
const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
|
|
2750
2977
|
if (requirements.length === 0) return current;
|
|
2751
2978
|
const at = now();
|
|
2752
2979
|
const result = yield* client.read({
|
|
@@ -2782,8 +3009,7 @@ const bindDomain = (definition, store) => {
|
|
|
2782
3009
|
return {
|
|
2783
3010
|
Model: remoteDataSchema(pageSchema(select.schema)),
|
|
2784
3011
|
dependencies: [],
|
|
2785
|
-
|
|
2786
|
-
connections: [requirement],
|
|
3012
|
+
metadata: RemoteConnections.of(requirement),
|
|
2787
3013
|
ref,
|
|
2788
3014
|
read: (root) => {
|
|
2789
3015
|
const remote = store.get(root);
|
|
@@ -2845,6 +3071,7 @@ const bindDomain = (definition, store) => {
|
|
|
2845
3071
|
}
|
|
2846
3072
|
} : void 0;
|
|
2847
3073
|
},
|
|
3074
|
+
refresh: (model, target) => Remote.refresh(bound, model, target),
|
|
2848
3075
|
fetch: (ref) => ({
|
|
2849
3076
|
name: `Remote.query(${ref.query})`,
|
|
2850
3077
|
args: {
|
|
@@ -2914,6 +3141,6 @@ const pageSize = (window, key) => {
|
|
|
2914
3141
|
return size === void 0 ? {} : { [key]: size };
|
|
2915
3142
|
};
|
|
2916
3143
|
//#endregion
|
|
2917
|
-
export { ConnectionChange, ConnectionChangeSchema, Entity, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRpc, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnection, isFieldStale, isGapped, isRefPage, isRemoteMessage, isStale, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, merge, missingFields, mutateRemote, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshConnection, refsIn, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, retentionRootsSchema, schemaOf, segment, setStale, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unknown, updateRemote, visibleItems, visibleStore, windowKey, writeEntities, writeEntity, writeRead };
|
|
3144
|
+
export { ConnectionChange, ConnectionChangeSchema, Entity, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnection, isFieldStale, isGapped, isLoading, isRefPage, isRemoteMessage, isStale, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, merge, missingFields, mutateRemote, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshConnection, refsIn, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unknown, updateRemote, visibleItems, visibleStore, windowKey, writeEntities, writeEntity, writeRead };
|
|
2918
3145
|
|
|
2919
3146
|
//# sourceMappingURL=index.mjs.map
|