├── .gitignore ├── Jobs.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Jobs.xcscheme ├── Jobs ├── Info.plist └── Jobs.h ├── LICENSE ├── Package.swift └── Sources ├── BlockObserver.swift ├── Composite.swift ├── Condition.swift ├── Delay.swift ├── Dependency.swift ├── Dispatch.swift ├── Errors.swift ├── Job+Group.swift ├── Job.swift ├── JobContext.swift ├── JobQueue.swift ├── JobQueueType.swift ├── JobState.swift ├── JobType.swift ├── Lock.swift ├── MutuallyExclusive.swift ├── Observer.swift ├── Ticket.swift └── Timeout.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /Jobs.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 044D3F411D39938800332A8C /* Jobs.h in Headers */ = {isa = PBXBuildFile; fileRef = 044D3F3F1D39938800332A8C /* Jobs.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 044D3F501D3993E000332A8C /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F471D3993E000332A8C /* Job.swift */; }; 12 | 044D3F511D3993E000332A8C /* BlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F481D3993E000332A8C /* BlockObserver.swift */; }; 13 | 044D3F521D3993E000332A8C /* Composite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F491D3993E000332A8C /* Composite.swift */; }; 14 | 044D3F531D3993E000332A8C /* Condition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F4A1D3993E000332A8C /* Condition.swift */; }; 15 | 044D3F541D3993E000332A8C /* Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F4B1D3993E000332A8C /* Delay.swift */; }; 16 | 044D3F551D3993E000332A8C /* Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F4C1D3993E000332A8C /* Dependency.swift */; }; 17 | 044D3F561D3993E000332A8C /* Dispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F4D1D3993E000332A8C /* Dispatch.swift */; }; 18 | 044D3F571D3993E000332A8C /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F4E1D3993E000332A8C /* Errors.swift */; }; 19 | 044D3F581D3993E000332A8C /* Job+Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F4F1D3993E000332A8C /* Job+Group.swift */; }; 20 | 044D3F5C1D3993EE00332A8C /* JobContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F591D3993EE00332A8C /* JobContext.swift */; }; 21 | 044D3F5D1D3993EE00332A8C /* JobQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F5A1D3993EE00332A8C /* JobQueue.swift */; }; 22 | 044D3F5E1D3993EE00332A8C /* JobQueueType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F5B1D3993EE00332A8C /* JobQueueType.swift */; }; 23 | 044D3F661D3993F600332A8C /* JobState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F5F1D3993F600332A8C /* JobState.swift */; }; 24 | 044D3F681D3993F600332A8C /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F611D3993F600332A8C /* Lock.swift */; }; 25 | 044D3F691D3993F600332A8C /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F621D3993F600332A8C /* MutuallyExclusive.swift */; }; 26 | 044D3F6A1D3993F600332A8C /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F631D3993F600332A8C /* Observer.swift */; }; 27 | 044D3F6B1D3993F600332A8C /* Ticket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F641D3993F600332A8C /* Ticket.swift */; }; 28 | 044D3F6C1D3993F600332A8C /* Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D3F651D3993F600332A8C /* Timeout.swift */; }; 29 | 6183573B1E03921700A52FDC /* JobType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6183573A1E03921700A52FDC /* JobType.swift */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 044D3F3C1D39938800332A8C /* Jobs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Jobs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 044D3F3F1D39938800332A8C /* Jobs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Jobs.h; path = ../Jobs/Jobs.h; sourceTree = ""; }; 35 | 044D3F401D39938800332A8C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Jobs/Info.plist; sourceTree = ""; }; 36 | 044D3F471D3993E000332A8C /* Job.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Job.swift; sourceTree = ""; }; 37 | 044D3F481D3993E000332A8C /* BlockObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockObserver.swift; sourceTree = ""; }; 38 | 044D3F491D3993E000332A8C /* Composite.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Composite.swift; sourceTree = ""; }; 39 | 044D3F4A1D3993E000332A8C /* Condition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Condition.swift; path = ../Sources/Condition.swift; sourceTree = ""; }; 40 | 044D3F4B1D3993E000332A8C /* Delay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Delay.swift; sourceTree = ""; }; 41 | 044D3F4C1D3993E000332A8C /* Dependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dependency.swift; sourceTree = ""; }; 42 | 044D3F4D1D3993E000332A8C /* Dispatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatch.swift; sourceTree = ""; }; 43 | 044D3F4E1D3993E000332A8C /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 44 | 044D3F4F1D3993E000332A8C /* Job+Group.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Job+Group.swift"; sourceTree = ""; }; 45 | 044D3F591D3993EE00332A8C /* JobContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JobContext.swift; path = ../Sources/JobContext.swift; sourceTree = ""; }; 46 | 044D3F5A1D3993EE00332A8C /* JobQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JobQueue.swift; path = ../Sources/JobQueue.swift; sourceTree = ""; }; 47 | 044D3F5B1D3993EE00332A8C /* JobQueueType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JobQueueType.swift; path = ../Sources/JobQueueType.swift; sourceTree = ""; }; 48 | 044D3F5F1D3993F600332A8C /* JobState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JobState.swift; path = ../Sources/JobState.swift; sourceTree = ""; }; 49 | 044D3F611D3993F600332A8C /* Lock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; 50 | 044D3F621D3993F600332A8C /* MutuallyExclusive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutuallyExclusive.swift; sourceTree = ""; }; 51 | 044D3F631D3993F600332A8C /* Observer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Observer.swift; path = ../Sources/Observer.swift; sourceTree = ""; }; 52 | 044D3F641D3993F600332A8C /* Ticket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Ticket.swift; path = ../Sources/Ticket.swift; sourceTree = ""; }; 53 | 044D3F651D3993F600332A8C /* Timeout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Timeout.swift; sourceTree = ""; }; 54 | 6183573A1E03921700A52FDC /* JobType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JobType.swift; path = Sources/JobType.swift; sourceTree = SOURCE_ROOT; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 044D3F381D39938800332A8C /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXFrameworksBuildPhase section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 044D3F321D39938800332A8C = { 69 | isa = PBXGroup; 70 | children = ( 71 | 044D3F3E1D39938800332A8C /* Jobs */, 72 | 044D3F3D1D39938800332A8C /* Products */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 044D3F3D1D39938800332A8C /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 044D3F3C1D39938800332A8C /* Jobs.framework */, 80 | ); 81 | name = Products; 82 | sourceTree = ""; 83 | }; 84 | 044D3F3E1D39938800332A8C /* Jobs */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 6183573A1E03921700A52FDC /* JobType.swift */, 88 | 044D3F5B1D3993EE00332A8C /* JobQueueType.swift */, 89 | 044D3F5A1D3993EE00332A8C /* JobQueue.swift */, 90 | 044D3F591D3993EE00332A8C /* JobContext.swift */, 91 | 044D3F5F1D3993F600332A8C /* JobState.swift */, 92 | 044D3F641D3993F600332A8C /* Ticket.swift */, 93 | 044D3F631D3993F600332A8C /* Observer.swift */, 94 | 044D3F4A1D3993E000332A8C /* Condition.swift */, 95 | 04EBCACF1D4243FD00D4C3A7 /* Conditions */, 96 | 04EBCAD01D42441100D4C3A7 /* Observers */, 97 | 04EBCAD11D42444A00D4C3A7 /* JobTypes */, 98 | 04EBCAD31D4244C500D4C3A7 /* Helpers */, 99 | 04EBCAD21D42448C00D4C3A7 /* Supporting Files */, 100 | ); 101 | name = Jobs; 102 | path = Sources; 103 | sourceTree = ""; 104 | }; 105 | 04EBCACF1D4243FD00D4C3A7 /* Conditions */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 044D3F4C1D3993E000332A8C /* Dependency.swift */, 109 | 044D3F491D3993E000332A8C /* Composite.swift */, 110 | 044D3F621D3993F600332A8C /* MutuallyExclusive.swift */, 111 | ); 112 | name = Conditions; 113 | sourceTree = ""; 114 | }; 115 | 04EBCAD01D42441100D4C3A7 /* Observers */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 044D3F481D3993E000332A8C /* BlockObserver.swift */, 119 | 044D3F651D3993F600332A8C /* Timeout.swift */, 120 | ); 121 | name = Observers; 122 | sourceTree = ""; 123 | }; 124 | 04EBCAD11D42444A00D4C3A7 /* JobTypes */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 044D3F471D3993E000332A8C /* Job.swift */, 128 | 044D3F4B1D3993E000332A8C /* Delay.swift */, 129 | 044D3F4F1D3993E000332A8C /* Job+Group.swift */, 130 | ); 131 | name = JobTypes; 132 | sourceTree = ""; 133 | }; 134 | 04EBCAD21D42448C00D4C3A7 /* Supporting Files */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 044D3F3F1D39938800332A8C /* Jobs.h */, 138 | 044D3F401D39938800332A8C /* Info.plist */, 139 | ); 140 | name = "Supporting Files"; 141 | sourceTree = ""; 142 | }; 143 | 04EBCAD31D4244C500D4C3A7 /* Helpers */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 044D3F4D1D3993E000332A8C /* Dispatch.swift */, 147 | 044D3F4E1D3993E000332A8C /* Errors.swift */, 148 | 044D3F611D3993F600332A8C /* Lock.swift */, 149 | ); 150 | name = Helpers; 151 | sourceTree = ""; 152 | }; 153 | /* End PBXGroup section */ 154 | 155 | /* Begin PBXHeadersBuildPhase section */ 156 | 044D3F391D39938800332A8C /* Headers */ = { 157 | isa = PBXHeadersBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | 044D3F411D39938800332A8C /* Jobs.h in Headers */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXHeadersBuildPhase section */ 165 | 166 | /* Begin PBXNativeTarget section */ 167 | 044D3F3B1D39938800332A8C /* Jobs */ = { 168 | isa = PBXNativeTarget; 169 | buildConfigurationList = 044D3F441D39938800332A8C /* Build configuration list for PBXNativeTarget "Jobs" */; 170 | buildPhases = ( 171 | 044D3F371D39938800332A8C /* Sources */, 172 | 044D3F381D39938800332A8C /* Frameworks */, 173 | 044D3F391D39938800332A8C /* Headers */, 174 | 044D3F3A1D39938800332A8C /* Resources */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | ); 180 | name = Jobs; 181 | productName = Jobs; 182 | productReference = 044D3F3C1D39938800332A8C /* Jobs.framework */; 183 | productType = "com.apple.product-type.framework"; 184 | }; 185 | /* End PBXNativeTarget section */ 186 | 187 | /* Begin PBXProject section */ 188 | 044D3F331D39938800332A8C /* Project object */ = { 189 | isa = PBXProject; 190 | attributes = { 191 | LastUpgradeCheck = 0810; 192 | ORGANIZATIONNAME = "Utah Developers"; 193 | TargetAttributes = { 194 | 044D3F3B1D39938800332A8C = { 195 | CreatedOnToolsVersion = 8.0; 196 | DevelopmentTeam = 5TSJ5VWTTU; 197 | DevelopmentTeamName = "Pluralsight, LLC"; 198 | LastSwiftMigration = 0800; 199 | ProvisioningStyle = Manual; 200 | }; 201 | }; 202 | }; 203 | buildConfigurationList = 044D3F361D39938800332A8C /* Build configuration list for PBXProject "Jobs" */; 204 | compatibilityVersion = "Xcode 3.2"; 205 | developmentRegion = English; 206 | hasScannedForEncodings = 0; 207 | knownRegions = ( 208 | en, 209 | ); 210 | mainGroup = 044D3F321D39938800332A8C; 211 | productRefGroup = 044D3F3D1D39938800332A8C /* Products */; 212 | projectDirPath = ""; 213 | projectRoot = ""; 214 | targets = ( 215 | 044D3F3B1D39938800332A8C /* Jobs */, 216 | ); 217 | }; 218 | /* End PBXProject section */ 219 | 220 | /* Begin PBXResourcesBuildPhase section */ 221 | 044D3F3A1D39938800332A8C /* Resources */ = { 222 | isa = PBXResourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXResourcesBuildPhase section */ 229 | 230 | /* Begin PBXSourcesBuildPhase section */ 231 | 044D3F371D39938800332A8C /* Sources */ = { 232 | isa = PBXSourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | 044D3F551D3993E000332A8C /* Dependency.swift in Sources */, 236 | 044D3F681D3993F600332A8C /* Lock.swift in Sources */, 237 | 044D3F501D3993E000332A8C /* Job.swift in Sources */, 238 | 044D3F531D3993E000332A8C /* Condition.swift in Sources */, 239 | 044D3F521D3993E000332A8C /* Composite.swift in Sources */, 240 | 044D3F6A1D3993F600332A8C /* Observer.swift in Sources */, 241 | 044D3F571D3993E000332A8C /* Errors.swift in Sources */, 242 | 044D3F691D3993F600332A8C /* MutuallyExclusive.swift in Sources */, 243 | 044D3F511D3993E000332A8C /* BlockObserver.swift in Sources */, 244 | 044D3F6C1D3993F600332A8C /* Timeout.swift in Sources */, 245 | 044D3F541D3993E000332A8C /* Delay.swift in Sources */, 246 | 044D3F661D3993F600332A8C /* JobState.swift in Sources */, 247 | 6183573B1E03921700A52FDC /* JobType.swift in Sources */, 248 | 044D3F5C1D3993EE00332A8C /* JobContext.swift in Sources */, 249 | 044D3F6B1D3993F600332A8C /* Ticket.swift in Sources */, 250 | 044D3F581D3993E000332A8C /* Job+Group.swift in Sources */, 251 | 044D3F5D1D3993EE00332A8C /* JobQueue.swift in Sources */, 252 | 044D3F561D3993E000332A8C /* Dispatch.swift in Sources */, 253 | 044D3F5E1D3993EE00332A8C /* JobQueueType.swift in Sources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXSourcesBuildPhase section */ 258 | 259 | /* Begin XCBuildConfiguration section */ 260 | 044D3F421D39938800332A8C /* Debug */ = { 261 | isa = XCBuildConfiguration; 262 | buildSettings = { 263 | ALWAYS_SEARCH_USER_PATHS = NO; 264 | CLANG_ANALYZER_NONNULL = YES; 265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 266 | CLANG_CXX_LIBRARY = "libc++"; 267 | CLANG_ENABLE_MODULES = YES; 268 | CLANG_ENABLE_OBJC_ARC = YES; 269 | CLANG_WARN_BOOL_CONVERSION = YES; 270 | CLANG_WARN_CONSTANT_CONVERSION = YES; 271 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 272 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 273 | CLANG_WARN_EMPTY_BODY = YES; 274 | CLANG_WARN_ENUM_CONVERSION = YES; 275 | CLANG_WARN_INFINITE_RECURSION = YES; 276 | CLANG_WARN_INT_CONVERSION = YES; 277 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 278 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 279 | CLANG_WARN_UNREACHABLE_CODE = YES; 280 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 281 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 282 | COPY_PHASE_STRIP = NO; 283 | CURRENT_PROJECT_VERSION = 1; 284 | DEBUG_INFORMATION_FORMAT = dwarf; 285 | ENABLE_STRICT_OBJC_MSGSEND = YES; 286 | ENABLE_TESTABILITY = YES; 287 | GCC_C_LANGUAGE_STANDARD = gnu99; 288 | GCC_DYNAMIC_NO_PIC = NO; 289 | GCC_NO_COMMON_BLOCKS = YES; 290 | GCC_OPTIMIZATION_LEVEL = 0; 291 | GCC_PREPROCESSOR_DEFINITIONS = ( 292 | "DEBUG=1", 293 | "$(inherited)", 294 | ); 295 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 296 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 297 | GCC_WARN_UNDECLARED_SELECTOR = YES; 298 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 299 | GCC_WARN_UNUSED_FUNCTION = YES; 300 | GCC_WARN_UNUSED_VARIABLE = YES; 301 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 302 | MTL_ENABLE_DEBUG_INFO = YES; 303 | ONLY_ACTIVE_ARCH = YES; 304 | SDKROOT = iphoneos; 305 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 306 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 307 | TARGETED_DEVICE_FAMILY = "1,2"; 308 | VERSIONING_SYSTEM = "apple-generic"; 309 | VERSION_INFO_PREFIX = ""; 310 | }; 311 | name = Debug; 312 | }; 313 | 044D3F431D39938800332A8C /* Release */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | ALWAYS_SEARCH_USER_PATHS = NO; 317 | CLANG_ANALYZER_NONNULL = YES; 318 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 319 | CLANG_CXX_LIBRARY = "libc++"; 320 | CLANG_ENABLE_MODULES = YES; 321 | CLANG_ENABLE_OBJC_ARC = YES; 322 | CLANG_WARN_BOOL_CONVERSION = YES; 323 | CLANG_WARN_CONSTANT_CONVERSION = YES; 324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 325 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 326 | CLANG_WARN_EMPTY_BODY = YES; 327 | CLANG_WARN_ENUM_CONVERSION = YES; 328 | CLANG_WARN_INFINITE_RECURSION = YES; 329 | CLANG_WARN_INT_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 332 | CLANG_WARN_UNREACHABLE_CODE = YES; 333 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 334 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 335 | COPY_PHASE_STRIP = NO; 336 | CURRENT_PROJECT_VERSION = 1; 337 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 338 | ENABLE_NS_ASSERTIONS = NO; 339 | ENABLE_STRICT_OBJC_MSGSEND = YES; 340 | GCC_C_LANGUAGE_STANDARD = gnu99; 341 | GCC_NO_COMMON_BLOCKS = YES; 342 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 343 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 344 | GCC_WARN_UNDECLARED_SELECTOR = YES; 345 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 346 | GCC_WARN_UNUSED_FUNCTION = YES; 347 | GCC_WARN_UNUSED_VARIABLE = YES; 348 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 349 | MTL_ENABLE_DEBUG_INFO = NO; 350 | SDKROOT = iphoneos; 351 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 352 | TARGETED_DEVICE_FAMILY = "1,2"; 353 | VALIDATE_PRODUCT = YES; 354 | VERSIONING_SYSTEM = "apple-generic"; 355 | VERSION_INFO_PREFIX = ""; 356 | }; 357 | name = Release; 358 | }; 359 | 044D3F451D39938800332A8C /* Debug */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | APPLICATION_EXTENSION_API_ONLY = YES; 363 | CLANG_ENABLE_MODULES = YES; 364 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 365 | DEFINES_MODULE = YES; 366 | DYLIB_COMPATIBILITY_VERSION = 1; 367 | DYLIB_CURRENT_VERSION = 1; 368 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 369 | INFOPLIST_FILE = Jobs/Info.plist; 370 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 371 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 372 | PRODUCT_BUNDLE_IDENTIFIER = org.utahdevelopers.jobs; 373 | PRODUCT_NAME = "$(TARGET_NAME)"; 374 | SKIP_INSTALL = YES; 375 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvsimulator appletvos watchsimulator watchos macosx"; 376 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 377 | SWIFT_VERSION = 3.0.1; 378 | }; 379 | name = Debug; 380 | }; 381 | 044D3F461D39938800332A8C /* Release */ = { 382 | isa = XCBuildConfiguration; 383 | buildSettings = { 384 | APPLICATION_EXTENSION_API_ONLY = YES; 385 | CLANG_ENABLE_MODULES = YES; 386 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 387 | DEFINES_MODULE = YES; 388 | DYLIB_COMPATIBILITY_VERSION = 1; 389 | DYLIB_CURRENT_VERSION = 1; 390 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 391 | INFOPLIST_FILE = Jobs/Info.plist; 392 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 393 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 394 | PRODUCT_BUNDLE_IDENTIFIER = org.utahdevelopers.jobs; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | SKIP_INSTALL = YES; 397 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvsimulator appletvos watchsimulator watchos macosx"; 398 | SWIFT_VERSION = 3.0.1; 399 | }; 400 | name = Release; 401 | }; 402 | /* End XCBuildConfiguration section */ 403 | 404 | /* Begin XCConfigurationList section */ 405 | 044D3F361D39938800332A8C /* Build configuration list for PBXProject "Jobs" */ = { 406 | isa = XCConfigurationList; 407 | buildConfigurations = ( 408 | 044D3F421D39938800332A8C /* Debug */, 409 | 044D3F431D39938800332A8C /* Release */, 410 | ); 411 | defaultConfigurationIsVisible = 0; 412 | defaultConfigurationName = Release; 413 | }; 414 | 044D3F441D39938800332A8C /* Build configuration list for PBXNativeTarget "Jobs" */ = { 415 | isa = XCConfigurationList; 416 | buildConfigurations = ( 417 | 044D3F451D39938800332A8C /* Debug */, 418 | 044D3F461D39938800332A8C /* Release */, 419 | ); 420 | defaultConfigurationIsVisible = 0; 421 | defaultConfigurationName = Release; 422 | }; 423 | /* End XCConfigurationList section */ 424 | }; 425 | rootObject = 044D3F331D39938800332A8C /* Project object */; 426 | } 427 | -------------------------------------------------------------------------------- /Jobs.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Jobs.xcodeproj/xcshareddata/xcschemes/Jobs.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Jobs/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Jobs/Jobs.h: -------------------------------------------------------------------------------- 1 | // 2 | // Jobs.h 3 | // Jobs 4 | // 5 | 6 | #import 7 | 8 | //! Project version number for Jobs. 9 | FOUNDATION_EXPORT double JobsVersionNumber; 10 | 11 | //! Project version string for Jobs. 12 | FOUNDATION_EXPORT const unsigned char JobsVersionString[]; 13 | 14 | // In this header, you should import all the public headers of your framework using statements like #import 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Utah iOS and Mac Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "Jobs" 5 | ) 6 | -------------------------------------------------------------------------------- /Sources/BlockObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockObserver.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public struct BlockObserver: Observer { 9 | internal let onStart: (() -> Void)? 10 | internal let onCancel: (() -> Void)? 11 | internal let onFinish: (([NSError]) -> Void)? 12 | internal let onProduce: ((Ticket) -> Void)? 13 | 14 | public init(onStart: (() -> Void)? = nil, onCancel: (() -> Void)? = nil, onFinish: (([NSError]) -> Void)? = nil, onProduce: ((Ticket) -> Void)? = nil) { 15 | self.onStart = onStart 16 | self.onCancel = onCancel 17 | self.onFinish = onFinish 18 | self.onProduce = onProduce 19 | } 20 | 21 | public func jobDidStart(job: Ticket) { 22 | onStart?() 23 | } 24 | 25 | public func jobDidCancel(job: Ticket) { 26 | onCancel?() 27 | } 28 | 29 | public func job(job: Ticket, didFinishWithErrors errors: [NSError]) { 30 | onFinish?(errors) 31 | } 32 | 33 | public func job(job: Ticket, didProduce newJob: Ticket) { 34 | onProduce?(job) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Composite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Composite.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Composite: Condition { 9 | public enum Requirement { 10 | case all 11 | case any 12 | } 13 | private let conditions: [Condition] 14 | private let requirement: Requirement 15 | 16 | public init(requirement: Requirement = .all, conditions: Condition ...) { 17 | self.requirement = requirement 18 | self.conditions = conditions 19 | } 20 | 21 | public func evaluate(ticket: Ticket, completion: @escaping (NSError?) -> Void) { 22 | let group = DispatchGroup() 23 | let queue = DispatchQueue(label: "CompositeCondition", attributes: [], target: DispatchQueue.global()) 24 | 25 | let conditionCount = conditions.count 26 | 27 | var results = Array(repeating: nil, count: conditionCount) 28 | for (index, condition) in conditions.enumerated() { 29 | group.enter() 30 | condition.evaluate(ticket: ticket) { error in 31 | results[index] = error 32 | group.leave() 33 | } 34 | } 35 | 36 | let r = requirement 37 | 38 | group.notify(queue: queue) { 39 | // called when all conditions have evaluated 40 | let errors = results.flatMap { $0 } 41 | switch r { 42 | case .all: 43 | let error: NSError? = (errors.count == 0) ? nil : errors[0] 44 | completion(error) 45 | case .any: 46 | let error: NSError? = (errors.count < conditionCount) ? nil : errors[0] 47 | completion(error) 48 | } 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Condition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Condition.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public protocol Condition { 9 | // passing nil to the completion means no error; the condition successfully evaluated 10 | func evaluate(ticket: Ticket, completion: @escaping (NSError?) -> Void) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Delay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delay.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public func Delay(interval: TimeInterval) -> JobType { 9 | var block = Job(block: { context in 10 | var c = context 11 | let q = DispatchQueue.global() 12 | q.after(timeInterval: interval) { 13 | c.finish() 14 | } 15 | c.onCancel { c.finish() } 16 | }) 17 | block.name = "Delay(\(interval))" 18 | return block 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Dependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dependency.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public final class Depend: Condition { 9 | private var dependency: Ticket 10 | 11 | public init(on job: Ticket) { 12 | self.dependency = job 13 | } 14 | 15 | public func evaluate(ticket: Ticket, completion: @escaping (NSError?) -> Void) { 16 | dependency.onFinish { _ in 17 | completion(nil) 18 | } 19 | } 20 | 21 | } 22 | 23 | public extension JobType { 24 | 25 | mutating func depend(on job: Ticket) { 26 | add(condition: Depend(on: job)) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Dispatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dispatch.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | extension DispatchQueue { 9 | 10 | internal func after(timeInterval: TimeInterval, execute: @escaping () -> Void) { 11 | let when = DispatchTime.now() + timeInterval 12 | asyncAfter(deadline: when, execute: execute) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public let JobErrorDomain = "JobErrorDomain" 9 | 10 | public enum JobError: Int { 11 | case cancelled 12 | case timedOut 13 | 14 | public var localizedDescription: String { 15 | switch self { 16 | case .cancelled: return "The job was cancelled" 17 | case .timedOut: return "The job timed out" 18 | } 19 | } 20 | } 21 | 22 | extension NSError { 23 | 24 | public convenience init(jobError: JobError, description: String? = nil, extra: Dictionary = [:]) { 25 | var info: [AnyHashable : Any] = extra 26 | info[NSLocalizedDescriptionKey] = description ?? jobError.localizedDescription 27 | 28 | self.init(domain: JobErrorDomain, code: jobError.rawValue, userInfo: info) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Job+Group.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Job+Group.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public extension Job { 9 | 10 | public static func group(setup: @escaping (JobQueueType) -> Void) -> Job { 11 | return Job(block: { context in 12 | let q = GroupJobQueue() 13 | q.suspended = true 14 | setup(q) 15 | q.onCompletion { 16 | context.finish() 17 | } 18 | q.suspended = false 19 | }) 20 | } 21 | 22 | } 23 | 24 | private class GroupJobQueue: JobQueueType { 25 | 26 | private let wrappedQueue = JobQueue() 27 | private let group = DispatchGroup() 28 | 29 | var suspended: Bool { 30 | get { return wrappedQueue.suspended } 31 | set { wrappedQueue.suspended = newValue } 32 | } 33 | 34 | fileprivate func enqueue(job: JobType) -> Ticket { 35 | let ticket = wrappedQueue.enqueue(job: job) 36 | ticket.add(observer: GroupJobObserver(group: group)) 37 | return ticket 38 | } 39 | 40 | fileprivate func onCompletion(completion: @escaping () -> Void) { 41 | group.notify(queue: DispatchQueue.global(), execute: completion) 42 | } 43 | 44 | } 45 | 46 | private struct GroupJobObserver: Observer { 47 | private let group: DispatchGroup 48 | 49 | init(group: DispatchGroup) { 50 | self.group = group 51 | group.enter() 52 | } 53 | 54 | fileprivate func jobDidStart(job: Ticket) { } 55 | fileprivate func jobDidCancel(job: Ticket) { } 56 | fileprivate func job(job: Ticket, didFinishWithErrors errors: [NSError]) { 57 | group.leave() 58 | } 59 | fileprivate func job(job: Ticket, didProduce newJob: Ticket) { 60 | newJob.add(observer: GroupJobObserver(group: group)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Job.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Job.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Job: JobType { 9 | public var priority = DispatchQoS.default 10 | public let block: JobBlock 11 | public var name: String? = nil 12 | public private(set) var conditions = [Condition]() 13 | public private(set) var observers = [Observer]() 14 | 15 | public mutating func add(condition: Condition) { 16 | conditions.append(condition) 17 | } 18 | public mutating func add(observer: Observer) { 19 | observers.append(observer) 20 | } 21 | 22 | public init(block: @escaping JobBlock) { 23 | self.block = block 24 | } 25 | 26 | public init(mainQueueBlock: @escaping () -> Void) { 27 | self.block = { context in 28 | DispatchQueue.main.async { 29 | mainQueueBlock() 30 | context.finish() 31 | } 32 | } 33 | } 34 | 35 | public init() { 36 | self.block = { context in 37 | context.finish() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/JobContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobContext.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public struct JobContext: Observable { 9 | private let state: JobState 10 | 11 | internal init(state: JobState) { 12 | self.state = state 13 | } 14 | 15 | public var isCancelled: Bool { return state.isCancelled } 16 | 17 | public func add(observer: Observer) { 18 | state.add(observer: observer) 19 | } 20 | 21 | public func cancel(error: NSError? = nil) { 22 | state.cancel(error: error) 23 | } 24 | 25 | public func finish(errors: [NSError] = []) { 26 | state.finish(errors: errors) 27 | } 28 | 29 | public func produce(job: JobType) -> Ticket { 30 | return state.produce(job: job) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/JobQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobQueue.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public final class JobQueue: JobQueueType { 9 | public static let main: JobQueue = JobQueue(targeting: .main) 10 | 11 | private let executionQueue: DispatchQueue 12 | 13 | public init(targeting: DispatchQueue? = nil) { 14 | executionQueue = DispatchQueue(label: "JobQueue", attributes: [], target: targeting) 15 | } 16 | 17 | private var _suspended = false 18 | public var suspended: Bool { 19 | get { return _suspended } 20 | set { 21 | _suspended = newValue 22 | newValue ? executionQueue.suspend() : executionQueue.resume() 23 | } 24 | } 25 | 26 | public func enqueue(job: JobType) -> Ticket { 27 | let production = { (job: JobType) -> Ticket in 28 | return self.enqueue(job: job) 29 | } 30 | let state = JobState(job: job, productionHandler: production) 31 | state.evaluateConditions { errors in 32 | self.job(state, didEvaluateConditionsWithErrors: errors) 33 | } 34 | return state.ticket() 35 | } 36 | 37 | private func job(_ job: JobState, didEvaluateConditionsWithErrors errors: [NSError]) { 38 | guard errors.isEmpty == true else { return } 39 | 40 | // we should be using DispatchWorkItem here (so we can specify priority), 41 | // but DispatchWorkItem has a memory leak 42 | 43 | let block = { job.execute() } 44 | // let work = DispatchWorkItem(group: nil, qos: job.job.priority, flags: [], block: block) 45 | executionQueue.async(execute: block) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/JobQueueType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobQueueType.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public protocol JobQueueType { 9 | 10 | func enqueue(job: JobType) -> Ticket 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Sources/JobState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobState.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | internal final class JobState { 9 | internal let enqueuedDate = Date() 10 | internal let job: JobType 11 | 12 | fileprivate enum State { 13 | case idle 14 | case evaluating 15 | case executing 16 | case finished([NSError]) 17 | 18 | var isFinished: Bool { 19 | if case .finished(_) = self { return true } 20 | return false 21 | } 22 | } 23 | 24 | fileprivate let lock = NSLock() 25 | 26 | // underscored properties must only be accessed within the lock 27 | fileprivate var _state = State.idle 28 | fileprivate var _cancelled = false 29 | fileprivate var _observers = [Observer]() 30 | 31 | internal var jobProductionHandler: (JobType) -> Ticket 32 | internal fileprivate(set) var cancellationError: NSError? 33 | 34 | init(job: JobType, productionHandler: @escaping (JobType) -> Ticket) { 35 | self.job = job 36 | self.jobProductionHandler = productionHandler 37 | add(observers: job.observers) 38 | } 39 | 40 | internal var isCancelled: Bool { return lock.withCriticalScope { _cancelled } } 41 | 42 | internal func add(observer: Observer) { 43 | add(observers: [observer]) 44 | } 45 | 46 | internal func ticket() -> Ticket { 47 | return Ticket(state: self) 48 | } 49 | 50 | } 51 | 52 | extension JobState { /* Actions */ 53 | 54 | internal func evaluateConditions(completion: @escaping ([NSError]) -> Void) { 55 | let shouldEvaluate = lock.withCriticalScope { _ -> Bool in 56 | if _state == .idle { 57 | _state = .evaluating 58 | return true 59 | } 60 | return false 61 | } 62 | 63 | guard shouldEvaluate == true else { 64 | fatalError("Attempting to evaluate conditions on a non-idle JobState") 65 | } 66 | 67 | let group = DispatchGroup() 68 | var results = Array(repeating: nil, count: job.conditions.count) 69 | 70 | let q = DispatchQueue(label: "JobConditions", attributes: [], target: nil) 71 | let ticket = self.ticket() 72 | for (index, condition) in job.conditions.enumerated() { 73 | group.enter() 74 | condition.evaluate(ticket: ticket) { error in 75 | q.async { 76 | results[index] = error 77 | group.leave() 78 | } 79 | } 80 | } 81 | 82 | group.notify(queue: q) { 83 | var errors = results.flatMap { $0 } 84 | 85 | if self.isCancelled { 86 | let error: NSError 87 | if let cancellationError = self.cancellationError { 88 | error = cancellationError 89 | } else { 90 | error = NSError(jobError: .cancelled) 91 | } 92 | // make the cancellation error first, because it must have been done manually 93 | // and manually-done things are more important 94 | errors.insert(error, at: 0) 95 | } 96 | completion(errors) 97 | 98 | if errors.isEmpty == false { 99 | self.finish(errors: errors) 100 | } 101 | } 102 | } 103 | 104 | internal func execute() { 105 | let handlers = lock.withCriticalScope { _ -> [Observer]? in 106 | if _state == .evaluating && _cancelled == false { 107 | _state = .executing 108 | return _observers 109 | } else { 110 | return nil 111 | } 112 | } 113 | 114 | if let handlers = handlers { 115 | let t = ticket() 116 | handlers.forEach { $0.jobDidStart(job: t) } 117 | let c = JobContext(state: self) 118 | job.block(c) 119 | } 120 | } 121 | 122 | internal func finish(errors: [NSError] = []) { 123 | let handlers = lock.withCriticalScope { _ -> [Observer] in 124 | if _state == .executing { 125 | _state = .finished(errors) 126 | let returnValue = _observers 127 | _observers = [] 128 | return returnValue 129 | } 130 | return [] 131 | } 132 | 133 | let t = ticket() 134 | handlers.forEach { $0.job(job: t, didFinishWithErrors: errors) } 135 | } 136 | 137 | internal func cancel(error: NSError? = nil) { 138 | let handlers = lock.withCriticalScope { _ -> [Observer] in 139 | // we can only cancel the job if it's not finished 140 | if _cancelled == false && _state.isFinished == false { 141 | _cancelled = true 142 | cancellationError = error 143 | return _observers 144 | } 145 | return [] 146 | } 147 | 148 | let t = ticket() 149 | handlers.forEach { $0.jobDidCancel(job: t) } 150 | } 151 | 152 | internal func produce(job: JobType) -> Ticket { 153 | let newTicket = jobProductionHandler(job) 154 | let handlers = lock.withCriticalScope { _observers } 155 | 156 | let t = ticket() 157 | handlers.forEach { $0.job(job: t, didProduce: newTicket) } 158 | return newTicket 159 | } 160 | 161 | } 162 | 163 | extension JobState { /* Observation */ 164 | 165 | func add(observers: [Observer]) { 166 | var actions = [() -> Void]() 167 | 168 | let t = ticket() 169 | lock.withCriticalScope { 170 | // we won't add observers to a job that has finished 171 | if _state.isFinished == false { 172 | _observers.append(contentsOf: observers) 173 | } 174 | 175 | if _cancelled == true && _state.isFinished == false { 176 | // we may need to notify of cancellation 177 | actions.append({ 178 | observers.forEach { $0.jobDidCancel(job: t) } 179 | }) 180 | } else if case .finished(let errors) = _state { 181 | // we may need to notify of finishing 182 | actions.append({ 183 | observers.forEach { $0.job(job: t, didFinishWithErrors: errors) } 184 | }) 185 | } 186 | } 187 | 188 | actions.forEach { $0() } 189 | } 190 | 191 | } 192 | 193 | private func ==(lhs: JobState.State, rhs: JobState.State) -> Bool { 194 | switch (lhs, rhs) { 195 | case (.idle, .idle): return true 196 | case (.evaluating, .evaluating): return true 197 | case (.executing, .executing): return true 198 | case (.finished(let lErrors), .finished(let rErrors)): return lErrors == rErrors 199 | default: return false 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Sources/JobType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobType.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public typealias JobBlock = (JobContext) -> Void 9 | 10 | public protocol JobType: Observable { 11 | var priority: DispatchQoS { get } 12 | var block: JobBlock { get } 13 | 14 | var conditions: [Condition] { get } 15 | var observers: [Observer] { get } 16 | 17 | var name: String? { get } 18 | 19 | mutating func add(condition: Condition) 20 | } 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/Lock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lock.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | internal extension NSLocking { 9 | 10 | func withCriticalScope(block: () -> T) -> T { 11 | lock() 12 | let value = block() 13 | unlock() 14 | return value 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sources/MutuallyExclusive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutuallyExclusive.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public struct MutuallyExclusive: Condition { 9 | 10 | public init() { } 11 | 12 | public func evaluate(ticket: Ticket, completion: @escaping (NSError?) -> Void) { 13 | let exclusivityClass = "\(type(of: self))" 14 | MutualExclusivityController.shared.add(ticket: ticket, for: exclusivityClass, completion: { completion(nil) }) 15 | } 16 | 17 | } 18 | 19 | private class MutualExclusivityController { 20 | 21 | static let shared = MutualExclusivityController() 22 | 23 | private let lock = NSLock() 24 | private var tickets = Dictionary() 25 | 26 | func add(ticket: Ticket, for exclusivityClass: String, completion: @escaping () -> Void) { 27 | var mutableTicket = ticket 28 | var previousTicket: Ticket? 29 | lock.withCriticalScope { 30 | var ticketsForThisClass = tickets[exclusivityClass] ?? [] 31 | previousTicket = ticketsForThisClass.last 32 | ticketsForThisClass.append(ticket) 33 | tickets[exclusivityClass] = ticketsForThisClass 34 | } 35 | 36 | mutableTicket.onFinish { _ in 37 | // clean up 38 | self.cleanUp(ticket: ticket, for: exclusivityClass) 39 | } 40 | 41 | if var previous = previousTicket { 42 | previous.onFinish { _ in completion() } 43 | } else { 44 | completion() 45 | } 46 | } 47 | 48 | private func cleanUp(ticket: Ticket, for exclusivityClass: String) { 49 | lock.withCriticalScope { 50 | if let ticketsForThisClass = tickets[exclusivityClass] { 51 | let filtered = ticketsForThisClass.filter { $0 != ticket } 52 | tickets[exclusivityClass] = filtered 53 | } 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Observer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observer.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public protocol Observer { 9 | 10 | /// Invoked immediately prior to the Job's block executing 11 | func jobDidStart(job: Ticket) 12 | 13 | /// Invoked immediately after the first time the Job is cancelled 14 | func jobDidCancel(job: Ticket) 15 | 16 | /// Invoked when JobContext.produce(job:) is executed. 17 | func job(job: Ticket, didProduce newJob: Ticket) 18 | 19 | /** 20 | Invoked as a Job finishes, along with any errors produced during 21 | execution (or condition evaluation). 22 | */ 23 | func job(job: Ticket, didFinishWithErrors errors: [NSError]) 24 | 25 | } 26 | 27 | public protocol Observable { 28 | 29 | mutating func add(observer: Observer) 30 | 31 | } 32 | 33 | extension Observable { 34 | 35 | public mutating func onStart(handler: @escaping () -> Void) { 36 | add(observer: BlockObserver(onStart: handler)) 37 | } 38 | 39 | public mutating func onCancel(handler: @escaping () -> Void) { 40 | add(observer: BlockObserver(onCancel: handler)) 41 | } 42 | 43 | public mutating func onFinish(handler: @escaping ([NSError]) -> Void) { 44 | add(observer: BlockObserver(onFinish: handler)) 45 | } 46 | 47 | public mutating func onProduce(handler: @escaping (Ticket) -> Void) { 48 | add(observer: BlockObserver(onProduce: handler)) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Ticket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ticket.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Ticket: Hashable, Observable { 9 | public var enqueuedDate: Date { return state.enqueuedDate } 10 | public var job: JobType { return state.job } 11 | 12 | fileprivate let state: JobState 13 | 14 | internal init(state: JobState) { 15 | self.state = state 16 | } 17 | 18 | public var isCancelled: Bool { return state.isCancelled } 19 | 20 | public var hashValue: Int { return enqueuedDate.hashValue ^ isCancelled.hashValue } 21 | 22 | public func cancel(error: NSError? = nil) { state.cancel(error: error) } 23 | 24 | public func add(observer: Observer) { state.add(observer: observer) } 25 | 26 | } 27 | 28 | public func ==(lhs: Ticket, rhs: Ticket) -> Bool { return lhs.state === rhs.state } 29 | -------------------------------------------------------------------------------- /Sources/Timeout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timeout.swift 3 | // Jobs 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Timeout: Observer { 9 | private let interval: TimeInterval 10 | 11 | public init(after: TimeInterval) { 12 | interval = after 13 | } 14 | 15 | public func jobDidStart(job: Ticket) { 16 | // When the job starts, queue up a block to cause it to time out. 17 | DispatchQueue.global().after(timeInterval: interval) { 18 | // always cancel the job 19 | // if the job has already been cancelled or has already finished, this has no effect 20 | let error = NSError(jobError: .timedOut) 21 | job.cancel(error: error) 22 | } 23 | } 24 | 25 | public func jobDidCancel(job: Ticket) { } 26 | public func job(job: Ticket, didFinishWithErrors errors: [NSError]) { } 27 | public func job(job: Ticket, didProduce newJob: Ticket) { } 28 | } 29 | --------------------------------------------------------------------------------