npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@loonylabs/react-native-offline-sync

v1.0.7

Published

Production-ready offline-first synchronization engine for React Native with WatermelonDB

Readme

@loonylabs/react-native-offline-sync

Production-ready offline-first synchronization engine for React Native with WatermelonDB.

npm version License: MIT

Features

  • Offline-First Architecture - Write locally first, sync in background
  • Automatic Sync - Background sync with configurable intervals
  • Network Detection - Auto-sync on reconnection
  • Conflict Resolution - Multiple strategies (Last-Write-Wins, Server-Wins, Client-Wins, Custom)
  • Retry Logic - Exponential backoff for failed operations
  • Type-Safe - Full TypeScript support
  • Observable - Reactive sync status updates
  • Optimistic UI - Instant user feedback
  • React Hooks - Easy integration with React Native apps
  • UI Components - Pre-built sync status indicators
  • Battle-Tested - Extracted from production apps

Installation

npm install @loonylabs/react-native-offline-sync

Peer Dependencies

npm install @nozbe/watermelondb @react-native-community/netinfo @react-native-async-storage/async-storage

Quick Start

1. Setup WatermelonDB Schema

import { appSchema } from '@nozbe/watermelondb';
import { syncQueueTableSchema, createTableSchemaWithSync } from '@loonylabs/react-native-offline-sync';

const schema = appSchema({
  version: 1,
  tables: [
    // Add sync queue table
    syncQueueTableSchema,

    // Your tables with sync metadata
    createTableSchemaWithSync('posts', [
      { name: 'title', type: 'string' },
      { name: 'content', type: 'string' },
    ]),
  ],
});

2. Create Models

import { BaseModel } from '@loonylabs/react-native-offline-sync';
import { text } from '@nozbe/watermelondb/decorators';

class Post extends BaseModel {
  static table = 'posts';

  @text('title') title!: string;
  @text('content') content!: string;
}

3. Initialize Sync Engine

import { SyncEngine } from '@loonylabs/react-native-offline-sync';
import { database } from './database';

// Create API client
const apiClient = {
  push: async (payload) => {
    const response = await fetch('https://api.example.com/sync/push', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    return response.json();
  },
  pull: async (payload) => {
    const response = await fetch('https://api.example.com/sync/pull', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    return response.json();
  },
};

// Initialize sync engine
const syncEngine = new SyncEngine({
  database,
  tables: ['posts'],
  apiClient,
  syncInterval: 5 * 60 * 1000, // 5 minutes
  conflictStrategy: 'last-write-wins',
});

await syncEngine.initialize();

4. Use in React Components

import { useSyncEngine, OfflineBanner } from '@loonylabs/react-native-offline-sync';

function App() {
  const { sync, syncStatus, pendingChanges, isSyncing } = useSyncEngine(syncEngine);

  return (
    <View>
      <OfflineBanner networkDetector={syncEngine.getNetworkDetector()} />

      <Button onPress={sync} disabled={isSyncing}>
        {isSyncing ? 'Syncing...' : `Sync (${pendingChanges} pending)`}
      </Button>
    </View>
  );
}

API Reference

SyncEngine

Main orchestrator for all sync operations.

const syncEngine = new SyncEngine({
  database: Database,           // WatermelonDB instance
  tables: string[],             // Tables to sync
  apiClient: ApiClient,         // API client for server communication
  conflictStrategy?: ConflictStrategy,  // Default: 'last-write-wins'
  syncInterval?: number,        // Default: 300000 (5 min)
  maxRetries?: number,          // Default: 3
  enableBackgroundSync?: boolean, // Default: true
  syncOnReconnect?: boolean,    // Default: true
  pushBatchSize?: number,       // Default: 50
  debug?: boolean,              // Default: false
});

await syncEngine.initialize();
await syncEngine.sync();
syncEngine.destroy();

Hooks

useSyncEngine

Access sync engine state and operations.

const {
  sync,           // () => Promise<SyncResult>
  syncStatus,     // 'idle' | 'syncing' | 'error'
  lastSyncAt,     // number | null
  pendingChanges, // number
  error,          // Error | null
  isSyncing,      // boolean
} = useSyncEngine(syncEngine);

useNetworkStatus

Monitor network connectivity.

const {
  isOnline,           // boolean
  isConnected,        // boolean
  isInternetReachable, // boolean | null
  type,               // string | null
} = useNetworkStatus(networkDetector);

useOptimisticUpdate

Perform optimistic UI updates.

const { execute, isOptimistic } = useOptimisticUpdate(database, syncEngine);

const createPost = async (data) => {
  return execute('posts', 'CREATE', async (collection) => {
    return await collection.create((post) => {
      post.title = data.title;
      post.content = data.content;
    });
  });
};

Components

SyncStatusBadge

Visual indicator of sync status.

<SyncStatusBadge syncEngine={syncEngine} />

OfflineBanner

Banner shown when device is offline.

<OfflineBanner
  networkDetector={networkDetector}
  message="You are offline"
/>

SyncRefreshControl

Pull-to-refresh with sync.

<ScrollView
  refreshControl={<SyncRefreshControl syncEngine={syncEngine} />}
>
  {/* content */}
</ScrollView>

Conflict Resolution

Built-in Strategies

  • Last-Write-Wins (default): Most recent timestamp wins
  • Server-Wins: Server data always takes precedence
  • Client-Wins: Local data always takes precedence
  • Custom: Provide your own resolution function

Custom Conflict Resolver

const syncEngine = new SyncEngine({
  // ... other config
  conflictStrategy: 'custom',
  customConflictResolver: (context) => {
    // context: { tableName, recordId, localData, serverData, localUpdatedAt, serverUpdatedAt }

    // Return 'local', 'server', or merged data object
    return {
      ...context.serverData,
      localField: context.localData.localField, // Keep local value
    };
  },
});

Backend API Requirements

Your backend needs to implement two endpoints:

POST /sync/push

Receives local changes to apply on server.

Request:

{
  "changes": [
    {
      "tableName": "posts",
      "operation": "CREATE",
      "recordId": "local-id-123",
      "data": { "title": "Hello", "content": "World" }
    }
  ]
}

Response:

{
  "success": true,
  "results": [
    {
      "recordId": "local-id-123",
      "serverId": "server-id-456",
      "serverUpdatedAt": 1234567890,
      "error": null
    }
  ]
}

POST /sync/pull

Returns server changes since last sync.

Request:

{
  "lastSyncAt": 1234567890,
  "tables": ["posts"]
}

Response:

{
  "timestamp": 1234567900,
  "changes": {
    "posts": {
      "created": [{ "id": "1", "title": "New Post" }],
      "updated": [{ "id": "2", "title": "Updated Post" }],
      "deleted": ["3"]
    }
  }
}

Performance Tips

  1. Batch Operations: Use pushBatchSize to control batch sizes
  2. Sync Interval: Adjust based on your app's needs
  3. Tables: Only sync tables that need it
  4. Network Detection: Disable if not needed
  5. Debug Mode: Disable in production

Documentation

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT © Loonylabs

Support

Credits

Built with: