Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,4 @@ app.*.symbols
.fvm/
.idea/workspace.xml
.idea/libraries/Dart_SDK.xml
.cursor/
58 changes: 56 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,57 @@
## 0.0.1
# Changelog

* TODO: Describe initial release.
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.1] - 2025-12-25

### Added

- Initial release of SyncVault
- **Core Features:**
- `SyncVault` singleton client for making offline-safe HTTP requests
- `SyncAction` model for representing queued requests
- `StorageAdapter` abstract interface for pluggable persistence
- `HiveStorageAdapter` default implementation using Hive
- `NetworkManager` for connectivity monitoring via `connectivity_plus`
- `Worker` for queue processing with retry logic

- **HTTP Methods:**
- `GET`, `POST`, `PUT`, `DELETE`, `PATCH` support
- Custom headers support
- Idempotency key support

- **Sync Features:**
- Automatic sync on network restoration
- Manual sync trigger via `processQueue()`
- FIFO queue processing
- Exponential backoff with jitter
- Configurable max retries
- Dead letter handling for failed requests

- **Observability:**
- `SyncStatus` stream for UI updates
- Pending action count
- Access to all pending actions

- **Example App:**
- Complete Todo app demonstrating all features
- Beautiful dark UI with sync status indicators
- Optimistic UI updates

### Architecture

- Clean architecture with separated layers:
- `core/` - Main business logic (SyncClient, Worker, NetworkManager)
- `data/` - Data models and storage (SyncAction, StorageAdapter)
- `utils/` - Utilities (Exceptions)

### Dependencies

- `dio: ^5.4.0` - HTTP client
- `hive: ^2.2.3` - Local persistence
- `connectivity_plus: ^6.0.0` - Network monitoring
- `uuid: ^4.3.0` - Unique ID generation
- `equatable: ^2.0.5` - Value equality
294 changes: 293 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,294 @@
# sync_vault
# SyncVault 🔄🗄️

**Stop writing connectivity logic.**

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

Designed for reliability in low-network environments.

