foldkit-remote 0.6.0 → 0.8.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 +471 -159
- package/dist/index.d.mts +378 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +644 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -8
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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
3
|
import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId, evaluate } from "foldkit-entity";
|
|
4
|
+
import { mapMessage } from "foldkit/command";
|
|
5
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
4
6
|
import * as Subscription from "foldkit/subscription";
|
|
5
7
|
import { Metadata } from "foldkit-surface";
|
|
6
8
|
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
@@ -208,6 +210,7 @@ const emptyEntry = {
|
|
|
208
210
|
values: {},
|
|
209
211
|
present: /* @__PURE__ */ new Set(),
|
|
210
212
|
stale: /* @__PURE__ */ new Set(),
|
|
213
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
211
214
|
tombstone: false,
|
|
212
215
|
updatedAt: 0,
|
|
213
216
|
windows: {}
|
|
@@ -220,10 +223,12 @@ const replace = (store, key, entry) => ({
|
|
|
220
223
|
const written = (previous, values, now, windows) => {
|
|
221
224
|
const present = new Set(previous.present);
|
|
222
225
|
const stale = new Set(previous.stale);
|
|
226
|
+
const unavailable = new Set(previous.unavailable);
|
|
223
227
|
const nextWindows = { ...previous.windows };
|
|
224
228
|
for (const field of Object.keys(values)) {
|
|
225
229
|
present.add(field);
|
|
226
230
|
stale.delete(field);
|
|
231
|
+
unavailable.delete(field);
|
|
227
232
|
const requested = windows?.[field];
|
|
228
233
|
if (requested !== void 0 && requested !== "") nextWindows[field] = requested;
|
|
229
234
|
else delete nextWindows[field];
|
|
@@ -238,6 +243,7 @@ const written = (previous, values, now, windows) => {
|
|
|
238
243
|
},
|
|
239
244
|
present,
|
|
240
245
|
stale,
|
|
246
|
+
unavailable,
|
|
241
247
|
tombstone: false,
|
|
242
248
|
updatedAt: now,
|
|
243
249
|
windows: nextWindows
|
|
@@ -287,11 +293,40 @@ const setStale = (store, marks, stale) => {
|
|
|
287
293
|
const markStale = (store, key, fields) => setStale(store, [[key, fields]], true);
|
|
288
294
|
/** Ends a refresh that did not land: the fields are present again as they were. */
|
|
289
295
|
const clearStale = (store, key, fields) => setStale(store, [[key, fields]], false);
|
|
296
|
+
/**
|
|
297
|
+
* Records fields the server settled without a value. A present field is left
|
|
298
|
+
* as it is: the answer that settled it was not about the value it holds.
|
|
299
|
+
* `false` forgets the marks, which is how a refresh asks again.
|
|
300
|
+
*/
|
|
301
|
+
const setUnavailable = (store, marks, unavailable) => {
|
|
302
|
+
let next;
|
|
303
|
+
for (const [key, fields] of marks) {
|
|
304
|
+
const previous = (next ?? store)[key] ?? emptyEntry;
|
|
305
|
+
if (previous.tombstone) continue;
|
|
306
|
+
const marked = new Set(previous.unavailable);
|
|
307
|
+
for (const field of fields) if (unavailable) {
|
|
308
|
+
if (!previous.present.has(field)) marked.add(field);
|
|
309
|
+
} else marked.delete(field);
|
|
310
|
+
if (marked.size === previous.unavailable.size) continue;
|
|
311
|
+
next ??= { ...store };
|
|
312
|
+
next[key] = {
|
|
313
|
+
...previous,
|
|
314
|
+
unavailable: marked
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return next ?? store;
|
|
318
|
+
};
|
|
319
|
+
/** Whether the server settled this field without a value, and nothing has written it since. */
|
|
320
|
+
const isFieldUnavailable = (store, key, field) => {
|
|
321
|
+
const value = store[key];
|
|
322
|
+
return value !== void 0 && !value.tombstone && value.unavailable.has(field);
|
|
323
|
+
};
|
|
290
324
|
/** Records that the entity is known to be absent, so it is not refetched. */
|
|
291
325
|
const tombstone = (store, key) => replace(store, key, {
|
|
292
326
|
values: {},
|
|
293
327
|
present: /* @__PURE__ */ new Set(),
|
|
294
328
|
stale: /* @__PURE__ */ new Set(),
|
|
329
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
295
330
|
tombstone: true,
|
|
296
331
|
updatedAt: 0,
|
|
297
332
|
windows: {}
|
|
@@ -329,7 +364,8 @@ const readField = (store, key, field) => {
|
|
|
329
364
|
};
|
|
330
365
|
/**
|
|
331
366
|
* The fields the planner must fetch. A tombstone makes every field known
|
|
332
|
-
* (absent), so it returns an empty list and the entity is not refetched
|
|
367
|
+
* (absent), so it returns an empty list and the entity is not refetched, and a
|
|
368
|
+
* field the server settled without a value is known the same way. A
|
|
333
369
|
* requested window that differs from the one a field was fetched with also
|
|
334
370
|
* marks it missing, but only when the stored window is known (never `""`), so a
|
|
335
371
|
* writer that does not record windows cannot cause a refetch loop.
|
|
@@ -338,7 +374,9 @@ const missingFields = (store, key, fields, windows) => {
|
|
|
338
374
|
const value = store[key];
|
|
339
375
|
if (value !== void 0 && value.tombstone) return [];
|
|
340
376
|
return [...fields].filter((field) => {
|
|
341
|
-
if (value === void 0
|
|
377
|
+
if (value === void 0) return true;
|
|
378
|
+
if (!value.present.has(field)) return !value.unavailable.has(field);
|
|
379
|
+
if (value.stale.has(field)) return true;
|
|
342
380
|
const requested = windows?.[field];
|
|
343
381
|
const stored = value.windows[field];
|
|
344
382
|
return requested !== void 0 && stored !== void 0 && stored !== "" && stored !== requested;
|
|
@@ -588,7 +626,7 @@ const Query = {
|
|
|
588
626
|
* server refuses a mismatch with `RemoteProtocolError`, so a shape change never
|
|
589
627
|
* drifts silently: bump it whenever `ReadRequest` or `LiveRequirement` change.
|
|
590
628
|
*/
|
|
591
|
-
const REMOTE_PROTOCOL_VERSION =
|
|
629
|
+
const REMOTE_PROTOCOL_VERSION = 4;
|
|
592
630
|
var RemoteReadError = class extends Schema.TaggedError()("RemoteReadError", { message: Schema.String }) {};
|
|
593
631
|
var RemoteMutationError = class extends Schema.TaggedError()("RemoteMutationError", { message: Schema.String }) {};
|
|
594
632
|
var RemoteLiveError = class extends Schema.TaggedError()("RemoteLiveError", { message: Schema.String }) {};
|
|
@@ -649,7 +687,20 @@ const NormalizedEntity = Schema.Struct({
|
|
|
649
687
|
id: Schema.String,
|
|
650
688
|
values: Schema.Record(Schema.String, Schema.Unknown)
|
|
651
689
|
});
|
|
652
|
-
|
|
690
|
+
/**
|
|
691
|
+
* Fields a read asked for that the server answered without, and will not
|
|
692
|
+
* answer with: the client stops asking. It says nothing about why, so a rule
|
|
693
|
+
* that withholds a field is not itself on the wire.
|
|
694
|
+
*/
|
|
695
|
+
const SettledFields = Schema.Struct({
|
|
696
|
+
entity: Schema.String,
|
|
697
|
+
id: Schema.String,
|
|
698
|
+
fields: Schema.Array(Schema.String)
|
|
699
|
+
});
|
|
700
|
+
const ReadBatchResult = Schema.Struct({
|
|
701
|
+
entities: Schema.Array(NormalizedEntity),
|
|
702
|
+
settled: Schema.Array(SettledFields)
|
|
703
|
+
});
|
|
653
704
|
const MutationRequest = Schema.Struct({
|
|
654
705
|
/** Stable across transport retries so a mutation is not applied twice. */
|
|
655
706
|
requestId: Schema.String,
|
|
@@ -867,7 +918,7 @@ const coalesceReads = (read, options = {}) => Effect.gen(function* () {
|
|
|
867
918
|
for (const entry of entries) if (own.has(entry.request.key)) owned.set(entry.request.key, entry.request.requirement);
|
|
868
919
|
yield* Effect.forEach(readsOf(owned), ({ keys, requests }) => Effect.gen(function* () {
|
|
869
920
|
const exit = yield* Effect.exit(read({
|
|
870
|
-
version:
|
|
921
|
+
version: 4,
|
|
871
922
|
requests
|
|
872
923
|
}));
|
|
873
924
|
for (const key of keys) {
|
|
@@ -883,10 +934,19 @@ const coalesceReads = (read, options = {}) => Effect.gen(function* () {
|
|
|
883
934
|
});
|
|
884
935
|
let resolver = RequestResolver.make(runAll);
|
|
885
936
|
if (options.window !== void 0) resolver = RequestResolver.setDelay(resolver, Duration.fromInputUnsafe(options.window));
|
|
886
|
-
return (batch) => batch.requests.length === 0 ? Effect.succeed({
|
|
937
|
+
return (batch) => batch.requests.length === 0 ? Effect.succeed({
|
|
938
|
+
entities: [],
|
|
939
|
+
settled: []
|
|
940
|
+
}) : Effect.forEach(batch.requests, (requirement) => Effect.request(new ReadRequirement({
|
|
887
941
|
key: requirementKey(requirement),
|
|
888
942
|
requirement
|
|
889
|
-
}), resolver), { concurrency: "unbounded" }).pipe(Effect.map((results) =>
|
|
943
|
+
}), resolver), { concurrency: "unbounded" }).pipe(Effect.map((results) => {
|
|
944
|
+
const distinct = [...new Set(results)];
|
|
945
|
+
return {
|
|
946
|
+
entities: distinct.flatMap((result) => result.entities),
|
|
947
|
+
settled: distinct.flatMap((result) => result.settled)
|
|
948
|
+
};
|
|
949
|
+
}));
|
|
890
950
|
});
|
|
891
951
|
//#endregion
|
|
892
952
|
//#region src/client.ts
|
|
@@ -1302,6 +1362,24 @@ const assemble = (store, key, requirement) => {
|
|
|
1302
1362
|
refreshing
|
|
1303
1363
|
};
|
|
1304
1364
|
};
|
|
1365
|
+
/**
|
|
1366
|
+
* The first field `requirement` reads, at any depth, that the server settled
|
|
1367
|
+
* without a value, as `Entity:id.field`. A read that consults it says why it
|
|
1368
|
+
* cannot assemble rather than waiting on a field nothing will fetch.
|
|
1369
|
+
*/
|
|
1370
|
+
const unavailableOf = (store, key, requirement) => {
|
|
1371
|
+
for (const field of requirement.fields) {
|
|
1372
|
+
if (isFieldUnavailable(store, key, field)) return `${key}.${field}`;
|
|
1373
|
+
const relation = requirement.relations?.[field];
|
|
1374
|
+
if (relation === void 0) continue;
|
|
1375
|
+
const value = readField(store, key, field);
|
|
1376
|
+
if (Option.isNone(value) || value.value === null || value.value === void 0) continue;
|
|
1377
|
+
for (const ref of targetsOf(value.value, relation)) {
|
|
1378
|
+
const found = unavailableOf(store, entityKey(ref.entity, ref.id), relation);
|
|
1379
|
+
if (found !== void 0) return found;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1305
1383
|
const assembleTarget = (store, ref, relation) => {
|
|
1306
1384
|
const key = entityKey(ref.entity, ref.id);
|
|
1307
1385
|
return isTombstone(store, key) ? "absent" : assemble(store, key, relation);
|
|
@@ -1344,12 +1422,6 @@ const assembleRelation = (store, value, relation) => {
|
|
|
1344
1422
|
* Entities: the descriptor an application declares, refs, ref pages, and
|
|
1345
1423
|
* patches, with the codecs that carry refs on the wire.
|
|
1346
1424
|
*/
|
|
1347
|
-
/**
|
|
1348
|
-
* Relations are **references**, never inline target schemas: a ref cannot
|
|
1349
|
-
* reconstruct a full entity, and dereferencing is a store concern. Because the
|
|
1350
|
-
* target schema is not inlined, recursive relations cannot arise through the
|
|
1351
|
-
* schema graph.
|
|
1352
|
-
*/
|
|
1353
1425
|
const refCodec = (entity) => Schema.Struct({
|
|
1354
1426
|
entity: Schema.String,
|
|
1355
1427
|
id: Schema.String
|
|
@@ -1551,6 +1623,7 @@ const inspectEntry = (key, entry) => ({
|
|
|
1551
1623
|
key,
|
|
1552
1624
|
present: [...entry.present],
|
|
1553
1625
|
stale: [...entry.stale],
|
|
1626
|
+
unavailable: [...entry.unavailable],
|
|
1554
1627
|
tombstone: entry.tombstone,
|
|
1555
1628
|
updatedAt: entry.updatedAt,
|
|
1556
1629
|
windows: { ...entry.windows }
|
|
@@ -1884,6 +1957,41 @@ const followRelation = (value, relation) => targetsOf(value, relation).map((ref)
|
|
|
1884
1957
|
...relation,
|
|
1885
1958
|
id: ref.id
|
|
1886
1959
|
}));
|
|
1960
|
+
/**
|
|
1961
|
+
* The earliest moment a value `requirements` read, following the relations the
|
|
1962
|
+
* store holds as `plan` does, goes stale under `freshness`, with the
|
|
1963
|
+
* requirements of the entities that go stale then. Nothing when no value is
|
|
1964
|
+
* both present and still fresh: what is already expired or stale is planned,
|
|
1965
|
+
* not awaited, which is what keeps a timer from firing on what it already
|
|
1966
|
+
* fired on.
|
|
1967
|
+
*/
|
|
1968
|
+
const deadlineOf = (store, requirements, freshness) => {
|
|
1969
|
+
let at;
|
|
1970
|
+
let due = [];
|
|
1971
|
+
const walk = (group) => {
|
|
1972
|
+
const key = entityKey(group.entity, group.id);
|
|
1973
|
+
const entry = store[key];
|
|
1974
|
+
if (entry === void 0 || entry.tombstone) return;
|
|
1975
|
+
if (group.fields.some((field) => entry.present.has(field) && !entry.stale.has(field))) {
|
|
1976
|
+
const expires = entry.updatedAt + freshness.freshness;
|
|
1977
|
+
if (expires > freshness.now) {
|
|
1978
|
+
if (at === void 0 || expires < at) {
|
|
1979
|
+
at = expires;
|
|
1980
|
+
due = [group];
|
|
1981
|
+
} else if (expires === at) due.push(group);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
for (const [field, relation] of Object.entries(group.relations ?? {})) {
|
|
1985
|
+
const value = readField(store, key, field);
|
|
1986
|
+
if (value._tag === "Some") for (const next of followRelation(value.value, relation)) walk(next);
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
for (const group of Requirement.merge(requirements)) walk(group);
|
|
1990
|
+
return at === void 0 ? void 0 : {
|
|
1991
|
+
at,
|
|
1992
|
+
due: Requirement.merge(due)
|
|
1993
|
+
};
|
|
1994
|
+
};
|
|
1887
1995
|
const plan = (store, requirements, options = {}) => {
|
|
1888
1996
|
const { freshness, force = false } = options;
|
|
1889
1997
|
const planned = [];
|
|
@@ -1995,7 +2103,7 @@ const gc = (state, roots) => {
|
|
|
1995
2103
|
* application's choice.
|
|
1996
2104
|
*/
|
|
1997
2105
|
/** Bump when the serialized shape changes; a mismatch discards the cache. */
|
|
1998
|
-
const REMOTE_CACHE_VERSION =
|
|
2106
|
+
const REMOTE_CACHE_VERSION = 5;
|
|
1999
2107
|
/**
|
|
2000
2108
|
* A snapshot of a Model, keeping the entity cache and only the connections
|
|
2001
2109
|
* named.
|
|
@@ -2018,13 +2126,14 @@ const sorted = (values) => [...values].sort();
|
|
|
2018
2126
|
* whatever order they were built in.
|
|
2019
2127
|
*/
|
|
2020
2128
|
const serializeStore = (snapshot, scope) => ({
|
|
2021
|
-
version:
|
|
2129
|
+
version: 5,
|
|
2022
2130
|
scope: scope ?? null,
|
|
2023
2131
|
connections: snapshot.connections,
|
|
2024
2132
|
entities: Object.fromEntries(Object.entries(snapshot.entities).map(([key, entry]) => [key, {
|
|
2025
2133
|
values: entry.values,
|
|
2026
2134
|
present: sorted(entry.present),
|
|
2027
2135
|
stale: sorted(entry.stale),
|
|
2136
|
+
unavailable: sorted(entry.unavailable),
|
|
2028
2137
|
tombstone: entry.tombstone,
|
|
2029
2138
|
updatedAt: entry.updatedAt,
|
|
2030
2139
|
windows: entry.windows
|
|
@@ -2039,6 +2148,7 @@ const parseEntry = (value) => {
|
|
|
2039
2148
|
if (entry.values === null || typeof entry.values !== "object" || Array.isArray(entry.values)) throw new Error("entry.values is not a record");
|
|
2040
2149
|
if (!isStringArray(entry.present)) throw new Error("entry.present is not a string array");
|
|
2041
2150
|
if (!isStringArray(entry.stale)) throw new Error("entry.stale is not a string array");
|
|
2151
|
+
if (!isStringArray(entry.unavailable)) throw new Error("entry.unavailable is not a string array");
|
|
2042
2152
|
if (typeof entry.tombstone !== "boolean") throw new Error("entry.tombstone is not a boolean");
|
|
2043
2153
|
if (typeof entry.updatedAt !== "number") throw new Error("entry.updatedAt is not a number");
|
|
2044
2154
|
if (!isStringRecord(entry.windows)) throw new Error("entry.windows is not a string record");
|
|
@@ -2046,6 +2156,7 @@ const parseEntry = (value) => {
|
|
|
2046
2156
|
values: entry.values,
|
|
2047
2157
|
present: new Set(entry.present),
|
|
2048
2158
|
stale: new Set(entry.stale),
|
|
2159
|
+
unavailable: new Set(entry.unavailable),
|
|
2049
2160
|
tombstone: entry.tombstone,
|
|
2050
2161
|
updatedAt: entry.updatedAt,
|
|
2051
2162
|
windows: entry.windows
|
|
@@ -2104,7 +2215,7 @@ const hydrate = (raw, options = {}) => {
|
|
|
2104
2215
|
}
|
|
2105
2216
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2106
2217
|
const candidate = parsed;
|
|
2107
|
-
if (candidate.version !==
|
|
2218
|
+
if (candidate.version !== 5) return void 0;
|
|
2108
2219
|
if ((candidate.scope ?? null) !== (options.scope ?? null)) return void 0;
|
|
2109
2220
|
if (candidate.entities === null || typeof candidate.entities !== "object") return void 0;
|
|
2110
2221
|
if (Array.isArray(candidate.entities)) return void 0;
|
|
@@ -2299,7 +2410,32 @@ const noFailures = {
|
|
|
2299
2410
|
const emptyRefresh = {
|
|
2300
2411
|
generation: 0,
|
|
2301
2412
|
requested: /* @__PURE__ */ new Map(),
|
|
2302
|
-
started: /* @__PURE__ */ new Map()
|
|
2413
|
+
started: /* @__PURE__ */ new Map(),
|
|
2414
|
+
floor: 0
|
|
2415
|
+
};
|
|
2416
|
+
/**
|
|
2417
|
+
* The Model with everything the server told it forgotten: values, tombstones,
|
|
2418
|
+
* unavailable fields, connections, live cursors, failures, and what is in
|
|
2419
|
+
* flight. For a change of principal, whose facts these were. The mutation
|
|
2420
|
+
* sequence is kept, so request ids stay unique, and a mutation in flight is
|
|
2421
|
+
* treated as applied: its answer, when it comes, writes nothing here.
|
|
2422
|
+
*/
|
|
2423
|
+
const forgetRemote = (model) => {
|
|
2424
|
+
const generation = model.refresh.generation + 1;
|
|
2425
|
+
return {
|
|
2426
|
+
...initialRemoteModel,
|
|
2427
|
+
refresh: {
|
|
2428
|
+
generation,
|
|
2429
|
+
requested: /* @__PURE__ */ new Map(),
|
|
2430
|
+
started: /* @__PURE__ */ new Map(),
|
|
2431
|
+
floor: generation
|
|
2432
|
+
},
|
|
2433
|
+
mutations: {
|
|
2434
|
+
...emptyMutationState,
|
|
2435
|
+
sequence: model.mutations.sequence,
|
|
2436
|
+
applied: /* @__PURE__ */ new Set([...model.mutations.applied, ...model.mutations.pending])
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2303
2439
|
};
|
|
2304
2440
|
const initialRemoteModel = {
|
|
2305
2441
|
entities: emptyStore,
|
|
@@ -2457,9 +2593,10 @@ const withoutQueryLoading = (loading, identity) => {
|
|
|
2457
2593
|
* leaves its mark behind, and a connection released with that mark would read
|
|
2458
2594
|
* `Loading` forever the next time something asked for it before sending.
|
|
2459
2595
|
*/
|
|
2460
|
-
const
|
|
2596
|
+
const prunedLoading = (loading, roots, reached) => {
|
|
2597
|
+
if (loading.size === 0) return loading;
|
|
2461
2598
|
const named = new Set(roots.connections.map((root) => connectionMark(root.identity)));
|
|
2462
|
-
const kept = [...loading].filter((mark) =>
|
|
2599
|
+
const kept = [...loading].filter((mark) => mark.startsWith(connectionPrefix) ? named.has(mark) : reached().has(markEntity(mark)));
|
|
2463
2600
|
return kept.length === loading.size ? loading : new Set(kept);
|
|
2464
2601
|
};
|
|
2465
2602
|
/**
|
|
@@ -2492,7 +2629,7 @@ const prunedRefresh = (refresh, retained) => {
|
|
|
2492
2629
|
* anything else is.
|
|
2493
2630
|
*/
|
|
2494
2631
|
const refreshedAt = (refresh, requests, connections = []) => {
|
|
2495
|
-
let at =
|
|
2632
|
+
let at = refresh.floor;
|
|
2496
2633
|
for (const mark of fieldMarks(requests)) at = Math.max(at, refresh.requested.get(mark) ?? 0);
|
|
2497
2634
|
for (const identity of connections) at = Math.max(at, refresh.requested.get(connectionMark(identity)) ?? 0);
|
|
2498
2635
|
return at;
|
|
@@ -2589,12 +2726,14 @@ const updateRemote = (model, message) => {
|
|
|
2589
2726
|
};
|
|
2590
2727
|
case "RetentionChanged": {
|
|
2591
2728
|
const retained = gc(model, message.roots);
|
|
2729
|
+
let reachedKeys;
|
|
2730
|
+
const reached = () => reachedKeys ??= reachable(model.entities, message.roots, model.connections, model.optimistic, model.mutations.pending);
|
|
2592
2731
|
return {
|
|
2593
2732
|
...model,
|
|
2594
2733
|
...retained,
|
|
2595
2734
|
refresh: prunedRefresh(model.refresh, retained),
|
|
2596
|
-
failures: prunedFailures(model, message.roots),
|
|
2597
|
-
loading:
|
|
2735
|
+
failures: prunedFailures(model.failures, message.roots, reached),
|
|
2736
|
+
loading: prunedLoading(model.loading, message.roots, reached)
|
|
2598
2737
|
};
|
|
2599
2738
|
}
|
|
2600
2739
|
case "Hydrated": {
|
|
@@ -2606,9 +2745,11 @@ const updateRemote = (model, message) => {
|
|
|
2606
2745
|
})),
|
|
2607
2746
|
stale: true
|
|
2608
2747
|
}]);
|
|
2748
|
+
const written = Object.entries(message.entities).flatMap(([key, entry]) => message.merge === "replace" || !(key in model.entities) ? [...entry.present].map((field) => `${key}\u0000${field}`) : []);
|
|
2609
2749
|
return {
|
|
2610
2750
|
...model,
|
|
2611
2751
|
entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
|
|
2752
|
+
failures: withoutFieldFailures(model.failures, written),
|
|
2612
2753
|
connections: message.merge === "replace" ? {
|
|
2613
2754
|
...model.connections,
|
|
2614
2755
|
...Object.fromEntries(restored)
|
|
@@ -2620,8 +2761,9 @@ const updateRemote = (model, message) => {
|
|
|
2620
2761
|
}
|
|
2621
2762
|
case "RefreshStarted": {
|
|
2622
2763
|
const asked = message.requests.reduce((store, request) => isTombstone(store, entityKey(request.entity, request.id)) ? remove(store, entityKey(request.entity, request.id)) : store, model.entities);
|
|
2623
|
-
const
|
|
2624
|
-
const
|
|
2764
|
+
const marks = marksOf(message.requests);
|
|
2765
|
+
const entities = setUnavailable(setStale(asked, marks, true), marks, false);
|
|
2766
|
+
const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
|
|
2625
2767
|
return entities === model.entities && failures === model.failures ? model : {
|
|
2626
2768
|
...model,
|
|
2627
2769
|
entities,
|
|
@@ -2737,7 +2879,6 @@ const updateRemote = (model, message) => {
|
|
|
2737
2879
|
};
|
|
2738
2880
|
case "QueryFailed": return {
|
|
2739
2881
|
...model,
|
|
2740
|
-
connections: message.connection in model.connections ? setConnectionStale(model.connections, message.connection, false) : model.connections,
|
|
2741
2882
|
failures: {
|
|
2742
2883
|
...model.failures,
|
|
2743
2884
|
connections: {
|
|
@@ -2793,6 +2934,35 @@ const patchedMarks = function* (patches) {
|
|
|
2793
2934
|
for (const patch of patches) for (const field of Object.keys(patch.values)) yield fieldMark(patch.entity, patch.id, field);
|
|
2794
2935
|
};
|
|
2795
2936
|
/**
|
|
2937
|
+
* Every field a read's requests reach: their own, and through each relation
|
|
2938
|
+
* the store already holds, the fields they ask of its targets. A request with
|
|
2939
|
+
* a relation riding on it names no target id; the store does, and a failure
|
|
2940
|
+
* recorded on a target is reached only this way.
|
|
2941
|
+
*/
|
|
2942
|
+
const reachedMarks = (store, requests) => {
|
|
2943
|
+
const marks = [];
|
|
2944
|
+
const walked = /* @__PURE__ */ new Map();
|
|
2945
|
+
const walk = (entity, id, requirement) => {
|
|
2946
|
+
const key = entityKey(entity, id);
|
|
2947
|
+
const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
|
|
2948
|
+
if (seen.has(requirement)) return;
|
|
2949
|
+
seen.add(requirement);
|
|
2950
|
+
walked.set(key, seen);
|
|
2951
|
+
for (const field of requirement.fields) {
|
|
2952
|
+
marks.push(fieldMark(entity, id, field));
|
|
2953
|
+
const nested = requirement.relations?.[field];
|
|
2954
|
+
if (nested === void 0) continue;
|
|
2955
|
+
const value = readField(store, key, field);
|
|
2956
|
+
if (value._tag === "None") continue;
|
|
2957
|
+
for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
|
|
2958
|
+
}
|
|
2959
|
+
};
|
|
2960
|
+
for (const request of requests) walk(request.entity, request.id, request);
|
|
2961
|
+
return marks;
|
|
2962
|
+
};
|
|
2963
|
+
/** The entity key a field mark belongs to. */
|
|
2964
|
+
const markEntity = (mark) => mark.slice(0, mark.indexOf("\0"));
|
|
2965
|
+
/**
|
|
2796
2966
|
* The failures of what the roots still reach. A released connection's or
|
|
2797
2967
|
* entity's failure goes with it, so asking for it again later asks the server
|
|
2798
2968
|
* again rather than repeating an error that may no longer hold.
|
|
@@ -2802,12 +2972,10 @@ const patchedMarks = function* (patches) {
|
|
|
2802
2972
|
* which is exactly a value whose first read failed: it is still on screen,
|
|
2803
2973
|
* behind the parent that refers to it.
|
|
2804
2974
|
*/
|
|
2805
|
-
const prunedFailures = (
|
|
2806
|
-
const { failures } = model;
|
|
2975
|
+
const prunedFailures = (failures, roots, reached) => {
|
|
2807
2976
|
const named = new Set(roots.connections.map((root) => root.identity));
|
|
2808
2977
|
const connections = Object.entries(failures.connections).filter(([identity]) => named.has(identity));
|
|
2809
|
-
const
|
|
2810
|
-
const fields = Object.entries(failures.fields).filter(([mark]) => kept.has(mark.slice(0, mark.indexOf("\0"))));
|
|
2978
|
+
const fields = Object.keys(failures.fields).length === 0 ? [] : Object.entries(failures.fields).filter(([mark]) => reached().has(markEntity(mark)));
|
|
2811
2979
|
return connections.length === Object.keys(failures.connections).length && fields.length === Object.keys(failures.fields).length ? failures : {
|
|
2812
2980
|
connections: Object.fromEntries(connections),
|
|
2813
2981
|
fields: Object.fromEntries(fields)
|
|
@@ -2816,6 +2984,32 @@ const prunedFailures = (model, roots) => {
|
|
|
2816
2984
|
/** Whether this field's last read failed and nothing has settled it since. */
|
|
2817
2985
|
const isFieldFailed = (model, entity, id, field) => fieldMark(entity, id, field) in model.failures.fields;
|
|
2818
2986
|
/**
|
|
2987
|
+
* Whether a read is fetching any field `relation` reads of an entity, its own
|
|
2988
|
+
* or, through each relation the store holds, its targets'. A value whose owner
|
|
2989
|
+
* is being fetched is loading, not `Initial`: nothing is missing a fetch.
|
|
2990
|
+
*/
|
|
2991
|
+
const isLoadingThrough = (model, entity, id, relation) => {
|
|
2992
|
+
if (model.loading.size === 0) return false;
|
|
2993
|
+
const walked = /* @__PURE__ */ new Map();
|
|
2994
|
+
const walk = (target, targetId, requirement) => {
|
|
2995
|
+
const key = entityKey(target, targetId);
|
|
2996
|
+
const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
|
|
2997
|
+
if (seen.has(requirement)) return false;
|
|
2998
|
+
seen.add(requirement);
|
|
2999
|
+
walked.set(key, seen);
|
|
3000
|
+
for (const field of requirement.fields) {
|
|
3001
|
+
if (model.loading.has(fieldMark(target, targetId, field))) return true;
|
|
3002
|
+
const nested = requirement.relations?.[field];
|
|
3003
|
+
if (nested === void 0) continue;
|
|
3004
|
+
const value = readField(model.entities, key, field);
|
|
3005
|
+
if (value._tag === "None") continue;
|
|
3006
|
+
for (const ref of targetsOf(value.value, nested)) if (walk(ref.entity, ref.id, nested)) return true;
|
|
3007
|
+
}
|
|
3008
|
+
return false;
|
|
3009
|
+
};
|
|
3010
|
+
return walk(entity, id, relation);
|
|
3011
|
+
};
|
|
3012
|
+
/**
|
|
2819
3013
|
* The first failure among the fields `relation` reads of an entity, following
|
|
2820
3014
|
* each relation the store holds into its targets, as `assemble` does. A read
|
|
2821
3015
|
* that consults it shows `Failed` for a related entity's failure too, rather
|
|
@@ -2923,12 +3117,13 @@ const writeRead = (store, requests, result, now = 0) => {
|
|
|
2923
3117
|
windows: entry?.windows
|
|
2924
3118
|
});
|
|
2925
3119
|
}
|
|
3120
|
+
const settled = new Set(result.settled.map((entry) => entityKey(entry.entity, entry.id)));
|
|
2926
3121
|
let answered = store;
|
|
2927
3122
|
for (const request of requests) {
|
|
2928
3123
|
const key = entityKey(request.entity, request.id);
|
|
2929
|
-
if (!returned.has(key)) answered = tombstone(answered, key);
|
|
3124
|
+
if (!returned.has(key) && !settled.has(key)) answered = tombstone(answered, key);
|
|
2930
3125
|
}
|
|
2931
|
-
return writeEntities(answered, writes, now);
|
|
3126
|
+
return setUnavailable(writeEntities(answered, writes, now), result.settled.map((entry) => [entityKey(entry.entity, entry.id), entry.fields]), true);
|
|
2932
3127
|
};
|
|
2933
3128
|
//#endregion
|
|
2934
3129
|
//#region src/policy.ts
|
|
@@ -2961,6 +3156,221 @@ const RemotePolicy = {
|
|
|
2961
3156
|
refreshes: (policy) => policy._tag !== "CacheFirst"
|
|
2962
3157
|
};
|
|
2963
3158
|
//#endregion
|
|
3159
|
+
//#region src/resume.ts
|
|
3160
|
+
/**
|
|
3161
|
+
* What of Remote's state crosses from a server render to the browser.
|
|
3162
|
+
*
|
|
3163
|
+
* A page rendered a moment ago sends exactly what its active Surfaces read:
|
|
3164
|
+
* for each requirement the fields it names, following relations through the
|
|
3165
|
+
* refs the store holds, and each connection whole, with its boundaries, so
|
|
3166
|
+
* "load more" knows where the server stopped. A live subscription's cursor
|
|
3167
|
+
* crosses with the entities it covers, so a subscription started in the
|
|
3168
|
+
* browser resumes where the server's left off.
|
|
3169
|
+
*
|
|
3170
|
+
* This is not a `Snapshot`, which is built for a cache that survived a reload
|
|
3171
|
+
* and deliberately keeps no cursors. Nothing else of `RemoteModel` crosses:
|
|
3172
|
+
* loading marks, failures, the mutation ledger, optimistic layers, gaps and
|
|
3173
|
+
* retention start from their initial values in the browser.
|
|
3174
|
+
*/
|
|
3175
|
+
const CapturedEntry = Schema.Struct({
|
|
3176
|
+
values: Schema.Record(Schema.String, Schema.Unknown),
|
|
3177
|
+
present: Schema.Array(Schema.String),
|
|
3178
|
+
stale: Schema.Array(Schema.String),
|
|
3179
|
+
unavailable: Schema.Array(Schema.String),
|
|
3180
|
+
windows: Schema.Record(Schema.String, Schema.String),
|
|
3181
|
+
tombstone: Schema.Boolean,
|
|
3182
|
+
updatedAt: Schema.Number
|
|
3183
|
+
});
|
|
3184
|
+
const CapturedBoundary = Schema.Union([
|
|
3185
|
+
Schema.TaggedStruct("Terminal", {}),
|
|
3186
|
+
Schema.TaggedStruct("Cursor", { cursor: Schema.String }),
|
|
3187
|
+
Schema.TaggedStruct("Unknown", {})
|
|
3188
|
+
]);
|
|
3189
|
+
const CapturedConnection = Schema.Struct({
|
|
3190
|
+
segments: Schema.Array(Schema.Struct({
|
|
3191
|
+
edges: Schema.Array(Schema.Struct({
|
|
3192
|
+
key: Schema.String,
|
|
3193
|
+
ref: Schema.Struct({
|
|
3194
|
+
entity: Schema.String,
|
|
3195
|
+
id: Schema.String
|
|
3196
|
+
})
|
|
3197
|
+
})),
|
|
3198
|
+
start: CapturedBoundary,
|
|
3199
|
+
end: CapturedBoundary
|
|
3200
|
+
})),
|
|
3201
|
+
stale: Schema.Boolean
|
|
3202
|
+
});
|
|
3203
|
+
const CapturedLive = Schema.Struct({
|
|
3204
|
+
cursor: Schema.Number,
|
|
3205
|
+
boundary: Schema.Record(Schema.String, Schema.Struct({
|
|
3206
|
+
before: Schema.Number,
|
|
3207
|
+
after: Schema.Number
|
|
3208
|
+
}))
|
|
3209
|
+
});
|
|
3210
|
+
/**
|
|
3211
|
+
* Marks a Subscription entry Remote built as one that may start late under
|
|
3212
|
+
* `foldkit-ssr`'s deferred boot (its decision 10): a read with resumed data
|
|
3213
|
+
* plans nothing, a live subscription resumes from the cursor the envelope
|
|
3214
|
+
* carries, and retention running late collects late. Non-enumerable, so the
|
|
3215
|
+
* runtime's iteration of an entry never sees it.
|
|
3216
|
+
*/
|
|
3217
|
+
const DEFERRABLE = Symbol.for("foldkit-remote/deferrable");
|
|
3218
|
+
/** Marks every entry of a record Remote built, lifted or not. */
|
|
3219
|
+
const markAll = (entries) => {
|
|
3220
|
+
for (const entry of Object.values(entries)) Object.defineProperty(entry, DEFERRABLE, {
|
|
3221
|
+
value: true,
|
|
3222
|
+
enumerable: false
|
|
3223
|
+
});
|
|
3224
|
+
return entries;
|
|
3225
|
+
};
|
|
3226
|
+
/** The captured part of a Remote store, as JSON. */
|
|
3227
|
+
const RemoteCapture = Schema.Struct({
|
|
3228
|
+
entities: Schema.Record(Schema.String, CapturedEntry),
|
|
3229
|
+
connections: Schema.Record(Schema.String, CapturedConnection),
|
|
3230
|
+
live: Schema.Record(Schema.String, CapturedLive)
|
|
3231
|
+
});
|
|
3232
|
+
/** What some projections read, from their metadata. */
|
|
3233
|
+
const askedOf$1 = (projections) => ({
|
|
3234
|
+
requirements: projections.flatMap((projection) => RemoteRequirements.get(projection.metadata)),
|
|
3235
|
+
connections: projections.flatMap((projection) => RemoteConnections.get(projection.metadata))
|
|
3236
|
+
});
|
|
3237
|
+
/** Whether every `entity:id:fields` part of a live stream key was captured with those fields. */
|
|
3238
|
+
const liveCovered = (stream, entries) => stream.split("|").every((part) => {
|
|
3239
|
+
const split = part.lastIndexOf(":");
|
|
3240
|
+
const entry = entries.get(part.slice(0, split));
|
|
3241
|
+
const fields = part.slice(split + 1).split(",").filter((field) => field !== "");
|
|
3242
|
+
return entry !== void 0 && fields.every((field) => entry.present.has(field));
|
|
3243
|
+
});
|
|
3244
|
+
/**
|
|
3245
|
+
* Captures what some projections read from a store: the fields each
|
|
3246
|
+
* requirement names, through its relations; each connection whole, and the
|
|
3247
|
+
* fields its selection reads of each item; and each live cursor whose
|
|
3248
|
+
* entities were all captured with the fields it follows.
|
|
3249
|
+
*/
|
|
3250
|
+
const captureRemote = (remote, projections) => {
|
|
3251
|
+
const store = remote.entities;
|
|
3252
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3253
|
+
const walked = /* @__PURE__ */ new Map();
|
|
3254
|
+
const walk = (entity, id, requirement) => {
|
|
3255
|
+
const key = entityKey(entity, id);
|
|
3256
|
+
const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
|
|
3257
|
+
if (seen.has(requirement)) return;
|
|
3258
|
+
seen.add(requirement);
|
|
3259
|
+
walked.set(key, seen);
|
|
3260
|
+
const stored = store[key];
|
|
3261
|
+
if (stored === void 0) return;
|
|
3262
|
+
const entry = entries.get(key) ?? {
|
|
3263
|
+
values: {},
|
|
3264
|
+
present: /* @__PURE__ */ new Set(),
|
|
3265
|
+
stale: /* @__PURE__ */ new Set(),
|
|
3266
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
3267
|
+
windows: {},
|
|
3268
|
+
tombstone: stored.tombstone,
|
|
3269
|
+
updatedAt: stored.updatedAt
|
|
3270
|
+
};
|
|
3271
|
+
entries.set(key, entry);
|
|
3272
|
+
for (const field of requirement.fields) {
|
|
3273
|
+
if (stored.unavailable.has(field)) entry.unavailable.add(field);
|
|
3274
|
+
if (!stored.present.has(field)) continue;
|
|
3275
|
+
entry.values[field] = stored.values[field];
|
|
3276
|
+
entry.present.add(field);
|
|
3277
|
+
if (stored.stale.has(field)) entry.stale.add(field);
|
|
3278
|
+
const window = stored.windows[field];
|
|
3279
|
+
if (window !== void 0) entry.windows[field] = window;
|
|
3280
|
+
const nested = requirement.relations?.[field];
|
|
3281
|
+
if (nested === void 0) continue;
|
|
3282
|
+
const value = readField(store, key, field);
|
|
3283
|
+
if (value._tag === "None") continue;
|
|
3284
|
+
for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
|
|
3285
|
+
}
|
|
3286
|
+
};
|
|
3287
|
+
const asked = askedOf$1(projections);
|
|
3288
|
+
for (const requirement of asked.requirements) walk(requirement.entity, requirement.id, requirement);
|
|
3289
|
+
const connections = {};
|
|
3290
|
+
for (const { identity, select } of asked.connections) {
|
|
3291
|
+
const connection = remote.connections[identity];
|
|
3292
|
+
if (connection === void 0) continue;
|
|
3293
|
+
connections[identity] = connection;
|
|
3294
|
+
for (const segment of connection.segments) for (const edge of segment.edges) walk(edge.ref.entity, edge.ref.id, select);
|
|
3295
|
+
}
|
|
3296
|
+
const live = {};
|
|
3297
|
+
for (const [stream, state] of Object.entries(remote.live)) if (liveCovered(stream, entries)) live[stream] = state;
|
|
3298
|
+
return {
|
|
3299
|
+
entities: Object.fromEntries([...entries].map(([key, entry]) => [key, {
|
|
3300
|
+
values: entry.values,
|
|
3301
|
+
present: [...entry.present],
|
|
3302
|
+
stale: [...entry.stale],
|
|
3303
|
+
unavailable: [...entry.unavailable],
|
|
3304
|
+
windows: entry.windows,
|
|
3305
|
+
tombstone: entry.tombstone,
|
|
3306
|
+
updatedAt: entry.updatedAt
|
|
3307
|
+
}])),
|
|
3308
|
+
connections: Object.fromEntries(Object.entries(connections).map(([identity, connection]) => [identity, {
|
|
3309
|
+
segments: connection.segments.map((segment) => ({
|
|
3310
|
+
edges: segment.edges.map((edge) => ({
|
|
3311
|
+
key: edge.key,
|
|
3312
|
+
ref: edge.ref
|
|
3313
|
+
})),
|
|
3314
|
+
start: segment.start,
|
|
3315
|
+
end: segment.end
|
|
3316
|
+
})),
|
|
3317
|
+
stale: connection.stale
|
|
3318
|
+
}])),
|
|
3319
|
+
live
|
|
3320
|
+
};
|
|
3321
|
+
};
|
|
3322
|
+
/**
|
|
3323
|
+
* Sets a capture onto a store: its entities, connections and live cursors
|
|
3324
|
+
* replace any held under the same keys, and nothing else of the store changes.
|
|
3325
|
+
*/
|
|
3326
|
+
const restoreRemote = (remote, capture) => ({
|
|
3327
|
+
...remote,
|
|
3328
|
+
entities: {
|
|
3329
|
+
...remote.entities,
|
|
3330
|
+
...Object.fromEntries(Object.entries(capture.entities).map(([key, entry]) => [key, {
|
|
3331
|
+
values: entry.values,
|
|
3332
|
+
present: new Set(entry.present),
|
|
3333
|
+
stale: new Set(entry.stale),
|
|
3334
|
+
unavailable: new Set(entry.unavailable),
|
|
3335
|
+
windows: entry.windows,
|
|
3336
|
+
tombstone: entry.tombstone,
|
|
3337
|
+
updatedAt: entry.updatedAt
|
|
3338
|
+
}]))
|
|
3339
|
+
},
|
|
3340
|
+
connections: {
|
|
3341
|
+
...remote.connections,
|
|
3342
|
+
...Object.fromEntries(Object.entries(capture.connections).map(([identity, connection]) => [identity, {
|
|
3343
|
+
segments: connection.segments.map((segment) => ({
|
|
3344
|
+
edges: segment.edges,
|
|
3345
|
+
start: segment.start,
|
|
3346
|
+
end: segment.end
|
|
3347
|
+
})),
|
|
3348
|
+
stale: connection.stale
|
|
3349
|
+
}]))
|
|
3350
|
+
},
|
|
3351
|
+
live: {
|
|
3352
|
+
...remote.live,
|
|
3353
|
+
...capture.live
|
|
3354
|
+
}
|
|
3355
|
+
});
|
|
3356
|
+
const encodeCapture = Schema.encodeSync(RemoteCapture);
|
|
3357
|
+
const decodeCapture = Schema.decodeUnknownResult(RemoteCapture);
|
|
3358
|
+
/**
|
|
3359
|
+
* The resume part for a Remote domain, for `SSR.plan`'s `parts`: it sends
|
|
3360
|
+
* what the plan's active Surfaces read from this domain's store, and nothing
|
|
3361
|
+
* else of it. `id` tells two domains in one application apart.
|
|
3362
|
+
*/
|
|
3363
|
+
const resumePart = (domain, options = {}) => ({
|
|
3364
|
+
id: options.id ?? "remote",
|
|
3365
|
+
covers: [RemoteRequirements.name, RemoteConnections.name],
|
|
3366
|
+
deferrable: (_key, entry) => typeof entry === "object" && entry !== null && DEFERRABLE in entry,
|
|
3367
|
+
capture: (model, projections) => encodeCapture(captureRemote(domain.store.get(model), projections)),
|
|
3368
|
+
restore: (model, value) => Result.match(decodeCapture(value), {
|
|
3369
|
+
onFailure: (error) => Result.fail(`Remote's state does not decode: ${error.message}`),
|
|
3370
|
+
onSuccess: (capture) => Result.succeed(domain.store.set(model, restoreRemote(domain.store.get(model), capture)))
|
|
3371
|
+
})
|
|
3372
|
+
});
|
|
3373
|
+
//#endregion
|
|
2964
3374
|
//#region src/index.ts
|
|
2965
3375
|
/**
|
|
2966
3376
|
* `foldkit-remote` — normalized application-facing server state.
|
|
@@ -3010,6 +3420,22 @@ const visibleStoreOf = (entities, optimistic) => {
|
|
|
3010
3420
|
}
|
|
3011
3421
|
return visible;
|
|
3012
3422
|
};
|
|
3423
|
+
/**
|
|
3424
|
+
* `Failed` naming a field the server settled without a value, when one is
|
|
3425
|
+
* among what `relation` reads of the entity; otherwise nothing, and the store
|
|
3426
|
+
* merely lacks the value. The reason stays with the server, so a view shows
|
|
3427
|
+
* the field it cannot have, not why.
|
|
3428
|
+
*/
|
|
3429
|
+
const unavailableFailure = (store, key, relation) => {
|
|
3430
|
+
const field = unavailableOf(store, key, relation);
|
|
3431
|
+
return field === void 0 ? void 0 : {
|
|
3432
|
+
_tag: "Failed",
|
|
3433
|
+
error: {
|
|
3434
|
+
_tag: "Unavailable",
|
|
3435
|
+
message: `The server answered without ${field}, and will not answer with it: the field is not available to this client.`
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3438
|
+
};
|
|
3013
3439
|
const readResults = /* @__PURE__ */ new WeakMap();
|
|
3014
3440
|
const memoRead = (snapshot, by, key, compute) => {
|
|
3015
3441
|
let byScope = readResults.get(snapshot);
|
|
@@ -3077,6 +3503,26 @@ const askedOf = (projection) => projection === void 0 ? nothingAsked : {
|
|
|
3077
3503
|
connections: connectionsOf(projection)
|
|
3078
3504
|
};
|
|
3079
3505
|
/**
|
|
3506
|
+
* Whether `outer` asks for everything `inner` does of one entity: each field,
|
|
3507
|
+
* each relation's own slice of its target, and each page window alike.
|
|
3508
|
+
*/
|
|
3509
|
+
const sliceCovers = (outer, inner) => outer.entity === inner.entity && inner.fields.every((field) => outer.fields.includes(field)) && Object.entries(inner.relations ?? {}).every(([field, relation]) => {
|
|
3510
|
+
const wider = outer.relations?.[field];
|
|
3511
|
+
return wider !== void 0 && sliceCovers(wider, relation);
|
|
3512
|
+
}) && Object.entries(inner.windows ?? {}).every(([field, window]) => outer.windows?.[field] !== void 0 && stableStringify(outer.windows[field]) === stableStringify(window));
|
|
3513
|
+
/**
|
|
3514
|
+
* Whether what `outer` asks includes everything `inner` asks: each list with
|
|
3515
|
+
* the slice it selects of its rows, and each entity with the slice it reads,
|
|
3516
|
+
* relations included. A Surface that shows a read is one whose projection
|
|
3517
|
+
* asks for all of it; one that reads part of it cannot be why it is fetched.
|
|
3518
|
+
* A Surface that asks for one entity in several pieces is already one
|
|
3519
|
+
* requirement here: projection metadata unions them per entity and id.
|
|
3520
|
+
*/
|
|
3521
|
+
const covers = (outer, inner) => {
|
|
3522
|
+
const { connections, requirements } = outer;
|
|
3523
|
+
return inner.connections.every((connection) => connections.some((wider) => wider.identity === connection.identity && (connection.select === void 0 || wider.select !== void 0 && sliceCovers(wider.select, connection.select)))) && inner.requirements.every((requirement) => requirements.some((wider) => wider.id === requirement.id && sliceCovers(wider, requirement)));
|
|
3524
|
+
};
|
|
3525
|
+
/**
|
|
3080
3526
|
* Plans what a projection asks against the Model. A connection the Model holds
|
|
3081
3527
|
* fresh contributes its visible items' selected fields to the entity plan; one
|
|
3082
3528
|
* it does not hold, or holds stale, is a query to run (its items are planned
|
|
@@ -3100,10 +3546,13 @@ const planAsked = (remote, asked, options) => {
|
|
|
3100
3546
|
const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
|
|
3101
3547
|
items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
|
|
3102
3548
|
}
|
|
3103
|
-
const
|
|
3549
|
+
const visible = visibleStoreOf(remote.entities, remote.optimistic);
|
|
3550
|
+
const read = [...asked.requirements, ...items];
|
|
3551
|
+
const planned = plan(visible, read, options);
|
|
3104
3552
|
return {
|
|
3105
3553
|
requirements: options.force === true ? planned : withoutFailedFields(remote, planned),
|
|
3106
|
-
queries
|
|
3554
|
+
queries,
|
|
3555
|
+
expires: options.freshness === void 0 ? null : deadlineOf(visible, read, options.freshness) ?? null
|
|
3107
3556
|
};
|
|
3108
3557
|
};
|
|
3109
3558
|
/**
|
|
@@ -3230,7 +3679,7 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
|
|
|
3230
3679
|
const client = yield* RemoteClient;
|
|
3231
3680
|
const at = now();
|
|
3232
3681
|
const result = yield* Effect.result(client.read({
|
|
3233
|
-
version:
|
|
3682
|
+
version: 4,
|
|
3234
3683
|
requests: requirements
|
|
3235
3684
|
}));
|
|
3236
3685
|
return Result.isFailure(result) ? {
|
|
@@ -3277,7 +3726,11 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3277
3726
|
dependenciesSchema: Schema.Struct({
|
|
3278
3727
|
requirements: Schema.Array(ReadRequest),
|
|
3279
3728
|
queries: Schema.Array(PlannedQuery),
|
|
3280
|
-
refresh: Schema.Number
|
|
3729
|
+
refresh: Schema.Number,
|
|
3730
|
+
expires: Schema.NullOr(Schema.Struct({
|
|
3731
|
+
at: Schema.Number,
|
|
3732
|
+
due: Schema.Array(ReadRequest)
|
|
3733
|
+
}))
|
|
3281
3734
|
}),
|
|
3282
3735
|
modelToDependencies: (model) => {
|
|
3283
3736
|
const remote = bound.store.get(model);
|
|
@@ -3289,10 +3742,11 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3289
3742
|
identity,
|
|
3290
3743
|
window,
|
|
3291
3744
|
select
|
|
3292
|
-
}))
|
|
3745
|
+
})),
|
|
3746
|
+
expires: planned.expires
|
|
3293
3747
|
};
|
|
3294
3748
|
},
|
|
3295
|
-
dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
|
|
3749
|
+
dependenciesToStream: ({ requirements, queries, refresh, expires }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
|
|
3296
3750
|
_tag: "ReadStarted",
|
|
3297
3751
|
requests: requirements,
|
|
3298
3752
|
refresh
|
|
@@ -3302,21 +3756,27 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3302
3756
|
})] : []], ...queries.length === 0 ? [] : [toMessage({
|
|
3303
3757
|
_tag: "QueryStarted",
|
|
3304
3758
|
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" }))
|
|
3759
|
+
})]]), Stream.merge(Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }), expires === null ? Stream.empty : Stream.fromEffect(Effect.as(Effect.sleep(Math.max(0, expires.at - now())), toMessage({
|
|
3760
|
+
_tag: "RefreshStarted",
|
|
3761
|
+
requests: expires.due
|
|
3762
|
+
})))))
|
|
3306
3763
|
};
|
|
3307
3764
|
};
|
|
3308
3765
|
/** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
|
|
3309
3766
|
const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
3310
3767
|
dependenciesSchema: Schema.Struct({
|
|
3311
3768
|
requirements: Schema.Array(ReadRequest),
|
|
3312
|
-
cursor: Schema.Number
|
|
3769
|
+
cursor: Schema.Number,
|
|
3770
|
+
floor: Schema.Number
|
|
3313
3771
|
}),
|
|
3314
3772
|
modelToDependencies: (model) => {
|
|
3315
3773
|
const requirements = requirementsOf(model);
|
|
3316
3774
|
const stream = liveStreamKey(requirements);
|
|
3775
|
+
const remote = bound.store.get(model);
|
|
3317
3776
|
return {
|
|
3318
3777
|
requirements,
|
|
3319
|
-
cursor:
|
|
3778
|
+
cursor: remote.live[stream]?.cursor ?? 0,
|
|
3779
|
+
floor: remote.refresh.floor
|
|
3320
3780
|
};
|
|
3321
3781
|
},
|
|
3322
3782
|
dependenciesToStream: ({ requirements, cursor }) => requirements.length === 0 ? Stream.empty : Stream.unwrap(Effect.gen(function* () {
|
|
@@ -3369,6 +3829,67 @@ const Remote = {
|
|
|
3369
3829
|
/** Whether a Message is one of Remote's, by tag. */
|
|
3370
3830
|
reduces: isRemoteMessage,
|
|
3371
3831
|
/**
|
|
3832
|
+
* Remote's Messages as one Schema, for a wrapper variant in the
|
|
3833
|
+
* application's union: `GotRemoteMessage: { message: Remote.Message }`. With
|
|
3834
|
+
* a wrapper, `update` matches the application's union exhaustively and
|
|
3835
|
+
* `Remote.fold` reduces what arrives inside it.
|
|
3836
|
+
*/
|
|
3837
|
+
Message: defineMessageUnion(remoteMessageCases),
|
|
3838
|
+
/**
|
|
3839
|
+
* A bound domain under one wrapper variant of the application's union, the
|
|
3840
|
+
* same shape Foldkit gives a Submodel: `toParentMessage` wraps Remote's
|
|
3841
|
+
* Messages, the fold reduces them, and its `fetch`, `mutate`, and
|
|
3842
|
+
* `subscriptions` yield the wrapper. Spreading `Remote.messages` and
|
|
3843
|
+
* narrowing with `Remote.reduces` remains the shorter path for an
|
|
3844
|
+
* application whose `update` only reduces.
|
|
3845
|
+
*
|
|
3846
|
+
* @example
|
|
3847
|
+
* ```ts
|
|
3848
|
+
* const Message = defineMessageUnion({ GotRemoteMessage: { message: Remote.Message } })
|
|
3849
|
+
* const foldData = Remote.fold(Data, message => Message.GotRemoteMessage({ message }))
|
|
3850
|
+
*
|
|
3851
|
+
* const update = (model: Model, message: Message): Return =>
|
|
3852
|
+
* Message.match(message, {
|
|
3853
|
+
* GotRemoteMessage: ({ message }) => foldData(model, message),
|
|
3854
|
+
* })
|
|
3855
|
+
* const subscriptions = Subscription.make<Model, Message, RemoteClient>()(() =>
|
|
3856
|
+
* foldData.subscriptions({ page: Surface.at(ProjectPage, …) }),
|
|
3857
|
+
* )
|
|
3858
|
+
* ```
|
|
3859
|
+
*/
|
|
3860
|
+
fold: (domain, toParentMessage) => {
|
|
3861
|
+
const fold = (model, message) => ({ model: domain.reduce(model, message) });
|
|
3862
|
+
return Object.assign(fold, {
|
|
3863
|
+
fetch: (ref) => mapMessage(domain.fetch(ref), toParentMessage),
|
|
3864
|
+
mutate: (...args) => {
|
|
3865
|
+
const started = domain.mutate(...args);
|
|
3866
|
+
return {
|
|
3867
|
+
...started,
|
|
3868
|
+
command: mapMessage(started.command, toParentMessage)
|
|
3869
|
+
};
|
|
3870
|
+
},
|
|
3871
|
+
subscriptions: (active, options) => markAll(Subscription.lift(brandEntries(domain.subscriptions(active, options)))({
|
|
3872
|
+
toChildModel: (model) => model,
|
|
3873
|
+
toParentMessage
|
|
3874
|
+
}))
|
|
3875
|
+
});
|
|
3876
|
+
},
|
|
3877
|
+
/**
|
|
3878
|
+
* A patch of one entity's values, for a mutation's `optimistic` list or an
|
|
3879
|
+
* overlay. Takes a `foldkit-entity` Entity or a Remote descriptor alike, so
|
|
3880
|
+
* the domain you declared once is the one you patch:
|
|
3881
|
+
* `Remote.patch(Project, 'p1', { name: 'Apollo II' })`. Values are the
|
|
3882
|
+
* fields' wire shape, as the server writes them; a relation is its ref key
|
|
3883
|
+
* (`'User:u1'`).
|
|
3884
|
+
*/
|
|
3885
|
+
patch: (entity, id, values) => descriptorOf(entity).patch(id, values),
|
|
3886
|
+
/**
|
|
3887
|
+
* A reference to one entity, for `ConnectionChange` and anything else that
|
|
3888
|
+
* names a row: `Remote.ref(Project, 'p3')`. Either kind of entity, as with
|
|
3889
|
+
* `Remote.patch`.
|
|
3890
|
+
*/
|
|
3891
|
+
ref: (entity, id) => descriptorOf(entity).ref(id),
|
|
3892
|
+
/**
|
|
3372
3893
|
* Declares a Remote domain and binds it to its place in the application Model
|
|
3373
3894
|
* in one step; the result carries the application-facing operations. The
|
|
3374
3895
|
* descriptor alone is `Remote.define`, the binding alone `Remote.at`.
|
|
@@ -3387,6 +3908,13 @@ const Remote = {
|
|
|
3387
3908
|
*/
|
|
3388
3909
|
at: bindRemote,
|
|
3389
3910
|
/**
|
|
3911
|
+
* The resume part for this domain, for `foldkit-ssr`'s `SSR.plan({ parts })`:
|
|
3912
|
+
* a server render sends what the plan's active Surfaces read from the store,
|
|
3913
|
+
* each connection with its boundaries and each live cursor with it, and
|
|
3914
|
+
* nothing else. `id` tells two domains in one application apart.
|
|
3915
|
+
*/
|
|
3916
|
+
resume: resumePart,
|
|
3917
|
+
/**
|
|
3390
3918
|
* A Projection node that reads a `RemoteData` value out of the store. The id
|
|
3391
3919
|
* is supplied by the caller, usually from a Surface's params. The assembled
|
|
3392
3920
|
* value is decoded against the Selection, so malformed server data surfaces
|
|
@@ -3410,7 +3938,7 @@ const Remote = {
|
|
|
3410
3938
|
const present = memoRead(store, store, `${key}\u0000${stableStringify(relation)}`, () => {
|
|
3411
3939
|
if (isTombstone(store, key)) return { _tag: "NotFound" };
|
|
3412
3940
|
const assembled = assemble(store, key, relation);
|
|
3413
|
-
if (assembled === void 0) return
|
|
3941
|
+
if (assembled === void 0) return unavailableFailure(store, key, relation);
|
|
3414
3942
|
const decoded = Schema.decodeUnknownResult(selection.schema)(assembled.values);
|
|
3415
3943
|
return Result.isFailure(decoded) ? {
|
|
3416
3944
|
_tag: "Failed",
|
|
@@ -3433,7 +3961,7 @@ const Remote = {
|
|
|
3433
3961
|
error: failure,
|
|
3434
3962
|
previous: present.value
|
|
3435
3963
|
} : present;
|
|
3436
|
-
if (
|
|
3964
|
+
if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
|
|
3437
3965
|
return failure === void 0 ? { _tag: "Initial" } : {
|
|
3438
3966
|
_tag: "Failed",
|
|
3439
3967
|
error: failure
|
|
@@ -3478,7 +4006,7 @@ const Remote = {
|
|
|
3478
4006
|
if (missing.length === 0) return store;
|
|
3479
4007
|
yield* Effect.annotateCurrentSpan("requirementCount", missing.length);
|
|
3480
4008
|
const result = yield* (yield* RemoteClient).read({
|
|
3481
|
-
version:
|
|
4009
|
+
version: 4,
|
|
3482
4010
|
requests: missing
|
|
3483
4011
|
});
|
|
3484
4012
|
return writeRead(store, missing, result, at);
|
|
@@ -3531,6 +4059,17 @@ const Remote = {
|
|
|
3531
4059
|
});
|
|
3532
4060
|
},
|
|
3533
4061
|
/**
|
|
4062
|
+
* Forgets everything the server told this Model: values, tombstones,
|
|
4063
|
+
* unavailable fields, connections, live cursors, failures. What is known is
|
|
4064
|
+
* known for a principal, and this is the one boundary for a login, a logout
|
|
4065
|
+
* or a switch of organization: called from `update`, it performs no I/O, and
|
|
4066
|
+
* every active Surface's read and live entries restart, so the screen asks
|
|
4067
|
+
* again as whoever the client now is. A read or stream begun before is
|
|
4068
|
+
* interrupted rather than landing after. A mutation in flight is treated as
|
|
4069
|
+
* applied: its answer writes nothing here.
|
|
4070
|
+
*/
|
|
4071
|
+
forget: (bound, model) => bound.store.set(model, forgetRemote(bound.store.get(model))),
|
|
4072
|
+
/**
|
|
3534
4073
|
* Writes a read result into the store, recording each field's applied window.
|
|
3535
4074
|
* `requests` are the planned requirements the result answers.
|
|
3536
4075
|
*/
|
|
@@ -3547,7 +4086,7 @@ const Remote = {
|
|
|
3547
4086
|
query: (request) => client.FoldkitRemoteQuery(request).pipe(Effect.provideContext(context)),
|
|
3548
4087
|
mutate: (request) => client.FoldkitRemoteMutate(request).pipe(Effect.provideContext(context)),
|
|
3549
4088
|
live: ({ requirements, after }) => client.FoldkitRemoteLive({
|
|
3550
|
-
version:
|
|
4089
|
+
version: 4,
|
|
3551
4090
|
requirements,
|
|
3552
4091
|
after
|
|
3553
4092
|
}).pipe(Stream.map(liveEventOf), Stream.provideContext(context))
|
|
@@ -3738,6 +4277,7 @@ const bindDomain = (definition, store) => {
|
|
|
3738
4277
|
roots: current
|
|
3739
4278
|
}).pipe(Effect.delay(options.grace ?? 0)))
|
|
3740
4279
|
};
|
|
4280
|
+
markAll(entries);
|
|
3741
4281
|
return entries;
|
|
3742
4282
|
},
|
|
3743
4283
|
plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
|
|
@@ -3756,7 +4296,7 @@ const bindDomain = (definition, store) => {
|
|
|
3756
4296
|
if (requirements.length === 0) return current;
|
|
3757
4297
|
const at = now();
|
|
3758
4298
|
const result = yield* client.read({
|
|
3759
|
-
version:
|
|
4299
|
+
version: 4,
|
|
3760
4300
|
requests: requirements
|
|
3761
4301
|
});
|
|
3762
4302
|
return reduce(current, {
|
|
@@ -3795,7 +4335,7 @@ const bindDomain = (definition, store) => {
|
|
|
3795
4335
|
if (remote.loading.size === 0) return void 0;
|
|
3796
4336
|
for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
|
|
3797
4337
|
if (edge.ref.entity !== relation.entity) continue;
|
|
3798
|
-
if (
|
|
4338
|
+
if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
|
|
3799
4339
|
}
|
|
3800
4340
|
};
|
|
3801
4341
|
const readPage = (remote, connection) => {
|
|
@@ -3805,8 +4345,9 @@ const bindDomain = (definition, store) => {
|
|
|
3805
4345
|
let refreshing = connection.stale;
|
|
3806
4346
|
const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
|
|
3807
4347
|
for (const edge of edges) {
|
|
3808
|
-
const
|
|
3809
|
-
|
|
4348
|
+
const key = entityKey(edge.ref.entity, edge.ref.id);
|
|
4349
|
+
const assembled = assemble(visible, key, relation);
|
|
4350
|
+
if (assembled === void 0) return unavailableFailure(visible, key, relation);
|
|
3810
4351
|
const decoded = Schema.decodeUnknownResult(select.schema)(assembled.values);
|
|
3811
4352
|
if (Result.isFailure(decoded)) return {
|
|
3812
4353
|
_tag: "Failed",
|
|
@@ -3931,6 +4472,59 @@ const bindDomain = (definition, store) => {
|
|
|
3931
4472
|
}
|
|
3932
4473
|
};
|
|
3933
4474
|
},
|
|
4475
|
+
why: (model, projection, options) => {
|
|
4476
|
+
const state = projection.read(model);
|
|
4477
|
+
const asked = askedOf(projection);
|
|
4478
|
+
const reading = options?.surfaces === void 0 ? void 0 : Object.values(options.surfaces).filter((active) => {
|
|
4479
|
+
const shown = active.projectionOf(model);
|
|
4480
|
+
return shown !== void 0 && covers(askedOf(shown), asked);
|
|
4481
|
+
}).map((active) => active.name);
|
|
4482
|
+
const withSurfaces = reading === void 0 ? {} : { surfaces: reading };
|
|
4483
|
+
switch (state._tag) {
|
|
4484
|
+
case "Ready": return {
|
|
4485
|
+
state: state._tag,
|
|
4486
|
+
message: "Everything it selects is here.",
|
|
4487
|
+
...withSurfaces
|
|
4488
|
+
};
|
|
4489
|
+
case "Refreshing": return {
|
|
4490
|
+
state: state._tag,
|
|
4491
|
+
message: "It is shown while a newer value is fetched.",
|
|
4492
|
+
...withSurfaces
|
|
4493
|
+
};
|
|
4494
|
+
case "Loading": return {
|
|
4495
|
+
state: state._tag,
|
|
4496
|
+
message: "A request for it is in flight.",
|
|
4497
|
+
...withSurfaces
|
|
4498
|
+
};
|
|
4499
|
+
case "NotFound": return {
|
|
4500
|
+
state: state._tag,
|
|
4501
|
+
message: "The server answered without it: it does not exist, or this principal may not see it.",
|
|
4502
|
+
...withSurfaces
|
|
4503
|
+
};
|
|
4504
|
+
case "Failed": return {
|
|
4505
|
+
state: state._tag,
|
|
4506
|
+
message: state.error._tag === "DecodeError" ? `What the server sent does not decode against the Selection: ${state.error.message}` : state.error._tag === "Unavailable" ? `${state.error.message} Select without it, or Data.refresh asks again.` : `Its request failed: ${state.error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`,
|
|
4507
|
+
...withSurfaces
|
|
4508
|
+
};
|
|
4509
|
+
case "Initial":
|
|
4510
|
+
if (reading === void 0) return {
|
|
4511
|
+
state: state._tag,
|
|
4512
|
+
reason: "Unknown",
|
|
4513
|
+
message: "Nothing is fetching it. Pass the active record you give Data.subscriptions as `surfaces` to tell whether an active Surface reads it."
|
|
4514
|
+
};
|
|
4515
|
+
return reading.length === 0 ? {
|
|
4516
|
+
state: state._tag,
|
|
4517
|
+
reason: "NotObserved",
|
|
4518
|
+
message: "No active Surface reads it, so nothing fetches it. Include it in a Surface that is active for this Model, in the record given to Data.subscriptions.",
|
|
4519
|
+
surfaces: reading
|
|
4520
|
+
} : {
|
|
4521
|
+
state: state._tag,
|
|
4522
|
+
reason: "NotFetching",
|
|
4523
|
+
message: `${reading.join(", ")} ${reading.length === 1 ? "reads it and is" : "read it and are"} active, yet nothing is fetching it. Remote's Subscriptions are most likely not installed: give the runtime Data.subscriptions (or Data.wiring) and a RemoteClient.`,
|
|
4524
|
+
surfaces: reading
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
},
|
|
3934
4528
|
filtered: (model, over, by, input) => {
|
|
3935
4529
|
assertRegistered(bound, "Query", definition.registry.queries, by.name);
|
|
3936
4530
|
const filters = by.body?.entity.name;
|
|
@@ -3962,6 +4556,7 @@ const bindDomain = (definition, store) => {
|
|
|
3962
4556
|
};
|
|
3963
4557
|
},
|
|
3964
4558
|
refresh: (model, target) => Remote.refresh(bound, model, target),
|
|
4559
|
+
forget: (model) => Remote.forget(bound, model),
|
|
3965
4560
|
overlay: (model, id, optimistic) => bound.store.set(model, updateRemote(bound.store.get(model), {
|
|
3966
4561
|
_tag: "OverlayShown",
|
|
3967
4562
|
id,
|
|
@@ -4054,6 +4649,6 @@ const pageSize = (window, key) => {
|
|
|
4054
4649
|
return size === void 0 ? {} : { [key]: size };
|
|
4055
4650
|
};
|
|
4056
4651
|
//#endregion
|
|
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 };
|
|
4652
|
+
export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, SettledFields, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, deadlineOf, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, forgetRemote, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isFieldUnavailable, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, setUnavailable, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unavailableOf, unknown, updateRemote, visibleItems, visibleStore, windowKey, withRefreshRequested, writeEntities, writeEntity, writeRead };
|
|
4058
4653
|
|
|
4059
4654
|
//# sourceMappingURL=index.mjs.map
|