qdadm 0.26.3 → 0.28.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.
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import {
3
+ parseStoragePattern,
4
+ storageFactory,
5
+ defaultStorageResolver,
6
+ createStorageFactory,
7
+ storageTypes
8
+ } from './factory.js'
9
+ import { IStorage } from './IStorage.js'
10
+ import { ApiStorage } from './ApiStorage.js'
11
+ import { LocalStorage } from './LocalStorage.js'
12
+ import { MemoryStorage } from './MemoryStorage.js'
13
+ import { MockApiStorage } from './MockApiStorage.js'
14
+
15
+ describe('parseStoragePattern', () => {
16
+ it('parses api pattern with endpoint', () => {
17
+ const result = parseStoragePattern('api:/api/bots')
18
+ expect(result).toEqual({ type: 'api', endpoint: '/api/bots' })
19
+ })
20
+
21
+ it('parses local pattern with key', () => {
22
+ const result = parseStoragePattern('local:myKey')
23
+ expect(result).toEqual({ type: 'local', key: 'myKey' })
24
+ })
25
+
26
+ it('parses memory pattern with key', () => {
27
+ const result = parseStoragePattern('memory:cache')
28
+ expect(result).toEqual({ type: 'memory', key: 'cache' })
29
+ })
30
+
31
+ it('parses mock pattern with entityName', () => {
32
+ const result = parseStoragePattern('mock:users')
33
+ expect(result).toEqual({ type: 'mock', entityName: 'users' })
34
+ })
35
+
36
+ it('parses sdk pattern with endpoint', () => {
37
+ const result = parseStoragePattern('sdk:users')
38
+ expect(result).toEqual({ type: 'sdk', endpoint: 'users' })
39
+ })
40
+
41
+ it('treats bare path as api endpoint', () => {
42
+ const result = parseStoragePattern('/api/tasks')
43
+ expect(result).toEqual({ type: 'api', endpoint: '/api/tasks' })
44
+ })
45
+
46
+ it('returns null for invalid pattern', () => {
47
+ const result = parseStoragePattern('invalid')
48
+ expect(result).toBeNull()
49
+ })
50
+ })
51
+
52
+ describe('defaultStorageResolver', () => {
53
+ it('creates ApiStorage for api type', () => {
54
+ const storage = defaultStorageResolver({ type: 'api', endpoint: '/api/bots' }, 'bots')
55
+ expect(storage).toBeInstanceOf(ApiStorage)
56
+ expect(storage.endpoint).toBe('/api/bots')
57
+ })
58
+
59
+ it('creates LocalStorage for local type', () => {
60
+ const storage = defaultStorageResolver({ type: 'local', key: 'myKey' }, 'items')
61
+ expect(storage).toBeInstanceOf(LocalStorage)
62
+ expect(storage.storageKey).toBe('myKey')
63
+ })
64
+
65
+ it('creates MemoryStorage for memory type', () => {
66
+ const storage = defaultStorageResolver({ type: 'memory', key: 'cache' }, 'items')
67
+ expect(storage).toBeInstanceOf(MemoryStorage)
68
+ })
69
+
70
+ it('creates MockApiStorage for mock type', () => {
71
+ const storage = defaultStorageResolver({ type: 'mock', entityName: 'users' }, 'users')
72
+ expect(storage).toBeInstanceOf(MockApiStorage)
73
+ })
74
+
75
+ it('throws for unknown type', () => {
76
+ expect(() => defaultStorageResolver({ type: 'unknown' }, 'items'))
77
+ .toThrow('Unknown storage type: unknown')
78
+ })
79
+ })
80
+
81
+ describe('storageFactory', () => {
82
+ it('returns IStorage instance directly', () => {
83
+ const storage = new ApiStorage({ endpoint: '/api/test' })
84
+ const result = storageFactory(storage, 'test')
85
+ expect(result).toBe(storage)
86
+ })
87
+
88
+ it('returns duck-typed storage directly', () => {
89
+ const duckStorage = {
90
+ list: () => Promise.resolve({ items: [], total: 0 }),
91
+ get: () => Promise.resolve(null)
92
+ }
93
+ const result = storageFactory(duckStorage, 'test')
94
+ expect(result).toBe(duckStorage)
95
+ })
96
+
97
+ it('parses string pattern and creates storage', () => {
98
+ const result = storageFactory('api:/api/bots', 'bots')
99
+ expect(result).toBeInstanceOf(ApiStorage)
100
+ expect(result.endpoint).toBe('/api/bots')
101
+ })
102
+
103
+ it('handles bare path as api endpoint', () => {
104
+ const result = storageFactory('/api/tasks', 'tasks')
105
+ expect(result).toBeInstanceOf(ApiStorage)
106
+ expect(result.endpoint).toBe('/api/tasks')
107
+ })
108
+
109
+ it('handles config object with type', () => {
110
+ const result = storageFactory({ type: 'memory', key: 'cache' }, 'items')
111
+ expect(result).toBeInstanceOf(MemoryStorage)
112
+ })
113
+
114
+ it('handles config object with string storage', () => {
115
+ const result = storageFactory({ storage: 'api:/api/items' }, 'items')
116
+ expect(result).toBeInstanceOf(ApiStorage)
117
+ })
118
+
119
+ it('uses custom resolver when provided', () => {
120
+ const customResolver = vi.fn().mockReturnValue(new MemoryStorage())
121
+ const result = storageFactory('api:/test', 'test', customResolver)
122
+ expect(customResolver).toHaveBeenCalledWith(
123
+ { type: 'api', endpoint: '/test' },
124
+ 'test'
125
+ )
126
+ expect(result).toBeInstanceOf(MemoryStorage)
127
+ })
128
+
129
+ it('throws for invalid config', () => {
130
+ expect(() => storageFactory(123, 'test'))
131
+ .toThrow('Invalid storage config')
132
+ })
133
+
134
+ it('throws for unparseable string', () => {
135
+ expect(() => storageFactory('invalid', 'test'))
136
+ .toThrow('Cannot parse storage pattern')
137
+ })
138
+ })
139
+
140
+ describe('createStorageFactory', () => {
141
+ it('creates factory with bound context', () => {
142
+ const customResolver = vi.fn().mockReturnValue(new MemoryStorage())
143
+ const factory = createStorageFactory(customResolver)
144
+
145
+ const result = factory('api:/test', 'test')
146
+
147
+ expect(customResolver).toHaveBeenCalled()
148
+ expect(result).toBeInstanceOf(MemoryStorage)
149
+ })
150
+ })
151
+
152
+ describe('storageTypes', () => {
153
+ it('exports all storage type classes', () => {
154
+ expect(storageTypes.api).toBe(ApiStorage)
155
+ expect(storageTypes.local).toBe(LocalStorage)
156
+ expect(storageTypes.memory).toBe(MemoryStorage)
157
+ expect(storageTypes.mock).toBe(MockApiStorage)
158
+ })
159
+ })
@@ -110,6 +110,19 @@ export function getStorageCapabilities(storage) {
110
110
  }
