@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,6 +1,7 @@
|
|
|
1
1
|
import { EntityRelatedHandle, embeddedDataMatchesSnapshot } from './BaseHandle.js'
|
|
2
2
|
import type { ActionDispatcher } from '../core/ActionDispatcher.js'
|
|
3
3
|
import type { SnapshotStore } from '../store/SnapshotStore.js'
|
|
4
|
+
import type { StoredRelationState } from '../store/RelationStore.js'
|
|
4
5
|
import type { SchemaRegistry } from '../schema/SchemaRegistry.js'
|
|
5
6
|
import type { SelectionMeta } from '../selection/types.js'
|
|
6
7
|
import {
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type Unsubscribe,
|
|
18
19
|
type EntityAccessor,
|
|
19
20
|
type HasOneAccessor,
|
|
21
|
+
type HasOneRelationState,
|
|
20
22
|
} from './types.js'
|
|
21
23
|
import { createClientError, type ErrorInput, type FieldError } from '../errors/types.js'
|
|
22
24
|
import type {
|
|
@@ -63,6 +65,10 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
63
65
|
private readonly schema: SchemaRegistry,
|
|
64
66
|
brands?: Set<symbol>,
|
|
65
67
|
private readonly selection?: SelectionMeta,
|
|
68
|
+
private readonly dataFieldName: string = fieldName,
|
|
69
|
+
// Segments from the query root down to this relation; also the entity path
|
|
70
|
+
// of whatever it resolves to. See FieldRefMeta.fullPath.
|
|
71
|
+
private readonly relationPath: readonly string[] = [fieldName],
|
|
66
72
|
) {
|
|
67
73
|
super(parentEntityType, parentEntityId, store, dispatcher)
|
|
68
74
|
this.__brands = brands
|
|
@@ -78,8 +84,10 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
78
84
|
schema: SchemaRegistry,
|
|
79
85
|
brands?: Set<symbol>,
|
|
80
86
|
selection?: SelectionMeta,
|
|
87
|
+
dataFieldName?: string,
|
|
88
|
+
relationPath?: readonly string[],
|
|
81
89
|
): HasOneAccessor<TEntity, TSelected> {
|
|
82
|
-
return
|
|
90
|
+
return HasOneHandle.wrapProxy(new HasOneHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, schema, brands, selection, dataFieldName, relationPath))
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
static createRaw<TEntity extends object = object, TSelected = TEntity>(
|
|
@@ -92,12 +100,18 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
92
100
|
schema: SchemaRegistry,
|
|
93
101
|
brands?: Set<symbol>,
|
|
94
102
|
selection?: SelectionMeta,
|
|
103
|
+
dataFieldName?: string,
|
|
104
|
+
relationPath?: readonly string[],
|
|
95
105
|
): HasOneHandle<TEntity, TSelected> {
|
|
96
|
-
return new HasOneHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, schema, brands, selection)
|
|
106
|
+
return new HasOneHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, schema, brands, selection, dataFieldName, relationPath)
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
static wrapProxy<TEntity extends object, TSelected>(handle: HasOneHandle<TEntity, TSelected>): HasOneAccessor<TEntity, TSelected> {
|
|
100
|
-
return createHandleProxy<HasOneHandle<TEntity, TSelected>, HasOneAccessor<TEntity, TSelected>>(
|
|
110
|
+
return createHandleProxy<HasOneHandle<TEntity, TSelected>, HasOneAccessor<TEntity, TSelected>>(
|
|
111
|
+
handle,
|
|
112
|
+
(target) => target.entityRaw.fields,
|
|
113
|
+
(target) => target.entityRaw.selectedFieldNames,
|
|
114
|
+
)
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
/**
|
|
@@ -113,6 +127,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
113
127
|
isArray: false,
|
|
114
128
|
isRelation: true,
|
|
115
129
|
targetType: this.targetType,
|
|
130
|
+
fullPath: this.relationPath,
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
|
|
@@ -137,51 +152,141 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
137
152
|
|
|
138
153
|
/**
|
|
139
154
|
* Gets the relation state.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* requiring a prior RelationStore entry.
|
|
155
|
+
* Materializes the RelationStore entry from embedded snapshot data first, so a
|
|
156
|
+
* server-loaded has-one reports 'connected' without a prior explicit entry.
|
|
143
157
|
*/
|
|
144
|
-
get state():
|
|
145
|
-
|
|
158
|
+
get state(): HasOneRelationState {
|
|
159
|
+
this.ensureEntry()
|
|
160
|
+
const relation = this.store.getPresentationRelation(
|
|
146
161
|
this.entityType,
|
|
147
162
|
this.entityId,
|
|
148
163
|
this.fieldName,
|
|
149
164
|
)
|
|
150
|
-
|
|
151
|
-
return relation.state
|
|
152
|
-
}
|
|
153
|
-
// No explicit relation state — check if snapshot has embedded data
|
|
154
|
-
if (this.relatedId !== null) {
|
|
155
|
-
return 'connected'
|
|
156
|
-
}
|
|
157
|
-
return 'disconnected'
|
|
165
|
+
return relation?.state ?? 'disconnected'
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
/**
|
|
161
|
-
* Gets the current related entity ID
|
|
162
|
-
*
|
|
169
|
+
* Gets the current related entity ID — read exclusively from the RelationStore
|
|
170
|
+
* after materializing the entry from embedded snapshot data.
|
|
163
171
|
*/
|
|
164
172
|
get relatedId(): string | null {
|
|
165
|
-
|
|
166
|
-
const relation = this.store.
|
|
173
|
+
this.ensureEntry()
|
|
174
|
+
const relation = this.store.getPresentationRelation(
|
|
167
175
|
this.entityType,
|
|
168
176
|
this.entityId,
|
|
169
177
|
this.fieldName,
|
|
170
178
|
)
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
return relation?.currentId ?? null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Materializes this has-one's RelationStore entry from the parent's embedded
|
|
184
|
+
* snapshot data, so the store is the single source of truth (symmetric with
|
|
185
|
+
* {@link HasManyListHandle.materializeEmbeddedItems}).
|
|
186
|
+
*
|
|
187
|
+
* Only the relation entry is touched here — child-snapshot propagation stays in
|
|
188
|
+
* {@link ensureRelatedEntitySnapshot}, which owns the per-relation propagation
|
|
189
|
+
* slot so the two paths never double-consume it.
|
|
190
|
+
*
|
|
191
|
+
* - No entry yet + the parent embeds a related object with an id → create a
|
|
192
|
+
* `connected` entry (non-notifying) whose server baseline comes from the
|
|
193
|
+
* parent's serverData, so a freshly loaded relation is not dirty
|
|
194
|
+
* (currentId === serverId, state === serverState).
|
|
195
|
+
* - Existing entry + parent re-fetch (embedded reference changed) that is NOT
|
|
196
|
+
* locally dirty → advance the server baseline to the new related id.
|
|
197
|
+
* - A local connect/disconnect, a placeholder, or a `creating` entry is left
|
|
198
|
+
* untouched — it is detected as a locally-dirty relation.
|
|
199
|
+
* - No embedded data and no entry → the relation stays unmaterialized (null).
|
|
200
|
+
*/
|
|
201
|
+
private ensureEntry(): void {
|
|
202
|
+
const existing = this.store.getRelation(this.entityType, this.entityId, this.fieldName)
|
|
203
|
+
const embeddedData = this.readEmbeddedRelatedData()
|
|
204
|
+
const embeddedReference = readEmbeddedRelatedReference(embeddedData)
|
|
205
|
+
|
|
206
|
+
if (!existing) {
|
|
207
|
+
if (embeddedReference.kind !== 'connected') return
|
|
208
|
+
const serverId = this.readServerRelatedId()
|
|
209
|
+
this.store.getOrCreateRelation(this.entityType, this.entityId, this.fieldName, {
|
|
210
|
+
currentId: embeddedReference.id,
|
|
211
|
+
serverId,
|
|
212
|
+
state: 'connected',
|
|
213
|
+
serverState: serverId !== null ? 'connected' : 'disconnected',
|
|
214
|
+
placeholderData: {},
|
|
215
|
+
})
|
|
216
|
+
return
|
|
173
217
|
}
|
|
174
218
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
219
|
+
this.advanceServerBaselineOnRefetch(existing, embeddedReference, embeddedData)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* On a parent re-fetch (embedded reference changed) advances the relation's
|
|
224
|
+
* server baseline to the embedded related id, but only when the relation is
|
|
225
|
+
* not locally dirty — a local connect/disconnect/create must survive a re-fetch.
|
|
226
|
+
*
|
|
227
|
+
* Does NOT consume the propagation slot — {@link ensureRelatedEntitySnapshot}
|
|
228
|
+
* owns it. The same reference-change signal drives both, so both run within the
|
|
229
|
+
* one render that observes a new parent reference.
|
|
230
|
+
*
|
|
231
|
+
* The baseline write is NON-NOTIFYING: it runs during a render-phase read, and
|
|
232
|
+
* the parent re-fetch that produced the new embedded reference already notified
|
|
233
|
+
* subscribers. Notifying again here would mutate the store and synchronously call
|
|
234
|
+
* subscribers mid-render, violating the external-store contract (cf.
|
|
235
|
+
* {@link ensureRelatedEntitySnapshot}, which refreshes server data with skipNotify
|
|
236
|
+
* for the same reason).
|
|
237
|
+
*/
|
|
238
|
+
private advanceServerBaselineOnRefetch(
|
|
239
|
+
existing: StoredRelationState,
|
|
240
|
+
embeddedReference: EmbeddedRelatedReference,
|
|
241
|
+
embeddedData: unknown,
|
|
242
|
+
): void {
|
|
243
|
+
if (embeddedReference.kind === 'absent') return
|
|
244
|
+
if (this.isLocallyDirty(existing)) return
|
|
245
|
+
|
|
246
|
+
if (embeddedReference.kind === 'null' && existing.serverId === null && existing.serverState === 'disconnected') {
|
|
247
|
+
return
|
|
182
248
|
}
|
|
249
|
+
if (embeddedReference.kind === 'connected' && existing.serverId === embeddedReference.id && existing.serverState === 'connected') {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
if (!this.store.hasEmbeddedDataChanged(this.entityType, this.entityId, this.dataFieldName, embeddedData)) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (embeddedReference.kind === 'null') {
|
|
257
|
+
this.store.setRelation(this.entityType, this.entityId, this.fieldName, {
|
|
258
|
+
currentId: null,
|
|
259
|
+
serverId: null,
|
|
260
|
+
state: 'disconnected',
|
|
261
|
+
serverState: 'disconnected',
|
|
262
|
+
}, true)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.store.setRelation(this.entityType, this.entityId, this.fieldName, {
|
|
267
|
+
currentId: embeddedReference.id,
|
|
268
|
+
serverId: embeddedReference.id,
|
|
269
|
+
state: 'connected',
|
|
270
|
+
serverState: 'connected',
|
|
271
|
+
}, true)
|
|
272
|
+
}
|
|
183
273
|
|
|
184
|
-
|
|
274
|
+
private isLocallyDirty(relation: StoredRelationState): boolean {
|
|
275
|
+
return (
|
|
276
|
+
relation.currentId !== relation.serverId ||
|
|
277
|
+
relation.state !== relation.serverState ||
|
|
278
|
+
Object.keys(relation.placeholderData).length > 0
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Reads the embedded related object from the parent's canonical current data. */
|
|
283
|
+
private readEmbeddedRelatedData(): unknown {
|
|
284
|
+
return this.getEntityData()?.[this.dataFieldName]
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Extracts the related id from the parent's embedded server data, or null. */
|
|
288
|
+
private readServerRelatedId(): string | null {
|
|
289
|
+
return extractRelatedId(this.getServerData()?.[this.dataFieldName])
|
|
185
290
|
}
|
|
186
291
|
|
|
187
292
|
/**
|
|
@@ -222,6 +327,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
222
327
|
this.schema,
|
|
223
328
|
this.__brands,
|
|
224
329
|
this.selection,
|
|
330
|
+
this.relationPath,
|
|
225
331
|
)
|
|
226
332
|
this.entityHandleCacheProxy = EntityHandle.wrapProxy(this.entityHandleCacheRaw)
|
|
227
333
|
}
|
|
@@ -238,6 +344,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
238
344
|
this.dispatcher,
|
|
239
345
|
this.schema,
|
|
240
346
|
this.__brands,
|
|
347
|
+
this.relationPath,
|
|
241
348
|
)
|
|
242
349
|
this.placeholderCacheProxy = PlaceholderHandle.wrapProxy(this.placeholderCacheRaw)
|
|
243
350
|
}
|
|
@@ -289,7 +396,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
289
396
|
return
|
|
290
397
|
}
|
|
291
398
|
|
|
292
|
-
const embeddedData = (parentSnapshot.data as Record<string, unknown>)[this.
|
|
399
|
+
const embeddedData = (parentSnapshot.data as Record<string, unknown>)[this.dataFieldName]
|
|
293
400
|
if (!embeddedData || typeof embeddedData !== 'object') {
|
|
294
401
|
return
|
|
295
402
|
}
|
|
@@ -307,7 +414,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
307
414
|
// A new reference means the parent was re-fetched from the server.
|
|
308
415
|
// Same reference means the embedded data is stale and must not overwrite
|
|
309
416
|
// child state that may have been updated by a local commit.
|
|
310
|
-
if (!this.store.hasEmbeddedDataChanged(this.entityType, this.entityId, this.
|
|
417
|
+
if (!this.store.hasEmbeddedDataChanged(this.entityType, this.entityId, this.dataFieldName, embeddedData)) {
|
|
311
418
|
return
|
|
312
419
|
}
|
|
313
420
|
|
|
@@ -316,7 +423,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
316
423
|
// (e.g. polling). A new reference with identical values means no actual change.
|
|
317
424
|
const existing = this.store.getEntitySnapshot(this.targetType, id)
|
|
318
425
|
if (existing?.serverData && embeddedDataMatchesSnapshot(embeddedData as Record<string, unknown>, existing.serverData as Record<string, unknown>)) {
|
|
319
|
-
this.store.markEmbeddedDataPropagated(this.entityType, this.entityId, this.
|
|
426
|
+
this.store.markEmbeddedDataPropagated(this.entityType, this.entityId, this.dataFieldName, embeddedData)
|
|
320
427
|
return
|
|
321
428
|
}
|
|
322
429
|
|
|
@@ -331,7 +438,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
331
438
|
embeddedData as Record<string, unknown>,
|
|
332
439
|
true, // skipNotify - called during render, data already exists embedded in parent
|
|
333
440
|
)
|
|
334
|
-
this.store.markEmbeddedDataPropagated(this.entityType, this.entityId, this.
|
|
441
|
+
this.store.markEmbeddedDataPropagated(this.entityType, this.entityId, this.dataFieldName, embeddedData)
|
|
335
442
|
}
|
|
336
443
|
|
|
337
444
|
/**
|
|
@@ -354,6 +461,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
354
461
|
* Checks if the relation is dirty.
|
|
355
462
|
*/
|
|
356
463
|
get isDirty(): boolean {
|
|
464
|
+
this.ensureEntry()
|
|
357
465
|
const relation = this.store.getRelation(
|
|
358
466
|
this.entityType,
|
|
359
467
|
this.entityId,
|
|
@@ -361,11 +469,7 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
361
469
|
)
|
|
362
470
|
if (!relation) return false
|
|
363
471
|
|
|
364
|
-
return (
|
|
365
|
-
relation.currentId !== relation.serverId ||
|
|
366
|
-
relation.state !== relation.serverState ||
|
|
367
|
-
Object.keys(relation.placeholderData).length > 0
|
|
368
|
-
)
|
|
472
|
+
return this.isLocallyDirty(relation)
|
|
369
473
|
}
|
|
370
474
|
|
|
371
475
|
/**
|
|
@@ -374,11 +478,15 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
374
478
|
* Accessible via proxy as `$create()`.
|
|
375
479
|
*/
|
|
376
480
|
create(data?: Partial<TEntity>): string {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
481
|
+
// One gesture = one undo entry: the pre-create and the connect are journaled
|
|
482
|
+
// together so undo removes (and redo re-creates) the created target.
|
|
483
|
+
return this.store.transaction(() => {
|
|
484
|
+
const tempId = this.store.createEntity(this.targetType, data as Record<string, unknown>)
|
|
485
|
+
this.dispatcher.dispatch(
|
|
486
|
+
connectRelation(this.entityType, this.entityId, this.fieldName, tempId, this.targetType),
|
|
487
|
+
)
|
|
488
|
+
return tempId
|
|
489
|
+
})
|
|
382
490
|
}
|
|
383
491
|
|
|
384
492
|
/**
|
|
@@ -574,3 +682,28 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
|
|
|
574
682
|
}
|
|
575
683
|
|
|
576
684
|
}
|
|
685
|
+
|
|
686
|
+
type EmbeddedRelatedReference =
|
|
687
|
+
| { kind: 'absent' }
|
|
688
|
+
| { kind: 'null' }
|
|
689
|
+
| { kind: 'connected'; id: string }
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Reads an embedded has-one value while preserving explicit null.
|
|
693
|
+
*/
|
|
694
|
+
function readEmbeddedRelatedReference(embedded: unknown): EmbeddedRelatedReference {
|
|
695
|
+
if (embedded === undefined) return { kind: 'absent' }
|
|
696
|
+
if (embedded === null) return { kind: 'null' }
|
|
697
|
+
if (typeof embedded !== 'object' || !('id' in embedded)) return { kind: 'absent' }
|
|
698
|
+
const id = embedded.id
|
|
699
|
+
return typeof id === 'string' ? { kind: 'connected', id } : { kind: 'absent' }
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Extracts the related entity id from an embedded has-one object, or null when
|
|
704
|
+
* the value is not an object with a string id.
|
|
705
|
+
*/
|
|
706
|
+
function extractRelatedId(embedded: unknown): string | null {
|
|
707
|
+
const reference = readEmbeddedRelatedReference(embedded)
|
|
708
|
+
return reference.kind === 'connected' ? reference.id : null
|
|
709
|
+
}
|