qdadm 1.13.1 → 1.19.2

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.
Files changed (86) hide show
  1. package/package.json +7 -3
  2. package/src/chain/ActiveStack.ts +79 -98
  3. package/src/chain/StackHydrator.ts +3 -2
  4. package/src/chain/index.ts +7 -1
  5. package/src/components/QdadmRoot.vue +52 -0
  6. package/src/components/edit/FormActions.vue +9 -6
  7. package/src/components/edit/LookupPickerDialog.vue +6 -3
  8. package/src/components/index.ts +6 -0
  9. package/src/composables/useEntityItemFormPage.ts +1 -0
  10. package/src/composables/useEntityItemShowPage.ts +1 -0
  11. package/src/composables/useFieldManager.ts +100 -3
  12. package/src/composables/useListPage.ts +50 -59
  13. package/src/composables/useListPage.utils.ts +101 -0
  14. package/src/composables/useNavigation.ts +26 -3
  15. package/src/composables/useOptionsLookup.ts +5 -1
  16. package/src/gen/generateManagers.test.js +27 -0
  17. package/src/gen/generateManagers.ts +12 -0
  18. package/src/hooks/HookRegistry.ts +14 -435
  19. package/src/i18n/I18n.ts +344 -0
  20. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  21. package/src/i18n/InlineTranslationProvider.ts +4 -0
  22. package/src/i18n/LazyTranslationProvider.ts +102 -0
  23. package/src/i18n/MessagesRegistry.ts +4 -0
  24. package/src/i18n/Resolver.ts +4 -0
  25. package/src/i18n/__tests__/I18n.test.ts +169 -0
  26. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  27. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  28. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  29. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  30. package/src/i18n/defaults/core.en.yml +55 -0
  31. package/src/i18n/defaults/core.fr.yml +55 -0
  32. package/src/i18n/index.ts +55 -0
  33. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  34. package/src/i18n/loaders/yaml.ts +35 -0
  35. package/src/i18n/strategies.ts +4 -0
  36. package/src/i18n/types.ts +34 -0
  37. package/src/i18n/useI18n.ts +34 -0
  38. package/src/index.ts +37 -0
  39. package/src/kernel/EventRouter.ts +17 -300
  40. package/src/kernel/Kernel.i18n.ts +29 -0
  41. package/src/kernel/Kernel.modules.ts +6 -0
  42. package/src/kernel/Kernel.registries.ts +10 -2
  43. package/src/kernel/Kernel.routing.ts +43 -1
  44. package/src/kernel/Kernel.ts +43 -0
  45. package/src/kernel/Kernel.types.ts +52 -1
  46. package/src/kernel/Kernel.vue.ts +121 -15
  47. package/src/kernel/KernelContext.entities.ts +80 -0
  48. package/src/kernel/KernelContext.events.ts +57 -0
  49. package/src/kernel/KernelContext.i18n.ts +37 -0
  50. package/src/kernel/KernelContext.permissions.ts +38 -0
  51. package/src/kernel/KernelContext.routing.ts +280 -0
  52. package/src/kernel/KernelContext.ts +125 -834
  53. package/src/kernel/KernelContext.types.ts +173 -0
  54. package/src/kernel/KernelContext.zones.ts +54 -0
  55. package/src/kernel/SSEBridge.ts +7 -362
  56. package/src/kernel/SignalBus.ts +24 -148
  57. package/src/modules/debug/AuthCollector.ts +48 -1
  58. package/src/modules/debug/Collector.ts +16 -302
  59. package/src/modules/debug/DebugBridge.ts +10 -171
  60. package/src/modules/debug/DebugModule.ts +35 -5
  61. package/src/modules/debug/EntitiesCollector.ts +97 -1
  62. package/src/modules/debug/ErrorCollector.ts +2 -77
  63. package/src/modules/debug/I18nCollector.ts +9 -0
  64. package/src/modules/debug/LocalStorageAdapter.ts +3 -147
  65. package/src/modules/debug/RouterCollector.ts +101 -1
  66. package/src/modules/debug/SignalCollector.ts +2 -150
  67. package/src/modules/debug/ToastCollector.ts +2 -91
  68. package/src/modules/debug/ZonesCollector.ts +93 -1
  69. package/src/modules/debug/components/DebugBar.vue +19 -775
  70. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  71. package/src/modules/debug/components/index.ts +4 -3
  72. package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
  73. package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
  74. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  75. package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
  76. package/src/modules/debug/components/panels/index.ts +10 -4
  77. package/src/modules/debug/index.ts +15 -0
  78. package/src/modules/debug/styles.scss +22 -18
  79. package/src/utils/index.ts +0 -3
  80. package/src/vite/qdadmDebugPlugin.ts +401 -0
  81. package/src/vite-env.d.ts +16 -0
  82. package/src/modules/debug/components/ObjectTree.vue +0 -123
  83. package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
  84. package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
  85. package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
  86. package/src/utils/debugInjector.ts +0 -306
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Default core provider — ships qdadm's `core.*` defaults (EN + FR) as a
3
+ * `LazyTranslationProvider` configured with a YAML loader.
4
+ *
5
+ * The YAML files (`core.en.yml`, `core.fr.yml`) are imported via Vite's
6
+ * `?raw` query so the text is fetched on-demand, then parsed at runtime.
7
+ * Splitting the bundle by locale (and, in the future, by domain) keeps the
8
+ * provider tree-shakeable and translator-friendly.
9
+ *
10
+ * Adding a built-in locale = drop `core.<locale>.yml` next to this file and
11
+ * add it to the loader map below.
12
+ */
13
+
14
+ import { LazyTranslationProvider } from '../LazyTranslationProvider'
15
+ import { createYamlLoader } from '../loaders/yaml'
16
+
17
+ export function createDefaultCoreProvider(): LazyTranslationProvider {
18
+ const yamlLoader = createYamlLoader({
19
+ en: () => import('./core.en.yml?raw'),
20
+ fr: () => import('./core.fr.yml?raw'),
21
+ })
22
+
23
+ return new LazyTranslationProvider({
24
+ name: 'default-core',
25
+ loaders: [yamlLoader],
26
+ locales: ['en', 'fr'],
27
+ })
28
+ }
@@ -0,0 +1,55 @@
1
+ core:
2
+ actions:
3
+ save: Save
4
+ cancel: Cancel
5
+ delete: Delete
6
+ edit: Edit
7
+ create: Create
8
+ update: Update
9
+ createAndClose: Create & Close
10
+ updateAndClose: Update & Close
11
+ add: Add
12
+ remove: Remove
13
+ submit: Submit
14
+ reset: Reset
15
+ back: Back
16
+ next: Next
17
+ previous: Previous
18
+ close: Close
19
+ confirm: Confirm
20
+ search: Search
21
+ filter: Filter
22
+ clear: Clear
23
+ refresh: Refresh
24
+ export: Export
25
+ import: Import
26
+ tooltips:
27
+ saveAndContinue: Save and continue editing
28
+ saveAndReturn: Save and return to list
29
+ unsavedChanges: Unsaved changes
30
+ placeholders:
31
+ search: Search…
32
+ noSelection: Click to select…
33
+ fields:
34
+ id: ID
35
+ created_at: Created at
36
+ updated_at: Updated at
37
+ created_by: Created by
38
+ updated_by: Updated by
39
+ messages:
40
+ empty: No items available
41
+ loading: Loading…
42
+ noResults: No results
43
+ noMatching: No matching items
44
+ saved: Saved
45
+ deleted: Deleted
46
+ created: Created
47
+ updated: Updated
48
+ confirmDelete: Are you sure you want to delete this item?
49
+ errors:
50
+ required: Required
51
+ tooShort: Too short (min {min})
52
+ tooLong: Too long (max {max})
53
+ invalid: Invalid value
54
+ notFound: Not found
55
+ unknown: An unknown error occurred
@@ -0,0 +1,55 @@
1
+ core:
2
+ actions:
3
+ save: Enregistrer
4
+ cancel: Annuler
5
+ delete: Supprimer
6
+ edit: Modifier
7
+ create: Créer
8
+ update: Mettre à jour
9
+ createAndClose: Créer et fermer
10
+ updateAndClose: Mettre à jour et fermer
11
+ add: Ajouter
12
+ remove: Retirer
13
+ submit: Valider
14
+ reset: Réinitialiser
15
+ back: Retour
16
+ next: Suivant
17
+ previous: Précédent
18
+ close: Fermer
19
+ confirm: Confirmer
20
+ search: Rechercher
21
+ filter: Filtrer
22
+ clear: Effacer
23
+ refresh: Actualiser
24
+ export: Exporter
25
+ import: Importer
26
+ tooltips:
27
+ saveAndContinue: Enregistrer et continuer la modification
28
+ saveAndReturn: Enregistrer et revenir à la liste
29
+ unsavedChanges: Modifications non enregistrées
30
+ placeholders:
31
+ search: Rechercher…
32
+ noSelection: Cliquez pour sélectionner…
33
+ fields:
34
+ id: Identifiant
35
+ created_at: Créé le
36
+ updated_at: Modifié le
37
+ created_by: Créé par
38
+ updated_by: Modifié par
39
+ messages:
40
+ empty: Aucun élément
41
+ loading: Chargement…
42
+ noResults: Aucun résultat
43
+ noMatching: Aucune correspondance
44
+ saved: Enregistré
45
+ deleted: Supprimé
46
+ created: Créé
47
+ updated: Modifié
48
+ confirmDelete: Êtes-vous sûr de vouloir supprimer cet élément ?
49
+ errors:
50
+ required: Requis
51
+ tooShort: Trop court (min {min})
52
+ tooLong: Trop long (max {max})
53
+ invalid: Valeur invalide
54
+ notFound: Introuvable
55
+ unknown: Une erreur inconnue est survenue
@@ -0,0 +1,55 @@
1
+ /**
2
+ * qdadm i18n - public exports.
3
+ *
4
+ * ## Two providers to choose from
5
+ *
6
+ * - **`LazyTranslationProvider`** — full bundle on `load(locale)`, split
7
+ * across multiple loaders (one per file/domain) deep-merged on resolve.
8
+ * Use when the locale bundle is reasonable to fetch up-front.
9
+ *
10
+ * - **`IncrementalDomainProvider`** — one loader per top-level domain,
11
+ * fetched on demand. `t()` on an unloaded domain returns the raw key
12
+ * synchronously and emits `i18n:domain-loaded` once merged. Use for
13
+ * large apps with rarely-used sections.
14
+ *
15
+ * Both support `createYamlLoader({ <locale>: () => import('./xx.yml?raw') })`
16
+ * for the typical "split YAML files in a directory" setup.
17
+ *
18
+ * See docs/todo-i18n.md for the broader design rationale.
19
+ */
20
+
21
+ export { I18n } from './I18n'
22
+ export type { I18nDeps } from './I18n'
23
+ export { MessagesRegistry } from './MessagesRegistry'
24
+ export { Resolver, snakeCaseToTitle } from './Resolver'
25
+ export { InlineTranslationProvider } from './InlineTranslationProvider'
26
+ export { defineStrategy, resolveStrategy, STRATEGIES } from './strategies'
27
+ export { LazyTranslationProvider } from './LazyTranslationProvider'
28
+ export type { LazyLoader, LazyTranslationProviderOptions } from './LazyTranslationProvider'
29
+ export {
30
+ IncrementalDomainProvider,
31
+ isDomainAwareProvider,
32
+ } from './IncrementalDomainProvider'
33
+ export type {
34
+ DomainLoader,
35
+ IncrementalDomainProviderOptions,
36
+ } from './IncrementalDomainProvider'
37
+ export { createYamlLoader } from './loaders/yaml'
38
+ export type { YamlTextImport } from './loaders/yaml'
39
+ export { createDefaultCoreProvider } from './defaults/DefaultCoreProvider'
40
+ export { useI18n, I18N_INJECTION_KEY } from './useI18n'
41
+ export type {
42
+ AliasPattern,
43
+ CoreMessages,
44
+ EntityMessages,
45
+ I18nOptions,
46
+ KeyStrategy,
47
+ KeyStrategyName,
48
+ MessagesBundle,
49
+ MessagesNode,
50
+ NavMessages,
51
+ ResolveStep,
52
+ ResolveTrace,
53
+ TranslateParams,
54
+ TranslationProvider,
55
+ } from './types'
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Ambient declarations for Vite/Vitest `?raw` imports of arbitrary text
3
+ * files (YAML, etc.). Lets `import yamlText from './foo.yml?raw'` type-check
4
+ * without per-call casts.
5
+ */
6
+
7
+ declare module '*.yml?raw' {
8
+ const content: string
9
+ export default content
10
+ }
11
+
12
+ declare module '*.yaml?raw' {
13
+ const content: string
14
+ export default content
15
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * YAML loader factory for LazyTranslationProvider.
3
+ *
4
+ * Given a `locale → () => Promise<{ default: string }>` map, returns a
5
+ * `LazyLoader` that resolves the YAML text for the requested locale and
6
+ * parses it through the `yaml` package.
7
+ *
8
+ * Typical usage with Vite/Vitest `?raw` imports (qdadm-internal default):
9
+ *
10
+ * const loader = createYamlLoader({
11
+ * en: () => import('./core.en.yml?raw'),
12
+ * fr: () => import('./core.fr.yml?raw'),
13
+ * })
14
+ *
15
+ * The dynamic-import boundary is what gives us code-splitting: the YAML for
16
+ * a locale is only fetched + parsed when that locale is requested.
17
+ */
18
+
19
+ import { parse as parseYaml } from 'yaml'
20
+
21
+ import type { MessagesBundle } from '@quazardous/qdcore'
22
+ import type { LazyLoader } from '../LazyTranslationProvider'
23
+
24
+ export type YamlTextImport = () => Promise<{ default: string }>
25
+
26
+ export function createYamlLoader(textImports: Record<string, YamlTextImport>): LazyLoader {
27
+ return async (locale: string) => {
28
+ const importer = textImports[locale]
29
+ if (!importer) return null
30
+ const mod = await importer()
31
+ const parsed = parseYaml(mod.default)
32
+ if (!parsed || typeof parsed !== 'object') return null
33
+ return parsed as MessagesBundle
34
+ }
35
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * strategies — qdadm re-export from `@quazardous/qdcore/i18n`.
3
+ */
4
+ export { STRATEGIES, resolveStrategy, defineStrategy } from '@quazardous/qdcore'
@@ -0,0 +1,34 @@
1
+ /**
2
+ * qdadm i18n types.
3
+ *
4
+ * Generic types come from `@quazardous/qdcore/i18n`; this module adds the
5
+ * admin-specific `I18nOptions` (kernel constructor option that toggles the
6
+ * default core admin bundles shipped via DefaultCoreProvider).
7
+ */
8
+
9
+ import type { BaseI18nOptions } from '@quazardous/qdcore'
10
+
11
+ export type {
12
+ AliasPattern,
13
+ BaseI18nOptions,
14
+ CoreMessages,
15
+ EntityMessages,
16
+ KeyStrategy,
17
+ KeyStrategyName,
18
+ MessagesBundle,
19
+ MessagesNode,
20
+ NavMessages,
21
+ ResolveStep,
22
+ ResolveTrace,
23
+ TranslateParams,
24
+ TranslationProvider,
25
+ } from '@quazardous/qdcore'
26
+
27
+ /**
28
+ * Kernel-level i18n options for qdadm. Extends the generic shared shape with
29
+ * admin-only knobs.
30
+ */
31
+ export interface I18nOptions extends BaseI18nOptions {
32
+ /** Disable shipping qdadm's default core.* bundles (EN + FR). Default: false. */
33
+ disableDefaultCoreBundle?: boolean
34
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * useI18n - Vue composable for accessing the kernel I18n instance.
3
+ *
4
+ * Falls back to a no-op shim if no I18n has been provided to the app — useful
5
+ * for components rendered outside a kernel (tests, isolated examples).
6
+ */
7
+
8
+ import { computed, inject, type Ref, type ComputedRef } from 'vue'
9
+ import type { I18n } from './I18n'
10
+ import type { TranslateParams } from './types'
11
+
12
+ export const I18N_INJECTION_KEY = Symbol('qdadm.i18n')
13
+
14
+ export interface UseI18nResult {
15
+ t: (key: string, params?: TranslateParams) => string
16
+ locale: ComputedRef<string> | Ref<string>
17
+ i18n: I18n | null
18
+ }
19
+
20
+ export function useI18n(): UseI18nResult {
21
+ const i18n = inject<I18n | null>(I18N_INJECTION_KEY, null)
22
+ if (!i18n) {
23
+ return {
24
+ t: (key: string) => key,
25
+ locale: computed(() => 'en'),
26
+ i18n: null,
27
+ }
28
+ }
29
+ return {
30
+ t: (key, params) => i18n.t(key, params),
31
+ locale: i18n.locale,
32
+ i18n,
33
+ }
34
+ }
package/src/index.ts CHANGED
@@ -410,6 +410,43 @@ export * from './notifications/index'
410
410
  // import { debugBar, DebugModule } from 'qdadm/debug'
411
411
  // ════════════════════════════════════════════════════════════════════════════
412
412
 
413
+ // ════════════════════════════════════════════════════════════════════════════
414
+ // I18N (schema-derived keys, pluggable providers, signal-driven locale)
415
+ // ════════════════════════════════════════════════════════════════════════════
416
+ export {
417
+ I18n,
418
+ MessagesRegistry,
419
+ Resolver,
420
+ snakeCaseToTitle,
421
+ InlineTranslationProvider,
422
+ defineStrategy,
423
+ resolveStrategy,
424
+ STRATEGIES,
425
+ LazyTranslationProvider,
426
+ IncrementalDomainProvider,
427
+ isDomainAwareProvider,
428
+ createYamlLoader,
429
+ createDefaultCoreProvider,
430
+ useI18n,
431
+ I18N_INJECTION_KEY,
432
+ } from './i18n'
433
+ export type {
434
+ AliasPattern,
435
+ CoreMessages,
436
+ EntityMessages,
437
+ I18nDeps,
438
+ I18nOptions,
439
+ KeyStrategy,
440
+ KeyStrategyName,
441
+ MessagesBundle,
442
+ MessagesNode,
443
+ NavMessages,
444
+ ResolveStep,
445
+ ResolveTrace,
446
+ TranslateParams,
447
+ TranslationProvider,
448
+ } from './i18n'
449
+
413
450
  // ════════════════════════════════════════════════════════════════════════════
414
451
  // ASSETS
415
452
  // ════════════════════════════════════════════════════════════════════════════
@@ -1,306 +1,23 @@
1
1
  /**
2
- * EventRouter - Declarative signal routing
2
+ * EventRouter qdadm re-export.
3
3
  *
4
- * Routes one signal to multiple targets (signals or callbacks).
5
- * Configured at Kernel level to keep components simple.
4
+ * The actual implementation now lives in `@quazardous/qdcore` so it can be
5
+ * shared with qdcms. The legacy `orchestrator` constructor option (which was
6
+ * never actually read by any callback in qdadm) has been dropped; callers
7
+ * needing extras in `RouteContext` should use the new `context` option:
6
8
  *
7
- * Usage:
8
- * ```js
9
- * const router = new EventRouter({
10
- * signals, // SignalBus instance
11
- * orchestrator, // Orchestrator instance (optional, for callbacks)
12
- * routes: {
13
- * // Auth events → datalayer invalidation with actuator (only authSensitive entities react)
14
- * 'auth:**': [{ signal: 'entity:datalayer-invalidate', transform: () => ({ actuator: 'auth' }) }],
15
- *
16
- * // Custom routing with transform
17
- * 'order:completed': [
18
- * { signal: 'notify', transform: (p) => ({ msg: `Order ${p.id} done` }) },
19
- * (payload, ctx) => { ... } // callback
20
- * ]
21
- * }
22
- * })
9
+ * ```ts
10
+ * createEventRouter({ signals, context: { orchestrator }, routes })
11
+ * // -> available inside callbacks as ctx.orchestrator
23
12
  * ```
24
13
  */
25
14
 
26
- import type { SignalBus } from './SignalBus'
27
- import type { Orchestrator } from '../orchestrator/Orchestrator'
28
-
29
- /**
30
- * Route target with signal and optional transform
31
- */
32
- export interface SignalTarget {
33
- signal: string
34
- transform?: (payload: unknown) => unknown
35
- }
36
-
37
- /**
38
- * Route callback context
39
- */
40
- export interface RouteContext {
41
- signals: SignalBus
42
- orchestrator: Orchestrator | null
43
- }
44
-
45
- /**
46
- * Route callback function
47
- */
48
- export type RouteCallback = (payload: unknown, context: RouteContext) => void
49
-
50
- /**
51
- * Route target types
52
- */
53
- export type RouteTarget = string | SignalTarget | RouteCallback
54
-
55
- /**
56
- * Routes configuration
57
- */
58
- export type RoutesConfig = Record<string, RouteTarget[]>
59
-
60
- /**
61
- * EventRouter options
62
- */
63
- export interface EventRouterOptions {
64
- signals: SignalBus
65
- orchestrator?: Orchestrator | null
66
- routes?: RoutesConfig
67
- debug?: boolean
68
- }
69
-
70
- /**
71
- * Detect cycles in route graph using DFS
72
- */
73
- function detectCycles(routes: RoutesConfig): string[] | null {
74
- // Build adjacency list (only signal targets, not callbacks)
75
- const graph = new Map<string, string[]>()
76
-
77
- for (const [source, targets] of Object.entries(routes)) {
78
- const signalTargets: string[] = []
79
- for (const target of targets) {
80
- if (typeof target === 'string') {
81
- signalTargets.push(target)
82
- } else if (target && typeof target === 'object' && 'signal' in target) {
83
- signalTargets.push(target.signal)
84
- }
85
- // Functions don't create edges in the graph
86
- }
87
- graph.set(source, signalTargets)
88
- }
89
-
90
- // DFS cycle detection
91
- const visited = new Set<string>()
92
- const recursionStack = new Set<string>()
93
- const path: string[] = []
94
-
95
- function dfs(node: string): string[] | null {
96
- visited.add(node)
97
- recursionStack.add(node)
98
- path.push(node)
99
-
100
- const neighbors = graph.get(node) || []
101
- for (const neighbor of neighbors) {
102
- if (!visited.has(neighbor)) {
103
- const cycle = dfs(neighbor)
104
- if (cycle) return cycle
105
- } else if (recursionStack.has(neighbor)) {
106
- // Found cycle - return path from neighbor to current
107
- const cycleStart = path.indexOf(neighbor)
108
- return [...path.slice(cycleStart), neighbor]
109
- }
110
- }
111
-
112
- path.pop()
113
- recursionStack.delete(node)
114
- return null
115
- }
116
-
117
- // Check all nodes
118
- for (const node of graph.keys()) {
119
- if (!visited.has(node)) {
120
- const cycle = dfs(node)
121
- if (cycle) return cycle
122
- }
123
- }
124
-
125
- return null
126
- }
127
-
128
- export class EventRouter {
129
- private _signals: SignalBus
130
- private _orchestrator: Orchestrator | null
131
- private _routes: RoutesConfig
132
- private _debug: boolean
133
- private _cleanups: Array<() => void> = []
134
-
135
- constructor(options: EventRouterOptions) {
136
- const { signals, orchestrator = null, routes = {}, debug = false } = options
137
-
138
- if (!signals) {
139
- throw new Error('[EventRouter] signals is required')
140
- }
141
-
142
- this._signals = signals
143
- this._orchestrator = orchestrator
144
- this._routes = routes
145
- this._debug = debug
146
-
147
- // Validate and setup
148
- this._validateRoutes()
149
- this._setupListeners()
150
- }
151
-
152
- /**
153
- * Validate routes configuration and check for cycles
154
- */
155
- private _validateRoutes(): void {
156
- // Check for cycles
157
- const cycle = detectCycles(this._routes)
158
- if (cycle) {
159
- throw new Error(`[EventRouter] Cycle detected: ${cycle.join(' -> ')}`)
160
- }
161
-
162
- // Validate each route
163
- for (const [source, targets] of Object.entries(this._routes)) {
164
- if (!Array.isArray(targets)) {
165
- throw new Error(
166
- `[EventRouter] Route "${source}" must be an array of targets`
167
- )
168
- }
169
-
170
- for (let i = 0; i < targets.length; i++) {
171
- const target = targets[i]
172
- const isString = typeof target === 'string'
173
- const isFunction = typeof target === 'function'
174
- const isObject =
175
- target && typeof target === 'object' && 'signal' in target
176
-
177
- if (!isString && !isFunction && !isObject) {
178
- throw new Error(
179
- `[EventRouter] Invalid target at "${source}"[${i}]: must be string, function, or { signal, transform? }`
180
- )
181
- }
182
- }
183
- }
184
- }
185
-
186
- /**
187
- * Setup signal listeners for all routes
188
- */
189
- private _setupListeners(): void {
190
- for (const [source, targets] of Object.entries(this._routes)) {
191
- const cleanup = this._signals.on(source, (event: { data: unknown }) => {
192
- this._handleRoute(source, event.data, targets)
193
- })
194
- this._cleanups.push(cleanup)
195
- }
196
-
197
- if (this._debug) {
198
- console.debug(
199
- `[EventRouter] Registered ${Object.keys(this._routes).length} routes`
200
- )
201
- }
202
- }
203
-
204
- /**
205
- * Handle a routed signal
206
- */
207
- private _handleRoute(
208
- source: string,
209
- payload: unknown,
210
- targets: RouteTarget[]
211
- ): void {
212
- if (this._debug) {
213
- console.debug(`[EventRouter] ${source} -> ${targets.length} targets`)
214
- }
215
-
216
- const context: RouteContext = {
217
- signals: this._signals,
218
- orchestrator: this._orchestrator,
219
- }
220
-
221
- for (const target of targets) {
222
- try {
223
- if (typeof target === 'string') {
224
- // String: emit signal with same payload
225
- this._signals.emit(target, payload)
226
- if (this._debug) {
227
- console.debug(`[EventRouter] -> ${target} (forward)`)
228
- }
229
- } else if (typeof target === 'function') {
230
- // Function: call callback
231
- target(payload, context)
232
- if (this._debug) {
233
- console.debug(`[EventRouter] -> callback()`)
234
- }
235
- } else if (target && 'signal' in target) {
236
- // Object: emit signal with transformed payload
237
- const transformedPayload = target.transform
238
- ? target.transform(payload)
239
- : payload
240
- this._signals.emit(target.signal, transformedPayload)
241
- if (this._debug) {
242
- console.debug(`[EventRouter] -> ${target.signal} (transform)`)
243
- }
244
- }
245
- } catch (error) {
246
- console.error(
247
- `[EventRouter] Error handling ${source} -> ${JSON.stringify(target)}:`,
248
- error
249
- )
250
- }
251
- }
252
- }
253
-
254
- /**
255
- * Add a route dynamically
256
- */
257
- addRoute(source: string, targets: RouteTarget[]): void {
258
- if (this._routes[source]) {
259
- throw new Error(`[EventRouter] Route "${source}" already exists`)
260
- }
261
-
262
- // Validate new route doesn't create cycle
263
- const testRoutes = { ...this._routes, [source]: targets }
264
- const cycle = detectCycles(testRoutes)
265
- if (cycle) {
266
- throw new Error(
267
- `[EventRouter] Adding route would create cycle: ${cycle.join(' -> ')}`
268
- )
269
- }
270
-
271
- this._routes[source] = targets
272
-
273
- // Setup listener
274
- const cleanup = this._signals.on(source, (event: { data: unknown }) => {
275
- this._handleRoute(source, event.data, targets)
276
- })
277
- this._cleanups.push(cleanup)
278
- }
279
-
280
- /**
281
- * Get all registered routes
282
- */
283
- getRoutes(): RoutesConfig {
284
- return { ...this._routes }
285
- }
286
-
287
- /**
288
- * Dispose the router, cleaning up all listeners
289
- */
290
- dispose(): void {
291
- for (const cleanup of this._cleanups) {
292
- if (typeof cleanup === 'function') {
293
- cleanup()
294
- }
295
- }
296
- this._cleanups = []
297
- this._routes = {}
298
- }
299
- }
300
-
301
- /**
302
- * Factory function for creating EventRouter
303
- */
304
- export function createEventRouter(options: EventRouterOptions): EventRouter {
305
- return new EventRouter(options)
306
- }
15
+ export { EventRouter, createEventRouter } from '@quazardous/qdcore'
16
+ export type {
17
+ SignalTarget,
18
+ RouteContext,
19
+ RouteCallback,
20
+ RouteTarget,
21
+ RoutesConfig,
22
+ EventRouterOptions,
23
+ } from '@quazardous/qdcore'