@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,199 @@
1
+ import type { QuerySpec } from '../selection/buildQuery.js'
2
+ import type { EntityWhere, EntityOrderBy } from '../selection/queryTypes.js'
3
+ import type { ContemberMutationResult } from '../errors/pathMapper.js'
4
+
5
+ /**
6
+ * Options for query operations
7
+ */
8
+ export interface QueryOptions {
9
+ /**
10
+ * AbortSignal for cancelling the request
11
+ */
12
+ readonly signal?: AbortSignal
13
+ }
14
+
15
+ /**
16
+ * Recursive value type for unique where fields (string or number identifiers).
17
+ * Structurally compatible with Input.UniqueWhere from @contember/schema.
18
+ */
19
+ export type UniqueWhereValue = string | number | { readonly [field: string]: UniqueWhereValue }
20
+
21
+ /**
22
+ * Unique field lookup for single entity queries.
23
+ * Supports id and any other unique field.
24
+ */
25
+ export type EntityUniqueWhere = { readonly [field: string]: UniqueWhereValue }
26
+
27
+ /**
28
+ * Query for a single entity by unique field(s)
29
+ */
30
+ export interface GetQuery {
31
+ readonly type: 'get'
32
+ readonly entityType: string
33
+ readonly by: EntityUniqueWhere
34
+ readonly spec: QuerySpec
35
+ }
36
+
37
+ /**
38
+ * Query for a list of entities
39
+ */
40
+ export interface ListQuery<TEntity = unknown> {
41
+ readonly type: 'list'
42
+ readonly entityType: string
43
+ readonly filter?: EntityWhere<TEntity>
44
+ readonly orderBy?: readonly EntityOrderBy<TEntity>[]
45
+ readonly limit?: number
46
+ readonly offset?: number
47
+ readonly spec: QuerySpec
48
+ }
49
+
50
+ /**
51
+ * Union of all query types
52
+ */
53
+ export type Query<TEntity = unknown> = GetQuery | ListQuery<TEntity>
54
+
55
+ /**
56
+ * Result for a single get query
57
+ */
58
+ export interface GetQueryResult {
59
+ readonly type: 'get'
60
+ readonly data: Record<string, unknown> | null
61
+ }
62
+
63
+ /**
64
+ * Result for a list query
65
+ */
66
+ export interface ListQueryResult {
67
+ readonly type: 'list'
68
+ readonly data: readonly Record<string, unknown>[]
69
+ }
70
+
71
+ /**
72
+ * Union of query results
73
+ */
74
+ export type QueryResult = GetQueryResult | ListQueryResult
75
+
76
+ /**
77
+ * Result of a persist operation.
78
+ * Contains detailed error information for error mapping.
79
+ */
80
+ export interface PersistResult {
81
+ readonly ok: boolean
82
+ readonly errorMessage?: string
83
+ /** Detailed mutation result for error mapping (Contember-specific) */
84
+ readonly mutationResult?: ContemberMutationResult
85
+ }
86
+
87
+ /**
88
+ * Result of a create operation.
89
+ * Contains the created entity data or error information.
90
+ */
91
+ export interface CreateResult {
92
+ readonly ok: boolean
93
+ readonly errorMessage?: string
94
+ /** The created entity data (when ok is true) */
95
+ readonly data?: Record<string, unknown>
96
+ /** Detailed mutation result for error mapping (Contember-specific) */
97
+ readonly mutationResult?: ContemberMutationResult
98
+ }
99
+
100
+ /**
101
+ * Result of a delete operation.
102
+ */
103
+ export interface DeleteResult {
104
+ readonly ok: boolean
105
+ readonly errorMessage?: string
106
+ /** Detailed mutation result for error mapping (Contember-specific) */
107
+ readonly mutationResult?: ContemberMutationResult
108
+ }
109
+
110
+ /**
111
+ * A single mutation in a transaction.
112
+ */
113
+ export interface TransactionMutation {
114
+ readonly entityType: string
115
+ readonly entityId: string
116
+ readonly operation: 'create' | 'update' | 'delete'
117
+ readonly data?: Record<string, unknown>
118
+ }
119
+
120
+ /**
121
+ * Result of a single mutation in a transaction.
122
+ */
123
+ export interface TransactionMutationResult {
124
+ readonly entityType: string
125
+ readonly entityId: string
126
+ readonly ok: boolean
127
+ /** Server-assigned ID for creates */
128
+ readonly persistedId?: string
129
+ readonly errorMessage?: string
130
+ readonly mutationResult?: ContemberMutationResult
131
+ }
132
+
133
+ /**
134
+ * Result of a transaction persist operation.
135
+ * All-or-nothing: if any mutation fails, all are rolled back.
136
+ */
137
+ export interface TransactionResult {
138
+ /** Whether all mutations succeeded */
139
+ readonly ok: boolean
140
+ /** Results per mutation, in same order as input */
141
+ readonly results: readonly TransactionMutationResult[]
142
+ }
143
+
144
+ /**
145
+ * Interface for backend adapters.
146
+ * Implement this to connect bindx to your data source.
147
+ */
148
+ export interface BackendAdapter {
149
+ /**
150
+ * Execute one or more queries in a single request.
151
+ * When multiple queries are provided, the adapter should batch them
152
+ * into a single network request where possible.
153
+ *
154
+ * @param queries - Array of queries to execute
155
+ * @param options - Optional query options including AbortSignal
156
+ * @returns Array of results in the same order as queries
157
+ */
158
+ query(queries: readonly Query[], options?: QueryOptions): Promise<QueryResult[]>
159
+
160
+ /**
161
+ * Persists changes to an entity.
162
+ * Returns a result object with success/failure status and error details.
163
+ *
164
+ * @param entityType - The type/name of the entity
165
+ * @param id - The entity's unique identifier
166
+ * @param changes - The changed fields to persist
167
+ * @returns Result with ok status and optional error details
168
+ */
169
+ persist(entityType: string, id: string, changes: Record<string, unknown>): Promise<PersistResult>
170
+
171
+ /**
172
+ * Creates a new entity.
173
+ * Returns a result object with the created entity or error details.
174
+ *
175
+ * @param entityType - The type/name of the entity
176
+ * @param data - The initial data for the entity
177
+ * @returns Result with ok status, created entity data, and optional error details
178
+ */
179
+ create?(entityType: string, data: Record<string, unknown>): Promise<CreateResult>
180
+
181
+ /**
182
+ * Deletes an entity.
183
+ * Returns a result object with success/failure status.
184
+ *
185
+ * @param entityType - The type/name of the entity
186
+ * @param id - The entity's unique identifier
187
+ * @returns Result with ok status and optional error details
188
+ */
189
+ delete?(entityType: string, id: string): Promise<DeleteResult>
190
+
191
+ /**
192
+ * Persists multiple mutations in a single transaction.
193
+ * All-or-nothing: if any mutation fails, all are rolled back.
194
+ *
195
+ * @param mutations - Array of mutations to execute
196
+ * @returns Result with success status and per-mutation results
197
+ */
198
+ persistTransaction?(mutations: readonly TransactionMutation[]): Promise<TransactionResult>
199
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ComponentBrand provides unique identity for each component created with createComponent.
3
+ * Each instance has a unique runtime Symbol for brand validation.
4
+ * The class structure itself provides nominal typing in TypeScript.
5
+ */
6
+ export class ComponentBrand {
7
+ /**
8
+ * Private property to ensure nominal typing.
9
+ * Each class instance is considered a unique type.
10
+ */
11
+ private readonly __nominal = true
12
+
13
+ /**
14
+ * Runtime symbol for brand validation.
15
+ * Used to check that EntityRef includes required component brands.
16
+ */
17
+ readonly brandSymbol: symbol
18
+
19
+ constructor(public readonly name: string) {
20
+ this.brandSymbol = Symbol(`component_${name}_${Math.random().toString(36).slice(2)}`)
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Type alias for any ComponentBrand.
26
+ * Used as default/base type in generic constraints.
27
+ */
28
+ export type AnyBrand = ComponentBrand
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Contember-specific utilities for bindx
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ // Re-export unified MutationCollector from persistence
8
+ export { MutationCollector, type EntityMutationResult } from '../persistence/MutationCollector.js'
9
+ export { ContemberSchemaMutationAdapter } from '../persistence/ContemberSchemaMutationAdapter.js'
10
+ export type { MutationSchemaProvider } from '../persistence/MutationSchemaProvider.js'
11
+
12
+ // Re-export commonly used types from @contember/client-content
13
+ export type {
14
+ SchemaNames,
15
+ SchemaEntityNames,
16
+ ContentClientInput,
17
+ } from '@contember/client-content'
@@ -0,0 +1,450 @@
1
+ import type { SnapshotStore } from '../store/SnapshotStore.js'
2
+ import type { Action } from './actions.js'
3
+ import { EventEmitter } from '../events/EventEmitter.js'
4
+ import {
5
+ createBeforeEvent,
6
+ createAfterEvent,
7
+ captureStateBeforeAction,
8
+ } from '../events/eventFactory.js'
9
+
10
+ /**
11
+ * ActionDispatcher centralizes all state mutations.
12
+ * All changes to the store go through this dispatcher.
13
+ *
14
+ * Benefits:
15
+ * - Single point of control for all mutations
16
+ * - Easy to add logging, debugging, middleware
17
+ * - Ensures consistent state updates
18
+ * - Enables action replay/undo if needed
19
+ * - Event system for subscriptions and interceptors
20
+ */
21
+ export class ActionDispatcher {
22
+ private readonly middlewares: ActionMiddleware[] = []
23
+ private readonly eventEmitter: EventEmitter
24
+
25
+ constructor(
26
+ private readonly store: SnapshotStore,
27
+ eventEmitter?: EventEmitter,
28
+ ) {
29
+ this.eventEmitter = eventEmitter ?? new EventEmitter()
30
+ }
31
+
32
+ /**
33
+ * Gets the event emitter for external access.
34
+ */
35
+ getEventEmitter(): EventEmitter {
36
+ return this.eventEmitter
37
+ }
38
+
39
+ /**
40
+ * Dispatches an action to update the store.
41
+ * Runs synchronous interceptors (before-events) and emits after-events.
42
+ * Async interceptors are skipped with a warning — use dispatchAsync() for those.
43
+ */
44
+ dispatch(action: Action): void {
45
+ // Run through middlewares first
46
+ for (const middleware of this.middlewares) {
47
+ const result = middleware(action, this.store)
48
+ if (result === false) {
49
+ // Middleware can cancel the action
50
+ return
51
+ }
52
+ }
53
+
54
+ // Create and run before event through interceptors (synchronously)
55
+ const beforeEvent = createBeforeEvent(action, this.store)
56
+ if (beforeEvent) {
57
+ const result = this.eventEmitter.runInterceptorsSync(beforeEvent)
58
+ if (result === null) {
59
+ // Interceptor cancelled the action
60
+ return
61
+ }
62
+ // If event was modified, update action accordingly
63
+ if (result !== beforeEvent) {
64
+ action = this.updateActionFromEvent(action, result)
65
+ }
66
+ }
67
+
68
+ // Capture state before execution (for after events)
69
+ const stateBefore = captureStateBeforeAction(action, this.store)
70
+
71
+ // Execute the action
72
+ this.execute(action)
73
+
74
+ // Emit after event
75
+ const afterEvent = createAfterEvent(action, stateBefore, this.store)
76
+ if (afterEvent) {
77
+ this.eventEmitter.emit(afterEvent)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Dispatches an action with full interceptor support.
83
+ * Returns true if the action was executed, false if it was cancelled by an interceptor.
84
+ */
85
+ async dispatchAsync(action: Action): Promise<boolean> {
86
+ // Run through middlewares first
87
+ for (const middleware of this.middlewares) {
88
+ const result = middleware(action, this.store)
89
+ if (result === false) {
90
+ return false
91
+ }
92
+ }
93
+
94
+ // Create and run before event through interceptors
95
+ const beforeEvent = createBeforeEvent(action, this.store)
96
+ if (beforeEvent) {
97
+ const result = await this.eventEmitter.runInterceptors(beforeEvent)
98
+ if (result === null) {
99
+ // Interceptor cancelled the action
100
+ return false
101
+ }
102
+ // If event was modified, update action accordingly
103
+ if (result !== beforeEvent) {
104
+ action = this.updateActionFromEvent(action, result)
105
+ }
106
+ }
107
+
108
+ // Capture state before execution (for after events)
109
+ const stateBefore = captureStateBeforeAction(action, this.store)
110
+
111
+ // Execute the action
112
+ this.execute(action)
113
+
114
+ // Emit after event
115
+ const afterEvent = createAfterEvent(action, stateBefore, this.store)
116
+ if (afterEvent) {
117
+ this.eventEmitter.emit(afterEvent)
118
+ }
119
+
120
+ return true
121
+ }
122
+
123
+ /**
124
+ * Updates an action based on a modified before event.
125
+ * Used when an interceptor modifies the event.
126
+ */
127
+ private updateActionFromEvent(action: Action, event: ReturnType<typeof createBeforeEvent>): Action {
128
+ if (!event) return action
129
+
130
+ switch (action.type) {
131
+ case 'SET_FIELD':
132
+ if (event.type === 'field:changing') {
133
+ return {
134
+ ...action,
135
+ value: event.newValue,
136
+ }
137
+ }
138
+ break
139
+
140
+ case 'CONNECT_RELATION':
141
+ if (event.type === 'relation:connecting') {
142
+ return {
143
+ ...action,
144
+ targetId: event.targetId,
145
+ }
146
+ }
147
+ break
148
+
149
+ // Other action types don't have modifiable properties in their before events
150
+ }
151
+
152
+ return action
153
+ }
154
+
155
+ /**
156
+ * Adds a middleware to the dispatcher.
157
+ * Middlewares are called in order before action execution.
158
+ */
159
+ addMiddleware(middleware: ActionMiddleware): void {
160
+ this.middlewares.push(middleware)
161
+ }
162
+
163
+ /**
164
+ * Removes a middleware from the dispatcher.
165
+ */
166
+ removeMiddleware(middleware: ActionMiddleware): void {
167
+ const index = this.middlewares.indexOf(middleware)
168
+ if (index !== -1) {
169
+ this.middlewares.splice(index, 1)
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Executes an action, updating the store.
175
+ */
176
+ private execute(action: Action): void {
177
+ switch (action.type) {
178
+ case 'SET_FIELD':
179
+ this.store.setFieldValue(
180
+ action.entityType,
181
+ action.entityId,
182
+ action.fieldPath,
183
+ action.value,
184
+ )
185
+ break
186
+
187
+ case 'RESET_ENTITY':
188
+ this.store.resetEntity(action.entityType, action.entityId)
189
+ break
190
+
191
+ case 'COMMIT_ENTITY':
192
+ this.store.commitEntity(action.entityType, action.entityId)
193
+ break
194
+
195
+ case 'SET_ENTITY_DATA':
196
+ this.store.setEntityData(
197
+ action.entityType,
198
+ action.entityId,
199
+ action.data,
200
+ action.isServerData,
201
+ )
202
+ break
203
+
204
+ case 'CONNECT_RELATION':
205
+ this.store.setRelation(
206
+ action.entityType,
207
+ action.entityId,
208
+ action.fieldName,
209
+ {
210
+ currentId: action.targetId,
211
+ state: 'connected',
212
+ },
213
+ )
214
+ break
215
+
216
+ case 'DISCONNECT_RELATION':
217
+ this.store.setRelation(
218
+ action.entityType,
219
+ action.entityId,
220
+ action.fieldName,
221
+ {
222
+ currentId: null,
223
+ state: 'disconnected',
224
+ placeholderData: {},
225
+ },
226
+ )
227
+ break
228
+
229
+ case 'DELETE_RELATION':
230
+ this.store.setRelation(
231
+ action.entityType,
232
+ action.entityId,
233
+ action.fieldName,
234
+ {
235
+ state: 'deleted',
236
+ },
237
+ )
238
+ break
239
+
240
+ case 'RESET_RELATION':
241
+ this.store.resetRelation(
242
+ action.entityType,
243
+ action.entityId,
244
+ action.fieldName,
245
+ )
246
+ break
247
+
248
+ case 'COMMIT_RELATION':
249
+ this.store.commitRelation(
250
+ action.entityType,
251
+ action.entityId,
252
+ action.fieldName,
253
+ )
254
+ break
255
+
256
+ case 'SET_PLACEHOLDER_DATA': {
257
+ const relation = this.store.getRelation(
258
+ action.entityType,
259
+ action.entityId,
260
+ action.fieldName,
261
+ )
262
+ const currentPlaceholderData = relation?.placeholderData ?? {}
263
+ const newPlaceholderData = setNestedValue(
264
+ { ...currentPlaceholderData },
265
+ action.fieldPath,
266
+ action.value,
267
+ )
268
+ this.store.setRelation(
269
+ action.entityType,
270
+ action.entityId,
271
+ action.fieldName,
272
+ { placeholderData: newPlaceholderData },
273
+ )
274
+ break
275
+ }
276
+
277
+ case 'ADD_TO_LIST': {
278
+ // Create entity if itemId not provided
279
+ const itemId = action.itemId ?? this.store.createEntity(action.targetType, action.itemData)
280
+ // Add to has-many relation
281
+ this.store.addToHasMany(
282
+ action.entityType,
283
+ action.entityId,
284
+ action.fieldName,
285
+ itemId,
286
+ )
287
+ break
288
+ }
289
+
290
+ case 'REMOVE_FROM_LIST':
291
+ this.store.removeFromHasMany(
292
+ action.entityType,
293
+ action.entityId,
294
+ action.fieldName,
295
+ action.itemKey,
296
+ )
297
+ break
298
+
299
+ case 'MOVE_IN_LIST':
300
+ this.store.moveInHasMany(
301
+ action.entityType,
302
+ action.entityId,
303
+ action.fieldName,
304
+ action.fromIndex,
305
+ action.toIndex,
306
+ )
307
+ break
308
+
309
+ case 'SET_LOAD_STATE':
310
+ this.store.setLoadState(
311
+ action.entityType,
312
+ action.entityId,
313
+ action.status,
314
+ action.error,
315
+ )
316
+ break
317
+
318
+ case 'SET_PERSISTING':
319
+ this.store.setPersisting(
320
+ action.entityType,
321
+ action.entityId,
322
+ action.isPersisting,
323
+ )
324
+ break
325
+
326
+ case 'ADD_FIELD_ERROR':
327
+ this.store.addFieldError(
328
+ action.entityType,
329
+ action.entityId,
330
+ action.fieldName,
331
+ action.error,
332
+ )
333
+ break
334
+
335
+ case 'CLEAR_FIELD_ERRORS':
336
+ this.store.clearFieldErrors(
337
+ action.entityType,
338
+ action.entityId,
339
+ action.fieldName,
340
+ action.source,
341
+ )
342
+ break
343
+
344
+ case 'ADD_ENTITY_ERROR':
345
+ this.store.addEntityError(
346
+ action.entityType,
347
+ action.entityId,
348
+ action.error,
349
+ )
350
+ break
351
+
352
+ case 'CLEAR_ENTITY_ERRORS':
353
+ this.store.clearEntityErrors(
354
+ action.entityType,
355
+ action.entityId,
356
+ action.source,
357
+ )
358
+ break
359
+
360
+ case 'ADD_RELATION_ERROR':
361
+ this.store.addRelationError(
362
+ action.entityType,
363
+ action.entityId,
364
+ action.relationName,
365
+ action.error,
366
+ )
367
+ break
368
+
369
+ case 'CLEAR_RELATION_ERRORS':
370
+ this.store.clearRelationErrors(
371
+ action.entityType,
372
+ action.entityId,
373
+ action.relationName,
374
+ action.source,
375
+ )
376
+ break
377
+
378
+ case 'CLEAR_ALL_SERVER_ERRORS':
379
+ this.store.clearAllServerErrors(
380
+ action.entityType,
381
+ action.entityId,
382
+ )
383
+ break
384
+
385
+ case 'CLEAR_ALL_ERRORS':
386
+ this.store.clearAllErrors(
387
+ action.entityType,
388
+ action.entityId,
389
+ )
390
+ break
391
+
392
+ default:
393
+ // Exhaustive check
394
+ const _exhaustive: never = action
395
+ throw new Error(`Unknown action type: ${(_exhaustive as Action).type}`)
396
+ }
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Middleware function type.
402
+ * Can inspect/modify actions before execution.
403
+ * Return false to cancel the action.
404
+ */
405
+ export type ActionMiddleware = (action: Action, store: SnapshotStore) => boolean | void
406
+
407
+ /**
408
+ * Creates a logging middleware for debugging.
409
+ */
410
+ export function createLoggingMiddleware(
411
+ prefix: string = '[Bindx]',
412
+ ): ActionMiddleware {
413
+ return (action) => {
414
+ console.log(`${prefix} ${action.type}`, action)
415
+ }
416
+ }
417
+
418
+ // ==================== Helper Functions ====================
419
+
420
+ /**
421
+ * Sets a nested value in an object, returning a new object.
422
+ */
423
+ function setNestedValue(
424
+ obj: Record<string, unknown>,
425
+ path: string[],
426
+ value: unknown,
427
+ ): Record<string, unknown> {
428
+ if (path.length === 0) return obj
429
+
430
+ const result = { ...obj }
431
+ let current: Record<string, unknown> = result
432
+
433
+ for (let i = 0; i < path.length - 1; i++) {
434
+ const key = path[i]!
435
+ const nextValue = current[key]
436
+
437
+ if (typeof nextValue === 'object' && nextValue !== null) {
438
+ current[key] = { ...(nextValue as Record<string, unknown>) }
439
+ } else {
440
+ current[key] = {}
441
+ }
442
+
443
+ current = current[key] as Record<string, unknown>
444
+ }
445
+
446
+ const lastKey = path[path.length - 1]!
447
+ current[lastKey] = value
448
+
449
+ return result
450
+ }