mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-22 00:01:25 +00:00
695e2b5a33
1) Contacts crash "c is null": contactRepository.Search declared `var contacts []models.Contact` so an empty result set returned a nil slice, which Go marshals as JSON null. The frontend's flatMap((p) => p.data) over null yields [null], and the page then accesses c.subscribed → throws. Initialize as make([]models.Contact, 0, limit+1) so the wire format is always []. Also defensive on the client: useSearchContacts + useCampaigns now coerce p.data ?? [] and drop nulls before returning. 2) Campaigns panic on any non-empty result: campaignRepository.Search allocated `make([]models.Campaign, 0, limit+1)` (length 0) then did `campaigns[i] = campaign`. That's an index-out-of-range on the first iteration. Switched to `append`. Anyone with at least one campaign would see a 500 / blank screen. 3) Websocket "Token expired": SocketTTL was 60s. The frontend reconnect backoff caps at 30s, so after a rejected handshake the next attempt could fire 30-60s later. Combined with rare back-pressure on /getaway the token was already past exp by the time the realtime saw it. Bumped to 10 min — short enough to keep the token low-impact, long enough to outlast the backoff schedule. 4) Websocket "Connection limit exceeded": Realtime.Connections only untracked on channel terminate, never on socket disconnect. Sockets that connected and disconnected without joining a channel leaked. Each reconnect loop bumped the counter until the per-user limit (10) was hit, after which every legitimate connect was rejected even after fixing #3. Fix: GenServer Process.monitor's the socket pid on track, and `:DOWN` handler calls do_untrack with the right (user_id, ip). 5) Phoenix protocol mismatch: Frontend appended vsn=2.0.0 to the WS URL, but sendRaw + joinChannel send the V1 object format. Realtime's Phoenix.Socket.V2.JSONSerializer crashed with a badmatch on the first phx_join, killing the socket right after connect. Switched to vsn=1.0.0 to match what the client actually emits.
Warmbly Realtime
WebSocket gateway for real-time events in Warmbly. Built with Phoenix Channels and Google Pub/Sub.
Architecture
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Go Backend │────>│ Google Pub/Sub │────>│ Elixir Realtime │
└─────────────┘ └─────────────────┘ └────────┬────────┘
│
│ WebSocket
v
┌─────────────────┐
│ React Frontend │
└─────────────────┘
Channels
user:{user_id}- User-specific events (emails, account status, bulk operations)campaign:{campaign_id}- Campaign progress and status updatesaccount:{account_id}- Email account sync status and errorsbulk:{operation_id}- Bulk operation progress
Event Types
User Events
EMAIL_RECEIVED- New email in inboxACCOUNT_CONNECTED/ACCOUNT_DISCONNECTED/ACCOUNT_ERRORBULK_STARTED/BULK_PROGRESS/BULK_COMPLETED
Campaign Events
CAMPAIGN_STARTED/CAMPAIGN_PAUSED/CAMPAIGN_COMPLETEDCAMPAIGN_PROGRESS- Emails sent, opens, clicks
Account Events
ACCOUNT_SYNCED- Sync completedWARMUP_UPDATE- Warmup statistics
Setup
Prerequisites
- Elixir 1.18+
- Google Cloud project with Pub/Sub enabled
Environment Variables
# Required
JWT_SECRET=your_jwt_secret
SECRET_KEY_BASE=your_secret_key_base_min_64_chars
GCP_PROJECT_ID=your_gcp_project
# Optional
PORT=4000
PUBSUB_ENABLED=true
SENTRY_DSN=your_sentry_dsn
GOOGLE_APPLICATION_CREDENTIALS_JSON='{"type":"service_account",...}'
Development
# Install dependencies
mix deps.get
# Start server
mix phx.server
# Or in interactive mode
iex -S mix phx.server
Production
# Build release
MIX_ENV=prod mix release
# Run
_build/prod/rel/realtime/bin/realtime start
Client Connection
import { Socket } from "phoenix";
const socket = new Socket("wss://realtime.warmbly.com/socket", {
params: { token: "jwt_token_from_api" }
});
socket.connect();
// Join user channel
const userChannel = socket.channel(`user:${userId}`, {});
userChannel.join()
.receive("ok", () => console.log("Joined user channel"))
.receive("error", (resp) => console.error("Unable to join", resp));
// Listen for events
userChannel.on("EMAIL_RECEIVED", (payload) => {
console.log("New email:", payload);
});
// Join campaign channel
const campaignChannel = socket.channel(`campaign:${campaignId}`, {});
campaignChannel.join();
campaignChannel.on("CAMPAIGN_PROGRESS", (payload) => {
console.log("Campaign progress:", payload);
});
Endpoints
GET /health- Health checkGET /stats- Connection statisticsWS /socket- WebSocket endpoint