package gamestore import ( "encoding/json" "fmt" "time" "galaxy/lobby/internal/domain/game" ) // runtimeSnapshotJSON is the on-disk JSONB shape used for the denormalised // runtime snapshot column on `games`. Keys mirror the field names in // `game.RuntimeSnapshot` so a round-trip remains naked-comparable. type runtimeSnapshotJSON struct { CurrentTurn int `json:"current_turn"` RuntimeStatus string `json:"runtime_status,omitempty"` EngineHealthSummary string `json:"engine_health_summary,omitempty"` } func marshalRuntimeSnapshot(snapshot game.RuntimeSnapshot) ([]byte, error) { payload := runtimeSnapshotJSON{ CurrentTurn: snapshot.CurrentTurn, RuntimeStatus: snapshot.RuntimeStatus, EngineHealthSummary: snapshot.EngineHealthSummary, } encoded, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("marshal runtime snapshot: %w", err) } return encoded, nil } func unmarshalRuntimeSnapshot(payload []byte) (game.RuntimeSnapshot, error) { if len(payload) == 0 { return game.RuntimeSnapshot{}, nil } var stored runtimeSnapshotJSON if err := json.Unmarshal(payload, &stored); err != nil { return game.RuntimeSnapshot{}, fmt.Errorf("unmarshal runtime snapshot: %w", err) } return game.RuntimeSnapshot{ CurrentTurn: stored.CurrentTurn, RuntimeStatus: stored.RuntimeStatus, EngineHealthSummary: stored.EngineHealthSummary, }, nil } // runtimeBindingJSON is the on-disk JSONB shape used for the optional // runtime binding column on `games`. The `bound_at_ms` field stores Unix // milliseconds so the JSON serialisation matches the previous Redis JSON // shape and the timezone is irrelevant inside the JSON payload itself; the // adapter still re-wraps the resulting time.Time with .UTC() before exposing // it to callers. type runtimeBindingJSON struct { ContainerID string `json:"container_id"` EngineEndpoint string `json:"engine_endpoint"` RuntimeJobID string `json:"runtime_job_id"` BoundAtMS int64 `json:"bound_at_ms"` } // marshalRuntimeBinding returns nil bytes (SQL NULL) when binding is nil, // otherwise the JSON encoding of the binding. func marshalRuntimeBinding(binding *game.RuntimeBinding) ([]byte, error) { if binding == nil { return nil, nil } payload := runtimeBindingJSON{ ContainerID: binding.ContainerID, EngineEndpoint: binding.EngineEndpoint, RuntimeJobID: binding.RuntimeJobID, BoundAtMS: binding.BoundAt.UTC().UnixMilli(), } encoded, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("marshal runtime binding: %w", err) } return encoded, nil } func unmarshalRuntimeBinding(payload []byte) (*game.RuntimeBinding, error) { if len(payload) == 0 { return nil, nil } var stored runtimeBindingJSON if err := json.Unmarshal(payload, &stored); err != nil { return nil, fmt.Errorf("unmarshal runtime binding: %w", err) } return &game.RuntimeBinding{ ContainerID: stored.ContainerID, EngineEndpoint: stored.EngineEndpoint, RuntimeJobID: stored.RuntimeJobID, BoundAt: time.UnixMilli(stored.BoundAtMS).UTC(), }, nil }