Skip to content

Persistent, adapter-driven mutation orchestration engine for React Native. Offline-safe, retry-capable, and type-safe.

License

Notifications You must be signed in to change notification settings

ycedrick/react-native-syncflow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-native-syncflow

A persistent, adapter-driven mutation orchestration engine for React Native applications.

License: MIT TypeScript

react-native-syncflow is designed to make your React Native app robust, offline-first, and resilient to crashes and network flakiness. Instead of firing API calls and hoping for the best, you queue mutations that are guaranteed to execute eventually.

Features

  • Offline-First: Mutations are persisted immediately and processed when online.
  • Retry Strategies: Configurable exponential backoff for transient failures.
  • Concurrency Control: Process mutations serially or in parallel.
  • Deduplication: Prevent duplicate actions (e.g., "like post") with strategies (drop, replace, allow).
  • Transactions: Group mutations atomically.
  • Adapter-Driven: Works with any storage (AsyncStorage, MMKV, SQLite) and network library (NetInfo).
  • React Hooks: useSyncStatus, useSyncEngine.
  • Type-Safe: Full TypeScript support for mutation payloads and responses.

Installation

npm install react-native-syncflow
# Peer dependencies
npm install @react-native-async-storage/async-storage @react-native-community/netinfo

Basic Usage

1. Define your mutations

// types.ts
export type AppMutations = {
  "user:update": { input: { id: string; name: string }; output: User };
  "post:create": { input: { content: string }; output: Post };
};

2. Configure the Engine

// sync.ts
import {
  SyncEngine,
  AsyncStorageAdapter,
  NetInfoAdapter,
} from "react-native-syncflow";
import { AppMutations } from "./types";

export const sync = new SyncEngine<AppMutations>({
  storage: new AsyncStorageAdapter(),
  network: new NetInfoAdapter(),
  logger: console,
});

// Register handlers (these are your API calls)
sync.register("user:update", async (data) => {
  return await api.updateUser(data.id, data.name);
});

sync.register("post:create", async (data) => {
  return await api.createPost(data.content);
});

3. Wrap your App

// App.tsx
import { SyncProvider } from 'react-native-syncflow';
import { sync } from './sync';

export default function App() {
  return (
    <SyncProvider engine={sync}>
      <Main />
    </SyncProvider>
  );
}

4. Use in Components

import { useSyncEngine, useSyncStatus } from 'react-native-syncflow';
import { AppMutations } from './types';

const Profile = () => {
  const sync = useSyncEngine<AppMutations>();
  const { isOnline, queueSize } = useSyncStatus();

  const handleSave = async () => {
    // This resolves immediately with an ID once queued
    const mutationId = await sync.mutate('user:update', {
      id: '123',
      name: 'New Name'
    });
    console.log('Mutation queued:', mutationId);
  };

  return (
    <View>
      <Text>Queue: {queueSize}</Text>
      <Button onPress={handleSave} title="Save Profile" disabled={!isOnline} />
    </View>
  );
};

Advanced Configuration

Deduplication

Prevent users from spamming actions.

sync.mutate(
  "post:like",
  { postId: "123" },
  {
    deduplication: {
      mode: "drop", // If 'post:like' for this key is already pending, ignore this one
      dedupKey: "like:123",
    },
  },
);

Transactions

Ensure a group of mutations are queued together.

await sync.transaction(async (tx) => {
  await tx.mutate("user:create", user);
  await tx.mutate("settings:update", settings);
});
// Both are persisted and queued atomically at the end of the block.

Custom Adapters

SyncFlow is designed to be completely decoupled. You can implement any of the following adapters to fit your tech stack.

1. Storage Adapters

SQLite (Relational/Robust)

import { StorageAdapter } from "react-native-syncflow";
import * as SQLite from "expo-sqlite";

export class SQLiteAdapter implements StorageAdapter {
  private db = SQLite.openDatabaseSync("syncflow.db");

  async get(key: string) {
    const result = await this.db.getFirstAsync<{ value: string }>(
      "SELECT value FROM sync_data WHERE key = ?",
      [key],
    );
    return result?.value || null;
  }

  async set(key: string, value: string) {
    await this.db.runAsync(
      "INSERT OR REPLACE INTO sync_data (key, value) VALUES (?, ?)",
      [key, value],
    );
  }

  async remove(key: string) {
    await this.db.runAsync("DELETE FROM sync_data WHERE key = ?", [key]);
  }
}

2. Network Adapters

Custom Polling / Manual Check

import { NetworkAdapter } from "react-native-syncflow";

export class ManualNetworkAdapter implements NetworkAdapter {
  private listeners: ((online: boolean) => void)[] = [];

  subscribe(listener: (online: boolean) => void) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter((l) => l !== listener);
    };
  }

  async getCurrentState() {
    try {
      const res = await fetch("https://google.com", { method: "HEAD" });
      return res.ok;
    } catch {
      return false;
    }
  }

  // Trigger manually from your app logic
  setOnline(status: boolean) {
    this.listeners.forEach((l) => l(status));
  }
}

3. Logger Adapters

Sentry / Remote Logging

import { LoggerAdapter } from "react-native-syncflow";
import * as Sentry from "@sentry/react-native";

export class SentryLoggerAdapter implements LoggerAdapter {
  log(msg: string) {
    console.log(msg);
  }
  warn(msg: string) {
    Sentry.captureMessage(msg, "warning");
  }
  error(msg: string, ...args: any[]) {
    Sentry.captureException(args[0] || new Error(msg));
  }
}

Retry Strategies

You can define custom backoff logic. Return null to stop retrying.

const customRetry: RetryStrategy = (attempt, error) => {
  if (attempt > 10) return null; // Max 10 retries
  if (error.status === 401) return null; // Don't retry auth errors

  return attempt * 5000; // Linear backoff: 5s, 10s, 15s...
};

Architecture

  1. Mutate: User calls mutate.
  2. Persist: Item is saved to Storage (status: pending).
  3. Process:
    • Engine checks network.
    • Picks item.
    • Executes registered handler.
  4. Result:
    • Success: Mark completed (remove from queue).
    • Fail: Check retry strategy. If retryable, mark pending with retryAfter.

License

MIT

About

Persistent, adapter-driven mutation orchestration engine for React Native. Offline-safe, retry-capable, and type-safe.

Topics

Resources

License

Stars

Watchers

Forks

Packages