0.1.1Updated 7 months ago
import { Application, Context, Next, Router } from "jsr:@oak/oak@^17.1.4";
import State from "@infinity-beyond/modules/state.ts";
import { randomUUID } from "node:crypto";
import { Entity } from "@infinity-beyond/modules/entity.ts";
import { ContextState } from "@infinity-beyond/modules/host/context_state.ts";

interface I_Host {
  port?: number
}

class Host {
  readonly port: number

  #server: Deno.HttpServer<Deno.NetAddr> | undefined
  #app: Application
  #router: Router

  get server() { return this.#server };

  constructor({ port }: I_Host = {}) {
    this.port = port || Number(Deno.env.get("PORT")) || 9090;
    this.#app = new Application({});

    this.#router = new Router();

    this.#router.use(this.verify_request.bind(this));

    this.#app.use(this.#router.routes());

    State.HOSTS.push(this);
  }

  // #region Analytics

  async RequestAnalytics(_ctx: Context<ContextState>, next: Next) {
    try {

      /*
      *  Do worker-side analytics and task monitoring here
      */

    } catch(e: any) {
      console.warn(`Host::RequestAnalytics() error! Continueing with request...`, e.message);
    };
    return next();
  }

  async ResponseAnalytics(ctx: Context<ContextState>, next: Next) {
    const { state: { date } } = ctx;

    const _request_total_time_ms = Date.now() - date.valueOf();

    try {

      /*
      *  Do worker-side analytics and task monitoring here
      */

    } catch(e: any) {
      console.warn(`Host::RequestAnalytics() error! Continueing with request...`, e.message);
    };

    return next();
  }

  listen(callback?: (port: number) => void) {
    this.#server = Deno.serve({
      port: this.port
    }, async (request, info) => {
      if(request.headers.get("upgrade") != "websocket") {
        const app_response = await this.#app.handle(request, info.remoteAddr);
        return app_response ?? new Response('', { status: 404 });
      }

      return this.handshake(request);
    });

    callback?.(this.port);
  }

  async verify_request(ctx: Context<ContextState>, next: Next) {
    ctx.state.date = new Date();
    ctx.state.id = randomUUID();
    ctx.state.headers = new Headers({
      'x-request-id': ctx.state.id
    });

    try {
      ctx.state.body = await ctx.request.body.json();
    } catch(_) { /**/ }
    ctx.state.ip = ctx.request.headers.get('X-Forwarded-For') || ctx.request.ip;

    return next();
  }

  handshake(request: Request) {
    const { socket, response } = Deno.upgradeWebSocket(request);

    const SIGNATURE = request.headers.get('sec-websocket-protocol')?.replace(/^SIGNATURE, (.+$)$/, '$1');
    if(!SIGNATURE || SIGNATURE !== State.SIGNATURE) {
      socket.close(1, 'Invalid signature');
      return new Response('', { status: 403 });
    }

    const query: Record<string, string> = new URL(request.url).search
      ?.replace(/^\?/, '')
      .split('&')
      .reduce((p, query) => {
        const [key, value] = query.split('=');
        p[key] = value;
        return p;
      }, {} as Record<string, string>) ?? {};
    
    const { uuid } = query;
    if(!uuid) {
      socket.close(1, 'No provided uuid');
      return new Response('', { status: 403 });
    }

    socket.onopen = () => {
      Entity.__AddWorker(uuid, socket);
    }

    return response;
  }

  get app() {
    return this.#app;
  }
  get router() {
    return this.#router;
  }
}

export default Host;