feat: Ipad app production readiness, Colony orchestration, Social posting
All checks were successful
Production Readiness / backend-contracts (pull_request) Successful in 3m19s
Production Readiness / webos-typecheck (pull_request) Successful in 2m38s
Production Readiness / ipad-parse (pull_request) Successful in 1m44s

This commit is contained in:
Sayan Datta
2026-05-03 18:28:04 +05:30
parent acfc602157
commit 6c93e31741
86 changed files with 20349 additions and 1655 deletions

View File

@@ -2,8 +2,7 @@ import Foundation
import Security
/// Central app configuration.
/// Build settings remain the fallback, but production installs should prefer
/// runtime configuration stored on-device.
/// Enterprise installs must use runtime configuration stored on-device.
enum AppConfig {
private static let runtimeBaseURLKey = "velocity.runtime.base_url"
private static let runtimeDreamWeaverBaseURLKey = "velocity.runtime.dream_weaver_base_url"
@@ -42,10 +41,6 @@ enum AppConfig {
return "Credentials required"
}
private static func value(for key: String) -> String? {
parsedValue(from: Bundle.main.infoDictionary, key: key)
}
private static func sanitizedValue(_ raw: String?, key: String) -> String? {
guard let raw else {
return nil
@@ -59,7 +54,7 @@ enum AppConfig {
/// Base URL for the Velocity backend / gateway.
static var baseURL: String {
runtimeBaseURL ?? value(for: "BASE_URL") ?? "https://velocity.desineuron.in/api"
runtimeBaseURL ?? SessionConfigurationDefaults.productionBaseURL
}
/// Dedicated Dream Weaver gateway endpoint when configured; otherwise
@@ -76,19 +71,19 @@ enum AppConfig {
}
static var dreamWeaverAPIKey: String? {
runtimeDreamWeaverAPIKey ?? value(for: "DREAM_WEAVER_API_KEY")
runtimeDreamWeaverAPIKey
}
static var apiEmail: String? {
runtimeEmail ?? value(for: "API_EMAIL")
runtimeEmail
}
static var apiPassword: String? {
runtimePassword ?? value(for: "API_PASSWORD")
runtimePassword
}
static var apiBearerToken: String? {
runtimeBearerToken ?? value(for: "API_BEARER_TOKEN")
runtimeBearerToken
}
static var apiAccessToken: String? {
@@ -132,7 +127,7 @@ enum AppConfig {
email: apiEmail,
hasPassword: apiPassword != nil,
hasBearerToken: apiBearerToken != nil,
source: hasStoredRuntimeConfiguration ? .secureDeviceStorage : .buildConfiguration
source: .secureDeviceStorage
)
}
@@ -144,23 +139,15 @@ enum AppConfig {
password: String?,
bearerToken: String?
) throws {
UserDefaults.standard.set(baseURL, forKey: runtimeBaseURLKey)
if let dreamWeaverBaseURL {
UserDefaults.standard.set(dreamWeaverBaseURL, forKey: runtimeDreamWeaverBaseURLKey)
} else {
UserDefaults.standard.removeObject(forKey: runtimeDreamWeaverBaseURLKey)
}
if let email {
UserDefaults.standard.set(email, forKey: runtimeEmailKey)
} else {
UserDefaults.standard.removeObject(forKey: runtimeEmailKey)
}
try storeSecret(baseURL, account: runtimeBaseURLKey)
try storeSecret(dreamWeaverBaseURL, account: runtimeDreamWeaverBaseURLKey)
try storeSecret(email, account: runtimeEmailKey)
try storeSecret(dreamWeaverAPIKey, account: runtimeDreamWeaverAPIKeyKey)
try storeSecret(password, account: runtimePasswordKey)
try storeSecret(bearerToken, account: runtimeBearerTokenKey)
UserDefaults.standard.removeObject(forKey: runtimeBaseURLKey)
UserDefaults.standard.removeObject(forKey: runtimeDreamWeaverBaseURLKey)
UserDefaults.standard.removeObject(forKey: runtimeEmailKey)
try clearStoredAccessToken()
}
@@ -168,6 +155,9 @@ enum AppConfig {
UserDefaults.standard.removeObject(forKey: runtimeBaseURLKey)
UserDefaults.standard.removeObject(forKey: runtimeDreamWeaverBaseURLKey)
UserDefaults.standard.removeObject(forKey: runtimeEmailKey)
try deleteSecret(account: runtimeBaseURLKey)
try deleteSecret(account: runtimeDreamWeaverBaseURLKey)
try deleteSecret(account: runtimeEmailKey)
try deleteSecret(account: runtimeDreamWeaverAPIKeyKey)
try deleteSecret(account: runtimePasswordKey)
try deleteSecret(account: runtimeBearerTokenKey)
@@ -186,16 +176,29 @@ enum AppConfig {
}
private static var runtimeBaseURL: String? {
sanitizedValue(UserDefaults.standard.string(forKey: runtimeBaseURLKey), key: runtimeBaseURLKey)
canonicalizedBackendBaseURL(
sanitizedValue(secret(account: runtimeBaseURLKey), key: runtimeBaseURLKey)
)
}
private static func canonicalizedBackendBaseURL(_ value: String?) -> String? {
guard let value else {
return nil
}
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.caseInsensitiveCompare(SessionConfigurationDefaults.legacyVelocityWebBaseURL) == .orderedSame {
return SessionConfigurationDefaults.productionBaseURL
}
return trimmed
}
private static var configuredDreamWeaverBaseURL: String? {
runtimeDreamWeaverBaseURL ?? value(for: "DREAM_WEAVER_BASE_URL")
runtimeDreamWeaverBaseURL
}
private static var runtimeDreamWeaverBaseURL: String? {
sanitizedValue(
UserDefaults.standard.string(forKey: runtimeDreamWeaverBaseURLKey),
secret(account: runtimeDreamWeaverBaseURLKey),
key: runtimeDreamWeaverBaseURLKey
)
}
@@ -205,7 +208,7 @@ enum AppConfig {
}
private static var runtimeEmail: String? {
sanitizedValue(UserDefaults.standard.string(forKey: runtimeEmailKey), key: runtimeEmailKey)
sanitizedValue(secret(account: runtimeEmailKey), key: runtimeEmailKey)
}
private static var runtimePassword: String? {

View File

@@ -12,6 +12,12 @@ enum SessionConfigurationSource: String {
case secureDeviceStorage = "Secure device storage"
}
enum SessionConfigurationDefaults {
static let productionBaseURL = "https://api.desineuron.in/api"
static let legacyVelocityWebBaseURL = "https://velocity.desineuron.in/api"
static let dreamWeaverBaseURL = "https://dreamweaver.desineuron.in"
}
struct AppSessionConfiguration: Equatable {
let baseURL: String
let dreamWeaverBaseURL: String
@@ -89,7 +95,13 @@ struct SessionConfigurationDraft: Equatable {
}
var normalizedBaseURL: String? {
Self.normalizedHTTPSOrigin(from: trimmedBaseURL)
guard let normalized = Self.normalizedHTTPSOrigin(from: trimmedBaseURL) else {
return nil
}
if normalized.caseInsensitiveCompare(SessionConfigurationDefaults.legacyVelocityWebBaseURL) == .orderedSame {
return SessionConfigurationDefaults.productionBaseURL
}
return normalized
}
var trimmedDreamWeaverBaseURL: String? {
@@ -118,7 +130,7 @@ struct SessionConfigurationDraft: Equatable {
}
guard normalizedBaseURL != nil else {
errors.append("Backend endpoint must be an HTTPS API base like https://velocity.desineuron.in/api.")
errors.append("Backend endpoint must be an HTTPS API base like \(SessionConfigurationDefaults.productionBaseURL).")
return errors
}

View File

@@ -75,8 +75,8 @@ final class SessionStore {
func reloadFromPersistedConfiguration() {
currentConfiguration = AppConfig.currentSessionConfiguration()
draftBaseURL = currentConfiguration.baseURL
draftDreamWeaverBaseURL = persistedDreamWeaverDraftValue
draftBaseURL = trimmedNonEmpty(currentConfiguration.baseURL) ?? SessionConfigurationDefaults.productionBaseURL
draftDreamWeaverBaseURL = trimmedNonEmpty(persistedDreamWeaverDraftValue) ?? SessionConfigurationDefaults.dreamWeaverBaseURL
draftDreamWeaverAPIKey = ""
draftAuthMode = currentConfiguration.authMode
draftEmail = currentConfiguration.email ?? ""
@@ -88,6 +88,11 @@ final class SessionStore {
baselineEmail = currentConfiguration.email
}
func markDraftEdited() {
errorMessage = nil
statusMessage = nil
}
func discardDraftChanges() {
errorMessage = nil
statusMessage = nil
@@ -185,6 +190,11 @@ final class SessionStore {
currentConfiguration.usesDedicatedDreamWeaverBaseURL ? currentConfiguration.dreamWeaverBaseURL : ""
}
private func trimmedNonEmpty(_ value: String) -> String? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private func verificationStatusMessage(
successPrefix: String,
backendRefreshError: String?,

View File

@@ -18,29 +18,71 @@ final class ComfyClient {
// MARK: - Health Check
/// Call on app launch to confirm the Dream Weaver gateway is reachable
/// and the Dream Weaver routes are actually mounted behind it.
func checkHealth() async -> Bool {
func checkReadiness() async -> DreamWeaverReadiness {
do {
var request = authorizedRequest(url: try resolvedURL(candidate: nil, fallbackPath: "/health"))
request.timeoutInterval = 30.0
let (data, response) = try await urlSession.data(for: request)
guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else {
return false
return DreamWeaverReadiness(
isReady: false,
label: "Gateway offline",
detail: "Dream Weaver gateway did not return a healthy /health response."
)
}
let json = try JSONDecoder().decode(HealthResponse.self, from: data)
guard ["ok", "healthy"].contains(json.status.lowercased()) else {
return false
let health = try JSONDecoder().decode(HealthResponse.self, from: data)
guard ["ok", "healthy"].contains(health.status.lowercased()) else {
return DreamWeaverReadiness(
isReady: false,
label: "Gateway unhealthy",
detail: "Dream Weaver gateway reported status: \(health.status)."
)
}
guard health.comfyui != false else {
return DreamWeaverReadiness(
isReady: false,
label: "ComfyUI offline",
detail: "The gateway is online, but ComfyUI/GPU is not reachable."
)
}
guard health.checkpointReady != false else {
return DreamWeaverReadiness(
isReady: false,
label: "Checkpoint missing",
detail: "ComfyUI is online, but no compatible Dream Weaver checkpoint is available."
)
}
guard try await probeDreamWeaverRoute() else {
return DreamWeaverReadiness(
isReady: false,
label: "Route not mounted",
detail: "The /dream-weaver route family is not mounted behind the configured gateway."
)
}
return try await probeDreamWeaverRoute()
return DreamWeaverReadiness(
isReady: true,
label: "Ready",
detail: "Gateway, Dream Weaver route, ComfyUI, and checkpoint checks passed."
)
} catch {
return false
return DreamWeaverReadiness(
isReady: false,
label: "Gateway offline",
detail: error.localizedDescription
)
}
}
/// Call on app launch to confirm the Dream Weaver gateway is reachable
/// and the Dream Weaver routes are actually mounted behind it.
func checkHealth() async -> Bool {
let readiness = await checkReadiness()
return readiness.isReady
}
// MARK: - Main Generation Pipeline
/// Full pipeline: upload queue poll download.
@@ -49,6 +91,10 @@ final class ComfyClient {
/// - roomType: The type of room being designed (e.g. "bedroom", "living_room").
/// - keywords: Comma-separated user keywords appended to the style prompt (can be empty).
func generateImage(source: UIImage, roomType: String, keywords: String) async throws -> UIImage {
try await generateImageResult(source: source, roomType: roomType, keywords: keywords).image
}
func generateImageResult(source: UIImage, roomType: String, keywords: String) async throws -> DreamWeaverGenerationResult {
let normalised = source.fixedOrientation()
let resized = normalised.resizedSquare(to: 1024)
guard let imageData = resized.jpegData(compressionQuality: 0.85) else {
@@ -62,7 +108,8 @@ final class ComfyClient {
let resultURL = try await pollUntilReady(job: job)
// 3. Download result PNG
return try await downloadResult(from: resultURL)
let image = try await downloadResult(from: resultURL)
return DreamWeaverGenerationResult(image: image, resultURL: resultURL)
}
// MARK: - Step 1: POST /dream-weaver
@@ -266,9 +313,48 @@ struct JobStatus: Codable {
}
}
struct HealthResponse: Codable {
struct DreamWeaverReadiness: Equatable {
let isReady: Bool
let label: String
let detail: String
}
struct DreamWeaverGenerationResult {
let image: UIImage
let resultURL: URL
}
struct HealthResponse: Decodable {
let status: String
let comfyui: Bool?
let checkpointReady: Bool?
enum CodingKeys: String, CodingKey {
case status
case comfyui
case checkpointReady = "checkpoint_ready"
case preferredCheckpointAvailable = "preferred_checkpoint_available"
case checkpointAvailable = "checkpoint_available"
case hasCheckpoint = "has_checkpoint"
case gpuReady = "gpu_ready"
}
init(status: String, comfyui: Bool?, checkpointReady: Bool?) {
self.status = status
self.comfyui = comfyui
self.checkpointReady = checkpointReady
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decodeIfPresent(String.self, forKey: .status) ?? "unknown"
comfyui = try container.decodeIfPresent(Bool.self, forKey: .comfyui)
?? container.decodeIfPresent(Bool.self, forKey: .gpuReady)
checkpointReady = try container.decodeIfPresent(Bool.self, forKey: .checkpointReady)
?? container.decodeIfPresent(Bool.self, forKey: .preferredCheckpointAvailable)
?? container.decodeIfPresent(Bool.self, forKey: .checkpointAvailable)
?? container.decodeIfPresent(Bool.self, forKey: .hasCheckpoint)
}
}
struct DreamWeaverErrorResponse: Codable {

View File

@@ -0,0 +1,130 @@
import SwiftUI
import UIKit
struct VelocityVaultShareAsset {
let leadId: String?
let assetName: String
let assetType: String
let storagePath: String?
var isShareable: Bool {
leadId?.trimmedNonEmpty != nil && storagePath?.trimmedNonEmpty != nil
}
}
extension URL {
var velocityStoragePath: String {
let cleaned = path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
if cleaned.hasPrefix("assets/") {
return String(cleaned.dropFirst("assets/".count))
}
return cleaned
}
}
extension View {
func vaultSwipeToShare(asset: VelocityVaultShareAsset?) -> some View {
modifier(VaultSwipeToShareModifier(asset: asset))
}
}
private struct VaultSwipeToShareModifier: ViewModifier {
@State private var appStore = AppStore.shared
let asset: VelocityVaultShareAsset?
func body(content: Content) -> some View {
content
.overlay {
ThreeFingerSwipeUpRecognizer {
Task { await shareAsset() }
}
.allowsHitTesting(asset != nil)
}
}
@MainActor
private func shareAsset() async {
guard let asset else { return }
guard let leadId = asset.leadId?.trimmedNonEmpty,
let storagePath = asset.storagePath?.trimmedNonEmpty else {
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) {
appStore.vaultShareError = "Vault share requires a backend lead and stored asset path."
appStore.vaultShareMessage = nil
}
return
}
guard let threadId = appStore.activeCommunicationsThreadID?.trimmedNonEmpty else {
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) {
appStore.vaultShareError = "Open a Communications thread before using Vault Swipe-to-Share."
appStore.vaultShareMessage = nil
}
return
}
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
do {
let link = try await VelocityAPIClient.shared.generateVaultLink(
leadId: leadId,
assetName: asset.assetName,
assetType: asset.assetType,
storagePath: storagePath
)
_ = try await VelocityAPIClient.shared.sendCommsMessage(
threadId: threadId,
body: "Secure Velocity Vault link: \(link.vaultUrl)"
)
withAnimation(.interactiveSpring(response: 0.42, dampingFraction: 0.82)) {
appStore.vaultShareMessage = "Vault link shared to the active thread."
appStore.vaultShareError = nil
}
} catch {
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) {
appStore.vaultShareError = error.localizedDescription
appStore.vaultShareMessage = nil
}
}
}
}
private struct ThreeFingerSwipeUpRecognizer: UIViewRepresentable {
let onSwipe: () -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let recognizer = UISwipeGestureRecognizer(
target: context.coordinator,
action: #selector(Coordinator.didSwipe(_:))
)
recognizer.direction = .up
recognizer.numberOfTouchesRequired = 3
view.addGestureRecognizer(recognizer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(onSwipe: onSwipe)
}
final class Coordinator: NSObject {
let onSwipe: () -> Void
init(onSwipe: @escaping () -> Void) {
self.onSwipe = onSwipe
}
@objc func didSwipe(_ recognizer: UISwipeGestureRecognizer) {
guard recognizer.state == .ended else { return }
onSwipe()
}
}
}
private extension String {
var trimmedNonEmpty: String? {
let value = trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
import Foundation
enum AppStoreRefreshPolicy {
/// Native iPad surfaces refresh on initial view load, pull-to-refresh, and
/// explicit user mutations. View-local repeating timers are intentionally
/// avoided so AppStore can coalesce in-flight refreshes and hydrate mobile
/// edge state through one bulk request.
static let timerDrivenRefreshesEnabled = false
/// Match the WebOS bootstrap so Inventory, Dashboard, and shared summaries
/// are based on the same production property slice by default.
static let inventoryPropertyLimit = 100
@@ -9,8 +15,9 @@ enum AppStoreRefreshPolicy {
/// the operator's active task load on iPad surfaces.
static let canonicalTaskLimit = 50
/// iPad surfaces only render a small operator-focused timeline, so keep the
/// lead-event hydration set intentionally narrower than WebOS.
/// Lead timelines are hydrated through the mobile-edge bulk endpoint. Keep
/// the selected lead set bounded so every shared refresh remains one
/// predictable backend call rather than N per-lead calls.
static let leadTimelineHydrationLimit = 6
/// Fetch enough recent communication context for the visible iPad rails

View File

@@ -0,0 +1,220 @@
import CoreData
import Foundation
struct OfflineReplayRecord: Identifiable {
let id: String
let kind: String
let operation: String
let targetID: String?
let payload: Data
let queuedAt: Date
let attemptCount: Int
let lastAttemptAt: Date?
let lastError: String?
}
actor OfflineReplayStore {
static let shared = OfflineReplayStore()
private enum Schema {
static let entityName = "OfflineReplayItem"
}
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
init() {
let model = Self.makeModel()
container = NSPersistentContainer(name: "VelocityOfflineReplay", managedObjectModel: model)
let applicationSupport = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first ?? FileManager.default.temporaryDirectory
let directory = applicationSupport.appendingPathComponent("Velocity", isDirectory: true)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let description = NSPersistentStoreDescription(
url: directory.appendingPathComponent("OfflineReplay.sqlite")
)
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
#if os(iOS)
description.setOption(
FileProtectionType.complete.rawValue as NSString,
forKey: NSPersistentStoreFileProtectionKey
)
#endif
container.persistentStoreDescriptions = [description]
var loadError: Error?
container.loadPersistentStores { _, error in
loadError = error
}
if let loadError {
assertionFailure("Velocity offline replay store failed to load: \(loadError.localizedDescription)")
}
context = container.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func enqueue(kind: String, operation: String, targetID: String?, payload: Data) {
let context = context
context.performAndWait {
if let targetID {
let existing = Self.fetchManagedObjects(kind: kind, targetID: targetID, in: context)
existing.forEach(context.delete)
}
guard let entity = NSEntityDescription.entity(forEntityName: Schema.entityName, in: context) else {
return
}
let item = NSManagedObject(entity: entity, insertInto: context)
item.setValue(UUID().uuidString, forKey: "id")
item.setValue(kind, forKey: "kind")
item.setValue(operation, forKey: "operation")
item.setValue(targetID, forKey: "targetID")
item.setValue(payload, forKey: "payload")
item.setValue(Date(), forKey: "queuedAt")
item.setValue(0, forKey: "attemptCount")
item.setValue(nil, forKey: "lastAttemptAt")
item.setValue(nil, forKey: "lastError")
Self.saveIfNeeded(context)
}
}
func pendingRecords(limit: Int = 100) -> [OfflineReplayRecord] {
let context = context
var records: [OfflineReplayRecord] = []
context.performAndWait {
let request = NSFetchRequest<NSManagedObject>(entityName: Schema.entityName)
request.sortDescriptors = [
NSSortDescriptor(key: "queuedAt", ascending: true)
]
request.fetchLimit = limit
let items = (try? context.fetch(request)) ?? []
records = items.compactMap(Self.record(from:))
}
return records
}
func markCompleted(id: String) {
let context = context
context.performAndWait {
Self.fetchManagedObjects(id: id, in: context).forEach(context.delete)
Self.saveIfNeeded(context)
}
}
func markFailed(id: String, error: Error) {
let context = context
context.performAndWait {
for item in Self.fetchManagedObjects(id: id, in: context) {
let currentAttempts = item.value(forKey: "attemptCount") as? Int ?? 0
item.setValue(currentAttempts + 1, forKey: "attemptCount")
item.setValue(Date(), forKey: "lastAttemptAt")
item.setValue(error.localizedDescription, forKey: "lastError")
}
Self.saveIfNeeded(context)
}
}
func remove(kind: String, targetID: String) {
let context = context
context.performAndWait {
Self.fetchManagedObjects(kind: kind, targetID: targetID, in: context).forEach(context.delete)
Self.saveIfNeeded(context)
}
}
func reset() {
let context = context
context.performAndWait {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: Schema.entityName)
let delete = NSBatchDeleteRequest(fetchRequest: request)
_ = try? context.execute(delete)
Self.saveIfNeeded(context)
}
}
private static func fetchManagedObjects(id: String, in context: NSManagedObjectContext) -> [NSManagedObject] {
let request = NSFetchRequest<NSManagedObject>(entityName: Schema.entityName)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
return (try? context.fetch(request)) ?? []
}
private static func fetchManagedObjects(
kind: String,
targetID: String,
in context: NSManagedObjectContext
) -> [NSManagedObject] {
let request = NSFetchRequest<NSManagedObject>(entityName: Schema.entityName)
request.predicate = NSPredicate(format: "kind == %@ AND targetID == %@", kind, targetID)
return (try? context.fetch(request)) ?? []
}
private static func saveIfNeeded(_ context: NSManagedObjectContext) {
guard context.hasChanges else { return }
try? context.save()
}
private static func record(from object: NSManagedObject) -> OfflineReplayRecord? {
guard
let id = object.value(forKey: "id") as? String,
let kind = object.value(forKey: "kind") as? String,
let operation = object.value(forKey: "operation") as? String,
let payload = object.value(forKey: "payload") as? Data,
let queuedAt = object.value(forKey: "queuedAt") as? Date
else {
return nil
}
return OfflineReplayRecord(
id: id,
kind: kind,
operation: operation,
targetID: object.value(forKey: "targetID") as? String,
payload: payload,
queuedAt: queuedAt,
attemptCount: object.value(forKey: "attemptCount") as? Int ?? 0,
lastAttemptAt: object.value(forKey: "lastAttemptAt") as? Date,
lastError: object.value(forKey: "lastError") as? String
)
}
private static func makeModel() -> NSManagedObjectModel {
let model = NSManagedObjectModel()
let entity = NSEntityDescription()
entity.name = Schema.entityName
entity.managedObjectClassName = NSStringFromClass(NSManagedObject.self)
entity.properties = [
attribute("id", type: .stringAttributeType, optional: false),
attribute("kind", type: .stringAttributeType, optional: false),
attribute("operation", type: .stringAttributeType, optional: false),
attribute("targetID", type: .stringAttributeType, optional: true),
attribute("payload", type: .binaryDataAttributeType, optional: false),
attribute("queuedAt", type: .dateAttributeType, optional: false),
attribute("attemptCount", type: .integer64AttributeType, optional: false, defaultValue: 0),
attribute("lastAttemptAt", type: .dateAttributeType, optional: true),
attribute("lastError", type: .stringAttributeType, optional: true),
]
model.entities = [entity]
return model
}
private static func attribute(
_ name: String,
type: NSAttributeType,
optional: Bool,
defaultValue: Any? = nil
) -> NSAttributeDescription {
let attribute = NSAttributeDescription()
attribute.name = name
attribute.attributeType = type
attribute.isOptional = optional
attribute.defaultValue = defaultValue
return attribute
}
}