Skip to content

Commit

Permalink
Merge pull request #26 from scorebet/connect-1.15.3
Browse files Browse the repository at this point in the history
update Connect with  1.15.3
  • Loading branch information
tahirmt authored Nov 27, 2024
2 parents a2ffea0 + 272e6d0 commit 826b0a1
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 61 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Change Log

## v1.15.3

### Improvements
- **Stable sort schema types for SchemaMetadata ([#514](https://github.com/apollographql/apollo-ios-dev/pull/514)):** _Thank you to [@asmundg](https://github.com/asmundg) for the contribution._

### Fixed
- **Fix multipart delimter boundary parsing ([#502](https://github.com/apollographql/apollo-ios-dev/pull/502)):** The multipart message parsing code was not splitting message chunks at the correct boundary when the dash boundary was present in the message body.
- **Fix Websocket error broadcast for unsubscribed ID ([#506](https://github.com/apollographql/apollo-ios-dev/pull/506))** Only broadcast an error to all subscribers if there was no id field in the message.
- **Fix bug with `AnyHashable` coercion for non-iOS platforms ([#517](https://github.com/apollographql/apollo-ios-dev/pull/517)):** Extended the _AnyHashableCanBeCoerced check to include macOS, watchOS, and tvOS with their respective minimum versions. _Thank you to [@VMLe](https://github.com/VMLe) for the fix._
- **Fix assigning websocket callback queue before connecting ([#529](https://github.com/apollographql/apollo-ios-dev/pull/529)):** Fixed a race condition with the callback queue assignment during an unstable connection.
- **Fix `GraphQLOperation` hash uniqueness ([#530](https://github.com/apollographql/apollo-ios-dev/pull/530)):** Adding uniqueness to GraphQLOperation hashing.

## v1.15.2

### Improvements
- **Set `URLRequest` cache policy on GET requests ([#476](https://github.com/apollographql/apollo-ios-dev/pull/476)):** Uses the Apollo cache policy to set a comparable cache policy on `URLRequest`. Previously there was no way to opt-out of default `URLRequest` caching behaviour.
- **Batch writing records to the SQLite store ([#498](https://github.com/apollographql/apollo-ios-dev/pull/498)):** Uses the `insertMany` to batch write records for a given operation vs previously performing a write for each individual record.

### Fixed
- **Fix `ListData` type check ([#473](https://github.com/apollographql/apollo-ios-dev/pull/473)):** Fixed bool type check in `ListData`.
- **Remove local cache mutation type condition setter ([#485](https://github.com/apollographql/apollo-ios-dev/pull/485)):** Removes the setter for mutable inline fragments. The correct way to initialize with a type condition is to use `asRootEntityType`.

## v1.15.1

### Fixed
- **Fix decoding of deprecated `selectionSetInitializer` option `localCacheMutations` ([#467](https://github.com/apollographql/apollo-ios-dev/pull/467)):** This option was deprecated in `1.15.0`, and the removal of the code to parse the option resulted in a validation error when the deprecated option was present in the JSON code generation config file. This is now fixed so that the option is ignored but does not cause code generation to fail.
- **Disfavour deprecated watch function ([#469](https://github.com/apollographql/apollo-ios-dev/pull/469)):** A deprecated version of the `watch` function matched the overload of the current version if certain parameters were omitted. This caused an incorrect deprecation warning in this situation. We've fixed this by adding `@_disfavoredOverload` to the deprecated function signature.

## v1.15.0

### New
Expand Down
Binary file modified CLI/apollo-ios-cli.tar.gz
Binary file not shown.
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// swift-tools-version:5.9
//
// The swift-tools-version declares the minimum version of Swift required to build this package.
// Swift 5.9 is available from Xcode 15.0.


import PackageDescription

Expand Down
28 changes: 20 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🔮 Apollo iOS Roadmap

**Last updated: 2024-08-13**
**Last updated: 2024-10-29**

For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-ios/blob/main/CHANGELOG.md).

Expand Down Expand Up @@ -33,7 +33,7 @@ The `@defer` directive enables your queries to receive data for specific fields

### [Improvements to code generation configuration and performance](https://github.com/apollographql/apollo-ios/milestone/67)

_Approximate Date: to be released incrementally_
_Status: To be released incrementally_

- This effort encompasses several smaller features:
- ✅ Make codegen support Swift concurrency (`async`/`await`): available in v1.7.0
Expand All @@ -44,40 +44,52 @@ _Approximate Date: to be released incrementally_

To support the breaking language changes in Swift 6, a major version 2.0 of Apollo iOS will be released. This version will include support for the new Swift Concurrency Model and improve upon networking and caching APIs.

_Approximate Date: Beta release in September alongside Xcode 16 & Swift 6 stable release
_Status: In design phase. Current RFC for design is available [here](https://github.com/apollographql/apollo-ios/issues/3411)._

-[`ExistentialAny` upcoming feature](https://github.com/apollographql/apollo-ios/issues/3205)
- (in progress) [`Sendable` types and `async/await` APIs](https://github.com/apollographql/apollo-ios/issues/3291)

### `@oneOf` Input Object Support

_Status: Awaiting final approval of RFC into the GraphQL specification._

For more information on this feature, see the [RFC](https://github.com/graphql/graphql-spec/pull/825) for its addition to the GraphQL specification.

### [Reduce generated schema types](https://github.com/apollographql/apollo-ios/milestone/71)

_Approximate Date: TBD_
_Status: Not started_

- Right now we are naively generating schema types that we don't always need. A smarter algorithm can reduce generated code for certain large schemas that are currently having every type in their schema generated
- Create configuration for manually indicating schema types you would like to have schema types and TestMocks generated for

### [Mutable generated reponse models](https://github.com/apollographql/apollo-ios/issues/3246)

_Approximate Date: TBD_
_Status: Not started_

- Provide a mechanism for making generated reponse models mutable.
- This will allow mutability on an opt-in basis per selection set or definition.

### [Support codegen of operations without response models](https://github.com/apollographql/apollo-ios/issues/3165)

_Approximate Date: TBD_
_Status: Not started_

- Support generating models that expose only the minimal necessary data for operation execution (networking and caching).
- This would remove the generated response models, exposing response data as a simple `JSONObject` (ie. [String: AnyHashable]).
- This would remove the generated response models, exposing response data as a simple `JSONObject` (ie. [String: AnyHashable]).
- This feature is useful for projects that want to use their own custom data models or have binary size constraints.

### Declarative caching

_Approximate Date: TBD_
_Status: Not started_

- Similar to Apollo Kotlin [declarative caching](https://www.apollographql.com/docs/kotlin/caching/declarative-ids) via the `@typePolicy` directive
- Provide ability to configure cache keys using directives on schema types as an alternative to programmatic cache key configuration

### Semantic Nullability

_Status: Feature Design_

We are active participants in the [Nullability Working Group](https://github.com/graphql/nullability-wg/) and are planning to ship experimental support for @semanticNonNull, @catch, etc. based on Apollo Kotlin’s (link-to-docs) soon. Future iterations are expected but it’s too early to tell what those might be.

## [Apollo iOS Pagination](https://github.com/apollographql/apollo-ios-pagination)

Version 0.1 of this module was released in March 2024. We are iterating quickly based on user feedback - please see the project's Issues and PRs for up-to-date information. We expect the API to become more stable over time and will consider a v1 release when appropriate.
Expand Down
1 change: 1 addition & 0 deletions Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ extension ApolloClient: ApolloClientProtocol {

extension ApolloClient {

@_disfavoredOverload
@available(*, deprecated,
renamed: "watch(query:cachePolicy:refetchOnFailedUpdates:context:callbackQueue:resultHandler:)")
public func watch<Query: GraphQLQuery>(
Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/Constants.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation

public enum Constants {
public static let ApolloVersion: String = "1.15.0"
public static let ApolloVersion: String = "1.15.3"
}
19 changes: 18 additions & 1 deletion Sources/Apollo/JSONRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ open class JSONRequest<Operation: GraphQLOperation>: HTTPRequest<Operation> {
if let urlForGet = transformer.createGetURL() {
request.url = urlForGet
request.httpMethod = GraphQLHTTPMethod.GET.rawValue

request.cachePolicy = requestCachePolicy

// GET requests shouldn't have a content-type since they do not provide actual content.
request.allHTTPHeaderFields?.removeValue(forKey: "Content-Type")
} else {
Expand Down Expand Up @@ -150,6 +151,22 @@ open class JSONRequest<Operation: GraphQLOperation>: HTTPRequest<Operation> {
return body
}

/// Convert the Apollo iOS cache policy into a matching cache policy for URLRequest.
private var requestCachePolicy: URLRequest.CachePolicy {
switch cachePolicy {
case .returnCacheDataElseFetch:
return .returnCacheDataElseLoad
case .fetchIgnoringCacheData:
return .reloadIgnoringLocalCacheData
case .fetchIgnoringCacheCompletely:
return .reloadIgnoringLocalAndRemoteCacheData
case .returnCacheDataDontFetch:
return .returnCacheDataDontLoad
case .returnCacheDataAndFetch:
return .reloadRevalidatingCacheData
}
}

// MARK: - Equtable/Hashable Conformance

public static func == (lhs: JSONRequest<Operation>, rhs: JSONRequest<Operation>) -> Bool {
Expand Down
48 changes: 44 additions & 4 deletions Sources/Apollo/MultipartResponseParsingInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,14 @@ public struct MultipartResponseParsingInterceptor: ApolloInterceptor {
return
}

for chunk in dataString.components(separatedBy: "--\(boundary)") {
if chunk.isEmpty || chunk.isBoundaryMarker { continue }
// Parsing Notes:
//
// Multipart messages arriving here may consist of more than one chunk, but they are always
// expected to be complete chunks. Downstream protocol specification parsers are only built
// to handle the protocol specific message formats, i.e.: data between the multipart delimiter.
let boundaryDelimiter = Self.boundaryDelimiter(with: boundary)
for chunk in dataString.components(separatedBy: boundaryDelimiter) {
if chunk.isEmpty || chunk.isDashBoundaryPrefix || chunk.isMultipartNewLine { continue }

switch parser.parse(chunk) {
case let .success(data):
Expand Down Expand Up @@ -119,6 +125,8 @@ public struct MultipartResponseParsingInterceptor: ApolloInterceptor {
}
}

// MARK: Specification Parser Protocol

/// A protocol that multipart response parsers must conform to in order to be added to the list of
/// available response specification parsers.
protocol MultipartResponseSpecificationParser {
Expand All @@ -140,6 +148,38 @@ extension MultipartResponseSpecificationParser {
static var dataLineSeparator: StaticString { "\r\n\r\n" }
}

fileprivate extension String {
var isBoundaryMarker: Bool { self == "--" }
// MARK: Helpers

extension MultipartResponseParsingInterceptor {
static func boundaryDelimiter(with boundary: String) -> String {
"\r\n--\(boundary)"
}

static func closeBoundaryDelimiter(with boundary: String) -> String {
boundaryDelimiter(with: boundary) + "--"
}
}

extension String {
fileprivate var isDashBoundaryPrefix: Bool { self == "--" }
fileprivate var isMultipartNewLine: Bool { self == "\r\n" }

/// Returns the range of a complete multipart chunk.
func multipartRange(using boundary: String) -> String.Index? {
// The end boundary marker indicates that no further chunks will follow so if this delimiter
// if found then include the delimiter in the index. Search for this first.
let closeBoundaryDelimiter = MultipartResponseParsingInterceptor.closeBoundaryDelimiter(with: boundary)
if let endIndex = range(of: closeBoundaryDelimiter, options: .backwards)?.upperBound {
return endIndex
}

// A chunk boundary indicates there may still be more chunks to follow so the index need not
// include the chunk boundary in the index.
let boundaryDelimiter = MultipartResponseParsingInterceptor.boundaryDelimiter(with: boundary)
if let chunkIndex = range(of: boundaryDelimiter, options: .backwards)?.lowerBound {
return chunkIndex
}

return nil
}
}
21 changes: 13 additions & 8 deletions Sources/Apollo/URLSessionClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,23 +298,28 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
taskData.append(additionalData: data)

if let httpResponse = dataTask.response as? HTTPURLResponse, httpResponse.isMultipart {
let multipartHeaderComponents = httpResponse.multipartHeaderComponents
guard let boundaryString = multipartHeaderComponents.boundary else {
guard let boundary = httpResponse.multipartHeaderComponents.boundary else {
taskData.completionBlock(.failure(URLSessionClientError.missingMultipartBoundary))
return
}

let boundaryMarker = "--\(boundaryString)"
// Parsing Notes:
//
// Multipart messages are parsed here only to look for complete chunks to pass on to the downstream
// parsers. Any leftover data beyond a delimited chunk is held back for more data to arrive.
//
// Do not return `.failure` here simply because there was no boundary delimiter found; the
// data may still be arriving. If the request ends without more data arriving it will get handled
// in urlSession(_:task:didCompleteWithError:).
guard
let dataString = String(data: taskData.data, encoding: .utf8)?.trimmingCharacters(in: .newlines),
let lastBoundaryIndex = dataString.range(of: boundaryMarker, options: .backwards)?.upperBound,
let boundaryData = dataString.prefix(upTo: lastBoundaryIndex).data(using: .utf8)
let dataString = String(data: taskData.data, encoding: .utf8),
let lastBoundaryDelimiterIndex = dataString.multipartRange(using: boundary),
let boundaryData = dataString.prefix(upTo: lastBoundaryDelimiterIndex).data(using: .utf8)
else {
taskData.completionBlock(.failure(URLSessionClientError.cannotParseBoundaryData))
return
}

let remainingData = dataString.suffix(from: lastBoundaryIndex).data(using: .utf8)
let remainingData = dataString.suffix(from: lastBoundaryDelimiterIndex).data(using: .utf8)
taskData.reset(data: remainingData)

if let rawCompletion = taskData.rawCompletion {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ApolloAPI/DataDict.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ extension DataDict {
/// we need to do some additional unwrapping and casting of the values to avoid crashes and other
/// run time bugs.
public static let _AnyHashableCanBeCoerced: Bool = {
if #available(iOS 14.5, *) {
if #available(iOS 14.5, macOS 11.3, tvOS 14.5, watchOS 7.4, *) {
return true
} else {
return false
Expand Down
2 changes: 2 additions & 0 deletions Sources/ApolloAPI/GraphQLOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public extension GraphQLOperation {
}

func hash(into hasher: inout Hasher) {
hasher.combine(Self.operationType)
hasher.combine(Self.operationName)
hasher.combine(__variables?._jsonEncodableValue?._jsonValue)
}
}
Expand Down
32 changes: 19 additions & 13 deletions Sources/ApolloAPI/ObjectData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ public struct ObjectData {
public let _transformer: any _ObjectData_Transformer
public let _rawData: [String: AnyHashable]

@usableFromInline internal static let _boolTrue = AnyHashable(true)
@usableFromInline internal static let _boolFalse = AnyHashable(false)

public init(
_transformer: any _ObjectData_Transformer,
_rawData: [String: AnyHashable]
Expand All @@ -29,7 +26,7 @@ public struct ObjectData {
// This check is based on AnyHashable using a canonical representation of the type-erased value so
// instances wrapping the same value of any type compare as equal. Therefore while Int(1) and Int(0)
// might be representable as Bool they will never equal Bool(true) nor Bool(false).
if let boolVal = value as? Bool, (value == Self._boolTrue || value == Self._boolFalse) {
if let boolVal = value as? Bool, value.isCanonicalBool {
value = boolVal

// Cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting
Expand Down Expand Up @@ -72,17 +69,17 @@ public struct ListData {
@inlinable public subscript(_ key: Int) -> (any ScalarType)? {
var value: AnyHashable = _rawData[key]

// Attempting cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting,
// also need to attempt `Bool` cast first to ensure a bool doesn't get inadvertently converted to `Int`
switch value {
case let boolVal as Bool:
// This check is based on AnyHashable using a canonical representation of the type-erased value so
// instances wrapping the same value of any type compare as equal. Therefore while Int(1) and Int(0)
// might be representable as Bool they will never equal Bool(true) nor Bool(false).
if let boolVal = value as? Bool, value.isCanonicalBool {
value = boolVal
case let intVal as Int:
value = intVal
default:
break

// Cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting
} else if let intValue = value as? Int {
value = intValue
}

return _transformer.transform(value)
}

Expand All @@ -96,3 +93,12 @@ public struct ListData {
return _transformer.transform(_rawData[key])
}
}

extension AnyHashable {
fileprivate static let boolTrue = AnyHashable(true)
fileprivate static let boolFalse = AnyHashable(false)

@usableFromInline var isCanonicalBool: Bool {
self == Self.boolTrue || self == Self.boolFalse
}
}
15 changes: 13 additions & 2 deletions Sources/ApolloSQLite/SQLiteDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,25 @@ public protocol SQLiteDatabase {

func selectRawRows(forKeys keys: Set<CacheKey>) throws -> [DatabaseRow]

func addOrUpdateRecordString(_ recordString: String, for cacheKey: CacheKey) throws
func addOrUpdate(records: [(cacheKey: CacheKey, recordString: String)]) throws

func deleteRecord(for cacheKey: CacheKey) throws

func deleteRecords(matching pattern: CacheKey) throws

func clearDatabase(shouldVacuumOnClear: Bool) throws

@available(*, deprecated, renamed: "addOrUpdate(records:)")
func addOrUpdateRecordString(_ recordString: String, for cacheKey: CacheKey) throws

}

extension SQLiteDatabase {

public func addOrUpdateRecordString(_ recordString: String, for cacheKey: CacheKey) throws {
try addOrUpdate(records: [(cacheKey, recordString)])
}

}

public extension SQLiteDatabase {
Expand Down
14 changes: 10 additions & 4 deletions Sources/ApolloSQLite/SQLiteDotSwiftDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,17 @@ public final class SQLiteDotSwiftDatabase: SQLiteDatabase {
return DatabaseRow(cacheKey: key, storedInfo: record)
}
}

public func addOrUpdateRecordString(_ recordString: String, for cacheKey: CacheKey) throws {
try self.db.run(self.records.insert(or: .replace, self.keyColumn <- cacheKey, self.recordColumn <- recordString))

public func addOrUpdate(records: [(cacheKey: CacheKey, recordString: String)]) throws {
guard !records.isEmpty else { return }

let setters = records.map {
[self.keyColumn <- $0.cacheKey, self.recordColumn <- $0.recordString]
}

try self.db.run(self.records.insertMany(or: .replace, setters))
}

public func deleteRecord(for cacheKey: CacheKey) throws {
let query = self.records.filter(keyColumn == cacheKey)
try self.db.run(query.delete())
Expand Down
Loading

0 comments on commit 826b0a1

Please sign in to comment.