qdadm 0.27.0 → 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.
@@ -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'
@@ -40,6 +40,8 @@ export class Orchestrator {
40
40
  signals = null,
41
41
  // HookRegistry instance for lifecycle hooks
42
42
  hooks = null,
43
+ // DeferredRegistry instance for async warmup
44
+ deferred = null,
43
45
  // Optional: AuthAdapter for entity permission checks (scope/silo)
44
46
  entityAuthAdapter = null
45
47
  } = options
@@ -48,6 +50,7 @@ export class Orchestrator {
48
50
  this._managers = new Map()
49
51
  this._signals = signals
50
52
  this._hooks = hooks
53
+ this._deferred = deferred
51
54
  this._entityAuthAdapter = entityAuthAdapter
52
55
 
53
56
  // Register pre-provided managers
@@ -106,6 +109,22 @@ export class Orchestrator {
106
109
  return this._hooks
107
110
  }
108
111
 
112
+ /**
113
+ * Set the DeferredRegistry instance
114
+ * @param {DeferredRegistry} deferred
115
+ */
116
+ setDeferred(deferred) {
117
+ this._deferred = deferred
118
+ }
119
+
120
+ /**
121
+ * Get the DeferredRegistry instance
122
+ * @returns {DeferredRegistry|null}
123
+ */
124
+ get deferred() {
125
+ return this._deferred
126
+ }
127
+
109
128
  /**
110
129
  * Set the entity factory
111
130
  * @param {function} factory - (entityName, entityConfig) => EntityManager
@@ -192,6 +211,47 @@ export class Orchestrator {
192
211
  return Array.from(this._managers.keys())
193
212
  }
194
213
 
214
+ /**
215
+ * Fire warmup for all managers (fire-and-forget)
216
+ *
217
+ * Triggers cache preloading for all managers with warmupEnabled.
218
+ * Each manager registers its warmup in the DeferredRegistry, allowing
219
+ * pages to await specific entities before rendering.
220
+ *
221
+ * IMPORTANT: This is fire-and-forget by design. Don't await this method.
222
+ * Components await what they need via DeferredRegistry:
223
+ * `await deferred.await('entity:books:cache')`
224
+ *
225
+ * @returns {Promise<Map<string, any>>} For debugging/logging only
226
+ *
227
+ * @example
228
+ * ```js
229
+ * // At boot (fire-and-forget)
230
+ * orchestrator.fireWarmups()
231
+ *
232
+ * // In component - await specific entity cache
233
+ * await deferred.await('entity:books:cache')
234
+ * const { items } = await booksManager.list() // Uses local cache
235
+ * ```
236
+ */
237
+ fireWarmups() {
238
+ const results = new Map()
239
+
240
+ for (const [name, manager] of this._managers) {
241
+ if (manager.warmup && manager.warmupEnabled) {
242
+ // Fire each warmup - they register themselves in DeferredRegistry
243
+ manager.warmup().then(result => {
244
+ results.set(name, result)
245
+ }).catch(error => {
246
+ console.warn(`[Orchestrator] Warmup failed for ${name}:`, error.message)
247
+ results.set(name, error)
248
+ })
249
+ }
250
+ }
251
+
252
+ return results
253
+ }
254
+
195
255
  /**
196
256
  * Dispose all managers
197
257
  */
@@ -115,9 +115,12 @@ export class FilterQuery {
115
115
  /**
116
116
  * Set the SignalBus for cache invalidation
117
117
  *
118
- * For entity sources, subscribes to CRUD signals ({entity}:created, {entity}:updated,
119
- * {entity}:deleted) to automatically invalidate cached options when the source
120
- * entity changes.
118
+ * Subscribes to entity CRUD signals ({entity}:created, {entity}:updated, {entity}:deleted)
119
+ * to invalidate cached options when the source entity changes.
120
+ *
121
+ * Note: Auth changes are handled by Vue component lifecycle - when user logs out,
122
+ * router guard redirects to login, component unmounts, FilterQuery is disposed.
123
+ * On re-login, new FilterQuery is created with empty cache.
121
124
  *
122
125
  * @param {SignalBus} signals
123
126
  * @returns {FilterQuery} this for chaining
@@ -128,8 +131,10 @@ export class FilterQuery {
128
131
 
129
132
  this._signals = signals
130
133
 
134
+ if (!signals) return this
135
+
131
136
  // Subscribe to entity CRUD signals for cache invalidation
132
- if (this.source === 'entity' && this.entity && signals) {
137
+ if (this.source === 'entity' && this.entity) {
133
138
  const actions = ['created', 'updated', 'deleted']
134
139
  for (const action of actions) {
135
140
  const signalName = `${this.entity}:${action}`