[![pub package](https://img.shields.io/pub/v/sync_vault.svg)](https://pub.dev/packages/sync_vault)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 🚀 Features

- **Offline Queue:** Automatically intercepts failed requests and stores them locally.
- **Auto-Sync:** Detects network restoration and processes the queue in order (FIFO).
- **Persistence:** Uses a pluggable storage adapter (Hive by default).
- **Idempotency:** Supports idempotency keys to prevent duplicate execution.
- **Configurable Retry:** Exponential backoff with jitter, configurable max retries.
- **Dead Letter Queue:** Failed requests (after max retries) are removed and can be handled via callback.
- **Status Stream:** Real-time sync status for UI updates.

## 📦 Installation

Add this to your `pubspec.yaml`:

```yaml
dependencies:
sync_vault: ^0.0.1
```

Then run:

```bash
flutter pub get
```

## 🛠 Usage

### 1. Initialize

Initialize SyncVault in your `main.dart` before running the app:

```dart
import 'package:sync_vault/sync_vault.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();

// Get storage directory (required for Hive)
final dir = await getApplicationDocumentsDirectory();

await SyncVault.init(
config: SyncVaultConfig(
baseUrl: 'https://api.myapp.com',
storagePath: dir.path,
maxRetries: 3,
),
);

runApp(MyApp());
}
```

### 2. Make Offline-Safe Requests

Instead of calling Dio or Http directly, use the `SyncVault` client:

```dart
// POST request
final response = await SyncVault.instance.post(
'/jobs/complete',
data: {'jobId': 123, 'status': 'DONE'},
idempotencyKey: 'job_123_complete', // Prevents duplicate execution
);

if (response.isQueued) {
print("Saved offline! Will sync later.");
} else if (response.isSent) {
print("Sent immediately! Status: ${response.response?.statusCode}");
}

// GET, PUT, DELETE, PATCH are also available
await SyncVault.instance.get('/users');
await SyncVault.instance.put('/users/123', data: {...});
await SyncVault.instance.delete('/users/123');
await SyncVault.instance.patch('/users/123', data: {...});
```

### 3. Listen to Sync Status

Perfect for showing a "Syncing..." indicator in your UI:

```dart
SyncVault.instance.statusStream.listen((status) {
switch (status) {
case SyncStatus.syncing:
showToast("Uploading offline data...");
case SyncStatus.synced:
showToast("All caught up!");
case SyncStatus.pending:
showToast("${await SyncVault.instance.pendingCount} items waiting");
case SyncStatus.error:
showToast("Sync error occurred");
}
});
```

### 4. Manual Sync

Trigger sync manually if needed:

```dart
final successCount = await SyncVault.instance.processQueue();
print('Synced $successCount items');
```

### 5. Check Pending Actions

```dart
// Get count
final count = await SyncVault.instance.pendingCount;

// Get all pending actions
final actions = await SyncVault.instance.getPendingActions();

// Clear all pending (use with caution)
await SyncVault.instance.clearQueue();
```

## 🏗️ Architecture

SyncVault follows a **Store-and-Forward** architecture:

```
┌─────────────────┐
│ Your App │
│ (Request) │
└────────┬────────┘
┌─────────────────┐
│ SyncVault │
│ (Intercept) │
└────────┬────────┘
┌────┴────┐
│ Online? │
└────┬────┘
┌────┴────┐
Yes No
│ │
▼ ▼
┌───────┐ ┌───────┐
│ Dio │ │ Queue │
│ (Send)│ │(Store)│
└───────┘ └───┬───┘
┌────────┐
│ Hive │
│(Persist)│
└────────┘
On Network Restore
┌────────┐
│ Worker │
│(Process)│
└────────┘
```

1. **Interceptor:** Captures outgoing requests via `SyncVault.instance.post()`, etc.
2. **Queue Manager:** Serializes requests to local storage (FIFO order).
3. **Network Watcher:** Listens for connectivity changes via `connectivity_plus`.
4. **Worker:** Dequeues requests and retries with exponential backoff.

## ⚙️ Configuration

```dart
SyncVaultConfig(
// Required
baseUrl: 'https://api.example.com',

// Optional: Custom storage path for Hive
storagePath: '/path/to/storage',

// Optional: Maximum retry attempts (default: 3)
maxRetries: 5,

// Optional: Custom Dio instance
dio: myCustomDio,

// Optional: Timeouts
connectTimeout: Duration(seconds: 30),
receiveTimeout: Duration(seconds: 30),

// Optional: Custom storage adapter
storage: MyCustomStorageAdapter(),
)
```

## 🔌 Custom Storage Adapter

Implement `StorageAdapter` to use your own persistence layer:

```dart
class MyStorageAdapter implements StorageAdapter {
@override
Future<void> init() async { ... }

@override
Future<void> saveAction(SyncAction action) async { ... }

@override
Future<void> removeAction(String id) async { ... }

@override
Future<List<SyncAction>> getAllActions() async { ... }

@override
Future<void> clear() async { ... }

@override
Future<void> dispose() async { ... }

@override
Future<int> get count async { ... }
}
```

## 🧪 Testing

SyncVault is designed to be testable. Reset between tests:

```dart
tearDown(() async {
await SyncVault.reset();
});
```

## 📱 Example

Check out the [example](./example) directory for a complete Todo app demonstrating all features.

```bash
cd example
flutter run
```

## 🔒 Idempotency

Use idempotency keys to prevent duplicate operations when retrying:

```dart
await SyncVault.instance.post(
'/payments',
data: {'amount': 100},
idempotencyKey: 'payment_${orderId}_${timestamp}',
);
```

If the same idempotency key is already in the queue, SyncVault will handle it appropriately.

## 📊 Retry Behavior

| Scenario | Behavior |
| -------------------- | -------------------- |
| Network error | Retry with backoff |
| Timeout | Retry with backoff |
| 5xx Server error | Retry with backoff |
| 429 Rate limited | Retry with backoff |
| 4xx Client error | Fail permanently |
| Max retries exceeded | Remove (dead letter) |

## 🤝 Contributing

PRs are welcome! Please read the contributing guidelines before submitting.

1. Fork the repo
2. Create your feature branch from `dev` (`git checkout -b feature/amazing-feature dev`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request against `dev`

## 📄 License

MIT © 2025 Nabhodipta Garai

---

Built with ❤️ for Flutter developers who work in the real world, where networks are unreliable.
Loading