StashBox

A lightweight, persistent JSON storage solution for Dart and Flutter applications

Git - API Docs

Dart - API Docs

Features

  • Persistent JSON Storage: Store and retrieve data in JSON format
  • Lazy Loading: Efficient handling of large files with on-demand loading
  • Transaction Support: Group operations with commit/rollback capabilities
  • Caching: Configurable in-memory caching for improved performance
  • Type Safety: Get typed values with default fallbacks
  • Object Serialization: Helpers for storing complex objects
  • Batch Operations: Perform multiple operations efficiently
  • Error Handling: Specialized exceptions for better error reporting
  • File Integrity: Automatic backup creation for corrupted files
  • Enhanced File Operations: Retry logic, file locking, and concurrent access protection
  • Backup & Restore: Full backup and restore functionality with pre-restore backups
  • Watch API: Real-time change notifications with event streaming

Getting Started

Add the package to your pubspec.yaml:

dependencies:
  stashbox: ^0.1.0

Usage

Basic Operations

import 'package:stashbox/stashbox.dart';

// Create a StashBox with a file path
final store = StashBox('data/mystore.json');

// Enable caching for better performance
store.configureCaching(enabled: true, maxSize: 100);

// Simple operations
await store.set('username', 'spicy_katsu');
await store.set('age', 25);
await store.set('isActive', true);

// Retrieve values
final username = await store.get<String>('username');
final age = await store.getTyped<int>('age', 0); // With default value
final isActive = await store.getRequired<bool>('isActive'); // Throws if not found

// Check if a key exists
if (await store.has('username')) {
  print('Username exists!');
}

// Remove a value
await store.remove('age');

// Clear all data
await store.clear();

Lazy Loading

For large datasets, enable lazy loading to keep memory usage low:

// Create a StashBox with lazy loading enabled
final store = StashBox(
  'data/large_dataset.json',
  lazyLoadConfig: LazyLoadConfig(
    enabled: true,
    maxMemoryItems: 1000,        // Max items to keep in memory
    fileSizeThreshold: 10 * 1024 * 1024, // 10MB file size threshold
  ),
);

// Only accessed items are loaded into memory
final value = await store.get('some_key'); // Loads from disk if needed

// Check if lazy mode is active
print('Is lazy mode: ${store.isLazyMode}');
print('Items in memory: ${store.memoryItemCount}');

Working with Objects

class User {
  final String name;
  final int age;

  User(this.name, this.age);

  // Convert User to JSON
  Map<String, dynamic> toJson() => {
    'name': name,
    'age': age,
  };

  // Create User from JSON
  static User fromJson(Map<String, dynamic> json) =>
      User(json['name'], json['age']);
}

// Store an object
final user = User('Alice', 30);
await store.setObject('user1', user, (u) => u.toJson());

// Retrieve an object
final retrievedUser = await store.getObject('user1', User.fromJson);

// Store a list of objects
final users = [User('Alice', 30), User('Bob', 25)];
await store.setObjectList('users', users, (u) => u.toJson());

// Retrieve a list of objects
final retrievedUsers = await store.getObjectList('users', User.fromJson);

Transactions

// Using the transaction helper
final result = await store.transaction(() async {
  await store.set('counter', 1);
  await store.set('message', 'Transaction in progress');
  return true;
});

// Manual transaction control
await store.beginTransaction();
try {
  await store.set('key1', 'value1');
  await store.set('key2', 'value2');
  await store.commitTransaction();
} catch (e) {
  store.rollbackTransaction();
  rethrow;
}

// Nested transactions are supported
await store.transaction(() async {
  await store.set('outer', 'value');
  await store.transaction(() async {
    await store.set('inner', 'value');
    // This can be rolled back independently
  });
});

Batch Operations

// Perform multiple operations with a single save
await store.batch(() async {
  await store.set('key1', 'value1');
  await store.set('key2', 'value2');
  await store.set('key3', 'value3');
});

// Update multiple values at once
await store.update({
  'name': 'John',
  'age': 30,
  'active': true,
});