111
111
  }
112
112
 
113
+ // Base class
114
+ export { IStorage } from './IStorage.js'
115
+
116
+ // Factory/Resolver
117
+ export {
118
+ storageFactory,
119
+ defaultStorageResolver,
120
+ createStorageFactory,
121
+ parseStoragePattern,
122
+ storageTypes
123
+ } from './factory.js'
124
+
125
+ // Storage adapters
113
126
  export { ApiStorage, createApiStorage } from './ApiStorage.js'
114
127
  export { LocalStorage, createLocalStorage } from './LocalStorage.js'
115
128
  export { MemoryStorage, createMemoryStorage } from './MemoryStorage.js'
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
+ }
@@ -52,6 +52,10 @@ import { createZoneRegistry } from '../zones/ZoneRegistry.js'
52
52
  import { registerStandardZones } from '../zones/zones.js'
53
53
  import { createHookRegistry } from '../hooks/HookRegistry.js'
54
54
  import { createSecurityChecker } from '../entity/auth/SecurityChecker.js'
55
+ import { createManagers } from '../entity/factory.js'
56
+ import { defaultStorageResolver } from '../entity/storage/factory.js'
57
+ import { createDeferredRegistry } from '../deferred/DeferredRegistry.js'
58
+ import { createEventRouter } from './EventRouter.js'
55
59
 
