Skip to content

Add lifecycle hooks, HMR persistence, and singleton components#68

Merged
MarcosBrendonDePaula merged 3 commits intomainfrom
claude/enhance-live-component-dx-wrYbV
Feb 27, 2026
Merged

Add lifecycle hooks, HMR persistence, and singleton components#68
MarcosBrendonDePaula merged 3 commits intomainfrom
claude/enhance-live-component-dx-wrYbV

Conversation

@MarcosBrendonDePaula
Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a comprehensive DX enhancement to LiveComponent with lifecycle hooks, HMR-safe persistent state, singleton component support, and improved error messages for missing publicActions entries.

Key Changes

1. Lifecycle Hooks (v1.14.0)

  • onConnect() - Called when WebSocket connection is established (before mount)
  • onMount() - Called after component is fully mounted; supports async initialization
  • onDisconnect() - Called when connection drops unexpectedly (before destroy)
  • onDestroy() - Called before internal cleanup; replaces constructor workarounds
  • onStateChange(changes) - Called after any state mutation (proxy or setState)
  • onRoomJoin(roomId) / onRoomLeave(roomId) - Called when joining/leaving rooms
  • onRehydrate(previousState) - Called after state is restored from localStorage
  • onAction(action, payload) - Called before action execution; can return false to cancel

All hooks are optional, error-safe (caught and logged), and blocked from client execution.

2. HMR Persistence (static persistent + $persistent)

  • Define shape and defaults via static persistent = { ... }
  • Access via this.$persistent at runtime
  • Data stored in globalThis and survives hot module reloads
  • Useful for caches, counters, and expensive computations that should persist across HMR

3. Singleton Components (static singleton = true)

  • Only one server-side instance exists for the component
  • All clients share the same state
  • State updates broadcast to every connected client via emit override
  • ComponentRegistry tracks singleton instances and connections
  • New clients joining a singleton receive current state and are added to broadcast list

4. Better publicActions Error Messages

  • When a method exists but is missing from publicActions, error message now includes:
    • The action name
    • The component name
    • Suggestion to add it to publicActions
  • Generic error for methods that don't exist at all

5. Security: BLOCKED_ACTIONS Expansion

  • All lifecycle hooks (onMount, onDestroy, onConnect, onDisconnect, onStateChange, onRoomJoin, onRoomLeave, onRehydrate, onAction) are blocked from client execution
  • $persistent property is blocked
  • _setEmitOverride() internal method is blocked

Implementation Details

ComponentRegistry Changes

  • Added singletons Map to track singleton instances and their connected clients
  • mountComponent() now checks for singleton flag and returns existing instance if found
  • New clients joining a singleton are added to the broadcast connections map
  • Emit override injected into singleton instances to broadcast to all connections
  • Lifecycle hooks (onConnect, onMount, onRehydrate) called at appropriate points
  • New ensureWsData() helper method to initialize WebSocket data structure

LiveComponent Changes

  • Added $persistent getter that reads/writes to globalThis.__fluxstack_persistent_${componentName}
  • Added _setEmitOverride() method for singleton broadcast support
  • All lifecycle hook methods added as protected (optional) methods
  • State proxy now calls onStateChange() after mutations
  • Room join/leave methods call onRoomJoin() / onRoomLeave() hooks
  • executeAction() calls onAction() before execution and respects false return value

Test Coverage

  • 870 lines of comprehensive tests covering all new features
  • Tests for lifecycle order, async onMount, HMR persistence across instances
  • Singleton broadcast behavior with multiple connections
  • Error message validation for missing publicActions
  • Security tests ensuring hooks cannot be called from client
  • Edge cases: errors in hooks don't break system, unchanged state doesn't trigger onStateChange

Breaking Changes

None. All changes are additive and backward compatible.

Documentation

Updated LLMD/resources/live-components.md with:

  • Lifecycle hook reference with examples
  • Lifecycle order diagram
  • HMR persistence guide with cache example
  • Singleton component pattern documentation

