Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)
1<?php
2
3namespace SocialDept\AtpParity\Sync;
4
5use Illuminate\Database\Eloquent\Model;
6use SocialDept\AtpParity\Contracts\RecordMapper;
7use SocialDept\AtpParity\Events\ConflictDetected;
8use SocialDept\AtpSchema\Data\Data;
9
10/**
11 * Resolves conflicts between local and remote record versions.
12 */
13class ConflictResolver
14{
15 /**
16 * Resolve a conflict according to the specified strategy.
17 */
18 public function resolve(
19 Model $model,
20 Data $record,
21 array $meta,
22 RecordMapper $mapper,
23 ConflictStrategy $strategy
24 ): ConflictResolution {
25 return match ($strategy) {
26 ConflictStrategy::RemoteWins => $this->applyRemote($model, $record, $meta, $mapper),
27 ConflictStrategy::LocalWins => $this->keepLocal($model),
28 ConflictStrategy::NewestWins => $this->compareAndApply($model, $record, $meta, $mapper),
29 ConflictStrategy::Manual => $this->flagForReview($model, $record, $meta, $mapper),
30 };
31 }
32
33 /**
34 * Apply the remote version, overwriting local changes.
35 */
36 protected function applyRemote(
37 Model $model,
38 Data $record,
39 array $meta,
40 RecordMapper $mapper
41 ): ConflictResolution {
42 $mapper->updateModel($model, $record, $meta);
43 $model->save();
44
45 return ConflictResolution::remoteWins($model);
46 }
47
48 /**
49 * Keep the local version, ignoring remote changes.
50 */
51 protected function keepLocal(Model $model): ConflictResolution
52 {
53 return ConflictResolution::localWins($model);
54 }
55
56 /**
57 * Compare timestamps and apply the newest version.
58 */
59 protected function compareAndApply(
60 Model $model,
61 Data $record,
62 array $meta,
63 RecordMapper $mapper
64 ): ConflictResolution {
65 $localUpdatedAt = $model->getAttribute('updated_at');
66
67 // Try to get remote timestamp from record
68 $remoteCreatedAt = $record->createdAt ?? null;
69
70 // If we can't compare, default to remote wins
71 if (! $localUpdatedAt || ! $remoteCreatedAt) {
72 return $this->applyRemote($model, $record, $meta, $mapper);
73 }
74
75 // Compare timestamps
76 if ($localUpdatedAt > $remoteCreatedAt) {
77 return $this->keepLocal($model);
78 }
79
80 return $this->applyRemote($model, $record, $meta, $mapper);
81 }
82
83 /**
84 * Flag the conflict for manual review.
85 */
86 protected function flagForReview(
87 Model $model,
88 Data $record,
89 array $meta,
90 RecordMapper $mapper
91 ): ConflictResolution {
92 // Create a pending conflict record
93 $conflict = PendingConflict::create([
94 'model_type' => get_class($model),
95 'model_id' => $model->getKey(),
96 'uri' => $meta['uri'] ?? null,
97 'local_data' => $model->toArray(),
98 'remote_data' => $this->buildRemoteData($record, $meta, $mapper),
99 'status' => 'pending',
100 ]);
101
102 // Dispatch event for notification
103 event(new ConflictDetected($model, $record, $meta, $conflict));
104
105 return ConflictResolution::pending($conflict);
106 }
107
108 /**
109 * Build the remote data array for storage.
110 */
111 protected function buildRemoteData(Data $record, array $meta, RecordMapper $mapper): array
112 {
113 // Create a temporary model with the remote data
114 $tempModel = $mapper->toModel($record, $meta);
115
116 return $tempModel->toArray();
117 }
118}