Skip to content

Offline Write Queue

Arun Prakash edited this page Nov 3, 2026 · 2 revisions

Offline Write Queue

OfflineWriteQueue is a middleware that keeps writes that failed because the device is offline and replays them later, in order. Reads are never queued.

final queue = OfflineWriteQueue(store: MemoryWriteQueueStore());
final client = WordpressClient.forSite(
  siteUrl: siteUrl,
  bootstrapper: (b) => b.withMiddleware(queue).withDefaultAuthorization(auth).build(),
);

final r = await client.posts.create(CreatePostRequest(title: 'Hi'));
if (r.wasQueued) {
  showSnack('Saved, will sync when online');         // r is still the failure response
}

// when connectivity returns, for example from a connectivity_plus listener
final result = await queue.flush(client);
print('${result.sent.length} sent, ${result.rejected.length} refused, ${result.remaining} left');
Behaviour
Queued POST, PUT, PATCH, DELETE (methods) that fail with connectionFailed. Timeouts only with queueOnTimeout: true, because the server may already have processed the write and a replay could duplicate it
Not queued Reads, server errors (4xx/5xx), cancellations, uploads and other non JSON bodies
flush Oldest first. Accepted writes are removed. A 4xx refusal (rest_forbidden, ...) is removed and listed in rejected. Stops at the first write that still cannot be delivered (offline, 5xx, 408, 429) and keeps it and the rest
maxLength Queue size cap (default 100); writes beyond it are not queued
Stored Method, URL, query, body and the requireAuth flag. Headers and credentials are not stored; the replay is authorized by the client you flush with

Stores:

Store Keeps the queue
MemoryWriteQueueStore() Until the app stops
JsonWriteQueueStore(read: ..., write: ...) In any text storage (shared_preferences, localStorage), on every platform
FileWriteQueueStore(path) from package:wordpress_client/io.dart In a JSON file, written atomically. Not on the web
your own WriteQueueStore Wherever you like (all, add, remove); QueuedWrite has toJson / fromJson
final queue = OfflineWriteQueue(
  store: JsonWriteQueueStore(
    read: () async => prefs.getString('wp_queue'),
    write: (json) => prefs.setString('wp_queue', json),
  ),
);

Queued bodies (the post you were saving, for example) are stored as plain text. Headers and credentials are not.

The response of a queued write

The original call still returns its failure (connectionFailed), because nothing reached the server. response.wasQueued tells you it was kept, and response.extra[OfflineWriteQueue.queuedKey] holds the QueuedWrite.id.

Inspect the queue

final waiting = await queue.pending;     // List<QueuedWrite>, oldest first
final count = await queue.length;

A QueuedWrite has method, url, query, body, queuedAt and id.

A store of your own

JsonWriteQueueStore covers most cases. To keep the queue in a database, implement WriteQueueStore:

class PrefsStore implements WriteQueueStore {
  PrefsStore(this.prefs);
  final SharedPreferences prefs;
  static const _key = 'wp_queue';

  List<QueuedWrite> _read() => [
        for (final s in prefs.getStringList(_key) ?? const <String>[])
          QueuedWrite.fromJson(jsonDecode(s) as Map<String, dynamic>),
      ];

  @override
  Future<List<QueuedWrite>> all() async => _read();

  @override
  Future<void> add(QueuedWrite w) =>
      prefs.setStringList(_key, [..._read().map((e) => jsonEncode(e.toJson())), jsonEncode(w.toJson())]);

  @override
  Future<void> remove(String id) => prefs.setStringList(
      _key, [for (final w in _read()) if (w.id != id) jsonEncode(w.toJson())]);
}

More ideas for storage: Integration with Storage Packages.

Flush when you are back online

Connectivity().onConnectivityChanged.listen((results) {
  if (!results.contains(ConnectivityResult.none)) queue.flush(client);
});

Calling flush while another is running returns the running one, so it is safe to trigger from several places.

Limits

  • Authentication is not stored. Flush with a client that has valid credentials; a write refused with 401/403 is reported in rejected and dropped.
  • The server may have changed since the write was made. Conflicts surface as 4xx answers in rejected.
  • Queued writes do not touch the cache until they are sent.

Clone this wiki locally