Add lifecycle hooks, HMR persistence, and singleton components#68
Merged
MarcosBrendonDePaula merged 3 commits intomainfrom Feb 27, 2026
Merged
Conversation
…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
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
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
publicActionsentries.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 initializationonDisconnect()- Called when connection drops unexpectedly (before destroy)onDestroy()- Called before internal cleanup; replaces constructor workaroundsonStateChange(changes)- Called after any state mutation (proxy or setState)onRoomJoin(roomId)/onRoomLeave(roomId)- Called when joining/leaving roomsonRehydrate(previousState)- Called after state is restored from localStorageonAction(action, payload)- Called before action execution; can returnfalseto cancelAll hooks are optional, error-safe (caught and logged), and blocked from client execution.
2. HMR Persistence (
static persistent+$persistent)static persistent = { ... }this.$persistentat runtimeglobalThisand survives hot module reloads3. Singleton Components (
static singleton = true)4. Better
publicActionsError MessagespublicActions, error message now includes:publicActions5. Security: BLOCKED_ACTIONS Expansion
onMount,onDestroy,onConnect,onDisconnect,onStateChange,onRoomJoin,onRoomLeave,onRehydrate,onAction) are blocked from client execution$persistentproperty is blocked_setEmitOverride()internal method is blockedImplementation Details
ComponentRegistry Changes
singletonsMap to track singleton instances and their connected clientsmountComponent()now checks for singleton flag and returns existing instance if foundonConnect,onMount,onRehydrate) called at appropriate pointsensureWsData()helper method to initialize WebSocket data structureLiveComponent Changes
$persistentgetter that reads/writes toglobalThis.__fluxstack_persistent_${componentName}_setEmitOverride()method for singleton broadcast supportonStateChange()after mutationsonRoomJoin()/onRoomLeave()hooksexecuteAction()callsonAction()before execution and respectsfalsereturn valueTest Coverage
Breaking Changes
None. All changes are additive and backward compatible.
Documentation
Updated
LLMD/resources/live-components.mdwith:https://claude.ai/code/session_01V12t1cDiYmAvDsMJsYaLys