Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions AirCasting.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
33050871276B568D00D18C69 /* ShareSessionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33050870276B568D00D18C69 /* ShareSessionViewModel.swift */; };
33050876276B7A4400D18C69 /* ShareSessionStreamOptionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33050875276B7A4400D18C69 /* ShareSessionStreamOptionViewModel.swift */; };
33056A812DF0773F0087FA75 /* SDCardMobileSessionFinisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33056A802DF0773F0087FA75 /* SDCardMobileSessionFinisher.swift */; };
3310E1F32E252F9400E0281A /* Sequences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3310E1F22E252F9400E0281A /* Sequences.swift */; };
3310E1F52E2534F800E0281A /* PartitionedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3310E1F42E2534F800E0281A /* PartitionedTests.swift */; };
3310E1F72E269C7200E0281A /* FutureAsyncExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3310E1F62E269C7200E0281A /* FutureAsyncExtensionTests.swift */; };
3310E1F92E27D6FE00E0281A /* BatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3310E1F82E27D6FE00E0281A /* BatchError.swift */; };
331D32D528328A92001A6414 /* Sessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331D32D428328A92001A6414 /* Sessionable.swift */; };
331EA80A25E32094009DEFDB /* ConfirmCreatingSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331EA80925E32094009DEFDB /* ConfirmCreatingSessionView.swift */; };
332020A6283CF6D1003E07F3 /* ExternalSessionEntity+Sessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332020A5283CF6D1003E07F3 /* ExternalSessionEntity+Sessionable.swift */; };
Expand Down Expand Up @@ -606,6 +610,10 @@
33050870276B568D00D18C69 /* ShareSessionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSessionViewModel.swift; sourceTree = "<group>"; };
33050875276B7A4400D18C69 /* ShareSessionStreamOptionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSessionStreamOptionViewModel.swift; sourceTree = "<group>"; };
33056A802DF0773F0087FA75 /* SDCardMobileSessionFinisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDCardMobileSessionFinisher.swift; sourceTree = "<group>"; };
3310E1F22E252F9400E0281A /* Sequences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequences.swift; sourceTree = "<group>"; };
3310E1F42E2534F800E0281A /* PartitionedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartitionedTests.swift; sourceTree = "<group>"; };
3310E1F62E269C7200E0281A /* FutureAsyncExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureAsyncExtensionTests.swift; sourceTree = "<group>"; };
3310E1F82E27D6FE00E0281A /* BatchError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchError.swift; sourceTree = "<group>"; };
331D32D428328A92001A6414 /* Sessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sessionable.swift; sourceTree = "<group>"; };
331EA80925E32094009DEFDB /* ConfirmCreatingSessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmCreatingSessionView.swift; sourceTree = "<group>"; };
332020A5283CF6D1003E07F3 /* ExternalSessionEntity+Sessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExternalSessionEntity+Sessionable.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1641,6 +1649,7 @@
AC32040625CC122900A68520 /* Utils */ = {
isa = PBXGroup;
children = (
3310E1F82E27D6FE00E0281A /* BatchError.swift */,
FAFBDBE32BC4942500A9E839 /* Notifications */,
3320F2E22922657C00EFFB2D /* Bluetooth */,
33ECC0E32901B15600A3BC85 /* Averaging */,
Expand Down Expand Up @@ -1686,6 +1695,7 @@
33F87610286343D70073A3D4 /* DownloadableImage.swift */,
ACBF1AD1286C618F006DF793 /* XMarkButton.swift */,
33A16E7A2DF6F76400D9142A /* Concurrency.swift */,
3310E1F22E252F9400E0281A /* Sequences.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -2434,6 +2444,8 @@
B3E26A042676DA14001A8E4C /* Other */ = {
isa = PBXGroup;
children = (
3310E1F62E269C7200E0281A /* FutureAsyncExtensionTests.swift */,
3310E1F42E2534F800E0281A /* PartitionedTests.swift */,
B3E26A052676DA23001A8E4C /* ArrayExtensionsTests.swift */,
B32D089C267A3232006C97ED /* LogErrorPublisherTests.swift */,
B3F1447726AE4337006A4612 /* FilterErrorTests.swift */,
Expand Down Expand Up @@ -3360,6 +3372,7 @@
FA5F782F289A8C0E0016DF79 /* MapDownloaderSensorTypeTests.swift in Sources */,
B30BAC212694AEAA00AE0476 /* GraphStatsDataSourceTests.swift in Sources */,
4FEEAED3262C53C20024AB7E /* UpdateSessionParamsServiceTests.swift in Sources */,
3310E1F52E2534F800E0281A /* PartitionedTests.swift in Sources */,
33F96EAC293F86F000FE7F0A /* Session.swift in Sources */,
33F96EAA293F846B00FE7F0A /* BluetoothDeviceMock.swift in Sources */,
FA5F782D289A8BF60016DF79 /* MapDownloaderMeasurementTypeTests.swift in Sources */,
Expand Down Expand Up @@ -3395,6 +3408,7 @@
B31B7ECD267E500B002DAC18 /* SyncDownstreamServiceTests.swift in Sources */,
B3C6D080289062E100A25F5F /* CLLocation+Helpers.swift in Sources */,
B33B160E27AF0B3600138FFA /* ScheduledLoggerTests.swift in Sources */,
3310E1F72E269C7200E0281A /* FutureAsyncExtensionTests.swift in Sources */,
3320F30929269AB000EFFB2D /* MobileAirBeamSessionRecordingControllerTests.swift in Sources */,
B30BABFB2693509D00AE0476 /* StandardStatisticsCalculatorTests.swift in Sources */,
B3C6D07E28905FCC00A25F5F /* LocationServiceAdapterTests.swift in Sources */,
Expand Down Expand Up @@ -3564,6 +3578,7 @@
DDCE5D3A26A213620033C211 /* DefaultDeleteSessionViewModel.swift in Sources */,
ACCEB21A2763A2230040140B /* SDSyncCompleteView.swift in Sources */,
DDB05581274BC60D00DA1A8A /* SessionUpdateService.swift in Sources */,
3310E1F32E252F9400E0281A /* Sequences.swift in Sources */,
B38E6CD226C5F85000C21C45 /* SyncTriggeringSesionStopperDecorator.swift in Sources */,
3320F2EE29250CA500EFFB2D /* BluetoothConnectionHandler.swift in Sources */,
ACCEB2292768A13B0040140B /* ThresholdsValue.swift in Sources */,
Expand Down Expand Up @@ -3828,6 +3843,7 @@
FAC3B67A283B96480095D915 /* ChartMeasurementsFilter.swift in Sources */,
AC7EB37625A6FB3A00A7F2AF /* AirCastingApp.swift in Sources */,
33AC7D5C273C1B9700EB1274 /* StandaloneSessionCardView.swift in Sources */,
3310E1F92E27D6FE00E0281A /* BatchError.swift in Sources */,
AC4A6A6C267CB3D40001F639 /* GraphRenderers.swift in Sources */,
338CA6CF27CD13E900505331 /* StaticSingleStreamView.swift in Sources */,
B31E361428AECB4300E85431 /* WindowAlertPresenter.swift in Sources */,
Expand Down Expand Up @@ -4078,7 +4094,7 @@
CODE_SIGN_ENTITLEMENTS = AirCasting/AirCasting.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = LXAGXH6CF6;
ENABLE_PREVIEWS = YES;
Expand All @@ -4088,7 +4104,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.23.0;
MARKETING_VERSION = 1.24.0;
PRODUCT_BUNDLE_IDENTIFIER = org.habitatmap.AirCasting;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -4106,7 +4122,7 @@
CODE_SIGN_ENTITLEMENTS = AirCasting/AirCasting.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = LXAGXH6CF6;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = AirCasting/Info.plist;
Expand All @@ -4115,7 +4131,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.23.0;
MARKETING_VERSION = 1.24.0;
PRODUCT_BUNDLE_IDENTIFIER = org.habitatmap.AirCasting;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -4194,7 +4210,7 @@
CODE_SIGN_ENTITLEMENTS = AirCasting/AirCasting.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = LXAGXH6CF6;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = AirCasting/Info.plist;
Expand All @@ -4203,7 +4219,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.23.0;
MARKETING_VERSION = 1.24.0;
PRODUCT_BUNDLE_IDENTIFIER = org.habitatmap.AirCasting;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
7 changes: 4 additions & 3 deletions AirCasting/CoreData/NSManagedObjectContext+Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ extension NSManagedObjectContext {
throw MissingSessionEntityError(uuid: uuid)
}

func optionalExistingSession(uuid: SessionUUID) throws -> SessionEntity? {
func optionalExistingSession(uuid: SessionUUID) -> SessionEntity? {
do {
return try existingSession(uuid: uuid)
} catch is MissingSessionEntityError {
return nil
} catch {
throw error
Log.error("Error when saving changes in \(uuid) session: \(error.localizedDescription)")
return nil
}
}
Comment on lines +50 to 53
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked the call chain of the method and in the end the only error handling was this line of logging, so I moved it here and made the method not throw, to handle the nil cases conveniently from the client code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behold, useless error throwing. Good call 👍


Expand Down Expand Up @@ -84,7 +85,7 @@ extension NSManagedObjectContext {
}

func existingSessionable(uuid: SessionUUID) throws -> Sessionable? {
if let session = try optionalExistingSession(uuid: uuid) { return session }
if let session = optionalExistingSession(uuid: uuid) { return session }
if let externalSession = try optionalExistingExternalSession(uuid: uuid) { return externalSession }
return nil
}
Expand Down
41 changes: 33 additions & 8 deletions AirCasting/CoreData/PersistenceController+Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ extension PersistenceController: SessionsFetchable {
}
}
}
func isExisting(uuid: SessionUUID) -> Bool {
return self.editContext.optionalExistingSession(uuid: uuid) != nil
}
}

extension PersistenceController: SessionInsertable {
func insertSessions(_ sessions: [Database.Session], completion: ((Error?) -> Void)?) {
func insertSessions(_ sessions: [Database.Session]) async throws {
let context = self.editContext
context.perform {
try await context.perform {
sessions.forEach {
// This is added to ensure that we don't add session if a session with this uuid already existis in the database
guard (try? context.existingSession(uuid: $0.uuid)) == nil else {
Expand Down Expand Up @@ -79,12 +82,8 @@ extension PersistenceController: SessionInsertable {
streamEntity.session = sessionEntity
}
}
do {
try context.save()
completion?(nil)
} catch {
completion?(error)
}

try context.save()
}
}
}
Expand Down Expand Up @@ -147,6 +146,32 @@ extension PersistenceController: SessionUpdateable {
}
}
}

func updateSessions(with sessionsData: [SessionsSynchronization.SessionStoreSessionData]) async throws {
let context = self.editContext
var errors: [Error] = []

await context.perform {
sessionsData.forEach {
do {
let sessionEntity = try context.existingSession(uuid: $0.uuid)
sessionEntity.name = $0.name
sessionEntity.tags = $0.tags
sessionEntity.endTime = $0.endTime
sessionEntity.version = if let version = $0.version { Int16(version) } else { sessionEntity.version }
sessionEntity.urlLocation = $0.urlLocation
try context.save()
} catch {
Log.error("Error updating session \($0.name): \(error)")
errors.append(error)
}
}
}

if !errors.isEmpty {
throw BatchError(errors: errors)
}
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ final class SessionSynchronizationDatabase: SessionSynchronizationStore {
}

func addSessions(with sessionsData: [SessionsSynchronization.SessionStoreSessionData]) -> Future<Void, Error> {
return .init { [sessionsInserter] promise in
sessionsInserter
.insertSessions(sessionsData.map { sessionData in
return .init {
let (existingSessionsUpdatedData, newSessionsData) = sessionsData.partitioned(by: {
self.sessionsFetcher.isExisting(uuid: $0.uuid)
})

try await self.sessionsInserter
.insertSessions(newSessionsData.map { sessionData in
let streams = sessionData.measurementStreams.map {
Database.MeasurementStream(id: MeasurementStreamID($0.id),
sensorName: $0.sensorName,
Expand Down Expand Up @@ -94,13 +98,9 @@ final class SessionSynchronizationDatabase: SessionSynchronizationStore {
measurementStreams: streams,
status: .FINISHED,
notes: notes)
}, completion: { error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(()))
}
})

try await self.sessionsUpdater.updateSessions(with: existingSessionsUpdatedData)
}
}

Expand Down
4 changes: 3 additions & 1 deletion AirCasting/SessionsSynchronization/Database/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ extension Database {

protocol SessionsFetchable {
func fetchSessions(constrained: Database.Constraint, completion: @escaping (Result<[Database.Session], Error>) -> Void)
func isExisting(uuid: SessionUUID) -> Bool
}

protocol SessionRemovable {
func removeSessions(where: Database.Constraint, completion: ((Error?) -> Void)?)
}

protocol SessionInsertable {
func insertSessions(_ sessions: [Database.Session], completion: ((Error?) -> Void)?)
func insertSessions(_ sessions: [Database.Session]) async throws
}

protocol SessionUpdateable {
func updateSessionUrl(_ url: String, for session: SessionUUID, completion: ((Error?) -> Void)?)
func updateNotesPhotosLocations(notesUrls: [(url: URL, noteNumber: Int)], for session: SessionUUID, completion: ((Error?) -> Void)?)
func updateSessions(with sessionsData: [SessionsSynchronization.SessionStoreSessionData]) async throws
}
18 changes: 18 additions & 0 deletions AirCasting/Utils/BatchError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
struct BatchError: Error {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, there is 99% chance it will not be used ever again ^^'

let errors: [Error]

var localizedDescription: String {
let failureCount = errors.count

var description = "Batch operation completed with \(failureCount) failures"

if !errors.isEmpty {
description += "\n\nFailure details:"
for (index, error) in errors.enumerated() {
description += "\n\(index + 1). \(error.localizedDescription)"
}
}

return description
}
}
16 changes: 16 additions & 0 deletions AirCasting/Utils/Concurrency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//

import Foundation
import Combine

func waitFor<T>(_ operation: @escaping () async throws -> T) throws -> T {
guard !Thread.isMainThread else {
Expand All @@ -28,3 +29,18 @@ func waitFor<T>(_ operation: @escaping () async throws -> T) throws -> T {
}
return result
}

extension Future where Failure == Error {
convenience init(asyncOperation: @escaping () async throws -> Output) {
self.init { promise in
Task {
do {
let result = try await asyncOperation()
promise(.success(result))
} catch {
promise(.failure(error))
}
}
}
}
}
Comment on lines +33 to +46
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this to bridge Future client code and refactored async code to get rid of the callbacks

21 changes: 21 additions & 0 deletions AirCasting/Utils/Sequences.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Created by Lunar on 14.07.25.
//

import Foundation

extension Sequence {
func partitioned(by condition: (Element) -> Bool) -> (matching: [Element], nonMatching: [Element]) {
var matching = [Element]()
var nonMatching = [Element]()

for element in self {
if condition(element) {
matching.append(element)
} else {
nonMatching.append(element)
}
}

return (matching, nonMatching)
}
}
Comment on lines +6 to +21
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Searched for some library that has this specifically but didn't find any

8 changes: 8 additions & 0 deletions AirCastingTests/BluetoothProtectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ class BluetoothProtectorTests: ACTestCase {
}

class AirBeamDatabaseSpy: SessionsFetchable {
func isExisting(uuid: AirCasting.SessionUUID) -> Bool {
true
}

var constrainedCalls: [Database.Constraint] = []

func fetchSessions(constrained: Database.Constraint, completion: @escaping (Result<[Database.Session], Error>) -> Void) {
Expand All @@ -191,6 +195,10 @@ class BluetoothProtectorTests: ACTestCase {
}

class AirBeamDatabseStub: SessionsFetchable {
func isExisting(uuid: AirCasting.SessionUUID) -> Bool {
true
}

private let toReturn: Result<[Database.Session], Error>

init(toReturn: Result<[Database.Session], Error>) {
Expand Down
Loading
Loading