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
8 changes: 6 additions & 2 deletions Projects/App/Sources/DI/Assembly/DataAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public class DataAssembly: Assembly {
DefaultExchangeRateService()
}

// MARK: DataSource
container.register(CoinTradeDataSource.self) { _ in
BinanceCoinTradeDataSource()
}

// MARK: Repository
container.register(UserConfigurationRepository.self) { _ in
Expand All @@ -63,8 +67,8 @@ public class DataAssembly: Assembly {
BinanceSingleMarketTickerRepository()
}

container.register(TradeRepository.self) { _ in
BinanceTradeRepository()
container.register(CoinTradeRepository.self) { _ in
BinanceCoinTradeRepository()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// CoinTradeDataSource.swift
// Data
//
// Created by choijunios on 4/21/25.
//

import Combine

import Foundation
import CoreUtil

public protocol CoinTradeDataSource {
func getTradeList(symbolPair: String, tableUpdateInterval: Double?) -> AnyPublisher<HashMap<Int64, BinanceCoinTradeDTO>, Never>
}

public final class BinanceCoinTradeDataSource: CoinTradeDataSource {
// Dependency
@Injected private var webSocketService: WebSocketService

// State
private let tradeList: ThreadSafeHashMap<Int64, BinanceCoinTradeDTO> = .init()

public init() { }

public func getTradeList(symbolPair: String, tableUpdateInterval: Double?) -> AnyPublisher<HashMap<Int64, BinanceCoinTradeDTO>, Never> {
webSocketService
.getMessageStream()
.filter { (dto: BinanceCoinTradeDTO) in
dto.symbol.lowercased() == symbolPair.lowercased()
}
.throttle(for: .init(floatLiteral: tableUpdateInterval ?? 0.0), scheduler: DispatchQueue.global(qos: .default), latest: true)
.unretained(self)
.asyncTransform { source, dto in
await source.tradeList.insert(key: dto.tradeTime, value: dto)
return await source.tradeList.hashMap.copy()
}
}
}
35 changes: 35 additions & 0 deletions Projects/Data/Repository/Sources/BinanceCoinTradeRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// BinanceTradeRepository.swift
// Data
//
// Created by choijunios on 4/15/25.
//

import Foundation
import Combine

import DomainInterface
import DataSource
import CoreUtil

public final class BinanceCoinTradeRepository: CoinTradeRepository {
// Dependency
@Injected private var webSocketService: WebSocketService
@Injected private var coinTradeDataSource: CoinTradeDataSource

public init() { }

public func getCoinTradeList(symbolPair: String, tableUpdateInterval: Double?) -> AnyPublisher<HashMap<Date, CoinTradeVO>, Never> {
coinTradeDataSource
.getTradeList(symbolPair: symbolPair, tableUpdateInterval: tableUpdateInterval)
.map { dtoList in
var entityList = HashMap<Date, CoinTradeVO>()
dtoList.values.forEach { dto in
let entity = dto.toEntity()
entityList[entity.tradeTime] = entity
}
return entityList
}
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public final class BinanceOrderbookRepository: OrderbookRepository {
@Injected private var httpService: HTTPService

// Store
private let bidOrderbookStore = ThreadSafeOrderbookHashMap()
private let askOrderbookStore = ThreadSafeOrderbookHashMap()
private let bidOrderbookStore = ThreadSafeHashMap<CVNumber, CVNumber>()
private let askOrderbookStore = ThreadSafeHashMap<CVNumber, CVNumber>()

public init() { }
}
Expand Down Expand Up @@ -79,7 +79,7 @@ private extension BinanceOrderbookRepository {

enum StoreType { case bid, ask }
func adaptToStore(orderbooks: [[String]], storeType: StoreType) async {
var store: ThreadSafeOrderbookHashMap
var store: ThreadSafeHashMap<CVNumber, CVNumber>
switch storeType {
case .bid:
store = bidOrderbookStore
Expand Down
35 changes: 0 additions & 35 deletions Projects/Data/Repository/Sources/BinanceTradeRepository.swift

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final public class DefaultCoinDetailPageUseCase: CoinDetailPageUseCase {
// Dependency
@Injected private var orderbookRepository: OrderbookRepository
@Injected private var singleTickerRepository: SingleMarketTickerRepository
@Injected private var coinTradeRepository: TradeRepository
@Injected private var coinTradeRepository: CoinTradeRepository
@Injected private var webSocketHelper: WebSocketManagementHelper

public init() { }
Expand Down Expand Up @@ -49,8 +49,18 @@ public extension DefaultCoinDetailPageUseCase {
singleTickerRepository.request24hTickerChange(pairSymbol: symbolPair)
}

func getRecentTrade(symbolPair: String) -> AsyncStream<CoinTradeVO> {
coinTradeRepository.getSingleTrade(symbolPair: symbolPair)
func getRecentTrade(symbolPair: String, maxRowCount: UInt) -> AnyPublisher<[CoinTradeVO], Never> {
coinTradeRepository
.getCoinTradeList(symbolPair: symbolPair, tableUpdateInterval: 0.5)
.map { entity in
// 최신으로 maxRowCount개수 만큼
let slicedList = entity
.keys(order: .DESC, maxCount: maxRowCount)
.compactMap { key in entity[key] }
.prefix(Int(maxRowCount))
return Array(slicedList)
}
.eraseToAnyPublisher()
}

func getOrderbookTable(symbolPair: String, rowCount: UInt) -> AnyPublisher<OrderbookTableVO, Error> {
Expand Down
15 changes: 15 additions & 0 deletions Projects/Domain/Interface/Repository/CoinTradeRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// CoinTradeRepository.swift
// Domain
//
// Created by choijunios on 4/15/25.
//

import Combine
import Foundation

import CoreUtil

public protocol CoinTradeRepository {
func getCoinTradeList(symbolPair: String, tableUpdateInterval: Double?) -> AnyPublisher<HashMap<Date, CoinTradeVO>, Never>
}
10 changes: 0 additions & 10 deletions Projects/Domain/Interface/Repository/TradeRepository.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ public protocol CoinDetailPageUseCase {
func disconnectToStreams(symbolPair: String)

func getOrderbookTable(symbolPair: String, rowCount: UInt) -> AnyPublisher<OrderbookTableVO, Error>
func getRecentTrade(symbolPair: String, maxRowCount: UInt) -> AnyPublisher<[CoinTradeVO], Never>
func get24hTickerChange(symbolPair: String) -> AsyncStream<Twenty4HourTickerForSymbolVO>
func getRecentTrade(symbolPair: String) -> AsyncStream<CoinTradeVO>
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public class Assemblies: Assembly {
}
.inObjectScope(.container)

// MARK: DataSource
container.register(CoinTradeDataSource.self) { _ in
BinanceCoinTradeDataSource()
}

// MARK: Shared
container.register(AlertShooter.self) { _ in AlertShooter() }
.inObjectScope(.container)
Expand All @@ -48,8 +53,8 @@ public class Assemblies: Assembly {
container.register(SingleMarketTickerRepository.self) { _ in
BinanceSingleMarketTickerRepository()
}
container.register(TradeRepository.self) { _ in
BinanceTradeRepository()
container.register(CoinTradeRepository.self) { _ in
BinanceCoinTradeRepository()
}

// MARK: UseCase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ final class CoinDetailPageViewModel: UDFObservableObject, CoinDetailPageViewMode
private var symbolPair: String { symbolInfo.pairSymbol }
private var hasAppeared = false
private let orderbookRowCount: Int = 15
private let tradeContainer: TradeContainer = .init(maxCount: 15)


private let recentTradeRowCount: Int = 20

// Action
typealias Action = CoinDetailPageAction
let action: PassthroughSubject<CoinDetailPageAction, Never> = .init()
Expand Down Expand Up @@ -213,26 +212,14 @@ private extension CoinDetailPageViewModel {
// MARK: Recent trade
private extension CoinDetailPageViewModel {
func listenToRecentTradeStream() {
let updateTracker = PassthroughSubject<CoinTradeVO, Never>()
streamUpdateObserverStore[.recentTrade]?.cancel()
streamUpdateObserverStore[.recentTrade] = updateTracker
streamUpdateObserverStore[.recentTrade] = useCase
.getRecentTrade(symbolPair: symbolPair, maxRowCount: UInt(recentTradeRowCount))
.throttle(for: 1.0, scheduler: DispatchQueue.global(), latest: true)
.unretained(self)
.asyncTransform { vm, update in
// - 주문정보 업데이트
await vm.tradeContainer.insert(element: update)

// - 새로운 리스트 추출
let newList = await vm.tradeContainer.getList()
return Action.updateTrades(trades: newList)
.map { list in
return Action.updateTrades(trades: list)
}
.subscribe(self.action)

streamTask[.recentTrade]?.cancel()
streamTask[.recentTrade] = Task { [updateTracker, weak self] in
guard let self else { return }
for await entity in useCase.getRecentTrade(symbolPair: symbolPair) { updateTracker.send(entity) }
}
}

func convertToRO(_ entity: CoinTradeVO) -> CoinTradeRO {
Expand Down

This file was deleted.

34 changes: 1 addition & 33 deletions Projects/Features/CoinDetail/Tests/TradeContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,5 @@ import Testing
@testable import CoinDetailFeature

struct TradeContainerRuntimeErrorTest {
@Test("단기간 동시 접근시 런타임 에러 확인")
func shortTermConcurrency() async {
// Given
let container = TradeContainer(maxCount: 1000)

// When
var tasks: [Task<Void, Never>] = []
for index in 0..<1000 {
let insertTask = Task<Void, Never>.detached {
try? await Task.sleep(for: .seconds(1))
await container.insert(element: .init(
tradeId: String(index),
tradeType: .buy,
price: .init(0.0),
quantity: .init(0.0),
tradeTime: .now
))
}
tasks.append(insertTask)

let getTask = Task<Void, Never>.detached {
try? await Task.sleep(for: .seconds(0.5))
_ = await container.getList()
}
tasks.append(getTask)
}

// Then
for task in tasks {
_ = await task.value
}
#expect(await container.getList().count == 1000)
}

}
Loading
Loading