Skip to content

Commit

Permalink
Merge pull request #121 from Infomaniak/core-db
Browse files Browse the repository at this point in the history
feat: Split ios-core into Core and Core DB
  • Loading branch information
adrien-coye committed Apr 24, 2024
2 parents 2fd0658 + 88e9c26 commit 6ec3499
Show file tree
Hide file tree
Showing 11 changed files with 851 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Build
run: xcodebuild -scheme InfomaniakCore build -destination "platform=iOS Simulator,name=iPhone 13,OS=latest"
- name: Test
run: xcodebuild -scheme InfomaniakCore test -destination "platform=iOS Simulator,name=iPhone 13,OS=latest"
run: xcodebuild -scheme InfomaniakCore-Package test -destination "platform=iOS Simulator,name=iPhone 13,OS=latest"

build_and_test_macOS:
name: Build and Test project on macOS
Expand Down
14 changes: 13 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ let package = Package(
.library(
name: "InfomaniakCore",
targets: ["InfomaniakCore"]
),
.library(
name: "InfomaniakCoreDB",
targets: ["InfomaniakCoreDB"]
)
],
dependencies: [
Expand All @@ -38,9 +42,17 @@ let package = Package(
.product(name: "OSInfo", package: "OSInfo"),
]
),
.target(
name: "InfomaniakCoreDB",
dependencies: [
"InfomaniakCore",
.product(name: "InfomaniakDI", package: "ios-dependency-injection"),
.product(name: "RealmSwift", package: "realm-swift"),
]
),
.testTarget(
name: "InfomaniakCoreTests",
dependencies: ["InfomaniakCore","ZIPFoundation"],
dependencies: ["InfomaniakCore", "InfomaniakCoreDB" ,"ZIPFoundation"],
resources: [Resource.copy("Resources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.heic"),
Resource.copy("Resources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.jpg"),
Resource.copy("Resources/dummy.pdf")]
Expand Down
129 changes: 129 additions & 0 deletions Sources/InfomaniakCore/SuddenTermination/ExpiringActivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// Delegation mechanism to notify the end of an `ExpiringActivity`
public protocol ExpiringActivityDelegate: AnyObject {
/// Called when the system is requiring us to terminate an expiring activity
func backgroundActivityExpiring()
}

/// Something that can perform arbitrary short background tasks, with a super simple API.
public protocol ExpiringActivityable {
/// Common init method
/// - Parameters:
/// - id: Something to identify the background activity in debug
/// - qos: QoS used by the underlying queues
/// - delegate: The delegate to notify we should terminate
init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?)

/// init method
/// - Parameters:
/// - id: Something to identify the background activity in debug
/// - delegate: The delegate to notify we should terminate
init(id: String, delegate: ExpiringActivityDelegate?)

/// Register with the system an expiring activity
func start()

/// Terminate all the expiring activities
func endAll()

/// True if the system asked to stop the background activity
var shouldTerminate: Bool { get }
}

public final class ExpiringActivity: ExpiringActivityable {
private let qos: DispatchQoS

private let queue: DispatchQueue

private let processInfo = ProcessInfo.processInfo

var locks = [TolerantDispatchGroup]()

let id: String

public var shouldTerminate = false

weak var delegate: ExpiringActivityDelegate?

// MARK: Lifecycle

public init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?) {
self.id = id
self.qos = qos
self.delegate = delegate
queue = DispatchQueue(label: "com.infomaniak.ExpiringActivity.sync", qos: qos)
}

public convenience init(id: String = "\(#function)-\(UUID().uuidString)", delegate: ExpiringActivityDelegate? = nil) {
self.init(id: id, qos: .userInitiated, delegate: delegate)
}

deinit {
queue.sync {
assert(locks.isEmpty, "please make sure to call 'endAll()' once explicitly before releasing this object")
}
}

