AngularAtlased. 22.2

Cap. 15Timp real și offline66 / 75

Actualizări optimiste și rollback

Interfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.

arhitectură ~0 min de citit

Să aștepți serverul înainte să schimbi ecranul e cinstit, dar lent: un like care apare după 400 ms pare stricat. O actualizare optimistă schimbă interfața imediat, presupune că cererea va reuși și anulează schimbarea dacă nu reușește.

Ideea încape într-o propoziție. Aplicațiile se strică la detalii: ce anume restaurezi când pică, ce se întâmplă când două răspunsuri vin în ordinea greșită și cum evită serverul să aplice aceeași acțiune de două ori când reîncerci.

De ce contează
Aproape toate cererile reușesc. Să-i faci pe toți utilizatorii să aștepte pentru puținele care pică e un schimb prost — dar o interfață optimistă care minte când una pică e și mai rea: utilizatorul pleacă crezând că modificarea s-a salvat.

Tiparul: ții minte, aplici, confirmi sau anulezi

Trei pași. Ții minte valoarea curentă, o schimbi local, trimiți cererea. La succes, răspunsul serverului înlocuiește presupunerea ta — poate să difere (un id, un timestamp, text curățat). La eșec, restaurezi ce ai ținut minte și îi spui utilizatorului: o valoare care sare înapoi fără niciun mesaj arată ca un bug.

import { Service, inject, signal } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { TodoApi, type Todo } from './todo-api';

@Service()
export class TodoStore {
  private readonly api = inject(TodoApi);
  readonly todos = signal<readonly Todo[]>([]);
  readonly error = signal<string | null>(null);

  async toggle(id: string): Promise<void> {
    const before = this.todos().find((t) => t.id === id);
    if (!before) return;

    this.patch(id, { done: !before.done }); // 1. ecranul se schimbă ACUM
    try {
      const saved = await firstValueFrom(this.api.update(id, { done: !before.done }));
      this.patch(id, saved); // 2. câștigă versiunea serverului (timestamp-uri, text normalizat...)
    } catch {
      this.patch(id, { done: before.done }); // 3. anulăm DOAR acest element...
      this.error.set(`Nu am putut salva „${before.title}”. Am anulat.`); // ...și o spunem
    }
  }

  private patch(id: string, change: Partial<Todo>): void {
    this.todos.update((list) => list.map((t) => (t.id === id ? { ...t, ...change } : t)));
  }
}
LiveOptimist vs pesimist, cu un server care mai cade

Interfața

Salvat pe server

  • Scrie notele de release
  • Revizuiește pull request-ul
  • Actualizează dependențele
  • Repară testul instabil
efect vizibil după
—
cereri
0
eșuate
0
interfață ≠ server
0
Bifează sarcini în ambele moduri și compară timpul până vezi efectul. Apoi alege „toată lista” și apasă „Rulează scenariul”.

Anulează ce ai schimbat, nu toată lista

Să salvezi un snapshot al întregii liste e varianta cea mai tentantă și e greșită de îndată ce două cereri sunt pe drum. Snapshot-ul luat de prima cerere nu conține a doua modificare; când prima pică, restaurarea lui șterge și o modificare pe care serverul a salvat-o deja. Scenariul din demo face exact asta: interfața și serverul ajung să nu mai fie de acord, fără nimic care să le repare. Anulează per element sau aplică operația inversă.

// ✗ Un snapshot al ÎNTREGII liste prinde și celelalte modificări aflate încă pe drum
async toggle(id: string) {
  const snapshot = this.todos(); // luat înainte de următorul click al utilizatorului...
  this.patch(id, { done: !this.find(id).done });
  try {
    await firstValueFrom(this.api.update(id, ...));
  } catch {
    this.todos.set(snapshot); // ...deci anulează și o modificare pe care serverul a salvat-o DEJA
  }
}

Crearea optimistă mai are un pas. Elementul nou nu are încă un id real, așa că îi dai unul temporar și îl înlocuiești când răspunde serverul. Până atunci, editările acelui element trebuie să aștepte sau folosesc id-ul temporar, pe care serverul nu îl cunoaște.

async add(title: string): Promise<void> {
  const temp: Todo = { id: `tmp-${crypto.randomUUID()}`, title, done: false };
  this.todos.update((list) => [...list, temp]);
  try {
    const saved = await firstValueFrom(this.api.create({ title }));
    // id-ul temporar dispare: orice editare ulterioară folosește id-ul real
    this.todos.update((list) => list.map((t) => (t.id === temp.id ? saved : t)));
  } catch {
    this.todos.update((list) => list.filter((t) => t.id !== temp.id));
    this.error.set(`„${title}” nu a fost adăugat.`);
  }
}

