Newer Older
167 lines | 7.978kb
Bogdan Timofte authored 2 weeks ago
1
# CloudKit Sync Schema
2

            
3
## Overview
4
USB Meter utilizează `NSPersistentCloudKitContainer` pentru a sincroniza setările dispozitivelor Bluetooth pe mai multe device-uri iOS/iPadOS via iCloud CloudKit.
5

            
6
**Container CloudKit:** `iCloud.ro.xdev.USB-Meter`
7

            
8
---
9

            
10
## Core Data Entity: DeviceSettings
11

            
12
### Purpose
13
Stocare centralizată a setărilor și metadatelor de conexiune pentru fiecare contor Bluetooth, cu sincronizare automată pe toate device-urile utilizatorului.
14

            
15
### Device Name Source
Bogdan Timofte authored a week ago
16
- **All platforms (iOS, iPadOS, macOS Catalyst, iPad on Mac):** `ProcessInfo.processInfo.hostName`
17
  - Examples: "iPhone-User", "MacBook-Pro.local", "iPad-WiFi"
18
  - Rationale: Platform-agnostic, avoids device model names ("iPad", "iPhone" are useless)
Bogdan Timofte authored 2 weeks ago
19

            
20
### Attributes
21

            
22
| Atribut | Tip | Optional | Descriere |
23
|---------|-----|----------|-----------|
24
| **macAddress** | String | YES | Adresa MAC unică a contorului BT (cheie candidat pentru deduplicare) |
25
| **meterName** | String | YES | Renumire personalizată a contorului (ex: "Kitchen Meter") |
26
| **tc66TemperatureUnit** | String | YES | Preferință unitate temperatură pentru TC66C (raw value enum: "celsius"/"fahrenheit") |
27
| **modelType** | String | YES | Tip model contor ("UM25C", "UM34C", "TC66C") |
28
| **connectedByDeviceID** | String | YES | UUID-ul device-ului iOS care l-a conectat ultima dată |
29
| **connectedByDeviceName** | String | YES | Numele device-ului iOS care l-a conectat |
30
| **connectedAt** | Date | YES | Timestamp al celei mai recente conexiuni |
31
| **connectedExpiryAt** | Date | YES | TTL pentru lock-ul de conexiune (alt device poate prelua dacă a expirat) |
32
| **lastSeenAt** | Date | YES | Timestamp al ultimei avertisment BT din device |
33
| **lastSeenByDeviceID** | String | YES | UUID device-ul iOS care l-a văzut ultima dată |
34
| **lastSeenByDeviceName** | String | YES | Nume device-ul iOS care l-a văzut |
35
| **lastSeenPeripheralName** | String | YES | Numele periferalei așa cum raporta dispozitivul |
36
| **updatedAt** | Date | YES | Timestamp al ultimei actualizări (audit trail) |
37

            
38
### CloudKit Mapping
39
Fiecare atribut Core Data → câmp CloudKit în record-ul `DeviceSettings`:
40
- Sincronizare automată bidirecțională
41
- Lightweight migration pentru schimbări de schemă
42
- Conflict resolution: **NSMergeByPropertyStoreTrumpMergePolicy** (CloudKit wins pe changed properties)
43

            
44
---
45

            
46
## Constraints
47

            
48
### ❌ **NOT** Used (v1 - v2):
49
- `uniquenessConstraint` pe `macAddress` — **incompatibil cu NSPersistentCloudKitContainer** (genereaza erori silentioase)
50

            
51
### ✅ Applied:
52
- **Logical deduplication** per macAddress în `rebuildCanonicalStoreIfNeeded`
53
  - Ruleaza o dată pe device (verifică `UserDefaults` cu key `"cloudStoreRebuildVersion.{n}"`)
54
  - Merges conflicting records pe baza timestamp-urilor
