@hypen-space/core 0.4.2 → 0.4.5

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 (66) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +379 -0
  3. package/dist/app.js +111 -18
  4. package/dist/app.js.map +4 -4
  5. package/dist/components/builtin.d.ts +50 -0
  6. package/dist/components/builtin.js +114 -21
  7. package/dist/components/builtin.js.map +5 -5
  8. package/dist/context.d.ts +90 -0
  9. package/dist/discovery.d.ts +90 -0
  10. package/dist/discovery.js +121 -23
  11. package/dist/discovery.js.map +5 -5
  12. package/dist/disposable.d.ts +122 -0
  13. package/dist/engine.browser.d.ts +101 -0
  14. package/dist/engine.browser.js +192 -5
  15. package/dist/engine.browser.js.map +5 -4
  16. package/dist/engine.d.ts +85 -0
  17. package/dist/engine.js +192 -5
  18. package/dist/engine.js.map +5 -4
  19. package/dist/events.d.ts +78 -0
  20. package/dist/hypen.d.ts +127 -0
  21. package/dist/index.browser.d.ts +25 -0
  22. package/dist/index.browser.js +116 -19
  23. package/dist/index.browser.js.map +6 -6
  24. package/dist/index.d.ts +76 -0
  25. package/dist/index.js +373 -1235
  26. package/dist/index.js.map +10 -15
  27. package/dist/loader.d.ts +51 -0
  28. package/dist/logger.d.ts +141 -0
  29. package/dist/managed-router.d.ts +59 -0
  30. package/dist/plugin.d.ts +39 -0
  31. package/dist/remote/client.d.ts +154 -0
  32. package/dist/remote/client.js +34 -2
  33. package/dist/remote/client.js.map +3 -3
  34. package/dist/remote/index.d.ts +13 -0
  35. package/dist/remote/index.js +472 -26
  36. package/dist/remote/index.js.map +8 -7
  37. package/dist/remote/server.d.ts +173 -0
  38. package/dist/remote/server.js +472 -26
  39. package/dist/remote/server.js.map +8 -7
  40. package/dist/remote/session.d.ts +115 -0
  41. package/dist/remote/types.d.ts +98 -0
  42. package/dist/renderer.d.ts +54 -0
  43. package/dist/renderer.js +6 -2
  44. package/dist/renderer.js.map +3 -3
  45. package/dist/resolver.d.ts +95 -0
  46. package/dist/result.d.ts +134 -0
  47. package/dist/retry.d.ts +150 -0
  48. package/dist/router.d.ts +94 -0
  49. package/dist/state.d.ts +42 -0
  50. package/dist/types.d.ts +30 -0
  51. package/package.json +2 -1
  52. package/src/app.ts +162 -41
  53. package/src/components/builtin.ts +3 -4
  54. package/src/discovery.ts +2 -2
  55. package/src/engine.browser.ts +27 -4
  56. package/src/engine.ts +27 -4
  57. package/src/index.browser.ts +0 -2
  58. package/src/index.ts +3 -2
  59. package/src/managed-router.ts +5 -6
  60. package/src/remote/server.ts +116 -21
  61. package/src/result.ts +48 -0
  62. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  63. package/wasm-browser/package.json +1 -1
  64. package/wasm-node/hypen_engine_bg.wasm +0 -0
  65. package/wasm-node/package.json +5 -3
  66. package/src/module-registry.ts +0 -76
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Retry Utility for Network Operations
3
+ *
4
+ * Provides configurable retry logic with exponential/linear backoff
5
+ * for handling transient failures in network operations.
6
+ */
7
+ import { type Result } from "./result.js";
8
+ /**
9
+ * Options for retry behavior
10
+ */
11
+ export interface RetryOptions {
12
+ /** Maximum number of attempts (default: 3) */
13
+ maxAttempts?: number;
14
+ /** Initial delay in milliseconds (default: 1000) */
15
+ delayMs?: number;
16
+ /** Backoff strategy (default: 'exponential') */
17
+ backoff?: "linear" | "exponential" | "none";
18
+ /** Maximum delay cap in milliseconds (default: 30000) */
19
+ maxDelayMs?: number;
20
+ /** Jitter factor 0-1 to randomize delays (default: 0.1) */
21
+ jitter?: number;
22
+ /** Callback on each retry attempt */
23
+ onRetry?: (attempt: number, error: Error, nextDelayMs: number) => void;
24
+ /** Optional predicate to determine if error is retryable */
25
+ shouldRetry?: (error: Error) => boolean;
26
+ /** AbortSignal for cancellation */
27
+ signal?: AbortSignal;
28
+ }
29
+ /**
30
+ * Retry a function with configurable backoff
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * // Basic usage
35
+ * const result = await retry(() => fetch('/api/data'));
36
+ *
37
+ * // With options
38
+ * const result = await retry(
39
+ * () => fetch('/api/data'),
40
+ * {
41
+ * maxAttempts: 5,
42
+ * delayMs: 2000,
43
+ * backoff: 'exponential',
44
+ * onRetry: (n, err) => console.log(`Attempt ${n} failed: ${err.message}`)
45
+ * }
46
+ * );
47
+ * ```
48
+ */
49
+ export declare function retry<T>(fn: () => T | Promise<T>, options?: RetryOptions): Promise<T>;
50
+ /**
51
+ * Retry a function, returning a Result instead of throwing
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const result = await retryResult(() => fetch('/api/data'));
56
+ * if (result.ok) {
57
+ * console.log('Success:', result.value);
58
+ * } else {
59
+ * console.error('All retries failed:', result.error);
60
+ * }
61
+ * ```
62
+ */
63
+ export declare function retryResult<T>(fn: () => T | Promise<T>, options?: RetryOptions): Promise<Result<T, Error>>;
64
+ /**
65
+ * Create a retryable version of a function
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const fetchWithRetry = withRetry(
70
+ * (url: string) => fetch(url),
71
+ * { maxAttempts: 3 }
72
+ * );
73
+ *
74
+ * const response = await fetchWithRetry('/api/data');
75
+ * ```
76
+ */
77
+ export declare function withRetry<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn | Promise<TReturn>, options?: RetryOptions): (...args: TArgs) => Promise<TReturn>;
78
+ /**
79
+ * Predefined retry conditions
80
+ */
81
+ export declare const RetryConditions: {
82
+ /**
83
+ * Retry on network errors (fetch failures, timeouts)
84
+ */
85
+ networkErrors: (error: Error) => boolean;
86
+ /**
87
+ * Retry on specific HTTP status codes (from fetch Response)
88
+ */
89
+ httpRetryable: (error: Error & {
90
+ status?: number;
91
+ }) => boolean;
92
+ /**
93
+ * Retry on transient WebSocket errors
94
+ */
95
+ websocketErrors: (error: Error) => boolean;
96
+ /**
97
+ * Combine multiple conditions (retry if any match)
98
+ */
99
+ any: (...conditions: Array<(error: Error) => boolean>) => (error: Error) => boolean;
100
+ /**
101
+ * Combine multiple conditions (retry if all match)
102
+ */
103
+ all: (...conditions: Array<(error: Error) => boolean>) => (error: Error) => boolean;
104
+ };
105
+ /**
106
+ * Preset configurations for common use cases
107
+ */
108
+ export declare const RetryPresets: {
109
+ /**
110
+ * Aggressive retry for critical operations
111
+ */
112
+ aggressive: {
113
+ maxAttempts: number;
114
+ delayMs: number;
115
+ backoff: "exponential";
116
+ maxDelayMs: number;
117
+ jitter: number;
118
+ };
119
+ /**
120
+ * Conservative retry for non-critical operations
121
+ */
122
+ conservative: {
123
+ maxAttempts: number;
124
+ delayMs: number;
125
+ backoff: "linear";
126
+ maxDelayMs: number;
127
+ jitter: number;
128
+ };
129
+ /**
130
+ * Fast retry for local operations (short delays)
131
+ */
132
+ fast: {
133
+ maxAttempts: number;
134
+ delayMs: number;
135
+ backoff: "exponential";
136
+ maxDelayMs: number;
137
+ jitter: number;
138
+ };
139
+ /**
140
+ * WebSocket reconnection preset
141
+ */
142
+ websocket: {
143
+ maxAttempts: number;
144
+ delayMs: number;
145
+ backoff: "exponential";
146
+ maxDelayMs: number;
147
+ jitter: number;
148
+ shouldRetry: (error: Error) => boolean;
149
+ };
150
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Hypen Router - Declarative routing system
3
+ * Integrated with Hypen's reactive state management
4
+ */
5
+ export type RouteMatch = {
6
+ params: Record<string, string>;
7
+ query: Record<string, string>;
8
+ path: string;
9
+ };
10
+ export type RouteState = {
11
+ currentPath: string;
12
+ params: Record<string, string>;
13
+ query: Record<string, string>;
14
+ previousPath: string | null;
15
+ };
16
+ export type RouteChangeCallback = (route: RouteState) => void;
17
+ /**
18
+ * Hypen Router - Manages application routing with pattern matching
19
+ */
20
+ export declare class HypenRouter {
21
+ private state;
22
+ private subscribers;
23
+ private isInitialized;
24
+ private isUpdating;
25
+ constructor();
26
+ /**
27
+ * Initialize browser history sync
28
+ */
29
+ private initializeBrowserSync;
30
+ /**
31
+ * Get path from browser URL (supports both hash and pathname)
32
+ */
33
+ private getPathFromBrowser;
34
+ /**
35
+ * Parse query string from URL
36
+ */
37
+ private parseQuery;
38
+ /**
39
+ * Navigate to a new path
40
+ */
41
+ push(path: string): void;
42
+ /**
43
+ * Replace current path without adding to history
44
+ */
45
+ replace(path: string): void;
46
+ /**
47
+ * Go back in history
48
+ */
49
+ back(): void;
50
+ /**
51
+ * Go forward in history
52
+ */
53
+ forward(): void;
54
+ /**
55
+ * Update the current path
56
+ */
57
+ private updatePath;
58
+ /**
59
+ * Get current path
60
+ */
61
+ getCurrentPath(): string;
62
+ /**
63
+ * Get current route params
64
+ */
65
+ getParams(): Record<string, string>;
66
+ /**
67
+ * Get current query params
68
+ */
69
+ getQuery(): Record<string, string>;
70
+ /**
71
+ * Get full route state snapshot
72
+ */
73
+ getState(): RouteState;
74
+ /**
75
+ * Match a pattern against a path
76
+ */
77
+ matchPath(pattern: string, path: string): RouteMatch | null;
78
+ /**
79
+ * Subscribe to route changes
80
+ */
81
+ onNavigate(callback: RouteChangeCallback): () => void;
82
+ /**
83
+ * Notify all subscribers of route change
84
+ */
85
+ private notifySubscribers;
86
+ /**
87
+ * Check if a path matches the current route
88
+ */
89
+ isActive(pattern: string): boolean;
90
+ /**
91
+ * Get a URL with query params
92
+ */
93
+ buildUrl(path: string, query?: Record<string, string>): string;
94
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * State management with observer pattern and diffing
3
+ *
4
+ * Uses a proxy-based approach with self-detection to avoid
5
+ * creating nested proxies repeatedly.
6
+ */
7
+ export type StatePath = string;
8
+ /**
9
+ * Represents a change in state with full path information
10
+ */
11
+ export interface StateChange {
12
+ paths: StatePath[];
13
+ newValues: Record<StatePath, any>;
14
+ }
15
+ /**
16
+ * Options for state observer
17
+ */
18
+ export interface StateObserverOptions {
19
+ onChange: (change: StateChange) => void;
20
+ pathPrefix?: string;
21
+ }
22
+ /**
23
+ * Create an observable state object that tracks changes
24
+ */
25
+ export declare function createObservableState<T extends object>(initialState: T, options?: StateObserverOptions): T;
26
+ /**
27
+ * Helper to batch multiple state updates
28
+ */
29
+ export declare function batchStateUpdates<T>(state: T, fn: () => void): void;
30
+ /**
31
+ * Get a snapshot of the current state
32
+ */
33
+ export declare function getStateSnapshot<T>(state: T): T;
34
+ /**
35
+ * Check if a value is a Hypen state proxy
36
+ */
37
+ export declare function isStateProxy(value: unknown): boolean;
38
+ /**
39
+ * Get the raw (unwrapped) target from a proxy
40
+ * Returns the value as-is if not a proxy
41
+ */
42
+ export declare function unwrapProxy<T>(value: T): T;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Core types shared across the engine and runtime.
3
+ *
4
+ * These types are intentionally decoupled from the WASM engine so that
5
+ * packages can import them without pulling in any WASM code.
6
+ */
7
+ export type Patch = {
8
+ type: "create" | "setProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent";
9
+ id?: string;
10
+ elementType?: string;
11
+ props?: Record<string, any>;
12
+ name?: string;
13
+ value?: any;
14
+ text?: string;
15
+ parentId?: string;
16
+ beforeId?: string;
17
+ eventName?: string;
18
+ };
19
+ export type Action = {
20
+ name: string;
21
+ payload?: any;
22
+ sender?: string;
23
+ };
24
+ export type RenderCallback = (patches: Patch[]) => void;
25
+ export type ActionHandler = (action: Action) => void | Promise<void>;
26
+ export type ResolvedComponent = {
27
+ source: string;
28
+ path: string;
29
+ };
30
+ export type ComponentResolver = (componentName: string, contextPath: string | null) => ResolvedComponent | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypen-space/core",
3
- "version": "0.4.2",
3
+ "version": "0.4.5",
4
4
  "description": "Hypen core engine - Platform-agnostic reactive UI runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -152,6 +152,7 @@
152
152
  "scripts": {
153
153
  "build": "bun run build.ts",
154
154
  "build:types": "tsc --emitDeclarationOnly",
155
+ "prepublishOnly": "bun run build",
155
156
  "typecheck": "tsc --noEmit",
156
157
  "clean": "rm -rf dist"
157
158
  },
package/src/app.ts CHANGED
@@ -21,22 +21,12 @@ import { createLogger } from "./logger.js";
21
21
 
22
22
  const log = createLogger("ModuleInstance");
23
23
 
24
- export type RouterContext = {
25
- root: HypenRouter;
26
- current?: HypenRouter;
27
- parent?: HypenRouter;
28
- };
29
-
30
- export type ActionContext = {
24
+ export type ActionContext<P = unknown> = {
31
25
  name: string;
32
- payload?: unknown;
26
+ payload?: P;
33
27
  sender?: string;
34
28
  };
35
29
 
36
- export type ActionNext = {
37
- router: HypenRouter; // Direct access to router instance (from nearest parent)
38
- };
39
-
40
30
  export type GlobalContext = {
41
31
  getModule: <T = unknown>(id: string) => ModuleReference<T>;
42
32
  hasModule: (id: string) => boolean;
@@ -44,7 +34,7 @@ export type GlobalContext = {
44
34
  getGlobalState: () => Record<string, unknown>;
45
35
  emit: (event: string, payload?: unknown) => void;
46
36
  on: (event: string, handler: (payload?: unknown) => void) => () => void;
47
- __router?: unknown; // Internal: router instance for built-in Router component
37
+ router: HypenRouter | null;
48
38
  };
49
39
 
50
40
  export type LifecycleHandler<T> = (
@@ -55,17 +45,16 @@ export type LifecycleHandler<T> = (
55
45
  /**
56
46
  * Action handler context - all parameters available explicitly
57
47
  */
58
- export interface ActionHandlerContext<T> {
59
- action: ActionContext;
48
+ export interface ActionHandlerContext<T, P = unknown> {
49
+ action: ActionContext<P>;
60
50
  state: T;
61
- next: ActionNext;
62
51
  context: GlobalContext;
63
52
  }
64
53
 
65
54
  /**
66
55
  * Action handler - receives all context in a single object
67
56
  */
68
- export type ActionHandler<T> = (ctx: ActionHandlerContext<T>) => void | Promise<void>;
57
+ export type ActionHandler<T, P = unknown> = (ctx: ActionHandlerContext<T, P>) => void | Promise<void>;
69
58
 
70
59
  /**
71
60
  * Context passed to onDisconnect handler
@@ -152,7 +141,7 @@ export interface HypenModuleDefinition<T = unknown> {
152
141
  template?: string;
153
142
  handlers: {
154
143
  onCreated?: LifecycleHandler<T>;
155
- onAction: Map<string, ActionHandler<T>>;
144
+ onAction: Map<string, ActionHandler<T, any>>;
156
145
  onDestroyed?: LifecycleHandler<T>;
157
146
  /** Called when client disconnects (session persists for TTL) */
158
147
  onDisconnect?: DisconnectHandler<T>;
@@ -177,20 +166,23 @@ export class HypenAppBuilder<T> {
177
166
  private initialState: T;
178
167
  private options: { persist?: boolean; version?: number; name?: string };
179
168
  private createdHandler?: LifecycleHandler<T>;
180
- private actionHandlers: Map<string, ActionHandler<T>> = new Map();
169
+ private actionHandlers: Map<string, ActionHandler<T, any>> = new Map();
181
170
  private destroyedHandler?: LifecycleHandler<T>;
182
171
  private disconnectHandler?: DisconnectHandler<T>;
183
172
  private reconnectHandler?: ReconnectHandler<T>;
184
173
  private expireHandler?: ExpireHandler;
185
174
  private errorHandler?: ErrorHandler<T>;
186
175
  private template?: string;
176
+ private _registry?: Map<string, HypenModuleDefinition>;
187
177
 
188
178
  constructor(
189
179
  initialState: T,
190
- options?: { persist?: boolean; version?: number; name?: string }
180
+ options?: { persist?: boolean; version?: number; name?: string },
181
+ registry?: Map<string, HypenModuleDefinition>
191
182
  ) {
192
183
  this.initialState = initialState;
193
184
  this.options = options || {};
185
+ this._registry = registry;
194
186
  }
195
187
 
196
188
  /**
@@ -202,10 +194,24 @@ export class HypenAppBuilder<T> {
202
194
  }
203
195
 
204
196
  /**
205
- * Register a handler for a specific action
197
+ * Register a handler for a specific action.
198
+ * Optionally provide a payload type parameter for typed action payloads.
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * // Typed payload:
203
+ * .onAction<{ amount: number }>("add", ({ action, state }) => {
204
+ * action.payload.amount; // fully typed
205
+ * })
206
+ *
207
+ * // Untyped (payload is unknown):
208
+ * .onAction("add", ({ action, state }) => {
209
+ * action.payload; // unknown
210
+ * })
211
+ * ```
206
212
  */
207
- onAction(name: string, fn: ActionHandler<T>): this {
208
- this.actionHandlers.set(name, fn);
213
+ onAction<P = unknown>(name: string, fn: ActionHandler<T, P>): this {
214
+ this.actionHandlers.set(name, fn as ActionHandler<T, any>);
209
215
  return this;
210
216
  }
211
217
 
@@ -319,7 +325,7 @@ export class HypenAppBuilder<T> {
319
325
  ? Object.keys(this.initialState)
320
326
  : [];
321
327
 
322
- return {
328
+ const definition: HypenModuleDefinition<T> = {
323
329
  name: this.options.name,
324
330
  actions: Array.from(this.actionHandlers.keys()),
325
331
  stateKeys,
@@ -337,13 +343,27 @@ export class HypenAppBuilder<T> {
337
343
  onError: this.errorHandler,
338
344
  },
339
345
  };
346
+
347
+ // Auto-register in the app registry when the module has a name
348
+ if (this.options.name && this._registry) {
349
+ this._registry.set(this.options.name, definition as HypenModuleDefinition);
350
+ }
351
+
352
+ return definition;
340
353
  }
341
354
  }
342
355
 
343
356
  /**
344
- * Hypen App API
357
+ * Hypen App API — singleton factory and component registry.
358
+ *
359
+ * Modules built with a `name` are automatically registered here.
360
+ * Consumers (ManagedRouter, RemoteServer, ComponentResolver) read from this
361
+ * registry instead of requiring a separate ModuleRegistry instance.
345
362
  */
346
363
  export class HypenApp {
364
+ /** @internal */
365
+ readonly _registry = new Map<string, HypenModuleDefinition>();
366
+
347
367
  /**
348
368
  * Define the initial state for a module
349
369
  */
@@ -351,7 +371,83 @@ export class HypenApp {
351
371
  initial: T,
352
372
  options?: { persist?: boolean; version?: number; name?: string }
353
373
  ): HypenAppBuilder<T> {
354
- return new HypenAppBuilder(initial, options);
374
+ return new HypenAppBuilder(initial, options, this._registry);
375
+ }
376
+
377
+ /**
378
+ * Convenience: start a module builder with a name pre-set.
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * export default app
383
+ * .module("Settings")
384
+ * .defineState({ theme: "dark" })
385
+ * .ui(hypen`...`);
386
+ * ```
387
+ */
388
+ module(name: string) {
389
+ const registry = this._registry;
390
+ return {
391
+ defineState: <T>(
392
+ initial: T,
393
+ options?: { persist?: boolean; version?: number }
394
+ ): HypenAppBuilder<T> => {
395
+ return new HypenAppBuilder(initial, { ...options, name }, registry);
396
+ },
397
+ };
398
+ }
399
+
400
+ // ---------------------------------------------------------------------------
401
+ // Registry API
402
+ // ---------------------------------------------------------------------------
403
+
404
+ /**
405
+ * Get a registered module definition by component name.
406
+ */
407
+ get(name: string): HypenModuleDefinition | undefined {
408
+ return this._registry.get(name);
409
+ }
410
+
411
+ /**
412
+ * Check if a module definition is registered.
413
+ */
414
+ has(name: string): boolean {
415
+ return this._registry.has(name);
416
+ }
417
+
418
+ /**
419
+ * Read-only view of all registered component definitions.
420
+ */
421
+ get components(): ReadonlyMap<string, HypenModuleDefinition> {
422
+ return this._registry;
423
+ }
424
+
425
+ /**
426
+ * Get all registered component names.
427
+ */
428
+ getNames(): string[] {
429
+ return Array.from(this._registry.keys());
430
+ }
431
+
432
+ /**
433
+ * Number of registered definitions.
434
+ */
435
+ get size(): number {
436
+ return this._registry.size;
437
+ }
438
+
439
+ /**
440
+ * Unregister a module definition.
441
+ */
442
+ unregister(name: string): void {
443
+ this._registry.delete(name);
444
+ }
445
+
446
+ /**
447
+ * Clear all registered definitions.
448
+ */
449
+ clear(): void {
450
+ this._registry.clear();
355
451
  }
356
452
  }
357
453
 
@@ -368,19 +464,19 @@ export class HypenModuleInstance<T extends object = any> {
368
464
  private definition: HypenModuleDefinition<T>;
369
465
  private state: T;
370
466
  private isDestroyed = false;
371
- private routerContext?: RouterContext;
467
+ private router: HypenRouter | null;
372
468
  private globalContext?: HypenGlobalContext;
373
469
  private stateChangeCallbacks: Array<() => void> = [];
374
470
 
375
471
  constructor(
376
472
  engine: IEngine,
377
473
  definition: HypenModuleDefinition<T>,
378
- routerContext?: RouterContext,
474
+ router?: HypenRouter | null,
379
475
  globalContext?: HypenGlobalContext
380
476
  ) {
381
477
  this.engine = engine;
382
478
  this.definition = definition;
383
- this.routerContext = routerContext;
479
+ this.router = router ?? null;
384
480
  this.globalContext = globalContext;
385
481
 
386
482
  // Named state prefix — all lowercase per convention
@@ -425,10 +521,6 @@ export class HypenModuleInstance<T extends object = any> {
425
521
  sender: action.sender,
426
522
  };
427
523
 
428
- const next: ActionNext = {
429
- router: this.routerContext?.root || (null as any),
430
- };
431
-
432
524
  const context: GlobalContext | undefined = this.globalContext
433
525
  ? this.createGlobalContextAPI()
434
526
  : undefined;
@@ -437,7 +529,6 @@ export class HypenModuleInstance<T extends object = any> {
437
529
  const result = await this.executeAction(actionName, handler, {
438
530
  action: actionCtx,
439
531
  state: this.state,
440
- next,
441
532
  context: context!,
442
533
  });
443
534
 
@@ -452,6 +543,22 @@ export class HypenModuleInstance<T extends object = any> {
452
543
  });
453
544
  }
454
545
 
546
+ // Auto-register __hypen_bind for .bind() two-way binding support
547
+ this.engine.onAction("__hypen_bind", (action: Action) => {
548
+ const payload = action.payload as { path?: string; value?: unknown } | null;
549
+ if (!payload?.path) return;
550
+
551
+ const segments = payload.path.split(".");
552
+ let target: any = this.state;
553
+ for (let i = 0; i < segments.length - 1; i++) {
554
+ const seg = segments[i]!;
555
+ target = target?.[seg];
556
+ if (target == null) return;
557
+ }
558
+ const lastSeg = segments[segments.length - 1]!;
559
+ target[lastSeg] = payload.value;
560
+ });
561
+
455
562
  // Call onCreated
456
563
  this.callCreatedHandler();
457
564
  }
@@ -473,12 +580,10 @@ export class HypenModuleInstance<T extends object = any> {
473
580
  emit: (event: string, payload?: any) => ctx.emit(event, payload),
474
581
  on: (event: string, handler: (payload?: any) => void) =>
475
582
  ctx.on(event, handler),
583
+ router: this.router,
476
584
  };
477
585
 
478
- // Expose router and hypen engine for built-in components (if available)
479
- if ((ctx as any).__router) {
480
- api.__router = (ctx as any).__router;
481
- }
586
+ // Expose hypen engine for built-in components (if available)
482
587
  if ((ctx as any).__hypenEngine) {
483
588
  (api as any).__hypenEngine = (ctx as any).__hypenEngine;
484
589
  }
@@ -492,7 +597,7 @@ export class HypenModuleInstance<T extends object = any> {
492
597
  */
493
598
  private async executeAction(
494
599
  actionName: string,
495
- handler: ActionHandler<T>,
600
+ handler: ActionHandler<T, any>,
496
601
  ctx: ActionHandlerContext<T>
497
602
  ): Promise<Result<void, ActionError>> {
498
603
  try {
@@ -575,7 +680,15 @@ export class HypenModuleInstance<T extends object = any> {
575
680
  private async callCreatedHandler(): Promise<void> {
576
681
  if (this.definition.handlers.onCreated) {
577
682
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
578
- await this.definition.handlers.onCreated(this.state, context);
683
+ try {
684
+ await this.definition.handlers.onCreated(this.state, context);
685
+ } catch (e) {
686
+ const error = e instanceof HypenError ? e : new ActionError("onCreated", e);
687
+ const shouldRethrow = await this.handleError(error, { lifecycle: "created" });
688
+ if (shouldRethrow) {
689
+ throw error;
690
+ }
691
+ }
579
692
  }
580
693
  }
581
694
 
@@ -593,7 +706,15 @@ export class HypenModuleInstance<T extends object = any> {
593
706
  if (this.isDestroyed) return;
594
707
 
595
708
  if (this.definition.handlers.onDestroyed) {
596
- await this.definition.handlers.onDestroyed(this.state);
709
+ try {
710
+ await this.definition.handlers.onDestroyed(this.state);
711
+ } catch (e) {
712
+ const error = e instanceof HypenError ? e : new ActionError("onDestroyed", e);
713
+ const shouldRethrow = await this.handleError(error, { lifecycle: "destroyed" });
714
+ if (shouldRethrow) {
715
+ throw error;
716
+ }
717
+ }
597
718
  }
598
719
 
599
720
  this.isDestroyed = true;