mirror of
https://github.com/ragestudio/vessel.git
synced 2025-06-08 18:14:17 +00:00
merge from 0.20
This commit is contained in:
parent
e897de6814
commit
fb190878b2
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,4 +19,5 @@
|
|||||||
/**/**/corenode.log
|
/**/**/corenode.log
|
||||||
|
|
||||||
# Temporal configurations
|
# Temporal configurations
|
||||||
/**/**/.aliaser
|
/**/**/.aliaser
|
||||||
|
.experimental
|
||||||
|
54
package.json
54
package.json
@ -1,31 +1,27 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"@loadable/component": "^5.16.4",
|
||||||
"react-dom": "^18.2.0"
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
},
|
"comlink": "^4.4.1",
|
||||||
"dependencies": {
|
"history": "^5.3.0",
|
||||||
"@loadable/component": "^5.16.4",
|
"less": "^4.2.0",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"million": "^3.1.11",
|
||||||
"comlink": "^4.4.1",
|
"object-observer": "^6.0.0",
|
||||||
"history": "^5.3.0",
|
"react": "18.3.1",
|
||||||
"less": "^4.2.0",
|
"react-dom": "18.3.1",
|
||||||
"million": "^3.1.11",
|
"react-helmet": "^6.1.0",
|
||||||
"object-observer": "^6.0.0",
|
"sucrase": "^3.35.0"
|
||||||
"react": "^18.3.1",
|
}
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-helmet": "^6.1.0",
|
|
||||||
"sucrase": "^3.35.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
112
src/classes/CoresManager/index.js
Normal file
112
src/classes/CoresManager/index.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,285 +1,323 @@
|
|||||||
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.
|
|
||||||
*/
|
|
||||||
public eventNames(): TemplateEventT<Template>[] {
|
|
||||||
const events = this.events;
|
|
||||||
|
|
||||||
return Object.keys(events).filter((event) => events[event] !== undefined);
|
/**
|
||||||
}
|
* Returns an array listing the events for which the emitter has registered listeners.
|
||||||
|
*/
|
||||||
|
public eventNames(): TemplateEventT<Template>[] {
|
||||||
|
const events = this.events
|
||||||
|
|
||||||
/**
|
return Object.keys(events).filter(
|
||||||
* Returns the array of listeners for the specified `event`.
|
(event) => events[event] !== undefined,
|
||||||
*
|
)
|
||||||
* @param event
|
}
|
||||||
*/
|
|
||||||
public rawListeners<Event extends TemplateEventT<Template>>(
|
|
||||||
event: Event,
|
|
||||||
): Listener<Template, Event, this>[] {
|
|
||||||
const listeners = this.events[event];
|
|
||||||
|
|
||||||
if (listeners === undefined) return [];
|
/**
|
||||||
|
* Returns the array of listeners for the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public rawListeners<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
): Listener<Template, Event, this>[] {
|
||||||
|
const listeners = this.events[event]
|
||||||
|
|
||||||
if (isSingleListener(listeners)) return [listeners];
|
if (listeners === undefined) return []
|
||||||
|
|
||||||
return listeners;
|
if (isSingleListener(listeners)) return [listeners]
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return listeners
|
||||||
* Returns a copy of the array of listeners for the specified `event`.
|
}
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
public listeners<Event extends TemplateEventT<Template>>(
|
|
||||||
event: Event,
|
|
||||||
): TemplateListenerT<Template, Event, this>[] {
|
|
||||||
const listeners = this.rawListeners(event);
|
|
||||||
const length = listeners.length;
|
|
||||||
|
|
||||||
if (length === 0) return [];
|
/**
|
||||||
|
* Returns a copy of the array of listeners for the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public listeners<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
): TemplateListenerT<Template, Event, this>[] {
|
||||||
|
const listeners = this.rawListeners(event)
|
||||||
|
const length = listeners.length
|
||||||
|
|
||||||
const result = new Array(length);
|
if (length === 0) return []
|
||||||
|
|
||||||
for (let index = 0; index < length; index++) result[index] = listeners[index].fn;
|
const result = new Array(length)
|
||||||
|
|
||||||
return result;
|
for (let index = 0; index < length; index++)
|
||||||
}
|
result[index] = listeners[index].fn
|
||||||
|
|
||||||
/**
|
return result
|
||||||
* Returns the number of listeners listening to the specified `event`.
|
}
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
public listenerCount<Event extends TemplateEventT<Template>>(
|
|
||||||
event: Event,
|
|
||||||
): number {
|
|
||||||
return this.rawListeners(event).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously calls each of the listeners registered for the specified `event`,
|
* Returns the number of listeners listening to the specified `event`.
|
||||||
* in the order they were registered, passing the supplied arguments to each.
|
*
|
||||||
*
|
* @param event
|
||||||
* @param event
|
*/
|
||||||
* @param args
|
public listenerCount<Event extends TemplateEventT<Template>>(
|
||||||
* @returns - `true` if the `event` had listeners, `false` otherwise.
|
event: Event,
|
||||||
*/
|
): number {
|
||||||
public emit<Event extends TemplateEventT<Template>>(
|
return this.rawListeners(event).length
|
||||||
event: Event,
|
}
|
||||||
...args: TemplateListenerArgsT<Template, Event>
|
|
||||||
): boolean {
|
|
||||||
const events = this.events;
|
|
||||||
const listeners = events[event];
|
|
||||||
|
|
||||||
if (listeners === undefined) {
|
/**
|
||||||
if (event === "error") throw args[0];
|
* Synchronously calls each of the listeners registered for the specified `event`,
|
||||||
|
* in the order they were registered, passing the supplied arguments to each.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param args
|
||||||
|
* @returns - `true` if the `event` had listeners, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public emit<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
...args: TemplateListenerArgsT<Template, Event>
|
||||||
|
): boolean {
|
||||||
|
const events = this.events
|
||||||
|
const listeners = events[event]
|
||||||
|
|
||||||
return false;
|
if (listeners === undefined) {
|
||||||
}
|
if (event === "error") throw args[0]
|
||||||
|
|
||||||
if (isSingleListener(listeners)) {
|
return false
|
||||||
const { fn, context, once } = listeners;
|
}
|
||||||
|
|
||||||
if (once) events[event] = undefined;
|
console.debug(
|
||||||
|
`[eventbus][${this.id}] emitted event (${event.toString()})`,
|
||||||
|
{
|
||||||
|
args: args,
|
||||||
|
listeners,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
fn.apply(context, args);
|
if (isSingleListener(listeners)) {
|
||||||
|
const { fn, context, once } = listeners
|
||||||
|
|
||||||
return true;
|
if (once) events[event] = undefined
|
||||||
}
|
|
||||||
|
|
||||||
let length = listeners.length;
|
fn.apply(context, args)
|
||||||
|
|
||||||
for (let index = 0; index < length; index++) {
|
return true
|
||||||
const { fn, context, once } = listeners[index];
|
}
|
||||||
|
|
||||||
if (once) {
|
let length = listeners.length
|
||||||
listeners.splice(index--, 1);
|
|
||||||
|
|
||||||
length--;
|
for (let index = 0; index < length; index++) {
|
||||||
}
|
const { fn, context, once } = listeners[index]
|
||||||
|
|
||||||
fn.apply(context, args);
|
if (once) {
|
||||||
}
|
listeners.splice(index--, 1)
|
||||||
|
|
||||||
if (length === 0) events[event] = undefined;
|
length--
|
||||||
else if (length === 1) events[event] = listeners[0];
|
}
|
||||||
|
|
||||||
return true;
|
fn.apply(context, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (length === 0) events[event] = undefined
|
||||||
* Adds the `listener` function to the end of the listeners array for the specified `event`.
|
else if (length === 1) events[event] = listeners[0]
|
||||||
* No checks are made to see if the listener has already been added.
|
|
||||||
* Multiple calls passing the same combination of `event` and `listener` will result in the listener being added,
|
|
||||||
* and called, multiple times.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @param listener
|
|
||||||
* @param context - default: `this` (EventEmitter instance)
|
|
||||||
*/
|
|
||||||
public addListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
|
||||||
event: Event,
|
|
||||||
listener: TemplateListenerT<Template, Event, Context>,
|
|
||||||
context?: Context,
|
|
||||||
): this {
|
|
||||||
return this._addListener(event, listener, context, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return true
|
||||||
* Adds a one-time `listener` function for the specified `event` to the end of the `listeners` array.
|
}
|
||||||
* The next time `event` is triggered, this listener is removed, and then invoked.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @param listener
|
|
||||||
* @param context - default: `this` (EventEmitter instance)
|
|
||||||
*/
|
|
||||||
public once<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
|
||||||
event: Event,
|
|
||||||
listener: TemplateListenerT<Template, Event, Context>,
|
|
||||||
context?: Context,
|
|
||||||
): this {
|
|
||||||
return this._addListener(event, listener, context, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the `listener` function to the beginning of the listeners array for the specified `event`.
|
* Adds the `listener` function to the end of the listeners array for the specified `event`.
|
||||||
* No checks are made to see if the listener has already been added.
|
* No checks are made to see if the listener has already been added.
|
||||||
* Multiple calls passing the same combination of `event` and `listener` will result in the listener being added,
|
* Multiple calls passing the same combination of `event` and `listener` will result in the listener being added,
|
||||||
* and called, multiple times.
|
* and called, multiple times.
|
||||||
*
|
*
|
||||||
* @param event
|
* @param event
|
||||||
* @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 addListener<
|
||||||
event: Event,
|
Event extends TemplateEventT<Template>,
|
||||||
listener: TemplateListenerT<Template, Event, Context>,
|
Context = TemplateListenerContextT<Template, Event, this>,
|
||||||
context?: Context,
|
>(
|
||||||
): this {
|
event: Event,
|
||||||
return this._addListener(event, listener, context, false, false);
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
}
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a one-time `listener` function for the specified `event` to the beginning of the `listeners` array.
|
* Adds a one-time `listener` function for the specified `event` to the end of the `listeners` array.
|
||||||
* The next time `event` is triggered, this listener is removed, and then invoked.
|
* The next time `event` is triggered, this listener is removed, and then invoked.
|
||||||
*
|
*
|
||||||
* @param event
|
* @param event
|
||||||
* @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 once<
|
||||||
event: Event,
|
Event extends TemplateEventT<Template>,
|
||||||
listener: TemplateListenerT<Template, Event, Context>,
|
Context = TemplateListenerContextT<Template, Event, this>,
|
||||||
context?: Context,
|
>(
|
||||||
): this {
|
event: Event,
|
||||||
return this._addListener(event, listener, context, false, true);
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
}
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all `listeners`, or those of the specified `event`.
|
* Adds the `listener` function to the beginning of the listeners array for the specified `event`.
|
||||||
*
|
* No checks are made to see if the listener has already been added.
|
||||||
* @param event
|
* Multiple calls passing the same combination of `event` and `listener` will result in the listener being added,
|
||||||
*/
|
* and called, multiple times.
|
||||||
public removeAllListeners<Event extends TemplateEventT<Template>>(
|
*
|
||||||
event?: Event,
|
* @param event
|
||||||
): this {
|
* @param listener
|
||||||
if (event === undefined) {
|
* @param context - default: `this` (EventEmitter instance)
|
||||||
this.events = {};
|
*/
|
||||||
|
public prependListener<
|
||||||
|
Event extends TemplateEventT<Template>,
|
||||||
|
Context = TemplateListenerContextT<Template, Event, this>,
|
||||||
|
>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
/**
|
||||||
}
|
* Adds a one-time `listener` function for the specified `event` to the beginning of the `listeners` array.
|
||||||
|
* The next time `event` is triggered, this listener is removed, and then invoked.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param listener
|
||||||
|
* @param context - default: `this` (EventEmitter instance)
|
||||||
|
*/
|
||||||
|
public prependOnceListener<
|
||||||
|
Event extends TemplateEventT<Template>,
|
||||||
|
Context = TemplateListenerContextT<Template, Event, this>,
|
||||||
|
>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.events[event] === undefined) return this;
|
/**
|
||||||
|
* Removes all `listeners`, or those of the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public removeAllListeners<Event extends TemplateEventT<Template>>(
|
||||||
|
event?: Event,
|
||||||
|
): this {
|
||||||
|
if (event === undefined) {
|
||||||
|
this.events = {}
|
||||||
|
|
||||||
this.events[event] = undefined;
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
if (this.events[event] === undefined) return this
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.events[event] = undefined
|
||||||
* Removes the specified `listener` from the `listeners` array for the specified `event`.
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @param listener
|
|
||||||
*/
|
|
||||||
public removeListener<Event extends TemplateEventT<Template>>(
|
|
||||||
event: Event,
|
|
||||||
listener: TemplateListenerT<Template, Event, this>,
|
|
||||||
): this {
|
|
||||||
const listeners = this.events[event];
|
|
||||||
|
|
||||||
if (listeners === undefined) return this;
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
if (isSingleListener(listeners)) {
|
/**
|
||||||
if (listeners.fn === listener) this.events[event] = undefined;
|
* Removes the specified `listener` from the `listeners` array for the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param listener
|
||||||
|
*/
|
||||||
|
public removeListener<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, this>,
|
||||||
|
): this {
|
||||||
|
const listeners = this.events[event]
|
||||||
|
|
||||||
return this;
|
if (listeners === undefined) return this
|
||||||
}
|
|
||||||
|
|
||||||
for (let index = listeners.length - 1; index >= 0; index--) {
|
if (isSingleListener(listeners)) {
|
||||||
if (listeners[index].fn === listener) listeners.splice(index, 1);
|
if (listeners.fn === listener) this.events[event] = undefined
|
||||||
}
|
|
||||||
|
|
||||||
if (listeners.length === 0) this.events[event] = undefined;
|
return this
|
||||||
else if (listeners.length === 1) this.events[event] = listeners[0];
|
}
|
||||||
|
|
||||||
return this;
|
for (let index = listeners.length - 1; index >= 0; index--) {
|
||||||
}
|
if (listeners[index].fn === listener) listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
protected _addListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
if (listeners.length === 0) this.events[event] = undefined
|
||||||
event: Event,
|
else if (listeners.length === 1) this.events[event] = listeners[0]
|
||||||
fn: TemplateListenerT<Template, Event, Context>,
|
|
||||||
context: Context = this as never,
|
|
||||||
append: boolean,
|
|
||||||
once: boolean,
|
|
||||||
): this {
|
|
||||||
const events = this.events;
|
|
||||||
const listeners = events[event];
|
|
||||||
const listener = new Listener<Template, Event, Context>(fn, context, once);
|
|
||||||
|
|
||||||
if (listeners === undefined) {
|
return this
|
||||||
events[event] = listener;
|
}
|
||||||
|
|
||||||
return this;
|
protected _addListener<
|
||||||
}
|
Event extends TemplateEventT<Template>,
|
||||||
|
Context = TemplateListenerContextT<Template, Event, this>,
|
||||||
|
>(
|
||||||
|
event: Event,
|
||||||
|
fn: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context: Context = this as never,
|
||||||
|
append: boolean,
|
||||||
|
once: boolean,
|
||||||
|
): this {
|
||||||
|
const events = this.events
|
||||||
|
const listeners = events[event]
|
||||||
|
const listener = new Listener<Template, Event, Context>(
|
||||||
|
fn,
|
||||||
|
context,
|
||||||
|
once,
|
||||||
|
)
|
||||||
|
|
||||||
if (isSingleListener(listeners)) {
|
if (listeners === undefined) {
|
||||||
events[event] = append ? [listeners, listener] : [listener, listeners];
|
events[event] = listener
|
||||||
|
|
||||||
return this;
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
if (append) {
|
if (isSingleListener(listeners)) {
|
||||||
listeners.push(listener);
|
events[event] = append
|
||||||
} else {
|
? [listeners, listener]
|
||||||
listeners.unshift(listener);
|
: [listener, listeners]
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
listeners.push(listener)
|
||||||
|
} else {
|
||||||
|
listeners.unshift(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>[]
|
||||||
|
}
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import EventEmitter from "./EventEmitter"
|
import EventEmitter from "./EventEmitter"
|
||||||
|
|
||||||
export default class EventBus extends EventEmitter {
|
export default class EventBus extends EventEmitter {
|
||||||
constructor(params = {}) {
|
constructor(params = {}) {
|
||||||
super({ ...params, captureRejections: true })
|
super({ ...params, captureRejections: true })
|
||||||
|
|
||||||
this.id = params?.id
|
this.id = params?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
_emit = this.emit
|
_emit = this.emit
|
||||||
|
|
||||||
emit = (event, ...args) => {
|
emit = (event, ...args) => {
|
||||||
return this._emit(event, ...args)
|
return this._emit(event, ...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
on = (event, listener, context) => {
|
on = (event, listener, context) => {
|
||||||
return this._addListener(event, listener, context, true, false)
|
return this._addListener(event, listener, context, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
off = (event, listener) => {
|
off = (event, listener) => {
|
||||||
return this.removeListener(event, listener)
|
return this.removeListener(event, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
once = (event, listener, context) => {
|
once = (event, listener, context) => {
|
||||||
return this.once(event, listener, context)
|
return this.once(event, listener, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,46 +5,46 @@ import * as Comlink from "comlink"
|
|||||||
import ExtensionWorker from "../../workers/extension.js?worker"
|
import ExtensionWorker from "../../workers/extension.js?worker"
|
||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
logger = new InternalConsole({
|
logger = new InternalConsole({
|
||||||
namespace: "ExtensionsManager",
|
namespace: "ExtensionsManager",
|
||||||
bgColor: "bgMagenta",
|
bgColor: "bgMagenta",
|
||||||
})
|
})
|
||||||
|
|
||||||
extensions = new Map()
|
extensions = new Map()
|
||||||
|
|
||||||
loadExtension = async (manifest) => {
|
loadExtension = async (manifest) => {
|
||||||
if (isUrl(manifest)) {
|
throw new Error("Not implemented")
|
||||||
manifest = await fetch(manifest)
|
|
||||||
manifest = await manifest.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
const worker = new ExtensionWorker()
|
if (isUrl(manifest)) {
|
||||||
|
manifest = await fetch(manifest)
|
||||||
|
manifest = await manifest.json()
|
||||||
|
}
|
||||||
|
|
||||||
worker.postMessage({
|
const worker = new ExtensionWorker()
|
||||||
event: "load",
|
|
||||||
manifest: manifest,
|
|
||||||
})
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
worker.postMessage({
|
||||||
worker.onmessage = ({data}) => {
|
event: "load",
|
||||||
if (data.event === "loaded") {
|
manifest: manifest,
|
||||||
resolve()
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(Comlink.wrap(worker))
|
await new Promise((resolve) => {
|
||||||
|
worker.onmessage = ({ data }) => {
|
||||||
|
if (data.event === "loaded") {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// if (typeof main.events === "object") {
|
console.log(Comlink.wrap(worker))
|
||||||
// Object.entries(main.events).forEach(([event, handler]) => {
|
|
||||||
// main.event.on(event, handler)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.extensions.set(manifest.registryId,main.public)
|
// if (typeof main.events === "object") {
|
||||||
}
|
// Object.entries(main.events).forEach(([event, handler]) => {
|
||||||
|
// main.event.on(event, handler)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
installExtension = async () => {
|
// this.extensions.set(manifest.registryId,main.public)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
installExtension = async () => {}
|
||||||
}
|
}
|
||||||
|
19
src/classes/SplashScreenManager/index.js
Normal file
19
src/classes/SplashScreenManager/index.js
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/managers/ContextManager.js
Normal file
33
src/managers/ContextManager.js
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
30
src/managers/CoreManager.js
Normal file
30
src/managers/CoreManager.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
27
src/managers/StateManager.js
Normal file
27
src/managers/StateManager.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
736
src/runtime.jsx
736
src/runtime.jsx
@ -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" }) {
|
||||||
debug: false,
|
this.baseAppClass = baseAppClass
|
||||||
})
|
this.params = params
|
||||||
|
|
||||||
INTERNAL_CONSOLE = new InternalConsole({
|
this.initialize().catch((error) => {
|
||||||
namespace: "Runtime",
|
this.eventBus.emit("runtime.initialize.crash", error)
|
||||||
bgColor: "bgMagenta",
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
EXTENSIONS = Object()
|
// contexts & managers
|
||||||
CORES = Object()
|
publicContext = (window.app = {})
|
||||||
|
cores = new CoresManager(this)
|
||||||
PublicContext = window.app = Object()
|
extensions = new ExtensionManager(this)
|
||||||
|
splash = new SplashScreenManager()
|
||||||
ExtensionsPublicContext = Object()
|
|
||||||
CoresPublicContext = Object()
|
flags = Observable.from({
|
||||||
|
debug: false,
|
||||||
STATES = Observable.from({
|
})
|
||||||
LOAD_STATE: "early",
|
console = new InternalConsole({
|
||||||
|
namespace: "Runtime",
|
||||||
INITIALIZER_TASKS: [],
|
bgColor: "bgMagenta",
|
||||||
|
})
|
||||||
LOADED_CORES: [],
|
history = createBrowserHistory()
|
||||||
|
eventBus = new EventBus({
|
||||||
ATTACHED_EXTENSIONS: [],
|
id: "runtime",
|
||||||
REJECTED_EXTENSIONS: [],
|
})
|
||||||
|
|
||||||
INITIALIZATION_START: null,
|
states = Observable.from({
|
||||||
INITIALIZATION_STOP: null,
|
LOAD_STATE: "early",
|
||||||
INITIALIZATION_TOOKS: null,
|
INITIALIZER_TASKS: [],
|
||||||
})
|
INITIALIZATION_START: null,
|
||||||
|
INITIALIZATION_STOP: null,
|
||||||
APP_RENDERER = null
|
INITIALIZATION_TOOKS: null,
|
||||||
SPLASH_RENDERER = null
|
})
|
||||||
|
|
||||||
constructor(
|
internalEvents = {
|
||||||
App,
|
"runtime.initialize.start": () => {
|
||||||
Params = {
|
this.states.LOAD_STATE = "initializing"
|
||||||
renderMount: "root",
|
this.states.INITIALIZATION_START = performance.now()
|
||||||
}
|
},
|
||||||
) {
|
"runtime.initialize.finish": () => {
|
||||||
this.INTERNAL_CONSOLE.log(`Using React ${React.version}`)
|
const time = performance.now()
|
||||||
|
this.states.INITIALIZATION_STOP = time
|
||||||
this.AppComponent = App
|
if (this.states.INITIALIZATION_START) {
|
||||||
this.Params = Params
|
this.states.INITIALIZATION_TOOKS =
|
||||||
|
time - this.states.INITIALIZATION_START
|
||||||
// toogle splash
|
}
|
||||||
this.attachSplashScreen()
|
this.states.LOAD_STATE = "initialized"
|
||||||
|
},
|
||||||
// controllers
|
"runtime.initialize.crash": (error) => {
|
||||||
this.root = createRoot(document.getElementById(this.Params.renderMount ?? "root"))
|
this.states.LOAD_STATE = "crashed"
|
||||||
this.history = this.registerPublicMethod({ key: "history", locked: true }, createBrowserHistory())
|
this.splash.detach()
|
||||||
this.eventBus = this.registerPublicMethod({ key: "eventBus", locked: true }, new EventBus())
|
this.console.error("Runtime crashed on initialization\n", error)
|
||||||
|
this.render(
|
||||||
window.app.cores = new Proxy(this.CoresPublicContext, {
|
this.baseAppClass.staticRenders?.Crash ?? StaticRenders.Crash,
|
||||||
get: (target, key) => {
|
{
|
||||||
return target[key]
|
crash: {
|
||||||
},
|
message: "Runtime crashed on initialization",
|
||||||
set: (target, key, value) => {
|
details: error.toString(),
|
||||||
throw new Error("You can't set a core value")
|
},
|
||||||
}
|
},
|
||||||
})
|
)
|
||||||
|
},
|
||||||
window.app.extensions = new ExtensionManager()
|
"runtime.crash": (crash) => {
|
||||||
|
this.states.LOAD_STATE = "crashed"
|
||||||
this.registerPublicMethod({ key: "isMobile", locked: true }, isMobile())
|
this.splash.detach()
|
||||||
this.registerPublicMethod({ key: "__version", locked: true }, pkgJson.version)
|
this.render(
|
||||||
|
this.baseAppClass.staticRenders?.Crash ?? StaticRenders.Crash,
|
||||||
if (typeof this.AppComponent.splashAwaitEvent === "string") {
|
{ crash },
|
||||||
this.eventBus.on(this.AppComponent.splashAwaitEvent, () => {
|
)
|
||||||
this.detachSplashScreen()
|
},
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
registerEventsToBus(eventsObj) {
|
||||||
for (const [key, fn] of Object.entries(this.internalEvents)) {
|
for (const [event, handler] of Object.entries(eventsObj)) {
|
||||||
this.eventBus.on(key, fn)
|
this.eventBus.on(event, handler.bind(this))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// emit attached extensions change events
|
|
||||||
Observable.observe(this.STATES.ATTACHED_EXTENSIONS, (changes) => {
|
async initialize() {
|
||||||
changes.forEach((change) => {
|
this.eventBus.emit("runtime.initialize.start")
|
||||||
if (change.type === "insert") {
|
|
||||||
this.eventBus.emit(`runtime.extension.attached`, change)
|
this.console.log(
|
||||||
}
|
`Using React ${React.version} | Runtime v${pkgJson.version}`,
|
||||||
})
|
)
|
||||||
})
|
|
||||||
|
this.splash.attach()
|
||||||
// emit rejected extensions change events
|
|
||||||
Observable.observe(this.STATES.REJECTED_EXTENSIONS, (changes) => {
|
this.root = createRoot(
|
||||||
changes.forEach((change) => {
|
document.getElementById(this.params.renderMount ?? "root"),
|
||||||
if (change.type === "insert") {
|
)
|
||||||
this.eventBus.emit(`runtime.extension.rejected`, change)
|
|
||||||
}
|
this.registerPublicField("history", this.history)
|
||||||
})
|
this.registerPublicField("eventBus", this.eventBus)
|
||||||
})
|
this.registerPublicField("isMobile", isMobile())
|
||||||
|
this.registerPublicField("__version", pkgJson.version)
|
||||||
return this.initialize().catch((error) => {
|
|
||||||
this.eventBus.emit("runtime.initialize.crash", error)
|
window.app.cores = this.cores.getCoreContext()
|
||||||
})
|
//window.app.extensions = new ExtensionManager()
|
||||||
}
|
|
||||||
|
this.registerEventsToBus(this.internalEvents)
|
||||||
internalEvents = {
|
|
||||||
"runtime.initialize.start": () => {
|
if (typeof this.baseAppClass.events === "object") {
|
||||||
this.STATES.LOAD_STATE = "initializing"
|
for (const [event, handler] of Object.entries(
|
||||||
this.STATES.INITIALIZATION_START = performance.now()
|
this.baseAppClass.events,
|
||||||
},
|
)) {
|
||||||
"runtime.initialize.finish": () => {
|
this.eventBus.on(event, (...args) => handler(this, ...args))
|
||||||
const time = performance.now()
|
}
|
||||||
|
}
|
||||||
this.STATES.INITIALIZATION_STOP = time
|
|
||||||
|
if (this.baseAppClass.splashAwaitEvent) {
|
||||||
if (this.STATES.INITIALIZATION_START) {
|
this.eventBus.on(this.baseAppClass.splashAwaitEvent, () => {
|
||||||
this.STATES.INITIALIZATION_TOOKS = time - this.STATES.INITIALIZATION_START
|
this.splash.detach()
|
||||||
}
|
})
|
||||||
|
}
|
||||||
this.STATES.LOAD_STATE = "initialized"
|
|
||||||
},
|
await this.cores.initialize()
|
||||||
"runtime.initialize.crash": (error) => {
|
await this.performInitializerTasks()
|
||||||
this.STATES.LOAD_STATE = "crashed"
|
|
||||||
|
if (typeof this.baseAppClass.initialize === "function") {
|
||||||
if (this.SPLASH_RENDERER) {
|
await this.baseAppClass.initialize.call(this)
|
||||||
this.detachSplashScreen()
|
}
|
||||||
}
|
|
||||||
|
if (typeof this.baseAppClass.publicEvents === "object") {
|
||||||
this.INTERNAL_CONSOLE.error("Runtime crashed on initialization \n", error)
|
for (const [event, handler] of Object.entries(
|
||||||
|
this.baseAppClass.publicEvents,
|
||||||
// render crash
|
)) {
|
||||||
this.render(this.AppComponent.staticRenders?.Crash ?? StaticRenders.Crash, {
|
this.eventBus.on(event, handler.bind(this))
|
||||||
crash: {
|
}
|
||||||
message: "Runtime crashed on initialization",
|
}
|
||||||
details: error.toString(),
|
|
||||||
}
|
if (typeof this.baseAppClass.publicMethods === "object") {
|
||||||
})
|
const boundedPublicMethods = await bindObjects(
|
||||||
},
|
this,
|
||||||
"runtime.crash": (crash) => {
|
this.baseAppClass.publicMethods,
|
||||||
this.STATES.LOAD_STATE = "crashed"
|
)
|
||||||
|
|
||||||
if (this.SPLASH_RENDERER) {
|
for (const [methodName, fn] of Object.entries(
|
||||||
this.detachSplashScreen()
|
boundedPublicMethods,
|
||||||
}
|
)) {
|
||||||
|
this.registerPublicField({ key: methodName, locked: true }, fn)
|
||||||
// render crash
|
}
|
||||||
this.render(this.AppComponent.staticRenders?.Crash ?? StaticRenders.Crash, {
|
}
|
||||||
crash
|
|
||||||
})
|
this.eventBus.emit("runtime.initialize.finish")
|
||||||
},
|
this.render(this.baseAppClass)
|
||||||
}
|
|
||||||
|
if (!this.baseAppClass.splashAwaitEvent) {
|
||||||
bindObjects = async (bind, events, parent) => {
|
this.splash.detach()
|
||||||
let boundEvents = {}
|
}
|
||||||
|
}
|
||||||
for await (let [event, handler] of Object.entries(events)) {
|
|
||||||
if (typeof handler === "object") {
|
appendToInitializer(task) {
|
||||||
boundEvents[event] = await this.bindObjects(bind, handler, parent)
|
let tasks = Array.isArray(task) ? task : [task]
|
||||||
}
|
tasks.forEach((_task) => {
|
||||||
|
if (typeof _task === "function") {
|
||||||
if (typeof handler === "function") {
|
this.states.INITIALIZER_TASKS.push(_task)
|
||||||
boundEvents[event] = handler.bind(bind)
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return boundEvents
|
async performInitializerTasks() {
|
||||||
}
|
if (this.states.INITIALIZER_TASKS.length === 0) {
|
||||||
|
this.console.warn("Cannot find any initializer tasks, skipping...")
|
||||||
async initialize() {
|
return true
|
||||||
this.eventBus.emit("runtime.initialize.start")
|
}
|
||||||
|
|
||||||
await this.initializeCores()
|
for (const task of this.states.INITIALIZER_TASKS) {
|
||||||
|
if (typeof task === "function") {
|
||||||
await this.performInitializerTasks()
|
try {
|
||||||
|
await task(this)
|
||||||
// call early app initializer
|
} catch (error) {
|
||||||
if (typeof this.AppComponent.initialize === "function") {
|
this.console.error("Error in initializer task:", error)
|
||||||
await this.AppComponent.initialize.apply(this)
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// handle app events handlers registration
|
}
|
||||||
if (typeof this.AppComponent.publicEvents === "object") {
|
|
||||||
for await (let [event, handler] of Object.entries(this.AppComponent.publicEvents)) {
|
registerPublicField = (params = {}, value, ...args) => {
|
||||||
this.eventBus.on(event, handler.bind(this))
|
const opts = {
|
||||||
}
|
key: typeof params === "string" ? params : params.key,
|
||||||
}
|
locked: params.locked ?? false,
|
||||||
|
enumerable: params.enumerable ?? true,
|
||||||
// handle app public methods registration
|
}
|
||||||
if (typeof this.AppComponent.publicMethods === "object") {
|
if (typeof opts.key === "undefined") {
|
||||||
await this.registerPublicMethods(this.AppComponent.publicMethods)
|
throw new Error("a key is required")
|
||||||
}
|
}
|
||||||
|
if (args.length > 0) {
|
||||||
// emit initialize finish event
|
value = value(...args)
|
||||||
this.eventBus.emit("runtime.initialize.finish")
|
}
|
||||||
|
try {
|
||||||
this.render()
|
Object.defineProperty(this.publicContext, opts.key, {
|
||||||
|
value,
|
||||||
if (!this.AppComponent.splashAwaitEvent) {
|
enumerable: opts.enumerable,
|
||||||
this.detachSplashScreen()
|
configurable: opts.locked,
|
||||||
}
|
})
|
||||||
}
|
} catch (error) {
|
||||||
|
this.console.error(error)
|
||||||
initializeCore = async (core) => {
|
}
|
||||||
if (!core.constructor) {
|
return this.publicContext[opts.key]
|
||||||
this.INTERNAL_CONSOLE.error(`Core [${core.name}] is not a class`)
|
}
|
||||||
|
|
||||||
return false
|
render(component, props = {}) {
|
||||||
}
|
const renderer = React.createElement(component, {
|
||||||
|
runtime: this,
|
||||||
const namespace = core.namespace ?? core.name
|
...props,
|
||||||
|
})
|
||||||
this.eventBus.emit(`runtime.initialize.core.${namespace}.start`)
|
|
||||||
|
this.root.render(renderer)
|
||||||
// construct class
|
}
|
||||||
let coreInstance = new core(this)
|
|
||||||
|
getRuntimeStatus() {
|
||||||
// set core to context
|
return {
|
||||||
this.CORES[namespace] = coreInstance
|
state: this.states.LOAD_STATE,
|
||||||
|
initializationDuration: this.states.INITIALIZATION_TOOKS,
|
||||||
const initializationResult = await coreInstance._init()
|
loadedCores: this.states.LOADED_CORES,
|
||||||
|
attachedExtensions: this.states.ATTACHED_EXTENSIONS,
|
||||||
if (!initializationResult) {
|
rejectedExtensions: this.states.REJECTED_EXTENSIONS,
|
||||||
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) => {
|
|
||||||
if (typeof _task === "function") {
|
|
||||||
this.STATES.INITIALIZER_TASKS.push(_task)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
performInitializerTasks = async () => {
|
|
||||||
if (this.STATES.INITIALIZER_TASKS.length === 0) {
|
|
||||||
this.INTERNAL_CONSOLE.warn("No initializer tasks found, skipping...")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (let task of this.STATES.INITIALIZER_TASKS) {
|
|
||||||
if (typeof task === "function") {
|
|
||||||
try {
|
|
||||||
await task(this)
|
|
||||||
} catch (error) {
|
|
||||||
this.INTERNAL_CONSOLE.error(`Failed to perform initializer task >`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CONTEXT CONTROL
|
|
||||||
registerPublicMethods = async (methods) => {
|
|
||||||
if (typeof methods !== "object") {
|
|
||||||
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,
|
|
||||||
enumerable: params.enumerable ?? true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof params === "string") {
|
|
||||||
opts.key = params
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof opts.key === "undefined") {
|
|
||||||
throw new Error("key is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.length > 0) {
|
|
||||||
value = value(...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Object.defineProperty(this.PublicContext, opts.key, {
|
|
||||||
value,
|
|
||||||
enumerable: opts.enumerable,
|
|
||||||
configurable: opts.locked
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
this.INTERNAL_CONSOLE.error(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.PublicContext[opts.key]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
this.root.render(this.APP_RENDERER)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
13
src/utils/bindObjects/index.js
Normal file
13
src/utils/bindObjects/index.js
Normal 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
|
||||||
|
}
|
11
src/utils/observeArrayInsertEmitter/index.js
Normal file
11
src/utils/observeArrayInsertEmitter/index.js
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
22
src/utils/sortCoresByDependencies/index.js
Normal file
22
src/utils/sortCoresByDependencies/index.js
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user