@contember/bindx 0.1.30 → 0.1.32

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.
Files changed (53) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts.map +1 -1
  2. package/dist/adapter/types.d.ts +4 -0
  3. package/dist/adapter/types.d.ts.map +1 -1
  4. package/dist/core/ActionDispatcher.d.ts.map +1 -1
  5. package/dist/core/actions.d.ts +4 -2
  6. package/dist/core/actions.d.ts.map +1 -1
  7. package/dist/handles/BaseHandle.d.ts +5 -0
  8. package/dist/handles/BaseHandle.d.ts.map +1 -1
  9. package/dist/handles/HasManyListHandle.d.ts +1 -0
  10. package/dist/handles/HasManyListHandle.d.ts.map +1 -1
  11. package/dist/handles/HasOneHandle.d.ts +6 -0
  12. package/dist/handles/HasOneHandle.d.ts.map +1 -1
  13. package/dist/handles/types.d.ts +1 -0
  14. package/dist/handles/types.d.ts.map +1 -1
  15. package/dist/persistence/BatchPersister.d.ts +30 -0
  16. package/dist/persistence/BatchPersister.d.ts.map +1 -1
  17. package/dist/persistence/MutationCollector.d.ts +28 -0
  18. package/dist/persistence/MutationCollector.d.ts.map +1 -1
  19. package/dist/persistence/types.d.ts +6 -0
  20. package/dist/persistence/types.d.ts.map +1 -1
  21. package/dist/store/EntityMetaStore.d.ts +4 -0
  22. package/dist/store/EntityMetaStore.d.ts.map +1 -1
  23. package/dist/store/EntitySnapshotStore.d.ts +4 -0
  24. package/dist/store/EntitySnapshotStore.d.ts.map +1 -1
  25. package/dist/store/ErrorStore.d.ts +6 -0
  26. package/dist/store/ErrorStore.d.ts.map +1 -1
  27. package/dist/store/RelationStore.d.ts +15 -0
  28. package/dist/store/RelationStore.d.ts.map +1 -1
  29. package/dist/store/SnapshotStore.d.ts +31 -0
  30. package/dist/store/SnapshotStore.d.ts.map +1 -1
  31. package/dist/store/SubscriptionManager.d.ts +15 -0
  32. package/dist/store/SubscriptionManager.d.ts.map +1 -1
  33. package/dist/store/TouchedStore.d.ts +4 -0
  34. package/dist/store/TouchedStore.d.ts.map +1 -1
  35. package/package.json +2 -2
  36. package/src/adapter/ContemberAdapter.ts +11 -5
  37. package/src/adapter/types.ts +4 -0
  38. package/src/core/ActionDispatcher.ts +3 -0
  39. package/src/core/actions.ts +6 -2
  40. package/src/handles/BaseHandle.ts +16 -0
  41. package/src/handles/HasManyListHandle.ts +35 -25
  42. package/src/handles/HasOneHandle.ts +35 -8
  43. package/src/handles/types.ts +1 -0
  44. package/src/persistence/BatchPersister.ts +237 -0
  45. package/src/persistence/MutationCollector.ts +167 -19
  46. package/src/persistence/types.ts +6 -0
  47. package/src/store/EntityMetaStore.ts +29 -0
  48. package/src/store/EntitySnapshotStore.ts +23 -0
  49. package/src/store/ErrorStore.ts +30 -0
  50. package/src/store/RelationStore.ts +157 -1
  51. package/src/store/SnapshotStore.ts +116 -5
  52. package/src/store/SubscriptionManager.ts +82 -2
  53. package/src/store/TouchedStore.ts +16 -0
@@ -1,6 +1,14 @@
1
1
  import type { HasOneRelationState } from '../handles/types.js'
2
2
  import type { EntitySnapshot } from './snapshots.js'
3
3
 
4
+ function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
5
+ if (a.size !== b.size) return false
6
+ for (const item of a) {
7
+ if (!b.has(item)) return false
8
+ }
9
+ return true
10
+ }
11
+
4
12
  /**
5
13
  * Removal type for has-many items
6
14
  */
