Implement subscribe/unsubscribe for ContractPriceWebSocket

This commit is contained in:
2023-08-21 23:13:17 +09:00
parent 02dcb4423e
commit 2f81e398b1
9 changed files with 224 additions and 57 deletions

View File

@@ -13,6 +13,8 @@ public enum GeneralError: Error {
case invalidInstance
case invalidAccessToken
case invalidAccountNo
case invalidWebSocket
case webSocketError(_ code: String, _ messageCode: String, _ message: String)
case unsupportedQueryAtMockServer
case emptyData(String)
case cannotReadFile

View File

@@ -9,19 +9,15 @@ import Foundation
public protocol WebSocket {
associatedtype KResult: Decodable
var isMockAvailable: Bool { get }
var domain: String { get }
var url: String { get }
var userAgent: String { get }
var header: [String: String?] { get }
var body: [String: Any] { get }
var result: KResult? { get set }
var timeout: TimeInterval { get }
var session: URLSession { get }
var socket: URLSessionWebSocketTask? { get set }
var credential: WebSocketCredential { get }
}
@@ -45,25 +41,27 @@ extension WebSocket {
URL(string: domain + url)
}
public mutating func connect() async throws {
return try await withUnsafeThrowingContinuation { continuation in
/*
mutating func connect() async throws -> URLSessionWebSocketTask {
return try await withUnsafeThrowingContinuation { continuation in
guard let queryUrl = queryUrl else {
continuation.resume(throwing: QueryError.invalidUrl)
return
}
socket = session.webSocketTask(with: queryUrl)
socket?.resume()
let socket = session.webSocketTask(with: queryUrl)
socket.resume()
self.socket = socket
continuation.resume(returning: ())
continuation.resume(returning: socket)
}
}
public mutating func disconnect() {
mutating func disconnect() {
socket?.cancel(with: .normalClosure, reason: nil)
socket = nil
}
*/
}
@@ -83,6 +81,41 @@ extension AuthWebSocket {
"ws://ops.koreainvestment.com:31000":
"ws://ops.koreainvestment.com:21000"
}
public mutating func connect() async throws {
return try await withUnsafeThrowingContinuation { continuation in
guard let queryUrl = queryUrl else {
continuation.resume(throwing: QueryError.invalidUrl)
return
}
let socket = session.webSocketTask(with: queryUrl)
socket.resume()
self.socket = socket
continuation.resume(returning: ())
}
}
public mutating func disconnect() {
socket?.cancel(with: .normalClosure, reason: nil)
socket = nil
}
func receive<T>() async throws -> T where T: Decodable {
guard let socket = socket else {
throw GeneralError.invalidWebSocket
}
let message = try await socket.receive()
guard let data = message.data else {
throw GeneralError.emptyData("subscribe()")
}
print(message)
let r = try JSONDecoder().decode(T.self, from: data)
return r
}
}
@@ -104,3 +137,17 @@ public struct KissWebSocketCredential: WebSocketCredential, Codable {
self.approvalKey = approvalKey
}
}
extension URLSessionWebSocketTask.Message {
var data: Data? {
switch self {
case .string(let str):
return Data(str.utf8)
case .data(let data):
return data
@unknown default:
return nil
}
}
}

View File