https://claude.ai/code/session_01V12t1cDiYmAvDsMJsYaLys

…ngletons, better errors

1. Lifecycle hooks: onMount() (async) and onDestroy() (sync) replace
   constructor/destroy workarounds. onMount is called by the registry
   after rooms, auth, and DI are ready. onDestroy is called before
   internal cleanup in destroy().

2. HMR persistence: `static persistent` + `this.$persistent` stores
   data in globalThis that survives hot module reloads. Each component
   class has its own namespace.

3. Singleton components: `static singleton = true` creates one shared
   server-side instance. All clients see the same state. State changes
   broadcast to every connected WebSocket. Singletons are destroyed
   when the last client disconnects.

4. Better publicActions errors: When a method exists on the component
   but isn't listed in publicActions, the error now says exactly what
   to fix: "Add it to: static publicActions = [..., 'methodName']"

All new internals (onMount, onDestroy, $persistent, _setEmitOverride)
are added to BLOCKED_ACTIONS for security.

23 new tests, 190 total passing, 0 TypeScript errors.

https://claude.ai/code/session_01V12t1cDiYmAvDsMJsYaLys
…teChange, onRoomJoin, onRoomLeave, onRehydrate, onAction

Complete lifecycle hooks for LiveComponent:

- onConnect(): fires when WebSocket is established, before onMount
- onDisconnect(): fires on unexpected connection loss (NOT on intentional unmount)
- onStateChange(changes): fires after every state mutation (proxy or setState)
- onRoomJoin(roomId): fires after $room.join()
- onRoomLeave(roomId): fires after $room.leave()
- onRehydrate(previousState): fires after state is restored from localStorage
- onAction(action, payload): fires before action execution, return false to cancel

Lifecycle order: onConnect → onMount → [onAction/onStateChange/onRoom*] → onDisconnect → onDestroy

All hooks are:
- Optional (override only what you need)
- Error-safe (caught and logged, never break the system)
- Blocked from client (in BLOCKED_ACTIONS)

37 tests, 204 total passing, 0 TypeScript errors.

https://claude.ai/code/session_01V12t1cDiYmAvDsMJsYaLys
Comprehensive edge case and integration tests covering:
- Error safety for onConnect, onDisconnect, onRoomJoin, onRoomLeave
- Async onAction (cancel and allow)
- onAction payload verification and selective cancellation
- onStateChange with multiple rapid changes, function updaters, and during actions
- onMount and onRehydrate state modification
- $persistent deep object mutations and runtime key addition
- Security: _ prefix blocking, # prefix blocking, no publicActions, Object.prototype
- Singleton setState broadcasts and action-triggered state sync
- $room default handle, $rooms tracking, idempotent join
- State proxy delta emissions (per-mutation and batch)
- defaultState merge behavior
- Default no-op hooks verification
- getSerializableState correctness
- Emit message format validation
- Rehydration lifecycle order (onConnect → onRehydrate → onMount)

Total: 74 tests (37 original + 37 new), all passing. 484 tests pass across full suite.

https://claude.ai/code/session_01V12t1cDiYmAvDsMJsYaLys
@MarcosBrendonDePaula MarcosBrendonDePaula merged commit a2d7e30 into main Feb 27, 2026
11 checks passed
MarcosBrendonDePaula pushed a commit that referenced this pull request Feb 27, 2026
- onAction cancellation no longer emits ERROR to client (info leak)
- setState skips emit/hooks when values are unchanged (parity with proxy)
- Silent catch {} replaced with console.error in onStateChange, onRoomJoin, onRoomLeave
- Singleton broadcast now includes userId and room metadata
- Add onClientJoin/onClientLeave lifecycle hooks for singletons
- Singleton onDisconnect only fires when last client leaves
- New hooks added to BLOCKED_ACTIONS for security
- 19 regression tests covering all fixed bugs

https://claude.ai/code/session_01JEtihEZe9cThDAadXAp3Rp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants