- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
616 lines
18 KiB
Markdown
616 lines
18 KiB
Markdown
# LiveComponents Error Recovery Strategy
|
|
|
|
## Error Categories
|
|
|
|
### 1. Component Errors
|
|
- Unknown component name
|
|
- Invalid component ID format
|
|
- Component instantiation failure
|
|
- Missing dependencies
|
|
|
|
### 2. Action Errors
|
|
- Unknown action method
|
|
- Action method not callable
|
|
- Invalid action parameters
|
|
- Action execution exception
|
|
|
|
### 3. State Errors
|
|
- Invalid state format
|
|
- State deserialization failure
|
|
- State validation error
|
|
- State version conflict
|
|
|
|
### 4. Rendering Errors
|
|
- Template not found
|
|
- Template syntax error
|
|
- Rendering exception
|
|
- Cache write failure
|
|
|
|
### 5. Upload Errors
|
|
- File validation failure
|
|
- Upload size limit exceeded
|
|
- Invalid file type
|
|
- Storage write failure
|
|
|
|
### 6. Lifecycle Errors
|
|
- onMount() exception
|
|
- onUpdate() exception
|
|
- onDestroy() exception
|
|
|
|
## Error Handling Strategy
|
|
|
|
### Server-Side Error Handling
|
|
|
|
#### 1. Component Resolution Errors
|
|
|
|
**Current Behavior:**
|
|
```php
|
|
public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract
|
|
{
|
|
if (!$className) {
|
|
throw new \InvalidArgumentException("Unknown component: {$componentName}");
|
|
}
|
|
}
|
|
```
|
|
|
|
**Improved Error Recovery:**
|
|
```php
|
|
use App\Framework\Exception\FrameworkException;
|
|
use App\Framework\Exception\ErrorCode;
|
|
|
|
public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract
|
|
{
|
|
[$componentName, $instanceId] = $this->parseComponentId($componentId->toString());
|
|
|
|
$className = $this->nameToClassMap[$componentName] ?? null;
|
|
|
|
if (!$className) {
|
|
// Find similar component names (Levenshtein distance)
|
|
$suggestions = $this->findSimilarComponents($componentName);
|
|
|
|
throw FrameworkException::create(
|
|
ErrorCode::COMPONENT_NOT_FOUND,
|
|
"Unknown component: '{$componentName}'"
|
|
)->withData([
|
|
'component_name' => $componentName,
|
|
'instance_id' => $instanceId,
|
|
'suggestions' => $suggestions,
|
|
'available_components' => array_keys($this->nameToClassMap)
|
|
]);
|
|
}
|
|
|
|
try {
|
|
return $this->container->invoker->make($className, [
|
|
'id' => $componentId,
|
|
'initialData' => $state
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::COMPONENT_INSTANTIATION_FAILED,
|
|
"Failed to instantiate component '{$componentName}'"
|
|
)->withData([
|
|
'component_class' => $className,
|
|
'error' => $e->getMessage()
|
|
])->withPrevious($e);
|
|
}
|
|
}
|
|
|
|
private function findSimilarComponents(string $componentName): array
|
|
{
|
|
$suggestions = [];
|
|
|
|
foreach (array_keys($this->nameToClassMap) as $availableName) {
|
|
$distance = levenshtein($componentName, $availableName);
|
|
|
|
if ($distance <= 3) { // Max 3 character difference
|
|
$suggestions[] = $availableName;
|
|
}
|
|
}
|
|
|
|
return $suggestions;
|
|
}
|
|
```
|
|
|
|
#### 2. Action Execution Errors
|
|
|
|
**Enhanced LiveComponentHandler:**
|
|
```php
|
|
public function handle(
|
|
LiveComponentContract $component,
|
|
string $actionName,
|
|
ActionParameters $params
|
|
): ComponentUpdate {
|
|
// Validate action exists
|
|
if (!method_exists($component, $actionName)) {
|
|
$availableActions = $this->getAvailableActions($component);
|
|
|
|
throw FrameworkException::create(
|
|
ErrorCode::ACTION_NOT_FOUND,
|
|
"Unknown action '{$actionName}' on component " . get_class($component)
|
|
)->withData([
|
|
'action_name' => $actionName,
|
|
'available_actions' => $availableActions,
|
|
'component_class' => get_class($component)
|
|
]);
|
|
}
|
|
|
|
// Validate action is public and non-static
|
|
$reflection = new \ReflectionMethod($component, $actionName);
|
|
|
|
if (!$reflection->isPublic() || $reflection->isStatic()) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::ACTION_NOT_CALLABLE,
|
|
"Action '{$actionName}' is not a public instance method"
|
|
)->withData([
|
|
'action_name' => $actionName,
|
|
'is_public' => $reflection->isPublic(),
|
|
'is_static' => $reflection->isStatic()
|
|
]);
|
|
}
|
|
|
|
// Execute with error recovery
|
|
try {
|
|
$result = $this->container->invoker->invoke($component, $actionName, $params->toArray());
|
|
|
|
// Validate result type
|
|
if (!$result instanceof ComponentData) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::ACTION_INVALID_RETURN,
|
|
"Action '{$actionName}' must return ComponentData"
|
|
)->withData([
|
|
'action_name' => $actionName,
|
|
'returned_type' => get_debug_type($result)
|
|
]);
|
|
}
|
|
|
|
return ComponentUpdate::fromActionResult($component, $result);
|
|
|
|
} catch (FrameworkException $e) {
|
|
// Re-throw framework exceptions
|
|
throw $e;
|
|
} catch (\Throwable $e) {
|
|
// Wrap other exceptions
|
|
throw FrameworkException::create(
|
|
ErrorCode::ACTION_EXECUTION_FAILED,
|
|
"Action '{$actionName}' execution failed"
|
|
)->withData([
|
|
'action_name' => $actionName,
|
|
'component_class' => get_class($component),
|
|
'error_message' => $e->getMessage(),
|
|
'error_type' => get_class($e)
|
|
])->withPrevious($e);
|
|
}
|
|
}
|
|
|
|
private function getAvailableActions(LiveComponentContract $component): array
|
|
{
|
|
$reflection = new \ReflectionClass($component);
|
|
$actions = [];
|
|
|
|
foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
|
if ($method->isStatic() || $method->isConstructor()) {
|
|
continue;
|
|
}
|
|
|
|
// Skip getter/setter/lifecycle methods
|
|
if (in_array($method->getName(), ['getId', 'getData', 'getRenderData', 'onMount', 'onUpdate', 'onDestroy'])) {
|
|
continue;
|
|
}
|
|
|
|
// Check return type
|
|
$returnType = $method->getReturnType();
|
|
if ($returnType instanceof \ReflectionNamedType &&
|
|
$returnType->getName() === ComponentData::class) {
|
|
$actions[] = $method->getName();
|
|
}
|
|
}
|
|
|
|
return $actions;
|
|
}
|
|
```
|
|
|
|
#### 3. State Deserialization Errors
|
|
|
|
**Safe State Handling:**
|
|
```php
|
|
#[Route('/live-component/{id}', method: Method::POST)]
|
|
public function handleAction(string $id, HttpRequest $request): JsonResult
|
|
{
|
|
try {
|
|
$action = ComponentAction::fromRequest($request, $id);
|
|
} catch (\Throwable $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Invalid action format',
|
|
'details' => $e->getMessage()
|
|
], Status::BAD_REQUEST);
|
|
}
|
|
|
|
// Safe state deserialization
|
|
try {
|
|
$stateArray = $request->parsedBody?->get('state', []);
|
|
|
|
if (!is_array($stateArray)) {
|
|
throw new \InvalidArgumentException('State must be an array');
|
|
}
|
|
|
|
$state = ComponentData::fromArray($stateArray);
|
|
} catch (\Throwable $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Invalid state format',
|
|
'details' => $e->getMessage(),
|
|
'recovery' => 'Client should reload component from server'
|
|
], Status::BAD_REQUEST);
|
|
}
|
|
|
|
// Component resolution with error recovery
|
|
try {
|
|
$component = $this->componentRegistry->resolve($action->componentId, $state);
|
|
} catch (FrameworkException $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
'data' => $e->getData(),
|
|
'recovery' => 'Check component name and reload page'
|
|
], Status::NOT_FOUND);
|
|
}
|
|
|
|
// Action handling with error recovery
|
|
try {
|
|
$update = $this->handler->handle($component, $action->method, $action->params);
|
|
} catch (FrameworkException $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
'data' => $e->getData(),
|
|
'recovery' => 'Try again or reload component'
|
|
], Status::UNPROCESSABLE_ENTITY);
|
|
} catch (\Throwable $e) {
|
|
// Unexpected errors
|
|
error_log("LiveComponent action error: " . $e->getMessage());
|
|
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Internal server error',
|
|
'recovery' => 'Please reload the page',
|
|
'error_id' => uniqid('lc_error_') // For support
|
|
], Status::INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
// Rendering with error recovery
|
|
try {
|
|
$updatedComponent = $this->componentRegistry->resolve(
|
|
$action->componentId,
|
|
ComponentData::fromArray($update->state->data)
|
|
);
|
|
|
|
$html = $this->componentRegistry->render($updatedComponent);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'html' => $html,
|
|
'state' => $update->state->toArray(),
|
|
'events' => $update->events
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
error_log("LiveComponent render error: " . $e->getMessage());
|
|
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Component rendering failed',
|
|
'state' => $update->state->toArray(), // Return state so client can retry
|
|
'recovery' => 'Try again',
|
|
'error_id' => uniqid('lc_render_')
|
|
], Status::INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. Lifecycle Hook Errors
|
|
|
|
**Safe Lifecycle Hook Execution:**
|
|
```php
|
|
// In LiveComponentHandler
|
|
public function callMountHook(LiveComponentContract $component): void
|
|
{
|
|
if (!$component instanceof LifecycleAware) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$component->onMount();
|
|
} catch (\Throwable $e) {
|
|
// Log but don't fail - lifecycle hooks are side effects only
|
|
error_log(sprintf(
|
|
"Lifecycle hook onMount() failed for %s: %s",
|
|
get_class($component),
|
|
$e->getMessage()
|
|
));
|
|
|
|
// Optional: Dispatch error event for monitoring
|
|
$this->eventDispatcher?->dispatch(
|
|
new ComponentLifecycleErrorEvent(
|
|
componentId: $component->getId(),
|
|
hook: 'onMount',
|
|
error: $e
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
private function callUpdateHook(LiveComponentContract $component): void
|
|
{
|
|
if (!$component instanceof LifecycleAware) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$component->onUpdate();
|
|
} catch (\Throwable $e) {
|
|
error_log(sprintf(
|
|
"Lifecycle hook onUpdate() failed for %s: %s",
|
|
get_class($component),
|
|
$e->getMessage()
|
|
));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Client-Side Error Recovery
|
|
|
|
#### JavaScript Error Handling
|
|
|
|
**Enhanced LiveComponents Client:**
|
|
```javascript
|
|
class LiveComponentManager {
|
|
async handleAction(componentId, actionName, params = {}) {
|
|
const component = this.components.get(componentId);
|
|
if (!component) {
|
|
console.error(`Component not found: ${componentId}`);
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
component.element.classList.add('lc-loading');
|
|
|
|
try {
|
|
const response = await this.sendAction(componentId, actionName, params);
|
|
|
|
if (!response.success) {
|
|
this.handleError(component, response);
|
|
return;
|
|
}
|
|
|
|
// Update component
|
|
this.updateComponent(component, response);
|
|
|
|
} catch (error) {
|
|
this.handleNetworkError(component, error);
|
|
} finally {
|
|
component.element.classList.remove('lc-loading');
|
|
}
|
|
}
|
|
|
|
handleError(component, response) {
|
|
const { error, recovery, data } = response;
|
|
|
|
console.error(`Component error: ${error}`, data);
|
|
|
|
// Show user-friendly error message
|
|
this.showErrorMessage(component.element, error);
|
|
|
|
// Implement recovery strategy
|
|
switch (recovery) {
|
|
case 'Try again':
|
|
this.showRetryButton(component);
|
|
break;
|
|
|
|
case 'Client should reload component from server':
|
|
this.reloadComponent(component);
|
|
break;
|
|
|
|
case 'Check component name and reload page':
|
|
this.showReloadPageMessage(component);
|
|
break;
|
|
|
|
default:
|
|
// Unknown recovery - show generic error
|
|
this.showGenericError(component);
|
|
}
|
|
|
|
// Dispatch error event for custom handling
|
|
component.element.dispatchEvent(new CustomEvent('lc:error', {
|
|
detail: { error, recovery, data },
|
|
bubbles: true
|
|
}));
|
|
}
|
|
|
|
handleNetworkError(component, error) {
|
|
console.error('Network error:', error);
|
|
|
|
// Retry logic with exponential backoff
|
|
if (component.retryCount < 3) {
|
|
const delay = Math.pow(2, component.retryCount) * 1000;
|
|
component.retryCount++;
|
|
|
|
setTimeout(() => {
|
|
console.log(`Retrying action (attempt ${component.retryCount})...`);
|
|
// Retry last action
|
|
this.handleAction(
|
|
component.id,
|
|
component.lastAction,
|
|
component.lastParams
|
|
);
|
|
}, delay);
|
|
} else {
|
|
this.showErrorMessage(
|
|
component.element,
|
|
'Network error. Please check your connection and reload the page.'
|
|
);
|
|
}
|
|
}
|
|
|
|
reloadComponent(component) {
|
|
// Reload component from server with current state
|
|
fetch(`/live-component/${component.id}/reload`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ state: component.state })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
this.updateComponent(component, data);
|
|
} else {
|
|
this.showReloadPageMessage(component);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.showReloadPageMessage(component);
|
|
});
|
|
}
|
|
|
|
showRetryButton(component) {
|
|
const retryHtml = `
|
|
<div class="lc-error-retry">
|
|
<p>Action failed. Would you like to try again?</p>
|
|
<button onclick="this.closest('.lc-error-retry').remove();
|
|
window.liveComponents.retry('${component.id}')">
|
|
Retry
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
component.element.insertAdjacentHTML('beforeend', retryHtml);
|
|
}
|
|
|
|
retry(componentId) {
|
|
const component = this.components.get(componentId);
|
|
if (component && component.lastAction) {
|
|
this.handleAction(
|
|
component.id,
|
|
component.lastAction,
|
|
component.lastParams
|
|
);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Codes
|
|
|
|
### Component Errors (COMP_*)
|
|
- `COMP_NOT_FOUND` - Component name not registered
|
|
- `COMP_INSTANTIATION_FAILED` - Failed to create component instance
|
|
- `COMP_INVALID_ID` - Invalid component ID format
|
|
|
|
### Action Errors (ACTION_*)
|
|
- `ACTION_NOT_FOUND` - Action method does not exist
|
|
- `ACTION_NOT_CALLABLE` - Action is not public or is static
|
|
- `ACTION_INVALID_PARAMS` - Invalid action parameters
|
|
- `ACTION_EXECUTION_FAILED` - Action threw exception
|
|
- `ACTION_INVALID_RETURN` - Action did not return ComponentData
|
|
|
|
### State Errors (STATE_*)
|
|
- `STATE_INVALID_FORMAT` - State is not valid JSON/array
|
|
- `STATE_DESERIALIZATION_FAILED` - Failed to parse state
|
|
- `STATE_VALIDATION_FAILED` - State validation error
|
|
- `STATE_VERSION_CONFLICT` - Optimistic locking conflict
|
|
|
|
### Rendering Errors (RENDER_*)
|
|
- `RENDER_TEMPLATE_NOT_FOUND` - Template file does not exist
|
|
- `RENDER_TEMPLATE_SYNTAX` - Template has syntax error
|
|
- `RENDER_EXCEPTION` - Rendering threw exception
|
|
- `RENDER_CACHE_WRITE_FAILED` - Failed to write to cache
|
|
|
|
## Testing Error Recovery
|
|
|
|
### Unit Tests
|
|
```php
|
|
it('suggests similar component names on not found error', function () {
|
|
$registry = new ComponentRegistry(...);
|
|
|
|
try {
|
|
$registry->resolve(ComponentId::fromString('conter:test'), null);
|
|
expect(false)->toBeTrue(); // Should have thrown
|
|
} catch (FrameworkException $e) {
|
|
expect($e->getData()['suggestions'])->toContain('counter');
|
|
}
|
|
});
|
|
|
|
it('lists available actions on unknown action error', function () {
|
|
$handler = new LiveComponentHandler(...);
|
|
$component = new CounterComponent(...);
|
|
|
|
try {
|
|
$handler->handle($component, 'unknownAction', ActionParameters::empty());
|
|
expect(false)->toBeTrue();
|
|
} catch (FrameworkException $e) {
|
|
expect($e->getData()['available_actions'])->toContain('increment');
|
|
expect($e->getData()['available_actions'])->toContain('decrement');
|
|
}
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
```php
|
|
it('recovers from state deserialization error', function () {
|
|
$response = $this->post('/live-component/counter:test', [
|
|
'action' => 'increment',
|
|
'state' => 'invalid json' // Invalid state
|
|
]);
|
|
|
|
expect($response->status)->toBe(Status::BAD_REQUEST);
|
|
expect($response->json('error'))->toBe('Invalid state format');
|
|
expect($response->json('recovery'))->toContain('reload');
|
|
});
|
|
|
|
it('returns state on rendering error', function () {
|
|
// Component that fails rendering but action succeeded
|
|
$response = $this->post('/live-component/broken:test', [
|
|
'action' => 'increment',
|
|
'state' => ['count' => 5]
|
|
]);
|
|
|
|
expect($response->json('success'))->toBeFalse();
|
|
expect($response->json('state'))->toBeArray();
|
|
expect($response->json('recovery'))->toBe('Try again');
|
|
});
|
|
```
|
|
|
|
## Monitoring
|
|
|
|
### Error Metrics
|
|
- `livecomponent.error.rate` - Errors per second by type
|
|
- `livecomponent.error.component` - Errors by component name
|
|
- `livecomponent.error.action` - Errors by action name
|
|
- `livecomponent.error.recovery_success_rate` - Recovery success rate
|
|
|
|
### Alerts
|
|
- Error rate > 5% - Warning
|
|
- Error rate > 10% - Critical
|
|
- Specific component error rate > 20% - Component issue
|
|
- Network error rate > 10% - Infrastructure issue
|
|
|
|
## Summary
|
|
|
|
✅ **Implemented:**
|
|
- Component not found suggestions
|
|
- Available actions listing
|
|
- Safe state deserialization
|
|
- Lifecycle hook error handling
|
|
- Client-side retry logic
|
|
|
|
⚠️ **To Implement:**
|
|
- Error code enum (FrameworkException integration)
|
|
- Client-side error recovery UI
|
|
- Comprehensive error testing
|
|
- Error monitoring dashboard
|
|
|
|
📋 **Phase 1 Requirements:**
|
|
- Implement all error codes
|
|
- Add client-side error boundaries
|
|
- Comprehensive error tests
|
|
- Error monitoring integration
|