Saltar a contenido

Acknowledgments Avanzados

Los acknowledgments (ACKs) en Socket.IO permiten implementar patrones request-response sobre la comunicación bidireccional. Esta guía cubre uso avanzado de acknowledgments.

Tabla de Contenidos

Conceptos Fundamentales

¿Qué son los Acknowledgments?

Los acknowledgments son callbacks que permiten al emisor recibir una respuesta del receptor:

// Cliente emite y espera respuesta
client.EmitWithAck("get-data", func(response ...interface{}) {
  fmt.Println("Servidor respondió:", response[0])
}, "param1", "param2")

Dirección de los Acknowledgments

Los ACKs pueden fluir en ambas direcciones:

Cliente → Servidor → Cliente:

// Cliente solicita datos
client.EmitWithAck("get-user", func(response ...interface{}) {
  user := response[0].(map[string]interface{})
  fmt.Println("Usuario:", user["name"])
}, "user-123")

Servidor → Cliente → Servidor:

// Servidor solicita confirmación, cliente responde
client.On("confirm-action", func(data ...interface{}) {
  action := data[0].(string)

  // Último argumento es el callback de ACK
  if ack, ok := data[len(data)-1].(func(...interface{})); ok {
    // Confirmar la acción
    ack(map[string]interface{}{
      "confirmed": true,
      "timestamp": time.Now().Unix(),
    })
  }
})

Patrones de Uso

Patrón: Request-Response Simple

Solicitud básica con respuesta:

client.EmitWithAck("get-data", func(response ...interface{}) {
  if response == nil {
    fmt.Println("Timeout: no se recibió respuesta")
    return
  }

  data := response[0]
  fmt.Printf("Datos recibidos: %v\n", data)
}, "data-id-123")

Patrón: Validación de Datos

Envía datos y valida que fueron procesados correctamente:

func saveData(client *socketio.Socket, data map[string]interface{}) error {
  var result error
  done := make(chan struct{})

  client.EmitWithAck("save-data", func(response ...interface{}) {
    defer close(done)

    if response == nil {
      result = fmt.Errorf("timeout esperando confirmación")
      return
    }

    resp := response[0].(map[string]interface{})
    if resp["success"].(bool) {
      fmt.Println("Datos guardados exitosamente")
    } else {
      result = fmt.Errorf("error: %s", resp["error"])
    }
  }, data)

  <-done
  return result
}

// Uso
err := saveData(client, map[string]interface{}{
  "name": "Juan",
  "age": 30,
})
if err != nil {
  fmt.Println("Error:", err)
}

Patrón: Múltiples Requests Paralelos

Ejecuta múltiples requests en paralelo y espera todas las respuestas:

func fetchMultipleUsers(client *socketio.Socket, ids []string) map[string]interface{} {
  results := make(map[string]interface{})
  var mu sync.Mutex
  var wg sync.WaitGroup

  for _, id := range ids {
    wg.Add(1)
    go func(userId string) {
      defer wg.Done()

      client.EmitWithAck("get-user", func(response ...interface{}) {
        if response != nil {
          mu.Lock()
          results[userId] = response[0]
          mu.Unlock()
        }
      }, userId)
    }(id)
  }

  wg.Wait()
  return results
}

// Uso
users := fetchMultipleUsers(client, []string{"user-1", "user-2", "user-3"})
fmt.Printf("Obtenidos %d usuarios\n", len(users))

Patrón: Request con Retry

Reintenta el request si falla o hace timeout:

func requestWithRetry(client *socketio.Socket, event string, maxRetries int, data ...interface{}) (interface{}, error) {
  for attempt := 0; attempt <= maxRetries; attempt++ {
    resultChan := make(chan interface{}, 1)
    errorChan := make(chan error, 1)

    client.EmitWithAck(event, func(response ...interface{}) {
      if response == nil {
        errorChan <- fmt.Errorf("timeout")
      } else {
        resultChan <- response[0]
      }
    }, data...)

    select {
    case result := <-resultChan:
      return result, nil
    case err := <-errorChan:
      if attempt < maxRetries {
        fmt.Printf("Intento %d falló: %v. Reintentando...\n", attempt+1, err)
        time.Sleep(time.Second * time.Duration(attempt+1)) // Backoff exponencial
      } else {
        return nil, fmt.Errorf("falló después de %d intentos: %v", maxRetries+1, err)
      }
    }
  }

  return nil, fmt.Errorf("falló después de %d intentos", maxRetries+1)
}

