Skip to content

SyncVault is an offline-first data synchronization layer for apps built with any JavaScript based frameworks. It automatically queues API requests when the device is offline and syncs them with exponential backoff when the connection returns.

License

Notifications You must be signed in to change notification settings

brownboycodes/sync-vault-js

Repository files navigation

SyncVault

The Universal Sync Layer

Write your offline logic once. Runs on Web, Mobile, and Desktop.

npm version License: MIT TypeScript


What is SyncVault?

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.

Key Features

  • 🔄 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

Installation

npm install @sync-vault-js/core
# or
yarn add @sync-vault-js/core
# or
pnpm add @sync-vault-js/core

🎮 Try the Demo

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:

  • ✅ 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:8000

See demo/README.md for more details.


Quick Start

Basic Usage (Vanilla JavaScript/TypeScript)

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");
});

Framework Integrations

React / Next.js

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>
  );
}

Additional React Hooks

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;
}

Vue 3 / Nuxt

<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>

Additional Vue Composables

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);

Angular

// 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");
    }
  }
}

Svelte / SvelteKit

<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>

Additional Svelte Stores

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();

Configuration

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,
});

Events

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();

Queue Management

// 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");

Dead Letter Queue (DLQ)

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();

Custom Storage Adapters

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(),
});

React Native Integration

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),
});

TypeScript Support

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);

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         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)  │           │     │
│  │  └──────────────┘         └──────────────┘           │     │
│  └───────────────────────────────────────────────────────┘     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting a PR.


Authors

Nabhodipta Garai

Swayam Debata (Contributor)


License

MIT © SyncVault


Built for the offline-first future 🚀

About

SyncVault is an offline-first data synchronization layer for apps built with any JavaScript based frameworks. It automatically queues API requests when the device is offline and syncs them with exponential backoff when the connection returns.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published