@coherent.js/client 1.1.1 → 2.0.0-rc.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/types/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Coherent.js Client Types
3
- * TypeScript definitions for client-side functionality
3
+ * TypeScript definitions for @coherent.js/client: hydration, event
4
+ * delegation, state serialization, mismatch detection and HMR.
4
5
  *
5
- * @version 1.0.0-beta.1
6
+ * The router is the `@coherent.js/client/router` entry point; its types are
7
+ * re-exported here.
6
8
  */
7
9
 
8
10
  // Import core types for component integration
@@ -28,39 +30,45 @@ export type {
28
30
  };
29
31
 
30
32
  // ============================================================================
31
- // DOM and Browser Types
32
- // ============================================================================
33
-
34
- /** HTML element with Coherent.js data attributes */
35
- export interface CoherentHTMLElement extends HTMLElement {
36
- 'data-coherent-state'?: string;
37
- 'data-coherent-component'?: string;
38
- 'data-coherent-id'?: string;
39
- 'data-action'?: string;
40
- 'data-event'?: string;
41
- 'data-count'?: string;
42
- 'data-step'?: string;
43
- 'data-active'?: string;
44
- 'data-loading'?: string;
45
- 'data-disabled'?: string;
46
- 'data-selected'?: string;
47
- 'data-expanded'?: string;
48
- 'data-visible'?: string;
49
- __coherentInstance?: HydratedInstance;
50
- }
51
-
52
- // ============================================================================
53
- // Event Handler Types - Specific DOM Events
33
+ // Events
54
34
  // ============================================================================
55
35
 
56
36
  /**
57
- * Generic event handler type.
58
- * The handler receives the DOM event, the element, and optional custom data.
37
+ * What a delegated handler (an `on*` prop bound by `hydrate()`) receives: the
38
+ * native event plus the component it belongs to.
59
39
  */
