foldkit-remote 0.20.0 → 0.22.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 +987 -198
- package/dist/answerGaps.d.ts +16 -0
- package/dist/answerGaps.d.ts.map +1 -0
- package/dist/answerGaps.js +60 -0
- package/dist/answerGaps.js.map +1 -0
- package/dist/client-B055kQuP.d.ts +110 -0
- package/dist/client-B055kQuP.d.ts.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +142 -0
- package/dist/client.js.map +1 -0
- package/dist/coalesce-CLAr92Nk.d.ts +34 -0
- package/dist/coalesce-CLAr92Nk.d.ts.map +1 -0
- package/dist/coalesce.d.ts +2 -0
- package/dist/coalesce.js +131 -0
- package/dist/coalesce.js.map +1 -0
- package/dist/connection-BRyaJy89.d.ts +45 -0
- package/dist/connection-BRyaJy89.d.ts.map +1 -0
- package/dist/connection.d.ts +2 -0
- package/dist/connection.js +170 -0
- package/dist/connection.js.map +1 -0
- package/dist/data.d.ts +7 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +21 -0
- package/dist/data.js.map +1 -0
- package/dist/diagnostics-DHhVYflV.d.ts +125 -0
- package/dist/diagnostics-DHhVYflV.d.ts.map +1 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +60 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/entity-CgsG5Uel.d.ts +196 -0
- package/dist/entity-CgsG5Uel.d.ts.map +1 -0
- package/dist/entity-DuDxaa3n.js +279 -0
- package/dist/entity-DuDxaa3n.js.map +1 -0
- package/dist/entity.d.ts +2 -0
- package/dist/entity.js +1 -0
- package/dist/held.d.ts +19 -0
- package/dist/held.d.ts.map +1 -0
- package/dist/held.js +70 -0
- package/dist/held.js.map +1 -0
- package/dist/impact.d.ts +79 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +166 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +1542 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2791 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-DtRQGCRP.d.ts +134 -0
- package/dist/inspect-DtRQGCRP.d.ts.map +1 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +33 -0
- package/dist/inspect.js.map +1 -0
- package/dist/json-DowLl1eB.d.ts +52 -0
- package/dist/json-DowLl1eB.d.ts.map +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +123 -0
- package/dist/json.js.map +1 -0
- package/dist/law.d.ts +56 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +201 -0
- package/dist/law.js.map +1 -0
- package/dist/live-BpTzlCnY.d.ts +79 -0
- package/dist/live-BpTzlCnY.d.ts.map +1 -0
- package/dist/live.d.ts +2 -0
- package/dist/live.js +126 -0
- package/dist/live.js.map +1 -0
- package/dist/matching-DF5F9Us8.d.ts +100 -0
- package/dist/matching-DF5F9Us8.d.ts.map +1 -0
- package/dist/matching.d.ts +2 -0
- package/dist/matching.js +172 -0
- package/dist/matching.js.map +1 -0
- package/dist/model-B6zk1NMB.d.ts +710 -0
- package/dist/model-B6zk1NMB.d.ts.map +1 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +1308 -0
- package/dist/model.js.map +1 -0
- package/dist/mutation.d.ts +2 -0
- package/dist/mutation.js +612 -0
- package/dist/mutation.js.map +1 -0
- package/dist/optimistic-CECsfNGY.d.ts +523 -0
- package/dist/optimistic-CECsfNGY.d.ts.map +1 -0
- package/dist/optimistic.d.ts +2 -0
- package/dist/optimistic.js +336 -0
- package/dist/optimistic.js.map +1 -0
- package/dist/outbox-DNuc4kZp.d.ts +259 -0
- package/dist/outbox-DNuc4kZp.d.ts.map +1 -0
- package/dist/outbox.d.ts +2 -0
- package/dist/outbox.js +326 -0
- package/dist/outbox.js.map +1 -0
- package/dist/patchValues.d.ts +10 -0
- package/dist/patchValues.d.ts.map +1 -0
- package/dist/patchValues.js +57 -0
- package/dist/patchValues.js.map +1 -0
- package/dist/persistence-CCMnvg7-.d.ts +114 -0
- package/dist/persistence-CCMnvg7-.d.ts.map +1 -0
- package/dist/persistence.d.ts +2 -0
- package/dist/persistence.js +205 -0
- package/dist/persistence.js.map +1 -0
- package/dist/placement-jZdsIpmj.d.ts +50 -0
- package/dist/placement-jZdsIpmj.d.ts.map +1 -0
- package/dist/placement.d.ts +2 -0
- package/dist/placement.js +116 -0
- package/dist/placement.js.map +1 -0
- package/dist/plan-D9cftxC_.d.ts +35 -0
- package/dist/plan-D9cftxC_.d.ts.map +1 -0
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +100 -0
- package/dist/plan.js.map +1 -0
- package/dist/policy-DsWHmIAq.d.ts +42 -0
- package/dist/policy-DsWHmIAq.d.ts.map +1 -0
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js +32 -0
- package/dist/policy.js.map +1 -0
- package/dist/{port.d.mts → port.d.ts} +3 -2
- package/dist/port.d.ts.map +1 -0
- package/dist/{port.mjs → port.js} +3 -2
- package/dist/port.js.map +1 -0
- package/dist/query-BuIU93xA.d.ts +141 -0
- package/dist/query-BuIU93xA.d.ts.map +1 -0
- package/dist/query.d.ts +2 -0
- package/dist/query.js +0 -0
- package/dist/query.js.map +1 -0
- package/dist/record.d.ts +11 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +21 -0
- package/dist/record.js.map +1 -0
- package/dist/relation-BLBGOZom.d.ts +59 -0
- package/dist/relation-BLBGOZom.d.ts.map +1 -0
- package/dist/relation.d.ts +2 -0
- package/dist/relation.js +93 -0
- package/dist/relation.js.map +1 -0
- package/dist/remoteData-mK1suYi1.d.ts +148 -0
- package/dist/remoteData-mK1suYi1.d.ts.map +1 -0
- package/dist/remoteData.d.ts +2 -0
- package/dist/remoteData.js +179 -0
- package/dist/remoteData.js.map +1 -0
- package/dist/reporting.d.ts +12 -0
- package/dist/reporting.d.ts.map +1 -0
- package/dist/reporting.js +45 -0
- package/dist/reporting.js.map +1 -0
- package/dist/requirement-D57S2LpH.d.ts +64 -0
- package/dist/requirement-D57S2LpH.d.ts.map +1 -0
- package/dist/requirement.d.ts +2 -0
- package/dist/requirement.js +114 -0
- package/dist/requirement.js.map +1 -0
- package/dist/resume-Ct5R6yli.d.ts +92 -0
- package/dist/resume-Ct5R6yli.d.ts.map +1 -0
- package/dist/resume.d.ts +2 -0
- package/dist/resume.js +222 -0
- package/dist/resume.js.map +1 -0
- package/dist/retain-BfNTvBRb.d.ts +48 -0
- package/dist/retain-BfNTvBRb.d.ts.map +1 -0
- package/dist/retain.d.ts +2 -0
- package/dist/retain.js +107 -0
- package/dist/retain.js.map +1 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +1 -0
- package/dist/shown.d.ts +20 -0
- package/dist/shown.d.ts.map +1 -0
- package/dist/shown.js +40 -0
- package/dist/shown.js.map +1 -0
- package/dist/sse-C7LhdbiQ.d.ts +17 -0
- package/dist/sse-C7LhdbiQ.d.ts.map +1 -0
- package/dist/sse.d.ts +2 -0
- package/dist/sse.js +127 -0
- package/dist/sse.js.map +1 -0
- package/dist/store-ah2qjcJW.d.ts +100 -0
- package/dist/store-ah2qjcJW.d.ts.map +1 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +218 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +133 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +0 -0
- package/dist/testing.js.map +1 -0
- package/dist/why-kgp_Wr3z.d.ts +483 -0
- package/dist/why-kgp_Wr3z.d.ts.map +1 -0
- package/dist/why.d.ts +2 -0
- package/dist/why.js +293 -0
- package/dist/why.js.map +1 -0
- package/dist/whyReasons.d.ts +42 -0
- package/dist/whyReasons.d.ts.map +1 -0
- package/dist/whyReasons.js +211 -0
- package/dist/whyReasons.js.map +1 -0
- package/dist/wire-DthPcTM6.d.ts +787 -0
- package/dist/wire-DthPcTM6.d.ts.map +1 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +329 -0
- package/dist/wire.js.map +1 -0
- package/package.json +19 -14
- package/dist/index-BEvVAL76.d.mts +0 -4097
- package/dist/index-BEvVAL76.d.mts.map +0 -1
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/port.d.mts.map +0 -1
- package/dist/port.mjs.map +0 -1
- package/dist/src-BDqn7Ocr.mjs +0 -6344
- package/dist/src-BDqn7Ocr.mjs.map +0 -1
package/dist/model.js
ADDED
|
@@ -0,0 +1,1308 @@
|
|
|
1
|
+
import { PredictionPart, liveRefused, outboxUnreadable, outboxUnsaved, predictionMismatch, withDiagnostics } from "./diagnostics.js";
|
|
2
|
+
import { put } from "./record.js";
|
|
3
|
+
import { isRefPage, targetsOf } from "./relation.js";
|
|
4
|
+
import { askAgain, emptyStore, entityKey, entry, isTombstone, readField, remove, setStale, setUnavailable, tombstone, writeEntities } from "./store.js";
|
|
5
|
+
import { beginMutation, emptyMutationState, failMutation, forgetFailure, settledLater } from "./mutation.js";
|
|
6
|
+
import { AnswerRead, NormalizedEntity, ReadBatchResult, ReadRequest, RelationRequest, SettledFields } from "./wire.js";
|
|
7
|
+
import { emptyConnection, merge, unknown } from "./connection.js";
|
|
8
|
+
import { remoteErrorSchema } from "./remoteData.js";
|
|
9
|
+
import { beginOptimistic, emptyOptimistic, pruneOverlays, settleFailure, settleSuccess } from "./optimistic.js";
|
|
10
|
+
import { applyConnectionEvent, applyEntityEvent, emptyLiveState } from "./live.js";
|
|
11
|
+
import { Queued, emptyOutbox, operationOf, revisionOf, withDone } from "./outbox.js";
|
|
12
|
+
import { windowKey } from "./plan.js";
|
|
13
|
+
import { gc, reachable, recentRoots } from "./retain.js";
|
|
14
|
+
import { RemotePersistence } from "./persistence.js";
|
|
15
|
+
import { Option, Schema } from "effect";
|
|
16
|
+
// src/model.ts
|
|
17
|
+
/**
|
|
18
|
+
* The Remote submodel: the store, connections, mutations, live state, and
|
|
19
|
+
* optimistic layers an application keeps in its Model, the Messages that
|
|
20
|
+
* change it, and the pure reducer over them.
|
|
21
|
+
*/
|
|
22
|
+
const noFailures = {
|
|
23
|
+
connections: {},
|
|
24
|
+
fields: {}
|
|
25
|
+
};
|
|
26
|
+
const emptyRefresh = {
|
|
27
|
+
generation: 0,
|
|
28
|
+
requested: /* @__PURE__ */ new Map(),
|
|
29
|
+
started: /* @__PURE__ */ new Map(),
|
|
30
|
+
floor: 0
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The Model with everything the server told it forgotten: values, tombstones,
|
|
34
|
+
* unavailable fields, connections, live cursors, failures, and what is in
|
|
35
|
+
* flight. For a change of principal, whose facts these were. The mutation
|
|
36
|
+
* sequence is kept, so request ids stay unique, and a mutation in flight is
|
|
37
|
+
* treated as applied: its answer, when it comes, writes nothing here.
|
|
38
|
+
*
|
|
39
|
+
* `restoredFrom` is kept too: the emptied store is now what that key holds,
|
|
40
|
+
* so `Data.persistence` saves it over the old snapshot and never restores the
|
|
41
|
+
* key again. Restoring would bring back exactly what was forgotten.
|
|
42
|
+
*/
|
|
43
|
+
const forgetRemote = (model) => {
|
|
44
|
+
const generation = model.refresh.generation + 1;
|
|
45
|
+
return {
|
|
46
|
+
...initialRemoteModel,
|
|
47
|
+
restoredFrom: model.restoredFrom,
|
|
48
|
+
refresh: {
|
|
49
|
+
generation,
|
|
50
|
+
requested: /* @__PURE__ */ new Map(),
|
|
51
|
+
started: /* @__PURE__ */ new Map(),
|
|
52
|
+
floor: generation
|
|
53
|
+
},
|
|
54
|
+
mutations: {
|
|
55
|
+
...emptyMutationState,
|
|
56
|
+
sequence: model.mutations.sequence,
|
|
57
|
+
applied: /* @__PURE__ */ new Set([...model.mutations.applied, ...model.mutations.pending])
|
|
58
|
+
},
|
|
59
|
+
outbox: {
|
|
60
|
+
...emptyOutbox,
|
|
61
|
+
session: model.outbox.session
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const initialRemoteModel = {
|
|
66
|
+
entities: emptyStore,
|
|
67
|
+
connections: {},
|
|
68
|
+
grown: {},
|
|
69
|
+
optimistic: emptyOptimistic,
|
|
70
|
+
live: {},
|
|
71
|
+
mutations: emptyMutationState,
|
|
72
|
+
outbox: emptyOutbox,
|
|
73
|
+
gaps: /* @__PURE__ */ new Set(),
|
|
74
|
+
streams: {},
|
|
75
|
+
loading: /* @__PURE__ */ new Set(),
|
|
76
|
+
refresh: emptyRefresh,
|
|
77
|
+
failures: noFailures,
|
|
78
|
+
restoredFrom: Option.none(),
|
|
79
|
+
placed: {},
|
|
80
|
+
diagnostics: [],
|
|
81
|
+
recent: {
|
|
82
|
+
requirements: [],
|
|
83
|
+
connections: []
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** The entity store and the mutation ledger are runtime values, not wire shapes. */
|
|
87
|
+
const runtimeSchema = Schema.Unknown;
|
|
88
|
+
const remoteModelSchema = () => Schema.Struct({
|
|
89
|
+
entities: runtimeSchema,
|
|
90
|
+
connections: Schema.Record(Schema.String, runtimeSchema),
|
|
91
|
+
grown: Schema.Record(Schema.String, Schema.Number),
|
|
92
|
+
optimistic: runtimeSchema,
|
|
93
|
+
live: Schema.Record(Schema.String, runtimeSchema),
|
|
94
|
+
mutations: runtimeSchema,
|
|
95
|
+
outbox: runtimeSchema,
|
|
96
|
+
gaps: runtimeSchema,
|
|
97
|
+
streams: Schema.Record(Schema.String, runtimeSchema),
|
|
98
|
+
loading: runtimeSchema,
|
|
99
|
+
refresh: runtimeSchema,
|
|
100
|
+
failures: Schema.Struct({
|
|
101
|
+
connections: Schema.Record(Schema.String, remoteErrorSchema),
|
|
102
|
+
fields: Schema.Record(Schema.String, remoteErrorSchema)
|
|
103
|
+
}),
|
|
104
|
+
restoredFrom: runtimeSchema,
|
|
105
|
+
placed: runtimeSchema,
|
|
106
|
+
diagnostics: runtimeSchema,
|
|
107
|
+
recent: runtimeSchema
|
|
108
|
+
});
|
|
109
|
+
const retentionRootsSchema = Schema.Struct({
|
|
110
|
+
requirements: Schema.Array(ReadRequest),
|
|
111
|
+
connections: Schema.Array(Schema.Struct({
|
|
112
|
+
identity: Schema.String,
|
|
113
|
+
select: Schema.optional(RelationRequest)
|
|
114
|
+
}))
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* The Messages' fields by tag, without `_tag`: what `defineMessageUnion` takes,
|
|
118
|
+
* so an application spreads them into its own union (`Remote.messages`).
|
|
119
|
+
*/
|
|
120
|
+
const remoteMessageCases = {
|
|
121
|
+
ReadReceived: {
|
|
122
|
+
requests: Schema.Array(ReadRequest),
|
|
123
|
+
result: ReadBatchResult,
|
|
124
|
+
now: Schema.Number
|
|
125
|
+
},
|
|
126
|
+
ReadFailed: {
|
|
127
|
+
requests: Schema.Array(ReadRequest),
|
|
128
|
+
error: remoteErrorSchema,
|
|
129
|
+
stream: Schema.optional(Schema.String)
|
|
130
|
+
},
|
|
131
|
+
RefreshStarted: { requests: Schema.Array(ReadRequest) },
|
|
132
|
+
ReadStarted: {
|
|
133
|
+
requests: Schema.Array(ReadRequest),
|
|
134
|
+
refresh: Schema.optional(Schema.Number)
|
|
135
|
+
},
|
|
136
|
+
QueryStarted: { connections: Schema.Array(Schema.String) },
|
|
137
|
+
RetentionChanged: { roots: retentionRootsSchema },
|
|
138
|
+
Hydrated: {
|
|
139
|
+
connections: Schema.optional(Schema.Unknown),
|
|
140
|
+
from: Schema.optional(Schema.String),
|
|
141
|
+
entities: runtimeSchema,
|
|
142
|
+
merge: Schema.Union([Schema.Literal("replace"), Schema.Literal("preserve-existing")])
|
|
143
|
+
},
|
|
144
|
+
MutationStarted: {
|
|
145
|
+
requestId: Schema.String,
|
|
146
|
+
optimistic: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
147
|
+
viewer: Schema.optional(Schema.Struct({
|
|
148
|
+
entity: Schema.String,
|
|
149
|
+
id: Schema.String
|
|
150
|
+
})),
|
|
151
|
+
write: Schema.optional(Schema.Struct({
|
|
152
|
+
mutation: Schema.String,
|
|
153
|
+
input: Schema.Unknown,
|
|
154
|
+
keys: Schema.optional(Schema.Array(Schema.String)),
|
|
155
|
+
now: Schema.optional(Schema.Number),
|
|
156
|
+
durable: Schema.optional(Schema.Struct({
|
|
157
|
+
input: Schema.Unknown,
|
|
158
|
+
shown: Schema.Array(RelationRequest)
|
|
159
|
+
}))
|
|
160
|
+
}))
|
|
161
|
+
},
|
|
162
|
+
MutationQueued: { requestId: Schema.String },
|
|
163
|
+
OutboxRead: {
|
|
164
|
+
key: Schema.String,
|
|
165
|
+
scope: Schema.String,
|
|
166
|
+
session: Schema.optional(Schema.String),
|
|
167
|
+
entries: Schema.Array(Schema.Unknown),
|
|
168
|
+
done: Schema.Array(Schema.String),
|
|
169
|
+
unsaved: Schema.optional(Schema.String)
|
|
170
|
+
},
|
|
171
|
+
OutboxDelayed: {
|
|
172
|
+
requestId: Schema.String,
|
|
173
|
+
attempt: Schema.Number,
|
|
174
|
+
error: remoteErrorSchema
|
|
175
|
+
},
|
|
176
|
+
MutationSucceeded: {
|
|
177
|
+
requestId: Schema.String,
|
|
178
|
+
entities: Schema.Array(NormalizedEntity),
|
|
179
|
+
connections: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
180
|
+
deleted: Schema.optional(Schema.Array(Schema.Struct({
|
|
181
|
+
entity: Schema.String,
|
|
182
|
+
id: Schema.String
|
|
183
|
+
}))),
|
|
184
|
+
read: Schema.optional(AnswerRead),
|
|
185
|
+
now: Schema.Number
|
|
186
|
+
},
|
|
187
|
+
MutationFailed: {
|
|
188
|
+
requestId: Schema.String,
|
|
189
|
+
error: remoteErrorSchema,
|
|
190
|
+
operation: Schema.String
|
|
191
|
+
},
|
|
192
|
+
OverlayShown: {
|
|
193
|
+
id: Schema.String,
|
|
194
|
+
optimistic: Schema.Array(Schema.Unknown)
|
|
195
|
+
},
|
|
196
|
+
OverlayLifted: { id: Schema.String },
|
|
197
|
+
WindowGrown: {
|
|
198
|
+
window: Schema.String,
|
|
199
|
+
size: Schema.Number
|
|
200
|
+
},
|
|
201
|
+
LiveReceived: {
|
|
202
|
+
stream: Schema.String,
|
|
203
|
+
event: Schema.Unknown,
|
|
204
|
+
policy: Schema.optional(Schema.Unknown),
|
|
205
|
+
now: Schema.Number
|
|
206
|
+
},
|
|
207
|
+
GapCleared: { stream: Schema.String },
|
|
208
|
+
ConnectionMerged: {
|
|
209
|
+
connection: Schema.String,
|
|
210
|
+
page: Schema.Unknown,
|
|
211
|
+
refreshes: Schema.optional(Schema.Boolean),
|
|
212
|
+
select: Schema.optional(RelationRequest),
|
|
213
|
+
entities: Schema.optional(Schema.Array(NormalizedEntity)),
|
|
214
|
+
settled: Schema.optional(Schema.Array(SettledFields)),
|
|
215
|
+
now: Schema.optional(Schema.Number)
|
|
216
|
+
},
|
|
217
|
+
ConnectionInvalidated: { connection: Schema.String },
|
|
218
|
+
ConnectionRefreshed: { connection: Schema.String },
|
|
219
|
+
QueryFailed: {
|
|
220
|
+
connection: Schema.String,
|
|
221
|
+
error: remoteErrorSchema
|
|
222
|
+
},
|
|
223
|
+
Preloaded: { messages: Schema.Array(Schema.suspend(() => remoteMessageSchema)) },
|
|
224
|
+
CompletedReportDiagnostics: {}
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Whether a Message is one of Remote's, by tag. Narrows an application's union
|
|
228
|
+
* to its Remote cases, and its complement to the application's own.
|
|
229
|
+
*/
|
|
230
|
+
const isRemoteMessage = (message) => Object.hasOwn(remoteMessageCases, message._tag);
|
|
231
|
+
const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(([tag, fields]) => Schema.Struct({
|
|
232
|
+
_tag: Schema.Literal(tag),
|
|
233
|
+
...fields
|
|
234
|
+
})));
|
|
235
|
+
/** An overlay's layer, named apart from every request's: a request id is the application's to choose too. */
|
|
236
|
+
const OVERLAY = "overlay:";
|
|
237
|
+
const overlayLayer = (id) => `${OVERLAY}${id}`;
|
|
238
|
+
/** The ids `OverlayShown` shows now, each once, in the order they are drawn. */
|
|
239
|
+
const overlaysOf = (model) => [...new Set([...model.optimistic.layers, ...model.optimistic.overlays].flatMap(({ id }) => id.startsWith(OVERLAY) ? [id.slice(8)] : []))];
|
|
240
|
+
/** The key a grown window is kept under: its connection and the window first asked for. */
|
|
241
|
+
const windowGrowthKey = (identity, window) => `${identity}\u0000${window}`;
|
|
242
|
+
/**
|
|
243
|
+
* Whether only overlays show this entity: the server has told the store
|
|
244
|
+
* nothing of it and no request's layer writes it, so nothing will fetch what an
|
|
245
|
+
* overlay leaves out.
|
|
246
|
+
*/
|
|
247
|
+
const onlyOverlaid = (model, entity, id) => {
|
|
248
|
+
if (Object.hasOwn(model.entities, entityKey(entity, id))) return false;
|
|
249
|
+
const writers = model.optimistic.layers.filter((layer) => layer.patches.some((patch) => patch.entity === entity && patch.id === id));
|
|
250
|
+
return writers.length > 0 && writers.every((layer) => layer.id.startsWith("overlay:"));
|
|
251
|
+
};
|
|
252
|
+
const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
|
|
253
|
+
/**
|
|
254
|
+
* One mark per requested field, `entity\0id\0field`. A read is identified by
|
|
255
|
+
* what it asks for, not by a request id: two reads asking for one field share
|
|
256
|
+
* its mark, so the first answer clears it. Over-clearing shows `Initial` rather
|
|
257
|
+
* than a spinner, which is the safer way to be wrong. The refresh generations
|
|
258
|
+
* below key off the same marks, so "what a read asks for" means one thing.
|
|
259
|
+
*/
|
|
260
|
+
const fieldMark = (entity, id, field) => `${entityKey(entity, id)}\u0000${field}`;
|
|
261
|
+
const fieldMarks = function* (requests) {
|
|
262
|
+
for (const request of requests) for (const field of request.fields) yield fieldMark(request.entity, request.id, field);
|
|
263
|
+
};
|
|
264
|
+
const withLoading = (loading, requests) => {
|
|
265
|
+
const next = new Set(loading);
|
|
266
|
+
for (const mark of fieldMarks(requests)) next.add(mark);
|
|
267
|
+
return next.size === loading.size ? loading : next;
|
|
268
|
+
};
|
|
269
|
+
const withoutLoading = (loading, requests) => {
|
|
270
|
+
if (loading.size === 0) return loading;
|
|
271
|
+
const next = new Set(loading);
|
|
272
|
+
for (const mark of fieldMarks(requests)) next.delete(mark);
|
|
273
|
+
return next.size === loading.size ? loading : next;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* A connection's mark. The leading separator keeps it out of the field marks'
|
|
277
|
+
* space, whatever a connection identity happens to spell.
|
|
278
|
+
*/
|
|
279
|
+
const connectionPrefix = "\0connection\0";
|
|
280
|
+
const connectionMark = (identity) => `${connectionPrefix}${identity}`;
|
|
281
|
+
/** Whether a query for this connection is in flight. */
|
|
282
|
+
const isQueryLoading = (model, identity) => model.loading.size !== 0 && model.loading.has(connectionMark(identity));
|
|
283
|
+
const withoutQueryLoading = (loading, identity) => {
|
|
284
|
+
if (!loading.has(connectionMark(identity))) return loading;
|
|
285
|
+
const next = new Set(loading);
|
|
286
|
+
next.delete(connectionMark(identity));
|
|
287
|
+
return next;
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* The in-flight marks of connections the roots still name. A query the entry
|
|
291
|
+
* stopped waiting for — it was restarted or deactivated before an answer came —
|
|
292
|
+
* leaves its mark behind, and a connection released with that mark would read
|
|
293
|
+
* `Loading` forever the next time something asked for it before sending.
|
|
294
|
+
*/
|
|
295
|
+
const prunedLoading = (loading, roots, reached) => {
|
|
296
|
+
if (loading.size === 0) return loading;
|
|
297
|
+
const named = new Set(roots.connections.map((root) => connectionMark(root.identity)));
|
|
298
|
+
const kept = [...loading].filter((mark) => mark.startsWith(connectionPrefix) ? named.has(mark) : reached().has(markEntity(mark)));
|
|
299
|
+
return kept.length === loading.size ? loading : new Set(kept);
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* The windows grown on connections retention keeps; a dropped one starts over.
|
|
303
|
+
* The same record when every one is kept.
|
|
304
|
+
*/
|
|
305
|
+
const keptWindows = (grown, connections) => {
|
|
306
|
+
const kept = Object.entries(grown).filter(([key]) => Object.hasOwn(connections, key.slice(0, key.lastIndexOf("\0"))));
|
|
307
|
+
return kept.length === Object.keys(grown).length ? grown : Object.fromEntries(kept);
|
|
308
|
+
};
|
|
309
|
+
/** The placements awaiting a page of a list retention still keeps; a dropped list's page never comes. */
|
|
310
|
+
const keptPlacements = (placed, connections) => {
|
|
311
|
+
const kept = Object.entries(placed).filter(([key]) => Object.hasOwn(connections, key));
|
|
312
|
+
return kept.length === Object.keys(placed).length ? placed : Object.fromEntries(kept);
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Drops the refresh marks of entities and connections that were collected.
|
|
316
|
+
* Nothing observes a collected entity — the retention roots are the active
|
|
317
|
+
* Surfaces — so no read entry's generation can fall back when its mark goes.
|
|
318
|
+
* Without this the generations would be bounded by how many things the
|
|
319
|
+
* application has ever refreshed rather than by what it currently holds, which
|
|
320
|
+
* for a list refreshed on a timer is the same as not bounded at all.
|
|
321
|
+
*/
|
|
322
|
+
const prunedRefresh = (refresh, retained) => {
|
|
323
|
+
const live = (mark) => mark.startsWith(connectionPrefix) ? retained.connections[mark.slice(12)] !== void 0 : retained.entities[mark.slice(0, mark.indexOf("\0"))] !== void 0;
|
|
324
|
+
const keep = (marks) => {
|
|
325
|
+
const next = /* @__PURE__ */ new Map();
|
|
326
|
+
for (const [mark, generation] of marks) if (live(mark)) next.set(mark, generation);
|
|
327
|
+
return next.size === marks.size ? marks : next;
|
|
328
|
+
};
|
|
329
|
+
const requested = keep(refresh.requested);
|
|
330
|
+
const started = keep(refresh.started);
|
|
331
|
+
return requested === refresh.requested && started === refresh.started ? refresh : {
|
|
332
|
+
...refresh,
|
|
333
|
+
requested,
|
|
334
|
+
started
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* The generation a read is planned under: the highest any of the fields it
|
|
339
|
+
* reads or connections it runs was refreshed at. A read entry carries this as a
|
|
340
|
+
* dependency, so it restarts when what it observes is refreshed and not when
|
|
341
|
+
* anything else is.
|
|
342
|
+
*/
|
|
343
|
+
const refreshedAt = (refresh, requests, connections = []) => {
|
|
344
|
+
let at = refresh.floor;
|
|
345
|
+
for (const mark of fieldMarks(requests)) at = Math.max(at, refresh.requested.get(mark) ?? 0);
|
|
346
|
+
for (const identity of connections) at = Math.max(at, refresh.requested.get(connectionMark(identity)) ?? 0);
|
|
347
|
+
return at;
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Marks every field of `requests` and every connection in `connections`
|
|
351
|
+
* refreshed at the next generation.
|
|
352
|
+
*/
|
|
353
|
+
const withRefreshRequested = (refresh, requests, connections = []) => {
|
|
354
|
+
const generation = refresh.generation + 1;
|
|
355
|
+
const requested = new Map(refresh.requested);
|
|
356
|
+
for (const mark of fieldMarks(requests)) requested.set(mark, generation);
|
|
357
|
+
for (const identity of connections) requested.set(connectionMark(identity), generation);
|
|
358
|
+
return {
|
|
359
|
+
...refresh,
|
|
360
|
+
generation,
|
|
361
|
+
requested
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Marks connections as needing to be asked again, and restarts a read of them
|
|
366
|
+
* already in flight: a page requested before what invalidated it could land
|
|
367
|
+
* after it, saying what is no longer true. `Remote.refresh` marks the same way.
|
|
368
|
+
*/
|
|
369
|
+
const invalidateConnections = (model, connections) => {
|
|
370
|
+
if (connections.length === 0) return model;
|
|
371
|
+
const marked = connections.reduce((current, connection) => updateRemote(current, {
|
|
372
|
+
_tag: "ConnectionInvalidated",
|
|
373
|
+
connection
|
|
374
|
+
}), model);
|
|
375
|
+
return {
|
|
376
|
+
...marked,
|
|
377
|
+
refresh: withRefreshRequested(marked.refresh, [], connections)
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Records that a read of `requests` began under `generation`. A read that
|
|
382
|
+
* carries no generation began under none, and leaves the marks as they are.
|
|
383
|
+
*/
|
|
384
|
+
const withRefreshStarted = (refresh, requests, generation) => {
|
|
385
|
+
if (generation === void 0 || generation === 0) return refresh;
|
|
386
|
+
let started;
|
|
387
|
+
for (const mark of fieldMarks(requests)) {
|
|
388
|
+
if ((refresh.started.get(mark) ?? 0) >= generation) continue;
|
|
389
|
+
started ??= new Map(refresh.started);
|
|
390
|
+
started.set(mark, generation);
|
|
391
|
+
}
|
|
392
|
+
return started === void 0 ? refresh : {
|
|
393
|
+
...refresh,
|
|
394
|
+
started
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* Whether one field's read already began under the generation that field was
|
|
399
|
+
* last refreshed at. Such a read is in flight and would otherwise outlive the
|
|
400
|
+
* refresh, so the refresh takes a new generation to restart it even though the
|
|
401
|
+
* field is already stale.
|
|
402
|
+
*/
|
|
403
|
+
const refreshIsInFlight = (refresh, request, field) => {
|
|
404
|
+
const mark = fieldMark(request.entity, request.id, field);
|
|
405
|
+
return (refresh.started.get(mark) ?? 0) === (refresh.requested.get(mark) ?? 0);
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* Whether a read is fetching any of `fields` for this entity. Consulted only
|
|
409
|
+
* when the store lacks the value, which is what separates `Loading` from
|
|
410
|
+
* `Initial`.
|
|
411
|
+
*/
|
|
412
|
+
const isLoading = (model, entity, id, fields) => {
|
|
413
|
+
if (model.loading.size === 0) return false;
|
|
414
|
+
return fields.some((field) => model.loading.has(fieldMark(entity, id, field)));
|
|
415
|
+
};
|
|
416
|
+
const setConnectionStale = (connections, connection, stale) => {
|
|
417
|
+
const current = connections[connection];
|
|
418
|
+
if (current?.stale === stale) return connections;
|
|
419
|
+
return {
|
|
420
|
+
...connections,
|
|
421
|
+
[connection]: {
|
|
422
|
+
...current ?? emptyConnection,
|
|
423
|
+
stale
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* How many streams `live` and `streams` keep. Which streams the active
|
|
429
|
+
* Surfaces hold is not in the Model (retention roots merge every Surface's
|
|
430
|
+
* requirements, and a stream key is one entry's), so the ledger keeps those
|
|
431
|
+
* most recently heard from, as `failed` keeps the latest failures. A stream
|
|
432
|
+
* dropped while still open meets its next event as a gap, which restarts it
|
|
433
|
+
* and refetches what it covers.
|
|
434
|
+
*/
|
|
435
|
+
const STREAM_WINDOW = 256;
|
|
436
|
+
/** `record` with `stream` set as the most recent, the oldest dropped past the window. */
|
|
437
|
+
const heard = (record, stream, value) => {
|
|
438
|
+
const { [stream]: _previous, ...rest } = record;
|
|
439
|
+
const next = {
|
|
440
|
+
...rest,
|
|
441
|
+
[stream]: value
|
|
442
|
+
};
|
|
443
|
+
const keys = Object.keys(next);
|
|
444
|
+
for (const key of keys.slice(0, Math.max(0, keys.length - STREAM_WINDOW))) delete next[key];
|
|
445
|
+
return next;
|
|
446
|
+
};
|
|
447
|
+
const withLiveState = (live, stream, state) => live[stream] === state ? live : heard(live, stream, state);
|
|
448
|
+
const noHealth = {
|
|
449
|
+
restarts: 0,
|
|
450
|
+
failures: 0,
|
|
451
|
+
error: Option.none(),
|
|
452
|
+
refused: Option.none()
|
|
453
|
+
};
|
|
454
|
+
/**
|
|
455
|
+
* A stream broke: it is marked a gap and its restart count moves, which
|
|
456
|
+
* restarts its live entry. A stream already broken and not yet restarted
|
|
457
|
+
* changes nothing, so the events a dying stream still delivers restart it once.
|
|
458
|
+
*/
|
|
459
|
+
const broke = (model, stream, error) => {
|
|
460
|
+
if (model.gaps.has(stream)) return model;
|
|
461
|
+
const health = model.streams[stream] ?? noHealth;
|
|
462
|
+
const streams = heard(model.streams, stream, {
|
|
463
|
+
restarts: health.restarts + 1,
|
|
464
|
+
failures: health.failures + 1,
|
|
465
|
+
error,
|
|
466
|
+
refused: Option.none()
|
|
467
|
+
});
|
|
468
|
+
const gaps = new Set([...model.gaps, stream].filter((key) => Object.hasOwn(streams, key)));
|
|
469
|
+
return {
|
|
470
|
+
...model,
|
|
471
|
+
gaps,
|
|
472
|
+
streams
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
/** The tag of the server's refusal of a live stream, which `RemoteError` carries open. */
|
|
476
|
+
const refusedTag = "RemoteLiveRefused";
|
|
477
|
+
/**
|
|
478
|
+
* The server refused a stream for what it asks: it is marked refused and not a
|
|
479
|
+
* gap, so its live entry does not restart it, and the refusal is recorded as
|
|
480
|
+
* a diagnostic once.
|
|
481
|
+
*/
|
|
482
|
+
const refused = (model, stream, error) => {
|
|
483
|
+
const health = model.streams[stream] ?? noHealth;
|
|
484
|
+
if (Option.isSome(health.refused)) return model;
|
|
485
|
+
const streams = heard(model.streams, stream, {
|
|
486
|
+
...health,
|
|
487
|
+
error: Option.some(error),
|
|
488
|
+
refused: Option.some(error)
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
...clearGap(model, stream),
|
|
492
|
+
streams,
|
|
493
|
+
diagnostics: withDiagnostics(model.diagnostics, [liveRefused({
|
|
494
|
+
stream,
|
|
495
|
+
error
|
|
496
|
+
})])
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
/**
|
|
500
|
+
* An event applied on a stream that resubscribed: it works, so its next break
|
|
501
|
+
* backs off from the start. A broken stream's last events prove nothing.
|
|
502
|
+
*/
|
|
503
|
+
const healthy = (model, stream) => {
|
|
504
|
+
const health = model.streams[stream];
|
|
505
|
+
const settled = health !== void 0 && health.failures === 0 && Option.isNone(health.refused);
|
|
506
|
+
return health === void 0 || settled || model.gaps.has(stream) ? model : {
|
|
507
|
+
...model,
|
|
508
|
+
streams: heard(model.streams, stream, {
|
|
509
|
+
...health,
|
|
510
|
+
failures: 0,
|
|
511
|
+
error: Option.none(),
|
|
512
|
+
refused: Option.none()
|
|
513
|
+
})
|
|
514
|
+
};
|
|
515
|
+
};
|
|
516
|
+
const clearGap = (model, stream) => model.gaps.has(stream) ? {
|
|
517
|
+
...model,
|
|
518
|
+
gaps: new Set([...model.gaps].filter((entry) => entry !== stream))
|
|
519
|
+
} : model;
|
|
520
|
+
/**
|
|
521
|
+
* The pure reducer all four producers share. A live event that arrives ahead of
|
|
522
|
+
* its cursor is a gap: it is not applied, and the stream is recorded so its
|
|
523
|
+
* live entry restarts it rather than silently missing facts. When every field a Message
|
|
524
|
+
* touches keeps its identity, `model` itself is returned, so the application's
|
|
525
|
+
* root keeps its identity and Foldkit does not render. The common no-ops do (a
|
|
526
|
+
* duplicate live event, a repeated `ReadStarted`); a repeated failure, or a
|
|
527
|
+
* `preserve-existing` restore of entities already held, still makes an equal
|
|
528
|
+
* copy.
|
|
529
|
+
*/
|
|
530
|
+
const updateRemote = (model, message) => {
|
|
531
|
+
const next = reduceRemote(model, message);
|
|
532
|
+
return next === model || !sameFields(model, next) ? next : model;
|
|
533
|
+
};
|
|
534
|
+
/** Whether every field of `next` is the one `model` holds. */
|
|
535
|
+
const sameFields = (model, next) => {
|
|
536
|
+
const keys = Object.keys(next);
|
|
537
|
+
return keys.length === Object.keys(model).length && keys.every((key) => Object.is(model[key], next[key]));
|
|
538
|
+
};
|
|
539
|
+
/**
|
|
540
|
+
* A placement compared with the server's first page of the list it placed
|
|
541
|
+
* in, under `verifyPredictions`: the keys as far as both reach must agree, and
|
|
542
|
+
* no row placing took out may be one the page still lists. The record goes
|
|
543
|
+
* once compared; a disagreement is kept.
|
|
544
|
+
*/
|
|
545
|
+
const checkedPlacement = (model, connection, page, refreshes) => {
|
|
546
|
+
const placed = model.placed[connection];
|
|
547
|
+
if (placed === void 0 || !refreshes || page.start._tag !== "Terminal") return {
|
|
548
|
+
placed: model.placed,
|
|
549
|
+
diagnostics: model.diagnostics
|
|
550
|
+
};
|
|
551
|
+
const { [connection]: _, ...rest } = model.placed;
|
|
552
|
+
const reach = Math.min(placed.order.length, page.edges.length);
|
|
553
|
+
const served = page.edges.slice(0, reach).map((edge) => edge.key);
|
|
554
|
+
const asPlaced = placed.order.slice(0, reach);
|
|
555
|
+
const listed = new Set(page.edges.map((edge) => edge.key));
|
|
556
|
+
return {
|
|
557
|
+
placed: rest,
|
|
558
|
+
diagnostics: served.every((key, index) => key === asPlaced[index]) && !placed.dropped.some((key) => listed.has(key)) ? model.diagnostics : withDiagnostics(model.diagnostics, [predictionMismatch(PredictionPart.Placement({
|
|
559
|
+
connection,
|
|
560
|
+
placed: asPlaced,
|
|
561
|
+
served
|
|
562
|
+
}))])
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
/** The queue with `queued` in place of the entry of its operation, or after the rest. */
|
|
566
|
+
const placedIn = (queue, queued) => {
|
|
567
|
+
const operation = operationOf(queued);
|
|
568
|
+
return queue.some((each) => operationOf(each) === operation) ? queue.map((each) => operationOf(each) === operation ? queued : each) : [...queue, queued];
|
|
569
|
+
};
|
|
570
|
+
/** A durable write queued to wait its turn: new, or a failed one retried, its revision moved on. */
|
|
571
|
+
const queuedWrite = (outbox, operation, write, message) => {
|
|
572
|
+
const prior = outbox.queue.find((queued) => operationOf(queued) === operation);
|
|
573
|
+
const stored = {
|
|
574
|
+
operation,
|
|
575
|
+
mutation: write.mutation,
|
|
576
|
+
input: write.durable?.input,
|
|
577
|
+
...write.keys === void 0 ? {} : { keys: write.keys },
|
|
578
|
+
...write.now === void 0 ? {} : { now: write.now },
|
|
579
|
+
optimistic: message.optimistic ?? [],
|
|
580
|
+
...message.viewer === void 0 ? {} : { viewer: message.viewer },
|
|
581
|
+
shown: write.durable?.shown ?? [],
|
|
582
|
+
revision: prior === void 0 ? 0 : revisionOf(prior) + 1,
|
|
583
|
+
state: { _tag: "Waiting" }
|
|
584
|
+
};
|
|
585
|
+
return {
|
|
586
|
+
...outbox,
|
|
587
|
+
queue: placedIn(outbox.queue, Queued.Readable({
|
|
588
|
+
write: stored,
|
|
589
|
+
stored: false,
|
|
590
|
+
attempt: 0,
|
|
591
|
+
delayed: Option.none()
|
|
592
|
+
}))
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
/** The outbox with the readable write of `operation` changed by `change`; itself when it has none. */
|
|
596
|
+
const withQueued = (outbox, operation, change) => {
|
|
597
|
+
const at = outbox.queue.findIndex((queued) => operationOf(queued) === operation);
|
|
598
|
+
const queued = outbox.queue[at];
|
|
599
|
+
if (queued?._tag !== "Readable") return outbox;
|
|
600
|
+
return {
|
|
601
|
+
...outbox,
|
|
602
|
+
queue: outbox.queue.map((each, index) => index === at ? change(queued) : each)
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
/** The outbox with `operation` settled: out of the queue, and remembered as done. */
|
|
606
|
+
const withoutQueued = (outbox, operation) => outbox.queue.some((queued) => operationOf(queued) === operation) ? {
|
|
607
|
+
...outbox,
|
|
608
|
+
queue: outbox.queue.filter((queued) => operationOf(queued) !== operation),
|
|
609
|
+
done: withDone(outbox.done, [operation])
|
|
610
|
+
} : outbox;
|
|
611
|
+
const storedAs = (queued, stored) => queued.stored === stored ? queued : {
|
|
612
|
+
...queued,
|
|
613
|
+
stored
|
|
614
|
+
};
|
|
615
|
+
/** A stored entry as the queue holds it: in the store, as read. */
|
|
616
|
+
const queuedOf = (entry) => entry._tag === "Readable" ? Queued.Readable({
|
|
617
|
+
write: entry.write,
|
|
618
|
+
stored: true,
|
|
619
|
+
attempt: 0,
|
|
620
|
+
delayed: Option.none()
|
|
621
|
+
}) : Queued.Unreadable({
|
|
622
|
+
operation: entry.operation,
|
|
623
|
+
revision: entry.revision,
|
|
624
|
+
raw: entry.raw,
|
|
625
|
+
reason: entry.reason,
|
|
626
|
+
stored: true
|
|
627
|
+
});
|
|
628
|
+
const entryOperation = (entry) => entry._tag === "Readable" ? entry.write.operation : entry.operation;
|
|
629
|
+
const entryRevision = (entry) => entry._tag === "Readable" ? entry.write.revision : entry.revision;
|
|
630
|
+
/** Whether the queued write waits its turn: in the ledger in flight, its layer shown. */
|
|
631
|
+
const isWaiting = (queued) => queued._tag === "Readable" && queued.write.state._tag === "Waiting";
|
|
632
|
+
/** The ledger and layers without a queued write: nothing of it shown or listed. */
|
|
633
|
+
const releasedFrom = (model, queued) => {
|
|
634
|
+
const operation = operationOf(queued);
|
|
635
|
+
if (!model.mutations.pending.has(operation)) return {
|
|
636
|
+
...model,
|
|
637
|
+
mutations: forgetFailure(model.mutations, operation)
|
|
638
|
+
};
|
|
639
|
+
const pending = new Set(model.mutations.pending);
|
|
640
|
+
pending.delete(operation);
|
|
641
|
+
const writes = new Map(model.mutations.writes);
|
|
642
|
+
writes.delete(operation);
|
|
643
|
+
const order = new Map(model.mutations.order);
|
|
644
|
+
order.delete(operation);
|
|
645
|
+
return {
|
|
646
|
+
...model,
|
|
647
|
+
mutations: {
|
|
648
|
+
...model.mutations,
|
|
649
|
+
pending,
|
|
650
|
+
writes,
|
|
651
|
+
order
|
|
652
|
+
},
|
|
653
|
+
optimistic: settleFailure(model.optimistic, operation)
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
/**
|
|
657
|
+
* The ledger and layers with a stored entry adopted in place of what the
|
|
658
|
+
* page held of it (`prior`): a write waiting is begun, its layer shown; one
|
|
659
|
+
* failed is failed, its layer gone; one this version cannot read is not in
|
|
660
|
+
* the ledger at all.
|
|
661
|
+
*/
|
|
662
|
+
const adopted = (model, prior, entry) => {
|
|
663
|
+
const released = prior === void 0 ? model : releasedFrom(model, prior);
|
|
664
|
+
if (entry._tag === "Unreadable") return released;
|
|
665
|
+
const { write } = entry;
|
|
666
|
+
const begun = {
|
|
667
|
+
...released,
|
|
668
|
+
mutations: beginMutation(released.mutations, write.operation, {
|
|
669
|
+
mutation: write.mutation,
|
|
670
|
+
input: entry.input,
|
|
671
|
+
keys: write.keys,
|
|
672
|
+
now: Option.fromUndefinedOr(write.now),
|
|
673
|
+
optimistic: write.optimistic
|
|
674
|
+
})
|
|
675
|
+
};
|
|
676
|
+
return write.state._tag === "Waiting" ? {
|
|
677
|
+
...begun,
|
|
678
|
+
optimistic: beginOptimistic(begun.optimistic, write.operation, write.optimistic, Option.fromUndefinedOr(write.viewer))
|
|
679
|
+
} : {
|
|
680
|
+
...begun,
|
|
681
|
+
mutations: failMutation(begun.mutations, write.operation, write.state.error, write.operation)
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
/**
|
|
685
|
+
* Writes another tab settled, whose answers this page did not see: their
|
|
686
|
+
* layers go, every row they showed is asked for again and every list is
|
|
687
|
+
* refetched, since what the server made of them is not known here.
|
|
688
|
+
*/
|
|
689
|
+
const settledElsewhere = (model, settled) => {
|
|
690
|
+
const released = settled.reduce(releasedFrom, model);
|
|
691
|
+
const rows = /* @__PURE__ */ new Map();
|
|
692
|
+
for (const queued of settled) {
|
|
693
|
+
if (queued._tag !== "Readable" || queued.write.state._tag !== "Waiting") continue;
|
|
694
|
+
for (const operation of queued.write.optimistic) if ("entity" in operation) rows.set(entityKey(operation.entity, operation.id), operation);
|
|
695
|
+
}
|
|
696
|
+
if (!settled.some(isWaiting)) return released;
|
|
697
|
+
const requests = [...rows.entries()].flatMap(([key, row]) => Option.match(entry(released.entities, key), {
|
|
698
|
+
onNone: () => [],
|
|
699
|
+
onSome: (held) => held.present.size === 0 ? [] : [{
|
|
700
|
+
entity: row.entity,
|
|
701
|
+
id: row.id,
|
|
702
|
+
fields: [...held.present]
|
|
703
|
+
}]
|
|
704
|
+
}));
|
|
705
|
+
const asked = requests.length === 0 ? released : reduceRemote(released, {
|
|
706
|
+
_tag: "RefreshStarted",
|
|
707
|
+
requests
|
|
708
|
+
});
|
|
709
|
+
return invalidateConnections(asked, Object.keys(asked.connections));
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
712
|
+
* What the store holds, met by what the page holds: an entry the store has a
|
|
713
|
+
* newer revision of is adopted, one it has as the page does is marked
|
|
714
|
+
* stored, one it lacks is written again, and one it settled goes. A read
|
|
715
|
+
* under another key replaces the page's queue (its writes stay in their own
|
|
716
|
+
* store). The Model itself when nothing changed.
|
|
717
|
+
*/
|
|
718
|
+
const readOutbox = (model, message) => {
|
|
719
|
+
const switching = Option.isSome(model.outbox.key) && model.outbox.key.value !== message.key || Option.isSome(model.outbox.scope) && model.outbox.scope.value !== message.scope;
|
|
720
|
+
const start = switching ? {
|
|
721
|
+
...model.outbox.queue.reduce(releasedFrom, model),
|
|
722
|
+
outbox: {
|
|
723
|
+
...model.outbox,
|
|
724
|
+
queue: [],
|
|
725
|
+
done: []
|
|
726
|
+
}
|
|
727
|
+
} : model;
|
|
728
|
+
const before = start.outbox;
|
|
729
|
+
const done = withDone(before.done, message.done);
|
|
730
|
+
const gone = new Set(done);
|
|
731
|
+
const held = new Map(before.queue.map((queued) => [operationOf(queued), queued]));
|
|
732
|
+
const read = /* @__PURE__ */ new Set();
|
|
733
|
+
const queue = [];
|
|
734
|
+
const recorded = [];
|
|
735
|
+
let next = start;
|
|
736
|
+
for (const entry of message.entries) {
|
|
737
|
+
const operation = entryOperation(entry);
|
|
738
|
+
if (gone.has(operation) || read.has(operation)) continue;
|
|
739
|
+
read.add(operation);
|
|
740
|
+
const prior = held.get(operation);
|
|
741
|
+
const revision = entryRevision(entry);
|
|
742
|
+
if (prior !== void 0 && revisionOf(prior) > revision) queue.push(prior);
|
|
743
|
+
else if (prior !== void 0 && revisionOf(prior) === revision) queue.push(storedAs(prior, true));
|
|
744
|
+
else {
|
|
745
|
+
next = adopted(next, prior, entry);
|
|
746
|
+
if (entry._tag === "Unreadable" && prior === void 0) recorded.push(outboxUnreadable({
|
|
747
|
+
requestId: operation,
|
|
748
|
+
reason: entry.reason
|
|
749
|
+
}));
|
|
750
|
+
queue.push(queuedOf(entry));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const settled = [];
|
|
754
|
+
for (const queued of before.queue) {
|
|
755
|
+
const operation = operationOf(queued);
|
|
756
|
+
if (read.has(operation)) continue;
|
|
757
|
+
if (gone.has(operation)) settled.push(queued);
|
|
758
|
+
else queue.push(storedAs(queued, false));
|
|
759
|
+
}
|
|
760
|
+
next = settledElsewhere(next, settled);
|
|
761
|
+
const said = next.diagnostics.some((diagnostic) => diagnostic._tag === "OutboxUnsaved" && diagnostic.key === message.key);
|
|
762
|
+
if (message.unsaved !== void 0 && !said) recorded.push(outboxUnsaved({
|
|
763
|
+
key: message.key,
|
|
764
|
+
reason: message.unsaved
|
|
765
|
+
}));
|
|
766
|
+
const session = Option.orElse(before.session, () => Option.fromUndefinedOr(message.session));
|
|
767
|
+
if (!switching && next === start && recorded.length === 0 && done === before.done && Option.contains(before.key, message.key) && Option.contains(before.scope, message.scope) && Option.isSome(before.session) === Option.isSome(session) && queue.length === before.queue.length && queue.every((queued, index) => queued === before.queue[index])) return model;
|
|
768
|
+
return {
|
|
769
|
+
...next,
|
|
770
|
+
outbox: {
|
|
771
|
+
session,
|
|
772
|
+
key: Option.some(message.key),
|
|
773
|
+
scope: Option.some(message.scope),
|
|
774
|
+
queue,
|
|
775
|
+
done
|
|
776
|
+
},
|
|
777
|
+
diagnostics: withDiagnostics(next.diagnostics, recorded)
|
|
778
|
+
};
|
|
779
|
+
};
|
|
780
|
+
/**
|
|
781
|
+
* The Remote Model with a failed or unreadable durable write settled by the
|
|
782
|
+
* reader (`Data.dismiss`): out of the queue and the ledger, and done, so every
|
|
783
|
+
* tab drops it. A waiting one, or one not queued, is the Model itself.
|
|
784
|
+
*/
|
|
785
|
+
const dismissQueued = (model, requestId) => {
|
|
786
|
+
const queued = model.outbox.queue.find((each) => operationOf(each) === requestId);
|
|
787
|
+
if (queued === void 0 || isWaiting(queued)) return model;
|
|
788
|
+
return {
|
|
789
|
+
...model,
|
|
790
|
+
mutations: forgetFailure(model.mutations, requestId),
|
|
791
|
+
outbox: withoutQueued(model.outbox, requestId)
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
const reduceRemote = (model, message) => {
|
|
795
|
+
switch (message._tag) {
|
|
796
|
+
case "Preloaded": return message.messages.reduce(reduceRemote, model);
|
|
797
|
+
case "CompletedReportDiagnostics": return model;
|
|
798
|
+
case "ReadReceived": return {
|
|
799
|
+
...model,
|
|
800
|
+
entities: writeRead(model.entities, message.requests, message.result, message.now),
|
|
801
|
+
loading: withoutLoading(model.loading, message.requests),
|
|
802
|
+
failures: withoutFieldFailures(model.failures, [...fieldMarks(message.requests), ...patchedMarks(message.result.entities)])
|
|
803
|
+
};
|
|
804
|
+
case "ReadFailed":
|
|
805
|
+
if (message.stream !== void 0) return message.error._tag === refusedTag ? refused(model, message.stream, message.error) : broke(model, message.stream, Option.some(message.error));
|
|
806
|
+
return {
|
|
807
|
+
...model,
|
|
808
|
+
entities: setStale(model.entities, marksOf(message.requests), false),
|
|
809
|
+
loading: withoutLoading(model.loading, message.requests),
|
|
810
|
+
failures: withFieldFailures(model.failures, message.requests, message.error)
|
|
811
|
+
};
|
|
812
|
+
case "RetentionChanged": {
|
|
813
|
+
const recent = recentRoots(model.recent, message.roots);
|
|
814
|
+
const retained = gc(model, recent);
|
|
815
|
+
const active = new Set(message.roots.connections.map((root) => root.identity));
|
|
816
|
+
const connections = recent.connections.reduce((held, { identity }) => active.has(identity) || held[identity] === void 0 ? held : setConnectionStale(held, identity, true), retained.connections);
|
|
817
|
+
let reachedKeys;
|
|
818
|
+
const reached = () => reachedKeys ??= reachable(model.entities, message.roots, model.connections, model.optimistic, model.mutations.pending);
|
|
819
|
+
return {
|
|
820
|
+
...model,
|
|
821
|
+
...retained,
|
|
822
|
+
connections,
|
|
823
|
+
recent,
|
|
824
|
+
grown: keptWindows(model.grown, retained.connections),
|
|
825
|
+
placed: keptPlacements(model.placed, retained.connections),
|
|
826
|
+
refresh: prunedRefresh(model.refresh, retained),
|
|
827
|
+
failures: prunedFailures(model.failures, message.roots, reached),
|
|
828
|
+
loading: prunedLoading(model.loading, message.roots, reached)
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
case "Hydrated": {
|
|
832
|
+
const restored = Object.entries(message.connections ?? {}).map(([identity, segments]) => [identity, {
|
|
833
|
+
segments: segments.map((edges) => ({
|
|
834
|
+
edges: [...edges],
|
|
835
|
+
start: unknown,
|
|
836
|
+
end: unknown
|
|
837
|
+
})),
|
|
838
|
+
stale: true
|
|
839
|
+
}]);
|
|
840
|
+
const written = Object.entries(message.entities).flatMap(([key, entry]) => message.merge === "replace" || !(key in model.entities) ? [...entry.present].map((field) => `${key}\u0000${field}`) : []);
|
|
841
|
+
return {
|
|
842
|
+
...model,
|
|
843
|
+
entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
|
|
844
|
+
failures: withoutFieldFailures(model.failures, written),
|
|
845
|
+
connections: restored.length === 0 ? model.connections : message.merge === "replace" ? {
|
|
846
|
+
...model.connections,
|
|
847
|
+
...Object.fromEntries(restored)
|
|
848
|
+
} : {
|
|
849
|
+
...Object.fromEntries(restored),
|
|
850
|
+
...model.connections
|
|
851
|
+
},
|
|
852
|
+
restoredFrom: message.from === void 0 ? model.restoredFrom : Option.some(message.from)
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
case "RefreshStarted": {
|
|
856
|
+
const marks = marksOf(message.requests);
|
|
857
|
+
const entities = setUnavailable(setStale(askAgain(model.entities, marks), marks, true), marks, false);
|
|
858
|
+
const reached = reachedMarks(model.entities, message.requests);
|
|
859
|
+
const failures = withoutFieldFailures(model.failures, reached);
|
|
860
|
+
const retried = reached.filter((mark) => mark in model.failures.fields);
|
|
861
|
+
const loading = retried.length === 0 ? model.loading : /* @__PURE__ */ new Set([...model.loading, ...retried]);
|
|
862
|
+
return {
|
|
863
|
+
...model,
|
|
864
|
+
entities,
|
|
865
|
+
failures,
|
|
866
|
+
loading
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
case "QueryStarted": {
|
|
870
|
+
const loading = new Set(model.loading);
|
|
871
|
+
for (const identity of message.connections) loading.add(connectionMark(identity));
|
|
872
|
+
return loading.size === model.loading.size ? model : {
|
|
873
|
+
...model,
|
|
874
|
+
loading
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
case "ReadStarted": return {
|
|
878
|
+
...model,
|
|
879
|
+
loading: withLoading(model.loading, message.requests),
|
|
880
|
+
refresh: withRefreshStarted(model.refresh, message.requests, message.refresh)
|
|
881
|
+
};
|
|
882
|
+
case "MutationStarted": return {
|
|
883
|
+
...model,
|
|
884
|
+
outbox: message.write?.durable === void 0 ? model.outbox : queuedWrite(model.outbox, message.requestId, message.write, message),
|
|
885
|
+
mutations: beginMutation(model.mutations, message.requestId, message.write === void 0 ? void 0 : {
|
|
886
|
+
mutation: message.write.mutation,
|
|
887
|
+
input: message.write.input,
|
|
888
|
+
keys: message.write.keys,
|
|
889
|
+
now: Option.fromUndefinedOr(message.write.now),
|
|
890
|
+
optimistic: message.optimistic ?? []
|
|
891
|
+
}),
|
|
892
|
+
optimistic: beginOptimistic(model.optimistic, message.requestId, message.optimistic ?? [], Option.fromUndefinedOr(message.viewer))
|
|
893
|
+
};
|
|
894
|
+
case "MutationSucceeded": {
|
|
895
|
+
const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message);
|
|
896
|
+
const read = message.read === void 0 || model.mutations.applied.has(message.requestId) ? void 0 : unsettledRead(message.read, settledLater(model.mutations, message.requestId));
|
|
897
|
+
return {
|
|
898
|
+
...model,
|
|
899
|
+
entities: read === void 0 ? settled.store : writeRead(settled.store, read.requests, read.result, message.now),
|
|
900
|
+
mutations: settled.state,
|
|
901
|
+
outbox: withoutQueued(model.outbox, message.requestId),
|
|
902
|
+
optimistic: settled.optimistic,
|
|
903
|
+
failures: withoutFieldFailures(model.failures, [
|
|
904
|
+
...read === void 0 ? [] : [...fieldMarks(read.requests), ...patchedMarks(read.result.entities)],
|
|
905
|
+
...patchedMarks(message.entities),
|
|
906
|
+
...(message.deleted ?? []).flatMap((ref) => entityFailureMarks(model.failures, entityKey(ref.entity, ref.id)))
|
|
907
|
+
])
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
case "MutationQueued": return model;
|
|
911
|
+
case "OutboxDelayed": return {
|
|
912
|
+
...model,
|
|
913
|
+
outbox: withQueued(model.outbox, message.requestId, (queued) => Queued.Readable({
|
|
914
|
+
...queued,
|
|
915
|
+
attempt: message.attempt,
|
|
916
|
+
delayed: Option.some(message.error)
|
|
917
|
+
}))
|
|
918
|
+
};
|
|
919
|
+
case "OutboxRead": return readOutbox(model, message);
|
|
920
|
+
case "OverlayShown": return {
|
|
921
|
+
...model,
|
|
922
|
+
optimistic: beginOptimistic(settleFailure(model.optimistic, overlayLayer(message.id)), overlayLayer(message.id), message.optimistic)
|
|
923
|
+
};
|
|
924
|
+
case "WindowGrown": return {
|
|
925
|
+
...model,
|
|
926
|
+
grown: {
|
|
927
|
+
...model.grown,
|
|
928
|
+
[message.window]: message.size
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
case "OverlayLifted": {
|
|
932
|
+
const optimistic = settleFailure(model.optimistic, overlayLayer(message.id));
|
|
933
|
+
return optimistic.layers.length === model.optimistic.layers.length && optimistic.overlays.length === model.optimistic.overlays.length ? model : {
|
|
934
|
+
...model,
|
|
935
|
+
optimistic
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
case "MutationFailed": return {
|
|
939
|
+
...model,
|
|
940
|
+
outbox: !model.mutations.pending.has(message.requestId) ? model.outbox : withQueued(model.outbox, message.requestId, (queued) => Queued.Readable({
|
|
941
|
+
write: {
|
|
942
|
+
...queued.write,
|
|
943
|
+
revision: queued.write.revision + 1,
|
|
944
|
+
state: {
|
|
945
|
+
_tag: "Failed",
|
|
946
|
+
error: message.error
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
stored: false,
|
|
950
|
+
attempt: 0,
|
|
951
|
+
delayed: Option.none()
|
|
952
|
+
})),
|
|
953
|
+
mutations: failMutation(model.mutations, message.requestId, message.error, message.operation),
|
|
954
|
+
optimistic: settleFailure(model.optimistic, message.requestId)
|
|
955
|
+
};
|
|
956
|
+
case "LiveReceived": {
|
|
957
|
+
const state = model.live[message.stream] ?? emptyLiveState;
|
|
958
|
+
if (message.event._tag === "EntityPatched" || message.event._tag === "EntityDeleted") {
|
|
959
|
+
const event = message.event;
|
|
960
|
+
const applied = applyEntityEvent(state, model.entities, event, message.now);
|
|
961
|
+
if (applied.outcome === "gap") return broke(model, message.stream, Option.none());
|
|
962
|
+
const settled = applied.outcome !== "applied" ? [] : event._tag === "EntityPatched" ? [...patchedMarks([{
|
|
963
|
+
...event.ref,
|
|
964
|
+
values: event.values
|
|
965
|
+
}])] : entityFailureMarks(model.failures, entityKey(event.ref.entity, event.ref.id));
|
|
966
|
+
const next = {
|
|
967
|
+
...model,
|
|
968
|
+
entities: applied.store,
|
|
969
|
+
live: withLiveState(model.live, message.stream, applied.state),
|
|
970
|
+
failures: withoutFieldFailures(model.failures, settled)
|
|
971
|
+
};
|
|
972
|
+
return applied.outcome === "applied" ? healthy(next, message.stream) : next;
|
|
973
|
+
}
|
|
974
|
+
const applied = applyConnectionEvent(state, model.optimistic, message.event, message.policy);
|
|
975
|
+
if (applied.outcome === "gap") return broke(model, message.stream, Option.none());
|
|
976
|
+
const written = {
|
|
977
|
+
...model,
|
|
978
|
+
optimistic: applied.optimistic,
|
|
979
|
+
live: withLiveState(model.live, message.stream, applied.state)
|
|
980
|
+
};
|
|
981
|
+
const next = applied.outcome === "applied" ? healthy(written, message.stream) : written;
|
|
982
|
+
return applied.invalidated === void 0 ? next : invalidateConnections(next, [applied.invalidated]);
|
|
983
|
+
}
|
|
984
|
+
case "GapCleared": return clearGap(model, message.stream);
|
|
985
|
+
case "ConnectionMerged": {
|
|
986
|
+
const current = model.connections[message.connection] ?? emptyConnection;
|
|
987
|
+
const loading = withoutQueryLoading(model.loading, message.connection);
|
|
988
|
+
const replaces = message.refreshes === true && current.stale;
|
|
989
|
+
const merged = merge(replaces ? emptyConnection : current, message.page);
|
|
990
|
+
const payload = message.select === void 0 ? [] : message.entities ?? [];
|
|
991
|
+
const settled = message.select === void 0 ? [] : message.settled ?? [];
|
|
992
|
+
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) => ({
|
|
993
|
+
...message.select,
|
|
994
|
+
id: edge.ref.id
|
|
995
|
+
}));
|
|
996
|
+
const entities = requests.length === 0 ? model.entities : writeRead(model.entities, requests, {
|
|
997
|
+
entities: [...payload],
|
|
998
|
+
settled: [...settled]
|
|
999
|
+
}, message.now ?? 0);
|
|
1000
|
+
const failures = withoutFieldFailures(withoutConnectionFailure(model.failures, message.connection), [...fieldMarks(requests), ...patchedMarks(payload)]);
|
|
1001
|
+
return {
|
|
1002
|
+
...model,
|
|
1003
|
+
...checkedPlacement(model, message.connection, message.page, message.refreshes === true),
|
|
1004
|
+
entities,
|
|
1005
|
+
connections: {
|
|
1006
|
+
...model.connections,
|
|
1007
|
+
[message.connection]: message.refreshes === true ? {
|
|
1008
|
+
...merged,
|
|
1009
|
+
stale: false
|
|
1010
|
+
} : merged
|
|
1011
|
+
},
|
|
1012
|
+
optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending),
|
|
1013
|
+
failures,
|
|
1014
|
+
loading
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
case "ConnectionInvalidated": return {
|
|
1018
|
+
...model,
|
|
1019
|
+
connections: setConnectionStale(model.connections, message.connection, true),
|
|
1020
|
+
failures: withoutConnectionFailure(model.failures, message.connection),
|
|
1021
|
+
loading: message.connection in model.failures.connections ? /* @__PURE__ */ new Set([...model.loading, connectionMark(message.connection)]) : model.loading
|
|
1022
|
+
};
|
|
1023
|
+
case "QueryFailed": return {
|
|
1024
|
+
...model,
|
|
1025
|
+
failures: {
|
|
1026
|
+
...model.failures,
|
|
1027
|
+
connections: {
|
|
1028
|
+
...model.failures.connections,
|
|
1029
|
+
[message.connection]: message.error
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
loading: withoutQueryLoading(model.loading, message.connection)
|
|
1033
|
+
};
|
|
1034
|
+
case "ConnectionRefreshed": return {
|
|
1035
|
+
...model,
|
|
1036
|
+
connections: setConnectionStale(model.connections, message.connection, false)
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
/** The failures without one connection's, keeping identity when it had none. */
|
|
1041
|
+
const withoutConnectionFailure = (failures, connection) => {
|
|
1042
|
+
if (!(connection in failures.connections)) return failures;
|
|
1043
|
+
const { [connection]: _settled, ...connections } = failures.connections;
|
|
1044
|
+
return {
|
|
1045
|
+
...failures,
|
|
1046
|
+
connections
|
|
1047
|
+
};
|
|
1048
|
+
};
|
|
1049
|
+
/** The failures with every field a read asked for failed with `error`. */
|
|
1050
|
+
const withFieldFailures = (failures, requests, error) => {
|
|
1051
|
+
const fields = { ...failures.fields };
|
|
1052
|
+
for (const mark of fieldMarks(requests)) fields[mark] = error;
|
|
1053
|
+
return {
|
|
1054
|
+
...failures,
|
|
1055
|
+
fields
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
/**
|
|
1059
|
+
* The failures without the fields `settled` names, keeping identity when none
|
|
1060
|
+
* of them had failed, which is nearly always: this runs on every write.
|
|
1061
|
+
*/
|
|
1062
|
+
const withoutFieldFailures = (failures, settled) => {
|
|
1063
|
+
if (Object.keys(failures.fields).length === 0) return failures;
|
|
1064
|
+
const marks = [...settled].filter((mark) => mark in failures.fields);
|
|
1065
|
+
if (marks.length === 0) return failures;
|
|
1066
|
+
const fields = { ...failures.fields };
|
|
1067
|
+
for (const mark of marks) delete fields[mark];
|
|
1068
|
+
return {
|
|
1069
|
+
...failures,
|
|
1070
|
+
fields
|
|
1071
|
+
};
|
|
1072
|
+
};
|
|
1073
|
+
/** Every failed field of one entity: what deleting the entity settles. */
|
|
1074
|
+
const entityFailureMarks = (failures, key) => Object.keys(failures.fields).filter((mark) => mark.startsWith(`${key}\u0000`));
|
|
1075
|
+
/** The field marks of written values: each patch settles the fields it carries. */
|
|
1076
|
+
const patchedMarks = function* (patches) {
|
|
1077
|
+
for (const patch of patches) for (const field of Object.keys(patch.values)) yield fieldMark(patch.entity, patch.id, field);
|
|
1078
|
+
};
|
|
1079
|
+
/**
|
|
1080
|
+
* Every field a read's requests reach: their own, and through each relation
|
|
1081
|
+
* the store already holds, the fields they ask of its targets. A request with
|
|
1082
|
+
* a relation riding on it names no target id; the store does, and a failure
|
|
1083
|
+
* recorded on a target is reached only this way.
|
|
1084
|
+
*/
|
|
1085
|
+
const reachedMarks = (store, requests) => {
|
|
1086
|
+
const marks = [];
|
|
1087
|
+
const walked = /* @__PURE__ */ new Map();
|
|
1088
|
+
const walk = (entity, id, requirement) => {
|
|
1089
|
+
const key = entityKey(entity, id);
|
|
1090
|
+
const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
|
|
1091
|
+
if (seen.has(requirement)) return;
|
|
1092
|
+
seen.add(requirement);
|
|
1093
|
+
walked.set(key, seen);
|
|
1094
|
+
for (const field of requirement.fields) {
|
|
1095
|
+
marks.push(fieldMark(entity, id, field));
|
|
1096
|
+
const nested = requirement.relations?.[field];
|
|
1097
|
+
if (nested === void 0) continue;
|
|
1098
|
+
const value = readField(store, key, field);
|
|
1099
|
+
if (value._tag === "None") continue;
|
|
1100
|
+
for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
for (const request of requests) walk(request.entity, request.id, request);
|
|
1104
|
+
return marks;
|
|
1105
|
+
};
|
|
1106
|
+
/** The entity key a field mark belongs to. */
|
|
1107
|
+
const markEntity = (mark) => mark.slice(0, mark.indexOf("\0"));
|
|
1108
|
+
/**
|
|
1109
|
+
* The failures of what the roots still reach. A released connection's or
|
|
1110
|
+
* entity's failure goes with it, so asking for it again later asks the server
|
|
1111
|
+
* again rather than repeating an error that may no longer hold.
|
|
1112
|
+
*
|
|
1113
|
+
* Reached is retention's own walk, taken over the Model before collection. It
|
|
1114
|
+
* counts a relation's target and a list's row that the store does not hold,
|
|
1115
|
+
* which is exactly a value whose first read failed: it is still on screen,
|
|
1116
|
+
* behind the parent that refers to it.
|
|
1117
|
+
*/
|
|
1118
|
+
const prunedFailures = (failures, roots, reached) => {
|
|
1119
|
+
const named = new Set(roots.connections.map((root) => root.identity));
|
|
1120
|
+
const connections = Object.entries(failures.connections).filter(([identity]) => named.has(identity));
|
|
1121
|
+
const fields = Object.keys(failures.fields).length === 0 ? [] : Object.entries(failures.fields).filter(([mark]) => reached().has(markEntity(mark)));
|
|
1122
|
+
return connections.length === Object.keys(failures.connections).length && fields.length === Object.keys(failures.fields).length ? failures : {
|
|
1123
|
+
connections: Object.fromEntries(connections),
|
|
1124
|
+
fields: Object.fromEntries(fields)
|
|
1125
|
+
};
|
|
1126
|
+
};
|
|
1127
|
+
/** Whether this field's last read failed and nothing has settled it since. */
|
|
1128
|
+
const isFieldFailed = (model, entity, id, field) => fieldMark(entity, id, field) in model.failures.fields;
|
|
1129
|
+
/**
|
|
1130
|
+
* Whether a read is fetching any field `relation` reads of an entity, its own
|
|
1131
|
+
* or, through each relation the store holds, its targets'. A value whose owner
|
|
1132
|
+
* is being fetched is loading, not `Initial`: nothing is missing a fetch.
|
|
1133
|
+
*/
|
|
1134
|
+
const isLoadingThrough = (model, entity, id, relation) => {
|
|
1135
|
+
if (model.loading.size === 0) return false;
|
|
1136
|
+
const walked = /* @__PURE__ */ new Map();
|
|
1137
|
+
const walk = (target, targetId, requirement) => {
|
|
1138
|
+
const key = entityKey(target, targetId);
|
|
1139
|
+
const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
|
|
1140
|
+
if (seen.has(requirement)) return false;
|
|
1141
|
+
seen.add(requirement);
|
|
1142
|
+
walked.set(key, seen);
|
|
1143
|
+
for (const field of requirement.fields) {
|
|
1144
|
+
if (model.loading.has(fieldMark(target, targetId, field))) return true;
|
|
1145
|
+
const nested = requirement.relations?.[field];
|
|
1146
|
+
if (nested === void 0) continue;
|
|
1147
|
+
const value = readField(model.entities, key, field);
|
|
1148
|
+
if (value._tag === "None") continue;
|
|
1149
|
+
for (const ref of targetsOf(value.value, nested)) if (walk(ref.entity, ref.id, nested)) return true;
|
|
1150
|
+
}
|
|
1151
|
+
return false;
|
|
1152
|
+
};
|
|
1153
|
+
return walk(entity, id, relation);
|
|
1154
|
+
};
|
|
1155
|
+
/**
|
|
1156
|
+
* The first failure among the fields `relation` reads of an entity, following
|
|
1157
|
+
* each relation the store holds into its targets, as `assemble` does. A read
|
|
1158
|
+
* that consults it shows `Failed` for a related entity's failure too, rather
|
|
1159
|
+
* than waiting on a field nothing will fetch.
|
|
1160
|
+
*/
|
|
1161
|
+
const failureOf = (model, entity, id, relation) => {
|
|
1162
|
+
const failed = model.failures.fields;
|
|
1163
|
+
if (Object.keys(failed).length === 0) return void 0;
|
|
1164
|
+
const walked = /* @__PURE__ */ new Map();
|
|
1165
|
+
const walk = (target, targetId, requirement) => {
|
|
1166
|
+
const key = entityKey(target, targetId);
|
|
1167
|
+
const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
|
|
1168
|
+
if (seen.has(requirement)) return void 0;
|
|
1169
|
+
seen.add(requirement);
|
|
1170
|
+
walked.set(key, seen);
|
|
1171
|
+
for (const field of requirement.fields) {
|
|
1172
|
+
const error = failed[fieldMark(target, targetId, field)];
|
|
1173
|
+
if (error !== void 0) return error;
|
|
1174
|
+
const nested = requirement.relations?.[field];
|
|
1175
|
+
if (nested === void 0) continue;
|
|
1176
|
+
const value = readField(model.entities, key, field);
|
|
1177
|
+
if (value._tag === "None") continue;
|
|
1178
|
+
for (const ref of targetsOf(value.value, nested)) {
|
|
1179
|
+
const found = walk(ref.entity, ref.id, nested);
|
|
1180
|
+
if (found !== void 0) return found;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
return walk(entity, id, relation);
|
|
1185
|
+
};
|
|
1186
|
+
/** Appends (after) or prepends (before) an incoming page onto the stored one. */
|
|
1187
|
+
const mergeWireRefPages = (current, next, direction) => {
|
|
1188
|
+
const refs = direction === "after" ? [.../* @__PURE__ */ new Set([...current.refs, ...next.refs])] : [.../* @__PURE__ */ new Set([...next.refs, ...current.refs])];
|
|
1189
|
+
return direction === "after" ? {
|
|
1190
|
+
refs,
|
|
1191
|
+
hasNext: next.hasNext,
|
|
1192
|
+
hasPrevious: current.hasPrevious
|
|
1193
|
+
} : {
|
|
1194
|
+
refs,
|
|
1195
|
+
hasNext: current.hasNext,
|
|
1196
|
+
hasPrevious: next.hasPrevious
|
|
1197
|
+
};
|
|
1198
|
+
};
|
|
1199
|
+
/**
|
|
1200
|
+
* A mutation answer's read without what a later request settled (#288): a
|
|
1201
|
+
* field it wrote, a row it deleted, and the absence of a row it wrote, which
|
|
1202
|
+
* the read would otherwise tombstone.
|
|
1203
|
+
*/
|
|
1204
|
+
const unsettledRead = (read, { newer, rewritten }) => {
|
|
1205
|
+
const entities = read.result.entities.flatMap((entity) => {
|
|
1206
|
+
if (newer(entityKey(entity.entity, entity.id))) return [];
|
|
1207
|
+
const values = Object.fromEntries(Object.entries(entity.values).filter(([field]) => !newer(fieldMark(entity.entity, entity.id, field))));
|
|
1208
|
+
return Object.keys(values).length === 0 ? [] : [{
|
|
1209
|
+
...entity,
|
|
1210
|
+
values
|
|
1211
|
+
}];
|
|
1212
|
+
});
|
|
1213
|
+
const settled = read.result.settled.flatMap((entry) => {
|
|
1214
|
+
if (newer(entityKey(entry.entity, entry.id))) return [];
|
|
1215
|
+
const fields = entry.fields.filter((field) => !newer(fieldMark(entry.entity, entry.id, field)));
|
|
1216
|
+
return fields.length === 0 ? [] : [{
|
|
1217
|
+
...entry,
|
|
1218
|
+
fields
|
|
1219
|
+
}];
|
|
1220
|
+
});
|
|
1221
|
+
const answered = new Set([...entities, ...settled].map((entry) => entityKey(entry.entity, entry.id)));
|
|
1222
|
+
return {
|
|
1223
|
+
requests: read.requests.filter((request) => {
|
|
1224
|
+
const key = entityKey(request.entity, request.id);
|
|
1225
|
+
return answered.has(key) || !rewritten(key);
|
|
1226
|
+
}),
|
|
1227
|
+
result: {
|
|
1228
|
+
entities,
|
|
1229
|
+
settled
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
};
|
|
1233
|
+
/**
|
|
1234
|
+
* Writes a read result into the store, recording each field's applied window so
|
|
1235
|
+
* a later request with a different window refetches. A relation page requested
|
|
1236
|
+
* with a cursor is merged onto the page already stored, so "load more"
|
|
1237
|
+
* accumulates rather than replaces. Every read path should use this rather than
|
|
1238
|
+
* reducing `writeEntity` by hand.
|
|
1239
|
+
*/
|
|
1240
|
+
const writeRead = (store, requests, result, now = 0) => {
|
|
1241
|
+
const returned = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (const entity of result.entities) {
|
|
1243
|
+
const key = entityKey(entity.entity, entity.id);
|
|
1244
|
+
returned.set(key, {
|
|
1245
|
+
...returned.get(key),
|
|
1246
|
+
...entity.values
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
const byEntity = /* @__PURE__ */ new Map();
|
|
1250
|
+
const record = (key, windows, relations) => {
|
|
1251
|
+
let entry = byEntity.get(key);
|
|
1252
|
+
if (entry === void 0) {
|
|
1253
|
+
entry = {
|
|
1254
|
+
windows: {},
|
|
1255
|
+
merge: /* @__PURE__ */ new Map()
|
|
1256
|
+
};
|
|
1257
|
+
byEntity.set(key, entry);
|
|
1258
|
+
}
|
|
1259
|
+
for (const [field, window] of Object.entries(windows ?? {})) {
|
|
1260
|
+
put(entry.windows, field, windowKey(window));
|
|
1261
|
+
const direction = window.after !== void 0 ? "after" : window.before !== void 0 ? "before" : void 0;
|
|
1262
|
+
if (direction !== void 0) entry.merge.set(field, direction);
|
|
1263
|
+
}
|
|
1264
|
+
const values = returned.get(key);
|
|
1265
|
+
if (values === void 0) return;
|
|
1266
|
+
for (const [field, relation] of Object.entries(relations ?? {})) for (const ref of targetsOf(values[field], relation)) record(entityKey(ref.entity, ref.id), relation.windows, relation.relations);
|
|
1267
|
+
};
|
|
1268
|
+
for (const request of requests) record(entityKey(request.entity, request.id), request.windows, request.relations);
|
|
1269
|
+
const writes = [];
|
|
1270
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1271
|
+
for (const entity of result.entities) {
|
|
1272
|
+
const key = entityKey(entity.entity, entity.id);
|
|
1273
|
+
const entry = byEntity.get(key);
|
|
1274
|
+
let values = entity.values;
|
|
1275
|
+
if (entry !== void 0 && entry.merge.size > 0) {
|
|
1276
|
+
const previous = pending.get(key) ?? store[key]?.values;
|
|
1277
|
+
if (previous !== void 0) {
|
|
1278
|
+
const merged = { ...values };
|
|
1279
|
+
for (const [field, direction] of entry.merge) {
|
|
1280
|
+
const incoming = values[field];
|
|
1281
|
+
const existing = previous[field];
|
|
1282
|
+
if (isRefPage(incoming) && isRefPage(existing)) merged[field] = mergeWireRefPages(existing, incoming, direction);
|
|
1283
|
+
}
|
|
1284
|
+
values = merged;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
pending.set(key, {
|
|
1288
|
+
...pending.get(key),
|
|
1289
|
+
...values
|
|
1290
|
+
});
|
|
1291
|
+
writes.push({
|
|
1292
|
+
key,
|
|
1293
|
+
values,
|
|
1294
|
+
windows: entry?.windows
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
const settled = new Set(result.settled.map((entry) => entityKey(entry.entity, entry.id)));
|
|
1298
|
+
let answered = store;
|
|
1299
|
+
for (const request of requests) {
|
|
1300
|
+
const key = entityKey(request.entity, request.id);
|
|
1301
|
+
if (!returned.has(key) && !settled.has(key)) answered = tombstone(answered, key);
|
|
1302
|
+
else if (settled.has(key) && isTombstone(answered, key)) answered = remove(answered, key);
|
|
1303
|
+
}
|
|
1304
|
+
return setUnavailable(writeEntities(answered, writes, now), result.settled.map((entry) => [entityKey(entry.entity, entry.id), entry.fields]), true);
|
|
1305
|
+
};
|
|
1306
|
+
export { dismissQueued, emptyRefresh, failureOf, fieldMark, forgetRemote, initialRemoteModel, invalidateConnections, isFieldFailed, isLoading, isLoadingThrough, isQueryLoading, isRemoteMessage, noFailures, onlyOverlaid, overlaysOf, refreshIsInFlight, refreshedAt, remoteMessageCases, remoteMessageSchema, remoteModelSchema, retentionRootsSchema, updateRemote, windowGrowthKey, withRefreshRequested, writeRead };
|
|
1307
|
+
|
|
1308
|
+
//# sourceMappingURL=model.js.map
|