Backup and Restore

// Create a backup (automatically timestamped)
final backupPath = await store.backup();
print('Backup created at: $backupPath');

// Create a backup at a specific location
final customBackup = File('data/my_backup.json');
await store.backup(destination: customBackup);

// Restore from backup
final backupFile = File(backupPath!);
final success = await store.restore(backupFile);

// Restore and replace the current file
await store.restore(backupFile, replaceCurrentFile: true);
// Note: This automatically creates a pre-restore backup

Caching

// Enable caching with custom size
store.configureCaching(enabled: true, maxSize: 200);

// Cache is automatically used for get operations
await store.get('cached_key'); // First time loads from file
await store.get('cached_key'); // Second time loads from cache

// Invalidate specific keys
store.invalidateCache(['key1', 'key2']);

// Clear entire cache
store.invalidateCache();

Watch API (NEW!)

Monitor your data store for real-time changes:

// Watch all changes
final subscription = store.watchAll();
subscription.stream.listen((event) {
  print('Event type: ${event.type}');
  print('Key: ${event.key}');
  print('Value: ${event.value}');
  print('Old value: ${event.oldValue}');
  print('Timestamp: ${event.timestamp}');
});

// Watch specific keys
final keySubscription = store.watchKeys(['username', 'email']);
keySubscription.stream.listen((event) {
  if (event.type == StashBoxEventType.update) {
    print('Key ${event.key} changed from ${event.oldValue} to ${event.value}');
  }
});

// Watch a single key
final singleSub = store.watchKey('counter');
singleSub.stream.listen((event) {
  print('Counter changed: ${event.value}');
});

// Event types
// - StashBoxEventType.update: Key added or updated
// - StashBoxEventType.remove: Key removed
// - StashBoxEventType.clear: All data cleared
// - StashBoxEventType.load: Data loaded from file
// - StashBoxEventType.save: Data saved to file

// Cancel subscriptions
await subscription.cancel();
await store.cancelAllWatches();

// Check number of active subscriptions
print('Active watchers: ${store.watchCount}');

Error Handling

StashBox provides specialized exceptions for better error handling:

try {
  await store.get('some_key');
} on StashBoxFileException catch (e) {
  print('File operation failed: ${e.message}');
  print('Original error: ${e.innerError}');
} on StashBoxKeyException catch (e) {
  print('Key operation failed: ${e.message}');
  print('Problematic key: ${e.key}');
} on StashBoxTransactionException catch (e) {
  print('Transaction failed: ${e.message}');
} on StashBoxException catch (e) {
  print('General StashBox error: ${e.message}');
}

File Integrity

// Verify file integrity (creates backup if corrupted)
final isValid = await store.verifyIntegrity();
if (!isValid) {
  print('File was corrupted, backup created');
}

// StashBox automatically handles corrupted files:
// - Creates a backup with timestamp
// - Resets to empty state
// - Throws StashBoxFileException

Advanced Configuration

Lazy Loading Configuration

final store = StashBox(
  'data/huge_dataset.json',
  lazyLoadConfig: LazyLoadConfig(
    enabled: true,
    maxMemoryItems: 500,           // Keep only 500 items in memory
    fileSizeThreshold: 5 * 1024 * 1024, // Enable for files > 5MB
  ),
);

Auto-Save Control

// Disable auto-save for better performance during bulk operations
store.setAutoSave(false);

// Perform many operations
for (int i = 0; i < 1000; i++) {
  await store.set('key_$i', 'value_$i');
}

// Manually save when done
await store.saveToFile();

// Re-enable auto-save
store.setAutoSave(true);

Performance Tips

  1. Use Lazy Loading for files larger than 10MB
  2. Enable Caching for frequently accessed data
  3. Use Batch Operations for multiple changes
  4. Disable Auto-Save during bulk operations
  5. Use Transactions for atomic operations

License

This project is licensed under the MIT License - see the LICENSE file for details.

Libraries

stashbox