diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 6a6a4255..942024d0 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -321,7 +321,29 @@ func (m *MetaClient) wrapBackfillEvents(ctx context.Context, portal *bridgev2.Po if anchor != nil { if forward { upsert.Messages = slices.DeleteFunc(upsert.Messages, func(message *table.WrappedMessage) bool { - return message.TimestampMs <= anchor.Timestamp.UnixMilli() + if message.TimestampMs > anchor.Timestamp.UnixMilli() { + return false + } + // This message is older than the portal's latest bridged message, so it would + // normally be assumed to already exist. However, Meta can skip threads in a + // reconnect snapshot while advancing the sync cursor past their messages (see + // PLAT-36990), which leaves permanent mid-timeline gaps. Only drop the message + // if it actually exists in the bridge database, otherwise recover it. + existing, err := m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.UserLogin.ID, metaid.MakeFBMessageID(message.MessageId)) + if err != nil { + zerolog.Ctx(ctx).Err(err). + Str("message_id", message.MessageId). + Msg("Failed to check message existence during forward backfill dedup, dropping") + return true + } + if existing == nil { + zerolog.Ctx(ctx).Warn(). + Str("message_id", message.MessageId). + Int64("timestamp_ms", message.TimestampMs). + Msg("Recovering pre-anchor message missing from database in forward backfill") + return false + } + return true }) } else { upsert.Messages = slices.DeleteFunc(upsert.Messages, func(message *table.WrappedMessage) bool { diff --git a/pkg/messagix/syncManager.go b/pkg/messagix/syncManager.go index e48ec4ef..428e8890 100644 --- a/pkg/messagix/syncManager.go +++ b/pkg/messagix/syncManager.go @@ -125,8 +125,12 @@ func (sm *SyncManager) SyncSocketData(ctx context.Context, databaseID int64, db resp.Finish() if len(resp.Table.LSHandleSyncFailure) > 0 { - // TODO handle these somehow? - sm.client.Logger.Warn(). + // These failures mean the server-side sync for the database errored and no + // cursor was obtained, so the database stays unsynced until the next attempt. + // A common cause is empty sync params (handled in getSyncParams, see + // PLAT-36990); log at error level with the database ID so it's visible. + sm.client.Logger.Error(). + Int64("database_id", databaseID). Any("sync_failures", resp.Table.LSHandleSyncFailure). Msg("Sync failures found") } @@ -236,18 +240,30 @@ func (sm *SyncManager) UpdateDatabaseSyncParams(dbs []*socket.QueryMetadata) err var dbID7Params = `{"mnet_rank_types":[44]}` +// defaultLocaleSyncParams is the fallback sync params payload used when the page +// config doesn't provide LSPlatformMessengerSyncParams. messenger-lite sessions +// don't include these, so an empty string would make Meta's server fail with +// "Invalid argument supplied for foreach()" (see PLAT-36990), permanently +// breaking the affected sync database (e.g. Contact sync on database 2). +var defaultLocaleSyncParams = `{"locale":"en_US"}` + func (sm *SyncManager) getSyncParams(dbID int64, ch socket.SyncChannel) *string { if dbID == 7 { return &dbID7Params } + var params *string switch ch { case socket.MailBox: - return &sm.syncParams.Mailbox + params = &sm.syncParams.Mailbox case socket.Contact: - return &sm.syncParams.Contact + params = &sm.syncParams.Contact default: - return &sm.syncParams.E2Ee + params = &sm.syncParams.E2Ee + } + if params == nil || *params == "" { + return &defaultLocaleSyncParams } + return params } func (sm *SyncManager) GetCursor(db int64) string {