Răspunsuri care vin în ordinea greșită

Pornește o setare, oprește-o și pornește-o din nou, repede. Pleacă trei cereri și nimic nu garantează ordinea în care se întorc răspunsurile. Dacă fiecare răspuns suprascrie starea, câștigă ultimul răspuns sosit — nu ultima alegere a utilizatorului.

LiveTrei click-uri rapide, latență aleatoare

Fiecare click trimite o cerere. Bara ei se termină când sosește răspunsul.

Ai ales
off
Câștigă ultimul răspuns
off …
Doar cererea cea mai nouă
off …

„Câștigă ultimul răspuns” a greșit de 0 ori din 0.

Apasă de câteva ori „Click de trei ori”. Fiecare cerere durează 0,3–2 s; urmărește ordinea în care sosesc răspunsurile și scorul de dedesubt.

Două remedii, pentru două probleme diferite. Doar cea mai nouă: fiecare cerere primește un număr și doar răspunsul la cea mai nouă are voie să scrie — asta repară interfața. Dar și serverul poate primi cererile în altă ordine. Dacă starea finală de pe server contează, serializează scrierile cu concatMap (câte o cerere, în ordinea click-urilor) sau trimite o versiune pe care serverul o verifică. Pentru căutări, unde contează doar ultima, switchMap le anulează pe cele vechi; vezi operatorii higher-order.

private latest = 0;
readonly enabled = signal(false); // ce vede utilizatorul (optimist)
private readonly confirmed = signal(false); // ce a confirmat serverul ultima dată

async setNotifications(on: boolean): Promise<void> {
  const id = ++this.latest;
  this.enabled.set(on);
  try {
    const saved = await firstValueFrom(this.api.saveSettings({ notifications: on }));
    this.confirmed.set(saved.notifications);
    // un răspuns mai vechi, sosit târziu, nu are voie să suprascrie o alegere mai nouă
    if (id === this.latest) this.enabled.set(saved.notifications);
  } catch {
    if (id === this.latest) this.enabled.set(this.confirmed());
  }
}

Idempotență: o acțiune, aplicată o singură dată

Interfețele optimiste reîncearcă, iar aici e capcana: cererea a ajuns la server, comanda s-a creat și doar răspunsul s-a pierdut. O reîncercare creează a doua comandă. Remediul e un Idempotency-Key: un UUID generat o singură dată per acțiune a utilizatorului și trimis la fiecare reîncercare. Serverul ține minte cheile procesate și, pentru o repetare, întoarce rezultatul original în loc să refacă munca.

import { HttpClient, HttpContext, HttpContextToken, HttpInterceptorFn } from '@angular/common/http';
import { retry } from 'rxjs';

export const IDEMPOTENCY_KEY = new HttpContextToken<string | null>(() => null);

/** Cheia călătorește în contextul cererii și devine header. */
export const idempotencyInterceptor: HttpInterceptorFn = (req, next) => {
  const key = req.context.get(IDEMPOTENCY_KEY);
  return next(key ? req.clone({ setHeaders: { 'Idempotency-Key': key } }) : req);
};

// O cheie per ACȚIUNE A UTILIZATORULUI, refolosită de fiecare reîncercare a ei.
placeOrder(order: Order) {
  const key = crypto.randomUUID();
  return this.http
    .post<OrderConfirmation>('/api/orders', order, {
      context: new HttpContext().set(IDEMPOTENCY_KEY, key),
    })
    .pipe(retry({ count: 3, delay: 1000 })); // același observable → același context → aceeași cheie
}

Cheia ține de acțiunea utilizatorului, nu de cererea HTTP: generează-o când utilizatorul apasă butonul, nu într-un interceptor care ar face una nouă la fiecare reîncercare. Reîncercările sunt tratate în HttpClient și interceptori.

Când DA, când NU

Când DA
  • Acțiuni frecvente și ieftine, care reușesc aproape mereu: like-uri, bife, marcarea ca citit.
  • Modificări ușor de anulat: reordonare, redenumire, cantitatea dintr-un coș.
  • Interfețe unde latența se simte la fiecare click: liste, board-uri, editoare.
Când NU
  • Plăți și orice implică bani: utilizatorul are nevoie de o confirmare reală.
  • Acțiuni ireversibile: trimiterea unui email, ștergerea definitivă, publicarea.
  • Validare care pică des pe server (un username deja luat): ai anula tot timpul.

