Skip to content

Commit

Permalink
Merge pull request #219 from synonymdev/backup-event-debounce
Browse files Browse the repository at this point in the history
Debounce backup state events to react native
  • Loading branch information
Jasonvdb authored Mar 5, 2024
2 parents 0f687ca + 882c160 commit 94c2028
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 41 deletions.
41 changes: 40 additions & 1 deletion lib/android/src/main/java/com/reactnativeldk/Helpers.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package com.reactnativeldk
import android.os.Handler
import android.os.Looper
import com.facebook.react.bridge.*
import org.json.JSONObject
import org.ldk.enums.Currency
Expand All @@ -8,6 +10,7 @@ import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.nio.channels.Channels
import java.util.Date

fun handleResolve(promise: Promise, res: LdkCallbackResponses) {
if (res != LdkCallbackResponses.log_write_success) {
Expand Down Expand Up @@ -204,6 +207,22 @@ fun WritableMap.putHexString(key: String, bytes: ByteArray?) {
}
}

/**
* Adds a Date object into the map.
* If the date is not null, it is converted to a double representing the unix timestamp.
* If it's null, a null value is added to the map.
*
* @param key The key under which the date will be stored in the map.
* @param date The date to be stored in the map. Can be null.
*/
fun WritableMap.putDateOrNull(key: String, date: Date?) {
if (date != null) {
putDouble(key, date.time.toDouble())
} else {
putNull(key)
}
}

fun WritableArray.pushHexString(bytes: ByteArray) {
pushString(bytes.hexEncodedString())
}
Expand Down Expand Up @@ -502,4 +521,24 @@ fun mergeObj(obj1: JSONObject, obj2: HashMap<String, Any>): HashMap<String, Any>
}

return newObj
}
}

