Aller au contenu

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 :

  1. Les requêtes reçues dans la fenêtre SYNC_CONCURRENT_WINDOW_MS sont groupées par object_id
  2. Si plusieurs requêtes du batch ont la même base_versionC1 détecté (§5.4.1 règle 1)
  3. Arbitrage LWW basé sur sync_request_id (tri lexicographique déterministe)
  4. Résultat : une seule requête devient canonical, les autres superseded
  5. Requêtes hors fenêtre avec base_version < canonicalC2 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/DELETE sur 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

  1. Entités TypeORM : SyncObject, SyncVersion, SyncRequestLog
  2. Migrations de base de données
  3. DTOs avec validation class-validator
  4. Enums : ConflictType, ResolutionRule, VersionStatus, SyncErrorCode

Phase 2 — Services core

  1. VersionService — gestion version logique monotone
  2. IdempotencyService — cache requêtes traitées
  3. HistoryService — archivage versions
  4. ConflictDetectorService — détection C1/C2/C3/C4
  5. ConflictResolverService — application R1/R2/R3/R4

Phase 3 — Orchestration

  1. SyncService — orchestration flux N1/N2/N3
  2. SyncController — endpoints REST
  3. Guards : DeviceAuthGuard, SchemaVersionGuard
  4. SyncAuditInterceptor — traçabilité automatique

Phase 4 — Export et finalisation

  1. AuditExportUtil — format canonique JSON
  2. Tests unitaires exhaustifs
  3. Tests d'intégration (scénarios S1-S4)
  4. Tests de charge (C1 concurrent)
  5. Documentation API OpenAPI

Document généré le 2025-12-29 Version : 1.0