56
60
  export class Kernel {
57
61
  /**
@@ -60,7 +64,10 @@ export class Kernel {
60
64
  * @param {object} options.modules - Result of import.meta.glob for module init files
61
65
  * @param {object} options.modulesOptions - Options for initModules (e.g., { coreNavItems })
62
66
  * @param {string[]} options.sectionOrder - Navigation section order
63
- * @param {object} options.managers - Entity managers { name: EntityManager }
67
+ * @param {object} options.managers - Entity managers { name: config } - can be instances, strings, or config objects
68
+ * @param {object} options.managerRegistry - Registry of manager classes from qdadm-gen { name: ManagerClass }
69
+ * @param {function} options.storageResolver - Custom storage resolver (config, entityName) => Storage
70
+ * @param {function} options.managerResolver - Custom manager resolver (config, entityName, context) => Manager
64
71
  * @param {object} options.authAdapter - Auth adapter for login/logout (app-level authentication)
65
72
  * @param {object} options.entityAuthAdapter - Auth adapter for entity permissions (scope/silo checks)
66
73
  * @param {object} options.pages - Page components { login, layout }
@@ -73,6 +80,8 @@ export class Kernel {
73
80
  * @param {object} options.primevue - PrimeVue config { plugin, theme, options }
74
81
  * @param {object} options.layouts - Layout components { list, form, dashboard, base }
75
82
  * @param {object} options.security - Security config { role_hierarchy, role_permissions, entity_permissions }
83
+ * @param {boolean} options.warmup - Enable warmup at boot (default: true)
84
+ * @param {object} options.eventRouter - EventRouter config { 'source:signal': ['target:signal', ...] }
76
85
  */
77
86
  constructor(options) {
78
87
  this.options = options
@@ -82,6 +91,8 @@ export class Kernel {
82
91
  this.orchestrator = null
83
92
  this.zoneRegistry = null
84
93
  this.hookRegistry = null
94
+ this.deferred = null
95
+ this.eventRouter = null
85
96
  this.layoutComponents = null
86
97
  this.securityChecker = null
87
98
  }
@@ -95,19 +106,57 @@ export class Kernel {
95
106
  this._createSignalBus()
96
107
  this._createHookRegistry()
97
108
  this._createZoneRegistry()
98
- // 2. Initialize modules (can use all services, registers routes)
109
+ this._createDeferredRegistry()
110
+ // 2. Register auth:ready deferred (if auth configured)
111
+ this._registerAuthDeferred()
112
+ // 3. Initialize modules (can use all services, registers routes)
99
113
  this._initModules()
100
- // 3. Create router (needs routes from modules)
114
+ // 4. Create router (needs routes from modules)
101
115
  this._createRouter()
102
- // 4. Create orchestrator and remaining components
116
+ // 5. Create orchestrator and remaining components
103
117
  this._createOrchestrator()
118
+ // 6. Create EventRouter (needs signals + orchestrator)
119
+ this._createEventRouter()
104
120
  this._setupSecurity()
105
121
  this._createLayoutComponents()
106
122
  this._createVueApp()
107
123
  this._installPlugins()
124
+ // 6. Fire warmups (fire-and-forget, pages await via DeferredRegistry)
125
+ this._fireWarmups()
108
126
  return this.vueApp
109
127
  }
110
128
 
129
+ /**
130
+ * Register auth:ready deferred if auth is configured
131
+ * This allows warmup and other services to await authentication.
132
+ */
133
+ _registerAuthDeferred() {
134
+ const { authAdapter } = this.options
135
+ if (!authAdapter) return
136
+
137
+ // Create a promise that resolves on first auth:login
138
+ this.deferred.queue('auth:ready', () => {
139
+ return new Promise(resolve => {
140
+ this.signals.once('auth:login', ({ user }) => {
141
+ resolve(user)
142
+ })
143
+ })
144
+ })
145
+ }
146
+
147
+ /**
148
+ * Fire entity cache warmups
149
+ * Fire-and-forget: pages that need cache will await via DeferredRegistry.
150
+ * Controlled by options.warmup (default: true).
151
+ */
152
+ _fireWarmups() {
153
+ const warmup = this.options.warmup ?? true
154
+ if (!warmup) return
155
+
156
+ // Fire-and-forget: each manager awaits its dependencies (auth:ready, etc.)
157
+ this.orchestrator.fireWarmups()
158
+ }
159
+
111
160
  /**
112
161
  * Initialize modules from glob import
113
162
  * Passes services to modules for zone/signal/hook registration
@@ -121,7 +170,8 @@ export class Kernel {
121
170
  ...this.options.modulesOptions,
122
171
  zones: this.zoneRegistry,
123
172
  signals: this.signals,
124
- hooks: this.hookRegistry
173
+ hooks: this.hookRegistry,
174
+ deferred: this.deferred
125
175
  })
126
176
  }
127
177
  }
@@ -215,12 +265,28 @@ export class Kernel {
215
265
  * Create orchestrator with managers and signal bus
216
266
  * Injects entityAuthAdapter and hookRegistry into all managers for permission checks
217
267
  * and lifecycle hook support.
268
+ *
269
+ * Uses createManagers() to resolve manager configs through the factory pattern:
270
+ * - String patterns ('api:/api/bots') → creates storage + manager
271
+ * - Config objects ({ storage: '...', label: '...' }) → resolved
272
+ * - Manager instances → passed through directly
218
273
  */
219
274
  _createOrchestrator() {
275
+ // Build factory context with resolvers and registry
276
+ const factoryContext = {
277
+ storageResolver: this.options.storageResolver || defaultStorageResolver,
278
+ managerResolver: this.options.managerResolver,
279
+ managerRegistry: this.options.managerRegistry || {}
280
+ }
281
+
282
+ // Resolve all managers through factory
283
+ const managers = createManagers(this.options.managers || {}, factoryContext)
284
+
220
285
  this.orchestrator = new Orchestrator({
221
- managers: this.options.managers || {},
286
+ managers,
222
287
  signals: this.signals,
223
288
  hooks: this.hookRegistry,
289
+ deferred: this.deferred,
224
290
  entityAuthAdapter: this.options.entityAuthAdapter || null
225
291
  })
226
292
  }
@@ -273,6 +339,35 @@ export class Kernel {
273
339
  registerStandardZones(this.zoneRegistry)
274
340
  }
275
341
 
342
+ /**
343
+ * Create deferred registry for async service loading
344
+ * Enables loose coupling between services and components via named promises.
345
+ */
346
+ _createDeferredRegistry() {
347
+ const debug = this.options.debug ?? false
348
+ this.deferred = createDeferredRegistry({
349
+ kernel: this.signals?.getKernel(),
350
+ debug
351
+ })
352
+ }
353
+
354
+ /**
355
+ * Create EventRouter for declarative signal routing
356
+ * Transforms high-level events into targeted signals.
357
+ */
358
+ _createEventRouter() {
359
+ const { eventRouter: routes } = this.options
360
+ if (!routes || Object.keys(routes).length === 0) return
361
+
362
+ const debug = this.options.debug ?? false
363
+ this.eventRouter = createEventRouter({
364
+ signals: this.signals,
365
+ orchestrator: this.orchestrator,
366
+ routes,
367
+ debug
368
+ })
369
+ }
370
+
276
371
  /**
277
372
  * Create layout components map for useLayoutResolver
278
373
  * Maps layout types to their Vue components.
@@ -302,7 +397,7 @@ export class Kernel {
302
397
  */
303
398
  _installPlugins() {
304
399
  const app = this.vueApp
305
- const { managers, authAdapter, features, primevue } = this.options
400
+ const { authAdapter, features, primevue } = this.options
306
401
 
307
402
  // Pinia
308
403
  app.use(createPinia())
@@ -346,13 +441,17 @@ export class Kernel {
346
441
  // Hook registry injection
347
442
  app.provide('qdadmHooks', this.hookRegistry)
348
443
 
444
+ // Deferred registry injection
445
+ app.provide('qdadmDeferred', this.deferred)
446
+
349
447
  // Layout components injection for useLayoutResolver
350
448
  app.provide('qdadmLayoutComponents', this.layoutComponents)
351
449
 
352
450
  // qdadm plugin
451
+ // Note: Don't pass managers here - orchestrator already has resolved managers
452
+ // from createManagers(). Passing raw configs would overwrite them.
353
453
  app.use(createQdadm({
354
454
  orchestrator: this.orchestrator,
355
- managers,
356
455
  authAdapter,
357
456
  router: this.router,
358
457
  toast: app.config.globalProperties.$toast,
@@ -423,6 +522,22 @@ export class Kernel {
423
522
  return this.hookRegistry
424
523
  }
425
524
 
525
+ /**
526
+ * Get the DeferredRegistry instance
527
+ * @returns {import('../deferred/DeferredRegistry.js').DeferredRegistry}
528
+ */
529
+ getDeferredRegistry() {
530
+ return this.deferred
531
+ }
532
+
533
+ /**
534
+ * Get the EventRouter instance
535
+ * @returns {import('./EventRouter.js').EventRouter|null}
536
+ */
537
+ getEventRouter() {
538
+ return this.eventRouter
539
+ }
540
+
426
541
  /**
427
542
  * Get the layout components map
428
543
  * @returns {object} Layout components by type
@@ -12,3 +12,7 @@ export {
12
12
  SIGNAL_ACTIONS,
13
13
  buildSignal,
14
14
  } from './SignalBus.js'
15
+ export {
16
+ EventRouter,
17
+ createEventRouter,
18
+ } from './EventRouter.js'