Skip to content

[Sync] 1 - Tombstone deleted Boards #279

Description

@KacperKozak

Parent

What to build

Deleting a Board stops removing its row. It sets deleted_at instead, on the phone and on the Vescape server, so a Board's Ride History outlives the Board that produced it.

The decision, its rejected alternatives and its consequences are in docs/adr/0027-boards-are-tombstoned-never-deleted.md and the server's docs/adr/0008-boards-are-tombstoned-never-deleted.md. Read those first; this issue is the implementation.

App side:

  • boards gains a nullable deleted_at, stamped from the native clock like every other change column, with sync_seq and updated_at moving as they do on any other write. Room 29 -> 30 and the matching GRDB migration, additive, existing rows left null.
  • Deleting a Board keeps its current behaviour for configuration — board settings, board warnings and Alert Rules are still hard-deleted — and keeps leaving telemetry untouched. The only change is that the Board row itself survives.
  • Tune Profiles are deliberately not deleted. They survive their Board and need their own deletion to go away.
  • The Rider-facing Board list filters tombstones. Lookup by id deliberately does not, because Ride History still has to name a deleted Board. Anything that acts on a Board rather than describing one — building a session config for a connect — refuses a tombstoned Board.

Server side, inside KacperKozak/vescape-server#12:

  • Applying a Board deletion sets deleted_at under the same WHERE updated_at <= deletedAt guard every action uses, and explicitly deletes the Board's configuration rows. The existing ON DELETE CASCADE behind those tables stops firing, since nothing is deleted any more.
  • Tune Profiles come out of that cascade, reversing what the schema shipped with.

This closes the mismatch flagged in the server PR at #7 and #8: an orphaned Tune Profile re-uploaded after a Board deletion currently hits the composite foreign key and refuses the whole Sync Batch. With a surviving parent row it lands.

Likely files

  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryEntities.kt - BoardEntity, where the column is declared
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDao.kt - deleteBoardWithSettings, getBoards, getBoard
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDatabase.kt - Room version and the migration
  • modules/vescape-core/ios/telemetry/TelemetryDatabase.swift - the GRDB peer migration
  • modules/vescape-core/ios/telemetry/TelemetryDao.swift - the GRDB peer of the delete and read paths
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/connection/SessionConfigBuilder.kt - buildSessionConfig, which must refuse a tombstoned Board
  • modules/vescape-core/src/index.ts - Board / BoardInput, which gain the field and must keep it un-fabricatable from JS
  • src/modules/board/store/boardStore.ts - the JS side of the Board list

Implementation hints

upsertBoard in TelemetryDao.kt already stamps both sync columns from the native clock and never trusts a bridge value, using ratchetUpdatedAt(previous, now). Follow that shape rather than adding a second stamping path — a tombstone is an ordinary write.

BoardInput is Omit<Board, 'updatedAt'> today, which is what stops a call site fabricating a timestamp. deletedAt needs the same treatment: deletion goes through the delete path, never through an upsert from JS.

deleteBoardWithSettings is the single Rider-facing delete and already sequences its children (deleteBoardSettings, deleteBoardWarnings, deleteAlertRules, deleteBoard). Replace only the last call; leave the ordering alone.

getBoards() and getBoard(id) are the only two SELECT ... FROM boards reads in the DAO, so the filtering change is genuinely two queries — but check the iOS peer separately, its DAO does not mirror them one-for-one.

Acceptance criteria

  • Deleting a Board leaves its row present with deleted_at set, on both platforms
  • Deleting a Board still removes its board settings, board warnings and Alert Rules
  • Deleting a Board leaves its telemetry frames, minute buckets and markers untouched
  • Deleting a Board leaves its Tune Profiles and their history untouched
  • A tombstoned Board does not appear in the Rider's Board list
  • A tombstoned Board is still resolvable by id, so history can name it
  • Connecting to a tombstoned Board is refused rather than attempted
  • Migration is additive, leaves existing rows null, and is a no-op on re-run, with a test on both platforms
  • Server applies a Board deletion as a tombstone plus explicit configuration deletes, under the existing replay guard
  • Server no longer cascades Tune Profiles off a Board deletion
  • A Sync Batch carrying a Tune Profile for a tombstoned Board is accepted rather than refused

Blocked by

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:boardBoard profiles, board table, and board settingsarea:dbTouches database / persistent storagearea:nativeTouches native side (modules/vesc-ble, Swift/Kotlin)area:syncBackup sync — native uploader, Sync Cursors, Sync Actions, Device Tokencomplexity:highCritical paths, subtle correctness, native pipelines. Use opus.ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions