That's a spicy meatball

* APIUtils additions
* Socket and web server listen on dedicated ports (see denoland/deno socket issue created by ZombieB1309 on GitHub)
* Coach and Server created automatically (untested)
* Profile content functions split into 'managers'
* Progression temporary implementation
* Settings placed into profile content manager
* Relationships and messages return temporary empty array
* Socket targets defined, message delivery to target, exec returned (goes unused for now)
This commit is contained in:
2025-03-29 01:59:28 -04:00
parent 6b97e3800a
commit 6aae9129b5
28 changed files with 529 additions and 148 deletions

View File

@@ -1,17 +1,27 @@
import { APIUtils } from "../apiutils.ts";
import { Config } from "../config.ts";
import { AuthType } from "../data/users.ts";
import { SocketHandoff } from "./handoff.ts";
import express from "express";
const config = Config.getConfig();
export const route = APIUtils.createRouter('/notify');
route.router.post('/hub/v1/negotiate',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }),
APIUtils.logBody,
(_rq, rs) => {
const handoff = new SocketHandoff();
rs.json({
connectionId: handoff.id,
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}]
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}],
url: `${config.web.socket.securepublichost ? 'https' : 'http'}://${config.web.socket.publichost}/`,
accessToken: rs.locals.token
});
},

View File

@@ -1,43 +1,77 @@
import WebSocket from "ws";
import { Profile } from "../data/profiles.ts";
import Logging from "@proxnet/undead-logging";
import { Message, MessageKind, SignalMessageType, SignalRMessage, SignalRMessageSchema, TargetResult, TargetResultFailure, TargetResultSuccess, TargetResultType } from "./types.ts";
import { SocketTarget } from "./targets/targetbase.ts";
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
export class SignalRSocketHandler {
log: Logging = new Logging("SignalMock-");
#log: Logging = new Logging("SignalMock-");
#socket: WebSocket;
#profile: Profile;
#Targets: Map<string, SocketTarget> = new Map();
constructor(socket: WebSocket, player: Profile) {
this.#socket = socket;
this.#initLogSource();
this.#profile = player;
this.#init();
player.setSocketHandler(this);
Deno.addSignalListener('SIGINT', this.destroy);
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget());
}
destroy() {
this.#socket.close();
Deno.removeSignalListener('SIGINT', this.destroy);
async #dispatchTarget<T = unknown>(target: string, args: object[]): Promise<TargetResult> {
const targetExec = this.#Targets.get(target);
if (!targetExec) return { type: TargetResultType.Failure } as TargetResultFailure;
else return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
}
async #initLogSource() {
this.log.source += this.#profile.getId().toString();
#onMessage(message: Message) {
if (message.kind == MessageKind.Protocol) {
this.#send({});
return;
} else {
this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type - 1]})\n ${JSON.stringify(message.data)}`);
}
}
this.log.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`);
async #init() {
this.#log.source += this.#profile.getId().toString();
this.#socket.on('open', () => {
this.log.d(`hello world`)
});
this.#socket.on('message', data => {
this.log.d(data.toString());
this.#log.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`);
this.#socket.addEventListener('message', message => {
try {
const dec = new TextDecoder();
const str = dec.decode(message.data);
const data = JSON.parse(str.substring(0, str.length - 1));
const parseResult = SignalRMessageSchema.safeParse(data);
if (parseResult.success) this.#onMessage({
kind: MessageKind.Data,
data: parseResult.data as SignalRMessage
});
else {
this.#onMessage({
kind: MessageKind.Protocol
});
}
} catch (err) {
this.#log.e(`Socket error: ${err}`);
}
});
}
#send(data: object) {
this.#socket.send(`${JSON.stringify(data)}\u001e`);
}
}

View File

@@ -0,0 +1,16 @@
import { SocketTarget } from "./targetbase.ts";
export class PlayerSocketSubscriptionTarget extends SocketTarget {
subscriptions: number[] = [];
setSubscriptions(subs: number[]) {
this.subscriptions = subs;
}
// deno-lint-ignore require-await
override async exec(_args: (object | string | number | boolean)[]) {
return;
}
}

View File

@@ -0,0 +1,20 @@
export class SocketTarget {
profileNotSetError = new Error("The profile on this target is not set.");
profileId: number | null = null;
setProfile(id: number) {
this.profileId = id;
}
profileIsSet() {
return this.profileId !== null;
}
// deno-lint-ignore require-await
async exec(_args: (object | string | number | boolean)[]) {
throw new Error("Execution for this target is not set.");
}
}

View File

@@ -1,22 +1,125 @@
export enum MessageTypes {
CancelInvocation,
Close,
Completion,
import { z } from "zod";
export enum MessageKind {
Protocol,
Data
}
interface MessageBase {
kind: MessageKind
}
interface DataMessage extends MessageBase {
kind: MessageKind.Data,
data: SignalRMessage
}
interface ProtocolMessage extends MessageBase {
kind: MessageKind.Protocol
}
export type Message = ProtocolMessage | DataMessage;
export type SignalRMessage =
| InvocationMessage
| StreamItemMessage
| CompletionMessage
| PingMessage
| CloseMessage;
export enum SignalMessageType {
Handshake,
Invocation,
Ping,
StreamInvocation,
StreamItem,
Ack
Completion,
StreamInvocation,
CancelInvocation,
Ping,
Close
}
export interface SignalRMessage {
arguments: object[],
error?: string,
invocationId?: string,
item?: object,
nonblocking: boolean,
result?: object,
target: string,
type: MessageTypes
}
interface BaseMessage {
type: SignalMessageType;
}
interface InvocationMessage extends BaseMessage {
type: SignalMessageType.Invocation;
target: string;
arguments: unknown[];
invocationId?: string;
}
interface StreamItemMessage extends BaseMessage {
type: SignalMessageType.StreamItem;
invocationId: string;
item: unknown;
}
interface CompletionMessage extends BaseMessage {
type: SignalMessageType.Completion;
invocationId: string;
result?: unknown;
error?: string;
}
interface PingMessage extends BaseMessage {
type: SignalMessageType.Ping;
}
interface CloseMessage extends BaseMessage {
type: SignalMessageType.Close;
error?: string;
}
const BaseMessageSchema = z.object({
type: z.nativeEnum(SignalMessageType),
});
const InvocationMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Invocation),
target: z.string(),
arguments: z.array(z.unknown()),
invocationId: z.string().optional(),
});
const StreamItemMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.StreamItem),
invocationId: z.string(),
item: z.unknown(),
});
const CompletionMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Completion),
invocationId: z.string(),
result: z.unknown().optional(),
error: z.string().optional(),
});
const PingMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Ping),
});
const CloseMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Close),
error: z.string().optional(),
});
export const SignalRMessageSchema = z.discriminatedUnion("type", [
InvocationMessageSchema,
StreamItemMessageSchema,
CompletionMessageSchema,
PingMessageSchema,
CloseMessageSchema,
]);
export enum TargetResultType {
Success,
Failure
}
interface TargetResultBase {
type: TargetResultType
}
export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
type: TargetResultType.Success,
data: T
}
export interface TargetResultFailure extends TargetResultBase {
type: TargetResultType.Failure
}
export type TargetResult = TargetResultSuccess | TargetResultFailure;