Skip to content

IndexedDbFileSystem.flush() does not await a batch already in flight (regression in 3.4.0) #408

Description

@DenisovAV

Summary

IndexedDbFileSystem.flush() returns without awaiting when a write batch is already in flight. Its own doc comment promises the opposite:

Waits for all pending operations to finish, then completes the future.
Each call to [flush] will await pending operations made before the call.

This is a regression in 3.4.0. In 3.3.4 flush() queued a marker behind the running batch and awaited it; the marker was removed in 11be8acb ("Optimize indexeddb flush on idle").

The change

-  Future<void> flush() async {
-    final item = _submitWorkFunction((_) async {}, 'flush');
-    _startWorkingIfNeeded(isImplicit: false);
-    await item;
+  Future<void> flush() {
+    return _startWorkingIfNeeded(isImplicit: false);
   }

The same commit made _startWorkingIfNeeded async and await the writes, which makes the idle path a genuine fence and removes the marker's overhead. But the busy path is now a no-op:

Future<void> _startWorkingIfNeeded({required bool isImplicit}) async {
  if (isImplicit && !_writeAutomatically) return;
  if (!_isWorking && _pendingWork.isNotEmpty) {   // ← skipped while a batch runs
    ...
    await _asynchronous._performWrites(items).whenComplete(...);
  }
}

With writeAutomatically: true (the default), xWrite_submitWork_startWorkingIfNeeded(isImplicit: true) sets _isWorking synchronously, so right after any write the flag is already set. A flush() at that moment takes no branch and its future completes immediately, before the IndexedDB transaction commits.

Reproduction

Any write followed by a flush:

final fs = await IndexedDbFileSystem.open(dbName: 'repro');
// ... open a database on this VFS and INSERT something ...
await fs.flush();   // returns before the IndexedDB transaction commits on >= 3.4.0

Measured in headless Chrome: on 3.5.2 flush() completed after 0 event-loop turns; on 3.3.3 it waited 7.

Version boundary, checked against the source at each tag:

version flush() body fence?
3.3.3, 3.3.4 _submitWorkFunction(() {}, 'flush') yes
3.4.0 … 3.5.2 _startWorkingIfNeeded(isImplicit: false) only when idle

Why it matters

flush() is the only way to force a transaction. open()'s doc says writes are "asynchronously written to IndexedDB without any durability guarantees. You can invoke [flush] to force a transaction", and the 3.4.0 changelog says writeAutomatically: false users "need to call flush manually" — for those users a flush that does not fence means the write may not have happened at all yet.

For default users the window is small (the VFS streams writes continuously, so what is missed is the batch in flight), but it is exactly the window an app closes in when it flushes on visibilitychange before the tab goes away.

Suggested fix

Keep the idle optimisation, restore the marker only when busy:

Future<void> flush() {
  if (!_isWorking) return _startWorkingIfNeeded(isImplicit: false);
  // A batch is already running: queue a marker behind it and await that.
  return _submitWorkFunction((_) async {}, 'flush');
}

_FunctionWorkItem does not override insertInto, so it always lands at the tail of _pendingWork and is never merged or skipped. When the running batch finishes, whenComplete restarts work and the next batch carries every write submitted before the flush, plus the marker last — so the marker's completer resolves only after they are committed. This is what close() already does.

Happy to open a PR for this.

Found while adding an explicit persistence call to a package that uses this VFS for a vector store (DenisovAV/flutter_gemma#507).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions