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.
- package/package.json +1 -1
- package/src/components/index.js +3 -0
- package/src/components/pages/LoginPage.vue +267 -0
- package/src/composables/index.js +2 -0
- package/src/composables/useDeferred.js +85 -0
- package/src/composables/useListPageBuilder.js +3 -0
- package/src/composables/useSSE.js +212 -0
- package/src/deferred/DeferredRegistry.js +323 -0
- package/src/deferred/index.js +7 -0
- package/src/entity/EntityManager.js +82 -14
- package/src/entity/factory.js +155 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/index.js +8 -0
- package/src/entity/storage/ApiStorage.js +4 -1
- package/src/entity/storage/IStorage.js +76 -0
- package/src/entity/storage/LocalStorage.js +4 -1
- package/src/entity/storage/MemoryStorage.js +4 -1
- package/src/entity/storage/MockApiStorage.js +4 -1
- package/src/entity/storage/SdkStorage.js +4 -1
- package/src/entity/storage/factory.js +193 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/index.js +13 -0
- package/src/index.js +3 -0
- package/src/kernel/EventRouter.js +264 -0
- package/src/kernel/Kernel.js +123 -8
- package/src/kernel/index.js +4 -0
- package/src/orchestrator/Orchestrator.js +60 -0
- package/src/query/FilterQuery.js +9 -4
|
@@ -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
|
@@ -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
|
+
}
|
package/src/kernel/Kernel.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
//
|
|
114
|
+
// 4. Create router (needs routes from modules)
|
|
101
115
|
this._createRouter()
|
|
102
|
-
//
|
|
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
|
|
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 {
|
|
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
|