qdadm 1.13.0 → 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.
- package/package.json +8 -4
- package/src/chain/ActiveStack.ts +79 -98
- package/src/chain/StackHydrator.ts +3 -2
- package/src/chain/index.ts +7 -1
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/edit/FormActions.vue +9 -6
- package/src/components/edit/LookupPickerDialog.vue +6 -3
- package/src/components/index.ts +6 -0
- package/src/composables/useEntityItemFormPage.ts +1 -0
- package/src/composables/useEntityItemShowPage.ts +1 -0
- package/src/composables/useFieldManager.ts +100 -3
- package/src/composables/useListPage.ts +50 -59
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavigation.ts +26 -3
- package/src/composables/useOptionsLookup.ts +5 -1
- package/src/gen/generateManagers.test.js +27 -0
- package/src/gen/generateManagers.ts +12 -0
- package/src/hooks/HookRegistry.ts +14 -435
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +37 -0
- package/src/kernel/EventRouter.ts +17 -300
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +6 -0
- package/src/kernel/Kernel.registries.ts +10 -2
- package/src/kernel/Kernel.routing.ts +43 -1
- package/src/kernel/Kernel.ts +43 -0
- package/src/kernel/Kernel.types.ts +52 -1
- package/src/kernel/Kernel.vue.ts +121 -15
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +125 -834
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/SSEBridge.ts +7 -362
- package/src/kernel/SignalBus.ts +24 -148
- package/src/modules/debug/AuthCollector.ts +48 -1
- package/src/modules/debug/Collector.ts +16 -302
- package/src/modules/debug/DebugBridge.ts +10 -171
- package/src/modules/debug/DebugModule.ts +35 -5
- package/src/modules/debug/EntitiesCollector.ts +97 -1
- package/src/modules/debug/ErrorCollector.ts +2 -77
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +3 -147
- package/src/modules/debug/RouterCollector.ts +101 -1
- package/src/modules/debug/SignalCollector.ts +2 -150
- package/src/modules/debug/ToastCollector.ts +2 -91
- package/src/modules/debug/ZonesCollector.ts +93 -1
- package/src/modules/debug/components/DebugBar.vue +19 -775
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +4 -3
- package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
- package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
- package/src/modules/debug/components/panels/index.ts +10 -4
- package/src/modules/debug/index.ts +15 -0
- package/src/modules/debug/styles.scss +22 -18
- package/src/utils/index.ts +0 -3
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +16 -0
- package/src/modules/debug/components/ObjectTree.vue +0 -123
- package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
- package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
- package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
- package/src/utils/debugInjector.ts +0 -306
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KernelContext types - shared between the class shell and the prototype-patch
|
|
3
|
+
* modules (KernelContext.{entities,routing,zones,events,permissions,i18n}.ts).
|
|
4
|
+
*
|
|
5
|
+
* Internal types (ModuleWithCleanup, AuthAdapter, KernelOptions,
|
|
6
|
+
* KernelInterface, function types) stay un-exported by KernelContext.ts so
|
|
7
|
+
* they remain private to qdadm internals; the public types (NavItem,
|
|
8
|
+
* ZoneOptions, …) are re-exported from KernelContext.ts to preserve the
|
|
9
|
+
* existing barrel API.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { App, Component } from 'vue'
|
|
13
|
+
import type { Router } from 'vue-router'
|
|
14
|
+
import type { UsersManagerOptions } from '../security/UsersManager'
|
|
15
|
+
import type { SignalBus } from './SignalBus'
|
|
16
|
+
import type { Orchestrator } from '../orchestrator/Orchestrator'
|
|
17
|
+
import type { HookRegistry } from '../hooks/HookRegistry'
|
|
18
|
+
import type { ZoneRegistry } from '../zones/ZoneRegistry'
|
|
19
|
+
import type { DeferredRegistry } from '../deferred/DeferredRegistry.js'
|
|
20
|
+
import type { SecurityChecker } from '../entity/auth/SecurityChecker'
|
|
21
|
+
import type { PermissionRegistry } from '../security/PermissionRegistry'
|
|
22
|
+
import type { I18n } from '../i18n/I18n'
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Internal types — shared across patch modules, not exposed by index.ts
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** Extended Module interface with internal cleanup method */
|
|
29
|
+
export interface ModuleWithCleanup {
|
|
30
|
+
constructor?: { name?: string }
|
|
31
|
+
_addSignalCleanup?: (cleanup: () => void) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Auth adapter interface */
|
|
35
|
+
export interface AuthAdapter {
|
|
36
|
+
getUser(): unknown
|
|
37
|
+
[key: string]: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Kernel options interface */
|
|
41
|
+
export interface KernelOptions {
|
|
42
|
+
debug?: boolean
|
|
43
|
+
authAdapter?: AuthAdapter | null
|
|
44
|
+
storageResolver?: unknown
|
|
45
|
+
managerResolver?: unknown
|
|
46
|
+
managerRegistry?: Record<string, unknown>
|
|
47
|
+
[key: string]: unknown
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Kernel interface for type safety inside KernelContext */
|
|
51
|
+
export interface KernelInterface {
|
|
52
|
+
vueApp: App | null
|
|
53
|
+
router: Router | null
|
|
54
|
+
signals: SignalBus | null
|
|
55
|
+
orchestrator: Orchestrator | null
|
|
56
|
+
zoneRegistry: ZoneRegistry | null
|
|
57
|
+
hookRegistry: HookRegistry | null
|
|
58
|
+
deferred: DeferredRegistry | null
|
|
59
|
+
securityChecker: SecurityChecker | null
|
|
60
|
+
permissionRegistry: PermissionRegistry | null
|
|
61
|
+
activeStack: unknown
|
|
62
|
+
stackHydrator: unknown
|
|
63
|
+
options: KernelOptions
|
|
64
|
+
i18nInstance: I18n | null
|
|
65
|
+
_pendingProvides: Map<string | symbol, unknown>
|
|
66
|
+
_pendingComponents: Map<string, Component>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Signal handler function type */
|
|
70
|
+
export type SignalHandler = (event: { data: unknown; [key: string]: unknown }) => void
|
|
71
|
+
|
|
72
|
+
/** Hook handler function type */
|
|
73
|
+
export type HookHandler = (context: unknown) => unknown | Promise<unknown>
|
|
74
|
+
|
|
75
|
+
/** Deferred factory function type */
|
|
76
|
+
export type DeferredFactory = () => Promise<void>
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// Public types — re-exported by KernelContext.ts (and ultimately by index.ts)
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** Navigation item configuration */
|
|
83
|
+
export interface NavItem {
|
|
84
|
+
section: string
|
|
85
|
+
route: string
|
|
86
|
+
label: string
|
|
87
|
+
icon?: string
|
|
88
|
+
exact?: boolean
|
|
89
|
+
entity?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Zone configuration */
|
|
93
|
+
export interface ZoneOptions {
|
|
94
|
+
default?: Component
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Block configuration */
|
|
98
|
+
export interface BlockConfig {
|
|
99
|
+
component: Component
|
|
100
|
+
weight?: number
|
|
101
|
+
props?: Record<string, unknown>
|
|
102
|
+
id?: string
|
|
103
|
+
operation?: 'add' | 'replace' | 'extend' | 'wrap'
|
|
104
|
+
/** Block ID to replace (required if operation='replace') */
|
|
105
|
+
replaces?: string
|
|
106
|
+
/** Block ID to insert before (for operation='extend') */
|
|
107
|
+
before?: string
|
|
108
|
+
/** Block ID to insert after (for operation='extend') */
|
|
109
|
+
after?: string
|
|
110
|
+
/** Block ID to wrap (required if operation='wrap') */
|
|
111
|
+
wraps?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Route options */
|
|
115
|
+
export interface RouteOptions {
|
|
116
|
+
entity?: string
|
|
117
|
+
parent?: ParentConfig
|
|
118
|
+
label?: string
|
|
119
|
+
layout?: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Parent route configuration */
|
|
123
|
+
export interface ParentConfig {
|
|
124
|
+
entity: string
|
|
125
|
+
param: string
|
|
126
|
+
foreignKey?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** CRUD page components */
|
|
130
|
+
export interface CrudPages {
|
|
131
|
+
list?: () => Promise<{ default: Component }>
|
|
132
|
+
show?: () => Promise<{ default: Component }>
|
|
133
|
+
form?: () => Promise<{ default: Component }>
|
|
134
|
+
create?: () => Promise<{ default: Component }>
|
|
135
|
+
edit?: () => Promise<{ default: Component }>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** CRUD options */
|
|
139
|
+
export interface CrudOptions {
|
|
140
|
+
nav?: {
|
|
141
|
+
section: string
|
|
142
|
+
icon?: string
|
|
143
|
+
label?: string
|
|
144
|
+
}
|
|
145
|
+
routePrefix?: string
|
|
146
|
+
/** Override the URL path segment (defaults to entity name). Useful for camelCase entities like 'jobTasks' → 'tasks'. */
|
|
147
|
+
pathSegment?: string
|
|
148
|
+
parentRoute?: string
|
|
149
|
+
foreignKey?: string
|
|
150
|
+
label?: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Child page options for non-entity child routes */
|
|
154
|
+
export interface ChildPageOptions {
|
|
155
|
+
component: () => Promise<{ default: Component }>
|
|
156
|
+
label?: string
|
|
157
|
+
icon?: string
|
|
158
|
+
meta?: Record<string, unknown>
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** User entity options */
|
|
162
|
+
export type UserEntityOptions = UsersManagerOptions
|
|
163
|
+
|
|
164
|
+
/** Permission metadata */
|
|
165
|
+
export interface PermissionMeta {
|
|
166
|
+
label: string
|
|
167
|
+
description?: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Permission options */
|
|
171
|
+
export interface PermissionOptions {
|
|
172
|
+
isEntity?: boolean
|
|
173
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch KernelContext prototype with UI extensibility methods:
|
|
3
|
+
* - zone(name, options) — define a zone slot
|
|
4
|
+
* - block(zoneName, config) — register a block in a zone
|
|
5
|
+
* - provide(key, value) — Vue dependency injection
|
|
6
|
+
* - component(name, component) — global Vue component registration
|
|
7
|
+
*
|
|
8
|
+
* provide/component fall back to the kernel's _pendingProvides /
|
|
9
|
+
* _pendingComponents maps when the Vue app isn't created yet, so modules can
|
|
10
|
+
* call them during connect().
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Component } from 'vue'
|
|
14
|
+
import type { BlockConfig, ZoneOptions } from './KernelContext.types'
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
type Self = any
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
export function applyZoneMethods(KernelContextClass: { prototype: any }): void {
|
|
21
|
+
const proto = KernelContextClass.prototype as Self
|
|
22
|
+
|
|
23
|
+
proto.zone = function (this: Self, name: string, options: ZoneOptions = {}): Self {
|
|
24
|
+
if (this._kernel.zoneRegistry) {
|
|
25
|
+
this._kernel.zoneRegistry.defineZone(name, options)
|
|
26
|
+
}
|
|
27
|
+
return this
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
proto.block = function (this: Self, zoneName: string, config: BlockConfig): Self {
|
|
31
|
+
if (this._kernel.zoneRegistry) {
|
|
32
|
+
this._kernel.zoneRegistry.registerBlock(zoneName, config)
|
|
33
|
+
}
|
|
34
|
+
return this
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
proto.provide = function (this: Self, key: string | symbol, value: unknown): Self {
|
|
38
|
+
if (this._kernel.vueApp) {
|
|
39
|
+
this._kernel.vueApp.provide(key, value)
|
|
40
|
+
} else {
|
|
41
|
+
this._kernel._pendingProvides.set(key, value)
|
|
42
|
+
}
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
proto.component = function (this: Self, name: string, component: Component): Self {
|
|
47
|
+
if (this._kernel.vueApp) {
|
|
48
|
+
this._kernel.vueApp.component(name, component)
|
|
49
|
+
} else {
|
|
50
|
+
this._kernel._pendingComponents.set(name, component)
|
|
51
|
+
}
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/kernel/SSEBridge.ts
CHANGED
|
@@ -1,366 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSEBridge
|
|
2
|
+
* SSEBridge — qdadm re-export.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Benefits:
|
|
9
|
-
* - Single SSE connection per app (vs. one per component)
|
|
10
|
-
* - Decoupled event handling via SignalBus
|
|
11
|
-
* - Automatic reconnection with configurable delay
|
|
12
|
-
* - Connection status available via signals
|
|
13
|
-
*
|
|
14
|
-
* Signal naming:
|
|
15
|
-
* - `sse:connected` - Emitted when connection established
|
|
16
|
-
* - `sse:disconnected` - Emitted when connection lost
|
|
17
|
-
* - `sse:error` - Emitted on connection error
|
|
18
|
-
* - `sse:{eventName}` - SSE event forwarded (e.g., sse:task:completed)
|
|
19
|
-
*
|
|
20
|
-
* Usage in Kernel:
|
|
21
|
-
* ```js
|
|
22
|
-
* new Kernel({
|
|
23
|
-
* sse: {
|
|
24
|
-
* url: '/api/events',
|
|
25
|
-
* reconnectDelay: 5000,
|
|
26
|
-
* signalPrefix: 'sse',
|
|
27
|
-
* autoConnect: true
|
|
28
|
-
* }
|
|
29
|
-
* })
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* Usage in components:
|
|
33
|
-
* ```js
|
|
34
|
-
* const signals = inject('qdadmSignals')
|
|
35
|
-
*
|
|
36
|
-
* // Subscribe to specific SSE events
|
|
37
|
-
* signals.on('sse:task:completed', ({ data }) => {
|
|
38
|
-
* console.log('Task completed:', data)
|
|
39
|
-
* })
|
|
40
|
-
*
|
|
41
|
-
* // Subscribe to all SSE events via wildcard
|
|
42
|
-
* signals.on('sse:*', ({ event, data }) => {
|
|
43
|
-
* console.log(`SSE event ${event}:`, data)
|
|
44
|
-
* })
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
import type { SignalBus } from './SignalBus'
|
|
49
|
-
|
|
50
|
-
export const SSE_SIGNALS = {
|
|
51
|
-
CONNECTED: 'sse:connected',
|
|
52
|
-
DISCONNECTED: 'sse:disconnected',
|
|
53
|
-
ERROR: 'sse:error',
|
|
54
|
-
MESSAGE: 'sse:message',
|
|
55
|
-
} as const
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* SSEBridge options
|
|
4
|
+
* The actual implementation now lives in `@quazardous/qdcore` so it can be
|
|
5
|
+
* shared with qdcms. The auth-aware connect/disconnect behaviour is now
|
|
6
|
+
* configurable via `connectOnSignal` / `disconnectOnSignal` options; defaults
|
|
7
|
+
* remain `'auth:login'` / `'auth:logout'` for backwards compatibility.
|
|
59
8
|
*/
|
|
60
|
-
export interface SSEBridgeOptions {
|
|
61
|
-
/** SignalBus instance */
|
|
62
|
-
signals: SignalBus
|
|
63
|
-
/** SSE endpoint URL */
|
|
64
|
-
url: string
|
|
65
|
-
/** Delay before reconnect (ms), 0 to disable */
|
|
66
|
-
reconnectDelay?: number
|
|
67
|
-
/** Prefix for emitted signals (default: 'sse') */
|
|
68
|
-
signalPrefix?: string
|
|
69
|
-
/** Connect immediately (default: false, waits for auth:login) */
|
|
70
|
-
autoConnect?: boolean
|
|
71
|
-
/** Include credentials in request */
|
|
72
|
-
withCredentials?: boolean
|
|
73
|
-
/** Query param name for auth token */
|
|
74
|
-
tokenParam?: string
|
|
75
|
-
/** Function to get auth token */
|
|
76
|
-
getToken?: (() => string | null) | null
|
|
77
|
-
/** Enable debug logging */
|
|
78
|
-
debug?: boolean
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export class SSEBridge {
|
|
82
|
-
private _signals: SignalBus
|
|
83
|
-
private _url: string
|
|
84
|
-
private _reconnectDelay: number
|
|
85
|
-
private _signalPrefix: string
|
|
86
|
-
private _withCredentials: boolean
|
|
87
|
-
private _tokenParam: string
|
|
88
|
-
private _getToken: (() => string | null) | null
|
|
89
|
-
private _debug: boolean
|
|
90
|
-
|
|
91
|
-
private _eventSource: EventSource | null = null
|
|
92
|
-
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
93
|
-
private _connected = false
|
|
94
|
-
private _reconnecting = false
|
|
95
|
-
|
|
96
|
-
constructor(options: SSEBridgeOptions) {
|
|
97
|
-
const {
|
|
98
|
-
signals,
|
|
99
|
-
url,
|
|
100
|
-
reconnectDelay = 5000,
|
|
101
|
-
signalPrefix = 'sse',
|
|
102
|
-
autoConnect = false,
|
|
103
|
-
withCredentials = false,
|
|
104
|
-
tokenParam = 'token',
|
|
105
|
-
getToken = null,
|
|
106
|
-
debug = false,
|
|
107
|
-
} = options
|
|
108
|
-
|
|
109
|
-
if (!signals) {
|
|
110
|
-
throw new Error('[SSEBridge] signals (SignalBus) is required')
|
|
111
|
-
}
|
|
112
|
-
if (!url) {
|
|
113
|
-
throw new Error('[SSEBridge] url is required')
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
this._signals = signals
|
|
117
|
-
this._url = url
|
|
118
|
-
this._reconnectDelay = reconnectDelay
|
|
119
|
-
this._signalPrefix = signalPrefix
|
|
120
|
-
this._withCredentials = withCredentials
|
|
121
|
-
this._tokenParam = tokenParam
|
|
122
|
-
this._getToken = getToken
|
|
123
|
-
this._debug = debug
|
|
124
|
-
|
|
125
|
-
// Auto-connect or wait for auth:login
|
|
126
|
-
if (autoConnect) {
|
|
127
|
-
this.connect()
|
|
128
|
-
} else {
|
|
129
|
-
// Wait for auth:login signal to connect
|
|
130
|
-
this._signals.once('auth:login', () => {
|
|
131
|
-
this._log('Received auth:login, connecting SSE')
|
|
132
|
-
this.connect()
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Disconnect on auth:logout
|
|
137
|
-
this._signals.on('auth:logout', () => {
|
|
138
|
-
this._log('Received auth:logout, disconnecting SSE')
|
|
139
|
-
this.disconnect()
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Build signal name with prefix
|
|
145
|
-
*/
|
|
146
|
-
private _buildSignal(eventName: string): string {
|
|
147
|
-
return `${this._signalPrefix}:${eventName}`
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Debug logging
|
|
152
|
-
*/
|
|
153
|
-
private _log(...args: unknown[]): void {
|
|
154
|
-
if (this._debug) {
|
|
155
|
-
console.debug('[SSEBridge]', ...args)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Build SSE URL with auth token
|
|
161
|
-
*/
|
|
162
|
-
private _buildUrl(): string {
|
|
163
|
-
const token = this._getToken?.()
|
|
164
|
-
const sseUrl = new URL(this._url, window.location.origin)
|
|
165
|
-
|
|
166
|
-
if (token && this._tokenParam) {
|
|
167
|
-
sseUrl.searchParams.set(this._tokenParam, token)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return sseUrl.toString()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Connect to SSE endpoint
|
|
175
|
-
*/
|
|
176
|
-
connect(): void {
|
|
177
|
-
// Clean up existing
|
|
178
|
-
if (this._eventSource) {
|
|
179
|
-
this._eventSource.close()
|
|
180
|
-
this._eventSource = null
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Clear pending reconnect
|
|
184
|
-
if (this._reconnectTimer) {
|
|
185
|
-
clearTimeout(this._reconnectTimer)
|
|
186
|
-
this._reconnectTimer = null
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const url = this._buildUrl()
|
|
191
|
-
this._log('Connecting to', url)
|
|
192
|
-
|
|
193
|
-
this._eventSource = new EventSource(url, {
|
|
194
|
-
withCredentials: this._withCredentials,
|
|
195
|
-
})
|
|
196
9
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this._reconnecting = false
|
|
200
|
-
this._log('Connected')
|
|
201
|
-
|
|
202
|
-
this._signals.emit(SSE_SIGNALS.CONNECTED, {
|
|
203
|
-
url: this._url,
|
|
204
|
-
timestamp: new Date(),
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this._eventSource.onerror = (): void => {
|
|
209
|
-
this._connected = false
|
|
210
|
-
this._log('Connection error')
|
|
211
|
-
|
|
212
|
-
this._signals.emit(SSE_SIGNALS.ERROR, {
|
|
213
|
-
error: 'Connection error',
|
|
214
|
-
timestamp: new Date(),
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
// Close broken connection
|
|
218
|
-
if (this._eventSource) {
|
|
219
|
-
this._eventSource.close()
|
|
220
|
-
this._eventSource = null
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
this._signals.emit(SSE_SIGNALS.DISCONNECTED, {
|
|
224
|
-
timestamp: new Date(),
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
// Schedule reconnect
|
|
228
|
-
this._scheduleReconnect()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Handle generic message events (event: message)
|
|
232
|
-
this._eventSource.onmessage = (event: MessageEvent): void => {
|
|
233
|
-
this._handleEvent('message', event)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// For named events, we need to add listeners dynamically
|
|
237
|
-
// Since EventSource doesn't expose event names, we use a wrapper approach:
|
|
238
|
-
// The server should send events with `event:` field, we handle them generically
|
|
239
|
-
|
|
240
|
-
// Note: Named events require explicit addEventListener.
|
|
241
|
-
// To support arbitrary event names, the app should either:
|
|
242
|
-
// 1. Use `message` event type and include event name in data
|
|
243
|
-
// 2. Pre-register known event names via registerEvents()
|
|
244
|
-
} catch (err) {
|
|
245
|
-
const error = err as Error
|
|
246
|
-
this._log('Connect error:', error.message)
|
|
247
|
-
this._signals.emit(SSE_SIGNALS.ERROR, {
|
|
248
|
-
error: error.message,
|
|
249
|
-
timestamp: new Date(),
|
|
250
|
-
})
|
|
251
|
-
this._scheduleReconnect()
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Register listeners for specific SSE event types
|
|
257
|
-
* Required because EventSource requires explicit addEventListener for named events.
|
|
258
|
-
*/
|
|
259
|
-
registerEvents(eventNames: string[]): void {
|
|
260
|
-
if (!this._eventSource) {
|
|
261
|
-
this._log('Cannot register events: not connected')
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
for (const eventName of eventNames) {
|
|
266
|
-
this._eventSource.addEventListener(eventName, (event) => {
|
|
267
|
-
this._handleEvent(eventName, event as MessageEvent)
|
|
268
|
-
})
|
|
269
|
-
this._log('Registered event:', eventName)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Handle incoming SSE event
|
|
275
|
-
*/
|
|
276
|
-
private _handleEvent(eventName: string, event: MessageEvent): void {
|
|
277
|
-
let data: unknown
|
|
278
|
-
try {
|
|
279
|
-
data = JSON.parse(event.data as string)
|
|
280
|
-
} catch {
|
|
281
|
-
data = event.data
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const signal = this._buildSignal(eventName)
|
|
285
|
-
this._log(`Emitting ${signal}:`, data)
|
|
286
|
-
|
|
287
|
-
this._signals.emit(signal, {
|
|
288
|
-
event: eventName,
|
|
289
|
-
data,
|
|
290
|
-
timestamp: new Date(),
|
|
291
|
-
lastEventId: event.lastEventId,
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Schedule reconnection
|
|
297
|
-
*/
|
|
298
|
-
private _scheduleReconnect(): void {
|
|
299
|
-
if (this._reconnectDelay <= 0) return
|
|
300
|
-
if (this._reconnectTimer) return
|
|
301
|
-
|
|
302
|
-
this._reconnecting = true
|
|
303
|
-
this._log(`Reconnecting in ${this._reconnectDelay}ms`)
|
|
304
|
-
|
|
305
|
-
this._reconnectTimer = setTimeout(() => {
|
|
306
|
-
this._reconnectTimer = null
|
|
307
|
-
if (!this._connected) {
|
|
308
|
-
this.connect()
|
|
309
|
-
}
|
|
310
|
-
}, this._reconnectDelay)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Disconnect from SSE endpoint
|
|
315
|
-
*/
|
|
316
|
-
disconnect(): void {
|
|
317
|
-
if (this._reconnectTimer) {
|
|
318
|
-
clearTimeout(this._reconnectTimer)
|
|
319
|
-
this._reconnectTimer = null
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (this._eventSource) {
|
|
323
|
-
this._eventSource.close()
|
|
324
|
-
this._eventSource = null
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (this._connected) {
|
|
328
|
-
this._connected = false
|
|
329
|
-
this._signals.emit(SSE_SIGNALS.DISCONNECTED, {
|
|
330
|
-
timestamp: new Date(),
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
this._reconnecting = false
|
|
335
|
-
this._log('Disconnected')
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Reconnect (disconnect + connect)
|
|
340
|
-
*/
|
|
341
|
-
reconnect(): void {
|
|
342
|
-
this.disconnect()
|
|
343
|
-
this.connect()
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Check if connected
|
|
348
|
-
*/
|
|
349
|
-
isConnected(): boolean {
|
|
350
|
-
return this._connected
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Check if reconnecting
|
|
355
|
-
*/
|
|
356
|
-
isReconnecting(): boolean {
|
|
357
|
-
return this._reconnecting
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Factory function to create SSEBridge
|
|
363
|
-
*/
|
|
364
|
-
export function createSSEBridge(options: SSEBridgeOptions): SSEBridge {
|
|
365
|
-
return new SSEBridge(options)
|
|
366
|
-
}
|
|
10
|
+
export { SSEBridge, createSSEBridge, SSE_SIGNALS } from '@quazardous/qdcore'
|
|
11
|
+
export type { SSEBridgeOptions } from '@quazardous/qdcore'
|