@dotrino/lobby 0.3.0 → 0.4.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/package.json +2 -2
- package/src/index.js +1 -1
- package/src/lobby.js +38 -10
- package/src/protocol.js +42 -21
- package/src/room.js +13 -3
- package/src/transport.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/lobby",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Lobby + matchmaking reciclable para juegos del ecosistema Dotrino: salas, asientos, espectadores, motor de turnos autoritativo, reputación y contactos sobre el proxy/identity/reputation compartidos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"web-of-trust"
|
|
32
32
|
],
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@dotrino/proxy-client": ">=0.1.0",
|
|
35
34
|
"@dotrino/identity": ">=0.5.0",
|
|
35
|
+
"@dotrino/proxy-client": "^0.10.0",
|
|
36
36
|
"@dotrino/reputation": ">=0.3.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
package/src/index.js
CHANGED
|
@@ -11,5 +11,5 @@ export {
|
|
|
11
11
|
createRepGate, rankRooms, ratePlayer,
|
|
12
12
|
receiptPayload, signReceiptHalf
|
|
13
13
|
} from './reputation.js'
|
|
14
|
-
export { K, discoveryChannel,
|
|
14
|
+
export { K, discoveryChannel, discoveryChannels, roomChannel, envelope, parseEnvelope } from './protocol.js'
|
|
15
15
|
export { Emitter, mulberry32, hashSeed, shuffle, normalizeSeats, samePubkey } from './util.js'
|
package/src/lobby.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// por reputación, e invita contactos por pubkey (cola offline).
|
|
4
4
|
|
|
5
5
|
import { Emitter, normalizeSeats } from './util.js'
|
|
6
|
-
import { K, envelope, discoveryChannel } from './protocol.js'
|
|
6
|
+
import { K, envelope, discoveryChannel, discoveryChannels } from './protocol.js'
|
|
7
7
|
import { Transport } from './transport.js'
|
|
8
8
|
import { Room, STATUS } from './room.js'
|
|
9
9
|
import { createRepGate, rankRooms } from './reputation.js'
|
|
@@ -28,21 +28,44 @@ export class Lobby extends Emitter {
|
|
|
28
28
|
if (env.k === K.INFO) { if (this._infoCollector) this._infoCollector(from, env.d && env.d.summary) }
|
|
29
29
|
else if (env.k === K.INVITE) this.emit('invite', { from, ...(env.d || {}) })
|
|
30
30
|
})
|
|
31
|
-
|
|
31
|
+
// Hay un canal de descubrimiento POR NODO (ver protocol.js): el
|
|
32
|
+
// descubrimiento no tiene dueño natural, así que en vez de designar un nodo
|
|
33
|
+
// árbitro se publica en el propio y se lee de todos.
|
|
34
|
+
const esDescubrimiento = (channel) => this._discoveryChannels().includes(channel)
|
|
32
35
|
// Descubrimiento en vivo: requiere observar el canal (watch) para recibir
|
|
33
36
|
// estos eventos sin publicarnos como sala. Si el proxy no soporta watch,
|
|
34
37
|
// _watchDiscovery degrada a no-op y la app sigue con su polling de respaldo.
|
|
35
|
-
this.transport.on('channel_joined', (channel, token) => { if (channel
|
|
36
|
-
this.transport.on('channel_left', (channel, token) => { if (channel
|
|
37
|
-
this.transport.on('peer_disconnected', (token, channel) => { if (!channel || channel
|
|
38
|
+
this.transport.on('channel_joined', (channel, token) => { if (esDescubrimiento(channel) && token !== this.transport.token) this.emit('rooms-changed', { type: 'joined', token }) })
|
|
39
|
+
this.transport.on('channel_left', (channel, token) => { if (esDescubrimiento(channel)) this.emit('rooms-changed', { type: 'left', token }) })
|
|
40
|
+
this.transport.on('peer_disconnected', (token, channel) => { if (!channel || esDescubrimiento(channel)) this.emit('rooms-changed', { type: 'left', token }) })
|
|
38
41
|
this.transport.on('reconnect', () => this._watchDiscovery())
|
|
39
42
|
this._watchDiscovery()
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
/** Los nodos que conoce el proxio al que estamos conectados (él incluido). */
|
|
46
|
+
_knownNodes () {
|
|
47
|
+
const t = this.transport
|
|
48
|
+
return (t && (t.knownNodes || (t.client && t.client.knownNodes))) || []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Un canal de descubrimiento por nodo conocido. */
|
|
52
|
+
_discoveryChannels () {
|
|
53
|
+
return discoveryChannels(this.gameId, this._knownNodes())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** El canal donde ANUNCIAMOS: el del proxio al que estamos conectados. */
|
|
57
|
+
_myDiscoveryChannel () {
|
|
58
|
+
const t = this.transport
|
|
59
|
+
return discoveryChannel(this.gameId, (t && (t.node || (t.client && t.client.node))) || null)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Observar los canales de descubrimiento para recibir altas/bajas de salas en
|
|
63
|
+
// vivo (proxy ≥ 0.6.2). Fire-and-forget: no bloquea ni rompe si no está
|
|
64
|
+
// soportado. Son N canales, uno por nodo conocido — hoy dos.
|
|
44
65
|
_watchDiscovery () {
|
|
45
|
-
|
|
66
|
+
for (const ch of this._discoveryChannels()) {
|
|
67
|
+
try { this.transport.watch(ch) } catch (_) {}
|
|
68
|
+
}
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
// ── Crear / unir ────────────────────────────────────────────────
|
|
@@ -77,8 +100,13 @@ export class Lobby extends Emitter {
|
|
|
77
100
|
async listRooms (opts = {}) {
|
|
78
101
|
await this.transport.connect()
|
|
79
102
|
const timeout = opts.timeout || 1500
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
// Se pregunta en CADA nodo y se mezcla. Un nodo que no conteste no rompe la
|
|
104
|
+
// lista: se ven las salas de los demás, que es justo lo que se gana al no
|
|
105
|
+
// tener un nodo árbitro.
|
|
106
|
+
const listas = await Promise.all(this._discoveryChannels().map(
|
|
107
|
+
(ch) => this.transport.list(ch).catch(() => [])
|
|
108
|
+
))
|
|
109
|
+
const tokens = [...new Set(listas.flat())]
|
|
82
110
|
const others = tokens.filter(t => t && t !== this.transport.token)
|
|
83
111
|
const summaries = await this._gatherInfo(others, timeout)
|
|
84
112
|
if (opts.enrich === false) return summaries
|
package/src/protocol.js
CHANGED
|
@@ -49,35 +49,56 @@ export const K = {
|
|
|
49
49
|
/**
|
|
50
50
|
* NODO DUEÑO DE LOS CANALES.
|
|
51
51
|
*
|
|
52
|
-
* Un canal del proxio
|
|
52
|
+
* Un canal del proxio lleva delante el id del nodo que lo hospeda
|
|
53
53
|
* (`<id de 12>/loquesea`). Ese nodo guarda la membresía y los demás le pasan las
|
|
54
|
-
* operaciones. Sin prefijo, cada proxio tiene su propia copia del canal
|
|
55
|
-
* lo que hacía que dos personas en proxios distintos NO se vieran en la misma
|
|
56
|
-
* lista de salas, aunque los mensajes entre ellas sí cruzaran.
|
|
54
|
+
* operaciones. Sin ese prefijo, cada proxio tiene su propia copia del canal.
|
|
57
55
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
56
|
+
* Los dos canales del lobby resuelven su dueño de forma distinta, y la razón es
|
|
57
|
+
* que uno tiene dueño natural y el otro no:
|
|
58
|
+
*
|
|
59
|
+
* · LA SALA lo tiene: vive en el proxio de quien la abrió, y eso se LEE del
|
|
60
|
+
* propio `roomId` (que es la instancia del host y lleva su id delante). Nadie
|
|
61
|
+
* lo declara ni lo acuerda.
|
|
62
|
+
*
|
|
63
|
+
* · EL DESCUBRIMIENTO no: es un nombre global del ecosistema y no lo crea
|
|
64
|
+
* nadie en particular. Poner el id del primero que llegue no sirve — el
|
|
65
|
+
* segundo host, en otro proxio, crearía otro canal y volverían a ser dos
|
|
66
|
+
* listas.
|
|
67
|
+
*
|
|
68
|
+
* Para el descubrimiento se eligió NO designar un árbitro: **cada proxio tiene su
|
|
69
|
+
* lista y quien busca pregunta en todos**. Se publica en uno, se lee de varios.
|
|
70
|
+
* Así no hay punto único de fallo (si un nodo no contesta, se ven las salas de
|
|
71
|
+
* los demás) ni nadie que decida dónde vive la lista del ecosistema. El costo es
|
|
72
|
+
* una consulta por nodo, y lo paga el CLIENTE —no el servidor— así que no
|
|
73
|
+
* reaparece el fan-out en el proxio, que ya habíamos descartado.
|
|
63
74
|
*/
|
|
64
75
|
const NODE_ID_LEN = 12
|
|
76
|
+
// Comprueba la FORMA, no el alfabeto exacto, y es a propósito: el alfabeto lo
|
|
77
|
+
// decide el proxio (que excluye los caracteres confundibles) y esta librería no
|
|
78
|
+
// tiene por qué llevar una copia que se desincronice. Un id con un símbolo que
|
|
79
|
+
// el proxio no emite simplemente no va a coincidir con ningún nodo.
|
|
65
80
|
const isNodeId = (s) => new RegExp(`^[1-9A-Z]{${NODE_ID_LEN}}$`).test(String(s || ''))
|
|
66
81
|
|
|
67
|
-
// Sin valor por defecto: el id de un proxio se DERIVA de su llave, así que no
|
|
68
|
-
// hay ninguno que se pueda escribir aquí de antemano. Mientras no se fije, el
|
|
69
|
-
// canal de descubrimiento es local a cada proxio (comportamiento de siempre).
|
|
70
|
-
let LOBBY_HOME_NODE = null
|
|
71
|
-
|
|
72
|
-
/** Fija en qué proxio vive el canal de descubrimiento de salas. */
|
|
73
|
-
export function setLobbyHomeNode (nodeId) {
|
|
74
|
-
LOBBY_HOME_NODE = isNodeId(nodeId) ? nodeId : null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
82
|
const withNode = (prefix, name) => (prefix ? `${prefix}/${name}` : name)
|
|
78
83
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Canal de descubrimiento de un juego EN UN NODO concreto.
|
|
86
|
+
* Sin `nodeId` (proxio sin identidad, o dev) devuelve el nombre pelado, que el
|
|
87
|
+
* servidor trata como canal local — el comportamiento de siempre.
|
|
88
|
+
*/
|
|
89
|
+
export const discoveryChannel = (gameId, nodeId) =>
|
|
90
|
+
withNode(isNodeId(nodeId) ? nodeId : null, `cclobby/${gameId}`)
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Todos los canales de descubrimiento a consultar: el del propio proxio y el de
|
|
94
|
+
* cada nodo que conozca. Se deduplica y se deja el propio PRIMERO, porque es el
|
|
95
|
+
* que va a contestar más rápido y el que se usa para publicar.
|
|
96
|
+
*/
|
|
97
|
+
export function discoveryChannels (gameId, nodeIds = []) {
|
|
98
|
+
const ids = nodeIds.filter((n, i, a) => isNodeId(n) && a.indexOf(n) === i)
|
|
99
|
+
if (!ids.length) return [`cclobby/${gameId}`]
|
|
100
|
+
return ids.map((id) => discoveryChannel(gameId, id))
|
|
101
|
+
}
|
|
81
102
|
|
|
82
103
|
/**
|
|
83
104
|
* Canal de presencia de una sala concreta (host + guests publican aquí).
|
package/src/room.js
CHANGED
|
@@ -114,20 +114,30 @@ export class Room extends Emitter {
|
|
|
114
114
|
this._members.set(this.transport.token, { pubkey: this.myPubkey, name: this.myName, verified: true, lastSeen: clock.now() })
|
|
115
115
|
this._wire()
|
|
116
116
|
// Anunciarse: canal de descubrimiento + canal de presencia de la sala.
|
|
117
|
-
try { await this.transport.publish(
|
|
117
|
+
try { await this.transport.publish(this._myDiscoveryChannel(), { roomName: this.name, gameType: this.gameId }) } catch (_) {}
|
|
118
118
|
try { await this.transport.publish(roomChannel(this.gameId, this.roomId)) } catch (_) {}
|
|
119
119
|
this._startHeartbeat()
|
|
120
120
|
this._afterStateChange()
|
|
121
121
|
return this
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* El canal de descubrimiento donde ANUNCIAMOS esta sala: el del proxio al que
|
|
126
|
+
* estamos conectados. Se publica en uno solo (el propio) y quien busca lee de
|
|
127
|
+
* todos — así el descubrimiento no necesita un nodo árbitro. Ver protocol.js.
|
|
128
|
+
*/
|
|
129
|
+
_myDiscoveryChannel () {
|
|
130
|
+
const t = this.transport
|
|
131
|
+
return discoveryChannel(this.gameId, (t && (t.node || (t.client && t.client.node))) || null)
|
|
132
|
+
}
|
|
133
|
+
|
|
124
134
|
// Re-publica la sala periódicamente: las entradas de canal del proxy expiran
|
|
125
135
|
// (~20 min), así que sin esto una sala desaparecería del descubrimiento en
|
|
126
136
|
// partidas largas. Reemplaza el _republish casero que hacían los bots.
|
|
127
137
|
_startHeartbeat () {
|
|
128
138
|
if (this._heartbeat) clearInterval(this._heartbeat)
|
|
129
139
|
this._heartbeat = setInterval(() => {
|
|
130
|
-
this.transport.publish(
|
|
140
|
+
this.transport.publish(this._myDiscoveryChannel(), { roomName: this.name, gameType: this.gameId }).catch(() => {})
|
|
131
141
|
this.transport.publish(roomChannel(this.gameId, this.roomId)).catch(() => {})
|
|
132
142
|
}, 10 * 60 * 1000)
|
|
133
143
|
if (this._heartbeat.unref) this._heartbeat.unref()
|
|
@@ -792,7 +802,7 @@ export class Room extends Emitter {
|
|
|
792
802
|
}, 20000)
|
|
793
803
|
if (this._rekeyTimer.unref) this._rekeyTimer.unref()
|
|
794
804
|
}
|
|
795
|
-
this.transport.publish(
|
|
805
|
+
this.transport.publish(this._myDiscoveryChannel(), { roomName: this.name, gameType: this.gameId }).catch(() => {})
|
|
796
806
|
this.transport.publish(roomChannel(this.gameId, this.roomId)).catch(() => {})
|
|
797
807
|
this._afterStateChange() // re-difundir estado a los miembros vivos
|
|
798
808
|
} else {
|
package/src/transport.js
CHANGED
|
@@ -46,6 +46,10 @@ export class Transport extends Emitter {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
get token () { return this.proxy ? this.proxy.token : null }
|
|
49
|
+
/** Id del proxio al que estamos conectados (12 chars, derivado de su llave). */
|
|
50
|
+
get node () { return this.proxy ? (this.proxy.node || null) : null }
|
|
51
|
+
/** Ese proxio + los que conoce. Es a quiénes se les pregunta por salas. */
|
|
52
|
+
get knownNodes () { return (this.proxy && this.proxy.knownNodes) || [] }
|
|
49
53
|
get isReady () { return this._ready && !!this.token }
|
|
50
54
|
|
|
51
55
|
/** Conecta (si hace falta) e identifica con el vault. Idempotente. */
|