1 //  Copyright © 2019 650 Industries. All rights reserved.
2 
3 import Foundation
4 
5 /**
6  * A ReaperSelectionPolicy which keeps a predefined maximum number of updates across all scopes,
7  * and, once that number is surpassed, selects the updates least recently accessed (and then least
8  * recently published) to delete. Ignores filters and scopes.
9  *
10  * Uses the `lastAccessed` property to determine ordering of updates.
11  */
12 @objc(EXUpdatesReaperSelectionPolicyDevelopmentClient)
13 @objcMembers
14 public final class ReaperSelectionPolicyDevelopmentClient: NSObject, ReaperSelectionPolicy {
15   private let maxUpdatesToKeep: Int
16 
17   public override init() {
18     self.maxUpdatesToKeep = 10
19   }
20 
21   public init(maxUpdatesToKeep: Int) {
22     self.maxUpdatesToKeep = maxUpdatesToKeep
23 
24     if maxUpdatesToKeep <= 0 {
25       NSException.init(
26         name: .invalidArgumentException,
27         reason: "Cannot initiailize ReaperSelectionPolicy with maxUpdatesToKeep <= 0"
28       )
29       .raise()
30     }
31   }
32   public func updatesToDelete(withLaunchedUpdate launchedUpdate: Update, updates: [Update], filters: [String: Any]?) -> [Update] {
33     if updates.count < maxUpdatesToKeep {
34       return []
35     }
36 
37     var updatesMutable = updates.sorted { update1, update2 in
38       if update1.lastAccessed.compare(update2.lastAccessed) == .orderedSame {
39         return update1.commitTime < update2.commitTime
40       }
41       return update1.lastAccessed < update2.lastAccessed
42     }
43 
44     var updatesToDelete: [Update] = []
45     var hasFoundLaunchedUpdate = false
46 
47     while updatesMutable.count > maxUpdatesToKeep {
48       let oldest = updatesMutable.first!
49       updatesMutable.remove(at: 0)
50 
51       if launchedUpdate.updateId == oldest.updateId {
52         if hasFoundLaunchedUpdate {
53           // avoid infinite loop
54           NSException.init(
55             name: .internalInconsistencyException,
56             reason: "Multiple updates with the same ID were passed into ReaperSelectionPolicyDevelopmentClient"
57           )
58           .raise()
59         }
60 
61         // we don't want to delete launchedUpdate, so put it back on the end of the stack
62         updatesMutable.append(oldest)
63         hasFoundLaunchedUpdate = true
64       } else {
65         updatesToDelete.append(oldest)
66       }
67     }
68 
69     return updatesToDelete
70   }
71 }
72