Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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,33 @@ 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 {
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