feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
#38 Ipad app production readiness, Colony orchestration, Social posting Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #44
This commit was merged in pull request #44.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
220
iOS/velocity-ipad/velocity/Core/State/OfflineReplayStore.swift
Normal file
220
iOS/velocity-ipad/velocity/Core/State/OfflineReplayStore.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user