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
7 changes: 6 additions & 1 deletion Application/DevLogData/Sources/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ public final class DataAssembler: Assembler {
)
}

container.register(TodoMutationEventBus.self) {
TodoMutationEventBusImpl()
}
Comment thread
opficdev marked this conversation as resolved.

container.register(TodoRepository.self) {
TodoRepositoryImpl(
todoService: container.resolve(TodoService.self),
todoCategoryService: container.resolve(TodoCategoryService.self),
widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self)
widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self),
todoMutationEventBus: container.resolve(TodoMutationEventBus.self)
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// TodoMutationEventBusImpl.swift
// DevLogData
//
// Created by opfic on 6/6/26.
//

import Foundation
import DevLogDomain

actor TodoMutationEventBusImpl: TodoMutationEventBus {
private var continuations = [UUID: AsyncStream<TodoMutationEvent>.Continuation]()

func publish(_ event: TodoMutationEvent) async {
continuations.values.forEach { $0.yield(event) }
}

func events() -> AsyncStream<TodoMutationEvent> {
let id = UUID()
let (stream, continuation) = AsyncStream.makeStream(of: TodoMutationEvent.self)

continuations[id] = continuation
continuation.onTermination = { [weak self] _ in
Task {
await self?.removeContinuation(id: id)
}
}

return stream
}
}

