import State from "@infinity-beyond/modules/state.ts";
import { randomUUID } from "node:crypto";
import { EntityWorker, EntityMeta } from "@infinity-beyond/classes/data_types/entity/mod.ts";
import { Sluggify } from "@infinity-beyond/utils/slug.ts";
import { CustomEventEmitter } from "@infinity-beyond/classes/custom_event_emitter.ts";
interface I_EntityTask extends I_ProcessorRequest {
is_static: boolean;
resolve: VagueFunction;
}
type EntityEvents = Record<string, any[]>;
type EntityPermissionMap<
PermissionMap extends readonly string[],
T extends string = PermissionMap[number],
> = { [TKey in T]: TKey };
export class Entity<
EventMap extends Record<string, any[]> = EntityEvents,
EntityName extends string = "",
PermissionMap extends readonly string[] = [],
EMC extends Entity.Meta.Config = Entity.Meta.Config,
PEMC extends Entity.Meta.Config = Entity.Meta.Config,
Options extends Entity.Options<PEMC> = Entity.Options<PEMC>
> extends CustomEventEmitter<EventMap> {
private meta_fields: EMC;
private passed_meta_fields: PEMC;
meta: EntityMeta<EMC, PEMC>;
readonly options: Options;
protected static Permissions: readonly string[] = [];
protected __permissions: EntityPermissionMap<PermissionMap> =
{} as EntityPermissionMap<PermissionMap>;
protected finalize_permissions() {
this.entity_class.Permissions.forEach((p) => {
const permission = p.replace(/{NAME}/gi, this.name.toUpperCase());
this.__permissions[permission as PermissionMap[number]] = permission;
});
Object.freeze(this.__permissions);
}
get permissions() {
return this.__permissions;
}
static readonly __is_typeof_entity = true;
readonly __is_instanceof_entity = true;
readonly name: string;
constructor(
name: string,
options: Options,
meta_fields: EMC,
) {
super();
this.name = name;
this.options = options;
this.meta_fields = meta_fields;
this.passed_meta_fields = options.meta_fields || {} as PEMC;
this.meta = new EntityMeta(
this.meta_table,
meta_fields,
options.meta_fields,
);
this.finalize_permissions();
}
get slug() {
return Sluggify(this.name);
}
private static __workers: EntityWorker[] = [];
static get __worker_count() {
return this.__workers.length;
}
private static __delegation_index = 0;
protected static get __next_worker() {
if (++this.__delegation_index >= this.__workers.length) {
this.__delegation_index = 0;
}
return this.__workers[this.__delegation_index];
}
// #region Queueing and Delegation
private static __tasks_in_progress: Map<string, true> = new Map();
protected static __task_queue: I_EntityTask[] = [];
protected static __queue_processing = false;
protected static __ProcessQueue() {
if (this.__queue_processing) return;
if (!this.__task_queue.length) return this.__queue_processing = false;
if (!this.__workers.length) {
this.warn(
"::no_workers -",
`${this.__task_queue.length} tasks waiting for a worker!`,
);
return;
}
this.__queue_processing = true;
while (this.__task_queue.length) {
const task = this.__task_queue.shift();
if (!task) continue;
this.__next_worker.dispatch(task).then((response) => {
task.resolve(response);
this.__tasks_in_progress.delete(task.id);
});
}
this.__queue_processing = false;
}
static __delegate_instance(
instance: Entity<any>,
method: string,
args: any[],
) {
return new Promise((resolve) => {
let id: string;
do {
id = randomUUID();
} while (this.__tasks_in_progress.has(id));
this.__tasks_in_progress.set(id, true);
this.__task_queue.push({
id,
entity_name: instance.name,
is_static: false,
method,
args,
resolve,
});
this.__ProcessQueue();
});
}
static __delegate(method: string, args: any[]) {
return new Promise((resolve) => {
let id: string;
do {
id = randomUUID();
} while (this.__tasks_in_progress.has(id));
this.__tasks_in_progress.set(id, true);
this.__task_queue.push({
id,
entity_name: this.name,
is_static: true,
method,
args,
resolve,
});
this.__ProcessQueue();
});
}
// #region Workers
static __AddWorker(uuid: string, socket: WebSocket) {
const worker = new EntityWorker(uuid, socket, this);
this.__workers.push(worker);
this.__ProcessQueue();
}
static __RemoveWorker(worker: EntityWorker) {
console.log(`Worker lost! [${worker.uuid}]`);
const index = this.__workers.findIndex((w) => w.uuid == worker.uuid);
if (index > -1) this.__workers.splice(index, 1);
}
// #region Logging
static log(...args: any[]) {
console.log(`[${this.name}]`, ...args);
}
static warn(...args: any[]) {
console.warn(`[${this.name}]`, ...args);
}
protected get meta_table() {
return `${this.slug}_meta`;
}
get entity_class() {
return this.constructor as typeof Entity;
}
async Setup() {
if (State.IS_BUILDING) return this;
const slug = this.slug;
const meta_table = this.meta_table;
const meta_entries = Object.entries({
...this.meta_fields,
...this.passed_meta_fields,
});
if (meta_entries.length) {
await State.PostgresClient.CreateTable(meta_table, {
key: "varchar(24) NOT NULL",
value_text: "varchar(128)",
value_int: "int4",
value_real: "real",
value_bool: "boolean",
}, {
async onCreate() {
await State.PostgresClient.query(`
CREATE UNIQUE INDEX ${slug}_meta_key_idx ON ${meta_table} (key);
`);
const placeholders: string[] = [];
const values: string[] = [];
for (const [key, type] of meta_entries) {
placeholders.push(`($${values.length + 1}, $${values.length + 2})`);
let value: string = "";
if (type == "int" || type == "real") value = "0";
values.push(key, value);
}
await State.PostgresClient.query(
`INSERT INTO ${meta_table} (key, value) VALUES ${placeholders}`,
values,
);
},
});
}
return this;
}
}
// #region Decorators
type OffloadDecoratorMethod = (
originalMethod: VagueAsyncFunction,
context: ClassMethodDecoratorContext,
) => void;
type OffloadDecoratorWithWrapper = <T>(
wrapper: new (...args: any[]) => T,
) => OffloadDecoratorMethod;
type OffloadDecorator = {
with_response_wrapper: OffloadDecoratorWithWrapper;
} & OffloadDecoratorMethod;
const run_on_worker_method: OffloadDecoratorMethod = function (
this: any,
originalMethod: VagueAsyncFunction,
{ name: methodName, static: isStatic, addInitializer }:
ClassMethodDecoratorContext,
) {
addInitializer(function () {
if (State.IS_PROCESSOR) return;
if (isStatic) {
const entityClass = this as typeof Entity;
Object.assign(entityClass, {
[methodName]: (...args: any[]) => {
return entityClass.__delegate(originalMethod.name, args);
},
});
} else {
const entityInstance = this as Entity<any>;
const entityClass = entityInstance.constructor as typeof Entity;
Object.assign(entityInstance!, {
[methodName]: function (this: Entity<any, any, any>, ...args: any[]) {
return entityClass.__delegate_instance(
entityInstance,
originalMethod.name,
args,
);
},
});
}
});
};
export const run_on_worker: OffloadDecorator = Object.assign(
run_on_worker_method,
{
with_response_wrapper: ((wrapper) => {
return run_on_worker_method.bind(wrapper);
}) as OffloadDecoratorWithWrapper,
},
);