mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
Replace the Go backend with the Rust workspace and route Android/iOS through UniFFI bindings. Include the migrated extension runtime, network, providers, media metadata, downloads and library operations, with version 5.0.0+147. Remove Go sources, adapters, native build selection and CI dependencies. Build Rust unconditionally and lock the iOS Rust pod using a relative path. Retain legacy source and migration evidence in a local ignored archive. Validation: Android Kotlin compile and 53 native tests; Swift Rust-branch equivalence and syntax; CocoaPods install; workflow, shell, Ruby, plist and diff checks. Reuse the preceding 100 Rust tests, fmt/Clippy and five-ABI build checkpoint; no new APK or iOS application build for this cleanup. Known follow-up: URL/URLSearchParams globals are missing from the Rust JS runtime. A controlled extension replay confirms a URL-resolution regression; this commit does not fix that runtime gap.
143 lines
4.9 KiB
Swift
143 lines
4.9 KiB
Swift
import Foundation
|
|
|
|
final class CoreDownloadProgress {
|
|
private let waiter: (Int64, Int64) throws -> String
|
|
private let closer: () -> Void
|
|
|
|
init(wait: @escaping (Int64, Int64) throws -> String, close: @escaping () -> Void = {}) {
|
|
waiter = wait
|
|
closer = close
|
|
}
|
|
|
|
func waitDelta(_ sequence: Int64, _ timeout: Int64) throws -> String {
|
|
try waiter(sequence, timeout)
|
|
}
|
|
|
|
func close() { closer() }
|
|
}
|
|
|
|
/// Each listener owns its cursor and pending waiter. Lifecycle and delivery run
|
|
/// on the main queue; a cancelled wait may finish, but can only touch its own state.
|
|
final class DownloadProgressSubscription {
|
|
typealias Waiter = (Int64, Int64) -> String
|
|
private final class Session {
|
|
let queue = DispatchQueue(label: "com.zarz.spotiflac.download_progress_subscription", qos: .utility)
|
|
private let lock = NSLock()
|
|
private var cancelled = false
|
|
private var source: CoreDownloadProgress?
|
|
// Accessed only on this session's queue.
|
|
var sequence: Int64 = 0
|
|
var lastPayload: String?
|
|
|
|
func cancel() {
|
|
lock.lock()
|
|
cancelled = true
|
|
let closing = source
|
|
source = nil
|
|
lock.unlock()
|
|
closing?.close()
|
|
}
|
|
|
|
func connection(_ connect: () throws -> CoreDownloadProgress) throws -> CoreDownloadProgress? {
|
|
lock.lock()
|
|
let stopped = cancelled
|
|
let existing = source
|
|
lock.unlock()
|
|
if stopped { return nil }
|
|
if let existing { return existing }
|
|
let opened = try connect()
|
|
lock.lock()
|
|
if cancelled {
|
|
lock.unlock()
|
|
opened.close()
|
|
return nil
|
|
}
|
|
source = opened
|
|
lock.unlock()
|
|
return opened
|
|
}
|
|
|
|
func disconnect() {
|
|
lock.lock()
|
|
let closing = source
|
|
source = nil
|
|
lock.unlock()
|
|
closing?.close()
|
|
}
|
|
|
|
var isCancelled: Bool {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return cancelled
|
|
}
|
|
}
|
|
|
|
private let connect: () throws -> CoreDownloadProgress
|
|
private let interval: TimeInterval
|
|
private var current: Session?
|
|
|
|
convenience init(interval: TimeInterval = 0.25, waiter: @escaping Waiter) {
|
|
self.init(interval: interval, connect: { CoreDownloadProgress(wait: waiter) })
|
|
}
|
|
|
|
init(interval: TimeInterval = 0.25, connect: @escaping () throws -> CoreDownloadProgress) {
|
|
self.interval = interval
|
|
self.connect = connect
|
|
}
|
|
|
|
func start(_ receive: @escaping (Any) -> Void) {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
stop()
|
|
let session = Session()
|
|
current = session
|
|
poll(session, receive: receive)
|
|
}
|
|
|
|
func stop() {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
current?.cancel()
|
|
current = nil
|
|
}
|
|
|
|
deinit { current?.cancel() }
|
|
|
|
private func poll(_ session: Session, receive: @escaping (Any) -> Void) {
|
|
let connect = self.connect
|
|
session.queue.async { [weak self] in
|
|
guard !session.isCancelled else { return }
|
|
let payload: String
|
|
do {
|
|
guard let source = try session.connection(connect) else { return }
|
|
payload = try source.waitDelta(session.sequence, 15_000)
|
|
} catch {
|
|
session.disconnect()
|
|
session.sequence = 0
|
|
session.lastPayload = nil
|
|
payload = ""
|
|
}
|
|
guard !session.isCancelled else { return }
|
|
if !payload.isEmpty && payload != session.lastPayload,
|
|
let data = payload.data(using: .utf8),
|
|
let object = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]),
|
|
let delta = object as? [String: Any],
|
|
let sequence = delta["seq"] as? NSNumber {
|
|
session.sequence = max(session.sequence, sequence.int64Value)
|
|
session.lastPayload = payload
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self, self.current === session, !session.isCancelled else { return }
|
|
receive(object)
|
|
}
|
|
}
|
|
// Schedule from main so subscription identity is never accessed
|
|
// concurrently, and stop/relisten does not wait for an old Go call.
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self, self.current === session, !session.isCancelled else { return }
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + self.interval) { [weak self] in
|
|
guard let self, self.current === session, !session.isCancelled else { return }
|
|
self.poll(session, receive: receive)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|