private extension TodoMutationEventBusImpl {
func removeContinuation(id: UUID) {
continuations[id] = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ final class TodoRepositoryImpl: TodoRepository {
private let todoService: TodoService
private let todoCategoryService: TodoCategoryService
private let widgetSyncEventBus: WidgetSyncEventBus
private let todoMutationEventBus: TodoMutationEventBus

init(
todoService: TodoService,
todoCategoryService: TodoCategoryService,
widgetSyncEventBus: WidgetSyncEventBus
widgetSyncEventBus: WidgetSyncEventBus,
todoMutationEventBus: TodoMutationEventBus
) {
self.todoService = todoService
self.todoCategoryService = todoCategoryService
self.widgetSyncEventBus = widgetSyncEventBus
self.todoMutationEventBus = todoMutationEventBus
}

func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage {
Expand Down Expand Up @@ -107,6 +110,7 @@ final class TodoRepositoryImpl: TodoRepository {
func upsertTodo(_ todo: Todo) async throws {
let todoRequest = TodoRequest.fromDomain(todo)
try await upsertTodo(todoRequest)
await todoMutationEventBus.publish(.updated(todo.id))
}

func upsertTodo(_ todoDraft: TodoDraft) async throws {
Expand All @@ -127,6 +131,7 @@ final class TodoRepositoryImpl: TodoRepository {
do {
try await todoService.deleteTodo(todoId: todoId)
widgetSyncEventBus.publish(.syncRequested)
await todoMutationEventBus.publish(.deleted(todoId))
} catch {
throw error.toDomain()
}
Expand All @@ -136,6 +141,7 @@ final class TodoRepositoryImpl: TodoRepository {
do {
try await todoService.undoDeleteTodo(todoId: todoId)
widgetSyncEventBus.publish(.syncRequested)
await todoMutationEventBus.publish(.restored(todoId))
} catch {
throw error.toDomain()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// TodoMutationEventBusImplTests.swift
// DevLogDataTests
//
// Created by opfic on 6/6/26.
//

import Testing
import DevLogDomain
@testable import DevLogData

struct TodoMutationEventBusImplTests {
@Test("TodoMutationEventBus는 발행된 이벤트를 관찰자에게 전달한다")
func todoMutationEventBus는_발행된_이벤트를_관찰자에게_전달한다() async {
let bus = TodoMutationEventBusImpl()
let events = await bus.events()
let task = Task {
var iterator = events.makeAsyncIterator()
return await iterator.next()
}

await bus.publish(.updated("todo-id"))

let event = await task.value
#expect(event == .updated("todo-id"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import Foundation
import Testing
import DevLogCore
import DevLogDomain
@testable import DevLogData
@testable @preconcurrency import DevLogData

struct TodoRepositoryImplTests {
@Test("Todo 변경 성공 시 위젯 동기화 이벤트를 발행한다")
func todo_변경_성공_시_위젯_동기화_이벤트를_발행한다() async throws {
@Test("Todo 변경 성공 시 위젯 동기화와 mutation 이벤트를 발행한다")
func todo_변경_성공_시_위젯_동기화와_mutation_이벤트를_발행한다() async throws {
let fixture = makeFixture()
let todo = makeTodo()

Expand All @@ -24,10 +24,13 @@ struct TodoRepositoryImplTests {

let events = fixture.widgetSyncEventBus.events
#expect(events == [.syncRequested, .syncRequested, .syncRequested])

let mutationEvents = await fixture.todoMutationEventBus.publishedEvents()
#expect(mutationEvents == [.updated(todo.id), .deleted(todo.id), .restored(todo.id)])
}

@Test("Todo 변경 실패 시 위젯 동기화 이벤트를 발행하지 않는다")
func todo_변경_실패_시_위젯_동기화_이벤트를_발행하지_않는다() async throws {
@Test("Todo 변경 실패 시 위젯 동기화와 mutation 이벤트를 발행하지 않는다")
func todo_변경_실패_시_위젯_동기화와_mutation_이벤트를_발행하지_않는다() async throws {
let fixture = makeFixture()
let todo = makeTodo()

Expand All @@ -52,24 +55,30 @@ struct TodoRepositoryImplTests {
#expect(error as? TodoRepositoryImplTestsError == .serviceFailed)
}

let events = fixture.widgetSyncEventBus.events
#expect(events.isEmpty)
let syncEvents = fixture.widgetSyncEventBus.events
#expect(syncEvents.isEmpty)

let mutationEvents = await fixture.todoMutationEventBus.publishedEvents()
#expect(mutationEvents.isEmpty)
}

private func makeFixture() -> Fixture {
let todoService = TodoServiceSpy()
let todoCategoryService = TodoCategoryServiceSpy()
let widgetSyncEventBus = WidgetSyncEventBusSpy()
let todoMutationEventBus = TodoMutationEventBusSpy()
let repository = TodoRepositoryImpl(
todoService: todoService,
todoCategoryService: todoCategoryService,
widgetSyncEventBus: widgetSyncEventBus
widgetSyncEventBus: widgetSyncEventBus,
todoMutationEventBus: todoMutationEventBus
)

return Fixture(
repository: repository,
todoService: todoService,
widgetSyncEventBus: widgetSyncEventBus
widgetSyncEventBus: widgetSyncEventBus,
todoMutationEventBus: todoMutationEventBus
)
}

Expand Down Expand Up @@ -97,6 +106,7 @@ private struct Fixture {
let repository: TodoRepositoryImpl
let todoService: TodoServiceSpy
let widgetSyncEventBus: WidgetSyncEventBusSpy
let todoMutationEventBus: TodoMutationEventBusSpy
}

private actor TodoServiceSpy: TodoService {
Expand Down Expand Up @@ -159,6 +169,24 @@ private final class WidgetSyncEventBusSpy: WidgetSyncEventBus {
}
}

private actor TodoMutationEventBusSpy: TodoMutationEventBus {
private var capturedEvents = [TodoMutationEvent]()

func publish(_ event: TodoMutationEvent) async {
capturedEvents.append(event)
}

func publishedEvents() -> [TodoMutationEvent] {
capturedEvents
}

func events() async -> AsyncStream<TodoMutationEvent> {
AsyncStream { continuation in
continuation.finish()
}
}
}

private enum TodoRepositoryImplTestsError: Error, Equatable {
case serviceFailed
case unexpectedCall
Expand Down
12 changes: 12 additions & 0 deletions Application/DevLogDomain/Sources/Entity/TodoMutationEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// TodoMutationEvent.swift
// DevLogDomain
//
// Created by opfic on 6/6/26.
//

public enum TodoMutationEvent: Equatable, Sendable {
case updated(String)
case deleted(String)
case restored(String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// TodoMutationEventBus.swift
// DevLogDomain
//
// Created by opfic on 6/6/26.
//

public protocol TodoMutationEventBus: Sendable {
func publish(_ event: TodoMutationEvent) async
func events() async -> AsyncStream<TodoMutationEvent>
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ import DevLogDomain
@MainActor
@Observable
final class HomeViewCoordinator {
private enum AsyncStreamTaskID {
case todoMutationEvent
}

let viewModel: HomeViewModel
let router = NavigationRouter<HomeRoute>()
private let container: DIContainer
@ObservationIgnored
private var cancellable: AnyCancellable?
@ObservationIgnored
private var streamTasks = [AsyncStreamTaskID: Task<Void, Never>]()

init(container: DIContainer) {
self.container = container
Expand All @@ -34,10 +40,34 @@ final class HomeViewCoordinator {
)
}

deinit {
streamTasks.values.forEach { $0.cancel() }
}

func fetchData() {
viewModel.send(.fetchData)
}

func refreshRecentTodos() {
viewModel.send(.refreshRecentTodos)
}

func bindTodoMutationEvent() {
guard streamTasks[.todoMutationEvent] == nil else { return }

let bus = container.resolve(TodoMutationEventBus.self)
streamTasks[.todoMutationEvent] = Task { [weak self] in
let events = await bus.events()
for await event in events {
guard let self else { break }
switch event {
case .updated, .deleted, .restored:
self.refreshRecentTodos()
}
}
}
Comment thread
opficdev marked this conversation as resolved.
}

func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) {
guard cancellable == nil else { return }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ final class HomeViewModel: StorePattern {

enum Action {
case fetchData
case refreshRecentTodos
case networkStatusChanged(Bool)
case setPresentation(Presentation, Bool)
case setAlert(isPresented: Bool, type: AlertType? = nil)
Expand Down Expand Up @@ -136,7 +137,7 @@ final class HomeViewModel: StorePattern {
switch action {
case .networkStatusChanged(let isConnected):
state.isNetworkConnected = isConnected
case .fetchData, .setPresentation, .setAlert, .refreshWebPages,
case .fetchData, .refreshRecentTodos, .setPresentation, .setAlert, .refreshWebPages,
.tapTodoCategory, .orderTodoCategory, .updateWebPageURLInput,
.addWebPage, .deleteWebPage, .undoDeleteWebPage, .finishDeleteWebPageToast:
effects = reduceByView(action, state: &state)
Expand Down Expand Up @@ -252,6 +253,8 @@ private extension HomeViewModel {
switch action {
case .fetchData:
return [.fetchTodoCategoryPreferences, .fetchRecentTodos, .fetchWebPages]
case .refreshRecentTodos:
return [.fetchRecentTodos]
case .refreshWebPages:
return [.fetchWebPages]
case .setPresentation(let presentation, let isPresented):
Expand Down
1 change: 1 addition & 0 deletions Application/DevLogPresentation/Sources/Main/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct MainView: View {
.onAppear {
coordinator.viewModel.send(.onAppear)
homeViewCoordinator.bindWindowEvent(windowEvent)
homeViewCoordinator.bindTodoMutationEvent()
todoWindowCoordinator.bindWindowEvent(windowEvent)
}
.onChange(of: selectedTab, initial: true) { _, newValue in
Expand Down