@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.
Files changed (49) hide show
  1. package/dist/chunk-5va59f7m.js +22 -0
  2. package/dist/chunk-5va59f7m.js.map +9 -0
  3. package/dist/engine.d.ts +101 -0
  4. package/dist/events.d.ts +78 -0
  5. package/dist/index.browser.d.ts +13 -0
  6. package/dist/index.d.ts +33 -0
  7. package/dist/remote/index.d.ts +6 -0
  8. package/dist/router.d.ts +93 -0
  9. package/dist/src/app.js +160 -0
  10. package/dist/src/app.js.map +10 -0
  11. package/dist/src/context.js +114 -0
  12. package/dist/src/context.js.map +10 -0
  13. package/dist/src/engine.browser.js +130 -0
  14. package/dist/src/engine.browser.js.map +10 -0
  15. package/dist/src/engine.js +101 -0
  16. package/dist/src/engine.js.map +10 -0
  17. package/dist/src/events.js +72 -0
  18. package/dist/src/events.js.map +10 -0
  19. package/dist/src/index.browser.js +51 -0
  20. package/dist/src/index.browser.js.map +9 -0
  21. package/dist/src/index.js +55 -0
  22. package/dist/src/index.js.map +9 -0
  23. package/dist/src/remote/client.js +176 -0
  24. package/dist/src/remote/client.js.map +10 -0
  25. package/dist/src/remote/index.js +9 -0
  26. package/dist/src/remote/index.js.map +9 -0
  27. package/dist/src/remote/types.js +2 -0
  28. package/dist/src/remote/types.js.map +9 -0
  29. package/dist/src/renderer.js +58 -0
  30. package/dist/src/renderer.js.map +10 -0
  31. package/dist/src/router.js +189 -0
  32. package/dist/src/router.js.map +10 -0
  33. package/dist/src/state.js +226 -0
  34. package/dist/src/state.js.map +10 -0
  35. package/dist/state.d.ts +30 -0
  36. package/package.json +124 -0
  37. package/src/app.ts +330 -0
  38. package/src/context.ts +201 -0
  39. package/src/engine.browser.ts +245 -0
  40. package/src/engine.ts +208 -0
  41. package/src/events.ts +126 -0
  42. package/src/index.browser.ts +104 -0
  43. package/src/index.ts +126 -0
  44. package/src/remote/client.ts +274 -0
  45. package/src/remote/index.ts +17 -0
  46. package/src/remote/types.ts +51 -0
  47. package/src/renderer.ts +102 -0
  48. package/src/router.ts +311 -0
  49. 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
+ }