work_db 1.3.0
work_db: ^1.3.0 copied to clipboard
A lightweight, cross-platform local database for Dart and Flutter. Simple key-value storage with collections, supporting Desktop, Web, and Mobile.
work_db #
A lightweight, cross-platform local database for Dart and Flutter with optional thread-safe locking. Simple key-value storage organized in collections, supporting Desktop, Web, Mobile platforms, and concurrent access scenarios.
Features #
- Cross-platform: Windows, macOS, Linux, Web, iOS, Android
- Simple API: CRUD operations, batch, upsert
- Collections: Organize data in named collections
- Batch operations: Create, retrieve, update multiple items
- Type-safe: Full Dart type safety
- Thread-Safe Locking (New in 1.1.0): File-based locking with stale lock detection
ClientWorkDbLockfor concurrent access protection- Automatic lock expiration (5000ms timeout)
- Stale lock cleanup with configurable timeout
- Lock information tracking (timestamp, user)
- Flexible Architecture: Choose
ClientWorkDb(lightweight) orClientWorkDbLock(thread-safe) - Factory Pattern: Polymorphic factory with dedicated input types for each implementation
- Minimal dependencies: Only
pathfor IO operations - Synchronous API (New in 1.2.0): Full
*Synccounterparts for all operations viaIWorkDbSynccreateSync,updateSync,retrieveSync,deleteSync, and all other operations available synchronouslyClientWorkDbLockSyncfor thread-safe synchronous operations
- Record Limit per Collection (New in 1.3.0): Optional
maxRecordsPerCollectionwith automatic eviction of oldest records - Well tested: 397 tests across all implementations + locking tests + full sync test coverage
Installation #
dependencies:
work_db: ^1.3.0
Quick Start #
import 'package:work_db/work_db.dart';
void main() async {
// Create a factory
final factory = WorkDbFactory();
// Create a database instance for desktop/server
final db = factory.create(IoWorkDbFactoryInput(dataPath: './data'));
// Create an item
await db.create(ItemWithId(
id: 'user-1',
collection: 'users',
item: {'name': 'John Doe', 'email': 'john@example.com'},
));
// Retrieve the item
final user = await db.retrieve(ItemId(id: 'user-1', collection: 'users'));
print(user?.item['name']); // John Doe
// Update the item
await db.update(ItemWithId(
id: 'user-1',
collection: 'users',
item: {'name': 'John Doe', 'email': 'john.updated@example.com'},
));
// Delete the item
await db.delete(ItemId(id: 'user-1', collection: 'users'));
}
Thread-Safe Operations with Locking (New in 1.1.0) #
For concurrent access across multiple clients or isolates, use ClientWorkDbLock for automatic lock management:
import 'package:work_db/work_db.dart';
void main() async {
// Create a locked database instance for thread-safe operations
final db = ClientWorkDbLock(IoWorkDb('./data'));
// All operations are protected by file locks
await db.create(ItemWithId(
id: 'user-1',
collection: 'users',
item: {'name': 'John Doe'},
));
// Locks are automatically acquired and released
await db.update(ItemWithId(
id: 'user-1',
collection: 'users',
item: {'name': 'John Smith'},
));
}
Locking Features #
- Automatic Lock Management: Locks are acquired at operation start and released after completion
- Stale Lock Cleanup: Expired locks are automatically detected and cleaned up
- Lock Expiration: Locks expire after 5000ms if not explicitly released
- Configurable Timeout: Use
ClientWorkDbLock.withWaitingMs()for stale lock detection with custom timeout
// Enable stale lock detection with 300ms timeout
final db = ClientWorkDbLock.withWaitingMs(
IoWorkDb('./data'),
waitingMs: 300,
);
Choosing Between Implementations #
| Use Case | Implementation | Benefits |
|---|---|---|
| Single-threaded app | ClientWorkDb |
Minimal overhead, simpler code |
| Concurrent access | ClientWorkDbLock |
Thread-safe, automatic lock management |
| Sync single-threaded | ClientWorkDb (sync API) |
No async overhead, blocking calls |
| Sync concurrent access | ClientWorkDbLockSync |
Thread-safe sync operations |
| Testing | In-memory backend with ClientWorkDb or ClientWorkDbLock |
No file I/O, isolated state |
Synchronous API (New in 1.2.0) #
All database operations are available as synchronous methods. ClientWorkDb implements IWorkDbSync directly — no wrapper needed:
import 'package:work_db/work_db.dart';
void main() {
final db = ClientWorkDb(IoWorkDb('./data'));
// Synchronous create
db.createSync(ItemWithId(
id: 'user-1',
collection: 'users',
item: {'name': 'John Doe'},
));
// Synchronous retrieve
final user = db.retrieveSync(ItemId(id: 'user-1', collection: 'users'));
print(user?.item['name']); // John Doe
// Synchronous update
db.updateSync(ItemWithId(
id: 'user-1',
collection: 'users',
item: {'name': 'John Smith'},
));
// Synchronous delete
db.deleteSync(ItemId(id: 'user-1', collection: 'users'));
}
Thread-Safe Synchronous Operations #
For concurrent synchronous access, use ClientWorkDbLockSync:
import 'package:work_db/work_db.dart';
void main() {
final db = ClientWorkDbLockSync(IoWorkDb('./data'));
// Sync operations are protected by file locks
db.createSync(ItemWithId(
id: 'doc-1',
collection: 'documents',
item: {'title': 'Hello'},
));
// With stale lock detection
final dbWithTimeout = ClientWorkDbLockSync.withWaitingMs(
IoWorkDb('./data'),
waitingMs: 300,
);
}
ClientWorkDbLockSync also inherits all async operations from ClientWorkDbLock, so you can mix sync and async calls on the same instance.
Record Limit per Collection (New in 1.3.0) #
Set an optional maximum number of records per collection. When the limit is exceeded, the oldest records are automatically evicted:
// Keep max 10 log entries per collection
final db = ClientWorkDb(
IoWorkDb('./data'),
maxRecordsPerCollection: 10,
);
Works with all backends and factory methods:
// Via factory
final db = factory.create(MemoryWorkDbFactoryInput(
maxRecordsPerCollection: 100,
));
// Via convenience class
final db = WorkDb.memory(maxRecordsPerCollection: 3);
// With locking
final db = ClientWorkDbLock(
IoWorkDb('./data'),
maxRecordsPerCollection: 50,
);
Eviction is triggered on create, createMultiple, createOrUpdate, and createOrUpdateMultiple (and their sync variants). Items are sorted by creation timestamp and the oldest are removed when the limit is exceeded.
Platform-Specific Setup #
Desktop (Windows, macOS, Linux) & Server #
import 'package:work_db/work_db.dart';
final factory = WorkDbFactory();
final db = factory.create(IoWorkDbFactoryInput(dataPath: './my_app_data'));
Web #
import 'package:work_db/work_db.dart';
final factory = WorkDbFactory();
final db = factory.create(WebWorkDbFactoryInput());
Flutter Mobile (iOS, Android) #
import 'package:work_db/work_db.dart';
import 'package:path_provider/path_provider.dart';
Future<IWorkDb> createDatabase() async {
final dir = await getApplicationDocumentsDirectory();
final factory = WorkDbFactory();
return factory.create(IoWorkDbFactoryInput(dataPath: dir.path));
}
Testing #
import 'package:work_db/work_db.dart';
final factory = WorkDbFactory();
final db = factory.create(MemoryWorkDbFactoryInput());
API Reference #
Factory Methods #
| Method | Description |
|---|---|
WorkDbFactory().create(IoWorkDbFactoryInput) |
File system storage (Desktop/Server) |
WorkDbFactory().create(WebWorkDbFactoryInput) |
localStorage storage (Web) |
WorkDbFactory().create(MemoryWorkDbFactoryInput) |
In-memory storage (Testing) |
Each call to create() returns a new independent instance. You can create multiple databases with different paths:
final db1 = factory.create(IoWorkDbFactoryInput(dataPath: './data1'));
final db2 = factory.create(IoWorkDbFactoryInput(dataPath: './data2'));
// db1 and db2 are completely independent
Database Operations #
All operations are available in both async (Future<T>) and sync (T) variants.
| Async Method | Sync Method | Description |
|---|---|---|
create(ItemWithId) |
createSync(ItemWithId) |
Create a new item (throws if exists) |
createMultiple(List<ItemWithId>) |
createMultipleSync(List<ItemWithId>) |
Create multiple items |
update(ItemWithId) |
updateSync(ItemWithId) |
Update existing item (throws if not exists) |
createOrUpdate(ItemWithId) |
createOrUpdateSync(ItemWithId) |
Create or update (upsert) |
createOrUpdateMultiple(List<ItemWithId>) |
createOrUpdateMultipleSync(List<ItemWithId>) |
Batch upsert |
retrieve(ItemId) |
retrieveSync(ItemId) |
Get item or null |
retrieveMultiple(List<ItemId>) |
retrieveMultipleSync(List<ItemId>) |
Get multiple items |
delete(ItemId) |
deleteSync(ItemId) |
Delete item (throws if not exists) |
deleteCollection(String) |
deleteCollectionSync(String) |
Delete entire collection |
clearDatabase() |
clearDatabaseSync() |
Delete all data |
getItemsInCollection(String) |
getItemsInCollectionSync(String) |
List item IDs in collection |
getCollections() |
getCollectionsSync() |
List all collection names |
Examples #
Batch Operations #
// Create multiple items at once
await db.createMultiple([
ItemWithId(id: 'user-1', collection: 'users', item: {'name': 'Alice'}),
ItemWithId(id: 'user-2', collection: 'users', item: {'name': 'Bob'}),
ItemWithId(id: 'user-3', collection: 'users', item: {'name': 'Charlie'}),
]);
// Retrieve multiple items
final users = await db.retrieveMultiple([
ItemId(id: 'user-1', collection: 'users'),
ItemId(id: 'user-2', collection: 'users'),
]);
Upsert (Create or Update) #
// Creates if not exists, updates if exists
await db.createOrUpdate(ItemWithId(
id: 'settings',
collection: 'config',
item: {'theme': 'dark', 'language': 'en'},
));
Collection Management #
// List all collections
final collections = await db.getCollections();
print(collections); // ['users', 'config', 'posts']
// List items in a collection
final userIds = await db.getItemsInCollection('users');
print(userIds); // ['user-1', 'user-2', 'user-3']
// Delete entire collection
await db.deleteCollection('cache');
// Clear everything
await db.clearDatabase();
Nested Data #
await db.create(ItemWithId(
id: 'post-1',
collection: 'posts',
item: {
'title': 'Hello World',
'content': 'This is my first post',
'author': {
'name': 'John',
'email': 'john@example.com',
},
'tags': ['dart', 'flutter', 'database'],
'metadata': {
'views': 0,
'likes': 0,
'published': true,
},
},
));
Storage Format #
Data is stored as JSON files in a simple directory structure:
<dataPath>/
└── WorkDB/
├── users/
│ ├── user-1 (JSON file)
│ └── user-2 (JSON file)
└── posts/
└── post-1 (JSON file)
Error Handling #
import 'package:work_db/work_db.dart';
try {
await db.create(ItemWithId(
id: 'duplicate',
collection: 'test',
item: {'data': 'value'},
));
// This will throw ItemAlreadyExistsException
await db.create(ItemWithId(
id: 'duplicate',
collection: 'test',
item: {'data': 'new value'},
));
} on ItemAlreadyExistsException catch (e) {
print('Item ${e.id} already exists in ${e.collection}');
}
// Use createOrUpdate to avoid exceptions
await db.createOrUpdate(ItemWithId(
id: 'safe',
collection: 'test',
item: {'data': 'value'},
));
Testing Your Code #
import 'package:test/test.dart';
import 'package:work_db/work_db.dart';
void main() {
late IWorkDb db;
late WorkDbFactory factory;
setUp(() {
factory = WorkDbFactory();
db = factory.createNew(MemoryWorkDbFactoryInput());
});
test('should store and retrieve data', () async {
await db.create(ItemWithId(
id: 'test-1',
collection: 'test',
item: {'value': 42},
));
final result = await db.retrieve(ItemId(id: 'test-1', collection: 'test'));
expect(result?.item['value'], equals(42));
});
}
License #
LGPL v3 License - see LICENSE for details.
Contributing #
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.