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:
- Fire-and-forget HTTP calls
- Offline detection (queues if backend is down)
- Persistent retry queue (survives page refresh)
- Exponential backoff (1s, 2s, 4s, 8s, 16s)
- Deduplication (latest value wins)
- Drain on reconnect
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:
- Must be called inside an Angular injection context
- Skips the first emission (IndexedDB hydration — avoids syncing stale data)
- Debounces rapid changes
- Cleans up on component/service destroy
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 |