1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 import Foundation 4 import ABI49_0_0EXManifests 5 6 func assertType<T>(value: Any, description: String) -> T { 7 if !(value is T) { 8 let exception = NSException( 9 name: NSExceptionName.internalInconsistencyException, 10 reason: description, 11 userInfo: [:] 12 ) 13 exception.raise() 14 } 15 16 // exception above will preempt force_cast 17 // swiftlint:disable:next force_cast 18 return value as! T 19 } 20 21 public extension Optional { 22 func require(_ desc: String) -> Wrapped { 23 if self == nil { 24 let exception = NSException( 25 name: NSExceptionName.internalInconsistencyException, 26 reason: desc, 27 userInfo: [:] 28 ) 29 exception.raise() 30 } 31 32 // exception above will preempt force_unwrapping 33 // swiftlint:disable:next force_unwrapping 34 return self! 35 } 36 } 37 38 /** 39 * Download status that indicates whether or under what conditions an 40 * update is able to be launched. 41 * 42 * It's important that the integer value of each status stays constant across 43 * all versions of this library since they are stored in SQLite on user devices. 44 */ 45 @objc(ABI49_0_0EXUpdatesUpdateStatus) 46 public enum UpdateStatus: Int { 47 case Status0_Unused = 0 48 /** 49 * The update has been fully downloaded and is ready to launch. 50 */ 51 case StatusReady = 1 52 case Status2_Unused = 2 53 /** 54 * The update manifest has been download from the server but not all 55 * assets have finished downloading successfully. 56 */ 57 case StatusPending = 3 58 case Status4_Unused = 4 59 /** 60 * The update has been partially loaded (copied) from its location 61 * embedded in the app bundle, but not all assets have been copied 62 * successfully. The update may be able to be launched directly from 63 * its embedded location unless a new binary version with a new 64 * embedded update has been installed. 65 */ 66 case StatusEmbedded = 5 67 /** 68 * The update manifest has been downloaded and indicates that the 69 * update is being served from a developer tool. It can be launched by a 70 * host application that can run a development bundle. 71 */ 72 case StatusDevelopment = 6 73 } 74 75 @objc(ABI49_0_0EXUpdatesUpdateError) 76 public enum UpdateError: Int, Error { 77 case invalidExpoProtocolVersion 78 } 79 80 @objc(ABI49_0_0EXUpdatesUpdate) 81 @objcMembers 82 public class Update: NSObject { 83 public let updateId: UUID 84 public let scopeKey: String? 85 public var commitTime: Date 86 public let runtimeVersion: String 87 public let keep: Bool 88 public let isDevelopmentMode: Bool 89 private let assetsFromManifest: [UpdateAsset]? 90 91 public let manifest: Manifest 92 93 public var status: UpdateStatus 94 public var lastAccessed: Date 95 public var successfulLaunchCount: Int 96 public var failedLaunchCount: Int 97 98 private let config: UpdatesConfig 99 private let database: UpdatesDatabase? 100 101 public init( 102 manifest: Manifest, 103 config: UpdatesConfig, 104 database: UpdatesDatabase?, 105 updateId: UUID, 106 scopeKey: String?, 107 commitTime: Date, 108 runtimeVersion: String, 109 keep: Bool, 110 status: UpdateStatus, 111 isDevelopmentMode: Bool, 112 assetsFromManifest: [UpdateAsset]? 113 ) { 114 self.updateId = updateId 115 self.commitTime = commitTime 116 self.runtimeVersion = runtimeVersion 117 self.keep = keep 118 self.manifest = manifest 119 self.config = config 120 self.database = database 121 self.scopeKey = scopeKey 122 self.status = status 123 self.assetsFromManifest = assetsFromManifest 124 125 self.lastAccessed = Date() 126 self.successfulLaunchCount = 0 127 self.failedLaunchCount = 0 128 self.isDevelopmentMode = isDevelopmentMode 129 } 130 131 internal static func update( 132 withManifest: [String: Any], 133 responseHeaderData: ResponseHeaderData, 134 extensions: [String: Any], 135 config: UpdatesConfig, 136 database: UpdatesDatabase 137 ) throws -> Update { 138 let protocolVersion = responseHeaderData.protocolVersion 139 switch protocolVersion { 140 case nil: 141 return LegacyUpdate.update( 142 withLegacyManifest: LegacyManifest(rawManifestJSON: withManifest), 143 config: config, 144 database: database 145 ) 146 case 0, 1: 147 return NewUpdate.update( 148 withNewManifest: NewManifest(rawManifestJSON: withManifest), 149 extensions: extensions, 150 config: config, 151 database: database 152 ) 153 default: 154 throw UpdateError.invalidExpoProtocolVersion 155 } 156 } 157 158 public static func update( 159 withEmbeddedManifest: [String: Any], 160 config: UpdatesConfig, 161 database: UpdatesDatabase? 162 ) -> Update { 163 if withEmbeddedManifest["releaseId"] != nil { 164 return LegacyUpdate.update( 165 withLegacyManifest: LegacyManifest(rawManifestJSON: withEmbeddedManifest), 166 config: config, 167 database: database 168 ) 169 } else { 170 return BareUpdate.update( 171 withBareManifest: BareManifest(rawManifestJSON: withEmbeddedManifest), 172 config: config, 173 database: database 174 ) 175 } 176 } 177 178 /** 179 * Accessing this property may lazily load the assets from the database, if this update object 180 * originated from the database. 181 */ 182 public func assets() -> [UpdateAsset]? { 183 guard let assetsFromManifest = self.assetsFromManifest else { 184 return self.assetsFromDatabase() 185 } 186 return assetsFromManifest 187 } 188 189 private func assetsFromDatabase() -> [UpdateAsset]? { 190 guard let database = self.database else { 191 return nil 192 } 193 194 var assetsLocal: [UpdateAsset] = [] 195 database.databaseQueue.sync { 196 // The pattern is valid, so it'll never throw 197 // swiftlint:disable:next force_try 198 assetsLocal = try! database.assets(withUpdateId: self.updateId) 199 } 200 return assetsLocal 201 } 202 203 public func loggingId() -> String { 204 self.updateId.uuidString.lowercased() 205 } 206 } 207