diff --git a/Example/SwiftCentrifuge.xcodeproj/project.pbxproj b/Example/SwiftCentrifuge.xcodeproj/project.pbxproj index d47cb14..e40afc0 100644 --- a/Example/SwiftCentrifuge.xcodeproj/project.pbxproj +++ b/Example/SwiftCentrifuge.xcodeproj/project.pbxproj @@ -3,16 +3,20 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 3A58470528C9CF4800D85C0E /* SwiftCentrifuge in Frameworks */ = {isa = PBXBuildFile; productRef = 3A58470428C9CF4800D85C0E /* SwiftCentrifuge */; }; 3A451D4028D2EE33004F85A5 /* PrintLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A451D3F28D2EE33004F85A5 /* PrintLogger.swift */; }; + 3A58470528C9CF4800D85C0E /* SwiftCentrifuge in Frameworks */ = {isa = PBXBuildFile; productRef = 3A58470428C9CF4800D85C0E /* SwiftCentrifuge */; }; 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; + 91D46D152CA2148500223592 /* DeltaFossilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D46D142CA2148500223592 /* DeltaFossilTests.swift */; }; + 91D46D312CA2216800223592 /* delta in Resources */ = {isa = PBXBuildFile; fileRef = 91D46D2E2CA2216800223592 /* delta */; }; + 91D46D322CA2216800223592 /* origin in Resources */ = {isa = PBXBuildFile; fileRef = 91D46D2F2CA2216800223592 /* origin */; }; + 91D46D332CA2216800223592 /* target in Resources */ = {isa = PBXBuildFile; fileRef = 91D46D302CA2216800223592 /* target */; }; DE2E2CA9220FEAC8004541DE /* BasicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7C1AA8220F0E1A0037AE1E /* BasicTests.swift */; }; DE7C1AA4220F0D950037AE1E /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE7C1AA3220F0D950037AE1E /* Launch Screen.storyboard */; }; /* End PBXBuildFile section */ @@ -35,6 +39,10 @@ 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 91D46D142CA2148500223592 /* DeltaFossilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeltaFossilTests.swift; sourceTree = ""; }; + 91D46D2E2CA2216800223592 /* delta */ = {isa = PBXFileReference; lastKnownFileType = file; path = delta; sourceTree = ""; }; + 91D46D2F2CA2216800223592 /* origin */ = {isa = PBXFileReference; lastKnownFileType = file; path = origin; sourceTree = ""; }; + 91D46D302CA2216800223592 /* target */ = {isa = PBXFileReference; lastKnownFileType = file; path = target; sourceTree = ""; }; DE2E2C9F220FEAC4004541DE /* SwiftCentrifuge_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftCentrifuge_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DE2E2CA3220FEAC4004541DE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DE7C1AA3220F0D950037AE1E /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; @@ -122,6 +130,10 @@ isa = PBXGroup; children = ( DE7C1AA8220F0E1A0037AE1E /* BasicTests.swift */, + 91D46D142CA2148500223592 /* DeltaFossilTests.swift */, + 91D46D2E2CA2216800223592 /* delta */, + 91D46D2F2CA2216800223592 /* origin */, + 91D46D302CA2216800223592 /* target */, ); name = SwiftCentrifugeTests; path = ../../Tests/SwiftCentrifugeTests; @@ -224,6 +236,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 91D46D312CA2216800223592 /* delta in Resources */, + 91D46D322CA2216800223592 /* origin in Resources */, + 91D46D332CA2216800223592 /* target in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -244,6 +259,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 91D46D152CA2148500223592 /* DeltaFossilTests.swift in Sources */, DE2E2CA9220FEAC8004541DE /* BasicTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/SwiftCentrifuge/Client.swift b/Sources/SwiftCentrifuge/Client.swift index 44dc1e7..9166ea7 100644 --- a/Sources/SwiftCentrifuge/Client.swift +++ b/Sources/SwiftCentrifuge/Client.swift @@ -83,6 +83,7 @@ public class CentrifugeClient { fileprivate var client: String? fileprivate var token: String? fileprivate var data: Data? + fileprivate var originalSourceData: Data? // to check and handle delta fossil implementation fileprivate var commandId: UInt32 = 0 fileprivate var commandIdLock: NSLock = NSLock() fileprivate var opCallbacks: [UInt32: ((CentrifugeResolveData) -> ())] = [:] @@ -518,8 +519,8 @@ internal extension CentrifugeClient { subscriptionsLock.unlock() } - func subscribe(channel: String, token: String, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) { - self.sendSubscribe(channel: channel, token: token, data: data, recover: recover, streamPosition: streamPosition, positioned: positioned, recoverable: recoverable, joinLeave: joinLeave, completion: completion) + func subscribe(channel: String, token: String, delta: String?, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) { + self.sendSubscribe(channel: channel, token: token, delta: delta, data: data, recover: recover, streamPosition: streamPosition, positioned: positioned, recoverable: recoverable, joinLeave: joinLeave, completion: completion) } func reconnect(code: UInt32, reason: String) { @@ -778,7 +779,22 @@ fileprivate extension CentrifugeClient { if pub.hasInfo { info = CentrifugeClientInfo(client: pub.info.client, user: pub.info.user, connInfo: pub.info.connInfo, chanInfo: pub.info.chanInfo) } - let event = CentrifugePublicationEvent(data: pub.data, offset: pub.offset, tags: pub.tags, info: info) + + var eventData: Data! + + if let originalSourceData { + // Assuming the pub.data contains delta-encoded data + do { + let data = try DeltaFossil.applyDelta(source: originalSourceData, delta: pub.data) + eventData = data + } catch { + eventData = pub.data + } + } else { + eventData = pub.data + } + self.originalSourceData = eventData + let event = CentrifugePublicationEvent(data: eventData, offset: pub.offset, tags: pub.tags, info: info) if pub.offset > 0 { sub.setOffset(offset: pub.offset) } @@ -842,6 +858,7 @@ fileprivate extension CentrifugeClient { private func handleSubscribe(channel: String, sub: Centrifugal_Centrifuge_Protocol_Subscribe) { self.serverSubs[channel] = ServerSubscription(recoverable: sub.recoverable, offset: sub.offset, epoch: sub.epoch) + self.originalSourceData = nil // make sure data will be nil on a new subscription let event = CentrifugeServerSubscribedEvent(channel: channel, wasRecovering: false, recovered: false, positioned: sub.positioned, recoverable: sub.recoverable, streamPosition: sub.positioned || sub.recoverable ? StreamPosition(offset: sub.offset, epoch: sub.epoch): nil, data: sub.data) self.delegate?.onSubscribed(self, event) } @@ -1123,7 +1140,7 @@ fileprivate extension CentrifugeClient { }) } - private func sendSubscribe(channel: String, token: String, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) { + private func sendSubscribe(channel: String, token: String, delta: String?, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) { var req = Centrifugal_Centrifuge_Protocol_SubscribeRequest() req.channel = channel if recover { @@ -1134,6 +1151,9 @@ fileprivate extension CentrifugeClient { req.positioned = positioned req.recoverable = recoverable req.joinLeave = joinLeave + if let delta { + req.delta = delta + } if data != nil { req.data = data! } diff --git a/Sources/SwiftCentrifuge/DeltaFossil.swift b/Sources/SwiftCentrifuge/DeltaFossil.swift new file mode 100644 index 0000000..39c573d --- /dev/null +++ b/Sources/SwiftCentrifuge/DeltaFossil.swift @@ -0,0 +1,200 @@ +// +// DeltaFossil.swift +// iOSApp +// +// Created by Mehdi on 9/21/24. +// + +import Foundation + +public enum DeltaFossil { + private struct ZValue { + static let values: [Int8] = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, + -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, -1, 37, + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, + ] + } + + enum DeltaError: Error { + case outOfBounds + case sizeIntegerNotTerminated + case copyCommandNotTerminated + case copyExceedsOutputSize + case copyExtendsPastEnd + case insertExceedsOutputSize + case insertCountExceedsDelta + case unknownDeltaOperator + case unterminatedDelta + case badChecksum + case sizeMismatch + } + + // Apply delta fossil algorithm + public static func applyDelta(source: Data, delta: Data) throws -> Data { + var total: UInt32 = 0 + var deltaReader = Reader(array: [UInt8](delta)) // Convert Data to ByteArray for Reader + var outputWriter = Writer() + + let sourceLength = source.count + let deltaLength = delta.count + + let limit = try deltaReader.getInt() + + guard try deltaReader.getChar() == "\n" else { + throw DeltaError.sizeIntegerNotTerminated + } + + while deltaReader.haveBytes() { + let count = try deltaReader.getInt() + let command = try deltaReader.getChar() + + switch command { + case "@": + let offset = try deltaReader.getInt() + guard try deltaReader.getChar() == "," else { + throw DeltaError.copyCommandNotTerminated + } + total += count + guard total <= limit else { throw DeltaError.copyExceedsOutputSize } + guard offset + count <= sourceLength else { throw DeltaError.copyExtendsPastEnd } + outputWriter.putArray([UInt8](source), start: Int(offset), end: Int(offset + count)) + + case ":": + total += count + guard total <= limit else { throw DeltaError.insertExceedsOutputSize } + guard count <= deltaLength else { throw DeltaError.insertCountExceedsDelta } + outputWriter.putArray(deltaReader.array, start: deltaReader.position, end: deltaReader.position + Int(count)) + deltaReader.position += Int(count) + + case ";": + let output = outputWriter.toByteArray() + guard count == checksum(array: output) else { throw DeltaError.badChecksum } + guard total == limit else { throw DeltaError.sizeMismatch } + return Data(output) // Convert ByteArray back to Data + + default: + throw DeltaError.unknownDeltaOperator + } + } + + throw DeltaError.unterminatedDelta + } + + // Reader + private struct Reader { + let array: [UInt8] + var position: Int = 0 + + init(array: [UInt8]) { + self.array = array + } + + func haveBytes() -> Bool { + return position < array.count + } + + mutating func getByte() throws -> UInt8 { + guard position < array.count else { throw DeltaError.outOfBounds } + let byte = array[position] + position += 1 + return byte + } + + mutating func getChar() throws -> Character { + return Character(UnicodeScalar(try getByte())) + } + + mutating func getInt() throws -> UInt32 { + var value: UInt32 = 0 + while haveBytes() { + let byte = try getByte() + let c = ZValue.values[Int(0x7f & byte)] + if c < 0 { + position -= 1 // Adjust for invalid character + return value + } + value = (value << 6) + UInt32(c) + } + return value + } + } + + // Writer + private struct Writer { + private var array: [UInt8] = [] + + func toByteArray() -> [UInt8] { + return array + } + + mutating func putArray(_ array: [UInt8], start: Int, end: Int) { + self.array.append(contentsOf: array[start.. UInt32 { + var sum0: UInt32 = 0 + var sum1: UInt32 = 0 + var sum2: UInt32 = 0 + var sum3: UInt32 = 0 + var z: Int = 0 + var N = array.count + + while N >= 16 { + sum0 = sum0 &+ UInt32(array[z + 0]) + sum1 = sum1 &+ UInt32(array[z + 1]) + sum2 = sum2 &+ UInt32(array[z + 2]) + sum3 = sum3 &+ UInt32(array[z + 3]) + + sum0 = sum0 &+ UInt32(array[z + 4]) + sum1 = sum1 &+ UInt32(array[z + 5]) + sum2 = sum2 &+ UInt32(array[z + 6]) + sum3 = sum3 &+ UInt32(array[z + 7]) + + sum0 = sum0 &+ UInt32(array[z + 8]) + sum1 = sum1 &+ UInt32(array[z + 9]) + sum2 = sum2 &+ UInt32(array[z + 10]) + sum3 = sum3 &+ UInt32(array[z + 11]) + + sum0 = sum0 &+ UInt32(array[z + 12]) + sum1 = sum1 &+ UInt32(array[z + 13]) + sum2 = sum2 &+ UInt32(array[z + 14]) + sum3 = sum3 &+ UInt32(array[z + 15]) + + z += 16 + N -= 16 + } + + while N >= 4 { + sum0 = sum0 &+ UInt32(array[z + 0]) + sum1 = sum1 &+ UInt32(array[z + 1]) + sum2 = sum2 &+ UInt32(array[z + 2]) + sum3 = sum3 &+ UInt32(array[z + 3]) + z += 4 + N -= 4 + } + + sum3 = (((sum3 &+ (sum2 << 8)) &+ (sum1 << 16)) &+ (sum0 << 24)) + + switch N { + case 3: + sum3 = sum3 &+ (UInt32(array[z + 2]) << 8) + fallthrough // Allow fallthrough for the next case + case 2: + sum3 = sum3 &+ (UInt32(array[z + 1]) << 16) + fallthrough // Allow fallthrough for the next case + case 1: + sum3 = sum3 &+ (UInt32(array[z + 0]) << 24) + default: + break + } + + return sum3 + } +} diff --git a/Sources/SwiftCentrifuge/Subscription.swift b/Sources/SwiftCentrifuge/Subscription.swift index e551d69..bc64bc9 100644 --- a/Sources/SwiftCentrifuge/Subscription.swift +++ b/Sources/SwiftCentrifuge/Subscription.swift @@ -28,6 +28,7 @@ public struct CentrifugeSubscriptionConfig { public var minResubscribeDelay = 0.5 public var maxResubscribeDelay = 20.0 public var token: String = "" + public var delta: String = "" public var data: Data? = nil public var since: CentrifugeStreamPosition? = nil public var positioned: Bool = false @@ -53,6 +54,7 @@ public class CentrifugeSubscription { private var epoch: String = "" fileprivate var token: String? + fileprivate var delta: String? fileprivate var refreshTask: DispatchWorkItem? fileprivate var resubscribeTask: DispatchWorkItem? fileprivate var resubscribeAttempts: Int = 0 @@ -75,6 +77,9 @@ public class CentrifugeSubscription { self.offset = since.offset self.epoch = since.epoch } + if !config.delta.isEmpty { + self.delta = config.delta + } } public var state: CentrifugeSubscriptionState { @@ -155,13 +160,13 @@ public class CentrifugeSubscription { ) } - func sendSubscribe(channel: String, token: String) { + func sendSubscribe(channel: String, token: String, delta: String?) { var streamPosition = StreamPosition() if self.recover { streamPosition.offset = self.offset streamPosition.epoch = self.epoch } - self.centrifuge?.subscribe(channel: self.channel, token: token, data: self.config.data, recover: self.recover, streamPosition: streamPosition, positioned: self.config.positioned, recoverable: self.config.recoverable, joinLeave: self.config.joinLeave, completion: { [weak self, weak centrifuge = self.centrifuge] res, error in + self.centrifuge?.subscribe(channel: self.channel, token: token, delta: delta, data: self.config.data, recover: self.recover, streamPosition: streamPosition, positioned: self.config.positioned, recoverable: self.config.recoverable, joinLeave: self.config.joinLeave, completion: { [weak self, weak centrifuge = self.centrifuge] res, error in guard let centrifuge = centrifuge else { return } guard let strongSelf = self else { return } guard strongSelf.state == .subscribing else { return } @@ -368,7 +373,7 @@ public class CentrifugeSubscription { if self.token != nil || self.config.tokenGetter != nil { if self.token != nil { let token = self.token! - self.sendSubscribe(channel: self.channel, token: token) + self.sendSubscribe(channel: self.channel, token: token, delta: delta) } else { self.getSubscriptionToken(channel: self.channel, completion: { [weak self] result in guard let strongSelf = self, strongSelf.state == .subscribing else { return } @@ -380,7 +385,7 @@ public class CentrifugeSubscription { } strongSelf.centrifuge?.syncQueue.async { [weak self] in guard let strongSelf = self, strongSelf.state == .subscribing else { return } - strongSelf.sendSubscribe(channel: strongSelf.channel, token: token) + strongSelf.sendSubscribe(channel: strongSelf.channel, token: token, delta: strongSelf.delta) } case .failure(let error): guard let strongSelf = self else { return } @@ -403,7 +408,7 @@ public class CentrifugeSubscription { }) } } else { - self.sendSubscribe(channel: self.channel, token: "") + self.sendSubscribe(channel: self.channel, token: "", delta: self.delta) } } diff --git a/Sources/SwiftCentrifuge/client.pb.swift b/Sources/SwiftCentrifuge/client.pb.swift index b2f7c7a..6a9a229 100644 --- a/Sources/SwiftCentrifuge/client.pb.swift +++ b/Sources/SwiftCentrifuge/client.pb.swift @@ -636,6 +636,8 @@ struct Centrifugal_Centrifuge_Protocol_ConnectRequest { var version: String = String() + var delta: String = String() + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -677,6 +679,7 @@ struct Centrifugal_Centrifuge_Protocol_RefreshRequest { // methods supported on all messages. var token: String = String() + var delta: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() @@ -724,6 +727,8 @@ struct Centrifugal_Centrifuge_Protocol_SubscribeRequest { var joinLeave: Bool = false + var delta: String = String() + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -767,6 +772,8 @@ struct Centrifugal_Centrifuge_Protocol_SubRefreshRequest { var channel: String = String() var token: String = String() + + var delta: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2044,6 +2051,7 @@ extension Centrifugal_Centrifuge_Protocol_ConnectRequest: SwiftProtobuf.Message, 3: .same(proto: "subs"), 4: .same(proto: "name"), 5: .same(proto: "version"), + 6: .same(proto: "delta") ] mutating func decodeMessage(decoder: inout D) throws { @@ -2057,6 +2065,7 @@ extension Centrifugal_Centrifuge_Protocol_ConnectRequest: SwiftProtobuf.Message, case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.subs) }() case 4: try { try decoder.decodeSingularStringField(value: &self.name) }() case 5: try { try decoder.decodeSingularStringField(value: &self.version) }() + case 6: try { try decoder.decodeSingularStringField(value: &self.delta) }() default: break } } @@ -2078,6 +2087,9 @@ extension Centrifugal_Centrifuge_Protocol_ConnectRequest: SwiftProtobuf.Message, if !self.version.isEmpty { try visitor.visitSingularStringField(value: self.version, fieldNumber: 5) } + if !self.delta.isEmpty { + try visitor.visitSingularStringField(value: self.delta, fieldNumber: 6) + } try unknownFields.traverse(visitor: &visitor) } @@ -2272,6 +2284,7 @@ extension Centrifugal_Centrifuge_Protocol_SubscribeRequest: SwiftProtobuf.Messag 9: .same(proto: "positioned"), 10: .same(proto: "recoverable"), 11: .standard(proto: "join_leave"), + 12: .same(proto: "delta"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2289,6 +2302,7 @@ extension Centrifugal_Centrifuge_Protocol_SubscribeRequest: SwiftProtobuf.Messag case 9: try { try decoder.decodeSingularBoolField(value: &self.positioned) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.recoverable) }() case 11: try { try decoder.decodeSingularBoolField(value: &self.joinLeave) }() + case 12: try { try decoder.decodeSingularStringField(value: &self.delta) }() default: break } } @@ -2322,6 +2336,9 @@ extension Centrifugal_Centrifuge_Protocol_SubscribeRequest: SwiftProtobuf.Messag if self.joinLeave != false { try visitor.visitSingularBoolField(value: self.joinLeave, fieldNumber: 11) } + if !self.delta.isEmpty { + try visitor.visitSingularStringField(value: self.delta, fieldNumber: 12) + } try unknownFields.traverse(visitor: &visitor) } diff --git a/Tests/SwiftCentrifugeTests/DeltaFossilTests.swift b/Tests/SwiftCentrifugeTests/DeltaFossilTests.swift new file mode 100644 index 0000000..9f92cd1 --- /dev/null +++ b/Tests/SwiftCentrifugeTests/DeltaFossilTests.swift @@ -0,0 +1,37 @@ +// +// DeltaFossilTests.swift +// SwiftCentrifuge +// +// Created by Mehdi on 9/22/24. +// + +import XCTest +@testable import SwiftCentrifuge + +final class DeltaFossilTests: XCTestCase { + func testDeltaCreateAndApply() throws { + guard let origin = loadData(from: "origin"), + let target = loadData(from: "target"), + let goodDelta = loadData(from: "delta") else { + return + } + + // Test with Data + do { + let delta = try DeltaFossil.applyDelta(source: origin, delta: goodDelta) + XCTAssertEqual(delta, target) + } catch { + XCTFail("Error applying delta: \(error)") + } + } + + // Helper function to load data from file + private func loadData(from fileName: String) -> Data? { + let bundle = Bundle(for: DeltaFossilTests.self) // Get the bundle for this test class + guard let url = bundle.url(forResource: fileName, withExtension: nil, subdirectory: nil) else { + XCTFail("Could not find file \(fileName)") + return nil + } + return try? Data(contentsOf: url) + } +} diff --git a/Tests/SwiftCentrifugeTests/delta b/Tests/SwiftCentrifugeTests/delta new file mode 100644 index 0000000..dc0d571 Binary files /dev/null and b/Tests/SwiftCentrifugeTests/delta differ diff --git a/Tests/SwiftCentrifugeTests/origin b/Tests/SwiftCentrifugeTests/origin new file mode 100644 index 0000000..956c49f Binary files /dev/null and b/Tests/SwiftCentrifugeTests/origin differ diff --git a/Tests/SwiftCentrifugeTests/target b/Tests/SwiftCentrifugeTests/target new file mode 100644 index 0000000..1cf6b78 Binary files /dev/null and b/Tests/SwiftCentrifugeTests/target differ