Add SPA enable/disable functionality and graceful error handling
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -29,25 +29,9 @@ class Debugger {
|
||||
static _on_framework_core_init() {
|
||||
// Check if browser error logging is enabled
|
||||
if (window.rsxapp && window.rsxapp.log_browser_errors) {
|
||||
// Register global error handler
|
||||
window.addEventListener('error', function (event) {
|
||||
Debugger._handle_browser_error({
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno,
|
||||
stack: event.error ? event.error.stack : null,
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
// Register unhandled promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function (event) {
|
||||
Debugger._handle_browser_error({
|
||||
message: event.reason ? event.reason.message || String(event.reason) : 'Unhandled promise rejection',
|
||||
stack: event.reason && event.reason.stack ? event.reason.stack : null,
|
||||
type: 'unhandledrejection',
|
||||
});
|
||||
// Listen for unhandled exceptions from Rsx event system
|
||||
Rsx.on('unhandled_exception', function (error_data) {
|
||||
Debugger._handle_browser_error(error_data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ class Rsx {
|
||||
// Gets set to true to interupt startup sequence
|
||||
static __stopped = false;
|
||||
|
||||
// Timestamp of last flash alert for unhandled exceptions (rate limiting)
|
||||
static _last_exception_flash = 0;
|
||||
|
||||
// Initialize event handlers storage
|
||||
static _init_events() {
|
||||
if (typeof Rsx._event_handlers === 'undefined') {
|
||||
@@ -107,6 +110,75 @@ class Rsx {
|
||||
this.trigger('refresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup global unhandled exception handlers
|
||||
* Must be called before framework initialization begins
|
||||
*/
|
||||
static _setup_exception_handlers() {
|
||||
// Handle uncaught JavaScript errors
|
||||
window.addEventListener('error', function (event) {
|
||||
Rsx._handle_unhandled_exception({
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno,
|
||||
stack: event.error ? event.error.stack : null,
|
||||
type: 'error',
|
||||
error: event.error,
|
||||
});
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', function (event) {
|
||||
Rsx._handle_unhandled_exception({
|
||||
message: event.reason ? event.reason.message || String(event.reason) : 'Unhandled promise rejection',
|
||||
stack: event.reason && event.reason.stack ? event.reason.stack : null,
|
||||
type: 'unhandledrejection',
|
||||
error: event.reason,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal handler for unhandled exceptions
|
||||
* Triggers event, shows flash alert (rate limited), disables SPA, and logs to console
|
||||
*/
|
||||
static _handle_unhandled_exception(error_data) {
|
||||
// Always log to console
|
||||
console.error('[Rsx] Unhandled exception:', error_data);
|
||||
|
||||
// Trigger event for listeners (e.g., Debugger for server logging)
|
||||
Rsx.trigger('unhandled_exception', error_data);
|
||||
|
||||
// Disable SPA navigation if in SPA mode
|
||||
// This allows user to navigate away from broken page using normal browser navigation
|
||||
if (typeof Spa !== 'undefined' && window.rsxapp && window.rsxapp.is_spa) {
|
||||
Spa.disable();
|
||||
}
|
||||
|
||||
// Show flash alert (rate limited to 1 per second)
|
||||
const now = Date.now();
|
||||
if (now - Rsx._last_exception_flash >= 1000) {
|
||||
Rsx._last_exception_flash = now;
|
||||
|
||||
// Determine message based on dev/prod mode
|
||||
let message;
|
||||
if (window.rsxapp && window.rsxapp.debug) {
|
||||
// Dev mode: Show actual error (shortened to 300 chars)
|
||||
const error_text = error_data.message || 'Unknown error';
|
||||
message = error_text.length > 300 ? error_text.substring(0, 300) + '...' : error_text;
|
||||
} else {
|
||||
// Production mode: Generic message
|
||||
message = 'An unhandled error has occurred, you may need to refresh your page';
|
||||
}
|
||||
|
||||
// Show flash alert if Flash_Alert is available
|
||||
if (typeof Flash_Alert !== 'undefined') {
|
||||
Flash_Alert.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log to server that an event happened
|
||||
static log(type, message = 'notice') {
|
||||
Core_Log.log(type, message);
|
||||
@@ -502,6 +574,9 @@ class Rsx {
|
||||
|
||||
Rsx.__booted = true;
|
||||
|
||||
// Setup exception handlers first, before any initialization phases
|
||||
Rsx._setup_exception_handlers();
|
||||
|
||||
// Get all registered classes from the manifest
|
||||
const all_classes = Manifest.get_all_classes();
|
||||
|
||||
|
||||
@@ -34,6 +34,27 @@ class Spa {
|
||||
// Pending redirect that occurred during dispatch (e.g., in action on_load)
|
||||
static pending_redirect = null;
|
||||
|
||||
// Flag to track if SPA is enabled (can be disabled on errors or dirty forms)
|
||||
static _spa_enabled = true;
|
||||
|
||||
/**
|
||||
* Disable SPA navigation - all navigation becomes full page loads
|
||||
* Call this when errors occur or forms are dirty
|
||||
*/
|
||||
static disable() {
|
||||
console.warn('[Spa] SPA navigation disabled - browser navigation mode active');
|
||||
Spa._spa_enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enable SPA navigation after it was disabled
|
||||
* Call this after forms are saved or errors are resolved
|
||||
*/
|
||||
static enable() {
|
||||
console.log('[Spa] SPA navigation enabled');
|
||||
Spa._spa_enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Framework module initialization hook called during framework boot
|
||||
* Only runs when window.rsxapp.is_spa === true
|
||||
@@ -303,6 +324,13 @@ class Spa {
|
||||
// Get target URL (browser has already updated location)
|
||||
const url = window.location.pathname + window.location.search + window.location.hash;
|
||||
|
||||
// If SPA is disabled, still handle back/forward as SPA navigation
|
||||
// (We can't convert existing history entries to full page loads)
|
||||
// Only forward navigation (link clicks) will become full page loads
|
||||
if (!Spa._spa_enabled) {
|
||||
console_debug('Spa', 'SPA disabled but handling popstate as SPA navigation (back/forward)');
|
||||
}
|
||||
|
||||
// Retrieve scroll position from history state
|
||||
const scroll = e.state?.scroll || null;
|
||||
|
||||
@@ -374,6 +402,12 @@ class Spa {
|
||||
|
||||
// Check if target URL matches a Spa route
|
||||
if (Spa.match_url_to_route(href)) {
|
||||
// Check if SPA is enabled
|
||||
if (!Spa._spa_enabled) {
|
||||
console_debug('Spa', 'SPA disabled, letting browser handle: ' + href);
|
||||
return; // Don't preventDefault - browser navigates normally
|
||||
}
|
||||
|
||||
console_debug('Spa', 'Intercepting link click: ' + href);
|
||||
e.preventDefault();
|
||||
Spa.dispatch(href, { history: 'auto' });
|
||||
@@ -401,6 +435,13 @@ class Spa {
|
||||
* @param {boolean} options.triggers - Fire before/after dispatch events (default: true)
|
||||
*/
|
||||
static async dispatch(url, options = {}) {
|
||||
// Check if SPA is disabled - do full page load
|
||||
if (!Spa._spa_enabled) {
|
||||
console.warn('[Spa.dispatch] SPA disabled, forcing full page load');
|
||||
document.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Spa.is_dispatching) {
|
||||
// Already dispatching - queue this as a pending redirect
|
||||
// This commonly happens when an action redirects in on_load()
|
||||
|
||||
@@ -77,6 +77,7 @@ class Spa_Layout extends Component {
|
||||
// Clear content area
|
||||
$content.empty();
|
||||
|
||||
try {
|
||||
// Get the action class to check for @title decorator
|
||||
const action_class = Manifest.get_class_by_name(action_name);
|
||||
|
||||
@@ -109,6 +110,40 @@ class Spa_Layout extends Component {
|
||||
|
||||
// Wait for action to be ready
|
||||
await action.ready();
|
||||
} catch (error) {
|
||||
// Action lifecycle failed - log error, trigger event, disable SPA, show error UI
|
||||
console.error('[Spa_Layout] Action lifecycle failed:', error);
|
||||
|
||||
// Trigger global exception event (goes to Debugger for server logging, Flash_Alert, etc)
|
||||
if (typeof Rsx !== 'undefined') {
|
||||
Rsx.trigger('unhandled_exception', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
type: 'action_lifecycle_error',
|
||||
action_name: action_name,
|
||||
error: error,
|
||||
});
|
||||
}
|
||||
|
||||
// Disable SPA so forward navigation becomes full page loads
|
||||
// (Back/forward still work as SPA to allow user to navigate away)
|
||||
if (typeof Spa !== 'undefined') {
|
||||
Spa.disable();
|
||||
}
|
||||
|
||||
// Show error UI in content area so user can navigate away
|
||||
$content.html(`
|
||||
<div class="alert alert-danger m-4">
|
||||
<h4>Page Failed to Load</h4>
|
||||
<p>An error occurred while loading this page. You can navigate back or to another page.</p>
|
||||
<p class="mb-0">
|
||||
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Don't re-throw - allow navigation to continue working
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user