- share.go: cross-machine vital sharing (share_subscribe/unsubscribe/share_*), faithful port of the peer-state snapshot + plugin fan-out + /vital-sharing/peers. The last ingest handler — the Go tracker now handles every plugin event type. - shadow consumer: drop the outbound keepalive ping (the firehose is never idle) and tighten the read-deadline watchdog to 12s for faster reconnect after the upstream's periodic eviction (full-firehose browser clients get evicted ~every 90s; the watchdog recovers it, ~90% duty cycle). Production-bound /ws/position is unaffected (plugins connect to us; no eviction). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 lines
3.4 KiB
Go
111 lines
3.4 KiB
Go
package main
|
|
|
|
// Cross-machine vital sharing (share_*), a faithful port of main.py:3658-3703 +
|
|
// _update_vital_sharing_peer_state / _broadcast_share_to_plugin_clients.
|
|
// Memory-only: subscriber set + last-known peer snapshot, fanned out to other
|
|
// opted-in plugin clients and to browsers. In shadow mode there are no plugin
|
|
// connections, so the fan-out is a no-op; the peer state still drives
|
|
// /vital-sharing/peers.
|
|
|
|
func (i *Ingestor) handleShareSubscribe(data map[string]any) {
|
|
char := toStr(data["character_name"])
|
|
if char == "" {
|
|
return
|
|
}
|
|
i.mu.Lock()
|
|
i.vitalSubscribers[char] = true
|
|
entry := i.vitalPeerEntry(char)
|
|
if tags, ok := data["tags"].([]any); ok {
|
|
entry["tags"] = tags
|
|
}
|
|
entry["connected"] = true
|
|
i.mu.Unlock()
|
|
}
|
|
|
|
func (i *Ingestor) handleShareUnsubscribe(data map[string]any) {
|
|
char := toStr(data["character_name"])
|
|
if char == "" {
|
|
return
|
|
}
|
|
i.mu.Lock()
|
|
delete(i.vitalSubscribers, char)
|
|
delete(i.vitalPeerState, char)
|
|
i.mu.Unlock()
|
|
if i.broadcast != nil {
|
|
i.broadcast(map[string]any{"type": "share_peer_removed", "character_name": char})
|
|
}
|
|
}
|
|
|
|
func (i *Ingestor) handleShareUpdate(msgType string, data map[string]any) {
|
|
origin := toStr(data["character_name"])
|
|
i.mu.Lock()
|
|
i.updateVitalPeerState(msgType, data)
|
|
// Snapshot subscribers for the fan-out.
|
|
subs := make(map[string]bool, len(i.vitalSubscribers))
|
|
for k := range i.vitalSubscribers {
|
|
subs[k] = true
|
|
}
|
|
i.mu.Unlock()
|
|
// Fan out to other opted-in plugin clients (no-op when no plugins connected).
|
|
if i.plugins != nil && len(subs) > 0 {
|
|
i.plugins.fanoutShare(data, origin, subs)
|
|
}
|
|
}
|
|
|
|
// vitalPeerEntry returns (creating if needed) the peer snapshot for char. Caller
|
|
// holds i.mu.
|
|
func (i *Ingestor) vitalPeerEntry(char string) map[string]any {
|
|
entry, ok := i.vitalPeerState[char]
|
|
if !ok {
|
|
entry = map[string]any{
|
|
"character_name": char, "tags": []any{}, "vitals": nil,
|
|
"position": nil, "items": nil, "connected": true, "last_update": nil,
|
|
}
|
|
i.vitalPeerState[char] = entry
|
|
}
|
|
return entry
|
|
}
|
|
|
|
// updateVitalPeerState mirrors _update_vital_sharing_peer_state. Caller holds i.mu.
|
|
func (i *Ingestor) updateVitalPeerState(msgType string, data map[string]any) {
|
|
char := toStr(data["character_name"])
|
|
if char == "" {
|
|
return
|
|
}
|
|
entry := i.vitalPeerEntry(char)
|
|
entry["last_update"] = data["timestamp"]
|
|
if tags, ok := data["tags"].([]any); ok {
|
|
entry["tags"] = tags
|
|
}
|
|
switch msgType {
|
|
case "share_vital_update":
|
|
entry["vitals"] = map[string]any{
|
|
"current_health": data["current_health"], "max_health": data["max_health"],
|
|
"current_stamina": data["current_stamina"], "max_stamina": data["max_stamina"],
|
|
"current_mana": data["current_mana"], "max_mana": data["max_mana"],
|
|
}
|
|
case "share_position_update":
|
|
entry["position"] = map[string]any{
|
|
"ew": data["ew"], "ns": data["ns"], "z": data["z"], "heading": data["heading"],
|
|
}
|
|
case "share_item_update":
|
|
entry["items"] = data["items"]
|
|
}
|
|
}
|
|
|
|
// vitalSharingPeers returns the peer list for /vital-sharing/peers (main.py:1800).
|
|
func (i *Ingestor) vitalSharingPeers() ([]map[string]any, int) {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
peers := make([]map[string]any, 0, len(i.vitalPeerState))
|
|
for char, entry := range i.vitalPeerState {
|
|
p := make(map[string]any, len(entry)+2)
|
|
for k, v := range entry {
|
|
p[k] = v
|
|
}
|
|
p["subscribed"] = i.vitalSubscribers[char]
|
|
p["plugin_connected"] = i.plugins != nil && i.plugins.isConnected(char)
|
|
peers = append(peers, p)
|
|
}
|
|
return peers, len(i.vitalSubscribers)
|
|
}
|