Capcane frecvente

  • Rollback tăcut. Bifa dispare fără niciun mesaj; utilizatorul crede că aplicația e stricată sau nu observă și merge mai departe.
  • Un snapshot al întregii liste. Anulează și alte modificări aflate pe drum, inclusiv unele pe care serverul le-a salvat deja.
  • Câștigă ultimul răspuns. Un răspuns vechi care sosește târziu suprascrie cea mai nouă alegere. Numerotează cererile.
  • Reîncercare fără cheie de idempotență. Un răspuns pierdut devine o comandă dublă.
  • Nicio reconciliere cu serverul. Id-uri temporare niciodată înlocuite, text normalizat care nu se mai întoarce: interfața se îndepărtează încet de date.

Fișă rapidă

actualizare optimistă  snapshot → aplici acum → trimiți → versiunea serverului | rollback + mesaj
rollback               doar ce a schimbat cererea asta (elementul / operația inversă)
nu toată lista         un snapshot al listei anulează și alte cereri aflate pe drum
răspunsul serverului   înlocuiește presupunerea locală (id-uri, timestamp-uri, text normalizat)
id-uri temporare       tmp-uuid până răspunde „create”, apoi îl schimbi cu id-ul real
răspunsuri în dezordine un id per cerere: doar cea mai nouă are voie să scrie (newest-only)
scrieri în ordine      concatMap: câte una, în ordinea click-urilor
citiri                 switchMap: anulezi căutarea veche, o păstrezi pe cea nouă
reîncercări            Idempotency-Key = un UUID per acțiune, același la fiecare reîncercare
niciodată optimist     plăți, acțiuni ireversibile, validare care pică des
rollback fără mesaj = o interfață care pare stricată

Cap. 15 · 0 din 3 terminate

Ai terminat lecția?

Marcheaz-o și apare pe harta ta: în cuprins și pe prima pagină.

Se marchează singură după ce citești toată lecția.

  1. WebSockets: timp real care rezistă rețelei
  2. Actualizări optimiste și rollback
  3. Offline-first: Service Worker și cache
Vezi toată harta ta

