chore init project
This commit is contained in:
82
src/WetSock/Client/Client.js
Normal file
82
src/WetSock/Client/Client.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
export default class WetSockClient {
|
||||||
|
constructor(url) {
|
||||||
|
this.socket = new WebSocket(url);
|
||||||
|
this.listeners = new Map();
|
||||||
|
this.plugins = new Map();
|
||||||
|
|
||||||
|
this.socket.onopen = () => {
|
||||||
|
console.log('Connected to WetSock server');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
this.handleMessage(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onclose = () => {
|
||||||
|
console.log('Disconnected from WetSock server');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, callback) {
|
||||||
|
if (!this.listeners.has(event)) {
|
||||||
|
this.listeners.set(event, []);
|
||||||
|
}
|
||||||
|
this.listeners.get(event).push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event, ...args) {
|
||||||
|
const listeners = this.listeners.get(event);
|
||||||
|
if (listeners) {
|
||||||
|
listeners.forEach((listener) => listener(...args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message) {
|
||||||
|
const { event, ...data } = message;
|
||||||
|
|
||||||
|
if (event === 'pluginList') {
|
||||||
|
this.updatePlugins(data.plugins);
|
||||||
|
} else {
|
||||||
|
this.emit(event, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlugins(plugins) {
|
||||||
|
plugins.forEach((plugin) => {
|
||||||
|
this.plugins.set(plugin.identifier, plugin.actions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message) {
|
||||||
|
this.socket.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic methods for interacting with plugins
|
||||||
|
createEntity(pluginIdentifier, entityData) {
|
||||||
|
this.send({ action: 'create', pluginIdentifier, data: entityData });
|
||||||
|
}
|
||||||
|
|
||||||
|
performAction(pluginIdentifier, entityId, actionName, ...args) {
|
||||||
|
this.send({ action: 'perform', pluginIdentifier, entityId, data: { actionName, args } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for common actions
|
||||||
|
joinRoom(roomId) {
|
||||||
|
this.performAction('room', roomId, 'join');
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveRoom(roomId) {
|
||||||
|
this.performAction('room', roomId, 'leave');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessageToRoom(roomId, message) {
|
||||||
|
this.performAction('room', roomId, 'sendMessage', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTask(taskData) {
|
||||||
|
this.createEntity('task', taskData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... other helper methods ...
|
||||||
|
}
|
||||||
3
src/WetSock/Client/index.js
Normal file
3
src/WetSock/Client/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import WetSockClient from './WetSockClient';
|
||||||
|
|
||||||
|
export default WetSockClient;
|
||||||
24
src/WetSock/Models/Repository.js
Normal file
24
src/WetSock/Models/Repository.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default class Repository {
|
||||||
|
#entityClass = undefined;
|
||||||
|
#actions = new Map();
|
||||||
|
constructor(config) {
|
||||||
|
this.#entityClass = config.entityClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(entityData) {
|
||||||
|
return new this.#entityClass(entityData);
|
||||||
|
}
|
||||||
|
|
||||||
|
perform(entity, actionName, ...args) {
|
||||||
|
if(!this.#actions.has(actionName)) throw new Error(`Action '${actionName}' is not registered.`);
|
||||||
|
return this.#actions.get(actionName)(entity, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
register(actionName, actionFn) {
|
||||||
|
this.#actions.set(actionName, actionFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActions() {
|
||||||
|
return Array.from(this.#actions.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/WetSock/Models/Room.js
Normal file
19
src/WetSock/Models/Room.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export default class Room {
|
||||||
|
constructor(data) {
|
||||||
|
this.id = data.id;
|
||||||
|
this.name = data.name;
|
||||||
|
this.users = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
addUser(userId) {
|
||||||
|
this.users.add(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser(userId) {
|
||||||
|
this.users.delete(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsers() {
|
||||||
|
return Array.from(this.users);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/WetSock/Plugins/BasePlugin.js
Normal file
13
src/WetSock/Plugins/BasePlugin.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default class BasePlugin {
|
||||||
|
#wetsock = undefined;
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(wetsock) {
|
||||||
|
this.#wetsock = wetsock;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(clientId, message) {}
|
||||||
|
onOpen(clientId) {}
|
||||||
|
onClose(clientId) {}
|
||||||
|
}
|
||||||
65
src/WetSock/Plugins/EntityPlugin.js
Normal file
65
src/WetSock/Plugins/EntityPlugin.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import BasePlugin from './BasePlugin';
|
||||||
|
|
||||||
|
class EntityPlugin extends BasePlugin {
|
||||||
|
#config = {};
|
||||||
|
#entities = new Map();
|
||||||
|
#repository = null;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
super();
|
||||||
|
this.#config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(wetsock) {
|
||||||
|
super.initialize(wetsock);
|
||||||
|
|
||||||
|
this.#repository = this.#config.repository;
|
||||||
|
this.#config.entities.forEach((data) => {
|
||||||
|
const entity = this.#repository.create(data);
|
||||||
|
this.#entities.set(entity.id, entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(clientId, message) {
|
||||||
|
const { action, entityId, data } = message;
|
||||||
|
|
||||||
|
if (action === 'create') {
|
||||||
|
const entity = this.#repository.create(data);
|
||||||
|
this.#entities.set(entity.id, entity);
|
||||||
|
this.broadcast(clientId, { event: 'entityCreated', entityId: entity.id });
|
||||||
|
} else if (action === 'perform') {
|
||||||
|
const entity = this.#entities.get(entityId);
|
||||||
|
if (entity) {
|
||||||
|
const result = this.#repository.perform(entity, data.actionName, ...data.args);
|
||||||
|
this.broadcast(clientId, { event: 'actionPerformed', entityId, actionName: data.actionName, result });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen(clientId) {
|
||||||
|
// Send the list of available entities to the client
|
||||||
|
const entityList = Array.from(this.#entities.values());
|
||||||
|
this.send(clientId, { event: 'entityList', entities: entityList });
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(clientId) {
|
||||||
|
// Handle client disconnection if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(clientId, message) {
|
||||||
|
// Broadcast the message to all connected clients
|
||||||
|
for (const [id, client] of this.#wetsock.clients) {
|
||||||
|
if (id !== clientId) {
|
||||||
|
client.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(clientId, message) {
|
||||||
|
// Send the message to a specific client
|
||||||
|
const client = this.#wetsock.clients.get(clientId);
|
||||||
|
if (client) {
|
||||||
|
client.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/WetSock/WetSock.js
Normal file
54
src/WetSock/WetSock.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export default class WetSock {
|
||||||
|
#config = undefined;
|
||||||
|
#server = undefined;
|
||||||
|
#clients = new Map();
|
||||||
|
#plugins = new Map();
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(server) {
|
||||||
|
this.#server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPlugin(identifier, plugin) {
|
||||||
|
plugin.initialize(this);
|
||||||
|
this.#plugins.set(identifier, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugin(identifier) {
|
||||||
|
return this.#plugins.get(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpgrade(req) {
|
||||||
|
const clientId = Math.random().toString(36).substring(2);
|
||||||
|
const success = this.#server.upgrade(req, { data: { clientId } });
|
||||||
|
if (success) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Response.redirect('/', 302);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(ws, message) {
|
||||||
|
const client = this.#clients.get(ws.data.clientId);
|
||||||
|
for(const plugin of this.#plugins) {
|
||||||
|
if(plugin.onMessage(ws.data.clientId, message)) break; // plugin consumes message by returning a truthy value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpen(ws) {
|
||||||
|
const client = this.#clients.get(ws.data.clientId);
|
||||||
|
this.#plugins.forEach((plugin) => plugin.onOpen(ws.data.clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose(ws) {
|
||||||
|
const client = this.#clients.get(ws.data.clientId);
|
||||||
|
this.#plugins.forEach((plugin) => plugin.onClose(ws.data.clientId));
|
||||||
|
this.#clients.delete(ws.data.clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrain(ws) {
|
||||||
|
// Handle socket drain event
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/WetSock/index.js
Normal file
3
src/WetSock/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import WetSock from './WetSock';
|
||||||
|
|
||||||
|
export default WetSock;
|
||||||
30
src/index.js
Normal file
30
src/index.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import WetSock from './WetSock';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
const wetsock = new WetSock();
|
||||||
|
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: process.env.APP_PORT || 43_069,
|
||||||
|
fetch(req, server) {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if(url.pathname == '/chat') return wetsock.handleFetch(req, server);
|
||||||
|
|
||||||
|
if(url.pathname == '/client.js') {
|
||||||
|
const filePath = resolve(__dirname, 'WetSock', 'Client', 'Client.js');
|
||||||
|
const file = Bun.file(filePath);
|
||||||
|
return new Response(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = resolve(__dirname, 'views', 'index.html');
|
||||||
|
const file = Bun.file(filePath);
|
||||||
|
return new Response(file);
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
open: wetsock.handleOpen.bind(wetsock),
|
||||||
|
close: wetsock.handleClose.bind(wetsock),
|
||||||
|
message: wetsock.handleMessage.bind(wetsock),
|
||||||
|
drain: wetsock.handleDrain.bind(wetsock),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wetsock.start(server);
|
||||||
126
src/views/index.html
Normal file
126
src/views/index.html
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WetSock Client Test</title>
|
||||||
|
<script src="/wetsock-client.js"></script>
|
||||||
|
<style>
|
||||||
|
/* Add some basic styling */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
input, button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
#output {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WetSock Client Test</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="pluginIdentifier">Plugin:</label>
|
||||||
|
<select id="pluginIdentifier">
|
||||||
|
<!-- Options will be populated dynamically -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="actionName">Action:</label>
|
||||||
|
<select id="actionName">
|
||||||
|
<!-- Options will be populated dynamically -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="entityId">Entity ID:</label>
|
||||||
|
<input type="text" id="entityId">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="args">Arguments (comma-separated):</label>
|
||||||
|
<input type="text" id="args">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="sendButton">Send</button>
|
||||||
|
|
||||||
|
<h2>Output:</h2>
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const client = new WetSockClient('ws://wetsock.ghosted.id:80/chat');
|
||||||
|
const pluginIdentifierSelect = document.getElementById('pluginIdentifier');
|
||||||
|
const actionNameSelect = document.getElementById('actionName');
|
||||||
|
const entityIdInput = document.getElementById('entityId');
|
||||||
|
const argsInput = document.getElementById('args');
|
||||||
|
const sendButton = document.getElementById('sendButton');
|
||||||
|
const outputDiv = document.getElementById('output');
|
||||||
|
|
||||||
|
client.on('connected', () => {
|
||||||
|
log('Connected to WetSock server');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('pluginList', (data) => {
|
||||||
|
updatePluginOptions(data.plugins);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('actionPerformed', (data) => {
|
||||||
|
log(`Action performed: ${JSON.stringify(data)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('entityCreated', (data) => {
|
||||||
|
log(`Entity created: ${JSON.stringify(data)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
sendButton.addEventListener('click', () => {
|
||||||
|
const pluginIdentifier = pluginIdentifierSelect.value;
|
||||||
|
const actionName = actionNameSelect.value;
|
||||||
|
const entityId = entityIdInput.value;
|
||||||
|
const args = argsInput.value.split(',').map(arg => arg.trim());
|
||||||
|
|
||||||
|
if (actionName === 'create') {
|
||||||
|
client.createEntity(pluginIdentifier, { /* entity data */ });
|
||||||
|
} else {
|
||||||
|
client.performAction(pluginIdentifier, entityId, actionName, ...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updatePluginOptions(plugins) {
|
||||||
|
pluginIdentifierSelect.innerHTML = '';
|
||||||
|
plugins.forEach((plugin) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = plugin.identifier;
|
||||||
|
option.textContent = plugin.identifier;
|
||||||
|
pluginIdentifierSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
updateActionOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActionOptions() {
|
||||||
|
const pluginIdentifier = pluginIdentifierSelect.value;
|
||||||
|
const actions = client.plugins.get(pluginIdentifier);
|
||||||
|
actionNameSelect.innerHTML = '';
|
||||||
|
actions.forEach((action) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = action;
|
||||||
|
option.textContent = action;
|
||||||
|
actionNameSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.textContent = message;
|
||||||
|
outputDiv.appendChild(entry);
|
||||||
|
outputDiv.scrollTop = outputDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginIdentifierSelect.addEventListener('change', updateActionOptions);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user