playsocketjs
Version:
WebSocket wrapper for creating simple multiplayer systems with ease.
256 lines (191 loc) • 10.8 kB
Markdown
# PlaySocket Client
A reactive, optimistic WebSocket library that simplifies game & app development by abstracting away complex sync logic.
## Why use PlaySocket?
PlaySocket eliminates the traditional complexity of collaborative experiences:
- **Streamlined architecture**: No additional backend code is required, but server-authoritative behavior supported
- **State synchronization**: Built-in storage system keeps the full state synchronized across all clients, always conflict-free and in order
- **Resilient & secure connections**: Automatic reconnection handling & strict rate-limiting
- **Lightweight**: Uses WebSockets for efficient, predictable, reliable communication and has little dependencies
## Installation
```bash
npm install playsocketjs
```
## Usage examples
Note that in production, you should **always try...catch** promises, such as socket.init() – they can reject!
Initializing the client:
```javascript
import PlaySocket from 'playsocketjs';
// Create a new instance
const socket = new PlaySocket('unique-client-id', { // You can pass no ID to let the server pick one
endpoint: 'wss://example.com/socket'
});
// Set up event handlers (optional)
socket.onEvent('status', status => console.log('Status:', status));
socket.onEvent('error', status => console.log('Error:', status));
const clientId = await socket.init(); // Initialize the socket
```
Creating a room:
```javascript
// Create a new room
const roomId = await socket.createRoom();
// Optionally, with initial storage
const roomId = await socket.createRoom({
players: ["this-player"],
latestPlayer: null,
});
```
Joining a room:
```javascript
await socket.joinRoom('room-id'); // Join an existing room
```
Leaving a room:
```javascript
socket.destroy(); // To leave the room, destroy the instance
```
Using the storage update event for reactivity:
```javascript
const reactiveVariable = useState(); // Or $state(), reactive(), depending on your framework
socket.onEvent('storageUpdated', storage => (reactiveVariable = storage)); // Assign on update
```
Interfacing with the synchronized storage (examples):
```javascript
const currentState = socket.getStorage; // Synchronous, local access
socket.updateStorage('players', 'array-add-unique', { username: 'Player4', level: 2 }); // Special method to enable conflict-free additions for arrays
socket.updateStorage('latestPlayer', 'set', 'Player4'); // Regular synced storage update
```
Sending traditional requests to the server:
```javascript
socket.sendRequest('chosen-request-name', { fact: "You can build server-authoritative logic using this!" })
```
## API reference
### Constructor
Creates a new PlaySocket instance with a specified ID and configuration options.
The ID can be set to `null` to let the server pick a unique one.
```javascript
new PlaySocket(id?: string, options: PlaySocketOptions)
```
#### Configuration options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `endpoint` | string | `none - set this!` | WebSocket server endpoint (e.g., 'wss://example.com/socket') |
| `customData` | object | `{}` | Arbitrary data to pass to the "clientRegistered" server event (optional) |
| `debug` | boolean | `false` | Set to true to enable extra logging |
### Methods
| Method | Parameters | Return type | Description |
|--------|------------|-------------|-------------|
| `init()` | - | `Promise<string>` | Initialize the WebSocket connection – Returns Promise which resolves with the client's ID |
| `createRoom()` | `initialStorage?: object, size?: number` | `Promise<string>` | Create a new room and become host – Returns Promise which resolves with the room ID. The room participant maximum is 100 |
| `joinRoom()` | `roomId: string` | `Promise<void>` | Join an existing room |
| `destroy()` | - | `void` | Use this to leave a room and close the connection |
| `updateStorage()` | `key: string, type: 'set' \| 'array-add' \| 'array-add-unique' \| 'array-remove-matching' \| 'array-update-matching', value: any, updateValue?: any` | `void` | Update a key in the shared storage (max. 100 keys). Array operation types allow for conflict-free simultaneous array updates. For '-matching' operations, value becomes the value to match, and updateValue the replacement |
| `sendRequest()` | `name: string, data?: any` | `void` | Send requests to the server with optional custom data (handle these in the `requestReceived` server event) |
| `onEvent()` | `event: string, callback: Function` | `void` | Register an event callback |
### Event types
| Event | Callback parameter | Description |
|-------|-------------------|-------------|
| `status` | `status: string` | Connection status updates |
| `error` | `error: string` | Error events |
| `instanceDestroyed` | - | Destruction event - triggered by manual .destroy() method invocation or by fatal errors and disconnects |
| `storageUpdated` | `storage: object` | Storage state changes |
| `hostMigrated` | `roomId: string` | Host changes |
| `clientConnected` | `clientId: string` | New client connected to the room |
| `clientDisconnected` | `clientId: string, roomId?: string` | Client disconnected from the room |
### Properties (Read-only)
| Property | Type | Description |
|----------|------|-------------|
| `id` | string | Client's unique identifier on the WebSocket server |
| `isHost` | boolean | If this user is currently assigned the host role |
| `connectionCount` | number | Number of active client connections in room (without yourself) |
| `getStorage` | object | Retrieve storage object |
# PlaySocket Server
PlaySocket includes a server implementation that can be set up in seconds.
## Installation
To use the server component, you'll need to install playsocketjs and the ws package:
```bash
npm install playsocketjs ws
```
## Usage examples
Here are usage examples for a standalone server and an Express.js application.
### Standalone server
```javascript
import PlaySocketServer from 'playsocketjs/server'; // Both ES Module & CommonJS Module syntax is supported
const server = new PlaySocketServer(); // Create and start the server (default path is /)
// Gracefully disconnect all clients and close the server (optional)
function shutdown() {
server.stop();
process.exit(0);
}
// Handle both SIGINT (Ctrl+C) and SIGTERM (Docker stop)
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
```
### Together with Express.js (or other Backend frameworks)
```javascript
const express = require('express');
const http = require('http');
const PlaySocketServer = require('playsocketjs/server');
const app = express();
const httpServer = http.createServer(app);
// Create PlaySocket server with your HTTP server
const playSocketServer = new PlaySocketServer({
server: httpServer,
path: '/socket'
});
// Start the server
httpServer.listen(3000, () => {
console.log('Server running on port 3000');
});
// Gracefully disconnect all clients and close the server (recommended)
function shutdown() {
server.stop();
process.exit(0);
}
// Handle both SIGINT (Ctrl+C) and SIGTERM (Docker stop)
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
```
## API reference
### Constructor
Creates a new PlaySocket Server instance with configuration options.
```javascript
new PlaySocket(options: PlaySocketServerOptions)
```
### Configuration options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `port` | number | 3000 | Port to listen on (used only if no server provided) |
| `path` | string | '/' | WebSocket endpoint path |
| `server` | http.Server | - | Existing http server (optional) |
| `rateLimit` | number | 20 | Adjust the messages/second rate limit |
| `debug` | boolean | false | Set to true to enable extra logging |
### Methods
| Method | Parameters | Return type | Description |
|--------|------------|-------------|-------------|
| `stop()` | - | `void` | Closes all active client connections, the websocket server and the underlying http server if it's standalone |
| `kick()` | `clientId: string, reason?: string` | `void` | Kick a client by their clientID – this will close their connection and set an error message |
| `onEvent()` | `event: string, callback: Function` | `void` | Register a server-side event callback |
| `getRoomStorage()` | `roomId: string` | `object` | Get a snapshot of the current room storage |
| `updateRoomStorage()` | `roomId: string, key: string, type: 'set' \| 'array-add' \| 'array-add-unique' \| 'array-remove-matching' \| 'array-update-matching', value: any, updateValue?: any` | `void` | Update a key in the shared room storage from the server |
| `createRoom()` | `initialStorage?: object, size?: number, host?: string` | `object` | Create a room (returns object containing room ID and state) – Rooms created with a non-player host like "server" (default) will not be deleted when the last participant leaves |
| `destroyRoom()` | `roomId: string` | `void` | Destroy a room & kick all participants |
### Event types
| Event | Callback parameters | Description | Return for action |
|-------|-------------------|-------------|--------------|
| `clientRegistered` | `clientId: string, customData: object` | Client registered with the server | - |
| `clientRegistrationRequested` | `clientId: string, customData: object` | Client requests to register | Return `false` or rejection reason `string` to block |
| `clientDisconnected` | `clientId: string` | Client disconnected from the server | - |
| `clientJoinedRoom` | `clientId: string, roomId: string` | Client joined a room (clients can only leave by disconnecting) | - |
| `clientJoinRequested` | `clientId: string, roomId: string` | Client requests to join a room | Return `false` or rejection reason `string` to block |
| `roomCreated` | `roomId: string` | Client created a room | - |
| `roomDestroyed` | `roomId: string` | Room was destroyed (happens when all participants leave) | - |
| `roomCreationRequested` | `{clientId: string, initialStorage: object}` | Room creation requested by client | Return `object` to override initial storage, `false` to deny |
| `storageUpdated` | `{clientId: string, roomId: string, update: object, storage: object}` | Room storage property updated | - |
| `storageUpdateRequested` | `{clientId: string, roomId: string, update: object, storage: object}` | Room storage property update requested by client | Return `false` to block the update |
| `requestReceived` | `{clientId: string, roomId?: string, requestName: string, data?: any}` | Request from client was received by the server | - |
### Properties (Read-only)
| Property | Type | Description |
|----------|------|-------------|
| `getRooms` | object | Retrieve the rooms object |
# License
MIT