Arquitectura de la Outbox: diseñada para no derramar mensajes
Esta página es una traducción automática del original en japonés. Leer el original en japonés
13 de febrero de 2026
Pregunto: No derrames el mensaje incluso si se borra inmediatamente
SimpleMemo, la interfaz de usuario se borra en el momento en que el usuario toca el botón de enviar y puede comenzar inmediatamente a escribir la siguiente nota. Esta es la experiencia de usuario que es la raíz del estilo Captio.
Sin embargo, la red real tiene una latencia de 300ms~2 segundos, puede fallar y puede estar fuera de línea en primer lugar. Muchas aplicaciones esperan la respuesta de la red antes de borrar la interfaz de usuario. Es seguro, pero la experiencia de usuario es mala. Por el contrario, si lo borras inmediatamente, existe el riesgo de perder el mensaje.
El patrón de la Outbox resuelve este requisito conflictivo al mismo tiempo. Guarde permanentemente el mensaje en el almacenamiento cifrado local (Outbox) en el momento de enviar, borre instantáneamente la interfaz de usuario y envíe la red en segundo plano. Si la transmisión tiene éxito, se eliminará de la Outbox y, si falla, se reintentará automáticamente. Como resultado, deja el mensaje no transmitido en el terminal y vuelve a enviarlo, y la latencia de la experiencia es cercana a cero.
Flujo de diseño del patrón Outbox
El siguiente es todo el flujo desde tocar el botón de enviar hasta enviar el mensaje.
- Enviar toque → Primero, cifre AES-GCM en la Outbox y guárdelo permanentemente
En el momento en que el usuario toque el botón de enviar, el mensaje se cifra con AES-GCM 256bit y se almacena permanentemente en el almacenamiento local del dispositivo. La interfaz de usuario no se borrará hasta que se complete este paso. En otras palabras, el almacenamiento de Outbox será el primer rompeolas para evitar derrames. - Borrar la interfaz de usuario inmediatamente (los usuarios pueden comenzar a escribir la siguiente nota)Cuando el guardado de
Outbox sea exitoso, borre la vista de texto y reproduzca la animación de transmisión. Desde el punto de vista del usuario, es sinónimo de "enviado". No estoy esperando ninguna respuesta de la red. El objetivo de Send-to-Reset es ser corto sin sentirse de espera (0.25 segundos, incluida la animación). - Enviar un correo electrónico a través de Relay API en segundo planoEn paralelo con la compensación
UI, SendManager envía una solicitud HTTP a Relay API en Cloudflare Trabajadores. Este proceso es completamente asíncrono y no bloquea el hilo principal. - Si tiene éxito, elimine de la Outbox
Relay API Después de recibir la respuesta 200, elimine de forma segura el mensaje correspondiente de la Outbox. Esto completa el ciclo de vida del mensaje. - Si falla, se reenviará automáticamente
En el caso de un error de red o error del servidor, déjelo esperando la transmisión, cuando la línea esté de vuelta, cuando se abra la aplicación, procesamiento en segundo plano (BGTaskScheduler. El momento de la ejecución lo decide iOS) y lo vuelve a enviar. Hay un límite superior en el número de veces para reenviar automáticamente. - Si está desconectado, NWPathMonitor detecta la recuperación de la línea y la vuelve a enviar automáticamenteSi está desconectado en el momento de enviar
, NWPathMonitor de Network.framework monitorea el estado de la línea, y cuando detecta la recuperación de Wi-Fi o celular, reenviará automáticamente el mensaje no transmitido de Outbox (al al igual que con 5 anterior, hay un límite superior en el número de veces para las remisiones automáticas).
Código de SendManager
La función send() de SendManager es el punto de entrada del patrón de Outbox. Primero, guárdelo en la Outbox, cola sin una conexión de red y envíelo inmediatamente si hay una conexión.
// 保存完了と送信結果を分ける説明用の簡略API
@MainActor
func send(message: String, onSaved: () -> Void,
completion: @escaping (SendResult) -> Void) {
// まずOutboxに保存(クラッシュや圏外で失わないように)
let outboxMessage: OutboxMessage
do {
outboxMessage = try OutboxManager.shared.add(body: message)
} catch {
DispatchQueue.main.async { completion(.failure(error)) }
return
}
// 同期保存に成功した場合だけ、メインスレッドでUIへ通知する
onSaved()
// ネットワーク接続がない場合はキューに入れたことを通知
guard NetworkMonitor.shared.isConnected else {
BackgroundTaskManager.shared.scheduleRetryTask()
DispatchQueue.main.async { completion(.queued) }
return
}
// 送信処理を実行
performSend(message: message, outboxId: outboxMessage.id, ...)
}
El punto importante es que OutboxManager.shared.add(body:) escribe en el almacenamiento persistente de forma sincrónica. Cuando esta llamada es exitosa, el mensaje se almacena de forma segura en el terminal. No importa cuál sea el resultado de la transmisión de red, el mensaje permanecerá en la Outbox.
Implementación del cifrado AES-GCM
El mensaje almacenado en la Outbox es la nota privada del usuario. Aunque es almacenamiento local, no se puede almacenar en texto sin formato. Usando AES-GCM de Apple CryptoKit, la implementación no utiliza ningún SDK externo para el cifrado.
- Apple CryptoKit — Disponible después de iOS 13. No utilice SDK externo para el procesamiento de cifrado (solo Apple genuino).
- Tecla simétrica 256-bit — Guardar en el llavero.
- Cifrado con autenticación — AES-GCM garantiza la confidencialidad e integridad de los datos al mismo tiempo. Los datos manipulados se detectan en el momento del descifrado.
enum OutboxEncryption {
private static var encryptionKey: SymmetricKey {
if let existingKey = KeychainHelper.load(key: "outbox_encryption_key") {
return SymmetricKey(data: existingKey)
}
let newKey = SymmetricKey(size: .bits256)
let keyData = newKey.withUnsafeBytes { Data($0) }
KeychainHelper.save(key: "outbox_encryption_key", data: keyData)
return newKey
}
static func encrypt(_ plainText: String) throws -> Data {
guard let data = plainText.data(using: .utf8) else {
throw OutboxError.encryptionFailed
}
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
guard let combined = sealedBox.combined else {
throw OutboxError.encryptionFailed
}
return combined
}
}
se crea automáticamente cuando se necesita por primera vez y se almacena en el llavero. La configuración de guardado es "solo este dispositivo, leído después del primer desbloqueo" (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), que no se puede leer desde otras aplicaciones y no se mueve de la copia de seguridad a otro terminal. Si lo desbloqueas una vez después de reiniciarlo, también puedes leerlo desde el procesamiento en segundo plano durante el bloqueo. Además, el ejemplo de código anterior es una versión simplificada en el momento de escribir el artículo, y el código actual no crea una nueva clave al tratarla como "sin clave" cuando el llavero no se lee.
El significado de configurar la ruta de datos solo con Apple genuino
Todos los marcos utilizados en la arquitectura de la Outbox son nativos de Apple.
- CryptoKit — cifrado AES-GCM. No se requiere una biblioteca de cifrado de terceros.
- Network.framework - Monitoreo del estado de la línea con NWPathMonitor. La biblioteca Reachability no es necesaria.
- BackgroundTasks — Reintento de fondo por BGTaskScheduler.
El hecho de que no haya una biblioteca externa en la ruta a la espera de transmisión tiene los siguientes significados.
- No afectado por actualizaciones externas — El procesamiento de la espera de transmisión no deja de funcionar debido a la actualización o terminación de bibliotecas de terceros.
- Seguridad - El código de terceros no entra en el procesamiento (cifrado, guardado, transmisión) que toca el cuerpo de la nota que espera ser enviada.
Además, toda la aplicación también utiliza un SDK externo (inicio de sesión Google, Firebase, AppsFlyer) (ver privacy first design). El "Apple solo genuino" mencionado aquí se trata de la ruta de los datos que esperan ser transmitidos.
Separación de la eliminación de la interfaz de usuario y la transmisión de red
El valor real del patrón de Outbox radica en la separación completa de la capa de interfaz de usuario y la capa de red. Echemos un vistazo al controlador de toque del botón de enviar.
@objc private func sendButtonTapped() {
PerformanceLogger.shared.beginSendToReset()
let message = textView.text ?? ""
isSending = true
sendBarButton.isEnabled = false
SendManager.shared.send(message: message, onSaved: { [weak self] in
// 端末への保存成功後にだけ消す。ネットワーク応答は待たない。
self?.performSendAnimation { [weak self] in
guard let self = self else { return }
// アニメーション中に編集された入力は残す。
if self.textView.text == message {
self.clearTextView()
}
PerformanceLogger.shared.endSendToReset()
}
}) { [weak self] result in
self?.isSending = false
self?.updateSendButtonState()
switch result {
case .success:
PerformanceLogger.shared.logEvent(name: "SendSuccess")
case .failure(let error):
self?.handleSendError(error)
case .queued:
self?.showQueuedFeedback()
}
}
}
En este ejemplo simplificado, onSaved se llama solo después de guardar correctamente en el dispositivo; a partir de ese momento se ejecuta la animación y se borra el texto introducido. Si falla el guardado, el texto se conserva. completion es una notificación independiente del resultado del envío o de la incorporación a la cola; el borrado de la interfaz no espera una respuesta de la red.
El objetivo de Send-to-Reset es la falta que no se siente esperando. 0.25 segundos incluyendo animación. Inmediatamente después de tocar el botón de enviar, el usuario puede comenzar a escribir la siguiente nota. Este es el estilo Captio UX.
Preguntas frecuentes
P. ¿Qué pasa con las notas enviadas sin conexión?
Se cifra y se guarda en la Outbox, y cuando se restaura la línea (detectada por NWPath Monitor), cuando se abre la aplicación, se envía automáticamente de vuelta después del procesamiento en segundo plano. Hay un límite superior en el número de veces para reenviar automáticamente, y para las notas que han alcanzado el límite superior, mantenga presionado en "Historial" y seleccione "Reenviar".
P. ¿No se perderán las notas incluso si cierro la aplicación?
Outbox es un almacenamiento persistente. Incluso si cierras la aplicación o reinicias el iPhone, permanecerá. La próxima vez que se inicie la aplicación, o la tarea en segundo plano se reintentará automáticamente.
P. ¿Por qué elegiste AES-GCM?
Apple CryptoKit es compatible de forma nativa, y la detección de manipulaciones también es posible mediante el cifrado autenticado. El hecho de que no haya necesidad de una biblioteca externa para procesar el cifrado también es una ventaja.
Q. ¿Cuál es el número de reintentos en caso de fallo de transmisión?
Se volverá a enviar automáticamente. El disparador es el procesamiento en segundo plano cuando se devuelve la línea, cuando se abre la aplicación (BGTaskScheduler. El tiempo de ejecución está determinado por iOS). Las notas que no se pudieron enviar se enviarán de nuevo sin eliminarlas de la espera de la transmisión, pero hay un límite en el número de veces que se reenvía automáticamente.
Artículos relacionados
Referencias
- Apple Desarrollador — AES.GCM (CryptoKit) — Referencia oficial al cifrado de autenticación AES-GCM utilizado para cifrar los datos de Outbox
- Apple Desarrollador — NWPathMonitor (Network Framework) — Utilizado para detección sin conexión y monitoreo del estado de la red
- Apple Desarrollador — BGTaskScheduler — Utilizado para reenviar la programación de procesamiento en segundo plano
- Chris Richardson — Patrón: Outbox transaccional (microservices.io) — Explicación del patrón de Outbox transaccional (escriba el mensaje que se enviará en la mesa de espera en el mismo proceso, y se envía otro proceso)