Skip to main content

WebSocket generation in Emi

Websockets are one of the most important aspect of reactive web programming now adays, which allow sending full-duplex messages between server and front-end. You can define an action method: 'reactive', and it would consider the endpoint is a websocket. Emi extends the standard WebSocket class in JavaScript, and in case of other generators (react for example), they would wrap the instance.

In this document, we will generate, document and test different type of the websocket messages for javascript.

Important to note that the javascript generated code, can work both in node.js and browser environment, but main focus of this generator is client side. Still, you can use the request and response classes create.

WebSocketX class is the base class which all code for js/ts will be generated on top of it. It's very similar to standard WebSocket class, with exception that message and send are overriden, to accept types specificed as generic. Also the constructor, will get factory for message creation, in case of working with json messages.

Sample 1: Json requests and json responses

Standard websocket allows for bytes, strings, arraybuffers to be sent on both ways. On top of that, Emi adds the option to send typesafe class structure over, in following example, we are working on a socket endpoint, which would expect a message with min,max,count variables, and would generate random number messages (as a json class, not primitive), and ui will print it.

name: socketTestingModule
actions:
- name: userStream
url: ws://localhost:8081
method: reactive
description: >-
A socket connection which would generate random numbers, based on min,
max, and count.
in:
fields:
- name: min
type: int
description: Minimum number which can be generated
- name: max
type: int
description: Maximum number which can be generated
- name: count
type: int
description: >-
How many numbers you want to be generated based on maximum and
minimum
out:
fields:
- name: number
type: int

Generated content

Now the result is going to be a complete WebRequestX instance, with proper typings:

