@azlib/sync
v0.2.6
Published
Standardized multi-entity offline-first database persistence, optimistic UI updates (0ms latency), background mutation queues, and server synchronization handlers for browser and Node.js applications.
Readme
@azlib/sync
Standardized multi-entity offline-first database persistence, optimistic UI updates (0ms latency), background mutation queues, and server synchronization handlers for browser and Node.js applications.
Capabilities
- Multi-Entity Repository Pattern: Manage database stores for arbitrary entities (
mind_maps,nodes,user_settings,documents,forms) under a singleSyncDatabasecontainer. - Optimistic UI Execution (0ms Latency): Local reads and writes resolve instantly to keep the UI lag-free, while background workers queue and push mutations to the server API.
- Offline First & Network Resiliency: Listens for network connection changes (
online/offline). Mutations performed offline are buffered in a persistent queue and flushed automatically upon reconnection with exponential backoff retries. - Pluggable Storage Drivers:
IndexedDbDriver: Primary browser driver for large, indexed JSON object stores.LocalStorageDriver: Lightweight key-value driver for settings or extension contexts (chrome.storage.local).MemoryDriver: Zero-overhead in-memory fallback for Next.js SSR and Vitest / Node test runners.
- Conflict Resolution Policies: Configurable policies (
last-write-wins,server-wins,client-wins, or custom merger function(localRecord, serverRecord) => MergedRecord). - Server Integration Module: Includes
@azlib/sync/serverexporting Express/Node batch sync helpers (createSyncServerHandler,processBatchSync).
AI Agent Quick Reference
Core Exports
| Subpath | Export | Description |
| -------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| @azlib/sync | createSyncDatabase(config) | Creates a central database container instance (SyncDatabase). |
| @azlib/sync | SyncDatabase | Database container class managing storage drivers, mutation queues, and repository registrations. |
| @azlib/sync | SyncRepository<T, K> | Entity repository providing find, list, save, delete, bulkSave, sync, and subscribe. |
| @azlib/sync | SyncQueue | Background offline mutation queue managing retry backoff and network reconnection listeners. |
| @azlib/sync | resolveConflict(local, server, strategy) | Conflict resolution helper. |
| @azlib/sync/server | createSyncServerHandler(config) | Express/Node HTTP middleware for multi-entity batch sync endpoints. |
| @azlib/sync/server | processBatchSync(payload, config) | Core server function processing batch sync payloads. |
Core Types & Signatures
type SyncState =
| "synced"
| "pending_create"
| "pending_update"
| "pending_delete"
| "conflict"
| "error";
type SyncMeta = {
entityName: string;
syncState: SyncState;
lastSyncedAt?: string;
updatedAt: string;
clientVersion: number;
};
type SyncRecord<T> = T & { _syncMeta: SyncMeta };
type ConflictStrategy =
| "last-write-wins"
| "server-wins"
| "client-wins"
| ((localRecord: unknown, serverRecord: unknown) => unknown);Basic Client Usage
import { createSyncDatabase } from "@azlib/sync";
import { httpClient } from "@azlib/http-client";
export interface MindMapSnapshot {
id: string;
title: string;
updatedAt: string;
}
// 1. Initialize Database Container
export const clientDb = createSyncDatabase({
name: "app_client_db",
version: 1,
driver: "indexeddb", // Falls back to memory driver automatically in SSR
});
// 2. Register Repositories
export const mindMapRepo = clientDb.registerRepository<MindMapSnapshot, string>(
{
name: "mind_maps",
keyPath: "id",
api: {
fetch: async (id) => (await httpClient.get(`/api/mind-maps/${id}`)).data,
push: async (record, op) =>
(
await httpClient.post(`/api/mind-maps/${record.id}/sync`, {
record,
op,
})
).data,
list: async () => (await httpClient.get("/api/mind-maps")).data,
},
conflictStrategy: "last-write-wins",
},
);
// 3. Optimistic CRUD Operations
await mindMapRepo.save({
id: "map_1",
title: "Architecture Blueprint",
updatedAt: new Date().toISOString(),
}); // Resolves instantly (0ms UI delay) & queues background API push
const map = await mindMapRepo.find("map_1");
const allMaps = await mindMapRepo.list();
await mindMapRepo.delete("map_1");React Component Integration
import React, { useEffect, useState } from "react";
import { mindMapRepo, clientDb } from "./db";
import type { MindMapSnapshot, SyncStatus } from "@azlib/sync";
export function MindMapEditor({ mapId }: { mapId: string }) {
const [map, setMap] = useState<MindMapSnapshot | null>(null);
const [syncStatus, setSyncStatus] = useState<SyncStatus>({
state: "idle",
pendingMutationsCount: 0,
});
useEffect(() => {
// Read from IndexedDB instantly
mindMapRepo.find(mapId).then(setMap);
// Subscribe to entity updates
const unsubscribeData = mindMapRepo.subscribe(mapId, setMap);
// Subscribe to sync queue status changes
const unsubscribeSync = clientDb.onSyncStatusChange(setSyncStatus);
return () => {
unsubscribeData();
unsubscribeSync();
};
}, [mapId]);
const handleUpdateTitle = async (newTitle: string) => {
if (!map) return;
// Writes locally instantly (0ms delay) -> UI updates via subscription
await mindMapRepo.save({
...map,
title: newTitle,
updatedAt: new Date().toISOString(),
});
};
return (
<div>
<div className="status-bar">
Status: {syncStatus.state} ({syncStatus.pendingMutationsCount} pending)
</div>
<input
value={map?.title ?? ""}
onChange={(e) => handleUpdateTitle(e.target.value)}
/>
</div>
);
}Server Integration (@azlib/sync/server)
// In apps/api (Express Server)
import { Router } from "express";
import { createSyncServerHandler } from "@azlib/sync/server";
import * as MindMapService from "./features/mind-map/mind-map.service";
const router = Router();
router.post(
"/api/sync",
createSyncServerHandler({
repositories: {
mind_maps: {
save: async (userId, data) =>
MindMapService.updateMindMap(userId, data.id, data),
delete: async (userId, id) => MindMapService.deleteMindMap(userId, id),
},
},
}),
);Behavioral Gotchas & Best Practices
- SSR Hydration Safety: The database automatically falls back to
MemoryDriverin SSR environments (Next.js server rendering) wherewindow.indexedDBorwindow.localStorageis unavailable. - Key Path Requirement: Every entity passed to
repo.save()must contain the designatedkeyPathproperty defined inregisterRepository({ keyPath: 'id' }). - Queue Idempotency: When offline mutations are flushed upon reconnection,
push(record, operation)callbacks should ideally handle idempotency or timestamp checks on the server side.
