merge from 0.20

This commit is contained in:
SrGooglo 2025-03-06 03:45:30 +00:00
parent e897de6814
commit fb190878b2
14 changed files with 835 additions and 817 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@
# Temporal configurations # Temporal configurations
/**/**/.aliaser /**/**/.aliaser
.experimental

View File

@ -1,6 +1,6 @@
{ {
"name": "@ragestudio/vessel", "name": "@ragestudio/vessel",
"version": "0.18.1", "version": "0.20.0",
"main": "./src/index.js", "main": "./src/index.js",
"repository": "https://github.com/ragestudio/vessel.git", "repository": "https://github.com/ragestudio/vessel.git",
"author": "RageStudio", "author": "RageStudio",
@ -11,10 +11,6 @@
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"dependencies": { "dependencies": {
"@loadable/component": "^5.16.4", "@loadable/component": "^5.16.4",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
@ -23,8 +19,8 @@
"less": "^4.2.0", "less": "^4.2.0",
"million": "^3.1.11", "million": "^3.1.11",
"object-observer": "^6.0.0", "object-observer": "^6.0.0",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"sucrase": "^3.35.0" "sucrase": "^3.35.0"
} }

View File

@ -0,0 +1,112 @@
import sortCoresByDependencies from "../../utils/sortCoresByDependencies"
export default class CoresManager {
constructor(runtime) {
this.runtime = runtime
}
cores = new Map()
context = Object()
async initialize() {
try {
const coresPaths = {
...import.meta.glob("/src/cores/*/*.core.jsx"),
...import.meta.glob("/src/cores/*/*.core.js"),
...import.meta.glob("/src/cores/*/*.core.ts"),
...import.meta.glob("/src/cores/*/*.core.tsx"),
}
const coresKeys = Object.keys(coresPaths)
if (coresKeys.length === 0) {
this.runtime.console.warn(
"Cannot find any cores to initialize.",
)
return true
}
let cores = await Promise.all(
coresKeys.map(async (key) => {
const coreModule = await coresPaths[key]().catch((err) => {
this.runtime.console.warn(
`Cannot load core [${key}]`,
err,
)
return false
})
return coreModule?.default ?? coreModule
}),
)
cores = cores.filter((core) => core && core.constructor)
if (!cores.length) {
this.console.warn(`Cannot find any valid cores to initialize.`)
return true
}
this.runtime.eventBus.emit("runtime.initialize.cores.start")
cores = sortCoresByDependencies(cores)
for (const coreClass of cores) {
await this.initializeCore(coreClass)
}
this.runtime.eventBus.emit("runtime.initialize.cores.finish")
} catch (error) {
this.runtime.eventBus.emit("runtime.initialize.cores.failed", error)
throw error
}
}
async initializeCore(coreClass) {
if (!coreClass.constructor) {
this.runtime.console.error(
`Core [${coreClass.name}] is not a valid class`,
)
return false
}
const namespace = coreClass.namespace ?? coreClass.name
this.runtime.eventBus.emit(`runtime.initialize.core.${namespace}.start`)
const coreInstance = new coreClass(this.runtime)
this.cores.set(namespace, coreInstance)
const result = await coreInstance._init()
if (!result) {
this.runtime.console.warn(
`[${namespace}] core initialized without a result`,
)
}
if (result.public_context) {
this.context[result.namespace] = result.public_context
}
this.runtime.eventBus.emit(
`runtime.initialize.core.${namespace}.finish`,
)
//this.states.LOADED_CORES.push(namespace)
return true
}
getCoreContext = () => {
return new Proxy(this.context, {
get: (target, key) => target[key],
set: () => {
throw new Error("Cannot modify the runtime property")
},
})
}
get = (key) => {
return this.cores.get(key)
}
}

View File

@ -1,26 +1,32 @@
import Listener from "./Listener"; import Listener from "./Listener"
import type { import type {
EventTemplateT, EventTemplateT,
TemplateEventT, TemplateEventT,
TemplateListenerArgsT, TemplateListenerArgsT,
TemplateListenerContextT, TemplateListenerContextT,
TemplateListenerT, TemplateListenerT,
} from "./Listener"; } from "./Listener"
export default class EventEmitter<Template extends EventTemplateT = EventTemplateT> { export default class EventEmitter<
protected events: EventsT<Template> = {}; Template extends EventTemplateT = EventTemplateT,
> {
protected events: EventsT<Template> = {}
public on = this.addListener; public on = this.addListener
public off = this.removeListener; public off = this.removeListener
public id = null
/** /**
* Returns an array listing the events for which the emitter has registered listeners. * Returns an array listing the events for which the emitter has registered listeners.
*/ */
public eventNames(): TemplateEventT<Template>[] { public eventNames(): TemplateEventT<Template>[] {
const events = this.events; const events = this.events
return Object.keys(events).filter((event) => events[event] !== undefined); return Object.keys(events).filter(
(event) => events[event] !== undefined,
)
} }
/** /**
@ -31,13 +37,13 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
public rawListeners<Event extends TemplateEventT<Template>>( public rawListeners<Event extends TemplateEventT<Template>>(
event: Event, event: Event,
): Listener<Template, Event, this>[] { ): Listener<Template, Event, this>[] {
const listeners = this.events[event]; const listeners = this.events[event]
if (listeners === undefined) return []; if (listeners === undefined) return []
if (isSingleListener(listeners)) return [listeners]; if (isSingleListener(listeners)) return [listeners]
return listeners; return listeners
} }
/** /**
@ -48,16 +54,17 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
public listeners<Event extends TemplateEventT<Template>>( public listeners<Event extends TemplateEventT<Template>>(
event: Event, event: Event,
): TemplateListenerT<Template, Event, this>[] { ): TemplateListenerT<Template, Event, this>[] {
const listeners = this.rawListeners(event); const listeners = this.rawListeners(event)
const length = listeners.length; const length = listeners.length
if (length === 0) return []; if (length === 0) return []
const result = new Array(length); const result = new Array(length)
for (let index = 0; index < length; index++) result[index] = listeners[index].fn; for (let index = 0; index < length; index++)
result[index] = listeners[index].fn
return result; return result
} }
/** /**
@ -68,7 +75,7 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
public listenerCount<Event extends TemplateEventT<Template>>( public listenerCount<Event extends TemplateEventT<Template>>(
event: Event, event: Event,
): number { ): number {
return this.rawListeners(event).length; return this.rawListeners(event).length
} }
/** /**
@ -83,43 +90,51 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
event: Event, event: Event,
...args: TemplateListenerArgsT<Template, Event> ...args: TemplateListenerArgsT<Template, Event>
): boolean { ): boolean {
const events = this.events; const events = this.events
const listeners = events[event]; const listeners = events[event]
if (listeners === undefined) { if (listeners === undefined) {
if (event === "error") throw args[0]; if (event === "error") throw args[0]
return false; return false
} }
console.debug(
`[eventbus][${this.id}] emitted event (${event.toString()})`,
{
args: args,
listeners,
},
)
if (isSingleListener(listeners)) { if (isSingleListener(listeners)) {
const { fn, context, once } = listeners; const { fn, context, once } = listeners
if (once) events[event] = undefined; if (once) events[event] = undefined
fn.apply(context, args); fn.apply(context, args)
return true; return true
} }
let length = listeners.length; let length = listeners.length
for (let index = 0; index < length; index++) { for (let index = 0; index < length; index++) {
const { fn, context, once } = listeners[index]; const { fn, context, once } = listeners[index]
if (once) { if (once) {
listeners.splice(index--, 1); listeners.splice(index--, 1)
length--; length--
} }
fn.apply(context, args); fn.apply(context, args)
} }
if (length === 0) events[event] = undefined; if (length === 0) events[event] = undefined
else if (length === 1) events[event] = listeners[0]; else if (length === 1) events[event] = listeners[0]
return true; return true
} }
/** /**
@ -132,12 +147,15 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
* @param listener * @param listener
* @param context - default: `this` (EventEmitter instance) * @param context - default: `this` (EventEmitter instance)
*/ */
public addListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>( public addListener<
Event extends TemplateEventT<Template>,
Context = TemplateListenerContextT<Template, Event, this>,
>(
event: Event, event: Event,
listener: TemplateListenerT<Template, Event, Context>, listener: TemplateListenerT<Template, Event, Context>,
context?: Context, context?: Context,
): this { ): this {
return this._addListener(event, listener, context, true, false); return this._addListener(event, listener, context, true, false)
} }
/** /**
@ -148,12 +166,15 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
* @param listener * @param listener
* @param context - default: `this` (EventEmitter instance) * @param context - default: `this` (EventEmitter instance)
*/ */
public once<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>( public once<
Event extends TemplateEventT<Template>,
Context = TemplateListenerContextT<Template, Event, this>,
>(
event: Event, event: Event,
listener: TemplateListenerT<Template, Event, Context>, listener: TemplateListenerT<Template, Event, Context>,
context?: Context, context?: Context,
): this { ): this {
return this._addListener(event, listener, context, true, true); return this._addListener(event, listener, context, true, true)
} }
/** /**
@ -166,12 +187,15 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
* @param listener * @param listener
* @param context - default: `this` (EventEmitter instance) * @param context - default: `this` (EventEmitter instance)
*/ */
public prependListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>( public prependListener<
Event extends TemplateEventT<Template>,
Context = TemplateListenerContextT<Template, Event, this>,
>(
event: Event, event: Event,
listener: TemplateListenerT<Template, Event, Context>, listener: TemplateListenerT<Template, Event, Context>,
context?: Context, context?: Context,
): this { ): this {
return this._addListener(event, listener, context, false, false); return this._addListener(event, listener, context, false, false)
} }
/** /**
@ -182,12 +206,15 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
* @param listener * @param listener
* @param context - default: `this` (EventEmitter instance) * @param context - default: `this` (EventEmitter instance)
*/ */
public prependOnceListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>( public prependOnceListener<
Event extends TemplateEventT<Template>,
Context = TemplateListenerContextT<Template, Event, this>,
>(
event: Event, event: Event,
listener: TemplateListenerT<Template, Event, Context>, listener: TemplateListenerT<Template, Event, Context>,
context?: Context, context?: Context,
): this { ): this {
return this._addListener(event, listener, context, false, true); return this._addListener(event, listener, context, false, true)
} }
/** /**
@ -199,16 +226,16 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
event?: Event, event?: Event,
): this { ): this {
if (event === undefined) { if (event === undefined) {
this.events = {}; this.events = {}
return this; return this
} }
if (this.events[event] === undefined) return this; if (this.events[event] === undefined) return this
this.events[event] = undefined; this.events[event] = undefined
return this; return this
} }
/** /**
@ -221,65 +248,76 @@ export default class EventEmitter<Template extends EventTemplateT = EventTemplat
event: Event, event: Event,
listener: TemplateListenerT<Template, Event, this>, listener: TemplateListenerT<Template, Event, this>,
): this { ): this {
const listeners = this.events[event]; const listeners = this.events[event]
if (listeners === undefined) return this; if (listeners === undefined) return this
if (isSingleListener(listeners)) { if (isSingleListener(listeners)) {
if (listeners.fn === listener) this.events[event] = undefined; if (listeners.fn === listener) this.events[event] = undefined
return this; return this
} }
for (let index = listeners.length - 1; index >= 0; index--) { for (let index = listeners.length - 1; index >= 0; index--) {
if (listeners[index].fn === listener) listeners.splice(index, 1); if (listeners[index].fn === listener) listeners.splice(index, 1)
} }
if (listeners.length === 0) this.events[event] = undefined; if (listeners.length === 0) this.events[event] = undefined
else if (listeners.length === 1) this.events[event] = listeners[0]; else if (listeners.length === 1) this.events[event] = listeners[0]
return this; return this
} }
protected _addListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>( protected _addListener<
Event extends TemplateEventT<Template>,
Context = TemplateListenerContextT<Template, Event, this>,
>(
event: Event, event: Event,
fn: TemplateListenerT<Template, Event, Context>, fn: TemplateListenerT<Template, Event, Context>,
context: Context = this as never, context: Context = this as never,
append: boolean, append: boolean,
once: boolean, once: boolean,
): this { ): this {
const events = this.events; const events = this.events
const listeners = events[event]; const listeners = events[event]
const listener = new Listener<Template, Event, Context>(fn, context, once); const listener = new Listener<Template, Event, Context>(
fn,
context,
once,
)
if (listeners === undefined) { if (listeners === undefined) {
events[event] = listener; events[event] = listener
return this; return this
} }
if (isSingleListener(listeners)) { if (isSingleListener(listeners)) {
events[event] = append ? [listeners, listener] : [listener, listeners]; events[event] = append
? [listeners, listener]
: [listener, listeners]
return this; return this
} }
if (append) { if (append) {
listeners.push(listener); listeners.push(listener)
} else { } else {
listeners.unshift(listener); listeners.unshift(listener)
} }
return this; return this
} }
} }
function isSingleListener<Type>(value: Type | Type[]): value is Type { function isSingleListener<Type>(value: Type | Type[]): value is Type {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return (value as any).length === undefined; return (value as any).length === undefined
} }
export type EventsT<Template extends EventTemplateT> = { export type EventsT<Template extends EventTemplateT> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
[Event in TemplateEventT<Template>]?: Listener<Template, Event, any> | Listener<Template, Event, any>[]; [Event in TemplateEventT<Template>]?:
}; | Listener<Template, Event, any>
| Listener<Template, Event, any>[]
}

View File

@ -13,6 +13,8 @@ export default class ExtensionManager {
extensions = new Map() extensions = new Map()
loadExtension = async (manifest) => { loadExtension = async (manifest) => {
throw new Error("Not implemented")
if (isUrl(manifest)) { if (isUrl(manifest)) {
manifest = await fetch(manifest) manifest = await fetch(manifest)
manifest = await manifest.json() manifest = await manifest.json()
@ -26,7 +28,7 @@ export default class ExtensionManager {
}) })
await new Promise((resolve) => { await new Promise((resolve) => {
worker.onmessage = ({data}) => { worker.onmessage = ({ data }) => {
if (data.event === "loaded") { if (data.event === "loaded") {
resolve() resolve()
} }
@ -44,7 +46,5 @@ export default class ExtensionManager {
// this.extensions.set(manifest.registryId,main.public) // this.extensions.set(manifest.registryId,main.public)
} }
installExtension = async () => { installExtension = async () => {}
}
} }

View File

@ -0,0 +1,19 @@
export default class SplashScreenManager {
constructor(containerId = "splash-screen") {
this.containerId = containerId
}
attach() {
const container = document.getElementById(this.containerId)
if (container && !container.classList.contains("app_splash_visible")) {
container.classList.add("app_splash_visible")
}
}
detach() {
const container = document.getElementById(this.containerId)
if (container && container.classList.contains("app_splash_visible")) {
container.classList.remove("app_splash_visible")
}
}
}

View File

@ -0,0 +1,33 @@
export default class ContextManager {
constructor(runtime) {
this.runtime = runtime
this.context = {}
this.proxy = this.createSecureProxy()
}
createSecureProxy() {
return new Proxy(this.context, {
get: (target, prop) => target[prop],
set: () => {
throw new Error("Direct context modification not allowed")
},
defineProperty: () => false,
})
}
registerConstant(key, value) {
Object.defineProperty(this.context, key, {
value,
writable: false,
configurable: false,
})
}
registerService(key, factory) {
Object.defineProperty(this.context, key, {
get: factory,
configurable: false,
enumerable: true,
})
}
}

View File

@ -0,0 +1,30 @@
export default class CoreManager {
constructor(runtime) {
this.runtime = runtime
this.cores = new Map()
}
async initializeCores() {
const coreModules = await this.discoverCoreModules()
const sortedCores = DependencyResolver.resolveDependencies(coreModules)
for (const CoreClass of sortedCores) {
try {
const coreInstance = new CoreClass(this.runtime)
await coreInstance.initialize()
this.registerCore(CoreClass, coreInstance)
} catch (error) {
this.runtime.console.error(
`Core ${CoreClass.name} failed:`,
error,
)
throw error
}
}
}
registerCore(CoreClass, instance) {
this.cores.set(CoreClass.name, instance)
this.runtime.contextManager.registerCore(CoreClass.name, instance)
}
}

View File

@ -0,0 +1,27 @@
import { Observable } from "object-observer"
export default class StateManager {
constructor(runtime) {
this.runtime = runtime
this.states = Observable.from({
status: "booting",
loadedCores: [],
activePlugins: [],
errors: [],
})
}
transitionTo(newState, payload) {
const validTransitions = {
booting: ["initializing", "crashed"],
initializing: ["ready", "crashed"],
ready: ["suspend", "crashed"],
crashed: [],
}
if (validTransitions[this.states.status].includes(newState)) {
this.states.status = newState
this.runtime.eventBus.emit(`runtime.state.${newState}`, payload)
}
}
}

View File

@ -1,526 +1,242 @@
import pkgJson from "../package.json" import pkgJson from "../package.json"
import React from "react" import React from "react"
import ReactDOM from "react-dom"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { createBrowserHistory } from "history" import { createBrowserHistory } from "history"
import { Observable } from "object-observer" import { Observable } from "object-observer"
import Extension from "./extension" import CoresManager from "./classes/CoresManager"
import ExtensionManager from "./classes/ExtensionsManager"
import EventBus from "./classes/EventBus" import EventBus from "./classes/EventBus"
import InternalConsole from "./classes/InternalConsole" import InternalConsole from "./classes/InternalConsole"
import ExtensionManager from "./classes/ExtensionsManager" import SplashScreenManager from "./classes/SplashScreenManager"
import bindObjects from "./utils/bindObjects"
import isMobile from "./utils/isMobile" import isMobile from "./utils/isMobile"
import * as StaticRenders from "./internals/renders" import * as StaticRenders from "./internals/renders"
import "./internals/style/index.css" import "./internals/style/index.css"
export default class EviteRuntime { export default class Runtime {
Flags = window.flags = Observable.from({ constructor(baseAppClass, params = { renderMount: "root" }) {
this.baseAppClass = baseAppClass
this.params = params
this.initialize().catch((error) => {
this.eventBus.emit("runtime.initialize.crash", error)
})
}
// contexts & managers
publicContext = (window.app = {})
cores = new CoresManager(this)
extensions = new ExtensionManager(this)
splash = new SplashScreenManager()
flags = Observable.from({
debug: false, debug: false,
}) })
console = new InternalConsole({
INTERNAL_CONSOLE = new InternalConsole({
namespace: "Runtime", namespace: "Runtime",
bgColor: "bgMagenta", bgColor: "bgMagenta",
}) })
history = createBrowserHistory()
eventBus = new EventBus({
id: "runtime",
})
EXTENSIONS = Object() states = Observable.from({
CORES = Object()
PublicContext = window.app = Object()
ExtensionsPublicContext = Object()
CoresPublicContext = Object()
STATES = Observable.from({
LOAD_STATE: "early", LOAD_STATE: "early",
INITIALIZER_TASKS: [], INITIALIZER_TASKS: [],
LOADED_CORES: [],
ATTACHED_EXTENSIONS: [],
REJECTED_EXTENSIONS: [],
INITIALIZATION_START: null, INITIALIZATION_START: null,
INITIALIZATION_STOP: null, INITIALIZATION_STOP: null,
INITIALIZATION_TOOKS: null, INITIALIZATION_TOOKS: null,
}) })
APP_RENDERER = null
SPLASH_RENDERER = null
constructor(
App,
Params = {
renderMount: "root",
}
) {
this.INTERNAL_CONSOLE.log(`Using React ${React.version}`)
this.AppComponent = App
this.Params = Params
// toogle splash
this.attachSplashScreen()
// controllers
this.root = createRoot(document.getElementById(this.Params.renderMount ?? "root"))
this.history = this.registerPublicMethod({ key: "history", locked: true }, createBrowserHistory())
this.eventBus = this.registerPublicMethod({ key: "eventBus", locked: true }, new EventBus())
window.app.cores = new Proxy(this.CoresPublicContext, {
get: (target, key) => {
return target[key]
},
set: (target, key, value) => {
throw new Error("You can't set a core value")
}
})
window.app.extensions = new ExtensionManager()
this.registerPublicMethod({ key: "isMobile", locked: true }, isMobile())
this.registerPublicMethod({ key: "__version", locked: true }, pkgJson.version)
if (typeof this.AppComponent.splashAwaitEvent === "string") {
this.eventBus.on(this.AppComponent.splashAwaitEvent, () => {
this.detachSplashScreen()
})
}
for (const [key, fn] of Object.entries(this.internalEvents)) {
this.eventBus.on(key, fn)
}
// emit attached extensions change events
Observable.observe(this.STATES.ATTACHED_EXTENSIONS, (changes) => {
changes.forEach((change) => {
if (change.type === "insert") {
this.eventBus.emit(`runtime.extension.attached`, change)
}
})
})
// emit rejected extensions change events
Observable.observe(this.STATES.REJECTED_EXTENSIONS, (changes) => {
changes.forEach((change) => {
if (change.type === "insert") {
this.eventBus.emit(`runtime.extension.rejected`, change)
}
})
})
return this.initialize().catch((error) => {
this.eventBus.emit("runtime.initialize.crash", error)
})
}
internalEvents = { internalEvents = {
"runtime.initialize.start": () => { "runtime.initialize.start": () => {
this.STATES.LOAD_STATE = "initializing" this.states.LOAD_STATE = "initializing"
this.STATES.INITIALIZATION_START = performance.now() this.states.INITIALIZATION_START = performance.now()
}, },
"runtime.initialize.finish": () => { "runtime.initialize.finish": () => {
const time = performance.now() const time = performance.now()
this.states.INITIALIZATION_STOP = time
this.STATES.INITIALIZATION_STOP = time if (this.states.INITIALIZATION_START) {
this.states.INITIALIZATION_TOOKS =
if (this.STATES.INITIALIZATION_START) { time - this.states.INITIALIZATION_START
this.STATES.INITIALIZATION_TOOKS = time - this.STATES.INITIALIZATION_START
} }
this.states.LOAD_STATE = "initialized"
this.STATES.LOAD_STATE = "initialized"
}, },
"runtime.initialize.crash": (error) => { "runtime.initialize.crash": (error) => {
this.STATES.LOAD_STATE = "crashed" this.states.LOAD_STATE = "crashed"
this.splash.detach()
if (this.SPLASH_RENDERER) { this.console.error("Runtime crashed on initialization\n", error)
this.detachSplashScreen() this.render(
} this.baseAppClass.staticRenders?.Crash ?? StaticRenders.Crash,
{
this.INTERNAL_CONSOLE.error("Runtime crashed on initialization \n", error)
// render crash
this.render(this.AppComponent.staticRenders?.Crash ?? StaticRenders.Crash, {
crash: { crash: {
message: "Runtime crashed on initialization", message: "Runtime crashed on initialization",
details: error.toString(), details: error.toString(),
} },
}) },
)
}, },
"runtime.crash": (crash) => { "runtime.crash": (crash) => {
this.STATES.LOAD_STATE = "crashed" this.states.LOAD_STATE = "crashed"
this.splash.detach()
if (this.SPLASH_RENDERER) { this.render(
this.detachSplashScreen() this.baseAppClass.staticRenders?.Crash ?? StaticRenders.Crash,
} { crash },
)
// render crash
this.render(this.AppComponent.staticRenders?.Crash ?? StaticRenders.Crash, {
crash
})
}, },
} }
bindObjects = async (bind, events, parent) => { registerEventsToBus(eventsObj) {
let boundEvents = {} for (const [event, handler] of Object.entries(eventsObj)) {
this.eventBus.on(event, handler.bind(this))
for await (let [event, handler] of Object.entries(events)) {
if (typeof handler === "object") {
boundEvents[event] = await this.bindObjects(bind, handler, parent)
} }
if (typeof handler === "function") {
boundEvents[event] = handler.bind(bind)
}
}
return boundEvents
} }
async initialize() { async initialize() {
this.eventBus.emit("runtime.initialize.start") this.eventBus.emit("runtime.initialize.start")
await this.initializeCores() this.console.log(
`Using React ${React.version} | Runtime v${pkgJson.version}`,
)
await this.performInitializerTasks() this.splash.attach()
// call early app initializer this.root = createRoot(
if (typeof this.AppComponent.initialize === "function") { document.getElementById(this.params.renderMount ?? "root"),
await this.AppComponent.initialize.apply(this) )
this.registerPublicField("history", this.history)
this.registerPublicField("eventBus", this.eventBus)
this.registerPublicField("isMobile", isMobile())
this.registerPublicField("__version", pkgJson.version)
window.app.cores = this.cores.getCoreContext()
//window.app.extensions = new ExtensionManager()
this.registerEventsToBus(this.internalEvents)
if (typeof this.baseAppClass.events === "object") {
for (const [event, handler] of Object.entries(
this.baseAppClass.events,
)) {
this.eventBus.on(event, (...args) => handler(this, ...args))
}
} }
// handle app events handlers registration if (this.baseAppClass.splashAwaitEvent) {
if (typeof this.AppComponent.publicEvents === "object") { this.eventBus.on(this.baseAppClass.splashAwaitEvent, () => {
for await (let [event, handler] of Object.entries(this.AppComponent.publicEvents)) { this.splash.detach()
})
}
await this.cores.initialize()
await this.performInitializerTasks()
if (typeof this.baseAppClass.initialize === "function") {
await this.baseAppClass.initialize.call(this)
}
if (typeof this.baseAppClass.publicEvents === "object") {
for (const [event, handler] of Object.entries(
this.baseAppClass.publicEvents,
)) {
this.eventBus.on(event, handler.bind(this)) this.eventBus.on(event, handler.bind(this))
} }
} }
// handle app public methods registration if (typeof this.baseAppClass.publicMethods === "object") {
if (typeof this.AppComponent.publicMethods === "object") { const boundedPublicMethods = await bindObjects(
await this.registerPublicMethods(this.AppComponent.publicMethods) this,
this.baseAppClass.publicMethods,
)
for (const [methodName, fn] of Object.entries(
boundedPublicMethods,
)) {
this.registerPublicField({ key: methodName, locked: true }, fn)
}
} }
// emit initialize finish event
this.eventBus.emit("runtime.initialize.finish") this.eventBus.emit("runtime.initialize.finish")
this.render(this.baseAppClass)
this.render() if (!this.baseAppClass.splashAwaitEvent) {
this.splash.detach()
if (!this.AppComponent.splashAwaitEvent) {
this.detachSplashScreen()
} }
} }
initializeCore = async (core) => { appendToInitializer(task) {
if (!core.constructor) { let tasks = Array.isArray(task) ? task : [task]
this.INTERNAL_CONSOLE.error(`Core [${core.name}] is not a class`)
return false
}
const namespace = core.namespace ?? core.name
this.eventBus.emit(`runtime.initialize.core.${namespace}.start`)
// construct class
let coreInstance = new core(this)
// set core to context
this.CORES[namespace] = coreInstance
const initializationResult = await coreInstance._init()
if (!initializationResult) {
this.INTERNAL_CONSOLE.warn(`[${namespace}] initializes without returning a result.`)
}
if (initializationResult.public_context) {
this.CoresPublicContext[initializationResult.namespace] = initializationResult.public_context
}
// emit event
this.eventBus.emit(`runtime.initialize.core.${namespace}.finish`)
// register internal core
this.STATES.LOADED_CORES.push(namespace)
return true
}
initializeCores = async () => {
try {
const coresPaths = {
...import.meta.glob("/src/cores/*/*.core.jsx"),
...import.meta.glob("/src/cores/*/*.core.js"),
...import.meta.glob("/src/cores/*/*.core.ts"),
...import.meta.glob("/src/cores/*/*.core.tsx"),
}
const coresKeys = Object.keys(coresPaths)
if (coresKeys.length === 0) {
this.INTERNAL_CONSOLE.warn(`Skipping cores initialization. No cores found.`)
return true
}
// import all cores
let cores = await Promise.all(coresKeys.map(async (key) => {
const core = await coresPaths[key]().catch((err) => {
this.INTERNAL_CONSOLE.warn(`Cannot load core from ${key}.`, err)
return false
})
return core.default ?? core
}))
cores = cores.filter((core) => {
return core.constructor
})
if (!cores) {
this.INTERNAL_CONSOLE.warn(`Skipping cores initialization. No valid cores found.`)
return true
}
this.eventBus.emit(`runtime.initialize.cores.start`)
if (!Array.isArray(cores)) {
this.INTERNAL_CONSOLE.error(`Cannot initialize cores, cause it is not an array. Core dependency is not supported yet. You must use an array to define your core load queue.`)
return
}
// sort cores by dependencies
cores.forEach((core) => {
if (core.dependencies) {
core.dependencies.forEach((dependency) => {
// find dependency
const dependencyIndex = cores.findIndex((_core) => {
return (_core.namespace ?? _core.name) === dependency
})
if (dependencyIndex === -1) {
this.INTERNAL_CONSOLE.error(`Cannot find dependency [${dependency}] for core [${core.name}]`)
return
}
// move dependency to top
cores.splice(0, 0, cores.splice(dependencyIndex, 1)[0])
})
}
})
for await (let coreClass of cores) {
await this.initializeCore(coreClass)
}
// emit event
this.eventBus.emit(`runtime.initialize.cores.finish`)
} catch (error) {
this.eventBus.emit(`runtime.initialize.cores.failed`, error)
// make sure to throw that, app must crash if core fails to load
throw error
}
}
appendToInitializer = (task) => {
let tasks = []
if (Array.isArray(task)) {
tasks = task
} else {
tasks.push(task)
}
tasks.forEach((_task) => { tasks.forEach((_task) => {
if (typeof _task === "function") { if (typeof _task === "function") {
this.STATES.INITIALIZER_TASKS.push(_task) this.states.INITIALIZER_TASKS.push(_task)
} }
}) })
} }
performInitializerTasks = async () => { async performInitializerTasks() {
if (this.STATES.INITIALIZER_TASKS.length === 0) { if (this.states.INITIALIZER_TASKS.length === 0) {
this.INTERNAL_CONSOLE.warn("No initializer tasks found, skipping...") this.console.warn("Cannot find any initializer tasks, skipping...")
return true return true
} }
for await (let task of this.STATES.INITIALIZER_TASKS) { for (const task of this.states.INITIALIZER_TASKS) {
if (typeof task === "function") { if (typeof task === "function") {
try { try {
await task(this) await task(this)
} catch (error) { } catch (error) {
this.INTERNAL_CONSOLE.error(`Failed to perform initializer task >`, error) this.console.error("Error in initializer task:", error)
} }
} }
} }
} }
// CONTEXT CONTROL registerPublicField = (params = {}, value, ...args) => {
registerPublicMethods = async (methods) => { const opts = {
if (typeof methods !== "object") { key: typeof params === "string" ? params : params.key,
this.INTERNAL_CONSOLE.error("Methods must be an object")
return false
}
const boundedPublicMethods = await this.bindObjects(this, methods)
for await (let [methodName, fn] of Object.entries(boundedPublicMethods)) {
this.registerPublicMethod({
key: methodName,
locked: true,
}, fn)
}
}
registerPublicMethod = (params = {}, value, ...args) => {
let opts = {
key: params.key,
locked: params.locked ?? false, locked: params.locked ?? false,
enumerable: params.enumerable ?? true, enumerable: params.enumerable ?? true,
} }
if (typeof params === "string") {
opts.key = params
}
if (typeof opts.key === "undefined") { if (typeof opts.key === "undefined") {
throw new Error("key is required") throw new Error("a key is required")
} }
if (args.length > 0) { if (args.length > 0) {
value = value(...args) value = value(...args)
} }
try { try {
Object.defineProperty(this.PublicContext, opts.key, { Object.defineProperty(this.publicContext, opts.key, {
value, value,
enumerable: opts.enumerable, enumerable: opts.enumerable,
configurable: opts.locked configurable: opts.locked,
}) })
} catch (error) { } catch (error) {
this.INTERNAL_CONSOLE.error(error) this.console.error(error)
}
return this.publicContext[opts.key]
} }
return this.PublicContext[opts.key] render(component, props = {}) {
} const renderer = React.createElement(component, {
runtime: this,
// EXTENSIONS CONTROL
initializeExtension = (extension) => {
return new Promise(async (resolve, reject) => {
if (typeof extension !== "function") {
return reject({
reason: "EXTENSION_NOT_VALID_CLASS",
})
}
extension = new extension(this.ExtensionsContext.getProxy(), this)
const extensionName = extension.refName ?? extension.constructor.name
if (extension instanceof Extension) {
// good for u
} else {
this.STATES.REJECTED_EXTENSIONS = [extensionName, ...this.STATES.REJECTED_EXTENSIONS]
return reject({
name: extensionName,
reason: "EXTENSION_NOT_VALID_INSTANCE",
})
}
// await to extension initializer
await extension.__initializer()
// appends initializers
if (typeof extension.initializers !== "undefined") {
for await (let initializer of extension.initializers) {
await initializer.apply(this.ExtensionsContext.getProxy(), initializer)
}
}
// set window context
if (typeof extension.publicMethods === "object") {
Object.keys(extension.publicMethods).forEach((key) => {
if (typeof extension.publicMethods[key] === "function") {
extension.publicMethods[key].bind(this.ExtensionsContext.getProxy())
}
this.registerPublicMethod({ key }, extension.publicMethods[key])
})
}
// update attached extensions
this.STATES.ATTACHED_EXTENSIONS.push(extensionName)
// set extension context
this.EXTENSIONS[extensionName] = extension
return resolve()
})
}
attachSplashScreen = () => {
// create a new div inside the container
let elementContainer = document.getElementById("splash-screen")
if (!elementContainer) {
elementContainer = document.createElement("div")
// set the id of the new div
elementContainer.id = "splash-screen"
document.body.appendChild(elementContainer)
}
if (this.AppComponent.staticRenders?.Initialization) {
this.SPLASH_RENDERER = ReactDOM.render(React.createElement((this.AppComponent.staticRenders?.Initialization), {
states: this.STATES,
}), elementContainer)
}
}
detachSplashScreen = async () => {
const container = document.getElementById("splash-screen")
if (container) {
if (this.SPLASH_RENDERER && typeof this.SPLASH_RENDERER.onUnmount) {
await this.SPLASH_RENDERER.onUnmount()
}
ReactDOM.unmountComponentAtNode(container)
container.remove()
this.SPLASH_RENDERER = null
}
}
// RENDER METHOD
render(component = this.AppComponent, props = {}) {
this.APP_RENDERER = React.createElement(
component,
{
runtime: new Proxy(this, {
get: (target, prop) => {
return target[prop]
},
set: (target, prop, value) => {
throw new Error("Cannot set property of runtime")
}
}),
cores: this.CORES,
...props, ...props,
} })
)
this.root.render(this.APP_RENDERER) this.root.render(renderer)
}
getRuntimeStatus() {
return {
state: this.states.LOAD_STATE,
initializationDuration: this.states.INITIALIZATION_TOOKS,
loadedCores: this.states.LOADED_CORES,
attachedExtensions: this.states.ATTACHED_EXTENSIONS,
rejectedExtensions: this.states.REJECTED_EXTENSIONS,
}
} }
} }

View File

@ -0,0 +1,13 @@
export default function bindObjects(bind, events) {
let boundEvents = {}
for (const [event, handler] of Object.entries(events)) {
if (typeof handler === "object") {
boundEvents[event] = bindObjects(bind, handler)
} else if (typeof handler === "function") {
boundEvents[event] = handler.bind(bind)
}
}
return boundEvents
}

View File

@ -0,0 +1,11 @@
import { Observable } from "object-observer"
export default (observableArray, eventBus, eventName) => {
Observable.observe(observableArray, (changes) => {
changes.forEach((change) => {
if (change.type === "insert") {
eventBus.emit(eventName, change)
}
})
})
}

View File

@ -0,0 +1,22 @@
export default (cores) => {
cores.forEach((core) => {
if (core.dependencies) {
core.dependencies.forEach((dependency) => {
const depIndex = cores.findIndex((_core) => {
return (_core.namespace ?? _core.name) === dependency
})
if (depIndex === -1) {
console.error(
`Cannot find dependency [${dependency}] for core [${core.name}]`,
)
return
}
if (depIndex !== 0) {
cores.unshift(cores.splice(depIndex, 1)[0])
}
})
}
})
return cores
}