playsocketjs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/playsocket-client.js +427 -0
- package/dist/playsocket-server.js +349 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Paul Rhomberg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# PlaySocket
|
|
2
|
+
|
|
3
|
+
A WebSocket-based multiplayer library that simplifies game development by abstracting away the backend logic and moving it to the frontend.
|
|
4
|
+
|
|
5
|
+
## Why use PlaySocket?
|
|
6
|
+
|
|
7
|
+
PlaySocket eliminates the traditional complexity of multiplayer implementations:
|
|
8
|
+
|
|
9
|
+
- **Streamlined Architecture**: No additional backend code is required
|
|
10
|
+
- **State Synchronization**: Built-in storage system keeps game state synchronized across all users
|
|
11
|
+
- **Resilient & Secure Connections**: Automatic reconnection handling and rate limiting
|
|
12
|
+
- **Lightweight**: Uses WebSockets for efficient, predictable & reliable communication with little code required
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install playsocketjs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Note that in production, you should **always try...catch** these promises, such as socket.init(), to ensure your application continues to run if errors occur.
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import PlaySocket from 'playsocketjs';
|
|
26
|
+
|
|
27
|
+
// Create a new instance
|
|
28
|
+
const socket = new PlaySocket('unique-client-id', {
|
|
29
|
+
endpoint: 'wss://example.com/socket'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Set up event handlers
|
|
33
|
+
socket.onEvent('status', status => console.log('Status:', status));
|
|
34
|
+
socket.onEvent('storageUpdated', storage => console.log('Storage update received:', storage));
|
|
35
|
+
|
|
36
|
+
// Initialize the socket
|
|
37
|
+
await socket.init();
|
|
38
|
+
|
|
39
|
+
// Create a new room
|
|
40
|
+
const roomId = await socket.createRoom();
|
|
41
|
+
|
|
42
|
+
// Optionally, you can create a room with an initial storage object
|
|
43
|
+
const roomId = await socket.createRoom({
|
|
44
|
+
players: [],
|
|
45
|
+
moreThings: {},
|
|
46
|
+
latestPlayer: null,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Join an existing room
|
|
50
|
+
await socket.joinRoom('room-id');
|
|
51
|
+
|
|
52
|
+
// Interact with the synced storage (available if in room)
|
|
53
|
+
const currentState = socket.getStorage;
|
|
54
|
+
socket.updateStorageArray('players', 'add-unique', { username: 'Player4', level: 2 }); // Special method to enable safe, simultaneous storage updates for arrays
|
|
55
|
+
socket.updateStorage('latestPlayer', 'Player4'); // Regular synced storage update
|
|
56
|
+
|
|
57
|
+
// To leave the room, destroy the instance
|
|
58
|
+
socket.destroy();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API Reference
|
|
62
|
+
|
|
63
|
+
### Constructor
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
new PlaySocket(id: string, options: PlaySocketOptions)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Creates a new PlaySocket instance with a specified ID and configuration options.
|
|
70
|
+
|
|
71
|
+
#### Configuration options
|
|
72
|
+
- `endpoint`: WebSocket server endpoint (e.g., 'wss://example.com/socket')
|
|
73
|
+
- `customData`: You can pass arbitrary data to the "clientRegistered" server event (optional)
|
|
74
|
+
|
|
75
|
+
### Methods
|
|
76
|
+
|
|
77
|
+
#### Core
|
|
78
|
+
|
|
79
|
+
- `init()`: Initialize the WebSocket connection – Returns Promise (async)
|
|
80
|
+
- `createRoom(initialStorage?: object, maxSize?: number)`: Create a new room and become host – Returns Promise (async) which resolves with the room id (matches the creator's id)
|
|
81
|
+
- `joinRoom(hostId: string)`: Join an existing room – Returns Promise (async)
|
|
82
|
+
- `destroy()`: Use this to leave a room and close the connection
|
|
83
|
+
|
|
84
|
+
#### State management
|
|
85
|
+
|
|
86
|
+
- `updateStorage(key: string, value: any)`: Update a value in the synchronized storage
|
|
87
|
+
- `updateStorageArray(key: string, operation: 'add' | 'add-unique' | 'remove-matching' | 'update-matching', value: any, updateValue?: any)`: Safely update arrays in storage by adding, removing, or updating items. This is necessary for when array updates might be happening simultaneously to ensure changes are being applied and not overwritten. Using add-unique instead of add ensures that this value can only be in the array once.
|
|
88
|
+
- `onEvent(event: string, callback: Function)`: Register an event callback
|
|
89
|
+
|
|
90
|
+
##### Event types
|
|
91
|
+
|
|
92
|
+
- `status`: Connection status updates (returns status `string`)
|
|
93
|
+
- `error`: Error events (returns error `string`)
|
|
94
|
+
- `instanceDestroyed`: Destruction event - triggered by manual .destroy() method invocation or by fatal errors and disconnects
|
|
95
|
+
- `storageUpdated`: Storage state changes (returns storage `object`)
|
|
96
|
+
- `hostMigrated`: Host changes (returns the new host's id `string`) – As compared to PlayPeerJS, the room id does NOT change when the host changes
|
|
97
|
+
- `clientConnected`: New client connected to the room (returns client-id `string`)
|
|
98
|
+
- `clientDisconnected`: Client disconnected from the room (returns client-id `string`)
|
|
99
|
+
|
|
100
|
+
### Properties (Read-only)
|
|
101
|
+
|
|
102
|
+
The `id` is used to distinguish the client from other clients on the WebSocket server.
|
|
103
|
+
Using a UUID is recommended, but it is also fine to use any other random string. If you're using a public WebSocket server, including your application's name in the `id` can help to prevent overlap (e.g. your-app-012345abcdef).
|
|
104
|
+
|
|
105
|
+
- `id`: Client's unique identifier
|
|
106
|
+
- `isHost`: If this user is currently assigned the host role
|
|
107
|
+
- `connectionCount`: Number of active client connections in room (without you)
|
|
108
|
+
- `getStorage`: Retrieve storage object
|
|
109
|
+
|
|
110
|
+
# PlaySocket Server
|
|
111
|
+
|
|
112
|
+
PlaySocket includes a server implementation that can be set up in seconds.
|
|
113
|
+
|
|
114
|
+
## Installation
|
|
115
|
+
|
|
116
|
+
To use the server component, you'll need to install playsocketjs and the ws package:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm install playsocketjs ws
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
Here are usage examples for a standalone server and an express application. The implementation is
|
|
125
|
+
framework agnostic and the Express example can be adapted to any other backend solution.
|
|
126
|
+
|
|
127
|
+
### Standalone server
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const PlaySocketServer = require('playsocketjs/server');
|
|
131
|
+
|
|
132
|
+
// Create and start the server
|
|
133
|
+
const server = new PlaySocketServer();
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### With Express.js
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const express = require('express');
|
|
140
|
+
const http = require('http');
|
|
141
|
+
const PlaySocketServer = require('playsocketjs/server');
|
|
142
|
+
const port = 3000;
|
|
143
|
+
|
|
144
|
+
const app = express();
|
|
145
|
+
const httpServer = http.createServer(app);
|
|
146
|
+
|
|
147
|
+
// Create PlaySocket server with your HTTP server
|
|
148
|
+
// Uses a custom path here (which you'll likely want to do if used this way)
|
|
149
|
+
const playSocketServer = new PlaySocketServer({
|
|
150
|
+
port,
|
|
151
|
+
server: httpServer,
|
|
152
|
+
path: '/socket'
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Start the server
|
|
156
|
+
httpServer.listen(port, () => {
|
|
157
|
+
console.log('Server running on port 3000');
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## API Reference
|
|
162
|
+
|
|
163
|
+
### Constructor
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
new PlaySocket(options: PlaySocketServerOptions)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Creates a new PlaySocket Server instance with configuration options.
|
|
170
|
+
|
|
171
|
+
### Configuration options
|
|
172
|
+
|
|
173
|
+
- `port`: Port to listen on (default: 3000, used only if no server provided)
|
|
174
|
+
- `path`: WebSocket endpoint path (default: '/')
|
|
175
|
+
- `server`: Existing http server (optional)
|
|
176
|
+
|
|
177
|
+
### Methods
|
|
178
|
+
|
|
179
|
+
- `onEvent(event: string, callback: Function)`: Register a server-side event callback
|
|
180
|
+
|
|
181
|
+
##### Event types
|
|
182
|
+
|
|
183
|
+
- `clientRegistered`: Client registered with the server (returns the client's id `string`, customData `object`)
|
|
184
|
+
- `clientDisconnected`: Client disconnected from the server (returns the client's id `string`)
|
|
185
|
+
- `roomCreated`: Client created a room (returns room id `string`)
|
|
186
|
+
- `roomJoined`: Client joined a room (returns the client's id `string`, room id `string`)
|
|
187
|
+
|
|
188
|
+
### Properties (Read-only)
|
|
189
|
+
|
|
190
|
+
- `getRooms`: Retrieve the rooms object
|
|
191
|
+
|
|
192
|
+
# License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
195
|
+
|
|
196
|
+
# Contributing
|
|
197
|
+
|
|
198
|
+
Please feel free to fork the repository and submit a Pull Request.
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlaySocket - WebSocket-based multiplayer for games
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const ERROR_PREFIX = "PlaySocket error: ";
|
|
6
|
+
const WARNING_PREFIX = "PlaySocket warning: ";
|
|
7
|
+
const TIMEOUT_MS = 3000; // 3 second timeout for operations
|
|
8
|
+
|
|
9
|
+
class PlaySocket {
|
|
10
|
+
// Core properties
|
|
11
|
+
#id; // Unique client ID
|
|
12
|
+
#endpoint;
|
|
13
|
+
#socket; // WebSocket connection
|
|
14
|
+
#initialized = false; // Initialization status
|
|
15
|
+
#customData;
|
|
16
|
+
|
|
17
|
+
// Room properties
|
|
18
|
+
#storage = {}; // Shared storage object
|
|
19
|
+
#isHost = false; // Whether this client is host
|
|
20
|
+
#roomHost;
|
|
21
|
+
#roomId; // ID of the host (client only)
|
|
22
|
+
#connectionCount = 0;
|
|
23
|
+
|
|
24
|
+
// Event handling
|
|
25
|
+
#callbacks = new Map(); // Event callbacks
|
|
26
|
+
|
|
27
|
+
// Async server operations
|
|
28
|
+
#pendingJoin;
|
|
29
|
+
#pendingHost;
|
|
30
|
+
#pendingInit;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new PlaySocket instance
|
|
34
|
+
* @param {string} id - Unique identifier for this client
|
|
35
|
+
* @param {object} options - Connection options
|
|
36
|
+
* @param {string} options.endpoint - WebSocket endpoint path
|
|
37
|
+
* @param {object} [options.customData] - Custom registration data
|
|
38
|
+
*/
|
|
39
|
+
constructor(id, options = {}) {
|
|
40
|
+
this.#id = id;
|
|
41
|
+
if (options.endpoint) this.#endpoint = options.endpoint;
|
|
42
|
+
if (options.customData) this.#customData = { ...options.customData };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Helper to create timeout promises for create room, join room etc.
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
#createTimeout(operation) {
|
|
50
|
+
return new Promise((_, reject) =>
|
|
51
|
+
setTimeout(() => reject(new Error(`${operation} timed out`)), TIMEOUT_MS)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register an event callback
|
|
57
|
+
* @param {string} event - Event name
|
|
58
|
+
* @param {Function} callback - Callback function
|
|
59
|
+
*/
|
|
60
|
+
onEvent(event, callback) {
|
|
61
|
+
const validEvents = [
|
|
62
|
+
"status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected",
|
|
63
|
+
];
|
|
64
|
+
if (!validEvents.includes(event)) return console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
|
|
65
|
+
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
66
|
+
this.#callbacks.get(event).push(callback);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Trigger an event to registered callbacks
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
#triggerEvent(event, ...args) {
|
|
74
|
+
const callbacks = this.#callbacks.get(event);
|
|
75
|
+
callbacks?.forEach(callback => {
|
|
76
|
+
try {
|
|
77
|
+
callback(...args);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(ERROR_PREFIX + `${event} callback error:`, error);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initialize the PlaySocket instance by connecting to the WS server
|
|
86
|
+
* @returns {Promise} Resolves when connection is established
|
|
87
|
+
*/
|
|
88
|
+
async init() {
|
|
89
|
+
if (this.#initialized) return Promise.reject(new Error("Already initialized."));
|
|
90
|
+
if (!this.#endpoint) return Promise.reject(new Error("No websocket endpoint provided."));
|
|
91
|
+
this.#triggerEvent("status", "Initializing...");
|
|
92
|
+
|
|
93
|
+
return Promise.race([
|
|
94
|
+
this.#connect(),
|
|
95
|
+
this.#createTimeout("Initialization")
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Connect to the WS server and register
|
|
102
|
+
* @returns {Promise} Resolves when user is registered on the WS server
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
async #connect() {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
this.#socket = new WebSocket(this.#endpoint);
|
|
108
|
+
this.#setupSocketHandlers(); // Message & close events
|
|
109
|
+
|
|
110
|
+
// Resolve or reject depending on answer to registration msg
|
|
111
|
+
this.#pendingInit = { resolve, reject };
|
|
112
|
+
|
|
113
|
+
// Register with server
|
|
114
|
+
this.#socket.onopen = () => {
|
|
115
|
+
this.#initialized = true;
|
|
116
|
+
this.#sendToServer({
|
|
117
|
+
type: 'register',
|
|
118
|
+
id: this.#id,
|
|
119
|
+
customData: this.#customData || undefined
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Set up WebSocket message & close handlers
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
#setupSocketHandlers() {
|
|
130
|
+
this.#socket.onmessage = (event) => {
|
|
131
|
+
try {
|
|
132
|
+
const message = JSON.parse(event.data);
|
|
133
|
+
if (!message.type) return;
|
|
134
|
+
|
|
135
|
+
switch (message.type) {
|
|
136
|
+
case 'connection_accepted':
|
|
137
|
+
// Connected to room
|
|
138
|
+
if (this.#pendingJoin) {
|
|
139
|
+
this.#storage = message.storage;
|
|
140
|
+
this.#connectionCount = message.participantCount - 1;
|
|
141
|
+
this.#triggerEvent("status", `Connected to room.`);
|
|
142
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
143
|
+
this.#pendingJoin.resolve();
|
|
144
|
+
this.#pendingJoin = null;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case 'connection_rejected':
|
|
149
|
+
// Connection to room failed
|
|
150
|
+
if (this.#pendingJoin) {
|
|
151
|
+
this.#triggerEvent("status", "Connection rejected: " + message.reason || 'Unknown reason');
|
|
152
|
+
this.#pendingJoin.reject(new Error("Connection rejected: " + message.reason || 'Unknown reason'));
|
|
153
|
+
this.#pendingJoin = null;
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'room_created':
|
|
158
|
+
if (this.#pendingHost) {
|
|
159
|
+
this.#pendingHost.resolve(this.#roomId);
|
|
160
|
+
this.#triggerEvent("status", `Room created${this.#pendingHost.maxSize ? ` with max size ${this.#pendingHost.maxSize}.` : '.'}`);
|
|
161
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
162
|
+
this.#pendingHost = null;
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'room_creation_failed':
|
|
167
|
+
if (this.#pendingHost) {
|
|
168
|
+
this.#pendingHost.reject(new Error("Failed to create room: " + message.reason || 'Unknown reason'));
|
|
169
|
+
this.#triggerEvent("error", "Failed to create room: " + message.reason || 'Unknown reason');
|
|
170
|
+
this.#pendingHost = null;
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case 'id_taken':
|
|
175
|
+
if (this.#pendingInit) {
|
|
176
|
+
this.#pendingInit.reject(new Error("This id is already in use."));
|
|
177
|
+
this.#triggerEvent("error", "This id is taken.");
|
|
178
|
+
this.#pendingInit = null;
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case 'registered':
|
|
183
|
+
if (this.#pendingInit) {
|
|
184
|
+
this.#pendingInit.resolve();
|
|
185
|
+
this.#triggerEvent("status", "Connected to server.");
|
|
186
|
+
this.#pendingInit = null;
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case 'storage_sync':
|
|
191
|
+
if (message.key && JSON.stringify(this.#storage[message.key]) !== JSON.stringify(message.value)) {
|
|
192
|
+
this.#storage[message.key] = message.value;
|
|
193
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case 'client_disconnected':
|
|
198
|
+
this.#isHost = this.#id == message.updatedHost;
|
|
199
|
+
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
200
|
+
|
|
201
|
+
// If host has changed...
|
|
202
|
+
if (this.#roomHost != message.updatedHost) {
|
|
203
|
+
this.#roomHost = message.updatedHost;
|
|
204
|
+
this.#triggerEvent("hostMigrated", this.#roomHost);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.#triggerEvent("clientDisconnected", message.client);
|
|
208
|
+
this.#triggerEvent("status", `Client ${message.client} disconnected.`);
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
case 'client_connected':
|
|
212
|
+
this.#connectionCount = message.participantCount - 1;
|
|
213
|
+
this.#triggerEvent("clientConnected", message.client);
|
|
214
|
+
this.#triggerEvent("status", `Client ${message.client} connected.`);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(ERROR_PREFIX + "Error handling message:", error);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Handle socket errors
|
|
223
|
+
this.#socket.onerror = () => {
|
|
224
|
+
this.#triggerEvent("error", "WebSocket error.");
|
|
225
|
+
if (this.#pendingInit) this.#pendingInit.reject(new Error("WebSocket error."));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Handle socket close & attempt reconnect
|
|
229
|
+
this.#socket.onclose = () => {
|
|
230
|
+
this.#triggerEvent("status", "Disconnected from server.");
|
|
231
|
+
|
|
232
|
+
setTimeout(async () => {
|
|
233
|
+
if (!this.#initialized || !this.#socket) return;
|
|
234
|
+
try {
|
|
235
|
+
await this.#connect(); // Will set status to connected if successful
|
|
236
|
+
|
|
237
|
+
// Rejoin room if needed
|
|
238
|
+
if (this.#roomId) {
|
|
239
|
+
this.#triggerEvent("status", "Rejoining room...");
|
|
240
|
+
await this.joinRoom(this.#roomId); // If this fails, it will be caught by the outer catch
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.#triggerEvent("error", "WebSocket connection permanently closed: " + error);
|
|
244
|
+
this.#socket = null;
|
|
245
|
+
this.destroy();
|
|
246
|
+
}
|
|
247
|
+
}, 1000);
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Send a message to the server
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
#sendToServer(data) {
|
|
256
|
+
if (!this.#initialized || !this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
|
|
257
|
+
return console.error(ERROR_PREFIX + "Cannot send message - not connected.");
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
this.#socket.send(JSON.stringify(data));
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error(ERROR_PREFIX + "Error sending message:", error);
|
|
263
|
+
this.#triggerEvent("error", "Error sending message: " + error.message);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create a new room and become host
|
|
269
|
+
* @param {object} initialStorage - Initial state
|
|
270
|
+
* @param {number} maxSize - Max number of participants
|
|
271
|
+
* @returns {Promise} Resolves with room ID
|
|
272
|
+
*/
|
|
273
|
+
createRoom(initialStorage = {}, maxSize) {
|
|
274
|
+
if (!this.#initialized) {
|
|
275
|
+
this.#triggerEvent("error", "Cannot create room - not initialized");
|
|
276
|
+
return Promise.reject(new Error("Not initialized"));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return Promise.race([
|
|
280
|
+
new Promise((resolve, reject) => {
|
|
281
|
+
this.#isHost = true;
|
|
282
|
+
this.#storage = initialStorage;
|
|
283
|
+
this.#roomId = this.#id; // Create room with your own ID as the room ID to mimic p2p
|
|
284
|
+
this.#roomHost = this.#id;
|
|
285
|
+
this.#pendingHost = { maxSize, resolve, reject };
|
|
286
|
+
|
|
287
|
+
this.#sendToServer({
|
|
288
|
+
type: 'create_room',
|
|
289
|
+
storage: { ...initialStorage },
|
|
290
|
+
size: maxSize,
|
|
291
|
+
from: this.#id,
|
|
292
|
+
});
|
|
293
|
+
}),
|
|
294
|
+
this.#createTimeout("Room creation")
|
|
295
|
+
]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Join an existing room
|
|
300
|
+
* @param {string} roomId - ID of the room
|
|
301
|
+
* @returns {Promise} Resolves when connected
|
|
302
|
+
*/
|
|
303
|
+
joinRoom(roomId) {
|
|
304
|
+
if (!this.#initialized) {
|
|
305
|
+
this.#triggerEvent("error", "Cannot join room - not initialized");
|
|
306
|
+
return Promise.reject(new Error("Not initialized"));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return Promise.race([
|
|
310
|
+
new Promise((resolve, reject) => {
|
|
311
|
+
this.#isHost = false;
|
|
312
|
+
this.#roomId = roomId;
|
|
313
|
+
this.#roomHost = roomId;
|
|
314
|
+
this.#pendingJoin = { resolve, reject };
|
|
315
|
+
|
|
316
|
+
// Send connection request
|
|
317
|
+
this.#triggerEvent("status", `Connecting to room ${roomId}...`);
|
|
318
|
+
this.#sendToServer({
|
|
319
|
+
type: 'join_room',
|
|
320
|
+
roomId
|
|
321
|
+
});
|
|
322
|
+
}),
|
|
323
|
+
this.#createTimeout("Room join")
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Update a value in the shared storage
|
|
329
|
+
* @param {string} key - Storage key
|
|
330
|
+
* @param {*} value - New value
|
|
331
|
+
*/
|
|
332
|
+
updateStorage(key, value) {
|
|
333
|
+
if (JSON.stringify(this.#storage[key]) === JSON.stringify(value)) return;
|
|
334
|
+
this.#storage[key] = value;
|
|
335
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
336
|
+
this.#sendToServer({
|
|
337
|
+
type: 'room_storage_update',
|
|
338
|
+
key,
|
|
339
|
+
value
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Update an array in storage with special operations
|
|
345
|
+
* @param {string} key - Storage key
|
|
346
|
+
* @param {string} operation - Operation type: add, add-unique, remove-matching, update-matching
|
|
347
|
+
* @param {*} value - Value to operate on
|
|
348
|
+
* @param {*} updateValue - New value for update-matching
|
|
349
|
+
*/
|
|
350
|
+
updateStorageArray(key, operation, value, updateValue) {
|
|
351
|
+
this.#handleArrayUpdate(key, operation, value, updateValue);
|
|
352
|
+
this.#sendToServer({
|
|
353
|
+
type: 'room_storage_array_update',
|
|
354
|
+
key,
|
|
355
|
+
operation,
|
|
356
|
+
value,
|
|
357
|
+
updateValue
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Handle array operations client-side
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
#handleArrayUpdate(key, operation, value, updateValue) {
|
|
366
|
+
if (!this.#storage[key] || !Array.isArray(this.#storage[key])) this.#storage[key] = [];
|
|
367
|
+
let array = this.#storage[key];
|
|
368
|
+
|
|
369
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
370
|
+
const compare = (item) => isObject ?
|
|
371
|
+
JSON.stringify(item) === JSON.stringify(value) : item === value;
|
|
372
|
+
|
|
373
|
+
switch (operation) {
|
|
374
|
+
case 'add':
|
|
375
|
+
array.push(value);
|
|
376
|
+
break;
|
|
377
|
+
|
|
378
|
+
case 'add-unique':
|
|
379
|
+
if (!array.some(compare)) array.push(value);
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case 'remove-matching':
|
|
383
|
+
this.#storage[key] = array.filter(item => !compare(item));
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case 'update-matching':
|
|
387
|
+
const index = array.findIndex(compare);
|
|
388
|
+
if (index !== -1) array[index] = updateValue;
|
|
389
|
+
break;
|
|
390
|
+
|
|
391
|
+
default:
|
|
392
|
+
console.error(ERROR_PREFIX + `Unknown array operation: ${operation}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Destroy the PlaySocket instance and disconnect
|
|
401
|
+
*/
|
|
402
|
+
destroy() {
|
|
403
|
+
if (this.#socket) {
|
|
404
|
+
this.#socket.close();
|
|
405
|
+
this.#socket = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Reset state
|
|
409
|
+
this.#initialized = false;
|
|
410
|
+
this.#isHost = false;
|
|
411
|
+
this.#roomHost = null;
|
|
412
|
+
this.#storage = {};
|
|
413
|
+
this.#roomId = null;
|
|
414
|
+
this.#connectionCount = 0;
|
|
415
|
+
|
|
416
|
+
this.#triggerEvent("status", "Destroyed.");
|
|
417
|
+
this.#triggerEvent("instanceDestroyed");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Public getters
|
|
421
|
+
get connectionCount() { return this.#connectionCount; }
|
|
422
|
+
get getStorage() { return { ...this.#storage }; }
|
|
423
|
+
get isHost() { return this.#isHost; }
|
|
424
|
+
get id() { return this.#id; }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export { PlaySocket as default };
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var require$$0 = require('ws');
|
|
4
|
+
var require$$1 = require('http');
|
|
5
|
+
|
|
6
|
+
function getDefaultExportFromCjs (x) {
|
|
7
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
var server$1;
|
|
11
|
+
var hasRequiredServer;
|
|
12
|
+
|
|
13
|
+
function requireServer () {
|
|
14
|
+
if (hasRequiredServer) return server$1;
|
|
15
|
+
hasRequiredServer = 1;
|
|
16
|
+
const WebSocket = require$$0;
|
|
17
|
+
const http = require$$1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* PlaySocketServer - WebSocket server for PlaySocket multiplayer library
|
|
21
|
+
*/
|
|
22
|
+
class PlaySocketServer {
|
|
23
|
+
#server;
|
|
24
|
+
#wss;
|
|
25
|
+
#clients = new Map(); // ClientId -> WebSocket instance
|
|
26
|
+
#rooms = {};
|
|
27
|
+
#clientRooms = new Map(); // ClientId -> RoomId
|
|
28
|
+
#rateLimits = new Map(); // Rate limiting storage
|
|
29
|
+
#callbacks = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a new PlaySocketServer instance
|
|
33
|
+
* @param {Object} options - Server configuration options
|
|
34
|
+
* @param {HttpServer} [options.server] - Existing http server
|
|
35
|
+
* @param {number} [options.port=3000] - Port to listen on (if no server provided)
|
|
36
|
+
* @param {string} [options.path='/socket'] - WebSocket endpoint path
|
|
37
|
+
*/
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
const { server, port = 3000, path = '/' } = options;
|
|
40
|
+
|
|
41
|
+
// Handle server creation / usage
|
|
42
|
+
if (server) {
|
|
43
|
+
this.#server = server; // Use provided HTTP server
|
|
44
|
+
} else {
|
|
45
|
+
this.#server = http.createServer(); // Create HTTP server and start it
|
|
46
|
+
this.#server.listen(port, () => {
|
|
47
|
+
console.log(`PlaySocket server running on port ${port}`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create WebSocket server
|
|
52
|
+
this.#wss = new WebSocket.Server({
|
|
53
|
+
server: this.#server,
|
|
54
|
+
path
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Set up ws event handlers
|
|
58
|
+
this.#wss.on('connection', ws => {
|
|
59
|
+
ws.on('message', msg => this.#handleMessage(ws, msg));
|
|
60
|
+
ws.on('close', () => this.#handleDisconnection(ws));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
process.on('SIGINT', () => this.#stop()); // Graceful shutdown
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handle WebSocket message
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
#handleMessage(ws, message) {
|
|
71
|
+
try {
|
|
72
|
+
const data = JSON.parse(message);
|
|
73
|
+
|
|
74
|
+
// Apply rate limiting after client is registered
|
|
75
|
+
if (ws.clientId && !this.#checkRateLimit(ws.clientId, data.type)) {
|
|
76
|
+
return console.error(`Client ${ws.clientId} rate limit exceeded.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
switch (data.type) {
|
|
80
|
+
case 'register':
|
|
81
|
+
// Register client ID
|
|
82
|
+
if (!data.id) return;
|
|
83
|
+
if (this.#clients.get(data.id)) {
|
|
84
|
+
ws.send(JSON.stringify({ type: 'id_taken' }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
ws.clientId = data.id; // Adds the provided id to the ws object as clientId
|
|
88
|
+
this.#clients.set(data.id, ws);
|
|
89
|
+
ws.send(JSON.stringify({ type: 'registered' }));
|
|
90
|
+
this.#triggerEvent("clientRegistered", data.id, data.customData);
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case 'create_room':
|
|
94
|
+
if (!ws.clientId) return;
|
|
95
|
+
const newRoomId = ws.clientId;
|
|
96
|
+
if (this.#clientRooms.get(ws.clientId) || this.#rooms[newRoomId]) {
|
|
97
|
+
ws.send(JSON.stringify({
|
|
98
|
+
type: 'room_creation_failed',
|
|
99
|
+
reason: this.#clientRooms.get(ws.clientId) ? 'Already in a room' : 'Room id is taken'
|
|
100
|
+
}));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.#rooms[newRoomId] = {
|
|
105
|
+
participants: [ws.clientId],
|
|
106
|
+
maxSize: data.size || null,
|
|
107
|
+
storage: data.storage || {},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.#clientRooms.set(ws.clientId, newRoomId);
|
|
111
|
+
ws.send(JSON.stringify({ type: 'room_created' }));
|
|
112
|
+
this.#triggerEvent("roomCreated", ws.clientId); // Client id = room id
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'join_room':
|
|
116
|
+
if (!data.roomId) return;
|
|
117
|
+
const roomId = data.roomId;
|
|
118
|
+
const room = this.#rooms[roomId];
|
|
119
|
+
|
|
120
|
+
if (!room ||
|
|
121
|
+
(room.maxSize && room.participants.length >= room.maxSize) ||
|
|
122
|
+
this.#clientRooms.get(ws.clientId)) {
|
|
123
|
+
ws.send(JSON.stringify({
|
|
124
|
+
type: 'connection_rejected',
|
|
125
|
+
reason: !room ? 'Room not found' :
|
|
126
|
+
room.maxSize && room.participants.length >= room.maxSize ? 'Room full' :
|
|
127
|
+
'Already in a room'
|
|
128
|
+
}));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
room.participants.push(ws.clientId);
|
|
133
|
+
this.#clientRooms.set(ws.clientId, roomId);
|
|
134
|
+
|
|
135
|
+
// Notify joiner with initial storage
|
|
136
|
+
ws.send(JSON.stringify({
|
|
137
|
+
type: 'connection_accepted',
|
|
138
|
+
storage: room.storage,
|
|
139
|
+
participantCount: room.participants.length
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
// Notify existing participants
|
|
143
|
+
room.participants.forEach(p => {
|
|
144
|
+
if (p === ws.clientId) return;
|
|
145
|
+
const client = this.#clients.get(p);
|
|
146
|
+
if (client) {
|
|
147
|
+
client.send(JSON.stringify({
|
|
148
|
+
type: 'client_connected',
|
|
149
|
+
client: ws.clientId,
|
|
150
|
+
participantCount: room.participants.length
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
this.#triggerEvent("roomJoined", ws.clientId, roomId);
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'room_storage_update':
|
|
158
|
+
const updateRoomId = this.#clientRooms.get(ws.clientId);
|
|
159
|
+
const updateRoom = updateRoomId ? this.#rooms[updateRoomId] : null;
|
|
160
|
+
if (updateRoom && data.key) {
|
|
161
|
+
updateRoom.storage[data.key] = data.value;
|
|
162
|
+
this.#syncRoomStorageKey(updateRoomId, data.key);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'room_storage_array_update':
|
|
167
|
+
const arrayRoomId = this.#clientRooms.get(ws.clientId);
|
|
168
|
+
const arrayRoom = arrayRoomId ? this.#rooms[arrayRoomId] : null;
|
|
169
|
+
if (arrayRoom && data.key) {
|
|
170
|
+
this.#arrayUpdate(arrayRoom.storage, data.key, data.operation, data.value, data.updateValue);
|
|
171
|
+
this.#syncRoomStorageKey(arrayRoomId, data.key);
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Error processing message:', error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check rate limit using token bucket algorithm
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
#checkRateLimit(clientId, operationType) {
|
|
185
|
+
const MAX_POINTS = 100;
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
|
|
188
|
+
if (!this.#rateLimits.has(clientId)) {
|
|
189
|
+
this.#rateLimits.set(clientId, { points: MAX_POINTS, lastReset: now });
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const limit = this.#rateLimits.get(clientId);
|
|
194
|
+
|
|
195
|
+
// Reset points if interval has passed (5s)
|
|
196
|
+
if (now - limit.lastReset > 5000) {
|
|
197
|
+
limit.points = MAX_POINTS;
|
|
198
|
+
limit.lastReset = now;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const pointCost = ['create_room', 'join_room'].includes(operationType) ? 20 : 1;
|
|
202
|
+
if (limit.points < pointCost) return false;
|
|
203
|
+
|
|
204
|
+
limit.points -= pointCost;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sync storage key to all clients in room
|
|
210
|
+
* @private
|
|
211
|
+
*/
|
|
212
|
+
#syncRoomStorageKey(roomId, key) {
|
|
213
|
+
const room = this.#rooms[roomId];
|
|
214
|
+
if (!room) return;
|
|
215
|
+
|
|
216
|
+
room.participants.forEach(p => {
|
|
217
|
+
const client = this.#clients.get(p);
|
|
218
|
+
if (client) {
|
|
219
|
+
client.send(JSON.stringify({
|
|
220
|
+
type: 'storage_sync',
|
|
221
|
+
key,
|
|
222
|
+
value: room.storage[key]
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Handle array operations
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
#arrayUpdate(storage, key, operation, value, updateValue) {
|
|
233
|
+
if (!storage[key] || !Array.isArray(storage[key])) storage[key] = [];
|
|
234
|
+
let array = storage[key];
|
|
235
|
+
|
|
236
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
237
|
+
const compare = (item) => isObject ?
|
|
238
|
+
JSON.stringify(item) === JSON.stringify(value) : item === value;
|
|
239
|
+
|
|
240
|
+
switch (operation) {
|
|
241
|
+
case 'add':
|
|
242
|
+
array.push(value);
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'add-unique':
|
|
246
|
+
if (!array.some(compare)) array.push(value);
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case 'remove-matching':
|
|
250
|
+
storage[key] = array.filter(item => !compare(item));
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'update-matching':
|
|
254
|
+
const index = array.findIndex(compare);
|
|
255
|
+
if (index !== -1) array[index] = updateValue;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Handle client disconnection
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
#handleDisconnection(ws) {
|
|
265
|
+
if (!ws.clientId) return;
|
|
266
|
+
|
|
267
|
+
this.#clients.delete(ws.clientId);
|
|
268
|
+
this.#rateLimits.delete(ws.clientId);
|
|
269
|
+
|
|
270
|
+
const roomId = this.#clientRooms.get(ws.clientId);
|
|
271
|
+
const room = roomId ? this.#rooms[roomId] : null;
|
|
272
|
+
|
|
273
|
+
if (room) {
|
|
274
|
+
room.participants = room.participants?.filter(p => p !== ws.clientId);
|
|
275
|
+
this.#clientRooms.delete(ws.clientId);
|
|
276
|
+
|
|
277
|
+
// Notify remaining participants
|
|
278
|
+
room.participants?.forEach(p => {
|
|
279
|
+
const client = this.#clients.get(p);
|
|
280
|
+
if (client) {
|
|
281
|
+
try {
|
|
282
|
+
client.send(JSON.stringify({
|
|
283
|
+
type: 'client_disconnected',
|
|
284
|
+
updatedHost: room.participants?.[0],
|
|
285
|
+
client: ws.clientId,
|
|
286
|
+
participantCount: room.participants?.length
|
|
287
|
+
}));
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(`Error notifying client of disconnection:`, error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
if (room.participants?.length === 0) delete this.#rooms[roomId]; // Delete room if now empty
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.#triggerEvent("clientDisconnected", ws.clientId);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Register an event callback
|
|
301
|
+
* @param {string} event - Event name
|
|
302
|
+
* @param {Function} callback - Callback function
|
|
303
|
+
*/
|
|
304
|
+
onEvent(event, callback) {
|
|
305
|
+
const validEvents = ["clientRegistered", "clientDisconnected", "roomCreated", "roomJoined"];
|
|
306
|
+
if (!validEvents.includes(event)) return console.warn(`Invalid PlaySocket event type "${event}"`);
|
|
307
|
+
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
308
|
+
this.#callbacks.get(event).push(callback);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Trigger an event to registered callbacks
|
|
313
|
+
* @private
|
|
314
|
+
*/
|
|
315
|
+
#triggerEvent(event, ...args) {
|
|
316
|
+
const callbacks = this.#callbacks.get(event);
|
|
317
|
+
callbacks?.forEach(callback => {
|
|
318
|
+
try { callback(...args); } catch (error) { console.error(`PlaySocket ${event} callback error:`, error); }
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Stop the server
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
#stop() {
|
|
327
|
+
if (this.#wss) {
|
|
328
|
+
this.#wss.clients.forEach(client => client.close());
|
|
329
|
+
this.#wss.close(); // TODO necessary?
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (this.#server) {
|
|
333
|
+
this.#server.close(() => {
|
|
334
|
+
console.log('PlaySocket server stopped');
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get getRooms() { return { ...this.#rooms } }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
server$1 = PlaySocketServer;
|
|
343
|
+
return server$1;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
var serverExports = requireServer();
|
|
347
|
+
var server = /*@__PURE__*/getDefaultExportFromCjs(serverExports);
|
|
348
|
+
|
|
349
|
+
module.exports = server;
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playsocketjs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
|
|
5
|
+
"module": "dist/playsocket-client.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/playsocket-client.js"
|
|
9
|
+
},
|
|
10
|
+
"./server": {
|
|
11
|
+
"require": "./dist/playsocket-server.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
19
|
+
"build": "rollup -c rollup.config.mjs"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/therealPaulPlay/PlaySocketJS.git"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"WebSocket",
|
|
27
|
+
"data channel",
|
|
28
|
+
"Socket",
|
|
29
|
+
"PlayPeerJS",
|
|
30
|
+
"multiplayer",
|
|
31
|
+
"game"
|
|
32
|
+
],
|
|
33
|
+
"author": "PaulPlay",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/therealPaulPlay/PlaySocketJS/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/therealPaulPlay/PlaySocketJS#readme",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@rollup/plugin-commonjs": "^28.0.2",
|
|
41
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
42
|
+
"rollup": "^4.31.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"ws": "^8.18.1"
|
|
46
|
+
}
|
|
47
|
+
}
|