From 47fda4bd64553bc90a9d4933bf00efc98f10d7d2 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:33:50 -0500 Subject: [PATCH 1/6] Refactor public classes, structs, and enums to be Sendable (#761) --- CHANGELOG.md | 1 + .../Adapters/GCDWebServer/GCDHTTPServer.swift | 8 +-- .../SQLiteLCPLicenseRepository.swift | 4 +- .../SQLiteLCPPassphraseRepository.swift | 4 +- Sources/Internal/Extensions/Task.swift | 2 +- Sources/Internal/Keychain.swift | 2 +- Sources/Internal/UTI.swift | 4 +- .../Authentications/LCPAuthenticating.swift | 6 +- Sources/LCP/Authentications/LCPDialog.swift | 4 +- .../LCPDialogAuthentication.swift | 2 +- .../LCPObservableAuthentication.swift | 4 +- .../LCPPassphraseAuthentication.swift | 2 +- Sources/LCP/LCPAcquiredPublication.swift | 2 +- Sources/LCP/LCPClient.swift | 2 +- Sources/LCP/LCPError.swift | 12 ++-- Sources/LCP/LCPLicenseRepository.swift | 2 +- Sources/LCP/LCPProgress.swift | 2 +- Sources/LCP/LCPRenewDelegate.swift | 2 +- Sources/LCP/LCPService.swift | 2 +- .../Model/Components/LCP/ContentKey.swift | 2 +- .../Model/Components/LCP/Encryption.swift | 2 +- .../License/Model/Components/LCP/Rights.swift | 2 +- .../Model/Components/LCP/Signature.swift | 2 +- .../License/Model/Components/LCP/User.swift | 2 +- .../Model/Components/LCP/UserKey.swift | 2 +- .../License/Model/Components/LSD/Event.swift | 4 +- .../Components/LSD/PotentialRights.swift | 2 +- .../LCP/License/Model/Components/Link.swift | 2 +- .../LCP/License/Model/Components/Links.swift | 2 +- .../LCP/License/Model/LicenseDocument.swift | 4 +- .../LCP/License/Model/StatusDocument.swift | 6 +- .../LCPKeychainLicenseRepository.swift | 2 +- .../LCPKeychainPassphraseRepository.swift | 2 +- Sources/LCP/Toolkit/DataCompression.swift | 4 +- .../Navigator/Audiobook/AudioNavigator.swift | 6 +- .../Preferences/AudioPreferences.swift | 2 +- .../Audiobook/Preferences/AudioSettings.swift | 4 +- .../Decorator/DecorableNavigator.swift | 2 +- .../DirectionalNavigationAdapter.swift | 6 +- .../Navigator/EPUB/CSS/CSSProperties.swift | 58 +++++++++---------- .../EPUB/CSS/HTMLFontFamilyDeclaration.swift | 10 ++-- .../EPUB/EPUBNavigatorViewController.swift | 4 +- .../EPUB/HTMLDecorationTemplate.swift | 4 +- .../EPUB/Preferences/EPUBPreferences.swift | 2 +- .../EPUB/Preferences/EPUBSettings.swift | 4 +- Sources/Navigator/EditingAction.swift | 2 +- Sources/Navigator/Input/Key/Key.swift | 2 +- Sources/Navigator/Input/Key/KeyEvent.swift | 4 +- .../Navigator/Input/Key/KeyModifiers.swift | 2 +- .../Input/Pointer/PointerEvent.swift | 6 +- Sources/Navigator/Navigator.swift | 4 +- .../PDF/PDFNavigatorViewController.swift | 2 +- .../PDF/Preferences/PDFPreferences.swift | 2 +- .../PDF/Preferences/PDFSettings.swift | 4 +- .../Navigator/Preferences/Configurable.swift | 2 +- .../Preferences/MappedPreference.swift | 6 +- .../Navigator/Preferences/Preference.swift | 4 +- .../Preferences/ProgressionStrategy.swift | 22 +++---- .../Preferences/ProxyPreference.swift | 4 +- Sources/Navigator/Preferences/Types.swift | 20 +++---- Sources/Navigator/SelectableNavigator.swift | 2 +- Sources/Navigator/TTS/AVTTSEngine.swift | 4 +- .../TTS/PublicationSpeechSynthesizer.swift | 10 ++-- Sources/Navigator/TTS/TTSEngine.swift | 8 +-- Sources/Navigator/TTS/TTSVoice.swift | 6 +- Sources/Navigator/VisualNavigator.swift | 2 +- Sources/OPDS/OPDS1Parser.swift | 36 ++++++------ Sources/OPDS/OPDS2Parser.swift | 34 +++++------ Sources/OPDS/OPDSParser.swift | 4 +- Sources/OPDS/ParseData.swift | 2 +- Sources/Shared/Logger/Loggable.swift | 2 +- Sources/Shared/Logger/LoggerStub.swift | 2 +- Sources/Shared/OPDS/Facet.swift | 2 +- Sources/Shared/OPDS/Feed.swift | 2 +- Sources/Shared/OPDS/Group.swift | 2 +- Sources/Shared/OPDS/OPDSAcquisition.swift | 2 +- Sources/Shared/OPDS/OPDSAvailability.swift | 4 +- Sources/Shared/OPDS/OPDSCopies.swift | 2 +- Sources/Shared/OPDS/OPDSHolds.swift | 2 +- Sources/Shared/OPDS/OPDSPrice.swift | 2 +- Sources/Shared/OPDS/OpdsMetadata.swift | 2 +- .../AccessibilityMetadataDisplayGuide.swift | 16 ++--- .../Extensions/EPUB/EPUBLayout.swift | 2 +- .../Extensions/Encryption/Encryption.swift | 2 +- .../Extensions/HTML/DOMRange.swift | 4 +- Sources/Shared/Publication/Link.swift | 2 +- .../Protection/ContentProtection.swift | 6 +- .../FallbackContentProtection.swift | 2 +- Sources/Shared/Publication/Publication.swift | 2 +- .../Content Protection/UserRights.swift | 4 +- .../Services/Content/Content.swift | 8 +-- .../Services/Content/ContentService.swift | 2 +- .../HTMLResourceContentIterator.swift | 4 +- .../PublicationContentIterator.swift | 4 +- .../Positions/InMemoryPositionsService.swift | 2 +- .../Services/Search/SearchService.swift | 4 +- .../Services/Search/StringSearchService.swift | 6 +- .../Toolkit/Archive/ArchiveOpener.swift | 4 +- .../Toolkit/Archive/ArchiveProperties.swift | 2 +- .../Archive/DefaultArchiveOpener.swift | 2 +- .../Toolkit/Data/Asset/AssetRetriever.swift | 4 +- .../Toolkit/Data/Container/Container.swift | 4 +- .../Container/SingleResourceContainer.swift | 2 +- Sources/Shared/Toolkit/Data/ReadError.swift | 4 +- .../Data/Resource/FailureResource.swift | 2 +- .../Resource/ResourceContentExtractor.swift | 4 +- .../Data/Resource/ResourceFactory.swift | 2 +- .../Data/Resource/ResourceProperties.swift | 2 +- Sources/Shared/Toolkit/DebugError.swift | 2 +- Sources/Shared/Toolkit/DocumentTypes.swift | 4 +- Sources/Shared/Toolkit/Either.swift | 2 +- .../Toolkit/File/DirectoryContainer.swift | 4 +- .../Shared/Toolkit/File/FileContainer.swift | 2 +- .../Toolkit/File/FileResourceFactory.swift | 2 +- .../Shared/Toolkit/File/FileSystemError.swift | 2 +- Sources/Shared/Toolkit/FileExtension.swift | 2 +- Sources/Shared/Toolkit/Format/Format.swift | 6 +- .../Shared/Toolkit/Format/FormatSniffer.swift | 2 +- .../Format/Sniffers/AudioFormatSniffer.swift | 2 +- .../Sniffers/AudiobookFormatSniffer.swift | 2 +- .../Format/Sniffers/BitmapFormatSniffer.swift | 2 +- .../Format/Sniffers/ComicFormatSniffer.swift | 2 +- .../Format/Sniffers/EPUBFormatSniffer.swift | 2 +- .../Format/Sniffers/HTMLFormatSniffer.swift | 2 +- .../Format/Sniffers/JSONFormatSniffer.swift | 2 +- .../Sniffers/LCPLicenseFormatSniffer.swift | 2 +- .../Sniffers/LanguageFormatSniffer.swift | 2 +- .../Format/Sniffers/OPDSFormatSniffer.swift | 2 +- .../Format/Sniffers/PDFFormatSniffer.swift | 2 +- .../Format/Sniffers/RARFormatSniffer.swift | 2 +- .../Format/Sniffers/RPFFormatSniffer.swift | 2 +- .../Format/Sniffers/RWPMFormatSniffer.swift | 2 +- .../Format/Sniffers/XMLFormatSniffer.swift | 2 +- .../Format/Sniffers/ZIPFormatSniffer.swift | 2 +- .../Toolkit/HTTP/DefaultHTTPClient.swift | 2 +- Sources/Shared/Toolkit/HTTP/HTTPClient.swift | 4 +- .../Toolkit/HTTP/HTTPProblemDetails.swift | 2 +- Sources/Shared/Toolkit/HTTP/HTTPRequest.swift | 6 +- .../Toolkit/HTTP/HTTPResourceFactory.swift | 2 +- Sources/Shared/Toolkit/HTTP/HTTPServer.swift | 2 +- Sources/Shared/Toolkit/JSONValue.swift | 6 +- .../Toolkit/Logging/WarningLogger.swift | 4 +- .../Shared/Toolkit/Media/AudioSession.swift | 4 +- .../Shared/Toolkit/Media/NowPlayingInfo.swift | 2 +- Sources/Shared/Toolkit/PDF/CGPDF.swift | 2 +- Sources/Shared/Toolkit/PDF/PDFDocument.swift | 6 +- Sources/Shared/Toolkit/PDF/PDFKit.swift | 2 +- .../Shared/Toolkit/PDF/PDFOutlineNode.swift | 2 +- .../Toolkit/Tokenizer/TextTokenizer.swift | 4 +- Sources/Shared/Toolkit/URL/AnyURL.swift | 2 +- Sources/Shared/Toolkit/URL/RelativeURL.swift | 2 +- Sources/Shared/Toolkit/URL/URITemplate.swift | 2 +- Sources/Shared/Toolkit/URL/URLQuery.swift | 4 +- Sources/Shared/Toolkit/Weak.swift | 24 +------- Sources/Shared/Toolkit/XML/XML.swift | 6 +- .../ZIP/Minizip/MinizipArchiveOpener.swift | 2 +- .../Shared/Toolkit/ZIP/ZIPArchiveOpener.swift | 2 +- .../ZIPFoundationArchiveOpener.swift | 2 +- .../AudioPublicationManifestAugmentor.swift | 2 +- Sources/Streamer/Parser/EPUB/EPUBParser.swift | 4 +- Sources/Streamer/Parser/EPUB/OPFParser.swift | 2 +- .../EPUBDeobfuscator.swift | 2 +- .../Parser/EPUB/SMIL/SMILParser.swift | 2 +- .../EPUB/Services/EPUBPositionsService.swift | 2 +- Sources/Streamer/Parser/PDF/PDFParser.swift | 2 +- .../Streamer/Parser/PublicationParser.swift | 2 +- .../Parser/Readium/ReadiumWebPubParser.swift | 8 +-- Sources/Streamer/PublicationOpener.swift | 4 +- .../Streamer/Toolkit/DataCompression.swift | 4 +- .../OPDS/OPDSFeeds/OPDSFeedViewModel.swift | 2 +- .../Locator/DefaultLocatorServiceTests.swift | 30 +++++----- .../Services/AudioLocatorServiceTests.swift | 22 +++---- docs/Migration Guide.md | 5 +- 173 files changed, 379 insertions(+), 399 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c38cce1ab4..d06d093873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to this project will be documented in this file. Take a look #### Shared * All public types that parsed or serialized JSON now use the new type-safe `JSONValue` enum instead of `Any` / `[String: Any]`. See [the migration guide](docs/Migration%20Guide.md) for upgrade instructions. +* OPDS models (`Feed`, `Group`, `Facet`, `OpdsMetadata`) are now structs with value semantics. #### Navigator diff --git a/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift b/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift index 6624771b58..d64bfcaa87 100644 --- a/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift +++ b/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift @@ -5,20 +5,20 @@ // import Foundation -import ReadiumGCDWebServer +@preconcurrency import ReadiumGCDWebServer import ReadiumInternal import ReadiumShared import UIKit -public enum GCDHTTPServerError: Error { - case failedToStartServer(cause: Error) +public enum GCDHTTPServerError: Error, Sendable { + case failedToStartServer(cause: any Error) case serverNotStarted case invalidEndpoint(HTTPServerEndpoint) case nullServerURL } /// Implementation of `HTTPServer` using ReadiumGCDWebServer under the hood. -public class GCDHTTPServer: HTTPServer, Loggable { +public final class GCDHTTPServer: HTTPServer, Loggable { /// The actual underlying HTTP server instance. private let server = ReadiumGCDWebServer() diff --git a/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift b/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift index 9dba0b82cf..ad44e0ccfb 100644 --- a/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift +++ b/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift @@ -7,10 +7,10 @@ import Foundation import ReadiumLCP import ReadiumShared -import SQLite +@preconcurrency import SQLite @available(*, deprecated, message: "Use LCPKeychainLicenseRepository from ReadiumLCP instead") -public class LCPSQLiteLicenseRepository: LCPLicenseRepository, Loggable { +public final class LCPSQLiteLicenseRepository: LCPLicenseRepository, Loggable, Sendable { let licenses = Table("Licenses") let id = SQLite.Expression("id") let printsLeft = SQLite.Expression("printsLeft") diff --git a/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift b/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift index 7255273d4f..d971465940 100644 --- a/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift +++ b/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift @@ -7,10 +7,10 @@ import Foundation import ReadiumLCP import ReadiumShared -import SQLite +@preconcurrency import SQLite @available(*, deprecated, message: "Use LCPKeychainPassphraseRepository from ReadiumLCP instead") -public class LCPSQLitePassphraseRepository: LCPPassphraseRepository, Loggable { +public final class LCPSQLitePassphraseRepository: LCPPassphraseRepository, Loggable, Sendable { let transactions = Table("Transactions") let licenseId = SQLite.Expression("licenseId") let provider = SQLite.Expression("origin") diff --git a/Sources/Internal/Extensions/Task.swift b/Sources/Internal/Extensions/Task.swift index f6358ec44a..46fa09859c 100644 --- a/Sources/Internal/Extensions/Task.swift +++ b/Sources/Internal/Extensions/Task.swift @@ -7,7 +7,7 @@ import Foundation @MainActor -public final class CancellableTasks { +public final class CancellableTasks: Sendable { private var tasks: Set> = [] public nonisolated init() {} diff --git a/Sources/Internal/Keychain.swift b/Sources/Internal/Keychain.swift index cd6e33b34c..8af65ff577 100644 --- a/Sources/Internal/Keychain.swift +++ b/Sources/Internal/Keychain.swift @@ -8,7 +8,7 @@ import Foundation import Security /// Errors occurring in ``Keychain``. -public enum KeychainError: Error { +public enum KeychainError: Error, Sendable { /// The item was not found in the Keychain. case itemNotFound diff --git a/Sources/Internal/UTI.swift b/Sources/Internal/UTI.swift index 63d1e89262..c9ac46bda7 100644 --- a/Sources/Internal/UTI.swift +++ b/Sources/Internal/UTI.swift @@ -8,9 +8,9 @@ import Foundation import UniformTypeIdentifiers /// Uniform Type Identifier. -public struct UTI { +public struct UTI: Sendable { /// Type tag class, eg. UTTagClass.mimeType. - public enum TagClass { + public enum TagClass: Sendable { case mediaType, fileExtension } diff --git a/Sources/LCP/Authentications/LCPAuthenticating.swift b/Sources/LCP/Authentications/LCPAuthenticating.swift index 6fdd1c8da6..2401167b3f 100644 --- a/Sources/LCP/Authentications/LCPAuthenticating.swift +++ b/Sources/LCP/Authentications/LCPAuthenticating.swift @@ -6,7 +6,7 @@ import Foundation -public protocol LCPAuthenticating { +public protocol LCPAuthenticating: Sendable { /// Retrieves the passphrase to decrypt the given license. /// /// If `allowUserInteraction` is true, the reading app can prompt the user to enter the @@ -31,14 +31,14 @@ public protocol LCPAuthenticating { ) async -> String? } -public enum LCPAuthenticationReason { +public enum LCPAuthenticationReason: Sendable { /// No matching passphrase was found. case passphraseNotFound /// The provided passphrase was invalid. case invalidPassphrase } -public struct LCPAuthenticatedLicense { +public struct LCPAuthenticatedLicense: Sendable { /// A hint to be displayed to the User to help them remember the User Passphrase. public var hint: String { document.encryption.userKey.textHint diff --git a/Sources/LCP/Authentications/LCPDialog.swift b/Sources/LCP/Authentications/LCPDialog.swift index 8cf54c9755..bf3bc0f609 100644 --- a/Sources/LCP/Authentications/LCPDialog.swift +++ b/Sources/LCP/Authentications/LCPDialog.swift @@ -46,8 +46,8 @@ import SwiftUI /// } /// } /// ``` -public struct LCPDialog: View { - public enum ErrorMessage { +public struct LCPDialog: View, Sendable { + public enum ErrorMessage: Sendable { case incorrectPassphrase var string: String { diff --git a/Sources/LCP/Authentications/LCPDialogAuthentication.swift b/Sources/LCP/Authentications/LCPDialogAuthentication.swift index 43a1c2e7ac..fb9e8d01cb 100644 --- a/Sources/LCP/Authentications/LCPDialogAuthentication.swift +++ b/Sources/LCP/Authentications/LCPDialogAuthentication.swift @@ -13,7 +13,7 @@ import UIKit /// For this authentication to trigger, you must provide a `sender` parameter of type /// `UIViewController` to `Streamer.open()` or `LCPService.retrieveLicense()`. It will be used /// as the presenting view controller for the dialog. -public class LCPDialogAuthentication: LCPAuthenticating, Loggable { +public final class LCPDialogAuthentication: LCPAuthenticating, Loggable, Sendable { private let animated: Bool private let modalPresentationStyle: UIModalPresentationStyle private let modalTransitionStyle: UIModalTransitionStyle diff --git a/Sources/LCP/Authentications/LCPObservableAuthentication.swift b/Sources/LCP/Authentications/LCPObservableAuthentication.swift index e088eccf98..7472ba7833 100644 --- a/Sources/LCP/Authentications/LCPObservableAuthentication.swift +++ b/Sources/LCP/Authentications/LCPObservableAuthentication.swift @@ -12,12 +12,12 @@ import SwiftUI /// Pair an ``LCPObservableAuthentication`` with an ``LCPDialog`` to implement /// the LCP authentication in SwiftUI. @MainActor -public final class LCPObservableAuthentication: LCPAuthenticating, ObservableObject { +public final class LCPObservableAuthentication: LCPAuthenticating, ObservableObject, Sendable { /// Represents an on-going LCP authentication request. /// /// You must call the `submit()` or `cancel()` API to conclude the request. @MainActor - public final class Request: Identifiable { + public final class Request: Identifiable, Sendable { /// LCP License requested to be unlocked. public let license: LCPAuthenticatedLicense diff --git a/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift b/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift index 4940739f34..3f678def10 100644 --- a/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift +++ b/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift @@ -10,7 +10,7 @@ import Foundation /// passphrase. /// /// If the provided `passphrase` is incorrect, the given `fallback` authentication is used. -public class LCPPassphraseAuthentication: LCPAuthenticating { +public final class LCPPassphraseAuthentication: LCPAuthenticating, Sendable { private let passphrase: String private let fallback: LCPAuthenticating? diff --git a/Sources/LCP/LCPAcquiredPublication.swift b/Sources/LCP/LCPAcquiredPublication.swift index aa1f9fe249..5438b5daec 100644 --- a/Sources/LCP/LCPAcquiredPublication.swift +++ b/Sources/LCP/LCPAcquiredPublication.swift @@ -9,7 +9,7 @@ import ReadiumShared /// Holds information about an LCP protected publication which was acquired /// from an LCPL. -public struct LCPAcquiredPublication { +public struct LCPAcquiredPublication: Sendable { /// Path to the downloaded publication. /// /// You must move this file to the user library's folder. diff --git a/Sources/LCP/LCPClient.swift b/Sources/LCP/LCPClient.swift index de7330fa5d..8dcc6f72b6 100644 --- a/Sources/LCP/LCPClient.swift +++ b/Sources/LCP/LCPClient.swift @@ -45,7 +45,7 @@ public typealias LCPClientContext = Any /// Copy of the R2LCPClient.LCPClientError enum. /// /// Order is important, because it is used to match the original enum cases. -public enum LCPClientError: Int, Error { +public enum LCPClientError: Int, Error, Sendable { case licenseOutOfDate = 0 case certificateRevoked case certificateSignatureInvalid diff --git a/Sources/LCP/LCPError.swift b/Sources/LCP/LCPError.swift index 9e156f3ad8..8f4f2245a7 100644 --- a/Sources/LCP/LCPError.swift +++ b/Sources/LCP/LCPError.swift @@ -67,7 +67,7 @@ public enum LCPError: Error { /// from the number of "register" events in the status document. If no event is /// logged in the status document, no such message should appear (certainly not /// "The license was registered by 0 devices"). -public enum StatusError: Error { +public enum StatusError: Error, Sendable { /// This license was cancelled on the given date. case cancelled(Date) /// This license has been returned on the given date. @@ -80,7 +80,7 @@ public enum StatusError: Error { } /// Errors while renewing a loan. -public enum RenewError: Error { +public enum RenewError: Error, Sendable { /// Your publication could not be renewed properly. case renewFailed /// Incorrect renewal period, your publication could not be renewed. @@ -90,7 +90,7 @@ public enum RenewError: Error { } /// Errors while returning a loan. -public enum ReturnError: Error { +public enum ReturnError: Error, Sendable { /// Your publication could not be returned properly. case returnFailed /// Your publication has already been returned before or is expired. @@ -100,7 +100,7 @@ public enum ReturnError: Error { } /// Errors while parsing the License or Status JSON Documents. -public enum ParsingError: Error { +public enum ParsingError: Error, Sendable { /// The JSON is malformed and can't be parsed. case malformedJSON /// The JSON is not representing a valid License Document. @@ -118,9 +118,9 @@ public enum ParsingError: Error { } /// Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) -public enum ContainerError: Error { +public enum ContainerError: Error, Sendable { /// Can't access the container, it's format is wrong. - case openFailed(Error?) + case openFailed((any Error)?) /// The file at given relative path is not found in the Container. case fileNotFound(String) /// Can't read the file at given relative path in the Container. diff --git a/Sources/LCP/LCPLicenseRepository.swift b/Sources/LCP/LCPLicenseRepository.swift index f8d99f9093..3d249d7587 100644 --- a/Sources/LCP/LCPLicenseRepository.swift +++ b/Sources/LCP/LCPLicenseRepository.swift @@ -39,7 +39,7 @@ public protocol LCPLicenseRepository { } /// Holds the current state of consumable user rights for a license. -public struct LCPConsumableUserRights { +public struct LCPConsumableUserRights: Sendable { /// Maximum number of pages left to be printed. /// /// If `nil`, there is no limit. diff --git a/Sources/LCP/LCPProgress.swift b/Sources/LCP/LCPProgress.swift index bdf8fa971e..5a061b27ed 100644 --- a/Sources/LCP/LCPProgress.swift +++ b/Sources/LCP/LCPProgress.swift @@ -7,7 +7,7 @@ import Foundation /// Percent-based progress of the acquisition. -public enum LCPProgress { +public enum LCPProgress: Sendable { /// Undetermined progress, a spinner should be shown to the user. case indefinite /// A finite progress from 0.0 to 1.0, a progress bar should be shown to the user. diff --git a/Sources/LCP/LCPRenewDelegate.swift b/Sources/LCP/LCPRenewDelegate.swift index 190f948705..3cad3c06a2 100644 --- a/Sources/LCP/LCPRenewDelegate.swift +++ b/Sources/LCP/LCPRenewDelegate.swift @@ -28,7 +28,7 @@ public protocol LCPRenewDelegate { /// /// No date picker is presented for selecting a preferred end date. If you want to support one, you can subclass or /// decorate `LCPRenewDelegate`. -public class LCPDefaultRenewDelegate: NSObject, LCPRenewDelegate { +public final class LCPDefaultRenewDelegate: NSObject, LCPRenewDelegate { private let presentingViewController: UIViewController private let modalPresentationStyle: UIModalPresentationStyle diff --git a/Sources/LCP/LCPService.swift b/Sources/LCP/LCPService.swift index 55b6c82552..fbfe23dcb0 100644 --- a/Sources/LCP/LCPService.swift +++ b/Sources/LCP/LCPService.swift @@ -157,7 +157,7 @@ public final class LCPService: Loggable { } /// Source of an LCP License Document (LCPL) file. -public enum LicenseDocumentSource { +public enum LicenseDocumentSource: Sendable { /// Raw bytes of the LCPL. case data(Data) diff --git a/Sources/LCP/License/Model/Components/LCP/ContentKey.swift b/Sources/LCP/License/Model/Components/LCP/ContentKey.swift index 174034decc..d936d441ad 100644 --- a/Sources/LCP/License/Model/Components/LCP/ContentKey.swift +++ b/Sources/LCP/License/Model/Components/LCP/ContentKey.swift @@ -9,7 +9,7 @@ import ReadiumShared /// Used to encrypt the Publication Resources. /// This is encrypted using the User Key. -public struct ContentKey: JSONValueDecodable { +public struct ContentKey: JSONValueDecodable, Sendable { /// Algorithm used to encrypt the Content Key, identified using the URIs defined in [XML-ENC]. This MUST match the Content Key encryption algorithm named in the Encryption Profile identified in `encryption/profile`. public let algorithm: String /// Encrypted Content Key. diff --git a/Sources/LCP/License/Model/Components/LCP/Encryption.swift b/Sources/LCP/License/Model/Components/LCP/Encryption.swift index 9b00cab48b..3422573016 100644 --- a/Sources/LCP/License/Model/Components/LCP/Encryption.swift +++ b/Sources/LCP/License/Model/Components/LCP/Encryption.swift @@ -7,7 +7,7 @@ import Foundation import ReadiumShared -public struct Encryption: JSONValueDecodable { +public struct Encryption: JSONValueDecodable, Sendable { /// Identifies the Encryption Profile used by this LCP-protected Publication. public let profile: String /// Used to encrypt the Publication Resources. diff --git a/Sources/LCP/License/Model/Components/LCP/Rights.swift b/Sources/LCP/License/Model/Components/LCP/Rights.swift index aee383bb50..5ce72af080 100644 --- a/Sources/LCP/License/Model/Components/LCP/Rights.swift +++ b/Sources/LCP/License/Model/Components/LCP/Rights.swift @@ -7,7 +7,7 @@ import Foundation import ReadiumShared -public struct Rights: JSONValueDecodable { +public struct Rights: JSONValueDecodable, Sendable { /// Maximum number of pages that can be printed over the lifetime of the license. public let print: Int? /// Maximum number of characters that can be copied to the clipboard over the lifetime of the license. diff --git a/Sources/LCP/License/Model/Components/LCP/Signature.swift b/Sources/LCP/License/Model/Components/LCP/Signature.swift index a77104220e..a55ad7cf65 100644 --- a/Sources/LCP/License/Model/Components/LCP/Signature.swift +++ b/Sources/LCP/License/Model/Components/LCP/Signature.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// Signature allowing to certify the License Document integrity. -public struct Signature: JSONValueDecodable { +public struct Signature: JSONValueDecodable, Sendable { /// Algorithm used to calculate the signature, identified using the URIs given in [XML-SIG]. This MUST match the signature algorithm named in the Encryption Profile identified in `encryption/profile`. public let algorithm: String /// The Provider Certificate: an X509 certificate used by the Content Provider. diff --git a/Sources/LCP/License/Model/Components/LCP/User.swift b/Sources/LCP/License/Model/Components/LCP/User.swift index c21be867f8..03dff6d128 100644 --- a/Sources/LCP/License/Model/Components/LCP/User.swift +++ b/Sources/LCP/License/Model/Components/LCP/User.swift @@ -7,7 +7,7 @@ import Foundation import ReadiumShared -public struct User: JSONValueDecodable { +public struct User: JSONValueDecodable, Sendable { public typealias ID = String /// Unique identifier for the User at a specific Provider. diff --git a/Sources/LCP/License/Model/Components/LCP/UserKey.swift b/Sources/LCP/License/Model/Components/LCP/UserKey.swift index 92760e92a2..0b58a7e690 100644 --- a/Sources/LCP/License/Model/Components/LCP/UserKey.swift +++ b/Sources/LCP/License/Model/Components/LCP/UserKey.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// Used to encrypt the ContentKey. -public struct UserKey: JSONValueDecodable { +public struct UserKey: JSONValueDecodable, Sendable { /// A hint to be displayed to the User to help them remember the User Passphrase. public let textHint: String /// Algorithm used to generate the User Key from the User Passphrase, identified using the URIs defined in [XML-ENC]. This MUST match the User Key hash algorithm named in the Encryption Profile identified in `encryption/profile`. diff --git a/Sources/LCP/License/Model/Components/LSD/Event.swift b/Sources/LCP/License/Model/Components/LSD/Event.swift index 7f8e1fdae5..7d463b4db6 100644 --- a/Sources/LCP/License/Model/Components/LSD/Event.swift +++ b/Sources/LCP/License/Model/Components/LSD/Event.swift @@ -8,8 +8,8 @@ import Foundation import ReadiumShared /// Event related to the change in status of a License Document. -public struct Event: JSONValueDecodable { - public enum EventType: String { +public struct Event: JSONValueDecodable, Sendable { + public enum EventType: String, Sendable { /// Signals a successful registration event by a device. case register /// Signals a successful renew event. diff --git a/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift b/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift index f70e910a2e..5caa03f577 100644 --- a/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift +++ b/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift @@ -7,7 +7,7 @@ import Foundation import ReadiumShared -public struct PotentialRights: JSONValueDecodable { +public struct PotentialRights: JSONValueDecodable, Sendable { /// Time and Date when the license ends. public let end: Date? diff --git a/Sources/LCP/License/Model/Components/Link.swift b/Sources/LCP/License/Model/Components/Link.swift index 0f676245f4..91edb5037d 100644 --- a/Sources/LCP/License/Model/Components/Link.swift +++ b/Sources/LCP/License/Model/Components/Link.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// A Link to a resource. -public struct Link: JSONValueDecodable { +public struct Link: JSONValueDecodable, Sendable { /// The link destination. public let href: String /// Indicates the relationship between the resource and its containing collection. diff --git a/Sources/LCP/License/Model/Components/Links.swift b/Sources/LCP/License/Model/Components/Links.swift index 3e937dd6f6..47752cd472 100644 --- a/Sources/LCP/License/Model/Components/Links.swift +++ b/Sources/LCP/License/Model/Components/Links.swift @@ -7,7 +7,7 @@ import Foundation import ReadiumShared -public struct Links: JSONValueDecodable { +public struct Links: JSONValueDecodable, Sendable { private let links: [Link] public init?(json: T?, warnings: WarningLogger?) throws { diff --git a/Sources/LCP/License/Model/LicenseDocument.swift b/Sources/LCP/License/Model/LicenseDocument.swift index beeb02ec1c..e5537b5519 100644 --- a/Sources/LCP/License/Model/LicenseDocument.swift +++ b/Sources/LCP/License/Model/LicenseDocument.swift @@ -9,12 +9,12 @@ import ReadiumShared /// Document that contains references to the various keys, links to related external resources, rights and restrictions that are applied to the Protected Publication, and user information. /// https://github.com/readium/lcp-specs/blob/master/schema/license.schema.json -public struct LicenseDocument { +public struct LicenseDocument: Sendable { public typealias ID = String public typealias Provider = String /// The possible rel of Links. - public enum Rel: String { + public enum Rel: String, Sendable { /// Location where a Reading System can redirect a User looking for additional information about the User Passphrase. case hint /// Location where the Publication associated with the License Document can be downloaded diff --git a/Sources/LCP/License/Model/StatusDocument.swift b/Sources/LCP/License/Model/StatusDocument.swift index 0857e7d2d2..cfca63dc5a 100644 --- a/Sources/LCP/License/Model/StatusDocument.swift +++ b/Sources/LCP/License/Model/StatusDocument.swift @@ -9,8 +9,8 @@ import ReadiumShared /// Document that contains information about the history of a License Document, along with its current status and available interactions. /// https://github.com/readium/lcp-specs/blob/master/schema/status.schema.json -public struct StatusDocument { - public enum Status: String { +public struct StatusDocument: Sendable { + public enum Status: String, Sendable { /// The License Document is available, but the user hasn't accessed the License and/or Status Document yet. case ready /// The license is active, and a device has been successfully registered for this license. This is the default value if the License Document does not contain a registration link, or a registration mechanism through the license itself. @@ -25,7 +25,7 @@ public struct StatusDocument { case expired } - public enum Rel: String { + public enum Rel: String, Sendable { case register case license case `return` diff --git a/Sources/LCP/Repositories/Keychain/LCPKeychainLicenseRepository.swift b/Sources/LCP/Repositories/Keychain/LCPKeychainLicenseRepository.swift index c42db9b1a4..63b029b8a1 100644 --- a/Sources/LCP/Repositories/Keychain/LCPKeychainLicenseRepository.swift +++ b/Sources/LCP/Repositories/Keychain/LCPKeychainLicenseRepository.swift @@ -9,7 +9,7 @@ import ReadiumInternal import ReadiumShared /// Errors occurring in ``LCPKeychainLicenseRepository``. -public enum LCPKeychainLicenseRepositoryError: Error { +public enum LCPKeychainLicenseRepositoryError: Error, Sendable { /// The license with the given `id` was not found in the repository. case licenseNotFound(id: LicenseDocument.ID) diff --git a/Sources/LCP/Repositories/Keychain/LCPKeychainPassphraseRepository.swift b/Sources/LCP/Repositories/Keychain/LCPKeychainPassphraseRepository.swift index 13ff5b1f22..425919f490 100644 --- a/Sources/LCP/Repositories/Keychain/LCPKeychainPassphraseRepository.swift +++ b/Sources/LCP/Repositories/Keychain/LCPKeychainPassphraseRepository.swift @@ -9,7 +9,7 @@ import ReadiumInternal import ReadiumShared /// Errors occurring in ``LCPKeychainPassphraseRepository``. -public enum LCPKeychainPassphraseRepositoryError: Error { +public enum LCPKeychainPassphraseRepositoryError: Error, Sendable { /// An error occurred while accessing the keychain. case keychain(KeychainError) diff --git a/Sources/LCP/Toolkit/DataCompression.swift b/Sources/LCP/Toolkit/DataCompression.swift index e6b93a33ab..de4b1bbf61 100644 --- a/Sources/LCP/Toolkit/DataCompression.swift +++ b/Sources/LCP/Toolkit/DataCompression.swift @@ -260,7 +260,7 @@ public extension Data { } /// Struct based type representing a Crc32 checksum. -public struct Crc32: CustomStringConvertible { +public struct Crc32: CustomStringConvertible, Sendable { private static let zLibCrc32: ZLibCrc32FuncPtr? = loadCrc32fromZLib() public init() {} @@ -349,7 +349,7 @@ public struct Crc32: CustomStringConvertible { } /// Struct based type representing a Adler32 checksum. -public struct Adler32: CustomStringConvertible { +public struct Adler32: CustomStringConvertible, Sendable { private static let zLibAdler32: ZLibAdler32FuncPtr? = loadAdler32fromZLib() public init() {} diff --git a/Sources/Navigator/Audiobook/AudioNavigator.swift b/Sources/Navigator/Audiobook/AudioNavigator.swift index a7753ec62a..8abcf37e3d 100644 --- a/Sources/Navigator/Audiobook/AudioNavigator.swift +++ b/Sources/Navigator/Audiobook/AudioNavigator.swift @@ -9,14 +9,14 @@ import Foundation import ReadiumShared /// Status of a played media resource. -public enum MediaPlaybackState { +public enum MediaPlaybackState: Sendable { case paused case loading case playing } /// Holds metadata about a played media resource. -public struct MediaPlaybackInfo { +public struct MediaPlaybackInfo: Sendable { /// Index of the current resource in the `readingOrder`. public let resourceIndex: Int @@ -80,7 +80,7 @@ public extension AudioNavigatorDelegate { public final class AudioNavigator: Navigator, Configurable, AudioSessionUser, Loggable { public weak var delegate: AudioNavigatorDelegate? - public struct Configuration { + public struct Configuration: Sendable { /// Initial set of setting preferences. public var preferences: AudioPreferences diff --git a/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift b/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift index 54e4da684e..0554b46985 100644 --- a/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift +++ b/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// Preferences for the `AudioNavigator`. -public struct AudioPreferences: ConfigurablePreferences { +public struct AudioPreferences: ConfigurablePreferences, Sendable { public static let empty: AudioPreferences = .init() /// Volume of playback, from 0.0 to 1.0. diff --git a/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift b/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift index be042b74d7..d3f117bca5 100644 --- a/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift +++ b/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift @@ -10,7 +10,7 @@ import ReadiumShared /// Setting values of the `AudioNavigator`. /// /// See `AudioPreferences` -public struct AudioSettings: ConfigurableSettings { +public struct AudioSettings: ConfigurableSettings, Sendable { public let volume: Double public let speed: Double @@ -30,7 +30,7 @@ public struct AudioSettings: ConfigurableSettings { /// These values will be used when no publication metadata or user preference takes precedence. /// /// See `AudioPreferences`. -public struct AudioDefaults { +public struct AudioDefaults: Sendable { public var volume: Double? public var speed: Double? diff --git a/Sources/Navigator/Decorator/DecorableNavigator.swift b/Sources/Navigator/Decorator/DecorableNavigator.swift index 2f8749b75d..f8b5d7c353 100644 --- a/Sources/Navigator/Decorator/DecorableNavigator.swift +++ b/Sources/Navigator/Decorator/DecorableNavigator.swift @@ -89,7 +89,7 @@ public struct Decoration: Hashable, JSONObjectEncodable { /// instructions which makes sense for the resource type. public struct Style: Hashable { /// Unique ID for a style. - public struct Id: RawRepresentable, ExpressibleByStringLiteral, Hashable, JSONValueEncodable { + public struct Id: RawRepresentable, ExpressibleByStringLiteral, Hashable, JSONValueEncodable, Sendable { public let rawValue: String public init(rawValue: String) { self.rawValue = rawValue diff --git a/Sources/Navigator/DirectionalNavigationAdapter.swift b/Sources/Navigator/DirectionalNavigationAdapter.swift index ce32177ef5..574052bb01 100644 --- a/Sources/Navigator/DirectionalNavigationAdapter.swift +++ b/Sources/Navigator/DirectionalNavigationAdapter.swift @@ -17,7 +17,7 @@ import Foundation public typealias TapEdges = Edges /// Indicates which viewport edges trigger page turns on pointer activation. - public struct Edges: OptionSet { + public struct Edges: OptionSet, Sendable { /// The user can turn pages when tapping on the edges of both the /// horizontal and vertical axes. public static let all: Edges = [.horizontal, .vertical] @@ -35,7 +35,7 @@ import Foundation /// Policy controlling how pointer events (touches, mouse clicks) trigger /// page turns. - public struct PointerPolicy { + public struct PointerPolicy: Sendable { /// The types of pointer that will trigger page turns. public var types: [PointerType] @@ -84,7 +84,7 @@ import Foundation } /// Policy controlling how keyboard events trigger page turns. - public struct KeyboardPolicy { + public struct KeyboardPolicy: Sendable { /// Indicates whether arrow keys should turn pages. public var handleArrowKeys: Bool diff --git a/Sources/Navigator/EPUB/CSS/CSSProperties.swift b/Sources/Navigator/EPUB/CSS/CSSProperties.swift index ec654d1dcd..a4765a5d83 100644 --- a/Sources/Navigator/EPUB/CSS/CSSProperties.swift +++ b/Sources/Navigator/EPUB/CSS/CSSProperties.swift @@ -35,7 +35,7 @@ public extension CSSProperties { /// User settings properties. /// /// See https://readium.org/readium-css/docs/CSS19-api.html#user-settings -public struct CSSUserProperties: CSSProperties { +public struct CSSUserProperties: CSSProperties, Sendable { // View mode /// User view: paged or scrolled. @@ -261,7 +261,7 @@ public struct CSSUserProperties: CSSProperties { /// Reading System properties. /// /// See https://readium.org/readium-css/docs/CSS19-api.html#reading-system-styles -public struct CSSRSProperties: CSSProperties { +public struct CSSRSProperties: CSSProperties, Sendable { // Pagination /// @param colWidth The optimal column’s width. It serves as a floor in our design. @@ -530,7 +530,7 @@ public struct CSSRSProperties: CSSProperties { } } -public enum CSSView: String, CSSConvertible { +public enum CSSView: String, CSSConvertible, Sendable { case paged = "readium-paged-on" case scroll = "readium-scroll-on" @@ -539,7 +539,7 @@ public enum CSSView: String, CSSConvertible { } } -public enum CSSColCount: String, CSSConvertible { +public enum CSSColCount: String, CSSConvertible, Sendable { case auto case one = "1" case two = "2" @@ -549,7 +549,7 @@ public enum CSSColCount: String, CSSConvertible { } } -public enum CSSAppearance: String, CSSConvertible { +public enum CSSAppearance: String, CSSConvertible, Sendable { case night = "readium-night-on" case sepia = "readium-sepia-on" @@ -558,9 +558,9 @@ public enum CSSAppearance: String, CSSConvertible { } } -public protocol CSSColor: CSSConvertible {} +public protocol CSSColor: CSSConvertible, Sendable {} -public struct CSSRGBColor: CSSColor { +public struct CSSRGBColor: CSSColor, Sendable { let red: Int let green: Int let blue: Int @@ -579,7 +579,7 @@ public struct CSSRGBColor: CSSColor { } } -public struct CSSHexColor: CSSColor { +public struct CSSHexColor: CSSColor, Sendable { let color: String public init(_ color: String) { @@ -591,7 +591,7 @@ public struct CSSHexColor: CSSColor { } } -public struct CSSIntColor: CSSColor { +public struct CSSIntColor: CSSColor, Sendable { let color: Int public init(_ color: Int) { @@ -603,12 +603,12 @@ public struct CSSIntColor: CSSColor { } } -public protocol CSSLength: CSSConvertible {} +public protocol CSSLength: CSSConvertible, Sendable {} public protocol CSSAbsoluteLength: CSSLength {} /// Centimeters -public struct CSSCmLength: CSSAbsoluteLength { +public struct CSSCmLength: CSSAbsoluteLength, Sendable { public let value: Double public init(_ value: Double) { @@ -621,7 +621,7 @@ public struct CSSCmLength: CSSAbsoluteLength { } /// Millimeters -public struct CSSMmLength: CSSAbsoluteLength { +public struct CSSMmLength: CSSAbsoluteLength, Sendable { public let value: Double public init(_ value: Double) { @@ -634,7 +634,7 @@ public struct CSSMmLength: CSSAbsoluteLength { } /// Inches -public struct CSSInLength: CSSAbsoluteLength { +public struct CSSInLength: CSSAbsoluteLength, Sendable { public let value: Double public init(_ value: Double) { @@ -647,7 +647,7 @@ public struct CSSInLength: CSSAbsoluteLength { } /// Pixels -public struct CSSPxLength: CSSAbsoluteLength { +public struct CSSPxLength: CSSAbsoluteLength, Sendable { public let value: Double public init(_ value: Double) { @@ -660,7 +660,7 @@ public struct CSSPxLength: CSSAbsoluteLength { } /// Points -public struct CSSPtLength: CSSAbsoluteLength { +public struct CSSPtLength: CSSAbsoluteLength, Sendable { public let value: Double public init(_ value: Double) { @@ -673,7 +673,7 @@ public struct CSSPtLength: CSSAbsoluteLength { } /// Picas -public struct CSSPcLength: CSSAbsoluteLength { +public struct CSSPcLength: CSSAbsoluteLength, Sendable { public let value: Double public init(_ value: Double) { @@ -688,7 +688,7 @@ public struct CSSPcLength: CSSAbsoluteLength { public protocol CSSRelativeLength: CSSLength {} /// Relative to the font-size of the element. -public struct CSSEmLength: CSSRelativeLength { +public struct CSSEmLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -701,7 +701,7 @@ public struct CSSEmLength: CSSRelativeLength { } /// Relative to the width of the "0" (zero). -public struct CSSChLength: CSSRelativeLength { +public struct CSSChLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -714,7 +714,7 @@ public struct CSSChLength: CSSRelativeLength { } /// Relative to font-size of the root element. -public struct CSSRemLength: CSSRelativeLength { +public struct CSSRemLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -727,7 +727,7 @@ public struct CSSRemLength: CSSRelativeLength { } /// Relative to 1% of the width of the viewport. -public struct CSSVwLength: CSSRelativeLength { +public struct CSSVwLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -740,7 +740,7 @@ public struct CSSVwLength: CSSRelativeLength { } /// Relative to 1% of the height of the viewport. -public struct CSSVhLength: CSSRelativeLength { +public struct CSSVhLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -753,7 +753,7 @@ public struct CSSVhLength: CSSRelativeLength { } /// Relative to 1% of viewport's smaller dimension. -public struct CSSVMinLength: CSSRelativeLength { +public struct CSSVMinLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -766,7 +766,7 @@ public struct CSSVMinLength: CSSRelativeLength { } /// Relative to 1% of viewport's larger dimension. -public struct CSSVMaxLength: CSSRelativeLength { +public struct CSSVMaxLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -779,7 +779,7 @@ public struct CSSVMaxLength: CSSRelativeLength { } /// Relative to the parent element. -public struct CSSPercentLength: CSSRelativeLength { +public struct CSSPercentLength: CSSRelativeLength, Sendable { public let value: Double public init(_ value: Double) { @@ -791,7 +791,7 @@ public struct CSSPercentLength: CSSRelativeLength { } } -public enum CSSTextAlign: String, CSSConvertible { +public enum CSSTextAlign: String, CSSConvertible, Sendable { case start case left case right @@ -803,7 +803,7 @@ public enum CSSTextAlign: String, CSSConvertible { } /// Line height supports unitless numbers. -public enum CSSLineHeight: CSSConvertible { +public enum CSSLineHeight: CSSConvertible, Sendable { case length(CSSLength) case unitless(Double) @@ -817,7 +817,7 @@ public enum CSSLineHeight: CSSConvertible { } } -public enum CSSHyphens: String, CSSConvertible { +public enum CSSHyphens: String, CSSConvertible, Sendable { case none case auto @@ -826,7 +826,7 @@ public enum CSSHyphens: String, CSSConvertible { } } -public enum CSSLigatures: String, CSSConvertible { +public enum CSSLigatures: String, CSSConvertible, Sendable { case none case common = "common-ligatures" @@ -835,7 +835,7 @@ public enum CSSLigatures: String, CSSConvertible { } } -public enum CSSBoxSizing: String, CSSConvertible { +public enum CSSBoxSizing: String, CSSConvertible, Sendable { case contentBox = "content-box" case borderBox = "border-box" diff --git a/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift b/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift index 186e7985d2..74eead1ccf 100644 --- a/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift +++ b/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift @@ -57,7 +57,7 @@ public extension HTMLFontFamilyDeclaration { } /// A font family declaration. -public struct CSSFontFamilyDeclaration: HTMLFontFamilyDeclaration { +public struct CSSFontFamilyDeclaration: HTMLFontFamilyDeclaration, Sendable { public let fontFamily: FontFamily public let alternates: [FontFamily] @@ -89,7 +89,7 @@ public struct CSSFontFamilyDeclaration: HTMLFontFamilyDeclaration { } /// Represents a single `@font-face` CSS rule. -public struct CSSFontFace { +public struct CSSFontFace: Sendable { /// Represents an individual font file. /// /// `preload` indicates whether this source will be declared for preloading @@ -161,13 +161,13 @@ public struct CSSFontFace { } /// Styles that a font can be styled with. -public enum CSSFontStyle: String, Codable { +public enum CSSFontStyle: String, Codable, Sendable { case normal case italic } /// Weight (or boldness) of a font. -public enum CSSFontWeight: Codable { +public enum CSSFontWeight: Codable, Sendable { case standard(CSSStandardFontWeight) case variable(ClosedRange) } @@ -175,7 +175,7 @@ public enum CSSFontWeight: Codable { /// Standard weights (or boldness) of a font. /// /// See https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping -public enum CSSStandardFontWeight: Int, Codable { +public enum CSSStandardFontWeight: Int, Codable, Sendable { case thin = 100 case extraLight = 200 case light = 300 diff --git a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift index 30a35eeef6..0475b6e66c 100644 --- a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift +++ b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift @@ -32,7 +32,7 @@ open class EPUBNavigatorViewController: InputObservableViewController, VisualNavigator, SelectableNavigator, DecorableNavigator, Configurable, Loggable { - public enum EPUBError: Error { + public enum EPUBError: Error, Sendable { /// The provided publication is restricted. Check that any DRM was /// properly unlocked using a Content Protection. case publicationRestricted @@ -148,7 +148,7 @@ open class EPUBNavigatorViewController: InputObservableViewController, } /// Information about the visible portion of the publication. - public struct Viewport: Equatable { + public struct Viewport: Equatable, Sendable { /// Visible reading order resources. public var readingOrder: [AnyURL] diff --git a/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift b/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift index ef1f945b96..96133d1f92 100644 --- a/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift +++ b/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift @@ -11,7 +11,7 @@ import UIKit /// An `HTMLDecorationTemplate` renders a `Decoration` into a set of HTML elements and associated stylesheet. public struct HTMLDecorationTemplate: JSONObjectEncodable { /// Determines the number of created HTML elements and their position relative to the matching DOM range. - public enum Layout: String { + public enum Layout: String, Sendable { /// A single HTML element covering the smallest region containing all CSS border boxes. case bounds /// One HTML element for each CSS border box (e.g. line of text). @@ -19,7 +19,7 @@ public struct HTMLDecorationTemplate: JSONObjectEncodable { } /// Indicates how the width of each created HTML element expands in the viewport. - public enum Width: String { + public enum Width: String, Sendable { /// Smallest width fitting the CSS border box. case wrap /// Fills the bounds layout. diff --git a/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift b/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift index a0e643e0ac..248620fd53 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// Preferences for the `EPUBNavigatorViewController`. -public struct EPUBPreferences: ConfigurablePreferences { +public struct EPUBPreferences: ConfigurablePreferences, Sendable { public static let empty: EPUBPreferences = .init() /// Default page background color. diff --git a/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift b/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift index 637040cf03..53210b232e 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift @@ -10,7 +10,7 @@ import ReadiumShared /// Setting values of the `EPUBNavigatorViewController`. /// /// See `EPUBPreferences` -public struct EPUBSettings: ConfigurableSettings { +public struct EPUBSettings: ConfigurableSettings, Sendable { public var backgroundColor: Color? public var columnCount: ColumnCount public var fit: Fit @@ -205,7 +205,7 @@ public struct EPUBSettings: ConfigurableSettings { /// takes precedence. /// /// See `EPUBPreferences`. -public struct EPUBDefaults { +public struct EPUBDefaults: Sendable { public var columnCount: ColumnCount? public var fit: Fit? public var fontSize: Double? diff --git a/Sources/Navigator/EditingAction.swift b/Sources/Navigator/EditingAction.swift index 4a001d6c9b..2adbc1e811 100644 --- a/Sources/Navigator/EditingAction.swift +++ b/Sources/Navigator/EditingAction.swift @@ -16,7 +16,7 @@ import UIKit /// Then, implement the selector in one of your classes in the responder chain. /// Typically, in the `UIViewController` wrapping the navigator view /// controller. -public struct EditingAction: Hashable { +public struct EditingAction: Hashable, Sendable { /// Default editing actions enabled in the navigator. public static var defaultActions: [EditingAction] { [copy, share, lookup, translate] diff --git a/Sources/Navigator/Input/Key/Key.swift b/Sources/Navigator/Input/Key/Key.swift index 8c5f5de167..8fe9e82d23 100644 --- a/Sources/Navigator/Input/Key/Key.swift +++ b/Sources/Navigator/Input/Key/Key.swift @@ -7,7 +7,7 @@ import Foundation import UIKit -public enum Key: Equatable, CustomStringConvertible { +public enum Key: Equatable, CustomStringConvertible, Sendable { /// Printable character. case character(String) diff --git a/Sources/Navigator/Input/Key/KeyEvent.swift b/Sources/Navigator/Input/Key/KeyEvent.swift index e170a27163..c962f941de 100644 --- a/Sources/Navigator/Input/Key/KeyEvent.swift +++ b/Sources/Navigator/Input/Key/KeyEvent.swift @@ -8,7 +8,7 @@ import Foundation import UIKit /// Represents a keyboard event emitted by a Navigator. -public struct KeyEvent: Equatable, CustomStringConvertible { +public struct KeyEvent: Equatable, CustomStringConvertible, Sendable { /// Phase of this event, e.g. pressed or released. public var phase: Phase @@ -29,7 +29,7 @@ public struct KeyEvent: Equatable, CustomStringConvertible { } /// Phase of a key event, e.g. pressed or released. - public enum Phase: Equatable, CustomStringConvertible { + public enum Phase: Equatable, CustomStringConvertible, Sendable { case down case change case up diff --git a/Sources/Navigator/Input/Key/KeyModifiers.swift b/Sources/Navigator/Input/Key/KeyModifiers.swift index 98542fce85..9aaa8b375a 100644 --- a/Sources/Navigator/Input/Key/KeyModifiers.swift +++ b/Sources/Navigator/Input/Key/KeyModifiers.swift @@ -7,7 +7,7 @@ import UIKit /// Represents a set of modifier keys held together. -public struct KeyModifiers: OptionSet, Equatable, CustomStringConvertible { +public struct KeyModifiers: OptionSet, Equatable, CustomStringConvertible, Sendable { public static let command = KeyModifiers(rawValue: 1 << 0) public static let control = KeyModifiers(rawValue: 1 << 1) public static let option = KeyModifiers(rawValue: 1 << 2) diff --git a/Sources/Navigator/Input/Pointer/PointerEvent.swift b/Sources/Navigator/Input/Pointer/PointerEvent.swift index f2a5973ef5..ed21dbcafd 100644 --- a/Sources/Navigator/Input/Pointer/PointerEvent.swift +++ b/Sources/Navigator/Input/Pointer/PointerEvent.swift @@ -21,7 +21,7 @@ public struct PointerEvent: Equatable { public var modifiers: KeyModifiers /// Phase of a pointer event. - public enum Phase: Equatable, CustomStringConvertible { + public enum Phase: Equatable, CustomStringConvertible, Sendable { /// Fired when a pointer becomes active. case down @@ -79,7 +79,7 @@ public enum Pointer: Equatable, CustomStringConvertible { } /// Type of a pointer. -public enum PointerType: Equatable, CaseIterable { +public enum PointerType: Equatable, CaseIterable, Sendable { case touch case mouse } @@ -111,7 +111,7 @@ public struct MousePointer: Identifiable, Equatable { /// Represents a set of mouse buttons. /// /// The values are derived from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#value -public struct MouseButtons: OptionSet, Equatable, CustomStringConvertible { +public struct MouseButtons: OptionSet, Equatable, CustomStringConvertible, Sendable { /// Main button, usually the left button. public static let main = MouseButtons(rawValue: 1 << 0) diff --git a/Sources/Navigator/Navigator.swift b/Sources/Navigator/Navigator.swift index 004154c87c..22ab39dca1 100644 --- a/Sources/Navigator/Navigator.swift +++ b/Sources/Navigator/Navigator.swift @@ -49,7 +49,7 @@ public protocol Navigator: AnyObject { func goBackward(options: NavigatorGoOptions) async -> Bool } -public struct NavigatorGoOptions: Hashable { +public struct NavigatorGoOptions: Hashable, Sendable { /// Indicates whether the move should be animated when possible. public var animated: Bool = false @@ -142,7 +142,7 @@ public extension NavigatorDelegate { func navigator(_ navigator: Navigator, didFailToLoadResourceAt href: RelativeURL, withError error: ReadError) {} } -public enum NavigatorError: Error { +public enum NavigatorError: Error, Sendable { /// The user tried to copy the text selection but the DRM License doesn't allow it. case copyForbidden } diff --git a/Sources/Navigator/PDF/PDFNavigatorViewController.swift b/Sources/Navigator/PDF/PDFNavigatorViewController.swift index 3ec4ff4204..cbd871fb27 100644 --- a/Sources/Navigator/PDF/PDFNavigatorViewController.swift +++ b/Sources/Navigator/PDF/PDFNavigatorViewController.swift @@ -25,7 +25,7 @@ open class PDFNavigatorViewController: InputObservableViewController, VisualNavigator, SelectableNavigator, Configurable, Loggable { - public struct Configuration { + public struct Configuration: Sendable { /// Initial set of setting preferences. public var preferences: PDFPreferences diff --git a/Sources/Navigator/PDF/Preferences/PDFPreferences.swift b/Sources/Navigator/PDF/Preferences/PDFPreferences.swift index 723565f582..fff37b957a 100644 --- a/Sources/Navigator/PDF/Preferences/PDFPreferences.swift +++ b/Sources/Navigator/PDF/Preferences/PDFPreferences.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// Preferences for the `PDFNavigatorViewController`. -public struct PDFPreferences: ConfigurablePreferences { +public struct PDFPreferences: ConfigurablePreferences, Sendable { public static let empty: PDFPreferences = .init() /// Background color behind the document pages. diff --git a/Sources/Navigator/PDF/Preferences/PDFSettings.swift b/Sources/Navigator/PDF/Preferences/PDFSettings.swift index 936daee8ab..1e328380c9 100644 --- a/Sources/Navigator/PDF/Preferences/PDFSettings.swift +++ b/Sources/Navigator/PDF/Preferences/PDFSettings.swift @@ -11,7 +11,7 @@ import ReadiumShared /// Setting values of the `PDFNavigatorViewController`. /// /// See `PDFPreferences` -public struct PDFSettings: ConfigurableSettings { +public struct PDFSettings: ConfigurableSettings, Sendable { public let backgroundColor: Color? public let fit: Fit public let offsetFirstPage: Bool @@ -67,7 +67,7 @@ public struct PDFSettings: ConfigurableSettings { /// takes precedence. /// /// See `PDFPreferences`. -public struct PDFDefaults { +public struct PDFDefaults: Sendable { public var backgroundColor: Color? public var fit: Fit? public var offsetFirstPage: Bool? diff --git a/Sources/Navigator/Preferences/Configurable.swift b/Sources/Navigator/Preferences/Configurable.swift index 7eee624d9c..afc6cd377c 100644 --- a/Sources/Navigator/Preferences/Configurable.swift +++ b/Sources/Navigator/Preferences/Configurable.swift @@ -50,7 +50,7 @@ public extension Configurable { } /// A type-erasing `Configurable` object. -public class AnyConfigurable< +public final class AnyConfigurable< Settings: ConfigurableSettings, Preferences: ConfigurablePreferences, Editor: PreferencesEditor diff --git a/Sources/Navigator/Preferences/MappedPreference.swift b/Sources/Navigator/Preferences/MappedPreference.swift index c66514db3a..1046b6148c 100644 --- a/Sources/Navigator/Preferences/MappedPreference.swift +++ b/Sources/Navigator/Preferences/MappedPreference.swift @@ -166,7 +166,7 @@ public class MappedPreference: Preference { } } -public class PreferenceWithSupportedValues: MappedPreference, EnumPreference { +public final class PreferenceWithSupportedValues: MappedPreference, EnumPreference { public let supportedValues: [Value] init(original: AnyPreference, supportedValues: [Value]) { @@ -175,7 +175,7 @@ public class PreferenceWithSupportedValues: MappedPreference: +public final class MappedEnumPreference: MappedPreference, EnumPreference { let originalEnum: AnyEnumPreference @@ -203,7 +203,7 @@ public class MappedEnumPreference: } } -public class MappedRangePreference: +public final class MappedRangePreference: MappedPreference, RangePreference { let originalRange: AnyRangePreference diff --git a/Sources/Navigator/Preferences/Preference.swift b/Sources/Navigator/Preferences/Preference.swift index 85f11f3637..bb8ab926d2 100644 --- a/Sources/Navigator/Preferences/Preference.swift +++ b/Sources/Navigator/Preferences/Preference.swift @@ -119,7 +119,7 @@ public extension EnumPreference { } /// A type-erasing `EnumPreference` object. -public class AnyEnumPreference: AnyPreference, EnumPreference { +public final class AnyEnumPreference: AnyPreference, EnumPreference { public var supportedValues: [Value] { _supportedValues() } @@ -140,7 +140,7 @@ public extension RangePreference { } /// A type-erasing `Preference` object. -public class AnyRangePreference: AnyPreference, RangePreference { +public final class AnyRangePreference: AnyPreference, RangePreference { public var supportedRange: ClosedRange { _supportedRange() } diff --git a/Sources/Navigator/Preferences/ProgressionStrategy.swift b/Sources/Navigator/Preferences/ProgressionStrategy.swift index 81b7f860c8..3eee3e6132 100644 --- a/Sources/Navigator/Preferences/ProgressionStrategy.swift +++ b/Sources/Navigator/Preferences/ProgressionStrategy.swift @@ -7,7 +7,7 @@ import Foundation /// A strategy to increment or decrement a setting. -public protocol ProgressionStrategy { +public protocol ProgressionStrategy: Sendable { associatedtype Value func increment(_ value: Value) -> Value @@ -16,7 +16,7 @@ public protocol ProgressionStrategy { /// Progression strategy based on a list of preferred values for the setting. /// Steps MUST be sorted in increasing order. -public class StepsProgressionStrategy: ProgressionStrategy { +public final class StepsProgressionStrategy: ProgressionStrategy, Sendable { private let steps: [Value] public init(steps: [Value]) { @@ -37,7 +37,7 @@ public class StepsProgressionStrategy: ProgressionStrategy { } /// Simple progression strategy which increments or decrements the setting by a fixed number. -public class IncrementProgressionStrategy: ProgressionStrategy { +public final class IncrementProgressionStrategy: ProgressionStrategy, Sendable { private let increment: Value public init(increment: Value) { @@ -53,13 +53,13 @@ public class IncrementProgressionStrategy: ProgressionStrategy { } } -public class AnyProgressionStrategy: ProgressionStrategy { - private let _increment: (Value) -> Value - private let _decrement: (Value) -> Value +public final class AnyProgressionStrategy: ProgressionStrategy, Sendable { + private let _increment: @Sendable (Value) -> Value + private let _decrement: @Sendable (Value) -> Value public init(_ strategy: S) where S.Value == Value { - _increment = strategy.increment - _decrement = strategy.decrement + _increment = { strategy.increment($0) } + _decrement = { strategy.decrement($0) } } public func increment(_ value: Value) -> Value { @@ -71,19 +71,19 @@ public class AnyProgressionStrategy: ProgressionStrategy { } } -public extension ProgressionStrategy { +public extension ProgressionStrategy where Value: Sendable { func eraseToAnyProgressionStrategy() -> AnyProgressionStrategy { AnyProgressionStrategy(self) } } -public extension AnyProgressionStrategy where Value: Numeric { +public extension AnyProgressionStrategy where Value: Numeric & Sendable { static func increment(_ increment: Value) -> AnyProgressionStrategy { IncrementProgressionStrategy(increment: increment).eraseToAnyProgressionStrategy() } } -public extension AnyProgressionStrategy where Value: Comparable { +public extension AnyProgressionStrategy where Value: Comparable & Sendable { static func steps(_ steps: Value...) -> AnyProgressionStrategy { StepsProgressionStrategy(steps: steps).eraseToAnyProgressionStrategy() } diff --git a/Sources/Navigator/Preferences/ProxyPreference.swift b/Sources/Navigator/Preferences/ProxyPreference.swift index 999457a186..cba59436f7 100644 --- a/Sources/Navigator/Preferences/ProxyPreference.swift +++ b/Sources/Navigator/Preferences/ProxyPreference.swift @@ -41,7 +41,7 @@ public class ProxyPreference: Preference { } } -public class ProxyEnumPreference: ProxyPreference, EnumPreference { +public final class ProxyEnumPreference: ProxyPreference, EnumPreference { public let supportedValues: [Value] init( @@ -61,7 +61,7 @@ public class ProxyEnumPreference: ProxyPreference, EnumP } } -public class ProxyRangePreference: ProxyPreference, RangePreference { +public final class ProxyRangePreference: ProxyPreference, RangePreference { public var supportedRange: ClosedRange private let progressionStrategy: AnyProgressionStrategy private let valueFormatter: (Value) -> String diff --git a/Sources/Navigator/Preferences/Types.swift b/Sources/Navigator/Preferences/Types.swift index 7672ad620a..a91d145cdf 100644 --- a/Sources/Navigator/Preferences/Types.swift +++ b/Sources/Navigator/Preferences/Types.swift @@ -9,13 +9,13 @@ import ReadiumShared import UIKit /// Layout axis. -public enum Axis: String, Codable, Hashable { +public enum Axis: String, Codable, Hashable, Sendable { case horizontal case vertical } /// Synthetic spread policy. -public enum Spread: String, Codable, Hashable { +public enum Spread: String, Codable, Hashable, Sendable { /// The publication should be displayed in a spread if the screen is large /// enough. case auto @@ -26,7 +26,7 @@ public enum Spread: String, Codable, Hashable { } /// Direction of the reading progression across resources. -public enum ReadingProgression: String, Codable, Hashable { +public enum ReadingProgression: String, Codable, Hashable, Sendable { case ltr case rtl @@ -49,7 +49,7 @@ extension ReadiumShared.ReadingProgression { } /// Method for fitting the content within the viewport. -public enum Fit: String, Codable, Hashable { +public enum Fit: String, Codable, Hashable, Sendable { /// Use the best fitting strategy depending on the current settings and /// content. case auto @@ -60,7 +60,7 @@ public enum Fit: String, Codable, Hashable { } /// Reader theme for reflowable documents. -public enum Theme: String, Codable, Hashable { +public enum Theme: String, Codable, Hashable, Sendable { case light case dark case sepia @@ -93,20 +93,20 @@ public enum Theme: String, Codable, Hashable { } /// Number of columns displayed in a reflowable document. -public enum ColumnCount: String, Codable, Hashable { +public enum ColumnCount: String, Codable, Hashable, Sendable { case auto case one = "1" case two = "2" } /// Filter used to render images in a reflowable document. -public enum ImageFilter: String, Codable, Hashable { +public enum ImageFilter: String, Codable, Hashable, Sendable { case darken case invert } /// Text alignment in a reflowable document. -public enum TextAlignment: String, Codable, Hashable { +public enum TextAlignment: String, Codable, Hashable, Sendable { /// Align the text in the center of the page. case center /// Stretch lines of text that end with a soft line break to fill the width @@ -123,7 +123,7 @@ public enum TextAlignment: String, Codable, Hashable { } /// Represents a color stored as a packed int. -public struct Color: RawRepresentable, Codable, Hashable { +public struct Color: RawRepresentable, Codable, Hashable, Sendable { /// Packed int representation. public var rawValue: Int @@ -190,7 +190,7 @@ public struct Color: RawRepresentable, Codable, Hashable { /// /// For a list of vetted font families, see /// https://readium.org/readium-css/docs/CSS10-libre_fonts. -public struct FontFamily: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable { +public struct FontFamily: RawRepresentable, ExpressibleByStringLiteral, Codable, Hashable, Sendable { // Generic font families // See https://www.w3.org/TR/css-fonts-4/#generic-font-families diff --git a/Sources/Navigator/SelectableNavigator.swift b/Sources/Navigator/SelectableNavigator.swift index c53f18396c..e3ee7129c4 100644 --- a/Sources/Navigator/SelectableNavigator.swift +++ b/Sources/Navigator/SelectableNavigator.swift @@ -20,7 +20,7 @@ public protocol SelectableNavigator: Navigator { /// Represents a user content selection in a navigator. /// /// In the case of a text selection, you can get its content using `locator.text.highlight`. -public struct Selection { +public struct Selection: Sendable { /// Location of the user selection in the `Publication`. public let locator: Locator diff --git a/Sources/Navigator/TTS/AVTTSEngine.swift b/Sources/Navigator/TTS/AVTTSEngine.swift index 22cdb90638..ce9e34f0a1 100644 --- a/Sources/Navigator/TTS/AVTTSEngine.swift +++ b/Sources/Navigator/TTS/AVTTSEngine.swift @@ -8,14 +8,14 @@ import AVFoundation import Foundation import ReadiumShared -public protocol AVTTSEngineDelegate: AnyObject { +public protocol AVTTSEngineDelegate: AnyObject, Sendable { /// Called when the engine created a new utterance to be played. /// You can customize additional properties of the utterance. func avTTSEngine(_ engine: AVTTSEngine, didCreateUtterance utterance: AVSpeechUtterance) } /// Implementation of a `TTSEngine` using Apple AVFoundation's `AVSpeechSynthesizer`. -public class AVTTSEngine: NSObject, TTSEngine, AVSpeechSynthesizerDelegate, Loggable { +public final class AVTTSEngine: NSObject, TTSEngine, AVSpeechSynthesizerDelegate, Loggable { /// Range of valid values for an AVUtterance rate. /// /// > The speech rate is a decimal representation within the range of `AVSpeechUtteranceMinimumSpeechRate` and diff --git a/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift b/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift index 51c20f59b2..7d0950ca33 100644 --- a/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift +++ b/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift @@ -20,7 +20,7 @@ public protocol PublicationSpeechSynthesizerDelegate: AnyObject { /// `PublicationSpeechSynthesizer` orchestrates the rendition of a `Publication` by iterating through its content, /// splitting it into individual utterances using a `ContentTokenizer`, then using a `TTSEngine` to read them aloud. -public class PublicationSpeechSynthesizer: Loggable { +public final class PublicationSpeechSynthesizer: Loggable { public typealias EngineFactory = () -> TTSEngine public typealias TokenizerFactory = (_ defaultLanguage: Language?) -> ContentTokenizer @@ -29,13 +29,13 @@ public class PublicationSpeechSynthesizer: Loggable { publication.content() != nil } - public enum Error: Swift.Error { + public enum Error: Swift.Error, Sendable { /// Underlying `TTSEngine` error. case engine(TTSError) } /// User configuration for the text-to-speech engine. - public struct Configuration: Equatable { + public struct Configuration: Equatable, Sendable { /// Language overriding the publication one. public var defaultLanguage: Language? @@ -53,7 +53,7 @@ public class PublicationSpeechSynthesizer: Loggable { /// An utterance is an arbitrary text (e.g. sentence) extracted from the publication, that can be synthesized by /// the TTS engine. - public struct Utterance: Equatable { + public struct Utterance: Equatable, Sendable { /// Text to be spoken. public let text: String /// Locator to the utterance in the publication. @@ -63,7 +63,7 @@ public class PublicationSpeechSynthesizer: Loggable { } /// Represents a state of the `PublicationSpeechSynthesizer`. - public enum State: Equatable { + public enum State: Equatable, Sendable { /// The synthesizer is completely stopped and must be (re)started from a given locator. case stopped diff --git a/Sources/Navigator/TTS/TTSEngine.swift b/Sources/Navigator/TTS/TTSEngine.swift index 66ea8e5eab..857476a19e 100644 --- a/Sources/Navigator/TTS/TTSEngine.swift +++ b/Sources/Navigator/TTS/TTSEngine.swift @@ -33,16 +33,16 @@ public extension TTSEngine { } } -public enum TTSError: Error { +public enum TTSError: Error, Sendable { /// Tried to synthesize an utterance with an unsupported language. - case languageNotSupported(language: Language, cause: Error?) + case languageNotSupported(language: Language, cause: (any Error)?) /// Other engine-specific errors. - case other(Error) + case other(any Error) } /// An utterance is an arbitrary text (e.g. sentence) that can be synthesized by the TTS engine. -public struct TTSUtterance { +public struct TTSUtterance: Sendable { /// Text to be spoken. public let text: String diff --git a/Sources/Navigator/TTS/TTSVoice.swift b/Sources/Navigator/TTS/TTSVoice.swift index d482ef0f4e..a2f36a7034 100644 --- a/Sources/Navigator/TTS/TTSVoice.swift +++ b/Sources/Navigator/TTS/TTSVoice.swift @@ -9,12 +9,12 @@ import Foundation import ReadiumShared /// Represents a voice provided by the TTS engine which can speak an utterance. -public struct TTSVoice: Hashable { - public enum Gender: Hashable { +public struct TTSVoice: Hashable, Sendable { + public enum Gender: Hashable, Sendable { case female, male, unspecified } - public enum Quality: Hashable { + public enum Quality: Hashable, Sendable { case lower, low, medium, high, higher } diff --git a/Sources/Navigator/VisualNavigator.swift b/Sources/Navigator/VisualNavigator.swift index c9bf1ccca5..c66b960ad8 100644 --- a/Sources/Navigator/VisualNavigator.swift +++ b/Sources/Navigator/VisualNavigator.swift @@ -65,7 +65,7 @@ public extension VisualNavigator { } } -public struct VisualNavigatorPresentation { +public struct VisualNavigatorPresentation: Sendable { /// Horizontal direction of progression across resources. public let readingProgression: ReadingProgression diff --git a/Sources/OPDS/OPDS1Parser.swift b/Sources/OPDS/OPDS1Parser.swift index 020f54d3c0..d6fc08ce0e 100644 --- a/Sources/OPDS/OPDS1Parser.swift +++ b/Sources/OPDS/OPDS1Parser.swift @@ -94,7 +94,7 @@ public class OPDS1Parser: Loggable { guard let title = root.firstChild(tag: "title")?.stringValue else { throw OPDS1ParserError.missingTitle } - let feed = Feed(title: title) + var feed = Feed(title: title) feed.metadata.identifier = root.firstChild(tag: "id")?.stringValue @@ -145,7 +145,7 @@ public class OPDS1Parser: Loggable { if let publication = parseEntry(entry: entry, feedURL: feedURL) { // Checking if this publication need to go into a group or in publications. if let collectionLink = collectionLink { - addPublicationInGroup(feed, publication, collectionLink) + addPublicationInGroup(&feed, publication, collectionLink) } else { feed.publications.append(publication) } @@ -170,7 +170,7 @@ public class OPDS1Parser: Loggable { // Check collection link if let collectionLink = collectionLink { - addNavigationInGroup(feed, newLink, collectionLink) + addNavigationInGroup(&feed, newLink, collectionLink) } else { feed.navigation.append(newLink) } @@ -210,7 +210,7 @@ public class OPDS1Parser: Loggable { if isFacet { if let facetGroupName = link.attributes["facetGroup"] { - addFacet(feed: feed, to: newLink, named: facetGroupName) + addFacet(feed: &feed, to: newLink, named: facetGroupName) } } else { feed.links.append(newLink) @@ -405,33 +405,31 @@ public class OPDS1Parser: Loggable { ) } - static func addFacet(feed: Feed, to link: Link, named title: String) { - for facet in feed.facets { - if facet.metadata.title == title { - facet.links.append(link) - return - } + static func addFacet(feed: inout Feed, to link: Link, named title: String) { + if let index = feed.facets.firstIndex(where: { $0.metadata.title == title }) { + feed.facets[index].links.append(link) + return } - let newFacet = Facet(title: title) + var newFacet = Facet(title: title) newFacet.links.append(link) feed.facets.append(newFacet) } - static func addPublicationInGroup(_ feed: Feed, + static func addPublicationInGroup(_ feed: inout Feed, _ publication: Publication, _ collectionLink: Link) { - for group in feed.groups { + for (i, group) in feed.groups.enumerated() { for l in group.links { if l.href == collectionLink.href { - group.publications.append(publication) + feed.groups[i].publications.append(publication) return } } } if let title = collectionLink.title { - let newGroup = Group(title: title) + var newGroup = Group(title: title) let selfLink = Link( href: collectionLink.href, title: collectionLink.title, @@ -443,20 +441,20 @@ public class OPDS1Parser: Loggable { } } - static func addNavigationInGroup(_ feed: Feed, + static func addNavigationInGroup(_ feed: inout Feed, _ link: Link, _ collectionLink: Link) { - for group in feed.groups { + for (i, group) in feed.groups.enumerated() { for l in group.links { if l.href == collectionLink.href { - group.navigation.append(link) + feed.groups[i].navigation.append(link) return } } } if let title = collectionLink.title { - let newGroup = Group(title: title) + var newGroup = Group(title: title) let selfLink = Link( href: collectionLink.href, title: collectionLink.title, diff --git a/Sources/OPDS/OPDS2Parser.swift b/Sources/OPDS/OPDS2Parser.swift index bd7189e43f..edd9df8670 100644 --- a/Sources/OPDS/OPDS2Parser.swift +++ b/Sources/OPDS/OPDS2Parser.swift @@ -91,8 +91,8 @@ public class OPDS2Parser: Loggable { throw OPDS2ParserError.missingTitle } - let feed = Feed(title: title) - parseMetadata(opdsMetadata: feed.metadata, metadataDict: metadataDict) + var feed = Feed(title: title) + parseMetadata(opdsMetadata: &feed.metadata, metadataDict: metadataDict) for (k, v) in jsonDict { switch k { @@ -108,27 +108,27 @@ public class OPDS2Parser: Loggable { guard let links = v.array else { throw OPDS2ParserError.invalidLink } - try parseLinks(feed: feed, feedURL: feedURL, links: links) + try parseLinks(feed: &feed, feedURL: feedURL, links: links) case "facets": guard let facets = v.array else { throw OPDS2ParserError.invalidFacet } - try parseFacets(feed: feed, feedURL: feedURL, facets: facets) + try parseFacets(feed: &feed, feedURL: feedURL, facets: facets) case "publications": guard let publications = v.array else { throw OPDS2ParserError.invalidPublication } - try parsePublications(feed: feed, feedURL: feedURL, publications: publications) + try parsePublications(feed: &feed, feedURL: feedURL, publications: publications) case "navigation": guard let navLinks = v.array else { throw OPDS2ParserError.invalidNavigation } - try parseNavigation(feed: feed, feedURL: feedURL, navLinks: navLinks) + try parseNavigation(feed: &feed, feedURL: feedURL, navLinks: navLinks) case "groups": guard let groups = v.array else { throw OPDS2ParserError.invalidGroup } - try parseGroups(feed: feed, feedURL: feedURL, groups: groups) + try parseGroups(feed: &feed, feedURL: feedURL, groups: groups) default: continue } @@ -137,7 +137,7 @@ public class OPDS2Parser: Loggable { return feed } - static func parseMetadata(opdsMetadata: OpdsMetadata, metadataDict: [String: JSONValue]) { + static func parseMetadata(opdsMetadata: inout OpdsMetadata, metadataDict: [String: JSONValue]) { for (k, v) in metadataDict { switch k { case "title": @@ -162,7 +162,7 @@ public class OPDS2Parser: Loggable { } } - static func parseFacets(feed: Feed, feedURL: URL, facets: [JSONValue]) throws { + static func parseFacets(feed: inout Feed, feedURL: URL, facets: [JSONValue]) throws { for facetValue in facets { guard let facetDict = facetValue.object else { continue } guard let metadata = facetDict["metadata"]?.object else { @@ -172,8 +172,8 @@ public class OPDS2Parser: Loggable { throw OPDS2ParserError.invalidFacet } - let facet = Facet(title: title) - parseMetadata(opdsMetadata: facet.metadata, metadataDict: metadata) + var facet = Facet(title: title) + parseMetadata(opdsMetadata: &facet.metadata, metadataDict: metadata) for (k, v) in facetDict { if k == "links" { @@ -192,7 +192,7 @@ public class OPDS2Parser: Loggable { } } - static func parseLinks(feed: Feed, feedURL: URL, links: [JSONValue]) throws { + static func parseLinks(feed: inout Feed, feedURL: URL, links: [JSONValue]) throws { for linkValue in links { if var link = try Link(json: linkValue) { try link.normalizeHREFs(to: feedURL) @@ -201,14 +201,14 @@ public class OPDS2Parser: Loggable { } } - static func parsePublications(feed: Feed, feedURL: URL, publications: [JSONValue]) throws { + static func parsePublications(feed: inout Feed, feedURL: URL, publications: [JSONValue]) throws { for pubValue in publications { let pub = try Publication(json: pubValue) feed.publications.append(pub) } } - static func parseNavigation(feed: Feed, feedURL: URL, navLinks: [JSONValue]) throws { + static func parseNavigation(feed: inout Feed, feedURL: URL, navLinks: [JSONValue]) throws { for navValue in navLinks { if var link = try Link(json: navValue) { try link.normalizeHREFs(to: feedURL) @@ -217,7 +217,7 @@ public class OPDS2Parser: Loggable { } } - static func parseGroups(feed: Feed, feedURL: URL, groups: [JSONValue]) throws { + static func parseGroups(feed: inout Feed, feedURL: URL, groups: [JSONValue]) throws { for groupValue in groups { guard let groupDict = groupValue.object else { continue } guard let metadata = groupDict["metadata"]?.object else { @@ -227,8 +227,8 @@ public class OPDS2Parser: Loggable { throw OPDS2ParserError.invalidGroup } - let group = Group(title: title) - parseMetadata(opdsMetadata: group.metadata, metadataDict: metadata) + var group = Group(title: title) + parseMetadata(opdsMetadata: &group.metadata, metadataDict: metadata) for (k, v) in groupDict { switch k { diff --git a/Sources/OPDS/OPDSParser.swift b/Sources/OPDS/OPDSParser.swift index 80031b9397..eb5142e789 100644 --- a/Sources/OPDS/OPDSParser.swift +++ b/Sources/OPDS/OPDSParser.swift @@ -7,12 +7,12 @@ import Foundation import ReadiumShared -public enum OPDSParserError: Error { +public enum OPDSParserError: Error, Sendable { case documentNotFound case documentNotValid } -public enum OPDSParser { +public enum OPDSParser: Sendable { static var feedURL: URL? /// Parse an OPDS feed or publication. diff --git a/Sources/OPDS/ParseData.swift b/Sources/OPDS/ParseData.swift index 116c57ef0d..bb8cb7223a 100644 --- a/Sources/OPDS/ParseData.swift +++ b/Sources/OPDS/ParseData.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// List of OPDS versions compliant with the parser. -public enum Version { +public enum Version: Sendable { /// OPDS 1.x must be an XML ressource case OPDS1 /// OPDS 2.x must be a JSON ressource diff --git a/Sources/Shared/Logger/Loggable.swift b/Sources/Shared/Logger/Loggable.swift index b15a9e7e19..1f8c1f16a4 100644 --- a/Sources/Shared/Logger/Loggable.swift +++ b/Sources/Shared/Logger/Loggable.swift @@ -7,7 +7,7 @@ import Foundation /// The different levels of log-severity available for logging. -public enum SeverityLevel: String { +public enum SeverityLevel: String, Sendable { case trace case debug case info diff --git a/Sources/Shared/Logger/LoggerStub.swift b/Sources/Shared/Logger/LoggerStub.swift index c4f55e5554..78798299a2 100644 --- a/Sources/Shared/Logger/LoggerStub.swift +++ b/Sources/Shared/Logger/LoggerStub.swift @@ -8,7 +8,7 @@ import Foundation /// A Logger implementation of the Loggable protocol. /// Used as default -public class LoggerStub: LoggerType { +public final class LoggerStub: LoggerType, Sendable { public init() {} /// Log `message` with a severity of `level`. diff --git a/Sources/Shared/OPDS/Facet.swift b/Sources/Shared/OPDS/Facet.swift index 8eb52e1c28..128a0e1a92 100644 --- a/Sources/Shared/OPDS/Facet.swift +++ b/Sources/Shared/OPDS/Facet.swift @@ -5,7 +5,7 @@ // /// Enables faceted navigation in OPDS. -public class Facet { +public struct Facet: Sendable { public var metadata: OpdsMetadata public var links = [Link]() diff --git a/Sources/Shared/OPDS/Feed.swift b/Sources/Shared/OPDS/Feed.swift index 1f2f0a97cb..8bde94d08a 100644 --- a/Sources/Shared/OPDS/Feed.swift +++ b/Sources/Shared/OPDS/Feed.swift @@ -5,7 +5,7 @@ // /// Main structure of an OPDS catalog. -public class Feed { +public struct Feed { public var metadata: OpdsMetadata public var links = [Link]() public var facets = [Facet]() diff --git a/Sources/Shared/OPDS/Group.swift b/Sources/Shared/OPDS/Group.swift index 992abf7bd1..dbc0ae96a6 100644 --- a/Sources/Shared/OPDS/Group.swift +++ b/Sources/Shared/OPDS/Group.swift @@ -5,7 +5,7 @@ // /// A substructure of a feed. -public class Group { +public struct Group { public var metadata: OpdsMetadata public var links = [Link]() public var publications = [Publication]() diff --git a/Sources/Shared/OPDS/OPDSAcquisition.swift b/Sources/Shared/OPDS/OPDSAcquisition.swift index c85bcaa741..41eba419f9 100644 --- a/Sources/Shared/OPDS/OPDSAcquisition.swift +++ b/Sources/Shared/OPDS/OPDSAcquisition.swift @@ -9,7 +9,7 @@ import ReadiumInternal /// OPDS Acquisition Object /// https://drafts.opds.io/schema/acquisition-object.schema.json -public struct OPDSAcquisition: Equatable, JSONObjectEncodable, JSONValueDecodable { +public struct OPDSAcquisition: Equatable, JSONObjectEncodable, JSONValueDecodable, Sendable { public var type: String public var children: [OPDSAcquisition] = [] diff --git a/Sources/Shared/OPDS/OPDSAvailability.swift b/Sources/Shared/OPDS/OPDSAvailability.swift index 9d967cbe8c..ca385bbaa4 100644 --- a/Sources/Shared/OPDS/OPDSAvailability.swift +++ b/Sources/Shared/OPDS/OPDSAvailability.swift @@ -9,7 +9,7 @@ import ReadiumInternal /// Indicated the availability of a given resource. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSAvailability: Equatable, JSONValueDecodable, JSONObjectEncodable { +public struct OPDSAvailability: Equatable, JSONValueDecodable, JSONObjectEncodable, Sendable { public let state: State /// Timestamp for the previous state change. @@ -50,7 +50,7 @@ public struct OPDSAvailability: Equatable, JSONValueDecodable, JSONObjectEncodab ]) } - public enum State: String { + public enum State: String, Sendable { case available, unavailable, reserved, ready } } diff --git a/Sources/Shared/OPDS/OPDSCopies.swift b/Sources/Shared/OPDS/OPDSCopies.swift index 08679620bc..8d192528c0 100644 --- a/Sources/Shared/OPDS/OPDSCopies.swift +++ b/Sources/Shared/OPDS/OPDSCopies.swift @@ -9,7 +9,7 @@ import ReadiumInternal /// Library-specific feature that contains information about the copies that a library has acquired. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSCopies: Equatable, JSONValueDecodable, JSONObjectEncodable { +public struct OPDSCopies: Equatable, JSONValueDecodable, JSONObjectEncodable, Sendable { public let total: Int? public let available: Int? diff --git a/Sources/Shared/OPDS/OPDSHolds.swift b/Sources/Shared/OPDS/OPDSHolds.swift index 12676477b1..6566ae4710 100644 --- a/Sources/Shared/OPDS/OPDSHolds.swift +++ b/Sources/Shared/OPDS/OPDSHolds.swift @@ -9,7 +9,7 @@ import ReadiumInternal /// Library-specific features when a specific book is unavailable but provides a hold list. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSHolds: Equatable, JSONValueDecodable, JSONObjectEncodable { +public struct OPDSHolds: Equatable, JSONValueDecodable, JSONObjectEncodable, Sendable { public let total: Int? public let position: Int? diff --git a/Sources/Shared/OPDS/OPDSPrice.swift b/Sources/Shared/OPDS/OPDSPrice.swift index 80416bb2fb..e79607c720 100644 --- a/Sources/Shared/OPDS/OPDSPrice.swift +++ b/Sources/Shared/OPDS/OPDSPrice.swift @@ -9,7 +9,7 @@ import ReadiumInternal /// The price of a publication in an OPDS link. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSPrice: Equatable, JSONValueDecodable, JSONObjectEncodable { +public struct OPDSPrice: Equatable, JSONValueDecodable, JSONObjectEncodable, Sendable { public var currency: String // eg. EUR /// Should only be used for display purposes, because of precision issues inherent with Double and the JSON parsing. diff --git a/Sources/Shared/OPDS/OpdsMetadata.swift b/Sources/Shared/OPDS/OpdsMetadata.swift index 26b9d0aa62..e5f44f5bc1 100644 --- a/Sources/Shared/OPDS/OpdsMetadata.swift +++ b/Sources/Shared/OPDS/OpdsMetadata.swift @@ -7,7 +7,7 @@ import Foundation /// OPDS metadata properties. -public class OpdsMetadata { +public struct OpdsMetadata: Sendable { public var title: String public var identifier: String? public var numberOfItem: Int? diff --git a/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift b/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift index 44708469be..2cde09253a 100644 --- a/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift +++ b/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift @@ -88,7 +88,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// access. /// /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#ways-of-reading - public struct WaysOfReading: AccessibilityDisplayField { + public struct WaysOfReading: AccessibilityDisplayField, Sendable { /// Indicates if users can modify the appearance of the text and the /// page layout according to the possibilities offered by the reading /// system. @@ -266,7 +266,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// Identifies the navigation features included in the publication. /// /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#navigation - public struct Navigation: AccessibilityDisplayField { + public struct Navigation: AccessibilityDisplayField, Sendable { /// Indicates whether no information about navigation features is /// available. public var noMetadata: Bool { @@ -348,7 +348,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// for prerecorded audio are available. /// /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#rich-content - public struct RichContent: AccessibilityDisplayField { + public struct RichContent: AccessibilityDisplayField, Sendable { /// Indicates whether no information about rich content is available. public var noMetadata: Bool { !extendedAltTextDescriptions && !mathFormula && !mathFormulaAsMathML && @@ -469,7 +469,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// better understand the accessibility characteristics of digital /// publications. These are for metadata that do not fit into the other /// categories or are rarely used in trade publishing. - public struct AdditionalInformation: AccessibilityDisplayField { + public struct AdditionalInformation: AccessibilityDisplayField, Sendable { /// No information is available. public var noMetadata: Bool { !pageBreakMarkers && !aria && !audioDescriptions && !braille && @@ -629,7 +629,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// when content is potentially dangerous to them. /// /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#hazards - public struct Hazards: AccessibilityDisplayField { + public struct Hazards: AccessibilityDisplayField, Sendable { public enum Hazard: Sendable { case yes case no @@ -780,7 +780,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// internationally recognized conformance standards for accessibility. /// /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#conformance-group - public struct Conformance: AccessibilityDisplayField { + public struct Conformance: AccessibilityDisplayField, Sendable { /// Accessibility conformance profiles. public var profiles: [Accessibility.Profile] @@ -838,7 +838,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// by legal counsel for each jurisdiction. /// /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#legal-considerations - public struct Legal: AccessibilityDisplayField { + public struct Legal: AccessibilityDisplayField, Sendable { /// No information is available. public var noMetadata: Bool { !exemption @@ -888,7 +888,7 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// duplicate, the other discoverability metadata. /// /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#accessibility-summary - public struct AccessibilitySummary: AccessibilityDisplayField { + public struct AccessibilitySummary: AccessibilityDisplayField, Sendable { public var summary: String? public let id: AccessibilityDisplayString = .accessibilitySummaryTitle diff --git a/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift b/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift index 3b9a8b9940..5091719938 100644 --- a/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift +++ b/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift @@ -7,6 +7,6 @@ import Foundation /// Hint about the nature of the layout for the linked resources. -public enum EPUBLayout: String { +public enum EPUBLayout: String, Sendable { case fixed, reflowable } diff --git a/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift b/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift index eb6e1ccec5..10ff4c75f6 100644 --- a/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift +++ b/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift @@ -9,7 +9,7 @@ import ReadiumInternal /// Indicates that a resource is encrypted/obfuscated and provides relevant information for /// decryption. -public struct Encryption: Equatable, JSONValueDecodable, JSONObjectEncodable { +public struct Encryption: Equatable, JSONValueDecodable, JSONObjectEncodable, Sendable { /// Identifies the algorithm used to encrypt the resource. public let algorithm: String // URI diff --git a/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift b/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift index c586b0e46f..14a9492d0b 100644 --- a/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift +++ b/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift @@ -22,7 +22,7 @@ import ReadiumInternal /// represents a "collapsed" range that has identical `start` and `end` boundary points. /// /// https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md#the-domrange-object -public struct DOMRange: Hashable, JSONValueDecodable, JSONObjectEncodable { +public struct DOMRange: Hashable, JSONValueDecodable, JSONObjectEncodable, Sendable { /// A serializable representation of the "start" boundary point of the DOM Range. let start: Point @@ -71,7 +71,7 @@ public struct DOMRange: Hashable, JSONValueDecodable, JSONObjectEncodable { /// node). /// /// https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md#the-start-and-end-object - public struct Point: Hashable, JSONValueDecodable, JSONObjectEncodable { + public struct Point: Hashable, JSONValueDecodable, JSONObjectEncodable, Sendable { let cssSelector: String let textNodeIndex: Int let charOffset: Int? diff --git a/Sources/Shared/Publication/Link.swift b/Sources/Shared/Publication/Link.swift index 3fbd30b971..6afb9dcae2 100644 --- a/Sources/Shared/Publication/Link.swift +++ b/Sources/Shared/Publication/Link.swift @@ -7,7 +7,7 @@ import Foundation import ReadiumInternal -public enum LinkError: Error, Equatable { +public enum LinkError: Error, Equatable, Sendable { /// The link's HREF is not a valid URL. case invalidHREF(String) } diff --git a/Sources/Shared/Publication/Protection/ContentProtection.swift b/Sources/Shared/Publication/Protection/ContentProtection.swift index 83ea59659d..53620b1b46 100644 --- a/Sources/Shared/Publication/Protection/ContentProtection.swift +++ b/Sources/Shared/Publication/Protection/ContentProtection.swift @@ -25,9 +25,9 @@ public protocol ContentProtection { ) async -> Result } -public enum ContentProtectionOpenError: Error { +public enum ContentProtectionOpenError: Error, Sendable { /// The asset is not supported by this ``ContentProtection`` - case assetNotSupported(Error?) + case assetNotSupported((any Error)?) /// An error occurred while reading the asset. case reading(ReadError) @@ -49,7 +49,7 @@ public struct ContentProtectionScheme: RawRepresentable, Equatable, Sendable { public static let adept = ContentProtectionScheme(rawValue: HTTPURL(string: "http://ns.adobe.com/adept")!) } -public struct ContentProtectionSchemeNotSupportedError: Error { +public struct ContentProtectionSchemeNotSupportedError: Error, Sendable { public let scheme: ContentProtectionScheme public init(scheme: ContentProtectionScheme) { diff --git a/Sources/Shared/Publication/Protection/FallbackContentProtection.swift b/Sources/Shared/Publication/Protection/FallbackContentProtection.swift index 1909fe6b79..169f9b4dce 100644 --- a/Sources/Shared/Publication/Protection/FallbackContentProtection.swift +++ b/Sources/Shared/Publication/Protection/FallbackContentProtection.swift @@ -8,7 +8,7 @@ import Foundation /// ``ContentProtection`` implementation used as a fallback when detecting /// known DRMs not supported by the app. -public final class _FallbackContentProtection: ContentProtection { +public final class _FallbackContentProtection: ContentProtection, Sendable { public init() {} public func open( diff --git a/Sources/Shared/Publication/Publication.swift b/Sources/Shared/Publication/Publication.swift index febb79d8c9..17c8fbffd5 100644 --- a/Sources/Shared/Publication/Publication.swift +++ b/Sources/Shared/Publication/Publication.swift @@ -9,7 +9,7 @@ import Foundation import ReadiumInternal /// Shared model for a Readium Publication. -public class Publication: Closeable, Loggable { +public final class Publication: Closeable, Loggable { public var manifest: Manifest private let container: Container private let services: [PublicationService] diff --git a/Sources/Shared/Publication/Services/Content Protection/UserRights.swift b/Sources/Shared/Publication/Services/Content Protection/UserRights.swift index 1a247a0536..a3996afd83 100644 --- a/Sources/Shared/Publication/Services/Content Protection/UserRights.swift +++ b/Sources/Shared/Publication/Services/Content Protection/UserRights.swift @@ -35,7 +35,7 @@ public protocol UserRights { } /// A `UserRights` without any restriction. -public class UnrestrictedUserRights: UserRights { +public final class UnrestrictedUserRights: UserRights, Sendable { public init() {} public func canCopy(text: String) async -> Bool { @@ -56,7 +56,7 @@ public class UnrestrictedUserRights: UserRights { } /// A `UserRights` which forbids all rights. -public class AllRestrictedUserRights: UserRights { +public final class AllRestrictedUserRights: UserRights, Sendable { public init() {} public func canCopy(text: String) async -> Bool { diff --git a/Sources/Shared/Publication/Services/Content/Content.swift b/Sources/Shared/Publication/Services/Content/Content.swift index ff1e497991..a5c04e47ee 100644 --- a/Sources/Shared/Publication/Services/Content/Content.swift +++ b/Sources/Shared/Publication/Services/Content/Content.swift @@ -170,7 +170,7 @@ public struct TextContentElement: Hashable, TextualContentElement { } /// Represents a purpose of an element in the broader context of the document. - public enum Role: Hashable { + public enum Role: Hashable, Sendable { /// Title of a section with its level (1 being the highest). case heading(level: Int) @@ -205,7 +205,7 @@ public struct TextContentElement: Hashable, TextualContentElement { /// An attribute key identifies uniquely a type of attribute. /// /// The `V` phantom type is there to perform static type checking when requesting an attribute. -public struct ContentAttributeKey: Hashable { +public struct ContentAttributeKey: Hashable, Sendable { public static var accessibilityLabel: ContentAttributeKey { .init("accessibilityLabel") } @@ -288,7 +288,7 @@ public protocol ContentIterator: AnyObject { } /// Helper class to treat a `Content` as a `Sequence`. -public class ContentSequence: AsyncSequence { +public final class ContentSequence: AsyncSequence { public typealias Element = ContentElement private let content: Content @@ -301,7 +301,7 @@ public class ContentSequence: AsyncSequence { Iterator(iterator: content.iterator()) } - public class Iterator: AsyncIteratorProtocol, Loggable { + public final class Iterator: AsyncIteratorProtocol, Loggable { private let iterator: ContentIterator public init(iterator: ContentIterator) { diff --git a/Sources/Shared/Publication/Services/Content/ContentService.swift b/Sources/Shared/Publication/Services/Content/ContentService.swift index eab94e7bc8..d3345e41d5 100644 --- a/Sources/Shared/Publication/Services/Content/ContentService.swift +++ b/Sources/Shared/Publication/Services/Content/ContentService.swift @@ -18,7 +18,7 @@ public protocol ContentService: PublicationService { } /// Default implementation of `ContentService`, delegating the content parsing to `ResourceContentIteratorFactory`. -public class DefaultContentService: ContentService { +public final class DefaultContentService: ContentService, Sendable { private let publication: Weak private let resourceContentIteratorFactories: [ResourceContentIteratorFactory] diff --git a/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift b/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift index 51f1dd867e..a5a0d334fd 100644 --- a/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift +++ b/Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift @@ -18,9 +18,9 @@ import SwiftSoup /// /// Locators will contain a `before` context of up to `beforeMaxLength` /// characters. -public class HTMLResourceContentIterator: ContentIterator { +public final class HTMLResourceContentIterator: ContentIterator { /// Factory for an `HTMLResourceContentIterator`. - public class Factory: ResourceContentIteratorFactory { + public final class Factory: ResourceContentIteratorFactory, Sendable { public init() {} public func make( diff --git a/Sources/Shared/Publication/Services/Content/Iterators/PublicationContentIterator.swift b/Sources/Shared/Publication/Services/Content/Iterators/PublicationContentIterator.swift index f2b3c15612..0aed23be0d 100644 --- a/Sources/Shared/Publication/Services/Content/Iterators/PublicationContentIterator.swift +++ b/Sources/Shared/Publication/Services/Content/Iterators/PublicationContentIterator.swift @@ -6,7 +6,7 @@ import Foundation -public protocol ResourceContentIteratorFactory { +public protocol ResourceContentIteratorFactory: Sendable { /// Creates a `ContentIterator` instance for the `resource`, starting from /// the given `locator`. /// @@ -21,7 +21,7 @@ public protocol ResourceContentIteratorFactory { /// A composite [Content.Iterator] which iterates through a whole [publication] and delegates the /// iteration inside a given resource to media type-specific iterators. -public class PublicationContentIterator: ContentIterator, Loggable { +public final class PublicationContentIterator: ContentIterator, Loggable { /// `ContentIterator` for a resource, associated with its index in the reading order. private typealias IndexedIterator = (index: Int, iterator: ContentIterator) diff --git a/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift b/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift index e34c7343d6..6790ea5080 100644 --- a/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift +++ b/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift @@ -7,7 +7,7 @@ import Foundation /// A ``PositionsService`` holding the pre-computed position locators in memory. -public class InMemoryPositionsService: PositionsService { +public final class InMemoryPositionsService: PositionsService, Sendable { private let _positions: [[Locator]] public init(positionsByReadingOrder: [[Locator]]) { diff --git a/Sources/Shared/Publication/Services/Search/SearchService.swift b/Sources/Shared/Publication/Services/Search/SearchService.swift index d16689af34..32a615a7f9 100644 --- a/Sources/Shared/Publication/Services/Search/SearchService.swift +++ b/Sources/Shared/Publication/Services/Search/SearchService.swift @@ -58,7 +58,7 @@ public extension SearchIterator { } /// Holds the available search options and their current values. -public struct SearchOptions: Hashable { +public struct SearchOptions: Hashable, Sendable { /// Whether the search will differentiate between capital and lower-case letters. public var caseSensitive: Bool? @@ -110,7 +110,7 @@ public struct SearchOptions: Hashable { public typealias SearchResult = Result /// Represents an error which might occur during a search activity. -public enum SearchError: Error { +public enum SearchError: Error, Sendable { /// The publication is not searchable. case publicationNotSearchable diff --git a/Sources/Shared/Publication/Services/Search/StringSearchService.swift b/Sources/Shared/Publication/Services/Search/StringSearchService.swift index 851c6d4372..dddac5c7c3 100644 --- a/Sources/Shared/Publication/Services/Search/StringSearchService.swift +++ b/Sources/Shared/Publication/Services/Search/StringSearchService.swift @@ -14,7 +14,7 @@ import Foundation /// content from markups (e.g. HTML) or binary (e.g. PDF) resources. /// /// The actual search is implemented by the provided `searchAlgorithm`. -public class StringSearchService: SearchService { +public final class StringSearchService: SearchService, Sendable { public static func makeFactory( snippetLength: Int = 200, searchAlgorithm: StringSearchAlgorithm = BasicStringSearchAlgorithm(), @@ -215,7 +215,7 @@ public class StringSearchService: SearchService { } /// Implements the actual search algorithm in sanitized text content. -public protocol StringSearchAlgorithm { +public protocol StringSearchAlgorithm: Sendable { /// Default value for the search options available with this algorithm. /// /// If an option does not have a value, it is not supported by the algorithm. @@ -231,7 +231,7 @@ public protocol StringSearchAlgorithm { } /// A basic `StringSearchAlgorithm` using the native `String.range(of:)` APIs. -public class BasicStringSearchAlgorithm: StringSearchAlgorithm { +public final class BasicStringSearchAlgorithm: StringSearchAlgorithm, Sendable { public let options: SearchOptions = .init( caseSensitive: false, diacriticSensitive: false, diff --git a/Sources/Shared/Toolkit/Archive/ArchiveOpener.swift b/Sources/Shared/Toolkit/Archive/ArchiveOpener.swift index e22b0c463f..aebab283b9 100644 --- a/Sources/Shared/Toolkit/Archive/ArchiveOpener.swift +++ b/Sources/Shared/Toolkit/Archive/ArchiveOpener.swift @@ -17,7 +17,7 @@ public protocol ArchiveOpener { func sniffOpen(resource: Resource) async -> Result } -public enum ArchiveOpenError: Error { +public enum ArchiveOpenError: Error, Sendable { /// Archive format not supported. case formatNotSupported(Format) @@ -25,7 +25,7 @@ public enum ArchiveOpenError: Error { case reading(ReadError) } -public enum ArchiveSniffOpenError: Error { +public enum ArchiveSniffOpenError: Error, Sendable { /// The format of the resource could not be inferred. case formatNotRecognized diff --git a/Sources/Shared/Toolkit/Archive/ArchiveProperties.swift b/Sources/Shared/Toolkit/Archive/ArchiveProperties.swift index c9460c5eb8..30558079c2 100644 --- a/Sources/Shared/Toolkit/Archive/ArchiveProperties.swift +++ b/Sources/Shared/Toolkit/Archive/ArchiveProperties.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumInternal /// Holds information about how the resource is stored in the archive. -public struct ArchiveProperties: Equatable, JSONValueDecodable, JSONObjectEncodable { +public struct ArchiveProperties: Equatable, JSONValueDecodable, JSONObjectEncodable, Sendable { /// The length of the entry stored in the archive. It might be a compressed /// length if the entry is deflated. public let entryLength: UInt64 diff --git a/Sources/Shared/Toolkit/Archive/DefaultArchiveOpener.swift b/Sources/Shared/Toolkit/Archive/DefaultArchiveOpener.swift index 73e1bdfa56..b60d612f58 100644 --- a/Sources/Shared/Toolkit/Archive/DefaultArchiveOpener.swift +++ b/Sources/Shared/Toolkit/Archive/DefaultArchiveOpener.swift @@ -7,7 +7,7 @@ import Foundation /// Default implementation of ``ArchiveOpener`` supporting ZIP archives. -public class DefaultArchiveOpener: CompositeArchiveOpener { +public final class DefaultArchiveOpener: CompositeArchiveOpener { /// - Parameter additionalArchiveOpeners: Additional archive openers to use. public init(additionalArchiveOpeners: [any ArchiveOpener] = []) { super.init(additionalArchiveOpeners + [ZIPArchiveOpener()]) diff --git a/Sources/Shared/Toolkit/Data/Asset/AssetRetriever.swift b/Sources/Shared/Toolkit/Data/Asset/AssetRetriever.swift index 76137ec203..9979728d98 100644 --- a/Sources/Shared/Toolkit/Data/Asset/AssetRetriever.swift +++ b/Sources/Shared/Toolkit/Data/Asset/AssetRetriever.swift @@ -8,7 +8,7 @@ import Foundation /// Error while trying to retrieve an asset from a ``Resource`` or a /// ``Container``. -public enum AssetRetrieveError: Error { +public enum AssetRetrieveError: Error, Sendable { /// The format of the resource is not recognized. case formatNotSupported @@ -17,7 +17,7 @@ public enum AssetRetrieveError: Error { } /// Error while trying to retrieve an asset from an URL. -public enum AssetRetrieveURLError: Error { +public enum AssetRetrieveURLError: Error, Sendable { /// The scheme (e.g. http, file, content) for the requested URL is not /// supported. case schemeNotSupported(URLScheme) diff --git a/Sources/Shared/Toolkit/Data/Container/Container.swift b/Sources/Shared/Toolkit/Data/Container/Container.swift index e6a18fd906..a3bf564085 100644 --- a/Sources/Shared/Toolkit/Data/Container/Container.swift +++ b/Sources/Shared/Toolkit/Data/Container/Container.swift @@ -28,7 +28,7 @@ public protocol Container: Closeable { } /// A `Container` providing no entries at all. -public struct EmptyContainer: Container { +public struct EmptyContainer: Container, Sendable { public init() {} public let sourceURL: AbsoluteURL? = nil @@ -46,7 +46,7 @@ public struct EmptyContainer: Container { /// sources. /// /// The `containers` will be tested in the given order. -public class CompositeContainer: Container { +public final class CompositeContainer: Container { private let containers: [Container] public convenience init(_ containers: Container...) { diff --git a/Sources/Shared/Toolkit/Data/Container/SingleResourceContainer.swift b/Sources/Shared/Toolkit/Data/Container/SingleResourceContainer.swift index 4b37c828cd..aa8ee6e227 100644 --- a/Sources/Shared/Toolkit/Data/Container/SingleResourceContainer.swift +++ b/Sources/Shared/Toolkit/Data/Container/SingleResourceContainer.swift @@ -7,7 +7,7 @@ import Foundation /// Encapsulates a single ``Resource`` into a ``Container``. -public class SingleResourceContainer: Container { +public final class SingleResourceContainer: Container { public let entry: AnyURL private let resource: Resource diff --git a/Sources/Shared/Toolkit/Data/ReadError.swift b/Sources/Shared/Toolkit/Data/ReadError.swift index da5f3d597a..9fe617f494 100644 --- a/Sources/Shared/Toolkit/Data/ReadError.swift +++ b/Sources/Shared/Toolkit/Data/ReadError.swift @@ -7,7 +7,7 @@ import Foundation /// Errors occurring while reading a resource. -public enum ReadError: Error { +public enum ReadError: Error, Sendable { /// An error occurred while trying to access the content. /// /// At the moment, `AccessError`s constructed by the toolkit can be either @@ -44,7 +44,7 @@ public enum ReadError: Error { } } -public enum AccessError: Error { +public enum AccessError: Error, Sendable { /// An error occurred while accessing content over HTTP. case http(HTTPError) diff --git a/Sources/Shared/Toolkit/Data/Resource/FailureResource.swift b/Sources/Shared/Toolkit/Data/Resource/FailureResource.swift index 7cc0f39341..3a64ea90cd 100644 --- a/Sources/Shared/Toolkit/Data/Resource/FailureResource.swift +++ b/Sources/Shared/Toolkit/Data/Resource/FailureResource.swift @@ -7,7 +7,7 @@ import Foundation /// Creates a Resource that will always return the given `error`. -public final class FailureResource: Resource { +public final class FailureResource: Resource, Sendable { private let error: ReadError public let sourceURL: AbsoluteURL? diff --git a/Sources/Shared/Toolkit/Data/Resource/ResourceContentExtractor.swift b/Sources/Shared/Toolkit/Data/Resource/ResourceContentExtractor.swift index bf91db37cf..891cfdbec0 100644 --- a/Sources/Shared/Toolkit/Data/Resource/ResourceContentExtractor.swift +++ b/Sources/Shared/Toolkit/Data/Resource/ResourceContentExtractor.swift @@ -18,7 +18,7 @@ public protocol _ResourceContentExtractor { /// **WARNING:** This API is experimental and may change or be removed in a future release without /// notice. Use with caution. -public protocol _ResourceContentExtractorFactory { +public protocol _ResourceContentExtractorFactory: Sendable { /// Creates a `ResourceContentExtractor` instance for the given `resource`. /// Returns null if the resource format is not supported. func makeExtractor(for resource: Resource, mediaType: MediaType) -> _ResourceContentExtractor? @@ -26,7 +26,7 @@ public protocol _ResourceContentExtractorFactory { /// **WARNING:** This API is experimental and may change or be removed in a future release without /// notice. Use with caution. -public class _DefaultResourceContentExtractorFactory: _ResourceContentExtractorFactory { +public final class _DefaultResourceContentExtractorFactory: _ResourceContentExtractorFactory, Sendable { public init() {} public func makeExtractor(for resource: Resource, mediaType: MediaType) -> _ResourceContentExtractor? { diff --git a/Sources/Shared/Toolkit/Data/Resource/ResourceFactory.swift b/Sources/Shared/Toolkit/Data/Resource/ResourceFactory.swift index 7745466786..f621fec8be 100644 --- a/Sources/Shared/Toolkit/Data/Resource/ResourceFactory.swift +++ b/Sources/Shared/Toolkit/Data/Resource/ResourceFactory.swift @@ -12,7 +12,7 @@ public protocol ResourceFactory { func make(url: AbsoluteURL) async -> Result } -public enum ResourceMakeError: Error { +public enum ResourceMakeError: Error, Sendable { /// URL scheme not supported by the ``ResourceFactory``. case schemeNotSupported(URLScheme) } diff --git a/Sources/Shared/Toolkit/Data/Resource/ResourceProperties.swift b/Sources/Shared/Toolkit/Data/Resource/ResourceProperties.swift index a6256a4d7f..38815d7024 100644 --- a/Sources/Shared/Toolkit/Data/Resource/ResourceProperties.swift +++ b/Sources/Shared/Toolkit/Data/Resource/ResourceProperties.swift @@ -7,7 +7,7 @@ import Foundation /// Properties associated to a resource. -public struct ResourceProperties: Hashable { +public struct ResourceProperties: Hashable, Sendable { public var properties: [String: JSONValue] public init(_ properties: [String: JSONValue] = [:]) { diff --git a/Sources/Shared/Toolkit/DebugError.swift b/Sources/Shared/Toolkit/DebugError.swift index 0268e1dfc3..1b62a5e79b 100644 --- a/Sources/Shared/Toolkit/DebugError.swift +++ b/Sources/Shared/Toolkit/DebugError.swift @@ -6,7 +6,7 @@ import Foundation -public struct DebugError: Error, CustomStringConvertible { +public struct DebugError: Error, CustomStringConvertible, Sendable { public let message: String public let cause: Error? diff --git a/Sources/Shared/Toolkit/DocumentTypes.swift b/Sources/Shared/Toolkit/DocumentTypes.swift index bf98d7de71..914389b93d 100644 --- a/Sources/Shared/Toolkit/DocumentTypes.swift +++ b/Sources/Shared/Toolkit/DocumentTypes.swift @@ -14,7 +14,7 @@ import ReadiumInternal /// Provides a convenient access layer to the Document Types declared in the `Info.plist`, /// under `CFBundleDocumentTypes`. -public struct DocumentTypes { +public struct DocumentTypes: Sendable { /// Default `DocumentTypes` instance extracted from the main bundle's Info.plist. public static let main = DocumentTypes(bundle: .main) @@ -90,7 +90,7 @@ public struct DocumentTypes { } /// Metadata about a Document Type declared in `CFBundleDocumentTypes`. -public struct DocumentType: Equatable, Loggable { +public struct DocumentType: Equatable, Loggable, Sendable { /// Abstract name for the document type, used to refer to the type. public let name: String diff --git a/Sources/Shared/Toolkit/Either.swift b/Sources/Shared/Toolkit/Either.swift index 122339ca17..2ab91639f9 100644 --- a/Sources/Shared/Toolkit/Either.swift +++ b/Sources/Shared/Toolkit/Either.swift @@ -6,7 +6,7 @@ import Foundation -public enum Either { +public enum Either: Sendable { case left(L) case right(R) } diff --git a/Sources/Shared/Toolkit/File/DirectoryContainer.swift b/Sources/Shared/Toolkit/File/DirectoryContainer.swift index 478661bbfe..39c3bca3b9 100644 --- a/Sources/Shared/Toolkit/File/DirectoryContainer.swift +++ b/Sources/Shared/Toolkit/File/DirectoryContainer.swift @@ -7,8 +7,8 @@ import Foundation /// A file system directory as a ``Container``. -public struct DirectoryContainer: Container, Loggable { - public struct NotADirectoryError: Error {} +public struct DirectoryContainer: Container, Loggable, Sendable { + public struct NotADirectoryError: Error, Sendable {} private let directoryURL: FileURL public var sourceURL: AbsoluteURL? { diff --git a/Sources/Shared/Toolkit/File/FileContainer.swift b/Sources/Shared/Toolkit/File/FileContainer.swift index 20990411d7..6730be13a0 100644 --- a/Sources/Shared/Toolkit/File/FileContainer.swift +++ b/Sources/Shared/Toolkit/File/FileContainer.swift @@ -7,7 +7,7 @@ import Foundation /// Provides access to individual file resources on the local file system. -public final class FileContainer: Container, Loggable { +public final class FileContainer: Container, Loggable, Sendable { private let files: [RelativeURL: FileURL] public let sourceURL: AbsoluteURL? = nil diff --git a/Sources/Shared/Toolkit/File/FileResourceFactory.swift b/Sources/Shared/Toolkit/File/FileResourceFactory.swift index b71b282ba7..2324ac9f29 100644 --- a/Sources/Shared/Toolkit/File/FileResourceFactory.swift +++ b/Sources/Shared/Toolkit/File/FileResourceFactory.swift @@ -8,7 +8,7 @@ import Foundation /// Creates ``FileResource`` instances granting access to `file://` URLs stored /// on the file system. -public class FileResourceFactory: ResourceFactory { +public final class FileResourceFactory: ResourceFactory, Sendable { public func make(url: any AbsoluteURL) async -> Result { guard let file = url.fileURL else { return .failure(.schemeNotSupported(url.scheme)) diff --git a/Sources/Shared/Toolkit/File/FileSystemError.swift b/Sources/Shared/Toolkit/File/FileSystemError.swift index 4e70f9f262..0746cd777b 100644 --- a/Sources/Shared/Toolkit/File/FileSystemError.swift +++ b/Sources/Shared/Toolkit/File/FileSystemError.swift @@ -7,7 +7,7 @@ import Foundation /// Error occurring on the file system. -public enum FileSystemError: Error { +public enum FileSystemError: Error, Sendable { /// File was not found. case fileNotFound(Error?) diff --git a/Sources/Shared/Toolkit/FileExtension.swift b/Sources/Shared/Toolkit/FileExtension.swift index 86380c0c3c..8f1f76b55b 100644 --- a/Sources/Shared/Toolkit/FileExtension.swift +++ b/Sources/Shared/Toolkit/FileExtension.swift @@ -7,7 +7,7 @@ import Foundation /// Represents a file extension. -public struct FileExtension: Hashable, RawRepresentable, ExpressibleByStringLiteral { +public struct FileExtension: Hashable, RawRepresentable, ExpressibleByStringLiteral, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/Shared/Toolkit/Format/Format.swift b/Sources/Shared/Toolkit/Format/Format.swift index 4f395e7035..a4728bbb50 100644 --- a/Sources/Shared/Toolkit/Format/Format.swift +++ b/Sources/Shared/Toolkit/Format/Format.swift @@ -8,7 +8,7 @@ import Foundation import ReadiumInternal /// Represents and holds information about the document format of an asset. -public struct Format: Hashable { +public struct Format: Hashable, Sendable { public var specifications: FormatSpecifications public var mediaType: MediaType? public var fileExtension: FileExtension? @@ -86,7 +86,7 @@ public struct Format: Hashable { ) } -public struct FormatSpecifications: Hashable { +public struct FormatSpecifications: Hashable, Sendable { public var specifications: Set public init(_ specifications: FormatSpecification...) { @@ -122,7 +122,7 @@ public struct FormatSpecifications: Hashable { } } -public struct FormatSpecification: RawRepresentable, Hashable { +public struct FormatSpecification: RawRepresentable, Hashable, Sendable { public var rawValue: String public init(rawValue: String) { diff --git a/Sources/Shared/Toolkit/Format/FormatSniffer.swift b/Sources/Shared/Toolkit/Format/FormatSniffer.swift index a87674ab5b..30a0cc3df7 100644 --- a/Sources/Shared/Toolkit/Format/FormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/FormatSniffer.swift @@ -53,7 +53,7 @@ public extension FormatSniffer { } /// Bundle of media type and file extension hints for the `FormatHintsSniffer`. -public struct FormatHints { +public struct FormatHints: Sendable { public var mediaTypes: [MediaType] public var fileExtensions: [FileExtension] diff --git a/Sources/Shared/Toolkit/Format/Sniffers/AudioFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/AudioFormatSniffer.swift index 50792893f2..80ac5fae68 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/AudioFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/AudioFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs audio formats. -public class AudioFormatSniffer: FormatSniffer { +public final class AudioFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/AudiobookFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/AudiobookFormatSniffer.swift index dfd5bdac27..ebaac322a7 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/AudiobookFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/AudiobookFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs an Audiobook. -public struct ZABFormatSniffer: FormatSniffer { +public struct ZABFormatSniffer: FormatSniffer, Sendable { /// Required extensions for an archive to be considered an audiobook public static let defaultRequiredExtensions: Set = audioExtensions diff --git a/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift index 165e44a59f..088316d797 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs bitmap formats. -public class BitmapFormatSniffer: FormatSniffer { +public final class BitmapFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift index 0cbe03383d..61634971a2 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs a ComicBook Archive. -public struct ComicFormatSniffer: FormatSniffer { +public struct ComicFormatSniffer: FormatSniffer, Sendable { /// Required extensions for an archive to be considered a ComicBook Archive. /// Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ public static let defaultRequiredExtensions: Set = bitmapExtensions diff --git a/Sources/Shared/Toolkit/Format/Sniffers/EPUBFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/EPUBFormatSniffer.swift index 5aa775bace..89e6947504 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/EPUBFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/EPUBFormatSniffer.swift @@ -9,7 +9,7 @@ import Foundation /// Sniffs an EPUB publication. /// /// Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime -public struct EPUBFormatSniffer: FormatSniffer { +public struct EPUBFormatSniffer: FormatSniffer, Sendable { private let xmlDocumentFactory: XMLDocumentFactory public init(xmlDocumentFactory: XMLDocumentFactory) { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/HTMLFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/HTMLFormatSniffer.swift index 8c324b2358..3e43e6e5b7 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/HTMLFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/HTMLFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs an HTML or XHTML document. -public struct HTMLFormatSniffer: FormatSniffer { +public struct HTMLFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/JSONFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/JSONFormatSniffer.swift index b3f588bd6f..b31d1e3cf6 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/JSONFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/JSONFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs a JSON document. -public struct JSONFormatSniffer: FormatSniffer { +public struct JSONFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/LCPLicenseFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/LCPLicenseFormatSniffer.swift index f780980f4b..ffb4550d15 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/LCPLicenseFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/LCPLicenseFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs an LCP License Document. -public struct LCPLicenseFormatSniffer: FormatSniffer { +public struct LCPLicenseFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/LanguageFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/LanguageFormatSniffer.swift index 84f952dd09..68a3261c71 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/LanguageFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/LanguageFormatSniffer.swift @@ -6,7 +6,7 @@ import Foundation -public class LanguageFormatSniffer: FormatSniffer { +public final class LanguageFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/OPDSFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/OPDSFormatSniffer.swift index 444ca3ffd9..354d01eecb 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/OPDSFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/OPDSFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs OPDS documents. -public class OPDSFormatSniffer: FormatSniffer { +public final class OPDSFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/PDFFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/PDFFormatSniffer.swift index cde825e181..bff410cf72 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/PDFFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/PDFFormatSniffer.swift @@ -9,7 +9,7 @@ import Foundation /// Sniffs a PDF document. /// /// Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml -public struct PDFFormatSniffer: FormatSniffer { +public struct PDFFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/RARFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/RARFormatSniffer.swift index 11bbf15d27..eb29bc3c40 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/RARFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/RARFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs a RAR file. -public struct RARFormatSniffer: FormatSniffer { +public struct RARFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/RPFFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/RPFFormatSniffer.swift index b5f6f8a6be..7a2d440b41 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/RPFFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/RPFFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs a Readium Web Publication package. -public struct RPFFormatSniffer: FormatSniffer { +public struct RPFFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/RWPMFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/RWPMFormatSniffer.swift index 4373e0a381..5e3f2c5e0d 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/RWPMFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/RWPMFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs a Readium Web Publication Manifest. -public struct RWPMFormatSniffer: FormatSniffer { +public struct RWPMFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/XMLFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/XMLFormatSniffer.swift index b2f8b00e99..601facad32 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/XMLFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/XMLFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs an XML document. -public struct XMLFormatSniffer: FormatSniffer { +public struct XMLFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/Format/Sniffers/ZIPFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/ZIPFormatSniffer.swift index 6b0f13ca97..b0ffe64d2d 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/ZIPFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/ZIPFormatSniffer.swift @@ -7,7 +7,7 @@ import Foundation /// Sniffs a ZIP file. -public struct ZIPFormatSniffer: FormatSniffer { +public struct ZIPFormatSniffer: FormatSniffer, Sendable { public init() {} public func sniffHints(_ hints: FormatHints) -> Format? { diff --git a/Sources/Shared/Toolkit/HTTP/DefaultHTTPClient.swift b/Sources/Shared/Toolkit/HTTP/DefaultHTTPClient.swift index 462070a657..f4ad5cca93 100644 --- a/Sources/Shared/Toolkit/HTTP/DefaultHTTPClient.swift +++ b/Sources/Shared/Toolkit/HTTP/DefaultHTTPClient.swift @@ -7,7 +7,7 @@ import Foundation import UIKit -public enum URLAuthenticationChallengeResponse { +public enum URLAuthenticationChallengeResponse: Sendable { /// Use the specified credential. case useCredential(URLCredential) /// Use the default handling for the challenge as though this delegate method were not implemented. diff --git a/Sources/Shared/Toolkit/HTTP/HTTPClient.swift b/Sources/Shared/Toolkit/HTTP/HTTPClient.swift index b43b27a3c3..f8e6109b17 100644 --- a/Sources/Shared/Toolkit/HTTP/HTTPClient.swift +++ b/Sources/Shared/Toolkit/HTTP/HTTPClient.swift @@ -151,7 +151,7 @@ public extension HTTPClient { } /// Status code of an HTTP response. -public struct HTTPStatus: Equatable, RawRepresentable, ExpressibleByIntegerLiteral { +public struct HTTPStatus: Equatable, RawRepresentable, ExpressibleByIntegerLiteral, Sendable { public let rawValue: Int public init(rawValue: RawValue) { @@ -284,7 +284,7 @@ public struct HTTPResponse: Equatable { } /// Holds the information about a successful download. -public struct HTTPDownload { +public struct HTTPDownload: Sendable { /// The location of a temporary file where the server's response is stored. /// You are responsible for moving or deleting the downloaded file.. public let location: FileURL diff --git a/Sources/Shared/Toolkit/HTTP/HTTPProblemDetails.swift b/Sources/Shared/Toolkit/HTTP/HTTPProblemDetails.swift index 2aa39d849c..0f41d92f4c 100644 --- a/Sources/Shared/Toolkit/HTTP/HTTPProblemDetails.swift +++ b/Sources/Shared/Toolkit/HTTP/HTTPProblemDetails.swift @@ -10,7 +10,7 @@ import Foundation /// /// https://tools.ietf.org/html/rfc7807 public struct HTTPProblemDetails: Decodable, Equatable, Sendable { - public enum Error: Swift.Error { + public enum Error: Swift.Error, Sendable { case malformed(json: String?) } diff --git a/Sources/Shared/Toolkit/HTTP/HTTPRequest.swift b/Sources/Shared/Toolkit/HTTP/HTTPRequest.swift index f6ca759d1e..7edad4384e 100644 --- a/Sources/Shared/Toolkit/HTTP/HTTPRequest.swift +++ b/Sources/Shared/Toolkit/HTTP/HTTPRequest.swift @@ -15,7 +15,7 @@ public struct HTTPRequest: Equatable { public var method: Method /// Supported HTTP methods. - public enum Method: String, Equatable { + public enum Method: String, Equatable, Sendable { case delete = "DELETE" case get = "GET" case head = "HEAD" @@ -32,7 +32,7 @@ public struct HTTPRequest: Equatable { public var body: Body? /// Supported body values. - public enum Body: Equatable { + public enum Body: Equatable, Sendable { case data(Data) case file(URL) } @@ -130,7 +130,7 @@ public protocol HTTPRequestConvertible { func httpRequest() -> HTTPResult } -public enum HTTPRequestError: Error { +public enum HTTPRequestError: Error, Sendable { case invalidURL(CustomStringConvertible & Sendable) } diff --git a/Sources/Shared/Toolkit/HTTP/HTTPResourceFactory.swift b/Sources/Shared/Toolkit/HTTP/HTTPResourceFactory.swift index 526dc7e85d..36a6d65409 100644 --- a/Sources/Shared/Toolkit/HTTP/HTTPResourceFactory.swift +++ b/Sources/Shared/Toolkit/HTTP/HTTPResourceFactory.swift @@ -8,7 +8,7 @@ import Foundation /// Creates ``HTTPResource`` instances granting access to `http(s)://` URLs /// using an ``HTTPClient``. -public class HTTPResourceFactory: ResourceFactory { +public final class HTTPResourceFactory: ResourceFactory { private let client: HTTPClient public init(client: HTTPClient) { diff --git a/Sources/Shared/Toolkit/HTTP/HTTPServer.swift b/Sources/Shared/Toolkit/HTTP/HTTPServer.swift index 24098445da..0f9132f7cd 100644 --- a/Sources/Shared/Toolkit/HTTP/HTTPServer.swift +++ b/Sources/Shared/Toolkit/HTTP/HTTPServer.swift @@ -118,7 +118,7 @@ public extension HTTPServer { public typealias HTTPServerEndpoint = String /// Request made to an `HTTPServer`. -public struct HTTPServerRequest { +public struct HTTPServerRequest: Sendable { /// Absolute URL on the server. public let url: HTTPURL diff --git a/Sources/Shared/Toolkit/JSONValue.swift b/Sources/Shared/Toolkit/JSONValue.swift index ef6acfc6ae..ca9d696008 100644 --- a/Sources/Shared/Toolkit/JSONValue.swift +++ b/Sources/Shared/Toolkit/JSONValue.swift @@ -127,11 +127,11 @@ public enum JSONValue: Sendable, Hashable, Loggable { // MARK: - Errors /// Errors thrown during JSON parsing and serialization. -public enum JSONError: Error { +public enum JSONError: Error, Sendable { /// The JSON data could not be parsed into the expected type. - case parsing(Any.Type, cause: Error? = nil) + case parsing(Any.Type, cause: (any Error)? = nil) /// The value could not be serialized to JSON. - case serializing(Any.Type, cause: Error? = nil) + case serializing(Any.Type, cause: (any Error)? = nil) } // MARK: - Decoding Protocols diff --git a/Sources/Shared/Toolkit/Logging/WarningLogger.swift b/Sources/Shared/Toolkit/Logging/WarningLogger.swift index d92fd9cacd..c205ee509d 100644 --- a/Sources/Shared/Toolkit/Logging/WarningLogger.swift +++ b/Sources/Shared/Toolkit/Logging/WarningLogger.swift @@ -30,7 +30,7 @@ public protocol Warning { } /// Indicates how the user experience might be affected by a warning. -public enum WarningSeverityLevel { +public enum WarningSeverityLevel: Sendable { /// The user probably won't notice the issue. case minor /// The user experience might be affected, but it shouldn't prevent the user from enjoying the @@ -41,7 +41,7 @@ public enum WarningSeverityLevel { } /// Warning raised when parsing a model object from its JSON representation fails. -public struct JSONWarning: Warning { +public struct JSONWarning: Warning, Sendable { /// Type of the model object to be parsed. public let modelType: Any.Type /// Details about the failure. diff --git a/Sources/Shared/Toolkit/Media/AudioSession.swift b/Sources/Shared/Toolkit/Media/AudioSession.swift index cd1f33bfd6..cc529e5ab0 100644 --- a/Sources/Shared/Toolkit/Media/AudioSession.swift +++ b/Sources/Shared/Toolkit/Media/AudioSession.swift @@ -26,8 +26,8 @@ public extension AudioSessionUser { /// Manages an activated `AVAudioSession`. @MainActor -public final class AudioSession: Loggable { - public struct Configuration: Equatable { +public final class AudioSession: Loggable, Sendable { + public struct Configuration: Equatable, Sendable { let category: AVAudioSession.Category let mode: AVAudioSession.Mode let routeSharingPolicy: AVAudioSession.RouteSharingPolicy diff --git a/Sources/Shared/Toolkit/Media/NowPlayingInfo.swift b/Sources/Shared/Toolkit/Media/NowPlayingInfo.swift index 5c2904f7e9..a5d781d6a3 100644 --- a/Sources/Shared/Toolkit/Media/NowPlayingInfo.swift +++ b/Sources/Shared/Toolkit/Media/NowPlayingInfo.swift @@ -36,7 +36,7 @@ public final class NowPlayingInfo { } } - public struct Playback: Equatable { + public struct Playback: Equatable, Sendable { /// The playback duration of the media item, in seconds. public var duration: Double? /// The elapsed time of the now playing item, in seconds. diff --git a/Sources/Shared/Toolkit/PDF/CGPDF.swift b/Sources/Shared/Toolkit/PDF/CGPDF.swift index 8b4d0c3c31..e30f740afe 100644 --- a/Sources/Shared/Toolkit/PDF/CGPDF.swift +++ b/Sources/Shared/Toolkit/PDF/CGPDF.swift @@ -226,7 +226,7 @@ extension CGPDFDocument: PDFDocument { } /// Creates a `PDFDocument` using Core Graphics. -public class CGPDFDocumentFactory: PDFDocumentFactory, Loggable { +public final class CGPDFDocumentFactory: PDFDocumentFactory, Loggable, Sendable { public func open(file: FileURL, password: String?) async throws -> PDFDocument { guard let document = CGPDFDocument(file.url as CFURL) else { throw PDFDocumentError.openFailed diff --git a/Sources/Shared/Toolkit/PDF/PDFDocument.swift b/Sources/Shared/Toolkit/PDF/PDFDocument.swift index 06f67f1243..70a1cedf78 100644 --- a/Sources/Shared/Toolkit/PDF/PDFDocument.swift +++ b/Sources/Shared/Toolkit/PDF/PDFDocument.swift @@ -7,7 +7,7 @@ import Foundation import UIKit -public enum PDFDocumentError: Error { +public enum PDFDocumentError: Error, Sendable { /// The provided password was incorrect. case invalidPassword /// Impossible to open the given PDF. @@ -57,7 +57,7 @@ public protocol PDFDocumentFactory { func open(resource: Resource, at href: HREF, password: String?) async throws -> PDFDocument } -public class DefaultPDFDocumentFactory: PDFDocumentFactory, Loggable { +public final class DefaultPDFDocumentFactory: PDFDocumentFactory, Loggable, Sendable { /// The default PDF document factory uses Core Graphics. private let factory = CGPDFDocumentFactory() @@ -73,7 +73,7 @@ public class DefaultPDFDocumentFactory: PDFDocumentFactory, Loggable { } /// A PDF document factory which will iterate over a list of factories until one works. -public class CompositePDFDocumentFactory: PDFDocumentFactory, Loggable { +public final class CompositePDFDocumentFactory: PDFDocumentFactory, Loggable { private let factories: [PDFDocumentFactory] public init(factories: [PDFDocumentFactory]) { diff --git a/Sources/Shared/Toolkit/PDF/PDFKit.swift b/Sources/Shared/Toolkit/PDF/PDFKit.swift index 156f4fe567..7d9b50c201 100644 --- a/Sources/Shared/Toolkit/PDF/PDFKit.swift +++ b/Sources/Shared/Toolkit/PDF/PDFKit.swift @@ -52,7 +52,7 @@ extension PDFKit.PDFDocument: PDFDocument { } /// Creates a `PDFDocument` using PDFKit. -public class PDFKitPDFDocumentFactory: PDFDocumentFactory { +public final class PDFKitPDFDocumentFactory: PDFDocumentFactory, Sendable { public func open(file: FileURL, password: String?) async throws -> PDFDocument { guard let document = PDFKit.PDFDocument(url: file.url) else { throw PDFDocumentError.openFailed diff --git a/Sources/Shared/Toolkit/PDF/PDFOutlineNode.swift b/Sources/Shared/Toolkit/PDF/PDFOutlineNode.swift index 72fa1f658c..3bc4a07bcf 100644 --- a/Sources/Shared/Toolkit/PDF/PDFOutlineNode.swift +++ b/Sources/Shared/Toolkit/PDF/PDFOutlineNode.swift @@ -6,7 +6,7 @@ import Foundation -public struct PDFOutlineNode { +public struct PDFOutlineNode: Sendable { /// Title of this outline item. public let title: String? diff --git a/Sources/Shared/Toolkit/Tokenizer/TextTokenizer.swift b/Sources/Shared/Toolkit/Tokenizer/TextTokenizer.swift index efd7cc9645..da9557b719 100644 --- a/Sources/Shared/Toolkit/Tokenizer/TextTokenizer.swift +++ b/Sources/Shared/Toolkit/Tokenizer/TextTokenizer.swift @@ -11,11 +11,11 @@ import NaturalLanguage public typealias TextTokenizer = Tokenizer> /// A text token unit which can be used with a `TextTokenizer`. -public enum TextUnit { +public enum TextUnit: Sendable { case word, sentence, paragraph } -public enum TextTokenizerError: Error { +public enum TextTokenizerError: Error, Sendable { case rangeConversionFailed(range: NSRange, string: String) } diff --git a/Sources/Shared/Toolkit/URL/AnyURL.swift b/Sources/Shared/Toolkit/URL/AnyURL.swift index 3f5ea72c48..8532e4457c 100644 --- a/Sources/Shared/Toolkit/URL/AnyURL.swift +++ b/Sources/Shared/Toolkit/URL/AnyURL.swift @@ -10,7 +10,7 @@ import ReadiumInternal /// Represents either an absolute or relative URL. /// /// See https://url.spec.whatwg.org -public enum AnyURL: URLProtocol { +public enum AnyURL: URLProtocol, Sendable { /// An absolute URL. case absolute(AbsoluteURL) diff --git a/Sources/Shared/Toolkit/URL/RelativeURL.swift b/Sources/Shared/Toolkit/URL/RelativeURL.swift index e2fcc40248..71fbe7890f 100644 --- a/Sources/Shared/Toolkit/URL/RelativeURL.swift +++ b/Sources/Shared/Toolkit/URL/RelativeURL.swift @@ -7,7 +7,7 @@ import Foundation /// Represents a relative URL. -public struct RelativeURL: URLProtocol, Hashable { +public struct RelativeURL: URLProtocol, Hashable, Sendable { public let url: URL /// Creates a ``RelativeURL`` from a standard Swift `URL`. diff --git a/Sources/Shared/Toolkit/URL/URITemplate.swift b/Sources/Shared/Toolkit/URL/URITemplate.swift index 2ac1b62e01..d73d9da81a 100644 --- a/Sources/Shared/Toolkit/URL/URITemplate.swift +++ b/Sources/Shared/Toolkit/URL/URITemplate.swift @@ -11,7 +11,7 @@ import ReadiumInternal /// /// Only handles simple cases, fitting Readium's use cases. /// See https://tools.ietf.org/html/rfc6570 -public struct URITemplate: CustomStringConvertible { +public struct URITemplate: CustomStringConvertible, Sendable { public let uri: String public init(_ uri: String) { diff --git a/Sources/Shared/Toolkit/URL/URLQuery.swift b/Sources/Shared/Toolkit/URL/URLQuery.swift index ef02bd5514..f477e197d1 100644 --- a/Sources/Shared/Toolkit/URL/URLQuery.swift +++ b/Sources/Shared/Toolkit/URL/URLQuery.swift @@ -7,9 +7,9 @@ import Foundation /// Represents a list of query parameters in a URL. -public struct URLQuery: Hashable { +public struct URLQuery: Hashable, Sendable { /// Represents a single query parameter and its value in a URL. - public struct Parameter: Hashable { + public struct Parameter: Hashable, Sendable { public let name: String public let value: String? } diff --git a/Sources/Shared/Toolkit/Weak.swift b/Sources/Shared/Toolkit/Weak.swift index 87d5970b9a..5e563f0a18 100644 --- a/Sources/Shared/Toolkit/Weak.swift +++ b/Sources/Shared/Toolkit/Weak.swift @@ -11,9 +11,8 @@ import Foundation /// Get the reference by calling `weakVar()`. /// Conveniently, the reference can be reset by setting the `ref` property. @dynamicCallable -public class Weak { - /// Weakly held reference. - public weak var ref: T? +public class Weak: @unchecked Sendable { + public package(set) weak var ref: T? public init(_ ref: T? = nil) { self.ref = ref @@ -23,22 +22,3 @@ public class Weak { ref } } - -/// Smart pointer passing as a Weak reference but preventing the reference from being lost. -/// Mainly useful for the unit test suite. -public class _Strong: Weak { - private var strongRef: T? - - override public var ref: T? { - get { super.ref } - set { - super.ref = newValue - strongRef = newValue - } - } - - override public init(_ ref: T? = nil) { - strongRef = ref - super.init(ref) - } -} diff --git a/Sources/Shared/Toolkit/XML/XML.swift b/Sources/Shared/Toolkit/XML/XML.swift index 25dd4fe184..2c74f83928 100644 --- a/Sources/Shared/Toolkit/XML/XML.swift +++ b/Sources/Shared/Toolkit/XML/XML.swift @@ -6,7 +6,7 @@ import Foundation -public struct XMLNamespace { +public struct XMLNamespace: Sendable { public let prefix: String public let uri: String @@ -70,7 +70,7 @@ public protocol XMLElement: XMLNode { func attribute(named localName: String, namespace: String?) -> String? } -public protocol XMLDocumentFactory { +public protocol XMLDocumentFactory: Sendable { /// Opens an XML document from a local file path. /// /// - Parameters: @@ -96,7 +96,7 @@ public protocol XMLDocumentFactory { func open(string: String, namespaces: [XMLNamespace]) throws -> XMLDocument } -public class DefaultXMLDocumentFactory: XMLDocumentFactory, Loggable { +public final class DefaultXMLDocumentFactory: XMLDocumentFactory, Loggable, Sendable { public init() {} public func open(file: FileURL, namespaces: [XMLNamespace]) async throws -> XMLDocument { diff --git a/Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift index 4312b1008a..145a1c1412 100644 --- a/Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift +++ b/Sources/Shared/Toolkit/ZIP/Minizip/MinizipArchiveOpener.swift @@ -12,7 +12,7 @@ import Foundation /// - Does not support HTTP streaming of ZIP archives. /// - Has better performance when reading an LCP-protected package containing /// large deflated ZIP entries (instead of stored). -public final class MinizipArchiveOpener: ArchiveOpener { +public final class MinizipArchiveOpener: ArchiveOpener, Sendable { public init() {} public func open(resource: any Resource, format: Format) async -> Result { diff --git a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift index 1b2476637f..97224051a1 100644 --- a/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift +++ b/Sources/Shared/Toolkit/ZIP/ZIPArchiveOpener.swift @@ -7,7 +7,7 @@ import Foundation /// An ``ArchiveOpener`` for ZIP resources. -public class ZIPArchiveOpener: CompositeArchiveOpener { +public final class ZIPArchiveOpener: CompositeArchiveOpener { public init() { super.init([ MinizipArchiveOpener(), diff --git a/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift index 1c09e67e55..3d320cdf40 100644 --- a/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift +++ b/Sources/Shared/Toolkit/ZIP/ZIPFoundation/ZIPFoundationArchiveOpener.swift @@ -7,7 +7,7 @@ import Foundation /// An ``ArchiveOpener`` able to open ZIP archives using ZIPFoundation. -public final class ZIPFoundationArchiveOpener: ArchiveOpener { +public final class ZIPFoundationArchiveOpener: ArchiveOpener, Sendable { public init() {} public func open(resource: any Resource, format: Format) async -> Result { diff --git a/Sources/Streamer/Parser/Audio/AudioPublicationManifestAugmentor.swift b/Sources/Streamer/Parser/Audio/AudioPublicationManifestAugmentor.swift index 24c25be8b1..e1e2d8457f 100644 --- a/Sources/Streamer/Parser/Audio/AudioPublicationManifestAugmentor.swift +++ b/Sources/Streamer/Parser/Audio/AudioPublicationManifestAugmentor.swift @@ -28,7 +28,7 @@ public struct AudioPublicationAugmentedManifest { /// An `AudioPublicationManifestAugmentor` using AVFoundation to retrieve the audio metadata. /// /// It will only work for local publications (file://). -public final class AVAudioPublicationManifestAugmentor: AudioPublicationManifestAugmentor { +public final class AVAudioPublicationManifestAugmentor: AudioPublicationManifestAugmentor, Sendable { public init() {} public func augment(_ manifest: Manifest, using container: Container) async -> AudioPublicationAugmentedManifest { diff --git a/Sources/Streamer/Parser/EPUB/EPUBParser.swift b/Sources/Streamer/Parser/EPUB/EPUBParser.swift index d9d18e3aa4..31955c1a81 100644 --- a/Sources/Streamer/Parser/EPUB/EPUBParser.swift +++ b/Sources/Streamer/Parser/EPUB/EPUBParser.swift @@ -15,7 +15,7 @@ import ReadiumShared /// - missingFile: A file is missing from the container at `path`. /// - xmlParse: An XML parsing error occurred. /// - missingElement: An XML element is missing. -public enum EPUBParserError: Error { +public enum EPUBParserError: Error, Sendable { /// The mimetype of the EPUB is not valid. case wrongMimeType case missingFile(path: String) @@ -28,7 +28,7 @@ extension EPUBParser: Loggable {} /// An EPUB container parser that extracts the information from the relevant /// files and builds a `Publication` instance out of it. -public final class EPUBParser: PublicationParser { +public final class EPUBParser: PublicationParser, Sendable { private let reflowablePositionsStrategy: EPUBPositionsService.ReflowableStrategy /// - Parameter reflowablePositionsStrategy: Strategy used to calculate the number of positions in a reflowable resource. diff --git a/Sources/Streamer/Parser/EPUB/OPFParser.swift b/Sources/Streamer/Parser/EPUB/OPFParser.swift index 2beb9340ae..62c5406b54 100644 --- a/Sources/Streamer/Parser/EPUB/OPFParser.swift +++ b/Sources/Streamer/Parser/EPUB/OPFParser.swift @@ -10,7 +10,7 @@ import ReadiumShared /// http://www.idpf.org/epub/30/spec/epub30-publications.html#title-type /// the six basic values of the "title-type" property specified by EPUB 3: -public enum EPUBTitleType: String { +public enum EPUBTitleType: String, Sendable { case main case subtitle case short diff --git a/Sources/Streamer/Parser/EPUB/Resource Transformers/EPUBDeobfuscator.swift b/Sources/Streamer/Parser/EPUB/Resource Transformers/EPUBDeobfuscator.swift index 7b9fbfe95d..b8b20469a8 100644 --- a/Sources/Streamer/Parser/EPUB/Resource Transformers/EPUBDeobfuscator.swift +++ b/Sources/Streamer/Parser/EPUB/Resource Transformers/EPUBDeobfuscator.swift @@ -94,7 +94,7 @@ final class EPUBDeobfuscator { } } -private protocol ObfuscationAlgorithm { +private protocol ObfuscationAlgorithm: Sendable { /// URI identifier for this algorithm. var identifier: String { get } diff --git a/Sources/Streamer/Parser/EPUB/SMIL/SMILParser.swift b/Sources/Streamer/Parser/EPUB/SMIL/SMILParser.swift index fabe4dc518..3947532226 100644 --- a/Sources/Streamer/Parser/EPUB/SMIL/SMILParser.swift +++ b/Sources/Streamer/Parser/EPUB/SMIL/SMILParser.swift @@ -311,7 +311,7 @@ private struct SMILGuidedNavigationDocumentParsing { /// Warning raised when parsing a model object from its SMIL representation /// fails. -public struct SMILWarning: Warning { +public struct SMILWarning: Warning, Sendable { /// Type of the model object to be parsed. public let modelType: Any.Type /// Details about the failure. diff --git a/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift b/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift index 26a81e59ab..bfbf5d9b2c 100644 --- a/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift +++ b/Sources/Streamer/Parser/EPUB/Services/EPUBPositionsService.swift @@ -30,7 +30,7 @@ public actor EPUBPositionsService: PositionsService { /// Strategy used to calculate the number of positions in a reflowable resource. /// /// Note that a fixed-layout resource always has a single position. - public enum ReflowableStrategy { + public enum ReflowableStrategy: Sendable { /// Use the archive entry length (whether it is compressed or stored) and split it by the given `pageLength`. case archiveEntryLength(pageLength: Int) diff --git a/Sources/Streamer/Parser/PDF/PDFParser.swift b/Sources/Streamer/Parser/PDF/PDFParser.swift index 3914e2146c..ff85aa0b27 100644 --- a/Sources/Streamer/Parser/PDF/PDFParser.swift +++ b/Sources/Streamer/Parser/PDF/PDFParser.swift @@ -9,7 +9,7 @@ import Foundation import ReadiumShared /// Errors thrown during the parsing of the PDF. -public enum PDFParserError: Error { +public enum PDFParserError: Error, Sendable { /// The file at 'path' is missing from the container. case missingFile(path: String) /// Failed to open the PDF diff --git a/Sources/Streamer/Parser/PublicationParser.swift b/Sources/Streamer/Parser/PublicationParser.swift index 3f2685cbc1..cbf6840b6e 100644 --- a/Sources/Streamer/Parser/PublicationParser.swift +++ b/Sources/Streamer/Parser/PublicationParser.swift @@ -20,7 +20,7 @@ public protocol PublicationParser { func parse(asset: Asset, warnings: WarningLogger?) async -> Result } -public enum PublicationParseError: Error { +public enum PublicationParseError: Error, Sendable { /// Asset format not supported. case formatNotSupported diff --git a/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift b/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift index 1ca7ad7362..e8eee9c4d7 100644 --- a/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift +++ b/Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift @@ -7,14 +7,14 @@ import Foundation import ReadiumShared -public enum ReadiumWebPubParserError: Error { +public enum ReadiumWebPubParserError: Error, Sendable { case parseFailure(url: URL, Error?) case missingFile(path: String) } /// Parser for a Readium Web Publication (packaged, or as a manifest). -public class ReadiumWebPubParser: PublicationParser, Loggable { - public enum Error: Swift.Error { +public final class ReadiumWebPubParser: PublicationParser, Loggable { + public enum Error: Swift.Error, Sendable { case manifestNotFound case invalidManifest } @@ -186,7 +186,7 @@ private extension ReadResult { } /// Warning raised when parsing a RWPM. -public struct RWPMWarning: Warning { +public struct RWPMWarning: Warning, Sendable { public let message: String public let severity: WarningSeverityLevel diff --git a/Sources/Streamer/PublicationOpener.swift b/Sources/Streamer/PublicationOpener.swift index cf2be56964..b8b293a9ac 100644 --- a/Sources/Streamer/PublicationOpener.swift +++ b/Sources/Streamer/PublicationOpener.swift @@ -15,7 +15,7 @@ import ReadiumShared /// - onCreatePublication: Called on every parsed `Publication.Builder`. It /// can be used to modify the manifest, the root container or the list of /// service factories of a `Publication`. -public class PublicationOpener { +public final class PublicationOpener { private let parser: PublicationParser private let contentProtections: [ContentProtection] private let onCreatePublication: Publication.Builder.Transform @@ -108,7 +108,7 @@ public class PublicationOpener { } } -public enum PublicationOpenError: Error { +public enum PublicationOpenError: Error, Sendable { /// The asset is not supported by the publication parser. case formatNotSupported diff --git a/Sources/Streamer/Toolkit/DataCompression.swift b/Sources/Streamer/Toolkit/DataCompression.swift index e6b93a33ab..de4b1bbf61 100644 --- a/Sources/Streamer/Toolkit/DataCompression.swift +++ b/Sources/Streamer/Toolkit/DataCompression.swift @@ -260,7 +260,7 @@ public extension Data { } /// Struct based type representing a Crc32 checksum. -public struct Crc32: CustomStringConvertible { +public struct Crc32: CustomStringConvertible, Sendable { private static let zLibCrc32: ZLibCrc32FuncPtr? = loadCrc32fromZLib() public init() {} @@ -349,7 +349,7 @@ public struct Crc32: CustomStringConvertible { } /// Struct based type representing a Adler32 checksum. -public struct Adler32: CustomStringConvertible { +public struct Adler32: CustomStringConvertible, Sendable { private static let zLibAdler32: ZLibAdler32FuncPtr? = loadAdler32fromZLib() public init() {} diff --git a/TestApp/Sources/OPDS/OPDSFeeds/OPDSFeedViewModel.swift b/TestApp/Sources/OPDS/OPDSFeeds/OPDSFeedViewModel.swift index df028f214c..6ffacc67b3 100644 --- a/TestApp/Sources/OPDS/OPDSFeeds/OPDSFeedViewModel.swift +++ b/TestApp/Sources/OPDS/OPDSFeeds/OPDSFeedViewModel.swift @@ -142,7 +142,7 @@ class OPDSFeedViewModel: ObservableObject { } // Create the group and assign publications - let pubGroup = ReadiumShared.Group(title: title) + var pubGroup = ReadiumShared.Group(title: title) pubGroup.publications = feed.publications return pubGroup } diff --git a/Tests/SharedTests/Publication/Services/Locator/DefaultLocatorServiceTests.swift b/Tests/SharedTests/Publication/Services/Locator/DefaultLocatorServiceTests.swift index f7a7a19883..2c3818988b 100644 --- a/Tests/SharedTests/Publication/Services/Locator/DefaultLocatorServiceTests.swift +++ b/Tests/SharedTests/Publication/Services/Locator/DefaultLocatorServiceTests.swift @@ -10,7 +10,7 @@ import XCTest class DefaultLocatorServiceTests: XCTestCase { /// locate(Locator) checks that the href exists. func testFromLocator() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "chap1", mediaType: .xml), Link(href: "chap2", mediaType: .xml), Link(href: "chap3", mediaType: .xml), @@ -21,13 +21,13 @@ class DefaultLocatorServiceTests: XCTestCase { } func testFromLocatorEmptyReadingOrder() async { - let service = makeService(readingOrder: []) + let (publication, service) = makeService(readingOrder: []) let result = await service.locate(Locator(href: "href", mediaType: .html)) XCTAssertNil(result) } func testFromLocatorNotFound() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "chap1", mediaType: .xml), Link(href: "chap3", mediaType: .xml), ]) @@ -37,7 +37,7 @@ class DefaultLocatorServiceTests: XCTestCase { } func testFromProgression() async { - let service = makeService(positions: positionsFixture) + let (publication, service) = makeService(positions: positionsFixture) var result = await service.locate(progression: 0.0) XCTAssertEqual(result, Locator( @@ -111,7 +111,7 @@ class DefaultLocatorServiceTests: XCTestCase { } func testFromIncorrectProgression() async { - let service = makeService(positions: positionsFixture) + let (publication, service) = makeService(positions: positionsFixture) var result = await service.locate(progression: -0.2) XCTAssertNil(result) @@ -121,13 +121,13 @@ class DefaultLocatorServiceTests: XCTestCase { } func testFromProgressionEmptyPositions() async { - let service = makeService(positions: []) + let (publication, service) = makeService(positions: []) let result = await service.locate(progression: 0.5) XCTAssertNil(result) } func testFromMinimalLink() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "/href", mediaType: .html, title: "Resource"), ]) @@ -139,7 +139,7 @@ class DefaultLocatorServiceTests: XCTestCase { } func testFromLinkInReadingOrderResourcesOrLinks() async { - let service = makeService( + let (publication, service) = makeService( links: [Link(href: "/href3", mediaType: .html)], readingOrder: [Link(href: "/href1", mediaType: .html)], resources: [Link(href: "/href2", mediaType: .html)] @@ -165,7 +165,7 @@ class DefaultLocatorServiceTests: XCTestCase { } func testFromLinkWithFragment() async throws { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "/href", mediaType: .html, title: "Resource"), ]) @@ -177,7 +177,7 @@ class DefaultLocatorServiceTests: XCTestCase { } func testTitleFallbackFromLink() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "/href", mediaType: .html), ]) @@ -189,7 +189,7 @@ class DefaultLocatorServiceTests: XCTestCase { } func testFromLinkNotFound() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "/href", mediaType: .html), ]) @@ -202,8 +202,8 @@ class DefaultLocatorServiceTests: XCTestCase { readingOrder: [Link] = [], resources: [Link] = [], positions: [[Locator]] = [] - ) -> DefaultLocatorService { - DefaultLocatorService(publication: _Strong(Publication( + ) -> (Publication, DefaultLocatorService) { + let publication = Publication( manifest: Manifest( metadata: Metadata(title: ""), links: links, @@ -213,7 +213,9 @@ class DefaultLocatorServiceTests: XCTestCase { servicesBuilder: PublicationServicesBuilder( positions: InMemoryPositionsService.makeFactory(positionsByReadingOrder: positions) ) - ))) + ) + let service = DefaultLocatorService(publication: Weak(publication)) + return (publication, service) } } diff --git a/Tests/StreamerTests/Parser/Audio/Services/AudioLocatorServiceTests.swift b/Tests/StreamerTests/Parser/Audio/Services/AudioLocatorServiceTests.swift index aba1465b6a..671520b545 100644 --- a/Tests/StreamerTests/Parser/Audio/Services/AudioLocatorServiceTests.swift +++ b/Tests/StreamerTests/Parser/Audio/Services/AudioLocatorServiceTests.swift @@ -11,7 +11,7 @@ import XCTest class AudioLocatorServiceTests: XCTestCase { func testLocateLocatorMatchingReadingOrderHREF() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "l1"), Link(href: "l2"), ]) @@ -22,7 +22,7 @@ class AudioLocatorServiceTests: XCTestCase { } func testLocateLocatorReturnsNilIfNoMatch() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "l1"), Link(href: "l2"), ]) @@ -33,7 +33,7 @@ class AudioLocatorServiceTests: XCTestCase { } func testLocateLocatorUsesTotalProgression() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "l1", mediaType: .mp3, duration: 100), Link(href: "l2", mediaType: .mp3, duration: 100), ]) @@ -70,7 +70,7 @@ class AudioLocatorServiceTests: XCTestCase { } func testLocateLocatorUsingTotalProgressionKeepsTitleAndText() async throws { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "l1", mediaType: .mp3, duration: 100), Link(href: "l2", mediaType: .mp3, duration: 100), ]) @@ -108,7 +108,7 @@ class AudioLocatorServiceTests: XCTestCase { } func testLocateProgression() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "l1", mediaType: .mp3, duration: 100), Link(href: "l2", mediaType: .mp3, duration: 100), ]) @@ -165,7 +165,7 @@ class AudioLocatorServiceTests: XCTestCase { } func testLocateInvalidProgression() async { - let service = makeService(readingOrder: [ + let (publication, service) = makeService(readingOrder: [ Link(href: "l1", mediaType: .mp3, duration: 100), Link(href: "l2", mediaType: .mp3, duration: 100), ]) @@ -177,11 +177,11 @@ class AudioLocatorServiceTests: XCTestCase { XCTAssertNil(result) } - private func makeService(readingOrder: [Link]) -> AudioLocatorService { - AudioLocatorService( - publication: _Strong(Publication( - manifest: Manifest(metadata: Metadata(title: ""), readingOrder: readingOrder) - )) + private func makeService(readingOrder: [Link]) -> (Publication, AudioLocatorService) { + let publication = Publication( + manifest: Manifest(metadata: Metadata(title: ""), readingOrder: readingOrder) ) + let service = AudioLocatorService(publication: Weak(publication)) + return (publication, service) } } diff --git a/docs/Migration Guide.md b/docs/Migration Guide.md index e4253017d4..aa5cde45ef 100644 --- a/docs/Migration Guide.md +++ b/docs/Migration Guide.md @@ -2,9 +2,7 @@ All migration steps necessary in reading apps to upgrade to major versions of the Swift Readium toolkit will be documented in this file. - - -## 3.8.0 +## Unreleased ### Migrating to `JSONValue` for JSON Parsing @@ -44,6 +42,7 @@ The free functions `serializeJSONString` and `serializeJSONData` have been repla +let data = locator.jsonData() ``` +## 3.8.0 ### Removing the HTTP Server from the EPUB Navigator From 5ad368db2bfa9e6fe4187ff8bb1cfd4f48b10c01 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:44:40 -0500 Subject: [PATCH 2/6] Make CoverService implementations Sendable --- .../Cover/GeneratedCoverService.swift | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Sources/Shared/Publication/Services/Cover/GeneratedCoverService.swift b/Sources/Shared/Publication/Services/Cover/GeneratedCoverService.swift index cca40733a3..26db8b0807 100644 --- a/Sources/Shared/Publication/Services/Cover/GeneratedCoverService.swift +++ b/Sources/Shared/Publication/Services/Cover/GeneratedCoverService.swift @@ -8,15 +8,28 @@ import Foundation import UIKit /// A `CoverService` which holds a lazily generated cover bitmap in memory. -public final class GeneratedCoverService: CoverService { +public final class GeneratedCoverService: CoverService, Sendable { enum Error: Swift.Error { case generationFailed } - private var _cover: ReadResult? - private let makeCover: () async -> ReadResult + private actor Cache { + var cover: ReadResult? - public init(makeCover: @escaping () async -> ReadResult) { + func getOrMake(make: () async -> ReadResult) async -> ReadResult { + if let cover = cover { + return cover + } + let newCover = await make() + cover = newCover + return newCover + } + } + + private let cache = Cache() + private let makeCover: @Sendable () async -> ReadResult + + public init(makeCover: @escaping @Sendable () async -> ReadResult) { self.makeCover = makeCover } @@ -31,10 +44,7 @@ public final class GeneratedCoverService: CoverService { ) private func cachedCover() async -> ReadResult { - if _cover == nil { - _cover = await makeCover() - } - return _cover! + await cache.getOrMake(make: makeCover) } public func cover() async -> ReadResult { @@ -53,11 +63,11 @@ public final class GeneratedCoverService: CoverService { return CoverResource(cover: cachedCover) } - public static func makeFactory(makeCover: @escaping () async -> ReadResult) -> (PublicationServiceContext) -> GeneratedCoverService? { + public static func makeFactory(makeCover: @escaping @Sendable () async -> ReadResult) -> @Sendable (PublicationServiceContext) -> GeneratedCoverService? { { _ in GeneratedCoverService(makeCover: makeCover) } } - public static func makeFactory(cover: UIImage) -> (PublicationServiceContext) -> GeneratedCoverService? { + public static func makeFactory(cover: UIImage) -> @Sendable (PublicationServiceContext) -> GeneratedCoverService? { { _ in GeneratedCoverService(cover: cover) } } From cc6855215a0485464e748e159fa01d8f3ffcdb6d Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:44:55 -0500 Subject: [PATCH 3/6] Make PositionsService implementations Sendable --- .../Positions/InMemoryPositionsService.swift | 2 +- .../PerResourcePositionsService.swift | 48 ++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift b/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift index 6790ea5080..54e8a2487c 100644 --- a/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift +++ b/Sources/Shared/Publication/Services/Positions/InMemoryPositionsService.swift @@ -18,7 +18,7 @@ public final class InMemoryPositionsService: PositionsService, Sendable { .success(_positions) } - public static func makeFactory(positionsByReadingOrder: [[Locator]]) -> (PublicationServiceContext) -> InMemoryPositionsService { + public static func makeFactory(positionsByReadingOrder: [[Locator]]) -> @Sendable (PublicationServiceContext) -> InMemoryPositionsService { { _ in InMemoryPositionsService(positionsByReadingOrder: positionsByReadingOrder) } diff --git a/Sources/Shared/Publication/Services/Positions/PerResourcePositionsService.swift b/Sources/Shared/Publication/Services/Positions/PerResourcePositionsService.swift index 1bdc239308..77483fd94b 100644 --- a/Sources/Shared/Publication/Services/Positions/PerResourcePositionsService.swift +++ b/Sources/Shared/Publication/Services/Positions/PerResourcePositionsService.swift @@ -8,7 +8,7 @@ import Foundation /// Simple `PositionsService` for a `Publication` which generates one position per `readingOrder` /// resource. -public final class PerResourcePositionsService: PositionsService { +public final class PerResourcePositionsService: PositionsService, Sendable { private let readingOrder: [Link] /// Media type that will be used as a fallback if the `Link` doesn't specify any. @@ -20,26 +20,40 @@ public final class PerResourcePositionsService: PositionsService { } public func positionsByReadingOrder() async -> ReadResult<[[Locator]]> { - .success(positions) + await .success(cache.getPositions(readingOrder: readingOrder, fallbackMediaType: fallbackMediaType)) } - private lazy var pageCount: Int = readingOrder.count - - private lazy var positions: [[Locator]] = readingOrder.enumerated().map { index, link in - [ - Locator( - href: link.url(), - mediaType: link.mediaType ?? fallbackMediaType, - title: link.title, - locations: Locator.Locations( - totalProgression: Double(index) / Double(pageCount), - position: index + 1 - ) - ), - ] + private actor Cache { + var positions: [[Locator]]? + + func getPositions(readingOrder: [Link], fallbackMediaType: MediaType) -> [[Locator]] { + if let positions = positions { + return positions + } + + let pageCount = readingOrder.count + let newPositions: [[Locator]] = readingOrder.enumerated().map { index, link in + [ + Locator( + href: link.url(), + mediaType: link.mediaType ?? fallbackMediaType, + title: link.title, + locations: Locator.Locations( + totalProgression: Double(index) / Double(pageCount), + position: index + 1 + ) + ), + ] + } + + positions = newPositions + return newPositions + } } - public static func makeFactory(fallbackMediaType: MediaType) -> (PublicationServiceContext) -> PerResourcePositionsService { + private let cache = Cache() + + public static func makeFactory(fallbackMediaType: MediaType) -> @Sendable (PublicationServiceContext) -> PerResourcePositionsService { { context in PerResourcePositionsService(readingOrder: context.manifest.readingOrder, fallbackMediaType: fallbackMediaType) } From 97d3a9b04e847f219f5ecba176c8e1ac275c8856 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:45:10 -0500 Subject: [PATCH 4/6] Make PDF parser services Sendable --- .../PDF/Services/LCPDFPositionsService.swift | 70 ++++++++++++------- .../LCPDFTableOfContentsService.swift | 53 ++++++++++---- .../PDF/Services/PDFPositionsService.swift | 4 +- 3 files changed, 86 insertions(+), 41 deletions(-) diff --git a/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift b/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift index bc8778a084..3ba4f09388 100644 --- a/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift +++ b/Sources/Streamer/Parser/PDF/Services/LCPDFPositionsService.swift @@ -20,38 +20,60 @@ final class LCPDFPositionsService: PositionsService, PDFPublicationService, Logg } func positionsByReadingOrder() async -> ReadResult<[[Locator]]> { - await positionsByReadingOrderTask.value + await cache.getOrMakeTask( + readingOrder: readingOrder, + container: container, + pdfFactory: pdfFactory + ).value } - private lazy var positionsByReadingOrderTask: Task, Never> = Task { - // Calculates the page count of each resource from the reading order. - let resources = await readingOrder.asyncMap { link -> (Int, Link) in - let href = link.url() - guard - let resource = container[href], - let document = try? await pdfFactory.open(resource: resource, at: href, password: nil), - let pageCount = try? await document.pageCount() - else { - log(.warning, "Can't get the number of pages from PDF document at \(link)") - return (0, link) + private actor Cache { + var task: Task, Never>? + + func getOrMakeTask( + readingOrder: [Link], + container: Container, + pdfFactory: PDFDocumentFactory + ) -> Task, Never> { + if let task = task { + return task } - return (pageCount, link) - } - let totalPageCount = resources.reduce(0) { count, current in count + current.0 } + let newTask = Task, Never> { + // Calculates the page count of each resource from the reading order. + let resources = await readingOrder.asyncMap { link -> (Int, Link) in + let href = link.url() + guard + let resource = container[href], + let document = try? await pdfFactory.open(resource: resource, at: href, password: nil), + let pageCount = try? await document.pageCount() + else { + return (0, link) + } + return (pageCount, link) + } + + let totalPageCount = resources.reduce(0) { count, current in count + current.0 } - var lastPositionOfPreviousResource = 0 - return .success(resources.map { pageCount, link -> [Locator] in - guard pageCount > 0 else { - return [] + var lastPositionOfPreviousResource = 0 + return .success(resources.map { pageCount, link -> [Locator] in + guard pageCount > 0 else { + return [] + } + let positionList = LCPDFPositionsService.makePositionList(of: link, pageCount: pageCount, totalPageCount: totalPageCount, startPosition: lastPositionOfPreviousResource) + lastPositionOfPreviousResource += pageCount + return positionList + }) } - let positionList = makePositionList(of: link, pageCount: pageCount, totalPageCount: totalPageCount, startPosition: lastPositionOfPreviousResource) - lastPositionOfPreviousResource += pageCount - return positionList - }) + + task = newTask + return newTask + } } - private func makePositionList(of link: Link, pageCount: Int, totalPageCount: Int, startPosition: Int = 0) -> [Locator] { + private let cache = Cache() + + private static func makePositionList(of link: Link, pageCount: Int, totalPageCount: Int, startPosition: Int = 0) -> [Locator] { assert(pageCount > 0, "Invalid PDF page count") assert(totalPageCount > 0, "Invalid PDF total page count") diff --git a/Sources/Streamer/Parser/PDF/Services/LCPDFTableOfContentsService.swift b/Sources/Streamer/Parser/PDF/Services/LCPDFTableOfContentsService.swift index 5e90a850f9..51e6ff9abe 100644 --- a/Sources/Streamer/Parser/PDF/Services/LCPDFTableOfContentsService.swift +++ b/Sources/Streamer/Parser/PDF/Services/LCPDFTableOfContentsService.swift @@ -27,27 +27,50 @@ final class LCPDFTableOfContentsService: TableOfContentsService, PDFPublicationS } func tableOfContents() async -> ReadResult<[Link]> { - await tableOfContentsTask.value + await cache.getOrMakeTask( + manifest: manifest, + container: container, + pdfFactory: pdfFactory + ).value } - private lazy var tableOfContentsTask: Task, Never> = Task { - guard - manifest.tableOfContents.isEmpty, - manifest.readingOrder.count == 1, - let url = manifest.readingOrder.first?.url(), - let resource = container[url] - else { - return .success(manifest.tableOfContents) - } + private actor Cache { + var task: Task, Never>? + + func getOrMakeTask( + manifest: Manifest, + container: Container, + pdfFactory: PDFDocumentFactory + ) -> Task, Never> { + if let task = task { + return task + } + + let newTask = Task, Never> { + guard + manifest.tableOfContents.isEmpty, + manifest.readingOrder.count == 1, + let url = manifest.readingOrder.first?.url(), + let resource = container[url] + else { + return .success(manifest.tableOfContents) + } - do { - let toc = try await pdfFactory.open(resource: resource, at: url, password: nil).tableOfContents() - return .success(toc.linksWithDocumentHREF(url)) - } catch { - return .failure(.wrap(error) ?? .decoding(error)) + do { + let toc = try await pdfFactory.open(resource: resource, at: url, password: nil).tableOfContents() + return .success(toc.linksWithDocumentHREF(url)) + } catch { + return .failure(.wrap(error) ?? .decoding(error)) + } + } + + task = newTask + return newTask } } + private let cache = Cache() + static func makeFactory(pdfFactory: PDFDocumentFactory) -> (PublicationServiceContext) -> LCPDFTableOfContentsService? { { context in LCPDFTableOfContentsService( diff --git a/Sources/Streamer/Parser/PDF/Services/PDFPositionsService.swift b/Sources/Streamer/Parser/PDF/Services/PDFPositionsService.swift index f4a3fc3b03..78895f484c 100644 --- a/Sources/Streamer/Parser/PDF/Services/PDFPositionsService.swift +++ b/Sources/Streamer/Parser/PDF/Services/PDFPositionsService.swift @@ -7,7 +7,7 @@ import Foundation import ReadiumShared -final class PDFPositionsService: PositionsService { +final class PDFPositionsService: PositionsService, Sendable { init(link: Link, pageCount: Int, tableOfContents: [Link]) { assert(pageCount > 0, "Invalid PDF page count") // FIXME: Use the `tableOfContents` to generate the titles @@ -35,7 +35,7 @@ final class PDFPositionsService: PositionsService { .success(_positionsByReadingOrder) } - static func makeFactory() -> (PublicationServiceContext) -> PDFPositionsService? { + static func makeFactory() -> @Sendable (PublicationServiceContext) -> PDFPositionsService? { { context in guard let link = context.manifest.readingOrder.first, From f3a5e12bbc29ad08f1afb1b0af1e5e2e6379b2ea Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:45:27 -0500 Subject: [PATCH 5/6] Make AudioLocatorService Sendable --- .../Audio/Services/AudioLocatorService.swift | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift b/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift index 5fea158fe0..9e08a59ac1 100644 --- a/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift +++ b/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift @@ -13,26 +13,51 @@ final class AudioLocatorService: DefaultLocatorService { { context in AudioLocatorService(publication: context.publication) } } - private lazy var readingOrder: [Link] = - publication()?.readingOrder ?? [] + private actor Cache { + let readingOrder: [Link] + let durations: [Double] + let totalDuration: Double? - /// Duration per reading order index. - private lazy var durations: [Double] = - readingOrder.map { $0.duration ?? 0 } + init(publication: Publication?) { + readingOrder = publication?.readingOrder ?? [] + durations = readingOrder.map { $0.duration ?? 0 } + let total = durations.reduce(0, +) + totalDuration = (total > 0) ? total : nil + } + + func readingOrderItemAtPosition(_ position: Double) -> (link: Link, startPosition: Double)? { + var current: Double = 0 + for (i, duration) in durations.enumerated() { + let link = readingOrder[i] + if current ..< current + duration ~= position { + return (link, startPosition: current) + } + + current += duration + } + + if position == totalDuration, let link = readingOrder.last { + return (link, startPosition: current - (link.duration ?? 0)) + } + + return nil + } + } - /// Total duration of the publication. - private lazy var totalDuration: Double? = { - let totalDuration = durations.reduce(0, +) - return (totalDuration > 0) ? totalDuration : nil - }() + private let cache: Cache + + override init(publication: Weak) { + cache = Cache(publication: publication()) + super.init(publication: publication) + } override func locate(progression: Double) async -> Locator? { - guard let totalDuration = totalDuration else { + guard let totalDuration = cache.totalDuration else { return nil } let positionInPublication = progression * totalDuration - guard let (link, resourcePosition) = readingOrderItemAtPosition(positionInPublication) else { + guard let (link, resourcePosition) = await cache.readingOrderItemAtPosition(positionInPublication) else { return nil } @@ -54,24 +79,4 @@ final class AudioLocatorService: DefaultLocatorService { ) ) } - - /// Finds the reading order item containing the time `position` (in seconds), as well as its - /// start time. - private func readingOrderItemAtPosition(_ position: Double) -> (link: Link, startPosition: Double)? { - var current: Double = 0 - for (i, duration) in durations.enumerated() { - let link = readingOrder[i] - if current ..< current + duration ~= position { - return (link, startPosition: current) - } - - current += duration - } - - if position == totalDuration, let link = readingOrder.last { - return (link, startPosition: current - (link.duration ?? 0)) - } - - return nil - } } From 13ce17b6f1a261102ebe2d8f6fa134641053ee83 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:50:45 -0500 Subject: [PATCH 6/6] Add back comments to AudioLocatorService --- .../Parser/Audio/Services/AudioLocatorService.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift b/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift index 9e08a59ac1..bee18740b5 100644 --- a/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift +++ b/Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift @@ -15,7 +15,11 @@ final class AudioLocatorService: DefaultLocatorService { private actor Cache { let readingOrder: [Link] + + /// Duration per reading order index. let durations: [Double] + + /// Total duration of the publication. let totalDuration: Double? init(publication: Publication?) { @@ -25,6 +29,8 @@ final class AudioLocatorService: DefaultLocatorService { totalDuration = (total > 0) ? total : nil } + /// Finds the reading order item containing the time `position` (in seconds), as well as its + /// start time. func readingOrderItemAtPosition(_ position: Double) -> (link: Link, startPosition: Double)? { var current: Double = 0 for (i, duration) in durations.enumerated() {