a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 15 kB view raw
1export type CleanupFunction = () => void; 2 3export type Scope = Record<string, unknown>; 4 5export type EvaluateOpts = { unwrapSignals?: boolean }; 6 7/** 8 * Context object available to all bindings 9 */ 10export type BindingContext = { element: Element; scope: Scope; cleanups: CleanupFunction[] }; 11 12/** 13 * Context object provided to plugin handlers. 14 * Contains utilities and references for implementing custom bindings. 15 */ 16export interface PluginContext { 17 /** 18 * The DOM element the plugin is bound to 19 */ 20 element: Element; 21 22 /** 23 * The scope object containing signals and data 24 */ 25 scope: Scope; 26 27 /** 28 * Register a cleanup function to be called on unmount. 29 * Plugins should use this to clean up subscriptions, event listeners, etc. 30 */ 31 addCleanup(fn: CleanupFunction): void; 32 33 /** 34 * Find a signal in the scope by property path. 35 * Returns undefined if not found or if the value is not a signal. 36 */ 37 findSignal(path: string): Signal<unknown> | undefined; 38 39 /** 40 * Evaluate an expression against the scope. 41 * Handles simple property paths, literals, and signal unwrapping. 42 */ 43 evaluate(expression: string, options?: EvaluateOpts): unknown; 44 45 /** 46 * Lifecycle hooks for plugin-specific mount/unmount behavior 47 */ 48 lifecycle: PluginLifecycle; 49} 50 51/** 52 * Plugin handler function signature. 53 * Receives context and the attribute value, performs binding setup. 54 */ 55export type PluginHandler = (context: PluginContext, value: string) => void; 56 57/** 58 * A reactive primitive that notifies subscribers when its value changes. 59 */ 60export interface Signal<T> { 61 /** 62 * Get the current value of the signal. 63 */ 64 get(): T; 65 66 /** 67 * Update the signal's value. 68 * 69 * If the new value differs from the current value, subscribers will be notified. 70 */ 71 set(value: T): void; 72 73 /** 74 * Subscribe to changes in the signal's value. 75 * 76 * The callback is invoked with the new value whenever it changes. 77 * 78 * Returns an unsubscribe function to remove the subscription. 79 */ 80 subscribe(callback: (value: T) => void): () => void; 81} 82 83/** 84 * A computed signal that derives its value from other signals. 85 */ 86export interface ComputedSignal<T> { 87 /** 88 * Get the current computed value. 89 */ 90 get(): T; 91 92 /** 93 * Subscribe to changes in the computed value. 94 * 95 * Returns an unsubscribe function to remove the subscription. 96 */ 97 subscribe(callback: (value: T) => void): () => void; 98} 99 100/** 101 * Utility type to unwrap reactive proxies and get the original type. 102 * Since reactive() returns a transparent proxy, this is mostly for documentation. 103 */ 104export type UnwrapReactive<T> = T extends object ? T : never; 105 106/** 107 * Utility type for reactive arrays with enhanced type safety. 108 */ 109export type ReactiveArray<T> = T[]; 110 111/** 112 * Utility type to check if a value is reactive (has the __v_isReactive marker). 113 */ 114export type IsReactive<T> = T extends { __v_isReactive: true } ? true : false; 115 116/** 117 * Storage adapter interface for custom persistence backends 118 */ 119export interface StorageAdapter { 120 get(key: string): Promise<unknown> | unknown; 121 set(key: string, value: unknown): Promise<void> | void; 122 remove(key: string): Promise<void> | void; 123} 124 125/** 126 * Information about a mounted Volt root after charging 127 * 128 * element: The root element that was mounted 129 * scope: The reactive scope created for this root 130 * cleanup: Cleanup function to unmount this root 131 */ 132export type ChargedRoot = { element: Element; scope: Scope; cleanup: CleanupFunction }; 133 134/** 135 * Result of charging Volt roots 136 * 137 * roots: Array of all charged roots 138 * cleanup: Cleanup function to unmount all roots 139 */ 140export type ChargeResult = { roots: ChargedRoot[]; cleanup: CleanupFunction }; 141 142export type Dep = { get: () => unknown; subscribe: (callback: (value: unknown) => void) => () => void }; 143 144/** 145 * Options for configuring async effects 146 */ 147export interface AsyncEffectOptions { 148 /** 149 * Enable automatic AbortController integration. 150 * When true, provides an AbortSignal to the effect function for canceling async operations. 151 */ 152 abortable?: boolean; 153 154 /** 155 * Debounce delay in milliseconds. 156 * Effect execution is delayed until this duration has passed without dependencies changing. 157 */ 158 debounce?: number; 159 160 /** 161 * Throttle delay in milliseconds. 162 * Effect execution is rate-limited to at most once per this duration. 163 */ 164 throttle?: number; 165 166 /** 167 * Error handler for async effect failures. 168 * Receives the error and a retry function. 169 */ 170 onError?: (error: Error, retry: () => void) => void; 171 172 /** 173 * Number of automatic retry attempts on error. 174 * Defaults to 0 (no retries). 175 */ 176 retries?: number; 177 178 /** 179 * Delay in milliseconds between retry attempts. 180 * Defaults to 0 (immediate retry). 181 */ 182 retryDelay?: number; 183} 184 185/** 186 * Async effect function signature. 187 * Receives an optional AbortSignal when abortable option is enabled. 188 * Can return a cleanup function or a Promise that resolves to a cleanup function. 189 */ 190export type AsyncEffectFunction = (signal?: AbortSignal) => Promise<void | (() => void)>; 191 192export type LifecycleHookCallback = () => void; 193export type MountHookCallback = (root: Element, scope: Scope) => void; 194export type UnmountHookCallback = (root: Element) => void; 195export type ElementMountHookCallback = (element: Element, scope: Scope) => void; 196export type ElementUnmountHookCallback = (element: Element) => void; 197export type BindingHookCallback = (element: Element, bindingName: string) => void; 198export type GlobalHookName = "beforeMount" | "afterMount" | "beforeUnmount" | "afterUnmount"; 199 200/** 201 * Extended plugin context with lifecycle hooks 202 */ 203export interface PluginLifecycle { 204 /** 205 * Register a callback to run when the plugin is initialized for an element 206 */ 207 onMount: (callback: LifecycleHookCallback) => void; 208 209 /** 210 * Register a callback to run when the element is being unmounted 211 */ 212 onUnmount: (callback: LifecycleHookCallback) => void; 213 214 /** 215 * Register a callback to run before the binding is created 216 */ 217 beforeBinding: (callback: LifecycleHookCallback) => void; 218 219 /** 220 * Register a callback to run after the binding is created 221 */ 222 afterBinding: (callback: LifecycleHookCallback) => void; 223} 224 225export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; 226 227/** 228 * Strategies for swapping response content into the DOM 229 * 230 * - innerHTML: Replace the target's inner HTML (default) 231 * - outerHTML: Replace the target element entirely 232 * - beforebegin: Insert before the target element 233 * - afterbegin: Insert at the start of the target's content 234 * - beforeend: Insert at the end of the target's content 235 * - afterend: Insert after the target element 236 * - delete: Remove the target element 237 * - none: No DOM update (for side effects only) 238 */ 239export type SwapStrategy = 240 | "innerHTML" 241 | "outerHTML" 242 | "beforebegin" 243 | "afterbegin" 244 | "beforeend" 245 | "afterend" 246 | "delete" 247 | "none"; 248 249export type RequestConfig = { 250 method: HttpMethod; 251 url: string; 252 headers?: Record<string, string>; 253 body?: string | FormData; 254 target?: string | Element; 255 swap?: SwapStrategy; 256}; 257 258export type HttpResponse = { 259 status: number; 260 statusText: string; 261 headers: Headers; 262 html?: string; 263 json?: unknown; 264 ok: boolean; 265}; 266 267/** 268 * Configuration parsed from element attributes 269 */ 270export type ParsedHttpConfig = { 271 trigger: string; 272 target: string | Element; 273 swap: SwapStrategy; 274 headers: Record<string, string>; 275 retry?: RetryConfig; 276 indicator?: string; 277}; 278 279/** 280 * Retry configuration for HTTP requests 281 */ 282export type RetryConfig = { 283 /** 284 * Maximum number of retry attempts 285 */ 286 maxAttempts: number; 287 288 /** 289 * Initial delay in milliseconds before first retry 290 */ 291 initialDelay: number; 292}; 293 294export type HydrateOptions = { rootSelector?: string; skipHydrated?: boolean }; 295 296/** 297 * Serialized scope data structure for SSR 298 */ 299export type SerializedScope = Record<string, unknown>; 300 301export type HydrateResult = ChargeResult; 302 303/** 304 * Element-level lifecycle tracking for per-element hooks 305 */ 306export type ElementLifecycleState = { 307 isMounted: boolean; 308 bindings: Set<string>; 309 onMount: Set<() => void>; 310 onUnmount: Set<() => void>; 311}; 312 313export type AnySignal = Signal<unknown> | ComputedSignal<unknown>; 314 315export type GraphNode = { 316 signal: AnySignal; 317 id: string; 318 name?: string; 319 type: SignalType; 320 value: unknown; 321 dependencies: string[]; 322 dependents: string[]; 323}; 324 325export type DepGraph = { nodes: GraphNode[]; edges: Array<{ from: string; to: string }> }; 326 327export type SignalType = "signal" | "computed" | "reactive"; 328 329export type SignalMetadata = { id: string; type: SignalType; name?: string; createdAt: number; stackTrace?: string }; 330 331export type FormControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; 332 333/** 334 * Throttle/Debounced function 335 */ 336export type TimedFunction<A extends unknown[]> = ((...args: A) => void) & { cancel: () => void }; 337 338/** 339 * Represents a parsed modifier with optional numeric value 340 */ 341export type Modifier = { name: string; value?: number }; 342 343/** 344 * Result of parsing an attribute name with modifiers 345 */ 346export type ParsedAttribute = { baseName: string; modifiers: Modifier[] }; 347 348/** 349 * Registry mapping pin names to DOM elements within a scope 350 */ 351export type PinRegistry = Map<string, Element>; 352 353/** 354 * Metadata associated with a reactive scope. 355 * Stored externally via WeakMap to avoid polluting scope object. 356 */ 357export type ScopeMetadata = { 358 /** 359 * The root element that owns this scope 360 */ 361 origin: Element; 362 363 /** 364 * Registry of pinned elements (data-volt-pin) 365 */ 366 pins: PinRegistry; 367 368 /** 369 * Counter for generating unique IDs within this scope 370 */ 371 uidCounter: number; 372 373 /** 374 * Optional parent scope reference (for debugging/inspection) 375 */ 376 parent?: Scope; 377}; 378 379/** 380 * Global reactive store interface. 381 * Holds signals accessible across all scopes via $store. 382 */ 383export interface GlobalStore { 384 /** 385 * Internal signal registry 386 */ 387 readonly _signals: Map<string, Signal<unknown>>; 388 389 /** 390 * Get a signal value from the store 391 */ 392 get<T = unknown>(key: string): T | undefined; 393 394 /** 395 * Set a signal value in the store. 396 * Creates a new signal if the key doesn't exist. 397 */ 398 set<T = unknown>(key: string, value: T): void; 399 400 /** 401 * Check if a key exists in the store 402 */ 403 has(key: string): boolean; 404 405 /** 406 * Access signals directly (for advanced use) 407 */ 408 [key: string]: unknown; 409} 410 411/** 412 * Function signature for $pulse() - microtask scheduler 413 */ 414export type PulseFunction = (callback: () => void) => void; 415 416/** 417 * Function signature for $uid() - unique ID generator 418 */ 419export type UidFunction = (prefix?: string) => string; 420 421/** 422 * Function signature for $arc() - CustomEvent dispatcher 423 */ 424export type ArcFunction = (eventName: string, detail?: unknown) => void; 425 426/** 427 * Function signature for $probe() - reactive observer 428 */ 429export type ProbeFunction = (expression: string, callback: (value: unknown) => void) => CleanupFunction; 430 431/** 432 * Configuration for a single transition phase (enter or leave) 433 */ 434export type TransitionPhase = { 435 /** 436 * Initial CSS properties (applied immediately) 437 */ 438 from?: Record<string, string | number>; 439 440 /** 441 * Target CSS properties (animated to) 442 */ 443 to?: Record<string, string | number>; 444 445 /** 446 * Duration in milliseconds (default: 300) 447 */ 448 duration?: number; 449 450 /** 451 * Delay in milliseconds (default: 0) 452 */ 453 delay?: number; 454 455 /** 456 * CSS easing function (default: 'ease') 457 */ 458 easing?: string; 459 460 /** 461 * CSS classes to apply during this phase 462 */ 463 classes?: string[]; 464}; 465 466/** 467 * Complete transition preset with enter and leave phases 468 */ 469export type TransitionPreset = { 470 /** 471 * Configuration for enter transition 472 */ 473 enter: TransitionPhase; 474 475 /** 476 * Configuration for leave transition 477 */ 478 leave: TransitionPhase; 479}; 480 481/** 482 * Parsed transition value with preset and modifiers 483 */ 484export type ParsedTransition = { 485 /** 486 * The transition preset to use 487 */ 488 preset: TransitionPreset; 489 490 /** 491 * Override duration from preset syntax (e.g., "fade.500") 492 */ 493 duration?: number; 494 495 /** 496 * Override delay from preset syntax (e.g., "fade.500.100") 497 */ 498 delay?: number; 499}; 500 501/** 502 * Animation preset for CSS keyframe animations 503 */ 504export type AnimationPreset = { 505 /** 506 * Array of keyframes for the animation 507 */ 508 keyframes: Keyframe[]; 509 510 /** 511 * Duration in milliseconds (default: varies by preset) 512 */ 513 duration: number; 514 515 /** 516 * Number of iterations (use Infinity for infinite) 517 */ 518 iterations: number; 519 520 /** 521 * CSS timing function (default: "ease") 522 */ 523 timing: string; 524}; 525 526/** 527 * Options for configuring a view transition 528 */ 529export type ViewTransitionOptions = { 530 /** 531 * Named view transition for specific element(s) 532 * Maps to view-transition-name CSS property 533 */ 534 name?: string; 535 536 /** 537 * Elements to apply named transitions to 538 * Each element will get a unique view-transition-name 539 */ 540 elements?: HTMLElement[]; 541 542 /** 543 * Skip transition if prefers-reduced-motion is enabled 544 * @default true 545 */ 546 respectReducedMotion?: boolean; 547 548 /** 549 * Force CSS fallback even if View Transitions API is supported 550 * Useful for testing or debugging 551 * @default false 552 */ 553 forceFallback?: boolean; 554}; 555 556/** 557 * Error source categories for identifying where errors occurred 558 */ 559export type ErrorSource = "evaluator" | "binding" | "effect" | "http" | "plugin" | "lifecycle" | "charge" | "user"; 560 561/** 562 * Error severity level 563 * 564 * - `warn`: Non-critical issues that don't prevent operation (e.g., deprecated usage, missing optional features) 565 * - `error`: Recoverable errors that prevent specific operations (e.g., failed evaluations, missing elements) 566 * - `fatal`: Unrecoverable errors that should halt execution (e.g., critical initialization failures) 567 */ 568export type ErrorLevel = "warn" | "error" | "fatal"; 569 570/** 571 * Context information for error reporting 572 */ 573export type ErrorContext = { 574 /** Error source category */ 575 source: ErrorSource; 576 /** Error severity level (defaults to "error") */ 577 level?: ErrorLevel; 578 /** DOM element where error occurred */ 579 element?: HTMLElement; 580 /** Directive name (e.g., "data-volt-text", "data-volt-on-click") */ 581 directive?: string; 582 /** Expression that failed */ 583 expression?: string; 584 /** Plugin name (for plugin errors) */ 585 pluginName?: string; 586 /** HTTP method and URL (for HTTP errors) */ 587 httpMethod?: string; 588 httpUrl?: string; 589 httpStatus?: number; 590 /** Lifecycle hook name (for lifecycle errors) */ 591 hookName?: string; 592 /** Additional custom context */ 593 [key: string]: unknown; 594}; 595 596/** 597 * Error handler function signature 598 */ 599export type ErrorHandler = (error: VoltError) => void;