angelia.io 5.0.1 → 5.0.2
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/package.json +3 -2
- package/readme.md +75 -2
- package/server/Emitter.js +50 -13
- package/server/Message.js +27 -0
- package/server/Room.js +24 -4
- package/server/Rooms.js +21 -10
- package/server/Server.js +33 -17
- package/server/Socket.js +12 -4
- package/server/constants.js +3 -0
- package/server/index.js +3 -0
- package/types/server/Emitter.d.ts +20 -11
- package/types/server/Message.d.ts +22 -0
- package/types/server/Room.d.ts +12 -5
- package/types/server/Rooms.d.ts +8 -0
- package/types/server/Server.d.ts +7 -0
- package/types/server/Socket.d.ts +9 -1
- package/types/server/constants.d.ts +2 -0
- package/types/server/index.d.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "angelia.io",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"description": "WebSockets Server and Client API for node.js and the browser, with rooms support.",
|
|
5
5
|
"author": "@titoBouzout @boredofnames",
|
|
6
6
|
"license": "GPL",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"demo": "node tests/demo/server.js",
|
|
44
44
|
"bench:worker": "node bench/browser/run.js",
|
|
45
45
|
"bench:server": "node bench/node-receive.js",
|
|
46
|
-
"bench:fanout": "node bench/fanout.js"
|
|
46
|
+
"bench:fanout": "node bench/fanout.js",
|
|
47
|
+
"bench:message": "node bench/message.js"
|
|
47
48
|
},
|
|
48
49
|
"homepage": "https://github.com/titoBouzout/angelia.io",
|
|
49
50
|
"repository": {
|
package/readme.md
CHANGED
|
@@ -129,7 +129,10 @@ them with the exported class: `import { Server } from
|
|
|
129
129
|
| `on(Class)` | Function | attaches all methods of a `Class` as listeners |
|
|
130
130
|
| `emit(key, [value])` | Function | emits to all connected sockets |
|
|
131
131
|
| `broadcast(sender, key, [value])` | Function | emits to all connected sockets except sender |
|
|
132
|
+
| `send(message)` | Function | sends a `Message` to all connected sockets |
|
|
133
|
+
| `sendExcept(sender, message)` | Function | sends a `Message` to all connected sockets except sender |
|
|
132
134
|
| `sockets` | Set | every currently connected socket |
|
|
135
|
+
| `SocketClass` | Class | class of the sockets it creates, `Socket` unless set before `listen` |
|
|
133
136
|
| `reloadCerts()` | Function | re-read `cert`/`privKey` from disk and swap them in. Open connections are untouched. Safe to call manually (e.g. on `SIGHUP`); the watcher uses it internally |
|
|
134
137
|
| `stop()` | Function | stops listening and drops every connection; `await using server = new Server()` calls it when the scope ends |
|
|
135
138
|
|
|
@@ -156,8 +159,53 @@ The `socket` object is given to you by a listener
|
|
|
156
159
|
| `rooms` | Set | the rooms this socket is in |
|
|
157
160
|
| `request(key, [value])` | Function | emits and returns a promise for the client's reply, rejected with an Error whose message starts with `disconnected:` if the socket closes first; without a value the listener gets `null` |
|
|
158
161
|
| `emit(key, [value, callback])` | Function | emits to client; with a callback, returns a promise for its result, rejected with an Error whose message starts with `disconnected:` if the socket closes first |
|
|
162
|
+
| `send(message)` | Function | sends a `Message` to the client |
|
|
159
163
|
| `disconnect([noReconnect, reason])` | Function | closes the WebSocket, pass `true` to prevent re-connections; `reason` (up to 123 bytes) is sent to the client |
|
|
160
164
|
|
|
165
|
+
### `Message`
|
|
166
|
+
|
|
167
|
+
`emit` encodes once per fan-out, in the tick it sends. A `Message` is
|
|
168
|
+
encoded once as it is made, and keeps its encoding for as long as you keep
|
|
169
|
+
it, for a payload sent to sockets one at a time or again later, as a list
|
|
170
|
+
sent to whoever arrives.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { Message } from 'angelia.io/server'
|
|
174
|
+
|
|
175
|
+
const rooms = new Message('rooms', list)
|
|
176
|
+
|
|
177
|
+
socket.send(rooms)
|
|
178
|
+
room.send(rooms)
|
|
179
|
+
room.sendExcept(socket, rooms)
|
|
180
|
+
|
|
181
|
+
// equal text means an equal message
|
|
182
|
+
if (rooms.json !== last.json) Server.send(rooms)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Later changes to the value never reach the wire, and `toJSON` runs as it
|
|
186
|
+
is made rather than as it sends. A message carries no reply callback, as
|
|
187
|
+
one belongs to a single socket: use `socket.emit` or `socket.request`.
|
|
188
|
+
|
|
189
|
+
### Your own `Socket`
|
|
190
|
+
|
|
191
|
+
Set `Server.SocketClass` before `listen` and each connection is your subclass, so what belongs to a user lives on the socket itself. `toJSON` says what sending a socket sends; by default it sends nothing of it.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
import Server, { Socket } from 'angelia.io/server'
|
|
195
|
+
|
|
196
|
+
class User extends Socket {
|
|
197
|
+
username = ''
|
|
198
|
+
toJSON() {
|
|
199
|
+
return { username: this.username }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Server.SocketClass = User
|
|
204
|
+
await Server.listen()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Listeners, hooks and every `sockets` then give you `User`s. Its constructor gets what `Socket`'s does, so pass them on with `super(...args)` if it has one.
|
|
208
|
+
|
|
161
209
|
### Server Listeners
|
|
162
210
|
|
|
163
211
|
Listen to an event by creating a class with any name, and give to
|
|
@@ -236,7 +284,7 @@ the right one by name.
|
|
|
236
284
|
| `timeout(socket, delay)` | when we are about to disconnect a timedout socket, gives the delay in milliseconds |
|
|
237
285
|
| `garbage(socket, data)` | when the client sends a message with no listener, a malformed one, or one named after a predefined event |
|
|
238
286
|
| `incoming(socket, messages)` | for debugging: ref to array of incoming messages received before dispatching to listeners |
|
|
239
|
-
| `outgoing(socket, messages)` | for debugging: ref to array of outgoing messages before sending to socket
|
|
287
|
+
| `outgoing(socket, messages)` | for debugging: ref to array of outgoing messages before sending to socket; changing a `Message`'s tuple here never changes what is sent |
|
|
240
288
|
|
|
241
289
|
## Client API (Browser)
|
|
242
290
|
|
|
@@ -409,10 +457,13 @@ await Server.listen()
|
|
|
409
457
|
| `get(id)` | method | gets room with such an `id` |
|
|
410
458
|
| `join(socket, [id])` | method | adds socket to room `id` |
|
|
411
459
|
| `leave(socket, [id])` | method | removes socket from room `id` |
|
|
460
|
+
| `of(socket)` | method | iterates the rooms of this list `socket` is in, walking the socket's rooms, not the list's |
|
|
412
461
|
| `sockets` | Set | sockets across every room in this list, each once |
|
|
413
462
|
| `rooms` | Map | every room in this list, by id |
|
|
414
463
|
| `emit(key, [value])` | method | emits to all sockets in this room list |
|
|
415
464
|
| `broadcast(socket, key, [value])` | method | emits to other sockets in this room list |
|
|
465
|
+
| `send(message)` | method | sends a `Message` to all sockets in this room list |
|
|
466
|
+
| `sendExcept(socket, message)` | method | sends a `Message` to other sockets in this room list |
|
|
416
467
|
|
|
417
468
|
### Via a `Room`
|
|
418
469
|
|
|
@@ -462,9 +513,31 @@ await Server.listen()
|
|
|
462
513
|
| `sockets` | Set | sockets in the room |
|
|
463
514
|
| `emit(key, [value])` | method | emits to all sockets in the room |
|
|
464
515
|
| `broadcast(socket, key, [value])` | method | emits to other sockets in the room |
|
|
516
|
+
| `send(message)` | method | sends a `Message` to all sockets in the room |
|
|
517
|
+
| `sendExcept(socket, message)` | method | sends a `Message` to other sockets in the room |
|
|
465
518
|
| `id` | any | the room id |
|
|
466
519
|
| `join(socket)` | method | adds a socket to the room |
|
|
467
|
-
| `leave(socket)` | method | removes a socket from the room
|
|
520
|
+
| `leave(socket)` | method | removes a socket from the room; in a `Rooms` list, an emptied room is deleted unless `persistent` |
|
|
521
|
+
|
|
522
|
+
### Your own `Emitter`
|
|
523
|
+
|
|
524
|
+
`Server`, `Room` and `Rooms` send through `Emitter`, which only reads a
|
|
525
|
+
`sockets` field. Extend it for something that sends to sockets it doesn't
|
|
526
|
+
own, as what a room is doing right now:
|
|
527
|
+
|
|
528
|
+
```js
|
|
529
|
+
import { Emitter } from 'angelia.io/server'
|
|
530
|
+
|
|
531
|
+
class Match extends Emitter {
|
|
532
|
+
constructor(room) {
|
|
533
|
+
super()
|
|
534
|
+
this.room = room
|
|
535
|
+
}
|
|
536
|
+
get sockets() {
|
|
537
|
+
return this.room.sockets
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
```
|
|
468
541
|
|
|
469
542
|
### `sockets` in `Server`, `Room` and `Rooms`
|
|
470
543
|
|
package/server/Emitter.js
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
/** @import { ClientEvents, EventKey, EventValue } from './index.js' */
|
|
2
|
+
/** @import { Message } from './Message.js' */
|
|
2
3
|
/** @import { Socket } from './Socket.js' */
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {{
|
|
6
7
|
* sockets: Iterable<Socket>
|
|
7
|
-
* broadcast(me: Socket | undefined, k: any, v?: any): void
|
|
8
8
|
* }} HasSockets
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @param {Iterable<Socket>} sockets
|
|
13
|
+
* @param {any[]} tuple
|
|
14
|
+
*/
|
|
15
|
+
function queueAll(sockets, tuple) {
|
|
16
|
+
for (const socket of sockets) {
|
|
17
|
+
socket.queue(tuple)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Iterable<Socket>} sockets
|
|
23
|
+
* @param {Socket | undefined} me
|
|
24
|
+
* @param {any[]} tuple
|
|
25
|
+
*/
|
|
26
|
+
function queueExcept(sockets, me, tuple) {
|
|
27
|
+
for (const socket of sockets) {
|
|
28
|
+
if (me !== socket) {
|
|
29
|
+
socket.queue(tuple)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
/**
|
|
12
35
|
* Common emit / broadcast logic. Subclasses own the `sockets` field
|
|
13
|
-
* (a Set on Server, Room and Rooms). Each fan-out
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* all sockets share a single encoding.
|
|
36
|
+
* (a Set on Server, Room and Rooms). Each fan-out queues **one** wire
|
|
37
|
+
* tuple on every recipient, and `Server.cacheMessages` keeps the encoded
|
|
38
|
+
* form on it, so all sockets share a single encoding.
|
|
17
39
|
*/
|
|
18
40
|
export class Emitter {
|
|
19
41
|
/**
|
|
@@ -25,7 +47,7 @@ export class Emitter {
|
|
|
25
47
|
* @param {EventValue<ClientEvents, K>} v
|
|
26
48
|
*/
|
|
27
49
|
emit(k, v) {
|
|
28
|
-
this.
|
|
50
|
+
queueAll(this.sockets, [k, v])
|
|
29
51
|
}
|
|
30
52
|
|
|
31
53
|
/**
|
|
@@ -38,12 +60,27 @@ export class Emitter {
|
|
|
38
60
|
* @param {EventValue<ClientEvents, K>} v
|
|
39
61
|
*/
|
|
40
62
|
broadcast(me, k, v) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
queueExcept(this.sockets, me, [k, v])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Send an encoded message to every socket in scope.
|
|
68
|
+
*
|
|
69
|
+
* @this {HasSockets}
|
|
70
|
+
* @param {Message<any>} message
|
|
71
|
+
*/
|
|
72
|
+
send(message) {
|
|
73
|
+
queueAll(this.sockets, message.tuple)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Send an encoded message to every socket in scope except `me`.
|
|
78
|
+
*
|
|
79
|
+
* @this {HasSockets}
|
|
80
|
+
* @param {Socket | undefined} me
|
|
81
|
+
* @param {Message<any>} message
|
|
82
|
+
*/
|
|
83
|
+
sendExcept(me, message) {
|
|
84
|
+
queueExcept(this.sockets, me, message.tuple)
|
|
48
85
|
}
|
|
49
86
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { encoded } from './constants.js'
|
|
2
|
+
import { stringify } from './utils.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ClientEvents, EventKey, EventValue } from './index.js' */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A message encoded once, here, to send to any socket at any time with
|
|
8
|
+
* `send` / `sendExcept`. Later changes to `v` don't reach the wire.
|
|
9
|
+
*
|
|
10
|
+
* @template {EventKey<ClientEvents>} K
|
|
11
|
+
*/
|
|
12
|
+
export class Message {
|
|
13
|
+
/**
|
|
14
|
+
* @param {K} k
|
|
15
|
+
* @param {EventValue<ClientEvents, K>} v
|
|
16
|
+
*/
|
|
17
|
+
constructor(k, v) {
|
|
18
|
+
/**
|
|
19
|
+
* @internal The wire tuple every send queues.
|
|
20
|
+
* @type {any[]}
|
|
21
|
+
*/
|
|
22
|
+
this.tuple = [k, v]
|
|
23
|
+
/** The encoded text; equal text means an equal message. */
|
|
24
|
+
this.json = stringify(this.tuple)
|
|
25
|
+
this.tuple[encoded] = this.json
|
|
26
|
+
}
|
|
27
|
+
}
|
package/server/Room.js
CHANGED
|
@@ -7,14 +7,20 @@ import { parent } from './constants.js'
|
|
|
7
7
|
/** A group of sockets to emit to; subclass it to hook its lifecycle. */
|
|
8
8
|
export class Room extends Emitter {
|
|
9
9
|
/** @type {Set<Socket>} */
|
|
10
|
-
sockets = new Set()
|
|
10
|
+
#sockets = new Set()
|
|
11
11
|
/** @type {any} Set by `Rooms` for its rooms; yours to set otherwise. */
|
|
12
12
|
id
|
|
13
|
-
/** Keep the room in its `Rooms` list when its last socket leaves. */
|
|
14
|
-
persistent = false
|
|
15
13
|
/** @type {Rooms<any> | undefined} The list the room belongs to, if any. */
|
|
16
14
|
;[parent]
|
|
17
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Off the room's own fields, as a subclass may spread its fields into
|
|
18
|
+
* what it sends.
|
|
19
|
+
*/
|
|
20
|
+
get sockets() {
|
|
21
|
+
return this.#sockets
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* Called when a `Rooms` list creates the room for `socket`.
|
|
20
26
|
*
|
|
@@ -41,16 +47,30 @@ export class Room extends Emitter {
|
|
|
41
47
|
this[parent]?.sockets.add(socket)
|
|
42
48
|
this.onJoin(socket)
|
|
43
49
|
}
|
|
44
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Leave the room; a room in a `Rooms` list that this empties is deleted
|
|
52
|
+
* unless it's `persistent`.
|
|
53
|
+
*
|
|
54
|
+
* @param {Socket} socket
|
|
55
|
+
*/
|
|
45
56
|
leave(socket) {
|
|
46
57
|
if (!this.sockets.delete(socket)) return
|
|
47
58
|
socket.rooms.delete(this)
|
|
48
59
|
const list = this[parent]
|
|
49
60
|
if (list && !inList(socket, list)) list.sockets.delete(socket)
|
|
50
61
|
this.onLeave(socket)
|
|
62
|
+
if (list && !this.persistent && this.sockets.size === 0) {
|
|
63
|
+
list.rooms.delete(this.id)
|
|
64
|
+
this.onDelete(socket)
|
|
65
|
+
// Joins through a kept reference no longer reach this list.
|
|
66
|
+
this[parent] = undefined
|
|
67
|
+
}
|
|
51
68
|
}
|
|
52
69
|
}
|
|
53
70
|
|
|
71
|
+
/** Keep the room in its `Rooms` list when its last socket leaves. */
|
|
72
|
+
Room.prototype.persistent = false
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* Whether `socket` is still in another room of `list`. Walks the
|
|
56
76
|
* socket's own rooms, which stay few, not the list's members.
|
package/server/Rooms.js
CHANGED
|
@@ -51,7 +51,14 @@ export class Rooms extends Emitter {
|
|
|
51
51
|
room.id = id
|
|
52
52
|
this.rooms.set(id, room)
|
|
53
53
|
|
|
54
|
-
room.
|
|
54
|
+
// In the list while it runs, for it to find itself; a failed one is no room.
|
|
55
|
+
try {
|
|
56
|
+
room.onCreate(socket)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.rooms.delete(id)
|
|
59
|
+
room[parent] = undefined
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
63
|
room.join(socket)
|
|
57
64
|
}
|
|
@@ -64,15 +71,19 @@ export class Rooms extends Emitter {
|
|
|
64
71
|
* @param {any} [id]
|
|
65
72
|
*/
|
|
66
73
|
leave(socket, id) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
this.rooms.get(id)?.leave(socket)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The rooms of this list `socket` is in. Walks the socket's own rooms,
|
|
79
|
+
* which stay few, not the list's. Leaving one while iterating is fine.
|
|
80
|
+
*
|
|
81
|
+
* @param {Socket} socket
|
|
82
|
+
* @returns {Generator<R>}
|
|
83
|
+
*/
|
|
84
|
+
*of(socket) {
|
|
85
|
+
for (const room of socket.rooms) {
|
|
86
|
+
if (room[parent] === this) yield /** @type {R} */ (room)
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
|
package/server/Server.js
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
DEFAULT_PORT,
|
|
15
15
|
LIFECYCLE_EVENTS,
|
|
16
16
|
MAX_PORT,
|
|
17
|
+
encoded,
|
|
18
|
+
framed,
|
|
17
19
|
} from './constants.js'
|
|
18
20
|
import {
|
|
19
21
|
DEFAULT_PATH,
|
|
@@ -156,6 +158,13 @@ export class Server extends Emitter {
|
|
|
156
158
|
* @type {Socket[]}
|
|
157
159
|
*/
|
|
158
160
|
drained = []
|
|
161
|
+
/**
|
|
162
|
+
* Class of the sockets it creates; set it before `listen` to have each
|
|
163
|
+
* connection be a subclass of `Socket`.
|
|
164
|
+
*
|
|
165
|
+
* @type {typeof Socket}
|
|
166
|
+
*/
|
|
167
|
+
SocketClass = Socket
|
|
159
168
|
/** @internal */
|
|
160
169
|
cacheSymbol = Symbol('cache')
|
|
161
170
|
/** @internal */
|
|
@@ -479,7 +488,7 @@ export class Server extends Emitter {
|
|
|
479
488
|
#onconnection = (io, request) => {
|
|
480
489
|
// URL-packed messages come off the params before `connect` sees them.
|
|
481
490
|
const [query, m] = params(request.url, PACKED_PARAM)
|
|
482
|
-
const socket = new
|
|
491
|
+
const socket = new this.SocketClass(io, request, this, query)
|
|
483
492
|
|
|
484
493
|
socket.listen()
|
|
485
494
|
|
|
@@ -539,33 +548,40 @@ export class Server extends Emitter {
|
|
|
539
548
|
}
|
|
540
549
|
|
|
541
550
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* bumped in `Socket.write`.
|
|
551
|
+
* The frame of `messages`. Each message stringifies once however many
|
|
552
|
+
* batches it lands in, and a batch frames once per tick. Byte counters
|
|
553
|
+
* are bumped in `Socket.write`.
|
|
545
554
|
*
|
|
546
555
|
* @internal Called from Socket.processQueue.
|
|
547
556
|
* @param {any[]} messages
|
|
548
557
|
* @returns {Buffer[]}
|
|
549
558
|
*/
|
|
550
559
|
cacheMessages(messages) {
|
|
551
|
-
// A fan-out is usually one message:
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
560
|
+
// A fan-out is usually one message: its frame lives on it, for as long as it does.
|
|
561
|
+
if (messages.length === 1) {
|
|
562
|
+
const m = messages[0]
|
|
563
|
+
if (m[framed]) {
|
|
564
|
+
this.messagesSentCacheHit++
|
|
565
|
+
} else {
|
|
566
|
+
m[framed] = frame(`[${m[encoded] ??= stringify(m)}]`)
|
|
567
|
+
}
|
|
568
|
+
return m[framed]
|
|
569
|
+
}
|
|
570
|
+
let key = ''
|
|
571
|
+
for (const m of messages) {
|
|
572
|
+
if (!m[this.cacheSymbol]) {
|
|
573
|
+
m[this.cacheSymbol] = this.cached++
|
|
559
574
|
}
|
|
575
|
+
key += m[this.cacheSymbol] + ','
|
|
560
576
|
}
|
|
561
|
-
let
|
|
562
|
-
if (
|
|
577
|
+
let batch = this.cache.get(key)
|
|
578
|
+
if (batch) {
|
|
563
579
|
this.messagesSentCacheHit++
|
|
564
580
|
} else {
|
|
565
|
-
|
|
566
|
-
this.cache.set(key,
|
|
581
|
+
batch = frame(`[${messages.map(m => (m[encoded] ??= stringify(m))).join()}]`)
|
|
582
|
+
this.cache.set(key, batch)
|
|
567
583
|
}
|
|
568
|
-
return
|
|
584
|
+
return batch
|
|
569
585
|
}
|
|
570
586
|
|
|
571
587
|
#updateNow = () => {
|
package/server/Socket.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parent } from './constants.js'
|
|
2
1
|
import {
|
|
3
2
|
NO_RECONNECT,
|
|
4
3
|
PROTOCOL_KEY,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
import { WebSocket } from 'ws'
|
|
17
16
|
|
|
18
17
|
/** @import { ClientEvents, ClientReplies, EventKey, EventValue } from './index.js' */
|
|
18
|
+
/** @import { Message } from './Message.js' */
|
|
19
19
|
/** @import { Room } from './Room.js' */
|
|
20
20
|
/** @import { Server } from './Server.js' */
|
|
21
21
|
|
|
@@ -120,6 +120,15 @@ export class Socket {
|
|
|
120
120
|
this.queue([k, v])
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Queue an encoded message for the next batch.
|
|
125
|
+
*
|
|
126
|
+
* @param {Message<any>} message
|
|
127
|
+
*/
|
|
128
|
+
send(message) {
|
|
129
|
+
this.queue(message.tuple)
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
/**
|
|
124
133
|
* @internal Queue a wire tuple; fan-outs pass one tuple to every
|
|
125
134
|
* recipient so `Server.cacheMessages` encodes it once.
|
|
@@ -224,9 +233,7 @@ export class Socket {
|
|
|
224
233
|
|
|
225
234
|
// Leaving deletes the room from this.rooms; Set iteration allows that.
|
|
226
235
|
for (const room of this.rooms) {
|
|
227
|
-
room
|
|
228
|
-
? room[parent].leave(this, room.id)
|
|
229
|
-
: room.leave(this)
|
|
236
|
+
room.leave(this)
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
this.server.hooks.disconnect?.(this, code, reason.toString())
|
|
@@ -376,6 +383,7 @@ export class Socket {
|
|
|
376
383
|
this.io._sender.sendFrame(chunks)
|
|
377
384
|
}
|
|
378
385
|
|
|
386
|
+
/** @returns {any} What sending a socket sends; a subclass says what. */
|
|
379
387
|
toJSON() {
|
|
380
388
|
return '[content of socket object omitted for toJSON]'
|
|
381
389
|
}
|
package/server/constants.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export const parent = Symbol('parent')
|
|
2
|
+
// A wire tuple's JSON, and its frame as a batch of one.
|
|
3
|
+
export const encoded = Symbol('encoded')
|
|
4
|
+
export const framed = Symbol('framed')
|
|
2
5
|
|
|
3
6
|
// Server events fired by the library itself; clients can't trigger them.
|
|
4
7
|
export const LIFECYCLE_EVENTS = new Set([
|
package/server/index.js
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
/** @import { ClientEvents, EventKey, EventValue } from './index.js' */
|
|
2
|
+
/** @import { Message } from './Message.js' */
|
|
2
3
|
/** @import { Socket } from './Socket.js' */
|
|
3
4
|
import type { ClientEvents, EventKey, EventValue } from './index.js';
|
|
5
|
+
import type { Message } from './Message.js';
|
|
4
6
|
import type { Socket } from './Socket.js';
|
|
5
7
|
export type HasSockets = {
|
|
6
8
|
sockets: Iterable<Socket>;
|
|
7
|
-
broadcast(me: Socket | undefined, k: any, v?: any): void;
|
|
8
9
|
};
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {{
|
|
11
|
-
* sockets: Iterable<Socket>
|
|
12
|
-
* broadcast(me: Socket | undefined, k: any, v?: any): void
|
|
13
|
-
* }} HasSockets
|
|
14
|
-
*/
|
|
15
10
|
/**
|
|
16
11
|
* Common emit / broadcast logic. Subclasses own the `sockets` field
|
|
17
|
-
* (a Set on Server, Room and Rooms). Each fan-out
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* all sockets share a single encoding.
|
|
12
|
+
* (a Set on Server, Room and Rooms). Each fan-out queues **one** wire
|
|
13
|
+
* tuple on every recipient, and `Server.cacheMessages` keeps the encoded
|
|
14
|
+
* form on it, so all sockets share a single encoding.
|
|
21
15
|
*/
|
|
22
16
|
export declare class Emitter {
|
|
23
17
|
/**
|
|
@@ -39,4 +33,19 @@ export declare class Emitter {
|
|
|
39
33
|
* @param {EventValue<ClientEvents, K>} v
|
|
40
34
|
*/
|
|
41
35
|
broadcast<K extends EventKey<ClientEvents>>(this: HasSockets, me: Socket | undefined, k: K, v: EventValue<ClientEvents, K>): void;
|
|
36
|
+
/**
|
|
37
|
+
* Send an encoded message to every socket in scope.
|
|
38
|
+
*
|
|
39
|
+
* @this {HasSockets}
|
|
40
|
+
* @param {Message<any>} message
|
|
41
|
+
*/
|
|
42
|
+
send(this: HasSockets, message: Message<any>): void;
|
|
43
|
+
/**
|
|
44
|
+
* Send an encoded message to every socket in scope except `me`.
|
|
45
|
+
*
|
|
46
|
+
* @this {HasSockets}
|
|
47
|
+
* @param {Socket | undefined} me
|
|
48
|
+
* @param {Message<any>} message
|
|
49
|
+
*/
|
|
50
|
+
sendExcept(this: HasSockets, me: Socket | undefined, message: Message<any>): void;
|
|
42
51
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ClientEvents, EventKey, EventValue } from './index.js';
|
|
2
|
+
/** @import { ClientEvents, EventKey, EventValue } from './index.js' */
|
|
3
|
+
/**
|
|
4
|
+
* A message encoded once, here, to send to any socket at any time with
|
|
5
|
+
* `send` / `sendExcept`. Later changes to `v` don't reach the wire.
|
|
6
|
+
*
|
|
7
|
+
* @template {EventKey<ClientEvents>} K
|
|
8
|
+
*/
|
|
9
|
+
export declare class Message<K extends EventKey<ClientEvents>> {
|
|
10
|
+
/**
|
|
11
|
+
* @internal The wire tuple every send queues.
|
|
12
|
+
* @type {any[]}
|
|
13
|
+
*/
|
|
14
|
+
tuple: any[];
|
|
15
|
+
/** The encoded text; equal text means an equal message. */
|
|
16
|
+
json: string;
|
|
17
|
+
/**
|
|
18
|
+
* @param {K} k
|
|
19
|
+
* @param {EventValue<ClientEvents, K>} v
|
|
20
|
+
*/
|
|
21
|
+
constructor(k: K, v: EventValue<ClientEvents, K>);
|
|
22
|
+
}
|
package/types/server/Room.d.ts
CHANGED
|
@@ -5,13 +5,15 @@ import type { Socket } from './Socket.js';
|
|
|
5
5
|
/** @import { Rooms } from './Rooms.js' */
|
|
6
6
|
/** A group of sockets to emit to; subclass it to hook its lifecycle. */
|
|
7
7
|
export declare class Room extends Emitter {
|
|
8
|
-
|
|
9
|
-
sockets: Set<Socket>;
|
|
8
|
+
#private;
|
|
10
9
|
/** @type {any} Set by `Rooms` for its rooms; yours to set otherwise. */
|
|
11
10
|
id: any;
|
|
12
|
-
/** Keep the room in its `Rooms` list when its last socket leaves. */
|
|
13
|
-
persistent: boolean;
|
|
14
11
|
[parent]: any;
|
|
12
|
+
/**
|
|
13
|
+
* Off the room's own fields, as a subclass may spread its fields into
|
|
14
|
+
* what it sends.
|
|
15
|
+
*/
|
|
16
|
+
get sockets(): Set<Socket>;
|
|
15
17
|
/**
|
|
16
18
|
* Called when a `Rooms` list creates the room for `socket`.
|
|
17
19
|
*
|
|
@@ -30,6 +32,11 @@ export declare class Room extends Emitter {
|
|
|
30
32
|
onLeave(socket: Socket): void;
|
|
31
33
|
/** @param {Socket} socket */
|
|
32
34
|
join(socket: Socket): void;
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Leave the room; a room in a `Rooms` list that this empties is deleted
|
|
37
|
+
* unless it's `persistent`.
|
|
38
|
+
*
|
|
39
|
+
* @param {Socket} socket
|
|
40
|
+
*/
|
|
34
41
|
leave(socket: Socket): void;
|
|
35
42
|
}
|
package/types/server/Rooms.d.ts
CHANGED
|
@@ -42,6 +42,14 @@ export declare class Rooms<R extends Room = Room> extends Emitter {
|
|
|
42
42
|
* @param {any} [id]
|
|
43
43
|
*/
|
|
44
44
|
leave(socket: Socket, id?: any): void;
|
|
45
|
+
/**
|
|
46
|
+
* The rooms of this list `socket` is in. Walks the socket's own rooms,
|
|
47
|
+
* which stay few, not the list's. Leaving one while iterating is fine.
|
|
48
|
+
*
|
|
49
|
+
* @param {Socket} socket
|
|
50
|
+
* @returns {Generator<R>}
|
|
51
|
+
*/
|
|
52
|
+
of(socket: Socket): Generator<R>;
|
|
45
53
|
/** @returns {R[]} */
|
|
46
54
|
toJSON(): R[];
|
|
47
55
|
}
|
package/types/server/Server.d.ts
CHANGED
|
@@ -115,6 +115,13 @@ export declare class Server extends Emitter {
|
|
|
115
115
|
io: WebSocketServer | null;
|
|
116
116
|
/** @type {import('node:http').Server | null} */
|
|
117
117
|
httpServer: import('node:http').Server | null;
|
|
118
|
+
/**
|
|
119
|
+
* Class of the sockets it creates; set it before `listen` to have each
|
|
120
|
+
* connection be a subclass of `Socket`.
|
|
121
|
+
*
|
|
122
|
+
* @type {typeof Socket}
|
|
123
|
+
*/
|
|
124
|
+
SocketClass: typeof Socket;
|
|
118
125
|
constructor();
|
|
119
126
|
/**
|
|
120
127
|
* Start listening for WebSocket connections. Serves `wss://` when
|
package/types/server/Socket.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { inspect } from './utils.js';
|
|
2
2
|
import { WebSocket } from 'ws';
|
|
3
3
|
import type { ClientEvents, ClientReplies, EventKey, EventValue } from './index.js';
|
|
4
|
+
import type { Message } from './Message.js';
|
|
4
5
|
import type { Room } from './Room.js';
|
|
5
6
|
import type { Server } from './Server.js';
|
|
6
7
|
/** One client connection, handed to every event handler and hook. */
|
|
@@ -78,6 +79,12 @@ export declare class Socket {
|
|
|
78
79
|
* @returns {Promise<any> | void}
|
|
79
80
|
*/
|
|
80
81
|
emit<K extends EventKey<ClientEvents>, R>(k: K, v: EventValue<ClientEvents, K>, callback: (reply: EventValue<ClientReplies, K>) => R): Promise<R>;
|
|
82
|
+
/**
|
|
83
|
+
* Queue an encoded message for the next batch.
|
|
84
|
+
*
|
|
85
|
+
* @param {Message<any>} message
|
|
86
|
+
*/
|
|
87
|
+
send(message: Message<any>): void;
|
|
81
88
|
/**
|
|
82
89
|
* Emit and resolve with the client's reply; rejected with
|
|
83
90
|
* a `disconnected:` Error if the socket closes first.
|
|
@@ -131,7 +138,8 @@ export declare class Socket {
|
|
|
131
138
|
* 123 bytes; longer throws a `RangeError` before anything closes.
|
|
132
139
|
*/
|
|
133
140
|
disconnect(noReconnect?: boolean, reason?: string): void;
|
|
134
|
-
|
|
141
|
+
/** @returns {any} What sending a socket sends; a subclass says what. */
|
|
142
|
+
toJSON(): any;
|
|
135
143
|
/**
|
|
136
144
|
* `console.log(socket)` shows who it is and how it's doing, not internals.
|
|
137
145
|
*
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const parent: unique symbol;
|
|
2
|
+
export declare const encoded: unique symbol;
|
|
3
|
+
export declare const framed: unique symbol;
|
|
2
4
|
export declare const LIFECYCLE_EVENTS: Set<string>;
|
|
3
5
|
export declare const DEFAULT_PORT = 3001;
|
|
4
6
|
export declare const DEFAULT_MAX_MESSAGE_SIZE_MB = 5;
|
package/types/server/index.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
export { default, Server } from './Server.js'
|
|
2
|
+
export { Emitter } from './Emitter.js'
|
|
3
|
+
export { Message } from './Message.js'
|
|
2
4
|
export { Room } from './Room.js'
|
|
3
5
|
export { Rooms } from './Rooms.js'
|
|
4
|
-
export
|
|
6
|
+
export { Socket } from './Socket.js'
|
|
7
|
+
|
|
8
|
+
declare module './Room.js' {
|
|
9
|
+
interface Room {
|
|
10
|
+
/** Keep the room in its `Rooms` list when its last socket leaves. */
|
|
11
|
+
persistent: boolean
|
|
12
|
+
}
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
16
|
* Event-map interfaces. Augment these from your app to get end-to-end
|