diff --git a/CHANGELOG.md b/CHANGELOG.md index c076fe5..e7d25b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,49 @@ called out explicitly even when nothing else did. release notes, so a version with no entry here does not release. Write the entry in the same PR that syncs the contract, while the diff is still in front of you. +## 0.8.0 + +Synced to [`flipcash2-protobuf-api@e1f4116c`](https://github.com/code-payments/flipcash2-protobuf-api/commit/e1f4116c499718401d002cf79ce64229149ee297), +picking up [#102](https://github.com/code-payments/flipcash2-protobuf-api/pull/102). + +`StartChat` gains a required idempotency key, carried by a new `chat.v1.IdempotencyKey` message. +Twenty added lines across two files, nothing removed and nothing renumbered — but this is not a free +upgrade, because the new field is required and 0.7.0 has no way to set it. + +Group chat is still in flux. `StartChat` itself only arrived one version ago, in 0.7.0, and the +surface is still being iterated on, so expect it to keep moving. The bump to 0.8.0 tracks the pinned +contract advancing, not the feature settling — if you are integrating group chat, plan on taking +further versions rather than pinning this one and walking away. + +### Added + +- `chat.v1.IdempotencyKey`, a single `bytes value` (field 1) constrained to exactly 16 bytes: + `min_len` and `max_len` are both 16, so a shorter or longer value fails validation rather than + being padded or truncated. The key is minted and owned by the client, typically a random UUID, and + is never exposed as the created chat's identity. + +- `idempotency_key` on `chat.v1.StartChatRequest`, field 9, + `[(validate.rules).message.required = true]`. It sits between the `parameters` oneof (field 1) and + `auth` (field 10), both of which keep their numbers. + +### Upgrading + +Every `StartChat` call site has to set the key, and where it is minted decides whether the field does +anything. The server derives the created chat's identity from the caller and the key, so a retry +carrying the same key returns the chat the first attempt created, with result `OK`, instead of +creating a second one. The parameters are not part of that identity: a retry with a different title, +picture or rules still returns the original chat, unchanged. So the key is only worth something if it +survives a retry — mint it where the user's intent to create a chat begins and carry it down through +the retry path. A key generated inside the call, fresh on each attempt, passes validation and buys +nothing: the duplicate chat it was meant to prevent is exactly what you get. + +### Unchanged + +Nothing was renumbered. `StartChatResponse.Result` keeps all six of its cases in place +(`OK`, `DENIED`, `TITLE_MODERATED`, `PICTURE_BLOB_NOT_ACCEPTED`, `INVALID_RULES`, +`RULES_NOT_SATISFIED`), so a positional `rawValue` mapping over it does not shift. No existing field +changed number or type, and no service, RPC, message or enum case was removed. + ## 0.7.0 Synced to [`flipcash2-protobuf-api@89b444bc`](https://github.com/code-payments/flipcash2-protobuf-api/commit/89b444bc4ae0d099dbaa90cc8b1b301398c94990), diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift index 3a09fd8..fd5d508 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift @@ -367,6 +367,18 @@ public struct Flipcash_Chat_V1_StartChatRequest: Sendable { set {parameters = .group(newValue)} } + /// Key for retrying this request safely. A retry with the same key from the + /// same caller returns the chat the first attempt created, with result OK, + /// even if the parameters differ. + public var idempotencyKey: Flipcash_Chat_V1_IdempotencyKey { + get {return _idempotencyKey ?? Flipcash_Chat_V1_IdempotencyKey()} + set {_idempotencyKey = newValue} + } + /// Returns true if `idempotencyKey` has been explicitly set. + public var hasIdempotencyKey: Bool {return self._idempotencyKey != nil} + /// Clears the value of `idempotencyKey`. Subsequent reads from it will return its default value. + public mutating func clearIdempotencyKey() {self._idempotencyKey = nil} + public var auth: Flipcash_Common_V1_Auth { get {return _auth ?? Flipcash_Common_V1_Auth()} set {_auth = newValue} @@ -427,6 +439,7 @@ public struct Flipcash_Chat_V1_StartChatRequest: Sendable { public init() {} + fileprivate var _idempotencyKey: Flipcash_Chat_V1_IdempotencyKey? = nil fileprivate var _auth: Flipcash_Common_V1_Auth? = nil } @@ -967,7 +980,7 @@ extension Flipcash_Chat_V1_GetGroupChatFeedResponse.Result: SwiftProtobuf._Proto extension Flipcash_Chat_V1_StartChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".StartChatRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}group\0\u{2}\u{9}auth\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}group\0\u{4}\u{8}idempotency_key\0\u{1}auth\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -988,6 +1001,7 @@ extension Flipcash_Chat_V1_StartChatRequest: SwiftProtobuf.Message, SwiftProtobu self.parameters = .group(v) } }() + case 9: try { try decoder.decodeSingularMessageField(value: &self._idempotencyKey) }() case 10: try { try decoder.decodeSingularMessageField(value: &self._auth) }() default: break } @@ -1002,6 +1016,9 @@ extension Flipcash_Chat_V1_StartChatRequest: SwiftProtobuf.Message, SwiftProtobu try { if case .group(let v)? = self.parameters { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) } }() + try { if let v = self._idempotencyKey { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } }() try { if let v = self._auth { try visitor.visitSingularMessageField(value: v, fieldNumber: 10) } }() @@ -1010,6 +1027,7 @@ extension Flipcash_Chat_V1_StartChatRequest: SwiftProtobuf.Message, SwiftProtobu public static func ==(lhs: Flipcash_Chat_V1_StartChatRequest, rhs: Flipcash_Chat_V1_StartChatRequest) -> Bool { if lhs.parameters != rhs.parameters {return false} + if lhs._idempotencyKey != rhs._idempotencyKey {return false} if lhs._auth != rhs._auth {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift index 81abb7d..2445f1e 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift @@ -8,6 +8,7 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ +import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -610,6 +611,26 @@ public struct Flipcash_Chat_V1_RosterUpdateBatch: Sendable { public init() {} } +/// IdempotencyKey lets a client safely retry a request that creates something. +/// The server derives the created object's identity from the caller and this +/// key, so a retry with the same key returns the original result instead of +/// creating a duplicate. Which parameters were sent is not part of the identity: +/// a retry with different parameters still returns the original. +/// +/// The key is owned by the client and is typically a randomly generated UUID. +/// It is never exposed as the object's identity. +public struct Flipcash_Chat_V1_IdempotencyKey: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var value: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "flipcash.chat.v1" @@ -1411,3 +1432,33 @@ extension Flipcash_Chat_V1_RosterUpdateBatch: SwiftProtobuf.Message, SwiftProtob return true } } + +extension Flipcash_Chat_V1_IdempotencyKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".IdempotencyKey" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}value\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBytesField(value: &self.value) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.value.isEmpty { + try visitor.visitSingularBytesField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_IdempotencyKey, rhs: Flipcash_Chat_V1_IdempotencyKey) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/flipcash2.lock b/flipcash2.lock index 18f30e8..d58056d 100644 --- a/flipcash2.lock +++ b/flipcash2.lock @@ -1,4 +1,4 @@ # Pinned upstream contract. Regenerate with scripts/sync-protos.sh . upstream: code-payments/flipcash2-protobuf-api -commit: 89b444bc4ae0d099dbaa90cc8b1b301398c94990 -subject: Add RPCs to start, join and leave group chats (#100) +commit: e1f4116c499718401d002cf79ce64229149ee297 +subject: Add idempotency key to StartChat RPC request (#102) diff --git a/proto/chat/v1/chat_service.proto b/proto/chat/v1/chat_service.proto index 220c782..69dab53 100644 --- a/proto/chat/v1/chat_service.proto +++ b/proto/chat/v1/chat_service.proto @@ -214,6 +214,11 @@ message StartChatRequest { Rules rules = 3; } + // Key for retrying this request safely. A retry with the same key from the + // same caller returns the chat the first attempt created, with result OK, + // even if the parameters differ. + IdempotencyKey idempotency_key = 9 [(validate.rules).message.required = true]; + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; } diff --git a/proto/chat/v1/model.proto b/proto/chat/v1/model.proto index 189d0e0..b8c4068 100644 --- a/proto/chat/v1/model.proto +++ b/proto/chat/v1/model.proto @@ -259,3 +259,18 @@ message RosterUpdateBatch { max_items: 100 }]; } + +// IdempotencyKey lets a client safely retry a request that creates something. +// The server derives the created object's identity from the caller and this +// key, so a retry with the same key returns the original result instead of +// creating a duplicate. Which parameters were sent is not part of the identity: +// a retry with different parameters still returns the original. +// +// The key is owned by the client and is typically a randomly generated UUID. +// It is never exposed as the object's identity. +message IdempotencyKey { + bytes value = 1 [(validate.rules).bytes = { + min_len: 16 + max_len: 16 + }]; +}