Files
luma/Sources/LumaCore/Persistence/NotebookOutboxRecord.swift
Ole André Vadla Ravnås 72fb0dec78 Sync notebook via op log with outbox
Replace the four per-mutation notification types (+add/+update/
+remove/+reorder) with a single +op envelope carrying {op_id,
kind, ...}. Pending mutations persist to a `notebook_outbox`
table on the client and replay on every .joined transition, so
an offline user's work propagates as soon as they're back on the
bus. Projects that never enable collaboration keep a zero-row
outbox — we only touch it once `collaboration_state.lab_id` is
set, and bulk-enqueue existing entries on createLab's success.

Model changes, in the same pass:

- NotebookEntry swaps `author: Author?` for `editors: [Author]`
  (creator at index 0, each editor appended as the server applies
  their update op). The boolean `isUserNote` is gone; callers now
  read `entry.kind == .note` directly.
- Entries carry a `position: Double` — server-authoritative on
  add (`max + 1000`), client-computed midpoint on drag. Clients
  sort by `(position, id)` so concurrent reorders converge.
- Schema folds the new columns into v1 (no migration — Luma isn't
  shipped).

UI:

- Mac: header renders a stack of editor avatars (tooltip = name,
  tap opens the GitHub profile). Entry rows are draggable and
  accept drops on top/bottom halves to compute the insertion slot.
- GTK: same avatar stack; reordering lives in the right-click
  menu (Move Up / Move Down) since SwiftGtk's DragSource binding
  isn't verified yet on this branch.
2026-04-20 09:30:07 +02:00

33 lines
1.0 KiB
Swift

import Foundation
import GRDB
/// Row in `notebook_outbox`. Backs ProjectStore's pending-ops queue: each
/// row is a serialized NotebookOp awaiting delivery to the server.
struct NotebookOutboxRecord: Codable, Sendable, FetchableRecord, PersistableRecord {
static let databaseTableName = "notebook_outbox"
var opID: String
var kind: String
var entryID: String
var payloadJSON: String
var binaryData: Data?
var createdAt: Date
enum CodingKeys: String, CodingKey {
case opID = "op_id"
case kind
case entryID = "entry_id"
case payloadJSON = "payload_json"
case binaryData = "binary_data"
case createdAt = "created_at"
}
func toOp() -> NotebookOp? {
guard let data = payloadJSON.data(using: .utf8),
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return nil }
let bin = binaryData.map { Array<UInt8>($0) }
return NotebookOp.fromJSON(obj, binaryData: bin)
}
}