// Uso
result, err := requestWithRetry(client, "get-data", 3, "param1", "param2")
if err != nil {
  fmt.Println("Error:", err)
} else {
  fmt.Println("Resultado:", result)
}

Patrón: Request Queue

Cola de requests con prioridad:

type Request struct {
  Event    string
  Data     []interface{}
  Callback func(...interface{})
  Priority int
}

type RequestQueue struct {
  client   *socketio.Socket
  queue    []*Request
  mu       sync.Mutex
  running  bool
}

func NewRequestQueue(client *socketio.Socket) *RequestQueue {
  return &RequestQueue{
    client: client,
    queue:  make([]*Request, 0),
  }
}

func (rq *RequestQueue) Add(req *Request) {
  rq.mu.Lock()
  defer rq.mu.Unlock()

  rq.queue = append(rq.queue, req)

  // Ordenar por prioridad (mayor primero)
  sort.Slice(rq.queue, func(i, j int) bool {
    return rq.queue[i].Priority > rq.queue[j].Priority
  })

  if !rq.running {
    go rq.process()
  }
}

func (rq *RequestQueue) process() {
  rq.running = true
  defer func() { rq.running = false }()

  for {
    rq.mu.Lock()
    if len(rq.queue) == 0 {
      rq.mu.Unlock()
      return
    }

    req := rq.queue[0]
    rq.queue = rq.queue[1:]
    rq.mu.Unlock()

    // Ejecutar request
    rq.client.EmitWithAck(req.Event, req.Callback, req.Data...)

    // Pequeño delay entre requests
    time.Sleep(50 * time.Millisecond)
  }
}

// Uso
queue := NewRequestQueue(client)

// Request de alta prioridad
queue.Add(&Request{
  Event:    "urgent-data",
  Data:     []interface{}{"id-1"},
  Priority: 10,
  Callback: func(response ...interface{}) {
    fmt.Println("Respuesta urgente:", response[0])
  },
})

// Request de prioridad normal
queue.Add(&Request{
  Event:    "normal-data",
  Data:     []interface{}{"id-2"},
  Priority: 5,
  Callback: func(response ...interface{}) {
    fmt.Println("Respuesta normal:", response[0])
  },
})

Manejo de Timeouts

Timeout Global

Configurar timeout global para todos los acknowledgments:

client := socketio.New("ws://localhost:3000", socketio.Options{
  AckTimeout: 10 * time.Second, // Timeout de 10 segundos
})

client.EmitWithAck("slow-operation", func(response ...interface{}) {
  if response == nil {
    fmt.Println("⏱️  Operación tardó más de 10 segundos")
    return
  }
  fmt.Println("Completado:", response[0])
}, "datos")

Timeout por Request

Implementar timeout personalizado por request:

func emitWithCustomTimeout(client *socketio.Socket, event string, timeout time.Duration, data ...interface{}) (interface{}, error) {
  resultChan := make(chan interface{}, 1)
  timeoutChan := time.After(timeout)

  client.EmitWithAck(event, func(response ...interface{}) {
    if response != nil {
      resultChan <- response[0]
    }
  }, data...)

  select {
  case result := <-resultChan:
    return result, nil
  case <-timeoutChan:
    return nil, fmt.Errorf("timeout después de %v", timeout)
  }
}

// Uso con timeout corto
result, err := emitWithCustomTimeout(client, "quick-op", 2*time.Second, "data")
if err != nil {
  fmt.Println("Error:", err)
}

// Uso con timeout largo
result, err = emitWithCustomTimeout(client, "slow-op", 30*time.Second, "data")
if err != nil {
  fmt.Println("Error:", err)
}

Detección de Timeouts

Distinguir entre timeout y error del servidor:

client.EmitWithAck("operation", func(response ...interface{}) {
  if response == nil {
    // Timeout: el servidor no respondió
    fmt.Println("⏱️  Timeout: el servidor no respondió a tiempo")
    handleTimeout()
    return
  }

  // Respuesta recibida, verificar si es error
  if resp, ok := response[0].(map[string]interface{}); ok {
    if errorMsg, hasError := resp["error"]; hasError {
      // Error del servidor
      fmt.Printf("❌ Error del servidor: %s\n", errorMsg)
      handleServerError(errorMsg.(string))
      return
    }

    // Éxito
    fmt.Println("✅ Operación exitosa:", resp["data"])
    handleSuccess(resp["data"])
  }
}, "datos")

