@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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-compatible wrapper around the WASM engine
|
|
3
|
+
* Uses web target for browser environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Dynamic import path - will be configured at build time
|
|
7
|
+
// For browser, the WASM needs to be served and initialized explicitly
|
|
8
|
+
let wasmInit: ((path?: string) => Promise<void>) | null = null;
|
|
9
|
+
let WasmEngineClass: any = null;
|
|
10
|
+
|
|
11
|
+
export type Patch = {
|
|
12
|
+
type: "create" | "setProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent";
|
|
13
|
+
id?: string;
|
|
14
|
+
element_type?: string;
|
|
15
|
+
props?: Record<string, any> | Map<string, any>;
|
|
16
|
+
name?: string;
|
|
17
|
+
value?: any;
|
|
18
|
+
text?: string;
|
|
19
|
+
parent_id?: string;
|
|
20
|
+
before_id?: string;
|
|
21
|
+
event_name?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type Action = {
|
|
25
|
+
name: string;
|
|
26
|
+
payload?: any;
|
|
27
|
+
sender?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RenderCallback = (patches: Patch[]) => void;
|
|
31
|
+
export type ActionHandler = (action: Action) => void | Promise<void>;
|
|
32
|
+
|
|
33
|
+
export type ResolvedComponent = {
|
|
34
|
+
source: string;
|
|
35
|
+
path: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ComponentResolver = (
|
|
39
|
+
componentName: string,
|
|
40
|
+
contextPath: string | null
|
|
41
|
+
) => ResolvedComponent | null;
|
|
42
|
+
|
|
43
|
+
export interface EngineInitOptions {
|
|
44
|
+
/** Path to the WASM file (default: "/hypen_engine_bg.wasm") */
|
|
45
|
+
wasmPath?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Recursively convert Maps and nested structures to plain objects
|
|
50
|
+
*/
|
|
51
|
+
function mapToObject(value: any): any {
|
|
52
|
+
if (value instanceof Map) {
|
|
53
|
+
const obj: Record<string, any> = {};
|
|
54
|
+
for (const [key, val] of value.entries()) {
|
|
55
|
+
obj[key] = mapToObject(val);
|
|
56
|
+
}
|
|
57
|
+
return obj;
|
|
58
|
+
} else if (Array.isArray(value)) {
|
|
59
|
+
return value.map(mapToObject);
|
|
60
|
+
} else if (value && typeof value === 'object' && value.constructor === Object) {
|
|
61
|
+
const obj: Record<string, any> = {};
|
|
62
|
+
for (const [key, val] of Object.entries(value)) {
|
|
63
|
+
obj[key] = mapToObject(val);
|
|
64
|
+
}
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Engine wraps the WASM engine and provides a TypeScript-friendly API
|
|
72
|
+
* Browser version with explicit WASM initialization
|
|
73
|
+
*/
|
|
74
|
+
export class Engine {
|
|
75
|
+
private wasmEngine: any = null;
|
|
76
|
+
private initialized = false;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize the WASM module
|
|
80
|
+
* @param options - Initialization options including wasmPath
|
|
81
|
+
*/
|
|
82
|
+
async init(options: EngineInitOptions = {}): Promise<void> {
|
|
83
|
+
if (this.initialized) return;
|
|
84
|
+
|
|
85
|
+
const wasmPath = options.wasmPath ?? "/hypen_engine_bg.wasm";
|
|
86
|
+
|
|
87
|
+
// Dynamically import the WASM module
|
|
88
|
+
// This allows the path to be configured at runtime
|
|
89
|
+
try {
|
|
90
|
+
// @ts-expect-error WASM module is generated at build time
|
|
91
|
+
const wasmModule = await import("../wasm/hypen_engine.js");
|
|
92
|
+
wasmInit = wasmModule.default;
|
|
93
|
+
WasmEngineClass = wasmModule.WasmEngine;
|
|
94
|
+
|
|
95
|
+
// Initialize WASM with explicit path
|
|
96
|
+
await wasmInit!(wasmPath);
|
|
97
|
+
|
|
98
|
+
this.wasmEngine = new WasmEngineClass();
|
|
99
|
+
this.initialized = true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("[Hypen] Failed to initialize WASM engine:", error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Ensure the engine is initialized before operations
|
|
108
|
+
*/
|
|
109
|
+
private ensureInitialized(): any {
|
|
110
|
+
if (!this.wasmEngine) {
|
|
111
|
+
throw new Error("Engine not initialized. Call init() first.");
|
|
112
|
+
}
|
|
113
|
+
return this.wasmEngine;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Set the render callback that receives patches
|
|
118
|
+
*/
|
|
119
|
+
setRenderCallback(callback: RenderCallback): void {
|
|
120
|
+
const engine = this.ensureInitialized();
|
|
121
|
+
engine.setRenderCallback((patches: Patch[]) => {
|
|
122
|
+
callback(patches);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Set the component resolver for dynamic component composition
|
|
128
|
+
*/
|
|
129
|
+
setComponentResolver(resolver: ComponentResolver): void {
|
|
130
|
+
const engine = this.ensureInitialized();
|
|
131
|
+
engine.setComponentResolver((componentName: string, contextPath: string | null) => {
|
|
132
|
+
const result = resolver(componentName, contextPath);
|
|
133
|
+
return result;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse and render Hypen DSL source code
|
|
139
|
+
*/
|
|
140
|
+
renderSource(source: string): void {
|
|
141
|
+
const engine = this.ensureInitialized();
|
|
142
|
+
engine.renderSource(source);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Render a lazy component (for lazy route loading)
|
|
147
|
+
*/
|
|
148
|
+
renderLazyComponent(source: string): void {
|
|
149
|
+
const engine = this.ensureInitialized();
|
|
150
|
+
engine.renderLazyComponent(source);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Render a component into a specific parent node (subtree rendering)
|
|
155
|
+
*/
|
|
156
|
+
renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
|
|
157
|
+
const engine = this.ensureInitialized();
|
|
158
|
+
const safeState = JSON.parse(JSON.stringify(state));
|
|
159
|
+
engine.renderInto(source, parentNodeId, safeState);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Notify the engine of state changes
|
|
164
|
+
*/
|
|
165
|
+
notifyStateChange(paths: string[], currentState: Record<string, any>): void {
|
|
166
|
+
const engine = this.ensureInitialized();
|
|
167
|
+
|
|
168
|
+
const plainObject = JSON.parse(JSON.stringify(currentState));
|
|
169
|
+
engine.updateState(plainObject);
|
|
170
|
+
|
|
171
|
+
if (paths.length > 0) {
|
|
172
|
+
console.debug("[Hypen] State changed:", paths);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Update state (triggers re-render of affected nodes)
|
|
178
|
+
* @deprecated Use notifyStateChange instead
|
|
179
|
+
*/
|
|
180
|
+
updateState(statePatch: Record<string, any>): void {
|
|
181
|
+
const engine = this.ensureInitialized();
|
|
182
|
+
const plainObject = JSON.parse(JSON.stringify(statePatch));
|
|
183
|
+
engine.updateState(plainObject);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Dispatch an action
|
|
188
|
+
*/
|
|
189
|
+
dispatchAction(name: string, payload?: any): void {
|
|
190
|
+
const engine = this.ensureInitialized();
|
|
191
|
+
console.log(`[Engine] Action dispatched: ${name}`);
|
|
192
|
+
engine.dispatchAction(name, payload ?? null);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Register an action handler
|
|
197
|
+
*/
|
|
198
|
+
onAction(actionName: string, handler: ActionHandler): void {
|
|
199
|
+
const engine = this.ensureInitialized();
|
|
200
|
+
engine.onAction(actionName, (action: Action) => {
|
|
201
|
+
const normalizedAction: Action = {
|
|
202
|
+
...action,
|
|
203
|
+
payload: action.payload ? mapToObject(action.payload) : action.payload,
|
|
204
|
+
};
|
|
205
|
+
Promise.resolve(handler(normalizedAction)).catch(console.error);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Initialize a module
|
|
211
|
+
*/
|
|
212
|
+
setModule(
|
|
213
|
+
name: string,
|
|
214
|
+
actions: string[],
|
|
215
|
+
stateKeys: string[],
|
|
216
|
+
initialState: Record<string, any>
|
|
217
|
+
): void {
|
|
218
|
+
const engine = this.ensureInitialized();
|
|
219
|
+
engine.setModule(name, actions, stateKeys, initialState);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get the current revision number
|
|
224
|
+
*/
|
|
225
|
+
getRevision(): bigint {
|
|
226
|
+
const engine = this.ensureInitialized();
|
|
227
|
+
return engine.getRevision();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Clear the engine tree
|
|
232
|
+
*/
|
|
233
|
+
clearTree(): void {
|
|
234
|
+
const engine = this.ensureInitialized();
|
|
235
|
+
engine.clearTree();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Debug method to inspect parsed components
|
|
240
|
+
*/
|
|
241
|
+
debugParseComponent(source: string): string {
|
|
242
|
+
const engine = this.ensureInitialized();
|
|
243
|
+
return engine.debugParseComponent(source);
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level wrapper around the WASM engine
|
|
3
|
+
* Node.js / Bundler target
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// @ts-expect-error WASM module is generated at build time
|
|
7
|
+
import { WasmEngine } from "../wasm/hypen_engine.js";
|
|
8
|
+
|
|
9
|
+
export type Patch = {
|
|
10
|
+
type: "create" | "setProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent";
|
|
11
|
+
id?: string;
|
|
12
|
+
elementType?: string;
|
|
13
|
+
props?: Record<string, any>;
|
|
14
|
+
name?: string;
|
|
15
|
+
value?: any;
|
|
16
|
+
text?: string;
|
|
17
|
+
parentId?: string;
|
|
18
|
+
beforeId?: string;
|
|
19
|
+
eventName?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type Action = {
|
|
23
|
+
name: string;
|
|
24
|
+
payload?: any;
|
|
25
|
+
sender?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type RenderCallback = (patches: Patch[]) => void;
|
|
29
|
+
export type ActionHandler = (action: Action) => void | Promise<void>;
|
|
30
|
+
|
|
31
|
+
export type ResolvedComponent = {
|
|
32
|
+
source: string;
|
|
33
|
+
path: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ComponentResolver = (
|
|
37
|
+
componentName: string,
|
|
38
|
+
contextPath: string | null
|
|
39
|
+
) => ResolvedComponent | null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Engine wraps the WASM engine and provides a TypeScript-friendly API
|
|
43
|
+
*/
|
|
44
|
+
export class Engine {
|
|
45
|
+
private wasmEngine: WasmEngine | null = null;
|
|
46
|
+
private initialized = false;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the WASM module
|
|
50
|
+
*/
|
|
51
|
+
async init(): Promise<void> {
|
|
52
|
+
if (this.initialized) return;
|
|
53
|
+
|
|
54
|
+
// For bundler target, WASM is auto-initialized
|
|
55
|
+
this.wasmEngine = new WasmEngine();
|
|
56
|
+
this.initialized = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Ensure the engine is initialized before operations
|
|
61
|
+
*/
|
|
62
|
+
private ensureInitialized(): WasmEngine {
|
|
63
|
+
if (!this.wasmEngine) {
|
|
64
|
+
throw new Error("Engine not initialized. Call init() first.");
|
|
65
|
+
}
|
|
66
|
+
return this.wasmEngine;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the render callback that receives patches
|
|
71
|
+
*/
|
|
72
|
+
setRenderCallback(callback: RenderCallback): void {
|
|
73
|
+
const engine = this.ensureInitialized();
|
|
74
|
+
engine.setRenderCallback((patches: Patch[]) => {
|
|
75
|
+
callback(patches);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set the component resolver for dynamic component composition
|
|
81
|
+
*/
|
|
82
|
+
setComponentResolver(resolver: ComponentResolver): void {
|
|
83
|
+
const engine = this.ensureInitialized();
|
|
84
|
+
engine.setComponentResolver((componentName: string, contextPath: string | null) => {
|
|
85
|
+
const result = resolver(componentName, contextPath);
|
|
86
|
+
return result;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse and render Hypen DSL source code
|
|
92
|
+
*/
|
|
93
|
+
renderSource(source: string): void {
|
|
94
|
+
const engine = this.ensureInitialized();
|
|
95
|
+
engine.renderSource(source);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Render a lazy component (for lazy route loading)
|
|
100
|
+
*/
|
|
101
|
+
renderLazyComponent(source: string): void {
|
|
102
|
+
const engine = this.ensureInitialized();
|
|
103
|
+
engine.renderLazyComponent(source);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render a component into a specific parent node (subtree rendering)
|
|
108
|
+
*/
|
|
109
|
+
renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
|
|
110
|
+
const engine = this.ensureInitialized();
|
|
111
|
+
const safeState = JSON.parse(JSON.stringify(state));
|
|
112
|
+
engine.renderInto(source, parentNodeId, safeState);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Notify the engine of state changes using sparse updates
|
|
117
|
+
*/
|
|
118
|
+
notifyStateChange(paths: string[], values: Record<string, any>): void {
|
|
119
|
+
const engine = this.ensureInitialized();
|
|
120
|
+
|
|
121
|
+
if (paths.length === 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
engine.updateStateSparse(paths, values);
|
|
126
|
+
console.debug("[Hypen] State changed (sparse):", paths);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Notify the engine of state changes using full state replacement
|
|
131
|
+
* @deprecated Use notifyStateChange with sparse values instead
|
|
132
|
+
*/
|
|
133
|
+
notifyStateChangeFull(paths: string[], currentState: Record<string, any>): void {
|
|
134
|
+
const engine = this.ensureInitialized();
|
|
135
|
+
|
|
136
|
+
const plainObject = JSON.parse(JSON.stringify(currentState));
|
|
137
|
+
engine.updateState(plainObject);
|
|
138
|
+
|
|
139
|
+
if (paths.length > 0) {
|
|
140
|
+
console.debug("[Hypen] State changed (full):", paths);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Update state (triggers re-render of affected nodes)
|
|
146
|
+
* @deprecated Use notifyStateChange instead
|
|
147
|
+
*/
|
|
148
|
+
updateState(statePatch: Record<string, any>): void {
|
|
149
|
+
const engine = this.ensureInitialized();
|
|
150
|
+
const plainObject = JSON.parse(JSON.stringify(statePatch));
|
|
151
|
+
engine.updateState(plainObject);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Dispatch an action
|
|
156
|
+
*/
|
|
157
|
+
dispatchAction(name: string, payload?: any): void {
|
|
158
|
+
const engine = this.ensureInitialized();
|
|
159
|
+
engine.dispatchAction(name, payload ?? null);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Register an action handler
|
|
164
|
+
*/
|
|
165
|
+
onAction(actionName: string, handler: ActionHandler): void {
|
|
166
|
+
const engine = this.ensureInitialized();
|
|
167
|
+
engine.onAction(actionName, (action: Action) => {
|
|
168
|
+
Promise.resolve(handler(action)).catch(console.error);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Initialize a module
|
|
174
|
+
*/
|
|
175
|
+
setModule(
|
|
176
|
+
name: string,
|
|
177
|
+
actions: string[],
|
|
178
|
+
stateKeys: string[],
|
|
179
|
+
initialState: Record<string, any>
|
|
180
|
+
): void {
|
|
181
|
+
const engine = this.ensureInitialized();
|
|
182
|
+
engine.setModule(name, actions, stateKeys, initialState);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the current revision number
|
|
187
|
+
*/
|
|
188
|
+
getRevision(): number {
|
|
189
|
+
const engine = this.ensureInitialized();
|
|
190
|
+
return engine.getRevision();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Clear the engine tree
|
|
195
|
+
*/
|
|
196
|
+
clearTree(): void {
|
|
197
|
+
const engine = this.ensureInitialized();
|
|
198
|
+
engine.clearTree();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Debug method to inspect parsed components
|
|
203
|
+
*/
|
|
204
|
+
debugParseComponent(source: string): string {
|
|
205
|
+
const engine = this.ensureInitialized();
|
|
206
|
+
return engine.debugParseComponent(source);
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe Event System
|
|
3
|
+
*
|
|
4
|
+
* Provides strongly-typed event emission and subscription with autocomplete support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type EventHandler<T = unknown> = (payload: T) => void;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type-safe event emitter with generics
|
|
11
|
+
*/
|
|
12
|
+
export class TypedEventEmitter<TEvents extends Record<string, unknown> = Record<string, unknown>> {
|
|
13
|
+
private eventBus = new Map<keyof TEvents, Set<EventHandler<any>>>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Emit an event with type-safe payload
|
|
17
|
+
*/
|
|
18
|
+
emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
|
|
19
|
+
const handlers = this.eventBus.get(event);
|
|
20
|
+
if (!handlers || handlers.size === 0) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
handlers.forEach((handler) => {
|
|
25
|
+
try {
|
|
26
|
+
handler(payload);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(`Error in event handler for "${String(event)}":`, error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to an event with type-safe payload
|
|
35
|
+
* Returns an unsubscribe function
|
|
36
|
+
*/
|
|
37
|
+
on<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): () => void {
|
|
38
|
+
if (!this.eventBus.has(event)) {
|
|
39
|
+
this.eventBus.set(event, new Set());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handlers = this.eventBus.get(event)!;
|
|
43
|
+
handlers.add(handler);
|
|
44
|
+
|
|
45
|
+
// Return unsubscribe function
|
|
46
|
+
return () => {
|
|
47
|
+
handlers.delete(handler);
|
|
48
|
+
if (handlers.size === 0) {
|
|
49
|
+
this.eventBus.delete(event);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Subscribe to an event once (auto-unsubscribe after first emit)
|
|
56
|
+
*/
|
|
57
|
+
once<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): () => void {
|
|
58
|
+
const wrappedHandler = (payload: TEvents[K]) => {
|
|
59
|
+
handler(payload);
|
|
60
|
+
unsubscribe();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const unsubscribe = this.on(event, wrappedHandler);
|
|
64
|
+
return unsubscribe;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Unsubscribe a specific handler from an event
|
|
69
|
+
*/
|
|
70
|
+
off<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void {
|
|
71
|
+
const handlers = this.eventBus.get(event);
|
|
72
|
+
if (handlers) {
|
|
73
|
+
handlers.delete(handler);
|
|
74
|
+
if (handlers.size === 0) {
|
|
75
|
+
this.eventBus.delete(event);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Remove all listeners for a specific event
|
|
82
|
+
*/
|
|
83
|
+
removeAllListeners<K extends keyof TEvents>(event: K): void {
|
|
84
|
+
this.eventBus.delete(event);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove all listeners for all events
|
|
89
|
+
*/
|
|
90
|
+
clearAll(): void {
|
|
91
|
+
this.eventBus.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the number of listeners for an event
|
|
96
|
+
*/
|
|
97
|
+
listenerCount<K extends keyof TEvents>(event: K): number {
|
|
98
|
+
return this.eventBus.get(event)?.size ?? 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all registered event names
|
|
103
|
+
*/
|
|
104
|
+
eventNames(): Array<keyof TEvents> {
|
|
105
|
+
return Array.from(this.eventBus.keys());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Default events for Hypen framework (can be extended by users)
|
|
111
|
+
*/
|
|
112
|
+
export type HypenFrameworkEvents = {
|
|
113
|
+
'module:created': { moduleId: string };
|
|
114
|
+
'module:destroyed': { moduleId: string };
|
|
115
|
+
'route:changed': { from: string | null; to: string };
|
|
116
|
+
'state:updated': { moduleId: string; paths: string[] };
|
|
117
|
+
'action:dispatched': { moduleId: string; actionName: string; payload?: unknown };
|
|
118
|
+
'error': { message: string; error?: Error; context?: string };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a typed event emitter with custom event map
|
|
123
|
+
*/
|
|
124
|
+
export function createEventEmitter<TEvents extends Record<string, unknown> = HypenFrameworkEvents>(): TypedEventEmitter<TEvents> {
|
|
125
|
+
return new TypedEventEmitter<TEvents>();
|
|
126
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hypen/core - Browser Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This entry point uses the browser-compatible engine with explicit WASM initialization.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// ENGINE API (Browser version)
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export { Engine } from "./engine.browser.js";
|
|
12
|
+
export type {
|
|
13
|
+
Patch,
|
|
14
|
+
Action,
|
|
15
|
+
RenderCallback,
|
|
16
|
+
ActionHandler as EngineActionHandler,
|
|
17
|
+
ResolvedComponent,
|
|
18
|
+
ComponentResolver,
|
|
19
|
+
EngineInitOptions,
|
|
20
|
+
} from "./engine.browser.js";
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// APP / MODULE SYSTEM
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export { app, HypenApp, HypenAppBuilder, HypenModuleInstance } from "./app.js";
|
|
27
|
+
export type {
|
|
28
|
+
IEngine,
|
|
29
|
+
RouterContext,
|
|
30
|
+
ActionContext,
|
|
31
|
+
ActionNext,
|
|
32
|
+
GlobalContext,
|
|
33
|
+
LifecycleHandler,
|
|
34
|
+
ActionHandlerContext,
|
|
35
|
+
ActionHandler,
|
|
36
|
+
HypenModuleDefinition,
|
|
37
|
+
HypenModule,
|
|
38
|
+
} from "./app.js";
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// STATE MANAGEMENT
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
createObservableState,
|
|
46
|
+
batchStateUpdates,
|
|
47
|
+
getStateSnapshot,
|
|
48
|
+
} from "./state.js";
|
|
49
|
+
export type {
|
|
50
|
+
StatePath,
|
|
51
|
+
StateChange,
|
|
52
|
+
StateObserverOptions,
|
|
53
|
+
} from "./state.js";
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// RENDERER ABSTRACTION
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
export { BaseRenderer, ConsoleRenderer } from "./renderer.js";
|
|
60
|
+
export type { Renderer } from "./renderer.js";
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// ROUTING
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
export { HypenRouter } from "./router.js";
|
|
67
|
+
export type {
|
|
68
|
+
RouteMatch,
|
|
69
|
+
RouteState,
|
|
70
|
+
RouteChangeCallback,
|
|
71
|
+
} from "./router.js";
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// EVENTS
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
export { TypedEventEmitter, createEventEmitter } from "./events.js";
|
|
78
|
+
export type { EventHandler, HypenFrameworkEvents } from "./events.js";
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// GLOBAL CONTEXT
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
export { HypenGlobalContext } from "./context.js";
|
|
85
|
+
export type { ModuleReference } from "./context.js";
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// REMOTE UI
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
export { RemoteEngine } from "./remote/client.js";
|
|
92
|
+
export type {
|
|
93
|
+
RemoteMessage,
|
|
94
|
+
InitialTreeMessage,
|
|
95
|
+
PatchMessage,
|
|
96
|
+
DispatchActionMessage,
|
|
97
|
+
StateUpdateMessage,
|
|
98
|
+
RemoteClient,
|
|
99
|
+
RemoteServerConfig,
|
|
100
|
+
} from "./remote/types.js";
|
|
101
|
+
export type {
|
|
102
|
+
RemoteConnectionState,
|
|
103
|
+
RemoteEngineOptions,
|
|
104
|
+
} from "./remote/client.js";
|