Files

144 lines
5.9 KiB
Swift

import Foundation
/// Shipped create-Done path for `MemoEditView` (Android `createMutation` parity).
///
/// After image upload, `materializeForImage` already creates a **server** memo and sets
/// `memoId`. Done must **update** that memo — never mint a second `local:` + create.
enum MemoCreateCommit {
enum Outcome: Equatable, Sendable {
case createdLocal(memoId: String)
case updatedMaterialized(memoId: String)
}
/// Server id from materialize (not offline `local:` placeholder).
static func isMaterializedServerId(_ memoId: String?) -> Bool {
guard let memoId else { return false }
let trimmed = memoId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
return !trimmed.hasPrefix("local:")
}
/// Commit create modal content.
/// - If `memoId` is a materialized server id → optimistic mirror update + outbox update.
/// - Else → offline-first `local:` create + outbox create (Android temporaryId path).
@discardableResult
static func commit(
scope: String,
memoId: String?,
expectedRevision: Int?,
expectedContentHash: String?,
notebookId: String,
title: String,
untitledTitle: String = "无标题笔记",
contentMarkdown: String,
contentJSON: String?,
tags: [String],
mirror: LocalMirrorRepository,
outbox: SyncOutboxRepository,
drafts: DraftRepository,
now: String = EdgeEverDate.nowString()
) throws -> Outcome {
guard !notebookId.isEmpty else {
throw APIError(status: 400, code: nil, message: "请选择笔记本")
}
let displayTitle = title.isEmpty ? untitledTitle : title
if isMaterializedServerId(memoId), let serverId = memoId {
guard var memo = try mirror.resolveMemo(scope: scope, id: serverId) else {
// Mirror miss: still queue update against known server id (Android upserts optimistic).
var placeholder = MemoDetail.localPlaceholder(
id: serverId,
notebookId: notebookId,
title: displayTitle,
contentMarkdown: contentMarkdown,
tags: tags,
createdAt: now
)
placeholder.revision = expectedRevision ?? 0
placeholder.contentHash = expectedContentHash ?? "local:\(serverId)"
if let contentJSON, let json = try? JSONValue.parse(contentJSON) {
placeholder.contentJson = json
}
try mirror.upsertMemo(scope: scope, memo: placeholder)
try outbox.enqueueUpdate(
scope: scope,
payload: MemoUpdatePayload(
memoId: serverId,
expectedRevision: expectedRevision ?? 0,
expectedContentHash: expectedContentHash ?? placeholder.contentHash,
title: displayTitle,
contentMarkdown: contentMarkdown,
contentJson: contentJSON,
notebookId: notebookId,
tags: tags
)
)
try drafts.clear(scope: scope, key: DraftRepository.newKey)
try? drafts.clear(scope: scope, key: DraftRepository.memoKey(serverId))
return .updatedMaterialized(memoId: serverId)
}
// Use mirror base (server tip). Stale caller expectedRevision causes 409 conflicts.
let rev = memo.revision
let hash = memo.contentHash
memo.title = displayTitle
memo.contentMarkdown = contentMarkdown
memo.contentText = contentMarkdown
memo.tags = tags
memo.notebookId = notebookId
memo.updatedAt = now
memo.excerpt = String(contentMarkdown.prefix(160))
if let contentJSON, let json = try? JSONValue.parse(contentJSON) {
memo.contentJson = json
}
try mirror.upsertMemo(scope: scope, memo: memo)
try outbox.enqueueUpdate(
scope: scope,
payload: MemoUpdatePayload(
memoId: memo.id,
expectedRevision: rev,
expectedContentHash: hash,
title: displayTitle,
contentMarkdown: contentMarkdown,
contentJson: contentJSON,
notebookId: notebookId,
tags: tags
)
)
try drafts.clear(scope: scope, key: DraftRepository.newKey)
try? drafts.clear(scope: scope, key: DraftRepository.memoKey(serverId))
return .updatedMaterialized(memoId: serverId)
}
let localId = "local:\(UUID().uuidString.lowercased())"
var placeholder = MemoDetail.localPlaceholder(
id: localId,
notebookId: notebookId,
title: title,
contentMarkdown: contentMarkdown,
tags: tags,
createdAt: now
)
// Persist TipTap JSON so image width attrs survive into detail/viewer.
if let contentJSON, let json = try? JSONValue.parse(contentJSON) {
placeholder.contentJson = json
}
try mirror.upsertMemo(scope: scope, memo: placeholder)
try outbox.enqueueCreate(
scope: scope,
payload: MemoCreatePayload(
memoId: localId,
title: displayTitle,
contentMarkdown: contentMarkdown,
contentJson: contentJSON,
notebookId: notebookId,
tags: tags,
createdAt: now
)
)
try drafts.clear(scope: scope, key: DraftRepository.newKey)
return .createdLocal(memoId: localId)
}
}