qdadm 0.27.0 → 0.29.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 (42) hide show
  1. package/package.json +4 -2
  2. package/src/composables/index.js +1 -0
  3. package/src/composables/useDeferred.js +85 -0
  4. package/src/composables/useListPageBuilder.js +3 -0
  5. package/src/deferred/DeferredRegistry.js +323 -0
  6. package/src/deferred/index.js +7 -0
  7. package/src/entity/EntityManager.js +82 -14
  8. package/src/entity/factory.js +155 -0
  9. package/src/entity/factory.test.js +189 -0
  10. package/src/entity/index.js +8 -0
  11. package/src/entity/storage/ApiStorage.js +4 -1
  12. package/src/entity/storage/IStorage.js +76 -0
  13. package/src/entity/storage/LocalStorage.js +4 -1
  14. package/src/entity/storage/MemoryStorage.js +4 -1
  15. package/src/entity/storage/MockApiStorage.js +4 -1
  16. package/src/entity/storage/SdkStorage.js +4 -1
  17. package/src/entity/storage/factory.js +193 -0
  18. package/src/entity/storage/factory.test.js +159 -0
  19. package/src/entity/storage/index.js +13 -0
  20. package/src/gen/FieldMapper.js +116 -0
  21. package/src/gen/StorageProfileFactory.js +109 -0
  22. package/src/gen/connectors/BaseConnector.js +142 -0
  23. package/src/gen/connectors/ManualConnector.js +385 -0
  24. package/src/gen/connectors/ManualConnector.test.js +499 -0
  25. package/src/gen/connectors/OpenAPIConnector.js +568 -0
  26. package/src/gen/connectors/OpenAPIConnector.test.js +737 -0
  27. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  28. package/src/gen/connectors/index.js +11 -0
  29. package/src/gen/createManagers.js +224 -0
  30. package/src/gen/decorators.js +129 -0
  31. package/src/gen/generateManagers.js +266 -0
  32. package/src/gen/generateManagers.test.js +358 -0
  33. package/src/gen/index.js +45 -0
  34. package/src/gen/schema.js +221 -0
  35. package/src/gen/vite-plugin.js +105 -0
  36. package/src/generated/managers/testManager.js +45 -0
  37. package/src/index.js +3 -0
  38. package/src/kernel/EventRouter.js +264 -0
  39. package/src/kernel/Kernel.js +123 -8
  40. package/src/kernel/index.js +4 -0
  41. package/src/orchestrator/Orchestrator.js +60 -0
  42. package/src/query/FilterQuery.js +9 -4
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Unified Schema Types
3
+ *
4
+ * Core contracts that abstract differences between OpenAPI, Pydantic, and manual schema sources.
5
+ * These types provide a common format for entity and field definitions that can be used
6
+ * by connectors (schema sources) and generators (code output).
7
+ *
8
+ * @module gen/schema
9
+ */
10
+
11
+ /**
12
+ * Field Type Enumeration
13
+ *
14
+ * Unified field types that abstract source-specific types (OpenAPI string/integer,
15
+ * Pydantic str/int, etc.) into a common set.
16
+ *
17
+ * @typedef {'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'email' | 'url' | 'uuid' | 'array' | 'object'} UnifiedFieldType
18
+ */
19
+
20
+ /**
21
+ * Reference Definition
22
+ *
23
+ * Describes a relation to another entity for foreign key fields.
24
+ *
25
+ * @typedef {object} UnifiedFieldReference
26
+ * @property {string} entity - Target entity name (e.g., 'users', 'categories')
27
+ * @property {string} [labelField] - Field to display as label (e.g., 'name', 'title')
28
+ */
29
+
30
+ /**
31
+ * Unified Field Schema
32
+ *
33
+ * Common field definition that abstracts OpenAPI property, Pydantic field, or manual definition.
34
+ * Connectors transform their source-specific formats into this common shape.
35
+ *
36
+ * @typedef {object} UnifiedFieldSchema
37
+ * @property {string} name - Field name (e.g., 'email', 'created_at')
38
+ * @property {UnifiedFieldType} type - Unified field type
39
+ * @property {string} [label] - Human-readable label (e.g., 'Email Address')
40
+ * @property {boolean} [required] - Whether field is required (default: false)
41
+ * @property {boolean} [readOnly] - Whether field is read-only (default: false)
42
+ * @property {boolean} [hidden] - Whether field should be hidden in UI (default: false)
43
+ * @property {string} [format] - Original format hint from source (e.g., 'date-time', 'uri', 'email')
44
+ * @property {string[]} [enum] - Allowed values for enumeration fields
45
+ * @property {*} [default] - Default value for the field
46
+ * @property {UnifiedFieldReference} [reference] - Relation to another entity
47
+ * @property {Record<string, *>} [extensions] - Project-specific extensions
48
+ *
49
+ * @example
50
+ * // Simple text field
51
+ * const nameField = {
52
+ * name: 'name',
53
+ * type: 'text',
54
+ * label: 'Full Name',
55
+ * required: true
56
+ * }
57
+ *
58
+ * @example
59
+ * // Enum field with options
60
+ * const statusField = {
61
+ * name: 'status',
62
+ * type: 'text',
63
+ * label: 'Status',
64
+ * enum: ['draft', 'published', 'archived'],
65
+ * default: 'draft'
66
+ * }
67
+ *
68
+ * @example
69
+ * // Foreign key with reference
70
+ * const authorField = {
71
+ * name: 'author_id',
72
+ * type: 'number',
73
+ * label: 'Author',
74
+ * reference: {
75
+ * entity: 'users',
76
+ * labelField: 'username'
77
+ * }
78
+ * }
79
+ */
80
+
81
+ /**
82
+ * Unified Entity Schema
83
+ *
84
+ * Common entity definition that abstracts OpenAPI path/schema, Pydantic model, or manual definition.
85
+ * This is the primary contract between schema sources (connectors) and consumers (generators, runtime).
86
+ *
87
+ * @typedef {object} UnifiedEntitySchema
88
+ * @property {string} name - Entity name, typically plural lowercase (e.g., 'users', 'blog_posts')
89
+ * @property {string} endpoint - API endpoint path (e.g., '/users', '/api/v1/posts')
90
+ * @property {string} [label] - Human-readable singular label (e.g., 'User', 'Blog Post')
91
+ * @property {string} [labelPlural] - Human-readable plural label (e.g., 'Users', 'Blog Posts')
92
+ * @property {string} [labelField] - Field used as display label (e.g., 'name', 'title')
93
+ * @property {string} [routePrefix] - Route prefix for admin UI (e.g., 'user', 'blog-post')
94
+ * @property {string} [idField] - Primary key field name (default: 'id')
95
+ * @property {boolean} [readOnly] - Whether entity is read-only (no create/update/delete)
96
+ * @property {Record<string, UnifiedFieldSchema>} fields - Field definitions keyed by field name
97
+ * @property {Record<string, *>} [extensions] - Project-specific extensions
98
+ *
99
+ * @example
100
+ * // Complete entity schema
101
+ * const usersSchema = {
102
+ * name: 'users',
103
+ * endpoint: '/api/users',
104
+ * label: 'User',
105
+ * labelPlural: 'Users',
106
+ * labelField: 'username',
107
+ * routePrefix: 'user',
108
+ * idField: 'id',
109
+ * readOnly: false,
110
+ * fields: {
111
+ * id: { name: 'id', type: 'number', readOnly: true },
112
+ * username: { name: 'username', type: 'text', required: true },
113
+ * email: { name: 'email', type: 'email', required: true },
114
+ * created_at: { name: 'created_at', type: 'datetime', readOnly: true }
115
+ * }
116
+ * }
117
+ *
118
+ * @example
119
+ * // Read-only entity (external API)
120
+ * const countriesSchema = {
121
+ * name: 'countries',
122
+ * endpoint: 'https://restcountries.com/v3.1/all',
123
+ * label: 'Country',
124
+ * labelPlural: 'Countries',
125
+ * labelField: 'name',
126
+ * idField: 'cca3',
127
+ * readOnly: true,
128
+ * fields: {
129
+ * cca3: { name: 'cca3', type: 'text', label: 'Code' },
130
+ * name: { name: 'name', type: 'text', label: 'Name' }
131
+ * }
132
+ * }
133
+ */
134
+
135
+ /**
136
+ * Valid field types for UnifiedFieldSchema
137
+ *
138
+ * Used for validation and documentation. Maps to common UI input types:
139
+ * - text: Single-line text input
140
+ * - number: Numeric input
141
+ * - boolean: Checkbox/toggle
142
+ * - date: Date picker (date only)
143
+ * - datetime: Date-time picker
144
+ * - email: Email input with validation
145
+ * - url: URL input with validation
146
+ * - uuid: UUID text input
147
+ * - array: Multi-value field (tags, list)
148
+ * - object: Nested object (JSON editor or subform)
149
+ *
150
+ * @type {readonly UnifiedFieldType[]}
151
+ */
152
+ export const UNIFIED_FIELD_TYPES = Object.freeze([
153
+ 'text',
154
+ 'number',
155
+ 'boolean',
156
+ 'date',
157
+ 'datetime',
158
+ 'email',
159
+ 'url',
160
+ 'uuid',
161
+ 'array',
162
+ 'object'
163
+ ])
164
+
165
+ /**
166
+ * Check if a type is a valid UnifiedFieldType
167
+ *
168
+ * @param {string} type - Type to validate
169
+ * @returns {type is UnifiedFieldType} - True if valid
170
+ *
171
+ * @example
172
+ * isValidFieldType('text') // true
173
+ * isValidFieldType('string') // false (OpenAPI type, not unified)
174
+ */
175
+ export function isValidFieldType(type) {
176
+ return UNIFIED_FIELD_TYPES.includes(type)
177
+ }
178
+
179
+ /**
180
+ * Create a minimal field schema with defaults
181
+ *
182
+ * @param {string} name - Field name
183
+ * @param {UnifiedFieldType} type - Field type
184
+ * @param {Partial<UnifiedFieldSchema>} [overrides] - Additional field properties
185
+ * @returns {UnifiedFieldSchema} - Complete field schema
186
+ *
187
+ * @example
188
+ * const field = createFieldSchema('email', 'email', { required: true })
189
+ * // { name: 'email', type: 'email', required: true }
190
+ */
191
+ export function createFieldSchema(name, type, overrides = {}) {
192
+ return {
193
+ name,
194
+ type,
195
+ ...overrides
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Create a minimal entity schema with defaults
201
+ *
202
+ * @param {string} name - Entity name
203
+ * @param {string} endpoint - API endpoint
204
+ * @param {Record<string, UnifiedFieldSchema>} fields - Field definitions
205
+ * @param {Partial<UnifiedEntitySchema>} [overrides] - Additional entity properties
206
+ * @returns {UnifiedEntitySchema} - Complete entity schema
207
+ *
208
+ * @example
209
+ * const schema = createEntitySchema('users', '/api/users', {
210
+ * id: createFieldSchema('id', 'number', { readOnly: true }),
211
+ * name: createFieldSchema('name', 'text', { required: true })
212
+ * })
213
+ */
214
+ export function createEntitySchema(name, endpoint, fields, overrides = {}) {
215
+ return {
216
+ name,
217
+ endpoint,
218
+ fields,
219
+ ...overrides
220
+ }
221
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Vite Plugin for qdadm EntityManager Generation
3
+ *
4
+ * Integrates generateManagers with Vite's build pipeline, triggering
5
+ * entity manager file generation during the buildStart hook.
6
+ *
7
+ * @module gen/vite-plugin
8
+ */
9
+
10
+ import { generateManagers } from './generateManagers.js'
11
+ import { pathToFileURL } from 'node:url'
12
+ import { resolve } from 'node:path'
13
+
14
+ /**
15
+ * Plugin options for qdadmGen
16
+ *
17
+ * @typedef {object} QdadmGenOptions
18
+ * @property {string} [config] - Path to qdadm config file (default: 'qdadm.config.js')
19
+ * @property {string} [output] - Override output directory for generated files
20
+ */
21
+
22
+ /**
23
+ * Vite plugin for generating EntityManager files at build time
24
+ *
25
+ * Loads the qdadm configuration file and calls generateManagers during
26
+ * Vite's buildStart hook. Supports both ESM and CommonJS config files.
27
+ *
28
+ * @param {QdadmGenOptions} [options={}] - Plugin options
29
+ * @returns {import('vite').Plugin} Vite plugin object
30
+ *
31
+ * @example
32
+ * // vite.config.js
33
+ * import { defineConfig } from 'vite'
34
+ * import { qdadmGen } from 'qdadm/gen/vite-plugin'
35
+ *
36
+ * export default defineConfig({
37
+ * plugins: [
38
+ * qdadmGen({
39
+ * config: './qdadm.config.js',
40
+ * output: 'src/generated/managers/'
41
+ * })
42
+ * ]
43
+ * })
44
+ *
45
+ * @example
46
+ * // qdadm.config.js
47
+ * export default {
48
+ * output: 'src/generated/managers/',
49
+ * entities: {
50
+ * users: {
51
+ * schema: { name: 'users', fields: { id: { type: 'integer' } } },
52
+ * endpoint: '/api/users',
53
+ * storageImport: 'qdadm',
54
+ * storageClass: 'ApiStorage'
55
+ * }
56
+ * }
57
+ * }
58
+ */
59
+ export function qdadmGen(options = {}) {
60
+ const configPath = options.config || 'qdadm.config.js'
61
+
62
+ return {
63
+ name: 'qdadm-gen',
64
+
65
+ async buildStart() {
66
+ try {
67
+ // Resolve config path relative to cwd
68
+ const resolvedConfigPath = resolve(process.cwd(), configPath)
69
+
70
+ // Load config file dynamically
71
+ const configUrl = pathToFileURL(resolvedConfigPath).href
72
+ const configModule = await import(configUrl)
73
+ const loadedConfig = configModule.default || configModule
74
+
75
+ // Validate loaded config
76
+ if (!loadedConfig || typeof loadedConfig !== 'object') {
77
+ throw new Error(`Invalid qdadm config at '${configPath}': must export an object`)
78
+ }
79
+
80
+ if (!loadedConfig.entities) {
81
+ throw new Error(`Invalid qdadm config at '${configPath}': missing 'entities' property`)
82
+ }
83
+
84
+ // Merge plugin options with config (plugin options take precedence)
85
+ const mergedConfig = {
86
+ ...loadedConfig,
87
+ ...(options.output && { output: options.output })
88
+ }
89
+
90
+ // Generate managers
91
+ const generatedFiles = await generateManagers(mergedConfig)
92
+
93
+ // Log results
94
+ console.log(`[qdadm-gen] Generated ${generatedFiles.length} manager file(s)`)
95
+ for (const file of generatedFiles) {
96
+ console.log(` - ${file}`)
97
+ }
98
+ } catch (error) {
99
+ // Wrap error with plugin context for clearer messages
100
+ const message = error instanceof Error ? error.message : String(error)
101
+ throw new Error(`[qdadm-gen] Failed to generate managers: ${message}`)
102
+ }
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * TestManager - Auto-generated EntityManager
3
+ *
4
+ * Generated by qdadm/gen/generateManagers
5
+ * DO NOT EDIT MANUALLY - Changes will be overwritten
6
+ *
7
+ * Entity: test
8
+ * Endpoint: /test
9
+ */
10
+
11
+ import { EntityManager } from 'qdadm'
12
+ import { ApiStorage } from 'qdadm'
13
+
14
+ /**
15
+ * Schema definition for test
16
+ * @type {import('qdadm/gen').UnifiedEntitySchema}
17
+ */
18
+ export const testSchema = {
19
+ name: "test",
20
+ endpoint: "/test",
21
+ fields: {}
22
+ }
23
+
24
+ /**
25
+ * Storage options for test
26
+ */
27
+ const storageOptions = {
28
+ endpoint: "/test"
29
+ }
30
+
31
+ /**
32
+ * TestManager instance
33
+ *
34
+ * Provides CRUD operations for test entity.
35
+ *
36
+ * @type {EntityManager}
37
+ */
38
+ export const testManager = new EntityManager({
39
+ ...{
40
+ name: "test",
41
+ idField: "id",
42
+ fields: {}
43
+ },
44
+ storage: new ApiStorage(storageOptions)
45
+ })
package/src/index.js CHANGED
@@ -35,6 +35,9 @@ export * from './zones/index.js'
35
35
  // Hooks
36
36
  export * from './hooks/index.js'
37
37
 
38
+ // Deferred (async service loading)
39
+ export * from './deferred/index.js'
40
+
38
41
  // Core (extension helpers)
39
42
  export * from './core/index.js'
40
43
 
@@ -0,0 +1,264 @@
1
+ /**
2
+ * EventRouter - Declarative signal routing
3
+ *
4
+ * Routes one signal to multiple targets (signals or callbacks).
5
+ * Configured at Kernel level to keep components simple.
6
+ *
7
+ * Usage:
8
+ * ```js
9
+ * const router = new EventRouter({
10
+ * signals, // SignalBus instance
11
+ * orchestrator, // Orchestrator instance (optional, for callbacks)
12
+ * routes: {
13
+ * 'auth:impersonate': [
14
+ * 'cache:entity:invalidate:loans', // string = emit signal
15
+ * { signal: 'notify', transform: (p) => ({ msg: p.user }) }, // object = transform
16
+ * (payload, ctx) => { ... } // function = callback
17
+ * ]
18
+ * }
19
+ * })
20
+ * ```
21
+ */
22
+
23
+ /**
24
+ * Detect cycles in route graph using DFS
25
+ *
26
+ * @param {object} routes - Route configuration
27
+ * @returns {string[]|null} - Cycle path if found, null otherwise
28
+ */
29
+ function detectCycles(routes) {
30
+ // Build adjacency list (only signal targets, not callbacks)
31
+ const graph = new Map()
32
+
33
+ for (const [source, targets] of Object.entries(routes)) {
34
+ const signalTargets = []
35
+ for (const target of targets) {
36
+ if (typeof target === 'string') {
37
+ signalTargets.push(target)
38
+ } else if (target && typeof target === 'object' && target.signal) {
39
+ signalTargets.push(target.signal)
40
+ }
41
+ // Functions don't create edges in the graph
42
+ }
43
+ graph.set(source, signalTargets)
44
+ }
45
+
46
+ // DFS cycle detection
47
+ const visited = new Set()
48
+ const recursionStack = new Set()
49
+ const path = []
50
+
51
+ function dfs(node) {
52
+ visited.add(node)
53
+ recursionStack.add(node)
54
+ path.push(node)
55
+
56
+ const neighbors = graph.get(node) || []
57
+ for (const neighbor of neighbors) {
58
+ if (!visited.has(neighbor)) {
59
+ const cycle = dfs(neighbor)
60
+ if (cycle) return cycle
61
+ } else if (recursionStack.has(neighbor)) {
62
+ // Found cycle - return path from neighbor to current
63
+ const cycleStart = path.indexOf(neighbor)
64
+ return [...path.slice(cycleStart), neighbor]
65
+ }
66
+ }
67
+
68
+ path.pop()
69
+ recursionStack.delete(node)
70
+ return null
71
+ }
72
+
73
+ // Check all nodes
74
+ for (const node of graph.keys()) {
75
+ if (!visited.has(node)) {
76
+ const cycle = dfs(node)
77
+ if (cycle) return cycle
78
+ }
79
+ }
80
+
81
+ return null
82
+ }
83
+
84
+ export class EventRouter {
85
+ /**
86
+ * @param {object} options
87
+ * @param {SignalBus} options.signals - SignalBus instance
88
+ * @param {Orchestrator} [options.orchestrator] - Orchestrator (for callback context)
89
+ * @param {object} options.routes - Route configuration
90
+ * @param {boolean} [options.debug=false] - Enable debug logging
91
+ */
92
+ constructor(options = {}) {
93
+ const { signals, orchestrator = null, routes = {}, debug = false } = options
94
+
95
+ if (!signals) {
96
+ throw new Error('[EventRouter] signals is required')
97
+ }
98
+
99
+ this._signals = signals
100
+ this._orchestrator = orchestrator
101
+ this._routes = routes
102
+ this._debug = debug
103
+ this._cleanups = []
104
+
105
+ // Validate and setup
106
+ this._validateRoutes()
107
+ this._setupListeners()
108
+ }
109
+
110
+ /**
111
+ * Validate routes configuration and check for cycles
112
+ * @private
113
+ */
114
+ _validateRoutes() {
115
+ // Check for cycles
116
+ const cycle = detectCycles(this._routes)
117
+ if (cycle) {
118
+ throw new Error(
119
+ `[EventRouter] Cycle detected: ${cycle.join(' -> ')}`
120
+ )
121
+ }
122
+
123
+ // Validate each route
124
+ for (const [source, targets] of Object.entries(this._routes)) {
125
+ if (!Array.isArray(targets)) {
126
+ throw new Error(
127
+ `[EventRouter] Route "${source}" must be an array of targets`
128
+ )
129
+ }
130
+
131
+ for (let i = 0; i < targets.length; i++) {
132
+ const target = targets[i]
133
+ const isString = typeof target === 'string'
134
+ const isFunction = typeof target === 'function'
135
+ const isObject = target && typeof target === 'object' && target.signal
136
+
137
+ if (!isString && !isFunction && !isObject) {
138
+ throw new Error(
139
+ `[EventRouter] Invalid target at "${source}"[${i}]: must be string, function, or { signal, transform? }`
140
+ )
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Setup signal listeners for all routes
148
+ * @private
149
+ */
150
+ _setupListeners() {
151
+ for (const [source, targets] of Object.entries(this._routes)) {
152
+ const cleanup = this._signals.on(source, (payload) => {
153
+ this._handleRoute(source, payload, targets)
154
+ })
155
+ this._cleanups.push(cleanup)
156
+ }
157
+
158
+ if (this._debug) {
159
+ console.debug(`[EventRouter] Registered ${Object.keys(this._routes).length} routes`)
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Handle a routed signal
165
+ * @private
166
+ */
167
+ _handleRoute(source, payload, targets) {
168
+ if (this._debug) {
169
+ console.debug(`[EventRouter] ${source} -> ${targets.length} targets`)
170
+ }
171
+
172
+ const context = {
173
+ signals: this._signals,
174
+ orchestrator: this._orchestrator
175
+ }
176
+
177
+ for (const target of targets) {
178
+ try {
179
+ if (typeof target === 'string') {
180
+ // String: emit signal with same payload
181
+ this._signals.emit(target, payload)
182
+ if (this._debug) {
183
+ console.debug(`[EventRouter] -> ${target} (forward)`)
184
+ }
185
+ } else if (typeof target === 'function') {
186
+ // Function: call callback
187
+ target(payload, context)
188
+ if (this._debug) {
189
+ console.debug(`[EventRouter] -> callback()`)
190
+ }
191
+ } else if (target && target.signal) {
192
+ // Object: emit signal with transformed payload
193
+ const transformedPayload = target.transform
194
+ ? target.transform(payload)
195
+ : payload
196
+ this._signals.emit(target.signal, transformedPayload)
197
+ if (this._debug) {
198
+ console.debug(`[EventRouter] -> ${target.signal} (transform)`)
199
+ }
200
+ }
201
+ } catch (error) {
202
+ console.error(`[EventRouter] Error handling ${source} -> ${JSON.stringify(target)}:`, error)
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Add a route dynamically
209
+ *
210
+ * @param {string} source - Source signal
211
+ * @param {Array} targets - Target array
212
+ */
213
+ addRoute(source, targets) {
214
+ if (this._routes[source]) {
215
+ throw new Error(`[EventRouter] Route "${source}" already exists`)
216
+ }
217
+
218
+ // Validate new route doesn't create cycle
219
+ const testRoutes = { ...this._routes, [source]: targets }
220
+ const cycle = detectCycles(testRoutes)
221
+ if (cycle) {
222
+ throw new Error(`[EventRouter] Adding route would create cycle: ${cycle.join(' -> ')}`)
223
+ }
224
+
225
+ this._routes[source] = targets
226
+
227
+ // Setup listener
228
+ const cleanup = this._signals.on(source, (payload) => {
229
+ this._handleRoute(source, payload, targets)
230
+ })
231
+ this._cleanups.push(cleanup)
232
+ }
233
+
234
+ /**
235
+ * Get all registered routes
236
+ * @returns {object}
237
+ */
238
+ getRoutes() {
239
+ return { ...this._routes }
240
+ }
241
+
242
+ /**
243
+ * Dispose the router, cleaning up all listeners
244
+ */
245
+ dispose() {
246
+ for (const cleanup of this._cleanups) {
247
+ if (typeof cleanup === 'function') {
248
+ cleanup()
249
+ }
250
+ }
251
+ this._cleanups = []
252
+ this._routes = {}
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Factory function for creating EventRouter
258
+ *
259
+ * @param {object} options
260
+ * @returns {EventRouter}
261
+ */
262
+ export function createEventRouter(options) {
263
+ return new EventRouter(options)
264
+ }