@contember/bindx 0.1.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/dist/adapter/ContemberAdapter.d.ts +40 -0
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
- package/dist/adapter/MockAdapter.d.ts +81 -0
- package/dist/adapter/MockAdapter.d.ts.map +1 -0
- package/dist/adapter/MockQueryEngine.d.ts +43 -0
- package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +5 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +184 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/brand/ComponentBrand.d.ts +25 -0
- package/dist/brand/ComponentBrand.d.ts.map +1 -0
- package/dist/contember/index.d.ts +10 -0
- package/dist/contember/index.d.ts.map +1 -0
- package/dist/core/ActionDispatcher.d.ts +64 -0
- package/dist/core/ActionDispatcher.d.ts.map +1 -0
- package/dist/core/Disposable.d.ts +57 -0
- package/dist/core/Disposable.d.ts.map +1 -0
- package/dist/core/EntityLoader.d.ts +80 -0
- package/dist/core/EntityLoader.d.ts.map +1 -0
- package/dist/core/PersistenceManager.d.ts +126 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -0
- package/dist/core/SelectionResolver.d.ts +21 -0
- package/dist/core/SelectionResolver.d.ts.map +1 -0
- package/dist/core/actionClassification.d.ts +48 -0
- package/dist/core/actionClassification.d.ts.map +1 -0
- package/dist/core/actions.d.ts +301 -0
- package/dist/core/actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/dataview/filterHandlers.d.ts +18 -0
- package/dist/dataview/filterHandlers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +3 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/types.d.ts +164 -0
- package/dist/dataview/types.d.ts.map +1 -0
- package/dist/errors/UnfetchedFieldError.d.ts +11 -0
- package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
- package/dist/errors/errorPathResolver.d.ts +52 -0
- package/dist/errors/errorPathResolver.d.ts.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/pathMapper.d.ts +42 -0
- package/dist/errors/pathMapper.d.ts.map +1 -0
- package/dist/errors/types.d.ts +131 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/events/EventEmitter.d.ts +97 -0
- package/dist/events/EventEmitter.d.ts.map +1 -0
- package/dist/events/eventFactory.d.ts +40 -0
- package/dist/events/eventFactory.d.ts.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/types.d.ts +264 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/handles/BaseHandle.d.ts +64 -0
- package/dist/handles/BaseHandle.d.ts.map +1 -0
- package/dist/handles/EntityHandle.d.ts +208 -0
- package/dist/handles/EntityHandle.d.ts.map +1 -0
- package/dist/handles/FieldHandle.d.ts +111 -0
- package/dist/handles/FieldHandle.d.ts.map +1 -0
- package/dist/handles/HasManyListHandle.d.ts +166 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -0
- package/dist/handles/HasOneHandle.d.ts +168 -0
- package/dist/handles/HasOneHandle.d.ts.map +1 -0
- package/dist/handles/PlaceholderHandle.d.ts +117 -0
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
- package/dist/handles/index.d.ts +8 -0
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/proxyFactory.d.ts +66 -0
- package/dist/handles/proxyFactory.d.ts.map +1 -0
- package/dist/handles/types.d.ts +430 -0
- package/dist/handles/types.d.ts.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/persistence/BatchPersister.d.ts +142 -0
- package/dist/persistence/BatchPersister.d.ts.map +1 -0
- package/dist/persistence/ChangeRegistry.d.ts +87 -0
- package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
- package/dist/persistence/MutationCollector.d.ts +96 -0
- package/dist/persistence/MutationCollector.d.ts.map +1 -0
- package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
- package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/types.d.ts +189 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
- package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
- package/dist/roles/index.d.ts +11 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/types.d.ts +242 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/schema/ContemberSchema.d.ts +98 -0
- package/dist/schema/ContemberSchema.d.ts.map +1 -0
- package/dist/schema/SchemaLoader.d.ts +28 -0
- package/dist/schema/SchemaLoader.d.ts.map +1 -0
- package/dist/schema/SchemaRegistry.d.ts +95 -0
- package/dist/schema/SchemaRegistry.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/roles.d.ts +62 -0
- package/dist/schema/roles.d.ts.map +1 -0
- package/dist/schema/types.d.ts +179 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/selection/SelectionMetaCollector.d.ts +32 -0
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
- package/dist/selection/SelectionScope.d.ts +77 -0
- package/dist/selection/SelectionScope.d.ts.map +1 -0
- package/dist/selection/buildQuery.d.ts +45 -0
- package/dist/selection/buildQuery.d.ts.map +1 -0
- package/dist/selection/createFragment.d.ts +29 -0
- package/dist/selection/createFragment.d.ts.map +1 -0
- package/dist/selection/createSelectionBuilder.d.ts +18 -0
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
- package/dist/selection/index.d.ts +8 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/queryTypes.d.ts +84 -0
- package/dist/selection/queryTypes.d.ts.map +1 -0
- package/dist/selection/types.d.ts +238 -0
- package/dist/selection/types.d.ts.map +1 -0
- package/dist/store/DirtyTracker.d.ts +26 -0
- package/dist/store/DirtyTracker.d.ts.map +1 -0
- package/dist/store/EntityMetaStore.d.ts +52 -0
- package/dist/store/EntityMetaStore.d.ts.map +1 -0
- package/dist/store/EntitySnapshotStore.d.ts +63 -0
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
- package/dist/store/ErrorStore.d.ts +82 -0
- package/dist/store/ErrorStore.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +175 -0
- package/dist/store/RelationStore.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +143 -0
- package/dist/store/SnapshotStore.d.ts.map +1 -0
- package/dist/store/SubscriptionManager.d.ts +90 -0
- package/dist/store/SubscriptionManager.d.ts.map +1 -0
- package/dist/store/TouchedStore.d.ts +24 -0
- package/dist/store/TouchedStore.d.ts.map +1 -0
- package/dist/store/entityId.d.ts +29 -0
- package/dist/store/entityId.d.ts.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/snapshots.d.ts +144 -0
- package/dist/store/snapshots.d.ts.map +1 -0
- package/dist/undo/UndoManager.d.ts +120 -0
- package/dist/undo/UndoManager.d.ts.map +1 -0
- package/dist/undo/index.d.ts +3 -0
- package/dist/undo/index.d.ts.map +1 -0
- package/dist/undo/types.d.ts +74 -0
- package/dist/undo/types.d.ts.map +1 -0
- package/dist/utils/aliasGenerator.d.ts +25 -0
- package/dist/utils/aliasGenerator.d.ts.map +1 -0
- package/dist/utils/deepEqual.d.ts +6 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +29 -0
- package/src/adapter/ContemberAdapter.ts +239 -0
- package/src/adapter/MockAdapter.ts +394 -0
- package/src/adapter/MockQueryEngine.ts +236 -0
- package/src/adapter/index.ts +21 -0
- package/src/adapter/types.ts +199 -0
- package/src/brand/ComponentBrand.ts +28 -0
- package/src/contember/index.ts +17 -0
- package/src/core/ActionDispatcher.ts +450 -0
- package/src/core/Disposable.ts +126 -0
- package/src/core/EntityLoader.ts +169 -0
- package/src/core/SelectionResolver.ts +78 -0
- package/src/core/actionClassification.ts +196 -0
- package/src/core/actions.ts +491 -0
- package/src/core/index.ts +20 -0
- package/src/dataview/filterHandlers.ts +443 -0
- package/src/dataview/index.ts +36 -0
- package/src/dataview/types.ts +220 -0
- package/src/errors/UnfetchedFieldError.ts +18 -0
- package/src/errors/errorPathResolver.ts +239 -0
- package/src/errors/index.ts +40 -0
- package/src/errors/pathMapper.ts +40 -0
- package/src/errors/types.ts +223 -0
- package/src/events/EventEmitter.ts +447 -0
- package/src/events/eventFactory.ts +430 -0
- package/src/events/index.ts +89 -0
- package/src/events/types.ts +374 -0
- package/src/handles/BaseHandle.ts +100 -0
- package/src/handles/EntityHandle.ts +538 -0
- package/src/handles/FieldHandle.ts +268 -0
- package/src/handles/HasManyListHandle.ts +500 -0
- package/src/handles/HasOneHandle.ts +500 -0
- package/src/handles/PlaceholderHandle.ts +321 -0
- package/src/handles/index.ts +37 -0
- package/src/handles/proxyFactory.ts +218 -0
- package/src/handles/types.ts +743 -0
- package/src/index.ts +384 -0
- package/src/internal.ts +19 -0
- package/src/persistence/BatchPersister.ts +911 -0
- package/src/persistence/ChangeRegistry.ts +150 -0
- package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
- package/src/persistence/MutationCollector.ts +567 -0
- package/src/persistence/MutationSchemaProvider.ts +33 -0
- package/src/persistence/index.ts +23 -0
- package/src/persistence/types.ts +220 -0
- package/src/schema/ContemberSchema.ts +132 -0
- package/src/schema/SchemaLoader.ts +93 -0
- package/src/schema/SchemaRegistry.ts +286 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/roles.ts +83 -0
- package/src/schema/types.ts +238 -0
- package/src/selection/SelectionMetaCollector.ts +122 -0
- package/src/selection/SelectionScope.ts +208 -0
- package/src/selection/buildQuery.ts +134 -0
- package/src/selection/createFragment.ts +50 -0
- package/src/selection/createSelectionBuilder.ts +283 -0
- package/src/selection/index.ts +31 -0
- package/src/selection/queryTypes.ts +135 -0
- package/src/selection/types.ts +320 -0
- package/src/store/DirtyTracker.ts +117 -0
- package/src/store/EntityMetaStore.ts +143 -0
- package/src/store/EntitySnapshotStore.ts +260 -0
- package/src/store/ErrorStore.ts +251 -0
- package/src/store/RelationStore.ts +696 -0
- package/src/store/SnapshotStore.ts +765 -0
- package/src/store/SubscriptionManager.ts +241 -0
- package/src/store/TouchedStore.ts +42 -0
- package/src/store/entityId.ts +43 -0
- package/src/store/index.ts +2 -0
- package/src/store/snapshots.ts +203 -0
- package/src/undo/UndoManager.ts +418 -0
- package/src/undo/index.ts +9 -0
- package/src/undo/types.ts +84 -0
- package/src/utils/aliasGenerator.ts +56 -0
- package/src/utils/deepEqual.ts +33 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import type { HasOneRelationState } from '../handles/types.js'
|
|
2
|
+
import type { EntitySnapshot } from './snapshots.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Removal type for has-many items
|
|
6
|
+
*/
|
|
7
|
+
export type HasManyRemovalType = 'disconnect' | 'delete'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Has-many list state stored in SnapshotStore
|
|
11
|
+
*/
|
|
12
|
+
export interface StoredHasManyState {
|
|
13
|
+
/** IDs of items from server */
|
|
14
|
+
serverIds: Set<string>
|
|
15
|
+
/** Explicit ordered list of item IDs, null means use default order (serverIds + plannedConnections) */
|
|
16
|
+
orderedIds: string[] | null
|
|
17
|
+
/** Planned removals (disconnect or delete) keyed by entity ID */
|
|
18
|
+
plannedRemovals: Map<string, HasManyRemovalType>
|
|
19
|
+
/** Planned connections (IDs to add to the list) */
|
|
20
|
+
plannedConnections: Set<string>
|
|
21
|
+
/** Entity IDs created via add() - tracked for proper remove() semantics and mutation generation */
|
|
22
|
+
createdEntities: Set<string>
|
|
23
|
+
version: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Relation state stored in SnapshotStore
|
|
28
|
+
*/
|
|
29
|
+
export interface StoredRelationState {
|
|
30
|
+
currentId: string | null
|
|
31
|
+
serverId: string | null
|
|
32
|
+
state: HasOneRelationState
|
|
33
|
+
serverState: HasOneRelationState
|
|
34
|
+
placeholderData: Record<string, unknown>
|
|
35
|
+
version: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Manages has-one and has-many relation state.
|
|
40
|
+
*
|
|
41
|
+
* Relation keys use the format "parentType:parentId:fieldName".
|
|
42
|
+
* Notification is handled by callers via callback returns.
|
|
43
|
+
*/
|
|
44
|
+
export class RelationStore {
|
|
45
|
+
/** Relation states keyed by "parentType:parentId:fieldName" */
|
|
46
|
+
private readonly relationStates = new Map<string, StoredRelationState>()
|
|
47
|
+
|
|
48
|
+
/** Has-many list states keyed by "parentType:parentId:fieldName" */
|
|
49
|
+
private readonly hasManyStates = new Map<string, StoredHasManyState>()
|
|
50
|
+
|
|
51
|
+
// ==================== Has-One Relations ====================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Gets or creates relation state.
|
|
55
|
+
*/
|
|
56
|
+
getOrCreateRelation(
|
|
57
|
+
key: string,
|
|
58
|
+
initial: Omit<StoredRelationState, 'version'>,
|
|
59
|
+
): StoredRelationState {
|
|
60
|
+
if (!this.relationStates.has(key)) {
|
|
61
|
+
this.relationStates.set(key, { ...initial, version: 0 })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return this.relationStates.get(key)!
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets relation state.
|
|
69
|
+
*/
|
|
70
|
+
getRelation(key: string): StoredRelationState | undefined {
|
|
71
|
+
return this.relationStates.get(key)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Updates relation state.
|
|
76
|
+
* If the relation state doesn't exist, creates it using entity snapshot for server data.
|
|
77
|
+
*/
|
|
78
|
+
setRelation(
|
|
79
|
+
key: string,
|
|
80
|
+
updates: Partial<Omit<StoredRelationState, 'version'>>,
|
|
81
|
+
entitySnapshot: EntitySnapshot | undefined,
|
|
82
|
+
fieldName: string,
|
|
83
|
+
): void {
|
|
84
|
+
const existing = this.relationStates.get(key)
|
|
85
|
+
|
|
86
|
+
if (!existing) {
|
|
87
|
+
let serverId: string | null = null
|
|
88
|
+
let serverState: HasOneRelationState = 'disconnected'
|
|
89
|
+
|
|
90
|
+
if (entitySnapshot?.serverData) {
|
|
91
|
+
const relatedData = (entitySnapshot.serverData as Record<string, unknown>)[fieldName]
|
|
92
|
+
if (relatedData && typeof relatedData === 'object' && 'id' in relatedData) {
|
|
93
|
+
serverId = (relatedData as { id: string }).id
|
|
94
|
+
serverState = 'connected'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.relationStates.set(key, {
|
|
99
|
+
currentId: 'currentId' in updates ? updates.currentId! : serverId,
|
|
100
|
+
serverId,
|
|
101
|
+
state: 'state' in updates ? updates.state! : serverState,
|
|
102
|
+
serverState,
|
|
103
|
+
placeholderData: updates.placeholderData ?? {},
|
|
104
|
+
version: 0,
|
|
105
|
+
})
|
|
106
|
+
} else {
|
|
107
|
+
this.relationStates.set(key, {
|
|
108
|
+
...existing,
|
|
109
|
+
...updates,
|
|
110
|
+
version: existing.version + 1,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Commits relation state (server = current).
|
|
117
|
+
*/
|
|
118
|
+
commitRelation(key: string): void {
|
|
119
|
+
const existing = this.relationStates.get(key)
|
|
120
|
+
if (!existing) return
|
|
121
|
+
|
|
122
|
+
this.relationStates.set(key, {
|
|
123
|
+
...existing,
|
|
124
|
+
serverId: existing.currentId,
|
|
125
|
+
serverState: existing.state === 'creating' ? 'connected' : existing.state,
|
|
126
|
+
placeholderData: {},
|
|
127
|
+
version: existing.version + 1,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resets relation to server state.
|
|
133
|
+
*/
|
|
134
|
+
resetRelation(key: string): void {
|
|
135
|
+
const existing = this.relationStates.get(key)
|
|
136
|
+
if (!existing) return
|
|
137
|
+
|
|
138
|
+
this.relationStates.set(key, {
|
|
139
|
+
...existing,
|
|
140
|
+
currentId: existing.serverId,
|
|
141
|
+
state: existing.serverState,
|
|
142
|
+
placeholderData: {},
|
|
143
|
+
version: existing.version + 1,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ==================== Has-Many Relations ====================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Gets or creates has-many list state.
|
|
151
|
+
*/
|
|
152
|
+
getOrCreateHasMany(key: string, serverIds?: string[]): StoredHasManyState {
|
|
153
|
+
if (!this.hasManyStates.has(key)) {
|
|
154
|
+
this.hasManyStates.set(key, {
|
|
155
|
+
serverIds: new Set(serverIds ?? []),
|
|
156
|
+
orderedIds: null,
|
|
157
|
+
plannedRemovals: new Map(),
|
|
158
|
+
plannedConnections: new Set(),
|
|
159
|
+
createdEntities: new Set(),
|
|
160
|
+
version: 0,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return this.hasManyStates.get(key)!
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Gets has-many list state.
|
|
169
|
+
*/
|
|
170
|
+
getHasMany(key: string): StoredHasManyState | undefined {
|
|
171
|
+
return this.hasManyStates.get(key)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Sets server IDs for a has-many relation.
|
|
176
|
+
*/
|
|
177
|
+
setHasManyServerIds(key: string, serverIds: string[]): void {
|
|
178
|
+
const existing = this.hasManyStates.get(key)
|
|
179
|
+
|
|
180
|
+
if (!existing) {
|
|
181
|
+
this.hasManyStates.set(key, {
|
|
182
|
+
serverIds: new Set(serverIds),
|
|
183
|
+
orderedIds: null,
|
|
184
|
+
plannedRemovals: new Map(),
|
|
185
|
+
plannedConnections: new Set(),
|
|
186
|
+
createdEntities: new Set(),
|
|
187
|
+
version: 0,
|
|
188
|
+
})
|
|
189
|
+
} else {
|
|
190
|
+
this.hasManyStates.set(key, {
|
|
191
|
+
...existing,
|
|
192
|
+
serverIds: new Set(serverIds),
|
|
193
|
+
orderedIds: null,
|
|
194
|
+
version: existing.version + 1,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Plans a removal for a has-many item.
|
|
201
|
+
*/
|
|
202
|
+
planHasManyRemoval(key: string, itemId: string, type: HasManyRemovalType): void {
|
|
203
|
+
const existing = this.hasManyStates.get(key)
|
|
204
|
+
|
|
205
|
+
if (!existing) {
|
|
206
|
+
this.hasManyStates.set(key, {
|
|
207
|
+
serverIds: new Set(),
|
|
208
|
+
orderedIds: null,
|
|
209
|
+
plannedRemovals: new Map([[itemId, type]]),
|
|
210
|
+
plannedConnections: new Set(),
|
|
211
|
+
createdEntities: new Set(),
|
|
212
|
+
version: 0,
|
|
213
|
+
})
|
|
214
|
+
} else {
|
|
215
|
+
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
216
|
+
newPlannedRemovals.set(itemId, type)
|
|
217
|
+
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
218
|
+
newPlannedConnections.delete(itemId)
|
|
219
|
+
let newOrderedIds = existing.orderedIds
|
|
220
|
+
if (newOrderedIds !== null) {
|
|
221
|
+
newOrderedIds = newOrderedIds.filter(id => id !== itemId)
|
|
222
|
+
}
|
|
223
|
+
const newCreatedEntities = new Set(existing.createdEntities)
|
|
224
|
+
newCreatedEntities.delete(itemId)
|
|
225
|
+
this.hasManyStates.set(key, {
|
|
226
|
+
...existing,
|
|
227
|
+
orderedIds: newOrderedIds,
|
|
228
|
+
plannedRemovals: newPlannedRemovals,
|
|
229
|
+
plannedConnections: newPlannedConnections,
|
|
230
|
+
createdEntities: newCreatedEntities,
|
|
231
|
+
version: existing.version + 1,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Cancels a planned removal for a has-many item.
|
|
238
|
+
*/
|
|
239
|
+
cancelHasManyRemoval(key: string, itemId: string): void {
|
|
240
|
+
const existing = this.hasManyStates.get(key)
|
|
241
|
+
if (!existing) return
|
|
242
|
+
|
|
243
|
+
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
244
|
+
newPlannedRemovals.delete(itemId)
|
|
245
|
+
this.hasManyStates.set(key, {
|
|
246
|
+
...existing,
|
|
247
|
+
plannedRemovals: newPlannedRemovals,
|
|
248
|
+
version: existing.version + 1,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Plans a connection for a has-many item.
|
|
254
|
+
*/
|
|
255
|
+
planHasManyConnection(key: string, itemId: string): void {
|
|
256
|
+
const existing = this.hasManyStates.get(key)
|
|
257
|
+
|
|
258
|
+
if (!existing) {
|
|
259
|
+
this.hasManyStates.set(key, {
|
|
260
|
+
serverIds: new Set(),
|
|
261
|
+
orderedIds: null,
|
|
262
|
+
plannedRemovals: new Map(),
|
|
263
|
+
plannedConnections: new Set([itemId]),
|
|
264
|
+
createdEntities: new Set(),
|
|
265
|
+
version: 0,
|
|
266
|
+
})
|
|
267
|
+
} else {
|
|
268
|
+
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
269
|
+
newPlannedConnections.add(itemId)
|
|
270
|
+
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
271
|
+
newPlannedRemovals.delete(itemId)
|
|
272
|
+
let newOrderedIds = existing.orderedIds
|
|
273
|
+
if (newOrderedIds !== null && !newOrderedIds.includes(itemId)) {
|
|
274
|
+
newOrderedIds = [...newOrderedIds, itemId]
|
|
275
|
+
}
|
|
276
|
+
this.hasManyStates.set(key, {
|
|
277
|
+
...existing,
|
|
278
|
+
orderedIds: newOrderedIds,
|
|
279
|
+
plannedConnections: newPlannedConnections,
|
|
280
|
+
plannedRemovals: newPlannedRemovals,
|
|
281
|
+
version: existing.version + 1,
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Cancels a planned connection for a has-many item.
|
|
288
|
+
*/
|
|
289
|
+
cancelHasManyConnection(key: string, itemId: string): void {
|
|
290
|
+
const existing = this.hasManyStates.get(key)
|
|
291
|
+
if (!existing) return
|
|
292
|
+
|
|
293
|
+
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
294
|
+
newPlannedConnections.delete(itemId)
|
|
295
|
+
this.hasManyStates.set(key, {
|
|
296
|
+
...existing,
|
|
297
|
+
plannedConnections: newPlannedConnections,
|
|
298
|
+
version: existing.version + 1,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Commits has-many state after successful persist.
|
|
304
|
+
*/
|
|
305
|
+
commitHasMany(key: string, newServerIds: string[]): void {
|
|
306
|
+
const existing = this.hasManyStates.get(key)
|
|
307
|
+
|
|
308
|
+
this.hasManyStates.set(key, {
|
|
309
|
+
serverIds: new Set(newServerIds),
|
|
310
|
+
orderedIds: null,
|
|
311
|
+
plannedRemovals: new Map(),
|
|
312
|
+
plannedConnections: new Set(),
|
|
313
|
+
createdEntities: new Set(),
|
|
314
|
+
version: (existing?.version ?? 0) + 1,
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Resets has-many state to server state (clears planned operations).
|
|
320
|
+
*/
|
|
321
|
+
resetHasMany(key: string): void {
|
|
322
|
+
const existing = this.hasManyStates.get(key)
|
|
323
|
+
if (!existing) return
|
|
324
|
+
|
|
325
|
+
this.hasManyStates.set(key, {
|
|
326
|
+
serverIds: existing.serverIds,
|
|
327
|
+
orderedIds: null,
|
|
328
|
+
plannedRemovals: new Map(),
|
|
329
|
+
plannedConnections: new Set(),
|
|
330
|
+
createdEntities: new Set(),
|
|
331
|
+
version: existing.version + 1,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Adds a newly created entity to a has-many relation.
|
|
337
|
+
* Used by HasManyListHandle.add() for inline entity creation.
|
|
338
|
+
*/
|
|
339
|
+
addToHasMany(key: string, itemId: string): void {
|
|
340
|
+
const existing = this.hasManyStates.get(key)
|
|
341
|
+
|
|
342
|
+
if (!existing) {
|
|
343
|
+
this.hasManyStates.set(key, {
|
|
344
|
+
serverIds: new Set(),
|
|
345
|
+
orderedIds: [itemId],
|
|
346
|
+
plannedRemovals: new Map(),
|
|
347
|
+
plannedConnections: new Set([itemId]),
|
|
348
|
+
createdEntities: new Set([itemId]),
|
|
349
|
+
version: 0,
|
|
350
|
+
})
|
|
351
|
+
} else {
|
|
352
|
+
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
353
|
+
newPlannedConnections.add(itemId)
|
|
354
|
+
const newCreatedEntities = new Set(existing.createdEntities)
|
|
355
|
+
newCreatedEntities.add(itemId)
|
|
356
|
+
const currentOrderedIds = existing.orderedIds ?? computeDefaultOrderedIds(existing)
|
|
357
|
+
const newOrderedIds = [...currentOrderedIds, itemId]
|
|
358
|
+
this.hasManyStates.set(key, {
|
|
359
|
+
...existing,
|
|
360
|
+
orderedIds: newOrderedIds,
|
|
361
|
+
plannedConnections: newPlannedConnections,
|
|
362
|
+
createdEntities: newCreatedEntities,
|
|
363
|
+
version: existing.version + 1,
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Removes an entity from a has-many relation.
|
|
370
|
+
* For newly created entities (via add()), cancels the connection.
|
|
371
|
+
* For existing server entities, plans a disconnect.
|
|
372
|
+
* Returns the key if a removal was planned (caller should notify), or null if handled inline.
|
|
373
|
+
*/
|
|
374
|
+
removeFromHasMany(key: string, itemId: string): 'planned_removal' | 'cancelled_connection' | null {
|
|
375
|
+
const existing = this.hasManyStates.get(key)
|
|
376
|
+
if (!existing) return null
|
|
377
|
+
|
|
378
|
+
const isCreatedEntity = existing.createdEntities.has(itemId)
|
|
379
|
+
|
|
380
|
+
if (isCreatedEntity) {
|
|
381
|
+
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
382
|
+
newPlannedConnections.delete(itemId)
|
|
383
|
+
const newCreatedEntities = new Set(existing.createdEntities)
|
|
384
|
+
newCreatedEntities.delete(itemId)
|
|
385
|
+
let newOrderedIds = existing.orderedIds
|
|
386
|
+
if (newOrderedIds !== null) {
|
|
387
|
+
newOrderedIds = newOrderedIds.filter(id => id !== itemId)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const newState: StoredHasManyState = {
|
|
391
|
+
...existing,
|
|
392
|
+
orderedIds: newOrderedIds,
|
|
393
|
+
plannedConnections: newPlannedConnections,
|
|
394
|
+
createdEntities: newCreatedEntities,
|
|
395
|
+
version: existing.version + 1,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (
|
|
399
|
+
newPlannedConnections.size === 0 &&
|
|
400
|
+
newCreatedEntities.size === 0 &&
|
|
401
|
+
existing.plannedRemovals.size === 0 &&
|
|
402
|
+
newOrderedIds !== null
|
|
403
|
+
) {
|
|
404
|
+
const defaultOrder = computeDefaultOrderedIds(newState)
|
|
405
|
+
if (arraysEqual(newOrderedIds, defaultOrder)) {
|
|
406
|
+
newState.orderedIds = null
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.hasManyStates.set(key, newState)
|
|
411
|
+
return 'cancelled_connection'
|
|
412
|
+
} else {
|
|
413
|
+
this.planHasManyRemoval(key, itemId, 'disconnect')
|
|
414
|
+
return 'planned_removal'
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Moves an item within a has-many relation from one index to another.
|
|
420
|
+
*/
|
|
421
|
+
moveInHasMany(key: string, fromIndex: number, toIndex: number): void {
|
|
422
|
+
const existing = this.hasManyStates.get(key)
|
|
423
|
+
if (!existing) return
|
|
424
|
+
|
|
425
|
+
const currentOrderedIds = existing.orderedIds ?? computeDefaultOrderedIds(existing)
|
|
426
|
+
|
|
427
|
+
if (fromIndex < 0 || fromIndex >= currentOrderedIds.length) return
|
|
428
|
+
if (toIndex < 0 || toIndex >= currentOrderedIds.length) return
|
|
429
|
+
if (fromIndex === toIndex) return
|
|
430
|
+
|
|
431
|
+
const newOrderedIds = [...currentOrderedIds]
|
|
432
|
+
const movedItem = newOrderedIds.splice(fromIndex, 1)[0]
|
|
433
|
+
if (movedItem === undefined) return
|
|
434
|
+
newOrderedIds.splice(toIndex, 0, movedItem)
|
|
435
|
+
|
|
436
|
+
this.hasManyStates.set(key, {
|
|
437
|
+
...existing,
|
|
438
|
+
orderedIds: newOrderedIds,
|
|
439
|
+
version: existing.version + 1,
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Gets the ordered list of item IDs for a has-many relation.
|
|
445
|
+
*/
|
|
446
|
+
getHasManyOrderedIds(key: string): string[] {
|
|
447
|
+
const existing = this.hasManyStates.get(key)
|
|
448
|
+
if (!existing) return []
|
|
449
|
+
|
|
450
|
+
if (existing.orderedIds !== null) {
|
|
451
|
+
return existing.orderedIds
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return computeDefaultOrderedIds(existing)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Restores a has-many state from a captured snapshot.
|
|
459
|
+
* Used in pessimistic mode after successful server confirmation.
|
|
460
|
+
*/
|
|
461
|
+
restoreHasManyState(key: string, state: StoredHasManyState): void {
|
|
462
|
+
this.hasManyStates.set(key, {
|
|
463
|
+
serverIds: new Set(state.serverIds),
|
|
464
|
+
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
465
|
+
plannedRemovals: new Map(state.plannedRemovals),
|
|
466
|
+
plannedConnections: new Set(state.plannedConnections),
|
|
467
|
+
createdEntities: new Set(state.createdEntities),
|
|
468
|
+
version: state.version + 1,
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ==================== Bulk Operations ====================
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Commits all relations (hasOne and hasMany) for an entity.
|
|
476
|
+
*/
|
|
477
|
+
commitAllRelations(keyPrefix: string): void {
|
|
478
|
+
for (const key of this.relationStates.keys()) {
|
|
479
|
+
if (key.startsWith(keyPrefix)) {
|
|
480
|
+
this.commitRelation(key)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
for (const [key, state] of this.hasManyStates) {
|
|
485
|
+
if (key.startsWith(keyPrefix)) {
|
|
486
|
+
const newServerIds = new Set(state.serverIds)
|
|
487
|
+
for (const removedId of state.plannedRemovals.keys()) {
|
|
488
|
+
newServerIds.delete(removedId)
|
|
489
|
+
}
|
|
490
|
+
for (const connectedId of state.plannedConnections) {
|
|
491
|
+
newServerIds.add(connectedId)
|
|
492
|
+
}
|
|
493
|
+
this.commitHasMany(key, Array.from(newServerIds))
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Resets all relations (hasOne and hasMany) for an entity to server state.
|
|
500
|
+
*/
|
|
501
|
+
resetAllRelations(keyPrefix: string): void {
|
|
502
|
+
for (const key of this.relationStates.keys()) {
|
|
503
|
+
if (key.startsWith(keyPrefix)) {
|
|
504
|
+
this.resetRelation(key)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
for (const key of this.hasManyStates.keys()) {
|
|
509
|
+
if (key.startsWith(keyPrefix)) {
|
|
510
|
+
this.resetHasMany(key)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Gets all relation states for an entity.
|
|
517
|
+
*/
|
|
518
|
+
getAllRelationsForEntity(keyPrefix: string): Map<string, StoredRelationState> {
|
|
519
|
+
const result = new Map<string, StoredRelationState>()
|
|
520
|
+
for (const [key, state] of this.relationStates) {
|
|
521
|
+
if (key.startsWith(keyPrefix)) {
|
|
522
|
+
result.set(key, { ...state })
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return result
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Gets all has-many states for an entity.
|
|
530
|
+
*/
|
|
531
|
+
getAllHasManyForEntity(keyPrefix: string): Map<string, StoredHasManyState> {
|
|
532
|
+
const result = new Map<string, StoredHasManyState>()
|
|
533
|
+
for (const [key, state] of this.hasManyStates) {
|
|
534
|
+
if (key.startsWith(keyPrefix)) {
|
|
535
|
+
result.set(key, {
|
|
536
|
+
serverIds: new Set(state.serverIds),
|
|
537
|
+
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
538
|
+
plannedRemovals: new Map(state.plannedRemovals),
|
|
539
|
+
plannedConnections: new Set(state.plannedConnections),
|
|
540
|
+
createdEntities: new Set(state.createdEntities),
|
|
541
|
+
version: state.version,
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return result
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ==================== Dirty Tracking ====================
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Gets the list of dirty relations for an entity.
|
|
552
|
+
*/
|
|
553
|
+
getDirtyRelations(keyPrefix: string): string[] {
|
|
554
|
+
const dirtyRelations: string[] = []
|
|
555
|
+
|
|
556
|
+
for (const [key, state] of this.relationStates) {
|
|
557
|
+
if (!key.startsWith(keyPrefix)) continue
|
|
558
|
+
const fieldName = key.slice(keyPrefix.length)
|
|
559
|
+
|
|
560
|
+
if (
|
|
561
|
+
state.currentId !== state.serverId ||
|
|
562
|
+
state.state !== state.serverState ||
|
|
563
|
+
Object.keys(state.placeholderData).length > 0
|
|
564
|
+
) {
|
|
565
|
+
dirtyRelations.push(fieldName)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
for (const [key, state] of this.hasManyStates) {
|
|
570
|
+
if (!key.startsWith(keyPrefix)) continue
|
|
571
|
+
const fieldName = key.slice(keyPrefix.length)
|
|
572
|
+
|
|
573
|
+
if (state.plannedRemovals.size > 0 || state.plannedConnections.size > 0) {
|
|
574
|
+
dirtyRelations.push(fieldName)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return dirtyRelations
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ==================== Export/Import ====================
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Exports relation states for given keys.
|
|
585
|
+
*/
|
|
586
|
+
exportRelationStates(keys: string[]): Map<string, StoredRelationState> {
|
|
587
|
+
const result = new Map<string, StoredRelationState>()
|
|
588
|
+
for (const key of keys) {
|
|
589
|
+
const state = this.relationStates.get(key)
|
|
590
|
+
if (state) {
|
|
591
|
+
result.set(key, {
|
|
592
|
+
...state,
|
|
593
|
+
placeholderData: { ...state.placeholderData },
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return result
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Exports has-many states for given keys.
|
|
602
|
+
*/
|
|
603
|
+
exportHasManyStates(keys: string[]): Map<string, StoredHasManyState> {
|
|
604
|
+
const result = new Map<string, StoredHasManyState>()
|
|
605
|
+
for (const key of keys) {
|
|
606
|
+
const state = this.hasManyStates.get(key)
|
|
607
|
+
if (state) {
|
|
608
|
+
result.set(key, {
|
|
609
|
+
serverIds: new Set(state.serverIds),
|
|
610
|
+
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
611
|
+
plannedRemovals: new Map(state.plannedRemovals),
|
|
612
|
+
plannedConnections: new Set(state.plannedConnections),
|
|
613
|
+
createdEntities: new Set(state.createdEntities),
|
|
614
|
+
version: state.version,
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Imports relation states from a snapshot.
|
|
623
|
+
* Returns the keys that were imported for notification.
|
|
624
|
+
*/
|
|
625
|
+
importRelationStates(states: Map<string, StoredRelationState>): string[] {
|
|
626
|
+
const keys: string[] = []
|
|
627
|
+
for (const [key, state] of states) {
|
|
628
|
+
this.relationStates.set(key, {
|
|
629
|
+
...state,
|
|
630
|
+
placeholderData: { ...state.placeholderData },
|
|
631
|
+
})
|
|
632
|
+
keys.push(key)
|
|
633
|
+
}
|
|
634
|
+
return keys
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Imports has-many states from a snapshot.
|
|
639
|
+
* Returns the keys that were imported for notification.
|
|
640
|
+
*/
|
|
641
|
+
importHasManyStates(states: Map<string, StoredHasManyState>): string[] {
|
|
642
|
+
const keys: string[] = []
|
|
643
|
+
for (const [key, state] of states) {
|
|
644
|
+
this.hasManyStates.set(key, {
|
|
645
|
+
serverIds: new Set(state.serverIds),
|
|
646
|
+
orderedIds: state.orderedIds ? [...state.orderedIds] : null,
|
|
647
|
+
plannedRemovals: new Map(state.plannedRemovals),
|
|
648
|
+
plannedConnections: new Set(state.plannedConnections),
|
|
649
|
+
createdEntities: new Set(state.createdEntities),
|
|
650
|
+
version: state.version + 1,
|
|
651
|
+
})
|
|
652
|
+
keys.push(key)
|
|
653
|
+
}
|
|
654
|
+
return keys
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Clears all relation data.
|
|
659
|
+
*/
|
|
660
|
+
clear(): void {
|
|
661
|
+
this.relationStates.clear()
|
|
662
|
+
this.hasManyStates.clear()
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ==================== Helper Functions ====================
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Computes the default ordered IDs for a has-many relation.
|
|
670
|
+
* Order is: serverIds (minus removals) + plannedConnections
|
|
671
|
+
*/
|
|
672
|
+
export function computeDefaultOrderedIds(state: StoredHasManyState): string[] {
|
|
673
|
+
const result: string[] = []
|
|
674
|
+
|
|
675
|
+
for (const id of state.serverIds) {
|
|
676
|
+
if (!state.plannedRemovals.has(id)) {
|
|
677
|
+
result.push(id)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for (const id of state.plannedConnections) {
|
|
682
|
+
if (!result.includes(id)) {
|
|
683
|
+
result.push(id)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return result
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
691
|
+
if (a.length !== b.length) return false
|
|
692
|
+
for (let i = 0; i < a.length; i++) {
|
|
693
|
+
if (a[i] !== b[i]) return false
|
|
694
|
+
}
|
|
695
|
+
return true
|
|
696
|
+
}
|