@contember/bindx 0.1.52 → 0.1.53
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/dist/core/ActionDispatcher.d.ts.map +1 -1
- package/dist/core/EntityLoader.d.ts +7 -0
- package/dist/core/EntityLoader.d.ts.map +1 -1
- package/dist/core/actions.d.ts +1 -2
- package/dist/core/actions.d.ts.map +1 -1
- package/dist/dataview/types.d.ts +6 -6
- package/dist/dataview/types.d.ts.map +1 -1
- package/dist/handles/EntityHandle.d.ts.map +1 -1
- package/dist/handles/HasManyListHandle.d.ts +8 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -1
- package/dist/handles/HasOneHandle.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/store/HasManyStore.d.ts +94 -78
- package/dist/store/HasManyStore.d.ts.map +1 -1
- package/dist/store/RelationStore.d.ts +21 -12
- package/dist/store/RelationStore.d.ts.map +1 -1
- package/dist/store/ResponseOccurrenceIndex.d.ts +65 -0
- package/dist/store/ResponseOccurrenceIndex.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +49 -14
- package/dist/store/SnapshotStore.d.ts.map +1 -1
- package/dist/store/hasManyState.d.ts +164 -0
- package/dist/store/hasManyState.d.ts.map +1 -0
- package/dist/store/relationKey.d.ts +11 -0
- package/dist/store/relationKey.d.ts.map +1 -1
- package/dist/undo/UndoJournal.d.ts.map +1 -1
- package/dist/undo/rekeyJournalEntry.d.ts +4 -4
- package/dist/undo/rekeyJournalEntry.d.ts.map +1 -1
- package/dist/utils/aliasGenerator.d.ts +1 -1
- package/dist/utils/aliasGenerator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/core/ActionDispatcher.ts +0 -1
- package/src/core/EntityLoader.ts +7 -0
- package/src/core/actions.ts +1 -3
- package/src/dataview/types.ts +6 -6
- package/src/handles/EntityHandle.ts +11 -0
- package/src/handles/HasManyListHandle.ts +15 -17
- package/src/handles/HasOneHandle.ts +4 -2
- package/src/index.ts +6 -0
- package/src/store/HasManyStore.ts +312 -488
- package/src/store/RelationStore.ts +60 -24
- package/src/store/ResponseOccurrenceIndex.ts +224 -0
- package/src/store/SnapshotStore.ts +144 -73
- package/src/store/hasManyState.ts +408 -0
- package/src/store/relationKey.ts +17 -0
- package/src/undo/UndoJournal.ts +3 -1
- package/src/undo/UndoManager.ts +1 -1
- package/src/undo/rekeyJournalEntry.ts +31 -43
- package/src/utils/aliasGenerator.ts +1 -1
|
@@ -1,115 +1,45 @@
|
|
|
1
|
-
import { parentKeyFromOwnerPrefix, parentKeyFromRelationKey } from './relationKey.js'
|
|
1
|
+
import { entityTypeFromRelationKey, fieldFromRelationKey, parentKeyFromOwnerPrefix, parentKeyFromRelationKey } from './relationKey.js'
|
|
2
2
|
import { PlannedDeleteIndex } from './PlannedDeleteIndex.js'
|
|
3
3
|
import { RelationEdgeIndex } from './RelationEdgeIndex.js'
|
|
4
4
|
import { RelationOwnerIndex } from './RelationOwnerIndex.js'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Kind of a planned has-many addition:
|
|
31
|
-
* - 'created': a newly created entity (via add())
|
|
32
|
-
* - 'connected': an existing persisted entity being connected (via connect())
|
|
33
|
-
*/
|
|
34
|
-
export type HasManyAdditionKind = 'created' | 'connected'
|
|
35
|
-
|
|
36
|
-
export interface SentHasManyAddition {
|
|
37
|
-
itemId: string
|
|
38
|
-
kind: HasManyAdditionKind
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface SentHasManyRemoval {
|
|
42
|
-
itemId: string
|
|
43
|
-
type: HasManyRemovalType
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface SentHasManyDelta {
|
|
47
|
-
additions: readonly SentHasManyAddition[]
|
|
48
|
-
removals: readonly SentHasManyRemoval[]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Has-many list state stored in SnapshotStore
|
|
53
|
-
*/
|
|
54
|
-
export interface StoredHasManyState {
|
|
55
|
-
/** IDs of items from server */
|
|
56
|
-
serverIds: Set<string>
|
|
57
|
-
/** Explicit ordered list of item IDs, null means use default order (serverIds + plannedAdditions) */
|
|
58
|
-
orderedIds: string[] | null
|
|
59
|
-
/** Planned removals (disconnect or delete) keyed by entity ID */
|
|
60
|
-
plannedRemovals: Map<string, HasManyRemovalType>
|
|
61
|
-
/**
|
|
62
|
-
* Planned additions (IDs to add to the list) keyed by entity ID, with the
|
|
63
|
-
* value distinguishing newly CREATED entities (add()) from existing PERSISTED
|
|
64
|
-
* entities being CONNECTED (connect()). The keys are exactly the connections;
|
|
65
|
-
* the keys whose value is 'created' are exactly the created entities, so the
|
|
66
|
-
* "created ⊆ connections" invariant is structural.
|
|
67
|
-
*/
|
|
68
|
-
plannedAdditions: Map<string, HasManyAdditionKind>
|
|
69
|
-
version: number
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function cloneHasManyState(state: StoredHasManyState): StoredHasManyState {
|
|
73
|
-
return {
|
|
74
|
-
serverIds: new Set(state.serverIds),
|
|
75
|
-
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
76
|
-
plannedRemovals: new Map(state.plannedRemovals),
|
|
77
|
-
plannedAdditions: new Map(state.plannedAdditions),
|
|
78
|
-
version: state.version,
|
|
79
|
-
}
|
|
80
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
additionFoldTargets,
|
|
7
|
+
additionRendersIn,
|
|
8
|
+
arraysEqual,
|
|
9
|
+
cloneHasManyState,
|
|
10
|
+
computeViewOrderedIds,
|
|
11
|
+
createHasManyView,
|
|
12
|
+
emptyHasManyState,
|
|
13
|
+
liveHasManyChildIds,
|
|
14
|
+
plannedDeleteChildIds,
|
|
15
|
+
reconcileHasManyState,
|
|
16
|
+
setsEqual,
|
|
17
|
+
toRelationProjection,
|
|
18
|
+
toViewProjection,
|
|
19
|
+
viewOrderedIds,
|
|
20
|
+
type HasManyRelationProjection,
|
|
21
|
+
type HasManyRemovalType,
|
|
22
|
+
type HasManyViewMembership,
|
|
23
|
+
type HasManyViewProjection,
|
|
24
|
+
type PlannedHasManyAddition,
|
|
25
|
+
type ReconciliationResult,
|
|
26
|
+
type SentHasManyDelta,
|
|
27
|
+
type StoredHasManyState,
|
|
28
|
+
} from './hasManyState.js'
|
|
81
29
|
|
|
82
30
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (const id of state.serverIds) {
|
|
90
|
-
if (!state.plannedRemovals.has(id)) {
|
|
91
|
-
result.push(id)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
for (const id of state.plannedAdditions.keys()) {
|
|
96
|
-
if (!result.includes(id)) {
|
|
97
|
-
result.push(id)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return result
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Owns has-many list state ("parentType:parentId:fieldName" → {@link StoredHasManyState}).
|
|
31
|
+
* Owns has-many relation state ("parentType:parentId:fieldName" → {@link StoredHasManyState}).
|
|
32
|
+
*
|
|
33
|
+
* The key's last segment is always the SCHEMA FIELD NAME; an args-view's alias
|
|
34
|
+
* addresses a view INSIDE the state, never a key of its own. See the module doc of
|
|
35
|
+
* `hasManyState.ts` for why reads are per view and writes are not.
|
|
106
36
|
*
|
|
107
37
|
* Has its own monotonic {@link mutationVersion} bumped on every actual write
|
|
108
38
|
* (funnelled through {@link writeHasMany} plus the delete/clear paths); the
|
|
109
39
|
* facade sums this with the has-one counter for {@link ReachabilityAnalyzer}.
|
|
110
40
|
*/
|
|
111
41
|
export class HasManyStore {
|
|
112
|
-
/** Has-many
|
|
42
|
+
/** Has-many relation states keyed by "parentType:parentId:fieldName" */
|
|
113
43
|
private readonly hasManyStates = new Map<string, StoredHasManyState>()
|
|
114
44
|
private readonly owners = new RelationOwnerIndex()
|
|
115
45
|
|
|
@@ -191,178 +121,154 @@ export class HasManyStore {
|
|
|
191
121
|
}
|
|
192
122
|
|
|
193
123
|
/**
|
|
194
|
-
*
|
|
124
|
+
* Membership declared for an args-view, keyed by "entityType:fieldName:alias" — one
|
|
125
|
+
* entry per distinct selection in the application, so it does not grow with the data.
|
|
126
|
+
* The alias alone would not do: two fields, or two entities' fields of the same name,
|
|
127
|
+
* may carry the same explicit `as`.
|
|
128
|
+
*
|
|
129
|
+
* Only the caller that selected the relation knows its params, and the alias is a
|
|
130
|
+
* hash it cannot reconstruct them from. {@link EntityHandle} declares them before
|
|
131
|
+
* it hands out a handle, which is strictly before any path can materialize the view.
|
|
195
132
|
*/
|
|
196
|
-
|
|
197
|
-
const existing = this.hasManyStates.get(key)
|
|
198
|
-
if (!existing) {
|
|
199
|
-
this.writeHasMany(key, {
|
|
200
|
-
serverIds: new Set(serverIds ?? []),
|
|
201
|
-
orderedIds: null,
|
|
202
|
-
plannedRemovals: new Map(),
|
|
203
|
-
plannedAdditions: new Map(),
|
|
204
|
-
version: 0,
|
|
205
|
-
})
|
|
206
|
-
} else if (serverIds !== undefined) {
|
|
207
|
-
const newServerIds = new Set(serverIds)
|
|
208
|
-
if (!setsEqual(existing.serverIds, newServerIds)) {
|
|
209
|
-
this.writeHasMany(key, {
|
|
210
|
-
...existing,
|
|
211
|
-
serverIds: newServerIds,
|
|
212
|
-
orderedIds: null,
|
|
213
|
-
version: existing.version + 1,
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
}
|
|
133
|
+
private readonly declaredMembership = new Map<string, HasManyViewMembership>()
|
|
217
134
|
|
|
218
|
-
|
|
135
|
+
declareViewMembership(
|
|
136
|
+
entityType: string,
|
|
137
|
+
fieldName: string,
|
|
138
|
+
alias: string,
|
|
139
|
+
membership: HasManyViewMembership,
|
|
140
|
+
): void {
|
|
141
|
+
this.declaredMembership.set(`${entityType}:${fieldName}:${alias}`, membership)
|
|
219
142
|
}
|
|
220
143
|
|
|
221
144
|
/**
|
|
222
|
-
*
|
|
145
|
+
* A view addressed by the field name itself is the unparameterized selection, so
|
|
146
|
+
* its args cannot exclude any member of the relation. Anything else needs a
|
|
147
|
+
* declaration; without one the view is assumed filtered, which is the safe
|
|
148
|
+
* direction — the client then never claims membership it cannot prove.
|
|
223
149
|
*/
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
150
|
+
private viewMembership(key: string, alias: string): HasManyViewMembership {
|
|
151
|
+
const fieldName = fieldFromRelationKey(key)
|
|
152
|
+
if (alias === fieldName) return 'total'
|
|
153
|
+
return this.declaredMembership.get(`${entityTypeFromRelationKey(key)}:${fieldName}:${alias}`) ?? 'partial'
|
|
227
154
|
}
|
|
228
155
|
|
|
229
156
|
/**
|
|
230
|
-
*
|
|
157
|
+
* Starts an edit: a deep copy of the current state (or a fresh one), with the
|
|
158
|
+
* addressed view materialized. Every mutator builds on this, so no path can hand
|
|
159
|
+
* {@link writeHasMany} a state sharing mutable structure with the live one — which
|
|
160
|
+
* SnapshotStore's dirty-version memo and the undo journal's images both rely on.
|
|
231
161
|
*/
|
|
232
|
-
|
|
162
|
+
private editableState(key: string, alias?: string): StoredHasManyState {
|
|
233
163
|
const existing = this.hasManyStates.get(key)
|
|
234
|
-
|
|
235
|
-
if (!
|
|
236
|
-
this.
|
|
237
|
-
serverIds: new Set(serverIds),
|
|
238
|
-
orderedIds: null,
|
|
239
|
-
plannedRemovals: new Map(),
|
|
240
|
-
plannedAdditions: new Map(),
|
|
241
|
-
version: 0,
|
|
242
|
-
})
|
|
243
|
-
} else {
|
|
244
|
-
this.writeHasMany(key, {
|
|
245
|
-
...existing,
|
|
246
|
-
serverIds: new Set(serverIds),
|
|
247
|
-
orderedIds: null,
|
|
248
|
-
version: existing.version + 1,
|
|
249
|
-
})
|
|
164
|
+
const state = existing ? cloneHasManyState(existing) : emptyHasManyState()
|
|
165
|
+
if (alias !== undefined && !state.views.has(alias)) {
|
|
166
|
+
state.views.set(alias, createHasManyView(this.viewMembership(key, alias)))
|
|
250
167
|
}
|
|
168
|
+
return state
|
|
251
169
|
}
|
|
252
170
|
|
|
253
|
-
|
|
254
|
-
* Plans a removal for a has-many item.
|
|
255
|
-
*/
|
|
256
|
-
planHasManyRemoval(key: string, itemId: string, type: HasManyRemovalType): void {
|
|
171
|
+
private commitEdit(key: string, state: StoredHasManyState): void {
|
|
257
172
|
const existing = this.hasManyStates.get(key)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
this.writeHasMany(key, {
|
|
261
|
-
serverIds: new Set(),
|
|
262
|
-
orderedIds: null,
|
|
263
|
-
plannedRemovals: new Map([[itemId, type]]),
|
|
264
|
-
plannedAdditions: new Map(),
|
|
265
|
-
version: 0,
|
|
266
|
-
})
|
|
267
|
-
} else {
|
|
268
|
-
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
269
|
-
newPlannedRemovals.set(itemId, type)
|
|
270
|
-
const newPlannedAdditions = new Map(existing.plannedAdditions)
|
|
271
|
-
newPlannedAdditions.delete(itemId)
|
|
272
|
-
let newOrderedIds = existing.orderedIds
|
|
273
|
-
if (newOrderedIds !== null) {
|
|
274
|
-
newOrderedIds = newOrderedIds.filter(id => id !== itemId)
|
|
275
|
-
}
|
|
276
|
-
this.writeHasMany(key, {
|
|
277
|
-
...existing,
|
|
278
|
-
orderedIds: newOrderedIds,
|
|
279
|
-
plannedRemovals: newPlannedRemovals,
|
|
280
|
-
plannedAdditions: newPlannedAdditions,
|
|
281
|
-
version: existing.version + 1,
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
this.editableWriteVersion++
|
|
173
|
+
state.version = existing ? existing.version + 1 : 0
|
|
174
|
+
this.writeHasMany(key, state)
|
|
285
175
|
}
|
|
286
176
|
|
|
287
177
|
/**
|
|
288
|
-
*
|
|
178
|
+
* Gets or creates the relation state and the addressed view, refreshing that
|
|
179
|
+
* view's server baseline when one is supplied.
|
|
289
180
|
*/
|
|
290
|
-
|
|
291
|
-
const
|
|
181
|
+
getOrCreateHasMany(key: string, alias: string, serverIds?: string[]): void {
|
|
182
|
+
const view = this.hasManyStates.get(key)?.views.get(alias)
|
|
183
|
+
if (view && (serverIds === undefined || setsEqual(view.serverIds, new Set(serverIds)))) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
292
186
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
version: 0,
|
|
300
|
-
})
|
|
301
|
-
} else {
|
|
302
|
-
const newPlannedAdditions = new Map(existing.plannedAdditions)
|
|
303
|
-
// Do not downgrade an existing 'created' addition to 'connected'.
|
|
304
|
-
if (newPlannedAdditions.get(itemId) !== 'created') {
|
|
305
|
-
newPlannedAdditions.set(itemId, 'connected')
|
|
306
|
-
}
|
|
307
|
-
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
308
|
-
newPlannedRemovals.delete(itemId)
|
|
309
|
-
let newOrderedIds = existing.orderedIds
|
|
310
|
-
if (newOrderedIds !== null && !newOrderedIds.includes(itemId)) {
|
|
311
|
-
newOrderedIds = [...newOrderedIds, itemId]
|
|
312
|
-
}
|
|
313
|
-
this.writeHasMany(key, {
|
|
314
|
-
...existing,
|
|
315
|
-
orderedIds: newOrderedIds,
|
|
316
|
-
plannedAdditions: newPlannedAdditions,
|
|
317
|
-
plannedRemovals: newPlannedRemovals,
|
|
318
|
-
version: existing.version + 1,
|
|
319
|
-
})
|
|
187
|
+
const state = this.editableState(key, alias)
|
|
188
|
+
if (serverIds !== undefined) {
|
|
189
|
+
const next = state.views.get(alias)!
|
|
190
|
+
next.serverIds = new Set(serverIds)
|
|
191
|
+
// A refreshed baseline invalidates only THIS view's manual ordering.
|
|
192
|
+
next.orderedIds = null
|
|
320
193
|
}
|
|
321
|
-
this.
|
|
194
|
+
this.commitEdit(key, state)
|
|
322
195
|
}
|
|
323
196
|
|
|
324
|
-
/**
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
197
|
+
/** Relation state, deep-copied. For the export / undo paths that move whole states. */
|
|
198
|
+
getHasMany(key: string): StoredHasManyState | undefined {
|
|
199
|
+
const state = this.hasManyStates.get(key)
|
|
200
|
+
return state ? cloneHasManyState(state) : undefined
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** What persistence, dirty tracking and error paths see: one relation, no views. */
|
|
204
|
+
getRelationProjection(key: string): HasManyRelationProjection | undefined {
|
|
205
|
+
const state = this.hasManyStates.get(key)
|
|
206
|
+
return state ? toRelationProjection(state) : undefined
|
|
207
|
+
}
|
|
329
208
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
209
|
+
/** What one mounted has-many handle sees. */
|
|
210
|
+
getViewProjection(key: string, alias: string): HasManyViewProjection {
|
|
211
|
+
return toViewProjection(this.hasManyStates.get(key), alias)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Whether the relation carries writes the next persist would send. */
|
|
215
|
+
hasPendingWrites(key: string): boolean {
|
|
216
|
+
const state = this.hasManyStates.get(key)
|
|
217
|
+
if (!state) return false
|
|
218
|
+
return state.plannedRemovals.size > 0 || state.plannedAdditions.size > 0
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Whether one view carries an order its user arranged rather than the default one. */
|
|
222
|
+
hasExplicitOrder(key: string, alias: string): boolean {
|
|
223
|
+
const orderedIds = this.hasManyStates.get(key)?.views.get(alias)?.orderedIds
|
|
224
|
+
return orderedIds !== null && orderedIds !== undefined
|
|
337
225
|
}
|
|
338
226
|
|
|
339
227
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
228
|
+
* Per-view server baselines by reference, for callers that only read them.
|
|
229
|
+
*
|
|
230
|
+
* Not cloned, deliberately: every mutator builds on {@link editableState}, so a
|
|
231
|
+
* stored Set is never mutated in place — only replaced. A handed-out reference can
|
|
232
|
+
* therefore go stale but can never be written through.
|
|
342
233
|
*/
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
234
|
+
collectViewServerIds(key: string): ReadonlyMap<string, ReadonlySet<string>> {
|
|
235
|
+
const byView = new Map<string, ReadonlySet<string>>()
|
|
236
|
+
const state = this.hasManyStates.get(key)
|
|
237
|
+
if (state) {
|
|
238
|
+
for (const [alias, view] of state.views) byView.set(alias, view.serverIds)
|
|
239
|
+
}
|
|
240
|
+
return byView
|
|
241
|
+
}
|
|
346
242
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
243
|
+
/** Replaces one view's server baseline. */
|
|
244
|
+
setHasManyServerIds(key: string, alias: string, serverIds: string[]): void {
|
|
245
|
+
const state = this.editableState(key, alias)
|
|
246
|
+
const view = state.views.get(alias)!
|
|
247
|
+
view.serverIds = new Set(serverIds)
|
|
248
|
+
view.orderedIds = null
|
|
249
|
+
this.commitEdit(key, state)
|
|
350
250
|
}
|
|
351
251
|
|
|
352
252
|
/**
|
|
353
|
-
*
|
|
253
|
+
* Plans a removal. Relation-level: the item stops being a member of the relation,
|
|
254
|
+
* so it leaves every view's ordering too.
|
|
354
255
|
*/
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
256
|
+
planHasManyRemoval(key: string, itemId: string, type: HasManyRemovalType): void {
|
|
257
|
+
const state = this.editableState(key)
|
|
258
|
+
state.plannedRemovals.set(itemId, type)
|
|
259
|
+
state.plannedAdditions.delete(itemId)
|
|
260
|
+
this.dropFromEveryExplicitOrder(state, itemId)
|
|
261
|
+
this.commitEdit(key, state)
|
|
262
|
+
this.editableWriteVersion++
|
|
263
|
+
}
|
|
358
264
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Plans a connection of an existing entity, rendered in the view it was made in.
|
|
267
|
+
*/
|
|
268
|
+
planHasManyConnection(key: string, alias: string, itemId: string): void {
|
|
269
|
+
const state = this.editableState(key, alias)
|
|
270
|
+
this.recordAddition(state, alias, itemId, 'connected')
|
|
271
|
+
this.commitEdit(key, state)
|
|
366
272
|
this.editableWriteVersion++
|
|
367
273
|
}
|
|
368
274
|
|
|
@@ -370,29 +276,14 @@ export class HasManyStore {
|
|
|
370
276
|
* Adds a newly created entity to a has-many relation.
|
|
371
277
|
* Used by HasManyListHandle.add() for inline entity creation.
|
|
372
278
|
*/
|
|
373
|
-
addToHasMany(key: string, itemId: string): void {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
plannedAdditions: new Map([[itemId, 'created']]),
|
|
382
|
-
version: 0,
|
|
383
|
-
})
|
|
384
|
-
} else {
|
|
385
|
-
const newPlannedAdditions = new Map(existing.plannedAdditions)
|
|
386
|
-
newPlannedAdditions.set(itemId, 'created')
|
|
387
|
-
const currentOrderedIds = existing.orderedIds ?? computeDefaultOrderedIds(existing)
|
|
388
|
-
const newOrderedIds = [...currentOrderedIds, itemId]
|
|
389
|
-
this.writeHasMany(key, {
|
|
390
|
-
...existing,
|
|
391
|
-
orderedIds: newOrderedIds,
|
|
392
|
-
plannedAdditions: newPlannedAdditions,
|
|
393
|
-
version: existing.version + 1,
|
|
394
|
-
})
|
|
395
|
-
}
|
|
279
|
+
addToHasMany(key: string, alias: string, itemId: string): void {
|
|
280
|
+
const state = this.editableState(key, alias)
|
|
281
|
+
// The order is materialized BEFORE the addition is recorded, so the append
|
|
282
|
+
// cannot duplicate an id the default order would by then already contain.
|
|
283
|
+
const order = viewOrderedIds(state, alias)
|
|
284
|
+
this.recordAddition(state, alias, itemId, 'created')
|
|
285
|
+
state.views.get(alias)!.orderedIds = [...order, itemId]
|
|
286
|
+
this.commitEdit(key, state)
|
|
396
287
|
this.editableWriteVersion++
|
|
397
288
|
}
|
|
398
289
|
|
|
@@ -401,45 +292,92 @@ export class HasManyStore {
|
|
|
401
292
|
* Unlike addToHasMany, records the addition as 'connected' (not 'created') —
|
|
402
293
|
* used for materializing embedded connect references to existing entities.
|
|
403
294
|
*/
|
|
404
|
-
connectExistingToHasMany(key: string, itemId: string): void {
|
|
405
|
-
const
|
|
295
|
+
connectExistingToHasMany(key: string, alias: string, itemId: string): void {
|
|
296
|
+
const fresh = !this.hasManyStates.has(key)
|
|
297
|
+
const state = this.editableState(key, alias)
|
|
298
|
+
this.recordAddition(state, alias, itemId, 'connected')
|
|
299
|
+
if (fresh) {
|
|
300
|
+
state.views.get(alias)!.orderedIds = [itemId]
|
|
301
|
+
}
|
|
302
|
+
this.commitEdit(key, state)
|
|
303
|
+
this.editableWriteVersion++
|
|
304
|
+
}
|
|
406
305
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Records an addition and the view it renders in. Never downgrades an existing
|
|
308
|
+
* 'created' addition to 'connected', and cancels a pending removal of the same
|
|
309
|
+
* item — leaving both recorded would hide a listed item from the live-edge index.
|
|
310
|
+
*/
|
|
311
|
+
private recordAddition(
|
|
312
|
+
state: StoredHasManyState,
|
|
313
|
+
alias: string,
|
|
314
|
+
itemId: string,
|
|
315
|
+
kind: 'created' | 'connected',
|
|
316
|
+
): void {
|
|
317
|
+
const existing = state.plannedAdditions.get(itemId)
|
|
318
|
+
const addition = existing ?? { kind, origins: new Set<string>() }
|
|
319
|
+
addition.origins.add(alias)
|
|
320
|
+
if (kind === 'created') addition.kind = 'created'
|
|
321
|
+
state.plannedAdditions.set(itemId, addition)
|
|
322
|
+
state.plannedRemovals.delete(itemId)
|
|
323
|
+
this.appendToShowingExplicitOrders(state, itemId, addition)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* An explicit order replaces the default one, which is where additions otherwise
|
|
328
|
+
* appear, so every view that shows the item must list it — not only the view it was
|
|
329
|
+
* made in. That includes a view whose own server rows hold it: a removal dropped it
|
|
330
|
+
* from every explicit order, and re-connecting it must give it back. Guards against
|
|
331
|
+
* re-appending an id that is already listed: the connect paths re-run whenever an
|
|
332
|
+
* embedded reference is re-materialized.
|
|
333
|
+
*/
|
|
334
|
+
private appendToShowingExplicitOrders(
|
|
335
|
+
state: StoredHasManyState,
|
|
336
|
+
itemId: string,
|
|
337
|
+
addition: PlannedHasManyAddition,
|
|
338
|
+
): void {
|
|
339
|
+
for (const [alias, view] of state.views) {
|
|
340
|
+
if (view.orderedIds === null || view.orderedIds.includes(itemId)) continue
|
|
341
|
+
if (view.serverIds.has(itemId) || additionRendersIn(state.views, alias, addition)) {
|
|
342
|
+
view.orderedIds = [...view.orderedIds, itemId]
|
|
420
343
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// live-edge index while it is still listed.
|
|
429
|
-
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
430
|
-
newPlannedRemovals.delete(itemId)
|
|
431
|
-
let newOrderedIds = existing.orderedIds
|
|
432
|
-
if (newOrderedIds !== null && !newOrderedIds.includes(itemId)) {
|
|
433
|
-
newOrderedIds = [...newOrderedIds, itemId]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private dropFromEveryExplicitOrder(state: StoredHasManyState, itemId: string): void {
|
|
348
|
+
for (const view of state.views.values()) {
|
|
349
|
+
if (view.orderedIds !== null) {
|
|
350
|
+
view.orderedIds = view.orderedIds.filter(id => id !== itemId)
|
|
434
351
|
}
|
|
435
|
-
this.writeHasMany(key, {
|
|
436
|
-
...existing,
|
|
437
|
-
orderedIds: newOrderedIds,
|
|
438
|
-
plannedAdditions: newPlannedAdditions,
|
|
439
|
-
plannedRemovals: newPlannedRemovals,
|
|
440
|
-
version: existing.version + 1,
|
|
441
|
-
})
|
|
442
352
|
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Advances the server baseline by the confirmed sent delta and rebases local
|
|
357
|
+
* edits made after the request started onto that new baseline.
|
|
358
|
+
*/
|
|
359
|
+
reconcileSentDelta(key: string, delta: SentHasManyDelta): ReconciliationResult {
|
|
360
|
+
const existing = this.hasManyStates.get(key)
|
|
361
|
+
if (!existing) return 'conflict'
|
|
362
|
+
|
|
363
|
+
const next = reconcileHasManyState(existing, delta, fieldFromRelationKey(key))
|
|
364
|
+
this.writeHasMany(key, next.state)
|
|
365
|
+
return next.result
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Resets has-many state to server state (clears planned operations).
|
|
370
|
+
* Relation-level: planned removals carry no view, so a per-view reset could not
|
|
371
|
+
* know which of them to drop.
|
|
372
|
+
*/
|
|
373
|
+
resetHasMany(key: string): void {
|
|
374
|
+
if (!this.hasManyStates.has(key)) return
|
|
375
|
+
|
|
376
|
+
const state = this.editableState(key)
|
|
377
|
+
state.plannedRemovals.clear()
|
|
378
|
+
state.plannedAdditions.clear()
|
|
379
|
+
for (const view of state.views.values()) view.orderedIds = null
|
|
380
|
+
this.commitEdit(key, state)
|
|
443
381
|
this.editableWriteVersion++
|
|
444
382
|
}
|
|
445
383
|
|
|
@@ -453,52 +391,40 @@ export class HasManyStore {
|
|
|
453
391
|
const existing = this.hasManyStates.get(key)
|
|
454
392
|
if (!existing) return false
|
|
455
393
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
let newOrderedIds = existing.orderedIds
|
|
462
|
-
if (newOrderedIds !== null) {
|
|
463
|
-
newOrderedIds = newOrderedIds.filter(id => id !== itemId)
|
|
464
|
-
}
|
|
394
|
+
if (existing.plannedAdditions.get(itemId)?.kind !== 'created') {
|
|
395
|
+
// planHasManyRemoval bumps editableWriteVersion itself.
|
|
396
|
+
this.planHasManyRemoval(key, itemId, removalType)
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
465
399
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
400
|
+
// Cancelling a pending create cancels it in every view at once: the entity is
|
|
401
|
+
// going away, so leaving it listed elsewhere would show a row nothing persists.
|
|
402
|
+
const state = this.editableState(key)
|
|
403
|
+
state.plannedAdditions.delete(itemId)
|
|
404
|
+
this.dropFromEveryExplicitOrder(state, itemId)
|
|
472
405
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
) {
|
|
478
|
-
const defaultOrder = computeDefaultOrderedIds(newState)
|
|
479
|
-
if (arraysEqual(newOrderedIds, defaultOrder)) {
|
|
480
|
-
newState.orderedIds = null
|
|
406
|
+
if (state.plannedAdditions.size === 0 && state.plannedRemovals.size === 0) {
|
|
407
|
+
for (const [alias, view] of state.views) {
|
|
408
|
+
if (view.orderedIds !== null && arraysEqual(view.orderedIds, computeViewOrderedIds(state, alias))) {
|
|
409
|
+
view.orderedIds = null
|
|
481
410
|
}
|
|
482
411
|
}
|
|
483
|
-
|
|
484
|
-
this.writeHasMany(key, newState)
|
|
485
|
-
this.editableWriteVersion++
|
|
486
|
-
return true
|
|
487
|
-
} else {
|
|
488
|
-
// planHasManyRemoval bumps editableWriteVersion itself.
|
|
489
|
-
this.planHasManyRemoval(key, itemId, removalType)
|
|
490
|
-
return true
|
|
491
412
|
}
|
|
413
|
+
|
|
414
|
+
this.commitEdit(key, state)
|
|
415
|
+
this.editableWriteVersion++
|
|
416
|
+
return true
|
|
492
417
|
}
|
|
493
418
|
|
|
494
419
|
/**
|
|
495
|
-
* Moves an item within a has-many relation
|
|
420
|
+
* Moves an item within one view of a has-many relation. Ordering is presentation
|
|
421
|
+
* only — nothing persists it — so it stays per view.
|
|
496
422
|
*/
|
|
497
|
-
moveInHasMany(key: string, fromIndex: number, toIndex: number): void {
|
|
423
|
+
moveInHasMany(key: string, alias: string, fromIndex: number, toIndex: number): void {
|
|
498
424
|
const existing = this.hasManyStates.get(key)
|
|
499
425
|
if (!existing) return
|
|
500
426
|
|
|
501
|
-
const currentOrderedIds = existing
|
|
427
|
+
const currentOrderedIds = viewOrderedIds(existing, alias)
|
|
502
428
|
|
|
503
429
|
if (fromIndex < 0 || fromIndex >= currentOrderedIds.length) return
|
|
504
430
|
if (toIndex < 0 || toIndex >= currentOrderedIds.length) return
|
|
@@ -509,26 +435,19 @@ export class HasManyStore {
|
|
|
509
435
|
if (movedItem === undefined) return
|
|
510
436
|
newOrderedIds.splice(toIndex, 0, movedItem)
|
|
511
437
|
|
|
512
|
-
this.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
version: existing.version + 1,
|
|
516
|
-
})
|
|
438
|
+
const state = this.editableState(key, alias)
|
|
439
|
+
state.views.get(alias)!.orderedIds = newOrderedIds
|
|
440
|
+
this.commitEdit(key, state)
|
|
517
441
|
this.editableWriteVersion++
|
|
518
442
|
}
|
|
519
443
|
|
|
520
444
|
/**
|
|
521
|
-
* Gets the ordered list of item IDs
|
|
445
|
+
* Gets the ordered list of item IDs shown by one view.
|
|
522
446
|
*/
|
|
523
|
-
getHasManyOrderedIds(key: string): string[] {
|
|
447
|
+
getHasManyOrderedIds(key: string, alias: string): string[] {
|
|
524
448
|
const existing = this.hasManyStates.get(key)
|
|
525
449
|
if (!existing) return []
|
|
526
|
-
|
|
527
|
-
if (existing.orderedIds !== null) {
|
|
528
|
-
return [...existing.orderedIds]
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return computeDefaultOrderedIds(existing)
|
|
450
|
+
return viewOrderedIds(existing, alias)
|
|
532
451
|
}
|
|
533
452
|
|
|
534
453
|
/**
|
|
@@ -583,38 +502,39 @@ export class HasManyStore {
|
|
|
583
502
|
*
|
|
584
503
|
* `pendingItems` (by relation key) names planned additions/removals that were NOT
|
|
585
504
|
* sent — they stay planned instead of being folded into the server baseline.
|
|
505
|
+
*
|
|
506
|
+
* A confirmed removal leaves every view; a confirmed addition joins the views it
|
|
507
|
+
* renders in. Only the views that actually changed lose their manual ordering —
|
|
508
|
+
* an untouched sibling view keeps the order its user arranged.
|
|
586
509
|
*/
|
|
587
510
|
commitAllRelations(keyPrefix: string, pendingItems?: ReadonlyMap<string, ReadonlySet<string>>): void {
|
|
588
|
-
for (const [key,
|
|
511
|
+
for (const [key, existing] of this.hasManyStates) {
|
|
589
512
|
if (!key.startsWith(keyPrefix)) continue
|
|
590
513
|
|
|
591
514
|
const pending = pendingItems?.get(key)
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
newServerIds.delete(removedId)
|
|
515
|
+
const state = cloneHasManyState(existing)
|
|
516
|
+
const touched = new Set<string>()
|
|
517
|
+
|
|
518
|
+
for (const removedId of existing.plannedRemovals.keys()) {
|
|
519
|
+
if (pending?.has(removedId)) continue
|
|
520
|
+
state.plannedRemovals.delete(removedId)
|
|
521
|
+
for (const [alias, view] of state.views) {
|
|
522
|
+
if (view.serverIds.delete(removedId)) touched.add(alias)
|
|
601
523
|
}
|
|
602
524
|
}
|
|
603
|
-
for (const [addedId,
|
|
604
|
-
if (pending?.has(addedId))
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
525
|
+
for (const [addedId, addition] of existing.plannedAdditions) {
|
|
526
|
+
if (pending?.has(addedId)) continue
|
|
527
|
+
state.plannedAdditions.delete(addedId)
|
|
528
|
+
for (const alias of additionFoldTargets(existing, addition)) {
|
|
529
|
+
state.views.get(alias)!.serverIds.add(addedId)
|
|
530
|
+
touched.add(alias)
|
|
608
531
|
}
|
|
609
532
|
}
|
|
610
533
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
plannedAdditions: keptAdditions,
|
|
616
|
-
version: state.version + 1,
|
|
617
|
-
})
|
|
534
|
+
for (const alias of touched) state.views.get(alias)!.orderedIds = null
|
|
535
|
+
|
|
536
|
+
state.version = existing.version + 1
|
|
537
|
+
this.writeHasMany(key, state)
|
|
618
538
|
}
|
|
619
539
|
}
|
|
620
540
|
|
|
@@ -664,13 +584,9 @@ export class HasManyStore {
|
|
|
664
584
|
importHasManyStates(states: Map<string, StoredHasManyState>): string[] {
|
|
665
585
|
const keys: string[] = []
|
|
666
586
|
for (const [key, state] of states) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
plannedRemovals: new Map(state.plannedRemovals),
|
|
671
|
-
plannedAdditions: new Map(state.plannedAdditions),
|
|
672
|
-
version: state.version + 1,
|
|
673
|
-
})
|
|
587
|
+
const imported = cloneHasManyState(state)
|
|
588
|
+
imported.version = state.version + 1
|
|
589
|
+
this.writeHasMany(key, imported)
|
|
674
590
|
keys.push(key)
|
|
675
591
|
}
|
|
676
592
|
return keys
|
|
@@ -678,58 +594,47 @@ export class HasManyStore {
|
|
|
678
594
|
|
|
679
595
|
/**
|
|
680
596
|
* Replaces all occurrences of oldId with newId across has-many states
|
|
681
|
-
* (serverIds
|
|
597
|
+
* (every view's serverIds/orderedIds, plannedAdditions, plannedRemovals).
|
|
682
598
|
*/
|
|
683
599
|
replaceEntityId(oldId: string, newId: string): void {
|
|
684
|
-
for (const [key,
|
|
600
|
+
for (const [key, existing] of this.hasManyStates) {
|
|
685
601
|
let changed = false
|
|
602
|
+
const state = cloneHasManyState(existing)
|
|
686
603
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
serverIds.delete(oldId)
|
|
691
|
-
serverIds.add(newId)
|
|
692
|
-
changed = true
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
let orderedIds = state.orderedIds
|
|
696
|
-
if (orderedIds) {
|
|
697
|
-
const idx = orderedIds.indexOf(oldId)
|
|
698
|
-
if (idx !== -1) {
|
|
699
|
-
orderedIds = [...orderedIds]
|
|
700
|
-
orderedIds[idx] = newId
|
|
604
|
+
for (const view of state.views.values()) {
|
|
605
|
+
if (view.serverIds.delete(oldId)) {
|
|
606
|
+
view.serverIds.add(newId)
|
|
701
607
|
changed = true
|
|
702
608
|
}
|
|
609
|
+
if (view.orderedIds) {
|
|
610
|
+
const idx = view.orderedIds.indexOf(oldId)
|
|
611
|
+
if (idx !== -1) {
|
|
612
|
+
view.orderedIds[idx] = newId
|
|
613
|
+
changed = true
|
|
614
|
+
}
|
|
615
|
+
}
|
|
703
616
|
}
|
|
704
617
|
|
|
705
|
-
|
|
706
|
-
if (
|
|
707
|
-
plannedAdditions
|
|
708
|
-
plannedAdditions.delete(oldId)
|
|
618
|
+
const addition = state.plannedAdditions.get(oldId)
|
|
619
|
+
if (addition) {
|
|
620
|
+
state.plannedAdditions.delete(oldId)
|
|
709
621
|
// The id only changes when the item was persisted, so a still-planned
|
|
710
622
|
// 'created' addition (the item went out on its own, not nested in this
|
|
711
623
|
// parent) must now connect the server row rather than create a second one.
|
|
712
|
-
plannedAdditions.set(newId, 'connected')
|
|
624
|
+
state.plannedAdditions.set(newId, { kind: 'connected', origins: addition.origins })
|
|
713
625
|
changed = true
|
|
714
626
|
}
|
|
715
627
|
|
|
716
|
-
|
|
717
|
-
if (
|
|
718
|
-
|
|
719
|
-
plannedRemovals
|
|
720
|
-
plannedRemovals.delete(oldId)
|
|
721
|
-
plannedRemovals.set(newId, removalType)
|
|
628
|
+
const removalType = state.plannedRemovals.get(oldId)
|
|
629
|
+
if (removalType !== undefined) {
|
|
630
|
+
state.plannedRemovals.delete(oldId)
|
|
631
|
+
state.plannedRemovals.set(newId, removalType)
|
|
722
632
|
changed = true
|
|
723
633
|
}
|
|
724
634
|
|
|
725
635
|
if (changed) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
orderedIds,
|
|
729
|
-
plannedRemovals,
|
|
730
|
-
plannedAdditions,
|
|
731
|
-
version: state.version + 1,
|
|
732
|
-
})
|
|
636
|
+
state.version = existing.version + 1
|
|
637
|
+
this.writeHasMany(key, state)
|
|
733
638
|
}
|
|
734
639
|
}
|
|
735
640
|
}
|
|
@@ -763,84 +668,3 @@ export class HasManyStore {
|
|
|
763
668
|
this.mutationVersion++
|
|
764
669
|
}
|
|
765
670
|
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* The single liveness predicate for has-many membership: effective members are
|
|
769
|
-
* (serverIds ∪ plannedAdditions) minus plannedRemovals. Defined once and consumed
|
|
770
|
-
* by the write chokepoint so the forward/reverse index can never drift from it.
|
|
771
|
-
*/
|
|
772
|
-
function liveHasManyChildIds(state: StoredHasManyState | undefined): Set<string> {
|
|
773
|
-
const live = new Set<string>()
|
|
774
|
-
if (!state) return live
|
|
775
|
-
for (const id of state.serverIds) {
|
|
776
|
-
if (!state.plannedRemovals.has(id)) live.add(id)
|
|
777
|
-
}
|
|
778
|
-
for (const id of state.plannedAdditions.keys()) {
|
|
779
|
-
if (!state.plannedRemovals.has(id)) live.add(id)
|
|
780
|
-
}
|
|
781
|
-
return live
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function plannedDeleteChildIds(state: StoredHasManyState | undefined): Set<string> {
|
|
785
|
-
const ids = new Set<string>()
|
|
786
|
-
if (!state) return ids
|
|
787
|
-
for (const [id, type] of state.plannedRemovals) {
|
|
788
|
-
if (type === 'delete') ids.add(id)
|
|
789
|
-
}
|
|
790
|
-
return ids
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
interface HasManyReconciliation {
|
|
794
|
-
state: StoredHasManyState
|
|
795
|
-
result: ReconciliationResult
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function reconcileHasManyState(
|
|
799
|
-
existing: StoredHasManyState,
|
|
800
|
-
delta: SentHasManyDelta,
|
|
801
|
-
): HasManyReconciliation {
|
|
802
|
-
const currentLive = liveHasManyChildIds(existing)
|
|
803
|
-
const serverIds = new Set(existing.serverIds)
|
|
804
|
-
const plannedAdditions = new Map(existing.plannedAdditions)
|
|
805
|
-
const plannedRemovals = new Map(existing.plannedRemovals)
|
|
806
|
-
let result: ReconciliationResult = 'applied'
|
|
807
|
-
|
|
808
|
-
for (const addition of delta.additions) {
|
|
809
|
-
serverIds.add(addition.itemId)
|
|
810
|
-
plannedAdditions.delete(addition.itemId)
|
|
811
|
-
plannedRemovals.delete(addition.itemId)
|
|
812
|
-
if (!currentLive.has(addition.itemId)) {
|
|
813
|
-
plannedRemovals.set(
|
|
814
|
-
addition.itemId,
|
|
815
|
-
addition.kind === 'created' ? 'delete' : 'disconnect',
|
|
816
|
-
)
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
for (const removal of delta.removals) {
|
|
821
|
-
serverIds.delete(removal.itemId)
|
|
822
|
-
const currentRemoval = plannedRemovals.get(removal.itemId)
|
|
823
|
-
if (currentRemoval === removal.type) plannedRemovals.delete(removal.itemId)
|
|
824
|
-
|
|
825
|
-
if (!currentLive.has(removal.itemId)) {
|
|
826
|
-
if (removal.type === 'delete') plannedRemovals.delete(removal.itemId)
|
|
827
|
-
continue
|
|
828
|
-
}
|
|
829
|
-
if (removal.type === 'delete') {
|
|
830
|
-
result = 'conflict'
|
|
831
|
-
} else if (!plannedAdditions.has(removal.itemId)) {
|
|
832
|
-
plannedAdditions.set(removal.itemId, 'connected')
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
return {
|
|
837
|
-
state: {
|
|
838
|
-
serverIds,
|
|
839
|
-
orderedIds: existing.orderedIds ? [...existing.orderedIds] : null,
|
|
840
|
-
plannedRemovals,
|
|
841
|
-
plannedAdditions,
|
|
842
|
-
version: existing.version + 1,
|
|
843
|
-
},
|
|
844
|
-
result,
|
|
845
|
-
}
|
|
846
|
-
}
|