Error Handling

Patrón: Error Response Estándar

Definir formato estándar para respuestas de error:

type AckResponse struct {
  Success bool                   `json:"success"`
  Data    interface{}            `json:"data,omitempty"`
  Error   string                 `json:"error,omitempty"`
  Code    int                    `json:"code,omitempty"`
}

func handleAckResponse(response ...interface{}) (*AckResponse, error) {
  if response == nil {
    return nil, fmt.Errorf("timeout: sin respuesta del servidor")
  }

  if len(response) == 0 {
    return nil, fmt.Errorf("respuesta vacía")
  }

  respMap, ok := response[0].(map[string]interface{})
  if !ok {
    return nil, fmt.Errorf("formato de respuesta inválido")
  }

  ackResp := &AckResponse{}

  if success, ok := respMap["success"].(bool); ok {
    ackResp.Success = success
  }

  if data, ok := respMap["data"]; ok {
    ackResp.Data = data
  }

  if errorMsg, ok := respMap["error"].(string); ok {
    ackResp.Error = errorMsg
  }

  if code, ok := respMap["code"].(float64); ok {
    ackResp.Code = int(code)
  }

  if !ackResp.Success && ackResp.Error != "" {
    return ackResp, fmt.Errorf("error %d: %s", ackResp.Code, ackResp.Error)
  }

  return ackResp, nil
}

// Uso
client.EmitWithAck("operation", func(response ...interface{}) {
  resp, err := handleAckResponse(response...)
  if err != nil {
    fmt.Println("Error:", err)
    return
  }

  fmt.Println("Datos:", resp.Data)
}, "params")

Patrón: Graceful Degradation

Manejar fallos de forma elegante con valores por defecto:

func getUserDataWithDefault(client *socketio.Socket, userId string) map[string]interface{} {
  defaultUser := map[string]interface{}{
    "id": userId,
    "name": "Usuario Desconocido",
    "status": "offline",
  }

  resultChan := make(chan map[string]interface{}, 1)

  client.EmitWithAck("get-user", func(response ...interface{}) {
    if response == nil {
      // Timeout: usar valores por defecto
      resultChan <- defaultUser
      return
    }

    if user, ok := response[0].(map[string]interface{}); ok {
      resultChan <- user
    } else {
      resultChan <- defaultUser
    }
  }, userId)

  // Esperar con timeout adicional
  select {
  case user := <-resultChan:
    return user
  case <-time.After(6 * time.Second):
    // Timeout adicional: retornar valores por defecto
    return defaultUser
  }
}

// Uso
user := getUserDataWithDefault(client, "user-123")
fmt.Printf("Usuario: %s (%s)\n", user["name"], user["status"])

Patrones Avanzados

Patrón: Promise-like API

Envolver acknowledgments en una API similar a Promises:

type Promise struct {
  done   chan struct{}
  result interface{}
  err    error
}

func NewPromise(client *socketio.Socket, event string, data ...interface{}) *Promise {
  p := &Promise{
    done: make(chan struct{}),
  }

  client.EmitWithAck(event, func(response ...interface{}) {
    defer close(p.done)

    if response == nil {
      p.err = fmt.Errorf("timeout")
      return
    }

    if resp, ok := response[0].(map[string]interface{}); ok {
      if errMsg, hasError := resp["error"]; hasError {
        p.err = fmt.Errorf("%s", errMsg)
        return
      }
      p.result = resp["data"]
    } else {
      p.result = response[0]
    }
  }, data...)

  return p
}

func (p *Promise) Then(callback func(interface{})) *Promise {
  go func() {
    <-p.done
    if p.err == nil {
      callback(p.result)
    }
  }()
  return p
}

func (p *Promise) Catch(callback func(error)) *Promise {
  go func() {
    <-p.done
    if p.err != nil {
      callback(p.err)
    }
  }()
  return p
}

func (p *Promise) Await() (interface{}, error) {
  <-p.done
  return p.result, p.err
}

// Uso estilo Promise
NewPromise(client, "get-data", "id-123").
  Then(func(data interface{}) {
    fmt.Println("Datos:", data)
  }).
  Catch(func(err error) {
    fmt.Println("Error:", err)
  })

// Uso estilo Await
data, err := NewPromise(client, "get-data", "id-123").Await()
if err != nil {
  fmt.Println("Error:", err)
} else {
  fmt.Println("Datos:", data)
}

