foldkit-remote 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +582 -193
- package/dist/index.d.mts +596 -42
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1101 -239
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -8
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
|
|
1
|
+
import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaAST, SchemaGetter, Stream } from "effect";
|
|
2
2
|
import * as Domain from "foldkit-entity";
|
|
3
|
-
import { Entity as Entity$1, Expr, Query as Query$1, SelectionTypeId, evaluate } from "foldkit-entity";
|
|
3
|
+
import { Entity as Entity$1, Expr, Query as Query$1, SchemaShape, SelectionTypeId, evaluate } from "foldkit-entity";
|
|
4
|
+
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";
|
|
@@ -108,7 +110,31 @@ const requirementsOf = (projection) => RemoteRequirements.get(projection.metadat
|
|
|
108
110
|
/** The query connections a Projection carries. */
|
|
109
111
|
const connectionsOf = (projection) => RemoteConnections.get(projection.metadata);
|
|
110
112
|
//#endregion
|
|
113
|
+
//#region src/data.ts
|
|
114
|
+
/**
|
|
115
|
+
* Whether two values the server sent are the same data: equal primitives, or
|
|
116
|
+
* arrays and plain objects whose entries are. Anything else is compared by
|
|
117
|
+
* identity.
|
|
118
|
+
*/
|
|
119
|
+
const sameData = (left, right) => {
|
|
120
|
+
if (Object.is(left, right)) return true;
|
|
121
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
|
|
122
|
+
if (Array.isArray(left)) return Array.isArray(right) && left.length === right.length && left.every((item, index) => sameData(item, right[index]));
|
|
123
|
+
if (!isPlain(left) || !isPlain(right)) return false;
|
|
124
|
+
const keys = Object.keys(left);
|
|
125
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameData(left[key], right[key]));
|
|
126
|
+
};
|
|
127
|
+
const isPlain = (value) => {
|
|
128
|
+
const prototype = Object.getPrototypeOf(value);
|
|
129
|
+
return prototype === Object.prototype || prototype === null;
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
111
132
|
//#region src/relation.ts
|
|
133
|
+
/**
|
|
134
|
+
* Relation values on the wire and in the store: a ref key (`"Entity:id"`), a
|
|
135
|
+
* nullable ref key, an array of ref keys, or a page of ref keys. The planner
|
|
136
|
+
* follows them into the store; the server follows them into the next read.
|
|
137
|
+
*/
|
|
112
138
|
/** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
|
|
113
139
|
const RelationAnnotation = "foldkitRemoteRelation";
|
|
114
140
|
/** The target entity's name on the same codec, so a nested selection of another entity is refused. */
|
|
@@ -174,8 +200,8 @@ const shapeOf = (ast, nullable) => {
|
|
|
174
200
|
nullable,
|
|
175
201
|
entity: String(ast.annotations?.[RelationEntityAnnotation])
|
|
176
202
|
};
|
|
177
|
-
if (ast
|
|
178
|
-
const item = ast.rest
|
|
203
|
+
if (SchemaAST.isArrays(ast)) {
|
|
204
|
+
const item = ast.rest[0];
|
|
179
205
|
const inner = item === void 0 ? void 0 : shapeOf(item, false);
|
|
180
206
|
return inner?.kind === "one" ? {
|
|
181
207
|
kind: "many",
|
|
@@ -183,10 +209,9 @@ const shapeOf = (ast, nullable) => {
|
|
|
183
209
|
entity: inner.entity
|
|
184
210
|
} : void 0;
|
|
185
211
|
}
|
|
186
|
-
if (ast
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
for (const member of members) {
|
|
212
|
+
if (SchemaAST.isUnion(ast)) {
|
|
213
|
+
const optional = ast.types.some(SchemaShape.isNullish);
|
|
214
|
+
for (const member of ast.types) {
|
|
190
215
|
const shape = shapeOf(member, optional);
|
|
191
216
|
if (shape !== void 0) return shape;
|
|
192
217
|
}
|
|
@@ -199,8 +224,8 @@ const shapeOf = (ast, nullable) => {
|
|
|
199
224
|
*
|
|
200
225
|
* Presence is tracked **separately** from values, so `undefined`/`null`/absent/
|
|
201
226
|
* stale/not-found are distinct states and a missing field is never inferred from
|
|
202
|
-
* `value === undefined`. Nothing here performs I/O
|
|
203
|
-
*
|
|
227
|
+
* `value === undefined`. Nothing here performs I/O or mutates its input; an
|
|
228
|
+
* operation that changes nothing returns the store it was given.
|
|
204
229
|
*/
|
|
205
230
|
/** The separator keeps an untrusted id from colliding with a prototype key. */
|
|
206
231
|
const entityKey = (entity, id) => `${entity}:${id}`;
|
|
@@ -208,6 +233,7 @@ const emptyEntry = {
|
|
|
208
233
|
values: {},
|
|
209
234
|
present: /* @__PURE__ */ new Set(),
|
|
210
235
|
stale: /* @__PURE__ */ new Set(),
|
|
236
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
211
237
|
tombstone: false,
|
|
212
238
|
updatedAt: 0,
|
|
213
239
|
windows: {}
|
|
@@ -220,10 +246,12 @@ const replace = (store, key, entry) => ({
|
|
|
220
246
|
const written = (previous, values, now, windows) => {
|
|
221
247
|
const present = new Set(previous.present);
|
|
222
248
|
const stale = new Set(previous.stale);
|
|
249
|
+
const unavailable = new Set(previous.unavailable);
|
|
223
250
|
const nextWindows = { ...previous.windows };
|
|
224
251
|
for (const field of Object.keys(values)) {
|
|
225
252
|
present.add(field);
|
|
226
253
|
stale.delete(field);
|
|
254
|
+
unavailable.delete(field);
|
|
227
255
|
const requested = windows?.[field];
|
|
228
256
|
if (requested !== void 0 && requested !== "") nextWindows[field] = requested;
|
|
229
257
|
else delete nextWindows[field];
|
|
@@ -232,12 +260,13 @@ const written = (previous, values, now, windows) => {
|
|
|
232
260
|
}
|
|
233
261
|
}
|
|
234
262
|
return {
|
|
235
|
-
values: {
|
|
263
|
+
values: Object.keys(values).every((field) => Object.hasOwn(previous.values, field) && sameData(previous.values[field], values[field])) ? previous.values : {
|
|
236
264
|
...previous.values,
|
|
237
265
|
...values
|
|
238
266
|
},
|
|
239
267
|
present,
|
|
240
268
|
stale,
|
|
269
|
+
unavailable,
|
|
241
270
|
tombstone: false,
|
|
242
271
|
updatedAt: now,
|
|
243
272
|
windows: nextWindows
|
|
@@ -251,7 +280,7 @@ const written = (previous, values, now, windows) => {
|
|
|
251
280
|
const writeEntities = (store, writes, now = 0) => {
|
|
252
281
|
if (writes.length === 0) return store;
|
|
253
282
|
const next = { ...store };
|
|
254
|
-
for (const write of writes) next[write.key] = written(next[write.key] ?? emptyEntry, write.values, now, write.windows);
|
|
283
|
+
for (const write of writes) next[write.key] = written(next[write.key] ?? emptyEntry, write.values, write.updatedAt ?? now, write.windows);
|
|
255
284
|
return next;
|
|
256
285
|
};
|
|
257
286
|
/** `writeEntities` for one entity. */
|
|
@@ -287,11 +316,40 @@ const setStale = (store, marks, stale) => {
|
|
|
287
316
|
const markStale = (store, key, fields) => setStale(store, [[key, fields]], true);
|
|
288
317
|
/** Ends a refresh that did not land: the fields are present again as they were. */
|
|
289
318
|
const clearStale = (store, key, fields) => setStale(store, [[key, fields]], false);
|
|
319
|
+
/**
|
|
320
|
+
* Records fields the server settled without a value. A present field is left
|
|
321
|
+
* as it is: the answer that settled it was not about the value it holds.
|
|
322
|
+
* `false` forgets the marks, which is how a refresh asks again.
|
|
323
|
+
*/
|
|
324
|
+
const setUnavailable = (store, marks, unavailable) => {
|
|
325
|
+
let next;
|
|
326
|
+
for (const [key, fields] of marks) {
|
|
327
|
+
const previous = (next ?? store)[key] ?? emptyEntry;
|
|
328
|
+
if (previous.tombstone) continue;
|
|
329
|
+
const marked = new Set(previous.unavailable);
|
|
330
|
+
for (const field of fields) if (unavailable) {
|
|
331
|
+
if (!previous.present.has(field)) marked.add(field);
|
|
332
|
+
} else marked.delete(field);
|
|
333
|
+
if (marked.size === previous.unavailable.size) continue;
|
|
334
|
+
next ??= { ...store };
|
|
335
|
+
next[key] = {
|
|
336
|
+
...previous,
|
|
337
|
+
unavailable: marked
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return next ?? store;
|
|
341
|
+
};
|
|
342
|
+
/** Whether the server settled this field without a value, and nothing has written it since. */
|
|
343
|
+
const isFieldUnavailable = (store, key, field) => {
|
|
344
|
+
const value = store[key];
|
|
345
|
+
return value !== void 0 && !value.tombstone && value.unavailable.has(field);
|
|
346
|
+
};
|
|
290
347
|
/** Records that the entity is known to be absent, so it is not refetched. */
|
|
291
|
-
const tombstone = (store, key) => replace(store, key, {
|
|
348
|
+
const tombstone = (store, key) => isTombstone(store, key) ? store : replace(store, key, {
|
|
292
349
|
values: {},
|
|
293
350
|
present: /* @__PURE__ */ new Set(),
|
|
294
351
|
stale: /* @__PURE__ */ new Set(),
|
|
352
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
295
353
|
tombstone: true,
|
|
296
354
|
updatedAt: 0,
|
|
297
355
|
windows: {}
|
|
@@ -329,7 +387,8 @@ const readField = (store, key, field) => {
|
|
|
329
387
|
};
|
|
330
388
|
/**
|
|
331
389
|
* 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
|
|
390
|
+
* (absent), so it returns an empty list and the entity is not refetched, and a
|
|
391
|
+
* field the server settled without a value is known the same way. A
|
|
333
392
|
* requested window that differs from the one a field was fetched with also
|
|
334
393
|
* marks it missing, but only when the stored window is known (never `""`), so a
|
|
335
394
|
* writer that does not record windows cannot cause a refetch loop.
|
|
@@ -338,7 +397,9 @@ const missingFields = (store, key, fields, windows) => {
|
|
|
338
397
|
const value = store[key];
|
|
339
398
|
if (value !== void 0 && value.tombstone) return [];
|
|
340
399
|
return [...fields].filter((field) => {
|
|
341
|
-
if (value === void 0
|
|
400
|
+
if (value === void 0) return true;
|
|
401
|
+
if (!value.present.has(field)) return !value.unavailable.has(field);
|
|
402
|
+
if (value.stale.has(field)) return true;
|
|
342
403
|
const requested = windows?.[field];
|
|
343
404
|
const stored = value.windows[field];
|
|
344
405
|
return requested !== void 0 && stored !== void 0 && stored !== "" && stored !== requested;
|
|
@@ -407,8 +468,11 @@ const mutationStatus = (state, requestId) => {
|
|
|
407
468
|
* with the same `requestId` (a retry, or a live event describing the same change)
|
|
408
469
|
* does not re-apply the entities, but still clears `pending`.
|
|
409
470
|
*/
|
|
410
|
-
const reconcileMutation = (store, state, requestId,
|
|
411
|
-
const next = state.applied.has(requestId) ? store : deleted.reduce((current, gone) => tombstone(current, entityKey(gone.entity, gone.id)), entities.
|
|
471
|
+
const reconcileMutation = (store, state, requestId, answer) => {
|
|
472
|
+
const next = state.applied.has(requestId) ? store : (answer.deleted ?? []).reduce((current, gone) => tombstone(current, entityKey(gone.entity, gone.id)), writeEntities(store, answer.entities.map((entity) => ({
|
|
473
|
+
key: entityKey(entity.entity, entity.id),
|
|
474
|
+
values: entity.values
|
|
475
|
+
})), answer.now));
|
|
412
476
|
const applied = remember(state.applied, requestId);
|
|
413
477
|
const pending = new Set(state.pending);
|
|
414
478
|
pending.delete(requestId);
|
|
@@ -588,7 +652,7 @@ const Query = {
|
|
|
588
652
|
* server refuses a mismatch with `RemoteProtocolError`, so a shape change never
|
|
589
653
|
* drifts silently: bump it whenever `ReadRequest` or `LiveRequirement` change.
|
|
590
654
|
*/
|
|
591
|
-
const REMOTE_PROTOCOL_VERSION =
|
|
655
|
+
const REMOTE_PROTOCOL_VERSION = 4;
|
|
592
656
|
var RemoteReadError = class extends Schema.TaggedError()("RemoteReadError", { message: Schema.String }) {};
|
|
593
657
|
var RemoteMutationError = class extends Schema.TaggedError()("RemoteMutationError", { message: Schema.String }) {};
|
|
594
658
|
var RemoteLiveError = class extends Schema.TaggedError()("RemoteLiveError", { message: Schema.String }) {};
|
|
@@ -649,7 +713,20 @@ const NormalizedEntity = Schema.Struct({
|
|
|
649
713
|
id: Schema.String,
|
|
650
714
|
values: Schema.Record(Schema.String, Schema.Unknown)
|
|
651
715
|
});
|
|
652
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Fields a read asked for that the server answered without, and will not
|
|
718
|
+
* answer with: the client stops asking. It says nothing about why, so a rule
|
|
719
|
+
* that withholds a field is not itself on the wire.
|
|
720
|
+
*/
|
|
721
|
+
const SettledFields = Schema.Struct({
|
|
722
|
+
entity: Schema.String,
|
|
723
|
+
id: Schema.String,
|
|
724
|
+
fields: Schema.Array(Schema.String)
|
|
725
|
+
});
|
|
726
|
+
const ReadBatchResult = Schema.Struct({
|
|
727
|
+
entities: Schema.Array(NormalizedEntity),
|
|
728
|
+
settled: Schema.Array(SettledFields)
|
|
729
|
+
});
|
|
653
730
|
const MutationRequest = Schema.Struct({
|
|
654
731
|
/** Stable across transport retries so a mutation is not applied twice. */
|
|
655
732
|
requestId: Schema.String,
|
|
@@ -755,7 +832,13 @@ const WireBoundary = Schema.Union([
|
|
|
755
832
|
const QueryRequest = Schema.Struct({
|
|
756
833
|
query: Schema.String,
|
|
757
834
|
input: Schema.Unknown,
|
|
758
|
-
window: WindowSchema
|
|
835
|
+
window: WindowSchema,
|
|
836
|
+
/**
|
|
837
|
+
* What to read of each item, as the relation slice a read asks the server
|
|
838
|
+
* for. Absent for a query run without one: the response carries only edges,
|
|
839
|
+
* and the client's read entry fetches fields in a second round trip.
|
|
840
|
+
*/
|
|
841
|
+
select: Schema.optional(RelationRequest)
|
|
759
842
|
});
|
|
760
843
|
const QueryEdge = Schema.Struct({
|
|
761
844
|
entity: Schema.String,
|
|
@@ -765,7 +848,14 @@ const QueryEdge = Schema.Struct({
|
|
|
765
848
|
const QueryResult = Schema.Struct({
|
|
766
849
|
edges: Schema.Array(QueryEdge),
|
|
767
850
|
start: WireBoundary,
|
|
768
|
-
end: WireBoundary
|
|
851
|
+
end: WireBoundary,
|
|
852
|
+
/**
|
|
853
|
+
* The selected fields of the page's items, in the same shape a read batch
|
|
854
|
+
* returns. Absent from a server that predates payload planning: the client
|
|
855
|
+
* reads it as empty and fetches fields as before.
|
|
856
|
+
*/
|
|
857
|
+
entities: Schema.optional(Schema.Array(NormalizedEntity)),
|
|
858
|
+
settled: Schema.optional(Schema.Array(SettledFields))
|
|
769
859
|
});
|
|
770
860
|
const QueryRpc = Rpc.make("FoldkitRemoteQuery", {
|
|
771
861
|
payload: QueryRequest,
|
|
@@ -833,7 +923,8 @@ const coalesceQueries = (query, options = {}) => Effect.gen(function* () {
|
|
|
833
923
|
key: stableStringify([
|
|
834
924
|
request.query,
|
|
835
925
|
request.input,
|
|
836
|
-
request.window
|
|
926
|
+
request.window,
|
|
927
|
+
request.select ?? null
|
|
837
928
|
]),
|
|
838
929
|
request
|
|
839
930
|
}), resolver);
|
|
@@ -867,7 +958,7 @@ const coalesceReads = (read, options = {}) => Effect.gen(function* () {
|
|
|
867
958
|
for (const entry of entries) if (own.has(entry.request.key)) owned.set(entry.request.key, entry.request.requirement);
|
|
868
959
|
yield* Effect.forEach(readsOf(owned), ({ keys, requests }) => Effect.gen(function* () {
|
|
869
960
|
const exit = yield* Effect.exit(read({
|
|
870
|
-
version:
|
|
961
|
+
version: 4,
|
|
871
962
|
requests
|
|
872
963
|
}));
|
|
873
964
|
for (const key of keys) {
|
|
@@ -883,10 +974,19 @@ const coalesceReads = (read, options = {}) => Effect.gen(function* () {
|
|
|
883
974
|
});
|
|
884
975
|
let resolver = RequestResolver.make(runAll);
|
|
885
976
|
if (options.window !== void 0) resolver = RequestResolver.setDelay(resolver, Duration.fromInputUnsafe(options.window));
|
|
886
|
-
return (batch) => batch.requests.length === 0 ? Effect.succeed({
|
|
977
|
+
return (batch) => batch.requests.length === 0 ? Effect.succeed({
|
|
978
|
+
entities: [],
|
|
979
|
+
settled: []
|
|
980
|
+
}) : Effect.forEach(batch.requests, (requirement) => Effect.request(new ReadRequirement({
|
|
887
981
|
key: requirementKey(requirement),
|
|
888
982
|
requirement
|
|
889
|
-
}), resolver), { concurrency: "unbounded" }).pipe(Effect.map((results) =>
|
|
983
|
+
}), resolver), { concurrency: "unbounded" }).pipe(Effect.map((results) => {
|
|
984
|
+
const distinct = [...new Set(results)];
|
|
985
|
+
return {
|
|
986
|
+
entities: distinct.flatMap((result) => result.entities),
|
|
987
|
+
settled: distinct.flatMap((result) => result.settled)
|
|
988
|
+
};
|
|
989
|
+
}));
|
|
890
990
|
});
|
|
891
991
|
//#endregion
|
|
892
992
|
//#region src/client.ts
|
|
@@ -1302,6 +1402,24 @@ const assemble = (store, key, requirement) => {
|
|
|
1302
1402
|
refreshing
|
|
1303
1403
|
};
|
|
1304
1404
|
};
|
|
1405
|
+
/**
|
|
1406
|
+
* The first field `requirement` reads, at any depth, that the server settled
|
|
1407
|
+
* without a value, as `Entity:id.field`. A read that consults it says why it
|
|
1408
|
+
* cannot assemble rather than waiting on a field nothing will fetch.
|
|
1409
|
+
*/
|
|
1410
|
+
const unavailableOf = (store, key, requirement) => {
|
|
1411
|
+
for (const field of requirement.fields) {
|
|
1412
|
+
if (isFieldUnavailable(store, key, field)) return `${key}.${field}`;
|
|
1413
|
+
const relation = requirement.relations?.[field];
|
|
1414
|
+
if (relation === void 0) continue;
|
|
1415
|
+
const value = readField(store, key, field);
|
|
1416
|
+
if (Option.isNone(value) || value.value === null || value.value === void 0) continue;
|
|
1417
|
+
for (const ref of targetsOf(value.value, relation)) {
|
|
1418
|
+
const found = unavailableOf(store, entityKey(ref.entity, ref.id), relation);
|
|
1419
|
+
if (found !== void 0) return found;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1305
1423
|
const assembleTarget = (store, ref, relation) => {
|
|
1306
1424
|
const key = entityKey(ref.entity, ref.id);
|
|
1307
1425
|
return isTombstone(store, key) ? "absent" : assemble(store, key, relation);
|
|
@@ -1545,6 +1663,7 @@ const inspectEntry = (key, entry) => ({
|
|
|
1545
1663
|
key,
|
|
1546
1664
|
present: [...entry.present],
|
|
1547
1665
|
stale: [...entry.stale],
|
|
1666
|
+
unavailable: [...entry.unavailable],
|
|
1548
1667
|
tombstone: entry.tombstone,
|
|
1549
1668
|
updatedAt: entry.updatedAt,
|
|
1550
1669
|
windows: { ...entry.windows }
|
|
@@ -1615,12 +1734,13 @@ const ConnectionChange = {
|
|
|
1615
1734
|
const beginOptimistic = (optimistic, requestId, operations) => {
|
|
1616
1735
|
const patches = operations.filter((operation) => !isConnectionChange(operation));
|
|
1617
1736
|
const changes = operations.filter(isConnectionChange);
|
|
1737
|
+
if (patches.length === 0 && changes.length === 0) return optimistic;
|
|
1618
1738
|
return {
|
|
1619
1739
|
layers: patches.length === 0 ? optimistic.layers : [...optimistic.layers, {
|
|
1620
1740
|
id: requestId,
|
|
1621
1741
|
patches
|
|
1622
1742
|
}],
|
|
1623
|
-
overlays: [...optimistic.overlays, ...toOverlays(requestId, changes)]
|
|
1743
|
+
overlays: changes.length === 0 ? optimistic.overlays : [...optimistic.overlays, ...toOverlays(requestId, changes)]
|
|
1624
1744
|
};
|
|
1625
1745
|
};
|
|
1626
1746
|
/**
|
|
@@ -1629,8 +1749,9 @@ const beginOptimistic = (optimistic, requestId, operations) => {
|
|
|
1629
1749
|
* its place among other pending inserts.
|
|
1630
1750
|
*/
|
|
1631
1751
|
const confirm = (optimistic, requestId, id, changes) => {
|
|
1632
|
-
const confirmed = toOverlays(id, changes);
|
|
1633
1752
|
const at = optimistic.overlays.findIndex((overlay) => overlay.id === requestId);
|
|
1753
|
+
if (at === -1 && changes.length === 0) return optimistic;
|
|
1754
|
+
const confirmed = toOverlays(id, changes);
|
|
1634
1755
|
const others = optimistic.overlays.filter((overlay) => overlay.id !== requestId);
|
|
1635
1756
|
return {
|
|
1636
1757
|
...optimistic,
|
|
@@ -1649,37 +1770,62 @@ const addLayer = (optimistic, layer) => ({
|
|
|
1649
1770
|
...optimistic,
|
|
1650
1771
|
layers: [...optimistic.layers, layer]
|
|
1651
1772
|
});
|
|
1652
|
-
const removeLayer = (optimistic, id) =>
|
|
1653
|
-
|
|
1654
|
-
layers
|
|
1655
|
-
|
|
1773
|
+
const removeLayer = (optimistic, id) => {
|
|
1774
|
+
const layers = optimistic.layers.filter((layer) => layer.id !== id);
|
|
1775
|
+
return layers.length === optimistic.layers.length ? optimistic : {
|
|
1776
|
+
...optimistic,
|
|
1777
|
+
layers
|
|
1778
|
+
};
|
|
1779
|
+
};
|
|
1656
1780
|
const addOverlay = (optimistic, overlay) => ({
|
|
1657
1781
|
...optimistic,
|
|
1658
1782
|
overlays: [...optimistic.overlays, overlay]
|
|
1659
1783
|
});
|
|
1660
|
-
const removeOverlay = (optimistic, id) =>
|
|
1661
|
-
|
|
1662
|
-
overlays
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1784
|
+
const removeOverlay = (optimistic, id) => {
|
|
1785
|
+
const overlays = optimistic.overlays.filter((overlay) => overlay.id !== id);
|
|
1786
|
+
return overlays.length === optimistic.overlays.length ? optimistic : {
|
|
1787
|
+
...optimistic,
|
|
1788
|
+
overlays
|
|
1789
|
+
};
|
|
1790
|
+
};
|
|
1791
|
+
/**
|
|
1792
|
+
* Base store with every layer applied in order. Later layers win. A patch is
|
|
1793
|
+
* not news from the server, so it keeps the date of the value under it; an
|
|
1794
|
+
* entity only a request holds (a temporary id) is not the server's to age, and
|
|
1795
|
+
* is dated `Infinity`, so freshness never plans or times it.
|
|
1796
|
+
*/
|
|
1797
|
+
const visibleStore = (base, optimistic) => writeEntities(base, optimistic.layers.flatMap((layer) => layer.patches.map((patch) => {
|
|
1798
|
+
const key = entityKey(patch.entity, patch.id);
|
|
1799
|
+
const under = base[key];
|
|
1800
|
+
return {
|
|
1801
|
+
key,
|
|
1802
|
+
values: patch.values,
|
|
1803
|
+
updatedAt: under === void 0 || under.tombstone ? Infinity : under.updatedAt
|
|
1804
|
+
};
|
|
1805
|
+
})));
|
|
1666
1806
|
/**
|
|
1667
1807
|
* A fresh page is newer than any settled overlay it covers: an edge the page
|
|
1668
1808
|
* carries no longer needs an insert overlay, and a `remove` overlay hiding it
|
|
1669
1809
|
* is stale (the server says it is back). A pending request's overlays are
|
|
1670
1810
|
* left alone; they settle with the request.
|
|
1671
1811
|
*/
|
|
1672
|
-
const pruneOverlays = (optimistic, connection, covered, pending) =>
|
|
1673
|
-
|
|
1674
|
-
overlays
|
|
1812
|
+
const pruneOverlays = (optimistic, connection, covered, pending) => {
|
|
1813
|
+
let changed = false;
|
|
1814
|
+
const overlays = optimistic.overlays.flatMap((overlay) => {
|
|
1675
1815
|
if (overlay.connection !== connection || pending.has(overlay.id)) return [overlay];
|
|
1676
1816
|
const edges = overlay.edges.filter((edge) => !covered.has(edge.key));
|
|
1817
|
+
if (edges.length > 0 && edges.length === overlay.edges.length) return [overlay];
|
|
1818
|
+
changed = true;
|
|
1677
1819
|
return edges.length === 0 ? [] : [{
|
|
1678
1820
|
...overlay,
|
|
1679
1821
|
edges
|
|
1680
1822
|
}];
|
|
1681
|
-
})
|
|
1682
|
-
|
|
1823
|
+
});
|
|
1824
|
+
return changed ? {
|
|
1825
|
+
...optimistic,
|
|
1826
|
+
overlays
|
|
1827
|
+
} : optimistic;
|
|
1828
|
+
};
|
|
1683
1829
|
/** Everything a request owns: its layer and its overlays. */
|
|
1684
1830
|
const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic, requestId), requestId);
|
|
1685
1831
|
/**
|
|
@@ -1687,12 +1833,12 @@ const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic,
|
|
|
1687
1833
|
* a later layer re-wins on rebase. Confirmed connection changes are recorded
|
|
1688
1834
|
* once per request, as overlays the request no longer owns.
|
|
1689
1835
|
*/
|
|
1690
|
-
const settleSuccess = (base, optimistic, state, requestId,
|
|
1691
|
-
const reconciled = reconcileMutation(base, state, requestId,
|
|
1836
|
+
const settleSuccess = (base, optimistic, state, requestId, answer) => {
|
|
1837
|
+
const reconciled = reconcileMutation(base, state, requestId, answer);
|
|
1692
1838
|
return {
|
|
1693
1839
|
store: reconciled.store,
|
|
1694
1840
|
state: reconciled.state,
|
|
1695
|
-
optimistic: state.applied.has(requestId) ? release(optimistic, requestId) : confirm(removeLayer(optimistic, requestId), requestId, `confirmed:${requestId}`, connections)
|
|
1841
|
+
optimistic: state.applied.has(requestId) ? release(optimistic, requestId) : confirm(removeLayer(optimistic, requestId), requestId, `confirmed:${requestId}`, answer.connections ?? [])
|
|
1696
1842
|
};
|
|
1697
1843
|
};
|
|
1698
1844
|
/** A failed request's layer and overlays are dropped; the base was never mutated. */
|
|
@@ -1767,11 +1913,6 @@ const recordBoundary = (state, connection, position) => {
|
|
|
1767
1913
|
}
|
|
1768
1914
|
};
|
|
1769
1915
|
};
|
|
1770
|
-
/**
|
|
1771
|
-
* A subscriber is woken only if the event changed a field it selects. A
|
|
1772
|
-
* change to an unselected field is skipped entirely.
|
|
1773
|
-
*/
|
|
1774
|
-
const shouldWake = (changed, selected) => changed.some((field) => selected.includes(field));
|
|
1775
1916
|
const applyEntityEvent = (state, store, event, now = 0) => {
|
|
1776
1917
|
const outcome = classifyLive(state, event.cursor);
|
|
1777
1918
|
if (outcome !== "applied") return {
|
|
@@ -1789,9 +1930,13 @@ const applyEntityEvent = (state, store, event, now = 0) => {
|
|
|
1789
1930
|
};
|
|
1790
1931
|
const removeEdgeOverlays = (optimistic, connection, key) => ({
|
|
1791
1932
|
...optimistic,
|
|
1792
|
-
overlays: optimistic.overlays.map((overlay) =>
|
|
1793
|
-
|
|
1794
|
-
edges
|
|
1933
|
+
overlays: optimistic.overlays.map((overlay) => {
|
|
1934
|
+
if (overlay.connection !== connection || overlay.position === "remove") return overlay;
|
|
1935
|
+
const edges = overlay.edges.filter((edge) => edge.key !== key);
|
|
1936
|
+
return edges.length === overlay.edges.length ? overlay : {
|
|
1937
|
+
...overlay,
|
|
1938
|
+
edges
|
|
1939
|
+
};
|
|
1795
1940
|
})
|
|
1796
1941
|
});
|
|
1797
1942
|
const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
|
|
@@ -1878,6 +2023,44 @@ const followRelation = (value, relation) => targetsOf(value, relation).map((ref)
|
|
|
1878
2023
|
...relation,
|
|
1879
2024
|
id: ref.id
|
|
1880
2025
|
}));
|
|
2026
|
+
/**
|
|
2027
|
+
* The earliest moment a value `requirements` read, following the relations the
|
|
2028
|
+
* store holds as `plan` does, goes stale under `freshness`, with the
|
|
2029
|
+
* requirements of the entities that go stale then. A field the server settled
|
|
2030
|
+
* without a value ages with its entity too, since `plan` asks for it again once
|
|
2031
|
+
* the entity expires. Nothing when no such field is still fresh: what is
|
|
2032
|
+
* already expired or stale is planned, not awaited, which is what keeps a
|
|
2033
|
+
* timer from firing on what it already fired on. It is the next moment the
|
|
2034
|
+
* plan can change with the clock alone, which the read entry relies on to
|
|
2035
|
+
* reuse its plan until then.
|
|
2036
|
+
*/
|
|
2037
|
+
const deadlineOf = (store, requirements, freshness) => {
|
|
2038
|
+
let at;
|
|
2039
|
+
let due = [];
|
|
2040
|
+
const walk = (group) => {
|
|
2041
|
+
const key = entityKey(group.entity, group.id);
|
|
2042
|
+
const entry = store[key];
|
|
2043
|
+
if (entry === void 0 || entry.tombstone) return;
|
|
2044
|
+
if (group.fields.some((field) => entry.present.has(field) && !entry.stale.has(field) || entry.unavailable.has(field))) {
|
|
2045
|
+
const expires = entry.updatedAt + freshness.freshness;
|
|
2046
|
+
if (expires >= freshness.now && Number.isFinite(expires)) {
|
|
2047
|
+
if (at === void 0 || expires < at) {
|
|
2048
|
+
at = expires;
|
|
2049
|
+
due = [group];
|
|
2050
|
+
} else if (expires === at) due.push(group);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
for (const [field, relation] of Object.entries(group.relations ?? {})) {
|
|
2054
|
+
const value = readField(store, key, field);
|
|
2055
|
+
if (value._tag === "Some") for (const next of followRelation(value.value, relation)) walk(next);
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
for (const group of Requirement.merge(requirements)) walk(group);
|
|
2059
|
+
return at === void 0 ? void 0 : {
|
|
2060
|
+
at,
|
|
2061
|
+
due: Requirement.merge(due)
|
|
2062
|
+
};
|
|
2063
|
+
};
|
|
1881
2064
|
const plan = (store, requirements, options = {}) => {
|
|
1882
2065
|
const { freshness, force = false } = options;
|
|
1883
2066
|
const planned = [];
|
|
@@ -1962,17 +2145,25 @@ const reachable = (store, roots, connections, optimistic, pending) => {
|
|
|
1962
2145
|
const gc = (state, roots) => {
|
|
1963
2146
|
const pending = state.mutations.pending;
|
|
1964
2147
|
const kept = reachable(state.entities, roots, state.connections, state.optimistic, pending);
|
|
1965
|
-
const entities =
|
|
2148
|
+
const entities = keptOf(state.entities, (key) => kept.has(key));
|
|
1966
2149
|
const keptConnections = /* @__PURE__ */ new Set([...roots.connections.map((root) => root.identity), ...state.optimistic.overlays.filter((overlay) => pending.has(overlay.id)).map((overlay) => overlay.connection)]);
|
|
2150
|
+
const connections = keptOf(state.connections, (identity) => keptConnections.has(identity));
|
|
2151
|
+
const overlays = state.optimistic.overlays.filter((overlay) => pending.has(overlay.id) || keptConnections.has(overlay.connection));
|
|
1967
2152
|
return {
|
|
1968
2153
|
entities,
|
|
1969
|
-
connections
|
|
1970
|
-
optimistic: {
|
|
2154
|
+
connections,
|
|
2155
|
+
optimistic: overlays.length === state.optimistic.overlays.length ? state.optimistic : {
|
|
1971
2156
|
layers: state.optimistic.layers,
|
|
1972
|
-
overlays
|
|
2157
|
+
overlays
|
|
1973
2158
|
}
|
|
1974
2159
|
};
|
|
1975
2160
|
};
|
|
2161
|
+
/** The entries whose key is kept; `record` itself when that is all of them. */
|
|
2162
|
+
const keptOf = (record, keep) => {
|
|
2163
|
+
const entries = Object.entries(record);
|
|
2164
|
+
const kept = entries.filter(([key]) => keep(key));
|
|
2165
|
+
return kept.length === entries.length ? record : Object.fromEntries(kept);
|
|
2166
|
+
};
|
|
1976
2167
|
//#endregion
|
|
1977
2168
|
//#region src/persistence.ts
|
|
1978
2169
|
/**
|
|
@@ -1989,7 +2180,7 @@ const gc = (state, roots) => {
|
|
|
1989
2180
|
* application's choice.
|
|
1990
2181
|
*/
|
|
1991
2182
|
/** Bump when the serialized shape changes; a mismatch discards the cache. */
|
|
1992
|
-
const REMOTE_CACHE_VERSION =
|
|
2183
|
+
const REMOTE_CACHE_VERSION = 5;
|
|
1993
2184
|
/**
|
|
1994
2185
|
* A snapshot of a Model, keeping the entity cache and only the connections
|
|
1995
2186
|
* named.
|
|
@@ -2012,13 +2203,14 @@ const sorted = (values) => [...values].sort();
|
|
|
2012
2203
|
* whatever order they were built in.
|
|
2013
2204
|
*/
|
|
2014
2205
|
const serializeStore = (snapshot, scope) => ({
|
|
2015
|
-
version:
|
|
2206
|
+
version: 5,
|
|
2016
2207
|
scope: scope ?? null,
|
|
2017
2208
|
connections: snapshot.connections,
|
|
2018
2209
|
entities: Object.fromEntries(Object.entries(snapshot.entities).map(([key, entry]) => [key, {
|
|
2019
2210
|
values: entry.values,
|
|
2020
2211
|
present: sorted(entry.present),
|
|
2021
2212
|
stale: sorted(entry.stale),
|
|
2213
|
+
unavailable: sorted(entry.unavailable),
|
|
2022
2214
|
tombstone: entry.tombstone,
|
|
2023
2215
|
updatedAt: entry.updatedAt,
|
|
2024
2216
|
windows: entry.windows
|
|
@@ -2033,6 +2225,7 @@ const parseEntry = (value) => {
|
|
|
2033
2225
|
if (entry.values === null || typeof entry.values !== "object" || Array.isArray(entry.values)) throw new Error("entry.values is not a record");
|
|
2034
2226
|
if (!isStringArray(entry.present)) throw new Error("entry.present is not a string array");
|
|
2035
2227
|
if (!isStringArray(entry.stale)) throw new Error("entry.stale is not a string array");
|
|
2228
|
+
if (!isStringArray(entry.unavailable)) throw new Error("entry.unavailable is not a string array");
|
|
2036
2229
|
if (typeof entry.tombstone !== "boolean") throw new Error("entry.tombstone is not a boolean");
|
|
2037
2230
|
if (typeof entry.updatedAt !== "number") throw new Error("entry.updatedAt is not a number");
|
|
2038
2231
|
if (!isStringRecord(entry.windows)) throw new Error("entry.windows is not a string record");
|
|
@@ -2040,6 +2233,7 @@ const parseEntry = (value) => {
|
|
|
2040
2233
|
values: entry.values,
|
|
2041
2234
|
present: new Set(entry.present),
|
|
2042
2235
|
stale: new Set(entry.stale),
|
|
2236
|
+
unavailable: new Set(entry.unavailable),
|
|
2043
2237
|
tombstone: entry.tombstone,
|
|
2044
2238
|
updatedAt: entry.updatedAt,
|
|
2045
2239
|
windows: entry.windows
|
|
@@ -2098,7 +2292,7 @@ const hydrate = (raw, options = {}) => {
|
|
|
2098
2292
|
}
|
|
2099
2293
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2100
2294
|
const candidate = parsed;
|
|
2101
|
-
if (candidate.version !==
|
|
2295
|
+
if (candidate.version !== 5) return void 0;
|
|
2102
2296
|
if ((candidate.scope ?? null) !== (options.scope ?? null)) return void 0;
|
|
2103
2297
|
if (candidate.entities === null || typeof candidate.entities !== "object") return void 0;
|
|
2104
2298
|
if (Array.isArray(candidate.entities)) return void 0;
|
|
@@ -2114,7 +2308,7 @@ const hydrate = (raw, options = {}) => {
|
|
|
2114
2308
|
* an entry the current store already has (it is at least as fresh as a
|
|
2115
2309
|
* snapshot taken earlier). Either way, keys only the current store has stay.
|
|
2116
2310
|
*/
|
|
2117
|
-
const mergeStores = (current, snapshot, policy) => policy === "replace" ? {
|
|
2311
|
+
const mergeStores = (current, snapshot, policy) => Object.keys(snapshot).length === 0 ? current : policy === "replace" ? {
|
|
2118
2312
|
...current,
|
|
2119
2313
|
...snapshot
|
|
2120
2314
|
} : {
|
|
@@ -2275,7 +2469,9 @@ const RemoteData = {
|
|
|
2275
2469
|
error: data.error,
|
|
2276
2470
|
previous: f(data.previous)
|
|
2277
2471
|
};
|
|
2278
|
-
|
|
2472
|
+
case "Initial":
|
|
2473
|
+
case "Loading":
|
|
2474
|
+
case "NotFound": return data;
|
|
2279
2475
|
}
|
|
2280
2476
|
}
|
|
2281
2477
|
};
|
|
@@ -2293,11 +2489,37 @@ const noFailures = {
|
|
|
2293
2489
|
const emptyRefresh = {
|
|
2294
2490
|
generation: 0,
|
|
2295
2491
|
requested: /* @__PURE__ */ new Map(),
|
|
2296
|
-
started: /* @__PURE__ */ new Map()
|
|
2492
|
+
started: /* @__PURE__ */ new Map(),
|
|
2493
|
+
floor: 0
|
|
2494
|
+
};
|
|
2495
|
+
/**
|
|
2496
|
+
* The Model with everything the server told it forgotten: values, tombstones,
|
|
2497
|
+
* unavailable fields, connections, live cursors, failures, and what is in
|
|
2498
|
+
* flight. For a change of principal, whose facts these were. The mutation
|
|
2499
|
+
* sequence is kept, so request ids stay unique, and a mutation in flight is
|
|
2500
|
+
* treated as applied: its answer, when it comes, writes nothing here.
|
|
2501
|
+
*/
|
|
2502
|
+
const forgetRemote = (model) => {
|
|
2503
|
+
const generation = model.refresh.generation + 1;
|
|
2504
|
+
return {
|
|
2505
|
+
...initialRemoteModel,
|
|
2506
|
+
refresh: {
|
|
2507
|
+
generation,
|
|
2508
|
+
requested: /* @__PURE__ */ new Map(),
|
|
2509
|
+
started: /* @__PURE__ */ new Map(),
|
|
2510
|
+
floor: generation
|
|
2511
|
+
},
|
|
2512
|
+
mutations: {
|
|
2513
|
+
...emptyMutationState,
|
|
2514
|
+
sequence: model.mutations.sequence,
|
|
2515
|
+
applied: /* @__PURE__ */ new Set([...model.mutations.applied, ...model.mutations.pending])
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2297
2518
|
};
|
|
2298
2519
|
const initialRemoteModel = {
|
|
2299
2520
|
entities: emptyStore,
|
|
2300
2521
|
connections: {},
|
|
2522
|
+
grown: {},
|
|
2301
2523
|
optimistic: emptyOptimistic,
|
|
2302
2524
|
live: {},
|
|
2303
2525
|
mutations: emptyMutationState,
|
|
@@ -2311,6 +2533,7 @@ const runtimeSchema = Schema.Unknown;
|
|
|
2311
2533
|
const remoteModelSchema = () => Schema.Struct({
|
|
2312
2534
|
entities: runtimeSchema,
|
|
2313
2535
|
connections: Schema.Record(Schema.String, runtimeSchema),
|
|
2536
|
+
grown: Schema.Record(Schema.String, Schema.Number),
|
|
2314
2537
|
optimistic: runtimeSchema,
|
|
2315
2538
|
live: Schema.Record(Schema.String, runtimeSchema),
|
|
2316
2539
|
mutations: runtimeSchema,
|
|
@@ -2367,7 +2590,8 @@ const remoteMessageCases = {
|
|
|
2367
2590
|
deleted: Schema.optional(Schema.Array(Schema.Struct({
|
|
2368
2591
|
entity: Schema.String,
|
|
2369
2592
|
id: Schema.String
|
|
2370
|
-
})))
|
|
2593
|
+
}))),
|
|
2594
|
+
now: Schema.Number
|
|
2371
2595
|
},
|
|
2372
2596
|
MutationFailed: {
|
|
2373
2597
|
requestId: Schema.String,
|
|
@@ -2378,6 +2602,10 @@ const remoteMessageCases = {
|
|
|
2378
2602
|
optimistic: Schema.Array(Schema.Unknown)
|
|
2379
2603
|
},
|
|
2380
2604
|
OverlayLifted: { id: Schema.String },
|
|
2605
|
+
WindowGrown: {
|
|
2606
|
+
window: Schema.String,
|
|
2607
|
+
size: Schema.Number
|
|
2608
|
+
},
|
|
2381
2609
|
LiveReceived: {
|
|
2382
2610
|
stream: Schema.String,
|
|
2383
2611
|
event: Schema.Unknown,
|
|
@@ -2388,7 +2616,11 @@ const remoteMessageCases = {
|
|
|
2388
2616
|
ConnectionMerged: {
|
|
2389
2617
|
connection: Schema.String,
|
|
2390
2618
|
page: Schema.Unknown,
|
|
2391
|
-
refreshes: Schema.optional(Schema.Boolean)
|
|
2619
|
+
refreshes: Schema.optional(Schema.Boolean),
|
|
2620
|
+
select: Schema.optional(RelationRequest),
|
|
2621
|
+
entities: Schema.optional(Schema.Array(NormalizedEntity)),
|
|
2622
|
+
settled: Schema.optional(Schema.Array(SettledFields)),
|
|
2623
|
+
now: Schema.optional(Schema.Number)
|
|
2392
2624
|
},
|
|
2393
2625
|
ConnectionInvalidated: { connection: Schema.String },
|
|
2394
2626
|
ConnectionRefreshed: { connection: Schema.String },
|
|
@@ -2408,6 +2640,18 @@ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(
|
|
|
2408
2640
|
})));
|
|
2409
2641
|
/** An overlay's layer, named apart from every request's: a request id is the application's to choose too. */
|
|
2410
2642
|
const overlayLayer = (id) => `overlay:${id}`;
|
|
2643
|
+
/** The key a grown window is kept under: its connection and the window first asked for. */
|
|
2644
|
+
const windowGrowthKey = (identity, window) => `${identity}\u0000${window}`;
|
|
2645
|
+
/**
|
|
2646
|
+
* Whether only overlays show this entity: the server has told the store
|
|
2647
|
+
* nothing of it and no request's layer writes it, so nothing will fetch what an
|
|
2648
|
+
* overlay leaves out.
|
|
2649
|
+
*/
|
|
2650
|
+
const onlyOverlaid = (model, entity, id) => {
|
|
2651
|
+
if (Object.hasOwn(model.entities, entityKey(entity, id))) return false;
|
|
2652
|
+
const writers = model.optimistic.layers.filter((layer) => layer.patches.some((patch) => patch.entity === entity && patch.id === id));
|
|
2653
|
+
return writers.length > 0 && writers.every((layer) => layer.id.startsWith("overlay:"));
|
|
2654
|
+
};
|
|
2411
2655
|
const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
|
|
2412
2656
|
/**
|
|
2413
2657
|
* One mark per requested field, `entity\0id\0field`. A read is identified by
|
|
@@ -2423,13 +2667,13 @@ const fieldMarks = function* (requests) {
|
|
|
2423
2667
|
const withLoading = (loading, requests) => {
|
|
2424
2668
|
const next = new Set(loading);
|
|
2425
2669
|
for (const mark of fieldMarks(requests)) next.add(mark);
|
|
2426
|
-
return next;
|
|
2670
|
+
return next.size === loading.size ? loading : next;
|
|
2427
2671
|
};
|
|
2428
2672
|
const withoutLoading = (loading, requests) => {
|
|
2429
2673
|
if (loading.size === 0) return loading;
|
|
2430
2674
|
const next = new Set(loading);
|
|
2431
2675
|
for (const mark of fieldMarks(requests)) next.delete(mark);
|
|
2432
|
-
return next;
|
|
2676
|
+
return next.size === loading.size ? loading : next;
|
|
2433
2677
|
};
|
|
2434
2678
|
/**
|
|
2435
2679
|
* A connection's mark. The leading separator keeps it out of the field marks'
|
|
@@ -2458,6 +2702,14 @@ const prunedLoading = (loading, roots, reached) => {
|
|
|
2458
2702
|
return kept.length === loading.size ? loading : new Set(kept);
|
|
2459
2703
|
};
|
|
2460
2704
|
/**
|
|
2705
|
+
* The windows grown on connections retention keeps; a dropped one starts over.
|
|
2706
|
+
* The same record when every one is kept.
|
|
2707
|
+
*/
|
|
2708
|
+
const keptWindows = (grown, connections) => {
|
|
2709
|
+
const kept = Object.entries(grown).filter(([key]) => Object.hasOwn(connections, key.slice(0, key.lastIndexOf("\0"))));
|
|
2710
|
+
return kept.length === Object.keys(grown).length ? grown : Object.fromEntries(kept);
|
|
2711
|
+
};
|
|
2712
|
+
/**
|
|
2461
2713
|
* Drops the refresh marks of entities and connections that were collected.
|
|
2462
2714
|
* Nothing observes a collected entity — the retention roots are the active
|
|
2463
2715
|
* Surfaces — so no read entry's generation can fall back when its mark goes.
|
|
@@ -2487,7 +2739,7 @@ const prunedRefresh = (refresh, retained) => {
|
|
|
2487
2739
|
* anything else is.
|
|
2488
2740
|
*/
|
|
2489
2741
|
const refreshedAt = (refresh, requests, connections = []) => {
|
|
2490
|
-
let at =
|
|
2742
|
+
let at = refresh.floor;
|
|
2491
2743
|
for (const mark of fieldMarks(requests)) at = Math.max(at, refresh.requested.get(mark) ?? 0);
|
|
2492
2744
|
for (const identity of connections) at = Math.max(at, refresh.requested.get(connectionMark(identity)) ?? 0);
|
|
2493
2745
|
return at;
|
|
@@ -2544,15 +2796,20 @@ const isLoading = (model, entity, id, fields) => {
|
|
|
2544
2796
|
return fields.some((field) => model.loading.has(fieldMark(entity, id, field)));
|
|
2545
2797
|
};
|
|
2546
2798
|
const setConnectionStale = (connections, connection, stale) => {
|
|
2547
|
-
const current = connections[connection]
|
|
2799
|
+
const current = connections[connection];
|
|
2800
|
+
if (current?.stale === stale) return connections;
|
|
2548
2801
|
return {
|
|
2549
2802
|
...connections,
|
|
2550
2803
|
[connection]: {
|
|
2551
|
-
...current,
|
|
2804
|
+
...current ?? emptyConnection,
|
|
2552
2805
|
stale
|
|
2553
2806
|
}
|
|
2554
2807
|
};
|
|
2555
2808
|
};
|
|
2809
|
+
const withLiveState = (live, stream, state) => live[stream] === state ? live : {
|
|
2810
|
+
...live,
|
|
2811
|
+
[stream]: state
|
|
2812
|
+
};
|
|
2556
2813
|
const markGap = (model, stream) => model.gaps.has(stream) ? model : {
|
|
2557
2814
|
...model,
|
|
2558
2815
|
gaps: /* @__PURE__ */ new Set([...model.gaps, stream])
|
|
@@ -2564,9 +2821,23 @@ const clearGap = (model, stream) => model.gaps.has(stream) ? {
|
|
|
2564
2821
|
/**
|
|
2565
2822
|
* The pure reducer all four producers share. A live event that arrives ahead of
|
|
2566
2823
|
* its cursor is a gap: it is not applied, and the stream is recorded so the host
|
|
2567
|
-
* can resubscribe rather than silently miss facts.
|
|
2824
|
+
* can resubscribe rather than silently miss facts. When every field a Message
|
|
2825
|
+
* touches keeps its identity, `model` itself is returned, so the application's
|
|
2826
|
+
* root keeps its identity and Foldkit does not render. The common no-ops do (a
|
|
2827
|
+
* duplicate live event, a repeated `ReadStarted`); a repeated failure, or a
|
|
2828
|
+
* `preserve-existing` restore of entities already held, still makes an equal
|
|
2829
|
+
* copy.
|
|
2568
2830
|
*/
|
|
2569
2831
|
const updateRemote = (model, message) => {
|
|
2832
|
+
const next = reduceRemote(model, message);
|
|
2833
|
+
return next === model || !sameFields(model, next) ? next : model;
|
|
2834
|
+
};
|
|
2835
|
+
/** Whether every field of `next` is the one `model` holds. */
|
|
2836
|
+
const sameFields = (model, next) => {
|
|
2837
|
+
const keys = Object.keys(next);
|
|
2838
|
+
return keys.length === Object.keys(model).length && keys.every((key) => Object.is(model[key], next[key]));
|
|
2839
|
+
};
|
|
2840
|
+
const reduceRemote = (model, message) => {
|
|
2570
2841
|
switch (message._tag) {
|
|
2571
2842
|
case "ReadReceived": return {
|
|
2572
2843
|
...model,
|
|
@@ -2589,6 +2860,7 @@ const updateRemote = (model, message) => {
|
|
|
2589
2860
|
return {
|
|
2590
2861
|
...model,
|
|
2591
2862
|
...retained,
|
|
2863
|
+
grown: keptWindows(model.grown, retained.connections),
|
|
2592
2864
|
refresh: prunedRefresh(model.refresh, retained),
|
|
2593
2865
|
failures: prunedFailures(model.failures, message.roots, reached),
|
|
2594
2866
|
loading: prunedLoading(model.loading, message.roots, reached)
|
|
@@ -2608,7 +2880,7 @@ const updateRemote = (model, message) => {
|
|
|
2608
2880
|
...model,
|
|
2609
2881
|
entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
|
|
2610
2882
|
failures: withoutFieldFailures(model.failures, written),
|
|
2611
|
-
connections: message.merge === "replace" ? {
|
|
2883
|
+
connections: restored.length === 0 ? model.connections : message.merge === "replace" ? {
|
|
2612
2884
|
...model.connections,
|
|
2613
2885
|
...Object.fromEntries(restored)
|
|
2614
2886
|
} : {
|
|
@@ -2619,9 +2891,10 @@ const updateRemote = (model, message) => {
|
|
|
2619
2891
|
}
|
|
2620
2892
|
case "RefreshStarted": {
|
|
2621
2893
|
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
|
|
2894
|
+
const marks = marksOf(message.requests);
|
|
2895
|
+
const entities = setUnavailable(setStale(asked, marks, true), marks, false);
|
|
2623
2896
|
const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
|
|
2624
|
-
return
|
|
2897
|
+
return {
|
|
2625
2898
|
...model,
|
|
2626
2899
|
entities,
|
|
2627
2900
|
failures
|
|
@@ -2646,7 +2919,7 @@ const updateRemote = (model, message) => {
|
|
|
2646
2919
|
optimistic: beginOptimistic(model.optimistic, message.requestId, message.optimistic ?? [])
|
|
2647
2920
|
};
|
|
2648
2921
|
case "MutationSucceeded": {
|
|
2649
|
-
const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message
|
|
2922
|
+
const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message);
|
|
2650
2923
|
return {
|
|
2651
2924
|
...model,
|
|
2652
2925
|
entities: settled.store,
|
|
@@ -2659,6 +2932,13 @@ const updateRemote = (model, message) => {
|
|
|
2659
2932
|
...model,
|
|
2660
2933
|
optimistic: beginOptimistic(settleFailure(model.optimistic, overlayLayer(message.id)), overlayLayer(message.id), message.optimistic)
|
|
2661
2934
|
};
|
|
2935
|
+
case "WindowGrown": return {
|
|
2936
|
+
...model,
|
|
2937
|
+
grown: {
|
|
2938
|
+
...model.grown,
|
|
2939
|
+
[message.window]: message.size
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2662
2942
|
case "OverlayLifted": {
|
|
2663
2943
|
const optimistic = settleFailure(model.optimistic, overlayLayer(message.id));
|
|
2664
2944
|
return optimistic.layers.length === model.optimistic.layers.length && optimistic.overlays.length === model.optimistic.overlays.length ? model : {
|
|
@@ -2684,10 +2964,7 @@ const updateRemote = (model, message) => {
|
|
|
2684
2964
|
return clearGap({
|
|
2685
2965
|
...model,
|
|
2686
2966
|
entities: applied.store,
|
|
2687
|
-
live:
|
|
2688
|
-
...model.live,
|
|
2689
|
-
[message.stream]: applied.state
|
|
2690
|
-
},
|
|
2967
|
+
live: withLiveState(model.live, message.stream, applied.state),
|
|
2691
2968
|
failures: withoutFieldFailures(model.failures, settled)
|
|
2692
2969
|
}, message.stream);
|
|
2693
2970
|
}
|
|
@@ -2696,10 +2973,7 @@ const updateRemote = (model, message) => {
|
|
|
2696
2973
|
const next = clearGap({
|
|
2697
2974
|
...model,
|
|
2698
2975
|
optimistic: applied.optimistic,
|
|
2699
|
-
live:
|
|
2700
|
-
...model.live,
|
|
2701
|
-
[message.stream]: applied.state
|
|
2702
|
-
}
|
|
2976
|
+
live: withLiveState(model.live, message.stream, applied.state)
|
|
2703
2977
|
}, message.stream);
|
|
2704
2978
|
return applied.invalidated === void 0 ? next : updateRemote(next, {
|
|
2705
2979
|
_tag: "ConnectionInvalidated",
|
|
@@ -2709,12 +2983,23 @@ const updateRemote = (model, message) => {
|
|
|
2709
2983
|
case "GapCleared": return clearGap(model, message.stream);
|
|
2710
2984
|
case "ConnectionMerged": {
|
|
2711
2985
|
const current = model.connections[message.connection] ?? emptyConnection;
|
|
2712
|
-
const failures = withoutConnectionFailure(model.failures, message.connection);
|
|
2713
2986
|
const loading = withoutQueryLoading(model.loading, message.connection);
|
|
2714
2987
|
const replaces = message.refreshes === true && current.stale;
|
|
2715
2988
|
const merged = merge(replaces ? emptyConnection : current, message.page);
|
|
2989
|
+
const payload = message.select === void 0 ? [] : message.entities ?? [];
|
|
2990
|
+
const settled = message.select === void 0 ? [] : message.settled ?? [];
|
|
2991
|
+
const requests = message.select === void 0 || payload.length === 0 && settled.length === 0 ? [] : message.page.edges.filter((edge) => edge.ref.entity === message.select.entity).map((edge) => ({
|
|
2992
|
+
...message.select,
|
|
2993
|
+
id: edge.ref.id
|
|
2994
|
+
}));
|
|
2995
|
+
const entities = requests.length === 0 ? model.entities : writeRead(model.entities, requests, {
|
|
2996
|
+
entities: [...payload],
|
|
2997
|
+
settled: [...settled]
|
|
2998
|
+
}, message.now ?? 0);
|
|
2999
|
+
const failures = withoutFieldFailures(withoutConnectionFailure(model.failures, message.connection), [...fieldMarks(requests), ...patchedMarks(payload)]);
|
|
2716
3000
|
return {
|
|
2717
3001
|
...model,
|
|
3002
|
+
entities,
|
|
2718
3003
|
connections: {
|
|
2719
3004
|
...model.connections,
|
|
2720
3005
|
[message.connection]: message.refreshes === true ? {
|
|
@@ -2727,13 +3012,11 @@ const updateRemote = (model, message) => {
|
|
|
2727
3012
|
loading
|
|
2728
3013
|
};
|
|
2729
3014
|
}
|
|
2730
|
-
case "ConnectionInvalidated":
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
failures: withoutConnectionFailure(model.failures, message.connection)
|
|
2736
|
-
};
|
|
3015
|
+
case "ConnectionInvalidated": return {
|
|
3016
|
+
...model,
|
|
3017
|
+
connections: setConnectionStale(model.connections, message.connection, true),
|
|
3018
|
+
failures: withoutConnectionFailure(model.failures, message.connection)
|
|
3019
|
+
};
|
|
2737
3020
|
case "QueryFailed": return {
|
|
2738
3021
|
...model,
|
|
2739
3022
|
failures: {
|
|
@@ -2974,12 +3257,13 @@ const writeRead = (store, requests, result, now = 0) => {
|
|
|
2974
3257
|
windows: entry?.windows
|
|
2975
3258
|
});
|
|
2976
3259
|
}
|
|
3260
|
+
const settled = new Set(result.settled.map((entry) => entityKey(entry.entity, entry.id)));
|
|
2977
3261
|
let answered = store;
|
|
2978
3262
|
for (const request of requests) {
|
|
2979
3263
|
const key = entityKey(request.entity, request.id);
|
|
2980
|
-
if (!returned.has(key)) answered = tombstone(answered, key);
|
|
3264
|
+
if (!returned.has(key) && !settled.has(key)) answered = tombstone(answered, key);
|
|
2981
3265
|
}
|
|
2982
|
-
return writeEntities(answered, writes, now);
|
|
3266
|
+
return setUnavailable(writeEntities(answered, writes, now), result.settled.map((entry) => [entityKey(entry.entity, entry.id), entry.fields]), true);
|
|
2983
3267
|
};
|
|
2984
3268
|
//#endregion
|
|
2985
3269
|
//#region src/policy.ts
|
|
@@ -3012,6 +3296,221 @@ const RemotePolicy = {
|
|
|
3012
3296
|
refreshes: (policy) => policy._tag !== "CacheFirst"
|
|
3013
3297
|
};
|
|
3014
3298
|
//#endregion
|
|
3299
|
+
//#region src/resume.ts
|
|
3300
|
+
/**
|
|
3301
|
+
* What of Remote's state crosses from a server render to the browser.
|
|
3302
|
+
*
|
|
3303
|
+
* A page rendered a moment ago sends exactly what its active Surfaces read:
|
|
3304
|
+
* for each requirement the fields it names, following relations through the
|
|
3305
|
+
* refs the store holds, and each connection whole, with its boundaries, so
|
|
3306
|
+
* "load more" knows where the server stopped. A live subscription's cursor
|
|
3307
|
+
* crosses with the entities it covers, so a subscription started in the
|
|
3308
|
+
* browser resumes where the server's left off.
|
|
3309
|
+
*
|
|
3310
|
+
* This is not a `Snapshot`, which is built for a cache that survived a reload
|
|
3311
|
+
* and deliberately keeps no cursors. Nothing else of `RemoteModel` crosses:
|
|
3312
|
+
* loading marks, failures, the mutation ledger, optimistic layers, gaps and
|
|
3313
|
+
* retention start from their initial values in the browser.
|
|
3314
|
+
*/
|
|
3315
|
+
const CapturedEntry = Schema.Struct({
|
|
3316
|
+
values: Schema.Record(Schema.String, Schema.Unknown),
|
|
3317
|
+
present: Schema.Array(Schema.String),
|
|
3318
|
+
stale: Schema.Array(Schema.String),
|
|
3319
|
+
unavailable: Schema.Array(Schema.String),
|
|
3320
|
+
windows: Schema.Record(Schema.String, Schema.String),
|
|
3321
|
+
tombstone: Schema.Boolean,
|
|
3322
|
+
updatedAt: Schema.Number
|
|
3323
|
+
});
|
|
3324
|
+
const CapturedBoundary = Schema.Union([
|
|
3325
|
+
Schema.TaggedStruct("Terminal", {}),
|
|
3326
|
+
Schema.TaggedStruct("Cursor", { cursor: Schema.String }),
|
|
3327
|
+
Schema.TaggedStruct("Unknown", {})
|
|
3328
|
+
]);
|
|
3329
|
+
const CapturedConnection = Schema.Struct({
|
|
3330
|
+
segments: Schema.Array(Schema.Struct({
|
|
3331
|
+
edges: Schema.Array(Schema.Struct({
|
|
3332
|
+
key: Schema.String,
|
|
3333
|
+
ref: Schema.Struct({
|
|
3334
|
+
entity: Schema.String,
|
|
3335
|
+
id: Schema.String
|
|
3336
|
+
})
|
|
3337
|
+
})),
|
|
3338
|
+
start: CapturedBoundary,
|
|
3339
|
+
end: CapturedBoundary
|
|
3340
|
+
})),
|
|
3341
|
+
stale: Schema.Boolean
|
|
3342
|
+
});
|
|
3343
|
+
const CapturedLive = Schema.Struct({
|
|
3344
|
+
cursor: Schema.Number,
|
|
3345
|
+
boundary: Schema.Record(Schema.String, Schema.Struct({
|
|
3346
|
+
before: Schema.Number,
|
|
3347
|
+
after: Schema.Number
|
|
3348
|
+
}))
|
|
3349
|
+
});
|
|
3350
|
+
/**
|
|
3351
|
+
* Marks a Subscription entry Remote built as one that may start late under
|
|
3352
|
+
* `foldkit-ssr`'s deferred boot (its decision 10): a read with resumed data
|
|
3353
|
+
* plans nothing, a live subscription resumes from the cursor the envelope
|
|
3354
|
+
* carries, and retention running late collects late. Non-enumerable, so the
|
|
3355
|
+
* runtime's iteration of an entry never sees it.
|
|
3356
|
+
*/
|
|
3357
|
+
const DEFERRABLE = Symbol.for("foldkit-remote/deferrable");
|
|
3358
|
+
/** Marks every entry of a record Remote built, lifted or not. */
|
|
3359
|
+
const markAll = (entries) => {
|
|
3360
|
+
for (const entry of Object.values(entries)) Object.defineProperty(entry, DEFERRABLE, {
|
|
3361
|
+
value: true,
|
|
3362
|
+
enumerable: false
|
|
3363
|
+
});
|
|
3364
|
+
return entries;
|
|
3365
|
+
};
|
|
3366
|
+
/** The captured part of a Remote store, as JSON. */
|
|
3367
|
+
const RemoteCapture = Schema.Struct({
|
|
3368
|
+
entities: Schema.Record(Schema.String, CapturedEntry),
|
|
3369
|
+
connections: Schema.Record(Schema.String, CapturedConnection),
|
|
3370
|
+
live: Schema.Record(Schema.String, CapturedLive)
|
|
3371
|
+
});
|
|
3372
|
+
/** What some projections read, from their metadata. */
|
|
3373
|
+
const askedOf$1 = (projections) => ({
|
|
3374
|
+
requirements: projections.flatMap((projection) => RemoteRequirements.get(projection.metadata)),
|
|
3375
|
+
connections: projections.flatMap((projection) => RemoteConnections.get(projection.metadata))
|
|
3376
|
+
});
|
|
3377
|
+
/** Whether every `entity:id:fields` part of a live stream key was captured with those fields. */
|
|
3378
|
+
const liveCovered = (stream, entries) => stream.split("|").every((part) => {
|
|
3379
|
+
const split = part.lastIndexOf(":");
|
|
3380
|
+
const entry = entries.get(part.slice(0, split));
|
|
3381
|
+
const fields = part.slice(split + 1).split(",").filter((field) => field !== "");
|
|
3382
|
+
return entry !== void 0 && fields.every((field) => entry.present.has(field));
|
|
3383
|
+
});
|
|
3384
|
+
/**
|
|
3385
|
+
* Captures what some projections read from a store: the fields each
|
|
3386
|
+
* requirement names, through its relations; each connection whole, and the
|
|
3387
|
+
* fields its selection reads of each item; and each live cursor whose
|
|
3388
|
+
* entities were all captured with the fields it follows.
|
|
3389
|
+
*/
|
|
3390
|
+
const captureRemote = (remote, projections) => {
|
|
3391
|
+
const store = remote.entities;
|
|
3392
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3393
|
+
const walked = /* @__PURE__ */ new Map();
|
|
3394
|
+
const walk = (entity, id, requirement) => {
|
|
3395
|
+
const key = entityKey(entity, id);
|
|
3396
|
+
const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
|
|
3397
|
+
if (seen.has(requirement)) return;
|
|
3398
|
+
seen.add(requirement);
|
|
3399
|
+
walked.set(key, seen);
|
|
3400
|
+
const stored = store[key];
|
|
3401
|
+
if (stored === void 0) return;
|
|
3402
|
+
const entry = entries.get(key) ?? {
|
|
3403
|
+
values: {},
|
|
3404
|
+
present: /* @__PURE__ */ new Set(),
|
|
3405
|
+
stale: /* @__PURE__ */ new Set(),
|
|
3406
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
3407
|
+
windows: {},
|
|
3408
|
+
tombstone: stored.tombstone,
|
|
3409
|
+
updatedAt: stored.updatedAt
|
|
3410
|
+
};
|
|
3411
|
+
entries.set(key, entry);
|
|
3412
|
+
for (const field of requirement.fields) {
|
|
3413
|
+
if (stored.unavailable.has(field)) entry.unavailable.add(field);
|
|
3414
|
+
if (!stored.present.has(field)) continue;
|
|
3415
|
+
entry.values[field] = stored.values[field];
|
|
3416
|
+
entry.present.add(field);
|
|
3417
|
+
if (stored.stale.has(field)) entry.stale.add(field);
|
|
3418
|
+
const window = stored.windows[field];
|
|
3419
|
+
if (window !== void 0) entry.windows[field] = window;
|
|
3420
|
+
const nested = requirement.relations?.[field];
|
|
3421
|
+
if (nested === void 0) continue;
|
|
3422
|
+
const value = readField(store, key, field);
|
|
3423
|
+
if (value._tag === "None") continue;
|
|
3424
|
+
for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
const asked = askedOf$1(projections);
|
|
3428
|
+
for (const requirement of asked.requirements) walk(requirement.entity, requirement.id, requirement);
|
|
3429
|
+
const connections = {};
|
|
3430
|
+
for (const { identity, select } of asked.connections) {
|
|
3431
|
+
const connection = remote.connections[identity];
|
|
3432
|
+
if (connection === void 0) continue;
|
|
3433
|
+
connections[identity] = connection;
|
|
3434
|
+
for (const segment of connection.segments) for (const edge of segment.edges) walk(edge.ref.entity, edge.ref.id, select);
|
|
3435
|
+
}
|
|
3436
|
+
const live = {};
|
|
3437
|
+
for (const [stream, state] of Object.entries(remote.live)) if (liveCovered(stream, entries)) live[stream] = state;
|
|
3438
|
+
return {
|
|
3439
|
+
entities: Object.fromEntries([...entries].map(([key, entry]) => [key, {
|
|
3440
|
+
values: entry.values,
|
|
3441
|
+
present: [...entry.present],
|
|
3442
|
+
stale: [...entry.stale],
|
|
3443
|
+
unavailable: [...entry.unavailable],
|
|
3444
|
+
windows: entry.windows,
|
|
3445
|
+
tombstone: entry.tombstone,
|
|
3446
|
+
updatedAt: entry.updatedAt
|
|
3447
|
+
}])),
|
|
3448
|
+
connections: Object.fromEntries(Object.entries(connections).map(([identity, connection]) => [identity, {
|
|
3449
|
+
segments: connection.segments.map((segment) => ({
|
|
3450
|
+
edges: segment.edges.map((edge) => ({
|
|
3451
|
+
key: edge.key,
|
|
3452
|
+
ref: edge.ref
|
|
3453
|
+
})),
|
|
3454
|
+
start: segment.start,
|
|
3455
|
+
end: segment.end
|
|
3456
|
+
})),
|
|
3457
|
+
stale: connection.stale
|
|
3458
|
+
}])),
|
|
3459
|
+
live
|
|
3460
|
+
};
|
|
3461
|
+
};
|
|
3462
|
+
/**
|
|
3463
|
+
* Sets a capture onto a store: its entities, connections and live cursors
|
|
3464
|
+
* replace any held under the same keys, and nothing else of the store changes.
|
|
3465
|
+
*/
|
|
3466
|
+
const restoreRemote = (remote, capture) => ({
|
|
3467
|
+
...remote,
|
|
3468
|
+
entities: {
|
|
3469
|
+
...remote.entities,
|
|
3470
|
+
...Object.fromEntries(Object.entries(capture.entities).map(([key, entry]) => [key, {
|
|
3471
|
+
values: entry.values,
|
|
3472
|
+
present: new Set(entry.present),
|
|
3473
|
+
stale: new Set(entry.stale),
|
|
3474
|
+
unavailable: new Set(entry.unavailable),
|
|
3475
|
+
windows: entry.windows,
|
|
3476
|
+
tombstone: entry.tombstone,
|
|
3477
|
+
updatedAt: entry.updatedAt
|
|
3478
|
+
}]))
|
|
3479
|
+
},
|
|
3480
|
+
connections: {
|
|
3481
|
+
...remote.connections,
|
|
3482
|
+
...Object.fromEntries(Object.entries(capture.connections).map(([identity, connection]) => [identity, {
|
|
3483
|
+
segments: connection.segments.map((segment) => ({
|
|
3484
|
+
edges: segment.edges,
|
|
3485
|
+
start: segment.start,
|
|
3486
|
+
end: segment.end
|
|
3487
|
+
})),
|
|
3488
|
+
stale: connection.stale
|
|
3489
|
+
}]))
|
|
3490
|
+
},
|
|
3491
|
+
live: {
|
|
3492
|
+
...remote.live,
|
|
3493
|
+
...capture.live
|
|
3494
|
+
}
|
|
3495
|
+
});
|
|
3496
|
+
const encodeCapture = Schema.encodeSync(RemoteCapture);
|
|
3497
|
+
const decodeCapture = Schema.decodeUnknownResult(RemoteCapture);
|
|
3498
|
+
/**
|
|
3499
|
+
* The resume part for a Remote domain, for `SSR.plan`'s `parts`: it sends
|
|
3500
|
+
* what the plan's active Surfaces read from this domain's store, and nothing
|
|
3501
|
+
* else of it. `id` tells two domains in one application apart.
|
|
3502
|
+
*/
|
|
3503
|
+
const resumePart = (domain, options = {}) => ({
|
|
3504
|
+
id: options.id ?? "remote",
|
|
3505
|
+
covers: [RemoteRequirements.name, RemoteConnections.name],
|
|
3506
|
+
deferrable: (_key, entry) => typeof entry === "object" && entry !== null && DEFERRABLE in entry,
|
|
3507
|
+
capture: (model, projections) => encodeCapture(captureRemote(domain.store.get(model), projections)),
|
|
3508
|
+
restore: (model, value) => Result.match(decodeCapture(value), {
|
|
3509
|
+
onFailure: (error) => Result.fail(`Remote's state does not decode: ${error.message}`),
|
|
3510
|
+
onSuccess: (capture) => Result.succeed(domain.store.set(model, restoreRemote(domain.store.get(model), capture)))
|
|
3511
|
+
})
|
|
3512
|
+
});
|
|
3513
|
+
//#endregion
|
|
3015
3514
|
//#region src/index.ts
|
|
3016
3515
|
/**
|
|
3017
3516
|
* `foldkit-remote` — normalized application-facing server state.
|
|
@@ -3022,6 +3521,67 @@ const RemotePolicy = {
|
|
|
3022
3521
|
*/
|
|
3023
3522
|
const descriptorOf = (entity) => Entity$1.is(entity) ? Entity.from(entity) : entity;
|
|
3024
3523
|
const selectionOf = (selection) => SelectionTypeId in selection ? Selection.from(selection) : selection;
|
|
3524
|
+
const readContract = (source, shape, window) => {
|
|
3525
|
+
const ref = {
|
|
3526
|
+
...source,
|
|
3527
|
+
window: pickWindow(window)
|
|
3528
|
+
};
|
|
3529
|
+
const relation = relationOf(shape);
|
|
3530
|
+
return {
|
|
3531
|
+
ref,
|
|
3532
|
+
relation,
|
|
3533
|
+
requirement: {
|
|
3534
|
+
identity: ref.identity,
|
|
3535
|
+
window: ref.window,
|
|
3536
|
+
select: relation,
|
|
3537
|
+
ref
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
};
|
|
3541
|
+
const metaOf = (remote, asked) => {
|
|
3542
|
+
const visible = visibleStoreOf(remote.entities, remote.optimistic);
|
|
3543
|
+
let updatedAt;
|
|
3544
|
+
let stale = false;
|
|
3545
|
+
let loading = false;
|
|
3546
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3547
|
+
const touch = (entity, id, requirement) => {
|
|
3548
|
+
const key = entityKey(entity, id);
|
|
3549
|
+
const memo = `${key}\u0000${stableStringify(requirement)}`;
|
|
3550
|
+
if (seen.has(memo)) return;
|
|
3551
|
+
seen.add(memo);
|
|
3552
|
+
const entry = visible[key];
|
|
3553
|
+
if (entry !== void 0 && !entry.tombstone) for (const field of requirement.fields) {
|
|
3554
|
+
if (!entry.present.has(field)) continue;
|
|
3555
|
+
if (Number.isFinite(entry.updatedAt)) updatedAt = updatedAt === void 0 ? entry.updatedAt : Math.max(updatedAt, entry.updatedAt);
|
|
3556
|
+
if (entry.stale.has(field)) stale = true;
|
|
3557
|
+
}
|
|
3558
|
+
if (isLoadingThrough(remote, entity, id, requirement)) loading = true;
|
|
3559
|
+
for (const [field, relation] of Object.entries(requirement.relations ?? {})) {
|
|
3560
|
+
const value = readField(visible, key, field);
|
|
3561
|
+
if (value._tag === "None" || value.value === null || value.value === void 0) continue;
|
|
3562
|
+
for (const ref of targetsOf(value.value, relation)) touch(ref.entity, ref.id, relation);
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
for (const requirement of asked.requirements) touch(requirement.entity, requirement.id, requirement);
|
|
3566
|
+
for (const connection of asked.connections) {
|
|
3567
|
+
const known = remote.connections[connection.identity];
|
|
3568
|
+
if (known === void 0) {
|
|
3569
|
+
if (isQueryLoading(remote, connection.identity)) loading = true;
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3572
|
+
if (known.stale) stale = true;
|
|
3573
|
+
if (isQueryLoading(remote, connection.identity)) loading = true;
|
|
3574
|
+
for (const edge of visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities)) {
|
|
3575
|
+
if (edge.ref.entity !== connection.select.entity) continue;
|
|
3576
|
+
touch(edge.ref.entity, edge.ref.id, connection.select);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return {
|
|
3580
|
+
updatedAt,
|
|
3581
|
+
stale,
|
|
3582
|
+
loading
|
|
3583
|
+
};
|
|
3584
|
+
};
|
|
3025
3585
|
/**
|
|
3026
3586
|
* The store a read or plan sees: the base with every pending optimistic layer
|
|
3027
3587
|
* applied, so a request's patches show until it settles and a temporary id is
|
|
@@ -3049,10 +3609,10 @@ const confirmed = (bound, projection) => ({
|
|
|
3049
3609
|
});
|
|
3050
3610
|
const visibleStores = /* @__PURE__ */ new WeakMap();
|
|
3051
3611
|
const visibleStoreOf = (entities, optimistic) => {
|
|
3052
|
-
let byStore = visibleStores.get(optimistic);
|
|
3612
|
+
let byStore = visibleStores.get(optimistic.layers);
|
|
3053
3613
|
if (byStore === void 0) {
|
|
3054
3614
|
byStore = /* @__PURE__ */ new WeakMap();
|
|
3055
|
-
visibleStores.set(optimistic, byStore);
|
|
3615
|
+
visibleStores.set(optimistic.layers, byStore);
|
|
3056
3616
|
}
|
|
3057
3617
|
let visible = byStore.get(entities);
|
|
3058
3618
|
if (visible === void 0) {
|
|
@@ -3061,25 +3621,87 @@ const visibleStoreOf = (entities, optimistic) => {
|
|
|
3061
3621
|
}
|
|
3062
3622
|
return visible;
|
|
3063
3623
|
};
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3624
|
+
/**
|
|
3625
|
+
* `Failed` naming a field the server settled without a value, when one is
|
|
3626
|
+
* among what `relation` reads of the entity; otherwise nothing, and the store
|
|
3627
|
+
* merely lacks the value. The reason stays with the server, so a view shows
|
|
3628
|
+
* the field it cannot have, not why.
|
|
3629
|
+
*/
|
|
3630
|
+
const unavailableFailure = (store, key, relation) => {
|
|
3631
|
+
const field = unavailableOf(store, key, relation);
|
|
3632
|
+
return field === void 0 ? void 0 : {
|
|
3633
|
+
_tag: "Failed",
|
|
3634
|
+
error: {
|
|
3635
|
+
_tag: "Unavailable",
|
|
3636
|
+
message: `The server answered without ${field}, and will not answer with it: the field is not available to this client.`
|
|
3637
|
+
}
|
|
3638
|
+
};
|
|
3639
|
+
};
|
|
3640
|
+
/**
|
|
3641
|
+
* A row's decoded value, kept while the data it was assembled from is the same,
|
|
3642
|
+
* so a refetch or live patch that brought equal data hands a view the object it
|
|
3643
|
+
* already rendered. Keyed on the row's own values object, which the store keeps
|
|
3644
|
+
* across equal writes; a relation's targets are covered by comparing what was
|
|
3645
|
+
* assembled.
|
|
3646
|
+
*/
|
|
3647
|
+
const decodedRows = /* @__PURE__ */ new WeakMap();
|
|
3648
|
+
const decodeRow = (schema, source, values) => {
|
|
3649
|
+
let bySchema = decodedRows.get(schema);
|
|
3650
|
+
if (bySchema === void 0) {
|
|
3651
|
+
bySchema = {
|
|
3652
|
+
decode: Schema.decodeUnknownResult(schema),
|
|
3653
|
+
rows: /* @__PURE__ */ new WeakMap()
|
|
3654
|
+
};
|
|
3655
|
+
decodedRows.set(schema, bySchema);
|
|
3070
3656
|
}
|
|
3071
|
-
|
|
3072
|
-
if (
|
|
3073
|
-
|
|
3074
|
-
|
|
3657
|
+
const cached = bySchema.rows.get(source);
|
|
3658
|
+
if (cached !== void 0 && sameData(cached.values, values)) return cached.decoded;
|
|
3659
|
+
const decoded = bySchema.decode(values);
|
|
3660
|
+
bySchema.rows.set(source, {
|
|
3661
|
+
values,
|
|
3662
|
+
decoded
|
|
3663
|
+
});
|
|
3664
|
+
return decoded;
|
|
3665
|
+
};
|
|
3666
|
+
const readResults = {
|
|
3667
|
+
inner: /* @__PURE__ */ new WeakMap(),
|
|
3668
|
+
results: /* @__PURE__ */ new Map()
|
|
3669
|
+
};
|
|
3670
|
+
const memoRead = (scopes, key, compute) => {
|
|
3671
|
+
let scope = readResults;
|
|
3672
|
+
for (const by of scopes) {
|
|
3673
|
+
let inner = scope.inner.get(by);
|
|
3674
|
+
if (inner === void 0) {
|
|
3675
|
+
inner = {
|
|
3676
|
+
inner: /* @__PURE__ */ new WeakMap(),
|
|
3677
|
+
results: /* @__PURE__ */ new Map()
|
|
3678
|
+
};
|
|
3679
|
+
scope.inner.set(by, inner);
|
|
3680
|
+
}
|
|
3681
|
+
scope = inner;
|
|
3075
3682
|
}
|
|
3076
|
-
if (results.has(key)) return results.get(key);
|
|
3683
|
+
if (scope.results.has(key)) return scope.results.get(key);
|
|
3077
3684
|
const value = compute();
|
|
3078
|
-
results.set(key, value);
|
|
3685
|
+
scope.results.set(key, value);
|
|
3079
3686
|
return value;
|
|
3080
3687
|
};
|
|
3688
|
+
/**
|
|
3689
|
+
* The default clock. It reads `Date.now` when called rather than holding the
|
|
3690
|
+
* function, so fake timers installed after the domain was made move it.
|
|
3691
|
+
*/
|
|
3692
|
+
const wallClock = () => Date.now();
|
|
3081
3693
|
/** The default `toMessage`: the application reduces `RemoteMessage` itself. */
|
|
3082
3694
|
const identityMessage = (message) => message;
|
|
3695
|
+
/**
|
|
3696
|
+
* `Data.satisfy` reached its bound with Surfaces still reading, such as a
|
|
3697
|
+
* chain whose every read reveals one more. It names them, so a page that
|
|
3698
|
+
* would have rendered them loading fails instead. (A read the server leaves
|
|
3699
|
+
* unanswered settles as missing and ends the loop.)
|
|
3700
|
+
*/
|
|
3701
|
+
var RemoteUnsatisfied = class extends Schema.TaggedError()("RemoteUnsatisfied", {
|
|
3702
|
+
surfaces: Schema.Array(Schema.String),
|
|
3703
|
+
passes: Schema.Number
|
|
3704
|
+
}) {};
|
|
3083
3705
|
/** A stable key for a live subscription's requirement set. */
|
|
3084
3706
|
const liveStreamKey = (requirements) => requirements.map((requirement) => `${entityKey(requirement.entity, requirement.id)}:${[...requirement.fields].sort().join(",")}`).sort().join("|");
|
|
3085
3707
|
/** Declares a Remote domain: its entities, queries, and mutations, plus the submodel. */
|
|
@@ -3123,10 +3745,15 @@ const nothingAsked = {
|
|
|
3123
3745
|
requirements: [],
|
|
3124
3746
|
connections: []
|
|
3125
3747
|
};
|
|
3126
|
-
const askedOf = (projection) =>
|
|
3748
|
+
const askedOf = (projection) => ({
|
|
3127
3749
|
requirements: requirementsOf(projection),
|
|
3128
3750
|
connections: connectionsOf(projection)
|
|
3129
|
-
};
|
|
3751
|
+
});
|
|
3752
|
+
/** What an active Surface asks for: nothing while it is inactive. */
|
|
3753
|
+
const askedWhile = (projection) => Option.match(projection, {
|
|
3754
|
+
onNone: () => nothingAsked,
|
|
3755
|
+
onSome: askedOf
|
|
3756
|
+
});
|
|
3130
3757
|
/**
|
|
3131
3758
|
* Whether `outer` asks for everything `inner` does of one entity: each field,
|
|
3132
3759
|
* each relation's own slice of its target, and each page window alike.
|
|
@@ -3157,24 +3784,88 @@ const covers = (outer, inner) => {
|
|
|
3157
3784
|
* failure is what its read shows, and retrying is `Remote.refresh`'s to ask
|
|
3158
3785
|
* for. The rows it already holds are still planned, since they are on screen.
|
|
3159
3786
|
*/
|
|
3787
|
+
/**
|
|
3788
|
+
* One requirement per connection and direction: of the sized windows reading
|
|
3789
|
+
* a connection from its start (or end), the widest, which covers the rest.
|
|
3790
|
+
* Windows with a cursor are pages of their own and stay as they are.
|
|
3791
|
+
*/
|
|
3792
|
+
const widest = (remote, connections) => {
|
|
3793
|
+
const kept = /* @__PURE__ */ new Map();
|
|
3794
|
+
const rest = [];
|
|
3795
|
+
for (const connection of connections) {
|
|
3796
|
+
const size = windowSize(remote, connection);
|
|
3797
|
+
if (size === void 0) {
|
|
3798
|
+
rest.push(connection);
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3801
|
+
const key = `${connection.identity}${String.fromCharCode(0)}${connection.window.first === void 0 ? "last" : "first"}`;
|
|
3802
|
+
const current = kept.get(key);
|
|
3803
|
+
if (current === void 0) kept.set(key, connection);
|
|
3804
|
+
else if (size > (windowSize(remote, current) ?? 0)) kept.set(key, {
|
|
3805
|
+
...connection,
|
|
3806
|
+
select: Requirement.mergeRelation(current.select, connection.select)
|
|
3807
|
+
});
|
|
3808
|
+
else kept.set(key, {
|
|
3809
|
+
...current,
|
|
3810
|
+
select: Requirement.mergeRelation(current.select, connection.select)
|
|
3811
|
+
});
|
|
3812
|
+
}
|
|
3813
|
+
return [...kept.values(), ...rest];
|
|
3814
|
+
};
|
|
3815
|
+
const withWindow = (connection, window) => ({
|
|
3816
|
+
...connection,
|
|
3817
|
+
window,
|
|
3818
|
+
ref: {
|
|
3819
|
+
...connection.ref,
|
|
3820
|
+
window
|
|
3821
|
+
}
|
|
3822
|
+
});
|
|
3823
|
+
/**
|
|
3824
|
+
* The page a connection lacks for a window of `size` from its start (or end):
|
|
3825
|
+
* the rows after its first segment's end cursor (or before its last's start).
|
|
3826
|
+
* Undefined when it holds enough, reaches the end, or does not know where it
|
|
3827
|
+
* starts.
|
|
3828
|
+
*/
|
|
3829
|
+
const missingRows = (known, window, size) => {
|
|
3830
|
+
const forward = window.first !== void 0;
|
|
3831
|
+
const segment = forward ? known.segments[0] : known.segments[known.segments.length - 1];
|
|
3832
|
+
if (segment === void 0) return void 0;
|
|
3833
|
+
const [from, to] = forward ? [segment.start, segment.end] : [segment.end, segment.start];
|
|
3834
|
+
if (from._tag !== "Terminal" || to._tag !== "Cursor" || segment.edges.length >= size) return void 0;
|
|
3835
|
+
const lacking = size - segment.edges.length;
|
|
3836
|
+
return forward ? {
|
|
3837
|
+
first: lacking,
|
|
3838
|
+
after: to.cursor
|
|
3839
|
+
} : {
|
|
3840
|
+
last: lacking,
|
|
3841
|
+
before: to.cursor
|
|
3842
|
+
};
|
|
3843
|
+
};
|
|
3160
3844
|
const planAsked = (remote, asked, options) => {
|
|
3161
3845
|
const queries = [];
|
|
3162
3846
|
const items = [];
|
|
3163
3847
|
const connections = Requirement.mergeConnections(asked.connections);
|
|
3164
|
-
for (const connection of connections) {
|
|
3848
|
+
for (const connection of widest(remote, connections)) {
|
|
3165
3849
|
const known = remote.connections[connection.identity];
|
|
3166
|
-
|
|
3167
|
-
|
|
3850
|
+
const failed = options.force !== true && connection.identity in remote.failures.connections;
|
|
3851
|
+
const size = windowSize(remote, connection);
|
|
3852
|
+
if (!failed && (known === void 0 || known.stale || options.force === true)) {
|
|
3853
|
+
queries.push(size === void 0 ? connection : withWindow(connection, connection.window.first === void 0 ? { last: size } : { first: size }));
|
|
3168
3854
|
continue;
|
|
3169
3855
|
}
|
|
3170
3856
|
if (known === void 0) continue;
|
|
3857
|
+
const tail = failed || size === void 0 ? void 0 : missingRows(known, connection.window, size);
|
|
3858
|
+
if (tail !== void 0) queries.push(withWindow(connection, tail));
|
|
3171
3859
|
const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities);
|
|
3172
3860
|
items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
|
|
3173
3861
|
}
|
|
3174
|
-
const
|
|
3862
|
+
const visible = visibleStoreOf(remote.entities, remote.optimistic);
|
|
3863
|
+
const read = [...asked.requirements, ...items];
|
|
3864
|
+
const planned = plan(visible, read, options);
|
|
3175
3865
|
return {
|
|
3176
3866
|
requirements: options.force === true ? planned : withoutFailedFields(remote, planned),
|
|
3177
|
-
queries
|
|
3867
|
+
queries,
|
|
3868
|
+
expires: options.freshness === void 0 ? null : deadlineOf(visible, read, options.freshness) ?? null
|
|
3178
3869
|
};
|
|
3179
3870
|
};
|
|
3180
3871
|
/**
|
|
@@ -3220,7 +3911,8 @@ const queryRequestOf = (query) => {
|
|
|
3220
3911
|
try: () => ({
|
|
3221
3912
|
query: query.identity.slice(0, separator),
|
|
3222
3913
|
input: JSON.parse(query.identity.slice(separator + 1)),
|
|
3223
|
-
window: query.window
|
|
3914
|
+
window: query.window,
|
|
3915
|
+
...query.select === void 0 ? {} : { select: query.select }
|
|
3224
3916
|
}),
|
|
3225
3917
|
catch: () => new RemoteQueryError({ message: `connection "${query.identity}" carries no encoded input` })
|
|
3226
3918
|
});
|
|
@@ -3249,7 +3941,7 @@ const overrun = (window, edges) => {
|
|
|
3249
3941
|
got: edges.length
|
|
3250
3942
|
} : void 0;
|
|
3251
3943
|
};
|
|
3252
|
-
const pageMessage = (connection, result, refreshes = false, window = {}) => {
|
|
3944
|
+
const pageMessage = (connection, result, refreshes = false, window = {}, select, now) => {
|
|
3253
3945
|
const over = overrun(window, result.edges);
|
|
3254
3946
|
if (over !== void 0) return {
|
|
3255
3947
|
_tag: "QueryFailed",
|
|
@@ -3259,6 +3951,7 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
|
|
|
3259
3951
|
message: `the server returned ${over.got} edges for a window of ${over.asked}`
|
|
3260
3952
|
}
|
|
3261
3953
|
};
|
|
3954
|
+
const hasPayload = select !== void 0 && (result.entities !== void 0 && result.entities.length > 0 || result.settled !== void 0 && result.settled.length > 0);
|
|
3262
3955
|
return {
|
|
3263
3956
|
_tag: "ConnectionMerged",
|
|
3264
3957
|
connection,
|
|
@@ -3273,7 +3966,13 @@ const pageMessage = (connection, result, refreshes = false, window = {}) => {
|
|
|
3273
3966
|
start: result.start,
|
|
3274
3967
|
end: result.end
|
|
3275
3968
|
},
|
|
3276
|
-
...refreshes ? { refreshes } : {}
|
|
3969
|
+
...refreshes ? { refreshes } : {},
|
|
3970
|
+
...select === void 0 || !hasPayload ? {} : { select },
|
|
3971
|
+
...hasPayload ? {
|
|
3972
|
+
entities: [...result.entities ?? []],
|
|
3973
|
+
settled: [...result.settled ?? []],
|
|
3974
|
+
now: now ?? 0
|
|
3975
|
+
} : {}
|
|
3277
3976
|
};
|
|
3278
3977
|
};
|
|
3279
3978
|
/**
|
|
@@ -3301,7 +4000,7 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
|
|
|
3301
4000
|
const client = yield* RemoteClient;
|
|
3302
4001
|
const at = now();
|
|
3303
4002
|
const result = yield* Effect.result(client.read({
|
|
3304
|
-
version:
|
|
4003
|
+
version: 4,
|
|
3305
4004
|
requests: requirements
|
|
3306
4005
|
}));
|
|
3307
4006
|
return Result.isFailure(result) ? {
|
|
@@ -3316,14 +4015,14 @@ const readMessage = (requirements, now) => Effect.gen(function* () {
|
|
|
3316
4015
|
};
|
|
3317
4016
|
});
|
|
3318
4017
|
/** Runs a query and reports the page that refreshes its connection, or the failure. Never fails. */
|
|
3319
|
-
const queryMessage = (query) => Effect.gen(function* () {
|
|
4018
|
+
const queryMessage = (query, now = wallClock) => Effect.gen(function* () {
|
|
3320
4019
|
const client = yield* RemoteClient;
|
|
3321
4020
|
const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
|
|
3322
4021
|
return Result.isFailure(result) ? {
|
|
3323
4022
|
_tag: "QueryFailed",
|
|
3324
4023
|
connection: query.identity,
|
|
3325
4024
|
error: remoteError(result.failure)
|
|
3326
|
-
} : pageMessage(query.identity, result.success, true, query.window);
|
|
4025
|
+
} : pageMessage(query.identity, result.success, true, query.window, query.select, now());
|
|
3327
4026
|
});
|
|
3328
4027
|
/** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
|
|
3329
4028
|
const PlannedQuery = Schema.Struct({
|
|
@@ -3341,29 +4040,50 @@ const PlannedQuery = Schema.Struct({
|
|
|
3341
4040
|
* to the restart; a page and its refresh are one `ConnectionMerged`.
|
|
3342
4041
|
*/
|
|
3343
4042
|
const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
3344
|
-
const { policy = RemotePolicy.cacheFirst, now =
|
|
4043
|
+
const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
|
|
3345
4044
|
const read = (requirements) => Effect.map(readMessage(requirements, now), toMessage);
|
|
3346
|
-
const run = (query) => Effect.map(queryMessage(query), toMessage);
|
|
4045
|
+
const run = (query) => Effect.map(queryMessage(query, now), toMessage);
|
|
4046
|
+
const planned = /* @__PURE__ */ new WeakMap();
|
|
3347
4047
|
return {
|
|
3348
4048
|
dependenciesSchema: Schema.Struct({
|
|
3349
4049
|
requirements: Schema.Array(ReadRequest),
|
|
3350
4050
|
queries: Schema.Array(PlannedQuery),
|
|
3351
|
-
refresh: Schema.Number
|
|
4051
|
+
refresh: Schema.Number,
|
|
4052
|
+
expires: Schema.NullOr(Schema.Struct({
|
|
4053
|
+
at: Schema.Number,
|
|
4054
|
+
due: Schema.Array(ReadRequest)
|
|
4055
|
+
}))
|
|
3352
4056
|
}),
|
|
3353
4057
|
modelToDependencies: (model) => {
|
|
3354
4058
|
const remote = bound.store.get(model);
|
|
3355
|
-
const
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
4059
|
+
const asked = askedOf(model);
|
|
4060
|
+
const askedKey = stableStringify(asked);
|
|
4061
|
+
const at = now();
|
|
4062
|
+
let byAsked = planned.get(remote);
|
|
4063
|
+
const known = byAsked?.get(askedKey);
|
|
4064
|
+
if (known !== void 0 && at < known.until) return known.dependencies;
|
|
4065
|
+
const plan = planAsked(remote, asked, RemotePolicy.toPlan(policy, at));
|
|
4066
|
+
const dependencies = {
|
|
4067
|
+
refresh: refreshedAt(remote.refresh, plan.requirements, plan.queries.map((query) => query.identity)),
|
|
4068
|
+
requirements: plan.requirements,
|
|
4069
|
+
queries: plan.queries.map(({ identity, window, select }) => ({
|
|
3360
4070
|
identity,
|
|
3361
4071
|
window,
|
|
3362
4072
|
select
|
|
3363
|
-
}))
|
|
4073
|
+
})),
|
|
4074
|
+
expires: plan.expires
|
|
3364
4075
|
};
|
|
4076
|
+
if (byAsked === void 0) {
|
|
4077
|
+
byAsked = /* @__PURE__ */ new Map();
|
|
4078
|
+
planned.set(remote, byAsked);
|
|
4079
|
+
}
|
|
4080
|
+
byAsked.set(askedKey, {
|
|
4081
|
+
until: plan.expires?.at ?? Infinity,
|
|
4082
|
+
dependencies
|
|
4083
|
+
});
|
|
4084
|
+
return dependencies;
|
|
3365
4085
|
},
|
|
3366
|
-
dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
|
|
4086
|
+
dependenciesToStream: ({ requirements, queries, refresh, expires }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
|
|
3367
4087
|
_tag: "ReadStarted",
|
|
3368
4088
|
requests: requirements,
|
|
3369
4089
|
refresh
|
|
@@ -3373,21 +4093,28 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
|
|
|
3373
4093
|
})] : []], ...queries.length === 0 ? [] : [toMessage({
|
|
3374
4094
|
_tag: "QueryStarted",
|
|
3375
4095
|
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" }))
|
|
4096
|
+
})]]), 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({
|
|
4097
|
+
_tag: "RefreshStarted",
|
|
4098
|
+
requests: expires.due
|
|
4099
|
+
})))))
|
|
3377
4100
|
};
|
|
3378
4101
|
};
|
|
4102
|
+
const resumeCursor = Schema.Number.pipe(Schema.overrideToEquivalence(() => () => true));
|
|
3379
4103
|
/** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
|
|
3380
4104
|
const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
3381
4105
|
dependenciesSchema: Schema.Struct({
|
|
3382
4106
|
requirements: Schema.Array(ReadRequest),
|
|
3383
|
-
cursor:
|
|
4107
|
+
cursor: resumeCursor,
|
|
4108
|
+
floor: Schema.Number
|
|
3384
4109
|
}),
|
|
3385
4110
|
modelToDependencies: (model) => {
|
|
3386
4111
|
const requirements = requirementsOf(model);
|
|
3387
4112
|
const stream = liveStreamKey(requirements);
|
|
4113
|
+
const remote = bound.store.get(model);
|
|
3388
4114
|
return {
|
|
3389
4115
|
requirements,
|
|
3390
|
-
cursor:
|
|
4116
|
+
cursor: remote.live[stream]?.cursor ?? 0,
|
|
4117
|
+
floor: remote.refresh.floor
|
|
3391
4118
|
};
|
|
3392
4119
|
},
|
|
3393
4120
|
dependenciesToStream: ({ requirements, cursor }) => requirements.length === 0 ? Stream.empty : Stream.unwrap(Effect.gen(function* () {
|
|
@@ -3399,7 +4126,7 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
|
3399
4126
|
_tag: "LiveReceived",
|
|
3400
4127
|
stream: liveStreamKey(requirements),
|
|
3401
4128
|
event,
|
|
3402
|
-
now: (options.now ??
|
|
4129
|
+
now: (options.now ?? wallClock)()
|
|
3403
4130
|
})), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
|
|
3404
4131
|
_tag: "ReadFailed",
|
|
3405
4132
|
requests: requirements,
|
|
@@ -3407,8 +4134,8 @@ const liveEntry = (bound, requirementsOf, toMessage, options) => ({
|
|
|
3407
4134
|
stream: liveStreamKey(requirements)
|
|
3408
4135
|
}))))
|
|
3409
4136
|
});
|
|
3410
|
-
/** The projection an active Surface has for this Model
|
|
3411
|
-
const projectionOf = (entry, model) => "projectionOf" in entry ? entry.projectionOf(model) : entry.projection();
|
|
4137
|
+
/** The projection an active Surface has for this Model; none while it is inactive. */
|
|
4138
|
+
const projectionOf = (entry, model) => "projectionOf" in entry ? entry.projectionOf(model) : Option.some(entry.projection());
|
|
3412
4139
|
/**
|
|
3413
4140
|
* `projectionOf` computed once per Model: the read, live, and retain entries
|
|
3414
4141
|
* all derive their dependencies from the same projection on the same Model
|
|
@@ -3420,7 +4147,8 @@ const memoizedProjectionOf = (entry) => {
|
|
|
3420
4147
|
return (model) => {
|
|
3421
4148
|
const key = model;
|
|
3422
4149
|
if (typeof key !== "object" || key === null) return projectionOf(entry, model);
|
|
3423
|
-
|
|
4150
|
+
const known = cache.get(key);
|
|
4151
|
+
if (known !== void 0) return known;
|
|
3424
4152
|
const projection = projectionOf(entry, model);
|
|
3425
4153
|
cache.set(key, projection);
|
|
3426
4154
|
return projection;
|
|
@@ -3440,6 +4168,51 @@ const Remote = {
|
|
|
3440
4168
|
/** Whether a Message is one of Remote's, by tag. */
|
|
3441
4169
|
reduces: isRemoteMessage,
|
|
3442
4170
|
/**
|
|
4171
|
+
* Remote's Messages as one Schema, for a wrapper variant in the
|
|
4172
|
+
* application's union: `GotRemoteMessage: { message: Remote.Message }`. With
|
|
4173
|
+
* a wrapper, `update` matches the application's union exhaustively and
|
|
4174
|
+
* `Remote.fold` reduces what arrives inside it.
|
|
4175
|
+
*/
|
|
4176
|
+
Message: defineMessageUnion(remoteMessageCases),
|
|
4177
|
+
/**
|
|
4178
|
+
* A bound domain under one wrapper variant of the application's union, the
|
|
4179
|
+
* same shape Foldkit gives a Submodel: `toParentMessage` wraps Remote's
|
|
4180
|
+
* Messages, the fold reduces them, and its `fetch`, `mutate`, and
|
|
4181
|
+
* `subscriptions` yield the wrapper. Spreading `Remote.messages` and
|
|
4182
|
+
* narrowing with `Remote.reduces` remains the shorter path for an
|
|
4183
|
+
* application whose `update` only reduces.
|
|
4184
|
+
*
|
|
4185
|
+
* @example
|
|
4186
|
+
* ```ts
|
|
4187
|
+
* const Message = defineMessageUnion({ GotRemoteMessage: { message: Remote.Message } })
|
|
4188
|
+
* const foldData = Remote.fold(Data, message => Message.GotRemoteMessage({ message }))
|
|
4189
|
+
*
|
|
4190
|
+
* const update = (model: Model, message: Message): Return =>
|
|
4191
|
+
* Message.match(message, {
|
|
4192
|
+
* GotRemoteMessage: ({ message }) => foldData(model, message),
|
|
4193
|
+
* })
|
|
4194
|
+
* const subscriptions = Subscription.make<Model, Message, RemoteClient>()(() =>
|
|
4195
|
+
* foldData.subscriptions({ page: Surface.at(ProjectPage, …) }),
|
|
4196
|
+
* )
|
|
4197
|
+
* ```
|
|
4198
|
+
*/
|
|
4199
|
+
fold: (domain, toParentMessage) => {
|
|
4200
|
+
const fold = (model, message) => ({ model: domain.reduce(model, message) });
|
|
4201
|
+
return Object.assign(fold, {
|
|
4202
|
+
mutate: (...args) => {
|
|
4203
|
+
const started = domain.mutate(...args);
|
|
4204
|
+
return {
|
|
4205
|
+
...started,
|
|
4206
|
+
command: mapMessage(started.command, toParentMessage)
|
|
4207
|
+
};
|
|
4208
|
+
},
|
|
4209
|
+
subscriptions: (active, options) => markAll(Subscription.lift(brandEntries(domain.subscriptions(active, options)))({
|
|
4210
|
+
toChildModel: (model) => model,
|
|
4211
|
+
toParentMessage
|
|
4212
|
+
}))
|
|
4213
|
+
});
|
|
4214
|
+
},
|
|
4215
|
+
/**
|
|
3443
4216
|
* A patch of one entity's values, for a mutation's `optimistic` list or an
|
|
3444
4217
|
* overlay. Takes a `foldkit-entity` Entity or a Remote descriptor alike, so
|
|
3445
4218
|
* the domain you declared once is the one you patch:
|
|
@@ -3473,6 +4246,13 @@ const Remote = {
|
|
|
3473
4246
|
*/
|
|
3474
4247
|
at: bindRemote,
|
|
3475
4248
|
/**
|
|
4249
|
+
* The resume part for this domain, for `foldkit-ssr`'s `SSR.plan({ parts })`:
|
|
4250
|
+
* a server render sends what the plan's active Surfaces read from the store,
|
|
4251
|
+
* each connection with its boundaries and each live cursor with it, and
|
|
4252
|
+
* nothing else. `id` tells two domains in one application apart.
|
|
4253
|
+
*/
|
|
4254
|
+
resume: resumePart,
|
|
4255
|
+
/**
|
|
3476
4256
|
* A Projection node that reads a `RemoteData` value out of the store. The id
|
|
3477
4257
|
* is supplied by the caller, usually from a Surface's params. The assembled
|
|
3478
4258
|
* value is decoded against the Selection, so malformed server data surfaces
|
|
@@ -3483,6 +4263,7 @@ const Remote = {
|
|
|
3483
4263
|
const selection = selectionOf(given);
|
|
3484
4264
|
assertRegistered(bound, "Entity", bound.definition.registry.entities, selection.entity);
|
|
3485
4265
|
const relation = relationOf(selection);
|
|
4266
|
+
const relationKey = stableStringify(relation);
|
|
3486
4267
|
return (id) => ({
|
|
3487
4268
|
Model: remoteDataSchema(selection.schema),
|
|
3488
4269
|
dependencies: [],
|
|
@@ -3493,11 +4274,11 @@ const Remote = {
|
|
|
3493
4274
|
read: (root) => {
|
|
3494
4275
|
const store = storeOf(bound, root);
|
|
3495
4276
|
const key = entityKey(selection.entity, id);
|
|
3496
|
-
const present = memoRead(store,
|
|
4277
|
+
const present = memoRead([store], `${key}\u0000${relationKey}`, () => {
|
|
3497
4278
|
if (isTombstone(store, key)) return { _tag: "NotFound" };
|
|
3498
4279
|
const assembled = assemble(store, key, relation);
|
|
3499
|
-
if (assembled === void 0) return
|
|
3500
|
-
const decoded =
|
|
4280
|
+
if (assembled === void 0) return unavailableFailure(store, key, relation);
|
|
4281
|
+
const decoded = decodeRow(selection.schema, store[key].values, assembled.values);
|
|
3501
4282
|
return Result.isFailure(decoded) ? {
|
|
3502
4283
|
_tag: "Failed",
|
|
3503
4284
|
error: {
|
|
@@ -3520,14 +4301,36 @@ const Remote = {
|
|
|
3520
4301
|
previous: present.value
|
|
3521
4302
|
} : present;
|
|
3522
4303
|
if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
|
|
3523
|
-
|
|
4304
|
+
if (failure !== void 0) return {
|
|
3524
4305
|
_tag: "Failed",
|
|
3525
4306
|
error: failure
|
|
3526
4307
|
};
|
|
4308
|
+
if (onlyOverlaid(remote, selection.entity, id)) {
|
|
4309
|
+
const lacking = missingFields(store, key, relation.fields);
|
|
4310
|
+
return {
|
|
4311
|
+
_tag: "Failed",
|
|
4312
|
+
error: {
|
|
4313
|
+
_tag: "Overlaid",
|
|
4314
|
+
message: `${selection.entity} ${id} is shown only by an overlay, which does not hold ${lacking.length === 0 ? "what its relations select" : lacking.join(", ")}; the server has not seen it, so nothing will fetch them.`
|
|
4315
|
+
}
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
return { _tag: "Initial" };
|
|
3527
4319
|
}
|
|
3528
4320
|
});
|
|
3529
4321
|
},
|
|
3530
4322
|
/**
|
|
4323
|
+
* When what a projection shows was last received, and whether it is stale
|
|
4324
|
+
* or loading. Pure, so a view can show "updated 5s ago" without I/O.
|
|
4325
|
+
*
|
|
4326
|
+
* `updatedAt` is the newest server write among what is shown, as the
|
|
4327
|
+
* store dates it; `undefined` when nothing shown was received (nothing
|
|
4328
|
+
* loaded, or only an optimistic preview). `stale` is any shown field or
|
|
4329
|
+
* connection marked stale; `loading` is any of them in flight. Optimistic
|
|
4330
|
+
* previews do not date: showing only one reads `updatedAt: undefined`.
|
|
4331
|
+
*/
|
|
4332
|
+
meta: (bound, model, projection) => metaOf(bound.store.get(model), askedOf(projection)),
|
|
4333
|
+
/**
|
|
3531
4334
|
* The pure plan for a projection against a Model: the requirements its
|
|
3532
4335
|
* remote store does not satisfy, under `options` (freshness, force). A
|
|
3533
4336
|
* Surface's projection is `surface.projection(params)`.
|
|
@@ -3558,13 +4361,13 @@ const Remote = {
|
|
|
3558
4361
|
*/
|
|
3559
4362
|
prefetch: Effect.fn("Remote.prefetch")(function* (bound, model, projection, options = {}) {
|
|
3560
4363
|
const store = storeOf(bound, model);
|
|
3561
|
-
const { policy = RemotePolicy.cacheFirst, now =
|
|
4364
|
+
const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
|
|
3562
4365
|
const at = now();
|
|
3563
4366
|
const missing = plan(store, requirementsOf(projection), RemotePolicy.toPlan(policy, at));
|
|
3564
4367
|
if (missing.length === 0) return store;
|
|
3565
4368
|
yield* Effect.annotateCurrentSpan("requirementCount", missing.length);
|
|
3566
4369
|
const result = yield* (yield* RemoteClient).read({
|
|
3567
|
-
version:
|
|
4370
|
+
version: 4,
|
|
3568
4371
|
requests: missing
|
|
3569
4372
|
});
|
|
3570
4373
|
return writeRead(store, missing, result, at);
|
|
@@ -3617,6 +4420,17 @@ const Remote = {
|
|
|
3617
4420
|
});
|
|
3618
4421
|
},
|
|
3619
4422
|
/**
|
|
4423
|
+
* Forgets everything the server told this Model: values, tombstones,
|
|
4424
|
+
* unavailable fields, connections, live cursors, failures. What is known is
|
|
4425
|
+
* known for a principal, and this is the one boundary for a login, a logout
|
|
4426
|
+
* or a switch of organization: called from `update`, it performs no I/O, and
|
|
4427
|
+
* every active Surface's read and live entries restart, so the screen asks
|
|
4428
|
+
* again as whoever the client now is. A read or stream begun before is
|
|
4429
|
+
* interrupted rather than landing after. A mutation in flight is treated as
|
|
4430
|
+
* applied: its answer writes nothing here.
|
|
4431
|
+
*/
|
|
4432
|
+
forget: (bound, model) => bound.store.set(model, forgetRemote(bound.store.get(model))),
|
|
4433
|
+
/**
|
|
3620
4434
|
* Writes a read result into the store, recording each field's applied window.
|
|
3621
4435
|
* `requests` are the planned requirements the result answers.
|
|
3622
4436
|
*/
|
|
@@ -3633,7 +4447,7 @@ const Remote = {
|
|
|
3633
4447
|
query: (request) => client.FoldkitRemoteQuery(request).pipe(Effect.provideContext(context)),
|
|
3634
4448
|
mutate: (request) => client.FoldkitRemoteMutate(request).pipe(Effect.provideContext(context)),
|
|
3635
4449
|
live: ({ requirements, after }) => client.FoldkitRemoteLive({
|
|
3636
|
-
version:
|
|
4450
|
+
version: 4,
|
|
3637
4451
|
requirements,
|
|
3638
4452
|
after
|
|
3639
4453
|
}).pipe(Stream.map(liveEventOf), Stream.provideContext(context))
|
|
@@ -3666,18 +4480,21 @@ const Remote = {
|
|
|
3666
4480
|
/**
|
|
3667
4481
|
* Runs a `Query` through `RemoteClient`, encoding its input from the ref.
|
|
3668
4482
|
* Pair the result with `Remote.queryMessage` to merge the page into the Model.
|
|
4483
|
+
* With `select`, the server also returns the selected fields of the page's
|
|
4484
|
+
* items, so one response populates both the connection and the store.
|
|
3669
4485
|
*/
|
|
3670
|
-
query: Effect.fn("Remote.query")(function* (ref) {
|
|
4486
|
+
query: Effect.fn("Remote.query")(function* (ref, select) {
|
|
3671
4487
|
const client = yield* RemoteClient;
|
|
3672
4488
|
const input = yield* Schema.encodeUnknownEffect(ref.Input)(ref.input).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new RemoteQueryError({ message: error.message }))));
|
|
3673
4489
|
return yield* client.query({
|
|
3674
4490
|
query: ref.query,
|
|
3675
4491
|
input,
|
|
3676
|
-
window: ref.window
|
|
4492
|
+
window: ref.window,
|
|
4493
|
+
...select === void 0 ? {} : { select }
|
|
3677
4494
|
});
|
|
3678
4495
|
}),
|
|
3679
4496
|
/** The `RemoteMessage` that merges a query page into its connection. */
|
|
3680
|
-
queryMessage: (ref, result) => pageMessage(ref.identity, result, false, ref.window),
|
|
4497
|
+
queryMessage: (ref, result, select, now) => pageMessage(ref.identity, result, false, ref.window, select, now),
|
|
3681
4498
|
/**
|
|
3682
4499
|
* The edges a connection shows: its server-known region with pending and
|
|
3683
4500
|
* confirmed overlays placed around it, minus removed edges and edges whose
|
|
@@ -3716,7 +4533,8 @@ const Remote = {
|
|
|
3716
4533
|
requestId,
|
|
3717
4534
|
entities: outcome.entities,
|
|
3718
4535
|
connections: outcome.connections,
|
|
3719
|
-
deleted: outcome.deleted
|
|
4536
|
+
deleted: outcome.deleted,
|
|
4537
|
+
now: (options.now ?? wallClock)()
|
|
3720
4538
|
});
|
|
3721
4539
|
return {
|
|
3722
4540
|
output: outcome.output,
|
|
@@ -3743,9 +4561,20 @@ const Remote = {
|
|
|
3743
4561
|
live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
|
|
3744
4562
|
};
|
|
3745
4563
|
const brandEntries = (entries) => Subscription.make()(() => entries);
|
|
4564
|
+
/** Why a read failed, in words: Remote's own failures say what to do, and a server's error says itself. */
|
|
4565
|
+
const failedReadWords = (error) => {
|
|
4566
|
+
switch (error._tag) {
|
|
4567
|
+
case "DecodeError": return `What the server sent does not decode against the Selection: ${error.message}`;
|
|
4568
|
+
case "Unavailable": return `${error.message} Select without it, or Data.refresh asks again.`;
|
|
4569
|
+
case "Overlaid": return `${error.message} Overlay every field the Selection reads, or select fewer.`;
|
|
4570
|
+
default: return `Its request failed: ${error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`;
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
3746
4573
|
/** The bound domain: the descriptor, the binding, and the operations over them. */
|
|
3747
4574
|
const bindDomain = (definition, store) => {
|
|
3748
4575
|
const bound = bindRemote(definition, store);
|
|
4576
|
+
const readers = /* @__PURE__ */ new Map();
|
|
4577
|
+
const keptConnections = /* @__PURE__ */ new Set();
|
|
3749
4578
|
/**
|
|
3750
4579
|
* What to do with a row a live event says was inserted into a connection.
|
|
3751
4580
|
*
|
|
@@ -3786,7 +4615,12 @@ const bindDomain = (definition, store) => {
|
|
|
3786
4615
|
...live,
|
|
3787
4616
|
policy: livePolicyFor(model, live.event)
|
|
3788
4617
|
};
|
|
3789
|
-
|
|
4618
|
+
const remote = store.get(model);
|
|
4619
|
+
const next = updateRemote(remote, resolved);
|
|
4620
|
+
return next === remote ? model : store.set(model, next);
|
|
4621
|
+
};
|
|
4622
|
+
const assertOwned = (entry) => {
|
|
4623
|
+
if (bound.contract.owner !== void 0 && entry.owner !== bound.contract.owner) throw new Error(`Remote: Surface "${entry.name}" belongs to another application than domain "${bound.contract.name}"`);
|
|
3790
4624
|
};
|
|
3791
4625
|
const domain = {
|
|
3792
4626
|
...definition,
|
|
@@ -3802,47 +4636,56 @@ const bindDomain = (definition, store) => {
|
|
|
3802
4636
|
})))
|
|
3803
4637
|
};
|
|
3804
4638
|
},
|
|
4639
|
+
active: (name, projectionOf) => {
|
|
4640
|
+
const { owner } = bound.contract;
|
|
4641
|
+
if (owner === void 0) throw new Error(`Remote: "${name}" reads domain "${bound.contract.name}", whose Model field is a raw optic that names no application; make the domain from an application's field (App.model.remote) to give it reads`);
|
|
4642
|
+
return {
|
|
4643
|
+
name,
|
|
4644
|
+
owner,
|
|
4645
|
+
messages: [],
|
|
4646
|
+
projectionOf
|
|
4647
|
+
};
|
|
4648
|
+
},
|
|
3805
4649
|
subscriptions: (active, options = {}) => {
|
|
3806
4650
|
const entries = {};
|
|
3807
|
-
const
|
|
4651
|
+
for (const entry of Object.values(active)) assertOwned(entry);
|
|
3808
4652
|
for (const [key, entry] of Object.entries(active)) {
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
const asked = (model) => askedOf(projectionAt(model));
|
|
4653
|
+
const projectionAt = readers.get(entry) ?? memoizedProjectionOf(entry);
|
|
4654
|
+
readers.set(entry, projectionAt);
|
|
4655
|
+
const asked = (model) => askedWhile(projectionAt(model));
|
|
3813
4656
|
entries[`${key}.read`] = observeEntry(bound, asked, identityMessage, options);
|
|
3814
4657
|
entries[`${key}.live`] = liveEntry(bound, (model) => asked(model).requirements.filter((requirement) => requirement.live === true), identityMessage, options);
|
|
3815
4658
|
}
|
|
4659
|
+
for (const identity of options.connections ?? []) keptConnections.add(connectionIdentity(identity));
|
|
3816
4660
|
entries.retain = {
|
|
3817
4661
|
dependenciesSchema: retentionRootsSchema,
|
|
3818
|
-
modelToDependencies: (model) => rootsOf(
|
|
3819
|
-
const projection = projectionAt(model);
|
|
3820
|
-
return projection === void 0 ? [] : [projection];
|
|
3821
|
-
}), options),
|
|
4662
|
+
modelToDependencies: (model) => rootsOf([...readers.values()].flatMap((projectionAt) => Option.toArray(projectionAt(model))), { connections: [...keptConnections] }),
|
|
3822
4663
|
dependenciesToStream: (current) => Stream.fromEffect(Effect.succeed({
|
|
3823
4664
|
_tag: "RetentionChanged",
|
|
3824
4665
|
roots: current
|
|
3825
4666
|
}).pipe(Effect.delay(options.grace ?? 0)))
|
|
3826
4667
|
};
|
|
4668
|
+
markAll(entries);
|
|
3827
4669
|
return entries;
|
|
3828
4670
|
},
|
|
3829
4671
|
plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
|
|
4672
|
+
meta: (model, projection) => Remote.meta(bound, model, projection),
|
|
3830
4673
|
storeOf: (model) => storeOf(bound, model),
|
|
3831
4674
|
confirmed: (projection) => confirmed(bound, projection),
|
|
3832
4675
|
prefetch: (model, projection, options = {}) => Effect.gen(function* () {
|
|
3833
|
-
const { policy = RemotePolicy.cacheFirst, now =
|
|
4676
|
+
const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
|
|
3834
4677
|
const client = yield* RemoteClient;
|
|
3835
4678
|
const planOptions = RemotePolicy.toPlan(policy, now());
|
|
3836
4679
|
let current = model;
|
|
3837
4680
|
for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
|
|
3838
4681
|
const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
|
|
3839
|
-
current = reduce(current, pageMessage(query.identity, page, true, query.window));
|
|
4682
|
+
current = reduce(current, pageMessage(query.identity, page, true, query.window, query.select, now()));
|
|
3840
4683
|
}
|
|
3841
4684
|
const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
|
|
3842
4685
|
if (requirements.length === 0) return current;
|
|
3843
4686
|
const at = now();
|
|
3844
4687
|
const result = yield* client.read({
|
|
3845
|
-
version:
|
|
4688
|
+
version: 4,
|
|
3846
4689
|
requests: requirements
|
|
3847
4690
|
});
|
|
3848
4691
|
return reduce(current, {
|
|
@@ -3852,6 +4695,30 @@ const bindDomain = (definition, store) => {
|
|
|
3852
4695
|
now: at
|
|
3853
4696
|
});
|
|
3854
4697
|
}),
|
|
4698
|
+
satisfy: (model, active, options = {}) => Effect.gen(function* () {
|
|
4699
|
+
const { passes = 8, now = wallClock } = options;
|
|
4700
|
+
const entries = Object.values(active);
|
|
4701
|
+
entries.forEach(assertOwned);
|
|
4702
|
+
const planOptions = RemotePolicy.toPlan(RemotePolicy.cacheFirst, now());
|
|
4703
|
+
const plans = (current, entry) => {
|
|
4704
|
+
const planned = planAsked(store.get(current), askedWhile(projectionOf(entry, current)), planOptions);
|
|
4705
|
+
return planned.queries.length > 0 || planned.requirements.length > 0;
|
|
4706
|
+
};
|
|
4707
|
+
let current = model;
|
|
4708
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
4709
|
+
if (!entries.some((entry) => plans(current, entry))) return current;
|
|
4710
|
+
for (const entry of entries) {
|
|
4711
|
+
const projection = projectionOf(entry, current);
|
|
4712
|
+
if (Option.isSome(projection)) current = yield* domain.prefetch(current, projection.value, { now });
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
const reading = entries.filter((entry) => plans(current, entry));
|
|
4716
|
+
if (reading.length === 0) return current;
|
|
4717
|
+
return yield* new RemoteUnsatisfied({
|
|
4718
|
+
surfaces: reading.map((entry) => entry.name),
|
|
4719
|
+
passes
|
|
4720
|
+
});
|
|
4721
|
+
}),
|
|
3855
4722
|
query: (query, input, options) => {
|
|
3856
4723
|
assertRegistered(bound, "Query", definition.registry.queries, query.name);
|
|
3857
4724
|
const { select: given, ...window } = options;
|
|
@@ -3860,15 +4727,13 @@ const bindDomain = (definition, store) => {
|
|
|
3860
4727
|
const listed = query.Result.entity;
|
|
3861
4728
|
if (listed === void 0) throw new Error(`Remote: query "${query.name}" is not a connection over an entity, so it has no page to select`);
|
|
3862
4729
|
if (select.entity !== listed) throw new Error(`Remote: the selection is of "${select.entity}", but query "${query.name}" lists "${listed}"`);
|
|
3863
|
-
const ref =
|
|
3864
|
-
...query.ref(input),
|
|
3865
|
-
window: pickWindow(window)
|
|
3866
|
-
};
|
|
3867
|
-
const relation = relationOf(select);
|
|
4730
|
+
const { ref, relation, requirement } = readContract(query.ref(input), select, window);
|
|
3868
4731
|
const relationKey = stableStringify(relation);
|
|
4732
|
+
const sizeOf = (remote) => windowSize(remote, ref);
|
|
4733
|
+
const shown = (remote, connection) => cutToWindow(visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities), ref.window, sizeOf(remote));
|
|
3869
4734
|
const failedItem = (remote, connection) => {
|
|
3870
4735
|
if (Object.keys(remote.failures.fields).length === 0) return void 0;
|
|
3871
|
-
for (const edge of
|
|
4736
|
+
for (const edge of shown(remote, connection).edges) {
|
|
3872
4737
|
if (edge.ref.entity !== relation.entity) continue;
|
|
3873
4738
|
const error = failureOf(remote, edge.ref.entity, edge.ref.id, relation);
|
|
3874
4739
|
if (error !== void 0) return {
|
|
@@ -3879,21 +4744,26 @@ const bindDomain = (definition, store) => {
|
|
|
3879
4744
|
};
|
|
3880
4745
|
const loadingItem = (remote, connection) => {
|
|
3881
4746
|
if (remote.loading.size === 0) return void 0;
|
|
3882
|
-
for (const edge of
|
|
4747
|
+
for (const edge of shown(remote, connection).edges) {
|
|
3883
4748
|
if (edge.ref.entity !== relation.entity) continue;
|
|
3884
4749
|
if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
|
|
3885
4750
|
}
|
|
3886
4751
|
};
|
|
3887
4752
|
const readPage = (remote, connection) => {
|
|
3888
4753
|
const visible = visibleStoreOf(remote.entities, remote.optimistic);
|
|
3889
|
-
return memoRead(
|
|
4754
|
+
return memoRead([
|
|
4755
|
+
visible,
|
|
4756
|
+
connection,
|
|
4757
|
+
remote.optimistic.overlays
|
|
4758
|
+
], `${ref.identity}\u0000${relationKey}\u0000${String(sizeOf(remote))}`, () => {
|
|
3890
4759
|
const items = [];
|
|
3891
4760
|
let refreshing = connection.stale;
|
|
3892
|
-
const edges =
|
|
4761
|
+
const { edges, cutBefore, cutAfter } = shown(remote, connection);
|
|
3893
4762
|
for (const edge of edges) {
|
|
3894
|
-
const
|
|
3895
|
-
|
|
3896
|
-
|
|
4763
|
+
const key = entityKey(edge.ref.entity, edge.ref.id);
|
|
4764
|
+
const assembled = assemble(visible, key, relation);
|
|
4765
|
+
if (assembled === void 0) return unavailableFailure(visible, key, relation);
|
|
4766
|
+
const decoded = decodeRow(select.schema, visible[key].values, assembled.values);
|
|
3897
4767
|
if (Result.isFailure(decoded)) return {
|
|
3898
4768
|
_tag: "Failed",
|
|
3899
4769
|
error: {
|
|
@@ -3906,8 +4776,8 @@ const bindDomain = (definition, store) => {
|
|
|
3906
4776
|
}
|
|
3907
4777
|
const page = {
|
|
3908
4778
|
items,
|
|
3909
|
-
hasNext: hasNext(connection),
|
|
3910
|
-
hasPrevious: hasPrevious(connection)
|
|
4779
|
+
hasNext: cutAfter || hasNext(connection),
|
|
4780
|
+
hasPrevious: cutBefore || hasPrevious(connection)
|
|
3911
4781
|
};
|
|
3912
4782
|
return refreshing ? {
|
|
3913
4783
|
_tag: "Refreshing",
|
|
@@ -3918,12 +4788,6 @@ const bindDomain = (definition, store) => {
|
|
|
3918
4788
|
};
|
|
3919
4789
|
});
|
|
3920
4790
|
};
|
|
3921
|
-
const requirement = {
|
|
3922
|
-
identity: ref.identity,
|
|
3923
|
-
window: ref.window,
|
|
3924
|
-
select: relation,
|
|
3925
|
-
ref
|
|
3926
|
-
};
|
|
3927
4791
|
return {
|
|
3928
4792
|
Model: remoteDataSchema(pageSchema(select.schema)),
|
|
3929
4793
|
dependencies: [],
|
|
@@ -3959,7 +4823,9 @@ const bindDomain = (definition, store) => {
|
|
|
3959
4823
|
previous: read.value
|
|
3960
4824
|
};
|
|
3961
4825
|
case "Failed": return read;
|
|
3962
|
-
|
|
4826
|
+
case "Initial":
|
|
4827
|
+
case "Loading":
|
|
4828
|
+
case "NotFound": return {
|
|
3963
4829
|
_tag: "Failed",
|
|
3964
4830
|
error: failure
|
|
3965
4831
|
};
|
|
@@ -3967,32 +4833,23 @@ const bindDomain = (definition, store) => {
|
|
|
3967
4833
|
}
|
|
3968
4834
|
};
|
|
3969
4835
|
},
|
|
3970
|
-
|
|
3971
|
-
const
|
|
3972
|
-
const
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
const start = store.get(model).connections[projection.ref.identity]?.segments[0]?.start;
|
|
3983
|
-
return start?._tag === "Cursor" ? {
|
|
3984
|
-
...projection.ref,
|
|
3985
|
-
window: {
|
|
3986
|
-
...pageSize(projection.ref.window, "last"),
|
|
3987
|
-
before: start.cursor
|
|
3988
|
-
}
|
|
3989
|
-
} : void 0;
|
|
4836
|
+
more: (model, projection) => {
|
|
4837
|
+
const remote = store.get(model);
|
|
4838
|
+
const { ref } = projection;
|
|
4839
|
+
const size = windowSize(remote, ref);
|
|
4840
|
+
const page = ref.window.first ?? ref.window.last;
|
|
4841
|
+
if (size === void 0 || page === void 0 || page === 0) return Option.none();
|
|
4842
|
+
const read = projection.read(model);
|
|
4843
|
+
return (read._tag === "Ready" || read._tag === "Refreshing") && (ref.window.first === void 0 ? read.value.hasPrevious : read.value.hasNext) ? Option.some(bound.store.set(model, updateRemote(remote, {
|
|
4844
|
+
_tag: "WindowGrown",
|
|
4845
|
+
window: windowGrowthKey(ref.identity, stableStringify(ref.window)),
|
|
4846
|
+
size: size + page
|
|
4847
|
+
}))) : Option.none();
|
|
3990
4848
|
},
|
|
3991
4849
|
explain: (model, projection, options) => {
|
|
3992
4850
|
const { ref } = projection;
|
|
3993
4851
|
const reading = Object.values(options?.surfaces ?? {}).filter((active) => {
|
|
3994
|
-
|
|
3995
|
-
return active_ !== void 0 && connectionsOf(active_).some((connection) => connection.identity === ref.identity);
|
|
4852
|
+
return Option.exists(active.projectionOf(model), (shown) => connectionsOf(shown).some((connection) => connection.identity === ref.identity));
|
|
3996
4853
|
});
|
|
3997
4854
|
const body = definition.registry.queries.get(ref.query)?.body;
|
|
3998
4855
|
const [connection] = RemoteConnections.get(projection.metadata);
|
|
@@ -4021,8 +4878,7 @@ const bindDomain = (definition, store) => {
|
|
|
4021
4878
|
const state = projection.read(model);
|
|
4022
4879
|
const asked = askedOf(projection);
|
|
4023
4880
|
const reading = options?.surfaces === void 0 ? void 0 : Object.values(options.surfaces).filter((active) => {
|
|
4024
|
-
|
|
4025
|
-
return shown !== void 0 && covers(askedOf(shown), asked);
|
|
4881
|
+
return Option.exists(active.projectionOf(model), (shown) => covers(askedOf(shown), asked));
|
|
4026
4882
|
}).map((active) => active.name);
|
|
4027
4883
|
const withSurfaces = reading === void 0 ? {} : { surfaces: reading };
|
|
4028
4884
|
switch (state._tag) {
|
|
@@ -4048,7 +4904,7 @@ const bindDomain = (definition, store) => {
|
|
|
4048
4904
|
};
|
|
4049
4905
|
case "Failed": return {
|
|
4050
4906
|
state: state._tag,
|
|
4051
|
-
message: state.error
|
|
4907
|
+
message: failedReadWords(state.error),
|
|
4052
4908
|
...withSurfaces
|
|
4053
4909
|
};
|
|
4054
4910
|
case "Initial":
|
|
@@ -4088,7 +4944,7 @@ const bindDomain = (definition, store) => {
|
|
|
4088
4944
|
assembledAll = false;
|
|
4089
4945
|
continue;
|
|
4090
4946
|
}
|
|
4091
|
-
const decoded =
|
|
4947
|
+
const decoded = decodeRow(over.selection.schema, visible[key].values, assembled.values);
|
|
4092
4948
|
if (Result.isFailure(decoded)) {
|
|
4093
4949
|
assembledAll = false;
|
|
4094
4950
|
continue;
|
|
@@ -4101,33 +4957,15 @@ const bindDomain = (definition, store) => {
|
|
|
4101
4957
|
};
|
|
4102
4958
|
},
|
|
4103
4959
|
refresh: (model, target) => Remote.refresh(bound, model, target),
|
|
4104
|
-
|
|
4960
|
+
forget: (model) => Remote.forget(bound, model),
|
|
4961
|
+
overlay: (model, id, optimistic) => reduce(model, {
|
|
4105
4962
|
_tag: "OverlayShown",
|
|
4106
4963
|
id,
|
|
4107
4964
|
optimistic
|
|
4108
|
-
})
|
|
4109
|
-
lift: (model, id) => {
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
_tag: "OverlayLifted",
|
|
4113
|
-
id
|
|
4114
|
-
});
|
|
4115
|
-
return lifted === remote ? model : bound.store.set(model, lifted);
|
|
4116
|
-
},
|
|
4117
|
-
fetch: (ref) => ({
|
|
4118
|
-
name: `Remote.query(${ref.query})`,
|
|
4119
|
-
args: {
|
|
4120
|
-
connection: ref.identity,
|
|
4121
|
-
window: ref.window
|
|
4122
|
-
},
|
|
4123
|
-
effect: Remote.query(ref).pipe(Effect.match({
|
|
4124
|
-
onFailure: (error) => ({
|
|
4125
|
-
_tag: "QueryFailed",
|
|
4126
|
-
connection: ref.identity,
|
|
4127
|
-
error: remoteError(error)
|
|
4128
|
-
}),
|
|
4129
|
-
onSuccess: (page) => pageMessage(ref.identity, page, false, ref.window)
|
|
4130
|
-
}))
|
|
4965
|
+
}),
|
|
4966
|
+
lift: (model, id) => reduce(model, {
|
|
4967
|
+
_tag: "OverlayLifted",
|
|
4968
|
+
id
|
|
4131
4969
|
}),
|
|
4132
4970
|
mutate: (model, mutation, input, options = {}) => {
|
|
4133
4971
|
assertRegistered(bound, "Mutation", definition.registry.mutations, mutation.name);
|
|
@@ -4161,7 +4999,8 @@ const bindDomain = (definition, store) => {
|
|
|
4161
4999
|
requestId,
|
|
4162
5000
|
entities: outcome.entities,
|
|
4163
5001
|
connections: outcome.connections,
|
|
4164
|
-
deleted: outcome.deleted
|
|
5002
|
+
deleted: outcome.deleted,
|
|
5003
|
+
now: (options.now ?? wallClock)()
|
|
4165
5004
|
})
|
|
4166
5005
|
}))
|
|
4167
5006
|
}
|
|
@@ -4181,18 +5020,41 @@ const bindDomain = (definition, store) => {
|
|
|
4181
5020
|
return domain;
|
|
4182
5021
|
};
|
|
4183
5022
|
/** The window keys of a `QueryOptions`, and only those, without the undefined ones. */
|
|
5023
|
+
/**
|
|
5024
|
+
* How many rows a read of `ref` shows: its window's `first` or `last`, as
|
|
5025
|
+
* `Data.more` has grown it. `undefined` for a window with a cursor or no size,
|
|
5026
|
+
* which shows whatever the connection holds.
|
|
5027
|
+
*/
|
|
5028
|
+
const windowSize = (remote, ref) => {
|
|
5029
|
+
const { first, last, after, before } = ref.window;
|
|
5030
|
+
if (after !== void 0 || before !== void 0) return void 0;
|
|
5031
|
+
const size = first ?? last;
|
|
5032
|
+
return size === void 0 ? void 0 : remote.grown[windowGrowthKey(ref.identity, stableStringify(ref.window))] ?? size;
|
|
5033
|
+
};
|
|
5034
|
+
/** The edges a window of `size` shows, from the start (`first`) or the end (`last`), and which end it cut. */
|
|
5035
|
+
const cutToWindow = (edges, window, size) => {
|
|
5036
|
+
if (size === void 0 || edges.length <= size) return {
|
|
5037
|
+
edges,
|
|
5038
|
+
cutBefore: false,
|
|
5039
|
+
cutAfter: false
|
|
5040
|
+
};
|
|
5041
|
+
return window.first === void 0 ? {
|
|
5042
|
+
edges: edges.slice(edges.length - size),
|
|
5043
|
+
cutBefore: true,
|
|
5044
|
+
cutAfter: false
|
|
5045
|
+
} : {
|
|
5046
|
+
edges: edges.slice(0, size),
|
|
5047
|
+
cutBefore: false,
|
|
5048
|
+
cutAfter: true
|
|
5049
|
+
};
|
|
5050
|
+
};
|
|
4184
5051
|
const pickWindow = (window) => ({
|
|
4185
5052
|
...window.first === void 0 ? {} : { first: window.first },
|
|
4186
5053
|
...window.last === void 0 ? {} : { last: window.last },
|
|
4187
5054
|
...window.after === void 0 ? {} : { after: window.after },
|
|
4188
5055
|
...window.before === void 0 ? {} : { before: window.before }
|
|
4189
5056
|
});
|
|
4190
|
-
/** The page size of a window, carried onto the next or previous page's window under `key`. */
|
|
4191
|
-
const pageSize = (window, key) => {
|
|
4192
|
-
const size = window.first ?? window.last;
|
|
4193
|
-
return size === void 0 ? {} : { [key]: size };
|
|
4194
|
-
};
|
|
4195
5057
|
//#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,
|
|
5058
|
+
export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, RemoteUnsatisfied, Requirement, Selection, SettledFields, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, deadlineOf, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, forgetRemote, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isFieldUnavailable, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, onlyOverlaid, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, setUnavailable, settleFailure, settleSuccess, stableStringify, targetsOf, terminal, tombstone, unavailableOf, unknown, updateRemote, visibleItems, visibleStore, windowGrowthKey, windowKey, withRefreshRequested, writeEntities, writeEntity, writeRead };
|
|
4197
5059
|
|
|
4198
5060
|
//# sourceMappingURL=index.mjs.map
|