object UiThreadDebouncer {
private val pending = hashMapOf<String, Runnable>()
private val handler = Handler(Looper.getMainLooper())

/**
* Used to debounce an [action] function call to be executed after a delay on the main thread
*
* @param interval The delay in milliseconds after which the action will be executed. Default value is 250ms.
* @param key The unique identifier for the action to be debounced. If an action with the same key is already pending, it will be cancelled.
* @param action The function to be executed after the interval.
*/
fun debounce(interval: Long = 250, key: String, action: () -> Unit) {
pending[key]?.let { handler.removeCallbacks(it) }
val runnable = Runnable(action)
pending[key] = runnable

handler.postDelayed(runnable, interval)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.reactnativeldk.classes

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.reactnativeldk.UiThreadDebouncer
import com.reactnativeldk.EventTypes
import com.reactnativeldk.LdkEventEmitter
import com.reactnativeldk.hexEncodedString
import com.reactnativeldk.hexa
import com.reactnativeldk.putDateOrNull
import org.json.JSONObject
import org.ldk.structs.Result_StrSecp256k1ErrorZ.Result_StrSecp256k1ErrorZ_OK
import org.ldk.structs.UtilMethods
Expand Down Expand Up @@ -61,17 +64,9 @@ data class BackupFileState(
val encoded: WritableMap
get() {
val body = Arguments.createMap()
body.putInt("lastQueued", lastQueued.time.toInt())
if (lastPersisted != null) {
body.putInt("lastPersisted", lastPersisted!!.time.toInt())
} else {
body.putNull("lastPersisted")
}
if (lastFailed != null) {
body.putInt("lastFailed", lastFailed!!.time.toInt())
} else {
body.putNull("lastFailed")
}
body.putDouble("lastQueued", lastQueued.time.toDouble())
body.putDateOrNull("lastPersisted", lastPersisted)
body.putDateOrNull("lastFailed", lastFailed)
if (lastErrorMessage != null) {
body.putString("lastErrorMessage", lastErrorMessage)
} else {
Expand Down Expand Up @@ -592,10 +587,9 @@ class BackupClient {
body.putMap(key, state.encoded)
}

LdkEventEmitter.send(
EventTypes.backup_state_update,
body
)
UiThreadDebouncer.debounce(interval = 250, key = "backupStateUpdate") {
LdkEventEmitter.send(EventTypes.backup_state_update, body)
}

backupStateLock.unlock()
}
Expand Down
51 changes: 26 additions & 25 deletions lib/ios/Classes/BackupClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class BackupClient {
}

static var backupState: [String: BackupFileState] = [:]

static func setup(secretKey: [UInt8], pubKey: [UInt8], network: String, server: String, serverPubKey: String) throws {
guard getNetwork(network) != nil else {
throw BackupError.invalidNetwork
Expand Down Expand Up @@ -195,9 +195,9 @@ class BackupClient {
guard let key = Self.encryptionKey else {
throw BackupError.requiresSetup
}

let sealedBox = try AES.GCM.seal(blob, using: key)

return sealedBox.combined!
}

Expand All @@ -209,7 +209,7 @@ class BackupClient {
guard let key = Self.encryptionKey else {
throw BackupError.requiresSetup
}

//Remove appended 12 bytes nonce and 16 byte trailing tag
let encryptedData: Data = {
var bytes = blob.subdata(in: 12..<blob.count)
Expand All @@ -223,7 +223,7 @@ class BackupClient {
do {
let sealedBox = try AES.GCM.SealedBox(nonce: .init(data: nonce), ciphertext: encryptedData, tag: tag)
let decryptedData = try AES.GCM.open(sealedBox, using: key)

return decryptedData
} catch {
if let ce = error as? CryptoKitError {
Expand Down Expand Up @@ -256,7 +256,7 @@ class BackupClient {
throw persistError
}
}

fileprivate static func persist(_ label: Label, _ bytes: [UInt8]) throws {
struct PersistResponse: Codable {
let success: Bool
Expand All @@ -267,25 +267,25 @@ class BackupClient {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Skipping remote backup for \(label.string)")
return
}

guard let pubKey, let serverPubKey else {
throw BackupError.requiresSetup
}

let pubKeyHex = Data(pubKey).hexEncodedString()
let encryptedBackup = try encrypt(Data(bytes))
let signedHash = try sign(hash(encryptedBackup))

//Hash of pubkey+timestamp
let clientChallenge = hash("\(pubKeyHex)\(Date().timeIntervalSince1970)".data(using: .utf8)!)

var request = URLRequest(url: try backupUrl(.persist, label))
request.httpMethod = "POST"
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(signedHash, forHTTPHeaderField: "Signed-Hash")
request.setValue(pubKeyHex, forHTTPHeaderField: "Public-Key")
request.setValue(clientChallenge, forHTTPHeaderField: "Challenge")

request.httpBody = encryptedBackup

var requestError: Error?
Expand Down Expand Up @@ -333,7 +333,7 @@ class BackupClient {
guard verifySignature(message: clientChallenge, signature: persistResponse.signature, pubKey: serverPubKey) else {
throw BackupError.serverChallengeResponseFailed
}

LdkEventEmitter.shared.send(withEvent: .native_log, body: "Remote persist success for \(label.string)")
}

Expand Down Expand Up @@ -436,10 +436,10 @@ class BackupClient {

static func retrieveCompleteBackup() throws -> CompleteBackup {
let backedUpFilenames = try listFiles()

var allFiles: [String: Data] = [:]
var channelFiles: [String: Data] = [:]

//Fetch each file's data
for fileName in backedUpFilenames.list {
guard fileName != "\(Label.ping.string).bin" else {
Expand Down Expand Up @@ -482,7 +482,7 @@ class BackupClient {
guard let secretKey else {
throw BackupError.requiresSetup
}

let fullMessage = "\(signedMessagePrefix)\(message)"
let signed = Bindings.swiftSign(msg: Array(fullMessage.utf8), sk: secretKey)
if let _ = signed.getError() {
Expand Down Expand Up @@ -511,7 +511,7 @@ class BackupClient {
guard let pubKey else {
throw BackupError.requiresSetup
}

//Fetch challenge with signed timestamp as nonce
let pubKeyHex = Data(pubKey).hexEncodedString()
let timestamp = String(Date().timeIntervalSince1970)
Expand All @@ -520,7 +520,7 @@ class BackupClient {
"timestamp": timestamp,
"signature": try sign(timestamp)
]

struct FetchChallengeResponse: Codable {
let challenge: String
}
Expand All @@ -530,9 +530,9 @@ class BackupClient {
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(pubKeyHex, forHTTPHeaderField: "Public-Key")

request.httpBody = try JSONSerialization.data(withJSONObject: payload)

var requestError: Error?
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
Expand Down Expand Up @@ -561,12 +561,12 @@ class BackupClient {

task.resume()
semaphore.wait()

if let error = requestError {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Fetch server challenge failed. \(error.localizedDescription)")
throw error
}

guard let fetchChallengeResponse else {
throw BackupError.missingResponse
}
Expand All @@ -579,9 +579,9 @@ class BackupClient {
fetchRequest.httpMethod = "POST"
fetchRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
fetchRequest.setValue(pubKeyHex, forHTTPHeaderField: "Public-Key")

fetchRequest.httpBody = try JSONSerialization.data(withJSONObject: fetchPayload)

var fetchRequestError: Error?
var fetchBearerResponse: BackupRetrieveBearer?
let fetchSemaphore = DispatchSemaphore(value: 0)
Expand Down Expand Up @@ -611,7 +611,7 @@ class BackupClient {

fetchTask.resume()
fetchSemaphore.wait()

if let error = fetchRequestError {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Fetch bearer token failed. \(error.localizedDescription)")
throw error
Expand All @@ -634,7 +634,6 @@ extension BackupClient {
return
}

//All updates on main queue
DispatchQueue.main.async {
backupState[key] = backupState[key] ?? .init(lastQueued: Date())

Expand All @@ -657,7 +656,9 @@ extension BackupClient {
body[key] = backupState[key]!.encoded
}

LdkEventEmitter.shared.send(withEvent: .backup_state_update, body: body)
debounce(interval: 0.25, key: "backup-state-event") {
LdkEventEmitter.shared.send(withEvent: .backup_state_update, body: body)
}()
}
}

Expand Down
20 changes: 20 additions & 0 deletions lib/ios/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,23 @@ extension String {
.reduce("") { $0 + String($1) }
}
}

var lastPendingWorkItems: [String: DispatchWorkItem] = [:]

func debounce(interval: TimeInterval, key: String, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void {
var lastFireTime = DispatchTime.now()

return {
lastPendingWorkItems.first { $0.key == key }?.value.cancel()

lastPendingWorkItems[key] = DispatchWorkItem {
let elapsed = DispatchTime.now().uptimeNanoseconds - lastFireTime.uptimeNanoseconds
if elapsed >= UInt64(interval * 1_000_000) {
action()
lastFireTime = DispatchTime.now()
}
}

queue.asyncAfter(deadline: .now() + interval, execute: lastPendingWorkItems[key]!)
}
}

0 comments on commit 94c2028

Please sign in to comment.