diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06448b12d..bf7b04c53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Fetch dep run: gradle fetchDependencies @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Fetch dep run: gradle fetchDependencies diff --git a/DXFeedFramework.xcodeproj/project.pbxproj b/DXFeedFramework.xcodeproj/project.pbxproj index 9a95ef0ea..9663e4685 100644 --- a/DXFeedFramework.xcodeproj/project.pbxproj +++ b/DXFeedFramework.xcodeproj/project.pbxproj @@ -343,10 +343,15 @@ 64E342502AAB083700457994 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8D22A3B401700846831 /* Colors.swift */; }; 64E342522AAB29CF00457994 /* InstrumentProfileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E342512AAB29CF00457994 /* InstrumentProfileType.swift */; }; 64E3637B2AD83459002E2B0D /* SeriesMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E3637A2AD83459002E2B0D /* SeriesMapper.swift */; }; + 64EAA1A22B7A38F8005087BC /* DXPromiseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EAA1A12B7A38F8005087BC /* DXPromiseTest.swift */; }; + 64EAA1A42B7B4DA1005087BC /* IndexedEventSource+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EAA1A32B7B4DA1005087BC /* IndexedEventSource+Ext.swift */; }; 64ECD67F2A9CF4CB00B36935 /* IPFTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64ECD67E2A9CF4CB00B36935 /* IPFTests.swift */; }; 64ECD6822A9DDC2800B36935 /* DXInstrumentProfileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64ECD6812A9DDC2800B36935 /* DXInstrumentProfileReader.swift */; }; 64ECD6852A9DDF6200B36935 /* DXInstrumentProfileCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64ECD6842A9DDF6200B36935 /* DXInstrumentProfileCollector.swift */; }; 64ECD6872A9DDFBE00B36935 /* DXInstrumentProfileConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64ECD6862A9DDFBE00B36935 /* DXInstrumentProfileConnection.swift */; }; + 64F73B9F2B6788B00088EC37 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F73B9E2B6788B00088EC37 /* Promise.swift */; }; + 64F73BA42B67B28B0088EC37 /* NativePromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F73BA32B67B28B0088EC37 /* NativePromise.swift */; }; + 64F73BA62B67CD5B0088EC37 /* GraalException+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F73BA52B67CD5B0088EC37 /* GraalException+Ext.swift */; }; 64FCAF902A572D4600971F4E /* libDxFeedGraalNativeSdk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 64125E352A1F689A00FB32BA /* libDxFeedGraalNativeSdk.dylib */; platformFilters = (macos, ); settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 803BAC1629BFA50700FFAB1C /* DXFeedFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 803BAC0D29BFA50700FFAB1C /* DXFeedFramework.framework */; }; 803BAC1C29BFA50700FFAB1C /* DxFeedSwiftFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 803BAC1029BFA50700FFAB1C /* DxFeedSwiftFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -850,10 +855,15 @@ 64DA26BF2AA224EB005B1757 /* NativeIPFConnectionListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeIPFConnectionListener.swift; sourceTree = ""; }; 64E342512AAB29CF00457994 /* InstrumentProfileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstrumentProfileType.swift; sourceTree = ""; }; 64E3637A2AD83459002E2B0D /* SeriesMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesMapper.swift; sourceTree = ""; }; + 64EAA1A12B7A38F8005087BC /* DXPromiseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXPromiseTest.swift; sourceTree = ""; }; + 64EAA1A32B7B4DA1005087BC /* IndexedEventSource+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IndexedEventSource+Ext.swift"; sourceTree = ""; }; 64ECD67E2A9CF4CB00B36935 /* IPFTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPFTests.swift; sourceTree = ""; }; 64ECD6812A9DDC2800B36935 /* DXInstrumentProfileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXInstrumentProfileReader.swift; sourceTree = ""; }; 64ECD6842A9DDF6200B36935 /* DXInstrumentProfileCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXInstrumentProfileCollector.swift; sourceTree = ""; }; 64ECD6862A9DDFBE00B36935 /* DXInstrumentProfileConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXInstrumentProfileConnection.swift; sourceTree = ""; }; + 64F73B9E2B6788B00088EC37 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = ""; }; + 64F73BA32B67B28B0088EC37 /* NativePromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePromise.swift; sourceTree = ""; }; + 64F73BA52B67CD5B0088EC37 /* GraalException+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraalException+Ext.swift"; sourceTree = ""; }; 64F9C6C12B4BFD8F003ED014 /* DXFeedconnect.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DXFeedconnect.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 803BAC0D29BFA50700FFAB1C /* DXFeedFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DXFeedFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 803BAC1029BFA50700FFAB1C /* DxFeedSwiftFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DxFeedSwiftFramework.h; sourceTree = ""; }; @@ -1101,6 +1111,7 @@ 64ABC1022AD9294C00904D78 /* OptionSaleMapper.swift */, 64AF04422AE6AA230091F6F1 /* TradeETH+Ext.swift */, 64AF04402AE6A9E90091F6F1 /* TradeETHMapper.swift */, + 64EAA1A32B7B4DA1005087BC /* IndexedEventSource+Ext.swift */, ); path = Markets; sourceTree = ""; @@ -1490,6 +1501,22 @@ path = Live; sourceTree = ""; }; + 64F73B9D2B67863B0088EC37 /* Promise */ = { + isa = PBXGroup; + children = ( + 64F73B9E2B6788B00088EC37 /* Promise.swift */, + ); + path = Promise; + sourceTree = ""; + }; + 64F73BA22B67B2750088EC37 /* Promise */ = { + isa = PBXGroup; + children = ( + 64F73BA32B67B28B0088EC37 /* NativePromise.swift */, + ); + path = Promise; + sourceTree = ""; + }; 803BAC0329BFA50700FFAB1C = { isa = PBXGroup; children = ( @@ -1529,6 +1556,7 @@ 8088D77A29C5D8AD00F240CB /* Native */, 64656F552A1B6A07006A0B19 /* Utils */, 64ECD6802A9DD80000B36935 /* Ipf */, + 64F73B9D2B67863B0088EC37 /* Promise */, 64C771FD2A9504D7009868C2 /* Extra */, 6498E6B02AB1D40C0093A065 /* Schedule */, 8088D77029C3A25D00F240CB /* SystemProperty.swift */, @@ -1564,6 +1592,7 @@ 641C64B32B347C430023CFAD /* DXObservableSubscriptionTest.swift */, 6423E4682B457000006B208D /* DXTimeSeriesSubscriptionTest.swift */, 646064E92B4D8973009201E2 /* DXLastEventTest.swift */, + 64EAA1A12B7A38F8005087BC /* DXPromiseTest.swift */, ); path = DXFeedFrameworkTests; sourceTree = ""; @@ -1590,6 +1619,7 @@ 64104FCA2A2629C400D1FC41 /* Subscription */, 64437A902A9DF1C4005929B2 /* Ipf */, 6498E6B32AB1D43A0093A065 /* Schedule */, + 64F73BA22B67B2750088EC37 /* Promise */, 64ACBCE42A28AEE200032C53 /* SymbolMappers */, 64A42F432B0B9320001C3ACC /* Utils */, ); @@ -1604,6 +1634,7 @@ 8088D77C29C8DAF800F240CB /* GraalErrorCode.swift */, 64656F612A1B9FF7006A0B19 /* EnumException.swift */, 640C3FCB2A616B6200555161 /* ArgumentException.swift */, + 64F73BA52B67CD5B0088EC37 /* GraalException+Ext.swift */, ); path = ErrorHandling; sourceTree = ""; @@ -2291,6 +2322,7 @@ files = ( 64BDDB322AD7E5A600694210 /* SpreadOrderMapper.swift in Sources */, 64ACBCE32A289A0700032C53 /* TimeSeriesSubscriptionSymbol.swift in Sources */, + 64F73B9F2B6788B00088EC37 /* Promise.swift in Sources */, 645A34952A937C7200709F29 /* BinaryInteger+Ext.swift in Sources */, 641C64B22B346A2E0023CFAD /* DXObservableSubscription.swift in Sources */, 64A42F502B0BA668001C3ACC /* DXTimeFormat.swift in Sources */, @@ -2337,6 +2369,7 @@ 6486B9732AD045C800D8D5FA /* TheoPrice.swift in Sources */, 64ACBCDF2A2897EA00032C53 /* String+Symbol.swift in Sources */, 64B4363A2AB86D1A0003919E /* DayFilter.swift in Sources */, + 64EAA1A42B7B4DA1005087BC /* IndexedEventSource+Ext.swift in Sources */, 64A42F3B2B07A7A3001C3ACC /* SymbolParser.swift in Sources */, 64ECD6872A9DDFBE00B36935 /* DXInstrumentProfileConnection.swift in Sources */, 6406F2572AD9820700B58C42 /* NativePublisher.swift in Sources */, @@ -2367,6 +2400,7 @@ 6498E6BD2AB1E0510093A065 /* ScheduleSession.swift in Sources */, 64BDDB262AD6F6B500694210 /* IcebergType.swift in Sources */, 8088D76529C0FBCE00F240CB /* ThreadManager.swift in Sources */, + 64F73BA62B67CD5B0088EC37 /* GraalException+Ext.swift in Sources */, 64DA26BE2AA20EDB005B1757 /* DXInstrumentProfileConnectionListener.swift in Sources */, 64656F732A1D0A84006A0B19 /* EndpointListener.swift in Sources */, 64BDDB282AD7D8D300694210 /* Order+Ext.swift in Sources */, @@ -2468,6 +2502,7 @@ 64BDDB1E2AD6CC6A00694210 /* Order.swift in Sources */, 64437A8F2A9DEE6F005929B2 /* InstrumentProfile.swift in Sources */, 64C771FA2A94D692009868C2 /* ShortSaleRestriction.swift in Sources */, + 64F73BA42B67B28B0088EC37 /* NativePromise.swift in Sources */, 64656F5E2A1B97F2006A0B19 /* NativeFeed.swift in Sources */, 6486B97D2AD057F200D8D5FA /* OrderSource.swift in Sources */, 64656F6F2A1CFC12006A0B19 /* WeakBox.swift in Sources */, @@ -2502,6 +2537,7 @@ 6498E6B72AB1DACE0093A065 /* ScheduleTest.swift in Sources */, 649282ED2AD593F3008F0F04 /* OrderSourceTest.swift in Sources */, 6426C8932A531AB500236784 /* ThreadsTest.swift in Sources */, + 64EAA1A22B7A38F8005087BC /* DXPromiseTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DXFeedFramework.xcodeproj/xcshareddata/xcschemes/Tools.xcscheme b/DXFeedFramework.xcodeproj/xcshareddata/xcschemes/Tools.xcscheme index 3d68c00f2..d52f6cf7f 100644 --- a/DXFeedFramework.xcodeproj/xcshareddata/xcschemes/Tools.xcscheme +++ b/DXFeedFramework.xcodeproj/xcshareddata/xcschemes/Tools.xcscheme @@ -64,8 +64,8 @@ isEnabled = "NO"> + argument = "Connect demo.dxfeed.com:7300 TRADE ETH/USD:GDAX" + isEnabled = "YES"> + isEnabled = "NO"> Promise? { + let nativePromise = try native.getLastEventPromise(type: type, symbol: symbol) + return Promise(native: nativePromise) + } + + func getLastEventPromises(type: IEventType.Type, symbols: [Symbol]) throws -> [Promise]? { + let nativePromises = try native.getLastEventPromises(type: type, symbols: symbols) + return nativePromises?.map({ promise in + Promise(native: promise) + }) + } + + func getIndexedEventsPromise(type: IEventType.Type, symbol: Symbol, source: IndexedEventSource) throws -> Promise? { + let nativePromise = try native.getIndexedEventsPromise(type: type, symbol: symbol, source: source) + return Promise(native: nativePromise) + } + + func getTimeSeriesPromise(type: IEventType.Type, symbol: Symbol, fromTime: Long, toTime: Long) throws -> Promise? { + let nativePromise = try native.getTimeSeriesPromise(type: type, + symbol: symbol, + fromTime: fromTime, + toTime: toTime) + return Promise(native: nativePromise) + + } +} diff --git a/DXFeedFramework/Native/ErrorHandling/GraalException+Ext.swift b/DXFeedFramework/Native/ErrorHandling/GraalException+Ext.swift new file mode 100644 index 000000000..8d948ea01 --- /dev/null +++ b/DXFeedFramework/Native/ErrorHandling/GraalException+Ext.swift @@ -0,0 +1,37 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation +@_implementationOnly import graal_api + +internal extension GraalException { + static func createNew(native: UnsafeMutablePointer) -> GraalException { + let pointee = native.pointee + let message = String(pointee: pointee.message, default: "Graall Exception") + let className = String(pointee: pointee.class_name, default: "") + let stackTrace = String(pointee: pointee.print_stack_trace, default: "") + let gException = GraalException.fail(message: message, + className: className, + stack: stackTrace) + return gException + } + + func toNative() -> UnsafeMutablePointer? { + switch self { + case .fail(message: let message, className: let className, stack: let stack): + let exception = UnsafeMutablePointer.allocate(capacity: 1) + var pointee = exception.pointee + pointee.class_name = className.toCStringRef() + pointee.message = message.toCStringRef() + pointee.print_stack_trace = stack.toCStringRef() + exception.pointee = pointee + return exception + default: + return nil + } + } +} diff --git a/DXFeedFramework/Native/Events/Markets/IndexedEventSource+Ext.swift b/DXFeedFramework/Native/Events/Markets/IndexedEventSource+Ext.swift new file mode 100644 index 000000000..6d61e1c25 --- /dev/null +++ b/DXFeedFramework/Native/Events/Markets/IndexedEventSource+Ext.swift @@ -0,0 +1,24 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation +@_implementationOnly import graal_api + +extension IndexedEventSource { + func toNative() -> UnsafeMutablePointer? { + let nativeSource = UnsafeMutablePointer.allocate(capacity: 1) + nativeSource.pointee.id = Int32(identifier) + nativeSource.pointee.name = name.toCStringRef() + switch self { + case _ as OrderSource: + nativeSource.pointee.type = ORDER_SOURCE + default: + nativeSource.pointee.type = INDEXED_EVENT_SOURCE + } + return nativeSource + } +} diff --git a/DXFeedFramework/Native/Feed/NativeFeed.swift b/DXFeedFramework/Native/Feed/NativeFeed.swift index d66d32aed..55364e826 100644 --- a/DXFeedFramework/Native/Feed/NativeFeed.swift +++ b/DXFeedFramework/Native/Feed/NativeFeed.swift @@ -148,4 +148,77 @@ class NativeFeed { } return results } + + func getLastEventPromise(type: IEventType.Type, symbol: Symbol) throws -> NativePromise { + let thread = currentThread() + let converted = SymbolMapper.newNative(symbol) + let native = try ErrorCheck.nativeCall(thread, + dxfg_DXFeed_getLastEventPromise(thread, + feed, + type.type.nativeCode(), + converted)) + return NativePromise(promise: &native.pointee.handler) + + } + + func getLastEventPromises(type: IEventType.Type, symbols: [Symbol]) throws -> [NativePromise]? { + let nativeSymbols = symbols.compactMap { SymbolMapper.newNative($0) } + let elements = ListNative(pointers: nativeSymbols) + let listPointer = elements.newList() + defer { + listPointer.deinitialize(count: 1) + listPointer.deallocate() + nativeSymbols.forEach { SymbolMapper.clearNative(symbol: $0) } + } + let thread = currentThread() + let native = try ErrorCheck.nativeCall(thread, + dxfg_DXFeed_getLastEventsPromises(thread, + feed, + type.type.nativeCode(), + listPointer)) + var result = [NativePromise]() + for index in 0.. NativePromise { + let thread = currentThread() + let converted = SymbolMapper.newNative(symbol) + let nativeSource = source.toNative() + defer { + nativeSource?.deinitialize(count: 1) + nativeSource?.deallocate() + } + let native = try ErrorCheck.nativeCall(thread, + dxfg_DXFeed_getIndexedEventsPromise(thread, + feed, + type.type.nativeCode(), + converted, + nativeSource)) + + return NativePromise(promise: &native.pointee.base) + } + + func getTimeSeriesPromise(type: IEventType.Type, + symbol: Symbol, + fromTime: Long, + toTime: Long) throws -> NativePromise { + let thread = currentThread() + let converted = SymbolMapper.newNative(symbol) + let native = try ErrorCheck.nativeCall(thread, + dxfg_DXFeed_getTimeSeriesPromise(thread, + feed, + type.type.nativeCode(), + converted, + fromTime, + toTime)) + return NativePromise(promise: &native.pointee.base) + } } diff --git a/DXFeedFramework/Native/Promise/NativePromise.swift b/DXFeedFramework/Native/Promise/NativePromise.swift new file mode 100644 index 000000000..403625b7c --- /dev/null +++ b/DXFeedFramework/Native/Promise/NativePromise.swift @@ -0,0 +1,245 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation +@_implementationOnly import graal_api + +internal protocol PromiseListener: AnyObject { + func done() +} + +/// Native wrapper over the Java com.dxfeed.promise.Promise class. +/// The location of the imported functions is in the header files "dxfg_feed.h". +class NativePromise { + private class WeakListener: WeakBox { } + typealias PromiseHandler = (_: NativePromise) -> Void + + let promise: UnsafeMutablePointer? + + private static let mapper = EventMapper() + + private var result: MarketEvent? + private var results: [MarketEvent]? + + private static let listeners = ConcurrentArray() + + static let listenerCallback: dxfg_promise_handler_function = { _, promise, context in + if let context = context { + + let listener: AnyObject = bridge(ptr: context) + if let weakListener = listener as? WeakListener { + defer { + NativePromise.listeners.removeAll(where: { + return $0 === weakListener + }) + } + guard let listener = weakListener.value else { + return + } + promise?.withMemoryRebound(to: dxfg_promise_event_t.self, capacity: 1, { pointer in + let native = NativePromise(promise: &pointer.pointee.handler) + listener.done() + }) + + } + } + } + + deinit { + if let promise = promise { + let thread = currentThread() + _ = try? ErrorCheck.nativeCall( + thread, + dxfg_JavaObjectHandler_release(thread, + &(promise.pointee.handler))) + } + } + + init(promise: UnsafeMutablePointer?) { + self.promise = promise + } + + func getResults() throws -> [MarketEvent]? { + if let results = results { + return results + } + let thread = currentThread() + let res = try promise?.withMemoryRebound(to: dxfg_promise_events_t.self, capacity: 1, { promiseEvents in + let listPointer = try ErrorCheck.nativeCall(thread, + dxfg_Promise_List_EventType_getResult(thread, + promiseEvents)) + var results = [MarketEvent]() + let size = listPointer.pointee.size + for index in 0.. MarketEvent? { + if let result = result { + return result + } + let thread = currentThread() + let res = try promise?.withMemoryRebound(to: dxfg_promise_event_t.self, capacity: 1, { promiseEvent in + let result = try ErrorCheck.nativeCall(thread, dxfg_Promise_EventType_getResult(thread, promiseEvent)) + let marketEvent = try EventMapper().fromNative(native: result) + self.result = marketEvent + return marketEvent + }) + return res + } + + func isDone() -> Bool { + let thread = currentThread() + if let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_isDone(thread, promise)) { + return result == 1 + } else { + return false + } + } + + func hasResult() -> Bool { + let thread = currentThread() + if let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_hasResult(thread, promise)) { + return result == 1 + } else { + return false + } + } + + func hasException() -> Bool { + let thread = currentThread() + if let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_hasException(thread, promise)) { + return result == 1 + } else { + return false + } + } + + func isCancelled() -> Bool { + let thread = currentThread() + if let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_isCancelled(thread, promise)) { + return result == 1 + } else { + return false + } + } + + func getException() -> GraalException? { + let thread = currentThread() + if let nativeException = try? ErrorCheck.nativeCall(thread, dxfg_Promise_getException(thread, promise)) { + defer { + _ = try? ErrorCheck.nativeCall(thread, dxfg_Exception_release(thread, nativeException)) + } + let exception = GraalException.createNew(native: nativeException) + return exception + } + return nil + } + + func await() throws -> Bool { + let thread = currentThread() + let success = try ErrorCheck.nativeCall(thread, dxfg_Promise_await(thread, promise)) + return success == ErrorCheck.Result.success.rawValue + } + + func await(millis timeOut: Int32) throws -> Bool { + let thread = currentThread() + let success = try ErrorCheck.nativeCall(thread, dxfg_Promise_await2(thread, promise, timeOut)) + return success == ErrorCheck.Result.success.rawValue + } + + func awaitWithoutException(millis timeOut: Int32) -> Bool { + let thread = currentThread() + let success = try? ErrorCheck.nativeCall(thread, dxfg_Promise_awaitWithoutException(thread, promise, timeOut)) + return success == ErrorCheck.Result.success.rawValue + } + + func cancel() { + let thread = currentThread() + _ = try? ErrorCheck.nativeCall(thread, dxfg_Promise_cancel(thread, promise)) + } + + func complete(result: MarketEvent) throws { + let thread = currentThread() + if let nativeEvent = try NativePromise.mapper.toNative(event: result) { + defer { + NativePromise.mapper.releaseNative(native: nativeEvent) + } + try ErrorCheck.nativeCall(thread, dxfg_Promise_EventType_complete(thread, promise, nativeEvent)) + } + } + + func completeExceptionally(_ exception: GraalException) throws { + if let nativeException = exception.toNative() { +// defer { +// nativeException.deinitialize(count: 1) +// nativeException.deallocate() +// } + let thread = currentThread() + try ErrorCheck.nativeCall(thread, dxfg_Promise_completeExceptionally(thread, promise, nativeException)) + } else { + throw ArgumentException.exception("Coudln't convert to native exception") + } + } + + func whenDone(handler: PromiseListener) { + let thread = currentThread() + let weakListener = WeakListener(value: handler) + NativePromise.listeners.append(newElement: weakListener) + let voidPtr = bridge(obj: weakListener) + _ = try? ErrorCheck.nativeCall(thread, + dxfg_Promise_whenDone(thread, + promise, + NativePromise.listenerCallback, + voidPtr)) + } + + static func allOf(promises: [NativePromise]) throws -> NativePromise? { + let promiseList = UnsafeMutablePointer.allocate(capacity: 1) + let nativeList = UnsafeMutablePointer.allocate(capacity: 1) + let classes = UnsafeMutablePointer?> + .allocate(capacity: promises.count) + var iterator = classes + for code in promises { + code.promise?.withMemoryRebound(to: dxfg_java_object_handler.self, capacity: 1, { pointer in + iterator.initialize(to: pointer) + iterator = iterator.successor() + }) + } + let thread = currentThread() + + defer { + + nativeList.deinitialize(count: 1) + nativeList.deallocate() + + promiseList.deinitialize(count: 1) + promiseList.deallocate() + } + nativeList.pointee.size = Int32(promises.count) + nativeList.pointee.elements = classes + promiseList.pointee.list = nativeList.pointee + + let result = try ErrorCheck.nativeCall(thread, dxfg_Promises_allOf(thread, promiseList)) + return NativePromise(promise: result) + } + + static func removeListener(listener: PromiseListener) { + listeners.removeAll { listener in + listener.value === listener + } + } +} diff --git a/DXFeedFramework/Promise/Promise.swift b/DXFeedFramework/Promise/Promise.swift new file mode 100644 index 000000000..7fbfe79a4 --- /dev/null +++ b/DXFeedFramework/Promise/Promise.swift @@ -0,0 +1,149 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +/// Result of a computation that will be completed normally or exceptionally in the future. +/// This class is designed to represent a promise to deliver certain result. +public class Promise { + public typealias PromiseHandler = (_: Promise) -> Void + + private let native: NativePromise + private var handlers = [PromiseHandler]() + + deinit { + NativePromise.removeListener(listener: self) + } + + internal init(native: NativePromise) { + self.native = native + } + + /// Returns results of computation. If computation has no result, then this method returns nil. + public func getResults() throws -> [MarketEvent]? { + return try native.getResults() + } + + /// Returns results of computation. If computation has no result, then this method returns nil. + public func getResult() throws -> MarketEvent? { + return try native.getResult() + } + /// Returns **true** when computation has + /// ``complete(result:)`` completed normally, + /// or ``completeExceptionally(_:)`` exceptionally, + /// or was ``cancel()`` canceled + public func isDone() -> Bool { + return native.isDone() + } + /// Returns **true** when computation has completed normally. + /// Use ``hasResult()`` method to get the result of the computation. + public func hasResult() -> Bool { + return native.hasResult() + } + /// Returns **true** when computation has completed exceptionally or was cancelled. + /// Use ``getException()`` method to get the exceptional outcome of the computation. + public func hasException() -> Bool { + return native.hasException() + } + /// Returns **true** when computation was canceled. + /// Use ``getException()`` method to get the corresponding exception. + public func isCancelled() -> Bool { + return native.isCancelled() + } + + /// Returns exceptional outcome of computation. If computation has no ``hasException()`` exception, + /// then this method returns ``nil``. If computation has completed exceptionally or was cancelled, then + /// the result of this method is not ``nil``. + /// If computation was ``isCancelled()`` cancelled, then this method returns an + // instance of GraalException. + /// - Returns: GraalException. Rethrows exception from Java. + public func getException() -> GraalException? { + return native.getException() + } + + /// Wait for computation to complete and return its result or throw an exception in case of exceptional completion. + /// This method waits forever. + /// - Returns: result of computation. + /// - Throws : GraalException. Rethrows exception from Java + public func await() throws -> MarketEvent? { + _ = try native.await() + return try getResult() + + } + + /// Wait for computation to complete and return its result or throw an exception in case of exceptional completion. + /// If the wait times out, then the computation is ``cancel()`` cancelled and exception is thrown. + /// - Returns: result of computation. + /// - Throws : GraalException. Rethrows exception from Java + public func await(millis timeOut: Int32) throws -> MarketEvent? { + if try native.await(millis: timeOut) { + return try getResult() + } + return nil + } + /// Wait for computation to complete and return its result or throw an exception in case of exceptional completion. + /// If the wait times out, then the computation is ``cancel()`` cancelled and exception is thrown. + /// - Returns: If the wait times out, then the computation is ``cancel()`` cancelled and this method returns **false**. + /// Use this method in the code that shall continue normal execution in case of timeout. + public func awaitWithoutException(millis timeOut: Int32) -> Bool { + return native.awaitWithoutException(millis: timeOut) + } + /// Cancels computation. This method does nothing if computation has already ``isDone()`` completed. + public func cancel() { + native.cancel() + } + /// Completes computation normally with a specified result. + /// This method does nothing if computation has already ``isDone()`` completed + /// (normally, exceptionally, or was cancelled), + /// - Throws : GraalException. Rethrows exception from Java + public func complete(result: MarketEvent) throws { + try native.complete(result: result) + } + /// Completes computation exceptionally with a specified exception. + /// This method does nothing if computation has already ``isDone()`` completed, + /// otherwise ``getException()`` will return the specified exception. + /// - Throws : GraalException. Rethrows exception from Java + public func completeExceptionally(_ exception: GraalException) throws { + try native.completeExceptionally(exception) + } + + /// Registers a handler to be invoked exactly once when computation ``isDone()`` completes. + /// The handler's method is invoked immediately when this computation has already completed, + /// otherwise it will be invoked **synchronously** in the future when computation + /// ``complete(result:)`` completes normally, + /// or ``completeExceptionally(_:)`` exceptionally, + /// or is ``cancel()`` cancelled from the same thread that had invoked one of the completion methods. + public func whenDone(handler: @escaping PromiseHandler) { + handlers.append(handler) + native.whenDone(handler: self) + } + + /// Returns a new promise that ``isDone()`` completes when all promises from the given array + /// complete normally or exceptionally. + /// The results of the given promises are not reflected in the returned promise, but may be + /// obtained by inspecting them individually. If no promises are provided, returns a promise completed + /// with the value null. + /// When the resulting promise completes for any reason ``cancel()`` canceled, for example) + /// then all of the promises from the given array are canceled. + /// - Throws : GraalException. Rethrows exception from Java + public static func allOf(promises: [Promise]) throws -> Promise? { + if let native = try NativePromise.allOf(promises: promises.map { $0.native }) { + return Promise(native: native) + } else { + return nil + } + } + +} + +extension Promise: PromiseListener { + func done() { + handlers.forEach { handler in + handler(self) + } + } +} diff --git a/DXFeedFrameworkTests/DXPromiseTest.swift b/DXFeedFrameworkTests/DXPromiseTest.swift new file mode 100644 index 000000000..7465a98d3 --- /dev/null +++ b/DXFeedFrameworkTests/DXPromiseTest.swift @@ -0,0 +1,346 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import XCTest +@testable import DXFeedFramework + +final class DXPromiseTest: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + private func eventPromise(type: IEventType.Type, symbol: String, feed: DXFeed) throws -> Promise { + guard let promise = try feed.getLastEventPromise(type: type, symbol: symbol) else { + fatalError("Couldnt receive promise") + } + return promise + } + + func testGetAsyncResultWithTimeout() { + getAsyncResult(timeOut: 1000) + } + + func testGetAsyncResultWithTimeoutWithoutException() { + getAsyncResult(timeOut: 1000, withException: false) + } + + func testGetAsyncResultNoTimeout() { + getAsyncResult(timeOut: nil) + } + + func getAsyncResult(timeOut: Int32?, withException: Bool = true) { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Trade.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + var result: MarketEvent? + if let timeOut = timeOut { + if withException { + result = try promise.await(millis: timeOut) + } else { + if promise.awaitWithoutException(millis: timeOut) { + result = try promise.getResult() + } + } + } else { + result = try promise.await() + } + XCTAssert(result != nil) + XCTAssert(promise.hasResult() == true) + XCTAssert(result === (try? promise.getResult())) + XCTAssert(promise.isDone() == true) + XCTAssert(promise.hasException() == false) + } catch { + XCTAssert(false, "testGetResult \(error)") + } + } + + func testGetAsyncResult() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Trade.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + let receivedEventExp = expectation(description: "Received promise") + promise.whenDone { [weak promise]_ in + if (try? promise?.getResult()) != nil { + receivedEventExp.fulfill() + } + } + wait(for: [receivedEventExp], timeout: 1) + + } catch { + XCTAssert(false, "testGetResult \(error)") + } + } + + func testGetResultWithException() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Trade.self, + symbol: "ETH/USD:GDAX_TEST", + feed: feed!) + XCTAssert(promise.hasResult() == false) + _ = try promise.await(millis: 1000) + XCTAssert(false, "Throws excpetion") + } catch GraalException.fail(let message, _, _) { + XCTAssert(message == "await timed out") + } catch { + XCTAssert(false, "testGetResultWithException \(error)") + } + } + + func testGetIndexedEventResult() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + guard let promise = try feed?.getIndexedEventsPromise(type: Trade.self, + symbol: "ETH/USD:GDAX", + source: OrderSource.agregateAsk!) else { + XCTAssert(false, "Promises is nil") + return + } + XCTAssert(!promise.hasResult()) + } catch { + XCTAssert(false, "testGetIndexedEventResult \(error)") + } + } + + func testGetIndexedEventResultWithException() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + guard let promise = try feed?.getIndexedEventsPromise(type: Trade.self, + symbol: "ETH/USD:GDAX_TEST", + source: OrderSource.agregateAsk!) else { + XCTAssert(false, "Promises is nil") + return + } + XCTAssert(!promise.hasResult()) + _ = try promise.await(millis: 100) + } catch GraalException.fail(_, _, _) { + } catch { + XCTAssert(false, "testGetIndexedEventResultWithException \(error)") + } + } + + func testGetMultipleResults() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + let promises = try feed?.getLastEventPromises(type: Quote.self, symbols: ["ETH/USD:GDAX", "AAPL"]) + if promises?.isEmpty != false { + XCTAssert(false, "Promises is empty") + } + try promises?.forEach({ promise in + guard let result = try promise.await(millis: 1500), result === (try? promise.getResult()) else { + XCTAssert(false, "Result shoud have value") + return + } + }) + } catch { + XCTAssert(false, "testGetIndexedEventResult \(error)") + } + } + + func testGetAyncMultipleResults() { + do { + let symbols = ["ETH/USD:GDAX", "AAPL"] + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + let promises = try feed?.getLastEventPromises(type: Quote.self, symbols: symbols) + if promises?.isEmpty != false { + XCTAssert(false, "Promises is empty") + } + let expectations = Dictionary(uniqueKeysWithValues: + symbols.map { ($0, expectation(description: "Received promise \($0)")) }) + promises?.forEach({ + $0.whenDone { promise in + if let result = try? promise.getResult() { + let expectation = expectations[result.eventSymbol] + expectation?.fulfill() + } + } + }) + wait(for: Array(expectations.values), timeout: 1) + } catch { + XCTAssert(false, "testGetIndexedEventResult \(error)") + } + } + + func testGetMultipleResultsWithException() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + let promises = try feed?.getLastEventPromises(type: Quote.self, symbols: ["ETH/USD:GDAX_TEST", "AAPL_TEST"]) + if promises?.isEmpty != false { + XCTAssert(false, "Promises is empty") + } + try promises?.forEach({ promise in + guard let result = try promise.await(millis: 1000), result === (try? promise.getResult()) else { + XCTAssert(false, "Result shoud have value") + return + } + }) + } catch GraalException.fail(let message, _, _) { + XCTAssert(message == "await timed out") + } catch { + XCTAssert(false, "testGetMultipleResultsWithException \(error)") + } + } + + func testGetTimeSeriesResult() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + guard let promise = try feed?.getTimeSeriesPromise(type: Candle.self, + symbol: "ETH/USD:GDAX", + fromTime: 1000, + toTime: 0) else { + XCTAssert(false, "Promises is nil") + return + } + _ = try promise.getResults() + } catch { + XCTAssert(false, "testGetTimeSeriesResult \(error)") + } + } + + func testAllOffPromises() { + do { + let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Profile.self, symbol: "IBM", feed: feed!) + guard var promises = try feed?.getLastEventPromises(type: Quote.self, + symbols: ["ETH/USD:GDAX", "AAPL"]) else { + XCTAssert(false, "Empty promises") + return + } + promises.append(promise) + let finalPromise = try Promise.allOf(promises: promises) + let receivedEventExp = expectation(description: "Received promise") + receivedEventExp.expectedFulfillmentCount = promises.count + finalPromise?.whenDone(handler: { _ in + promises.forEach { promise in + if (try? promise.getResult()) != nil { + receivedEventExp.fulfill() + } + } + }) + wait(for: [receivedEventExp], timeout: 1) + promises.removeAll() + wait(seconds: 1) + } catch { + XCTAssert(false, "testAllOffPromises \(error)") + } + + } + + func testCompletePromise() { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + let receivedEventExp = expectation(description: "Received promise") + promise.whenDone { [weak promise]_ in + if (try? promise?.getResult()) != nil { + receivedEventExp.fulfill() + } + } + try promise.complete(result: Quote("AAPL")) + wait(for: [receivedEventExp], timeout: 1) + + } catch { + XCTAssert(false, "testCompletePromise \(error)") + } + } + + func testCompleteExceptPromise() throws { + throw XCTSkip("Graal doesn't have impl for ExceptionMapper.toJava and always throws exception illegalStateException") + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + let receivedEventExp = expectation(description: "Received promise") + promise.whenDone { [weak promise]_ in + receivedEventExp.fulfill() + } + try promise.completeExceptionally(GraalException.fail(message: "Failed from iOS", className: "TestClas", stack: "Stack empty")) + wait(for: [receivedEventExp], timeout: 1) + } catch { + XCTAssert(false, "testCompleteExceptPromise \(error)") + } + } + + func testIsCanceled() throws { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + let isCanceled = promise.isCancelled() + XCTAssert(isCanceled == false) + } catch { + XCTAssert(false, "testIsCanceled \(error)") + } + } + + func testGetException() throws { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + let exception = promise.getException() + XCTAssert(exception == nil) + } catch { + XCTAssert(false, "testGetException \(error)") + } + } + + func testCancelPromise() { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + let receivedEventExp = expectation(description: "Received promise") + promise.whenDone { [weak promise]_ in + if promise?.isCancelled() == true { + receivedEventExp.fulfill() + } + } + try promise.cancel() + wait(for: [receivedEventExp], timeout: 1) + + } catch { + XCTAssert(false, "testCompletePromise \(error)") + } + } + +}