Files
rspade_system/app/RSpade/Core/Js/Rsx.js
2025-10-22 01:34:31 +00:00

331 lines
12 KiB
JavaScript
Executable File

// @ROUTE-EXISTS-01-EXCEPTION - This file contains documentation examples with fictional route names
/**
* Rsx - Core JavaScript Runtime System
*
* The Rsx class is the central hub for the RSX JavaScript runtime, providing essential
* system-level utilities that all other framework components depend on. It serves as the
* foundation for the client-side framework, handling core operations that must be globally
* accessible and consistently available.
*
* Core Responsibilities:
* - Event System: Application-wide event bus for framework lifecycle and custom events
* - Environment Detection: Runtime environment identification (dev/production)
* - Route Management: Type-safe route generation and URL building
* - Unique ID Generation: Client-side unique identifier generation
* - Framework Bootstrap: Multi-phase initialization orchestration
* - Logging: Centralized logging interface (delegates to console_debug)
*
* The Rsx class deliberately keeps its scope limited to core utilities. Advanced features
* are delegated to specialized classes:
* - Manifest operations → Manifest class
* - Caching → Rsx_Cache class
* - AJAX/API calls → Ajax_* classes
* - Route proxies → Rsx_Route_Proxy class
* - Behaviors → Rsx_Behaviors class
*
* All methods are static - Rsx is never instantiated. It's available globally from the
* moment bundles load and remains constant throughout the application lifecycle.
*
* Usage Examples:
* ```javascript
* // Event system
* Rsx.on('app_ready', () => console.log('App initialized'));
* Rsx.trigger('custom_event', {data: 'value'});
*
* // Environment detection
* if (Rsx.is_dev()) { console.log('Development mode'); }
*
* // Route generation
* const url = Rsx.Route('Controller', 'action').url();
*
* // Unique IDs
* const uniqueId = Rsx.uid(); // e.g., "rsx_1234567890_1"
* ```
*
* @static
* @global
*/
class Rsx {
// Gets set to true to interupt startup sequence
static __stopped = false;
// Initialize event handlers storage
static _init_events() {
if (typeof Rsx._event_handlers === 'undefined') {
Rsx._event_handlers = {};
}
if (typeof Rsx._triggered_events === 'undefined') {
Rsx._triggered_events = {};
}
}
// Register an event handler
static on(event, callback) {
Rsx._init_events();
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
if (!Rsx._event_handlers[event]) {
Rsx._event_handlers[event] = [];
}
Rsx._event_handlers[event].push(callback);
// If this event was already triggered, call the callback immediately
if (Rsx._triggered_events[event]) {
console_debug('RSX_INIT', 'Triggering ' + event + ' for late registered callback');
callback(Rsx._triggered_events[event]);
}
}
// Trigger an event with optional data
static trigger(event, data = {}) {
Rsx._init_events();
// Record that this event was triggered
Rsx._triggered_events[event] = data;
if (!Rsx._event_handlers[event]) {
return;
}
console_debug('RSX_INIT', 'Triggering ' + event + ' for ' + Rsx._event_handlers[event].length + ' callbacks');
// Call all registered handlers for this event in order
for (const callback of Rsx._event_handlers[event]) {
callback(data);
}
}
// Alias for trigger.refresh(''), should be called after major UI updates to apply such effects as
// underlining links to unimplemented # routes
static trigger_refresh() {
// Use Rsx.on('refresh', callback); to register a callback for refresh
this.trigger('refresh');
}
// Log to server that an event happened
static log(type, message = 'notice') {
Core_Log.log(type, message);
}
// Returns true if the app is being run in dev mode
// This should affect caching and some debug checks
static is_dev() {
return window.rsxapp.debug;
}
static is_prod() {
return !window.rsxapp.debug;
}
// Generates a unique number for the application instance
static uid() {
if (typeof Rsx._uid == undef) {
Rsx._uid = 0;
}
return Rsx._uid++;
}
// Storage for route definitions loaded from bundles
static _routes = {};
/**
* Define routes from bundled data
* Called by generated JavaScript in bundles
*/
static _define_routes(routes) {
// Merge routes into the global route storage
for (const class_name in routes) {
if (!Rsx._routes[class_name]) {
Rsx._routes[class_name] = {};
}
for (const method_name in routes[class_name]) {
Rsx._routes[class_name][method_name] = routes[class_name][method_name];
}
}
}
/**
* Create a route proxy for type-safe URL generation
*
* This method creates a route proxy that can generate URLs for a specific controller action.
* The proxy ensures all required route parameters are provided and handles extra parameters
* as query string values.
*
* If the route is not found in the route definitions, a default pattern is used:
* `/_/{controller}/{action}` with all parameters appended as query strings.
*
* Usage examples:
* ```javascript
* // Simple route without parameters (defaults to 'index' action)
* const url = Rsx.Route('Frontend_Index_Controller').url();
* // Returns: /dashboard
*
* // Route with explicit action
* const url = Rsx.Route('Frontend_Index_Controller', 'index').url();
* // Returns: /dashboard
*
* // Route with required parameter
* const url = Rsx.Route('Frontend_Client_View_Controller').url({id: 'C001'});
* // Returns: /clients/view/C001
*
* // Route with required and query parameters
* const url = Rsx.Route('Frontend_Client_View_Controller').url({
* id: 'C001',
* tab: 'history'
* });
* // Returns: /clients/view/C001?tab=history
*
* // Route not found - uses default pattern
* const url = Rsx.Route('Unimplemented_Controller', 'some_action').url({foo: 'bar'});
* // Returns: /_/Unimplemented_Controller/some_action?foo=bar
*
* // Generate absolute URL
* const absolute = Rsx.Route('Frontend_Index_Controller').absolute_url();
* // Returns: https://example.com/dashboard
*
* // Navigate to route
* Rsx.Route('Frontend_Index_Controller').navigate();
* // Redirects browser to /dashboard
*
* // Check if route is current
* if (Rsx.Route('Frontend_Index_Controller').is_current()) {
* // This is the currently executing route
* }
* ```
*
* @param {string} class_name The controller class name (e.g., 'User_Controller')
* @param {string} [action_name='index'] The action/method name (defaults to 'index')
* @returns {Rsx_Route_Proxy} Route proxy instance for URL generation
*/
static Route(class_name, action_name = 'index') {
// Check if route exists in definitions
if (Rsx._routes[class_name] && Rsx._routes[class_name][action_name]) {
const pattern = Rsx._routes[class_name][action_name];
return new Rsx_Route_Proxy(class_name, action_name, pattern);
}
// Route not found - use default pattern /_/{controller}/{action}
const default_pattern = `/_/${class_name}/${action_name}`;
return new Rsx_Route_Proxy(class_name, action_name, default_pattern);
}
/**
* Internal: Call a specific method on all classes that have it
* Collects promises from return values and waits for all to resolve
* @param {string} method_name The method name to call on all classes
* @returns {Promise} Promise that resolves when all method calls complete
*/
static async _rsx_call_all_classes(method_name) {
const all_classes = Manifest.get_all_classes();
const classes_with_method = [];
const promise_pile = [];
for (const class_info of all_classes) {
const class_object = class_info.class_object;
const class_name = class_info.class_name;
// Check if this class has the method (static methods are on the class itself)
if (typeof class_object[method_name] === 'function') {
classes_with_method.push(class_name);
const return_value = await class_object[method_name]();
// Collect promises from return value
if (return_value instanceof Promise) {
promise_pile.push(return_value);
} else if (Array.isArray(return_value)) {
for (const item of return_value) {
if (item instanceof Promise) {
promise_pile.push(item);
}
}
}
if (Rsx.__stopped) {
return;
}
}
}
if (classes_with_method.length > 0) {
console_debug('RSX_INIT', `${method_name}: ${classes_with_method.length} classes`);
}
// Await all promises before returning
if (promise_pile.length > 0) {
console_debug('RSX_INIT', `${method_name}: Awaiting ${promise_pile.length} promises`);
await Promise.all(promise_pile);
}
}
/**
* Internal: Execute multi-phase initialization for all registered classes
* This runs various initialization phases in order to properly set up the application
* @returns {Promise} Promise that resolves when all initialization phases complete
*/
static async _rsx_core_boot() {
if (Rsx.__booted) {
console.error('Rsx._rsx_core_boot called more than once');
return;
}
Rsx.__booted = true;
// Get all registered classes from the manifest
const all_classes = Manifest.get_all_classes();
console_debug('RSX_INIT', `Starting _rsx_core_boot with ${all_classes.length} classes`);
if (!all_classes || all_classes.length === 0) {
// No classes to initialize
shouldnt_happen('No classes registered in js - there should be at least the core framework classes');
return;
}
// Define initialization phases in order
const phases = [
{ event: 'framework_core_define', method: '_on_framework_core_define' },
{ event: 'framework_modules_define', method: '_on_framework_modules_define' },
{ event: 'framework_core_init', method: '_on_framework_core_init' },
{ event: 'app_modules_define', method: 'on_app_modules_define' },
{ event: 'app_define', method: 'on_app_define' },
{ event: 'framework_modules_init', method: '_on_framework_modules_init' },
{ event: 'app_modules_init', method: 'on_app_modules_init' },
{ event: 'app_init', method: 'on_app_init' },
{ event: 'app_ready', method: 'on_app_ready' },
];
// Execute each phase in order
for (const phase of phases) {
await Rsx._rsx_call_all_classes(phase.method);
if (Rsx.__stopped) {
return;
}
Rsx.trigger(phase.event);
}
// Ui refresh callbacks
Rsx.trigger_refresh();
// All phases complete
console_debug('RSX_INIT', 'Initialization complete');
// Trigger _debug_ready event - this is ONLY for tooling like rsx:debug
// DO NOT use this in application code - use on_app_ready() phase instead
// This event exists solely for debugging tools that need to run after full initialization
Rsx.trigger('_debug_ready');
}
/* Calling this stops the boot process. */
static async _rsx_core_boot_stop(reason) {
console.error(reason);
Rsx.__stopped = true;
}
}