chore init project

This commit is contained in:
Benjamin Wegener
2024-04-02 15:57:41 +00:00
commit d161d8826e
10 changed files with 419 additions and 0 deletions

View 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 ...
}

View File

@@ -0,0 +1,3 @@
import WetSockClient from './WetSockClient';
export default WetSockClient;

View 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());
}
}

View 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);
}
}

View File

@@ -0,0 +1,13 @@
export default class BasePlugin {
#wetsock = undefined;
constructor() {
}
initialize(wetsock) {
this.#wetsock = wetsock;
}
onMessage(clientId, message) {}
onOpen(clientId) {}
onClose(clientId) {}
}

View 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
View 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
View File

@@ -0,0 +1,3 @@
import WetSock from './WetSock';
export default WetSock;

30
src/index.js Normal file
View 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
View 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>