From 9ba25a2d778c908846b864265ac5b0f2913aad07 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:27:04 +0900 Subject: [PATCH 1/5] Implement --- .../Protocols/ChainableProcessor.swift | 23 +++ Sources/LiveKit/Support/ProcessorChain.swift | 71 ++++++++++ Sources/LiveKit/Support/WeakRef.swift | 23 +++ Tests/LiveKitTests/ProcessorChainTests.swift | 134 ++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 Sources/LiveKit/Protocols/ChainableProcessor.swift create mode 100644 Sources/LiveKit/Support/ProcessorChain.swift create mode 100644 Sources/LiveKit/Support/WeakRef.swift create mode 100644 Tests/LiveKitTests/ProcessorChainTests.swift diff --git a/Sources/LiveKit/Protocols/ChainableProcessor.swift b/Sources/LiveKit/Protocols/ChainableProcessor.swift new file mode 100644 index 000000000..5817948fd --- /dev/null +++ b/Sources/LiveKit/Protocols/ChainableProcessor.swift @@ -0,0 +1,23 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +@objc +public protocol ChainableProcessor: AnyObject { + // Next object in the chain. + weak var nextProcessor: Self? { get set } +} diff --git a/Sources/LiveKit/Support/ProcessorChain.swift b/Sources/LiveKit/Support/ProcessorChain.swift new file mode 100644 index 000000000..27eac0845 --- /dev/null +++ b/Sources/LiveKit/Support/ProcessorChain.swift @@ -0,0 +1,71 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +public class ProcessorChain: NSObject, Loggable { + // MARK: - Public properties + + public var isProcessorsEmpty: Bool { countProcessors == 0 } + + public var isProcessorsNotEmpty: Bool { countProcessors != 0 } + + public var countProcessors: Int { + _state.read { $0.processors.compactMap(\.value).count } + } + + public var allProcessors: [T] { + _state.read { $0.processors.compactMap(\.value) } + } + + // MARK: - Private properties + + private struct State { + var processors = [WeakRef]() + } + + private let _state = StateSync(State()) + + public func add(processor: T) { + _state.mutate { $0.processors.append(WeakRef(processor)) } + } + + public func remove(processor _: T) { + // TODO: Implement + } + + public func removeAllProcessors() { + _state.mutate { $0.processors.removeAll() } + } + + public func buildProcessorChain() -> T? { + let processors = _state.read { $0.processors.compactMap(\.value) } + guard !processors.isEmpty else { return nil } + + for i in 0 ..< (processors.count - 1) { + processors[i].nextProcessor = processors[i + 1] + } + // The last one doesn't have a successor + processors.last?.nextProcessor = nil + + return processors.first + } + + public func invokeProcessor(_ fnc: @escaping (T) -> Void) { + guard let chain = buildProcessorChain() else { return } + fnc(chain) + } +} diff --git a/Sources/LiveKit/Support/WeakRef.swift b/Sources/LiveKit/Support/WeakRef.swift new file mode 100644 index 000000000..f8c521b60 --- /dev/null +++ b/Sources/LiveKit/Support/WeakRef.swift @@ -0,0 +1,23 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +final class WeakRef { + weak var value: T? + + init(_ value: T) { + self.value = value + } +} diff --git a/Tests/LiveKitTests/ProcessorChainTests.swift b/Tests/LiveKitTests/ProcessorChainTests.swift new file mode 100644 index 000000000..abafdebd2 --- /dev/null +++ b/Tests/LiveKitTests/ProcessorChainTests.swift @@ -0,0 +1,134 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@testable import LiveKit +import XCTest + +class ProcessorChainTests: XCTestCase { + // Mock processor for testing + class MockProcessor: NSObject, ChainableProcessor { + weak var nextProcessor: MockProcessor? + var processedValue: Int = 0 + + func process(value: Int) { + processedValue = value + nextProcessor?.process(value: value + 1) + } + } + + var chain: ProcessorChain! + + override func setUp() { + super.setUp() + chain = ProcessorChain() + } + + override func tearDown() { + chain = nil + super.tearDown() + } + + func test_initialState() { + XCTAssertTrue(chain.isProcessorsEmpty) + XCTAssertFalse(chain.isProcessorsNotEmpty) + XCTAssertEqual(chain.countProcessors, 0) + XCTAssertTrue(chain.allProcessors.isEmpty) + } + + func test_addProcessor() { + let processor = MockProcessor() + chain.add(processor: processor) + + XCTAssertFalse(chain.isProcessorsEmpty) + XCTAssertTrue(chain.isProcessorsNotEmpty) + XCTAssertEqual(chain.countProcessors, 1) + XCTAssertEqual(chain.allProcessors.count, 1) + } + + func test_removeProcessor() { + let processor = MockProcessor() + chain.add(processor: processor) + chain.remove(processor: processor) + + XCTAssertTrue(chain.isProcessorsEmpty) + XCTAssertEqual(chain.countProcessors, 0) + XCTAssertTrue(chain.allProcessors.isEmpty) + } + + func test_removeAllProcessors() { + let processor1 = MockProcessor() + let processor2 = MockProcessor() + + chain.add(processor: processor1) + chain.add(processor: processor2) + XCTAssertEqual(chain.countProcessors, 2) + + chain.removeAllProcessors() + XCTAssertTrue(chain.isProcessorsEmpty) + XCTAssertEqual(chain.countProcessors, 0) + } + + func test_buildProcessorChain() { + let processor1 = MockProcessor() + let processor2 = MockProcessor() + let processor3 = MockProcessor() + + chain.add(processor: processor1) + chain.add(processor: processor2) + chain.add(processor: processor3) + + let builtChain = chain.buildProcessorChain() + + XCTAssertNotNil(builtChain) + XCTAssertTrue(builtChain === processor1) + XCTAssertTrue(processor1.nextProcessor === processor2) + XCTAssertTrue(processor2.nextProcessor === processor3) + XCTAssertNil(processor3.nextProcessor) + } + + func test_buildEmptyChain() { + XCTAssertNil(chain.buildProcessorChain()) + } + + func test_invokeProcessor() { + let processor1 = MockProcessor() + let processor2 = MockProcessor() + let processor3 = MockProcessor() + + chain.add(processor: processor1) + chain.add(processor: processor2) + chain.add(processor: processor3) + + chain.invokeProcessor { $0.process(value: 1) } + + XCTAssertEqual(processor1.processedValue, 1) + XCTAssertEqual(processor2.processedValue, 2) + XCTAssertEqual(processor3.processedValue, 3) + } + + func test_weakReference() { + var processor: MockProcessor? = MockProcessor() + chain.add(processor: processor!) + + XCTAssertEqual(chain.countProcessors, 1) + + // Remove strong reference to processor + processor = nil + + // Since we're using weak references, the processor should be removed + XCTAssertEqual(chain.countProcessors, 0) + } +} From 14a122c27edbbe17af1a1755127926a760f84c7a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:31:50 +0900 Subject: [PATCH 2/5] Remove method --- Sources/LiveKit/Support/ProcessorChain.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/LiveKit/Support/ProcessorChain.swift b/Sources/LiveKit/Support/ProcessorChain.swift index 27eac0845..475dad687 100644 --- a/Sources/LiveKit/Support/ProcessorChain.swift +++ b/Sources/LiveKit/Support/ProcessorChain.swift @@ -43,8 +43,13 @@ public class ProcessorChain: NSObject, Loggable { _state.mutate { $0.processors.append(WeakRef(processor)) } } - public func remove(processor _: T) { - // TODO: Implement + public func remove(processor: T) { + _state.mutate { + $0.processors.removeAll { weakRef in + guard let value = weakRef.value else { return false } + return value === processor + } + } } public func removeAllProcessors() { From 89cdeb0902a1e1f6ac3344262af612dfac02895c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:36:35 +0900 Subject: [PATCH 3/5] Update tests --- Sources/LiveKit/Support/ProcessorChain.swift | 6 +++--- Tests/LiveKitTests/ProcessorChainTests.swift | 14 ++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Sources/LiveKit/Support/ProcessorChain.swift b/Sources/LiveKit/Support/ProcessorChain.swift index 475dad687..a3f2a7d7a 100644 --- a/Sources/LiveKit/Support/ProcessorChain.swift +++ b/Sources/LiveKit/Support/ProcessorChain.swift @@ -69,8 +69,8 @@ public class ProcessorChain: NSObject, Loggable { return processors.first } - public func invokeProcessor(_ fnc: @escaping (T) -> Void) { - guard let chain = buildProcessorChain() else { return } - fnc(chain) + public func invokeProcessor(_ fnc: @escaping (T) -> R) -> R? { + guard let chain = buildProcessorChain() else { return nil } + return fnc(chain) } } diff --git a/Tests/LiveKitTests/ProcessorChainTests.swift b/Tests/LiveKitTests/ProcessorChainTests.swift index abafdebd2..e9428b27f 100644 --- a/Tests/LiveKitTests/ProcessorChainTests.swift +++ b/Tests/LiveKitTests/ProcessorChainTests.swift @@ -21,11 +21,10 @@ class ProcessorChainTests: XCTestCase { // Mock processor for testing class MockProcessor: NSObject, ChainableProcessor { weak var nextProcessor: MockProcessor? - var processedValue: Int = 0 - func process(value: Int) { - processedValue = value - nextProcessor?.process(value: value + 1) + func process(value: Int) -> Int { + let result = value + 1 + return nextProcessor?.process(value: result) ?? result } } @@ -112,11 +111,10 @@ class ProcessorChainTests: XCTestCase { chain.add(processor: processor2) chain.add(processor: processor3) - chain.invokeProcessor { $0.process(value: 1) } + let result = chain.invokeProcessor { $0.process(value: 0) } - XCTAssertEqual(processor1.processedValue, 1) - XCTAssertEqual(processor2.processedValue, 2) - XCTAssertEqual(processor3.processedValue, 3) + // Each processor adds 1, so with 3 processors the final result should be 3 + XCTAssertEqual(result, 3) } func test_weakReference() { From 792e85dc592bac22019b733407e263d8a6fe6b86 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:47:47 +0900 Subject: [PATCH 4/5] Refactor --- .../{ChainableProcessor.swift => ChainedProcessor.swift} | 2 +- .../{ProcessorChain.swift => ProcessingChain.swift} | 2 +- ...ocessorChainTests.swift => ProcessingChainTests.swift} | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename Sources/LiveKit/Protocols/{ChainableProcessor.swift => ChainedProcessor.swift} (93%) rename Sources/LiveKit/Support/{ProcessorChain.swift => ProcessingChain.swift} (96%) rename Tests/LiveKitTests/{ProcessorChainTests.swift => ProcessingChainTests.swift} (95%) diff --git a/Sources/LiveKit/Protocols/ChainableProcessor.swift b/Sources/LiveKit/Protocols/ChainedProcessor.swift similarity index 93% rename from Sources/LiveKit/Protocols/ChainableProcessor.swift rename to Sources/LiveKit/Protocols/ChainedProcessor.swift index 5817948fd..7580ef56a 100644 --- a/Sources/LiveKit/Protocols/ChainableProcessor.swift +++ b/Sources/LiveKit/Protocols/ChainedProcessor.swift @@ -17,7 +17,7 @@ import Foundation @objc -public protocol ChainableProcessor: AnyObject { +public protocol ChainedProcessor: AnyObject { // Next object in the chain. weak var nextProcessor: Self? { get set } } diff --git a/Sources/LiveKit/Support/ProcessorChain.swift b/Sources/LiveKit/Support/ProcessingChain.swift similarity index 96% rename from Sources/LiveKit/Support/ProcessorChain.swift rename to Sources/LiveKit/Support/ProcessingChain.swift index a3f2a7d7a..5070d8bd8 100644 --- a/Sources/LiveKit/Support/ProcessorChain.swift +++ b/Sources/LiveKit/Support/ProcessingChain.swift @@ -16,7 +16,7 @@ import Foundation -public class ProcessorChain: NSObject, Loggable { +public class ProcessingChain: NSObject, Loggable { // MARK: - Public properties public var isProcessorsEmpty: Bool { countProcessors == 0 } diff --git a/Tests/LiveKitTests/ProcessorChainTests.swift b/Tests/LiveKitTests/ProcessingChainTests.swift similarity index 95% rename from Tests/LiveKitTests/ProcessorChainTests.swift rename to Tests/LiveKitTests/ProcessingChainTests.swift index e9428b27f..5008f973e 100644 --- a/Tests/LiveKitTests/ProcessorChainTests.swift +++ b/Tests/LiveKitTests/ProcessingChainTests.swift @@ -17,9 +17,9 @@ @testable import LiveKit import XCTest -class ProcessorChainTests: XCTestCase { +class ProcessingChainTests: XCTestCase { // Mock processor for testing - class MockProcessor: NSObject, ChainableProcessor { + class MockProcessor: NSObject, ChainedProcessor { weak var nextProcessor: MockProcessor? func process(value: Int) -> Int { @@ -28,11 +28,11 @@ class ProcessorChainTests: XCTestCase { } } - var chain: ProcessorChain! + var chain: ProcessingChain! override func setUp() { super.setUp() - chain = ProcessorChain() + chain = ProcessingChain() } override func tearDown() { From 89d167227c834145b1689f10f95c2a2e377bb5c4 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:08:47 +0900 Subject: [PATCH 5/5] Fix tests --- Sources/LiveKit/Protocols/ChainedProcessor.swift | 3 +-- Tests/LiveKitTests/ProcessingChainTests.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/LiveKit/Protocols/ChainedProcessor.swift b/Sources/LiveKit/Protocols/ChainedProcessor.swift index 7580ef56a..0647f3170 100644 --- a/Sources/LiveKit/Protocols/ChainedProcessor.swift +++ b/Sources/LiveKit/Protocols/ChainedProcessor.swift @@ -16,8 +16,7 @@ import Foundation -@objc public protocol ChainedProcessor: AnyObject { // Next object in the chain. - weak var nextProcessor: Self? { get set } + var nextProcessor: Self? { get set } } diff --git a/Tests/LiveKitTests/ProcessingChainTests.swift b/Tests/LiveKitTests/ProcessingChainTests.swift index 5008f973e..dad346f8c 100644 --- a/Tests/LiveKitTests/ProcessingChainTests.swift +++ b/Tests/LiveKitTests/ProcessingChainTests.swift @@ -19,7 +19,7 @@ import XCTest class ProcessingChainTests: XCTestCase { // Mock processor for testing - class MockProcessor: NSObject, ChainedProcessor { + final class MockProcessor: NSObject, ChainedProcessor { weak var nextProcessor: MockProcessor? func process(value: Int) -> Int {