import { WebSocketX } from "./sdk/common/WebSocketX";
import { buildUrl } from "./sdk/common/buildUrl";
/**
* Action to communicate with the action userStream
*/
export type UserStreamActionOptions = {
queryKey?: unknown[];
qs?: URLSearchParams;
};
/**
* UserStreamAction
*/
export class UserStreamAction {
static URL = "ws://localhost:8081";
static NewUrl = (qs?: URLSearchParams) =>
buildUrl(UserStreamAction.URL, undefined, qs);
static Method = "reactive";
static Create = (overrideUrl?: string, qs?: URLSearchParams) => {
const url = overrideUrl ?? UserStreamAction.NewUrl(qs);
return new WebSocketX<UserStreamActionReq, UserStreamActionRes>(
url,
undefined,
{
MessageFactoryClass: UserStreamActionRes,
},
);
};
static Definition = {
name: "userStream",
url: "ws://localhost:8081",
method: "reactive",
description:
"A socket connection which would generate random numbers, based on min, max, and count.",
in: {
fields: [
{
name: "min",
description: "Minimum number which can be generated",
type: "int",
},
{
name: "max",
description: "Maximum number which can be generated",
type: "int",
},
{
name: "count",
description:
"How many numbers you want to be generated based on maximum and minimum",
type: "int",
},
],
},
out: {
fields: [
{
name: "number",
type: "int",
},
],
},
};
}
/**
* The base class definition for userStreamActionReq
**/
export class UserStreamActionReq {
/**
* Minimum number which can be generated
* @type {number}
**/
#min: number = 0;
/**
* Minimum number which can be generated
* @returns {number}
**/
get min() {
return this.#min;
}
/**
* Minimum number which can be generated
* @type {number}
**/
set min(value: number) {
const correctType = typeof value === "number";
const parsedValue = correctType ? value : Number(value);
if (!Number.isNaN(parsedValue)) {
this.#min = parsedValue;
}
}
setMin(value: number) {
this.min = value;
return this;
}
/**
* Maximum number which can be generated
* @type {number}
**/
#max: number = 0;
/**
* Maximum number which can be generated
* @returns {number}
**/
get max() {
return this.#max;
}
/**
* Maximum number which can be generated
* @type {number}
**/
set max(value: number) {
const correctType = typeof value === "number";
const parsedValue = correctType ? value : Number(value);
if (!Number.isNaN(parsedValue)) {
this.#max = parsedValue;
}
}
setMax(value: number) {
this.max = value;
return this;
}
/**
* How many numbers you want to be generated based on maximum and minimum
* @type {number}
**/
#count: number = 0;
/**
* How many numbers you want to be generated based on maximum and minimum
* @returns {number}
**/
get count() {
return this.#count;
}
/**
* How many numbers you want to be generated based on maximum and minimum
* @type {number}
**/
set count(value: number) {
const correctType = typeof value === "number";
const parsedValue = correctType ? value : Number(value);
if (!Number.isNaN(parsedValue)) {
this.#count = parsedValue;
}
}
setCount(value: number) {
this.count = value;
return this;
}
constructor(data: unknown = undefined) {
if (data === null || data === undefined) {
return;
}
if (typeof data === "string") {
this.applyFromObject(JSON.parse(data));
} else if (this.#isJsonAppliable(data)) {
this.applyFromObject(data);
} else {
throw new Error(
"Instance cannot be created on an unknown value, check the content being passed. got: " +
typeof data,
);
}
}
#isJsonAppliable(obj: unknown) {
const g = globalThis as unknown as { Buffer: any; Blob: any };
const isBuffer =
typeof g.Buffer !== "undefined" &&
typeof g.Buffer.isBuffer === "function" &&
g.Buffer.isBuffer(obj);
const isBlob = typeof g.Blob !== "undefined" && obj instanceof g.Blob;
return (
obj &&
typeof obj === "object" &&
!Array.isArray(obj) &&
!isBuffer &&
!(obj instanceof ArrayBuffer) &&
!isBlob
);
}
/**
* casts the fields of a javascript object into the class properties one by one
**/
applyFromObject(data = {}) {
const d = data as Partial<UserStreamActionReq>;
if (d.min !== undefined) {
this.min = d.min;
}
if (d.max !== undefined) {
this.max = d.max;
}
if (d.count !== undefined) {
this.count = d.count;
}
}
/**
* Special toJSON override, since the field are private,
* Json stringify won't see them unless we mention it explicitly.
**/
toJSON() {
return {
min: this.#min,
max: this.#max,
count: this.#count,
};
}
toString() {
return JSON.stringify(this);
}
static get Fields() {
return {
min: "min",
max: "max",
count: "count",
};
}
/**
* Creates an instance of UserStreamActionReq, and possibleDtoObject
* needs to satisfy the type requirement fully, otherwise typescript compile would
* be complaining.
**/
static from(possibleDtoObject: UserStreamActionReqType) {
return new UserStreamActionReq(possibleDtoObject);
}
/**
* Creates an instance of UserStreamActionReq, and partialDtoObject
* needs to satisfy the type, but partially, and rest of the content would
* be constructed according to data types and nullability.
**/
static with(partialDtoObject: PartialDeep<UserStreamActionReqType>) {
return new UserStreamActionReq(partialDtoObject);
}
copyWith(
partial: PartialDeep<UserStreamActionReqType>,
): InstanceType<typeof UserStreamActionReq> {
return new UserStreamActionReq({ ...this.toJSON(), ...partial });
}
clone(): InstanceType<typeof UserStreamActionReq> {
return new UserStreamActionReq(this.toJSON());
}
}
export abstract class UserStreamActionReqFactory {
abstract create(data: unknown): UserStreamActionReq;
}
type PartialDeep<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<PartialDeep<U>>
: T[P] extends object
? PartialDeep<T[P]>
: T[P];
};
/**
* The base type definition for userStreamActionReq
**/
export type UserStreamActionReqType = {
/**
* Minimum number which can be generated
* @type {number}
**/
min: number;
/**
* Maximum number which can be generated
* @type {number}
**/
max: number;
/**
* How many numbers you want to be generated based on maximum and minimum
* @type {number}
**/
count: number;
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace UserStreamActionReqType {}
/**
* The base class definition for userStreamActionRes
**/
export class UserStreamActionRes {
/**
*
* @type {number}
**/
#number: number = 0;
/**
*
* @returns {number}
**/
get number() {
return this.#number;
}
/**
*
* @type {number}
**/
set number(value: number) {
const correctType = typeof value === "number";
const parsedValue = correctType ? value : Number(value);
if (!Number.isNaN(parsedValue)) {
this.#number = parsedValue;
}
}
setNumber(value: number) {
this.number = value;
return this;
}
constructor(data: unknown = undefined) {
if (data === null || data === undefined) {
return;
}
if (typeof data === "string") {
this.applyFromObject(JSON.parse(data));
} else if (this.#isJsonAppliable(data)) {
this.applyFromObject(data);
} else {
throw new Error(
"Instance cannot be created on an unknown value, check the content being passed. got: " +
typeof data,
);
}
}
#isJsonAppliable(obj: unknown) {
const g = globalThis as unknown as { Buffer: any; Blob: any };
const isBuffer =
typeof g.Buffer !== "undefined" &&
typeof g.Buffer.isBuffer === "function" &&
g.Buffer.isBuffer(obj);
const isBlob = typeof g.Blob !== "undefined" && obj instanceof g.Blob;
return (
obj &&
typeof obj === "object" &&
!Array.isArray(obj) &&
!isBuffer &&
!(obj instanceof ArrayBuffer) &&
!isBlob
);
}
/**
* casts the fields of a javascript object into the class properties one by one
**/
applyFromObject(data = {}) {
const d = data as Partial<UserStreamActionRes>;
if (d.number !== undefined) {
this.number = d.number;
}
}
/**
* Special toJSON override, since the field are private,
* Json stringify won't see them unless we mention it explicitly.
**/
toJSON() {
return {
number: this.#number,
};
}
toString() {
return JSON.stringify(this);
}
static get Fields() {
return {
number: "number",
};
}
/**
* Creates an instance of UserStreamActionRes, and possibleDtoObject
* needs to satisfy the type requirement fully, otherwise typescript compile would
* be complaining.
**/
static from(possibleDtoObject: UserStreamActionResType) {
return new UserStreamActionRes(possibleDtoObject);
}
/**
* Creates an instance of UserStreamActionRes, and partialDtoObject
* needs to satisfy the type, but partially, and rest of the content would
* be constructed according to data types and nullability.
**/
static with(partialDtoObject: PartialDeep<UserStreamActionResType>) {
return new UserStreamActionRes(partialDtoObject);
}
copyWith(
partial: PartialDeep<UserStreamActionResType>,
): InstanceType<typeof UserStreamActionRes> {
return new UserStreamActionRes({ ...this.toJSON(), ...partial });
}
clone(): InstanceType<typeof UserStreamActionRes> {
return new UserStreamActionRes(this.toJSON());
}
}
export abstract class UserStreamActionResFactory {
abstract create(data: unknown): UserStreamActionRes;
}
type PartialDeep<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<PartialDeep<U>>
: T[P] extends object
? PartialDeep<T[P]>
: T[P];
};
/**
* The base type definition for userStreamActionRes
**/
export type UserStreamActionResType = {
/**
*
* @type {number}
**/
number: number;
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace UserStreamActionResType {}