143 lines
5.9 KiB
Swift
143 lines
5.9 KiB
Swift
import Foundation
|
|
|
|
/// Wire-Format eines Events auf der mana-event-sourced Plattform.
|
|
///
|
|
/// Spiegel von `EventEnvelope<Payload>` aus `@mana/event-kit`. Der
|
|
/// Server (`mana-sync`) kennt nur die Routing-Felder; `payload` ist
|
|
/// opaque JSON (ggf. `enc:1:`-verschlüsselt).
|
|
///
|
|
/// `sequenceNumber` + `receivedAt` werden Server-seitig beim Append
|
|
/// vergeben — beim Emit lokal `nil`, beim Pull gesetzt. `clientId` +
|
|
/// `payloadHash` sind für Wire-Parität optional vorgesehen; die Engine
|
|
/// stempelt sie beim Emit.
|
|
public struct EventEnvelope: Codable, Equatable, Sendable {
|
|
public let eventId: String
|
|
public let aggregateId: String
|
|
public let appId: String
|
|
public let eventType: String
|
|
public let eventVersion: Int
|
|
public let occurredAt: String
|
|
public var receivedAt: String?
|
|
public var sequenceNumber: Int64?
|
|
public var causationId: String?
|
|
public var correlationId: String?
|
|
public let actor: ActorContext
|
|
public var attributedToUserId: String
|
|
public let origin: String
|
|
public let idempotencyKey: String
|
|
public var clientId: String?
|
|
public var payloadHash: String?
|
|
public var payload: JSONValue
|
|
|
|
public init(
|
|
eventId: String,
|
|
aggregateId: String,
|
|
appId: String,
|
|
eventType: String,
|
|
eventVersion: Int,
|
|
occurredAt: String,
|
|
receivedAt: String? = nil,
|
|
sequenceNumber: Int64? = nil,
|
|
causationId: String? = nil,
|
|
correlationId: String? = nil,
|
|
actor: ActorContext,
|
|
attributedToUserId: String,
|
|
origin: String,
|
|
idempotencyKey: String,
|
|
clientId: String? = nil,
|
|
payloadHash: String? = nil,
|
|
payload: JSONValue
|
|
) {
|
|
self.eventId = eventId
|
|
self.aggregateId = aggregateId
|
|
self.appId = appId
|
|
self.eventType = eventType
|
|
self.eventVersion = eventVersion
|
|
self.occurredAt = occurredAt
|
|
self.receivedAt = receivedAt
|
|
self.sequenceNumber = sequenceNumber
|
|
self.causationId = causationId
|
|
self.correlationId = correlationId
|
|
self.actor = actor
|
|
self.attributedToUserId = attributedToUserId
|
|
self.origin = origin
|
|
self.idempotencyKey = idempotencyKey
|
|
self.clientId = clientId
|
|
self.payloadHash = payloadHash
|
|
self.payload = payload
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case eventId, aggregateId, appId, eventType, eventVersion, occurredAt, receivedAt
|
|
case sequenceNumber, causationId, correlationId, actor, attributedToUserId
|
|
case origin, idempotencyKey, clientId, payloadHash, payload
|
|
}
|
|
|
|
/// Custom-Decode, weil `mana-sync` `sequenceNumber` als JSON-**String**
|
|
/// serialisiert (`json:"...,string"`). Default-Int64-Decode würde werfen
|
|
/// → ganzer Pull scheitert → Cross-Device-Sync still tot. Wir tolerieren
|
|
/// String **und** Zahl.
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
eventId = try container.decode(String.self, forKey: .eventId)
|
|
aggregateId = try container.decode(String.self, forKey: .aggregateId)
|
|
appId = try container.decode(String.self, forKey: .appId)
|
|
eventType = try container.decode(String.self, forKey: .eventType)
|
|
eventVersion = try container.decode(Int.self, forKey: .eventVersion)
|
|
occurredAt = try container.decode(String.self, forKey: .occurredAt)
|
|
receivedAt = try container.decodeIfPresent(String.self, forKey: .receivedAt)
|
|
sequenceNumber = container.decodeFlexibleInt64(forKey: .sequenceNumber)
|
|
causationId = try container.decodeIfPresent(String.self, forKey: .causationId)
|
|
correlationId = try container.decodeIfPresent(String.self, forKey: .correlationId)
|
|
actor = try container.decode(ActorContext.self, forKey: .actor)
|
|
attributedToUserId = try container.decode(String.self, forKey: .attributedToUserId)
|
|
origin = try container.decode(String.self, forKey: .origin)
|
|
idempotencyKey = try container.decode(String.self, forKey: .idempotencyKey)
|
|
clientId = try container.decodeIfPresent(String.self, forKey: .clientId)
|
|
payloadHash = try container.decodeIfPresent(String.self, forKey: .payloadHash)
|
|
payload = try container.decode(JSONValue.self, forKey: .payload)
|
|
}
|
|
|
|
/// Kopie mit ersetztem Payload — genutzt von der Crypto-Pipeline
|
|
/// (encrypt vor Outbox, decrypt nach Pull).
|
|
public func withPayload(_ newPayload: JSONValue) -> EventEnvelope {
|
|
var copy = self
|
|
copy.payload = newPayload
|
|
return copy
|
|
}
|
|
|
|
/// Kopie mit neu attribuiertem User — genutzt vom `signIn`-Claim
|
|
/// (re-taggt `anon:*` → echter `sub`).
|
|
public func attributed(to userId: String) -> EventEnvelope {
|
|
var copy = self
|
|
copy.attributedToUserId = userId
|
|
return copy
|
|
}
|
|
}
|
|
|
|
/// Actor-Context — wer hat das Event ausgelöst.
|
|
public struct ActorContext: Codable, Equatable, Sendable {
|
|
public let kind: String // "user" | "system" | "ai"
|
|
public let principalId: String
|
|
public var displayName: String?
|
|
|
|
public init(kind: String, principalId: String, displayName: String? = nil) {
|
|
self.kind = kind
|
|
self.principalId = principalId
|
|
self.displayName = displayName
|
|
}
|
|
|
|
public static func user(principalId: String, displayName: String? = "ios") -> ActorContext {
|
|
ActorContext(kind: "user", principalId: principalId, displayName: displayName)
|
|
}
|
|
}
|
|
|
|
extension KeyedDecodingContainer {
|
|
/// Dekodiert ein optionales Int64 aus JSON-Zahl **oder** -String —
|
|
/// `mana-sync` sendet BIGINTs (`sequenceNumber`) als String.
|
|
func decodeFlexibleInt64(forKey key: Key) -> Int64? {
|
|
if let value = (try? decodeIfPresent(Int64.self, forKey: key)) ?? nil { return value }
|
|
if let str = (try? decodeIfPresent(String.self, forKey: key)) ?? nil { return Int64(str) }
|
|
return nil
|
|
}
|
|
}
|