Skip to content

Commit e6626fa

Browse files
Add latency to lobby listings
`latency` contains the average latency to all the peers in the lobby. This can be useful to filter on or sort by to find lobbies weer peers close to you.
1 parent e51c4da commit e6626fa

File tree

19 files changed

+587
-68
lines changed

19 files changed

+587
-68
lines changed

example/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ n.on('ready', () => {
8080
lobbies.forEach(lobby => {
8181
const li = document.createElement('li')
8282
li.id = lobby.code
83-
li.innerHTML = `<a href="javascript:void(0)" class="code">${lobby.code}</a> - <span class="map_name">${lobby.customData?.map as string ?? 'unknown map'}</span> - <span class="players">${lobby.playerCount}</span> players`
83+
li.innerHTML = `<a href="javascript:void(0)" class="code">${lobby.code}</a> - <span class="map_name">${lobby.customData?.map as string ?? 'unknown map'}</span> - <span class="players">${lobby.playerCount}</span> players (${lobby.latency ?? '<unknown>'}ms)`
8484
el.appendChild(li)
8585
if (n.currentLobby === undefined) {
8686
li.querySelector('a.code')?.addEventListener('click', () => {

features/latency.feature

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
Feature: Latency
2+
3+
Background:
4+
Given the "signaling" backend is running
5+
And the "testproxy" backend is running
6+
7+
8+
Scenario: Lobby listings include the latency to the peer
9+
Given the next peer's latency vector is set to:
10+
"""
11+
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10
12+
"""
13+
And "green" is connected as "1u8fw4aph5ypt" and ready for game "b6f7fc97-8545-4ffd-b714-7cf339048556"
14+
And "green" creates a lobby with these settings:
15+
"""json
16+
{
17+
"public": true
18+
}
19+
"""
20+
And "green" receives the network event "lobby" with the argument "h5yzwyizlwao"
21+
And the next peer's latency vector is set to:
22+
"""
23+
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20
24+
"""
25+
And "blue" is connected as "19yrzmetd2bn7" and ready for game "b6f7fc97-8545-4ffd-b714-7cf339048556"
26+
27+
When "blue" requests lobbies with:
28+
"""json
29+
{}
30+
"""
31+
32+
Then "blue" should have received only these lobbies:
33+
| code | latency |
34+
| h5yzwyizlwao | 24 |
35+
36+
37+
Scenario: Lobby with multiple peers
38+
Given the next peer's latency vector is set to:
39+
"""
40+
99, 99, 99, 99, 10, 10, 10, 10, 10, 10, 10
41+
"""
42+
And "blue" is connected as "1u8fw4aph5ypt" and ready for game "4307bd86-e1df-41b8-b9df-e22afcf084bd"
43+
And the next peer's latency vector is set to:
44+
"""
45+
10, 10, 10, 99, 99, 99, 99, 10, 10, 10, 10
46+
"""
47+
And "yellow" is connected as "h5yzwyizlwao" and ready for game "4307bd86-e1df-41b8-b9df-e22afcf084bd"
48+
And "blue,yellow" are joined in a public lobby
49+
And the next peer's latency vector is set to:
50+
"""
51+
10, 10, 10, 10, 10, 10, 99, 99, 99, 99, 10
52+
"""
53+
And "green" is connected as "3t3cfgcqup9e" and ready for game "4307bd86-e1df-41b8-b9df-e22afcf084bd"
54+
55+
When "green" requests lobbies with:
56+
"""json
57+
{}
58+
"""
59+
60+
Then "green" should have received only these lobbies:
61+
| code | latency |
62+
| 19yrzmetd2bn7 | 89 |
63+
64+
65+
Scenario: Sort lobbies by latency
66+
Given the next peer's latency vector is set to:
67+
"""
68+
30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30
69+
"""
70+
And "blue" is connected as "1u8fw4aph5ypt" and ready for game "4307bd86-e1df-41b8-b9df-e22afcf084bd"
71+
And "blue" creates a lobby with these settings:
72+
"""json
73+
{
74+
"public": true,
75+
"customData": {
76+
"map": "de_nuke"
77+
}
78+
}
79+
"""
80+
And the next peer's latency vector is set to:
81+
"""
82+
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99
83+
"""
84+
And "yellow" is connected as "19yrzmetd2bn7" and ready for game "4307bd86-e1df-41b8-b9df-e22afcf084bd"
85+
And "yellow" creates a lobby with these settings:
86+
"""json
87+
{
88+
"public": true,
89+
"customData": {
90+
"map": "de_dust"
91+
}
92+
}
93+
"""
94+
And the next peer's latency vector is set to:
95+
"""
96+
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10
97+
"""
98+
And "green" is connected as "prb67ouj837u" and ready for game "4307bd86-e1df-41b8-b9df-e22afcf084bd"
99+
100+
When "green" requests lobbies with:
101+
| filter | {} |
102+
| sort | { "latency": 1 } |
103+
| limit | 1 |
104+
105+
Then "green" should have received only these lobbies:
106+
| code | latency | customData |
107+
| h5yzwyizlwao | 34 | {"map":"de_nuke"} |
108+
109+
110+
Scenario: Latency to your own lobby
111+
Given the next peer's latency vector is set to:
112+
"""
113+
325, 523, 64, 21, 76, 23, 54, 235, 76, 23, 142
114+
"""
115+
And "green" is connected as "1u8fw4aph5ypt" and ready for game "b6f7fc97-8545-4ffd-b714-7cf339048556"
116+
And "green" creates a lobby with these settings:
117+
"""json
118+
{
119+
"public": true
120+
}
121+
"""
122+
And "green" receives the network event "lobby" with the argument "h5yzwyizlwao"
123+
124+
When "green" requests lobbies with:
125+
"""json
126+
{}
127+
"""
128+
129+
Then "green" should have received only these lobbies:
130+
| code | latency |
131+
| h5yzwyizlwao | 0 |
132+
133+
134+
Scenario: Peers without latency vectors are not included in the estimate
135+
Given "green" is connected as "1u8fw4aph5ypt" and ready for game "f666036d-d9e1-4d70-b0c3-4a68b24a9884"
136+
And these lobbies exist:
137+
| code | game | peers | public |
138+
| 1u8fw4aph5ypt | f666036d-d9e1-4d70-b0c3-4a68b24a9884 | {"peer1"} | true |
139+
| 0qva9vyurwbbl | f666036d-d9e1-4d70-b0c3-4a68b24a9884 | {"peer2", "peer3"} | true |
140+
And these peers exist:
141+
| peer | game | latency_vector |
142+
| peer1 | f666036d-d9e1-4d70-b0c3-4a68b24a9884 | null |
143+
| peer2 | f666036d-d9e1-4d70-b0c3-4a68b24a9884 | null |
144+
| peer3 | f666036d-d9e1-4d70-b0c3-4a68b24a9884 | {10,10,10,10,10,10,10,10,10,10,10} |
145+
146+
When "green" requests lobbies with:
147+
| filter | {} |
148+
| sort | { "latency": 1 } |
149+
Then "green" should have received only these lobbies:
150+
| code | latency |
151+
| 0qva9vyurwbbl | 10 |
152+
| 1u8fw4aph5ypt | undefined |
153+
154+
155+
Scenario: Client without latency vectors gives null latency estimates
156+
Given the next peer's latency vector is set to:
157+
"""
158+
null
159+
"""
160+
Given "green" is connected as "1u8fw4aph5ypt" and ready for game "f666036d-d9e1-4d70-b0c3-4a68b24a9884"
161+
And these lobbies exist:
162+
| code | game | peers | public |
163+
| 0qva9vyurwbbl | f666036d-d9e1-4d70-b0c3-4a68b24a9884 | {"peer1", "peer2"} | true |
164+
And these peers exist:
165+
| peer | game | latency_vector |
166+
| peer1 | f666036d-d9e1-4d70-b0c3-4a68b24a9884 | {10,10,10,10,10,10,10,10,10,10,10} |
167+
168+
When "green" requests lobbies with:
169+
"""json
170+
{}
171+
"""
172+
Then "green" should have received only these lobbies:
173+
| code | latency |
174+
| 0qva9vyurwbbl | undefined |

features/support/steps/network.ts

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Given('{string} is connected as {string} and ready for game {string}', async fun
1919
}
2020
})
2121

22-
async function areJoinedInALobby (this: World, playerNamesRaw: string): Promise<void> {
22+
async function areJoinedInALobby (this: World, playerNamesRaw: string, publc: boolean): Promise<void> {
2323
const playerNames = playerNamesRaw.split(',').map(s => s.trim())
2424
if (playerNames.length < 2) {
2525
throw new Error('need at least 2 players to join a lobby')
@@ -29,7 +29,9 @@ async function areJoinedInALobby (this: World, playerNamesRaw: string): Promise<
2929
throw new Error(`player ${playerNames[0]} not found`)
3030
}
3131

32-
void first.network.create()
32+
void first.network.create({
33+
public: publc
34+
})
3335
const lobbyEvent = await first.waitForEvent('lobby')
3436
const lobbyCode = lobbyEvent.eventPayload[0] as string
3537

@@ -55,7 +57,13 @@ async function areJoinedInALobby (this: World, playerNamesRaw: string): Promise<
5557
}
5658
}
5759

58-
Given('{string} are joined in a lobby', areJoinedInALobby)
60+
Given('{string} are joined in a lobby', async function (this: World, playerNamesRaw: string) {
61+
await areJoinedInALobby.call(this, playerNamesRaw, false)
62+
})
63+
64+
Given('{string} are joined in a public lobby', async function (this: World, playerNamesRaw: string) {
65+
await areJoinedInALobby.call(this, playerNamesRaw, true)
66+
})
5967

6068
Given('{string} are joined in a lobby for game {string}', async function (this: World, playerNamesRaw: string, gameID: string) {
6169
const playerNames = playerNamesRaw.split(',').map(s => s.trim())
@@ -72,7 +80,7 @@ Given('{string} are joined in a lobby for game {string}', async function (this:
7280
}
7381
}
7482

75-
await areJoinedInALobby.call(this, playerNamesRaw)
83+
await areJoinedInALobby.call(this, playerNamesRaw, false)
7684
})
7785

7886
Given('these lobbies exist:', async function (this: World, lobbies: DataTable) {
@@ -123,6 +131,41 @@ Given('these lobbies exist:', async function (this: World, lobbies: DataTable) {
123131
})
124132
})
125133

134+
Given('these peers exist:', async function (this: World, peers: DataTable) {
135+
if (this.testproxyURL === undefined) {
136+
throw new Error('testproxy not active')
137+
}
138+
139+
const columns: string[] = []
140+
const values: string[] = []
141+
142+
peers.hashes().forEach(row => {
143+
const v: string[] = []
144+
145+
Object.keys(row).forEach(key => {
146+
const value = row[key]
147+
if (!columns.includes(key)) {
148+
columns.push(key)
149+
}
150+
151+
if (value === 'null') {
152+
v.push('NULL')
153+
} else if (key === 'latency_vector') {
154+
v.push(`ARRAY[${value.substring(1, value.length - 1)}]::vector(11)`)
155+
} else {
156+
v.push(`'${value}'`)
157+
}
158+
})
159+
160+
values.push(`(${v.join(', ')})`)
161+
})
162+
163+
await fetch(`${this.testproxyURL}/sql`, {
164+
method: 'POST',
165+
body: 'INSERT INTO peers (' + columns.join(', ') + ') VALUES ' + values.join(', ')
166+
})
167+
})
168+
126169
When('{string} creates a network for game {string}', async function (this: World, playerName: string, gameID: string) {
127170
await this.createPlayer(playerName, gameID)
128171
})
@@ -307,7 +350,7 @@ Then('{string} should have received only these lobbies:', function (this: World,
307350
expectedLobbies.hashes().forEach(row => {
308351
const correctCodeLobby = player.lastReceivedLobbies.filter(lobby => lobby.code === row.code)
309352
if (correctCodeLobby.length !== 1) {
310-
throw new Error(`expected to find one lobby with code ${row.code} but found ${correctCodeLobby.length}`)
353+
throw new Error(`expected to find one lobby with code ${row.code} but found ${correctCodeLobby.length} in [${player.lastReceivedLobbies.map(l => l.code).join(', ')}]`)
311354
}
312355
const lobby = correctCodeLobby[0] as any
313356
Object.keys(lobby).forEach(key => {
@@ -318,8 +361,14 @@ Then('{string} should have received only these lobbies:', function (this: World,
318361
})
319362
const want = row as any
320363
Object.keys(row).forEach(key => {
321-
if (`${lobby[key] as string}` !== `${want[key] as string}`) {
322-
throw new Error(`expected ${key} to be ${want[key] as string} but got ${lobby[key] as string}`)
364+
if (typeof lobby[key] === 'object') {
365+
if (JSON.stringify(lobby[key]) !== `${want[key] as string}`) {
366+
throw new Error(`expected ${key} to be ${want[key] as string} but got ${JSON.stringify(lobby[key])}`)
367+
}
368+
} else {
369+
if (`${lobby[key] as string}` !== `${want[key] as string}`) {
370+
throw new Error(`expected ${key} to be ${want[key] as string} but got ${lobby[key] as string}`)
371+
}
323372
}
324373
})
325374
})
@@ -435,3 +484,16 @@ Then('{string} failed to join the lobby', function (playerName: string) {
435484
throw new Error(`player is in lobby ${player.network.currentLobby as string}`)
436485
}
437486
})
487+
488+
When('the next peer\'s latency vector is set to:', function (latencies: string) {
489+
if (latencies === 'null') {
490+
this.latencyVector = null
491+
return
492+
}
493+
494+
const lv = latencies.split(',').map(s => parseInt(s.trim(), 10))
495+
if (lv.length !== 11) {
496+
throw new Error('latency vector must have 11 elements')
497+
}
498+
this.latencyVector = lv
499+
})

features/support/steps/util.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

features/support/world.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ws from 'ws'
77
import wrtc from '@roamhq/wrtc'
88

99
import { Player } from './types'
10+
import { PeerConfiguration } from '../../lib/types'
1011

1112
import { Network } from '../../lib'
1213

@@ -35,6 +36,7 @@ export class World extends CucumberWorld {
3536
public testproxyURL?: string
3637
public useTestProxy: boolean = false
3738
public databaseURL?: string
39+
public latencyVector: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
3840

3941
public players: Map<string, Player> = new Map<string, Player>()
4042
public lastError: Map<string, Error> = new Map<string, Error>()
@@ -49,7 +51,13 @@ export class World extends CucumberWorld {
4951

5052
public async createPlayer (playerName: string, gameID: string): Promise<Player> {
5153
return await new Promise((resolve) => {
52-
const config = this.useTestProxy ? { testproxyURL: this.testproxyURL } : undefined
54+
const config: PeerConfiguration = {}
55+
if (this.useTestProxy) {
56+
config.testproxyURL = this.testproxyURL
57+
}
58+
config.testLatency = {
59+
vector: this.latencyVector
60+
}
5361
const network = new Network(gameID, config, this.signalingURL)
5462
const player = new Player(playerName, network)
5563
this.players.set(playerName, player)
@@ -100,6 +108,7 @@ AfterAll(function () {
100108

101109
Before(function (this: World) {
102110
this.scenarioRunning = true
111+
this.latencyVector = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
103112
})
104113
After(function (this: World, { result }) {
105114
this.scenarioRunning = false

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/jackc/pgx/v5 v5.7.6
99
github.com/koenbollen/logging v0.0.0-20230520102501-e01d64214504
1010
github.com/ory/dockertest/v3 v3.12.0
11+
github.com/pgvector/pgvector-go v0.3.0
1112
github.com/poki/mongodb-filter-to-postgres v1.0.7
1213
github.com/rs/cors v1.11.1
1314
github.com/rs/xid v1.6.0
@@ -44,6 +45,7 @@ require (
4445
github.com/pkg/errors v0.9.1 // indirect
4546
github.com/rogpeppe/go-internal v1.14.1 // indirect
4647
github.com/sirupsen/logrus v1.9.3 // indirect
48+
github.com/x448/float16 v0.8.4 // indirect
4749
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
4850
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
4951
github.com/xeipuuv/gojsonschema v1.2.0 // indirect

0 commit comments

Comments
 (0)