@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.
Files changed (234) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +40 -0
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
  3. package/dist/adapter/MockAdapter.d.ts +81 -0
  4. package/dist/adapter/MockAdapter.d.ts.map +1 -0
  5. package/dist/adapter/MockQueryEngine.d.ts +43 -0
  6. package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
  7. package/dist/adapter/index.d.ts +5 -0
  8. package/dist/adapter/index.d.ts.map +1 -0
  9. package/dist/adapter/types.d.ts +184 -0
  10. package/dist/adapter/types.d.ts.map +1 -0
  11. package/dist/brand/ComponentBrand.d.ts +25 -0
  12. package/dist/brand/ComponentBrand.d.ts.map +1 -0
  13. package/dist/contember/index.d.ts +10 -0
  14. package/dist/contember/index.d.ts.map +1 -0
  15. package/dist/core/ActionDispatcher.d.ts +64 -0
  16. package/dist/core/ActionDispatcher.d.ts.map +1 -0
  17. package/dist/core/Disposable.d.ts +57 -0
  18. package/dist/core/Disposable.d.ts.map +1 -0
  19. package/dist/core/EntityLoader.d.ts +80 -0
  20. package/dist/core/EntityLoader.d.ts.map +1 -0
  21. package/dist/core/PersistenceManager.d.ts +126 -0
  22. package/dist/core/PersistenceManager.d.ts.map +1 -0
  23. package/dist/core/SelectionResolver.d.ts +21 -0
  24. package/dist/core/SelectionResolver.d.ts.map +1 -0
  25. package/dist/core/actionClassification.d.ts +48 -0
  26. package/dist/core/actionClassification.d.ts.map +1 -0
  27. package/dist/core/actions.d.ts +301 -0
  28. package/dist/core/actions.d.ts.map +1 -0
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/dataview/filterHandlers.d.ts +18 -0
  32. package/dist/dataview/filterHandlers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +3 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/types.d.ts +164 -0
  36. package/dist/dataview/types.d.ts.map +1 -0
  37. package/dist/errors/UnfetchedFieldError.d.ts +11 -0
  38. package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
  39. package/dist/errors/errorPathResolver.d.ts +52 -0
  40. package/dist/errors/errorPathResolver.d.ts.map +1 -0
  41. package/dist/errors/index.d.ts +5 -0
  42. package/dist/errors/index.d.ts.map +1 -0
  43. package/dist/errors/pathMapper.d.ts +42 -0
  44. package/dist/errors/pathMapper.d.ts.map +1 -0
  45. package/dist/errors/types.d.ts +131 -0
  46. package/dist/errors/types.d.ts.map +1 -0
  47. package/dist/events/EventEmitter.d.ts +97 -0
  48. package/dist/events/EventEmitter.d.ts.map +1 -0
  49. package/dist/events/eventFactory.d.ts +40 -0
  50. package/dist/events/eventFactory.d.ts.map +1 -0
  51. package/dist/events/index.d.ts +15 -0
  52. package/dist/events/index.d.ts.map +1 -0
  53. package/dist/events/types.d.ts +264 -0
  54. package/dist/events/types.d.ts.map +1 -0
  55. package/dist/handles/BaseHandle.d.ts +64 -0
  56. package/dist/handles/BaseHandle.d.ts.map +1 -0
  57. package/dist/handles/EntityHandle.d.ts +208 -0
  58. package/dist/handles/EntityHandle.d.ts.map +1 -0
  59. package/dist/handles/FieldHandle.d.ts +111 -0
  60. package/dist/handles/FieldHandle.d.ts.map +1 -0
  61. package/dist/handles/HasManyListHandle.d.ts +166 -0
  62. package/dist/handles/HasManyListHandle.d.ts.map +1 -0
  63. package/dist/handles/HasOneHandle.d.ts +168 -0
  64. package/dist/handles/HasOneHandle.d.ts.map +1 -0
  65. package/dist/handles/PlaceholderHandle.d.ts +117 -0
  66. package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
  67. package/dist/handles/index.d.ts +8 -0
  68. package/dist/handles/index.d.ts.map +1 -0
  69. package/dist/handles/proxyFactory.d.ts +66 -0
  70. package/dist/handles/proxyFactory.d.ts.map +1 -0
  71. package/dist/handles/types.d.ts +430 -0
  72. package/dist/handles/types.d.ts.map +1 -0
  73. package/dist/index.d.ts +53 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/internal.d.ts +11 -0
  76. package/dist/internal.d.ts.map +1 -0
  77. package/dist/persistence/BatchPersister.d.ts +142 -0
  78. package/dist/persistence/BatchPersister.d.ts.map +1 -0
  79. package/dist/persistence/ChangeRegistry.d.ts +87 -0
  80. package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
  81. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
  82. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
  83. package/dist/persistence/MutationCollector.d.ts +96 -0
  84. package/dist/persistence/MutationCollector.d.ts.map +1 -0
  85. package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
  86. package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
  87. package/dist/persistence/index.d.ts +7 -0
  88. package/dist/persistence/index.d.ts.map +1 -0
  89. package/dist/persistence/types.d.ts +189 -0
  90. package/dist/persistence/types.d.ts.map +1 -0
  91. package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
  92. package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
  93. package/dist/roles/index.d.ts +11 -0
  94. package/dist/roles/index.d.ts.map +1 -0
  95. package/dist/roles/types.d.ts +242 -0
  96. package/dist/roles/types.d.ts.map +1 -0
  97. package/dist/schema/ContemberSchema.d.ts +98 -0
  98. package/dist/schema/ContemberSchema.d.ts.map +1 -0
  99. package/dist/schema/SchemaLoader.d.ts +28 -0
  100. package/dist/schema/SchemaLoader.d.ts.map +1 -0
  101. package/dist/schema/SchemaRegistry.d.ts +95 -0
  102. package/dist/schema/SchemaRegistry.d.ts.map +1 -0
  103. package/dist/schema/index.d.ts +6 -0
  104. package/dist/schema/index.d.ts.map +1 -0
  105. package/dist/schema/roles.d.ts +62 -0
  106. package/dist/schema/roles.d.ts.map +1 -0
  107. package/dist/schema/types.d.ts +179 -0
  108. package/dist/schema/types.d.ts.map +1 -0
  109. package/dist/selection/SelectionMetaCollector.d.ts +32 -0
  110. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
  111. package/dist/selection/SelectionScope.d.ts +77 -0
  112. package/dist/selection/SelectionScope.d.ts.map +1 -0
  113. package/dist/selection/buildQuery.d.ts +45 -0
  114. package/dist/selection/buildQuery.d.ts.map +1 -0
  115. package/dist/selection/createFragment.d.ts +29 -0
  116. package/dist/selection/createFragment.d.ts.map +1 -0
  117. package/dist/selection/createSelectionBuilder.d.ts +18 -0
  118. package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
  119. package/dist/selection/index.d.ts +8 -0
  120. package/dist/selection/index.d.ts.map +1 -0
  121. package/dist/selection/queryTypes.d.ts +84 -0
  122. package/dist/selection/queryTypes.d.ts.map +1 -0
  123. package/dist/selection/types.d.ts +238 -0
  124. package/dist/selection/types.d.ts.map +1 -0
  125. package/dist/store/DirtyTracker.d.ts +26 -0
  126. package/dist/store/DirtyTracker.d.ts.map +1 -0
  127. package/dist/store/EntityMetaStore.d.ts +52 -0
  128. package/dist/store/EntityMetaStore.d.ts.map +1 -0
  129. package/dist/store/EntitySnapshotStore.d.ts +63 -0
  130. package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
  131. package/dist/store/ErrorStore.d.ts +82 -0
  132. package/dist/store/ErrorStore.d.ts.map +1 -0
  133. package/dist/store/RelationStore.d.ts +175 -0
  134. package/dist/store/RelationStore.d.ts.map +1 -0
  135. package/dist/store/SnapshotStore.d.ts +143 -0
  136. package/dist/store/SnapshotStore.d.ts.map +1 -0
  137. package/dist/store/SubscriptionManager.d.ts +90 -0
  138. package/dist/store/SubscriptionManager.d.ts.map +1 -0
  139. package/dist/store/TouchedStore.d.ts +24 -0
  140. package/dist/store/TouchedStore.d.ts.map +1 -0
  141. package/dist/store/entityId.d.ts +29 -0
  142. package/dist/store/entityId.d.ts.map +1 -0
  143. package/dist/store/index.d.ts +1 -0
  144. package/dist/store/index.d.ts.map +1 -0
  145. package/dist/store/snapshots.d.ts +144 -0
  146. package/dist/store/snapshots.d.ts.map +1 -0
  147. package/dist/undo/UndoManager.d.ts +120 -0
  148. package/dist/undo/UndoManager.d.ts.map +1 -0
  149. package/dist/undo/index.d.ts +3 -0
  150. package/dist/undo/index.d.ts.map +1 -0
  151. package/dist/undo/types.d.ts +74 -0
  152. package/dist/undo/types.d.ts.map +1 -0
  153. package/dist/utils/aliasGenerator.d.ts +25 -0
  154. package/dist/utils/aliasGenerator.d.ts.map +1 -0
  155. package/dist/utils/deepEqual.d.ts +6 -0
  156. package/dist/utils/deepEqual.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +3 -0
  158. package/dist/utils/index.d.ts.map +1 -0
  159. package/package.json +29 -0
  160. package/src/adapter/ContemberAdapter.ts +239 -0
  161. package/src/adapter/MockAdapter.ts +394 -0
  162. package/src/adapter/MockQueryEngine.ts +236 -0
  163. package/src/adapter/index.ts +21 -0
  164. package/src/adapter/types.ts +199 -0
  165. package/src/brand/ComponentBrand.ts +28 -0
  166. package/src/contember/index.ts +17 -0
  167. package/src/core/ActionDispatcher.ts +450 -0
  168. package/src/core/Disposable.ts +126 -0
  169. package/src/core/EntityLoader.ts +169 -0
  170. package/src/core/SelectionResolver.ts +78 -0
  171. package/src/core/actionClassification.ts +196 -0
  172. package/src/core/actions.ts +491 -0
  173. package/src/core/index.ts +20 -0
  174. package/src/dataview/filterHandlers.ts +443 -0
  175. package/src/dataview/index.ts +36 -0
  176. package/src/dataview/types.ts +220 -0
  177. package/src/errors/UnfetchedFieldError.ts +18 -0
  178. package/src/errors/errorPathResolver.ts +239 -0
  179. package/src/errors/index.ts +40 -0
  180. package/src/errors/pathMapper.ts +40 -0
  181. package/src/errors/types.ts +223 -0
  182. package/src/events/EventEmitter.ts +447 -0
  183. package/src/events/eventFactory.ts +430 -0
  184. package/src/events/index.ts +89 -0
  185. package/src/events/types.ts +374 -0
  186. package/src/handles/BaseHandle.ts +100 -0
  187. package/src/handles/EntityHandle.ts +538 -0
  188. package/src/handles/FieldHandle.ts +268 -0
  189. package/src/handles/HasManyListHandle.ts +500 -0
  190. package/src/handles/HasOneHandle.ts +500 -0
  191. package/src/handles/PlaceholderHandle.ts +321 -0
  192. package/src/handles/index.ts +37 -0
  193. package/src/handles/proxyFactory.ts +218 -0
  194. package/src/handles/types.ts +743 -0
  195. package/src/index.ts +384 -0
  196. package/src/internal.ts +19 -0
  197. package/src/persistence/BatchPersister.ts +911 -0
  198. package/src/persistence/ChangeRegistry.ts +150 -0
  199. package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
  200. package/src/persistence/MutationCollector.ts +567 -0
  201. package/src/persistence/MutationSchemaProvider.ts +33 -0
  202. package/src/persistence/index.ts +23 -0
  203. package/src/persistence/types.ts +220 -0
  204. package/src/schema/ContemberSchema.ts +132 -0
  205. package/src/schema/SchemaLoader.ts +93 -0
  206. package/src/schema/SchemaRegistry.ts +286 -0
  207. package/src/schema/index.ts +51 -0
  208. package/src/schema/roles.ts +83 -0
  209. package/src/schema/types.ts +238 -0
  210. package/src/selection/SelectionMetaCollector.ts +122 -0
  211. package/src/selection/SelectionScope.ts +208 -0
  212. package/src/selection/buildQuery.ts +134 -0
  213. package/src/selection/createFragment.ts +50 -0
  214. package/src/selection/createSelectionBuilder.ts +283 -0
  215. package/src/selection/index.ts +31 -0
  216. package/src/selection/queryTypes.ts +135 -0
  217. package/src/selection/types.ts +320 -0
  218. package/src/store/DirtyTracker.ts +117 -0
  219. package/src/store/EntityMetaStore.ts +143 -0
  220. package/src/store/EntitySnapshotStore.ts +260 -0
  221. package/src/store/ErrorStore.ts +251 -0
  222. package/src/store/RelationStore.ts +696 -0
  223. package/src/store/SnapshotStore.ts +765 -0
  224. package/src/store/SubscriptionManager.ts +241 -0
  225. package/src/store/TouchedStore.ts +42 -0
  226. package/src/store/entityId.ts +43 -0
  227. package/src/store/index.ts +2 -0
  228. package/src/store/snapshots.ts +203 -0
  229. package/src/undo/UndoManager.ts +418 -0
  230. package/src/undo/index.ts +9 -0
  231. package/src/undo/types.ts +84 -0
  232. package/src/utils/aliasGenerator.ts +56 -0
  233. package/src/utils/deepEqual.ts +33 -0
  234. package/src/utils/index.ts +2 -0
