Skip to content

Phase 1.f — Sync ops protocol + WebSocket + desktop integration #133

@InstaZDLL

Description

@InstaZDLL

Tracking issue for sub-phase 1.f of RFC-001. Largest sub-phase by surface area.

Implement the multi-device sync protocol from RFC §6.6 end-to-end: server-side ops log + watermark + cursor + compaction job, WebSocket fan-out, desktop client integration with a "Server mode" toggle.

Scope — waveflow-server

Schema (per RFC §6.6):

  • sync_op table with BIGSERIAL id, operation_id UUID, lamport_ts BIGINT, double UNIQUE on (user_id, device_id, operation_id) and (user_id, device_id, lamport_ts).
  • device_sync_cursor (user_id, device_id, last_seen_id, last_seen_at).
  • sync_compaction_watermark (user_id, compacted_up_to, updated_at).
  • Composite index device_sync_cursor (user_id, last_seen_at, last_seen_id) for the compaction MIN query.

Endpoints:

  • POST /sync/ops — push batch. Idempotency check on operation_id first (returns 200 + existing row on dup), then per-device monotonicity check (409 with stored max on violation), then BIGSERIAL assignment in a transaction, then WebSocket broadcast.
  • GET /sync/ops?since=N — pull. Resurrected-device guard: since > 0 AND since < compacted_up_to410 Gone {"type":"resync_required","compacted_up_to":N}. No cursor write here.
  • POST /sync/ack {"last_seen_id": N}only path that advances device_sync_cursor.
  • GET /sync/ws — WebSocket. Server pushes new ops; client sends {"ack": N} frames.

Background:

  • ACK debouncing: in-memory latest per (user_id, device_id), flush every 5s or on disconnect. Compaction MIN reads the in-memory value, never the stale Postgres row.
  • Nightly compaction job: reads MIN(last_seen_id) excluding devices stale > 90 days (configurable), collapses ops with id < min per (entity_id, field), updates sync_compaction_watermark.compacted_up_to in the same transaction as the delete.

Scope — waveflow (desktop)

  • Settings → "Server mode" toggle (Local / Server-connected / Hybrid).
  • Repository swap at runtime: existing Tauri commands route to either local SQLite or HTTP/WebSocket axum based on mode.
  • Device-side Lamport clock (persisted in app_setting), increment-on-emit, max+1 on receive.
  • Pending-ops queue persisted locally so offline edits sync when the connection comes back.
  • Login flow: in-app webview to the Better Auth magic-link page, JWT stored in OS keyring via secure-storage plugin.

Critical invariants (must hold or compaction is unsafe)

  • Compaction job's delete + watermark update happen in the same Postgres transaction.
  • Compaction MIN computation includes any unflushed in-memory ACK values.
  • Server never writes device_sync_cursor outside of an explicit ACK path.

Dependencies

  • Blocks on: 1.d (auth) + 1.e (HTTP fundamentals). Web integration may stay minimal in this sub-phase — focus is server + desktop.

Acceptance criteria

  • Two desktop instances logged into the same user account see playlist edits cross-sync within ~1s over WebSocket.
  • Crash-recovery: kill the server mid-push; client retry with same operation_id is idempotent.
  • Resurrected-device simulation: bump system clock 91 days, run compaction, reconnect old device → receives 410 + full-resync.
  • Offline-then-online: edit on a disconnected client → reconnect → edits sync.
  • Lamport monotonicity 409 retry round-trip works.
  • Compaction job atomicity: kill the process mid-compaction → watermark stays consistent with table state.

Estimate

~4-5 weeks. Most complex sub-phase.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphase: 1Phase 1 — waveflow-server (RFC-001)rustPull requests that update rust codescope: backendRust/Tauri backend (src-tauri/)scope: frontendReact/Vite frontend (src/)

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions