dartvex_local 0.2.0
dartvex_local: ^0.2.0 copied to clipboard
Offline-capable extension for Dartvex. SQLite query cache and mutation queue with optimistic updates.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased] #
0.2.0 - 2026-06-12 #
Changed #
- Require
dartvex^0.2.0. - Require Dart
^3.10.0to align with the SQLite 3.x runtime dependency. CacheStorageimplementations must now providedeleteCacheEntry, because single-entry deletion is required for correct optimistic rollback. The optionalCacheStorageMaintenanceinterface now only covers maximum-entry pruning.QueueStorageimplementations must now providesaveFailedLocalId,loadFailedLocalIds, andclearFailedLocalIds, because replay must persist failed locally-generated IDs to keep dependent mutations from being sent with stale IDs after a restart.
Fixed #
- Remote snapshot writes and their pending-patch rebases are now serialized per query key. Two rapid remote results for the same query used to interleave those multi-await spans, and when their rebase iterations diverged (a replay dropping a pending mutation mid-flight) the older event's rebased value could land last — leaving a stale value in the cache and as the subscribers' latest emission until the next remote event.
- Caller-provided args are now deep-snapshotted at the
query,subscribe,mutate, andactionentry points. ALocalQueryDescriptorkey is recomputed from its stored args on every use, and the previous shallow copy shared nested values with the caller — so mutating a nested value after subscribing made the recomputed key diverge, orphaned the query state on cancel, and leaked its remote subscription. Concurrentmutatecalls also snapshot before waiting on the FIFO mutation chain, so a later caller-side args mutation cannot alter a queued call that has not started yet. - Disposing the client while a
setNetworkMode(LocalNetworkMode.offline)orclearQueuecall was emitting cached snapshots no longer routes an event into a just-closed subscription controller, which failed the in-flight call's future with "Cannot add new events after calling close". Dispose now clears the subscription registry before closing its controllers, and the emission loops stop once the client is disposed. setNetworkModetransitions are now serialized. A mode change issued while the previous transition was still suspending or resuming remote subscriptions could interleave with it — re-attaching some queries mid-suspension while the rest were detached after the resume, leaving them unsubscribed (and silently stale) in auto mode.disposeandsetNetworkMode(LocalNetworkMode.offline)no longer throw aConcurrentModificationErrorwhen a subscription cancel that drops a query's last subscriber is in flight at the same time; the faileddisposealso skipped closing the underlying storage.clearQueuenow rolls back the discarded mutations' optimistic patches: every affected cached query is restored to its oldest pending rollback baseline (the last server-confirmed value) and subscribers are notified, so the cache no longer presents writes that will never be sent as authoritative data. An optimistic-only cache entry (one created from an absent baseline) is deleted and its subscribers receive an error event instead of a stale optimistic value.PendingMutation.copyWithcan now clear replay error metadata explicitly viaclearErrorMessage, avoiding stale errors when callers reset queue state.ConvexLocalClient.mutatenow serializes calls in FIFO order, so concurrent mutations can no longer race the "send directly while the queue is empty" fast path and commit (or queue) out of call order. Mutations awaited sequentially are unaffected.- The offline replay retry backoff now clamps its exponent before shifting, so it stays monotonic and never wraps after many consecutive failures (web uses 32-bit left shifts).
- Replay no longer rewrites a queued mutation's args when id remapping leaves them unchanged, avoiding a redundant SQLite write per replayed mutation.
- Remote query loading events from
dartvexare now handled explicitly by the local runtime adapter, keeping switches exhaustive while preserving cached local results until a remote success or error arrives. LocalClientConfig.queryCachePolicycan now expire stale cached query results and prune the SQLite query cache to a maximum entry count, preventing unbounded growth and arbitrarily old offline reads when configured.- SQLite database handles are now closed if schema migration fails during
SqliteLocalStore.openoropenInMemory. - Auto-mode mutations now queue behind any existing replay work instead of bypassing a non-empty offline queue and committing newer mutations before older queued ones.
- Drops queued mutations that still reference unresolved
local-*IDs during replay, so dependents of a failed create are reported viaonConflictinstead of being sent to the backend with stale local IDs. - Local ID replay remaps can now be captured from create mutations returning
either a string id or an object containing
_id/id. - Local ID replay remaps now also rewrite
local-*IDs used as object keys in a queued mutation's args, not just as values, so an offline mutation keyed by a freshly-created document is replayed against the real server ID. The unresolved-ID drop guard likewise inspects keys, so a dependent keyed by a failed create is dropped instead of sent with a stale local ID. - Rolling back a dropped mutation now re-snapshots every surviving pending mutation's rollback baseline to the restored cache value, so dropping a second mutation that touches the same query can no longer resurface the first, already-dropped mutation when the best-effort post-drop server refresh fails.
- Replay now tracks actual locally generated IDs instead of treating every
local-<digits>-<digits>shaped user string as an unresolved local ID, while persisting failed local IDs so dependents are still dropped correctly after a crash. - Replay now rejects local-ID map-key remaps that would collide with an existing
key after resolution, reporting the queued mutation through
onConflictinstead of silently dropping one of the map entries before sending to Convex. - Stops in-flight replay cleanly during
dispose(), preventing writes to closed SQLite stores or closed mutation streams after a delayed remote mutation returns. - Map the new terminal
dartvexConnectionState.fatalErrorto a disconnected local connection state, keeping the remote-client adapter's connection-state mapping exhaustive and analyzer-clean. - Delivering a query event no longer throws when a subscriber cancels (or
re-subscribes the same query) synchronously from its listener: the fan-out
iterates a snapshot of subscribers and defers closing the subscription stream
past the in-progress dispatch, fixing a
ConcurrentModificationErrorand a "Cannot fire new event" state error. - A synchronous
LocalRemoteClient.subscribefailure is now reported as a local query error event instead of escaping fromConvexLocalClient.subscribeafter local subscription state has already been registered. The returned local subscription remains cancellable. - Raw errors emitted by a remote query stream now count as remote events before they are surfaced locally, so a delayed cache seed cannot overwrite the remote error with stale cached data.
- Errors emitted by a custom
LocalRemoteClient.connectionStatestream are now logged and treated as a remote disconnect instead of surfacing as unhandled zone errors while leaving the local client marked online. SqliteLocalStore.close()no longer deletes the database file's parent directory, which could remove unrelated files placed alongside it.- Queues auto-mode mutations immediately while the remote client is disconnected, and waits for a connected remote before replaying queued mutations.
- Uses the Convex value codec for local query keys and storage payloads so
BigInt, bytes, and special floating-point values round-trip correctly. - Queues retryable auto-mode mutations when the remote client is unavailable.
- Retains retryable replay failures for later retry instead of dropping queued mutations.
- Rolls back failed optimistic patches and permanently rejected queued mutations against the previous local cache value, so retryable or failed mutations do not leave stale optimistic query data visible after the error path runs.
- Rollback now deletes optimistic-only cache entries through every
CacheStorageimplementation, including custom stores that do not implementCacheStorageMaintenance. - Falls back to cached query data on retryable remote failures.
- Preserves structured remote query error data and server log lines in the local runtime adapter.
ConvexLocalClient.openWithRemotenow respectsLocalClientConfig.disposeRemoteClient, leaving caller-owned custom remotes alive by default and disposing them only when explicitly configured.
0.1.2 - 2026-04-30 #
Improved #
- Refreshed README metadata, logo links, installation snippets, and example code for pub.dev.
- Declared native platform support explicitly so pub.dev does not advertise web support for the SQLite-backed package.