diff --git a/Sources/XXMessengerClient/Utils/ReceiveFileCallbacksRegistry.swift b/Sources/XXMessengerClient/Utils/ReceiveFileCallbacksRegistry.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e82670ca748e4d28469d3b5c66b5eae0f77e4903
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/ReceiveFileCallbacksRegistry.swift
@@ -0,0 +1,36 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct ReceiveFileCallbacksRegistry {
+  public var register: (ReceiveFileCallback) -> Cancellable
+  public var registered: () -> ReceiveFileCallback
+}
+
+extension ReceiveFileCallbacksRegistry {
+  public static func live() -> ReceiveFileCallbacksRegistry {
+    class Registry {
+      var callbacks: [UUID: ReceiveFileCallback] = [:]
+    }
+    let registry = Registry()
+    return ReceiveFileCallbacksRegistry(
+      register: { callback in
+        let id = UUID()
+        registry.callbacks[id] = callback
+        return Cancellable { registry.callbacks[id] = nil }
+      },
+      registered: {
+        ReceiveFileCallback { result in
+          registry.callbacks.values.forEach { $0.handle(result) }
+        }
+      }
+    )
+  }
+}
+
+extension ReceiveFileCallbacksRegistry {
+  public static let unimplemented = ReceiveFileCallbacksRegistry(
+    register: XCTUnimplemented("\(Self.self).register", placeholder: Cancellable {}),
+    registered: XCTUnimplemented("\(Self.self).registered", placeholder: .unimplemented)
+  )
+}
diff --git a/Tests/XXMessengerClientTests/Utils/ReceiveFileCallbacksRegistryTests.swift b/Tests/XXMessengerClientTests/Utils/ReceiveFileCallbacksRegistryTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..d40e6d7923c1e8a69cda09d1d8bcaccd93c18d2b
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Utils/ReceiveFileCallbacksRegistryTests.swift
@@ -0,0 +1,57 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class ReceiveFileCallbacksRegistryTests: XCTestCase {
+  func testRegistry() {
+    var firstCallbackDidHandle: [Result<ReceivedFile, NSError>] = []
+    var secondCallbackDidHandle: [Result<ReceivedFile, NSError>] = []
+
+    let firstCallback = ReceiveFileCallback { result in
+      firstCallbackDidHandle.append(result)
+    }
+    let secondCallback = ReceiveFileCallback { result in
+      secondCallbackDidHandle.append(result)
+    }
+    let callbackRegistry: ReceiveFileCallbacksRegistry = .live()
+    let registeredCallbacks = callbackRegistry.registered()
+    let firstCallbackCancellable = callbackRegistry.register(firstCallback)
+    let secondCallbackCancellable = callbackRegistry.register(secondCallback)
+
+    let firstResult = Result<ReceivedFile, NSError>.success(.stub(1))
+    registeredCallbacks.handle(firstResult)
+
+    XCTAssertNoDifference(firstCallbackDidHandle, [firstResult])
+    XCTAssertNoDifference(secondCallbackDidHandle, [firstResult])
+
+    firstCallbackCancellable.cancel()
+    let secondError = NSError(domain: "test", code: 321)
+    let secondResult = Result<ReceivedFile, NSError>.failure(secondError)
+    registeredCallbacks.handle(secondResult)
+
+    XCTAssertNoDifference(firstCallbackDidHandle, [firstResult])
+    XCTAssertNoDifference(secondCallbackDidHandle, [firstResult, secondResult])
+
+    secondCallbackCancellable.cancel()
+
+    let thirdData = Result<ReceivedFile, NSError>.success(.stub(2))
+    registeredCallbacks.handle(thirdData)
+
+    XCTAssertNoDifference(firstCallbackDidHandle, [firstResult])
+    XCTAssertNoDifference(secondCallbackDidHandle, [firstResult, secondResult])
+  }
+}
+
+private extension ReceivedFile {
+  static func stub(_ id: Int) -> ReceivedFile {
+    ReceivedFile(
+      transferId: "transfer-id-\(id)".data(using: .utf8)!,
+      senderId: "sender-id-\(id)".data(using: .utf8)!,
+      preview: "preview-\(id)".data(using: .utf8)!,
+      name: "name-\(id)",
+      type: "type-\(id)",
+      size: id
+    )
+  }
+}