@@ -14,9 +14,7 @@ extension Domestic {
/// [-004]
///
public struct AskingPriceWebSocket: AuthWebSocket {
public typealias KResult = String
public class AskingPriceWebSocket: AuthWebSocket {
public var url: String {
"/tryitout/H0STASP0"
}
@@ -36,7 +34,6 @@ extension Domestic {
}
public var socket: URLSessionWebSocketTask?
public var result: KResult? = nil
public var credential: WebSocketCredential
var event: Event!
let productCode: String

View File

@@ -15,8 +15,6 @@ extension Domestic {
/// [-005]
///
public struct ContractNoticeWebSocket: AuthWebSocket {
public typealias KResult = String
public var url: String {
"/tryitout/H0STCNI0"
}
@@ -36,7 +34,6 @@ extension Domestic {
}
public var socket: URLSessionWebSocketTask?
public var result: KResult? = nil
public var credential: WebSocketCredential
var event: Event!
let htsID: String

View File

@@ -8,7 +8,7 @@
import Foundation
enum KissWebSocketSubscription: String {
enum KissWebSocketSubscription: String, Codable {
case subscribed = "1"
case unsubscribed = "2"
}
@@ -26,28 +26,11 @@ extension Domestic {
/// [-003]
///
public class ContractPriceWebSocket: AuthWebSocket {
public typealias KResult = String
public var url: String {
"/tryitout/H0STCNT0"
}
public var header: [String: String?] {
[
"approval_key": credential.approvalKey,
"custtype": "P",
"tr_type": KissWebSocketSubscription.subscribed.rawValue,
"content-type": "utf-8"
]
}
public var body: [String: Any] {
[
"tr_id": "H0STASP0",
"tr_key": productCode,
]
}
public var socket: URLSessionWebSocketTask?
public var result: KResult? = nil
public var credential: WebSocketCredential
var event: Event!
let productCode: String
@@ -59,25 +42,41 @@ extension Domestic {
}
struct Message: KissWebSocketMessage {
let subscription: KissWebSocketSubscription
init(subscription: KissWebSocketSubscription) {
self.subscription = subscription
public func subscribe() async throws -> Bool {
guard let socket = socket else {
throw GeneralError.invalidWebSocket
}
}
func subscribe() {
// json
let request = SubscriptionRequest(approvalKey: credential.approvalKey, type: .subscribed, productCode: productCode)
let requestData = try JSONEncoder().encode(request)
let requestJson = String(data: requestData, encoding: .utf8)!
print(requestJson)
try await socket.send(.string(requestJson))
let r: SubscriptionResult = try await receive()
guard r.body.output != nil, r.body.resultCode == "0" else {
throw GeneralError.webSocketError(r.body.resultCode, r.body.messageCode, r.body.message)
}
return true
}
func unsubscribe() {
// json
public func unsubscribe() async throws -> Bool {
guard let socket = socket else {
throw GeneralError.invalidWebSocket
}
let request = SubscriptionRequest(approvalKey: credential.approvalKey, type: .unsubscribed, productCode: productCode)
let requestData = try JSONEncoder().encode(request)
let requestJson = String(data: requestData, encoding: .utf8)!
print(requestJson)
try await socket.send(.string(requestJson))
let r: SubscriptionResult = try await receive()
guard r.body.output != nil, r.body.resultCode == "0" else {
throw GeneralError.webSocketError(r.body.resultCode, r.body.messageCode, r.body.message)
}
return true
}
}
}
@@ -92,6 +91,83 @@ extension Domestic.ContractPriceWebSocket {
self.socket = socket
}
}
struct SubscriptionRequest: Codable {
let header: Header
let body: Body
struct Header: Codable {
let approvalKey: String
let customerType: CustomerType
let trType: KissWebSocketSubscription
let contentType: String
private enum CodingKeys: String, CodingKey {
case approvalKey = "approval_key"
case customerType = "custtype"
case trType = "tr_type"
case contentType = "content-type"
}
}
struct Body: Codable {
let input: Input
}
struct Input: Codable {
let trId: String
let trKey: String
private enum CodingKeys: String, CodingKey {
case trId = "tr_id"
case trKey = "tr_key"
}
}
init(approvalKey: String, type: KissWebSocketSubscription, productCode: String) {
header = Header(approvalKey: approvalKey, customerType: .personal, trType: type, contentType: "utf-8")
body = Body(input: Input(trId: "H0STASP0", trKey: productCode))
}
}
struct SubscriptionResult: Codable {
let header: Header
let body: Body
struct Header: Codable {
let trId: String
let trKey: String
let encrypted: YesNo
private enum CodingKeys: String, CodingKey {
case trId = "tr_id"
case trKey = "tr_key"
case encrypted = "encrypt"
}
}
struct Body: Codable {
let resultCode: String
let messageCode: String
let message: String
let output: OutputDetail?
private enum CodingKeys: String, CodingKey {
case resultCode = "rt_cd"
case messageCode = "msg_cd"
case message = "msg1"
case output
}
}
struct OutputDetail: Codable {
let iv: String
let key: String
}
}
}

View File

@@ -8,7 +8,7 @@
import Foundation
public enum CustomerType: String {
public enum CustomerType: String, Codable {
case corporation = "B"
case personal = "P"
}

View File

@@ -37,13 +37,35 @@ public class KissProfile {
return profile.recent?.isMock
}
public var approvalKey: String? {
profileLock.lock()
defer {
profileLock.unlock()
}
guard let expired = profile.recent?.approvalKeyExpired, expired > Date() else {
return nil
}
return profile.recent?.approvalKey
}
public init() {
loadProfile()
}
func setAccessToken(_ accessToken: String, expired: Date, isMock: Bool) {
profileLock.lock()
profile.recent = Recent(isMock: isMock, accessToken: accessToken, accessTokenExpired: expired)
let approvalKey = profile.recent?.approvalKey
let approvalKeyExpired = profile.recent?.approvalKeyExpired
profile.recent = Recent(isMock: isMock, accessToken: accessToken, accessTokenExpired: expired, approvalKey: approvalKey, approvalKeyExpired: approvalKeyExpired)
profileLock.unlock()
saveProfile()
}
func setApprovalKey(_ approvalKey: String, expired: Date) {
profileLock.lock()
let recent = profile.recent?.setted(approvalKey: approvalKey, approvalKeyExpired: expired)
profile.recent = recent
profileLock.unlock()
saveProfile()
@@ -75,6 +97,12 @@ extension KissProfile {
public let isMock: Bool
public let accessToken: String
public let accessTokenExpired: Date
public let approvalKey: String?
public let approvalKeyExpired: Date?
func setted(approvalKey: String, approvalKeyExpired: Date) -> Recent {
return Recent(isMock: self.isMock, accessToken: self.accessToken, accessTokenExpired: self.accessTokenExpired, approvalKey: approvalKey, approvalKeyExpired: approvalKeyExpired)
}
}
public struct Love: Codable {

View File

@@ -108,6 +108,10 @@ public struct ApprovalKeyAuthRequest: AuthRequest {
public var result: KResult? = nil
public let credential: Credential
// TEMP: write logging temporarily
public var responseDataLoggable: Bool { return true }
public var traceable: Bool { return true }
public init(credential: Credential) {
self.credential = credential
}
@@ -178,13 +182,19 @@ extension KissAccount {
}
public func approvalKey() async throws -> String {
public func getApprovalKey() async throws -> String {
if let approvalKey = approvalKey {
return approvalKey
}
return try await withUnsafeThrowingContinuation { continuation in
let SecondsForOneDay: TimeInterval = 60 * 60 * 24
let request = ApprovalKeyAuthRequest(credential: credential)
request.query { result in
switch result {
case .success(let result):
self.setApprovalKey(result.approvalKey, expired: Date().addingTimeInterval(SecondsForOneDay - 60))
continuation.resume(returning: result.approvalKey)
case .failure(let error):
continuation.resume(throwing: error)

View File

@@ -25,11 +25,16 @@ func test_get_websocket_key_and_request_simple() {
let webSocketCredential = KissWebSocketCredential(isMock: isMock, accountNo: account.accountNo, approvalKey: approvalKey)
var socket = Domestic.ContractPriceWebSocket(credential: webSocketCredential, productCode: KissConsole.defaultProductNo)
let socket = Domestic.ContractPriceWebSocket(credential: webSocketCredential, productCode: KissConsole.defaultProductNo)
do {
try await socket.connect()
let result = try await socket.subscribe()
print(result)
try await Task.sleep(nanoseconds: 1_000_000_000 * 3)
let result2 = try await socket.unsubscribe()
print(result2)
} catch {
print(error)
}
@@ -56,8 +61,13 @@ func test_get_websocket_key(isMock: Bool) async -> (KissAccount, String)? {
let account = KissAccount(credential: credential)
do {
/// Return existing valid key
if let approvalKey = account.approvalKey {
return (account, approvalKey)
}
if try await account.login() {
let approvalKey = try await account.approvalKey()
let approvalKey = try await account.getApprovalKey()
print("approvalKey : \(approvalKey)")
return (account, approvalKey)
}