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:
@@ -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
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
}
|
||||
16
src/socket/targets/SubscribeToPlayers.ts
Normal file
16
src/socket/targets/SubscribeToPlayers.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
20
src/socket/targets/targetbase.ts
Normal file
20
src/socket/targets/targetbase.ts
Normal 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.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user