qdadm 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +1 -0
- package/src/composables/useAuth.js +43 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/debug/AuthCollector.js +254 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +376 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +103 -0
- package/src/debug/components/panels/EntitiesPanel.vue +616 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/storage/MockApiStorage.js +17 -1
- package/src/entity/storage/index.js +9 -2
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +436 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SignalBus.js +2 -7
- package/src/kernel/index.js +14 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- package/src/toast/useSignalToast.js +113 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModuleLoader - Duck typing module loader with dependency resolution
|
|
3
|
+
*
|
|
4
|
+
* Detects and normalizes multiple module formats:
|
|
5
|
+
* 1. Module instance - use directly
|
|
6
|
+
* 2. Module class - instantiate with new
|
|
7
|
+
* 3. Plain object with connect() - wrap in adapter
|
|
8
|
+
* 4. Plain function - wrap as legacy init({ registry, zones })
|
|
9
|
+
*
|
|
10
|
+
* Provides topological sorting based on requires + priority.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Module } from './Module.js'
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Custom Errors
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Error thrown when a required module is not registered
|
|
21
|
+
*/
|
|
22
|
+
export class ModuleNotFoundError extends Error {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} moduleName - Name of the missing module
|
|
25
|
+
* @param {string} requiredBy - Name of the module that requires it
|
|
26
|
+
*/
|
|
27
|
+
constructor(moduleName, requiredBy) {
|
|
28
|
+
super(`Module '${moduleName}' not found (required by '${requiredBy}')`)
|
|
29
|
+
this.name = 'ModuleNotFoundError'
|
|
30
|
+
this.moduleName = moduleName
|
|
31
|
+
this.requiredBy = requiredBy
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Error thrown when circular dependencies are detected
|
|
37
|
+
*/
|
|
38
|
+
export class CircularDependencyError extends Error {
|
|
39
|
+
/**
|
|
40
|
+
* @param {string[]} cycle - Array of module names forming the cycle
|
|
41
|
+
*/
|
|
42
|
+
constructor(cycle) {
|
|
43
|
+
const cyclePath = cycle.join(' → ')
|
|
44
|
+
super(`Circular dependency detected: ${cyclePath}`)
|
|
45
|
+
this.name = 'CircularDependencyError'
|
|
46
|
+
this.cycle = cycle
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Error thrown when module connect() fails
|
|
52
|
+
*/
|
|
53
|
+
export class ModuleLoadError extends Error {
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} moduleName - Name of the module that failed
|
|
56
|
+
* @param {Error} cause - Original error
|
|
57
|
+
*/
|
|
58
|
+
constructor(moduleName, cause) {
|
|
59
|
+
super(`Failed to load module '${moduleName}': ${cause.message}`)
|
|
60
|
+
this.name = 'ModuleLoadError'
|
|
61
|
+
this.moduleName = moduleName
|
|
62
|
+
this.cause = cause
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Module Adapter for plain objects
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wraps a plain object with connect() into a Module-like interface
|
|
72
|
+
*/
|
|
73
|
+
class ObjectModuleAdapter {
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} def - Plain object module definition
|
|
76
|
+
*/
|
|
77
|
+
constructor(def) {
|
|
78
|
+
this._def = def
|
|
79
|
+
this._ctx = null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get name() {
|
|
83
|
+
return this._def.name || 'anonymous'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get requires() {
|
|
87
|
+
return this._def.requires || []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get priority() {
|
|
91
|
+
return this._def.priority ?? 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
enabled(ctx) {
|
|
95
|
+
if (typeof this._def.enabled === 'function') {
|
|
96
|
+
return this._def.enabled(ctx)
|
|
97
|
+
}
|
|
98
|
+
return this._def.enabled !== false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async connect(ctx) {
|
|
102
|
+
this._ctx = ctx
|
|
103
|
+
if (typeof this._def.connect === 'function') {
|
|
104
|
+
await this._def.connect(ctx)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async disconnect() {
|
|
109
|
+
if (typeof this._def.disconnect === 'function') {
|
|
110
|
+
await this._def.disconnect()
|
|
111
|
+
}
|
|
112
|
+
this._ctx = null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// Class Adapter for non-Module classes
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Wraps a class with static name and connect method into a Module-like interface
|
|
122
|
+
*/
|
|
123
|
+
class ClassModuleAdapter {
|
|
124
|
+
/**
|
|
125
|
+
* @param {Function} ClassDef - Class definition with static name
|
|
126
|
+
*/
|
|
127
|
+
constructor(ClassDef) {
|
|
128
|
+
this._ClassDef = ClassDef
|
|
129
|
+
this._instance = new ClassDef()
|
|
130
|
+
this._ctx = null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get name() {
|
|
134
|
+
return this._ClassDef.name || 'anonymous'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get requires() {
|
|
138
|
+
return this._ClassDef.requires || []
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get priority() {
|
|
142
|
+
return this._ClassDef.priority ?? 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
enabled(ctx) {
|
|
146
|
+
if (typeof this._instance.enabled === 'function') {
|
|
147
|
+
return this._instance.enabled(ctx)
|
|
148
|
+
}
|
|
149
|
+
if (typeof this._ClassDef.enabled === 'function') {
|
|
150
|
+
return this._ClassDef.enabled(ctx)
|
|
151
|
+
}
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async connect(ctx) {
|
|
156
|
+
this._ctx = ctx
|
|
157
|
+
if (typeof this._instance.connect === 'function') {
|
|
158
|
+
await this._instance.connect(ctx)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async disconnect() {
|
|
163
|
+
if (typeof this._instance.disconnect === 'function') {
|
|
164
|
+
await this._instance.disconnect()
|
|
165
|
+
}
|
|
166
|
+
this._ctx = null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
// Legacy Function Adapter
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Wraps a legacy init function into a Module-like interface
|
|
176
|
+
*/
|
|
177
|
+
class LegacyFunctionAdapter {
|
|
178
|
+
/**
|
|
179
|
+
* @param {Function} initFn - Legacy init function
|
|
180
|
+
*/
|
|
181
|
+
constructor(initFn) {
|
|
182
|
+
this._initFn = initFn
|
|
183
|
+
this._name = initFn.name || 'legacyModule'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get name() {
|
|
187
|
+
return this._name
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get requires() {
|
|
191
|
+
return []
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
get priority() {
|
|
195
|
+
return 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
enabled() {
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async connect(ctx) {
|
|
203
|
+
// Legacy pattern: init({ registry, zones })
|
|
204
|
+
// Adapt KernelContext to legacy interface
|
|
205
|
+
const legacyApi = {
|
|
206
|
+
registry: {
|
|
207
|
+
addRoutes: (basePath, routes, opts) => ctx.routes(basePath, routes, opts),
|
|
208
|
+
addNavItem: (item) => ctx.navItem(item),
|
|
209
|
+
addRouteFamily: (base, prefixes) => ctx.routeFamily(base, prefixes),
|
|
210
|
+
},
|
|
211
|
+
zones: ctx.zones,
|
|
212
|
+
ctx,
|
|
213
|
+
}
|
|
214
|
+
await this._initFn(legacyApi)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async disconnect() {
|
|
218
|
+
// Legacy functions don't support disconnect
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
223
|
+
// ModuleLoader
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* ModuleLoader - Loads and manages modules with dependency resolution
|
|
228
|
+
*/
|
|
229
|
+
export class ModuleLoader {
|
|
230
|
+
constructor() {
|
|
231
|
+
/** @type {Map<string, object>} Registered module definitions (before normalization) */
|
|
232
|
+
this._registered = new Map()
|
|
233
|
+
|
|
234
|
+
/** @type {Map<string, object>} Loaded module instances */
|
|
235
|
+
this._loaded = new Map()
|
|
236
|
+
|
|
237
|
+
/** @type {string[]} Load order for proper unloading */
|
|
238
|
+
this._loadOrder = []
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Register a module (any format)
|
|
243
|
+
*
|
|
244
|
+
* Accepts:
|
|
245
|
+
* - Module instance (instanceof Module)
|
|
246
|
+
* - Module class (has static name + prototype.connect or extends Module)
|
|
247
|
+
* - Plain object with connect function
|
|
248
|
+
* - Plain function (legacy init pattern)
|
|
249
|
+
*
|
|
250
|
+
* @param {Module|Function|object} moduleDef - Module definition in any format
|
|
251
|
+
* @returns {this} For chaining
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* loader.add(new UsersModule())
|
|
255
|
+
* loader.add(UsersModule)
|
|
256
|
+
* loader.add({ name: 'simple', connect(ctx) { ... } })
|
|
257
|
+
* loader.add(function initLegacy({ registry }) { ... })
|
|
258
|
+
*/
|
|
259
|
+
add(moduleDef) {
|
|
260
|
+
const normalized = this._normalize(moduleDef)
|
|
261
|
+
const name = normalized.name
|
|
262
|
+
|
|
263
|
+
if (!name || name === 'anonymous') {
|
|
264
|
+
throw new Error('Module must have a name (static name property, options.name, or function name)')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (this._registered.has(name)) {
|
|
268
|
+
throw new Error(`Module '${name}' is already registered`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this._registered.set(name, normalized)
|
|
272
|
+
return this
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Load all registered modules in dependency order
|
|
277
|
+
*
|
|
278
|
+
* @param {object} ctx - Context to pass to connect() (typically KernelContext-like)
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
* @throws {ModuleNotFoundError} When a required module is not registered
|
|
281
|
+
* @throws {CircularDependencyError} When circular dependencies exist
|
|
282
|
+
* @throws {ModuleLoadError} When a module's connect() fails
|
|
283
|
+
*/
|
|
284
|
+
async loadAll(ctx) {
|
|
285
|
+
// Get sorted modules
|
|
286
|
+
const sorted = this._topologicalSort()
|
|
287
|
+
|
|
288
|
+
// Load in order
|
|
289
|
+
for (const name of sorted) {
|
|
290
|
+
const module = this._registered.get(name)
|
|
291
|
+
|
|
292
|
+
// Check if enabled
|
|
293
|
+
if (!module.enabled(ctx)) {
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Connect module
|
|
298
|
+
try {
|
|
299
|
+
await module.connect(ctx)
|
|
300
|
+
this._loaded.set(name, module)
|
|
301
|
+
this._loadOrder.push(name)
|
|
302
|
+
} catch (err) {
|
|
303
|
+
throw new ModuleLoadError(name, err)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Unload all modules in reverse order
|
|
310
|
+
*
|
|
311
|
+
* @returns {Promise<void>}
|
|
312
|
+
*/
|
|
313
|
+
async unloadAll() {
|
|
314
|
+
// Unload in reverse order
|
|
315
|
+
const reversed = [...this._loadOrder].reverse()
|
|
316
|
+
|
|
317
|
+
for (const name of reversed) {
|
|
318
|
+
const module = this._loaded.get(name)
|
|
319
|
+
if (module && typeof module.disconnect === 'function') {
|
|
320
|
+
await module.disconnect()
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this._loaded.clear()
|
|
325
|
+
this._loadOrder = []
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get loaded modules (for debug/introspection)
|
|
330
|
+
*
|
|
331
|
+
* @returns {Map<string, object>} Map of module name to module instance
|
|
332
|
+
*/
|
|
333
|
+
getModules() {
|
|
334
|
+
return new Map(this._loaded)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Normalize any module format to Module-like interface
|
|
339
|
+
*
|
|
340
|
+
* @param {Module|Function|object} moduleDef
|
|
341
|
+
* @returns {object} Normalized module with name, requires, priority, enabled, connect, disconnect
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
_normalize(moduleDef) {
|
|
345
|
+
// 1. Already a Module instance
|
|
346
|
+
if (moduleDef instanceof Module) {
|
|
347
|
+
return moduleDef
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 2. Module class (constructor that extends Module or has static name + connect method)
|
|
351
|
+
if (typeof moduleDef === 'function') {
|
|
352
|
+
// Check if it's a class extending Module
|
|
353
|
+
if (moduleDef.prototype instanceof Module) {
|
|
354
|
+
return new moduleDef()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Check if it looks like a Module class:
|
|
358
|
+
// - Has own static 'name' property (not just inherited function name)
|
|
359
|
+
// - Prototype has connect method
|
|
360
|
+
const hasOwnStaticName = Object.prototype.hasOwnProperty.call(moduleDef, 'name')
|
|
361
|
+
if (
|
|
362
|
+
hasOwnStaticName &&
|
|
363
|
+
typeof moduleDef.name === 'string' &&
|
|
364
|
+
moduleDef.name !== '' &&
|
|
365
|
+
typeof moduleDef.prototype?.connect === 'function'
|
|
366
|
+
) {
|
|
367
|
+
// Use adapter to properly expose static properties
|
|
368
|
+
return new ClassModuleAdapter(moduleDef)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Otherwise it's a legacy init function
|
|
372
|
+
return new LegacyFunctionAdapter(moduleDef)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 3. Plain object with connect function
|
|
376
|
+
if (
|
|
377
|
+
moduleDef &&
|
|
378
|
+
typeof moduleDef === 'object' &&
|
|
379
|
+
typeof moduleDef.connect === 'function'
|
|
380
|
+
) {
|
|
381
|
+
return new ObjectModuleAdapter(moduleDef)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
throw new Error(
|
|
385
|
+
'Invalid module format. Expected: Module instance, Module class, object with connect(), or function'
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get the requires array from a module (handles static vs instance properties)
|
|
391
|
+
*
|
|
392
|
+
* @param {object} module - Module instance
|
|
393
|
+
* @returns {string[]}
|
|
394
|
+
* @private
|
|
395
|
+
*/
|
|
396
|
+
_getRequires(module) {
|
|
397
|
+
// Check instance property first
|
|
398
|
+
if (Array.isArray(module.requires)) {
|
|
399
|
+
return module.requires
|
|
400
|
+
}
|
|
401
|
+
// Check constructor (static) property for Module subclasses
|
|
402
|
+
if (module.constructor && Array.isArray(module.constructor.requires)) {
|
|
403
|
+
return module.constructor.requires
|
|
404
|
+
}
|
|
405
|
+
return []
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get the priority from a module (handles static vs instance properties)
|
|
410
|
+
*
|
|
411
|
+
* @param {object} module - Module instance
|
|
412
|
+
* @returns {number}
|
|
413
|
+
* @private
|
|
414
|
+
*/
|
|
415
|
+
_getPriority(module) {
|
|
416
|
+
// Check instance property first
|
|
417
|
+
if (typeof module.priority === 'number') {
|
|
418
|
+
return module.priority
|
|
419
|
+
}
|
|
420
|
+
// Check constructor (static) property for Module subclasses
|
|
421
|
+
if (module.constructor && typeof module.constructor.priority === 'number') {
|
|
422
|
+
return module.constructor.priority
|
|
423
|
+
}
|
|
424
|
+
return 0
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Sort modules topologically based on requires + priority
|
|
429
|
+
*
|
|
430
|
+
* Uses Kahn's algorithm for topological sort with priority tie-breaking.
|
|
431
|
+
*
|
|
432
|
+
* @returns {string[]} Sorted module names
|
|
433
|
+
* @throws {ModuleNotFoundError} When a required module is not registered
|
|
434
|
+
* @throws {CircularDependencyError} When circular dependencies exist
|
|
435
|
+
* @private
|
|
436
|
+
*/
|
|
437
|
+
_topologicalSort() {
|
|
438
|
+
const modules = this._registered
|
|
439
|
+
const names = Array.from(modules.keys())
|
|
440
|
+
|
|
441
|
+
// Build dependency graph
|
|
442
|
+
// inDegree: number of dependencies for each module
|
|
443
|
+
// dependents: modules that depend on this module
|
|
444
|
+
const inDegree = new Map()
|
|
445
|
+
const dependents = new Map()
|
|
446
|
+
|
|
447
|
+
for (const name of names) {
|
|
448
|
+
inDegree.set(name, 0)
|
|
449
|
+
dependents.set(name, [])
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Process requires for each module
|
|
453
|
+
for (const [name, module] of modules) {
|
|
454
|
+
const requires = this._getRequires(module)
|
|
455
|
+
|
|
456
|
+
for (const req of requires) {
|
|
457
|
+
if (!modules.has(req)) {
|
|
458
|
+
throw new ModuleNotFoundError(req, name)
|
|
459
|
+
}
|
|
460
|
+
inDegree.set(name, inDegree.get(name) + 1)
|
|
461
|
+
dependents.get(req).push(name)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Initialize queue with modules that have no dependencies
|
|
466
|
+
// Sort by priority (lower first) for consistent ordering
|
|
467
|
+
let queue = names
|
|
468
|
+
.filter((name) => inDegree.get(name) === 0)
|
|
469
|
+
.sort((a, b) => {
|
|
470
|
+
const modA = modules.get(a)
|
|
471
|
+
const modB = modules.get(b)
|
|
472
|
+
return this._getPriority(modA) - this._getPriority(modB)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
const result = []
|
|
476
|
+
|
|
477
|
+
while (queue.length > 0) {
|
|
478
|
+
// Take first (lowest priority)
|
|
479
|
+
const current = queue.shift()
|
|
480
|
+
result.push(current)
|
|
481
|
+
|
|
482
|
+
// Reduce in-degree for dependents
|
|
483
|
+
for (const dep of dependents.get(current)) {
|
|
484
|
+
inDegree.set(dep, inDegree.get(dep) - 1)
|
|
485
|
+
|
|
486
|
+
if (inDegree.get(dep) === 0) {
|
|
487
|
+
queue.push(dep)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Re-sort queue by priority
|
|
492
|
+
queue.sort((a, b) => {
|
|
493
|
+
const modA = modules.get(a)
|
|
494
|
+
const modB = modules.get(b)
|
|
495
|
+
return this._getPriority(modA) - this._getPriority(modB)
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Check for cycles
|
|
500
|
+
if (result.length !== names.length) {
|
|
501
|
+
// Find the cycle for error message
|
|
502
|
+
const cycle = this._findCycle(modules)
|
|
503
|
+
throw new CircularDependencyError(cycle)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return result
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Find a cycle in the dependency graph for error reporting
|
|
511
|
+
*
|
|
512
|
+
* @param {Map<string, object>} modules
|
|
513
|
+
* @returns {string[]} Cycle path
|
|
514
|
+
* @private
|
|
515
|
+
*/
|
|
516
|
+
_findCycle(modules) {
|
|
517
|
+
const visited = new Set()
|
|
518
|
+
const stack = new Set()
|
|
519
|
+
const path = []
|
|
520
|
+
|
|
521
|
+
const dfs = (name) => {
|
|
522
|
+
if (stack.has(name)) {
|
|
523
|
+
// Found cycle - extract it from path
|
|
524
|
+
const cycleStart = path.indexOf(name)
|
|
525
|
+
return [...path.slice(cycleStart), name]
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (visited.has(name)) {
|
|
529
|
+
return null
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
visited.add(name)
|
|
533
|
+
stack.add(name)
|
|
534
|
+
path.push(name)
|
|
535
|
+
|
|
536
|
+
const module = modules.get(name)
|
|
537
|
+
const requires = module ? this._getRequires(module) : []
|
|
538
|
+
|
|
539
|
+
for (const req of requires) {
|
|
540
|
+
if (modules.has(req)) {
|
|
541
|
+
const cycle = dfs(req)
|
|
542
|
+
if (cycle) {
|
|
543
|
+
return cycle
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
stack.delete(name)
|
|
549
|
+
path.pop()
|
|
550
|
+
return null
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for (const name of modules.keys()) {
|
|
554
|
+
const cycle = dfs(name)
|
|
555
|
+
if (cycle) {
|
|
556
|
+
return cycle
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return ['unknown cycle']
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Factory function to create a ModuleLoader instance
|
|
566
|
+
*
|
|
567
|
+
* @returns {ModuleLoader}
|
|
568
|
+
*/
|
|
569
|
+
export function createModuleLoader() {
|
|
570
|
+
return new ModuleLoader()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export default ModuleLoader
|
package/src/kernel/SignalBus.js
CHANGED
|
@@ -126,13 +126,8 @@ export class SignalBus {
|
|
|
126
126
|
* @returns {Promise<void>}
|
|
127
127
|
*/
|
|
128
128
|
async emitEntity(entityName, action, data) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Emit both specific and generic signals
|
|
133
|
-
// Specific first, then generic
|
|
134
|
-
await this.emit(specificSignal, { entity: entityName, data })
|
|
135
|
-
await this.emit(genericSignal, { entity: entityName, data })
|
|
129
|
+
const signal = buildSignal('entity', action)
|
|
130
|
+
await this.emit(signal, { entity: entityName, data })
|
|
136
131
|
}
|
|
137
132
|
|
|
138
133
|
/**
|
package/src/kernel/index.js
CHANGED
|
@@ -21,3 +21,17 @@ export {
|
|
|
21
21
|
createSSEBridge,
|
|
22
22
|
SSE_SIGNALS,
|
|
23
23
|
} from './SSEBridge.js'
|
|
24
|
+
export {
|
|
25
|
+
Module,
|
|
26
|
+
} from './Module.js'
|
|
27
|
+
export {
|
|
28
|
+
KernelContext,
|
|
29
|
+
createKernelContext,
|
|
30
|
+
} from './KernelContext.js'
|
|
31
|
+
export {
|
|
32
|
+
ModuleLoader,
|
|
33
|
+
createModuleLoader,
|
|
34
|
+
ModuleNotFoundError,
|
|
35
|
+
CircularDependencyError,
|
|
36
|
+
ModuleLoadError,
|
|
37
|
+
} from './ModuleLoader.js'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToastBridgeModule - Bridges signal bus to PrimeVue Toast
|
|
3
|
+
*
|
|
4
|
+
* This module integrates toast functionality via the signal bus:
|
|
5
|
+
* - Registers ToastListener component to handle toast:* signals
|
|
6
|
+
* - Provides useSignalToast() composable for emitting toasts
|
|
7
|
+
*
|
|
8
|
+
* Usage in modules:
|
|
9
|
+
* import { useSignalToast } from 'qdadm'
|
|
10
|
+
* const toast = useSignalToast()
|
|
11
|
+
* toast.success('Saved!', 'Your changes have been saved')
|
|
12
|
+
* toast.error('Error', 'Something went wrong')
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { createKernel, ToastBridgeModule } from 'qdadm'
|
|
16
|
+
*
|
|
17
|
+
* const kernel = createKernel()
|
|
18
|
+
* kernel.use(new ToastBridgeModule())
|
|
19
|
+
* await kernel.boot()
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Module } from '../kernel/Module.js'
|
|
23
|
+
import ToastListener from './ToastListener.vue'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Zone name for toast listener component
|
|
27
|
+
* Prefixed with _ to hide from ZonesCollector (internal zone)
|
|
28
|
+
*/
|
|
29
|
+
export const TOAST_ZONE = '_app:toasts'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ToastBridgeModule - Handles toast notifications via signals
|
|
33
|
+
*/
|
|
34
|
+
export class ToastBridgeModule extends Module {
|
|
35
|
+
/**
|
|
36
|
+
* Module identifier
|
|
37
|
+
* @type {string}
|
|
38
|
+
*/
|
|
39
|
+
static name = 'toast-bridge'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* No dependencies
|
|
43
|
+
* @type {string[]}
|
|
44
|
+
*/
|
|
45
|
+
static requires = []
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* High priority - should load early
|
|
49
|
+
* @type {number}
|
|
50
|
+
*/
|
|
51
|
+
static priority = 10
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Connect module to kernel
|
|
55
|
+
* @param {import('../kernel/KernelContext.js').KernelContext} ctx
|
|
56
|
+
*/
|
|
57
|
+
async connect(ctx) {
|
|
58
|
+
// Define the toast zone
|
|
59
|
+
ctx.zone(TOAST_ZONE)
|
|
60
|
+
|
|
61
|
+
// Register ToastListener component in the zone
|
|
62
|
+
ctx.block(TOAST_ZONE, {
|
|
63
|
+
id: 'toast-listener',
|
|
64
|
+
component: ToastListener,
|
|
65
|
+
weight: 0
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default ToastBridgeModule
|