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