@contember/bindx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/ContemberAdapter.d.ts +40 -0
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
- package/dist/adapter/MockAdapter.d.ts +81 -0
- package/dist/adapter/MockAdapter.d.ts.map +1 -0
- package/dist/adapter/MockQueryEngine.d.ts +43 -0
- package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +5 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +184 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/brand/ComponentBrand.d.ts +25 -0
- package/dist/brand/ComponentBrand.d.ts.map +1 -0
- package/dist/contember/index.d.ts +10 -0
- package/dist/contember/index.d.ts.map +1 -0
- package/dist/core/ActionDispatcher.d.ts +64 -0
- package/dist/core/ActionDispatcher.d.ts.map +1 -0
- package/dist/core/Disposable.d.ts +57 -0
- package/dist/core/Disposable.d.ts.map +1 -0
- package/dist/core/EntityLoader.d.ts +80 -0
- package/dist/core/EntityLoader.d.ts.map +1 -0
- package/dist/core/PersistenceManager.d.ts +126 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -0
- package/dist/core/SelectionResolver.d.ts +21 -0
- package/dist/core/SelectionResolver.d.ts.map +1 -0
- package/dist/core/actionClassification.d.ts +48 -0
- package/dist/core/actionClassification.d.ts.map +1 -0
- package/dist/core/actions.d.ts +301 -0
- package/dist/core/actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/dataview/filterHandlers.d.ts +18 -0
- package/dist/dataview/filterHandlers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +3 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/types.d.ts +164 -0
- package/dist/dataview/types.d.ts.map +1 -0
- package/dist/errors/UnfetchedFieldError.d.ts +11 -0
- package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
- package/dist/errors/errorPathResolver.d.ts +52 -0
- package/dist/errors/errorPathResolver.d.ts.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/pathMapper.d.ts +42 -0
- package/dist/errors/pathMapper.d.ts.map +1 -0
- package/dist/errors/types.d.ts +131 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/events/EventEmitter.d.ts +97 -0
- package/dist/events/EventEmitter.d.ts.map +1 -0
- package/dist/events/eventFactory.d.ts +40 -0
- package/dist/events/eventFactory.d.ts.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/types.d.ts +264 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/handles/BaseHandle.d.ts +64 -0
- package/dist/handles/BaseHandle.d.ts.map +1 -0
- package/dist/handles/EntityHandle.d.ts +208 -0
- package/dist/handles/EntityHandle.d.ts.map +1 -0
- package/dist/handles/FieldHandle.d.ts +111 -0
- package/dist/handles/FieldHandle.d.ts.map +1 -0
- package/dist/handles/HasManyListHandle.d.ts +166 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -0
- package/dist/handles/HasOneHandle.d.ts +168 -0
- package/dist/handles/HasOneHandle.d.ts.map +1 -0
- package/dist/handles/PlaceholderHandle.d.ts +117 -0
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
- package/dist/handles/index.d.ts +8 -0
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/proxyFactory.d.ts +66 -0
- package/dist/handles/proxyFactory.d.ts.map +1 -0
- package/dist/handles/types.d.ts +430 -0
- package/dist/handles/types.d.ts.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/persistence/BatchPersister.d.ts +142 -0
- package/dist/persistence/BatchPersister.d.ts.map +1 -0
- package/dist/persistence/ChangeRegistry.d.ts +87 -0
- package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
- package/dist/persistence/MutationCollector.d.ts +96 -0
- package/dist/persistence/MutationCollector.d.ts.map +1 -0
- package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
- package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/types.d.ts +189 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
- package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
- package/dist/roles/index.d.ts +11 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/types.d.ts +242 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/schema/ContemberSchema.d.ts +98 -0
- package/dist/schema/ContemberSchema.d.ts.map +1 -0
- package/dist/schema/SchemaLoader.d.ts +28 -0
- package/dist/schema/SchemaLoader.d.ts.map +1 -0
- package/dist/schema/SchemaRegistry.d.ts +95 -0
- package/dist/schema/SchemaRegistry.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/roles.d.ts +62 -0
- package/dist/schema/roles.d.ts.map +1 -0
- package/dist/schema/types.d.ts +179 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/selection/SelectionMetaCollector.d.ts +32 -0
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
- package/dist/selection/SelectionScope.d.ts +77 -0
- package/dist/selection/SelectionScope.d.ts.map +1 -0
- package/dist/selection/buildQuery.d.ts +45 -0
- package/dist/selection/buildQuery.d.ts.map +1 -0
- package/dist/selection/createFragment.d.ts +29 -0
- package/dist/selection/createFragment.d.ts.map +1 -0
- package/dist/selection/createSelectionBuilder.d.ts +18 -0
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
- package/dist/selection/index.d.ts +8 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/queryTypes.d.ts +84 -0
- package/dist/selection/queryTypes.d.ts.map +1 -0
- package/dist/selection/types.d.ts +238 -0
- package/dist/selection/types.d.ts.map +1 -0
- package/dist/store/DirtyTracker.d.ts +26 -0
- package/dist/store/DirtyTracker.d.ts.map +1 -0
- package/dist/store/EntityMetaStore.d.ts +52 -0
- package/dist/store/EntityMetaStore.d.ts.map +1 -0
- package/dist/store/EntitySnapshotStore.d.ts +63 -0
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
- package/dist/store/ErrorStore.d.ts +82 -0
- package/dist/store/ErrorStore.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +175 -0
- package/dist/store/RelationStore.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +143 -0
- package/dist/store/SnapshotStore.d.ts.map +1 -0
- package/dist/store/SubscriptionManager.d.ts +90 -0
- package/dist/store/SubscriptionManager.d.ts.map +1 -0
- package/dist/store/TouchedStore.d.ts +24 -0
- package/dist/store/TouchedStore.d.ts.map +1 -0
- package/dist/store/entityId.d.ts +29 -0
- package/dist/store/entityId.d.ts.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/snapshots.d.ts +144 -0
- package/dist/store/snapshots.d.ts.map +1 -0
- package/dist/undo/UndoManager.d.ts +120 -0
- package/dist/undo/UndoManager.d.ts.map +1 -0
- package/dist/undo/index.d.ts +3 -0
- package/dist/undo/index.d.ts.map +1 -0
- package/dist/undo/types.d.ts +74 -0
- package/dist/undo/types.d.ts.map +1 -0
- package/dist/utils/aliasGenerator.d.ts +25 -0
- package/dist/utils/aliasGenerator.d.ts.map +1 -0
- package/dist/utils/deepEqual.d.ts +6 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +29 -0
- package/src/adapter/ContemberAdapter.ts +239 -0
- package/src/adapter/MockAdapter.ts +394 -0
- package/src/adapter/MockQueryEngine.ts +236 -0
- package/src/adapter/index.ts +21 -0
- package/src/adapter/types.ts +199 -0
- package/src/brand/ComponentBrand.ts +28 -0
- package/src/contember/index.ts +17 -0
- package/src/core/ActionDispatcher.ts +450 -0
- package/src/core/Disposable.ts +126 -0
- package/src/core/EntityLoader.ts +169 -0
- package/src/core/SelectionResolver.ts +78 -0
- package/src/core/actionClassification.ts +196 -0
- package/src/core/actions.ts +491 -0
- package/src/core/index.ts +20 -0
- package/src/dataview/filterHandlers.ts +443 -0
- package/src/dataview/index.ts +36 -0
- package/src/dataview/types.ts +220 -0
- package/src/errors/UnfetchedFieldError.ts +18 -0
- package/src/errors/errorPathResolver.ts +239 -0
- package/src/errors/index.ts +40 -0
- package/src/errors/pathMapper.ts +40 -0
- package/src/errors/types.ts +223 -0
- package/src/events/EventEmitter.ts +447 -0
- package/src/events/eventFactory.ts +430 -0
- package/src/events/index.ts +89 -0
- package/src/events/types.ts +374 -0
- package/src/handles/BaseHandle.ts +100 -0
- package/src/handles/EntityHandle.ts +538 -0
- package/src/handles/FieldHandle.ts +268 -0
- package/src/handles/HasManyListHandle.ts +500 -0
- package/src/handles/HasOneHandle.ts +500 -0
- package/src/handles/PlaceholderHandle.ts +321 -0
- package/src/handles/index.ts +37 -0
- package/src/handles/proxyFactory.ts +218 -0
- package/src/handles/types.ts +743 -0
- package/src/index.ts +384 -0
- package/src/internal.ts +19 -0
- package/src/persistence/BatchPersister.ts +911 -0
- package/src/persistence/ChangeRegistry.ts +150 -0
- package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
- package/src/persistence/MutationCollector.ts +567 -0
- package/src/persistence/MutationSchemaProvider.ts +33 -0
- package/src/persistence/index.ts +23 -0
- package/src/persistence/types.ts +220 -0
- package/src/schema/ContemberSchema.ts +132 -0
- package/src/schema/SchemaLoader.ts +93 -0
- package/src/schema/SchemaRegistry.ts +286 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/roles.ts +83 -0
- package/src/schema/types.ts +238 -0
- package/src/selection/SelectionMetaCollector.ts +122 -0
- package/src/selection/SelectionScope.ts +208 -0
- package/src/selection/buildQuery.ts +134 -0
- package/src/selection/createFragment.ts +50 -0
- package/src/selection/createSelectionBuilder.ts +283 -0
- package/src/selection/index.ts +31 -0
- package/src/selection/queryTypes.ts +135 -0
- package/src/selection/types.ts +320 -0
- package/src/store/DirtyTracker.ts +117 -0
- package/src/store/EntityMetaStore.ts +143 -0
- package/src/store/EntitySnapshotStore.ts +260 -0
- package/src/store/ErrorStore.ts +251 -0
- package/src/store/RelationStore.ts +696 -0
- package/src/store/SnapshotStore.ts +765 -0
- package/src/store/SubscriptionManager.ts +241 -0
- package/src/store/TouchedStore.ts +42 -0
- package/src/store/entityId.ts +43 -0
- package/src/store/index.ts +2 -0
- package/src/store/snapshots.ts +203 -0
- package/src/undo/UndoManager.ts +418 -0
- package/src/undo/index.ts +9 -0
- package/src/undo/types.ts +84 -0
- package/src/utils/aliasGenerator.ts +56 -0
- package/src/utils/deepEqual.ts +33 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
type Subscriber = () => void
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Callback interface for SubscriptionManager to request snapshot version bumps.
|
|
5
|
+
* This decouples notification logic from entity snapshot storage.
|
|
6
|
+
*/
|
|
7
|
+
export interface SnapshotVersionBumper {
|
|
8
|
+
bumpEntitySnapshotVersion(key: string): void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manages subscriptions for entity and relation changes.
|
|
13
|
+
*
|
|
14
|
+
* Provides:
|
|
15
|
+
* - Entity-level subscriptions
|
|
16
|
+
* - Relation-level subscriptions
|
|
17
|
+
* - Global subscriptions (any change)
|
|
18
|
+
* - Parent-child change propagation
|
|
19
|
+
* - Global version tracking for change detection
|
|
20
|
+
*/
|
|
21
|
+
export class SubscriptionManager {
|
|
22
|
+
/** Subscribers per entity key */
|
|
23
|
+
private readonly entitySubscribers = new Map<string, Set<Subscriber>>()
|
|
24
|
+
|
|
25
|
+
/** Subscribers per relation key */
|
|
26
|
+
private readonly relationSubscribers = new Map<string, Set<Subscriber>>()
|
|
27
|
+
|
|
28
|
+
/** Global subscribers (notified on any change) */
|
|
29
|
+
private readonly globalSubscribers = new Set<Subscriber>()
|
|
30
|
+
|
|
31
|
+
/** Parent-child relationships: childKey -> Set of parentKeys */
|
|
32
|
+
private readonly childToParents = new Map<string, Set<string>>()
|
|
33
|
+
|
|
34
|
+
/** Global version number for change detection */
|
|
35
|
+
private globalVersion = 0
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Subscribe to changes on a specific entity.
|
|
39
|
+
* Returns unsubscribe function.
|
|
40
|
+
*/
|
|
41
|
+
subscribeToEntity(key: string, callback: Subscriber): () => void {
|
|
42
|
+
if (!this.entitySubscribers.has(key)) {
|
|
43
|
+
this.entitySubscribers.set(key, new Set())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.entitySubscribers.get(key)!.add(callback)
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
this.entitySubscribers.get(key)?.delete(callback)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Subscribe to changes on a specific relation.
|
|
55
|
+
* Returns unsubscribe function.
|
|
56
|
+
*/
|
|
57
|
+
subscribeToRelation(key: string, callback: Subscriber): () => void {
|
|
58
|
+
if (!this.relationSubscribers.has(key)) {
|
|
59
|
+
this.relationSubscribers.set(key, new Set())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.relationSubscribers.get(key)!.add(callback)
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
this.relationSubscribers.get(key)?.delete(callback)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to all changes (global).
|
|
71
|
+
* Returns unsubscribe function.
|
|
72
|
+
*/
|
|
73
|
+
subscribe(callback: Subscriber): () => void {
|
|
74
|
+
this.globalSubscribers.add(callback)
|
|
75
|
+
return () => {
|
|
76
|
+
this.globalSubscribers.delete(callback)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets the global version number for change detection.
|
|
82
|
+
*/
|
|
83
|
+
getVersion(): number {
|
|
84
|
+
return this.globalVersion
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Triggers a global notification to all subscribers.
|
|
89
|
+
* Use when external state changes need to trigger re-renders.
|
|
90
|
+
*/
|
|
91
|
+
notify(): void {
|
|
92
|
+
this.globalVersion++
|
|
93
|
+
for (const sub of this.globalSubscribers) {
|
|
94
|
+
sub()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ==================== Parent-Child Relationships ====================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Registers a parent-child relationship for change propagation.
|
|
102
|
+
* When the child entity changes, parent entity subscribers will be notified.
|
|
103
|
+
*/
|
|
104
|
+
registerParentChild(parentKey: string, childKey: string): void {
|
|
105
|
+
let parents = this.childToParents.get(childKey)
|
|
106
|
+
if (!parents) {
|
|
107
|
+
parents = new Set()
|
|
108
|
+
this.childToParents.set(childKey, parents)
|
|
109
|
+
}
|
|
110
|
+
parents.add(parentKey)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Unregisters a parent-child relationship.
|
|
115
|
+
*/
|
|
116
|
+
unregisterParentChild(parentKey: string, childKey: string): void {
|
|
117
|
+
const parents = this.childToParents.get(childKey)
|
|
118
|
+
if (parents) {
|
|
119
|
+
parents.delete(parentKey)
|
|
120
|
+
if (parents.size === 0) {
|
|
121
|
+
this.childToParents.delete(childKey)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ==================== Notification ====================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Notifies entity subscribers and propagates changes to parents.
|
|
130
|
+
* The bumper callback is used to bump parent entity snapshot versions.
|
|
131
|
+
*/
|
|
132
|
+
notifyEntitySubscribers(
|
|
133
|
+
key: string,
|
|
134
|
+
bumper: SnapshotVersionBumper,
|
|
135
|
+
notifiedKeys: Set<string> = new Set(),
|
|
136
|
+
): void {
|
|
137
|
+
// Prevent infinite recursion
|
|
138
|
+
if (notifiedKeys.has(key)) return
|
|
139
|
+
notifiedKeys.add(key)
|
|
140
|
+
|
|
141
|
+
this.globalVersion++
|
|
142
|
+
|
|
143
|
+
// Notify entity-specific subscribers
|
|
144
|
+
const entitySubs = this.entitySubscribers.get(key)
|
|
145
|
+
if (entitySubs) {
|
|
146
|
+
for (const sub of entitySubs) {
|
|
147
|
+
sub()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Notify parent entity subscribers (propagate change up the tree)
|
|
152
|
+
const parents = this.childToParents.get(key)
|
|
153
|
+
if (parents) {
|
|
154
|
+
for (const parentKey of parents) {
|
|
155
|
+
// Bump parent snapshot version so useSyncExternalStore detects a change
|
|
156
|
+
bumper.bumpEntitySnapshotVersion(parentKey)
|
|
157
|
+
this.notifyEntitySubscribers(parentKey, bumper, notifiedKeys)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Notify global subscribers (only once, not for each parent)
|
|
162
|
+
if (notifiedKeys.size === 1) {
|
|
163
|
+
for (const sub of this.globalSubscribers) {
|
|
164
|
+
sub()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Notifies relation subscribers and the parent entity's subscribers.
|
|
171
|
+
* The bumper callback is used to bump the parent entity snapshot version.
|
|
172
|
+
* The entityKey is the parent entity key derived from the relation key.
|
|
173
|
+
*/
|
|
174
|
+
notifyRelationSubscribers(
|
|
175
|
+
key: string,
|
|
176
|
+
entityKey: string,
|
|
177
|
+
bumper: SnapshotVersionBumper,
|
|
178
|
+
): void {
|
|
179
|
+
this.globalVersion++
|
|
180
|
+
|
|
181
|
+
// Notify relation-specific subscribers
|
|
182
|
+
const relationSubs = this.relationSubscribers.get(key)
|
|
183
|
+
if (relationSubs) {
|
|
184
|
+
for (const sub of relationSubs) {
|
|
185
|
+
sub()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Bump entity snapshot version so isEqual detects a change
|
|
190
|
+
bumper.bumpEntitySnapshotVersion(entityKey)
|
|
191
|
+
|
|
192
|
+
// Notify entity subscribers
|
|
193
|
+
const entitySubs = this.entitySubscribers.get(entityKey)
|
|
194
|
+
if (entitySubs) {
|
|
195
|
+
for (const sub of entitySubs) {
|
|
196
|
+
sub()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Notify global subscribers
|
|
201
|
+
for (const sub of this.globalSubscribers) {
|
|
202
|
+
sub()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Notifies entity subscribers without propagation.
|
|
208
|
+
* Used during batch imports (e.g., undo/redo).
|
|
209
|
+
*/
|
|
210
|
+
notifyEntityDirect(key: string): void {
|
|
211
|
+
const subs = this.entitySubscribers.get(key)
|
|
212
|
+
if (subs) {
|
|
213
|
+
for (const sub of subs) {
|
|
214
|
+
sub()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Notifies relation subscribers without propagation.
|
|
221
|
+
* Used during batch imports (e.g., undo/redo).
|
|
222
|
+
*/
|
|
223
|
+
notifyRelationDirect(key: string): void {
|
|
224
|
+
const subs = this.relationSubscribers.get(key)
|
|
225
|
+
if (subs) {
|
|
226
|
+
for (const sub of subs) {
|
|
227
|
+
sub()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Bumps global version and notifies global subscribers.
|
|
234
|
+
*/
|
|
235
|
+
notifyGlobal(): void {
|
|
236
|
+
this.globalVersion++
|
|
237
|
+
for (const sub of this.globalSubscribers) {
|
|
238
|
+
sub()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages touched state for entity fields.
|
|
3
|
+
*
|
|
4
|
+
* A field is "touched" when the user has interacted with it.
|
|
5
|
+
* Used for conditional error display (show errors only after user interaction).
|
|
6
|
+
*
|
|
7
|
+
* Keys are pre-computed composite strings (e.g., "entityType:id:fieldName").
|
|
8
|
+
*/
|
|
9
|
+
export class TouchedStore {
|
|
10
|
+
/** Touched state keyed by "entityType:id:fieldName" */
|
|
11
|
+
private readonly touchedFields = new Map<string, boolean>()
|
|
12
|
+
|
|
13
|
+
isFieldTouched(key: string): boolean {
|
|
14
|
+
return this.touchedFields.get(key) ?? false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sets the touched state for a field.
|
|
19
|
+
* Returns true if the state actually changed.
|
|
20
|
+
*/
|
|
21
|
+
setFieldTouched(key: string, touched: boolean): boolean {
|
|
22
|
+
const current = this.touchedFields.get(key) ?? false
|
|
23
|
+
if (current === touched) return false
|
|
24
|
+
this.touchedFields.set(key, touched)
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clears all touched state for an entity.
|
|
30
|
+
*/
|
|
31
|
+
clearForEntity(keyPrefix: string): void {
|
|
32
|
+
for (const key of [...this.touchedFields.keys()]) {
|
|
33
|
+
if (key.startsWith(keyPrefix)) {
|
|
34
|
+
this.touchedFields.delete(key)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
clear(): void {
|
|
40
|
+
this.touchedFields.clear()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity ID type detection and generation utilities.
|
|
3
|
+
*
|
|
4
|
+
* IDs in Bindx can be:
|
|
5
|
+
* - Persisted: server-assigned UUIDs
|
|
6
|
+
* - Temporary: locally created, prefixed with "__temp_"
|
|
7
|
+
* - Placeholder: disconnected relation placeholders, prefixed with "__placeholder_"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if an ID is a temporary ID (created locally, not yet persisted).
|
|
12
|
+
*/
|
|
13
|
+
export function isTempId(id: string): boolean {
|
|
14
|
+
return id.startsWith('__temp_')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if an ID is a placeholder ID (disconnected relation placeholder).
|
|
19
|
+
*/
|
|
20
|
+
export function isPlaceholderId(id: string): boolean {
|
|
21
|
+
return id.startsWith('__placeholder_')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if an ID is a persisted/real ID from the server.
|
|
26
|
+
*/
|
|
27
|
+
export function isPersistedId(id: string): boolean {
|
|
28
|
+
return !isTempId(id) && !isPlaceholderId(id)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generates a new placeholder ID.
|
|
33
|
+
*/
|
|
34
|
+
export function generatePlaceholderId(): string {
|
|
35
|
+
return `__placeholder_${crypto.randomUUID()}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates a new temporary ID.
|
|
40
|
+
*/
|
|
41
|
+
export function generateTempId(): string {
|
|
42
|
+
return `__temp_${crypto.randomUUID()}`
|
|
43
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable snapshot types for React integration.
|
|
3
|
+
* All snapshots are frozen objects that are replaced (not mutated) on changes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { HasOneRelationState } from '../handles/types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Status of a data load operation
|
|
10
|
+
*/
|
|
11
|
+
export type LoadStatus = 'idle' | 'loading' | 'success' | 'error' | 'not_found'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Immutable snapshot of entity data stored in the IdentityMap.
|
|
15
|
+
* A new snapshot is created whenever the entity data changes.
|
|
16
|
+
*/
|
|
17
|
+
export interface EntitySnapshot<T extends object = object> {
|
|
18
|
+
/** Entity ID */
|
|
19
|
+
readonly id: string
|
|
20
|
+
/** Entity type name */
|
|
21
|
+
readonly entityType: string
|
|
22
|
+
/** Current (local) data - may differ from serverData if modified */
|
|
23
|
+
readonly data: Readonly<T>
|
|
24
|
+
/** Data as received from server - used for dirty tracking and reset */
|
|
25
|
+
readonly serverData: Readonly<T>
|
|
26
|
+
/** Version number - incremented on each change, used for comparison */
|
|
27
|
+
readonly version: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Immutable snapshot of a scalar field value.
|
|
32
|
+
*/
|
|
33
|
+
export interface FieldSnapshot<T = unknown> {
|
|
34
|
+
/** Current value */
|
|
35
|
+
readonly value: T | null
|
|
36
|
+
/** Server value (for dirty tracking) */
|
|
37
|
+
readonly serverValue: T | null
|
|
38
|
+
/** Whether the field has been modified */
|
|
39
|
+
readonly isDirty: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* State information for a has-one relation.
|
|
44
|
+
*/
|
|
45
|
+
export interface HasOneRelationSnapshot<T extends object = object> {
|
|
46
|
+
/** Current relation state */
|
|
47
|
+
readonly state: HasOneRelationState
|
|
48
|
+
/** Current referenced entity ID (null if disconnected) */
|
|
49
|
+
readonly entityId: string | null
|
|
50
|
+
/** Server-side entity ID (for dirty tracking) */
|
|
51
|
+
readonly serverEntityId: string | null
|
|
52
|
+
/** The related entity snapshot (if connected and loaded) */
|
|
53
|
+
readonly entity: EntitySnapshot<T> | null
|
|
54
|
+
/** Whether the relation has been modified */
|
|
55
|
+
readonly isDirty: boolean
|
|
56
|
+
/** Version for change detection */
|
|
57
|
+
readonly version: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Item in a has-many relation list.
|
|
62
|
+
*/
|
|
63
|
+
export interface HasManyItemSnapshot<T extends object = object> {
|
|
64
|
+
/** Unique key for React reconciliation */
|
|
65
|
+
readonly key: string
|
|
66
|
+
/** The entity snapshot */
|
|
67
|
+
readonly entity: EntitySnapshot<T>
|
|
68
|
+
/** Whether this item was added locally (not from server) */
|
|
69
|
+
readonly isNew: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Immutable snapshot of a has-many relation (entity list).
|
|
74
|
+
*/
|
|
75
|
+
export interface HasManySnapshot<T extends object = object> {
|
|
76
|
+
/** List items in current order */
|
|
77
|
+
readonly items: ReadonlyArray<HasManyItemSnapshot<T>>
|
|
78
|
+
/** Keys of items that came from the server */
|
|
79
|
+
readonly serverItemKeys: ReadonlySet<string>
|
|
80
|
+
/** Keys of items that were added locally */
|
|
81
|
+
readonly addedKeys: ReadonlySet<string>
|
|
82
|
+
/** Keys of items that were removed locally */
|
|
83
|
+
readonly removedKeys: ReadonlySet<string>
|
|
84
|
+
/** Whether the order has been changed */
|
|
85
|
+
readonly orderChanged: boolean
|
|
86
|
+
/** Whether the list has any modifications */
|
|
87
|
+
readonly isDirty: boolean
|
|
88
|
+
/** Version for change detection */
|
|
89
|
+
readonly version: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Loading state for an entity.
|
|
94
|
+
*/
|
|
95
|
+
export interface LoadingState {
|
|
96
|
+
readonly status: 'loading'
|
|
97
|
+
readonly id: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Error state for an entity.
|
|
102
|
+
*/
|
|
103
|
+
export interface ErrorState {
|
|
104
|
+
readonly status: 'error'
|
|
105
|
+
readonly id: string
|
|
106
|
+
readonly error: Error
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Not found state for an entity.
|
|
111
|
+
*/
|
|
112
|
+
export interface NotFoundState {
|
|
113
|
+
readonly status: 'not_found'
|
|
114
|
+
readonly id: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ready state with entity data.
|
|
119
|
+
*/
|
|
120
|
+
export interface ReadyState<T extends object = object> {
|
|
121
|
+
readonly status: 'ready'
|
|
122
|
+
readonly id: string
|
|
123
|
+
readonly entityType: string
|
|
124
|
+
readonly snapshot: EntitySnapshot<T>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Union of all possible entity states.
|
|
129
|
+
*/
|
|
130
|
+
export type EntityState<T extends object = object> =
|
|
131
|
+
| LoadingState
|
|
132
|
+
| ErrorState
|
|
133
|
+
| NotFoundState
|
|
134
|
+
| ReadyState<T>
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Helper to check if state is ready
|
|
138
|
+
*/
|
|
139
|
+
export function isReadyState<T extends object>(state: EntityState<T>): state is ReadyState<T> {
|
|
140
|
+
return state.status === 'ready'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Helper to check if state is loading
|
|
145
|
+
*/
|
|
146
|
+
export function isLoadingState<T extends object>(state: EntityState<T>): state is LoadingState {
|
|
147
|
+
return state.status === 'loading'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Helper to check if state is error
|
|
152
|
+
*/
|
|
153
|
+
export function isErrorState<T extends object>(state: EntityState<T>): state is ErrorState {
|
|
154
|
+
return state.status === 'error'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates an immutable entity snapshot from data.
|
|
159
|
+
*/
|
|
160
|
+
export function createEntitySnapshot<T extends object>(
|
|
161
|
+
id: string,
|
|
162
|
+
entityType: string,
|
|
163
|
+
data: T,
|
|
164
|
+
serverData: T,
|
|
165
|
+
version: number,
|
|
166
|
+
): EntitySnapshot<T> {
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
id,
|
|
169
|
+
entityType,
|
|
170
|
+
data: Object.freeze({ ...data }) as Readonly<T>,
|
|
171
|
+
serverData: Object.freeze({ ...serverData }) as Readonly<T>,
|
|
172
|
+
version,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Creates a new entity snapshot with updated data.
|
|
178
|
+
* Returns a new frozen object with incremented version.
|
|
179
|
+
*/
|
|
180
|
+
export function updateEntitySnapshot<T extends object>(
|
|
181
|
+
snapshot: EntitySnapshot<T>,
|
|
182
|
+
updates: Partial<T>,
|
|
183
|
+
): EntitySnapshot<T> {
|
|
184
|
+
return Object.freeze({
|
|
185
|
+
...snapshot,
|
|
186
|
+
data: Object.freeze({ ...snapshot.data, ...updates }) as Readonly<T>,
|
|
187
|
+
version: snapshot.version + 1,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Creates a field snapshot from entity snapshot.
|
|
193
|
+
*/
|
|
194
|
+
export function createFieldSnapshot<T>(
|
|
195
|
+
value: T | null,
|
|
196
|
+
serverValue: T | null,
|
|
197
|
+
): FieldSnapshot<T> {
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
value,
|
|
200
|
+
serverValue,
|
|
201
|
+
isDirty: value !== serverValue,
|
|
202
|
+
})
|
|
203
|
+
}
|