Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Application/App/Sources/App/Delegate/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}

// 앱이 온그라운드로 되었을 때, 로그인 세션이 존재한다면 현재 유저의 timeZone 저장
NotificationCenter.default.post(name: .didRequestUserTimeZoneSync, object: nil)

// Firebase Messaging 설정
container.resolve(PushMessagingService.self).setDelegate(self)

Expand Down
57 changes: 53 additions & 4 deletions Application/App/Sources/App/Handler/UserTimeZoneSyncHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ import Data
import Foundation

final class UserTimeZoneSyncHandler {
private struct SyncKey: Equatable {
private struct SyncKey: Hashable {
let uid: String
let timeZoneIdentifier: String
}

private enum SyncStartResult {
case started
case alreadySynced
case alreadySyncing
}

private let authService: AuthService
private let userService: UserService
private let logger = Logger(category: "UserTimeZoneSyncHandler")
private let lock = NSLock()
private var lastSyncedKey: SyncKey?
private var syncingKeys = Set<SyncKey>()
Comment thread
opficdev marked this conversation as resolved.
private var cancellables = Set<AnyCancellable>()

init(
Expand All @@ -31,6 +39,7 @@ final class UserTimeZoneSyncHandler {
self.userService = userService

authService.observeSignedIn()
.removeDuplicates()
Comment thread
opficdev marked this conversation as resolved.
.sink { [weak self] isSignedIn in
self?.handleSessionUpdate(isSignedIn: isSignedIn)
}
Expand All @@ -47,7 +56,7 @@ final class UserTimeZoneSyncHandler {
private extension UserTimeZoneSyncHandler {
func handleSessionUpdate(isSignedIn: Bool) {
guard isSignedIn else {
lastSyncedKey = nil
resetSyncState()
return
}

Expand All @@ -71,16 +80,56 @@ private extension UserTimeZoneSyncHandler {
timeZoneIdentifier: TimeZone.autoupdatingCurrent.identifier
)

guard lastSyncedKey != key else {
switch beginSync(for: key) {
case .started:
break
case .alreadySynced:
logger.info("Skipping timeZone update because the current user timeZone is already synced")
return
case .alreadySyncing:
logger.info("Skipping timeZone update because the current user timeZone is already syncing")
return
}

do {
try await userService.updateUserTimeZone()
lastSyncedKey = key
finishSync(for: key, didSucceed: true)
} catch {
finishSync(for: key, didSucceed: false)
logger.error("Failed to sync user timeZone", error: error)
}
}

private func resetSyncState() {
lock.lock()
defer { lock.unlock() }

lastSyncedKey = nil
syncingKeys.removeAll()
}

private func beginSync(for key: SyncKey) -> SyncStartResult {
lock.lock()
defer { lock.unlock() }

if lastSyncedKey == key {
return .alreadySynced
}
if syncingKeys.contains(key) {
return .alreadySyncing
}

syncingKeys.insert(key)
return .started
}

private func finishSync(for key: SyncKey, didSucceed: Bool) {
lock.lock()
defer { lock.unlock() }

syncingKeys.remove(key)
if didSucceed {
lastSyncedKey = key
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ struct UserTimeZoneSyncHandlerTests {
_ = handler
}

@Test("같은 로그인 상태가 연속 방출되면 현재 timeZone을 한 번만 저장한다")
func 같은_로그인_상태가_연속_방출되면_현재_timeZone을_한_번만_저장한다() async throws {
let userService = UserServiceSpy(updateDelay: .milliseconds(100))
let authService = AuthServiceSpy(uid: "user-id")
let handler = UserTimeZoneSyncHandler(
authService: authService,
userService: userService
)

authService.updateSession(uid: "user-id")
authService.updateSession(uid: "user-id")

try await waitUntil {
await userService.updateUserTimeZoneCallCount == 1
}
try await Task.sleep(for: .milliseconds(150))
#expect(await userService.updateUserTimeZoneCallCount == 1)
_ = handler
}

@Test("foreground 복귀 시 현재 timeZone을 요청하되 같은 사용자와 같은 timeZone은 중복 저장하지 않는다")
func foreground_복귀_시_현재_timeZone을_요청하되_같은_사용자와_같은_timeZone은_중복_저장하지_않는다() async throws {
let notificationCenter = NotificationCenter()
Expand All @@ -71,8 +91,30 @@ struct UserTimeZoneSyncHandlerTests {
_ = handler
}

@Test("같은 timeZone이어도 사용자가 바뀌면 다시 저장한다")
func 같은_timeZone이어도_사용자가_바뀌면_다시_저장한다() async throws {
@Test("현재 timeZone 저장 중 foreground 요청이 들어오면 중복 저장하지 않는다")
func 현재_timeZone_저장_중_foreground_요청이_들어오면_중복_저장하지_않는다() async throws {
let notificationCenter = NotificationCenter()
let userService = UserServiceSpy(updateDelay: .milliseconds(100))
let authService = AuthServiceSpy(uid: "user-id")
let handler = UserTimeZoneSyncHandler(
authService: authService,
userService: userService,
notificationCenter: notificationCenter
)

authService.updateSession(uid: "user-id")
try await waitUntil {
await userService.updateUserTimeZoneCallCount == 1
}

notificationCenter.post(name: .didRequestUserTimeZoneSync, object: nil)
try await Task.sleep(for: .milliseconds(150))
#expect(await userService.updateUserTimeZoneCallCount == 1)
_ = handler
}

@Test("로그아웃 후 다른 사용자로 로그인하면 같은 timeZone이어도 다시 저장한다")
func 로그아웃_후_다른_사용자로_로그인하면_같은_timeZone이어도_다시_저장한다() async throws {
let userService = UserServiceSpy()
let authService = AuthServiceSpy(uid: "first-user")
let handler = UserTimeZoneSyncHandler(
Expand All @@ -85,6 +127,7 @@ struct UserTimeZoneSyncHandlerTests {
await userService.updateUserTimeZoneCallCount == 1
}

authService.updateSession(uid: nil)
authService.updateSession(uid: "second-user")
try await waitUntil {
await userService.updateUserTimeZoneCallCount == 2
Expand Down Expand Up @@ -119,11 +162,13 @@ struct UserTimeZoneSyncHandlerTests {

@Test("timeZone 저장 실패 시 캐시를 갱신하지 않는다")
func timeZone_저장_실패_시_캐시를_갱신하지_않는다() async throws {
let notificationCenter = NotificationCenter()
let userService = UserServiceSpy(updateError: TestError.updateFailed)
let authService = AuthServiceSpy(uid: "user-id")
let handler = UserTimeZoneSyncHandler(
authService: authService,
userService: userService
userService: userService,
notificationCenter: notificationCenter
)

authService.updateSession(uid: "user-id")
Expand All @@ -132,7 +177,7 @@ struct UserTimeZoneSyncHandlerTests {
}

await userService.setUpdateError(nil)
authService.updateSession(uid: "user-id")
notificationCenter.post(name: .didRequestUserTimeZoneSync, object: nil)
try await waitUntil {
await userService.updateUserTimeZoneCallCount == 2
}
Expand All @@ -142,10 +187,12 @@ struct UserTimeZoneSyncHandlerTests {

private actor UserServiceSpy: UserService {
private var updateError: Error?
private let updateDelay: Duration?
private(set) var updateUserTimeZoneCallCount = 0

init(updateError: Error? = nil) {
init(updateError: Error? = nil, updateDelay: Duration? = nil) {
self.updateError = updateError
self.updateDelay = updateDelay
}

func upsertUser(_ response: AuthDataResponse) async throws { }
Expand All @@ -155,6 +202,9 @@ private actor UserServiceSpy: UserService {

func updateUserTimeZone() async throws {
updateUserTimeZoneCallCount += 1
if let updateDelay {
try await Task.sleep(for: updateDelay)
}
if let updateError {
throw updateError
}
Expand Down
Loading