// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
import Foundation
import GRDB
public protocol UniqueHashable {
var customHash : Int { get }
}
public struct Job : Codable , Equatable , Hashable , Identifiable , FetchableRecord , MutablePersistableRecord , TableRecord , ColumnExpressible {
public static var databaseTableName : String { " job " }
internal static let dependencyForeignKey = ForeignKey ( [ Columns . id ] , to : [ JobDependencies . Columns . dependantId ] )
public static let dependantJobDependency = hasMany (
JobDependencies . self ,
using : JobDependencies . jobForeignKey
)
public static let dependancyJobDependency = hasMany (
JobDependencies . self ,
using : JobDependencies . dependantForeignKey
)
internal static let jobsThisJobDependsOn = hasMany (
Job . self ,
through : dependantJobDependency ,
using : JobDependencies . dependant
)
internal static let jobsThatDependOnThisJob = hasMany (
Job . self ,
through : dependancyJobDependency ,
using : JobDependencies . job
)
public typealias Columns = CodingKeys
public enum CodingKeys : String , CodingKey , ColumnExpression {
case id
case priority
case failureCount
case variant
case behaviour
case shouldBlock
case shouldSkipLaunchBecomeActive
case nextRunTimestamp
case threadId
case interactionId
case details
case uniqueHashValue
}
public enum Variant : Int , Codable , DatabaseValueConvertible , CaseIterable {
// D e p r e c a t e d J o b s
case _legacy_getSnodePool = 1
case _legacy_buildPaths = 3009
case _legacy_getSwarm = 3010
// / T h i s i s a r e c u r r i n g j o b t h a t h a n d l e s t h e r e m o v a l o f d i s a p p e a r i n g m e s s a g e s a n d i s t r i g g e r e d
// / a t t h e t i m e s t a m p o f t h e n e x t d i s a p p e a r i n g m e s s a g e
case disappearingMessages = 0
// / T h i s i s a r e c u r r i n g j o b t h a t c h e c k s i f t h e u s e r n e e d s t o u p d a t e t h e i r p r o f i l e p i c t u r e o n l a u n c h , a n d i f s o
// / a t t e m p t t o d o w n l o a d t h e l a t e s t
case updateProfilePicture = 2
// / T h i s i s a r e c u r r i n g j o b t h a t e n s u r e s t h e a p p f e t c h e s t h e d e f a u l t o p e n g r o u p r o o m s o n l a u n c h
case retrieveDefaultOpenGroupRooms
// / T h i s i s a r e c u r r i n g j o b t h a t r e m o v e s e x p i r e d a n d o r p h a n e d d a t a , i t r u n s o n l a u n c h a n d c a n a l s o b e t r i g g e r e d
// / a s ' r u n O n c e ' t o a v o i d w a i t i n g u n t i l t h e n e x t l a u n c h t o c l e a r d a t a
case garbageCollection
// / T h i s i s a r e c u r r i n g j o b t h a t r u n s o n l a u n c h a n d f l a g s a n y m e s s a g e s m a r k e d a s ' s e n d i n g ' t o
// / b e i n t h e i r ' f a i l e d ' s t a t e
// /
// / * * N o t e : * * T h i s i s a b l o c k i n g j o b s o i t w i l l r u n b e f o r e a n y o t h e r j o b s a n d p r e v e n t t h e m f r o m
// / r u n n i n g u n t i l i t ' s c o m p l e t e
case failedMessageSends = 1000
// / T h i s i s a r e c u r r i n g j o b t h a t r u n s o n l a u n c h a n d f l a g s a n y a t t a c h m e n t s m a r k e d a s ' u p l o a d i n g ' t o
// / b e i n t h e i r ' f a i l e d ' s t a t e
// /
// / * * N o t e : * * T h i s i s a b l o c k i n g j o b s o i t w i l l r u n b e f o r e a n y o t h e r j o b s a n d p r e v e n t t h e m f r o m
// / r u n n i n g u n t i l i t ' s c o m p l e t e
case failedAttachmentDownloads
// / T h i s i s a r e c u r r i n g j o b t h a t r u n s o n r e t u r n f r o m b a c k g r o u n d a n d r e g i s t e r e s a n d u p l o a d s t h e
// / l a t e s t d e v i c e p u s h t o k e n s
case syncPushTokens = 2000
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r a m e s s a g e i s s e n t t o n o t i f y t h e p u s h n o t i f i c a t i o n s e r v e r
// / a b o u t t h e m e s s a g e
case notifyPushServer
// / T h i s i s a j o b t h a t r u n s o n c e a t m o s t e v e r y 3 s e c o n d s p e r t h r e a d w h e n e v e r a m e s s a g e i s m a r k e d a s r e a d
// / ( i f r e a d r e c e i p t s a r e e n a b l e d ) t o n o t i f y o t h e r m e m b e r s i n a c o n v e r s a t i o n t h a t t h e i r m e s s a g e w a s r e a d
case sendReadReceipts
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r a m e s s a g e i s r e c e i v e d t o a t t e m p t t o d e c o d e a n d p r o p e r l y
// / p r o c e s s t h e m e s s a g e
case messageReceive = 3000
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r a m e s s a g e i s s e n t t o a t t e m p t t o e n c o d e a n d p r o p e r l y
// / s e n d t h e m e s s a g e
case messageSend
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r a n a t t a c h m e n t i s u p l o a d e d t o a t t e m p t t o e n c o d e a n d p r o p e r l y
// / u p l o a d t h e a t t a c h m e n t
case attachmentUpload
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r a n a t t a c h m e n t i s d o w n l o a d e d t o a t t e m p t t o d e c o d e a n d p r o p e r l y
// / d o w n l o a d t h e a t t a c h m e n t
case attachmentDownload
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r t h e u s e r l e a v e s a g r o u p t o s e n d a g r o u p l e a v i n g m e s s a g e , r e m o v e g r o u p
// / r e c o r d a n d g r o u p m e m b e r r e c o r d
case groupLeaving
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r t h e u s e r c o n f i g o r a c l o s e d g r o u p c o n f i g c h a n g e s , i t r e t r i e v e s t h e
// / s t a t e o f a l l c o n f i g o b j e c t s a n d s y n c s a n y t h a t a r e f l a g g e d a s n e e d i n g t o b e s y n c e d
case configurationSync
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r a c o n f i g m e s s a g e i s r e c e i v e d t o a t t e m p t t o d e c o d e i t a n d u p d a t e t h e
// / c o n f i g s t a t e w i t h t h e c h a n g e s ; t h i s j o b w i l l g e n e r a l l y b e s c h e d u l e d a l o n g s i n c e a ` m e s s a g e R e c e i v e ` j o b
// / a n d w i l l b l o c k t h e s t a n d a r d m e s s a g e r e c e i v e j o b
case configMessageReceive
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r d i s a p p e a r i n g a f t e r r e a d m e s s a g e s a r e r e a d a n d n e e d e d t o u p d a t e t h e
// / e x p i r a t i o n o n t h e n e t w o r k
case expirationUpdate
// / T h i s i s a j o b t h a t r u n s o n c e w h e n e v e r a m e s s a g e i s m a r k e d a s r e a d b e c a u s e o f s y n c i n g f r o m u s e r c o n f i g a n d
// / n e e d s t o g e t e x p i r a t i o n f r o m n e t w o r k
case getExpiration
// / T h i s i s a j o b t h a t r u n s a t m o s t o n c e e v e r y 2 4 h o u r s i n o r d e r t o c h e c k i f t h e r e i s a n e w u p d a t e a v a i l a b l e o n G i t H u b
case checkForAppUpdates = 3011
}
public enum Behaviour : Int , Codable , DatabaseValueConvertible , CaseIterable {
// / T h i s j o b w i l l r u n o n c e a n d t h e n b e r e m o v e d f r o m t h e j o b s t a b l e
case runOnce
// / T h i s j o b w i l l r u n o n c e t h e n e x t t i m e t h e a p p l a u n c h e s a n d t h e n b e r e m o v e d f r o m t h e j o b s t a b l e
case runOnceNextLaunch
// / T h i s j o b w i l l r u n a n d t h e n w i l l b e u p d a t e d w i t h a n e w ` n e x t R u n T i m e s t a m p ` ( a t l e a s t 1 s e c o n d i n
// / t h e f u t u r e ) i n o r d e r t o b e r u n a g a i n
case recurring
// / T h i s j o b w i l l r u n o n c e e a c h l a u n c h a n d m a y r u n a g a i n d u r i n g t h e s a m e s e s s i o n i f ` n e x t R u n T i m e s t a m p `
// / g e t s s e t
case recurringOnLaunch
// / T h i s j o b w i l l r u n o n c e e a c h w h e n e v e r t h e a p p b e c o m e s a c t i v e ( l a u n c h a n d r e t u r n f r o m b a c k g r o u n d ) a n d
// / m a y r u n a g a i n d u r i n g t h e s a m e s e s s i o n i f ` n e x t R u n T i m e s t a m p ` g e t s s e t
case recurringOnActive
}
// / T h e ` i d ` v a l u e i s a u t o i n c r e m e n t e d b y t h e d a t a b a s e , i f t h e ` J o b ` h a s n ' t b e e n i n s e r t e d i n t o
// / t h e d a t a b a s e y e t t h i s v a l u e w i l l b e ` n i l `
public var id : Int64 ? = nil
// / T h e ` p r i o r i t y ` v a l u e i s u s e d t o a l l o w f o r f o r c i n g s o m e j o b s t o r u n b e f o r e o t h e r s ( D e f a u l t v a l u e ` 0 ` )
// /
// / J o b s w i l l b e r u n i n t h e f o l l o w i n g o r d e r :
// / - J o b s s c h e d u l e d i n t h e p a s t ( o r w i t h n o ` n e x t R u n T i m e s t a m p ` ) f i r s t
// / - J o b s w i t h a h i g h e r ` p r i o r i t y ` v a l u e
// / - J o b s w i t h a s o o n e r ` n e x t R u n T i m e s t a m p ` v a l u e
// / - T h e o r d e r t h e j o b w a s i n s e r t e d i n t o t h e d a t a b a s e
public var priority : Int64
// / A c o u n t e r f o r t h e n u m b e r o f t i m e s t h i s j o b h a s f a i l e d
public let failureCount : UInt
// / T h e t y p e o f j o b
public let variant : Variant
// / H o w t h e j o b s h o u l d b e h a v e
public let behaviour : Behaviour
// / W h e n t h e a p p s t a r t s t h i s f l a g c o n t r o l s w h e t h e r t h e j o b s h o u l d p r e v e n t o t h e r j o b s f r o m s t a r t i n g u n t i l a f t e r i t c o m p l e t e s
// /
// / * * N o t e : * * T h i s f l a g i s o n l y s u p p o r t e d f o r j o b s w i t h a n ` O n L a u n c h ` b e h a v i o u r b e c a u s e t h e r e i s n o w a y t o g u a r a n t e e
// / j o b s w i t h a n y o t h e r b e h a v i o u r s w i l l b e a d d e d t o t h e J o b R u n n e r b e f o r e a l l t h e ` O n L a u n c h ` b l o c k i n g j o b s a r e c o m p l e t e d
// / r e s u l t i n g i n t h e J o b R u n n e r n o l o n g e r b l o c k i n g
public let shouldBlock : Bool
// / W h e n t h e a p p s t a r t s i t a l s o t r i g g e r s a n y ` O n A c t i v e ` j o b s , t h i s f l a g c o n t r o l s w h e t h e r t h e j o b s h o u l d s k i p t h i s i n i t i a l ` O n A c t i v e `
// / t r i g g e r ( g e n e r a l l y u s e d f o r t h e s a m e j o b r e g i s t e r e d w i t h b o t h ` O n L a u n c h ` a n d ` O n A c t i v e ` b e h a v i o u r s )
public let shouldSkipLaunchBecomeActive : Bool
// / S e c o n d s s i n c e e p o c h t o i n d i c a t e t h e n e x t d a t e t i m e t h a t t h i s j o b s h o u l d r u n
public let nextRunTimestamp : TimeInterval
// / T h e i d o f t h e t h r e a d t h i s j o b i s a s s o c i a t e d w i t h , i f t h e a s s o c i a t e d t h r e a d i s d e l e t e d t h i s j o b w i l l
// / a l s o b e d e l e t e d
// /
// / * * N o t e : * * T h i s w i l l o n l y b e p o p u l a t e d f o r J o b s a s s o c i a t e d t o t h r e a d s
public let threadId : String ?
// / T h e i d o f t h e i n t e r a c t i o n t h i s j o b i s a s s o c i a t e d w i t h , i f t h e a s s o c i a t e d i n t e r a c t i o n i s d e l e t e d t h i s
// / j o b w i l l a l s o b e d e l e t e d
// /
// / * * N o t e : * * T h i s w i l l o n l y b e p o p u l a t e d f o r J o b s a s s o c i a t e d t o i n t e r a c t i o n s
public let interactionId : Int64 ?
// / J S O N e n c o d e d d a t a r e q u i r e d f o r t h e j o b
public let details : Data ?
// / W h e n i n i t a l i z i n g w i t h ` s h o u l d B e U n i q u e ` s e t t o ` t r u e ` t h i s v a l u e w i l l b e p o p u l a t e d w i t h a h a s h c o n s t r u c t e d b y
// / c o m b i n i n g t h e ` v a r i a n t ` , ` t h r e a d I d ` , ` i n t e r a c t i o n I d ` a n d ` d e t a i l s ` a n d i f t h i s v a l u e i s p o p u l a t e d
// / a d d i n g / i n s e r t i n g a ` J o b ` w i l l f a i l i f t h e r e i s a l r e a d y a j o b w i t h t h e s a m e ` u n i q u e H a s h V a l u e ` i n t h e d a t a b a s e o r
// / i n t h e ` J o b R u n n e r `
public let uniqueHashValue : Int ?
// / T h e o t h e r j o b s w h i c h t h i s j o b i s d e p e n d a n t o n
// /
// / * * N o t e : * * W h e n c o m p l e t i n g a j o b t h e d e p e n d e n c i e s * * M U S T * * b e c l e a r e d b e f o r e t h e j o b i s
// / d e l e t e d o r i t w i l l a u t o m a t i c a l l y d e l e t e a n y d e p e n d a n t j o b s
public var dependencies : QueryInterfaceRequest < Job > {
request ( for : Job . jobsThisJobDependsOn )
}
// / T h e o t h e r j o b s w h i c h d e p e n d o n t h i s j o b
// /
// / * * N o t e : * * W h e n c o m p l e t i n g a j o b t h e d e p e n d e n c i e s * * M U S T * * b e c l e a r e d b e f o r e t h e j o b i s
// / d e l e t e d o r i t w i l l a u t o m a t i c a l l y d e l e t e a n y d e p e n d a n t j o b s
public var dependantJobs : QueryInterfaceRequest < Job > {
request ( for : Job . jobsThatDependOnThisJob )
}
// MARK: - I n i t i a l i z a t i o n
internal init (
id : Int64 ? ,
priority : Int64 = 0 ,
failureCount : UInt ,
variant : Variant ,
behaviour : Behaviour ,
shouldBlock : Bool ,
shouldBeUnique : Bool ,
shouldSkipLaunchBecomeActive : Bool ,
nextRunTimestamp : TimeInterval ,
threadId : String ? ,
interactionId : Int64 ? ,
details : Data ?
) {
Job . ensureValidBehaviour (
behaviour : behaviour ,
shouldBlock : shouldBlock ,
shouldSkipLaunchBecomeActive : shouldSkipLaunchBecomeActive
)
self . id = id
self . priority = priority
self . failureCount = failureCount
self . variant = variant
self . behaviour = behaviour
self . shouldBlock = shouldBlock
self . shouldSkipLaunchBecomeActive = shouldSkipLaunchBecomeActive
self . nextRunTimestamp = nextRunTimestamp
self . threadId = threadId
self . interactionId = interactionId
self . details = details
self . uniqueHashValue = Job . createUniqueHash (
shouldBeUnique : shouldBeUnique ,
customHash : nil ,
variant : variant ,
threadId : threadId ,
interactionId : interactionId ,
detailsData : details
)
}
public init (
priority : Int64 = 0 ,
failureCount : UInt = 0 ,
variant : Variant ,
behaviour : Behaviour = . runOnce ,
shouldBlock : Bool = false ,
shouldBeUnique : Bool = false ,
shouldSkipLaunchBecomeActive : Bool = false ,
nextRunTimestamp : TimeInterval = 0 ,
threadId : String ? = nil ,
interactionId : Int64 ? = nil
) {
Job . ensureValidBehaviour (
behaviour : behaviour ,
shouldBlock : shouldBlock ,
shouldSkipLaunchBecomeActive : shouldSkipLaunchBecomeActive
)
self . priority = priority
self . failureCount = failureCount
self . variant = variant
self . behaviour = behaviour
self . shouldBlock = shouldBlock
self . shouldSkipLaunchBecomeActive = shouldSkipLaunchBecomeActive
self . nextRunTimestamp = nextRunTimestamp
self . threadId = threadId
self . interactionId = interactionId
self . details = nil
self . uniqueHashValue = Job . createUniqueHash (
shouldBeUnique : shouldBeUnique ,
customHash : nil ,
variant : variant ,
threadId : threadId ,
interactionId : interactionId ,
detailsData : nil
)
}
public init ? < T : Encodable > (
priority : Int64 = 0 ,
failureCount : UInt = 0 ,
variant : Variant ,
behaviour : Behaviour = . runOnce ,
shouldBlock : Bool = false ,
shouldBeUnique : Bool = false ,
shouldSkipLaunchBecomeActive : Bool = false ,
nextRunTimestamp : TimeInterval = 0 ,
threadId : String ? = nil ,
interactionId : Int64 ? = nil ,
details : T ?
) {
precondition ( T . self != Job . self , " [Job] Fatal error trying to create a Job with a Job as it's details " )
Job . ensureValidBehaviour (
behaviour : behaviour ,
shouldBlock : shouldBlock ,
shouldSkipLaunchBecomeActive : shouldSkipLaunchBecomeActive
)
guard
let details : T = details ,
let detailsData : Data = try ? JSONEncoder ( )
. with ( outputFormatting : . sortedKeys ) // N e e d e d f o r d e t e r m i n i s t i c c o m p a r i s o n
. encode ( details )
else { return nil }
self . priority = priority
self . failureCount = failureCount
self . variant = variant
self . behaviour = behaviour
self . shouldBlock = shouldBlock
self . shouldSkipLaunchBecomeActive = shouldSkipLaunchBecomeActive
self . nextRunTimestamp = nextRunTimestamp
self . threadId = threadId
self . interactionId = interactionId
self . details = detailsData
self . uniqueHashValue = Job . createUniqueHash (
shouldBeUnique : shouldBeUnique ,
customHash : ( details as ? UniqueHashable ) ? . customHash ,
variant : variant ,
threadId : threadId ,
interactionId : interactionId ,
detailsData : detailsData
)
}
fileprivate static func ensureValidBehaviour (
behaviour : Behaviour ,
shouldBlock : Bool ,
shouldSkipLaunchBecomeActive : Bool
) {
// B l o c k i n g j o b s c a n o n l y r u n o n l a u n c h a s w e c a n ' t g u a r a n t e e t h a t a n y o t h e r b e h a v i o u r s w i l l g e t a d d e d
// t o t h e J o b R u n n e r b e f o r e a n y p r i o r b l o c k i n g j o b s h a v e c o m p l e t e d ( r e s u l t i n g i n t h e m b e i n g n o n - b l o c k i n g )
let blockingValid : Bool = ( ! shouldBlock || behaviour = = . recurringOnLaunch || behaviour = = . runOnceNextLaunch )
let becomeActiveValid : Bool = ( ! shouldSkipLaunchBecomeActive || behaviour = = . recurringOnActive )
precondition ( blockingValid , " [Job] Fatal error trying to create a blocking job which doesn't run on launch " )
precondition ( becomeActiveValid , " [Job] Fatal error trying to create a job which skips on 'OnActive' triggered during launch with doesn't run on active " )
}
private static func createUniqueHash (
shouldBeUnique : Bool ,
customHash : Int ? ,
variant : Variant ,
threadId : String ? ,
interactionId : Int64 ? ,
detailsData : Data ?
) -> Int ? {
// O n l y g e n e r a t e a u n i q u e h a s h i f t h e J o b s h o u l d a c t u a l l y b e u n i q u e ( w e d o n ' t w a n t t o p r e v e n t
// a l l d u p l i c a t e j o b s , j u s t t h e o n e s e x p l i c i t l y m a r k e d a s u n i q u e )
guard shouldBeUnique else { return nil }
switch customHash {
case . some ( let customHash ) : return customHash
default :
var hasher : Hasher = Hasher ( )
variant . hash ( into : & hasher )
threadId ? . hash ( into : & hasher )
interactionId ? . hash ( into : & hasher )
detailsData ? . hash ( into : & hasher )
return hasher . finalize ( )
}
}
// MARK: - C u s t o m D a t a b a s e I n t e r a c t i o n
public mutating func didInsert ( _ inserted : InsertionSuccess ) {
self . id = inserted . rowID
}
}
// MARK: - G R D B I n t e r a c t i o n s
extension Job {
internal static func filterPendingJobs (
variants : [ Variant ] ,
excludeFutureJobs : Bool ,
includeJobsWithDependencies : Bool
) -> QueryInterfaceRequest < Job > {
var query : QueryInterfaceRequest < Job > = Job
. filter (
// R e t r i e v e a l l ' r u n O n c e ' a n d ' r e c u r r i n g ' j o b s
[
Job . Behaviour . runOnce ,
Job . Behaviour . recurring
] . contains ( Job . Columns . behaviour ) || (
// R e t r i e v e a n y ' r e c u r r i n g O n L a u n c h ' a n d ' r e c u r r i n g O n A c t i v e ' j o b s t h a t h a v e a
// ' n e x t R u n T i m e s t a m p '
[
Job . Behaviour . recurringOnLaunch ,
Job . Behaviour . recurringOnActive
] . contains ( Job . Columns . behaviour ) &&
Job . Columns . nextRunTimestamp > 0
)
)
. filter ( variants . contains ( Job . Columns . variant ) )
. order (
Job . Columns . nextRunTimestamp > Date ( ) . timeIntervalSince1970 , // P a s t j o b s f i r s t
Job . Columns . priority . desc ,
Job . Columns . nextRunTimestamp ,
Job . Columns . id
)
if excludeFutureJobs {
query = query . filter ( Job . Columns . nextRunTimestamp <= Date ( ) . timeIntervalSince1970 )
}
if ! includeJobsWithDependencies {
query = query . having ( Job . jobsThisJobDependsOn . isEmpty )
}
return query
}
}
// MARK: - C o n v e n i e n c e
public extension Job {
func with (
failureCount : UInt = 0 ,
nextRunTimestamp : TimeInterval
) -> Job {
return Job (
id : self . id ,
priority : self . priority ,
failureCount : failureCount ,
variant : self . variant ,
behaviour : self . behaviour ,
shouldBlock : self . shouldBlock ,
shouldBeUnique : ( self . uniqueHashValue != nil ) ,
shouldSkipLaunchBecomeActive : self . shouldSkipLaunchBecomeActive ,
nextRunTimestamp : nextRunTimestamp ,
threadId : self . threadId ,
interactionId : self . interactionId ,
details : self . details
)
}
func with < T : Encodable > ( details : T ) -> Job ? {
guard
let detailsData : Data = try ? JSONEncoder ( )
. with ( outputFormatting : . sortedKeys ) // N e e d e d f o r d e t e r m i n i s t i c c o m p a r i s o n
. encode ( details )
else { return nil }
return Job (
id : self . id ,
priority : self . priority ,
failureCount : self . failureCount ,
variant : self . variant ,
behaviour : self . behaviour ,
shouldBlock : self . shouldBlock ,
shouldBeUnique : ( self . uniqueHashValue != nil ) ,
shouldSkipLaunchBecomeActive : self . shouldSkipLaunchBecomeActive ,
nextRunTimestamp : self . nextRunTimestamp ,
threadId : self . threadId ,
interactionId : self . interactionId ,
details : detailsData
)
}
}