commit d161d8826e510ab627c57654abc11e01116efecc Author: Benjamin Wegener Date: Tue Apr 2 15:57:41 2024 +0000 chore init project diff --git a/src/WetSock/Client/Client.js b/src/WetSock/Client/Client.js new file mode 100644 index 0000000..a05a894 --- /dev/null +++ b/src/WetSock/Client/Client.js @@ -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 ... +} diff --git a/src/WetSock/Client/index.js b/src/WetSock/Client/index.js new file mode 100644 index 0000000..25a8445 --- /dev/null +++ b/src/WetSock/Client/index.js @@ -0,0 +1,3 @@ +import WetSockClient from './WetSockClient'; + +export default WetSockClient; diff --git a/src/WetSock/Models/Repository.js b/src/WetSock/Models/Repository.js new file mode 100644 index 0000000..3f512c8 --- /dev/null +++ b/src/WetSock/Models/Repository.js @@ -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()); + } +} diff --git a/src/WetSock/Models/Room.js b/src/WetSock/Models/Room.js new file mode 100644 index 0000000..233c9fe --- /dev/null +++ b/src/WetSock/Models/Room.js @@ -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); + } +} diff --git a/src/WetSock/Plugins/BasePlugin.js b/src/WetSock/Plugins/BasePlugin.js new file mode 100644 index 0000000..b76de63 --- /dev/null +++ b/src/WetSock/Plugins/BasePlugin.js @@ -0,0 +1,13 @@ +export default class BasePlugin { + #wetsock = undefined; + constructor() { + } + + initialize(wetsock) { + this.#wetsock = wetsock; + } + + onMessage(clientId, message) {} + onOpen(clientId) {} + onClose(clientId) {} +} diff --git a/src/WetSock/Plugins/EntityPlugin.js b/src/WetSock/Plugins/EntityPlugin.js new file mode 100644 index 0000000..0490e4d --- /dev/null +++ b/src/WetSock/Plugins/EntityPlugin.js @@ -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)); + } + } +} diff --git a/src/WetSock/WetSock.js b/src/WetSock/WetSock.js new file mode 100644 index 0000000..feae80a --- /dev/null +++ b/src/WetSock/WetSock.js @@ -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 + } +} diff --git a/src/WetSock/index.js b/src/WetSock/index.js new file mode 100644 index 0000000..0f29329 --- /dev/null +++ b/src/WetSock/index.js @@ -0,0 +1,3 @@ +import WetSock from './WetSock'; + +export default WetSock; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..6018ef6 --- /dev/null +++ b/src/index.js @@ -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); diff --git a/src/views/index.html b/src/views/index.html new file mode 100644 index 0000000..4da3fb3 --- /dev/null +++ b/src/views/index.html @@ -0,0 +1,126 @@ + + + + WetSock Client Test + + + + +

WetSock Client Test

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +

Output:

+
+ + + +