Long time not working

This commit is contained in:
2023-09-29 20:54:52 +09:00
parent 1004ff468a
commit cc1009c987
14 changed files with 454 additions and 28 deletions

View File

@@ -27,6 +27,7 @@ public enum GeneralError: Error {
case noData
// MARK: WebSocket
case cannotIssueApprovalKey
case invalidWebSocket
case webSocketError(_ code: String, _ messageCode: String, _ message: String)
case notStringButData
@@ -36,4 +37,5 @@ public enum GeneralError: Error {
case invalidWebSocketData_EncryptionField
case invalidWebSocketData_TrIdField
case notEnoughWebSocketData
case cannotSubscribeWebSocket
}

View File

@@ -8,6 +8,9 @@
import Foundation
let SecondsForOneDay: TimeInterval = 60 * 60 * 24
public protocol WebSocket {
var isMockAvailable: Bool { get }
var domain: String { get }
@@ -81,6 +84,30 @@ extension AuthWebSocket {
public var responseDataLoggable: Bool { true }
public func newSession() -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.waitsForConnectivity = false
config.timeoutIntervalForRequest = 60 // 60 seconds
config.timeoutIntervalForResource = SecondsForOneDay * 7 // 7 days
config.allowsCellularAccess = true
config.requestCachePolicy = .reloadIgnoringLocalCacheData
// config.multipathServiceType = .handover
// config.httpShouldSetCookies = true
// config.httpCookieAcceptPolicy = .always
config.httpMaximumConnectionsPerHost = 1
config.networkServiceType = .responsiveData
let queue = OperationQueue()
// 6 -1
print("\(config.httpMaximumConnectionsPerHost) \(queue.maxConcurrentOperationCount)")
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = self.queue
let session = URLSession(configuration: config, delegate: nil, delegateQueue: queue)
return session
}
public mutating func connect() async throws {
return try await withUnsafeThrowingContinuation { continuation in
@@ -126,6 +153,7 @@ extension AuthWebSocket {
let request = Domestic.SubscriptionRequest(approvalKey: credential.approvalKey, type: .subscribed, trId: transactionId, trKey: transactionKey)
let requestData = try JSONEncoder().encode(request)
let requestJson = String(data: requestData, encoding: .utf8)!
print(requestJson)
let stream = AsyncStream<String> { continuation in
self.streamContinuation = continuation
@@ -154,6 +182,7 @@ extension AuthWebSocket {
let request = Domestic.SubscriptionRequest(approvalKey: credential.approvalKey, type: .unsubscribed, trId: transactionId, trKey: transactionKey)
let requestData = try JSONEncoder().encode(request)
let requestJson = String(data: requestData, encoding: .utf8)!
print(requestJson)
let stream = AsyncStream<String> { continuation in
self.streamContinuation = continuation
@@ -222,6 +251,10 @@ extension AuthWebSocket {
streamContinuation?.yield(string)
streamContinuation?.finish()
case "(null)":
streamContinuation?.yield(string)
streamContinuation?.finish()
default:
assertionFailure("Unknown trId \(result.header.trId)")
}

View File

@@ -22,6 +22,7 @@ extension Domestic {
"H0STASP0"
}
public var session: URLSession { newSession() }
public var socket: URLSessionWebSocketTask?
public var socketDelegate: URLSessionWebSocketDelegate? { event }
public var message: String = ""
@@ -33,6 +34,7 @@ extension Domestic {
public var credential: WebSocketCredential
public var delegate: WebSocketDelegate?
public let transactionKey: String
public var productCode: String { transactionKey }
var event: Event!
public init(credential: WebSocketCredential, productCode: String) {

View File

@@ -19,9 +19,10 @@ extension Domestic {
"/tryitout/\(transactionId)"
}
public var transactionId: String {
"H0STCNI0"
credential.isMock ? "H0STCNI9": "H0STCNI0"
}
public var session: URLSession { newSession() }
public var socket: URLSessionWebSocketTask?
public var socketDelegate: URLSessionWebSocketDelegate? { event }
public var message: String = ""
@@ -33,6 +34,7 @@ extension Domestic {
public var credential: WebSocketCredential
public var delegate: WebSocketDelegate?
public let transactionKey: String
public var htsID: String { transactionKey }
var event: Event!
public init(credential: WebSocketCredential, htsID: String) {

View File

@@ -22,6 +22,7 @@ extension Domestic {
"H0STCNT0"
}
public var session: URLSession { newSession() }
public var socket: URLSessionWebSocketTask?
public var socketDelegate: URLSessionWebSocketDelegate? { event }
public var message: String = ""
@@ -33,6 +34,7 @@ extension Domestic {
public var credential: WebSocketCredential
public var delegate: WebSocketDelegate?
public let transactionKey: String
public var productCode: String { transactionKey }
var event: Event!
public init(credential: WebSocketCredential, productCode: String) {

View File

@@ -37,15 +37,12 @@ public class KissProfile {
return profile.recent?.isMock
}
public var approvalKey: String? {
public var approvalKeys: [ApprovalKey] {
profileLock.lock()
defer {
profileLock.unlock()
}
guard let expired = profile.recent?.approvalKeyExpired, expired > Date() else {
return nil
}
return profile.recent?.approvalKey
return profile.recent?.approvalKeys ?? []
}
public init() {
@@ -54,18 +51,18 @@ public class KissProfile {
func setAccessToken(_ accessToken: String, expired: Date, isMock: Bool) {
profileLock.lock()
let approvalKey = profile.recent?.approvalKey
let approvalKeyExpired = profile.recent?.approvalKeyExpired
profile.recent = Recent(isMock: isMock, accessToken: accessToken, accessTokenExpired: expired, approvalKey: approvalKey, approvalKeyExpired: approvalKeyExpired)
let keys = profile.recent?.approvalKeys ?? []
profile.recent = Recent(isMock: isMock, accessToken: accessToken, accessTokenExpired: expired, approvalKeys: keys)
profileLock.unlock()
saveProfile()
}
func setApprovalKey(_ approvalKey: String, expired: Date) {
func addApprovalKey(_ approvalKey: String, expired: Date) {
guard let recent = profile.recent else { return }
profileLock.lock()
let recent = profile.recent?.setted(approvalKey: approvalKey, approvalKeyExpired: expired)
profile.recent = recent
profile.recent = recent.added(approvalKey: approvalKey, approvalKeyExpired: expired)
profileLock.unlock()
saveProfile()
@@ -93,15 +90,23 @@ extension KissProfile {
}
}
public struct ApprovalKey: Codable {
public let key: String
public let keyExpired: Date
public var isExpired: Bool { keyExpired >= Date() }
}
public struct Recent: Codable {
public let isMock: Bool
public let accessToken: String
public let accessTokenExpired: Date
public let approvalKey: String?
public let approvalKeyExpired: Date?
public var approvalKeys: [ApprovalKey]
func setted(approvalKey: String, approvalKeyExpired: Date) -> Recent {
return Recent(isMock: self.isMock, accessToken: self.accessToken, accessTokenExpired: self.accessTokenExpired, approvalKey: approvalKey, approvalKeyExpired: approvalKeyExpired)
func added(approvalKey: String, approvalKeyExpired: Date) -> Recent {
var keys = self.approvalKeys
keys.append(ApprovalKey(key: approvalKey, keyExpired: approvalKeyExpired))
return Recent(isMock: self.isMock, accessToken: self.accessToken, accessTokenExpired: self.accessTokenExpired, approvalKeys: keys)
}
}

View File

@@ -183,18 +183,13 @@ extension KissAccount {
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))
self.addApprovalKey(result.approvalKey, expired: Date().addingTimeInterval(SecondsForOneDay - 60))
continuation.resume(returning: result.approvalKey)
case .failure(let error):
continuation.resume(throwing: error)

View File

@@ -0,0 +1,264 @@
//
// KissConsole+WebSocket.swift
// KissMeConsole
//
// Created by ened-book-m1 on 2023/09/04.
//
import Foundation
import KissMe
extension KissConsole {
}
public protocol KissAuctionPriceDelegate: AnyObject {
func auction(_ auction: KissAuction, contactPrice: Domestic.ContractPrice)
func auction(_ auction: KissAuction, askingPrice: Domestic.AskingPrice)
}
public protocol KissAutionNoticeDelegate: AnyObject {
func auction(_ auction: KissAuction, contactNotice: Domestic.ContractNotice)
}
public class KissAuction: NSObject {
public class Slot {
var contractPrice: Domestic.ContractPriceWebSocket!
var askingPrice: Domestic.AskingPriceWebSocket!
weak var delegate: KissAuctionPriceDelegate?
var contractPriceReceiver: ContractPriceReceiver!
var askingPriceReceiver: AskingPriceReceiver!
init(delegate: KissAuctionPriceDelegate?) {
self.delegate = delegate
}
func setup(contractPrice: Domestic.ContractPriceWebSocket, askingPrice: Domestic.AskingPriceWebSocket) {
self.contractPrice = contractPrice
self.askingPrice = askingPrice
}
func cleanup() {
contractPriceReceiver.slot = nil
contractPriceReceiver.owner = nil
askingPriceReceiver.slot = nil
askingPriceReceiver.owner = nil
}
}
class ContractPriceReceiver {
var slot: Slot?
weak var owner: KissAuction?
init(slot: Slot?, owner: KissAuction) {
self.slot = slot
self.owner = owner
}
}
class AskingPriceReceiver {
var slot: Slot?
weak var owner: KissAuction?
init(slot: Slot?, owner: KissAuction) {
self.slot = slot
self.owner = owner
}
}
class ContractNoticeReceiver {
weak var delegate: KissAutionNoticeDelegate?
weak var owner: KissAuction?
init(delegate: KissAutionNoticeDelegate?, owner: KissAuction) {
self.delegate = delegate
self.owner = owner
}
}
// TODO: ensure thread-safe
var slots: [Slot]
let loggable: Bool
let account: KissAccount
var contractNotice: Domestic.ContractNoticeWebSocket?
var contractNoticeReceiver: ContractNoticeReceiver?
public init(account: KissAccount, loggable: Bool) {
self.slots = [Slot]()
self.loggable = loggable
self.account = account
self.contractNotice = nil
self.contractNoticeReceiver = nil
}
public func startNotice(delegate: KissAutionNoticeDelegate?) async throws -> Bool {
//account.approvalKeys.
guard let isMock = account.isMock else {
throw GeneralError.invalidAccessToken
}
let approvalKey = try await account.getApprovalKey()
print("notice price key: \(approvalKey)")
let credential = KissWebSocketCredential(isMock: isMock, accountNo: account.accountNo, approvalKey: approvalKey)
contractNotice = Domestic.ContractNoticeWebSocket(credential: credential, htsID: account.accountNo)
contractNoticeReceiver = ContractNoticeReceiver(delegate: delegate, owner: self)
try await contractNotice!.connect()
let result = try await contractNotice!.subscribe()
return result
}
public func stopNotice() async throws -> Bool {
guard var contractNotice = contractNotice else {
return false
}
let result = try await contractNotice.unsubscribe()
contractNotice.disconnect()
contractNoticeReceiver?.delegate = nil
return result
}
public func addSlot(productNo: String, delegate: KissAuctionPriceDelegate?) async throws -> Slot {
guard let isMock = account.isMock else {
throw GeneralError.invalidAccessToken
}
func createContractPrice() async throws -> Domestic.ContractPriceWebSocket {
let approvalKey = try await account.getApprovalKey()
print("contract price key: \(approvalKey)")
let credential = KissWebSocketCredential(isMock: isMock, accountNo: account.accountNo, approvalKey: approvalKey)
let contractPrice = Domestic.ContractPriceWebSocket(credential: credential, productCode: productNo)
return contractPrice
}
func createAskingPrice() async throws -> Domestic.AskingPriceWebSocket {
let approvalKey = try await account.getApprovalKey()
print("asking price key: \(approvalKey)")
let credential = KissWebSocketCredential(isMock: isMock, accountNo: account.accountNo, approvalKey: approvalKey)
let askingPrice = Domestic.AskingPriceWebSocket(credential: credential, productCode: productNo)
return askingPrice
}
let slot = Slot(delegate: delegate)
slot.askingPriceReceiver = AskingPriceReceiver(slot: slot, owner: self)
slot.contractPriceReceiver = ContractPriceReceiver(slot: slot, owner: self)
var contractPrice = try await createContractPrice()
try await contractPrice.connect()
let result = try await contractPrice.subscribe()
guard result else {
throw GeneralError.cannotSubscribeWebSocket
}
var askingPrice = try await createAskingPrice()
try await askingPrice.connect()
let result2 = try await askingPrice.subscribe()
guard result2 else {
throw GeneralError.cannotSubscribeWebSocket
}
slot.setup(contractPrice: contractPrice, askingPrice: askingPrice)
slots.append(slot)
return slot
}
public func getSlot(productNo: String) -> Slot? {
guard let slot = slots.first(where: { $0.contractPrice.productCode == productNo }) else {
return nil
}
return slot
}
public func removeSlot(productNo: String) async throws -> Bool {
guard let slotIndex = slots.firstIndex(where: { $0.contractPrice.productCode == productNo }) else {
return false
}
let slot = slots.remove(at: slotIndex)
slot.cleanup()
return true
}
private func getWebSocketKey() async throws -> KissWebSocketCredential {
guard let isMock = account.isMock else {
throw GeneralError.invalidAccessToken
}
if try await account.login() {
let approvalKey = try await account.getApprovalKey()
return KissWebSocketCredential(isMock: isMock, accountNo: account.accountNo, approvalKey: approvalKey)
}
throw GeneralError.cannotIssueApprovalKey
}
}
extension KissAuction.ContractPriceReceiver: WebSocketDelegate {
func webSocket(_ webSocket: WebSocket, didPingpong dateTime: String) {
}
func webSocket(_ webSocket: WebSocket, didReceive data: Any) {
guard let owner = owner else { return }
guard let data = data as? Domestic.WebSocketData else { return }
switch data {
case .contractPrice(let price):
slot?.delegate?.auction(owner, contactPrice: price)
default:
break
}
}
}
extension KissAuction.AskingPriceReceiver: WebSocketDelegate {
func webSocket(_ webSocket: WebSocket, didPingpong dateTime: String) {
}
func webSocket(_ webSocket: WebSocket, didReceive data: Any) {
guard let owner = owner else { return }
guard let data = data as? Domestic.WebSocketData else { return }
switch data {
case .askingPrice(let price):
slot?.delegate?.auction(owner, askingPrice: price)
default:
break
}
}
}
extension KissAuction.ContractNoticeReceiver: WebSocketDelegate {
func webSocket(_ webSocket: WebSocket, didPingpong dateTime: String) {
}
func webSocket(_ webSocket: WebSocket, didReceive data: Any) {
guard let owner = owner else { return }
guard let data = data as? Domestic.WebSocketData else { return }
switch data {
case .contractNotice(let notice):
delegate?.auction(owner, contactNotice: notice)
default:
break
}
}
}

View File

@@ -101,6 +101,9 @@ class KissConsole: KissMe.ShopContext {
case localizeOnOff = "localize"
case test = "test"
//
case real = "real"
//
case news = "news"
case newsAll = "news all"
@@ -131,6 +134,8 @@ class KissConsole: KissMe.ShopContext {
return false
case .test:
return true
case .real:
return true
case .news, .newsAll:
return false
}
@@ -277,6 +282,8 @@ class KissConsole: KissMe.ShopContext {
case .localizeOnOff: await onLocalizeOnOff(args)
case .test: onTest(args)
case .real: await onReal(args)
case .news: await onNews(args)
case .newsAll: onNewsAll(args)
@@ -1226,6 +1233,26 @@ extension KissConsole {
}
private func onReal(_ args: [String]) async {
guard args.count == 2 else {
print("Missing PNO and on/off")
return
}
let productNo = args[0]
guard let option = OnOff(rawValue: args[1]) else {
print("Invalid on/off option")
return
}
switch option {
case .on:
break
case .off:
break
}
print("WebSocket listening \(option.rawValue) for \(productNo)")
}
private func onNews(_ args: [String]) async {
guard args.count == 1, let day = args[0].yyyyMMdd_toDate else {
print("Missing day")

View File

@@ -14,4 +14,6 @@ import KissMe
//test_get_websocket_key_and_contact_price()
//test_get_websocket_key_and_asking_price()
//test_parse_contact_price_response()
test_websocket_dump_data()
//test_websocket_dump_data()
test_auction()

View File

@@ -9,10 +9,68 @@ import Foundation
import KissMe
class DumpWebSocketData: NSObject {
class TestAution: NSObject {
}
extension TestAution: KissAutionNoticeDelegate {
func auction(_ auction: KissAuction, contactNotice: KissMe.Domestic.ContractNotice) {
}
}
extension TestAution: KissAuctionPriceDelegate {
func auction(_ auction: KissAuction, contactPrice: KissMe.Domestic.ContractPrice) {
}
func auction(_ auction: KissAuction, askingPrice: KissMe.Domestic.AskingPrice) {
}
}
func test_auction() {
let isMock = true
let productNo = "065350"
let semaphore = DispatchSemaphore(value: 0)
Task {
guard let account = await test_get_account(isMock: isMock) else {
return
}
let auction = KissAuction(account: account, loggable: true)
let testAuction = TestAution()
var success: Bool = false
do {
// success = try await auction.startNotice(delegate: testAuction)
// print("startNotice: \(success)")
let _ = try await auction.addSlot(productNo: productNo, delegate: testAuction)
print("addSlot: ok")
try await Task.sleep(nanoseconds: 1_000_000_000 * 60)
success = try await auction.removeSlot(productNo: productNo)
print("removeSlot: \(success)")
// success = try await auction.stopNotice()
// print("stopNotice: \(success)")
} catch {
print(error)
}
semaphore.signal()
}
semaphore.wait()
}
class DumpWebSocketData: NSObject {
}
extension DumpWebSocketData: WebSocketDelegate {
@@ -167,6 +225,29 @@ func test_get_websocket_key_and_contact_price() {
}
func test_get_account(isMock: Bool) async -> KissAccount? {
let credential: Credential
do {
credential = try KissCredential(isMock: isMock)
} catch {
print(error)
return nil
}
let account = KissAccount(credential: credential)
do {
if try await account.login() {
return account
}
} catch {
print(error)
return nil
}
return nil
}
func test_get_websocket_key(isMock: Bool) async -> (KissAccount, String)? {
let credential: Credential
@@ -176,15 +257,20 @@ func test_get_websocket_key(isMock: Bool) async -> (KissAccount, String)? {
print(error)
return nil
}
let account = KissAccount(credential: credential)
do {
/// Return existing valid key
if let approvalKey = account.approvalKey {
return (account, approvalKey)
if let approvalKey = account.approvalKeys.first, !approvalKey.isExpired {
return (account, approvalKey.key)
}
if try await account.login() {
if let approvalKey = account.approvalKeys.first, !approvalKey.isExpired {
return (account, approvalKey.key)
}
let approvalKey = try await account.getApprovalKey()
print("approvalKey : \(approvalKey)")
return (account, approvalKey)

View File

@@ -51,6 +51,7 @@ WIP `showcase` | 추천 상품을 제안함.
`hate (탭) (PNO)` | 관심 종목에서 삭제함.
`localize names` | csv field name 에 대해서 한글명을 제공하는 **data/localized-names.csv** 를 저장.
`localize (on/off)`| 앞으로 저장하는 모든 csv file 의 field 에 (on) 이면 한글명으로, (off) 이면 영문으로 저장.
`real (PNO) (on/off)` | 실시간 웹소켓을 접속하여 수신된 데이터를 기록합니다. (on) 이면 파일로 기록, (off) 이면 기록하지 않음.
* PNO 는 `Product NO` 의 약자이고, 상품의 `단축코드` (shortCode) 와 동일합니다.
* ONO 는 `Order NO` 의 약자이고, 고유한 주문번호 입니다.

View File

@@ -17,6 +17,7 @@
349843212A242AC900E85B08 /* KissConsole+CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349843202A242AC900E85B08 /* KissConsole+CSV.swift */; };
34D3680D2A280801005E6756 /* KissConsole+Candle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D3680C2A280801005E6756 /* KissConsole+Candle.swift */; };
34DA3EA42A9A176B00BB3439 /* test_websocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34DA3EA32A9A176B00BB3439 /* test_websocket.swift */; };
34DB3C452AA6071D00B6763E /* KissConsole+WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34DB3C442AA6071D00B6763E /* KissConsole+WebSocket.swift */; };
34EC4D1F2A7A7365002F947C /* KissConsole+News.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EC4D1E2A7A7365002F947C /* KissConsole+News.swift */; };
34EE76862A1C391B009761D2 /* KissMe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 341F5EDB2A0A8C4600962D48 /* KissMe.framework */; };
34EE76872A1C391B009761D2 /* KissMe.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 341F5EDB2A0A8C4600962D48 /* KissMe.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -60,6 +61,7 @@
349843202A242AC900E85B08 /* KissConsole+CSV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+CSV.swift"; sourceTree = "<group>"; };
34D3680C2A280801005E6756 /* KissConsole+Candle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+Candle.swift"; sourceTree = "<group>"; };
34DA3EA32A9A176B00BB3439 /* test_websocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = test_websocket.swift; sourceTree = "<group>"; };
34DB3C442AA6071D00B6763E /* KissConsole+WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+WebSocket.swift"; sourceTree = "<group>"; };
34EC4D1E2A7A7365002F947C /* KissConsole+News.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+News.swift"; sourceTree = "<group>"; };
34F190122A4441F00068C697 /* KissConsole+Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+Test.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -108,6 +110,7 @@
3435A7F12A35A8A900D604F1 /* KissConsole+Investor.swift */,
34EC4D1E2A7A7365002F947C /* KissConsole+News.swift */,
34F190122A4441F00068C697 /* KissConsole+Test.swift */,
34DB3C442AA6071D00B6763E /* KissConsole+WebSocket.swift */,
349327F62A20E3E300097063 /* Foundation+Extensions.swift */,
);
name = KissMeConsole;
@@ -198,6 +201,7 @@
341F5F052A13B82F00962D48 /* test.swift in Sources */,
349843212A242AC900E85B08 /* KissConsole+CSV.swift in Sources */,
348168492A2F92AC00A50BD3 /* KissContext.swift in Sources */,
34DB3C452AA6071D00B6763E /* KissConsole+WebSocket.swift in Sources */,
3435A7F42A35B4D000D604F1 /* KissConsole+Price.swift in Sources */,
34DA3EA42A9A176B00BB3439 /* test_websocket.swift in Sources */,
34F190132A4441F00068C697 /* KissConsole+Test.swift in Sources */,

View File

@@ -41,7 +41,8 @@
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
viewDebuggingEnabled = "No"
consoleMode = "1">
consoleMode = "1"
structuredConsoleMode = "3">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference