local_first 0.9.0
local_first: ^0.9.0 copied to clipboard
Local-first solution 100% self hosted, for offline and live data sync between multiple devices with support for Hive, SQLite, websockets, etc.
0.9.0 #
Dropping local data without telling the server #
Two members on LocalFirstStorage (and so on every repository's storage) for
the cases where the device has to let data go, while what it still owes the
server is kept:
deleteAllSynced(tableName)drops every item whose events are all synced, together with those events, and returns how many items went. An item with a write still waiting to be sent stays, with its events. This is the primitive behind a full re-sync of one repository: what the device holds is replaced by what the server sends, and what the device wrote and has not sent is sent afterwards — a re-sync that used to mean losing offline writes.deleteWhere(tableName, field:, value:)drops every item whosefieldequalsvalue, with their events, and returns how many went. It is a local drop, not a delete to be synced: nothing is queued for the remote. It is for data that stopped being the device's to hold — the rows of a conversation the account left, of a page it unfollowed — where the server already knows and only the local copy has to go. A field declared in the repository's schema is matched on its own column; any other field is matched inside the stored payload.
A delete that needs sync is sent #
_markAllPreviousEventAsOk recognized the incoming event by its moment, and a
stored moment is kept in milliseconds: the event read back from storage was a
few microseconds behind the one in hand, so it counted as previous and was
marked ok — the write it carried was never sent. A removal made offline
vanished silently. The event is now recognized by its id, and is never one of
its own previous events.
Watched queries — InMemoryLocalFirstStorage #
- A watcher reads one query at a time. Two writes side by side notified twice, the two reads could finish in either order, and the watcher was left showing the older result. A notification that arrives while a read is running asks for exactly one more read, so the last emission is the newest state.
- A read iterates a snapshot of the table, so a row deleted while the read is
in progress no longer breaks it with a concurrent-modification error — which
is exactly what
deleteWheredoes to a watcher of the rows it removes. - Watchers belong to the storage, not to a namespace. A watcher registered
before
useNamespacewas kept in the old namespace's bucket and went silent after the switch; it now follows the data of whichever namespace is current and is re-emitted on every switch. This is the contractSqliteLocalFirstStoragealready kept, so a test written over the in-memory storage now proves the behaviour the device gets.
⚠️ For custom
LocalFirstStorageimplementations:deleteAllSyncedanddeleteWhereare new required members — a storage that does not implement them no longer compiles. A backend with no way to ask for unsynced events can return0and do nothing, as long as callers are not offered a re-sync. The built-in Hive, SQLite and in-memory backends are already updated.
0.8.2 #
Performance — large remote-event batches (cold sync) are now ~O(n) #
Applying a large batch of remote events (typically a device's first sync) used to be O(n²) and dominated sync time. In profiling, applying ~98 records dropped from ~70s to ~3s.
_markAllPreviousEventAsOknow skips events already markedokinstead of re-writing every earlier same-record event on every incoming event (the main O(n²) source).mergeRemoteEventreads each record's event history once and reuses it for both the pending-conflict check and the mark-previous pass (previously two identical queries per event).LocalFirstStorage.getAllEventsgained an optionaldataIdso callers can read a single record's history instead of the whole event log; the SQLite backend pushes it down to a SQLWHERE, the in-memory backend filters in Dart.- Added
LocalFirstStorage.runInTransaction(action).LocalFirstClient.pullChangesnow applies a whole batch inside one storage transaction, so backends that support it (SQLite) commit once and defer watcher notifications until after the commit.
Read performance #
- Added
LocalFirstStorage.watchChanges(repositoryName)andLocalFirstRepository.watchChanges()— a lightweight change signal that emits on any write to a repository (plus one initial tick on listen), so callers reload through their own read path instead of receiving rows through the stream. - Added an indexed single-item read path (
getById) so a lookup by id uses the storage's primary-key index instead of scanning and deserializing the whole table.
⚠️ For custom
LocalFirstStorageimplementations: addrunInTransaction(backends without a real transaction may just run the action), the optionaldataIdparameter ongetAllEvents, andwatchChanges(a broadcastStream<void>that ticks on writes). The built-in Hive/SQLite/in-memory backends are already updated.
0.8.1 #
- Fixed analyzer warning in
BackupService(unused local variable).
0.8.0 #
- Added backup & restore system with
BackupService,BackupData,BackupStorageProviderinterface, and AES-256 + gzip encryption pipeline. - Added
local_first_firebase_backup,local_first_gdrive_backup, andlocal_first_icloud_backupcompanion packages. - Added configurable logger system (
LocalFirstLogger) for all plugins with adjustable log levels. - Added repository filtering to sync strategies via
onBuildSyncFiltercallback. - Fixed
DateTimeserialization to ISO 8601 string inLocalFirstEvent.toJson(). - Fixed orphaned events being deserialized in
getAllEvents, preventing runtime errors. - Fixed server event field name alignment with client convention.
⚠️ Breaking change — event metadata field rename #
All internal event metadata keys now use a _ prefix to prevent collision with your entity fields (e.g. a user.created_at field no longer conflicts with the event's own _created_at).
If you store or transmit raw event maps (e.g. via a custom sync backend or direct storage queries), update your field references:
| Before | After |
|---|---|
eventId |
_event_id |
repository |
_repository |
operation |
_operation |
createdAt |
_created_at |
data |
_data |
dataId |
_data_id |
syncStatus |
_sync_status |
lastEventId |
_last_event_id |
The Dart constants (LocalFirstEvent.kEventId, kSyncCreatedAt, etc.) remain unchanged — only the string values they hold have changed. If you access event fields exclusively through these constants, no code changes are needed. A one-time database migration may be required if you have existing persisted events.
0.7.2 #
- Updated README with absolute GitHub URLs for proper rendering on pub.dev
- Added sync strategy packages to Installation section (periodic and websocket)
- Reorganized Running examples section with storage adapters and sync strategies categories
- Updated package versions in Installation documentation
0.7.1 #
- Added chat app example with real-time messaging using dual sync strategy (WebSocket + Periodic)
- Enhanced documentation with data flow diagram and fixed pub.dev package links
- Improved code examples in README with corrected syntax and API usage
- Tuned counter app sync intervals for better performance (60s heartbeat, 30s periodic)
0.7.0 #
- Added comprehensive counter app example demonstrating real-time WebSocket synchronization
- Improved test coverage and reliability across the framework
- Enhanced documentation and code examples
0.6.0 #
- Added
local_first_shared_preferencesadapter with namespaced config storage and example app. - Unified example apps across adapters and defaulted core example to in-memory storage.
- Expanded documentation with supported config types tables, example run instructions, and contribution links.
- Refined config storage APIs (optional delegate, namespace propagation) and achieved full test coverage.
- Simplified remote pull API to make per-repository backend integrations easier.
0.5.0 #
- Split storage adapters into separate publishable packages:
local_first_hive_storagefor Hive-based storagelocal_first_sqlite_storagefor SQLite-based storage
- Core package no longer bundles adapter implementations.
- Documentation and tooling updated for addon packages.
0.4.0 #
- Add SQLite storage adapter (
SqliteLocalFirstStorage) with schema/index support and query filtering - Document how to choose between Hive and SQLite storage backends
- Expand example tooling with launch configs and relational sample polish
0.3.0 #
- Switch models to the
LocalFirstModelmixin for direct field access without wrappers - Expand automated test suite to achieve full 100% test coverage of core flows and APIs
- Refresh README roadmap/goals to reflect documentation and testing updates
0.2.0 #
- Replace singleton
LocalFirstwith injectableLocalFirstClient - Standardize storage interface as
LocalFirstStoragewith Hive implementation - Repositories now carry serialization/conflict logic directly
- Example app updated to new client/repository APIs and string-based metadata
0.0.1 #
- Initial scaffolding