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.
- package/package.json +1 -1
- package/src/composables/index.js +1 -0
- package/src/composables/useDeferred.js +85 -0
- package/src/composables/useListPageBuilder.js +3 -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
package/package.json
CHANGED
package/src/composables/index.js
CHANGED
|
@@ -21,3 +21,4 @@ export { useZoneRegistry } from './useZoneRegistry'
|
|
|
21
21
|
export { useHooks } from './useHooks'
|
|
22
22
|
export { useLayoutResolver, createLayoutComponents, layoutMeta, LAYOUT_TYPES } from './useLayoutResolver'
|
|
23
23
|
export { useSSE } from './useSSE'
|
|
24
|
+
export { useDeferred, useDeferredValue, DEFERRED_INJECTION_KEY } from './useDeferred'
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDeferred - Access the DeferredRegistry from components
|
|
3
|
+
*
|
|
4
|
+
* Provides loose async coupling between services and components.
|
|
5
|
+
* Components can await dependencies without knowing when they're loaded.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```js
|
|
9
|
+
* import { useDeferred } from 'qdadm/composables'
|
|
10
|
+
*
|
|
11
|
+
* // In a component
|
|
12
|
+
* const deferred = useDeferred()
|
|
13
|
+
*
|
|
14
|
+
* // Await a single dependency
|
|
15
|
+
* const config = await deferred.await('config')
|
|
16
|
+
*
|
|
17
|
+
* // Await multiple dependencies
|
|
18
|
+
* const [users, settings] = await Promise.all([
|
|
19
|
+
* deferred.await('users-service'),
|
|
20
|
+
* deferred.await('settings')
|
|
21
|
+
* ])
|
|
22
|
+
*
|
|
23
|
+
* // Check status without waiting
|
|
24
|
+
* if (deferred.isSettled('config')) {
|
|
25
|
+
* const config = deferred.value('config')
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { inject } from 'vue'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Injection key for DeferredRegistry
|
|
34
|
+
*/
|
|
35
|
+
export const DEFERRED_INJECTION_KEY = 'qdadmDeferred'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the DeferredRegistry instance
|
|
39
|
+
* @returns {import('../deferred/DeferredRegistry.js').DeferredRegistry}
|
|
40
|
+
*/
|
|
41
|
+
export function useDeferred() {
|
|
42
|
+
const deferred = inject(DEFERRED_INJECTION_KEY)
|
|
43
|
+
|
|
44
|
+
if (!deferred) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'[useDeferred] DeferredRegistry not found. ' +
|
|
47
|
+
'Make sure you are using the Kernel to bootstrap your app.'
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return deferred
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Helper: await a deferred value in setup
|
|
56
|
+
* Returns a ref that updates when the deferred resolves
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```js
|
|
60
|
+
* const { data: config, loading, error } = useDeferredValue('config')
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @param {string} key - Deferred key
|
|
64
|
+
* @returns {{ data: Ref, loading: Ref<boolean>, error: Ref<Error|null> }}
|
|
65
|
+
*/
|
|
66
|
+
export function useDeferredValue(key) {
|
|
67
|
+
const deferred = useDeferred()
|
|
68
|
+
const data = ref(null)
|
|
69
|
+
const loading = ref(true)
|
|
70
|
+
const error = ref(null)
|
|
71
|
+
|
|
72
|
+
deferred.await(key)
|
|
73
|
+
.then(value => {
|
|
74
|
+
data.value = value
|
|
75
|
+
loading.value = false
|
|
76
|
+
})
|
|
77
|
+
.catch(err => {
|
|
78
|
+
error.value = err
|
|
79
|
+
loading.value = false
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return { data, loading, error }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
import { ref } from 'vue'
|
|
@@ -994,6 +994,9 @@ export function useListPageBuilder(config = {}) {
|
|
|
994
994
|
const displayItems = computed(() => filteredItems.value)
|
|
995
995
|
|
|
996
996
|
// ============ LOADING ============
|
|
997
|
+
// Note: Auth changes are handled by Vue component lifecycle - when user logs out,
|
|
998
|
+
// router guard redirects to login, component unmounts. On re-login, new component
|
|
999
|
+
// instance is created with fresh state (filterOptionsLoaded = false).
|
|
997
1000
|
let filterOptionsLoaded = false
|
|
998
1001
|
|
|
999
1002
|
async function loadItems(extraParams = {}, { force = false } = {}) {
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeferredRegistry - Named promise registry for loose coupling
|
|
3
|
+
*
|
|
4
|
+
* Enables async dependencies between services/components:
|
|
5
|
+
* - Services queue work during warmup
|
|
6
|
+
* - Components await dependencies without tight coupling
|
|
7
|
+
* - External signals (SSE) can resolve promises
|
|
8
|
+
*
|
|
9
|
+
* Key insight: await() can be called BEFORE queue() - the promise
|
|
10
|
+
* is created on first access and resolved when the task completes.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```js
|
|
14
|
+
* // During app boot - queue lazy services
|
|
15
|
+
* deferred.queue('users-service', () => usersService.init())
|
|
16
|
+
* deferred.queue('config', () => configService.load())
|
|
17
|
+
*
|
|
18
|
+
* // In component - await dependencies (can be called before queue!)
|
|
19
|
+
* const config = await deferred.await('config')
|
|
20
|
+
*
|
|
21
|
+
* // Wait for multiple with Promise.all
|
|
22
|
+
* const [users, config] = await Promise.all([
|
|
23
|
+
* deferred.await('users-service'),
|
|
24
|
+
* deferred.await('config')
|
|
25
|
+
* ])
|
|
26
|
+
*
|
|
27
|
+
* // External resolution (SSE, webhooks)
|
|
28
|
+
* deferred.resolve('job-123', { status: 'done', data: result })
|
|
29
|
+
* deferred.reject('job-456', new Error('Failed'))
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {'pending' | 'running' | 'completed' | 'failed'} DeferredStatus
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {object} DeferredEntry
|
|
39
|
+
* @property {Promise} promise - The deferred promise
|
|
40
|
+
* @property {function} resolve - Resolve function
|
|
41
|
+
* @property {function} reject - Reject function
|
|
42
|
+
* @property {DeferredStatus} status - Current status
|
|
43
|
+
* @property {any} value - Resolved value or error
|
|
44
|
+
* @property {number} timestamp - Creation timestamp
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export class DeferredRegistry {
|
|
48
|
+
/**
|
|
49
|
+
* @param {object} options
|
|
50
|
+
* @param {object} [options.kernel] - Optional QuarKernel for event emission
|
|
51
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
52
|
+
*/
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this._entries = new Map()
|
|
55
|
+
this._kernel = options.kernel || null
|
|
56
|
+
this._debug = options.debug || false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get or create a deferred entry for a key
|
|
61
|
+
* @param {string} key - Unique identifier
|
|
62
|
+
* @returns {DeferredEntry}
|
|
63
|
+
*/
|
|
64
|
+
_getOrCreate(key) {
|
|
65
|
+
if (!this._entries.has(key)) {
|
|
66
|
+
let resolve, reject
|
|
67
|
+
const promise = new Promise((res, rej) => {
|
|
68
|
+
resolve = res
|
|
69
|
+
reject = rej
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const entry = {
|
|
73
|
+
promise,
|
|
74
|
+
resolve,
|
|
75
|
+
reject,
|
|
76
|
+
status: 'pending',
|
|
77
|
+
value: undefined,
|
|
78
|
+
timestamp: Date.now()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this._entries.set(key, entry)
|
|
82
|
+
|
|
83
|
+
if (this._debug) {
|
|
84
|
+
console.debug(`[deferred] Created entry: ${key}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this._entries.get(key)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the promise for a key (creates if doesn't exist)
|
|
93
|
+
* Can be called BEFORE queue() - promise resolves when task completes
|
|
94
|
+
*
|
|
95
|
+
* @param {string} key - Unique identifier
|
|
96
|
+
* @returns {Promise<any>}
|
|
97
|
+
*/
|
|
98
|
+
await(key) {
|
|
99
|
+
return this._getOrCreate(key).promise
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Queue a task for execution
|
|
104
|
+
* If already running/completed, returns existing promise (deduplication)
|
|
105
|
+
*
|
|
106
|
+
* @param {string} key - Unique identifier
|
|
107
|
+
* @param {function(): Promise<any>} executor - Async function to execute
|
|
108
|
+
* @returns {Promise<any>} Promise that resolves with task result
|
|
109
|
+
*/
|
|
110
|
+
queue(key, executor) {
|
|
111
|
+
const entry = this._getOrCreate(key)
|
|
112
|
+
|
|
113
|
+
// Already started - return existing promise (idempotent)
|
|
114
|
+
if (entry.status !== 'pending') {
|
|
115
|
+
if (this._debug) {
|
|
116
|
+
console.debug(`[deferred] Already ${entry.status}: ${key}`)
|
|
117
|
+
}
|
|
118
|
+
return entry.promise
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
entry.status = 'running'
|
|
122
|
+
this._emit('deferred:started', { key })
|
|
123
|
+
|
|
124
|
+
if (this._debug) {
|
|
125
|
+
console.debug(`[deferred] Started: ${key}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Execute and handle result
|
|
129
|
+
Promise.resolve()
|
|
130
|
+
.then(() => executor())
|
|
131
|
+
.then(value => {
|
|
132
|
+
entry.status = 'completed'
|
|
133
|
+
entry.value = value
|
|
134
|
+
entry.resolve(value)
|
|
135
|
+
this._emit('deferred:completed', { key, value })
|
|
136
|
+
|
|
137
|
+
if (this._debug) {
|
|
138
|
+
console.debug(`[deferred] Completed: ${key}`)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.catch(error => {
|
|
142
|
+
entry.status = 'failed'
|
|
143
|
+
entry.value = error
|
|
144
|
+
entry.reject(error)
|
|
145
|
+
this._emit('deferred:failed', { key, error })
|
|
146
|
+
|
|
147
|
+
if (this._debug) {
|
|
148
|
+
console.debug(`[deferred] Failed: ${key}`, error)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return entry.promise
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve a deferred externally (for SSE, webhooks, etc.)
|
|
157
|
+
* No-op if already completed/failed
|
|
158
|
+
*
|
|
159
|
+
* @param {string} key - Unique identifier
|
|
160
|
+
* @param {any} value - Value to resolve with
|
|
161
|
+
* @returns {boolean} True if resolved, false if already settled
|
|
162
|
+
*/
|
|
163
|
+
resolve(key, value) {
|
|
164
|
+
const entry = this._getOrCreate(key)
|
|
165
|
+
|
|
166
|
+
if (entry.status === 'completed' || entry.status === 'failed') {
|
|
167
|
+
if (this._debug) {
|
|
168
|
+
console.debug(`[deferred] Cannot resolve (already ${entry.status}): ${key}`)
|
|
169
|
+
}
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
entry.status = 'completed'
|
|
174
|
+
entry.value = value
|
|
175
|
+
entry.resolve(value)
|
|
176
|
+
this._emit('deferred:completed', { key, value })
|
|
177
|
+
|
|
178
|
+
if (this._debug) {
|
|
179
|
+
console.debug(`[deferred] Resolved externally: ${key}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Reject a deferred externally (for SSE, webhooks, etc.)
|
|
187
|
+
* No-op if already completed/failed
|
|
188
|
+
*
|
|
189
|
+
* @param {string} key - Unique identifier
|
|
190
|
+
* @param {Error} error - Error to reject with
|
|
191
|
+
* @returns {boolean} True if rejected, false if already settled
|
|
192
|
+
*/
|
|
193
|
+
reject(key, error) {
|
|
194
|
+
const entry = this._getOrCreate(key)
|
|
195
|
+
|
|
196
|
+
if (entry.status === 'completed' || entry.status === 'failed') {
|
|
197
|
+
if (this._debug) {
|
|
198
|
+
console.debug(`[deferred] Cannot reject (already ${entry.status}): ${key}`)
|
|
199
|
+
}
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
entry.status = 'failed'
|
|
204
|
+
entry.value = error
|
|
205
|
+
entry.reject(error)
|
|
206
|
+
this._emit('deferred:failed', { key, error })
|
|
207
|
+
|
|
208
|
+
if (this._debug) {
|
|
209
|
+
console.debug(`[deferred] Rejected externally: ${key}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a key exists in the registry
|
|
217
|
+
* @param {string} key
|
|
218
|
+
* @returns {boolean}
|
|
219
|
+
*/
|
|
220
|
+
has(key) {
|
|
221
|
+
return this._entries.has(key)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get the status of a deferred
|
|
226
|
+
* @param {string} key
|
|
227
|
+
* @returns {DeferredStatus|null} Status or null if not found
|
|
228
|
+
*/
|
|
229
|
+
status(key) {
|
|
230
|
+
return this._entries.get(key)?.status || null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get the resolved value (only if completed)
|
|
235
|
+
* @param {string} key
|
|
236
|
+
* @returns {any} Value or undefined
|
|
237
|
+
*/
|
|
238
|
+
value(key) {
|
|
239
|
+
const entry = this._entries.get(key)
|
|
240
|
+
return entry?.status === 'completed' ? entry.value : undefined
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Check if a deferred is settled (completed or failed)
|
|
245
|
+
* @param {string} key
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
isSettled(key) {
|
|
249
|
+
const status = this.status(key)
|
|
250
|
+
return status === 'completed' || status === 'failed'
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get all keys in the registry
|
|
255
|
+
* @returns {string[]}
|
|
256
|
+
*/
|
|
257
|
+
keys() {
|
|
258
|
+
return Array.from(this._entries.keys())
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get all entries with their status
|
|
263
|
+
* @returns {Array<{key: string, status: DeferredStatus, timestamp: number}>}
|
|
264
|
+
*/
|
|
265
|
+
entries() {
|
|
266
|
+
return Array.from(this._entries.entries()).map(([key, entry]) => ({
|
|
267
|
+
key,
|
|
268
|
+
status: entry.status,
|
|
269
|
+
timestamp: entry.timestamp
|
|
270
|
+
}))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Clear a specific entry (for cleanup/retry)
|
|
275
|
+
* @param {string} key
|
|
276
|
+
* @returns {boolean} True if cleared
|
|
277
|
+
*/
|
|
278
|
+
clear(key) {
|
|
279
|
+
return this._entries.delete(key)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Clear all entries
|
|
284
|
+
*/
|
|
285
|
+
clearAll() {
|
|
286
|
+
this._entries.clear()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Emit event via kernel (if provided)
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
_emit(event, data) {
|
|
294
|
+
if (this._kernel) {
|
|
295
|
+
this._kernel.emit(event, data)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Set kernel for event emission
|
|
301
|
+
* @param {object} kernel - QuarKernel instance
|
|
302
|
+
*/
|
|
303
|
+
setKernel(kernel) {
|
|
304
|
+
this._kernel = kernel
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Enable/disable debug mode
|
|
309
|
+
* @param {boolean} enabled
|
|
310
|
+
*/
|
|
311
|
+
debug(enabled) {
|
|
312
|
+
this._debug = enabled
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Factory function
|
|
318
|
+
* @param {object} options
|
|
319
|
+
* @returns {DeferredRegistry}
|
|
320
|
+
*/
|
|
321
|
+
export function createDeferredRegistry(options = {}) {
|
|
322
|
+
return new DeferredRegistry(options)
|
|
323
|
+
}
|
|
@@ -58,6 +58,7 @@ export class EntityManager {
|
|
|
58
58
|
// List behavior
|
|
59
59
|
localFilterThreshold = null, // Items threshold to switch to local filtering (null = use default)
|
|
60
60
|
readOnly = false, // If true, canCreate/canUpdate/canDelete return false
|
|
61
|
+
warmup = true, // If true, cache is preloaded at boot via DeferredRegistry
|
|
61
62
|
// Scope control
|
|
62
63
|
scopeWhitelist = null, // Array of scopes/modules that can bypass restrictions
|
|
63
64
|
// Relations
|
|
@@ -83,6 +84,7 @@ export class EntityManager {
|
|
|
83
84
|
// List behavior
|
|
84
85
|
this.localFilterThreshold = localFilterThreshold
|
|
85
86
|
this._readOnly = readOnly
|
|
87
|
+
this._warmup = warmup
|
|
86
88
|
|
|
87
89
|
// Scope control
|
|
88
90
|
this._scopeWhitelist = scopeWhitelist
|
|
@@ -1039,31 +1041,48 @@ export class EntityManager {
|
|
|
1039
1041
|
}
|
|
1040
1042
|
|
|
1041
1043
|
/**
|
|
1042
|
-
* Set up signal listeners for
|
|
1044
|
+
* Set up signal listeners for cache invalidation
|
|
1043
1045
|
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
+
* Listens for:
|
|
1047
|
+
* - Parent entity invalidation: clears _search cache and invalidates
|
|
1048
|
+
* - Auth changes: invalidates cache on login/logout (user context changed)
|
|
1046
1049
|
*/
|
|
1047
1050
|
_setupCacheListeners() {
|
|
1048
|
-
// Nothing to do if no parents or no signals
|
|
1049
|
-
if (!this._parents || Object.keys(this._parents).length === 0) return
|
|
1050
1051
|
if (!this._signals) return
|
|
1051
1052
|
|
|
1052
|
-
// Clean up existing
|
|
1053
|
+
// Clean up existing listeners if any
|
|
1053
1054
|
if (this._signalCleanup) {
|
|
1054
1055
|
this._signalCleanup()
|
|
1055
1056
|
this._signalCleanup = null
|
|
1056
1057
|
}
|
|
1057
1058
|
|
|
1058
|
-
|
|
1059
|
-
const parentEntities = Object.values(this._parents).map(p => p.entity)
|
|
1059
|
+
const cleanups = []
|
|
1060
1060
|
|
|
1061
|
-
// Listen for parent cache invalidation
|
|
1062
|
-
this.
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1061
|
+
// Listen for parent cache invalidation (if parents defined)
|
|
1062
|
+
if (this._parents && Object.keys(this._parents).length > 0) {
|
|
1063
|
+
const parentEntities = Object.values(this._parents).map(p => p.entity)
|
|
1064
|
+
cleanups.push(
|
|
1065
|
+
this._signals.on('cache:entity:invalidated', ({ entity }) => {
|
|
1066
|
+
if (parentEntities.includes(entity)) {
|
|
1067
|
+
this._clearSearchCache()
|
|
1068
|
+
}
|
|
1069
|
+
})
|
|
1070
|
+
)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Listen for targeted cache invalidation (routed by EventRouter)
|
|
1074
|
+
// EntityManager only listens to its own signal, staying simple.
|
|
1075
|
+
// EventRouter transforms high-level events (auth:impersonate) into targeted signals.
|
|
1076
|
+
cleanups.push(
|
|
1077
|
+
this._signals.on(`cache:entity:invalidate:${this.name}`, () => {
|
|
1078
|
+
this.invalidateCache()
|
|
1079
|
+
})
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
// Combined cleanup function
|
|
1083
|
+
this._signalCleanup = () => {
|
|
1084
|
+
cleanups.forEach(cleanup => cleanup())
|
|
1085
|
+
}
|
|
1067
1086
|
}
|
|
1068
1087
|
|
|
1069
1088
|
/**
|
|
@@ -1178,6 +1197,55 @@ export class EntityManager {
|
|
|
1178
1197
|
return true
|
|
1179
1198
|
}
|
|
1180
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Warmup: preload cache via DeferredRegistry for loose async coupling
|
|
1202
|
+
*
|
|
1203
|
+
* Called by Orchestrator.fireWarmups() at boot. Registers the cache loading
|
|
1204
|
+
* in DeferredRegistry so pages can await it before rendering.
|
|
1205
|
+
*
|
|
1206
|
+
* @returns {Promise<boolean>|null} Promise if warmup started, null if disabled/not applicable
|
|
1207
|
+
*
|
|
1208
|
+
* @example
|
|
1209
|
+
* ```js
|
|
1210
|
+
* // At boot (automatic via Kernel)
|
|
1211
|
+
* orchestrator.warmupAll()
|
|
1212
|
+
*
|
|
1213
|
+
* // In component - await cache ready
|
|
1214
|
+
* await deferred.await('entity:books:cache')
|
|
1215
|
+
* const { items } = await booksManager.list() // Uses local cache
|
|
1216
|
+
* ```
|
|
1217
|
+
*/
|
|
1218
|
+
async warmup() {
|
|
1219
|
+
// Skip if warmup disabled or caching not applicable
|
|
1220
|
+
if (!this._warmup) return null
|
|
1221
|
+
if (!this.isCacheEnabled) return null
|
|
1222
|
+
|
|
1223
|
+
const deferred = this._orchestrator?.deferred
|
|
1224
|
+
|
|
1225
|
+
// Wait for auth if app uses authentication (user context affects cache)
|
|
1226
|
+
if (deferred?.has('auth:ready')) {
|
|
1227
|
+
await deferred.await('auth:ready')
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const key = `entity:${this.name}:cache`
|
|
1231
|
+
|
|
1232
|
+
if (!deferred) {
|
|
1233
|
+
// Fallback: direct cache load without DeferredRegistry
|
|
1234
|
+
return this.ensureCache()
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Register in DeferredRegistry for loose coupling
|
|
1238
|
+
return deferred.queue(key, () => this.ensureCache())
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Check if warmup is enabled for this manager
|
|
1243
|
+
* @returns {boolean}
|
|
1244
|
+
*/
|
|
1245
|
+
get warmupEnabled() {
|
|
1246
|
+
return this._warmup && this.isCacheEnabled
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1181
1249
|
/**
|
|
1182
1250
|
* Query entities with automatic cache/API decision
|
|
1183
1251
|
*
|