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
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
20 changes: 19 additions & 1 deletion Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
Expand All @@ -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
}
Expand All @@ -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)
} }()
Expand All @@ -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
Expand Down
51 changes: 51 additions & 0 deletions Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
}
}
4 changes: 2 additions & 2 deletions flipcash2.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Pinned upstream contract. Regenerate with scripts/sync-protos.sh <sha>.
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)
5 changes: 5 additions & 0 deletions proto/chat/v1/chat_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down
15 changes: 15 additions & 0 deletions proto/chat/v1/model.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
}];
}
Loading