diff --git a/appleApp/AppleCore/FluxaAppleHeadlessCoordinator.swift b/appleApp/AppleCore/FluxaAppleHeadlessCoordinator.swift new file mode 100644 index 0000000..138021d --- /dev/null +++ b/appleApp/AppleCore/FluxaAppleHeadlessCoordinator.swift @@ -0,0 +1,129 @@ +import Foundation + +indirect enum FluxaAppleJsonValue: Codable, Sendable { + case null + case boolean(Bool) + case number(Double) + case string(String) + case array([FluxaAppleJsonValue]) + case object([String: FluxaAppleJsonValue]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let value = try? container.decode(Bool.self) { + self = .boolean(value) + } else if let value = try? container.decode(Double.self) { + self = .number(value) + } else if let value = try? container.decode(String.self) { + self = .string(value) + } else if let value = try? container.decode([FluxaAppleJsonValue].self) { + self = .array(value) + } else { + self = .object(try container.decode([String: FluxaAppleJsonValue].self)) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .null: + try container.encodeNil() + case .boolean(let value): + try container.encode(value) + case .number(let value): + try container.encode(value) + case .string(let value): + try container.encode(value) + case .array(let value): + try container.encode(value) + case .object(let value): + try container.encode(value) + } + } +} + +struct FluxaAppleHeadlessEffect: Decodable, Sendable { + let id: String + let type: String + let generation: UInt64 + let payload: FluxaAppleJsonValue +} + +struct FluxaAppleHeadlessCompletion: Encodable, Sendable { + let effectId: String + let status: String + let value: FluxaAppleJsonValue + let error: FluxaAppleJsonValue + + static func success(effectId: String, value: FluxaAppleJsonValue = .null) -> Self { + Self(effectId: effectId, status: "ok", value: value, error: .null) + } + + static func failure(effectId: String, code: String) -> Self { + Self( + effectId: effectId, + status: "error", + value: .null, + error: .object(["code": .string(code)]) + ) + } +} + +struct FluxaAppleHeadlessResult: Decodable, Sendable { + let state: [String: FluxaAppleJsonValue] + let effects: [FluxaAppleHeadlessEffect] +} + +protocol FluxaAppleHeadlessEffectExecutor: AnyObject { + func execute(effect: FluxaAppleHeadlessEffect) async -> FluxaAppleHeadlessCompletion +} + +final class FluxaAppleHeadlessCoordinator { + private let runtime: FluxaAppleHeadlessRuntime + private weak var executor: (any FluxaAppleHeadlessEffectExecutor)? + private let maxEffectsPerDispatch: Int + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + init( + runtime: FluxaAppleHeadlessRuntime, + executor: any FluxaAppleHeadlessEffectExecutor, + maxEffectsPerDispatch: Int = 64 + ) { + self.runtime = runtime + self.executor = executor + self.maxEffectsPerDispatch = maxEffectsPerDispatch + } + + func dispatch(actionJson: String) async throws -> FluxaAppleHeadlessResult { + try await drain(runtime.dispatch(actionJson: actionJson)) + } + + func complete(completion: FluxaAppleHeadlessCompletion) async throws -> FluxaAppleHeadlessResult { + try await drain(runtime.completeEffect(resultJson: try encode(completion))) + } + + private func drain(_ initialJson: String) async throws -> FluxaAppleHeadlessResult { + var current = try decode(initialJson) + var remaining = maxEffectsPerDispatch + while let effect = current.effects.first, remaining > 0 { + guard let executor else { + return current + } + let completion = await executor.execute(effect: effect) + current = try decode(runtime.completeEffect(resultJson: try encode(completion))) + remaining -= 1 + } + return current + } + + private func decode(_ json: String) throws -> FluxaAppleHeadlessResult { + try decoder.decode(FluxaAppleHeadlessResult.self, from: Data(json.utf8)) + } + + private func encode(_ completion: FluxaAppleHeadlessCompletion) throws -> String { + String(decoding: try encoder.encode(completion), as: UTF8.self) + } +}