feat: reconcile retained client commands

This commit is contained in:
2026-09-06 07:18:52 +00:00
parent 3ecab55faa
commit 7e902d1103
3 changed files with 81 additions and 7 deletions
+63
View File
@@ -0,0 +1,63 @@
package store
import (
"context"
"database/sql"
"errors"
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
"github.com/rvbox/rvbox/internal/domain"
)
// ReconcileClientSnapshot compares client evidence with this client's durable
// server rows. It intentionally performs no lifecycle mutation yet: callers
// receive only the safe local terminate/discard instructions.
func (store *Store) ReconcileClientSnapshot(ctx context.Context, clientID string, snapshot *rvboxv1.ReconcileSnapshot) (*rvboxv1.ReconcileResult, error) {
if snapshot == nil {
return nil, errors.New("missing client reconciliation snapshot")
}
result := &rvboxv1.ReconcileResult{}
for _, client := range snapshot.GetRetainedCommands() {
issue, err := domain.ParseUUIDv7(client.GetIssueUuid())
if err != nil {
return nil, err
}
input := domain.ReconcileInput{ClientEvidence: domain.ClientEvidenceRetained, ClientLifecycle: client.GetLifecycle(), ClientRevision: domain.CommandRevision(client.GetCommandRevision()), ClientLastEventSeq: client.GetLastClientEventSeq(), ImmutableHashMatches: true}
if client.GetTombstoned() {
input.ClientEvidence = domain.ClientEvidenceTombstone
}
var lifecycle uint32
var revision, lastSequence uint64
var hash []byte
err = store.db.QueryRowContext(ctx, `SELECT lifecycle, revision, last_event_seq, immutable_request_sha256 FROM commands WHERE issue_uuid = ? AND client_id = ?`, issue[:], clientID).Scan(&lifecycle, &revision, &lastSequence, &hash)
if err == nil {
input.ServerPresent = true
input.ServerLifecycle = rvboxv1.CommandLifecycle(lifecycle)
input.ServerRevision = domain.CommandRevision(revision)
input.ServerLastEventSeq = lastSequence
input.ImmutableHashMatches = len(hash) == len(client.GetImmutableRequestSha256()) && string(hash) == string(client.GetImmutableRequestSha256())
} else if !errors.Is(err, sql.ErrNoRows) {
return nil, err
} else {
var tombstoneHash []byte
err = store.db.QueryRowContext(ctx, `SELECT immutable_sha256 FROM command_tombstones WHERE issue_uuid = ? AND client_id = ?`, issue[:], clientID).Scan(&tombstoneHash)
if err == nil {
input.ServerHasTombstone = true
input.ImmutableHashMatches = len(tombstoneHash) == len(client.GetImmutableRequestSha256()) && string(tombstoneHash) == string(client.GetImmutableRequestSha256())
} else if !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
}
decision, err := domain.DecideReconciliation(input)
if err != nil && !errors.Is(err, domain.ErrReconcileContradiction) {
return nil, err
}
switch decision.Action {
case domain.ReconcileTerminateLocal:
result.TerminateLocalIssueUuids = append(result.TerminateLocalIssueUuids, issue.String())
case domain.ReconcileDiscardLocalTerminal:
result.DiscardLocalTerminalIssueUuids = append(result.DiscardLocalTerminalIssueUuids, issue.String())
}
}
return result, nil
}