foldkit-remote 0.7.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 +436 -168
- package/dist/index.d.mts +303 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +488 -32
- 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);
|
|
@@ -1545,6 +1623,7 @@ const inspectEntry = (key, entry) => ({
|
|
|
1545
1623
|
key,
|
|
1546
1624
|
present: [...entry.present],
|
|
1547
1625
|
stale: [...entry.stale],
|
|
1626
|
+
unavailable: [...entry.unavailable],
|
|
1548
1627
|
tombstone: entry.tombstone,
|
|
1549
1628
|
updatedAt: entry.updatedAt,
|
|
1550
1629
|
windows: { ...entry.windows }
|
|
@@ -1878,6 +1957,41 @@ const followRelation = (value, relation) => targetsOf(value, relation).map((ref)
|
|
|
1878
1957
|
...relation,
|
|
1879
1958
|
id: ref.id
|
|
1880
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
|
+
};
|
|
1881
1995
|
const plan = (store, requirements, options = {}) => {
|
|
1882
1996
|
const { freshness, force = false } = options;
|
|
1883
1997
|
const planned = [];
|
|
@@ -1989,7 +2103,7 @@ const gc = (state, roots) => {
|
|
|
1989
2103
|
* application's choice.
|
|
1990
2104
|
*/
|
|
1991
2105
|
/** Bump when the serialized shape changes; a mismatch discards the cache. */
|
|
1992
|
-
const REMOTE_CACHE_VERSION =
|
|
2106
|
+
const REMOTE_CACHE_VERSION = 5;
|
|
1993
2107
|
/**
|
|
1994
2108
|
* A snapshot of a Model, keeping the entity cache and only the connections
|
|
1995
2109
|
* named.
|
|
@@ -2012,13 +2126,14 @@ const sorted = (values) => [...values].sort();
|
|
|
2012
2126
|
* whatever order they were built in.
|
|
2013
2127
|
*/
|
|
2014
2128
|
const serializeStore = (snapshot, scope) => ({
|
|
2015
|
-
version:
|
|
2129
|
+
version: 5,
|
|
2016
2130
|
scope: scope ?? null,
|
|
2017
2131
|
connections: snapshot.connections,
|
|
2018
2132
|
entities: Object.fromEntries(Object.entries(snapshot.entities).map(([key, entry]) => [key, {
|
|
2019
2133
|
values: entry.values,
|
|
2020
2134
|
present: sorted(entry.present),
|
|
2021
2135
|
stale: sorted(entry.stale),
|
|
2136
|
+
unavailable: sorted(entry.unavailable),
|
|
2022
2137
|
tombstone: entry.tombstone,
|
|
2023
2138
|
updatedAt: entry.updatedAt,
|
|
2024
2139
|
windows: entry.windows
|
|
@@ -2033,6 +2148,7 @@ const parseEntry = (value) => {
|
|
|
2033
2148
|
if (entry.values === null || typeof entry.values !== "object" || Array.isArray(entry.values)) throw new Error("entry.values is not a record");
|
|
2034
2149
|
if (!isStringArray(entry.present)) throw new Error("entry.present is not a string array");
|
|
2035
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");
|
|
2036
2152
|
if (typeof entry.tombstone !== "boolean") throw new Error("entry.tombstone is not a boolean");
|
|
2037
2153
|
if (typeof entry.updatedAt !== "number") throw new Error("entry.updatedAt is not a number");
|
|
2038
2154
|
if (!isStringRecord(entry.windows)) throw new Error("entry.windows is not a string record");
|
|
@@ -2040,6 +2156,7 @@ const parseEntry = (value) => {
|
|
|
2040
2156
|
values: entry.values,
|
|
2041
2157
|
present: new Set(entry.present),
|
|
2042
2158
|
stale: new Set(entry.stale),
|
|
2159
|
+
unavailable: new Set(entry.unavailable),
|
|
2043
2160
|
tombstone: entry.tombstone,
|
|
2044
2161
|
updatedAt: entry.updatedAt,
|
|
2045
2162
|
windows: entry.windows
|
|
@@ -2098,7 +2215,7 @@ const hydrate = (raw, options = {}) => {
|
|
|
2098
2215
|
}
|
|
2099
2216
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2100
2217
|
const candidate = parsed;
|
|
2101
|
-
if (candidate.version !==
|
|
2218
|
+
if (candidate.version !== 5) return void 0;
|
|
2102
2219
|
if ((candidate.scope ?? null) !== (options.scope ?? null)) return void 0;
|
|
2103
2220
|
if (candidate.entities === null || typeof candidate.entities !== "object") return void 0;
|
|
2104
2221
|
if (Array.isArray(candidate.entities)) return void 0;
|
|
@@ -2293,7 +2410,32 @@ const noFailures = {
|
|
|
2293
2410
|
const emptyRefresh = {
|
|
2294
2411
|
generation: 0,
|
|
2295
2412
|
requested: /* @__PURE__ */ new Map(),
|
|
2296
|
-
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
|
+
};
|
|
2297
2439
|
};
|
|
2298
2440
|
const initialRemoteModel = {
|
|
2299
2441
|
entities: emptyStore,
|
|
@@ -2487,7 +2629,7 @@ const prunedRefresh = (refresh, retained) => {
|
|
|
2487
2629
|
* anything else is.
|
|
2488
2630
|
*/
|
|
2489
2631
|
const refreshedAt = (refresh, requests, connections = []) => {
|
|
2490
|
-
let at =
|
|
2632
|
+
let at = refresh.floor;
|
|
2491
2633
|
for (const mark of fieldMarks(requests)) at = Math.max(at, refresh.requested.get(mark) ?? 0);
|
|
2492
2634
|
for (const identity of connections) at = Math.max(at, refresh.requested.get(connectionMark(identity)) ?? 0);
|
|
2493
2635
|
return at;
|
|
@@ -2619,7 +2761,8 @@ const updateRemote = (model, message) => {
|
|
|
2619
2761
|
}
|
|
2620
2762
|
case "RefreshStarted": {
|
|
2621
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);
|
|
2622
|
-
const
|
|
2764
|
+
const marks = marksOf(message.requests);
|
|
2765
|
+
const entities = setUnavailable(setStale(asked, marks, true), marks, false);
|
|
2623
2766
|
const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
|
|
2624
2767
|
return entities === model.entities && failures === model.failures ? model : {
|
|
2625
2768
|
...model,
|
|
@@ -2974,12 +3117,13 @@ const writeRead = (store, requests, result, now = 0) => {
|
|
|
2974
3117
|
windows: entry?.windows
|
|
2975
3118
|
});
|
|
2976
3119
|
}
|
|
3120
|
+
const settled = new Set(result.settled.map((entry) => entityKey(entry.entity, entry.id)));
|
|
2977
3121
|
let answered = store;
|
|
2978
3122
|
for (const request of requests) {
|
|
2979
3123
|
const key = entityKey(request.entity, request.id);
|
|
2980
|
-
if (!returned.has(key)) answered = tombstone(answered, key);
|
|
3124
|
+
if (!returned.has(key) && !settled.has(key)) answered = tombstone(answered, key);
|
|
2981
3125
|
}
|
|
2982
|
-
return writeEntities(answered, writes, now);
|
|
3126
|
+
return setUnavailable(writeEntities(answered, writes, now), result.settled.map((entry) => [entityKey(entry.entity, entry.id), entry.fields]), true);
|
|
2983
3127
|
};
|
|
2984
3128
|
//#endregion
|
|
2985
3129
|
//#region src/policy.ts
|
|
@@ -3012,6 +3156,221 @@ const RemotePolicy = {
|
|
|
3012
3156
|
refreshes: (policy) => policy._tag !== "CacheFirst"
|
|
3013
3157
|
};
|
|
3014
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
|
|
3015
3374
|
//#region src/index.ts
|
|
3016
3375
|
/**
|
|
3017
3376
|
* `foldkit-remote` — normalized application-facing server state.
|
|
@@ -3061,6 +3420,22 @@ const visibleStoreOf = (entities, optimistic) => {
|
|
|
3061
3420
|
}
|
|
3062
3421
|
return visible;
|
|
3063
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
|
+
};
|
|
3064
3439
|
const readResults = /* @__PURE__ */ new WeakMap();
|
|
3065
3440
|
const memoRead = (snapshot, by, key, compute) => {
|
|
3066
3441
|
let byScope = readResults.get(snapshot);
|
|
@@ -3171,10 +3546,13 @@ const planAsked = (remote, asked, options) => {
|
|
|
3171
3546
|
const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
|
|
3172
3547
|
items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
|
|
3173
3548
|
}
|
|
3174
|
-
const
|
|
3549
|
+
const visible = visibleStoreOf(remote.entities, remote.optimistic);
|
|
3550
|
+
const read = [...asked.requirements, ...items];
|
|
3551
|
+
const planned = plan(visible, read, options);
|
|
3175
3552
|
return {
|
|
3176
3553
|
requirements: options.force === true ? planned : withoutFailedFields(remote, planned),
|
|
3177
|
-
queries
|
|
3554
|
+
queries,
|
|
3555
|
+
expires: options.freshness === void 0 ? null : deadlineOf(visible, read, options.freshness) ?? null
|
|
3178
3556
|
};
|
|
3179
3557
|
};
|
|
3180
3558
|
/**
|
|
@@ -3301,7 +3679,7 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
|
|
|
3301
3679
|
const client = yield* RemoteClient;
|
|
3302
3680
|
const at = now();
|
|
3303
3681
|
const result = yield* Effect.result(client.read({
|
|
3304
|
-
version:
|
|
3682
|
+
version: 4,
|
|
3305
3683
|
requests: requirements
|
|
3306
3684
|
}));
|
|
3307
3685
|
return Result.isFailure(result) ? {
|
|
@@ -3348,7 +3726,11 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3348
3726
|
dependenciesSchema: Schema.Struct({
|
|
3349
3727
|
requirements: Schema.Array(ReadRequest),
|
|
3350
3728
|
queries: Schema.Array(PlannedQuery),
|
|
3351
|
-
refresh: Schema.Number
|
|
3729
|
+
refresh: Schema.Number,
|
|
3730
|
+
expires: Schema.NullOr(Schema.Struct({
|
|
3731
|
+
at: Schema.Number,
|
|
3732
|
+
due: Schema.Array(ReadRequest)
|
|
3733
|
+
}))
|
|
3352
3734
|
}),
|
|
3353
3735
|
modelToDependencies: (model) => {
|
|
3354
3736
|
const remote = bound.store.get(model);
|
|
@@ -3360,10 +3742,11 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3360
3742
|
identity,
|
|
3361
3743
|
window,
|
|
3362
3744
|
select
|
|
3363
|
-
}))
|
|
3745
|
+
})),
|
|
3746
|
+
expires: planned.expires
|
|
3364
3747
|
};
|
|
3365
3748
|
},
|
|
3366
|
-
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({
|
|
3367
3750
|
_tag: "ReadStarted",
|
|
3368
3751
|
requests: requirements,
|
|
3369
3752
|
refresh
|
|
@@ -3373,21 +3756,27 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3373
3756
|
})] : []], ...queries.length === 0 ? [] : [toMessage({
|
|
3374
3757
|
_tag: "QueryStarted",
|
|
3375
3758
|
connections: queries.map((query) => query.identity)
|
|
3376
|
-
})]]), 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
|
+
})))))
|
|
3377
3763
|
};
|
|
3378
3764
|
};
|
|
3379
3765
|
/** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
|
|
3380
3766
|
const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
3381
3767
|
dependenciesSchema: Schema.Struct({
|
|
3382
3768
|
requirements: Schema.Array(ReadRequest),
|
|
3383
|
-
cursor: Schema.Number
|
|
3769
|
+
cursor: Schema.Number,
|
|
3770
|
+
floor: Schema.Number
|
|
3384
3771
|
}),
|
|
3385
3772
|
modelToDependencies: (model) => {
|
|
3386
3773
|
const requirements = requirementsOf(model);
|
|
3387
3774
|
const stream = liveStreamKey(requirements);
|
|
3775
|
+
const remote = bound.store.get(model);
|
|
3388
3776
|
return {
|
|
3389
3777
|
requirements,
|
|
3390
|
-
cursor:
|
|
3778
|
+
cursor: remote.live[stream]?.cursor ?? 0,
|
|
3779
|
+
floor: remote.refresh.floor
|
|
3391
3780
|
};
|
|
3392
3781
|
},
|
|
3393
3782
|
dependenciesToStream: ({ requirements, cursor }) => requirements.length === 0 ? Stream.empty : Stream.unwrap(Effect.gen(function* () {
|
|
@@ -3440,6 +3829,52 @@ const Remote = {
|
|
|
3440
3829
|
/** Whether a Message is one of Remote's, by tag. */
|
|
3441
3830
|
reduces: isRemoteMessage,
|
|
3442
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
|
+
/**
|
|
3443
3878
|
* A patch of one entity's values, for a mutation's `optimistic` list or an
|
|
3444
3879
|
* overlay. Takes a `foldkit-entity` Entity or a Remote descriptor alike, so
|
|
3445
3880
|
* the domain you declared once is the one you patch:
|
|
@@ -3473,6 +3908,13 @@ const Remote = {
|
|
|
3473
3908
|
*/
|
|
3474
3909
|
at: bindRemote,
|
|
3475
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
|
+
/**
|
|
3476
3918
|
* A Projection node that reads a `RemoteData` value out of the store. The id
|
|
3477
3919
|
* is supplied by the caller, usually from a Surface's params. The assembled
|
|
3478
3920
|
* value is decoded against the Selection, so malformed server data surfaces
|
|
@@ -3496,7 +3938,7 @@ const Remote = {
|
|
|
3496
3938
|
const present = memoRead(store, store, `${key}\u0000${stableStringify(relation)}`, () => {
|
|
3497
3939
|
if (isTombstone(store, key)) return { _tag: "NotFound" };
|
|
3498
3940
|
const assembled = assemble(store, key, relation);
|
|
3499
|
-
if (assembled === void 0) return
|
|
3941
|
+
if (assembled === void 0) return unavailableFailure(store, key, relation);
|
|
3500
3942
|
const decoded = Schema.decodeUnknownResult(selection.schema)(assembled.values);
|
|
3501
3943
|
return Result.isFailure(decoded) ? {
|
|
3502
3944
|
_tag: "Failed",
|
|
@@ -3564,7 +4006,7 @@ const Remote = {
|
|
|
3564
4006
|
if (missing.length === 0) return store;
|
|
3565
4007
|
yield* Effect.annotateCurrentSpan("requirementCount", missing.length);
|
|
3566
4008
|
const result = yield* (yield* RemoteClient).read({
|
|
3567
|
-
version:
|
|
4009
|
+
version: 4,
|
|
3568
4010
|
requests: missing
|
|
3569
4011
|
});
|
|
3570
4012
|
return writeRead(store, missing, result, at);
|
|
@@ -3617,6 +4059,17 @@ const Remote = {
|
|
|
3617
4059
|
});
|
|
3618
4060
|
},
|
|
3619
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
|
+
/**
|
|
3620
4073
|
* Writes a read result into the store, recording each field's applied window.
|
|
3621
4074
|
* `requests` are the planned requirements the result answers.
|
|
3622
4075
|
*/
|
|
@@ -3633,7 +4086,7 @@ const Remote = {
|
|
|
3633
4086
|
query: (request) => client.FoldkitRemoteQuery(request).pipe(Effect.provideContext(context)),
|
|
3634
4087
|
mutate: (request) => client.FoldkitRemoteMutate(request).pipe(Effect.provideContext(context)),
|
|
3635
4088
|
live: ({ requirements, after }) => client.FoldkitRemoteLive({
|
|
3636
|
-
version:
|
|
4089
|
+
version: 4,
|
|
3637
4090
|
requirements,
|
|
3638
4091
|
after
|
|
3639
4092
|
}).pipe(Stream.map(liveEventOf), Stream.provideContext(context))
|
|
@@ -3824,6 +4277,7 @@ const bindDomain = (definition, store) => {
|
|
|
3824
4277
|
roots: current
|
|
3825
4278
|
}).pipe(Effect.delay(options.grace ?? 0)))
|
|
3826
4279
|
};
|
|
4280
|
+
markAll(entries);
|
|
3827
4281
|
return entries;
|
|
3828
4282
|
},
|
|
3829
4283
|
plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
|
|
@@ -3842,7 +4296,7 @@ const bindDomain = (definition, store) => {
|
|
|
3842
4296
|
if (requirements.length === 0) return current;
|
|
3843
4297
|
const at = now();
|
|
3844
4298
|
const result = yield* client.read({
|
|
3845
|
-
version:
|
|
4299
|
+
version: 4,
|
|
3846
4300
|
requests: requirements
|
|
3847
4301
|
});
|
|
3848
4302
|
return reduce(current, {
|
|
@@ -3891,8 +4345,9 @@ const bindDomain = (definition, store) => {
|
|
|
3891
4345
|
let refreshing = connection.stale;
|
|
3892
4346
|
const edges = visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities);
|
|
3893
4347
|
for (const edge of edges) {
|
|
3894
|
-
const
|
|
3895
|
-
|
|
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);
|
|
3896
4351
|
const decoded = Schema.decodeUnknownResult(select.schema)(assembled.values);
|
|
3897
4352
|
if (Result.isFailure(decoded)) return {
|
|
3898
4353
|
_tag: "Failed",
|
|
@@ -4048,7 +4503,7 @@ const bindDomain = (definition, store) => {
|
|
|
4048
4503
|
};
|
|
4049
4504
|
case "Failed": return {
|
|
4050
4505
|
state: state._tag,
|
|
4051
|
-
message: state.error._tag === "DecodeError" ? `What the server sent does not decode against the Selection: ${state.error.message}` : `Its request failed: ${state.error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`,
|
|
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.`,
|
|
4052
4507
|
...withSurfaces
|
|
4053
4508
|
};
|
|
4054
4509
|
case "Initial":
|
|
@@ -4101,6 +4556,7 @@ const bindDomain = (definition, store) => {
|
|
|
4101
4556
|
};
|
|
4102
4557
|
},
|
|
4103
4558
|
refresh: (model, target) => Remote.refresh(bound, model, target),
|
|
4559
|
+
forget: (model) => Remote.forget(bound, model),
|
|
4104
4560
|
overlay: (model, id, optimistic) => bound.store.set(model, updateRemote(bound.store.get(model), {
|
|
4105
4561
|
_tag: "OverlayShown",
|
|
4106
4562
|
id,
|
|
@@ -4193,6 +4649,6 @@ const pageSize = (window, key) => {
|
|
|
4193
4649
|
return size === void 0 ? {} : { [key]: size };
|
|
4194
4650
|
};
|
|
4195
4651
|
//#endregion
|
|
4196
|
-
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, 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, 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 };
|
|
4197
4653
|
|
|
4198
4654
|
//# sourceMappingURL=index.mjs.map
|