@@ -0,0 +1,260 @@
1
+ import {
2
+ createEntitySnapshot,
3
+ type EntitySnapshot,
4
+ } from './snapshots.js'
5
+
6
+ /**
7
+ * Manages entity snapshots — core CRUD for immutable entity data.
8
+ *
9
+ * Entity snapshots are keyed by composite strings (e.g., "entityType:id").
10
+ * All snapshots are immutable (frozen) objects. Mutations create new snapshot instances.
11
+ *
12
+ * Follows the same sub-store pattern as ErrorStore, RelationStore, etc.
13
+ * SnapshotStore delegates entity snapshot operations here.
14
+ */
15
+ export class EntitySnapshotStore {
16
+ private readonly snapshots = new Map<string, EntitySnapshot>()
17
+
18
+ get(key: string): EntitySnapshot | undefined {
19
+ return this.snapshots.get(key)
20
+ }
21
+
22
+ has(key: string): boolean {
23
+ return this.snapshots.has(key)
24
+ }
25
+
26
+ /**
27
+ * Sets entity data, merging with existing data if present.
28
+ * Returns the new snapshot. Does NOT handle notification or meta updates.
29
+ */
30
+ setData<T extends object>(
31
+ key: string,
32
+ id: string,
33
+ entityType: string,
34
+ data: T,
35
+ isServerData: boolean,
36
+ ): EntitySnapshot<T> {
37
+ const existing = this.snapshots.get(key)
38
+
39
+ const mergedData = existing?.data
40
+ ? { ...existing.data, ...data } as T
41
+ : data
42
+
43
+ const serverData = isServerData
44
+ ? (existing?.serverData ? { ...existing.serverData, ...data } as T : data)
45
+ : (existing?.serverData as T) ?? mergedData
46
+
47
+ const newSnapshot = createEntitySnapshot(
48
+ id,
49
+ entityType,
50
+ mergedData,
51
+ serverData,
52
+ (existing?.version ?? 0) + 1,
53
+ )
54
+
55
+ this.snapshots.set(key, newSnapshot)
56
+ return newSnapshot
57
+ }
58
+
59
+ /**
60
+ * Updates specific fields on an existing entity snapshot.
61
+ * Returns the new snapshot, or undefined if entity doesn't exist.
62
+ */
63
+ updateFields<T extends object>(
64
+ key: string,
65
+ updates: Partial<T>,
66
+ ): EntitySnapshot<T> | undefined {
67
+ const existing = this.snapshots.get(key)
68
+ if (!existing) return undefined
69
+
70
+ const newData = { ...existing.data, ...updates } as T
71
+ const newSnapshot = createEntitySnapshot(
72
+ existing.id,
73
+ existing.entityType,
74
+ newData,
75
+ existing.serverData as T,
76
+ existing.version + 1,
77
+ )
78
+
79
+ this.snapshots.set(key, newSnapshot)
80
+ return newSnapshot
81
+ }
82
+
83
+ /**
84
+ * Sets a nested field value on an existing entity snapshot.
85
+ * Returns true if the snapshot was updated, false if entity doesn't exist.
86
+ */
87
+ setFieldValue(key: string, fieldPath: string[], value: unknown): boolean {
88
+ const existing = this.snapshots.get(key)
89
+ if (!existing) return false
90
+
91
+ const newData = setNestedValue({ ...existing.data }, fieldPath, value)
92
+ const newSnapshot = createEntitySnapshot(
93
+ existing.id,
94
+ existing.entityType,
95
+ newData,
96
+ existing.serverData,
97
+ existing.version + 1,
98
+ )
99
+
100
+ this.snapshots.set(key, newSnapshot)
101
+ return true
102
+ }
103
+
104
+ /**
105
+ * Commits current data as server data (serverData = data).
106
+ */
107
+ commit(key: string): void {
108
+ const existing = this.snapshots.get(key)
109
+ if (!existing) return
110
+
111
+ const newSnapshot = createEntitySnapshot(
112
+ existing.id,
113
+ existing.entityType,
114
+ existing.data,
115
+ existing.data,
116
+ existing.version + 1,
117
+ )
118
+
119
+ this.snapshots.set(key, newSnapshot)
120
+ }
121
+
122
+ /**
123
+ * Resets data to server data (data = serverData).
124
+ */
125
+ reset(key: string): void {
126
+ const existing = this.snapshots.get(key)
127
+ if (!existing) return
128
+
129
+ const newSnapshot = createEntitySnapshot(
130
+ existing.id,
131
+ existing.entityType,
132
+ existing.serverData,
133
+ existing.serverData,
134
+ existing.version + 1,
135
+ )
136
+
137
+ this.snapshots.set(key, newSnapshot)
138
+ }
139
+
140
+ /**
141
+ * Removes an entity snapshot.
142
+ */
143
+ remove(key: string): void {
144
+ this.snapshots.delete(key)
145
+ }
146
+
147
+ /**
148
+ * Bumps the version of an entity snapshot without changing data.
149
+ * Used by SubscriptionManager when child entities change.
150
+ */
151
+ bumpVersion(key: string): void {
152
+ const existing = this.snapshots.get(key)
153
+ if (!existing) return
154
+
155
+ const newSnapshot = createEntitySnapshot(
156
+ existing.id,
157
+ existing.entityType,
158
+ existing.data,
159
+ existing.serverData,
160
+ existing.version + 1,
161
+ )
162
+
163
+ this.snapshots.set(key, newSnapshot)
164
+ }
165
+
166
+ /**
167
+ * Commits specific fields by copying their current values to server data.
168
+ */
169
+ commitFields(key: string, fieldNames: string[]): void {
170
+ const existing = this.snapshots.get(key)
171
+ if (!existing) return
172
+
173
+ const data = existing.data as Record<string, unknown>
174
+ const serverData = existing.serverData as Record<string, unknown>
175
+
176
+ const newServerData = { ...serverData }
177
+ for (const fieldName of fieldNames) {
178
+ if (fieldName in data) {
179
+ newServerData[fieldName] = data[fieldName]
180
+ }
181
+ }
182
+
183
+ const newSnapshot = createEntitySnapshot(
184
+ existing.id,
185
+ existing.entityType,
186
+ data,
187
+ newServerData,
188
+ existing.version + 1,
189
+ )
190
+
191
+ this.snapshots.set(key, newSnapshot)
192
+ }
193
+
194
+ /**
195
+ * Exports snapshots for the given keys.
196
+ */
197
+ exportSnapshots(keys: string[]): Map<string, EntitySnapshot> {
198
+ const result = new Map<string, EntitySnapshot>()
199
+ for (const key of keys) {
200
+ const snapshot = this.snapshots.get(key)
201
+ if (snapshot) {
202
+ result.set(key, snapshot)
203
+ }
204
+ }
205
+ return result
206
+ }
207
+
208
+ /**
209
+ * Imports snapshots, overwriting any existing entries.
210
+ * Returns the set of keys that were imported.
211
+ */
212
+ importSnapshots(snapshots: Map<string, EntitySnapshot>): Set<string> {
213
+ const keys = new Set<string>()
214
+ for (const [key, snapshot] of snapshots) {
215
+ this.snapshots.set(key, snapshot)
216
+ keys.add(key)
217
+ }
218
+ return keys
219
+ }
220
+
221
+ keys(): IterableIterator<string> {
222
+ return this.snapshots.keys()
223
+ }
224
+
225
+ clear(): void {
226
+ this.snapshots.clear()
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Sets a nested value in an object, returning a new object.
232
+ */
233
+ function setNestedValue<T extends Record<string, unknown>>(
234
+ obj: T,
235
+ path: string[],
236
+ value: unknown,
237
+ ): T {
238
+ if (path.length === 0) return obj
239
+
240
+ const result = { ...obj }
241
+ let current: Record<string, unknown> = result
242
+
243
+ for (let i = 0; i < path.length - 1; i++) {
244
+ const key = path[i]!
245
+ const nextValue = current[key]
246
+
247
+ if (typeof nextValue === 'object' && nextValue !== null) {
248
+ current[key] = { ...nextValue as Record<string, unknown> }
249
+ } else {
250
+ current[key] = {}
251
+ }
252
+
253
+ current = current[key] as Record<string, unknown>
254
+ }
255
+
256
+ const lastKey = path[path.length - 1]!
257
+ current[lastKey] = value
258
+
259
+ return result
260
+ }
@@ -0,0 +1,251 @@
1
+ import type { ErrorState, FieldError } from '../errors/types.js'
2
+ import { filterStickyErrors } from '../errors/types.js'
3
+
4
+ /**
5
+ * Manages error state for fields, entities, and relations.
6
+ *
7
+ * Errors are keyed by composite strings:
8
+ * - Field errors: "entityType:id:fieldName"
9
+ * - Entity errors: "entityType:id"
10
+ * - Relation errors: "entityType:id:relationName"
11
+ */
12
+ export class ErrorStore {
13
+ /** Field errors keyed by "entityType:id:fieldName" */
14
+ private readonly fieldErrors = new Map<string, ErrorState>()
15
+
16
+ /** Entity-level errors keyed by "entityType:id" */
17
+ private readonly entityErrors = new Map<string, ErrorState>()
18
+
19
+ /** Relation errors keyed by "entityType:id:relationName" */
20
+ private readonly relationErrors = new Map<string, ErrorState>()
21
+
22
+ // ==================== Field Errors ====================
23
+
24
+ /**
25
+ * Gets field errors for a specific field.
26
+ */
27
+ getFieldErrors(fieldKey: string): readonly FieldError[] {
28
+ return this.fieldErrors.get(fieldKey)?.errors ?? []
29
+ }
30
+
31
+ /**
32
+ * Adds an error to a field.
33
+ */
34
+ addFieldError(fieldKey: string, error: FieldError): void {
35
+ const existing = this.fieldErrors.get(fieldKey)
36
+ const errors = existing ? [...existing.errors, error] : [error]
37
+ this.fieldErrors.set(fieldKey, { errors, version: (existing?.version ?? 0) + 1 })
38
+ }
39
+
40
+ /**
41
+ * Clears field errors, optionally filtering by source.
42
+ */
43
+ clearFieldErrors(fieldKey: string, source?: 'client' | 'server'): void {
44
+ const existing = this.fieldErrors.get(fieldKey)
45
+ if (!existing) return
46
+
47
+ if (source === undefined) {
48
+ this.fieldErrors.delete(fieldKey)
49
+ } else {
50
+ const filtered = existing.errors.filter(e => e.source !== source)
51
+ if (filtered.length === 0) {
52
+ this.fieldErrors.delete(fieldKey)
53
+ } else {
54
+ this.fieldErrors.set(fieldKey, { errors: filtered, version: existing.version + 1 })
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Clears non-sticky client errors for a field.
61
+ * Called when field value changes.
62
+ * Returns true if any errors were removed.
63
+ */
64
+ clearNonStickyFieldErrors(fieldKey: string): boolean {
65
+ const existing = this.fieldErrors.get(fieldKey)
66
+ if (!existing) return false
67
+
68
+ const filtered = filterStickyErrors(existing.errors)
69
+ if (filtered.length === existing.errors.length) return false
70
+
71
+ if (filtered.length === 0) {
72
+ this.fieldErrors.delete(fieldKey)
73
+ } else {
74
+ this.fieldErrors.set(fieldKey, { errors: filtered, version: existing.version + 1 })
75
+ }
76
+ return true
77
+ }
78
+
79
+ // ==================== Entity Errors ====================
80
+
81
+ /**
82
+ * Gets entity-level errors.
83
+ */
84
+ getEntityErrors(entityKey: string): readonly FieldError[] {
85
+ return this.entityErrors.get(entityKey)?.errors ?? []
86
+ }
87
+
88
+ /**
89
+ * Adds an entity-level error.
90
+ */
91
+ addEntityError(entityKey: string, error: FieldError): void {
92
+ const existing = this.entityErrors.get(entityKey)
93
+ const errors = existing ? [...existing.errors, error] : [error]
94
+ this.entityErrors.set(entityKey, { errors, version: (existing?.version ?? 0) + 1 })
95
+ }
96
+
97
+ /**
98
+ * Clears entity-level errors, optionally filtering by source.
99
+ */
100
+ clearEntityErrors(entityKey: string, source?: 'client' | 'server'): void {
101
+ const existing = this.entityErrors.get(entityKey)
102
+ if (!existing) return
103
+
104
+ if (source === undefined) {
105
+ this.entityErrors.delete(entityKey)
106
+ } else {
107
+ const filtered = existing.errors.filter(e => e.source !== source)
108
+ if (filtered.length === 0) {
109
+ this.entityErrors.delete(entityKey)
110
+ } else {
111
+ this.entityErrors.set(entityKey, { errors: filtered, version: existing.version + 1 })
112
+ }
113
+ }
114
+ }
115
+
116
+ // ==================== Relation Errors ====================
117
+
118
+ /**
119
+ * Gets relation errors.
120
+ */
121
+ getRelationErrors(relationKey: string): readonly FieldError[] {
122
+ return this.relationErrors.get(relationKey)?.errors ?? []
123
+ }
124
+
125
+ /**
126
+ * Adds a relation error.
127
+ */
128
+ addRelationError(relationKey: string, error: FieldError): void {
129
+ const existing = this.relationErrors.get(relationKey)
130
+ const errors = existing ? [...existing.errors, error] : [error]
131
+ this.relationErrors.set(relationKey, { errors, version: (existing?.version ?? 0) + 1 })
132
+ }
133
+
134
+ /**
135
+ * Clears relation errors, optionally filtering by source.
136
+ */
137
+ clearRelationErrors(relationKey: string, source?: 'client' | 'server'): void {
138
+ const existing = this.relationErrors.get(relationKey)
139
+ if (!existing) return
140
+
141
+ if (source === undefined) {
142
+ this.relationErrors.delete(relationKey)
143
+ } else {
144
+ const filtered = existing.errors.filter(e => e.source !== source)
145
+ if (filtered.length === 0) {
146
+ this.relationErrors.delete(relationKey)
147
+ } else {
148
+ this.relationErrors.set(relationKey, { errors: filtered, version: existing.version + 1 })
149
+ }
150
+ }
151
+ }
152
+
153
+ // ==================== Bulk Operations ====================
154
+
155
+ /**
156
+ * Clears all server errors for an entity (entity-level, fields, and relations).
157
+ * Called before persist to clear stale server errors.
158
+ */
159
+ clearAllServerErrors(entityKey: string, keyPrefix: string): void {
160
+ this.clearEntityErrors(entityKey, 'server')
161
+
162
+ for (const key of this.fieldErrors.keys()) {
163
+ if (key.startsWith(keyPrefix)) {
164
+ this.clearFieldErrors(key, 'server')
165
+ }
166
+ }
167
+
168
+ for (const key of this.relationErrors.keys()) {
169
+ if (key.startsWith(keyPrefix)) {
170
+ this.clearRelationErrors(key, 'server')
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Clears all errors for an entity (entity-level, fields, and relations).
177
+ */
178
+ clearAllErrors(entityKey: string, keyPrefix: string): void {
179
+ this.entityErrors.delete(entityKey)
180
+
181
+ for (const key of [...this.fieldErrors.keys()]) {
182
+ if (key.startsWith(keyPrefix)) {
183
+ this.fieldErrors.delete(key)
184
+ }
185
+ }
186
+
187
+ for (const key of [...this.relationErrors.keys()]) {
188
+ if (key.startsWith(keyPrefix)) {
189
+ this.relationErrors.delete(key)
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Checks if an entity has any client errors (entity-level, fields, or relations).
196
+ * Used to block persist when validation errors exist.
197
+ */
198
+ hasClientErrors(entityKey: string, keyPrefix: string): boolean {
199
+ const entityErrs = this.entityErrors.get(entityKey)
200
+ if (entityErrs?.errors.some(e => e.source === 'client')) {
201
+ return true
202
+ }
203
+
204
+ for (const [key, state] of this.fieldErrors) {
205
+ if (key.startsWith(keyPrefix) && state.errors.some(e => e.source === 'client')) {
206
+ return true
207
+ }
208
+ }
209
+
210
+ for (const [key, state] of this.relationErrors) {
211
+ if (key.startsWith(keyPrefix) && state.errors.some(e => e.source === 'client')) {
212
+ return true
213
+ }
214
+ }
215
+
216
+ return false
217
+ }
218
+
219
+ /**
220
+ * Checks if an entity has any errors at all.
221
+ */
222
+ hasAnyErrors(entityKey: string, keyPrefix: string): boolean {
223
+ const entityErrs = this.entityErrors.get(entityKey)
224
+ if (entityErrs && entityErrs.errors.length > 0) {
225
+ return true
226
+ }
227
+
228
+ for (const [key, state] of this.fieldErrors) {
229
+ if (key.startsWith(keyPrefix) && state.errors.length > 0) {
230
+ return true
231
+ }
232
+ }
233
+
234
+ for (const [key, state] of this.relationErrors) {
235
+ if (key.startsWith(keyPrefix) && state.errors.length > 0) {
236
+ return true
237
+ }
238
+ }
239
+
240
+ return false
241
+ }
242
+
243
+ /**
244
+ * Clears all error data.
245
+ */
246
+ clear(): void {
247
+ this.fieldErrors.clear()
248
+ this.entityErrors.clear()
249
+ this.relationErrors.clear()
250
+ }
251
+ }