Toate cele 75 de pagini · scrie ca să filtrezi

  • 01Ce e nou în Angular 21 & 22Toate schimbările importante din v21 și v22, pe scurt, cu link către pagina care le explică.Start
  • 01Prima ta aplicație, pas cu pasO listă de lecturi construită de la o componentă goală la o aplicație terminată — signals, @for, input-uri și output-uri, un serviciu și un formular, pas cu pas.Start
  • 01Decizii rapide„Unde pun asta?”, „Signal sau Observable?”, „Shared sau feature?” — răspunsuri scurte la întrebările de zi cu zi.Start
  • 01Întrebări de interviu Angular60 de întrebări reale de interviu pentru junior, mid și senior, cu răspunsul de dat, întrebarea care urmează și greșelile de evitat. Plus o simulare de interviu.Start
  • 02Standalone & bootstrapCum pornește o aplicație fără NgModule: bootstrapApplication, app.config.ts, imports în componentă.Fundamente moderne
  • 02Data binding și evenimenteInterpolare, binding pe proprietăți și pe atribute, evenimente cu $event și filtre de taste și cum e construit [( )] — ce intră în DOM și ce se întoarce.Fundamente moderne
  • 02Control flow: @if, @for, @switchSintaxa de blocuri care înlocuiește *ngIf/*ngFor/ngSwitch, cu track, @empty și verificare exhaustivă.Fundamente moderne
  • 02@let și expresiile noi din templateVariabile locale cu @let, arrow functions, spread, regex, instanceof, comentarii în tag-uri.Fundamente moderne
  • 02@defer — lazy loading în templateÎncarci componente grele doar când e nevoie: triggere, prefetch, placeholder, loading, error.Fundamente moderne
  • 02Class & style bindingsDe ce [class.x] și [style.x] au înlocuit ngClass și ngStyle (cu migrare automată din v21).Fundamente moderne
  • 02Stilurile componentelor și încapsularea:host, cele trei moduri de încapsulare, teme cu variabile CSS și de ce ::ng-deep e ultima soluție.Fundamente moderne
  • 02Pipes: când și cumPipe pur vs impur, pipe custom și cum alegi între pipe, computed() și o metodă.Fundamente moderne
  • 03signal() și computed()Primitivele reactive: stare scriibilă, stare derivată, egalitate, untracked și reguli de imutabilitate.Signals & reactivitate
  • 03effect() — când DA, când NUEfecte secundare sincronizate cu semnale: localStorage, logging, API-uri imperative. Și capcanele lor.Signals & reactivitate
  • 03linkedSignal() — stare derivată editabilăUn semnal care se resetează când se schimbă sursa, dar pe care îl poți și seta manual.Signals & reactivitate
  • 03resource, rxResource, httpResourceDate asincrone ca semnale: status, value, error, reload, anulare automată. Stabile din v22.Signals & reactivitate
  • 03debounced() — semnale cu întârziereVarianta „debounce” a unui semnal, fără RxJS. Perfect pentru căutare.Signals & reactivitate
  • 03Liste: căutare, filtrare, sortare, paginareUn lanț de computed-uri care recalculează doar ce s-a schimbat, cu filtrele ținute în URL, ca un link să redeschidă aceeași vedere.Signals & reactivitate
  • 03Observables de la zeroCe e un Observable, subscribe și unsubscribe, operatorii pe care îi folosești în fiecare săptămână, cold vs hot — și când ajunge un signal.Signals & reactivitate
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — și când RxJS rămâne alegerea bună.Signals & reactivitate
  • 04input() și output()Inputuri ca semnale (required, transform, alias) și outputuri tipizate. Data down, events up.Componente: comunicare
  • 04model() — two-way bindingBanana-in-a-box [( )] cu un singur API. Când îl folosești și când e mai bine input + output.Componente: comunicare
  • 04viewChild & contentChildInterogări ca semnale: viewChild, viewChildren, contentChild, contentChildren, opțiunea read.Componente: comunicare
  • 04Lifecycle & DestroyRefCe hook-uri mai contează în era signals, afterNextRender, DestroyRef și ngOnChanges tipizat.Componente: comunicare
  • 04Content projectionng-content cu select, sloturi multiple, conținut implicit și capcana proiecției condiționale.Componente: comunicare
  • 04ng-template & ngTemplateOutletTemplate-uri ca valori: render props în Angular, context tipizat și personalizare de componente.Componente: comunicare
  • 05Smart vs DumbContainer vs presentational: cine ține starea, cine doar afișează, și cum le împarți corect.Design de componente
  • 05Componente generice <T>Un DataTable<T> sau Select<T> tipizat corect, cu template-uri pentru celule.Design de componente
  • 05Componente dinamiceNgComponentOutlet, createComponent cu inputBinding/outputBinding și un dashboard construit din config.Design de componente
  • 05Directive & host compositionDirective atribut, metadata host: {} și Directive Composition API (hostDirectives).Design de componente
  • 05Controale custom de formularFormValueControl (Signal Forms) vs ControlValueAccessor: un rating cu stele, în ambele variante.Design de componente
  • 05Change detectionOnPush e implicit din v22, zone.js lipsește din v21. Ce declanșează randarea și cum să nu te arzi.Design de componente
  • 06inject() & @Service()Noul decorator @Service() din v22, inject() vs constructor și contextul de injecție.Dependency Injection
  • 06Providers & ierarhia de injectoriRoot vs rută vs componentă, useClass/useValue/useFactory/useExisting și modificatorii optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Configurare tipizată, funcții provideFeature(), makeEnvironmentProviders și inițializatori.Dependency Injection
  • 06injectAsync() — servicii lazyÎncarci un serviciu greu abia când ai nevoie de el, cu prefetch la idle.Dependency Injection
  • 07Structura de folderecore / shared / features, ce intră în fiecare și cum arată un feature pe dinăuntru.Arhitectura aplicației
  • 07Shared vs featureCând muți o componentă în shared și (mai ales) când NU. Checklist concret.Arhitectura aplicației
  • 07Interfețe & modeleInterfață inline sau fișier separat? DTO vs model de domeniu, type vs interface, unde le pui.Arhitectura aplicației
  • 07State management cu signalsStore-uri simple pe semnale, facade, stare locală vs globală și când îți trebuie NgRx.Arhitectura aplicației
  • 07Style guide & namingConvențiile oficiale actuale: nume de fișiere fără sufixe, protected, readonly, ordinea membrilor.Arhitectura aplicației
  • 07Barrels, aliasuri & granițeindex.ts, path aliases, API public pe feature și reguli de dependență care se pot verifica automat.Arhitectura aplicației
  • 07Erori: @boundary & ErrorHandlerNoul bloc @boundary din v22.2, ErrorHandler global, erori HTTP și erori din resource.Arhitectura aplicației
  • 08Routing modern & lazy loadingloadComponent, rute copil, parametri ca input-uri și moștenirea parametrilor (default nou în v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate și resolve ca funcții, cu redirect prin UrlTree / RedirectCommand.Routing
  • 09Signal Forms — bazeleFormulare construite pe semnale: form(), [formField], validare prin schemă. Stabile din v22.Formulare
  • 09Signal Forms — avansatValidare custom și cross-field, async/HTTP cu debounce, liste dinamice, disabled/hidden, scheme reutilizabile.Formulare
  • 09Reactive Forms tipizateFormGroup tipizat, nonNullable, FormArray — și cum conviețuiesc cu Signal Forms.Formulare
  • 10HttpClient, interceptors & API layerHttpClient fără setup (v21), Fetch implicit (v22), interceptori funcționali și stratul de API cu mapare DTO.HTTP & date
  • 11Performanță — checklistCe chiar contează: OnPush + signals, track, @defer, lazy routes, imagini, bundle și hidratare.Calitate & performanță
  • 11Testare cu VitestVitest e runner-ul implicit din v21. TestBed, componente cu semnale, servicii, HTTP și harness-uri.Calitate & performanță
  • 11DevTools: cum te uiți în aplicațieChrome DevTools și Angular DevTools pas cu pas: ce arată fiecare panou, cu exerciții care merg chiar pe site-ul ăsta.Calitate & performanță
  • 11Angular AriaComponente headless, accesibile, pe care le stilizezi cum vrei: tabs, accordion, menu, listbox...Calitate & performanță
  • 12SSR, prerender & render modesCând randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.SSR & hidratare
  • 12Hidratare: event replay & incrementalCum preia browserul HTML-ul de pe server fără să-l refacă: hidratare completă, event replay, @defer (hydrate on …) și erorile de nepotrivire.SSR & hidratare
  • 12Cod compatibil cu serverulwindow nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.SSR & hidratare
  • 13Micro frontendsCând merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.Arhitectură la scară
  • 13Web Components cu Angular ElementscreateCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.Arhitectură la scară
  • 13Monorepo, librării & granițeUn workspace cu mai multe aplicații și librării: ng generate library, Nx, entry points secundare, publicare și reguli de dependență.Arhitectură la scară
  • 13Arhitectură hexagonală (ports & adapters)Domeniul fără Angular, porturi ca clase abstracte și adaptoare interschimbabile: HTTP, localStorage sau in-memory, fără să atingi UI-ul.Arhitectură la scară
  • 14Semnalele pe dinăuntruGraful reactiv: producători și consumatori, push-pull, de ce computed e lazy și glitch-free, problema diamantului și când rulează efectele.Reactivitate avansată
  • 14RxJS avansat: operatori de ordin superiorswitchMap, mergeMap, concatMap și exhaustMap pe o diagramă live, plus retry cu backoff, shareReplay și cum eviți scurgerile.Reactivitate avansată
  • 14NgRx SignalStoreState management la scară pe semnale: withState, withComputed, withMethods, rxMethod, entități și hooks — și când un store simplu e suficient.Reactivitate avansată
  • 14Mașini de stări cu signalsFără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.Reactivitate avansată
  • 15WebSockets: timp real care rezistă rețeleiUn socket tipizat ca signals: reconectare cu backoff și jitter, heartbeat, coadă de mesaje cât ești offline și siguranță la SSR.Timp real și offline
  • 15Actualizări optimiste și rollbackInterfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.Timp real și offline
  • 15Offline-first: Service Worker și cacheService Worker-ul Angular: strategii de cache, anunțarea versiunilor noi și o coadă pentru modificările făcute offline.Timp real și offline
  • 16Web WorkersCalcule grele fără să blochezi interfața: un worker real în pagină, comunicare prin mesaje și când nu merită.UI & performanță avansată
  • 16Liste uriașe: virtual scroll100.000 de rânduri fără lag: CDK virtual scroll, itemSize, trackBy și alternativele (paginare, infinite scroll).UI & performanță avansată
  • 16Animații moderne & View Transitionsanimate.enter și animate.leave în locul @angular/animations (depreciat), tranziții CSS și View Transitions la navigare.UI & performanță avansată
  • 16Memory leaks: cum le găsești și le repariSubscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.UI & performanță avansată
  • 17Securitate: XSS, sanitizare & CSPCe face Angular automat împotriva XSS, când devine periculos bypassSecurityTrust, plus CSP cu nonce, Trusted Types și XSRF.Securitate & globalizare
  • 17Autentificare: token-uri & refreshUn interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.Securitate & globalizare
  • 17OAuth 2.1 și OpenID Connect cu PKCEFluxul authorization code cu PKCE, pas cu pas: verifier și challenge, state și nonce, ID token-ul și de ce un BFF e mai sigur.Securitate & globalizare
  • 17Internaționalizare (i18n)$localize, atributul i18n, ICU plural/select, build per limbă vs. traduceri la runtime și formatarea după locale.Securitate & globalizare