apprun
Version:
JavaScript library that has Elm inspired architecture, event pub-sub and components
174 lines (155 loc) • 6.19 kB
text/typescript
/**
* Core AppRun application class and singleton instance
*
* This file provides:
* 1. App class - The core event system implementation with pub/sub capabilities
* - on(): Subscribe to events with options (once, delay, global)
* - off(): Unsubscribe from events with proper cleanup
* - run(): Publish events synchronously with error handling
* - runAsync(): Publish events asynchronously with Promise support, returns handler values
* - query(): Deprecated alias for runAsync() - use runAsync() instead
*
* 2. Default app singleton - Global event bus instance
* - Created once and reused across the application
* - Stored in global scope (window/global) with version tracking
* - Prevents duplicate instances across different versions
*
* Features:
* - Event wildcards support (events ending with '*')
* - Delayed event execution with timeout management
* - Once-only event subscriptions
* - Async event handling with Promise.all
* - Global event bus shared across components
* - Memory leak prevention with proper cleanup
*
* Type Safety Improvements (v3.35.1):
* - Added validation for event handler functions
* - Enhanced error handling in event execution
* - Improved null checks in delayed event handling
* - Better error reporting for invalid handlers
*
* Usage:
* ```ts
* // Subscribe to events
* app.on('event-name', (state, ...args) => {
* // Handle event
* });
*
* // Publish events (fire-and-forget)
* app.run('event-name', ...args);
*
* // Get return values from event handlers
* app.runAsync('event-name', data).then(results => {
* // Handle results array
* });
* ```
*/
import { EventOptions } from './types'
import { APPRUN_VERSION_GLOBAL } from './version'
export class App {
_events: { [key: string]: Array<{ fn: (...args: any[]) => any, options: EventOptions }> };
constructor() {
this._events = {} as { [key: string]: Array<{ fn: (...args: any[]) => any, options: EventOptions }> };
}
on(name: string, fn: (...args: any[]) => any, options: EventOptions = {}): void {
this._events[name] = this._events[name] || [];
this._events[name].push({ fn, options });
}
off(name: string, fn: (...args: any[]) => any): void {
const subscribers = this._events[name] || [];
this._events[name] = subscribers.filter((sub) => sub.fn !== fn);
}
find(name: string): any {
return this._events[name];
}
run(name: string, ...args: any[]): number {
const subscribers = this.getSubscribers(name, this._events);
console.assert(subscribers && subscribers.length > 0, 'No subscriber for event: ' + name);
subscribers.forEach((sub) => {
const { fn, options } = sub;
if (!fn || typeof fn !== 'function') {
console.error(`AppRun event handler for '${name}' is not a function:`, fn);
return false;
}
if (options.delay) {
this.delay(name, fn, args, options);
} else {
try {
Object.keys(options).length > 0 ? fn.apply(this, [...args, options]) : fn.apply(this, args);
} catch (error) {
console.error(`Error in event handler for '${name}':`, error);
}
}
return !sub.options.once;
});
return subscribers.length;
}
once(name: string, fn: (...args: any[]) => any, options: EventOptions = {}): void {
this.on(name, fn, { ...options, once: true });
}
private delay(name: string, fn: (...args: any[]) => any, args: any[], options: EventOptions): void {
if (options._t) clearTimeout(options._t);
options._t = setTimeout(() => {
clearTimeout(options._t);
try {
Object.keys(options).length > 0 ? fn.apply(this, [...args, options]) : fn.apply(this, args);
} catch (error) {
console.error(`Error in delayed event handler for '${name}':`, error);
}
}, options.delay);
}
runAsync(name: string, ...args: any[]): Promise<any[]> {
const subscribers = this.getSubscribers(name, this._events);
console.assert(subscribers && subscribers.length > 0, 'No subscriber for event: ' + name);
const promises = subscribers.map(sub => {
const { fn, options } = sub;
if (!fn || typeof fn !== 'function') {
console.error(`AppRun async event handler for '${name}' is not a function:`, fn);
return Promise.resolve(null);
}
try {
return Object.keys(options).length > 0 ? fn.apply(this, [...args, options]) : fn.apply(this, args);
} catch (error) {
console.error(`Error in async event handler for '${name}':`, error);
return Promise.reject(error);
}
});
return Promise.all(promises);
}
/**
* @deprecated Use runAsync() instead. app.query() will be removed in a future version.
*/
query(name: string, ...args: any[]): Promise<any[]> {
console.warn('app.query() is deprecated. Use app.runAsync() instead.');
return this.runAsync(name, ...args);
}
private getSubscribers(name: string, events: { [key: string]: Array<{ fn: (...args: any[]) => any, options: EventOptions }> }): Array<{ fn: (...args: any[]) => any, options: EventOptions }> {
const subscribers = events[name] || [];
// Update the list of subscribers by pulling out those which will run once.
// We must do this update prior to running any of the events in case they
// cause additional events to be turned off or on.
events[name] = subscribers.filter((sub) => {
return !sub.options.once;
});
Object.keys(events).filter(evt => evt.endsWith('*') && name.startsWith(evt.replace('*', '')))
.sort((a, b) => b.length - a.length)
.forEach(evt => subscribers.push(...events[evt].map(sub => ({
...sub,
options: { ...sub.options, event: name }
}))));
return subscribers;
}
}
const AppRunVersions = APPRUN_VERSION_GLOBAL;
let _app: App;
const root = (typeof window !== 'undefined' ? window :
typeof global !== 'undefined' ? global :
typeof self !== 'undefined' ? self : {}) as any;
if (root.app && root._AppRunVersions) {
_app = root.app;
} else {
_app = new App();
root.app = _app;
root._AppRunVersions = AppRunVersions;
}
export default _app;