diff --git a/.changes/remove-swift-collections b/.changes/remove-swift-collections new file mode 100644 index 000000000..083b0382e --- /dev/null +++ b/.changes/remove-swift-collections @@ -0,0 +1 @@ +patch type="changed" "Remove swift-collections dependency by replacing with minimal internal implementations" diff --git a/AGENTS.md b/AGENTS.md index 5ca5a4c28..cd819cc5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ Key components: - `StateSync` - thread-safe state container with `@dynamicMemberLookup`; triggers `onDidMutate` callbacks - `MulticastDelegate` - weak-reference delegate collection for event broadcasting -Dependencies: LiveKitWebRTC, LiveKitUniFFI, SwiftProtobuf, swift-collections. +Dependencies: LiveKitWebRTC, LiveKitUniFFI, SwiftProtobuf. ## WebRTC diff --git a/LiveKitClient.podspec b/LiveKitClient.podspec index d731224a4..b42ccc746 100644 --- a/LiveKitClient.podspec +++ b/LiveKitClient.podspec @@ -17,8 +17,6 @@ Pod::Spec.new do |spec| spec.dependency("LiveKitWebRTC", "= 144.7559.01") spec.dependency("LiveKitUniFFI", "= 0.0.5") spec.dependency("SwiftProtobuf") - spec.dependency("DequeModule", "= 1.1.4") - spec.dependency("OrderedCollections", " = 1.1.4") spec.resource_bundles = {"Privacy" => ["Sources/LiveKit/PrivacyInfo.xcprivacy"]} diff --git a/Package.swift b/Package.swift index 64ebe29fe..6d1deb55c 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,6 @@ let package = Package( .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "144.7559.01"), .package(url: "https://github.com/livekit/livekit-uniffi-xcframework.git", exact: "0.0.5"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.0"), - .package(url: "https://github.com/apple/swift-collections.git", "1.1.0" ..< "1.3.0"), // Only used for DocC generation .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"), ], @@ -37,8 +36,6 @@ let package = Package( .product(name: "LiveKitWebRTC", package: "webrtc-xcframework"), .product(name: "LiveKitUniFFI", package: "livekit-uniffi-xcframework"), .product(name: "SwiftProtobuf", package: "swift-protobuf"), - .product(name: "DequeModule", package: "swift-collections"), - .product(name: "OrderedCollections", package: "swift-collections"), "LKObjCHelpers", ], exclude: [ diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index debe7873a..2e9d7dbfc 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -22,7 +22,6 @@ let package = Package( .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "144.7559.01"), .package(url: "https://github.com/livekit/livekit-uniffi-xcframework.git", exact: "0.0.5"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.0"), - .package(url: "https://github.com/apple/swift-collections.git", "1.1.0" ..< "1.3.0"), // Only used for DocC generation .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"), ], @@ -37,8 +36,6 @@ let package = Package( .product(name: "LiveKitWebRTC", package: "webrtc-xcframework"), .product(name: "LiveKitUniFFI", package: "livekit-uniffi-xcframework"), .product(name: "SwiftProtobuf", package: "swift-protobuf"), - .product(name: "DequeModule", package: "swift-collections"), - .product(name: "OrderedCollections", package: "swift-collections"), "LKObjCHelpers", ], exclude: [ diff --git a/Package@swift-6.2.swift b/Package@swift-6.2.swift index a3797d98f..24b2d1a9c 100644 --- a/Package@swift-6.2.swift +++ b/Package@swift-6.2.swift @@ -23,7 +23,6 @@ let package = Package( .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "144.7559.01"), .package(url: "https://github.com/livekit/livekit-uniffi-xcframework.git", exact: "0.0.5"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.0"), - .package(url: "https://github.com/apple/swift-collections.git", "1.1.0" ..< "1.3.0"), // Only used for DocC generation .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"), ], @@ -38,8 +37,6 @@ let package = Package( .product(name: "LiveKitWebRTC", package: "webrtc-xcframework"), .product(name: "LiveKitUniFFI", package: "livekit-uniffi-xcframework"), .product(name: "SwiftProtobuf", package: "swift-protobuf"), - .product(name: "DequeModule", package: "swift-collections"), - .product(name: "OrderedCollections", package: "swift-collections"), "LKObjCHelpers", ], exclude: [ diff --git a/Sources/LiveKit/Agent/Session.swift b/Sources/LiveKit/Agent/Session.swift index 5c8d5c48b..f21c4972a 100644 --- a/Sources/LiveKit/Agent/Session.swift +++ b/Sources/LiveKit/Agent/Session.swift @@ -16,7 +16,6 @@ import Combine import Foundation -import OrderedCollections /// A ``Session`` represents a connection to a LiveKit Room that can contain an ``Agent``. /// diff --git a/Sources/LiveKit/Core/DataChannelPair.swift b/Sources/LiveKit/Core/DataChannelPair.swift index 53f22e3f0..85710f8e1 100644 --- a/Sources/LiveKit/Core/DataChannelPair.swift +++ b/Sources/LiveKit/Core/DataChannelPair.swift @@ -16,7 +16,6 @@ // swiftlint:disable file_length -import DequeModule import Foundation internal import LiveKitWebRTC diff --git a/Sources/LiveKit/Support/DataStructures/Deque.swift b/Sources/LiveKit/Support/DataStructures/Deque.swift new file mode 100644 index 000000000..d523c922b --- /dev/null +++ b/Sources/LiveKit/Support/DataStructures/Deque.swift @@ -0,0 +1,74 @@ +/* + * Copyright 2026 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. + */ + +// API inspired by swift-collections by Apple Inc. +// https://github.com/apple/swift-collections + +/// A double-ended queue backed by a circular buffer. +/// Provides O(1) amortized `append` and `removeFirst`. +struct Deque: ExpressibleByArrayLiteral { + private var buffer: [Element?] = [] + private var head = 0 + private var count_ = 0 + + init() {} + + init(arrayLiteral elements: Element...) { + buffer = elements.map { $0 } + count_ = elements.count + } + + var isEmpty: Bool { count_ == 0 } + + var first: Element? { + guard count_ > 0 else { return nil } + return buffer[head] + } + + mutating func append(_ element: Element) { + if count_ == buffer.count { + grow() + } + let tail = (head + count_) & (buffer.count - 1) + buffer[tail] = element + count_ += 1 + } + + @discardableResult + mutating func removeFirst() -> Element { + guard count_ > 0 else { + preconditionFailure("Cannot removeFirst from an empty Deque") + } + let element = buffer[head]! + buffer[head] = nil + head = (head + 1) & (buffer.count - 1) + count_ -= 1 + return element + } + + private mutating func grow() { + let newCapacity = max(buffer.count * 2, 4) + var newBuffer = [Element?](repeating: nil, count: newCapacity) + let mask = buffer.count - 1 + for i in 0 ..< count_ { + newBuffer[i] = buffer[(head + i) & mask] + } + buffer = newBuffer + head = 0 + } +} + +extension Deque: Sendable where Element: Sendable {} diff --git a/Sources/LiveKit/Support/DataStructures/OrderedDictionary.swift b/Sources/LiveKit/Support/DataStructures/OrderedDictionary.swift new file mode 100644 index 000000000..58227104e --- /dev/null +++ b/Sources/LiveKit/Support/DataStructures/OrderedDictionary.swift @@ -0,0 +1,97 @@ +/* + * Copyright 2026 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. + */ + +// API inspired by swift-collections by Apple Inc. +// https://github.com/apple/swift-collections + +struct OrderedDictionary: ExpressibleByDictionaryLiteral { + private var pairs: [(key: Key, value: Value)] = [] + private var index: [Key: Int] = [:] + + init() {} + + init(dictionaryLiteral elements: (Key, Value)...) { + for (key, value) in elements { + pairs.append((key, value)) + index[key] = pairs.count - 1 + } + } + + init(uniqueKeysWithValues keysAndValues: some Sequence<(Key, Value)>) { + for (key, value) in keysAndValues { + if let i = index[key] { + pairs[i] = (key, value) + } else { + index[key] = pairs.count + pairs.append((key, value)) + } + } + } + + var count: Int { pairs.count } + + var keys: Keys { Keys(pairs: pairs) } + var values: Values { Values(pairs: pairs) } + + subscript(key: Key) -> Value? { + get { + guard let i = index[key] else { return nil } + return pairs[i].value + } + set { + if let newValue { + if let i = index[key] { + pairs[i] = (key, newValue) + } else { + index[key] = pairs.count + pairs.append((key, newValue)) + } + } else if let i = index.removeValue(forKey: key) { + pairs.remove(at: i) + for j in i ..< pairs.count { + index[pairs[j].key] = j + } + } + } + } + + @discardableResult + mutating func updateValue(_ value: Value, forKey key: Key) -> Value? { + if let i = index[key] { + let old = pairs[i].value + pairs[i] = (key, value) + return old + } + index[key] = pairs.count + pairs.append((key, value)) + return nil + } + + struct Keys { + fileprivate let pairs: [(key: Key, value: Value)] + subscript(position: Int) -> Key { pairs[position].key } + } + + struct Values { + fileprivate let pairs: [(key: Key, value: Value)] + var elements: [Value] { pairs.map(\.value) } + subscript(position: Int) -> Value { pairs[position].value } + } +} + +extension OrderedDictionary: Sendable where Key: Sendable, Value: Sendable {} +extension OrderedDictionary.Keys: Sendable where Key: Sendable, Value: Sendable {} +extension OrderedDictionary.Values: Sendable where Key: Sendable, Value: Sendable {} diff --git a/Sources/LiveKit/Support/DataStructures/OrderedSet.swift b/Sources/LiveKit/Support/DataStructures/OrderedSet.swift new file mode 100644 index 000000000..da1cd46ed --- /dev/null +++ b/Sources/LiveKit/Support/DataStructures/OrderedSet.swift @@ -0,0 +1,40 @@ +/* + * Copyright 2026 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. + */ + +// API inspired by swift-collections by Apple Inc. +// https://github.com/apple/swift-collections + +struct OrderedSet { + private var array: [Element] = [] + private var set: [Element: Int] = [:] + + init() {} + + var elements: [Element] { array } + + @discardableResult + mutating func append(_ element: Element) -> (inserted: Bool, index: Int) { + if let existing = set[element] { + return (inserted: false, index: existing) + } + let index = array.count + array.append(element) + set[element] = index + return (inserted: true, index: index) + } +} + +extension OrderedSet: Sendable where Element: Sendable {} diff --git a/Sources/LiveKit/Track/Metrics/MetricsManager.swift b/Sources/LiveKit/Track/Metrics/MetricsManager.swift index 562e3cf3b..de7c7c6e1 100644 --- a/Sources/LiveKit/Track/Metrics/MetricsManager.swift +++ b/Sources/LiveKit/Track/Metrics/MetricsManager.swift @@ -15,7 +15,6 @@ */ import Foundation -import OrderedCollections // MARK: - Triggers diff --git a/Tests/LiveKitCoreTests/Agent/TranscriptionTests.swift b/Tests/LiveKitCoreTests/Agent/TranscriptionTests.swift index 6a1f81610..683493a78 100644 --- a/Tests/LiveKitCoreTests/Agent/TranscriptionTests.swift +++ b/Tests/LiveKitCoreTests/Agent/TranscriptionTests.swift @@ -15,7 +15,6 @@ */ @testable import LiveKit -import OrderedCollections #if canImport(LiveKitTestSupport) import LiveKitTestSupport #endif