@contember/bindx 0.1.46 → 0.1.48
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/adapter/MockAdapter.d.ts +4 -1
- package/dist/adapter/MockAdapter.d.ts.map +1 -1
- package/dist/core/ActionDispatcher.d.ts +4 -1
- package/dist/core/ActionDispatcher.d.ts.map +1 -1
- package/dist/core/actions.d.ts +7 -1
- package/dist/core/actions.d.ts.map +1 -1
- package/dist/events/EventEmitter.d.ts +13 -1
- package/dist/events/EventEmitter.d.ts.map +1 -1
- package/dist/handles/BaseHandle.d.ts +14 -3
- package/dist/handles/BaseHandle.d.ts.map +1 -1
- package/dist/handles/EntityHandle.d.ts +14 -5
- package/dist/handles/EntityHandle.d.ts.map +1 -1
- package/dist/handles/FieldHandle.d.ts +11 -4
- package/dist/handles/FieldHandle.d.ts.map +1 -1
- package/dist/handles/HasManyListHandle.d.ts +36 -2
- package/dist/handles/HasManyListHandle.d.ts.map +1 -1
- package/dist/handles/HasOneHandle.d.ts +52 -9
- package/dist/handles/HasOneHandle.d.ts.map +1 -1
- package/dist/handles/PlaceholderHandle.d.ts +25 -13
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -1
- package/dist/handles/index.d.ts +1 -1
- package/dist/handles/index.d.ts.map +1 -1
- package/dist/handles/proxyFactory.d.ts +7 -1
- package/dist/handles/proxyFactory.d.ts.map +1 -1
- package/dist/handles/types.d.ts +33 -0
- package/dist/handles/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/BatchPersister.d.ts +53 -37
- package/dist/persistence/BatchPersister.d.ts.map +1 -1
- package/dist/persistence/ChangeRegistry.d.ts +15 -0
- package/dist/persistence/ChangeRegistry.d.ts.map +1 -1
- package/dist/persistence/MutationCollector.d.ts +106 -3
- package/dist/persistence/MutationCollector.d.ts.map +1 -1
- package/dist/persistence/PersistExecution.d.ts +26 -0
- package/dist/persistence/PersistExecution.d.ts.map +1 -0
- package/dist/persistence/types.d.ts +8 -2
- package/dist/persistence/types.d.ts.map +1 -1
- package/dist/store/EntityMetaStore.d.ts +32 -10
- package/dist/store/EntityMetaStore.d.ts.map +1 -1
- package/dist/store/EntitySnapshotStore.d.ts +50 -3
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -1
- package/dist/store/ErrorStore.d.ts +3 -2
- package/dist/store/ErrorStore.d.ts.map +1 -1
- package/dist/store/HasManyStore.d.ts +214 -0
- package/dist/store/HasManyStore.d.ts.map +1 -0
- package/dist/store/HasOneStore.d.ts +159 -0
- package/dist/store/HasOneStore.d.ts.map +1 -0
- package/dist/store/ReachabilityAnalyzer.d.ts +19 -0
- package/dist/store/ReachabilityAnalyzer.d.ts.map +1 -1
- package/dist/store/RekeyOrchestrator.d.ts +76 -0
- package/dist/store/RekeyOrchestrator.d.ts.map +1 -0
- package/dist/store/RelationEdgeIndex.d.ts +34 -0
- package/dist/store/RelationEdgeIndex.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +74 -137
- package/dist/store/RelationStore.d.ts.map +1 -1
- package/dist/store/RootRegistry.d.ts +25 -4
- package/dist/store/RootRegistry.d.ts.map +1 -1
- package/dist/store/SnapshotStore.d.ts +137 -13
- package/dist/store/SnapshotStore.d.ts.map +1 -1
- package/dist/store/SubscriptionManager.d.ts +63 -14
- package/dist/store/SubscriptionManager.d.ts.map +1 -1
- package/dist/store/TouchedStore.d.ts +3 -2
- package/dist/store/TouchedStore.d.ts.map +1 -1
- package/dist/store/relationKey.d.ts +15 -0
- package/dist/store/relationKey.d.ts.map +1 -0
- package/dist/undo/UndoJournal.d.ts +133 -0
- package/dist/undo/UndoJournal.d.ts.map +1 -0
- package/dist/undo/UndoManager.d.ts +25 -82
- package/dist/undo/UndoManager.d.ts.map +1 -1
- package/dist/undo/UnrecordedWriteError.d.ts +16 -0
- package/dist/undo/UnrecordedWriteError.d.ts.map +1 -0
- package/dist/undo/index.d.ts +3 -1
- package/dist/undo/index.d.ts.map +1 -1
- package/dist/undo/rekeyJournalEntry.d.ts +20 -0
- package/dist/undo/rekeyJournalEntry.d.ts.map +1 -0
- package/dist/undo/types.d.ts +0 -48
- package/dist/undo/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/adapter/MockAdapter.ts +15 -5
- package/src/core/ActionDispatcher.ts +56 -36
- package/src/core/actions.ts +8 -1
- package/src/events/EventEmitter.ts +78 -24
- package/src/handles/BaseHandle.ts +19 -2
- package/src/handles/EntityHandle.ts +48 -17
- package/src/handles/FieldHandle.ts +36 -10
- package/src/handles/HasManyListHandle.ts +120 -16
- package/src/handles/HasOneHandle.ts +177 -44
- package/src/handles/PlaceholderHandle.ts +184 -36
- package/src/handles/index.ts +3 -0
- package/src/handles/proxyFactory.ts +27 -0
- package/src/handles/types.ts +39 -0
- package/src/index.ts +4 -4
- package/src/persistence/BatchPersister.ts +810 -446
- package/src/persistence/ChangeRegistry.ts +27 -3
- package/src/persistence/MutationCollector.ts +265 -31
- package/src/persistence/PersistExecution.ts +217 -0
- package/src/persistence/types.ts +8 -2
- package/src/store/EntityMetaStore.ts +89 -45
- package/src/store/EntitySnapshotStore.ts +102 -19
- package/src/store/ErrorStore.ts +6 -5
- package/src/store/HasManyStore.ts +810 -0
- package/src/store/HasOneStore.ts +454 -0
- package/src/store/ReachabilityAnalyzer.ts +37 -0
- package/src/store/RekeyOrchestrator.ts +123 -0
- package/src/store/RelationEdgeIndex.ts +79 -0
- package/src/store/RelationStore.ts +176 -725
- package/src/store/RootRegistry.ts +50 -8
- package/src/store/SnapshotStore.ts +572 -101
- package/src/store/SubscriptionManager.ts +122 -75
- package/src/store/TouchedStore.ts +5 -2
- package/src/store/relationKey.ts +20 -0
- package/src/undo/UndoJournal.ts +258 -0
- package/src/undo/UndoManager.ts +151 -281
- package/src/undo/UnrecordedWriteError.ts +23 -0
- package/src/undo/index.ts +8 -7
- package/src/undo/rekeyJournalEntry.ts +145 -0
- package/src/undo/types.ts +0 -58
- package/dist/core/actionClassification.d.ts +0 -48
- package/dist/core/actionClassification.d.ts.map +0 -1
- package/src/core/actionClassification.ts +0 -199
|
@@ -1,876 +1,327 @@
|
|
|
1
|
-
import type { HasOneRelationState } from '../handles/types.js'
|
|
2
1
|
import type { EntitySnapshot } from './snapshots.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Relation state stored in SnapshotStore
|
|
36
|
-
*/
|
|
37
|
-
export interface StoredRelationState {
|
|
38
|
-
currentId: string | null
|
|
39
|
-
serverId: string | null
|
|
40
|
-
state: HasOneRelationState
|
|
41
|
-
serverState: HasOneRelationState
|
|
42
|
-
placeholderData: Record<string, unknown>
|
|
43
|
-
version: number
|
|
44
|
-
}
|
|
2
|
+
import type { RekeyContext, Rekeyable } from './RekeyOrchestrator.js'
|
|
3
|
+
import {
|
|
4
|
+
HasOneStore,
|
|
5
|
+
type SentHasOneTransition,
|
|
6
|
+
type StoredRelationState,
|
|
7
|
+
} from './HasOneStore.js'
|
|
8
|
+
import {
|
|
9
|
+
HasManyStore,
|
|
10
|
+
computeDefaultOrderedIds,
|
|
11
|
+
type HasManyAdditionKind,
|
|
12
|
+
type HasManyRemovalType,
|
|
13
|
+
type SentHasManyDelta,
|
|
14
|
+
type StoredHasManyState,
|
|
15
|
+
} from './HasManyStore.js'
|
|
16
|
+
|
|
17
|
+
// Re-exported so existing imports from './RelationStore.js' keep resolving.
|
|
18
|
+
export type { StoredRelationState } from './HasOneStore.js'
|
|
19
|
+
export type { SentHasOneTransition } from './HasOneStore.js'
|
|
20
|
+
export type {
|
|
21
|
+
HasManyAdditionKind,
|
|
22
|
+
HasManyRemovalType,
|
|
23
|
+
SentHasManyAddition,
|
|
24
|
+
SentHasManyDelta,
|
|
25
|
+
SentHasManyRemoval,
|
|
26
|
+
StoredHasManyState,
|
|
27
|
+
} from './HasManyStore.js'
|
|
28
|
+
export { computeDefaultOrderedIds } from './HasManyStore.js'
|
|
29
|
+
|
|
30
|
+
export type RelationReconciliationResult = 'applied' | 'conflict'
|
|
45
31
|
|
|
46
32
|
/**
|
|
47
33
|
* Manages has-one and has-many relation state.
|
|
48
34
|
*
|
|
49
35
|
* Relation keys use the format "parentType:parentId:fieldName".
|
|
50
36
|
* Notification is handled by callers via callback returns.
|
|
37
|
+
*
|
|
38
|
+
* Thin facade composing a {@link HasOneStore} and a {@link HasManyStore}: each
|
|
39
|
+
* sub-store owns its own state map and write helper, and the facade merges the
|
|
40
|
+
* cross-cutting queries (mutation version sum; live-child/parent-key unions;
|
|
41
|
+
* dirty union) and fans out the cross-cutting mutations (rekey, removeOwned,
|
|
42
|
+
* clear) to both.
|
|
51
43
|
*/
|
|
52
|
-
export class RelationStore {
|
|
53
|
-
|
|
54
|
-
private readonly
|
|
44
|
+
export class RelationStore implements Rekeyable {
|
|
45
|
+
private readonly hasOne = new HasOneStore()
|
|
46
|
+
private readonly hasMany = new HasManyStore()
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Increases whenever EITHER sub-store mutates. Used by {@link ReachabilityAnalyzer}
|
|
50
|
+
* to memoize its walk; only monotonic-increase-on-mutation matters, so the sum
|
|
51
|
+
* of the two per-store counters is sufficient.
|
|
52
|
+
*/
|
|
53
|
+
getMutationVersion(): number {
|
|
54
|
+
return this.hasOne.getMutationVersion() + this.hasMany.getMutationVersion()
|
|
55
|
+
}
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
57
|
+
/** Editable-layer has-one write counter — read by the undo write-guard. */
|
|
58
|
+
getEditableHasOneWriteVersion(): number {
|
|
59
|
+
return this.hasOne.getEditableWriteVersion()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Editable-layer has-many write counter — read by the undo write-guard. */
|
|
63
|
+
getEditableHasManyWriteVersion(): number {
|
|
64
|
+
return this.hasMany.getEditableWriteVersion()
|
|
65
|
+
}
|
|
58
66
|
|
|
59
67
|
// ==================== Has-One Relations ====================
|
|
60
68
|
|
|
61
|
-
/**
|
|
62
|
-
* Gets or creates relation state.
|
|
63
|
-
*/
|
|
64
69
|
getOrCreateRelation(
|
|
65
70
|
key: string,
|
|
66
71
|
initial: Omit<StoredRelationState, 'version'>,
|
|
67
72
|
): StoredRelationState {
|
|
68
|
-
|
|
69
|
-
this.relationStates.set(key, { ...initial, version: 0 })
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return this.relationStates.get(key)!
|
|
73
|
+
return this.hasOne.getOrCreateRelation(key, initial)
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/**
|
|
76
|
-
* Gets relation state.
|
|
77
|
-
*/
|
|
78
76
|
getRelation(key: string): StoredRelationState | undefined {
|
|
79
|
-
return this.
|
|
77
|
+
return this.hasOne.getRelation(key)
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
/**
|
|
83
|
-
* Updates relation state.
|
|
84
|
-
* If the relation state doesn't exist, creates it using entity snapshot for server data.
|
|
85
|
-
*/
|
|
86
80
|
setRelation(
|
|
87
81
|
key: string,
|
|
88
82
|
updates: Partial<Omit<StoredRelationState, 'version'>>,
|
|
89
83
|
entitySnapshot: EntitySnapshot | undefined,
|
|
90
84
|
fieldName: string,
|
|
91
85
|
): void {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!existing) {
|
|
95
|
-
let serverId: string | null = null
|
|
96
|
-
let serverState: HasOneRelationState = 'disconnected'
|
|
97
|
-
|
|
98
|
-
if (entitySnapshot?.serverData) {
|
|
99
|
-
const relatedData = (entitySnapshot.serverData as Record<string, unknown>)[fieldName]
|
|
100
|
-
if (relatedData && typeof relatedData === 'object' && 'id' in relatedData) {
|
|
101
|
-
serverId = (relatedData as { id: string }).id
|
|
102
|
-
serverState = 'connected'
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.relationStates.set(key, {
|
|
107
|
-
currentId: 'currentId' in updates ? updates.currentId! : serverId,
|
|
108
|
-
serverId,
|
|
109
|
-
state: 'state' in updates ? updates.state! : serverState,
|
|
110
|
-
serverState,
|
|
111
|
-
placeholderData: updates.placeholderData ?? {},
|
|
112
|
-
version: 0,
|
|
113
|
-
})
|
|
114
|
-
} else {
|
|
115
|
-
this.relationStates.set(key, {
|
|
116
|
-
...existing,
|
|
117
|
-
...updates,
|
|
118
|
-
version: existing.version + 1,
|
|
119
|
-
})
|
|
120
|
-
}
|
|
86
|
+
this.hasOne.setRelation(key, updates, entitySnapshot, fieldName)
|
|
121
87
|
}
|
|
122
88
|
|
|
123
|
-
/**
|
|
124
|
-
* Commits relation state (server = current).
|
|
125
|
-
*/
|
|
126
89
|
commitRelation(key: string): void {
|
|
127
|
-
|
|
128
|
-
|
|
90
|
+
this.hasOne.commitRelation(key)
|
|
91
|
+
}
|
|
129
92
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
version: existing.version + 1,
|
|
136
|
-
})
|
|
93
|
+
reconcileSentRelation(
|
|
94
|
+
key: string,
|
|
95
|
+
transition: SentHasOneTransition,
|
|
96
|
+
): RelationReconciliationResult {
|
|
97
|
+
return this.hasOne.reconcileSentTransition(key, transition)
|
|
137
98
|
}
|
|
138
99
|
|
|
139
|
-
/**
|
|
140
|
-
* Resets relation to server state.
|
|
141
|
-
*/
|
|
142
100
|
resetRelation(key: string): void {
|
|
143
|
-
|
|
144
|
-
if (!existing) return
|
|
145
|
-
|
|
146
|
-
this.relationStates.set(key, {
|
|
147
|
-
...existing,
|
|
148
|
-
currentId: existing.serverId,
|
|
149
|
-
state: existing.serverState,
|
|
150
|
-
placeholderData: {},
|
|
151
|
-
version: existing.version + 1,
|
|
152
|
-
})
|
|
101
|
+
this.hasOne.resetRelation(key)
|
|
153
102
|
}
|
|
154
103
|
|
|
155
104
|
// ==================== Has-Many Relations ====================
|
|
156
105
|
|
|
157
|
-
/**
|
|
158
|
-
* Gets or creates has-many list state.
|
|
159
|
-
*/
|
|
160
106
|
getOrCreateHasMany(key: string, serverIds?: string[]): StoredHasManyState {
|
|
161
|
-
|
|
162
|
-
if (!existing) {
|
|
163
|
-
this.hasManyStates.set(key, {
|
|
164
|
-
serverIds: new Set(serverIds ?? []),
|
|
165
|
-
orderedIds: null,
|
|
166
|
-
plannedRemovals: new Map(),
|
|
167
|
-
plannedConnections: new Set(),
|
|
168
|
-
createdEntities: new Set(),
|
|
169
|
-
version: 0,
|
|
170
|
-
})
|
|
171
|
-
} else if (serverIds !== undefined) {
|
|
172
|
-
const newServerIds = new Set(serverIds)
|
|
173
|
-
if (!setsEqual(existing.serverIds, newServerIds)) {
|
|
174
|
-
this.hasManyStates.set(key, {
|
|
175
|
-
...existing,
|
|
176
|
-
serverIds: newServerIds,
|
|
177
|
-
orderedIds: null,
|
|
178
|
-
version: existing.version + 1,
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return this.hasManyStates.get(key)!
|
|
107
|
+
return this.hasMany.getOrCreateHasMany(key, serverIds)
|
|
184
108
|
}
|
|
185
109
|
|
|
186
|
-
/**
|
|
187
|
-
* Gets has-many list state.
|
|
188
|
-
*/
|
|
189
110
|
getHasMany(key: string): StoredHasManyState | undefined {
|
|
190
|
-
return this.
|
|
111
|
+
return this.hasMany.getHasMany(key)
|
|
191
112
|
}
|
|
192
113
|
|
|
193
|
-
/**
|
|
194
|
-
* Sets server IDs for a has-many relation.
|
|
195
|
-
*/
|
|
196
114
|
setHasManyServerIds(key: string, serverIds: string[]): void {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (!existing) {
|
|
200
|
-
this.hasManyStates.set(key, {
|
|
201
|
-
serverIds: new Set(serverIds),
|
|
202
|
-
orderedIds: null,
|
|
203
|
-
plannedRemovals: new Map(),
|
|
204
|
-
plannedConnections: new Set(),
|
|
205
|
-
createdEntities: new Set(),
|
|
206
|
-
version: 0,
|
|
207
|
-
})
|
|
208
|
-
} else {
|
|
209
|
-
this.hasManyStates.set(key, {
|
|
210
|
-
...existing,
|
|
211
|
-
serverIds: new Set(serverIds),
|
|
212
|
-
orderedIds: null,
|
|
213
|
-
version: existing.version + 1,
|
|
214
|
-
})
|
|
215
|
-
}
|
|
115
|
+
this.hasMany.setHasManyServerIds(key, serverIds)
|
|
216
116
|
}
|
|
217
117
|
|
|
218
|
-
/**
|
|
219
|
-
* Plans a removal for a has-many item.
|
|
220
|
-
*/
|
|
221
118
|
planHasManyRemoval(key: string, itemId: string, type: HasManyRemovalType): void {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (!existing) {
|
|
225
|
-
this.hasManyStates.set(key, {
|
|
226
|
-
serverIds: new Set(),
|
|
227
|
-
orderedIds: null,
|
|
228
|
-
plannedRemovals: new Map([[itemId, type]]),
|
|
229
|
-
plannedConnections: new Set(),
|
|
230
|
-
createdEntities: new Set(),
|
|
231
|
-
version: 0,
|
|
232
|
-
})
|
|
233
|
-
} else {
|
|
234
|
-
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
235
|
-
newPlannedRemovals.set(itemId, type)
|
|
236
|
-
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
237
|
-
newPlannedConnections.delete(itemId)
|
|
238
|
-
let newOrderedIds = existing.orderedIds
|
|
239
|
-
if (newOrderedIds !== null) {
|
|
240
|
-
newOrderedIds = newOrderedIds.filter(id => id !== itemId)
|
|
241
|
-
}
|
|
242
|
-
const newCreatedEntities = new Set(existing.createdEntities)
|
|
243
|
-
newCreatedEntities.delete(itemId)
|
|
244
|
-
this.hasManyStates.set(key, {
|
|
245
|
-
...existing,
|
|
246
|
-
orderedIds: newOrderedIds,
|
|
247
|
-
plannedRemovals: newPlannedRemovals,
|
|
248
|
-
plannedConnections: newPlannedConnections,
|
|
249
|
-
createdEntities: newCreatedEntities,
|
|
250
|
-
version: existing.version + 1,
|
|
251
|
-
})
|
|
252
|
-
}
|
|
119
|
+
this.hasMany.planHasManyRemoval(key, itemId, type)
|
|
253
120
|
}
|
|
254
121
|
|
|
255
|
-
/**
|
|
256
|
-
* Plans a connection for a has-many item.
|
|
257
|
-
*/
|
|
258
122
|
planHasManyConnection(key: string, itemId: string): void {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (!existing) {
|
|
262
|
-
this.hasManyStates.set(key, {
|
|
263
|
-
serverIds: new Set(),
|
|
264
|
-
orderedIds: null,
|
|
265
|
-
plannedRemovals: new Map(),
|
|
266
|
-
plannedConnections: new Set([itemId]),
|
|
267
|
-
createdEntities: new Set(),
|
|
268
|
-
version: 0,
|
|
269
|
-
})
|
|
270
|
-
} else {
|
|
271
|
-
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
272
|
-
newPlannedConnections.add(itemId)
|
|
273
|
-
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
274
|
-
newPlannedRemovals.delete(itemId)
|
|
275
|
-
let newOrderedIds = existing.orderedIds
|
|
276
|
-
if (newOrderedIds !== null && !newOrderedIds.includes(itemId)) {
|
|
277
|
-
newOrderedIds = [...newOrderedIds, itemId]
|
|
278
|
-
}
|
|
279
|
-
this.hasManyStates.set(key, {
|
|
280
|
-
...existing,
|
|
281
|
-
orderedIds: newOrderedIds,
|
|
282
|
-
plannedConnections: newPlannedConnections,
|
|
283
|
-
plannedRemovals: newPlannedRemovals,
|
|
284
|
-
version: existing.version + 1,
|
|
285
|
-
})
|
|
286
|
-
}
|
|
123
|
+
this.hasMany.planHasManyConnection(key, itemId)
|
|
287
124
|
}
|
|
288
125
|
|
|
289
|
-
/**
|
|
290
|
-
* Commits has-many state after successful persist.
|
|
291
|
-
*/
|
|
292
126
|
commitHasMany(key: string, newServerIds: string[]): void {
|
|
293
|
-
|
|
127
|
+
this.hasMany.commitHasMany(key, newServerIds)
|
|
128
|
+
}
|
|
294
129
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
createdEntities: new Set(),
|
|
301
|
-
version: (existing?.version ?? 0) + 1,
|
|
302
|
-
})
|
|
130
|
+
reconcileSentHasMany(
|
|
131
|
+
key: string,
|
|
132
|
+
delta: SentHasManyDelta,
|
|
133
|
+
): RelationReconciliationResult {
|
|
134
|
+
return this.hasMany.reconcileSentDelta(key, delta)
|
|
303
135
|
}
|
|
304
136
|
|
|
305
|
-
/**
|
|
306
|
-
* Resets has-many state to server state (clears planned operations).
|
|
307
|
-
*/
|
|
308
137
|
resetHasMany(key: string): void {
|
|
309
|
-
|
|
310
|
-
if (!existing) return
|
|
311
|
-
|
|
312
|
-
this.hasManyStates.set(key, {
|
|
313
|
-
serverIds: existing.serverIds,
|
|
314
|
-
orderedIds: null,
|
|
315
|
-
plannedRemovals: new Map(),
|
|
316
|
-
plannedConnections: new Set(),
|
|
317
|
-
createdEntities: new Set(),
|
|
318
|
-
version: existing.version + 1,
|
|
319
|
-
})
|
|
138
|
+
this.hasMany.resetHasMany(key)
|
|
320
139
|
}
|
|
321
140
|
|
|
322
|
-
/**
|
|
323
|
-
* Adds a newly created entity to a has-many relation.
|
|
324
|
-
* Used by HasManyListHandle.add() for inline entity creation.
|
|
325
|
-
*/
|
|
326
141
|
addToHasMany(key: string, itemId: string): void {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (!existing) {
|
|
330
|
-
this.hasManyStates.set(key, {
|
|
331
|
-
serverIds: new Set(),
|
|
332
|
-
orderedIds: [itemId],
|
|
333
|
-
plannedRemovals: new Map(),
|
|
334
|
-
plannedConnections: new Set([itemId]),
|
|
335
|
-
createdEntities: new Set([itemId]),
|
|
336
|
-
version: 0,
|
|
337
|
-
})
|
|
338
|
-
} else {
|
|
339
|
-
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
340
|
-
newPlannedConnections.add(itemId)
|
|
341
|
-
const newCreatedEntities = new Set(existing.createdEntities)
|
|
342
|
-
newCreatedEntities.add(itemId)
|
|
343
|
-
const currentOrderedIds = existing.orderedIds ?? computeDefaultOrderedIds(existing)
|
|
344
|
-
const newOrderedIds = [...currentOrderedIds, itemId]
|
|
345
|
-
this.hasManyStates.set(key, {
|
|
346
|
-
...existing,
|
|
347
|
-
orderedIds: newOrderedIds,
|
|
348
|
-
plannedConnections: newPlannedConnections,
|
|
349
|
-
createdEntities: newCreatedEntities,
|
|
350
|
-
version: existing.version + 1,
|
|
351
|
-
})
|
|
352
|
-
}
|
|
142
|
+
this.hasMany.addToHasMany(key, itemId)
|
|
353
143
|
}
|
|
354
144
|
|
|
355
|
-
/**
|
|
356
|
-
* Connects an existing (persisted) entity to a has-many relation.
|
|
357
|
-
* Unlike addToHasMany, does NOT add to createdEntities — used for
|
|
358
|
-
* materializing embedded connect references to existing entities.
|
|
359
|
-
*/
|
|
360
145
|
connectExistingToHasMany(key: string, itemId: string): void {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (!existing) {
|
|
364
|
-
this.hasManyStates.set(key, {
|
|
365
|
-
serverIds: new Set(),
|
|
366
|
-
orderedIds: [itemId],
|
|
367
|
-
plannedRemovals: new Map(),
|
|
368
|
-
plannedConnections: new Set([itemId]),
|
|
369
|
-
createdEntities: new Set(),
|
|
370
|
-
version: 0,
|
|
371
|
-
})
|
|
372
|
-
} else {
|
|
373
|
-
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
374
|
-
newPlannedConnections.add(itemId)
|
|
375
|
-
const currentOrderedIds = existing.orderedIds ?? computeDefaultOrderedIds(existing)
|
|
376
|
-
const newOrderedIds = [...currentOrderedIds, itemId]
|
|
377
|
-
this.hasManyStates.set(key, {
|
|
378
|
-
...existing,
|
|
379
|
-
orderedIds: newOrderedIds,
|
|
380
|
-
plannedConnections: newPlannedConnections,
|
|
381
|
-
version: existing.version + 1,
|
|
382
|
-
})
|
|
383
|
-
}
|
|
146
|
+
this.hasMany.connectExistingToHasMany(key, itemId)
|
|
384
147
|
}
|
|
385
148
|
|
|
386
|
-
/**
|
|
387
|
-
* Removes an entity from a has-many relation.
|
|
388
|
-
* For newly created entities (via add()), cancels the connection.
|
|
389
|
-
* For existing server entities, plans the specified removal type.
|
|
390
|
-
* Returns true if the state changed (caller should notify), false if it was a no-op.
|
|
391
|
-
*/
|
|
392
149
|
removeFromHasMany(key: string, itemId: string, removalType: HasManyRemovalType): boolean {
|
|
393
|
-
|
|
394
|
-
if (!existing) return false
|
|
395
|
-
|
|
396
|
-
const isCreatedEntity = existing.createdEntities.has(itemId)
|
|
397
|
-
|
|
398
|
-
if (isCreatedEntity) {
|
|
399
|
-
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
400
|
-
newPlannedConnections.delete(itemId)
|
|
401
|
-
const newCreatedEntities = new Set(existing.createdEntities)
|
|
402
|
-
newCreatedEntities.delete(itemId)
|
|
403
|
-
let newOrderedIds = existing.orderedIds
|
|
404
|
-
if (newOrderedIds !== null) {
|
|
405
|
-
newOrderedIds = newOrderedIds.filter(id => id !== itemId)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const newState: StoredHasManyState = {
|
|
409
|
-
...existing,
|
|
410
|
-
orderedIds: newOrderedIds,
|
|
411
|
-
plannedConnections: newPlannedConnections,
|
|
412
|
-
createdEntities: newCreatedEntities,
|
|
413
|
-
version: existing.version + 1,
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
newPlannedConnections.size === 0 &&
|
|
418
|
-
newCreatedEntities.size === 0 &&
|
|
419
|
-
existing.plannedRemovals.size === 0 &&
|
|
420
|
-
newOrderedIds !== null
|
|
421
|
-
) {
|
|
422
|
-
const defaultOrder = computeDefaultOrderedIds(newState)
|
|
423
|
-
if (arraysEqual(newOrderedIds, defaultOrder)) {
|
|
424
|
-
newState.orderedIds = null
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
this.hasManyStates.set(key, newState)
|
|
429
|
-
return true
|
|
430
|
-
} else {
|
|
431
|
-
this.planHasManyRemoval(key, itemId, removalType)
|
|
432
|
-
return true
|
|
433
|
-
}
|
|
150
|
+
return this.hasMany.removeFromHasMany(key, itemId, removalType)
|
|
434
151
|
}
|
|
435
152
|
|
|
153
|
+
moveInHasMany(key: string, fromIndex: number, toIndex: number): void {
|
|
154
|
+
this.hasMany.moveInHasMany(key, fromIndex, toIndex)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getHasManyOrderedIds(key: string): string[] {
|
|
158
|
+
return this.hasMany.getHasManyOrderedIds(key)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ==================== Reachability / Reverse Lookup ====================
|
|
162
|
+
|
|
436
163
|
/**
|
|
437
164
|
* Collects the ids of child entities currently reachable through an entity's
|
|
438
|
-
* LIVE relations (key prefix "parentType:parentId:").
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* Live edges are:
|
|
442
|
-
* - has-one: currentId, when the relation is not disconnected/deleted
|
|
443
|
-
* (a disconnected relation has a null currentId; a 'deleted' relation is
|
|
444
|
-
* removing its target, so the target is not anchored by it).
|
|
445
|
-
* - has-many: effective members = (serverIds ∪ plannedConnections ∪
|
|
446
|
-
* createdEntities) minus plannedRemovals.
|
|
165
|
+
* LIVE relations (key prefix "parentType:parentId:"). Unions the live has-one
|
|
166
|
+
* edges with the live has-many members.
|
|
447
167
|
*/
|
|
448
168
|
getLiveChildIds(keyPrefix: string): string[] {
|
|
449
169
|
const ids = new Set<string>()
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (!key.startsWith(keyPrefix)) continue
|
|
453
|
-
if (state.currentId !== null && state.state !== 'deleted') {
|
|
454
|
-
ids.add(state.currentId)
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
for (const [key, state] of this.hasManyStates) {
|
|
459
|
-
if (!key.startsWith(keyPrefix)) continue
|
|
460
|
-
for (const id of state.serverIds) {
|
|
461
|
-
if (!state.plannedRemovals.has(id)) ids.add(id)
|
|
462
|
-
}
|
|
463
|
-
for (const id of state.plannedConnections) {
|
|
464
|
-
if (!state.plannedRemovals.has(id)) ids.add(id)
|
|
465
|
-
}
|
|
466
|
-
for (const id of state.createdEntities) {
|
|
467
|
-
if (!state.plannedRemovals.has(id)) ids.add(id)
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
170
|
+
this.hasOne.collectLiveChildIds(keyPrefix, ids)
|
|
171
|
+
this.hasMany.collectLiveChildIds(keyPrefix, ids)
|
|
471
172
|
return Array.from(ids)
|
|
472
173
|
}
|
|
473
174
|
|
|
474
175
|
/**
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
176
|
+
* Collects the composite keys ("parentType:parentId") of every entity that
|
|
177
|
+
* currently has a LIVE relation edge pointing at {@link childId} — the reverse
|
|
178
|
+
* of {@link getLiveChildIds}. Unions both sub-stores' results, scanning the
|
|
179
|
+
* same live edges the forward query reads so the two never disagree.
|
|
180
|
+
*
|
|
181
|
+
* {@link childId} is a BARE entity id (no type): relation state records child ids
|
|
182
|
+
* without their type, so the match is by id alone. This deliberately relies on
|
|
183
|
+
* the store-wide invariant that entity ids are globally unique across types
|
|
184
|
+
* (server UUIDs / minted temp ids — the same invariant behind
|
|
185
|
+
* EntitySnapshotStore's idIndex / keyForId), so a bare id resolves to exactly one
|
|
186
|
+
* entity. The forward reachability walk matches on the same bare ids, so making
|
|
187
|
+
* this type-aware in isolation would only diverge the two halves of one query.
|
|
478
188
|
*/
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
for (const key of [...this.hasManyStates.keys()]) {
|
|
486
|
-
if (key.startsWith(keyPrefix)) {
|
|
487
|
-
this.hasManyStates.delete(key)
|
|
488
|
-
}
|
|
489
|
-
}
|
|
189
|
+
getParentKeysForChild(childId: string): Set<string> {
|
|
190
|
+
const parents = new Set<string>()
|
|
191
|
+
this.hasOne.collectParentKeysForChild(childId, parents)
|
|
192
|
+
this.hasMany.collectParentKeysForChild(childId, parents)
|
|
193
|
+
return parents
|
|
490
194
|
}
|
|
491
195
|
|
|
196
|
+
// ==================== Bulk Operations ====================
|
|
197
|
+
|
|
492
198
|
/**
|
|
493
|
-
*
|
|
199
|
+
* Keys of every has-one relation owned by an entity (owner prefix
|
|
200
|
+
* "parentType:parentId:"). Used by the journal's entry-closure to enumerate a
|
|
201
|
+
* detached created entity's own relation cells.
|
|
494
202
|
*/
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const currentOrderedIds = existing.orderedIds ?? computeDefaultOrderedIds(existing)
|
|
500
|
-
|
|
501
|
-
if (fromIndex < 0 || fromIndex >= currentOrderedIds.length) return
|
|
502
|
-
if (toIndex < 0 || toIndex >= currentOrderedIds.length) return
|
|
503
|
-
if (fromIndex === toIndex) return
|
|
504
|
-
|
|
505
|
-
const newOrderedIds = [...currentOrderedIds]
|
|
506
|
-
const movedItem = newOrderedIds.splice(fromIndex, 1)[0]
|
|
507
|
-
if (movedItem === undefined) return
|
|
508
|
-
newOrderedIds.splice(toIndex, 0, movedItem)
|
|
509
|
-
|
|
510
|
-
this.hasManyStates.set(key, {
|
|
511
|
-
...existing,
|
|
512
|
-
orderedIds: newOrderedIds,
|
|
513
|
-
version: existing.version + 1,
|
|
514
|
-
})
|
|
203
|
+
getOwnedRelationKeys(keyPrefix: string): string[] {
|
|
204
|
+
const keys: string[] = []
|
|
205
|
+
this.hasOne.collectOwnedKeys(keyPrefix, keys)
|
|
206
|
+
return keys
|
|
515
207
|
}
|
|
516
208
|
|
|
517
209
|
/**
|
|
518
|
-
*
|
|
210
|
+
* Keys of every has-many list owned by an entity (owner prefix
|
|
211
|
+
* "parentType:parentId:"). Counterpart of {@link getOwnedRelationKeys}.
|
|
519
212
|
*/
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (existing.orderedIds !== null) {
|
|
525
|
-
return existing.orderedIds
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return computeDefaultOrderedIds(existing)
|
|
213
|
+
getOwnedHasManyKeys(keyPrefix: string): string[] {
|
|
214
|
+
const keys: string[] = []
|
|
215
|
+
this.hasMany.collectOwnedKeys(keyPrefix, keys)
|
|
216
|
+
return keys
|
|
529
217
|
}
|
|
530
218
|
|
|
531
219
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
220
|
+
* Removes a single has-one relation state by key (undo restore of an
|
|
221
|
+
* absent-before-the-gesture relation).
|
|
534
222
|
*/
|
|
535
|
-
|
|
536
|
-
this.
|
|
537
|
-
serverIds: new Set(state.serverIds),
|
|
538
|
-
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
539
|
-
plannedRemovals: new Map(state.plannedRemovals),
|
|
540
|
-
plannedConnections: new Set(state.plannedConnections),
|
|
541
|
-
createdEntities: new Set(state.createdEntities),
|
|
542
|
-
version: state.version + 1,
|
|
543
|
-
})
|
|
223
|
+
removeRelationState(key: string): void {
|
|
224
|
+
this.hasOne.removeRelation(key)
|
|
544
225
|
}
|
|
545
226
|
|
|
546
|
-
// ==================== Bulk Operations ====================
|
|
547
|
-
|
|
548
227
|
/**
|
|
549
|
-
*
|
|
228
|
+
* Removes a single has-many list state by key (undo restore of an
|
|
229
|
+
* absent-before-the-gesture list).
|
|
550
230
|
*/
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (key.startsWith(keyPrefix)) {
|
|
554
|
-
this.commitRelation(key)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
for (const [key, state] of this.hasManyStates) {
|
|
559
|
-
if (key.startsWith(keyPrefix)) {
|
|
560
|
-
const newServerIds = new Set(state.serverIds)
|
|
561
|
-
for (const removedId of state.plannedRemovals.keys()) {
|
|
562
|
-
newServerIds.delete(removedId)
|
|
563
|
-
}
|
|
564
|
-
for (const connectedId of state.plannedConnections) {
|
|
565
|
-
newServerIds.add(connectedId)
|
|
566
|
-
}
|
|
567
|
-
this.commitHasMany(key, Array.from(newServerIds))
|
|
568
|
-
}
|
|
569
|
-
}
|
|
231
|
+
removeHasManyState(key: string): void {
|
|
232
|
+
this.hasMany.removeHasMany(key)
|
|
570
233
|
}
|
|
571
234
|
|
|
572
235
|
/**
|
|
573
|
-
*
|
|
236
|
+
* Removes all relation and has-many state owned by an entity (keys under the
|
|
237
|
+
* given owner prefix). Called by removeEntity so a removed entity leaves no
|
|
238
|
+
* stale relation state behind.
|
|
574
239
|
*/
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
this.resetRelation(key)
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
for (const key of this.hasManyStates.keys()) {
|
|
583
|
-
if (key.startsWith(keyPrefix)) {
|
|
584
|
-
this.resetHasMany(key)
|
|
585
|
-
}
|
|
586
|
-
}
|
|
240
|
+
removeOwnedRelations(keyPrefix: string): void {
|
|
241
|
+
this.hasOne.removeOwnedRelations(keyPrefix)
|
|
242
|
+
this.hasMany.removeOwnedRelations(keyPrefix)
|
|
587
243
|
}
|
|
588
244
|
|
|
589
245
|
/**
|
|
590
|
-
*
|
|
246
|
+
* Commits all relations (hasOne and hasMany) for an entity.
|
|
591
247
|
*/
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (key.startsWith(keyPrefix)) {
|
|
596
|
-
result.set(key, { ...state })
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return result
|
|
248
|
+
commitAllRelations(keyPrefix: string, pendingItems?: ReadonlyMap<string, ReadonlySet<string>>): void {
|
|
249
|
+
this.hasOne.commitAllRelations(keyPrefix, pendingItems)
|
|
250
|
+
this.hasMany.commitAllRelations(keyPrefix, pendingItems)
|
|
600
251
|
}
|
|
601
252
|
|
|
602
253
|
/**
|
|
603
|
-
*
|
|
254
|
+
* Resets all relations (hasOne and hasMany) for an entity to server state.
|
|
604
255
|
*/
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (key.startsWith(keyPrefix)) {
|
|
609
|
-
result.set(key, {
|
|
610
|
-
serverIds: new Set(state.serverIds),
|
|
611
|
-
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
612
|
-
plannedRemovals: new Map(state.plannedRemovals),
|
|
613
|
-
plannedConnections: new Set(state.plannedConnections),
|
|
614
|
-
createdEntities: new Set(state.createdEntities),
|
|
615
|
-
version: state.version,
|
|
616
|
-
})
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return result
|
|
256
|
+
resetAllRelations(keyPrefix: string): void {
|
|
257
|
+
this.hasOne.resetAllRelations(keyPrefix)
|
|
258
|
+
this.hasMany.resetAllRelations(keyPrefix)
|
|
620
259
|
}
|
|
621
260
|
|
|
622
261
|
// ==================== Dirty Tracking ====================
|
|
623
262
|
|
|
624
263
|
/**
|
|
625
|
-
* Gets the list of dirty relations for an entity.
|
|
264
|
+
* Gets the list of dirty relations (has-one and has-many) for an entity.
|
|
626
265
|
*/
|
|
627
266
|
getDirtyRelations(keyPrefix: string): string[] {
|
|
628
267
|
const dirtyRelations: string[] = []
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (!key.startsWith(keyPrefix)) continue
|
|
632
|
-
const fieldName = key.slice(keyPrefix.length)
|
|
633
|
-
|
|
634
|
-
if (
|
|
635
|
-
state.currentId !== state.serverId ||
|
|
636
|
-
state.state !== state.serverState ||
|
|
637
|
-
Object.keys(state.placeholderData).length > 0
|
|
638
|
-
) {
|
|
639
|
-
dirtyRelations.push(fieldName)
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
for (const [key, state] of this.hasManyStates) {
|
|
644
|
-
if (!key.startsWith(keyPrefix)) continue
|
|
645
|
-
const fieldName = key.slice(keyPrefix.length)
|
|
646
|
-
|
|
647
|
-
if (state.plannedRemovals.size > 0 || state.plannedConnections.size > 0) {
|
|
648
|
-
dirtyRelations.push(fieldName)
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
268
|
+
this.hasOne.collectDirtyRelations(keyPrefix, dirtyRelations)
|
|
269
|
+
this.hasMany.collectDirtyRelations(keyPrefix, dirtyRelations)
|
|
652
270
|
return dirtyRelations
|
|
653
271
|
}
|
|
654
272
|
|
|
655
273
|
// ==================== Export/Import ====================
|
|
656
274
|
|
|
657
|
-
/**
|
|
658
|
-
* Exports relation states for given keys.
|
|
659
|
-
*/
|
|
660
275
|
exportRelationStates(keys: string[]): Map<string, StoredRelationState> {
|
|
661
|
-
|
|
662
|
-
for (const key of keys) {
|
|
663
|
-
const state = this.relationStates.get(key)
|
|
664
|
-
if (state) {
|
|
665
|
-
result.set(key, {
|
|
666
|
-
...state,
|
|
667
|
-
placeholderData: { ...state.placeholderData },
|
|
668
|
-
})
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return result
|
|
276
|
+
return this.hasOne.exportRelationStates(keys)
|
|
672
277
|
}
|
|
673
278
|
|
|
674
|
-
/**
|
|
675
|
-
* Exports has-many states for given keys.
|
|
676
|
-
*/
|
|
677
279
|
exportHasManyStates(keys: string[]): Map<string, StoredHasManyState> {
|
|
678
|
-
|
|
679
|
-
for (const key of keys) {
|
|
680
|
-
const state = this.hasManyStates.get(key)
|
|
681
|
-
if (state) {
|
|
682
|
-
result.set(key, {
|
|
683
|
-
serverIds: new Set(state.serverIds),
|
|
684
|
-
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
685
|
-
plannedRemovals: new Map(state.plannedRemovals),
|
|
686
|
-
plannedConnections: new Set(state.plannedConnections),
|
|
687
|
-
createdEntities: new Set(state.createdEntities),
|
|
688
|
-
version: state.version,
|
|
689
|
-
})
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
return result
|
|
280
|
+
return this.hasMany.exportHasManyStates(keys)
|
|
693
281
|
}
|
|
694
282
|
|
|
695
|
-
/**
|
|
696
|
-
* Imports relation states from a snapshot.
|
|
697
|
-
* Returns the keys that were imported for notification.
|
|
698
|
-
*/
|
|
699
283
|
importRelationStates(states: Map<string, StoredRelationState>): string[] {
|
|
700
|
-
|
|
701
|
-
for (const [key, state] of states) {
|
|
702
|
-
this.relationStates.set(key, {
|
|
703
|
-
...state,
|
|
704
|
-
placeholderData: { ...state.placeholderData },
|
|
705
|
-
})
|
|
706
|
-
keys.push(key)
|
|
707
|
-
}
|
|
708
|
-
return keys
|
|
284
|
+
return this.hasOne.importRelationStates(states)
|
|
709
285
|
}
|
|
710
286
|
|
|
711
|
-
/**
|
|
712
|
-
* Imports has-many states from a snapshot.
|
|
713
|
-
* Returns the keys that were imported for notification.
|
|
714
|
-
*/
|
|
715
287
|
importHasManyStates(states: Map<string, StoredHasManyState>): string[] {
|
|
716
|
-
|
|
717
|
-
for (const [key, state] of states) {
|
|
718
|
-
this.hasManyStates.set(key, {
|
|
719
|
-
serverIds: new Set(state.serverIds),
|
|
720
|
-
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
721
|
-
plannedRemovals: new Map(state.plannedRemovals),
|
|
722
|
-
plannedConnections: new Set(state.plannedConnections),
|
|
723
|
-
createdEntities: new Set(state.createdEntities),
|
|
724
|
-
version: state.version + 1,
|
|
725
|
-
})
|
|
726
|
-
keys.push(key)
|
|
727
|
-
}
|
|
728
|
-
return keys
|
|
288
|
+
return this.hasMany.importHasManyStates(states)
|
|
729
289
|
}
|
|
730
290
|
|
|
291
|
+
// ==================== Rekey ====================
|
|
292
|
+
|
|
731
293
|
/**
|
|
732
294
|
* Replaces all occurrences of oldId with newId across relation and hasMany states.
|
|
733
295
|
* Used after persist to rekey temp IDs to server-assigned IDs.
|
|
734
296
|
*/
|
|
735
297
|
replaceEntityId(oldId: string, newId: string): void {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Replace in has-many states: serverIds, orderedIds, plannedConnections, createdEntities, plannedRemovals
|
|
751
|
-
for (const [key, state] of this.hasManyStates) {
|
|
752
|
-
let changed = false
|
|
753
|
-
|
|
754
|
-
let serverIds = state.serverIds
|
|
755
|
-
if (serverIds.has(oldId)) {
|
|
756
|
-
serverIds = new Set(serverIds)
|
|
757
|
-
serverIds.delete(oldId)
|
|
758
|
-
serverIds.add(newId)
|
|
759
|
-
changed = true
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
let orderedIds = state.orderedIds
|
|
763
|
-
if (orderedIds) {
|
|
764
|
-
const idx = orderedIds.indexOf(oldId)
|
|
765
|
-
if (idx !== -1) {
|
|
766
|
-
orderedIds = [...orderedIds]
|
|
767
|
-
orderedIds[idx] = newId
|
|
768
|
-
changed = true
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
let plannedConnections = state.plannedConnections
|
|
773
|
-
if (plannedConnections.has(oldId)) {
|
|
774
|
-
plannedConnections = new Set(plannedConnections)
|
|
775
|
-
plannedConnections.delete(oldId)
|
|
776
|
-
plannedConnections.add(newId)
|
|
777
|
-
changed = true
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
let createdEntities = state.createdEntities
|
|
781
|
-
if (createdEntities.has(oldId)) {
|
|
782
|
-
createdEntities = new Set(createdEntities)
|
|
783
|
-
createdEntities.delete(oldId)
|
|
784
|
-
createdEntities.add(newId)
|
|
785
|
-
changed = true
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
let plannedRemovals = state.plannedRemovals
|
|
789
|
-
if (plannedRemovals.has(oldId)) {
|
|
790
|
-
const removalType = plannedRemovals.get(oldId)!
|
|
791
|
-
plannedRemovals = new Map(plannedRemovals)
|
|
792
|
-
plannedRemovals.delete(oldId)
|
|
793
|
-
plannedRemovals.set(newId, removalType)
|
|
794
|
-
changed = true
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
if (changed) {
|
|
798
|
-
this.hasManyStates.set(key, {
|
|
799
|
-
serverIds,
|
|
800
|
-
orderedIds,
|
|
801
|
-
plannedRemovals,
|
|
802
|
-
plannedConnections,
|
|
803
|
-
createdEntities,
|
|
804
|
-
version: state.version + 1,
|
|
805
|
-
})
|
|
806
|
-
}
|
|
807
|
-
}
|
|
298
|
+
this.hasOne.replaceEntityId(oldId, newId)
|
|
299
|
+
this.hasMany.replaceEntityId(oldId, newId)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Migrates an entity's relation state for a temp→persisted rekey: first the
|
|
304
|
+
* relation/has-many keys it owns (the parent id in the key), then every value
|
|
305
|
+
* reference to its id from other entities' relations.
|
|
306
|
+
*/
|
|
307
|
+
rekey(ctx: RekeyContext): void {
|
|
308
|
+
this.rekeyOwner(ctx.oldKeyPrefix, ctx.newKeyPrefix)
|
|
309
|
+
this.replaceEntityId(ctx.oldId, ctx.newId)
|
|
808
310
|
}
|
|
809
311
|
|
|
810
312
|
/**
|
|
811
313
|
* Rekeys relation/hasMany entries owned by an entity (changes the parent ID in the key).
|
|
812
314
|
*/
|
|
813
315
|
rekeyOwner(oldKeyPrefix: string, newKeyPrefix: string): void {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if (key.startsWith(oldKeyPrefix)) {
|
|
817
|
-
toMoveRelations.push([key, value])
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
for (const [oldKey, value] of toMoveRelations) {
|
|
821
|
-
this.relationStates.delete(oldKey)
|
|
822
|
-
this.relationStates.set(newKeyPrefix + oldKey.slice(oldKeyPrefix.length), value)
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const toMoveHasMany: [string, StoredHasManyState][] = []
|
|
826
|
-
for (const [key, value] of this.hasManyStates) {
|
|
827
|
-
if (key.startsWith(oldKeyPrefix)) {
|
|
828
|
-
toMoveHasMany.push([key, value])
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
for (const [oldKey, value] of toMoveHasMany) {
|
|
832
|
-
this.hasManyStates.delete(oldKey)
|
|
833
|
-
this.hasManyStates.set(newKeyPrefix + oldKey.slice(oldKeyPrefix.length), value)
|
|
834
|
-
}
|
|
316
|
+
this.hasOne.rekeyOwner(oldKeyPrefix, newKeyPrefix)
|
|
317
|
+
this.hasMany.rekeyOwner(oldKeyPrefix, newKeyPrefix)
|
|
835
318
|
}
|
|
836
319
|
|
|
837
320
|
/**
|
|
838
321
|
* Clears all relation data.
|
|
839
322
|
*/
|
|
840
323
|
clear(): void {
|
|
841
|
-
this.
|
|
842
|
-
this.
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// ==================== Helper Functions ====================
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Computes the default ordered IDs for a has-many relation.
|
|
850
|
-
* Order is: serverIds (minus removals) + plannedConnections
|
|
851
|
-
*/
|
|
852
|
-
export function computeDefaultOrderedIds(state: StoredHasManyState): string[] {
|
|
853
|
-
const result: string[] = []
|
|
854
|
-
|
|
855
|
-
for (const id of state.serverIds) {
|
|
856
|
-
if (!state.plannedRemovals.has(id)) {
|
|
857
|
-
result.push(id)
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
for (const id of state.plannedConnections) {
|
|
862
|
-
if (!result.includes(id)) {
|
|
863
|
-
result.push(id)
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return result
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function arraysEqual(a: string[], b: string[]): boolean {
|
|
871
|
-
if (a.length !== b.length) return false
|
|
872
|
-
for (let i = 0; i < a.length; i++) {
|
|
873
|
-
if (a[i] !== b[i]) return false
|
|
324
|
+
this.hasOne.clear()
|
|
325
|
+
this.hasMany.clear()
|
|
874
326
|
}
|
|
875
|
-
return true
|
|
876
327
|
}
|