Extends CachingOutlineAPIClient with a scoped offline write queue:
updateDocument, updateCollection, pin/unpin, star/unstar, and
subscribe/unsubscribe now apply optimistically and queue via a new
PendingOperation (SwiftData) when they fail (or when a new manual
"Offline Mode" toggle forces it), then replay on reconnect via
flushPendingOperations(). Same-target edits coalesce into one queued
operation; a pin/unpin pair that never syncs cancels out instead of
queuing a delete the server never saw. Actions that would invent new
tree structure (create/move/archive/delete/duplicate) stay live-only —
reconciling a locally-invented id against the server's real one is a
separate, harder problem this pass doesn't take on. Sharing,
permissions, search, and export also stay live-only.
Added a "Full Local Sync" toggle that eagerly walks and caches the
whole workspace instead of only what's been opened, running
immediately on enable and every 20 minutes after while online.
Settings moved from a popup (Settings {} scene / PreferencesView) to
a full-page view rendered inside the root window (AppNavigation),
including the ⌘, shortcut. Folds in offline/sync management (storage
size, clear cache, pending-sync list with per-item retry) and the
About window's content (version, check for updates) so it's all in
one place.
8 new OutlineKit tests covering coalescing, cancel-out, flush
success/failure, and manual offline mode — 51/51 passing.
41 lines
1.4 KiB
Swift
41 lines
1.4 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
|
|
/// A queued mutation made while offline, waiting to replay against the live
|
|
/// server. `id` is deliberately overloaded: for actions that create a new
|
|
/// server-side record (pin, star, subscription), it's also the synthesized
|
|
/// placeholder id handed back to the caller immediately — so a matching
|
|
/// delete queued before that create ever syncs can cancel both out by id
|
|
/// instead of hitting a server that's never heard of the placeholder. For
|
|
/// actions that edit an existing record (document/collection updates), it's
|
|
/// deterministic per target id, so a second edit before the first syncs
|
|
/// coalesces into one queued operation instead of piling up.
|
|
@Model
|
|
public final class PendingOperation {
|
|
@Attribute(.unique) public var id: String
|
|
public var kind: String
|
|
public var payload: Data
|
|
public var createdAt: Date
|
|
public var lastAttemptAt: Date?
|
|
public var lastError: String?
|
|
public var attemptCount: Int
|
|
|
|
public init(
|
|
id: String,
|
|
kind: String,
|
|
payload: Data,
|
|
createdAt: Date,
|
|
lastAttemptAt: Date? = nil,
|
|
lastError: String? = nil,
|
|
attemptCount: Int = 0
|
|
) {
|
|
self.id = id
|
|
self.kind = kind
|
|
self.payload = payload
|
|
self.createdAt = createdAt
|
|
self.lastAttemptAt = lastAttemptAt
|
|
self.lastError = lastError
|
|
self.attemptCount = attemptCount
|
|
}
|
|
}
|