55

            
56
---
57

            
58
## Data Flow
59

            
60
```
61
┌─────────────────────────────────────────────────────────────────┐
62
│                      Device A (iPhone)                          │
63
├─────────────────────────────────────────────────────────────────┤
64
│  User connects UM25C (MAC: aa:bb:cc:dd:ee:ff)                  │
65
│         ↓                                                        │
66
│  Meter.swift → appData.publishMeterConnection(mac, type)       │
67
│         ↓                                                        │
68
│  CloudDeviceSettingsStore.setConnection()                      │
69
│    - Creates / Updates DeviceSettings record                   │
70
│    - Sets connectedByDeviceID = Device A UUID                 │
71
│    - connectedAt = now                                         │
72
│    - connectedExpiryAt = now + 120s                           │
73
│         ↓                                                        │
74
│  NSPersistentCloudKitContainer saves to Core Data              │
75
│         ↓                                                        │
76
│  CloudKit framework (automatic sync):                          │
77
│    - Pushes record modificare → iCloud server                  │
78
│    - Records: [CKRecord with RecordID "DeviceSettings/mac"]   │
79
│         ↓                                                        │
80
│  iCloud Server → Device B (iPad)                               │
81
│         ↓                                                        │
82
│  Device B receives remote change notification                  │
83
│  (NSPersistentStoreRemoteChangeNotificationPostOptionKey)      │
84
│         ↓                                                        │
85
│  AppData.reloadSettingsFromCloudStore()                        │
86
│    - Fetches updated record din local Core Data                │
87
│    - Sees connectedByDeviceID = Device A UUID                 │
88
│    - Checks connectedExpiryAt (not expired)                   │
89
│    - Knows someone else is connected                          │
90
│         ↓                                                        │
91
│  UI updates: shows "Connected on iPhone"                       │
92
└─────────────────────────────────────────────────────────────────┘
93
```
94

            
95
---
96

            
97
## Conflict Resolution Scenarios
98

            
99
### Scenario 1: Simultaneous Connection Attempt
100
**Device A** și **Device B** conectează același contor ~același timp.
101

            
102
```
103
Device A:                          Device B:
104
t=0.0s: setConnection()           t=0.1s: setConnection()
105
  - connectedByDeviceID = A         - connectedByDeviceID = B
106
  - connectedAt = 0.0s              - connectedAt = 0.1s
107
       ↓ save                             ↓ save
108
  CloudKit pushes A's record    CloudKit pushes B's record
109
       ↓                              ↓
110
       └──────→ iCloud Server ←──────┘
111
               (concurrent writes)
112

            
113
Merge Strategy:
114
  - NSMergeByPropertyStoreTrumpMergePolicy
115
  - Latest CloudKit record "wins" per property
116
  - Result: B's connectedByDeviceID + connectedAt win
117
    (mai recent timestamp)
118
```
119

            
120
### Scenario 2: Reconnection After Expiry
121
**Device A** conectează la t=0. Expiry la t=120. **Device B** incearcă conexiune la t=150.
122

            
123
```
124
t=0s:   Device A connects (connectedAt=0, expiry=120)
125
t=120s: Connection expires
126
t=150s: Device B checks:
127
        if (connectedExpiryAt < now):
128
          // Lock expired, can connect
129
        setConnection() → Device B becomes new owner
130
```
131

            
132
---
133

            
134
## Deduplication Logic (v3+)
135

            
136
Rulează la `AppDelegate.didFinishLaunchingWithOptions`:
137

            
138
```swift
139
rebuildCanonicalStoreIfNeeded(version: 3)
140
  1. Fetch ALL DeviceSettings records
141
  2. Group by normalized macAddress
142
  3. Per group:
143
     - Select "winner" (prefer: more recent updatedAt, more data fields populated)
144
     - Merge other records INTO winner
145
     - Delete duplicates (only delete-uri, no inserts)
146
  4. Save context → CloudKit deletes propagate
147
  5. Mark in UserDefaults as "done for v3"
148
```
149

            
150
**De ce in-place?** Pentru a nu cauza "delete storm":
151
- ❌ Old way: Delete ALL + Insert ALL → alt device primește DELETE-uri înainte de INSERT-uri → pierde date
152
- ✅ New way: Update IN-PLACE + Delete only duplicates → record ID-ul DeviceSettings este păstrat → sincronizare progresivă
153

            
154
---
155

            
156
## Version History
157

            
158
| Model | Version | Changes |
159
|-------|---------|---------|
160
| USB_Meter.xcdatamodel | 1 | Original schema cu `uniquenessConstraint` |
161
| USB_Meter 2.xcdatamodel | 2 | ❌ Removed constraint (but rebuild was destructive) |
162
| USB_Meter 2.xcdatamodel | 3 | ✅ Rebuilt with in-place merge strategy |
163

            
164
Migration path:
165
```
166
v1 → v2 (lightweight, auto) → v3 (rebuild in-place, UserDefaults gated)
167
```