@hypen-space/core 0.2.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/dist/chunk-5va59f7m.js +22 -0
- package/dist/chunk-5va59f7m.js.map +9 -0
- package/dist/engine.d.ts +101 -0
- package/dist/events.d.ts +78 -0
- package/dist/index.browser.d.ts +13 -0
- package/dist/index.d.ts +33 -0
- package/dist/remote/index.d.ts +6 -0
- package/dist/router.d.ts +93 -0
- package/dist/src/app.js +160 -0
- package/dist/src/app.js.map +10 -0
- package/dist/src/context.js +114 -0
- package/dist/src/context.js.map +10 -0
- package/dist/src/engine.browser.js +130 -0
- package/dist/src/engine.browser.js.map +10 -0
- package/dist/src/engine.js +101 -0
- package/dist/src/engine.js.map +10 -0
- package/dist/src/events.js +72 -0
- package/dist/src/events.js.map +10 -0
- package/dist/src/index.browser.js +51 -0
- package/dist/src/index.browser.js.map +9 -0
- package/dist/src/index.js +55 -0
- package/dist/src/index.js.map +9 -0
- package/dist/src/remote/client.js +176 -0
- package/dist/src/remote/client.js.map +10 -0
- package/dist/src/remote/index.js +9 -0
- package/dist/src/remote/index.js.map +9 -0
- package/dist/src/remote/types.js +2 -0
- package/dist/src/remote/types.js.map +9 -0
- package/dist/src/renderer.js +58 -0
- package/dist/src/renderer.js.map +10 -0
- package/dist/src/router.js +189 -0
- package/dist/src/router.js.map +10 -0
- package/dist/src/state.js +226 -0
- package/dist/src/state.js.map +10 -0
- package/dist/state.d.ts +30 -0
- package/package.json +124 -0
- package/src/app.ts +330 -0
- package/src/context.ts +201 -0
- package/src/engine.browser.ts +245 -0
- package/src/engine.ts +208 -0
- package/src/events.ts +126 -0
- package/src/index.browser.ts +104 -0
- package/src/index.ts +126 -0
- package/src/remote/client.ts +274 -0
- package/src/remote/index.ts +17 -0
- package/src/remote/types.ts +51 -0
- package/src/renderer.ts +102 -0
- package/src/router.ts +311 -0
- package/src/state.ts +363 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hypen App Builder API
|
|
3
|
+
* Implements the stateful module system from RFC-0001
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Action } from "./engine.js";
|
|
7
|
+
|
|
8
|
+
// Interface for engine compatibility (works with both engine.js and engine.browser.js)
|
|
9
|
+
export interface IEngine {
|
|
10
|
+
setModule(name: string, actions: string[], stateKeys: string[], initialState: unknown): void;
|
|
11
|
+
onAction(actionName: string, handler: (action: Action) => void | Promise<void>): void;
|
|
12
|
+
notifyStateChange(paths: string[], changedValues: Record<string, unknown>): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
import { createObservableState, type StateChange, getStateSnapshot } from "./state.js";
|
|
16
|
+
import type { HypenRouter } from "./router.js";
|
|
17
|
+
import type { HypenGlobalContext, ModuleReference } from "./context.js";
|
|
18
|
+
|
|
19
|
+
export type RouterContext = {
|
|
20
|
+
root: HypenRouter;
|
|
21
|
+
current?: HypenRouter;
|
|
22
|
+
parent?: HypenRouter;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ActionContext = {
|
|
26
|
+
name: string;
|
|
27
|
+
payload?: unknown;
|
|
28
|
+
sender?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ActionNext = {
|
|
32
|
+
router: HypenRouter; // Direct access to router instance (from nearest parent)
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type GlobalContext = {
|
|
36
|
+
getModule: <T = unknown>(id: string) => ModuleReference<T>;
|
|
37
|
+
hasModule: (id: string) => boolean;
|
|
38
|
+
getModuleIds: () => string[];
|
|
39
|
+
getGlobalState: () => Record<string, unknown>;
|
|
40
|
+
emit: (event: string, payload?: unknown) => void;
|
|
41
|
+
on: (event: string, handler: (payload?: unknown) => void) => () => void;
|
|
42
|
+
__router?: unknown; // Internal: router instance for built-in Router component
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type LifecycleHandler<T> = (
|
|
46
|
+
state: T,
|
|
47
|
+
context?: GlobalContext
|
|
48
|
+
) => void | Promise<void>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Action handler context - all parameters available explicitly
|
|
52
|
+
*/
|
|
53
|
+
export interface ActionHandlerContext<T> {
|
|
54
|
+
action: ActionContext;
|
|
55
|
+
state: T;
|
|
56
|
+
next: ActionNext;
|
|
57
|
+
context: GlobalContext;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Action handler - receives all context in a single object
|
|
62
|
+
*/
|
|
63
|
+
export type ActionHandler<T> = (ctx: ActionHandlerContext<T>) => void | Promise<void>;
|
|
64
|
+
|
|
65
|
+
export interface HypenModuleDefinition<T = unknown> {
|
|
66
|
+
name?: string;
|
|
67
|
+
actions: string[];
|
|
68
|
+
stateKeys: string[];
|
|
69
|
+
persist?: boolean;
|
|
70
|
+
version?: number;
|
|
71
|
+
initialState: T;
|
|
72
|
+
handlers: {
|
|
73
|
+
onCreated?: LifecycleHandler<T>;
|
|
74
|
+
onAction: Map<string, ActionHandler<T>>;
|
|
75
|
+
onDestroyed?: LifecycleHandler<T>;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Alias for HypenModuleDefinition for backward compatibility
|
|
81
|
+
*/
|
|
82
|
+
export type HypenModule<T = unknown> = HypenModuleDefinition<T>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Builder for creating Hypen app modules
|
|
86
|
+
*/
|
|
87
|
+
export class HypenAppBuilder<T> {
|
|
88
|
+
private initialState: T;
|
|
89
|
+
private options: { persist?: boolean; version?: number; name?: string };
|
|
90
|
+
private createdHandler?: LifecycleHandler<T>;
|
|
91
|
+
private actionHandlers: Map<string, ActionHandler<T>> = new Map();
|
|
92
|
+
private destroyedHandler?: LifecycleHandler<T>;
|
|
93
|
+
|
|
94
|
+
constructor(
|
|
95
|
+
initialState: T,
|
|
96
|
+
options?: { persist?: boolean; version?: number; name?: string }
|
|
97
|
+
) {
|
|
98
|
+
this.initialState = initialState;
|
|
99
|
+
this.options = options || {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register a handler for module creation
|
|
104
|
+
*/
|
|
105
|
+
onCreated(fn: LifecycleHandler<T>): this {
|
|
106
|
+
this.createdHandler = fn;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register a handler for a specific action
|
|
112
|
+
*/
|
|
113
|
+
onAction(name: string, fn: ActionHandler<T>): this {
|
|
114
|
+
this.actionHandlers.set(name, fn);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register a handler for module destruction
|
|
120
|
+
*/
|
|
121
|
+
onDestroyed(fn: LifecycleHandler<T>): this {
|
|
122
|
+
this.destroyedHandler = fn;
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build the module definition
|
|
128
|
+
*/
|
|
129
|
+
build(): HypenModuleDefinition<T> {
|
|
130
|
+
// Safe way to get keys from initialState
|
|
131
|
+
const stateKeys = this.initialState !== null && typeof this.initialState === 'object'
|
|
132
|
+
? Object.keys(this.initialState)
|
|
133
|
+
: [];
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
name: this.options.name,
|
|
137
|
+
actions: Array.from(this.actionHandlers.keys()),
|
|
138
|
+
stateKeys,
|
|
139
|
+
persist: this.options.persist,
|
|
140
|
+
version: this.options.version,
|
|
141
|
+
initialState: this.initialState,
|
|
142
|
+
handlers: {
|
|
143
|
+
onCreated: this.createdHandler,
|
|
144
|
+
onAction: this.actionHandlers,
|
|
145
|
+
onDestroyed: this.destroyedHandler,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hypen App API
|
|
153
|
+
*/
|
|
154
|
+
export class HypenApp {
|
|
155
|
+
/**
|
|
156
|
+
* Define the initial state for a module
|
|
157
|
+
*/
|
|
158
|
+
defineState<T>(
|
|
159
|
+
initial: T,
|
|
160
|
+
options?: { persist?: boolean; version?: number; name?: string }
|
|
161
|
+
): HypenAppBuilder<T> {
|
|
162
|
+
return new HypenAppBuilder(initial, options);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The main app instance for creating modules
|
|
168
|
+
*/
|
|
169
|
+
export const app = new HypenApp();
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Module Instance - manages a running module with typed state
|
|
173
|
+
*/
|
|
174
|
+
export class HypenModuleInstance<T extends object = any> {
|
|
175
|
+
private engine: IEngine;
|
|
176
|
+
private definition: HypenModuleDefinition<T>;
|
|
177
|
+
private state: T;
|
|
178
|
+
private isDestroyed = false;
|
|
179
|
+
private routerContext?: RouterContext;
|
|
180
|
+
private globalContext?: HypenGlobalContext;
|
|
181
|
+
private stateChangeCallbacks: Array<() => void> = [];
|
|
182
|
+
|
|
183
|
+
constructor(
|
|
184
|
+
engine: IEngine,
|
|
185
|
+
definition: HypenModuleDefinition<T>,
|
|
186
|
+
routerContext?: RouterContext,
|
|
187
|
+
globalContext?: HypenGlobalContext
|
|
188
|
+
) {
|
|
189
|
+
this.engine = engine;
|
|
190
|
+
this.definition = definition;
|
|
191
|
+
this.routerContext = routerContext;
|
|
192
|
+
this.globalContext = globalContext;
|
|
193
|
+
|
|
194
|
+
// Create observable state that sends only changed paths and values to engine
|
|
195
|
+
this.state = createObservableState<T>(definition.initialState as T & object, {
|
|
196
|
+
onChange: (change: StateChange) => {
|
|
197
|
+
// Send only the changed paths and their new values (not the whole state)
|
|
198
|
+
this.engine.notifyStateChange(change.paths, change.newValues);
|
|
199
|
+
// Notify all registered callbacks
|
|
200
|
+
this.stateChangeCallbacks.forEach(cb => cb());
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Register with engine (initial state registration)
|
|
205
|
+
this.engine.setModule(
|
|
206
|
+
definition.name || "AnonymousModule",
|
|
207
|
+
definition.actions,
|
|
208
|
+
definition.stateKeys,
|
|
209
|
+
getStateSnapshot(this.state)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Register action handlers with flexible parameter support
|
|
213
|
+
for (const [actionName, handler] of definition.handlers.onAction) {
|
|
214
|
+
console.log(`[ModuleInstance] Registering action handler: ${actionName} for module ${definition.name}`);
|
|
215
|
+
this.engine.onAction(actionName, async (action: Action) => {
|
|
216
|
+
console.log(`[ModuleInstance] Action handler fired: ${actionName}`, action);
|
|
217
|
+
|
|
218
|
+
const actionCtx: ActionContext = {
|
|
219
|
+
name: action.name,
|
|
220
|
+
payload: action.payload,
|
|
221
|
+
sender: action.sender,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const next: ActionNext = {
|
|
225
|
+
router: this.routerContext?.root || (null as any),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const context: GlobalContext | undefined = this.globalContext
|
|
229
|
+
? this.createGlobalContextAPI()
|
|
230
|
+
: undefined;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await handler({
|
|
234
|
+
action: actionCtx,
|
|
235
|
+
state: this.state,
|
|
236
|
+
next,
|
|
237
|
+
context: context!,
|
|
238
|
+
});
|
|
239
|
+
console.log(`[ModuleInstance] Action handler completed: ${actionName}`);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error(`[ModuleInstance] Action handler error for ${actionName}:`, error);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Call onCreated
|
|
247
|
+
this.callCreatedHandler();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create the global context API for this module
|
|
252
|
+
*/
|
|
253
|
+
private createGlobalContextAPI(): GlobalContext {
|
|
254
|
+
if (!this.globalContext) {
|
|
255
|
+
throw new Error("Global context not available");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const ctx = this.globalContext;
|
|
259
|
+
const api: GlobalContext = {
|
|
260
|
+
getModule: (id: string) => ctx.getModule(id),
|
|
261
|
+
hasModule: (id: string) => ctx.hasModule(id),
|
|
262
|
+
getModuleIds: () => ctx.getModuleIds(),
|
|
263
|
+
getGlobalState: () => ctx.getGlobalState(),
|
|
264
|
+
emit: (event: string, payload?: any) => ctx.emit(event, payload),
|
|
265
|
+
on: (event: string, handler: (payload?: any) => void) =>
|
|
266
|
+
ctx.on(event, handler),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Expose router and hypen engine for built-in components (if available)
|
|
270
|
+
if ((ctx as any).__router) {
|
|
271
|
+
api.__router = (ctx as any).__router;
|
|
272
|
+
}
|
|
273
|
+
if ((ctx as any).__hypenEngine) {
|
|
274
|
+
(api as any).__hypenEngine = (ctx as any).__hypenEngine;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return api;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Call the onCreated handler
|
|
282
|
+
*/
|
|
283
|
+
private async callCreatedHandler(): Promise<void> {
|
|
284
|
+
if (this.definition.handlers.onCreated) {
|
|
285
|
+
const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
|
|
286
|
+
await this.definition.handlers.onCreated(this.state, context);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Register a callback to be notified when state changes
|
|
292
|
+
*/
|
|
293
|
+
onStateChange(callback: () => void): void {
|
|
294
|
+
this.stateChangeCallbacks.push(callback);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Destroy the module instance
|
|
299
|
+
*/
|
|
300
|
+
async destroy(): Promise<void> {
|
|
301
|
+
if (this.isDestroyed) return;
|
|
302
|
+
|
|
303
|
+
if (this.definition.handlers.onDestroyed) {
|
|
304
|
+
await this.definition.handlers.onDestroyed(this.state);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.isDestroyed = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get the current state (returns a snapshot)
|
|
312
|
+
*/
|
|
313
|
+
getState(): T {
|
|
314
|
+
return getStateSnapshot(this.state);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get the live observable state
|
|
319
|
+
*/
|
|
320
|
+
getLiveState(): T {
|
|
321
|
+
return this.state;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Update state directly (merges with existing state)
|
|
326
|
+
*/
|
|
327
|
+
updateState(patch: Partial<T>): void {
|
|
328
|
+
Object.assign(this.state, patch);
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Context - Cross-module communication and state access
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { HypenModuleInstance } from "./app.js";
|
|
6
|
+
import { getStateSnapshot } from "./state.js";
|
|
7
|
+
import { TypedEventEmitter, type HypenFrameworkEvents } from "./events.js";
|
|
8
|
+
|
|
9
|
+
export type ModuleReference<T = unknown> = {
|
|
10
|
+
state: T; // Live proxy state
|
|
11
|
+
setState: (patch: Partial<T>) => void;
|
|
12
|
+
getState: () => T; // Snapshot
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Use TypedEventEmitter from events.ts for type-safe events
|
|
17
|
+
*/
|
|
18
|
+
export type EventHandler = (payload?: unknown) => void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Global Context - Provides access to all modules and cross-module communication
|
|
22
|
+
*
|
|
23
|
+
* @template TEvents - Custom event map (extends HypenFrameworkEvents)
|
|
24
|
+
*/
|
|
25
|
+
export class HypenGlobalContext<TEvents extends Record<string, unknown> = HypenFrameworkEvents> {
|
|
26
|
+
private modules = new Map<string, HypenModuleInstance>();
|
|
27
|
+
private typedEvents: TypedEventEmitter<TEvents>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Legacy event bus for backward compatibility
|
|
31
|
+
*/
|
|
32
|
+
private legacyEventBus = new Map<string, Set<EventHandler>>();
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
this.typedEvents = new TypedEventEmitter<TEvents>();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the typed event emitter for type-safe event handling
|
|
40
|
+
*/
|
|
41
|
+
get events(): TypedEventEmitter<TEvents> {
|
|
42
|
+
return this.typedEvents;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a module instance with an ID
|
|
47
|
+
*/
|
|
48
|
+
registerModule(id: string, instance: HypenModuleInstance) {
|
|
49
|
+
if (this.modules.has(id)) {
|
|
50
|
+
console.warn(`Module "${id}" is already registered. Overwriting.`);
|
|
51
|
+
}
|
|
52
|
+
this.modules.set(id, instance);
|
|
53
|
+
console.log(`Registered module: ${id}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Unregister a module
|
|
58
|
+
*/
|
|
59
|
+
unregisterModule(id: string) {
|
|
60
|
+
this.modules.delete(id);
|
|
61
|
+
console.log(`Unregistered module: ${id}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get a module by ID with type safety
|
|
66
|
+
*/
|
|
67
|
+
getModule<T = unknown>(id: string): ModuleReference<T> {
|
|
68
|
+
const module = this.modules.get(id);
|
|
69
|
+
if (!module) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Module "${id}" not found. Available modules: ${Array.from(this.modules.keys()).join(", ")}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
state: module.getLiveState() as T,
|
|
77
|
+
setState: (patch: Partial<T>) => module.updateState(patch),
|
|
78
|
+
getState: () => module.getState() as T,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if a module exists
|
|
84
|
+
*/
|
|
85
|
+
hasModule(id: string): boolean {
|
|
86
|
+
return this.modules.has(id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all registered module IDs
|
|
91
|
+
*/
|
|
92
|
+
getModuleIds(): string[] {
|
|
93
|
+
return Array.from(this.modules.keys());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the entire app state tree (snapshot)
|
|
98
|
+
*/
|
|
99
|
+
getGlobalState(): Record<string, unknown> {
|
|
100
|
+
const state: Record<string, unknown> = {};
|
|
101
|
+
this.modules.forEach((module, id) => {
|
|
102
|
+
state[id] = module.getState();
|
|
103
|
+
});
|
|
104
|
+
return state;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Emit an event to the event bus (legacy API for backward compatibility)
|
|
109
|
+
* @deprecated Use `context.events.emit()` for type-safe events
|
|
110
|
+
*/
|
|
111
|
+
emit(event: string, payload?: unknown): void {
|
|
112
|
+
const handlers = this.legacyEventBus.get(event);
|
|
113
|
+
if (!handlers || handlers.size === 0) {
|
|
114
|
+
console.log(`Event "${event}" emitted but no listeners`);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`Emitting event: ${event}`, payload);
|
|
117
|
+
handlers.forEach((handler) => {
|
|
118
|
+
try {
|
|
119
|
+
handler(payload);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`Error in event handler for "${event}":`, error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Also emit to typed event system if the event is registered there
|
|
127
|
+
if (this.typedEvents.listenerCount(event as keyof TEvents) > 0) {
|
|
128
|
+
this.typedEvents.emit(event as keyof TEvents, payload as TEvents[keyof TEvents]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Subscribe to an event (legacy API for backward compatibility)
|
|
134
|
+
* @deprecated Use `context.events.on()` for type-safe events
|
|
135
|
+
*/
|
|
136
|
+
on(event: string, handler: EventHandler): () => void {
|
|
137
|
+
if (!this.legacyEventBus.has(event)) {
|
|
138
|
+
this.legacyEventBus.set(event, new Set());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handlers = this.legacyEventBus.get(event)!;
|
|
142
|
+
handlers.add(handler);
|
|
143
|
+
|
|
144
|
+
console.log(`Listening to event: ${event}`);
|
|
145
|
+
|
|
146
|
+
// Return unsubscribe function
|
|
147
|
+
return () => {
|
|
148
|
+
handlers.delete(handler);
|
|
149
|
+
if (handlers.size === 0) {
|
|
150
|
+
this.legacyEventBus.delete(event);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Unsubscribe from an event (legacy API)
|
|
157
|
+
* @deprecated Use the unsubscribe function returned by `context.events.on()`
|
|
158
|
+
*/
|
|
159
|
+
off(event: string, handler: EventHandler): void {
|
|
160
|
+
const handlers = this.legacyEventBus.get(event);
|
|
161
|
+
if (handlers) {
|
|
162
|
+
handlers.delete(handler);
|
|
163
|
+
if (handlers.size === 0) {
|
|
164
|
+
this.legacyEventBus.delete(event);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Clear all event listeners for an event (legacy API)
|
|
171
|
+
* @deprecated Use `context.events.removeAllListeners()`
|
|
172
|
+
*/
|
|
173
|
+
clearEvent(event: string): void {
|
|
174
|
+
this.legacyEventBus.delete(event);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Clear all event listeners (legacy API)
|
|
179
|
+
* @deprecated Use `context.events.clearAll()`
|
|
180
|
+
*/
|
|
181
|
+
clearAllEvents(): void {
|
|
182
|
+
this.legacyEventBus.clear();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get debug info about the context
|
|
187
|
+
*/
|
|
188
|
+
debug(): {
|
|
189
|
+
modules: string[];
|
|
190
|
+
events: string[];
|
|
191
|
+
typedEvents: Array<keyof TEvents>;
|
|
192
|
+
state: Record<string, unknown>;
|
|
193
|
+
} {
|
|
194
|
+
return {
|
|
195
|
+
modules: this.getModuleIds(),
|
|
196
|
+
events: Array.from(this.legacyEventBus.keys()),
|
|
197
|
+
typedEvents: this.typedEvents.eventNames(),
|
|
198
|
+
state: this.getGlobalState(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|