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.
npm install @sync-vault-js/core
# or
yarn add @sync-vault-js/core
# or
pnpm add @sync-vault-js/core
Want 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 demo
The demo will open in your browser at http://localhost:8000 and showcases:
Or use a simple HTTP server:
cd demo
python3 -m http.server 8000
# Then open http://localhost:8000
See 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: "[email protected]",
});
// 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: "[email protected]",
});
// 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

Swayam Debata (Contributor)

MIT ยฉ SyncVault
Built for the offline-first future ๐