@@ -150,7 +158,8 @@ export class RelationStore {
150
158
  * Gets or creates has-many list state.
151
159
  */
152
160
  getOrCreateHasMany(key: string, serverIds?: string[]): StoredHasManyState {
153
- if (!this.hasManyStates.has(key)) {
161
+ const existing = this.hasManyStates.get(key)
162
+ if (!existing) {
154
163
  this.hasManyStates.set(key, {
155
164
  serverIds: new Set(serverIds ?? []),
156
165
  orderedIds: null,
@@ -159,6 +168,16 @@ export class RelationStore {
159
168
  createdEntities: new Set(),
160
169
  version: 0,
161
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
+ }
162
181
  }
163
182
 
164
183
  return this.hasManyStates.get(key)!
@@ -365,6 +384,37 @@ export class RelationStore {
365
384
  }
366
385
  }
367
386
 
387
+ /**
388
+ * Connects an existing (persisted) entity to a has-many relation.
389
+ * Unlike addToHasMany, does NOT add to createdEntities — used for
390
+ * materializing embedded connect references to existing entities.
391
+ */
392
+ connectExistingToHasMany(key: string, itemId: string): void {
393
+ const existing = this.hasManyStates.get(key)
394
+
395
+ if (!existing) {
396
+ this.hasManyStates.set(key, {
397
+ serverIds: new Set(),
398
+ orderedIds: [itemId],
399
+ plannedRemovals: new Map(),
400
+ plannedConnections: new Set([itemId]),
401
+ createdEntities: new Set(),
402
+ version: 0,
403
+ })
404
+ } else {
405
+ const newPlannedConnections = new Set(existing.plannedConnections)
406
+ newPlannedConnections.add(itemId)
407
+ const currentOrderedIds = existing.orderedIds ?? computeDefaultOrderedIds(existing)
408
+ const newOrderedIds = [...currentOrderedIds, itemId]
409
+ this.hasManyStates.set(key, {
410
+ ...existing,
411
+ orderedIds: newOrderedIds,
412
+ plannedConnections: newPlannedConnections,
413
+ version: existing.version + 1,
414
+ })
415
+ }
416
+ }
417
+
368
418
  /**
369
419
  * Removes an entity from a has-many relation.
370
420
  * For newly created entities (via add()), cancels the connection.
@@ -654,6 +704,112 @@ export class RelationStore {
654
704
  return keys
655
705
  }
656
706
 
707
+ /**
708
+ * Replaces all occurrences of oldId with newId across relation and hasMany states.
709
+ * Used after persist to rekey temp IDs to server-assigned IDs.
710
+ */
711
+ replaceEntityId(oldId: string, newId: string): void {
712
+ // Replace in has-one relation states: currentId, serverId
713
+ for (const [key, state] of this.relationStates) {
714
+ let changed = false
715
+ let currentId = state.currentId
716
+ let serverId = state.serverId
717
+
718
+ if (currentId === oldId) { currentId = newId; changed = true }
719
+ if (serverId === oldId) { serverId = newId; changed = true }
720
+
721
+ if (changed) {
722
+ this.relationStates.set(key, { ...state, currentId, serverId, version: state.version + 1 })
723
+ }
724
+ }
725
+
726
+ // Replace in has-many states: serverIds, orderedIds, plannedConnections, createdEntities, plannedRemovals
727
+ for (const [key, state] of this.hasManyStates) {
728
+ let changed = false
729
+
730
+ let serverIds = state.serverIds
731
+ if (serverIds.has(oldId)) {
732
+ serverIds = new Set(serverIds)
733
+ serverIds.delete(oldId)
734
+ serverIds.add(newId)
735
+ changed = true
736
+ }
737
+
738
+ let orderedIds = state.orderedIds
739
+ if (orderedIds) {
740
+ const idx = orderedIds.indexOf(oldId)
741
+ if (idx !== -1) {
742
+ orderedIds = [...orderedIds]
743
+ orderedIds[idx] = newId
744
+ changed = true
745
+ }
746
+ }
747
+
748
+ let plannedConnections = state.plannedConnections
749
+ if (plannedConnections.has(oldId)) {
750
+ plannedConnections = new Set(plannedConnections)
751
+ plannedConnections.delete(oldId)
752
+ plannedConnections.add(newId)
753
+ changed = true
754
+ }
755
+
756
+ let createdEntities = state.createdEntities
757
+ if (createdEntities.has(oldId)) {
758
+ createdEntities = new Set(createdEntities)
759
+ createdEntities.delete(oldId)
760
+ createdEntities.add(newId)
761
+ changed = true
762
+ }
763
+
764
+ let plannedRemovals = state.plannedRemovals
765
+ if (plannedRemovals.has(oldId)) {
766
+ const removalType = plannedRemovals.get(oldId)!
767
+ plannedRemovals = new Map(plannedRemovals)
768
+ plannedRemovals.delete(oldId)
769
+ plannedRemovals.set(newId, removalType)
770
+ changed = true
771
+ }
772
+
773
+ if (changed) {
774
+ this.hasManyStates.set(key, {
775
+ serverIds,
776
+ orderedIds,
777
+ plannedRemovals,
778
+ plannedConnections,
779
+ createdEntities,
780
+ version: state.version + 1,
781
+ })
782
+ }
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Rekeys relation/hasMany entries owned by an entity (changes the parent ID in the key).
788
+ */
789
+ rekeyOwner(oldKeyPrefix: string, newKeyPrefix: string): void {
790
+ const toMoveRelations: [string, StoredRelationState][] = []
791
+ for (const [key, value] of this.relationStates) {
792
+ if (key.startsWith(oldKeyPrefix)) {
793
+ toMoveRelations.push([key, value])
794
+ }
795
+ }
796
+ for (const [oldKey, value] of toMoveRelations) {
797
+ this.relationStates.delete(oldKey)
798
+ this.relationStates.set(newKeyPrefix + oldKey.slice(oldKeyPrefix.length), value)
799
+ }
800
+
801
+ const toMoveHasMany: [string, StoredHasManyState][] = []
802
+ for (const [key, value] of this.hasManyStates) {
803
+ if (key.startsWith(oldKeyPrefix)) {
804
+ toMoveHasMany.push([key, value])
805
+ }
806
+ }
807
+ for (const [oldKey, value] of toMoveHasMany) {
808
+ this.hasManyStates.delete(oldKey)
809
+ this.hasManyStates.set(newKeyPrefix + oldKey.slice(oldKeyPrefix.length), value)
810
+ }
811
+ }
812
+
657
813
  /**
658
814
  * Clears all relation data.
659
815
  */
@@ -46,18 +46,44 @@ export class SnapshotStore implements SnapshotVersionBumper {
46
46
  private readonly touched = new TouchedStore()
47
47
  private readonly dirtyTracker: DirtyTracker
48
48
 
49
+ /**
50
+ * Tracks the last embedded data reference propagated from parent to child.
51
+ * Used to detect whether the parent was re-fetched (new reference) vs. stale
52
+ * embedded data that should not overwrite committed child state.
53
+ * Keyed by "parentType:parentId:fieldName".
54
+ */
55
+ private readonly lastPropagatedData = new Map<string, unknown>()
56
+
49
57
  constructor() {
50
58
  this.dirtyTracker = new DirtyTracker(this.entitySnapshots, this.meta, this.relations)
51
59
  }
52
60
 
53
61
  // ==================== Key Generation ====================
54
62
 
63
+ /** Maps temp ID entity keys to their persisted ID entity keys for transparent resolution */
64
+ private readonly rekeyedEntities = new Map<string, string>()
65
+
55
66
  private getEntityKey(entityType: string, id: string): string {
56
- return `${entityType}:${id}`
67
+ const key = `${entityType}:${id}`
68
+ return this.rekeyedEntities.get(key) ?? key
57
69
  }
58
70
 
59
71
  private getRelationKey(parentType: string, parentId: string, fieldName: string): string {
60
- return `${parentType}:${parentId}:${fieldName}`
72
+ const resolvedParentId = this.resolveId(parentType, parentId)
73
+ return `${parentType}:${resolvedParentId}:${fieldName}`
74
+ }
75
+
76
+ /**
77
+ * Resolves an ID to its persisted ID if it has been rekeyed.
78
+ */
79
+ private resolveId(entityType: string, id: string): string {
80
+ const key = `${entityType}:${id}`
81
+ const rekeyed = this.rekeyedEntities.get(key)
82
+ if (rekeyed) {
83
+ // Extract the ID from the rekeyed key (format: "EntityType:id")
84
+ return rekeyed.slice(entityType.length + 1)
85
+ }
86
+ return id
61
87
  }
62
88
 
63
89
  // ==================== SnapshotVersionBumper ====================
@@ -80,6 +106,49 @@ export class SnapshotStore implements SnapshotVersionBumper {
80
106
  }
81
107
  }
82
108
 
109
+ // ==================== Embedded Data Propagation Tracking ====================
110
+
111
+ /**
112
+ * Returns true if the embedded data reference differs from what was last propagated.
113
+ * Uses reference identity — a new reference means the parent was re-fetched.
114
+ */
115
+ hasEmbeddedDataChanged(parentType: string, parentId: string, fieldName: string, currentData: unknown): boolean {
116
+ const key = this.getRelationKey(parentType, parentId, fieldName)
117
+ return this.lastPropagatedData.get(key) !== currentData
118
+ }
119
+
120
+ /**
121
+ * Records the embedded data reference that was propagated to the child.
122
+ */
123
+ markEmbeddedDataPropagated(parentType: string, parentId: string, fieldName: string, data: unknown): void {
124
+ const key = this.getRelationKey(parentType, parentId, fieldName)
125
+ this.lastPropagatedData.set(key, data)
126
+ }
127
+
128
+ /**
129
+ * Removes all propagation tracking entries for a given parent entity.
130
+ */
131
+ clearPropagatedDataForEntity(parentType: string, parentId: string): void {
132
+ const prefix = `${parentType}:${this.resolveId(parentType, parentId)}:`
133
+ for (const key of this.lastPropagatedData.keys()) {
134
+ if (key.startsWith(prefix)) {
135
+ this.lastPropagatedData.delete(key)
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Rekeys propagation tracking entries when a temp ID is replaced by a persisted ID.
142
+ */
143
+ private rekeyPropagatedData(oldPrefix: string, newPrefix: string): void {
144
+ for (const [key, value] of this.lastPropagatedData) {
145
+ if (key.startsWith(oldPrefix)) {
146
+ this.lastPropagatedData.delete(key)
147
+ this.lastPropagatedData.set(newPrefix + key.slice(oldPrefix.length), value)
148
+ }
149
+ }
150
+ }
151
+
83
152
  // ==================== Entity Snapshots ====================
84
153
 
85
154
  getEntitySnapshot<T extends object>(entityType: string, id: string): EntitySnapshot<T> | undefined {
@@ -154,6 +223,7 @@ export class SnapshotStore implements SnapshotVersionBumper {
154
223
  const key = this.getEntityKey(entityType, id)
155
224
  this.entitySnapshots.remove(key)
156
225
  this.meta.clearLoadState(key)
226
+ this.clearPropagatedDataForEntity(entityType, id)
157
227
  this.notifyEntitySubscribers(key)
158
228
  }
159
229
 
@@ -218,9 +288,38 @@ export class SnapshotStore implements SnapshotVersionBumper {
218
288
  }
219
289
 
220
290
  mapTempIdToPersistedId(entityType: string, tempId: string, persistedId: string): void {
221
- const key = this.getEntityKey(entityType, tempId)
222
- this.meta.mapTempIdToPersistedId(key, persistedId)
223
- this.notifyEntitySubscribers(key)
291
+ // Use raw keys (bypass resolveId) since we're the ones creating the mapping
292
+ const oldKey = `${entityType}:${tempId}`
293
+ const newKey = `${entityType}:${persistedId}`
294
+ const oldKeyPrefix = `${entityType}:${tempId}:`
295
+ const newKeyPrefix = `${entityType}:${persistedId}:`
296
+
297
+ // Register redirect so future lookups by temp ID resolve to persisted key
298
+ this.rekeyedEntities.set(oldKey, newKey)
299
+
300
+ // Rekey entity snapshot (moves data, updates id field)
301
+ this.entitySnapshots.rekey(oldKey, newKey, persistedId)
302
+
303
+ // Rekey metadata FIRST, then set persisted mapping (which sets existsOnServer)
304
+ this.meta.rekey(oldKey, newKey)
305
+ this.meta.mapTempIdToPersistedId(newKey, persistedId)
306
+
307
+ // Rekey subscriptions, parent-child relationships, and relation subscribers
308
+ this.subscriptions.rekey(oldKey, newKey, oldKeyPrefix, newKeyPrefix)
309
+
310
+ // Rekey relations/hasMany owned by this entity (parent key changes)
311
+ this.relations.rekeyOwner(oldKeyPrefix, newKeyPrefix)
312
+
313
+ // Replace tempId with persistedId in all relation/hasMany VALUE references
314
+ this.relations.replaceEntityId(tempId, persistedId)
315
+
316
+ // Rekey errors, touched state, and propagation tracking
317
+ this.errors.rekey(oldKey, newKey, oldKeyPrefix, newKeyPrefix)
318
+ this.touched.rekey(oldKeyPrefix, newKeyPrefix)
319
+ this.rekeyPropagatedData(oldKeyPrefix, newKeyPrefix)
320
+
321
+ // Notify on the NEW key so React picks up the change
322
+ this.notifyEntitySubscribers(newKey)
224
323
  }
225
324
 
226
325
  getPersistedId(entityType: string, id: string): string | null {
@@ -372,6 +471,17 @@ export class SnapshotStore implements SnapshotVersionBumper {
372
471
  this.notifyRelationSubscribers(key)
373
472
  }
374
473
 
474
+ connectExistingToHasMany(
475
+ parentType: string,
476
+ parentId: string,
477
+ fieldName: string,
478
+ itemId: string,
479
+ ): void {
480
+ const key = this.getRelationKey(parentType, parentId, fieldName)
481
+ this.relations.connectExistingToHasMany(key, itemId)
482
+ this.notifyRelationSubscribers(key)
483
+ }
484
+
375
485
  removeFromHasMany(
376
486
  parentType: string,
377
487
  parentId: string,
@@ -760,6 +870,7 @@ export class SnapshotStore implements SnapshotVersionBumper {
760
870
  this.relations.clear()
761
871
  this.errors.clear()
762
872
  this.touched.clear()
873
+ this.lastPropagatedData.clear()
763
874
 
764
875
  this.subscriptions.notify()
765
876
  }
@@ -31,9 +31,28 @@ export class SubscriptionManager {
31
31
  /** Parent-child relationships: childKey -> Set of parentKeys */
32
32
  private readonly childToParents = new Map<string, Set<string>>()
33
33
 
34
+ /** Maps old keys → new keys after rekey, so unsubscribe closures can find migrated callbacks */
35
+ private readonly rekeyedKeys = new Map<string, string>()
36
+
34
37
  /** Global version number for change detection */
35
38
  private globalVersion = 0
36
39
 
40
+ /**
41
+ * Resolves a key through the rekey redirect chain.
42
+ * The rekey() method collapses chains (A→C instead of A→B→C), so this should
43
+ * always resolve in one hop. The loop with depth limit is a safety net in case
44
+ * chain collapsing has a bug — prevents infinite loops instead of silently hanging.
45
+ */
46
+ private resolveKey(key: string): string {
47
+ let resolved = key
48
+ for (let depth = 0; depth < 10; depth++) {
49
+ const next = this.rekeyedKeys.get(resolved)
50
+ if (!next || next === resolved) return resolved
51
+ resolved = next
52
+ }
53
+ return resolved
54
+ }
55
+
37
56
  /**
38
57
  * Subscribe to changes on a specific entity.
39
58
  * Returns unsubscribe function.
@@ -46,7 +65,8 @@ export class SubscriptionManager {
46
65
  this.entitySubscribers.get(key)!.add(callback)
47
66
 
48
67
  return () => {
49
- this.entitySubscribers.get(key)?.delete(callback)
68
+ const resolvedKey = this.resolveKey(key)
69
+ this.entitySubscribers.get(resolvedKey)?.delete(callback)
50
70
  }
51
71
  }
52
72
 
@@ -62,7 +82,8 @@ export class SubscriptionManager {
62
82
  this.relationSubscribers.get(key)!.add(callback)
63
83
 
64
84
  return () => {
65
- this.relationSubscribers.get(key)?.delete(callback)
85
+ const resolvedKey = this.resolveKey(key)
86
+ this.relationSubscribers.get(resolvedKey)?.delete(callback)
66
87
  }
67
88
  }
68
89
 
@@ -238,4 +259,63 @@ export class SubscriptionManager {
238
259
  sub()
239
260
  }
240
261
  }
262
+
263
+ /**
264
+ * Moves subscriptions and parent-child relationships from oldKey to newKey.
265
+ * Also rekeys relation subscribers under oldKeyPrefix to newKeyPrefix.
266
+ * Registers redirects so unsubscribe closures can find migrated callbacks.
267
+ */
268
+ rekey(oldKey: string, newKey: string, oldKeyPrefix: string, newKeyPrefix: string): void {
269
+ // Register redirect for entity key (update existing chains first)
270
+ for (const [fromKey, toKey] of this.rekeyedKeys) {
271
+ if (toKey === oldKey) {
272
+ this.rekeyedKeys.set(fromKey, newKey)
273
+ }
274
+ }
275
+ this.rekeyedKeys.set(oldKey, newKey)
276
+
277
+ // Move entity subscribers
278
+ const entitySubs = this.entitySubscribers.get(oldKey)
279
+ if (entitySubs) {
280
+ this.entitySubscribers.delete(oldKey)
281
+ this.entitySubscribers.set(newKey, entitySubs)
282
+ }
283
+
284
+ // Move relation subscribers by prefix (e.g. "Entity:tempId:" → "Entity:persistedId:")
285
+ const toMoveRelations: [string, Set<Subscriber>][] = []
286
+ for (const [key, subs] of this.relationSubscribers) {
287
+ if (key.startsWith(oldKeyPrefix)) {
288
+ toMoveRelations.push([key, subs])
289
+ }
290
+ }
291
+ for (const [oldRelKey, subs] of toMoveRelations) {
292
+ const newRelKey = newKeyPrefix + oldRelKey.slice(oldKeyPrefix.length)
293
+
294
+ // Register redirect for relation key (update existing chains first)
295
+ for (const [fromKey, toKey] of this.rekeyedKeys) {
296
+ if (toKey === oldRelKey) {
297
+ this.rekeyedKeys.set(fromKey, newRelKey)
298
+ }
299
+ }
300
+ this.rekeyedKeys.set(oldRelKey, newRelKey)
301
+
302
+ this.relationSubscribers.delete(oldRelKey)
303
+ this.relationSubscribers.set(newRelKey, subs)
304
+ }
305
+
306
+ // Move parent-child: update child→parent mappings
307
+ const parents = this.childToParents.get(oldKey)
308
+ if (parents) {
309
+ this.childToParents.delete(oldKey)
310
+ this.childToParents.set(newKey, parents)
311
+ }
312
+
313
+ // Update parent-child: replace oldKey in any parent sets that reference it
314
+ for (const parentSet of this.childToParents.values()) {
315
+ if (parentSet.has(oldKey)) {
316
+ parentSet.delete(oldKey)
317
+ parentSet.add(newKey)
318
+ }
319
+ }
320
+ }
241
321
  }
@@ -36,6 +36,22 @@ export class TouchedStore {
36
36
  }
37
37
  }
38
38
 
39
+ /**
40
+ * Rekeys all touched fields from oldKeyPrefix to newKeyPrefix.
41
+ */
42
+ rekey(oldKeyPrefix: string, newKeyPrefix: string): void {
43
+ const toMove: [string, boolean][] = []
44
+ for (const [key, value] of this.touchedFields) {
45
+ if (key.startsWith(oldKeyPrefix)) {
46
+ toMove.push([key, value])
47
+ }
48
+ }
49
+ for (const [oldKey, value] of toMove) {
50
+ this.touchedFields.delete(oldKey)
51
+ this.touchedFields.set(newKeyPrefix + oldKey.slice(oldKeyPrefix.length), value)
52
+ }
53
+ }
54
+
39
55
  clear(): void {
40
56
  this.touchedFields.clear()
41
57
  }