Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ custom_rules:
newline_before_brace:
name: "Closing braces shouldn't have empty lines before them"
regex: '\n\n\}'
sendable_order:
name: "@escaping should precede @Sendable when used together"
regex: '@Sendable\s+@escaping'
space_before_comma:
name: "Commas should never have a space before them"
regex: '\s+,'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SwiftProtobuf
/// superficiality of human-computer communication. DOCTOR simulates a
/// psychotherapist, and is commonly found as an Easter egg in emacs
/// distributions.
internal protocol Connectrpc_Eliza_V1_ElizaServiceClientInterface {
internal protocol Connectrpc_Eliza_V1_ElizaServiceClientInterface: Sendable {

/// Say is a unary RPC. Eliza responds to the prompt with a single sentence.
@available(iOS 13, *)
Expand All @@ -32,7 +32,7 @@ internal protocol Connectrpc_Eliza_V1_ElizaServiceClientInterface {
}

/// Concrete implementation of `Connectrpc_Eliza_V1_ElizaServiceClientInterface`.
internal final class Connectrpc_Eliza_V1_ElizaServiceClient: Connectrpc_Eliza_V1_ElizaServiceClientInterface {
internal final class Connectrpc_Eliza_V1_ElizaServiceClient: Connectrpc_Eliza_V1_ElizaServiceClientInterface, Sendable {
private let client: Connect.ProtocolClientInterface

internal init(client: Connect.ProtocolClientInterface) {
Expand Down
9 changes: 5 additions & 4 deletions Libraries/Connect/Implementation/Codecs/JSONCodec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
// limitations under the License.

import Foundation
import SwiftProtobuf
// TODO: Remove `@preconcurrency` once `SwiftProtobuf.JSON{Encoding|Decoding}Options` are `Sendable`
@preconcurrency import SwiftProtobuf

/// Codec providing functionality for serializing to/from JSON.
public struct JSONCodec {
public struct JSONCodec: Sendable {
private let encodingOptions: JSONEncodingOptions
private let decodingOptions: JSONDecodingOptions = {
var options = JSONDecodingOptions()
Expand All @@ -43,11 +44,11 @@ extension JSONCodec: Codec {
return "json"
}

public func serialize<Input: SwiftProtobuf.Message>(message: Input) throws -> Data {
public func serialize<Input: ProtobufMessage>(message: Input) throws -> Data {
return try message.jsonUTF8Data(options: self.encodingOptions)
}

public func deserialize<Output: SwiftProtobuf.Message>(source: Data) throws -> Output {
public func deserialize<Output: ProtobufMessage>(source: Data) throws -> Output {
return try Output(jsonUTF8Data: source, options: self.decodingOptions)
}
}
4 changes: 2 additions & 2 deletions Libraries/Connect/Implementation/Codecs/ProtoCodec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ extension ProtoCodec: Codec {
return "proto"
}

public func serialize<Input: SwiftProtobuf.Message>(message: Input) throws -> Data {
public func serialize<Input: ProtobufMessage>(message: Input) throws -> Data {
return try message.serializedData()
}

public func deserialize<Output: SwiftProtobuf.Message>(source: Data) throws -> Output {
public func deserialize<Output: ProtobufMessage>(source: Data) throws -> Output {
return try Output(serializedData: source)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ extension ConnectInterceptor: Interceptor {
}

func streamFunction() -> StreamFunction {
var responseHeaders: Headers?
let responseHeaders = Locked<Headers?>(nil)
return StreamFunction(
requestFunction: { request in
var headers = request.headers
Expand All @@ -124,12 +124,12 @@ extension ConnectInterceptor: Interceptor {
streamResultFunction: { result in
switch result {
case .headers(let headers):
responseHeaders = headers
responseHeaders.value = headers
return result

case .message(let data):
do {
let responseCompressionPool = responseHeaders?[
let responseCompressionPool = responseHeaders.value?[
HeaderConstants.connectStreamingContentEncoding
]?.first.flatMap { self.config.responseCompressionPool(forName: $0) }
let (headerByte, message) = try Envelope.unpackMessage(
Expand Down Expand Up @@ -161,7 +161,7 @@ extension ConnectInterceptor: Interceptor {
code: code,
error: ConnectError.from(
code: code,
headers: responseHeaders ?? [:],
headers: responseHeaders.value ?? [:],
source: nil
),
trailers: trailers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ extension GRPCWebInterceptor: Interceptor {
}

func streamFunction() -> StreamFunction {
var responseHeaders: Headers?
let responseHeaders = Locked<Headers?>(nil)
return StreamFunction(
requestFunction: { request in
return HTTPRequest(
Expand All @@ -137,13 +137,13 @@ extension GRPCWebInterceptor: Interceptor {
trailers: headers
)
} else {
responseHeaders = headers
responseHeaders.value = headers
return result
}

case .message(let data):
do {
let responseCompressionPool = responseHeaders?[
let responseCompressionPool = responseHeaders.value?[
HeaderConstants.grpcContentEncoding
]?.first.flatMap { self.config.responseCompressionPool(forName: $0) }
let (headerByte, unpackedData) = try Envelope.unpackMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

/// Represents a chain of interceptors that is used for a single request/stream,
/// and orchestrates invoking each of them in the proper order.
struct InterceptorChain {
struct InterceptorChain: Sendable {
private let interceptors: [Interceptor]

/// Initialize the interceptor chain.
Expand All @@ -23,9 +23,7 @@ struct InterceptorChain {
///
/// - parameter interceptors: Closures that should be called to create interceptors.
/// - parameter config: Config to use for setting up interceptors.
init(
interceptors: [(ProtocolClientConfig) -> Interceptor], config: ProtocolClientConfig
) {
init(interceptors: [InterceptorInitializer], config: ProtocolClientConfig) {
self.interceptors = interceptors.map { initialize in initialize(config) }
}

Expand Down Expand Up @@ -85,7 +83,7 @@ struct InterceptorChain {
}
}

private func executeInterceptors<T>(_ interceptors: [(T) -> T], initial: T) -> T {
private func executeInterceptors<T>(_ interceptors: [@Sendable (T) -> T], initial: T) -> T {
var next = initial
for interceptor in interceptors {
next = interceptor(next)
Expand Down
7 changes: 6 additions & 1 deletion Libraries/Connect/Implementation/Lock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import Foundation

/// Internal implementation of a lock. Wraps usage of `os_unfair_lock`.
final class Lock {
final class Lock: Sendable {
private let underlyingLock: UnsafeMutablePointer<os_unfair_lock>

init() {
Expand All @@ -24,6 +24,11 @@ final class Lock {
self.underlyingLock.initialize(to: os_unfair_lock())
}

/// Perform an action within the context of the lock.
///
/// - parameter action: Closure to be executed in the context of the lock.
///
/// - returns: The result of the closure.
func perform<T>(action: @escaping () -> T) -> T {
os_unfair_lock_lock(self.underlyingLock)
defer { os_unfair_lock_unlock(self.underlyingLock) }
Expand Down
41 changes: 41 additions & 0 deletions Libraries/Connect/Implementation/Locked.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2022-2023 Buf Technologies, Inc.
//
// 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

/// Class containing an internal lock which can be used to ensure thread-safe access to an
/// underlying value. Conforms to `Sendable`, making it accessible from `@Sendable` closures.
public final class Locked<T>: @unchecked Sendable {
private let lock = Lock()
private var wrappedValue: T

/// Thread-safe access to the underlying value.
public var value: T {
get { self.lock.perform { self.wrappedValue } }
set { self.lock.perform { self.wrappedValue = newValue } }
}

/// Perform an action with the underlying value, potentially updating that value.
///
/// - parameter action: Closure to perform with the underlying value.
public func perform(action: @escaping (inout T) -> Void) {
self.lock.perform {
action(&self.wrappedValue)
}
}

public init(_ value: T) {
self.wrappedValue = value
}
}
Loading