0.1.6Updated a month ago
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,
  },
);