@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
|
@@ -37,6 +37,9 @@ export class FieldHandle<T = unknown> extends EntityRelatedHandle {
|
|
|
37
37
|
dispatcher: ActionDispatcher,
|
|
38
38
|
private readonly _enumName?: string,
|
|
39
39
|
private readonly _columnType?: string,
|
|
40
|
+
private readonly dataFieldPath: string[] = fieldPath,
|
|
41
|
+
// Absolute chain from the query root; see FieldRefMeta.fullPath.
|
|
42
|
+
private readonly _fullPath: readonly string[] = fieldPath,
|
|
40
43
|
) {
|
|
41
44
|
super(entityType, entityId, store, dispatcher)
|
|
42
45
|
}
|
|
@@ -49,8 +52,10 @@ export class FieldHandle<T = unknown> extends EntityRelatedHandle {
|
|
|
49
52
|
dispatcher: ActionDispatcher,
|
|
50
53
|
enumName?: string,
|
|
51
54
|
columnType?: string,
|
|
55
|
+
dataFieldPath?: string[],
|
|
56
|
+
fullPath?: readonly string[],
|
|
52
57
|
): FieldAccessor<T> {
|
|
53
|
-
return createAliasProxy<FieldHandle<T>, FieldAccessor<T>>(new FieldHandle<T>(entityType, entityId, fieldPath, store, dispatcher, enumName, columnType))
|
|
58
|
+
return createAliasProxy<FieldHandle<T>, FieldAccessor<T>>(new FieldHandle<T>(entityType, entityId, fieldPath, store, dispatcher, enumName, columnType, dataFieldPath, fullPath))
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
static createRaw<T = unknown>(
|
|
@@ -61,8 +66,10 @@ export class FieldHandle<T = unknown> extends EntityRelatedHandle {
|
|
|
61
66
|
dispatcher: ActionDispatcher,
|
|
62
67
|
enumName?: string,
|
|
63
68
|
columnType?: string,
|
|
69
|
+
dataFieldPath?: string[],
|
|
70
|
+
fullPath?: readonly string[],
|
|
64
71
|
): FieldHandle<T> {
|
|
65
|
-
return new FieldHandle<T>(entityType, entityId, fieldPath, store, dispatcher, enumName, columnType)
|
|
72
|
+
return new FieldHandle<T>(entityType, entityId, fieldPath, store, dispatcher, enumName, columnType, dataFieldPath, fullPath)
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
static wrapProxy<T>(handle: FieldHandle<T>): FieldAccessor<T> {
|
|
@@ -83,16 +90,18 @@ export class FieldHandle<T = unknown> extends EntityRelatedHandle {
|
|
|
83
90
|
isRelation: false,
|
|
84
91
|
enumName: this._enumName,
|
|
85
92
|
columnType: this._columnType,
|
|
93
|
+
fullPath: this._fullPath,
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
/**
|
|
90
|
-
* Gets the current field value.
|
|
98
|
+
* Gets the current field value to DISPLAY. While a pessimistic persist is
|
|
99
|
+
* in-flight this is the server baseline; otherwise it is the live value.
|
|
91
100
|
*/
|
|
92
101
|
get value(): T | null {
|
|
93
|
-
const data = this.
|
|
102
|
+
const data = this.getPresentationData()
|
|
94
103
|
if (!data) return null
|
|
95
|
-
return
|
|
104
|
+
return this.getCurrentValue(data) as T | null
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
/**
|
|
@@ -101,14 +110,28 @@ export class FieldHandle<T = unknown> extends EntityRelatedHandle {
|
|
|
101
110
|
get serverValue(): T | null {
|
|
102
111
|
const serverData = this.getServerData()
|
|
103
112
|
if (!serverData) return null
|
|
104
|
-
return
|
|
113
|
+
return this.getServerValue(serverData) as T | null
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
/**
|
|
108
|
-
* Checks if the field has been modified.
|
|
117
|
+
* Checks if the field has been modified. Compares the CANONICAL value against
|
|
118
|
+
* the server value — never the presented value, so a field stays correctly
|
|
119
|
+
* dirty even while its display shows the server baseline mid-pessimistic-persist.
|
|
109
120
|
*/
|
|
110
121
|
get isDirty(): boolean {
|
|
111
|
-
|
|
122
|
+
const data = this.getEntityData()
|
|
123
|
+
const value = data ? (this.getCurrentValue(data) as T | null) : null
|
|
124
|
+
return !deepEqual(value, this.serverValue)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private getCurrentValue(data: Record<string, unknown>): unknown {
|
|
128
|
+
const value = getNestedValue(data, this.fieldPath)
|
|
129
|
+
return value === undefined ? getNestedValue(data, this.dataFieldPath) : value
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private getServerValue(data: Record<string, unknown>): unknown {
|
|
133
|
+
const value = getNestedValue(data, this.fieldPath)
|
|
134
|
+
return value === undefined ? getNestedValue(data, this.dataFieldPath) : value
|
|
112
135
|
}
|
|
113
136
|
|
|
114
137
|
/**
|
|
@@ -217,12 +240,16 @@ export class FieldHandle<T = unknown> extends EntityRelatedHandle {
|
|
|
217
240
|
nested<K extends keyof NonNullable<T>>(
|
|
218
241
|
key: K,
|
|
219
242
|
): FieldAccessor<NonNullable<T>[K]> {
|
|
243
|
+
const fieldName = String(key)
|
|
220
244
|
return FieldHandle.create<NonNullable<T>[K]>(
|
|
221
245
|
this.entityType,
|
|
222
246
|
this.entityId,
|
|
223
|
-
[...this.fieldPath,
|
|
247
|
+
[...this.fieldPath, fieldName],
|
|
224
248
|
this.store,
|
|
225
249
|
this.dispatcher,
|
|
250
|
+
undefined,
|
|
251
|
+
undefined,
|
|
252
|
+
[...this.dataFieldPath, fieldName],
|
|
226
253
|
)
|
|
227
254
|
}
|
|
228
255
|
|
|
@@ -277,4 +304,3 @@ function getNestedValue(obj: Record<string, unknown>, path: string[]): unknown {
|
|
|
277
304
|
|
|
278
305
|
return current
|
|
279
306
|
}
|
|
280
|
-
|
|
@@ -33,7 +33,10 @@ import { createAliasProxy } from './proxyFactory.js'
|
|
|
33
33
|
* @typeParam TSelected - The selected subset of fields (defaults to TEntity for backwards compatibility)
|
|
34
34
|
*/
|
|
35
35
|
export class HasManyListHandle<TEntity extends object = object, TSelected = TEntity> extends EntityRelatedHandle {
|
|
36
|
-
|
|
36
|
+
// Per-item accessor cache, keyed by the item's canonical (post-rekey) id. Identity is the
|
|
37
|
+
// point: a still-listed item keeps the same proxy — and with it the same handle, which the
|
|
38
|
+
// proxy is the only reference to. Kept bounded by syncItemHandleCache, which drops ids
|
|
39
|
+
// that have left the list.
|
|
37
40
|
private itemHandleCacheProxy = new Map<string, EntityAccessor<TEntity, TSelected>>()
|
|
38
41
|
|
|
39
42
|
/** Runtime brand symbols for validation */
|
|
@@ -73,6 +76,9 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
73
76
|
brands?: Set<symbol>,
|
|
74
77
|
alias?: string,
|
|
75
78
|
private readonly selection?: SelectionMeta,
|
|
79
|
+
// Segments from the query root down to this relation. Items do NOT inherit it:
|
|
80
|
+
// a has-many restarts the chain, mirroring the collector proxy.
|
|
81
|
+
private readonly relationPath: readonly string[] = [fieldName],
|
|
76
82
|
) {
|
|
77
83
|
super(parentEntityType, parentEntityId, store, dispatcher)
|
|
78
84
|
this.__brands = brands
|
|
@@ -90,8 +96,31 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
90
96
|
brands?: Set<symbol>,
|
|
91
97
|
alias?: string,
|
|
92
98
|
selection?: SelectionMeta,
|
|
99
|
+
relationPath?: readonly string[],
|
|
93
100
|
): HasManyAccessor<TEntity, TSelected> {
|
|
94
|
-
return createAliasProxy<HasManyListHandle<TEntity, TSelected>, HasManyAccessor<TEntity, TSelected>>(
|
|
101
|
+
return createAliasProxy<HasManyListHandle<TEntity, TSelected>, HasManyAccessor<TEntity, TSelected>>(
|
|
102
|
+
HasManyListHandle.createRaw<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, itemType, store, dispatcher, schema, brands, alias, selection, relationPath),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates the handle without the alias proxy. Mirrors {@link EntityHandle.createRaw}:
|
|
108
|
+
* the class surface (e.g. {@link itemHandleCacheSize}) is not part of HasManyAccessor.
|
|
109
|
+
*/
|
|
110
|
+
static createRaw<TEntity extends object = object, TSelected = TEntity>(
|
|
111
|
+
parentEntityType: string,
|
|
112
|
+
parentEntityId: string,
|
|
113
|
+
fieldName: string,
|
|
114
|
+
itemType: string,
|
|
115
|
+
store: SnapshotStore,
|
|
116
|
+
dispatcher: ActionDispatcher,
|
|
117
|
+
schema: SchemaRegistry,
|
|
118
|
+
brands?: Set<symbol>,
|
|
119
|
+
alias?: string,
|
|
120
|
+
selection?: SelectionMeta,
|
|
121
|
+
relationPath?: readonly string[],
|
|
122
|
+
): HasManyListHandle<TEntity, TSelected> {
|
|
123
|
+
return new HasManyListHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, itemType, store, dispatcher, schema, brands, alias, selection, relationPath)
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
/**
|
|
@@ -107,6 +136,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
107
136
|
isArray: true,
|
|
108
137
|
isRelation: true,
|
|
109
138
|
targetType: this.itemType,
|
|
139
|
+
fullPath: this.relationPath,
|
|
110
140
|
}
|
|
111
141
|
}
|
|
112
142
|
|
|
@@ -115,21 +145,76 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
115
145
|
* Returns selection-aware EntityAccessors that support `item.fieldName.value`.
|
|
116
146
|
* Includes planned connections and excludes planned removals.
|
|
117
147
|
* Uses ordered IDs to preserve order including after move() operations.
|
|
148
|
+
*
|
|
149
|
+
* This is also where the item-accessor cache is pruned — see {@link syncItemHandleCache}.
|
|
118
150
|
*/
|
|
119
151
|
get items(): EntityAccessor<TEntity, TSelected>[] {
|
|
120
152
|
this.materializeEmbeddedItems()
|
|
121
153
|
|
|
122
154
|
// Use ordered IDs from store (handles removals, connections, and ordering)
|
|
123
|
-
const orderedIds = this.store.
|
|
155
|
+
const orderedIds = this.store.getPresentationHasManyOrderedIds(
|
|
124
156
|
this.entityType,
|
|
125
157
|
this.entityId,
|
|
126
158
|
this.fieldName,
|
|
127
159
|
this.alias,
|
|
128
160
|
)
|
|
129
161
|
|
|
162
|
+
this.syncItemHandleCache(orderedIds)
|
|
163
|
+
|
|
130
164
|
return orderedIds.map((id) => this.getItemHandle(id))
|
|
131
165
|
}
|
|
132
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Canonical cache key for an item id: a temp id follows its temp→persisted rekey, so a
|
|
169
|
+
* lookup by the now-dead temp id reuses the persisted item's handle instead of minting a
|
|
170
|
+
* duplicate for the same entity.
|
|
171
|
+
*/
|
|
172
|
+
private resolveItemKey(itemId: string): string {
|
|
173
|
+
return this.store.resolveEntityId(this.itemType, itemId)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Drops cached item accessors for ids that have left the list. Without it the cache keeps an
|
|
178
|
+
* accessor for every id the relation has ever shown, for as long as the parent handle lives
|
|
179
|
+
* — a paginated / filtered / refetched has-many grows without bound.
|
|
180
|
+
*
|
|
181
|
+
* Runs before the accessors are resolved and never touches a live id, so an item that is
|
|
182
|
+
* still listed keeps the exact same proxy and the exact same handle behind it.
|
|
183
|
+
*/
|
|
184
|
+
private syncItemHandleCache(liveIds: readonly string[]): void {
|
|
185
|
+
if (this.itemHandleCacheProxy.size === 0) return
|
|
186
|
+
this.canonicalizeItemHandleCache()
|
|
187
|
+
|
|
188
|
+
// While the parent persists, the presented list hides planned additions — pruning
|
|
189
|
+
// against it would drop handles that reappear as soon as the persist settles.
|
|
190
|
+
if (this.store.isPersisting(this.entityType, this.entityId)) return
|
|
191
|
+
|
|
192
|
+
const liveKeys = new Set(liveIds.map(id => this.resolveItemKey(id)))
|
|
193
|
+
for (const key of this.itemHandleCacheProxy.keys()) {
|
|
194
|
+
if (liveKeys.has(key)) continue
|
|
195
|
+
this.itemHandleCacheProxy.delete(key)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private canonicalizeItemHandleCache(): void {
|
|
200
|
+
for (const [key, proxy] of [...this.itemHandleCacheProxy]) {
|
|
201
|
+
const canonicalKey = this.resolveItemKey(key)
|
|
202
|
+
if (canonicalKey === key) continue
|
|
203
|
+
if (!this.itemHandleCacheProxy.has(canonicalKey)) {
|
|
204
|
+
this.itemHandleCacheProxy.set(canonicalKey, proxy)
|
|
205
|
+
}
|
|
206
|
+
this.itemHandleCacheProxy.delete(key)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @internal Occupancy of the item-accessor cache. Exposed only so eviction tests can
|
|
212
|
+
* assert the cache stays bounded; not part of the HasManyAccessor API.
|
|
213
|
+
*/
|
|
214
|
+
get itemHandleCacheSize(): number {
|
|
215
|
+
return this.itemHandleCacheProxy.size
|
|
216
|
+
}
|
|
217
|
+
|
|
133
218
|
/**
|
|
134
219
|
* Propagates the parent's embedded has-many data into per-item snapshots and
|
|
135
220
|
* ensures the has-many state exists in the store.
|
|
@@ -139,6 +224,12 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
139
224
|
* without ever reading `items`, and that path must also see populated data.
|
|
140
225
|
* Idempotent within a render — guarded by hasEmbeddedDataChanged.
|
|
141
226
|
*
|
|
227
|
+
* For the same reason EVERY entry point that reads or writes the has-many state
|
|
228
|
+
* calls this first — isDirty and the mutators as well as items/getById. An
|
|
229
|
+
* embedded list has no state in the store until it is materialised, so a
|
|
230
|
+
* consumer that never iterated saw isDirty === false and had its mutation land
|
|
231
|
+
* on a state nobody reads. Keep new entry points on this guard.
|
|
232
|
+
*
|
|
142
233
|
* @returns false when there is no embedded list data to materialise.
|
|
143
234
|
*/
|
|
144
235
|
private materializeEmbeddedItems(): boolean {
|
|
@@ -274,11 +365,13 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
274
365
|
* Returns selection-aware EntityAccessor that supports direct field access.
|
|
275
366
|
*/
|
|
276
367
|
getItemHandle(itemId: string): EntityAccessor<TEntity, TSelected> {
|
|
277
|
-
|
|
368
|
+
this.canonicalizeItemHandleCache()
|
|
369
|
+
const key = this.resolveItemKey(itemId)
|
|
370
|
+
let proxy = this.itemHandleCacheProxy.get(key)
|
|
278
371
|
|
|
279
372
|
if (!proxy) {
|
|
280
373
|
const raw = EntityHandle.createRaw<TEntity, TSelected>(
|
|
281
|
-
|
|
374
|
+
key,
|
|
282
375
|
this.itemType,
|
|
283
376
|
this.store,
|
|
284
377
|
this.dispatcher,
|
|
@@ -287,8 +380,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
287
380
|
this.selection,
|
|
288
381
|
)
|
|
289
382
|
proxy = EntityHandle.wrapProxy(raw)
|
|
290
|
-
this.
|
|
291
|
-
this.itemHandleCacheProxy.set(itemId, proxy)
|
|
383
|
+
this.itemHandleCacheProxy.set(key, proxy)
|
|
292
384
|
}
|
|
293
385
|
|
|
294
386
|
return proxy
|
|
@@ -311,6 +403,8 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
311
403
|
* Checks if the list is dirty (items added/removed/moved/connected/disconnected).
|
|
312
404
|
*/
|
|
313
405
|
get isDirty(): boolean {
|
|
406
|
+
this.materializeEmbeddedItems()
|
|
407
|
+
|
|
314
408
|
const state = this.store.getHasMany(
|
|
315
409
|
this.entityType,
|
|
316
410
|
this.entityId,
|
|
@@ -322,8 +416,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
322
416
|
|
|
323
417
|
return (
|
|
324
418
|
state.plannedRemovals.size > 0 ||
|
|
325
|
-
state.
|
|
326
|
-
state.createdEntities.size > 0 ||
|
|
419
|
+
state.plannedAdditions.size > 0 ||
|
|
327
420
|
state.orderedIds !== null
|
|
328
421
|
)
|
|
329
422
|
}
|
|
@@ -340,6 +433,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
340
433
|
* Connects an existing entity to this has-many relation.
|
|
341
434
|
*/
|
|
342
435
|
connect(itemId: string): void {
|
|
436
|
+
this.materializeEmbeddedItems()
|
|
343
437
|
this.dispatcher.dispatch(
|
|
344
438
|
connectToList(this.entityType, this.entityId, this.fieldName, itemId, this.itemType, this.alias),
|
|
345
439
|
)
|
|
@@ -351,6 +445,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
351
445
|
* For created entities (via add()), cancels the add operation.
|
|
352
446
|
*/
|
|
353
447
|
disconnect(itemId: string): void {
|
|
448
|
+
this.materializeEmbeddedItems()
|
|
354
449
|
this.dispatcher.dispatch(
|
|
355
450
|
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, 'disconnect', this.alias),
|
|
356
451
|
)
|
|
@@ -362,6 +457,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
362
457
|
* For created entities (via add()), cancels the add operation.
|
|
363
458
|
*/
|
|
364
459
|
delete(itemId: string): void {
|
|
460
|
+
this.materializeEmbeddedItems()
|
|
365
461
|
this.dispatcher.dispatch(
|
|
366
462
|
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, 'delete', this.alias),
|
|
367
463
|
)
|
|
@@ -373,16 +469,21 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
373
469
|
* For connecting existing entities, use connect() instead.
|
|
374
470
|
*/
|
|
375
471
|
add(data?: Partial<TEntity>): string {
|
|
472
|
+
this.materializeEmbeddedItems()
|
|
376
473
|
|
|
377
|
-
//
|
|
378
|
-
|
|
474
|
+
// One gesture = one undo entry: the pre-create and the list-add are journaled
|
|
475
|
+
// together so undo removes (and redo re-creates) the whole row.
|
|
476
|
+
return this.store.transaction(() => {
|
|
477
|
+
// Pre-create entity so we can return tempId
|
|
478
|
+
const tempId = this.store.createEntity(this.itemType, data as Record<string, unknown>)
|
|
379
479
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
480
|
+
// Add to has-many relation through dispatcher (enables events/interceptors)
|
|
481
|
+
this.dispatcher.dispatch(
|
|
482
|
+
addToList(this.entityType, this.entityId, this.fieldName, this.itemType, tempId, this.alias),
|
|
483
|
+
)
|
|
384
484
|
|
|
385
|
-
|
|
485
|
+
return tempId
|
|
486
|
+
})
|
|
386
487
|
}
|
|
387
488
|
|
|
388
489
|
/**
|
|
@@ -395,6 +496,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
395
496
|
* Falls back to disconnect when schema metadata is not available.
|
|
396
497
|
*/
|
|
397
498
|
remove(itemId: string): void {
|
|
499
|
+
this.materializeEmbeddedItems()
|
|
398
500
|
this.dispatcher.dispatch(
|
|
399
501
|
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, this.resolveRemovalType(), this.alias),
|
|
400
502
|
)
|
|
@@ -425,6 +527,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
425
527
|
* use an 'order' field on the entity.
|
|
426
528
|
*/
|
|
427
529
|
move(fromIndex: number, toIndex: number): void {
|
|
530
|
+
this.materializeEmbeddedItems()
|
|
428
531
|
this.dispatcher.dispatch(
|
|
429
532
|
moveInList(this.entityType, this.entityId, this.fieldName, fromIndex, toIndex, this.alias),
|
|
430
533
|
)
|
|
@@ -435,6 +538,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
|
|
|
435
538
|
* Clears all planned connections and removals.
|
|
436
539
|
*/
|
|
437
540
|
reset(): void {
|
|
541
|
+
this.materializeEmbeddedItems()
|
|
438
542
|
this.store.resetHasMany(
|
|
439
543
|
this.entityType,
|
|
440
544
|
this.entityId,
|