60
- export type EventHandler<E extends Event = Event> = (
61
- event: E,
62
- element: HTMLElement,
63
- data?: any
40
+ export interface CoherentEvent<S = any, E extends Event = Event> {
41
+ /** The native DOM event */
42
+ originalEvent: E;
43
+ /** The event type, e.g. `'click'` */
44
+ type: string;
45
+ /** The element whose handler runs (the one carrying the handler) */
46
+ target: Element;
47
+ /** Same as `target` */
48
+ currentTarget: Element;
49
+ /** Whether the default action was prevented */
50
+ readonly defaultPrevented: boolean;
51
+ /** Whether a handler stopped propagation to ancestor handlers */
52
+ propagationStopped: boolean;
53
+ /** Prevent the default action (all delegated types but touch/wheel/scroll) */
54
+ preventDefault(): void;
55
+ /** Stop ancestor handlers and native propagation */
56
+ stopPropagation(): void;
57
+ /** Like stopPropagation(), also for other native listeners */
58
+ stopImmediatePropagation(): void;
59
+ /** The hydrated component function */
60
+ component: ((props?: any) => CoherentNode) | null;
61
+ /** The component's state when the event fired */
62
+ state: S | null;
63
+ /** Merge state and re-render the component */
64
+ setState: ((newState: Partial<S> | ((prev: S) => Partial<S>)) => void) | null;
65
+ /** The props the component last rendered with (state included) */
66
+ props: Record<string, any> | null;
67
+ }
68
+
69
+ /** A delegated event handler. */
70
+ export type EventHandler<E extends Event = Event, S = any> = (
71
+ event: CoherentEvent<S, E>
64
72
  ) => void | Promise<void>;
65
73
 
66
74
  /** Click event handler (MouseEvent) */
@@ -96,14 +104,9 @@ export type TouchHandler = EventHandler<TouchEvent>;
96
104
  /** Wheel event handler (WheelEvent) */
97
105
  export type WheelHandler = EventHandler<WheelEvent>;
98
106
 
99
- /**
100
- * State-aware event handler used in components.
101
- * Receives event, current state, and setState function.
102
- */
107
+ /** A delegated handler typed by the component's state. */
103
108
  export type StateAwareHandler<S = any, E extends Event = Event> = (
104
- event: E,
105
- state: S,
106
- setState: (newState: Partial<S> | ((prev: S) => Partial<S>)) => void
109
+ event: CoherentEvent<S, E>
107
110
  ) => void | Promise<void>;
108
111
 
109
112
  // ============================================================================
@@ -130,98 +133,46 @@ export interface SerializableState {
130
133
  // Hydration Types
131
134
  // ============================================================================
132
135
 
133
- /** Hydration options */
136
+ /** Options for {@link hydrate} */
134
137
  export interface HydrationOptions {
138
+ /** State to hydrate with; defaults to the container's `data-state` */
135
139
  initialState?: SerializableState;
136
- autoHydrate?: boolean;
137
- selector?: string;
138
- preserveState?: boolean;
139
- enableEvents?: boolean;
140
- debugMode?: boolean;
141
- timeout?: number;
142
- onError?: (error: Error, element?: HTMLElement) => void;
143
- onSuccess?: (element: HTMLElement, state: SerializableState) => void;
144
- transforms?: StateTransforms;
145
- validators?: StateValidators;
146
- /** Enable mismatch detection (dev mode default: true) */
140
+ /**
141
+ * Compare the server DOM with the component's output. Defaults to on when
142
+ * `strict` or `onMismatch` is given or `process.env.NODE_ENV` is
143
+ * `'development'`, off otherwise.
144
+ */
147
145
  detectMismatch?: boolean;
148
146
  /** Throw on mismatch instead of warning */
149
147
  strict?: boolean;
150
- /** Custom mismatch handler */
148
+ /** Receive mismatches instead of the console warning */
151
149
  onMismatch?: (mismatches: HydrationMismatch[]) => void;
152
150
  /** Additional props to pass to component */
153
151
  props?: Record<string, any>;
154
152
  }
155
153
 
156
- /** State transformation functions */
157
- export interface StateTransforms {
158
- [key: string]: (value: any) => any;
159
- }
160
-
161
- /** State validation functions */
162
- export interface StateValidators {
163
- [key: string]: (value: any) => boolean;
164
- }
165
-
166
- /** Hydration mismatch information */
154
+ /** One difference between the server DOM and the component's output */
167
155
  export interface HydrationMismatch {
156
+ /** Position in the virtual tree, e.g. `children[1].@class` */
168
157
  path: string;
169
- type: 'text' | 'attribute' | 'tag' | 'children' | 'missing' | 'extra';
158
+ type:
159
+ | 'text'
160
+ | 'tagName'
161
+ | 'attribute'
162
+ | 'children_count'
163
+ | 'missing_dom_child'
164
+ | 'extra_dom_child';
170
165
  expected: any;
171
166
  actual: any;
172
- }
173
-
174
- /** Component hydration result */
175
- export interface HydrationResult {
176
- success: boolean;
177
- element: HTMLElement;
178
- state: SerializableState;
179
- component?: ClientComponent;
180
- error?: Error;
181
- duration?: number;
182
- }
183
-
184
- /** Batch hydration result */
185
- export interface BatchHydrationResult {
186
- total: number;
187
- successful: number;
188
- failed: number;
189
- results: HydrationResult[];
190
- errors: Array<{ element: HTMLElement; error: Error }>;
191
- duration: number;
192
- }
193
-
194
- /**
195
- * Hydrated component instance returned by hydrate().
196
- * Provides control methods for state management and lifecycle.
197
- */
198
- export interface HydratedInstance {
199
- /** The DOM element being hydrated */
200
- element: HTMLElement;
201
- /** The component function */
202
- component: CoherentComponent;
203
- /** Current props */
204
- props: Record<string, any>;
205
- /** Current state */
206
- state: SerializableState;
207
- /** Whether hydration is complete */
208
- isHydrated: boolean;
209
-
210
- /** Update props and re-render */
211
- update(newProps?: Record<string, any>): HydratedInstance;
212
- /** Re-render with current state */
213
- rerender(): void;
214
- /** Destroy the instance and clean up */
215
- destroy(): void;
216
- /** Set state and trigger re-render */
217
- setState(newState: Partial<SerializableState> | ((prev: SerializableState) => Partial<SerializableState>)): void;
167
+ /** CSS-like path of the DOM element, for debugging */
168
+ domPath: string;
218
169
  }
219
170
 
220
171
  /**
221
172
  * Control object returned by the clean hydrate() API.
222
173
  */
223
174
  export interface HydrateControl {
224
- /** Unmount the component and clean up event handlers */
175
+ /** Unmount the component and clean up event handlers; terminal */
225
176
  unmount(): void;
226
177
  /** Re-render with optional new props */
227
178
  rerender(newProps?: Record<string, any>): void;
@@ -231,90 +182,15 @@ export interface HydrateControl {
231
182
  setState(newState: Partial<SerializableState> | ((prev: SerializableState) => Partial<SerializableState>)): void;
232
183
  }
233
184
 
234
- export interface MakeHydratableOptions {
235
- componentName?: string;
236
- initialState?: SerializableState;
237
- }
238
-
239
- // ============================================================================
240
- // Client Component Types
241
- // ============================================================================
242
-
243
- /** Client-side component interface */
244
- export interface ClientComponent {
245
- readonly element: HTMLElement;
246
- readonly state: SerializableState;
247
- readonly isHydrated: boolean;
248
- readonly id: string;
249
-
250
- setState(newState: Partial<SerializableState>): void;
251
- updateState(updater: (state: SerializableState) => Partial<SerializableState>): void;
252
- getState(): SerializableState;
253
- resetState(): void;
254
-
255
- render(): void;
256
- destroy(): void;
257
- refresh(): void;
258
-
259
- addEventListener(event: string, handler: EventHandler): void;
260
- removeEventListener(event: string, handler: EventHandler): void;
261
- trigger(event: string, data?: any): void;
262
-
263
- serialize(): string;
264
- toJSON(): SerializableState;
265
- }
266
-
267
- /** Component factory function */
268
- export type ComponentFactory = (element: HTMLElement, initialState?: SerializableState) => ClientComponent;
269
-
270
- /** Component registry entry */
271
- export interface ComponentRegistryEntry {
272
- name: string;
273
- factory: ComponentFactory;
274
- selector?: string;
275
- autoHydrate?: boolean;
276
- }
277
-
278
185
  // ============================================================================
279
- // Event System Types
186
+ // Event Delegation Types
280
187
  // ============================================================================
281
188
 
282
- /** Event binding configuration */
283
- export interface EventBinding {
284
- event: string;
285
- selector?: string;
286
- handler: EventHandler;
287
- options?: EventListenerOptions | boolean;
288
- delegate?: boolean;
289
- once?: boolean;
290
- }
291
-
292
- /** Event manager interface */
293
- export interface EventManager {
294
- bind(element: HTMLElement, bindings: EventBinding[]): void;
295
- unbind(element: HTMLElement, event?: string): void;
296
- trigger(element: HTMLElement, event: string, data?: any): void;
297
- delegate(container: HTMLElement, selector: string, event: string, handler: EventHandler): void;
298
- undelegate(container: HTMLElement, selector?: string, event?: string): void;
299
- once(element: HTMLElement, event: string, handler: EventHandler): void;
300
- debounce(handler: EventHandler, delay: number): EventHandler;
301
- throttle(handler: EventHandler, limit: number): EventHandler;
302
- }
303
-
304
- /** Custom event data */
305
- export interface CustomEventData {
306
- detail: any;
307
- bubbles?: boolean;
308
- cancelable?: boolean;
309
- composed?: boolean;
310
- }
311
-
312
- // ============================================================================
313
- // Event Delegation Types (Plan 02-01)
314
- // ============================================================================
315
-
316
- /** A component whose state a delegated handler may read and write */
189
+ /** The component a delegated handler belongs to */
317
190
  export interface HandlerComponentRef {
191
+ component?: (props?: any) => CoherentNode;
192
+ state?: any;
193
+ props?: Record<string, any>;
318
194
  getState?: () => any;
319
195
  setState?: (state: any) => void;
320
196
  [key: string]: any;
@@ -322,14 +198,19 @@ export interface HandlerComponentRef {
322
198
 
323
199
  /** A registered handler and the component it belongs to */
324
200
  export interface RegisteredHandler {
325
- handler: StateAwareHandler;
201
+ handler: StateAwareHandler<any, any>;
326
202
  componentRef: HandlerComponentRef | null;
327
203
  }
328
204
 
329
205
  /**
330
206
  * Routes document-level events to handlers registered by id.
331
207
  *
332
- * Focus and blur are delegated in the capture phase, since they do not bubble.
208
+ * Listeners are non-passive, so handlers can call `preventDefault()`, except
209
+ * for scroll-blocking types (touchstart, touchmove, wheel, scroll). Handlers
210
+ * run from the target's nearest `data-coherent-{type}` element outwards, like
211
+ * bubbling, until one stops propagation. Focus and blur are captured, since
212
+ * they do not bubble natively; other non-bubbling events (mouseenter, load,
213
+ * ...) only reach a handler on the target itself.
333
214
  */
334
215
  export class EventDelegation {
335
216
  constructor(registry?: HandlerRegistry);
@@ -343,10 +224,16 @@ export class EventDelegation {
343
224
  /** Attach listeners to `root`; idempotent, and a no-op without a document */
344
225
  initialize(root?: Document | Element | null): void;
345
226
 
346
- /** Dispatch one delegated event to its registered handler */
227
+ /**
228
+ * Delegate `eventType` too. hydrate() calls this for every event type a
229
+ * component handles, so any DOM event works, not only the defaults.
230
+ */
231
+ listen(eventType: string): void;
232
+
233
+ /** Dispatch one delegated event to its registered handlers */
347
234
  handleEvent(event: Event, eventType: string): void;
348
235
 
349
- /** Remove every listener attached by `initialize()` */
236
+ /** Remove every listener attached by `initialize()` and `listen()` */
350
237
  destroy(): void;
351
238
 
352
239
  isInitialized(): boolean;
@@ -361,7 +248,7 @@ export class HandlerRegistry {
361
248
  /** Register a handler, optionally bound to a component */
362
249
  register(
363
250
  handlerId: string,
364
- handler: StateAwareHandler,
251
+ handler: StateAwareHandler<any, any>,
365
252
  componentRef?: HandlerComponentRef | null
366
253
  ): void;
367
254
 
@@ -380,489 +267,239 @@ export class HandlerRegistry {
380
267
  }
381
268
 
382
269
  // ============================================================================
383
- // State Management Types
270
+ // Hot Module Replacement Types
384
271
  // ============================================================================
385
272
 
386
- /** Client state manager */
387
- export interface ClientStateManager {
388
- get<T = any>(key: string): T | undefined;
389
- set<T = any>(key: string, value: T): void;
390
- has(key: string): boolean;
391
- delete(key: string): boolean;
392
- clear(): void;
393
- keys(): string[];
394
- values(): any[];
395
- entries(): Array<[string, any]>;
396
- size(): number;
397
-
398
- subscribe(key: string, callback: (value: any, oldValue?: any) => void): () => void;
399
- unsubscribe(key: string, callback?: Function): void;
400
-
401
- persist(key: string, storage?: Storage): void;
402
- unpersist(key: string): void;
403
-
404
- batch(fn: () => void): void;
405
-
406
- toJSON(): Record<string, any>;
407
- fromJSON(data: Record<string, any>): void;
408
- }
409
-
410
- /** State synchronization options */
411
- export interface StateSyncOptions {
412
- key: string;
413
- storage?: Storage;
414
- serializer?: {
415
- stringify: (value: any) => string;
416
- parse: (value: string) => any;
417
- };
418
- debounce?: number;
419
- validate?: (value: any) => boolean;
420
- transform?: (value: any) => any;
273
+ /** Hot context API for a module (see {@link createHotContext}) */
274
+ export interface HotContext {
275
+ /** Data persisted across HMR updates of this module */
276
+ readonly data: Record<string, any>;
277
+ /** Accept self updates; without it an update reloads the page */
278
+ accept(callback?: (newModule: any) => void): void;
279
+ /** Accept updates of dependencies */
280
+ acceptDeps(deps: string | string[], callback: (modules: Record<string, any>) => void): void;
281
+ /** Cleanup before the module is replaced; receives `data` */
282
+ dispose(callback: (data: Record<string, any>) => void): void;
283
+ /** Called when the module is removed from the module graph */
284
+ prune(callback: () => void): void;
285
+ /** Ask the dev server to propagate the update to importers */
286
+ invalidate(message?: string): void;
287
+ }
288
+
289
+ /** Tracked timers, listeners and fetches of one module */
290
+ export interface HMRModuleContext {
291
+ setTimeout(callback: (...args: any[]) => void, delay?: number, ...args: any[]): ReturnType<typeof setTimeout>;
292
+ setInterval(callback: (...args: any[]) => void, delay?: number, ...args: any[]): ReturnType<typeof setInterval>;
293
+ clearTimeout(id: ReturnType<typeof setTimeout>): void;
294
+ clearInterval(id: ReturnType<typeof setInterval>): void;
295
+ addEventListener(
296
+ target: EventTarget,
297
+ event: string,
298
+ handler: EventListenerOrEventListenerObject,
299
+ options?: AddEventListenerOptions | boolean
300
+ ): void;
301
+ createAbortController(): AbortController;
302
+ /** fetch() aborted on module disposal and by the caller's own `signal` */
303
+ fetch(url: string | URL, options?: RequestInit): Promise<Response>;
421
304
  }
422
305
 
423
- // ============================================================================
424
- // Hot Module Replacement Types
425
- // ============================================================================
426
-
427
- /** HMR update information */
428
- export interface HMRUpdate {
429
- type: 'component' | 'style' | 'script' | 'template' | 'full-reload';
430
- id: string;
431
- path: string;
432
- content?: string;
433
- timestamp: number;
434
- /** File that changed (for error display) */
306
+ /** An error shown by the overlay */
307
+ export interface HMRErrorDetails {
308
+ message: string;
435
309
  file?: string;
436
- /** Line number for error */
437
310
  line?: number;
438
- /** Column number for error */
439
311
  column?: number;
312
+ frame?: string;
313
+ stack?: string;
440
314
  }
441
315
 
442
- /** HMR event listener */
443
- export type HMRListener = (update: HMRUpdate) => void | Promise<void>;
444
-
445
- /** HMR configuration */
446
- export interface HMRConfig {
447
- enabled: boolean;
448
- websocketUrl?: string;
449
- reconnectInterval?: number;
450
- maxReconnectAttempts?: number;
451
- debug?: boolean;
452
- onUpdate?: HMRListener;
453
- onError?: (error: Error) => void;
454
- onReconnect?: () => void;
455
- /** Show error overlay */
456
- overlay?: boolean;
457
- /** Show connection indicator */
458
- indicator?: boolean;
459
- }
316
+ /** WebSocket client that applies dev-server updates */
317
+ export class HMRClient {
318
+ constructor();
460
319
 
461
- /** HMR client interface */
462
- export interface HMRClient {
463
- readonly isConnected: boolean;
464
- readonly config: HMRConfig;
320
+ socket: WebSocket | null;
321
+ connected: boolean;
322
+ reconnectAttempts: number;
323
+ maxReconnectAttempts: number;
324
+ reconnectDelay: number;
325
+ hadDisconnect: boolean;
326
+ reconnectTimeout: ReturnType<typeof setTimeout> | null;
327
+ initialized: boolean;
465
328
 
466
- connect(): Promise<void>;
329
+ /** Connect once per page (no-op without `window`) */
330
+ initialize(): void;
331
+ /** Open the WebSocket, reconnecting with backoff when it closes */
332
+ connect(): void;
333
+ /** Close the socket without reconnecting */
467
334
  disconnect(): void;
335
+ isConnected(): boolean;
336
+ scheduleReconnect(): void;
337
+ handleMessage(event: MessageEvent): void;
338
+ /** Re-import a changed module; reloads the page when it does not accept updates */
339
+ handleUpdate(data: { filePath?: string; webPath?: string; updateType?: string }): Promise<void>;
340
+ /** Import an updated module (overridable) */
341
+ importModule(url: string): Promise<any>;
342
+ /** Reload the page */
343
+ reload(): void;
344
+ handleUpdateError(error: Error, filePath: string): void;
345
+ showError(error: HMRErrorDetails): void;
346
+ hideError(): void;
347
+ }
348
+
349
+ /** Hot contexts and update handlers by module id */
350
+ export class ModuleTracker {
351
+ constructor();
468
352
 
469
- onUpdate(listener: HMRListener): () => void;
470
- onError(listener: (error: Error) => void): () => void;
471
- onReconnect(listener: () => void): () => void;
472
-
473
- updateComponent(id: string, factory: ComponentFactory): void;
474
- updateStyle(id: string, css: string): void;
475
- reloadPage(): void;
476
- }
477
-
478
- /** Hot context API for modules (Vite-compatible) */
479
- export interface HotContext {
480
- /** Accept self updates */
481
- accept(): void;
482
- /** Accept updates from dependencies */
483
- accept(deps: string | string[], callback?: (modules: any[]) => void): void;
484
- /** Dispose callback for cleanup */
485
- dispose(callback: (data: any) => void): void;
486
- /** Prune callback when module is removed */
487
- prune(callback: () => void): void;
488
- /** Invalidate to trigger parent update */
489
- invalidate(): void;
490
- /** Decline to fall back to full reload */
491
- decline(): void;
492
- /** Data persisted across HMR updates */
493
- data: Record<string, any>;
494
- }
495
-
496
- /** Module tracker for HMR boundary detection */
497
- export interface ModuleTracker {
498
- /** Track a module with its hot context */
499
- track(moduleId: string, context: HotContext): void;
500
- /** Check if module is a boundary */
501
- isBoundary(moduleId: string): boolean;
502
- /** Get hot context for module */
503
- getContext(moduleId: string): HotContext | undefined;
504
- /** Clear tracked modules */
353
+ modules: Map<string, any>;
354
+ socket: WebSocket | null;
355
+
356
+ setSocket(socket: WebSocket | null): void;
357
+ createHotContext(moduleId: string): HotContext;
358
+ canHotUpdate(moduleId: string): boolean;
359
+ isHmrBoundary(moduleId: string, moduleExports?: Record<string, any>): boolean;
360
+ extractComponentName(moduleId: string): string | null;
361
+ executeDispose(moduleId: string): Record<string, any> | null;
362
+ executeAccept(moduleId: string, newModule?: any): boolean;
363
+ executeAcceptDeps(moduleId: string, updatedDeps: Record<string, any>): boolean;
364
+ executePrune(moduleId: string): void;
365
+ hasModule(moduleId: string): boolean;
366
+ getModuleData(moduleId: string): Record<string, any> | null;
505
367
  clear(): void;
506
368
  }
507
369
 
508
- /** Cleanup tracker for resource management during HMR */
509
- export interface CleanupTracker {
510
- /** Track a timer for cleanup */
511
- trackTimer(moduleId: string, timerId: number): void;
512
- /** Track an event listener for cleanup */
513
- trackListener(moduleId: string, element: EventTarget, event: string, handler: EventListener): void;
514
- /** Track a fetch request for cleanup */
515
- trackFetch(moduleId: string, controller: AbortController): void;
516
- /** Clean up all resources for a module */
517
- cleanup(moduleId: string): void;
518
- /** Clear all tracked resources */
519
- clearAll(): void;
520
- }
521
-
522
- /** State capturer for preserving form state during HMR */
523
- export interface StateCapturer {
524
- /** Capture current input values and scroll positions */
525
- capture(): Record<string, any>;
526
- /** Restore captured state */
527
- restore(state: Record<string, any>): void;
528
- }
529
-
530
- /** Error overlay for displaying HMR errors */
531
- export interface ErrorOverlay {
532
- /** Show error overlay */
533
- show(error: { message: string; file?: string; line?: number; column?: number; frame?: string }): void;
534
- /** Hide error overlay */
535
- hide(): void;
536
- /** Check if overlay is visible */
537
- isVisible(): boolean;
538
- }
539
-
540
- /** Connection indicator for WebSocket status */
541
- export interface ConnectionIndicator {
542
- /** Show indicator with status */
543
- show(status: 'connected' | 'disconnected' | 'connecting' | 'error'): void;
544
- /** Hide indicator */
545
- hide(): void;
546
- /** Update status */
547
- setStatus(status: 'connected' | 'disconnected' | 'connecting' | 'error'): void;
548
- }
549
-
550
- // ============================================================================
551
- // Performance Types
552
- // ============================================================================
553
-
554
- /** Performance metrics */
555
- export interface PerformanceMetrics {
556
- hydrationTime: number;
557
- componentCount: number;
558
- eventBindings: number;
559
- memoryUsage?: number;
560
- renderTime?: number;
561
- stateUpdates: number;
562
- }
370
+ /** Tracks module resources so HMR can release them */
371
+ export class CleanupTracker {
372
+ constructor();
563
373
 
564
- /** Performance monitor interface */
565
- export interface PerformanceMonitor {
566
- start(label: string): void;
567
- end(label: string): number;
568
- measure(label: string, fn: () => any): any;
569
- measureAsync(label: string, fn: () => Promise<any>): Promise<any>;
570
- getMetrics(): PerformanceMetrics;
571
- reset(): void;
572
- report(): void;
573
- }
374
+ moduleResources: Map<string, any>;
574
375
 
575
- // ============================================================================
576
- // Router Types
577
- // ============================================================================
376
+ createContext(moduleId: string): HMRModuleContext;
377
+ cleanup(moduleId: string): void;
378
+ checkForLeaks(moduleId: string): void;
379
+ hasResources(moduleId: string): boolean;
380
+ getResourceCounts(moduleId: string): {
381
+ timers: number;
382
+ intervals: number;
383
+ listeners: number;
384
+ abortControllers: number;
385
+ } | null;
386
+ }
387
+
388
+ /** Preserves form input and scroll state across HMR updates */
389
+ export class StateCapturer {
390
+ constructor();
578
391
 
579
- /** Route configuration */
580
- export interface RouteConfig {
581
- /** Route path pattern */
582
- path: string;
583
- /** Component to render (can be async for code splitting) */
584
- component: CoherentComponent | (() => Promise<CoherentComponent>);
585
- /** Route metadata */
586
- meta?: Record<string, any>;
587
- /** Before enter guard */
588
- beforeEnter?: (to: Route, from: Route | null) => boolean | Promise<boolean>;
589
- /** Before leave guard */
590
- beforeLeave?: (to: Route, from: Route) => boolean | Promise<boolean>;
591
- /** Prefetch priority */
592
- priority?: number;
593
- /** Custom transition for this route */
594
- transition?: RouteTransition;
392
+ capturedInputs: Map<string, any>;
393
+ scrollPositions: Map<string, { top: number; left: number }>;
394
+ layoutSnapshot: Record<string, any> | null;
395
+
396
+ getInputKey(input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): string;
397
+ getElementPath(element: Element): string;
398
+ captureFormState(): Map<string, any>;
399
+ restoreFormState(): void;
400
+ findInputsByKey(key: string): HTMLElement[];
401
+ captureScrollPositions(): Map<string, { top: number; left: number }>;
402
+ getScrollableKey(element: Element): string;
403
+ captureLayout(): void;
404
+ layoutChangedSignificantly(): boolean;
405
+ findElementByKey(key: string): HTMLElement | null;
406
+ restoreScrollPositions(): void;
407
+ captureAll(): void;
408
+ restoreAll(): void;
409
+ clear(): void;
595
410
  }
596
411
 
597
- /** Current route state */
598
- export interface Route {
599
- path: string;
600
- component?: CoherentComponent;
601
- meta?: Record<string, any>;
602
- hash?: string;
603
- query?: Record<string, string>;
604
- }
412
+ /** Full-screen overlay for HMR errors */
413
+ export class ErrorOverlay {
414
+ constructor();
605
415
 
606
- /** Route transition configuration */
607
- export interface RouteTransition {
608
- enter: string;
609
- leave: string;
610
- duration: number;
611
- }
416
+ overlay: { host: HTMLElement; shadow: ShadowRoot } | null;
417
+ editor: string;
612
418
 
613
- /** Scroll behavior configuration */
614
- export interface ScrollBehaviorConfig {
615
- enabled?: boolean;
616
- behavior?: ScrollBehavior;
617
- position?: 'top' | 'saved';
618
- delay?: number;
619
- savePosition?: boolean;
620
- custom?: (to: Route, from: Route | null, savedPosition: { x: number; y: number } | null) => { x: number; y: number } | { el: Element };
419
+ createOverlay(): { host: HTMLElement; shadow: ShadowRoot };
420
+ show(error: HMRErrorDetails): void;
421
+ hide(): void;
422
+ openInEditor(file: string, line?: number): void;
423
+ setEditor(editor: string): void;
621
424
  }
622
425
 
623
- /** Router configuration */
624
- export interface RouterConfig {
625
- mode?: 'history' | 'hash';
626
- base?: string;
627
- prefetch?: {
628
- enabled?: boolean;
629
- strategy?: 'hover' | 'visible' | 'idle';
630
- delay?: number;
631
- maxConcurrent?: number;
632
- priority?: {
633
- critical?: number;
634
- high?: number;
635
- normal?: number;
636
- low?: number;
637
- };
638
- };
639
- transitions?: {
640
- enabled?: boolean;
641
- default?: RouteTransition;
642
- routes?: Record<string, RouteTransition>;
643
- onStart?: (from: string | null, to: string) => void;
644
- onComplete?: (from: string | null, to: string) => void;
645
- };
646
- codeSplitting?: {
647
- enabled?: boolean;
648
- strategy?: 'route';
649
- chunkNaming?: string;
650
- preload?: string[];
651
- onLoad?: (path: string, component: any, loadTime: number) => void;
652
- };
653
- scrollBehavior?: ScrollBehaviorConfig;
654
- }
426
+ /** Small dot showing the HMR connection status */
427
+ export class ConnectionIndicator {
428
+ constructor();
655
429
 
656
- /** Router statistics */
657
- export interface RouterStats {
658
- navigations: number;
659
- prefetches: number;
660
- transitionsCompleted: number;
661
- chunksLoaded: number;
662
- scrollRestores: number;
663
- routesRegistered: number;
664
- prefetchQueueSize: number;
665
- activePrefetches: number;
666
- loadedChunks: number;
667
- savedPositions: number;
668
- historyLength: number;
669
- }
430
+ indicator: HTMLElement | null;
670
431
 
671
- /** Router instance */
672
- export interface Router {
673
- addRoute(path: string, config: RouteConfig): void;
674
- push(path: string, options?: Partial<Route>): Promise<boolean>;
675
- replace(path: string, options?: Partial<Route>): Promise<boolean>;
676
- back(): void;
677
- forward(): void;
678
- prefetchRoute(path: string, priority?: number): Promise<void>;
679
- prefetchRoutes(paths: string[], priority?: number): void;
680
- setupPrefetchStrategy(element: HTMLElement, path: string): void;
681
- getRoute(path: string): RouteConfig | undefined;
682
- getRoutes(): RouteConfig[];
683
- getCurrentRoute(): Route | null;
684
- getStats(): RouterStats;
685
- clearCaches(): void;
432
+ create(): void;
433
+ update(status: 'connected' | 'disconnected' | 'reconnecting' | 'error'): void;
434
+ destroy(): void;
686
435
  }
687
436
 
688
437
  // ============================================================================
689
- // Utility Types
438
+ // Router Types (the router itself is the @coherent.js/client/router entry)
690
439
  // ============================================================================
691
440
 
692
- /** DOM ready state */
693
- export type ReadyState = 'loading' | 'interactive' | 'complete';
694
-
695
- /** DOM ready callback */
696
- export type ReadyCallback = () => void | Promise<void>;
697
-
698
- /** Selector types */
699
- export type Selector = string | HTMLElement | NodeList | HTMLElement[];
700
-
701
- /** Animation frame callback */
702
- export type AnimationCallback = (timestamp: number) => void;
703
-
704
- /** Intersection observer entry with extended data */
705
- export interface ExtendedIntersectionObserverEntry extends IntersectionObserverEntry {
706
- element: HTMLElement;
707
- isVisible: boolean;
708
- percentage: number;
709
- }
441
+ export type {
442
+ RouteConfig,
443
+ Route,
444
+ RouteTransition,
445
+ ScrollBehaviorConfig,
446
+ RouterConfig,
447
+ RouterStats,
448
+ Router,
449
+ } from './router.js';
710
450
 
711
451
  // ============================================================================
712
- // Main Functions
452
+ // Hydration
713
453
  // ============================================================================
714
454
 
715
- /** Extract initial state from DOM element */
716
- declare function extractInitialState(
717
- element: HTMLElement,
718
- options?: Pick<HydrationOptions, 'initialState' | 'transforms' | 'validators'>
719
- ): SerializableState | null;
720
-
721
455
  /**
722
456
  * Hydrate a server-rendered component (clean API).
723
457
  * Returns a control object with unmount, rerender, getState, and setState.
458
+ * Hydrating a container again replaces its previous hydration.
724
459
  */
725
460
  export function hydrate(
726
- component: CoherentComponent,
461
+ component: CoherentComponent | ((props: any) => CoherentNode),
727
462
  container: HTMLElement,
728
463
  options?: HydrationOptions
729
464
  ): HydrateControl;
730
465
 
731
- /** Register a component for auto-hydration */
732
- declare function registerComponent(
733
- name: string,
734
- factory: ComponentFactory,
735
- options?: Partial<ComponentRegistryEntry>
736
- ): void;
737
-
738
- /** Unregister a component */
739
- declare function unregisterComponent(name: string): boolean;
740
-
741
- /** Get registered component */
742
- declare function getComponent(name: string): ComponentRegistryEntry | undefined;
743
-
744
- /** Get all registered components */
745
- declare function getAllComponents(): ComponentRegistryEntry[];
746
-
747
- /** Create a client component */
748
- declare function createClientComponent(
749
- element: HTMLElement,
750
- initialState?: SerializableState
751
- ): ClientComponent;
752
-
753
- /** Wait for DOM to be ready */
754
- declare function ready(callback: ReadyCallback): Promise<void>;
755
-
756
- /** DOM query utilities */
757
- declare function $(selector: Selector): HTMLElement[];
758
- declare function $$(selector: string): HTMLElement | null;
759
-
760
- /** Event utilities */
761
- declare function on(
762
- element: HTMLElement | string,
763
- event: string,
764
- handler: EventHandler,
765
- options?: EventListenerOptions | boolean
766
- ): void;
767
-
768
- declare function off(
769
- element: HTMLElement | string,
770
- event?: string,
771
- handler?: EventHandler
772
- ): void;
773
-
774
- declare function trigger(
775
- element: HTMLElement,
776
- event: string,
777
- data?: CustomEventData
778
- ): boolean;
779
-
780
- declare function delegate(
781
- container: HTMLElement,
782
- selector: string,
783
- event: string,
784
- handler: EventHandler
785
- ): void;
786
-
787
- /** Animation utilities */
788
- declare function requestAnimationFrame(callback: AnimationCallback): number;
789
- declare function cancelAnimationFrame(id: number): void;
790
-
791
- /** Debounce and throttle utilities */
792
- declare function debounce<T extends (...args: any[]) => any>(
793
- func: T,
794
- delay: number
795
- ): (...args: Parameters<T>) => void;
796
-
797
- declare function throttle<T extends (...args: any[]) => any>(
798
- func: T,
799
- limit: number
800
- ): (...args: Parameters<T>) => void;
801
-
802
- /** State management utilities */
803
- declare function createStateManager(): ClientStateManager;
804
- declare function syncState(options: StateSyncOptions): void;
805
-
806
- /** Performance utilities */
807
- declare function createPerformanceMonitor(): PerformanceMonitor;
808
-
809
- /** HMR utilities */
810
- declare function createHMRClient(config?: Partial<HMRConfig>): HMRClient;
811
- declare function enableHMR(config?: Partial<HMRConfig>): Promise<void>;
812
- export function createHotContext(moduleId: string): HotContext;
813
-
814
- /** Intersection observer utilities */
815
- declare function observeVisibility(
816
- elements: HTMLElement | HTMLElement[],
817
- callback: (entries: ExtendedIntersectionObserverEntry[]) => void,
818
- options?: IntersectionObserverInit
819
- ): IntersectionObserver;
820
-
821
- /** Lazy loading utilities */
822
- declare function lazyLoad(
823
- elements: HTMLElement | HTMLElement[],
824
- options?: {
825
- threshold?: number;
826
- rootMargin?: string;
827
- attribute?: string;
828
- placeholder?: string;
829
- }
830
- ): IntersectionObserver;
831
-
832
466
  // ============================================================================
833
- // State Serialization Functions (Plan 02-02)
467
+ // State Serialization
834
468
  // ============================================================================
835
469
 
836
- /** Serialize state to base64-encoded JSON */
837
- export function serializeState(state: SerializableState): string;
470
+ /** Serialize state to base64-encoded JSON; `null` when nothing is serializable */
471
+ export function serializeState(state: SerializableState): string | null;
838
472
 
839
- /** Deserialize state from base64-encoded JSON */
840
- export function deserializeState(encoded: string): SerializableState;
473
+ /** Deserialize state from base64-encoded JSON; `null` when invalid */
474
+ export function deserializeState(encoded: string | null | undefined): SerializableState | null;
841
475
 
842
476
  /** Extract state from DOM element's data-state attribute */
843
477
  export function extractState(element: HTMLElement): SerializableState | null;
844
478
 
845
479
  /** Serialize state with size warning (10KB threshold) */
846
- export function serializeStateWithWarning(state: SerializableState, componentName?: string): string;
480
+ export function serializeStateWithWarning(state: SerializableState, componentName?: string): string | null;
847
481
 
848
482
  // ============================================================================
849
- // Mismatch Detection Functions (Plan 02-03)
483
+ // Mismatch Detection
850
484
  // ============================================================================
851
485
 
852
- /** Detect mismatches between DOM and virtual DOM */
486
+ /**
487
+ * Detect mismatches between DOM and virtual DOM. An array `vNode` is compared
488
+ * with the element's children.
489
+ */
853
490
  export function detectMismatch(element: HTMLElement, vNode: CoherentNode): HydrationMismatch[];
854
491
 
855
- /** Report mismatches with warnings or errors */
492
+ /** Report mismatches with warnings, or throw with `strict` */
856
493
  export function reportMismatches(
857
494
  mismatches: HydrationMismatch[],
858
495
  options?: { componentName?: string; strict?: boolean }
859
496
  ): void;
860
497
 
861
498
  /** Format path for mismatch reporting */
862
- export function formatPath(path: (string | number)[]): string;
499
+ export function formatPath(path: (string | number)[] | null | undefined): string;
863
500
 
864
501
  // ============================================================================
865
- // Event Delegation Exports (Plan 02-01)
502
+ // Event Delegation
866
503
  // ============================================================================
867
504
 
868
505
  /** Event delegation singleton */
@@ -871,106 +508,26 @@ export const eventDelegation: EventDelegation;
871
508
  /** Handler registry singleton */
872
509
  export const handlerRegistry: HandlerRegistry;
873
510
 
874
- /** Wrap an event handler for use with event delegation */
511
+ /**
512
+ * Wrap a native event for a delegated handler: `target` is the element the
513
+ * handler is registered on, `componentRef` supplies state/setState/props.
514
+ */
875
515
  export function wrapEvent<S = any, E extends Event = Event>(
876
- eventType: string,
877
- handler: StateAwareHandler<S, E>,
878
- handlerId?: string
879
- ): { handlerId: string; dataAttribute: string };
516
+ originalEvent: E,
517
+ target: Element,
518
+ componentRef?: HandlerComponentRef | null
519
+ ): CoherentEvent<S, E>;
880
520
 
881
521
  // ============================================================================
882
- // HMR Exports (Phase 4)
522
+ // HMR
883
523
  // ============================================================================
884
524
 
885
- export const HMRClient: new (config?: Partial<HMRConfig>) => HMRClient;
886
525
  export const hmrClient: HMRClient;
887
- export const ModuleTracker: new () => ModuleTracker;
888
526
  export const moduleTracker: ModuleTracker;
889
- export const CleanupTracker: new () => CleanupTracker;
890
527
  export const cleanupTracker: CleanupTracker;
891
- export const StateCapturer: new () => StateCapturer;
892
528
  export const stateCapturer: StateCapturer;
893
- export const ErrorOverlay: new () => ErrorOverlay;
894
529
  export const errorOverlay: ErrorOverlay;
895
- export const ConnectionIndicator: new () => ConnectionIndicator;
896
530
  export const connectionIndicator: ConnectionIndicator;
897
531
 
898
- // ============================================================================
899
- // Global Constants
900
- // ============================================================================
901
-
902
- /** Default hydration selector */
903
- declare const DEFAULT_HYDRATION_SELECTOR: string;
904
-
905
- /** Component registry */
906
- declare const componentRegistry: Map<string, ComponentRegistryEntry>;
907
-
908
- /** Global state manager instance */
909
- declare const globalStateManager: ClientStateManager;
910
-
911
- /** Global event manager instance */
912
- declare const globalEventManager: EventManager;
913
-
914
- /** Global performance monitor instance */
915
- declare const globalPerformanceMonitor: PerformanceMonitor;
916
-
917
- // ============================================================================
918
- // Default Export
919
- // ============================================================================
920
-
921
- declare const coherentClient: {
922
- // Hydration
923
- extractInitialState: typeof extractInitialState;
924
- hydrate: typeof hydrate;
925
-
926
- // Component registration
927
- registerComponent: typeof registerComponent;
928
- unregisterComponent: typeof unregisterComponent;
929
- getComponent: typeof getComponent;
930
- getAllComponents: typeof getAllComponents;
931
- createClientComponent: typeof createClientComponent;
932
-
933
- // DOM utilities
934
- ready: typeof ready;
935
- $: typeof $;
936
- $$: typeof $$;
937
-
938
- // Event utilities
939
- on: typeof on;
940
- off: typeof off;
941
- trigger: typeof trigger;
942
- delegate: typeof delegate;
943
-
944
- // Animation utilities
945
- requestAnimationFrame: typeof requestAnimationFrame;
946
- cancelAnimationFrame: typeof cancelAnimationFrame;
947
-
948
- // Utility functions
949
- debounce: typeof debounce;
950
- throttle: typeof throttle;
951
-
952
- // State management
953
- createStateManager: typeof createStateManager;
954
- syncState: typeof syncState;
955
- globalStateManager: typeof globalStateManager;
956
-
957
- // Performance
958
- createPerformanceMonitor: typeof createPerformanceMonitor;
959
- globalPerformanceMonitor: typeof globalPerformanceMonitor;
960
-
961
- // HMR
962
- createHMRClient: typeof createHMRClient;
963
- enableHMR: typeof enableHMR;
964
- createHotContext: typeof createHotContext;
965
-
966
- // Intersection Observer
967
- observeVisibility: typeof observeVisibility;
968
- lazyLoad: typeof lazyLoad;
969
-
970
- // Constants
971
- DEFAULT_HYDRATION_SELECTOR: typeof DEFAULT_HYDRATION_SELECTOR;
972
- componentRegistry: typeof componentRegistry;
973
- globalEventManager: typeof globalEventManager;
974
- };
975
-
976
- declare const _unused: typeof coherentClient;
532
+ /** Hot context for a module: `createHotContext(import.meta.url)` */
533
+ export function createHotContext(moduleId: string): HotContext;