SyncVault is an offline-first "Store-and-Forward" engine for JavaScript/TypeScript applications. It seamlessly handles network failures by automatically queuing requests when offline and syncing them with exponential backoff when connectivity returns.
- 🔄 Automatic Request Interception - Queues requests when offline
- 💾 Persistent Storage - IndexedDB for browsers, memory adapter for SSR/Node
- ⚡ Exponential Backoff - Smart retry logic with jitter
- 📡 Real-time Events - Subscribe to sync status, job completion, and errors
- 🎯 Framework Agnostic - Works with React, Vue, Angular, Svelte, or vanilla JS
- 📱 Universal - Browser, Node.js, React Native ready
- 🔒 Type Safe - Full TypeScript support with strict typing
npm install @sync-vault-js/core
# or
yarn add @sync-vault-js/core
# or
pnpm add @sync-vault-js/coreWant to see SyncVault in action? We've included a fully interactive demo application!
# Clone the repository
git clone https://github.com/syncvault/sync-vault-js.git
cd sync-vault-js
# Install dependencies
npm install
# Run the demo
npm run demoThe demo will open in your browser at http://localhost:8000 and showcases:
- ✅ Real-time request queuing
- ✅ Online/offline detection
- ✅ Queue management UI
- ✅ Event logging
- ✅ Processing controls
- ✅ Dead Letter Queue visualization
Or use a simple HTTP server:
cd demo
python3 -m http.server 8000
# Then open http://localhost:8000See demo/README.md for more details.
import { createSyncVault } from "@sync-vault-js/core";
// Create a SyncVault instance
const vault = createSyncVault({
debug: true,
retry: {
maxRetries: 3,
initialDelay: 1000,
},
});
// Make requests - automatically queued when offline
const response = await vault.post("/api/users", {
name: "John Doe",
email: "john@example.com",
});
// Check if request was queued
if (response.fromQueue) {
console.log("Request queued for later sync");
}
// Listen to events
vault.on("job_success", (event) => {
console.log("Job completed:", event.data.job.id);
});
vault.on("network_offline", () => {
console.log("Gone offline - requests will be queued");
});
vault.on("network_online", () => {
console.log("Back online - processing queue");
});import { useSyncVault } from "@sync-vault-js/core/react";
function TodoApp() {
const { isOnline, isProcessing, queueLength, post } = useSyncVault();
const addTodo = async (title: string) => {
const response = await post("/api/todos", { title });
if (response.fromQueue) {
// Show optimistic UI
toast.info("Saved offline - will sync when online");
}
};
return (
<div>
<StatusBar
online={isOnline}
syncing={isProcessing}
pending={queueLength}
/>
<TodoForm onSubmit={addTodo} />
</div>
);
}import {
useSyncVault,
useOnlineStatus,
useQueueStatus,
useSyncRequest,
useSyncVaultEvent,
} from "@sync-vault-js/core/react";
// Simple online status
function OnlineIndicator() {
const isOnline = useOnlineStatus();
return <span>{isOnline ? "🟢" : "🔴"}</span>;
}
// Queue status with auto-refresh
function QueueStatus() {
const { length, isProcessing } = useQueueStatus();
return (
<span>
{length} pending, {isProcessing ? "syncing..." : "idle"}
</span>
);
}
// Request with loading/error state
function CreateUser() {
const { execute, loading, error, queued } = useSyncRequest();
const handleSubmit = async (data) => {
await execute({
url: "/api/users",
method: "POST",
data,
});
};
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
if (queued) return <Badge>Queued for sync</Badge>;
return <Form onSubmit={handleSubmit} />;
}
// Event subscription
function SyncNotifications() {
useSyncVaultEvent("job_success", (event) => {
toast.success(`Synced: ${event.data.job.url}`);
});
useSyncVaultEvent("job_failed", (event) => {
if (!event.data.willRetry) {
toast.error(`Failed: ${event.data.error.message}`);
}
});
return null;
}<script setup lang="ts">
import { useSyncVault } from "@sync-vault-js/core/vue";
const { isOnline, isProcessing, queueLength, post } = useSyncVault();
const addTodo = async (title: string) => {
const response = await post("/api/todos", { title });
if (response.fromQueue) {
showNotification("Saved offline - will sync when online");
}
};
</script>
<template>
<div>
<StatusBar
:online="isOnline"
:syncing="isProcessing"
:pending="queueLength"
/>
<TodoForm @submit="addTodo" />
</div>
</template>import {
useSyncVault,
useOnlineStatus,
useQueueStatus,
useSyncRequest,
useSyncVaultEvent,
useJobWatcher,
} from "@sync-vault-js/core/vue";
// Online status
const isOnline = useOnlineStatus();
// Queue status
const { length, isProcessing, refresh } = useQueueStatus();
// Request with state
const { execute, data, loading, error, queued, reset } = useSyncRequest();
// Watch a specific job
const jobId = ref(null);
const { completed, success, error } = useJobWatcher(jobId);// sync-vault.service.ts
import { Injectable } from "@angular/core";
import { SyncVaultService } from "@sync-vault-js/core/angular";
@Injectable({ providedIn: "root" })
export class AppSyncVaultService extends SyncVaultService {
constructor() {
super({ debug: true });
}
}
// todo.component.ts
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs";
import { AppSyncVaultService } from "./sync-vault.service";
@Component({
selector: "app-todo",
template: `
<div class="status">
<span [class.online]="state.isOnline">
{{ state.isOnline ? "Online" : "Offline" }}
</span>
<span *ngIf="state.queueLength > 0">
{{ state.queueLength }} pending
</span>
</div>
<button (click)="addTodo()">Add Todo</button>
`,
})
export class TodoComponent implements OnInit, OnDestroy {
state = { isOnline: true, queueLength: 0, isProcessing: false };
private subscription = new Subscription();
constructor(private syncVault: AppSyncVaultService) {}
ngOnInit() {
this.subscription.add(
this.syncVault.state$.subscribe((state) => {
this.state = state;
})
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
async addTodo() {
const response = await this.syncVault.post("/api/todos", {
title: "New Todo",
});
if (response.fromQueue) {
alert("Saved offline - will sync when online");
}
}
}<script lang="ts">
import { useSyncVault } from '@sync-vault-js/core/svelte';
const {
isOnline,
isProcessing,
queueLength,
post,
} = useSyncVault();
async function addTodo(title: string) {
const response = await post('/api/todos', { title });
if (response.fromQueue) {
showNotification('Saved offline');
}
}
</script>
<div>
<StatusBar
online={$isOnline}
syncing={$isProcessing}
pending={$queueLength}
/>
<TodoForm on:submit={(e) => addTodo(e.detail.title)} />
</div>import {
createSyncVaultStores,
createSyncVaultActions,
createEventStore,
createJobStore,
createRequestStore,
} from "@sync-vault-js/core/svelte";
// Create all stores
const { state, isOnline, isProcessing, queueLength, destroy } =
createSyncVaultStores();
// Create actions
const { post, get, clearQueue } = createSyncVaultActions();
// Event store
const successEvents = createEventStore("job_success");
// Track specific job
const jobStatus = createJobStore("job-123");
// Request with state management
const { data, loading, error, queued, execute, reset } = createRequestStore();import { createSyncVault } from "@sync-vault-js/core";
const vault = createSyncVault({
// Storage configuration
dbName: "my-app-sync", // IndexedDB database name
// Queue configuration
queue: {
concurrency: 1, // Process one job at a time
processingDelay: 100, // Delay between jobs (ms)
maxSize: 0, // Max queue size (0 = unlimited)
},
// Retry configuration
retry: {
maxRetries: 3, // Max retry attempts
initialDelay: 1000, // Initial backoff delay (ms)
maxDelay: 30000, // Maximum delay cap (ms)
multiplier: 2, // Exponential multiplier
jitter: true, // Add randomness to prevent thundering herd
},
// Behavior
debug: false, // Enable debug logging
autoStart: true, // Auto-start processing when online
// Custom adapters (advanced)
storage: customStorageAdapter,
networkChecker: customNetworkChecker,
httpClient: customHttpClient,
});SyncVault emits various events throughout its lifecycle:
| Event | Description | Data |
|---|---|---|
sync_started |
Queue processing started | - |
sync_completed |
Queue processing finished | - |
sync_paused |
Processing paused | - |
job_queued |
New job added to queue | { job, queueLength } |
job_started |
Job processing started | { job } |
job_success |
Job completed successfully | { job, response, duration } |
job_failed |
Job failed | { job, error, willRetry } |
job_retry |
Job will be retried | { job, attempt, maxRetries, nextRetryIn } |
job_dead |
Job moved to Dead Letter Queue | { job, error, movedToDLQ } |
network_online |
Device came online | - |
network_offline |
Device went offline | - |
queue_empty |
Queue is now empty | - |
queue_cleared |
Queue was manually cleared | - |
// Subscribe to events
const unsubscribe = vault.on("job_success", (event) => {
console.log("Job completed:", event.data);
});
// One-time subscription
vault.once("sync_completed", (event) => {
console.log("Initial sync done");
});
// Unsubscribe
unsubscribe();// Get queue status
const length = await vault.getQueueLength();
const jobs = await vault.getQueue();
// Manual control
vault.startProcessing();
vault.stopProcessing();
const syncing = vault.isProcessing();
// Clear queue
await vault.clearQueue();
// Manage specific jobs
await vault.retryJob("job-id");
await vault.removeJob("job-id");Jobs that fail after max retries are moved to the DLQ:
// Get failed jobs
const deadJobs = await vault.getDLQ();
// Retry all dead jobs
await vault.retryDLQ();
// Clear DLQ
await vault.clearDLQ();Implement the StorageAdapter interface for custom storage:
import type { StorageAdapter, QueuedJob } from "@sync-vault-js/core";
class CustomStorageAdapter implements StorageAdapter {
async init(): Promise<void> {
/* ... */
}
async add(job: QueuedJob): Promise<void> {
/* ... */
}
async getAll(): Promise<QueuedJob[]> {
/* ... */
}
async get(id: string): Promise<QueuedJob | undefined> {
/* ... */
}
async update(id: string, updates: Partial<QueuedJob>): Promise<void> {
/* ... */
}
async remove(id: string): Promise<void> {
/* ... */
}
async count(): Promise<number> {
/* ... */
}
async clear(): Promise<void> {
/* ... */
}
async moveToDLQ(job: QueuedJob): Promise<void> {
/* ... */
}
async getDLQ(): Promise<QueuedJob[]> {
/* ... */
}
async clearDLQ(): Promise<void> {
/* ... */
}
async close(): Promise<void> {
/* ... */
}
}
const vault = createSyncVault({
storage: new CustomStorageAdapter(),
});For React Native, use the custom network checker with @react-native-community/netinfo:
import NetInfo from "@react-native-community/netinfo";
import {
createSyncVault,
createReactNativeNetworkChecker,
} from "@sync-vault-js/core";
const vault = createSyncVault({
networkChecker: createReactNativeNetworkChecker(NetInfo),
});SyncVault is written in strict TypeScript with full type inference:
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserPayload {
name: string;
email: string;
}
// Fully typed request and response
const response = await vault.post<User, CreateUserPayload>("/api/users", {
name: "John",
email: "john@example.com",
});
// response.data is typed as User
console.log(response.data.id);┌─────────────────────────────────────────────────────────────────┐
│ SyncVault Client │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTTP Client │ │ Event Emitter│ │Network Checker│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Queue Processor │ │
│ │ ┌─────────┐ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ │ FIFO │ │ Exp Backoff │ │ Dead Letter Q │ │ │
│ │ │ Queue │ │ Retry │ │ │ │ │
│ │ └─────────┘ └─────────────┘ └──────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Storage Adapter (Interface) │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ IndexedDB │ │ Memory │ │ │
│ │ │ (Browser) │ │ (SSR/Node) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
Nabhodipta Garai
- GitHub: @brownboycodes
- LinkedIn: Nabhodipta Garai
Swayam Debata (Contributor)
- GitHub: @SwayamDebata
- LinkedIn: Swayam Debata
MIT © SyncVault
Built for the offline-first future 🚀
