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";
import type { REST_Wrapper } from "@infinity-beyond/classes/rest_wrapper.ts";
import type { VNode } from "preact";
import { plural } from "https://deno.land/x/deno_plural@2.0.0/mod.ts";
interface I_EntityTask extends I_ProcessorRequest {
is_static: boolean;
resolve: VagueFunction;
}
type EntityEvents = Record<string, any[]>;
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: Entity.Permissions.Map<PermissionMap> =
{} as Entity.Permissions.Map<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;
REST!: REST_Wrapper;
readonly name: string;
constructor(
name: string,
options: Entity.Options.Optional<PEMC>,
meta_fields: EMC,
) {
super();
this.name = name;
options.key ||= {};
this.options = options as 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,
) as unknown as EntityMeta<EMC, PEMC>;
this.finalize_permissions();
}
get slug() {
return Sluggify(this.name);
}
get slug_plural() {
return plural(Sluggify(this.name));
}
format_and_validate(key: string) {
return this.validate_key(this.format_key(key));
}
format_key(key: string) {
return this.options.key.formatter?.(key) ?? key;
}
validate_key(key: string): ValidateKeyResponse {
if (!this.options.key.validator) {
return { key, valid: true, reason: null };
}
const validation_result = this.options.key.validator(key);
if (validation_result === true) {
return { key, valid: true, reason: null };
}
if (validation_result === false) {
return {
key,
valid: false,
reason: "Unspecified: Validator did not return a failure reason.",
};
}
return { key, ...validation_result };
}
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 UI Helpers
charts(): VNode<any> | null {
return null;
}
stats(): VNode<any> | null {
return null;
}
// #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 meta_table = this.meta_table;
const meta_columns = {
...this.meta_fields,
...this.passed_meta_fields,
};
if (Object.keys(meta_columns).length) {
const columns: string[] = [];
const placeholders: string[] = [];
const values: any[] = [];
for (const [key, type] of Object.entries(meta_columns)) {
columns.push(key);
placeholders.push(`$${values.length + 1}`);
switch(type) {
case 'int':
values.push(0);
break;
case 'boolean':
values.push(false);
break;
case 'real':
values.push(0);
break;
case 'text':
values.push('');
break;
}
}
await State.PostgresClient.CreateTable(meta_table, {
id: 'serial primary key',
type: { enum_values: [ 'live', 'snapshot_m', 'snapshot_h', 'snapshot_d' ] },
timestamp: 'timestamptz',
...meta_columns
}, {
async onCreate() {
await State.PostgresClient.query(
`INSERT INTO ${meta_table} (id, type, timestamp, ${columns.join(', ')}) VALUES (-1, 'live', NULL, ${placeholders.join(', ')})`,
values,
);
await State.PostgresClient.query(`
CREATE UNIQUE INDEX on ${meta_table} (timestamp, type) WHERE timestamp IS NOT NULL;
CREATE INDEX ${meta_table}_type_idx on ${meta_table} using hash(type);
`);
},
crons: [
{
name: `${this.meta_table}_snapshot_m`,
schedule: '* * * * *',
command: `insert into ${meta_table} select nextval('${meta_table}_id_seq'::regclass), 'snapshot_m', date_trunc('minute', NOW() + interval '5 second') ${['', ...columns].join(', ')} from ${meta_table} where type = 'live';`,
runOnStart: true
},
{
name: `${this.meta_table}_snapshot_h`,
schedule: '0 * * * *',
command: `insert into ${meta_table} select nextval('${meta_table}_id_seq'::regclass), 'snapshot_h', date_trunc('hour', NOW() + interval '5 second') ${['', ...columns].join(', ')} from ${meta_table} where type = 'live';`,
runOnStart: true
},
{
name: `${this.meta_table}_snapshot_d`,
schedule: '0 0 * * *',
command: `insert into ${meta_table} select nextval('${meta_table}_id_seq'::regclass), 'snapshot_d', date_trunc('day', NOW() + interval '5 second') ${['', ...columns].join(', ')} from ${meta_table} where type = 'live';`,
runOnStart: true
},
]
});
}
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,
},
);