topsyde-utils 1.0.149 → 1.0.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.js.map +1 -1
- package/dist/client/rxjs/index.js.map +1 -1
- package/dist/client/rxjs/rxjs.js.map +1 -1
- package/dist/client/rxjs/useRxjs.js.map +1 -1
- package/dist/client/vite/plugins/index.js.map +1 -1
- package/dist/client/vite/plugins/topsydeUtilsVitePlugin.js.map +1 -1
- package/dist/consts.js.map +1 -1
- package/dist/enums.js.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/initializable.js.map +1 -1
- package/dist/server/bun/index.js.map +1 -1
- package/dist/server/bun/router/controller-discovery.js.map +1 -1
- package/dist/server/bun/router/index.js.map +1 -1
- package/dist/server/bun/router/router.internal.js.map +1 -1
- package/dist/server/bun/router/router.js.map +1 -1
- package/dist/server/bun/router/routes.js.map +1 -1
- package/dist/server/bun/websocket/Channel.js.map +1 -1
- package/dist/server/bun/websocket/Client.js.map +1 -1
- package/dist/server/bun/websocket/Message.js.map +1 -1
- package/dist/server/bun/websocket/Websocket.js.map +1 -1
- package/dist/server/bun/websocket/index.js.map +1 -1
- package/dist/server/bun/websocket/websocket.enums.js.map +1 -1
- package/dist/server/bun/websocket/websocket.guards.js.map +1 -1
- package/dist/server/bun/websocket/websocket.types.js.map +1 -1
- package/dist/server/controller.js.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/service.js.map +1 -1
- package/dist/singleton.js.map +1 -1
- package/dist/throwable.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/Console.js.map +1 -1
- package/dist/utils/Guards.js.map +1 -1
- package/dist/utils/Lib.js.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/app.test.ts +205 -0
- package/src/__tests__/singleton.test.ts +402 -0
- package/src/__tests__/type-inference.test.ts +60 -0
- package/src/application.ts +43 -0
- package/src/client/rxjs/index.ts +5 -0
- package/src/client/rxjs/rxjs.ts +122 -0
- package/src/client/rxjs/useRxjs.ts +111 -0
- package/src/client/vite/plugins/index.ts +5 -0
- package/src/client/vite/plugins/topsydeUtilsVitePlugin.ts +80 -0
- package/src/consts.ts +48 -0
- package/src/enums.ts +14 -0
- package/src/errors.ts +56 -0
- package/src/index.ts +81 -0
- package/src/initializable.ts +375 -0
- package/src/server/bun/index.ts +6 -0
- package/src/server/bun/router/controller-discovery.ts +94 -0
- package/src/server/bun/router/index.ts +9 -0
- package/src/server/bun/router/router.internal.ts +64 -0
- package/src/server/bun/router/router.ts +51 -0
- package/src/server/bun/router/routes.ts +7 -0
- package/src/server/bun/websocket/Channel.ts +157 -0
- package/src/server/bun/websocket/Client.ts +129 -0
- package/src/server/bun/websocket/Message.ts +106 -0
- package/src/server/bun/websocket/Websocket.ts +221 -0
- package/src/server/bun/websocket/index.ts +14 -0
- package/src/server/bun/websocket/websocket.enums.ts +22 -0
- package/src/server/bun/websocket/websocket.guards.ts +6 -0
- package/src/server/bun/websocket/websocket.types.ts +186 -0
- package/src/server/controller.ts +121 -0
- package/src/server/index.ts +7 -0
- package/src/server/service.ts +36 -0
- package/src/singleton.ts +28 -0
- package/src/throwable.ts +87 -0
- package/src/types.ts +10 -0
- package/src/utils/Console.ts +85 -0
- package/src/utils/Guards.ts +61 -0
- package/src/utils/Lib.ts +506 -0
- package/src/utils/index.ts +9 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
import { Guards, Lib } from "../../../utils";
|
2
|
+
import Message from "./Message";
|
3
|
+
import Websocket from "./Websocket";
|
4
|
+
import type {
|
5
|
+
BroadcastOptions,
|
6
|
+
I_WebsocketChannel,
|
7
|
+
I_WebsocketClient,
|
8
|
+
I_WebsocketEntity,
|
9
|
+
WebsocketChannel,
|
10
|
+
WebsocketMessage,
|
11
|
+
WebsocketStructuredMessage,
|
12
|
+
} from "./websocket.types";
|
13
|
+
|
14
|
+
export default class Channel<T extends Websocket = Websocket> implements I_WebsocketChannel<T> {
|
15
|
+
public createdAt: Date = new Date();
|
16
|
+
public id: string;
|
17
|
+
public name: string;
|
18
|
+
public limit: number;
|
19
|
+
public members: Map<string, I_WebsocketClient>;
|
20
|
+
public metadata: Record<string, string>;
|
21
|
+
public ws: T;
|
22
|
+
// Message template for reuse
|
23
|
+
private messageTemplate: WebsocketStructuredMessage<any>;
|
24
|
+
private message: Message;
|
25
|
+
|
26
|
+
constructor(id: string, name: string, ws: T, limit?: number, members?: Map<string, I_WebsocketClient>, metadata?: Record<string, string>) {
|
27
|
+
this.id = id;
|
28
|
+
this.name = name;
|
29
|
+
this.limit = limit ?? 5;
|
30
|
+
this.members = members ?? new Map();
|
31
|
+
this.metadata = metadata ?? {};
|
32
|
+
this.ws = ws;
|
33
|
+
this.message = new Message();
|
34
|
+
this.messageTemplate = {
|
35
|
+
type: "",
|
36
|
+
content: {},
|
37
|
+
channel: this.id,
|
38
|
+
timestamp: "",
|
39
|
+
};
|
40
|
+
}
|
41
|
+
|
42
|
+
public broadcast(message: WebsocketMessage, options?: BroadcastOptions) {
|
43
|
+
const output = this.message.create(message, { ...options, channel: this.id });
|
44
|
+
if (options) {
|
45
|
+
// Include channel metadata if requested
|
46
|
+
if (options.includeMetadata) {
|
47
|
+
output.metadata = options.includeMetadata === true ? this.getMetadata() : this.getFilteredMetadata(options.includeMetadata);
|
48
|
+
}
|
49
|
+
|
50
|
+
// Handle excluded clients if needed
|
51
|
+
if (options.excludeClients && options.excludeClients.length > 0) {
|
52
|
+
// For large channels with many excluded clients, it might be more efficient
|
53
|
+
// to send directly to each client instead of using channel publish
|
54
|
+
if (this.members.size > 10 && options.excludeClients.length > this.members.size / 3) {
|
55
|
+
const serializedMessage = this.message.serialize(output);
|
56
|
+
for (const [clientId, client] of this.members) {
|
57
|
+
if (!options.excludeClients.includes(clientId)) {
|
58
|
+
client.ws.send(serializedMessage);
|
59
|
+
}
|
60
|
+
}
|
61
|
+
return;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
// Publish to the channel
|
66
|
+
this.ws.server.publish(this.id, this.message.serialize(output));
|
67
|
+
}
|
68
|
+
|
69
|
+
// Helper method for filtered metadata
|
70
|
+
private getFilteredMetadata(keys: string[]) {
|
71
|
+
const metadata = this.getMetadata();
|
72
|
+
const filtered: Record<string, string> = {};
|
73
|
+
|
74
|
+
for (const key of keys) {
|
75
|
+
if (metadata[key] !== undefined) {
|
76
|
+
filtered[key] = metadata[key];
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
return filtered;
|
81
|
+
}
|
82
|
+
|
83
|
+
public hasMember(client: I_WebsocketEntity | string) {
|
84
|
+
if (typeof client === "string") return this.members.has(client);
|
85
|
+
return this.members.has(client.id);
|
86
|
+
}
|
87
|
+
|
88
|
+
public addMember(client: I_WebsocketClient) {
|
89
|
+
if (!this.canAddMember()) return false;
|
90
|
+
this.members.set(client.id, client);
|
91
|
+
client.joinChannel(this);
|
92
|
+
return client;
|
93
|
+
}
|
94
|
+
|
95
|
+
public removeMember(entity: I_WebsocketEntity) {
|
96
|
+
if (!this.members.has(entity.id)) return false;
|
97
|
+
const client = this.members.get(entity.id);
|
98
|
+
if (!client) return false;
|
99
|
+
client.leaveChannel(this);
|
100
|
+
this.members.delete(entity.id);
|
101
|
+
return client;
|
102
|
+
}
|
103
|
+
|
104
|
+
public getMember(client: I_WebsocketEntity | string) {
|
105
|
+
if (typeof client === "string") return this.members.get(client);
|
106
|
+
return this.members.get(client.id);
|
107
|
+
}
|
108
|
+
|
109
|
+
public getMembers(clients?: string[] | I_WebsocketEntity[]): I_WebsocketClient[] {
|
110
|
+
if (!clients) return Array.from(this.members.values());
|
111
|
+
return clients.map((client) => this.getMember(client)).filter((client) => client !== undefined) as I_WebsocketClient[];
|
112
|
+
}
|
113
|
+
|
114
|
+
public getMetadata() {
|
115
|
+
return this.metadata;
|
116
|
+
}
|
117
|
+
|
118
|
+
public getCreatedAt() {
|
119
|
+
return this.createdAt;
|
120
|
+
}
|
121
|
+
|
122
|
+
public getId() {
|
123
|
+
return this.id;
|
124
|
+
}
|
125
|
+
|
126
|
+
public getName() {
|
127
|
+
return this.name;
|
128
|
+
}
|
129
|
+
|
130
|
+
public getLimit() {
|
131
|
+
return this.limit;
|
132
|
+
}
|
133
|
+
|
134
|
+
public getSize() {
|
135
|
+
return this.members.size;
|
136
|
+
}
|
137
|
+
|
138
|
+
public canAddMember() {
|
139
|
+
const size = this.getSize();
|
140
|
+
return size < this.limit;
|
141
|
+
}
|
142
|
+
|
143
|
+
public static GetChannelType(channels: WebsocketChannel<I_WebsocketChannel> | undefined) {
|
144
|
+
if (!channels) return Channel;
|
145
|
+
if (channels.size > 0) {
|
146
|
+
const firstChannel = channels.values().next().value;
|
147
|
+
if (firstChannel) {
|
148
|
+
return firstChannel.constructor as typeof Channel;
|
149
|
+
} else {
|
150
|
+
return Channel;
|
151
|
+
}
|
152
|
+
} else {
|
153
|
+
Lib.Warn("Channels are empty, using default channel class");
|
154
|
+
return Channel;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
@@ -0,0 +1,129 @@
|
|
1
|
+
import { ServerWebSocket } from "bun";
|
2
|
+
import type {
|
3
|
+
I_WebsocketClient,
|
4
|
+
WebsocketEntityData,
|
5
|
+
WebsocketChannel,
|
6
|
+
WebsocketStructuredMessage,
|
7
|
+
I_WebsocketEntity,
|
8
|
+
I_WebsocketChannel,
|
9
|
+
} from "./websocket.types";
|
10
|
+
import { E_WebsocketMessageType } from "./websocket.enums";
|
11
|
+
import { Lib } from "../../../utils";
|
12
|
+
|
13
|
+
export default class Client implements I_WebsocketClient {
|
14
|
+
private _id: string;
|
15
|
+
private _name: string;
|
16
|
+
private _ws: ServerWebSocket<WebsocketEntityData>;
|
17
|
+
private _channels: WebsocketChannel<I_WebsocketChannel>;
|
18
|
+
|
19
|
+
private set ws(value: ServerWebSocket<WebsocketEntityData>) {
|
20
|
+
this._ws = value;
|
21
|
+
}
|
22
|
+
|
23
|
+
public get ws(): ServerWebSocket<WebsocketEntityData> {
|
24
|
+
return this._ws;
|
25
|
+
}
|
26
|
+
|
27
|
+
private set id(value: string) {
|
28
|
+
this._id = value;
|
29
|
+
}
|
30
|
+
|
31
|
+
public get id(): string {
|
32
|
+
return this._id;
|
33
|
+
}
|
34
|
+
|
35
|
+
public get name(): string {
|
36
|
+
return this._name;
|
37
|
+
}
|
38
|
+
|
39
|
+
private set name(value: string) {
|
40
|
+
this._name = value;
|
41
|
+
}
|
42
|
+
|
43
|
+
private set channels(value: WebsocketChannel<I_WebsocketChannel>) {
|
44
|
+
this._channels = value;
|
45
|
+
}
|
46
|
+
|
47
|
+
public get channels(): WebsocketChannel<I_WebsocketChannel> {
|
48
|
+
return this._channels;
|
49
|
+
}
|
50
|
+
|
51
|
+
constructor(entity: I_WebsocketEntity) {
|
52
|
+
this._id = entity.id;
|
53
|
+
this._name = entity.name;
|
54
|
+
this._ws = entity.ws;
|
55
|
+
this.ws = entity.ws;
|
56
|
+
this._channels = new Map();
|
57
|
+
}
|
58
|
+
|
59
|
+
public joinChannel(channel: I_WebsocketChannel, send: boolean = true) {
|
60
|
+
const channel_id = channel.getId();
|
61
|
+
this.subscribe(channel_id);
|
62
|
+
this.channels.set(channel_id, channel);
|
63
|
+
if (send)
|
64
|
+
this.send({
|
65
|
+
type: E_WebsocketMessageType.CLIENT_JOIN_CHANNEL,
|
66
|
+
content: { message: "Welcome to the channel" },
|
67
|
+
channel: channel_id,
|
68
|
+
client: this.whoami(),
|
69
|
+
});
|
70
|
+
}
|
71
|
+
|
72
|
+
public leaveChannel(channel: I_WebsocketChannel, send: boolean = true) {
|
73
|
+
const channel_id = channel.getId();
|
74
|
+
this.channels.delete(channel_id);
|
75
|
+
this.unsubscribe(channel_id);
|
76
|
+
if (send)
|
77
|
+
this.send({
|
78
|
+
type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNEL,
|
79
|
+
content: { message: "Left the channel" },
|
80
|
+
channel: channel_id,
|
81
|
+
client: this.whoami(),
|
82
|
+
});
|
83
|
+
}
|
84
|
+
|
85
|
+
public joinChannels(channels: I_WebsocketChannel[], send: boolean = true) {
|
86
|
+
channels.forEach((channel) => {
|
87
|
+
this.joinChannel(channel, false);
|
88
|
+
});
|
89
|
+
if (send) this.send({ type: E_WebsocketMessageType.CLIENT_JOIN_CHANNELS, content: { channels }, client: this.whoami() });
|
90
|
+
}
|
91
|
+
|
92
|
+
public leaveChannels(channels?: I_WebsocketChannel[], send: boolean = true) {
|
93
|
+
if (!channels) channels = Array.from(this.channels.values());
|
94
|
+
channels.forEach((channel) => {
|
95
|
+
this.leaveChannel(channel, false);
|
96
|
+
});
|
97
|
+
if (send) this.send({ type: E_WebsocketMessageType.CLIENT_LEAVE_CHANNELS, content: { channels }, client: this.whoami() });
|
98
|
+
}
|
99
|
+
|
100
|
+
public whoami(): { id: string; name: string } {
|
101
|
+
return { id: this.id, name: this.name };
|
102
|
+
}
|
103
|
+
|
104
|
+
public send(message: WebsocketStructuredMessage) {
|
105
|
+
this.ws.send(JSON.stringify({ client: this.whoami(), ...message }));
|
106
|
+
}
|
107
|
+
|
108
|
+
public subscribe(channel: string): void {
|
109
|
+
this.ws.subscribe(channel);
|
110
|
+
}
|
111
|
+
|
112
|
+
public unsubscribe(channel: string): void {
|
113
|
+
this.ws.unsubscribe(channel);
|
114
|
+
}
|
115
|
+
|
116
|
+
public static GetClientType(clients: Map<string, I_WebsocketClient> | undefined): typeof Client {
|
117
|
+
if (!clients) return Client;
|
118
|
+
if (clients.size > 0) {
|
119
|
+
const firstClient = clients.values().next().value;
|
120
|
+
if (firstClient) {
|
121
|
+
return firstClient.constructor as typeof Client;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
// Fallback to default Client class
|
126
|
+
Lib.Warn("Clients map is empty, using default client class");
|
127
|
+
return Client;
|
128
|
+
}
|
129
|
+
}
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import Guards from "../../../utils/Guards";
|
2
|
+
import { WebsocketStructuredMessage, WebsocketMessage, WebsocketMessageOptions, I_WebsocketChannel, I_WebsocketClient } from "./websocket.types";
|
3
|
+
|
4
|
+
export default class Message {
|
5
|
+
private messageTemplate: WebsocketStructuredMessage<any>;
|
6
|
+
|
7
|
+
constructor() {
|
8
|
+
this.messageTemplate = { type: "", content: {}, channel: "", timestamp: "", client: undefined };
|
9
|
+
}
|
10
|
+
|
11
|
+
public create(message: WebsocketMessage, options?: WebsocketMessageOptions): WebsocketStructuredMessage {
|
12
|
+
// Clone the template (faster than creating new objects)
|
13
|
+
const output = Object.assign({}, this.messageTemplate);
|
14
|
+
// Set the dynamic properties in a single pass
|
15
|
+
output.type = message.type;
|
16
|
+
output.channel = message.channel || options?.channel || "N/A";
|
17
|
+
|
18
|
+
// Process message content based on type
|
19
|
+
if (typeof message.content === "string") {
|
20
|
+
output.content = { message: message.content };
|
21
|
+
} else if (typeof message.content === "object" && message.content !== null) {
|
22
|
+
output.content = { ...message.content };
|
23
|
+
} else {
|
24
|
+
output.content = {};
|
25
|
+
}
|
26
|
+
|
27
|
+
// Process options in a single pass if provided
|
28
|
+
if (options) {
|
29
|
+
// Add data if provided
|
30
|
+
if (options.data !== undefined) {
|
31
|
+
if (typeof options.data === "object" && options.data !== null && !Array.isArray(options.data)) {
|
32
|
+
// Merge object data with content
|
33
|
+
Object.assign(output.content, options.data);
|
34
|
+
} else {
|
35
|
+
// Set as data property for other types
|
36
|
+
output.content.data = options.data;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
// Add client information if provided
|
40
|
+
if (options.client && Guards.IsObject(options.client) && Guards.IsString(options.client.id, true)) {
|
41
|
+
output.client = {
|
42
|
+
id: options.client.id,
|
43
|
+
name: options.client.name,
|
44
|
+
};
|
45
|
+
} else {
|
46
|
+
delete output.client;
|
47
|
+
}
|
48
|
+
|
49
|
+
// Include channel metadata if requested
|
50
|
+
if (options.includeMetadata !== false) output.metadata = options.metadata;
|
51
|
+
|
52
|
+
// Add timestamp if requested (default: true)
|
53
|
+
if (options.includeTimestamp !== false) {
|
54
|
+
output.timestamp = new Date().toISOString();
|
55
|
+
} else {
|
56
|
+
// Remove timestamp if explicitly disabled
|
57
|
+
delete output.timestamp;
|
58
|
+
}
|
59
|
+
|
60
|
+
// Add priority if specified
|
61
|
+
if (options.priority !== undefined) {
|
62
|
+
output.priority = options.priority;
|
63
|
+
}
|
64
|
+
|
65
|
+
// Add expiration if specified
|
66
|
+
if (options.expiresAt !== undefined) {
|
67
|
+
output.expiresAt = options.expiresAt;
|
68
|
+
}
|
69
|
+
|
70
|
+
// Add any custom fields to the root of the message
|
71
|
+
if (options.customFields) {
|
72
|
+
Object.assign(output, options.customFields);
|
73
|
+
}
|
74
|
+
|
75
|
+
// Apply custom transformation if provided
|
76
|
+
if (options.transform) {
|
77
|
+
const transformed = options.transform(output);
|
78
|
+
return transformed;
|
79
|
+
}
|
80
|
+
} else {
|
81
|
+
output.timestamp = new Date().toISOString();
|
82
|
+
}
|
83
|
+
|
84
|
+
return output;
|
85
|
+
}
|
86
|
+
|
87
|
+
public createWhisper(message: Omit<WebsocketMessage, "type">, options?: WebsocketMessageOptions) {
|
88
|
+
return this.create({ ...message, content: message.content, channel: message.channel, type: "whisper" }, options);
|
89
|
+
}
|
90
|
+
|
91
|
+
public send(target: I_WebsocketClient, message: WebsocketStructuredMessage): void;
|
92
|
+
public send(target: I_WebsocketClient, message: WebsocketMessage, options?: WebsocketMessageOptions): void;
|
93
|
+
public send(target: I_WebsocketClient, message: WebsocketStructuredMessage | WebsocketMessage, options?: WebsocketMessageOptions): void {
|
94
|
+
target.send(this.create(message, options));
|
95
|
+
}
|
96
|
+
|
97
|
+
public serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T) {
|
98
|
+
return transform ? transform(message) : JSON.stringify(message);
|
99
|
+
}
|
100
|
+
|
101
|
+
public static Serialize<T = string>(message: WebsocketStructuredMessage, transform: (message: WebsocketStructuredMessage) => T): T;
|
102
|
+
public static Serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T;
|
103
|
+
public static Serialize<T = string>(message: WebsocketStructuredMessage, transform?: (message: WebsocketStructuredMessage) => T): string | T {
|
104
|
+
return transform ? transform(message) : JSON.stringify(message);
|
105
|
+
}
|
106
|
+
}
|
@@ -0,0 +1,221 @@
|
|
1
|
+
import { Server, ServerWebSocket, WebSocketHandler } from "bun";
|
2
|
+
import Singleton from "../../../singleton";
|
3
|
+
import { Lib } from "../../../utils";
|
4
|
+
import { Console } from "../../../utils/Console";
|
5
|
+
import Channel from "./Channel";
|
6
|
+
import Client from "./Client";
|
7
|
+
import type {
|
8
|
+
I_WebsocketChannel,
|
9
|
+
I_WebsocketClient,
|
10
|
+
I_WebsocketEntity,
|
11
|
+
I_WebsocketInterface,
|
12
|
+
WebsocketChannel,
|
13
|
+
WebsocketEntityData,
|
14
|
+
BunWebsocketMessage,
|
15
|
+
WebsocketStructuredMessage,
|
16
|
+
} from "./websocket.types";
|
17
|
+
import { E_WebsocketMessageType } from "./websocket.enums";
|
18
|
+
|
19
|
+
export type WebsocketConstructorOptions = {
|
20
|
+
debug?: boolean;
|
21
|
+
};
|
22
|
+
|
23
|
+
export interface I_WebsocketConstructor {
|
24
|
+
ws_interface?: I_WebsocketInterface;
|
25
|
+
channels?: WebsocketChannel;
|
26
|
+
clientClass?: typeof Client;
|
27
|
+
channelClass?: typeof Channel;
|
28
|
+
options?: WebsocketConstructorOptions;
|
29
|
+
}
|
30
|
+
|
31
|
+
export default class Websocket extends Singleton {
|
32
|
+
protected _channels: WebsocketChannel;
|
33
|
+
protected _clients: Map<string, I_WebsocketClient> = new Map();
|
34
|
+
protected _server!: Server;
|
35
|
+
protected _channelClass: typeof Channel;
|
36
|
+
protected _clientClass: typeof Client;
|
37
|
+
protected _ws_interface?: I_WebsocketInterface;
|
38
|
+
protected _options: WebsocketConstructorOptions;
|
39
|
+
protected _ws_interface_handlers: Partial<WebSocketHandler<WebsocketEntityData>>;
|
40
|
+
protected constructor(options?: I_WebsocketConstructor) {
|
41
|
+
super();
|
42
|
+
this._ws_interface = options?.ws_interface;
|
43
|
+
this._channels = options?.channels ?? new Map<string, Channel>();
|
44
|
+
this._clientClass = options?.clientClass ?? Client;
|
45
|
+
this._channelClass = options?.channelClass ?? Channel.GetChannelType(options?.channels);
|
46
|
+
this._options = options?.options ?? { debug: false };
|
47
|
+
this.createChannel("global", "Global", 1000);
|
48
|
+
this._ws_interface_handlers = this._ws_interface?.handlers(this._channels, this._clients) ?? {};
|
49
|
+
}
|
50
|
+
|
51
|
+
protected set server(value: Server) {
|
52
|
+
this._server = value;
|
53
|
+
}
|
54
|
+
|
55
|
+
public get server(): Server {
|
56
|
+
return this._server;
|
57
|
+
}
|
58
|
+
|
59
|
+
public set(server: Server) {
|
60
|
+
this.server = server;
|
61
|
+
Console.success("Websocket server set");
|
62
|
+
}
|
63
|
+
|
64
|
+
public createChannel(id: string, name: string, limit?: number): I_WebsocketChannel {
|
65
|
+
if (this._channels.has(id)) return this._channels.get(id) as I_WebsocketChannel;
|
66
|
+
const channel = new this._channelClass(id, name, this, limit);
|
67
|
+
this._channels.set(id, channel);
|
68
|
+
return channel;
|
69
|
+
}
|
70
|
+
|
71
|
+
public removeChannel(id: string) {
|
72
|
+
this._channels.delete(id);
|
73
|
+
}
|
74
|
+
|
75
|
+
public static CreateChannel(id: string, name: string, limit?: number) {
|
76
|
+
const ws = this.GetInstance<Websocket>();
|
77
|
+
return ws.createChannel(id, name, limit);
|
78
|
+
}
|
79
|
+
|
80
|
+
public handlers(): WebSocketHandler<WebsocketEntityData> {
|
81
|
+
return {
|
82
|
+
open: this.clientConnected,
|
83
|
+
message: this.clientMessageReceived,
|
84
|
+
close: this.clientDisconnected,
|
85
|
+
};
|
86
|
+
}
|
87
|
+
|
88
|
+
private clientMessageReceived = (ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) => {
|
89
|
+
if (Websocket.Heartbeat(ws, message)) return;
|
90
|
+
|
91
|
+
if (this._ws_interface_handlers.message) return this._ws_interface_handlers.message(ws, message);
|
92
|
+
|
93
|
+
ws.send("This is the message from the server: " + message);
|
94
|
+
Websocket.BraodcastAll({ type: "client.message.received", content: { message } });
|
95
|
+
};
|
96
|
+
|
97
|
+
private clientConnected = (ws: ServerWebSocket<WebsocketEntityData>) => {
|
98
|
+
if (this._options.debug) Lib.Log("Client connected", ws.data);
|
99
|
+
|
100
|
+
if (this._ws_interface_handlers.open) this._ws_interface_handlers.open(ws);
|
101
|
+
|
102
|
+
const global = this._channels.get("global");
|
103
|
+
if (!global) throw new Error("Global channel not found");
|
104
|
+
|
105
|
+
const client = Websocket.CreateClient({ id: ws.data.id, ws: ws, name: ws.data.name });
|
106
|
+
this._clients.set(client.id, client);
|
107
|
+
|
108
|
+
client.send({ type: E_WebsocketMessageType.CLIENT_CONNECTED, content: { message: "Welcome to the server", client: client.whoami() } });
|
109
|
+
global.addMember(client);
|
110
|
+
|
111
|
+
if (this._ws_interface_handlers.open) this._ws_interface_handlers.open(ws);
|
112
|
+
};
|
113
|
+
|
114
|
+
private clientDisconnected = (ws: ServerWebSocket<WebsocketEntityData>, code: number, reason: string) => {
|
115
|
+
if (this._options.debug) Lib.Log("Client disconnected", ws.data);
|
116
|
+
|
117
|
+
if (this._ws_interface_handlers.close) this._ws_interface_handlers.close(ws, code, reason);
|
118
|
+
|
119
|
+
const client = this._clients.get(ws.data.id);
|
120
|
+
if (!client) return;
|
121
|
+
|
122
|
+
this._channels.forEach((channel) => {
|
123
|
+
channel.removeMember(client);
|
124
|
+
});
|
125
|
+
|
126
|
+
this._clients.delete(ws.data.id);
|
127
|
+
};
|
128
|
+
|
129
|
+
private handleHeartbeat = (ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) => {
|
130
|
+
if (message === "ping") {
|
131
|
+
const pong: WebsocketStructuredMessage = { type: "pong", content: { message: "pong" } };
|
132
|
+
ws.send(JSON.stringify(pong));
|
133
|
+
return true;
|
134
|
+
}
|
135
|
+
return false;
|
136
|
+
};
|
137
|
+
|
138
|
+
protected createClient(entity: I_WebsocketEntity): I_WebsocketClient {
|
139
|
+
return new this._clientClass(entity);
|
140
|
+
}
|
141
|
+
|
142
|
+
public static Heartbeat(ws: ServerWebSocket<WebsocketEntityData>, message: BunWebsocketMessage) {
|
143
|
+
const self = this.GetInstance<Websocket>();
|
144
|
+
return self.handleHeartbeat(ws, message);
|
145
|
+
}
|
146
|
+
|
147
|
+
public static Server() {
|
148
|
+
return this.GetInstance<Websocket>().server;
|
149
|
+
}
|
150
|
+
|
151
|
+
public static Broadcast(channel: string, message: WebsocketStructuredMessage, ...args: any[]) {
|
152
|
+
// Get the server from the singleton instance
|
153
|
+
const ws = this.GetInstance<Websocket>();
|
154
|
+
if (!ws.server) {
|
155
|
+
throw new Error("Websocket server not set");
|
156
|
+
}
|
157
|
+
ws.server.publish(channel, JSON.stringify({ message, args }));
|
158
|
+
}
|
159
|
+
|
160
|
+
public static BraodcastAll(message: WebsocketStructuredMessage, ...args: any[]) {
|
161
|
+
const ws = this.GetInstance<Websocket>();
|
162
|
+
ws._channels.forEach((channel) => channel.broadcast(message, ...args));
|
163
|
+
}
|
164
|
+
|
165
|
+
public static Join(channel: string, entity: I_WebsocketEntity) {
|
166
|
+
const ws = this.GetInstance<Websocket>();
|
167
|
+
const client = ws._clients.get(entity.id);
|
168
|
+
if (!client) return;
|
169
|
+
ws._channels.get(channel)?.addMember(client);
|
170
|
+
}
|
171
|
+
|
172
|
+
public static Leave(channel: string, entity: I_WebsocketEntity) {
|
173
|
+
const ws = this.GetInstance<Websocket>();
|
174
|
+
const client = ws._clients.get(entity.id);
|
175
|
+
if (!client) return;
|
176
|
+
ws._channels.get(channel)?.removeMember(client);
|
177
|
+
}
|
178
|
+
|
179
|
+
public static GetClient(id: string) {
|
180
|
+
const ws = this.GetInstance<Websocket>();
|
181
|
+
return ws._clients.get(id);
|
182
|
+
}
|
183
|
+
|
184
|
+
public static GetChannel(id: string) {
|
185
|
+
const ws = this.GetInstance<Websocket>();
|
186
|
+
return ws._channels.get(id);
|
187
|
+
}
|
188
|
+
|
189
|
+
public static GetChannels() {
|
190
|
+
const ws = this.GetInstance<Websocket>();
|
191
|
+
return Array.from(ws._channels.values());
|
192
|
+
}
|
193
|
+
|
194
|
+
public static GetClients() {
|
195
|
+
const ws = this.GetInstance<Websocket>();
|
196
|
+
return Array.from(ws._clients.values());
|
197
|
+
}
|
198
|
+
|
199
|
+
public static GetClientCount() {
|
200
|
+
const ws = this.GetInstance<Websocket>();
|
201
|
+
return ws._clients.size;
|
202
|
+
}
|
203
|
+
|
204
|
+
public static GetChannelCount() {
|
205
|
+
const ws = this.GetInstance<Websocket>();
|
206
|
+
return ws._channels.size;
|
207
|
+
}
|
208
|
+
|
209
|
+
public static CreateClient(entity: I_WebsocketEntity): I_WebsocketClient {
|
210
|
+
const ws = this.GetInstance<Websocket>();
|
211
|
+
return ws.createClient(entity);
|
212
|
+
}
|
213
|
+
|
214
|
+
public static GenerateMessage(): WebsocketStructuredMessage {
|
215
|
+
const msg: WebsocketStructuredMessage = {
|
216
|
+
type: "",
|
217
|
+
content: {},
|
218
|
+
};
|
219
|
+
return msg;
|
220
|
+
}
|
221
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
// This file is auto-generated by scripts/generate-indexes.ts
|
2
|
+
// Do not edit this file directly
|
3
|
+
|
4
|
+
export * from './websocket.guards';
|
5
|
+
export * from './Client';
|
6
|
+
export * from './Websocket';
|
7
|
+
export * from './websocket.enums';
|
8
|
+
export * from './websocket.types';
|
9
|
+
export * from './Message';
|
10
|
+
export * from './Channel';
|
11
|
+
export { default as Client } from './Client';
|
12
|
+
export { default as Websocket } from './Websocket';
|
13
|
+
export { default as Message } from './Message';
|
14
|
+
export { default as Channel } from './Channel';
|
@@ -0,0 +1,22 @@
|
|
1
|
+
export enum E_WebsocketMessageType {
|
2
|
+
CLIENT_CONNECTED = "client.connected",
|
3
|
+
CLIENT_DISCONNECTED = "client.disconnected",
|
4
|
+
CLIENT_JOIN_CHANNEL = "client.join.channel",
|
5
|
+
CLIENT_LEAVE_CHANNEL = "client.leave.channel",
|
6
|
+
CLIENT_JOIN_CHANNELS = "client.join.channels",
|
7
|
+
CLIENT_LEAVE_CHANNELS = "client.leave.channels",
|
8
|
+
PING = "ping",
|
9
|
+
PONG = "pong",
|
10
|
+
MESSAGE = "message",
|
11
|
+
WHISPER = "whisper",
|
12
|
+
BROADCAST = "broadcast",
|
13
|
+
PROMPT = "prompt",
|
14
|
+
ERROR = "error",
|
15
|
+
SYSTEM = "system",
|
16
|
+
}
|
17
|
+
|
18
|
+
export enum E_WebsocketMessagePriority {
|
19
|
+
LOW = 0,
|
20
|
+
MEDIUM = 1,
|
21
|
+
HIGH = 2,
|
22
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { WebsocketStructuredMessage } from "./websocket.types";
|
2
|
+
|
3
|
+
export function IsWebsocketStructuredMessage(message: any): message is WebsocketStructuredMessage {
|
4
|
+
return typeof message === "object" && message !== null && "type" in message && "content" in message;
|
5
|
+
}
|
6
|
+
|