0.1.3Updated 6 months ago
import State from "@infinity-beyond/modules/state.ts";
import { randomUUID } from "node:crypto";
import { EntityWorker } from "@infinity-beyond/classes/entity_worker.ts";
import { Sluggify } from "@infinity-beyond/utils/slug.ts";
import { CustomEventEmitter } from "@infinity-beyond/classes/custom_event_emitter.ts";
import { EntityMeta, type EntityMetaConfig } from "@infinity-beyond/classes/entity_meta.ts";

interface I_EntityTask extends I_ProcessorRequest {
  is_static: boolean
  instance_data?: any
  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 EntityMetaConfig = EntityMetaConfig, PEMC extends EntityMetaConfig = EntityMetaConfig> extends CustomEventEmitter<EventMap> {
  private meta_fields: EMC
  private passed_meta_fields: PEMC
  meta: EntityMeta<EMC, PEMC>

  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, meta_fields: EMC, passed_meta_fields: PEMC) {
    super();

    this.name = name;

    this.meta_fields = meta_fields;
    this.passed_meta_fields = passed_meta_fields;

    this.meta = new EntityMeta(this.meta_table, meta_fields, passed_meta_fields);

    this.finalize_permissions();
  }

  get slug() {
    return Sluggify(this.name);
  }

  private static __workers: EntityWorker[] = []
  static get __worker_count() {
    return this.__workers.length;
  }

  protected static get __worker_methods() { return this.___worker_methods[this.name] ||= []; }
  private static ___worker_methods: Record<string, string[]> = {}

  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));
  
      const instance_data = Object.assign(
        {},
        ...Object.entries(instance)
          .map(([key, value]) => {
            if(Object.hasOwn(instance, key)) return { [key]: value };
            return false;
          })
          .filter(e => e !== false)
      );

      this.__tasks_in_progress.set(id, true);

      this.__task_queue.push({
        id,
        entity_name: instance.name,
        is_static: false,
        instance_data,
        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() {
    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: 'int4',
      }, {
        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;

    let cls: typeof Entity;
    let instance: Entity<any>;

    if(isStatic) {
      cls = this as typeof Entity;
    } else {
      instance = (this as Entity<any>);
      cls = instance.constructor as typeof Entity;
    }

    (cls as any).__worker_methods.push(originalMethod.name);

    if(isStatic) {
      Object.assign(cls, {
        [methodName]: (...args: any[]) => {
          return cls.__delegate(originalMethod.name, args);
        }
      })
    } else {
      Object.assign(cls.prototype, {
        [methodName]: function(this: Entity<any, any, any>, ...args: any[]) {
          return cls.__delegate_instance(instance, 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
})