-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathCrossProcessChangeNotifier.swift
More file actions
119 lines (100 loc) · 3.34 KB
/
Copy pathCrossProcessChangeNotifier.swift
File metadata and controls
119 lines (100 loc) · 3.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import Combine
import Foundation
import Synchronized
final class CrossProcessChangeNotifier: NSObject, @unchecked Sendable {
private let id = UUID().uuidString
private let changeTrackerURL: URL?
private let isStarted = Locked<Bool>(false)
private var localChangeSubscription: AnyCancellable?
private let remoteChange = PassthroughSubject<Void, Never>()
private var remoteChangeSubscription: AnyCancellable?
private let queue: OperationQueue = {
let queue = OperationQueue()
queue.name = "app.shareup.sqlite.cross-process-change-notifier"
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = .default
queue.underlyingQueue = .global()
return queue
}()
init(
databasePath: String,
databaseChangePublisher: AnyPublisher<Void, Error>,
onRemoteChange: @Sendable @escaping () -> Void
) {
changeTrackerURL = Self.changeTrackerURL(
databasePath: databasePath
)
super.init()
localChangeSubscription = databaseChangePublisher
.replaceError(with: ())
.receive(on: queue)
.sink { [weak self] in self?.notifyOtherProcesses() }
remoteChangeSubscription = remoteChange
.throttle(
for: .seconds(1),
scheduler: RunLoop.main,
latest: true
)
.receive(on: queue)
.sink { onRemoteChange() }
}
deinit {
removeFilePresenter()
}
func start() {
let needsRegistration = isStarted.access { isStarted in
guard !isStarted else { return false }
isStarted = true
return true
}
guard needsRegistration else { return }
registerFilePresenter()
}
func stop() {
let needsRemoval = isStarted.access { isStarted in
guard isStarted else { return false }
isStarted = false
return true
}
guard needsRemoval else { return }
removeFilePresenter()
}
}
extension CrossProcessChangeNotifier: NSFilePresenter {
var presentedItemURL: URL? { changeTrackerURL }
var presentedItemOperationQueue: OperationQueue { queue }
func presentedItemDidChange() {
remoteChange.send()
}
private func registerFilePresenter() {
guard let url = changeTrackerURL else { return }
touch(url)
NSFileCoordinator.addFilePresenter(self)
}
private func removeFilePresenter() {
NSFileCoordinator.removeFilePresenter(self)
}
private func notifyOtherProcesses() {
guard let url = changeTrackerURL else { return }
let coordinator = NSFileCoordinator(filePresenter: self)
coordinator.coordinate(
writingItemAt: url,
options: .forReplacing,
error: nil,
byAccessor: touch
)
}
private var touch: (URL?) -> Void {
{ [id] url in
guard let url else { return }
try? id.write(to: url, atomically: true, encoding: .utf8)
}
}
private static func changeTrackerURL(databasePath: String) -> URL? {
guard databasePath != ":memory:" else { return nil }
return URL(
fileURLWithPath: databasePath.appending("-change-tracker"),
isDirectory: false
)
}
}