PD-171 — Plan d'implémentation¶
EPIC de référence : PD-186 — BACKEND CORE
1. Découpage en composants¶
1.1 Architecture globale¶
src/sync/
├── sync.module.ts # Module NestJS principal
├── sync.controller.ts # Endpoints REST
├── sync.service.ts # Orchestration synchronisation
├── dto/
│ ├── sync-request.dto.ts # DTO requête de synchronisation
│ ├── sync-response.dto.ts # DTO réponse
│ └── conflict-event.dto.ts # DTO événement de conflit
├── entities/
│ ├── sync-object.entity.ts # Objet synchronisé (état canonique)
│ ├── sync-version.entity.ts # Version historique
│ └── sync-request-log.entity.ts # Log des requêtes (idempotence)
├── services/
│ ├── version.service.ts # Gestion version logique
│ ├── conflict-detector.service.ts # Détection des conflits
│ ├── conflict-resolver.service.ts # Résolution des conflits
│ ├── history.service.ts # Gestion historique versions
│ └── idempotency.service.ts # Gestion idempotence
├── guards/
│ ├── device-auth.guard.ts # Validation device_id
│ └── schema-version.guard.ts # Validation schema_version client
├── interceptors/
│ └── sync-audit.interceptor.ts # Traçabilité automatique
├── constants/
│ ├── conflict-types.constant.ts # Types C1, C2, C3, C4
│ ├── resolution-rules.constant.ts # Règles R1, R2, R3, R4
│ └── version-status.constant.ts # canonical, superseded, rejected
└── utils/
├── version-comparator.util.ts # Comparaison versions
└── audit-export.util.ts # Export format canonique
1.2 Composants détaillés¶
1.2.1 SyncModule¶
Responsabilité : Point d'entrée du mécanisme de synchronisation.
@Module({
imports: [
TypeOrmModule.forFeature([SyncObject, SyncVersion, SyncRequestLog]),
AuditModule,
DeviceModule,
],
controllers: [SyncController],
providers: [
SyncService,
VersionService,
ConflictDetectorService,
ConflictResolverService,
HistoryService,
IdempotencyService,
],
exports: [SyncService],
})
export class SyncModule {}
1.2.2 SyncObject (Entity)¶
Responsabilité : État canonique d'un objet synchronisé.
| Champ | Type | Contrainte |
|---|---|---|
object_id | UUID | PK |
version | INTEGER | >= 0, monotone |
schema_version | INTEGER | >= 1 |
data | BYTEA | Blob chiffré opaque |
device_id | UUID | FK → devices |
user_id | UUID | FK → users |
deleted_at | TIMESTAMP | NULL si actif |
created_at | TIMESTAMP | immutable |
updated_at | TIMESTAMP | auto |
1.2.3 SyncVersion (Entity)¶
Responsabilité : Historique des versions (superseded, rejected).
| Champ | Type | Contrainte |
|---|---|---|
id | UUID | PK |
object_id | UUID | FK → sync_objects |
version | INTEGER | >= 0 |
base_version | INTEGER | >= 0 |
status | ENUM | canonical, superseded, rejected |
conflict_type | ENUM | C1, C2, C3, C4, NONE |
rule_applied | ENUM | R1_LWW, R2_REJECT, R3_DELETE_PREVAILS, R4_IDEMPOTENT, NONE |
sync_request_id | UUID | unique |
device_id | UUID | FK → devices |
data | BYTEA | Blob chiffré opaque |
timestamp | TIMESTAMP | immutable |
Immuabilité stricte (§5.8.2) — Table append-only :
-- Trigger PostgreSQL interdisant UPDATE/DELETE sur sync_versions
CREATE OR REPLACE FUNCTION prevent_sync_version_mutation()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'SYNC_HISTORY_IMMUTABLE: sync_versions is append-only (§5.8.2)';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER sync_versions_immutable_update
BEFORE UPDATE ON sync_versions
FOR EACH ROW EXECUTE FUNCTION prevent_sync_version_mutation();
CREATE TRIGGER sync_versions_immutable_delete
BEFORE DELETE ON sync_versions
FOR EACH ROW EXECUTE FUNCTION prevent_sync_version_mutation();
-- Revoke DELETE/UPDATE au niveau des privilèges
REVOKE UPDATE, DELETE ON sync_versions FROM app_user;
1.2.4 SyncRequestLog (Entity)¶
Responsabilité : Idempotence protocolaire (§5.9).
| Champ | Type | Contrainte |
|---|---|---|
sync_request_id | UUID | PK |
object_id | UUID | FK |
device_id | UUID | FK |
result_version | INTEGER | version résultante |
result_status | ENUM | success, conflict_resolved, rejected |
response_payload | JSONB | réponse mise en cache |
created_at | TIMESTAMP | immutable (rétention illimitée §5.9.3) |
Immuabilité stricte (§5.9.3) — Table append-only :
-- Mêmes triggers que sync_versions pour garantir l'idempotence illimitée
CREATE TRIGGER sync_request_logs_immutable_update
BEFORE UPDATE ON sync_request_logs
FOR EACH ROW EXECUTE FUNCTION prevent_sync_version_mutation();
CREATE TRIGGER sync_request_logs_immutable_delete
BEFORE DELETE ON sync_request_logs
FOR EACH ROW EXECUTE FUNCTION prevent_sync_version_mutation();
REVOKE UPDATE, DELETE ON sync_request_logs FROM app_user;
2. Flux techniques¶
2.1 Flux N1 — Synchronisation sans conflit¶
┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Device │────▶│ SyncController│────▶│ IdempotencyService│────▶│ Déjà traité? │
└─────────┘ └──────────────┘ └─────────────────┘ └──────┬───────┘
│
┌──────────────────────────────────────────────┘
│ NON
▼
┌─────────────────┐ ┌────────────────────┐
│ VersionService │────▶│ base_version check │
└─────────────────┘ └────────┬───────────┘
│
┌─────────────────────────┘
│ base_version == canonical_version
▼
┌─────────────────┐ ┌────────────────────┐
│ SyncService │────▶│ Apply & increment │
└─────────────────┘ │ version := v + 1 │
└────────┬───────────┘
│
▼
┌─────────────────┐ ┌────────────────────┐
│ HistoryService │────▶│ Archive previous │
└─────────────────┘ │ status: superseded │
└────────┬───────────┘
│
▼
┌─────────────────┐ ┌────────────────────┐
│ AuditInterceptor│────▶│ Log sync event │
└─────────────────┘ └────────────────────┘
2.2 Flux N2 — Synchronisation avec conflit¶
┌─────────┐ ┌───────────────────────┐
│ Device │───sync_request────▶│ ConflictDetectorService│
└─────────┘ └───────────┬───────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ C1: Concurrent│ │ C2: Stale │ │ C3: Delete/ │
│ Update │ │ Update │ │ Update │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ R1: LWW │ │ R2: REJECT │ │ R3: DELETE │
│ Keep highest │ │ Return error │ │ PREVAILS │
└──────┬───────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ HistoryService: Archive loser version with status = 'superseded' │
└──────────────────────────────────────────────────────────────────────────┘
2.3 Diagramme temporel C1 vs C2¶
Timeline C1 — Concurrent Update Conflict
═══════════════════════════════════════════════════════════════════════════════
Backend (canonical) │ v=1 │ v=2 (LWW)
│ │
Device A ────────────────┼────── sync(base=1, data=A) ───────────▶│
│ │ │
Device B ────────────────┼──────────────┼── sync(base=1, data=B) ─▶│
│ │ │ │
│ └─────────┴───────────────┤
│ Les deux requêtes arrivent │
│ AVANT commit canonique │
│ → Conflit C1, règle LWW │
│ │
─────────────────────────┴────────────────────────────────────────┴────────────
t0 t1
Timeline C2 — Stale Update Conflict
═══════════════════════════════════════════════════════════════════════════════
Backend (canonical) │ v=1 │ v=2 (commit) │ REJECT
│ │ │
Device A ────────────────┼── sync(1,A) ─▶│ ✓ Applied │
│ │ │
Device B (offline) │ │ │
───────────────────────┼──────────────┼── sync(base=1, data=B) ─▶│ ✗
│ │ │ │
│ │ └───────────────┤
│ │ base_version=1 │
│ │ canonical_version=2
│ │ → Conflit C2, REJECT
│ │ │
─────────────────────────┴──────────────┴─────────────────────────┴────────────
t0 t1 t2
2.4 Algorithme de détection de conflit (déterministe)¶
Principe : La détection est basée uniquement sur les données d'entrée (request, état canonique), jamais sur l'état transitoire du système (requêtes "pending"). Cela garantit le déterminisme (Invariant 3 : mêmes entrées → même décision).
Modèle transactionnel :
-- Transaction sérialisée avec lock pessimiste
BEGIN;
SELECT * FROM sync_objects WHERE object_id = :id FOR UPDATE;
-- Évaluation déterministe basée sur l'état verrouillé
COMMIT;
Fenêtre d'évaluation concurrente (§5.4.1) : Les conflits C1 (Concurrent Update) sont détectés via le mécanisme normalisé de fenêtre d'évaluation concurrente (concurrent_evaluation_window) défini en spec §5.4.1.
Stratégie retenue : Batch temporel (stratégie recommandée par §5.4.1)
| Paramètre | Valeur | Contrainte spec |
|---|---|---|
SYNC_CONCURRENT_WINDOW_MS | 75 | ≤ 1000ms (§5.4.1) |
| Stratégie | Batch temporel | Documentée (§5.4.1) |
Algorithme :
- Les requêtes reçues dans la fenêtre
SYNC_CONCURRENT_WINDOW_MSsont groupées parobject_id - Si plusieurs requêtes du batch ont la même
base_version→ C1 détecté (§5.4.1 règle 1) - Arbitrage LWW basé sur
sync_request_id(tri lexicographique déterministe) - Résultat : une seule requête devient
canonical, les autressuperseded - Requêtes hors fenêtre avec
base_version < canonical→ C2 détecté (§5.4.1 règle 2)
Ce mécanisme garantit que la classification C1/C2 est déterministe et reproductible conformément aux propriétés garanties de §5.4.1.
function detectConflict(
request: SyncRequest,
canonical: SyncObject,
batchRequests: SyncRequest[], // Requêtes de la même fenêtre d'évaluation (§5.4.1)
): ConflictType | null {
// C4 — Duplicate Operation (idempotence déjà gérée en amont)
// Non applicable ici
// C3 — Delete / Update Conflict
if (canonical.deleted_at !== null) {
return ConflictType.C3_DELETE_UPDATE;
}
// C2 — Stale Update Conflict (base_version < canonical_version)
if (request.base_version < canonical.version) {
return ConflictType.C2_STALE_UPDATE;
}
// C1 — Concurrent Update Conflict
// Détecté si plusieurs requêtes du batch ont la même base_version
const concurrentRequests = batchRequests.filter(
r => r.base_version === request.base_version && r.sync_request_id !== request.sync_request_id
);
if (concurrentRequests.length > 0) {
return ConflictType.C1_CONCURRENT_UPDATE;
}
// Pas de conflit
return null;
}
2.5 Format d'export audit (§5.10 — Normative)¶
Responsabilité : AuditExportUtil.toCanonicalJson() produit un export déterministe, auto-suffisant et conforme au format probatoire.
Structure canonique (§5.10.3) :
interface AuditExport {
object_id: string; // UUID de l'objet
exported_at: string; // RFC3339 timestamp
canonical_version: number; // Version canonique courante
history: AuditHistoryEntry[]; // Historique COMPLET
}
interface AuditHistoryEntry {
version: number; // Version logique
base_version: number; // Version de base
status: 'canonical' | 'superseded' | 'rejected';
conflict_type: 'C1' | 'C2' | 'C3' | 'C4' | 'NONE';
rule_applied: 'R1_LWW' | 'R2_REJECT' | 'R3_DELETE_PREVAILS' | 'R4_IDEMPOTENT' | 'NONE';
sync_request_id: string; // UUID requête
device_id: string; // UUID device émetteur
timestamp: string; // RFC3339 timestamp
}
Contraintes normatives (§5.10.4) :
| Contrainte | Implémentation |
|---|---|
Ordre history strictement croissant par version | history.sort((a, b) => a.version - b.version) |
| Chaque entrée = décision effective | Pas d'entrées fictives ou interpolées |
| Aucune info implicite omise | Tous les champs obligatoires présents |
| Pas de signature/hash/scellement | Export brut, scellement externe |
| Format JSON UTF-8 déterministe | Ordre des clés fixe, pas d'espaces superflus |
Mapping règles → valeurs rule_applied :
| Règle spec | Valeur export | Cas d'application |
|---|---|---|
| R1 — LWW | R1_LWW | C1 résolu, version la plus haute retenue |
| R2 — Reject | R2_REJECT | C2 stale update rejeté |
| R3 — Delete prevails | R3_DELETE_PREVAILS | C3 update sur objet supprimé |
| R4 — Idempotent | R4_IDEMPOTENT | C4 opération dupliquée |
| Aucune | NONE | Pas de conflit (flux N1) |
Exemple d'export conforme :
{
"object_id": "550e8400-e29b-41d4-a716-446655440000",
"exported_at": "2025-12-29T14:30:00.000Z",
"canonical_version": 3,
"history": [
{
"version": 1,
"base_version": 0,
"status": "superseded",
"conflict_type": "NONE",
"rule_applied": "NONE",
"sync_request_id": "a1b2c3d4-...",
"device_id": "device-001",
"timestamp": "2025-12-29T10:00:00.000Z"
},
{
"version": 2,
"base_version": 1,
"status": "superseded",
"conflict_type": "C1",
"rule_applied": "R1_LWW",
"sync_request_id": "e5f6g7h8-...",
"device_id": "device-002",
"timestamp": "2025-12-29T12:00:00.000Z"
},
{
"version": 3,
"base_version": 2,
"status": "canonical",
"conflict_type": "NONE",
"rule_applied": "NONE",
"sync_request_id": "i9j0k1l2-...",
"device_id": "device-001",
"timestamp": "2025-12-29T14:00:00.000Z"
}
]
}
Garanties : - Déterminisme : mêmes données → même JSON byte-for-byte - Auto-suffisance : interprétable sans dépendance externe - Compatibilité probatoire : scellable, stockable WORM, lisible sans logiciel propriétaire
2.6 Diagrammes Mermaid¶
2.6.1 Graphe de dépendances des composants¶
graph TD
subgraph "Guards & Interceptors"
DAG[DeviceAuthGuard]
SVG[SchemaVersionGuard]
SAI[SyncAuditInterceptor]
end
subgraph "Controller"
SC[SyncController]
end
subgraph "Orchestration"
SS[SyncService]
end
subgraph "Services Core"
VS[VersionService]
IS[IdempotencyService]
HS[HistoryService]
CDS[ConflictDetectorService]
CRS[ConflictResolverService]
end
subgraph "Entities (TypeORM)"
SO[(SyncObject)]
SV[(SyncVersion)]
SRL[(SyncRequestLog)]
end
subgraph "Utils"
AEU[AuditExportUtil]
end
subgraph "Modules externes"
AM[AuditModule]
DM[DeviceModule]
end
SC --> DAG
SC --> SVG
SC --> SAI
SC --> SS
SS --> VS
SS --> IS
SS --> CDS
SS --> CRS
SS --> HS
VS --> SO
IS --> SRL
HS --> SV
CDS --> SO
CDS --> SV
CRS --> HS
SAI --> AM
DAG --> DM
SC --> AEU
AEU --> SO
AEU --> SV 2.6.2 Diagramme de séquence — Flux N1 (synchronisation sans conflit)¶
sequenceDiagram
participant D as Device
participant SC as SyncController
participant DAG as DeviceAuthGuard
participant IS as IdempotencyService
participant VS as VersionService
participant SS as SyncService
participant HS as HistoryService
participant SAI as SyncAuditInterceptor
participant DB as PostgreSQL
D->>SC: POST /sync (sync_request_id, object_id, base_version, data)
SC->>DAG: validate device_id (JWT claim)
DAG-->>SC: OK
SC->>IS: check(sync_request_id)
IS->>DB: SELECT FROM sync_request_logs WHERE sync_request_id = ?
DB-->>IS: NOT FOUND
IS-->>SC: new request
SC->>SS: sync(request)
SS->>DB: BEGIN; SELECT * FROM sync_objects WHERE object_id = ? FOR UPDATE
DB-->>SS: canonical (version=N)
SS->>VS: checkBaseVersion(base_version=N, canonical_version=N)
VS-->>SS: OK (no conflict)
SS->>DB: UPDATE sync_objects SET version=N+1, data=?
SS->>HS: archivePrevious(object_id, version=N, status='superseded')
HS->>DB: INSERT INTO sync_versions (...)
SS->>IS: log(sync_request_id, result)
IS->>DB: INSERT INTO sync_request_logs (...)
SS->>DB: COMMIT
SS-->>SC: SyncResponse (version=N+1)
SC->>SAI: log audit event
SAI-->>SC: OK
SC-->>D: 200 OK {version: N+1} 2.6.3 Diagramme de séquence — Flux N2 (conflit C1 — Concurrent Update, résolution LWW)¶
sequenceDiagram
participant DA as Device A
participant DB2 as Device B
participant SC as SyncController
participant CDS as ConflictDetectorService
participant CRS as ConflictResolverService
participant HS as HistoryService
participant DB as PostgreSQL
Note over DA, DB2: Les deux devices ont base_version=1
DA->>SC: POST /sync (req_id=A, base=1, data=dataA)
DB2->>SC: POST /sync (req_id=B, base=1, data=dataB)
Note over SC: Requêtes groupées dans la fenêtre<br/>SYNC_CONCURRENT_WINDOW_MS (75ms)
SC->>DB: BEGIN; SELECT * FROM sync_objects FOR UPDATE
DB-->>SC: canonical (version=1)
SC->>CDS: detectConflict(batchRequests=[A, B], canonical_version=1)
CDS-->>SC: C1_CONCURRENT_UPDATE
SC->>CRS: resolve(C1, requests=[A, B])
Note over CRS: R1_LWW : tri lexicographique<br/>par sync_request_id
CRS->>HS: archive loser (status='superseded', rule=R1_LWW)
HS->>DB: INSERT INTO sync_versions (loser)
CRS->>DB: UPDATE sync_objects SET version=2, data=winner_data
CRS->>HS: archive winner history
HS->>DB: INSERT INTO sync_versions (winner, status='canonical')
CRS->>DB: COMMIT
SC-->>DA: 200 OK {version: 2, conflict: C1, rule: R1_LWW}
SC-->>DB2: 200 OK {version: 2, conflict: C1, rule: R1_LWW, your_status: superseded} 2.6.4 Diagramme de séquence — Flux idempotence (replay détecté)¶
sequenceDiagram
participant D as Device
participant SC as SyncController
participant IS as IdempotencyService
participant DB as PostgreSQL
D->>SC: POST /sync (sync_request_id=X, ...)
SC->>IS: check(sync_request_id=X)
IS->>DB: SELECT FROM sync_request_logs WHERE sync_request_id = X
DB-->>IS: FOUND (response_payload cached)
IS-->>SC: cached response
Note over SC: Aucune mutation en DB,<br/>réponse cachée retournée telle quelle
SC-->>D: 200 OK {cached response_payload} 3. Mapping invariants → mécanismes¶
| # | Invariant (Spec §4) | Mécanisme technique |
|---|---|---|
| 1 | Aucune perte silencieuse de données | HistoryService archive TOUTES les versions (superseded, rejected) dans sync_versions. Contrainte NOT NULL sur status. Transaction ACID. |
| 2 | Chaque modification associée à device + versions | DTO SyncRequest avec champs obligatoires device_id, base_version. Entity SyncVersion stocke device_id, version, base_version. |
| 3 | Détection de conflit déterministe | ConflictDetectorService avec algorithme pur basé sur données d'entrée (batch + état canonique), jamais sur état transitoire. Batch window pour C1 déterministe. Enum ConflictType exhaustif (C1-C4). Tests unitaires paramétrés. |
| 4 | Règles de résolution documentées et testables | Enum ResolutionRule (R1-R4). ConflictResolverService avec switch exhaustif. Documentation inline. Tests de non-régression. |
| 5 | Rejet explicite si non résoluble | ConflictResolverService.resolve() throw SyncConflictException avec code explicite. Aucun fallback silencieux. |
| 6 | Backend détient état canonique final | Table sync_objects unique source de vérité. Pas de réplication master-master. Constraint UNIQUE sur (object_id). |
| 7 | Device connaît version de référence | DTO validation base_version required. Guard @ValidBaseVersion() vérifie cohérence. Rejet si absent/invalide. |
| 8 | Idempotence protocolaire | IdempotencyService + table sync_request_logs. Clé unique sync_request_id. Retour réponse cachée si replay. |
3.1 Mapping sections normatives → composants¶
| Section Spec | Mécanisme |
|---|---|
| §4.1 Modèle version logique | VersionService.increment() — monotone, autorité backend |
| §4.2 Versionnement schéma | SchemaVersionGuard — rejet client obsolète, migration auto |
| §4.3 Longues périodes offline | ConflictDetectorService — détection C2 si base < canonical (règle §4.1.5), divergence excessive si écart > 1 |
| §4.4 Contraintes performance | Séparation flux primaire/secondaire, index sur status='canonical' |
| §4.5 Intégration sécurité | DeviceAuthGuard, isolation device_id, rotation = nouveau device |
| §5.3 Types de conflits | Enum ConflictType : C1, C2, C3, C4 |
| §5.4 Règles résolution | Enum ResolutionRule + ConflictResolverService |
| §5.4.1 Fenêtre évaluation concurrente | SYNC_CONCURRENT_WINDOW_MS=75, stratégie batch temporel, classification C1/C2 déterministe |
| §5.8 Historique versions | HistoryService + entity SyncVersion |
| §5.8.2 Immuabilité | Triggers PostgreSQL BEFORE UPDATE/DELETE + REVOKE UPDATE, DELETE — table append-only |
| §5.9 Idempotence | IdempotencyService + entity SyncRequestLog |
| §5.10 Format export audit | AuditExportUtil.toCanonicalJson() |
4. Gestion des erreurs¶
4.1 Enum SyncErrorCode¶
export enum SyncErrorCode {
// E1 — Version inconnue ou absente
MISSING_BASE_VERSION = 'SYNC_001',
INVALID_BASE_VERSION = 'SYNC_002',
// E2 — Device non reconnu
UNKNOWN_DEVICE = 'SYNC_003',
DEVICE_REVOKED = 'SYNC_004',
// E3 — Conflit non résoluble
UNRESOLVABLE_CONFLICT = 'SYNC_005',
VERSION_GAP_TOO_LARGE = 'SYNC_006',
// E4 — Violation d'invariant
INVARIANT_VIOLATION = 'SYNC_007',
// E5 — Erreur de cohérence interne
CANONICAL_STATE_UNAVAILABLE = 'SYNC_008',
TRANSACTION_FAILED = 'SYNC_009',
// Schema version
CLIENT_SCHEMA_OUTDATED = 'SYNC_010',
// Idempotency
DUPLICATE_REQUEST_MISMATCH = 'SYNC_011',
}
4.2 Exception SyncException¶
export class SyncException extends Error {
constructor(
public readonly code: SyncErrorCode,
public readonly message: string,
public readonly context: {
object_id?: string;
device_id?: string;
base_version?: number;
canonical_version?: number;
conflict_type?: ConflictType;
},
) {
super(message);
}
}
4.3 Mapping erreurs → codes HTTP¶
| SyncErrorCode | HTTP Status | Réponse |
|---|---|---|
| MISSING_BASE_VERSION | 400 | Bad Request |
| INVALID_BASE_VERSION | 400 | Bad Request |
| UNKNOWN_DEVICE | 401 | Unauthorized |
| DEVICE_REVOKED | 403 | Forbidden |
| UNRESOLVABLE_CONFLICT | 409 | Conflict |
| VERSION_GAP_TOO_LARGE | 409 | Conflict |
| INVARIANT_VIOLATION | 422 | Unprocessable Entity |
| CANONICAL_STATE_UNAVAILABLE | 503 | Service Unavailable |
| TRANSACTION_FAILED | 500 | Internal Server Error |
| CLIENT_SCHEMA_OUTDATED | 426 | Upgrade Required |
| DUPLICATE_REQUEST_MISMATCH | 409 | Conflict |
4.4 Réponse d'erreur standardisée¶
interface SyncErrorResponse {
error: {
code: SyncErrorCode;
message: string;
conflict?: {
type: ConflictType;
canonical_version: number;
your_base_version: number;
};
required_action?: 'RESYNC' | 'UPGRADE_CLIENT' | 'RETRY';
};
}
5. Impacts sécurité¶
5.1 Isolation des devices (Spec §4.5.2)¶
| Risque | Mitigation |
|---|---|
| Usurpation de device_id | DeviceAuthGuard valide token JWT contenant device_id signé |
| Accès cross-user | Clause WHERE user_id = :currentUserId sur toutes les requêtes |
| Replay attack | sync_request_id unique, rétention illimitée (idempotence garantie §5.9.3) |
5.2 Données chiffrées (Spec §4.5.1)¶
| Principe | Implémentation |
|---|---|
| Backend traite blobs opaques | Colonne data de type BYTEA, aucun parsing |
| Pas de dépendance au contenu clair | Détection/résolution basée uniquement sur métadonnées |
| Aucune indexation sur contenu | Pas d'index full-text sur data |
5.3 Rotation d'identifiants (Spec §4.5.3)¶
// Lors d'une rotation de clé
async rotateDevice(userId: string, oldDeviceId: string): Promise<string> {
// 1. Créer nouveau device_id
const newDeviceId = await this.deviceService.create(userId);
// 2. NE PAS migrer l'historique (continuité probatoire)
// L'ancien device_id reste dans l'historique
// 3. Le nouveau device doit resynchroniser
return newDeviceId;
}
5.4 Audit trail¶
| Événement | Données tracées |
|---|---|
| Sync success | object_id, device_id, old_version, new_version, timestamp |
| Conflict detected | conflict_type, competing_versions, rule_applied |
| Conflict resolved | winner_version, loser_versions (archived) |
| Sync rejected | reason, base_version, canonical_version |
6. Hypothèses techniques¶
| ID | Hypothèse | Justification | Impact si invalide |
|---|---|---|---|
| H1 | PostgreSQL comme SGBD | Transactions ACID, JSONB, BYTEA natifs | Adapter les types et transactions |
| H2 | Un seul backend (pas de multi-master) | Backend = autorité unique (Invariant 6) | Architecture à repenser si distribué |
| H3 | device_id fourni par module Device existant | Spec §4.5.2 exige device_id unique stable | Implémenter DeviceModule si absent |
| H4 | Authentification JWT avec device_id claim | Isolation device basée sur token | Adapter le guard d'authentification |
| H5 | Idempotence sans limite de durée | Spec §5.9.3 impose rétention illimitée des sync_request_id | Archivage froid possible, JAMAIS de purge |
| H6 | Volume historique acceptable | Spec §5.8.3 : rétention illimitée | Prévoir archivage froid si volume explose |
| H7 | Chiffrement côté client effectif | Backend ne déchiffre jamais data | Pas d'impact (blob opaque) |
| H8 | NestJS comme framework | Structure modules/guards/interceptors | Adapter patterns si autre framework |
| H9 | Batch window configurable (50-100ms) | C1 déterministe via groupement temporel des requêtes | Ajuster la fenêtre selon latence réseau, ou désactiver (C1 impossible = tout devient C2) |
7. Points de vigilance¶
7.1 Concurrence et locks¶
| Point | Risque | Mitigation |
|---|---|---|
| Race condition C1 | Deux requêtes simultanées avec même base_version | Batch window : requêtes groupées par fenêtre temporelle, C1 détecté de manière déterministe sur le batch, arbitrage LWW par sync_request_id |
| Deadlock | Transactions croisées sur plusieurs objets | Ordonner les locks par object_id, timeout court |
| Double incrément | Retry après timeout réseau | Idempotence via sync_request_id |
7.2 Performance¶
| Point | Risque | Mitigation |
|---|---|---|
| Historique volumineux | Requêtes lentes sur sync_versions | Index partiel sur status = 'canonical', pagination |
| Export audit | Timeout sur objets avec long historique | Streaming JSON, pagination, async job |
| Idempotency table | Table sync_request_logs volumineuse | Archivage froid (S3/Glacier), index partiel, sharding par date — JAMAIS de purge (§5.9.3) |
7.3 Cohérence des données¶
| Point | Risque | Mitigation |
|---|---|---|
| Gap de version | Version manquante dans l'historique | Constraint CHECK sur continuité, tests d'intégrité |
| Orphan versions | sync_version sans sync_object parent | FK avec ON DELETE CASCADE, ou orphan cleanup job |
| Schema migration | Objet ancien non migré | Migration automatique à l'écriture (§4.2.4) |
7.4 Tests critiques¶
| Scénario | Priorité | Type |
|---|---|---|
| C1 batch window déterministe | CRITIQUE | Integration — rejouer même batch produit même résultat (LWW par sync_request_id) |
| Immuabilité sync_versions (§5.8.2) | CRITIQUE | Integration — vérifie que UPDATE/DELETE lèvent exception |
| Immuabilité sync_request_logs (§5.9.3) | CRITIQUE | Integration — vérifie que UPDATE/DELETE lèvent exception |
| C2 après longue période offline | HAUTE | Integration |
| Idempotence sur retry réseau | HAUTE | Unit + Integration |
| Export audit format canonique | HAUTE | Unit |
| Migration schema_version | MOYENNE | Integration |
| Rotation device_id continuité historique | MOYENNE | Integration |
7.5 Checklist pré-implémentation¶
- Module Device existant et fonctionnel
- JWT contient claim device_id
- Transaction PostgreSQL configurée (isolation level)
- AuditModule disponible pour traçabilité
- Tests de charge prévus pour scénarios C1
- Stratégie d'archivage sync_request_logs définie (rétention illimitée §5.9.3)
- Triggers d'immuabilité déployés (§5.8.2, §5.9.3) —
BEFORE UPDATE/DELETEsur sync_versions et sync_request_logs - Privilèges REVOKE configurés pour app_user (pas de UPDATE/DELETE sur tables append-only)
- Index PostgreSQL planifiés
- Migration schéma base de données préparée
8. Séquence d'implémentation recommandée¶
Phase 1 — Fondations¶
- Entités TypeORM :
SyncObject,SyncVersion,SyncRequestLog - Migrations de base de données
- DTOs avec validation class-validator
- Enums :
ConflictType,ResolutionRule,VersionStatus,SyncErrorCode
Phase 2 — Services core¶
VersionService— gestion version logique monotoneIdempotencyService— cache requêtes traitéesHistoryService— archivage versionsConflictDetectorService— détection C1/C2/C3/C4ConflictResolverService— application R1/R2/R3/R4
Phase 3 — Orchestration¶
SyncService— orchestration flux N1/N2/N3SyncController— endpoints REST- Guards :
DeviceAuthGuard,SchemaVersionGuard SyncAuditInterceptor— traçabilité automatique
Phase 4 — Export et finalisation¶
AuditExportUtil— format canonique JSON- Tests unitaires exhaustifs
- Tests d'intégration (scénarios S1-S4)
- Tests de charge (C1 concurrent)
- Documentation API OpenAPI
Document généré le 2025-12-29 Version : 1.0