public func start() {
let group = TolerantDispatchGroup(qos: qos)

queue.sync {
self.locks.append(group)
}

#if os(macOS)
// We block a non cooperative queue that matches current QoS
DispatchQueue.global(qos: qos.qosClass).async {
self.processInfo.performActivity(options: .suddenTerminationDisabled, reason: self.id) {
// No expiration handler as we are running on macOS
group.enter()
group.wait()
}
}
#else
// Make sure to not lock an unexpected thread that would deinit()
processInfo.performExpiringActivity(withReason: id) { [weak self] shouldTerminate in
guard let self else {
return
}

if shouldTerminate {
self.shouldTerminate = true
delegate?.backgroundActivityExpiring()
}

group.enter()
group.wait()
}
#endif
}

public func endAll() {
queue.sync {
// Release locks, oldest first
for group in locks.reversed() {
group.leave()
}
locks.removeAll()
}
}
}
36 changes: 36 additions & 0 deletions Sources/InfomaniakCoreDB/Extensions/Realm+SafeWrite.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import RealmSwift

public extension Realm {
/// Only emits a write signal if not already within a write transaction
func safeWrite(_ block: () throws -> Void) throws {
if isInWriteTransaction {
try block()
} else {
try write(block)
}
}

/// Shorthand for object from primary key
func getObject<RealmObject: Object, KeyType>(id: KeyType) -> RealmObject? {
object(ofType: RealmObject.self, forPrimaryKey: id)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
Infomaniak Core - iOS
Copyright (C) 2023 Infomaniak Network SA
Copyright (C) 2024 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand Down
39 changes: 39 additions & 0 deletions Sources/InfomaniakCoreDB/RealmConfigurable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import RealmSwift

/// Something that can access a realm
///
/// Do not expose a realm directly in your app, use Transactionable instead
///
public protocol RealmAccessible {
/// Fetches an up to date realm for a given configuration, or fail in a controlled manner
func getRealm() -> Realm
}

/// Something that can provide a realm configuration
public protocol RealmConfigurable {
/// Configuration for a given realm
var realmConfiguration: Realm.Configuration { get }

/// Set `isExcludedFromBackup = true` to the folder where realm is located to exclude a realm cache from an iCloud backup
/// - Important: Avoid calling this method too often as this can be expensive, prefer calling it once at init time
func excludeRealmFromBackup()
}
79 changes: 79 additions & 0 deletions Sources/InfomaniakCoreDB/TransactionExecutor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import InfomaniakCore
import RealmSwift

/// Shared protected DB transaction implementation.
///
/// Only write transactions are protected from sudden termination, will extend to read if required.
public struct TransactionExecutor: Transactionable {
let realmAccessible: RealmAccessible

public init(realmAccessible: RealmAccessible) {
self.realmAccessible = realmAccessible
}

// MARK: Transactionable

public func fetchObject<Element: Object, KeyType>(ofType type: Element.Type, forPrimaryKey key: KeyType) -> Element? {
autoreleasepool {
let realm = realmAccessible.getRealm()
let object = realm.object(ofType: type, forPrimaryKey: key)
return object
}
}

public func fetchObject<Element: RealmFetchable>(ofType type: Element.Type,
filtering: (Results<Element>) -> Element?) -> Element? {
autoreleasepool {
let realm = realmAccessible.getRealm()
let objects = realm.objects(type)
let filteredObject = filtering(objects)
return filteredObject
}
}

public func fetchResults<Element: RealmFetchable>(
ofType type: Element.Type,
filtering: (RealmSwift.Results<Element>) -> RealmSwift.Results<Element>
) -> RealmSwift.Results<Element> {
autoreleasepool {
let realm = realmAccessible.getRealm()
let objects = realm.objects(type)
let filteredCollection = filtering(objects)
return filteredCollection
}
}

public func writeTransaction(withRealm realmClosure: (Realm) throws -> Void) throws {
try autoreleasepool {
let expiringActivity = ExpiringActivity()
expiringActivity.start()
defer {
expiringActivity.endAll()
}

let realm = realmAccessible.getRealm()
try realm.safeWrite {
try realmClosure(realm)
}
}
}
}
Loading

0 comments on commit 6ec3499

Please sign in to comment.