Patrón: Batch Requests

Enviar múltiples requests en un solo acknowledgment:

type BatchRequest struct {
  ID   string        `json:"id"`
  Data interface{}   `json:"data"`
}

type BatchResponse struct {
  ID     string      `json:"id"`
  Result interface{} `json:"result"`
  Error  string      `json:"error,omitempty"`
}

func batchRequest(client *socketio.Socket, requests []BatchRequest) (map[string]interface{}, error) {
  results := make(map[string]interface{})
  done := make(chan struct{})

  client.EmitWithAck("batch", func(response ...interface{}) {
    defer close(done)

    if response == nil {
      return
    }

    if responses, ok := response[0].([]interface{}); ok {
      for _, r := range responses {
        resp := r.(map[string]interface{})
        id := resp["id"].(string)

        if errMsg, hasError := resp["error"]; hasError && errMsg != "" {
          results[id] = fmt.Errorf("%s", errMsg)
        } else {
          results[id] = resp["result"]
        }
      }
    }
  }, requests)

  <-done

  if len(results) == 0 {
    return nil, fmt.Errorf("timeout o sin respuestas")
  }

  return results, nil
}

// Uso
requests := []BatchRequest{
  {ID: "req-1", Data: "user-1"},
  {ID: "req-2", Data: "user-2"},
  {ID: "req-3", Data: "user-3"},
}

results, err := batchRequest(client, requests)
if err != nil {
  fmt.Println("Error:", err)
  return
}

for id, result := range results {
  if err, isError := result.(error); isError {
    fmt.Printf("%s: Error - %v\n", id, err)
  } else {
    fmt.Printf("%s: Success - %v\n", id, result)
  }
}

Patrón: Streaming con Acknowledgments

Implementar streaming de datos con confirmaciones incrementales:

func streamDataWithAcks(client *socketio.Socket, chunks [][]byte) error {
  for i, chunk := range chunks {
    done := make(chan error, 1)

    client.EmitBinaryWithAck("stream-chunk", func(response ...interface{}) {
      if response == nil {
        done <- fmt.Errorf("timeout en chunk %d", i)
        return
      }

      resp := response[0].(map[string]interface{})
      if resp["success"].(bool) {
        done <- nil
      } else {
        done <- fmt.Errorf("error en chunk %d: %s", i, resp["error"])
      }
    }, chunk, map[string]interface{}{
      "index": i,
      "total": len(chunks),
      "last": i == len(chunks)-1,
    })

    if err := <-done; err != nil {
      return err
    }

    fmt.Printf("Chunk %d/%d enviado\n", i+1, len(chunks))
  }

  return nil
}

// Uso
data, _ := os.ReadFile("large-file.bin")
chunks := splitIntoChunks(data, 1024*64) // Chunks de 64KB

if err := streamDataWithAcks(client, chunks); err != nil {
  fmt.Println("Error en streaming:", err)
} else {
  fmt.Println("Archivo enviado completamente")
}

Mejores Prácticas

1. Siempre Verificar nil

// ✅ Bueno
client.EmitWithAck("event", func(response ...interface{}) {
  if response == nil {
    fmt.Println("Timeout")
    return
  }
  // Procesar respuesta
}, data)

// ❌ Malo
client.EmitWithAck("event", func(response ...interface{}) {
  data := response[0] // Panic si response == nil!
}, data)

2. Usar Type Assertions Seguras

// ✅ Bueno
if user, ok := response[0].(map[string]interface{}); ok {
  fmt.Println(user["name"])
} else {
  fmt.Println("Formato inesperado")
}

// ❌ Malo
user := response[0].(map[string]interface{}) // Panic si tipo incorrecto!

3. Configurar Timeouts Apropiados

// ✅ Bueno: timeout según el tipo de operación
fastOps := socketio.Options{AckTimeout: 2 * time.Second}
slowOps := socketio.Options{AckTimeout: 30 * time.Second}

// ❌ Malo: mismo timeout para todo
opts := socketio.Options{AckTimeout: 5 * time.Second} // Puede ser muy corto o muy largo

4. Documentar Formato de Respuestas

// ✅ Bueno: documentar qué se espera
// Respuesta esperada: {success: bool, data: User, error: string}
client.EmitWithAck("get-user", func(response ...interface{}) {
  // ...
}, userId)

Ver También