[lcfeaturecontrol-review-fixes] Fix JWT log leak, drop macOS/tvOS, coalesce requests

- FeatureControlManager/NotificationsClient: pass debug: false at every
  api.request(...) call site — the API default (debug: true) was printing
  Authorization: Bearer <jwt> on every request, release builds included.
- Package.swift: drop macOS/tvOS from platforms — LCEssentials.API (used by
  LCFeatureControl) is iOS/watchOS-only and the vendored xcframework has no
  macOS/tvOS slice.
- FeatureControlManager: coalesce concurrent evaluate() calls on a cold cache
  into a single in-flight request per key instead of firing one per caller.
- FeatureControlManager/NotificationsClient: treat a 200 response carrying
  {"error": true} as a failure instead of caching/returning it as success.
- FeatureControlNotificationsClient: thread a cursor param through list() so
  the already-decoded nextCursor can actually be used to page.
- Tests: 9 new tests (coalescing, envelope-error-on-200, cursor param, a real
  object payload decoded through a full evaluate response, date-decode
  failures) and removed the 7 remaining force-unwraps in test scaffolding.
- Documentation/FeatureControl.md: new guide for the LCFeatureControl product,
  cross-linked from README.md and Extensions.md.
This commit is contained in:
Daniel Arantes Loverde
2026-09-16 13:21:10 -03:00
parent 2ba487a6e8
commit c6e9803c75
12 changed files with 392 additions and 69 deletions

View File

@@ -3,7 +3,7 @@ import LCEssentials
/// Protocol seam for DI, mirroring `FeatureControlEvaluating`.
public protocol FeatureControlNotifying: Sendable {
func list(status: FeatureControlNotificationStatus, limit: Int) async throws
func list(status: FeatureControlNotificationStatus, limit: Int, cursor: String?) async throws
-> (items: [FeatureControlNotification], nextCursor: String?)
func markRead(id: String) async throws
func markAllRead() async throws
@@ -22,20 +22,29 @@ public actor FeatureControlNotificationsClient: FeatureControlNotifying {
self.api = api
}
public func list(status: FeatureControlNotificationStatus = .all, limit: Int = 20) async throws
public func list(status: FeatureControlNotificationStatus = .all, limit: Int = 20, cursor: String? = nil) async throws
-> (items: [FeatureControlNotification], nextCursor: String?) {
var headers: [String: String] = [:]
await configuration.auth.authorize(&headers)
let query = "?status=\(status.rawValue)&limit=\(limit)"
var query = "?status=\(status.rawValue)&limit=\(limit)"
if let cursor, let encodedCursor = cursor.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
query += "&cursor=\(encodedCursor)"
}
do {
let envelope: FeatureControlNotificationListEnvelope = try await api.request(
url: configuration.baseURL + configuration.notificationsPath + query,
method: .get,
headers: headers,
debug: false,
timeoutInterval: configuration.requestTimeout
)
guard !envelope.error else {
throw FeatureControlError.server(code: "UNKNOWN", status: 200)
}
return (envelope.result.items, envelope.result.nextCursor)
} catch let error as FeatureControlError {
throw error
} catch {
throw FeatureControlErrorMapper.map(error)
}
@@ -51,6 +60,7 @@ public actor FeatureControlNotificationsClient: FeatureControlNotifying {
url: configuration.baseURL + configuration.notificationsPath + "/\(encodedId)/read",
method: .post,
headers: headers,
debug: false,
timeoutInterval: configuration.requestTimeout
)
} catch {
@@ -67,6 +77,7 @@ public actor FeatureControlNotificationsClient: FeatureControlNotifying {
url: configuration.baseURL + configuration.notificationsPath + "/read-all",
method: .post,
headers: headers,
debug: false,
timeoutInterval: configuration.requestTimeout
)
} catch {