Keep the gateway connection alive and resume hosted queues after reconnecting
CI / build (push) Successful in 30s

The client doesn't ping while it's sending, so when the proxy answers its frames itself (everything it
sends in a hosted room), IMVU's gateway saw an idle connection and closed it after about a minute. A
ping now goes to the gateway in place of each answered frame. And after a reconnect, hosted rooms'
queues are taken out of C2gOpenFloodgates and answered here (the gateway never answered them, so the
client gave up and closed the room).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 05:37:53 -07:00
1 parent c483099558
commit 08a2ed94a3
4 files changed
+94 -12

No files matched your search

+14
View File
@@ -97,3 +97,17 @@ func TestSubscriptionOps(t *testing.T) {
t.Fatalf("all %v", all)
}
}
func TestFloodgates(t *testing.T) {
state := func(q string, seq, op uint64) []byte {
return appendVarintField(appendVarintField(appendBytesField(nil, 1, []byte(q)), 2, seq), 3, op)
}
data := appendBytesField(appendBytesField(nil, 1, state("/user/1", 38, 19)), 1, state("/chat/2100000001", 29, 20))
states, err := ParseFloodgates(data)
if err != nil || len(states) != 2 || states[1].Queue != "/chat/2100000001" || states[1].OpID != 20 {
t.Fatalf("%+v %v", states, err)
}
if string(MarshalFloodgates(states)) != string(data) || string(MarshalFloodgates(states[:1])) != string(appendBytesField(nil, 1, state("/user/1", 38, 19))) {
t.Fatal("re-encoded differently")
}
}
+50 -9
View File
@@ -4,15 +4,17 @@ import "errors"
// Message types the proxy reads or writes.
const (
C2gSendMessage = 1304
C2gUnsubscribe = 1308
C2gSubscribe = 1309
G2cResult = 3102
G2cSendMessage = 3104
G2cStateChange = 3105
G2cJoinedQueue = 3106
G2cLeftQueue = 3107
G2cCreateMount = 3108
C2gPing = 1301
C2gSendMessage = 1304
C2gOpenFloodgates = 1306
C2gUnsubscribe = 1308
C2gSubscribe = 1309
G2cResult = 3102
G2cSendMessage = 3104
G2cStateChange = 3105
G2cJoinedQueue = 3106
G2cLeftQueue = 3107
G2cCreateMount = 3108
)
// Mount types (G2cCreateMount).
@@ -314,3 +316,42 @@ func StateChange(opID uint64, userID, queue, mount string, props []Property, seq
b = appendBytesField(appendBytesField(appendBytesField(b, 2, []byte(userID)), 3, []byte(queue)), 4, []byte(mount))
return appendVarintField(appendProperties(b, 5, props), 6, sequence)
}
// ParseFloodgates reads a C2gOpenFloodgates's data (after reconnecting, the queues to resume): each queue's state
// as a QueueOp (its op ID for a result), keeping the entry as sent.
func ParseFloodgates(data []byte) ([]QueueOp, error) {
fs, err := fields(data)
if err != nil {
return nil, err
}
var out []QueueOp
for _, f := range fs {
if f.num != 1 || !f.isLen {
return nil, errors.New("imq: unexpected field in C2gOpenFloodgates")
}
sub, err := fields(f.bytes)
if err != nil {
return nil, err
}
q := QueueOp{raw: f.bytes}
for _, sf := range sub {
switch {
case sf.num == 1 && sf.isLen:
q.Queue = string(sf.bytes)
case sf.num == 3 && !sf.isLen:
q.OpID = sf.varint
}
}
out = append(out, q)
}
return out, nil
}
// MarshalFloodgates encodes a C2gOpenFloodgates's data from queue states read by ParseFloodgates.
func MarshalFloodgates(states []QueueOp) []byte {
var b []byte
for _, q := range states {
b = appendBytesField(b, 1, q.raw)
}
return b
}