Overview

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

BackendSyncEngine — Universal Backend Persistence

Date: 2026-03-03 | Session: 166.3 | Status: Production-Ready

The BackendSyncEngine is a generic, reusable sync engine inside @storage-system/sync. It provides retry-resilient, fire-and-forget backend persistence for any data type. Any feature can get full offline-aware backend sync by registering a SyncDescriptor (5 lines) and optionally calling syncToBackend() (1 line).


Quick Start

1. Provide the transport function (once per app)

// app.config.ts
import { provideBackendSyncTransport } from '@storage-system';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBackendSyncTransport(),
    // ... other providers
  ]
};

2. Register a descriptor + use syncToBackend()

import { Injectable, inject } from '@angular/core';
import { autoFlashState, BackendSyncEngine, syncToBackend } from '@storage-system';
import { API_BASE_URL } from '@libs-foundational/api-config';

@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
  private readonly engine = inject(BackendSyncEngine);
  private readonly apiUrl = inject(API_BASE_URL);

  // Step 1: Local persistence (IndexedDB, instant, cross-tab synced)
  readonly preferences = autoFlashState('user-prefs', {
    theme: 'light',
    language: 'en',
    sidebarOpen: true,
  });

  constructor() {
    // Step 2: Register backend sync descriptor
    this.engine.register({
      key: 'user-prefs',
      method: 'PUT',
      buildUrl: () => `${this.apiUrl}/users/me/preferences`,
      toPayload: (op) => op.data,
      dedupKey: () => 'user-prefs', // Latest value always wins
    });

    // Step 3: Auto-sync signal changes to backend
    syncToBackend(this.preferences, {
      descriptorKey: 'user-prefs',
      debounceMs: 1000,
    });
  }
}

That's it. The engine handles:


Architecture

┌─────────────────────┐
│  autoFlashState()    │  Instant local persistence (IndexedDB)
│  WritableSignal<T>   │  Zero-config, cross-tab, < 1ms
└──────────┬──────────┘
           │ syncToBackend() watches signal
           ▼
┌─────────────────────┐
│  BackendSyncEngine   │  Generic sync orchestrator
│  ├─ register()       │  Define HOW to sync (SyncDescriptor)
│  ├─ sync()           │  Fire-and-forget with auto-queue
│  └─ drainRetryQueue  │  Process pending items on reconnect
└──────────┬──────────┘
           │ via SYNC_TRANSPORT_FN (HttpClient bridge)
           ▼
┌─────────────────────┐
│  Backend API         │  Any NestJS/Express endpoint
│  (MongoDB/Postgres)  │
└─────────────────────┘

Key Design: HttpClient Abstraction

@storage-system never imports HttpClient directly. Instead, the consuming app provides a transport function via SYNC_TRANSPORT_FN injection token:

provideBackendSyncTransport()  // Creates HttpClient → SyncTransportFn bridge

This keeps the storage library framework-agnostic at its core.


SyncDescriptor API

interface SyncDescriptor<T> {
  key: string;                                        // Unique identifier
  method: 'POST' | 'PUT' | 'PATCH' | 'DELETE';       // HTTP method
  buildUrl: (op: PendingSyncItem<T>) => string;       // Dynamic URL builder
  toPayload: (op: PendingSyncItem<T>) => object|null; // Request body builder
  dedupKey: (data: T) => string | null;               // Dedup strategy (null = no dedup)
  isSuccess?: (response: unknown) => boolean;          // Custom success check
  priority?: 'critical' | 'high' | 'normal' | 'low';  // Queue priority
}

Examples

Chat messages (append, per-session URL):

engine.register({
  key: 'chat-messages',
  method: 'POST',
  buildUrl: (op) => `${aiApiUrl}/sessions/${op.contextId}/messages`,
  toPayload: (op) => ({ messages: op.data.messages, userId: op.data.userId }),
  dedupKey: (data) => data.messages.map(m => m.id).join(','),
  priority: 'high',
});

Theme config (upsert, fixed URL):

engine.register({
  key: 'theme-config',
  method: 'PUT',
  buildUrl: () => `${apiUrl}/themes/current`,
  toPayload: (op) => op.data,
  dedupKey: () => 'theme', // Always dedup — only latest matters
});

Delete operation:

engine.register({
  key: 'delete-item',
  method: 'DELETE',
  buildUrl: (op) => `${apiUrl}/items/${op.contextId}`,
  toPayload: () => null,
  dedupKey: (data) => data.id,
});

syncToBackend() API

function syncToBackend<T>(
  signalRef: WritableSignal<T>,
  options: {
    descriptorKey: string;       // Must match a registered descriptor
    contextId?: string;          // Passed to buildUrl()
    debounceMs?: number;         // Default: 500
    shouldSync?: (v: T) => boolean; // Predicate filter
    transform?: (v: T) => unknown;  // Transform before sending
  }
): void;

Behavior:


Observability

The StorageObservabilityService collects metrics from the engine:

import { StorageObservabilityService } from '@storage-system';

const obs = inject(StorageObservabilityService);
obs.startPeriodicCollection(30000); // Every 30s

// Read metrics
const metrics = obs.metrics();
// metrics.syncQueue.depth, metrics.quota.percentage, metrics.cache.hitRate

// Check alerts
const alerts = obs.alerts(); // string[] of threshold breaches

// Prometheus export
const text = obs.toPrometheusText();

Migration Guide

From direct localStorage

// BEFORE
const theme = localStorage.getItem('theme') || 'light';
localStorage.setItem('theme', newTheme);

// AFTER
const theme = autoFlashState('theme', 'light');
// + register descriptor + syncToBackend() for backend persistence

From NGXS @State

// BEFORE (NGXS)
@State({ name: 'prefs', defaults: { theme: 'light' } })
export class PrefsState {
  @Action(UpdateTheme)
  update(ctx, action) { ctx.patchState({ theme: action.theme }); }
}

// AFTER (autoFlashState + syncToBackend)
const prefs = autoFlashState('prefs', { theme: 'light' });
syncToBackend(prefs, { descriptorKey: 'prefs' });
// Update: prefs.update(p => ({ ...p, theme: 'dark' }));

From custom Map<> cache

// BEFORE
private cache = new Map<string, Data>();

// AFTER (if persistence needed)
private cache = autoFlashState<Record<string, Data>>('my-cache', {});
// + syncToBackend() if backend persistence needed

Key Files

File Purpose
storage-system/src/sync/backend-sync/services/backend-sync-engine.service.ts Core engine
storage-system/src/sync/backend-sync/helpers/sync-to-backend.ts Signal companion
storage-system/src/sync/backend-sync/models/backend-sync.types.ts Types
storage-system/src/sync/backend-sync/tokens/backend-sync.tokens.ts SYNC_TRANSPORT_FN token
storage-system/src/sync/backend-sync/providers/provide-backend-sync.ts Angular providers
storage-system/src/sync/observability/services/storage-observability.service.ts Metrics
storage-system/src/flash/utils/state-versioning.ts Schema versioning with rollback safety