From 7b7bc5210c8bcf63433852e9dab7ade91bd26d33 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Thu, 26 Sep 2024 11:15:20 +0200 Subject: [PATCH 01/19] feat(offline_first_with_rest): add request/response callbacks to the RestOfflineQueueClient --- .../offline_first_with_rest_repository.dart | 9 ++++++ .../rest_offline_queue_client.dart | 29 +++++++++++++++---- ...ffline_first_with_supabase_repository.dart | 5 ++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index e68d43db..96c924b6 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -47,6 +47,13 @@ abstract class OfflineFirstWithRestRepository /// This property is forwarded to `RestOfflineQueueClient` and assumes /// its defaults List? reattemptForStatusCodes, + + /// This property is forwarded to `RestOfflineRequestQueue`. + void Function(http.Request request, http.StreamedResponse response)? + onReattemptableResponse, + + /// This property is forwarded to `RestOfflineRequestQueue`. + void Function(http.Request, Object)? onRequestError, required RestProvider restProvider, required super.sqliteProvider, }) : remoteProvider = restProvider, @@ -57,6 +64,8 @@ abstract class OfflineFirstWithRestRepository restProvider.client, offlineQueueManager, reattemptForStatusCodes: reattemptForStatusCodes, + onRequestError: onRequestError, + onReattemptableResponse: onReattemptableResponse, ); offlineRequestQueue = RestOfflineRequestQueue( client: remoteProvider.client as RestOfflineQueueClient, diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart index 70928355..007835e9 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart @@ -19,6 +19,14 @@ class RestOfflineQueueClient extends http.BaseClient { final RequestSqliteCacheManager requestManager; + /// A callback triggered when the response of a request has a status code + /// which is present in the [reattemptForStatusCodes] list. + void Function(http.Request request, http.StreamedResponse response)? + onReattemptableResponse; + + /// A callback triggered when a request throws an exception during execution. + void Function(http.Request request, Object error)? onRequestError; + /// If the response returned from the client is one of these error codes, the request /// **will not** be removed from the queue. For example, if the result of a request produces a /// 404 status code response (such as in a Tunnel not found exception), the request will @@ -37,6 +45,8 @@ class RestOfflineQueueClient extends http.BaseClient { RestOfflineQueueClient( this._inner, this.requestManager, { + this.onReattemptableResponse, + this.onRequestError, List? reattemptForStatusCodes, /// Any request URI that begins with one of these paths will not be @@ -89,17 +99,26 @@ class RestOfflineQueueClient extends http.BaseClient { // Attempt to make HTTP Request final resp = await _inner.send(request); - if (cacheItem.requestIsPush && !reattemptForStatusCodes.contains(resp.statusCode)) { - final db = await requestManager.getDb(); - // request was successfully sent and can be removed - _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); - await cacheItem.delete(db); + if (cacheItem.requestIsPush) { + if (!reattemptForStatusCodes.contains(resp.statusCode)) { + final db = await requestManager.getDb(); + // request was successfully sent and can be removed + _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); + await cacheItem.delete(db); + } else { + _logger.finest( + 'request failed, will be reattempted: ${cacheItem.toSqlite()}'); + onReattemptableResponse?.call(request, resp); + } } return resp; } catch (e) { + // e.g. SocketExceptions will be caught here + onRequestError?.call(request, e); _logger.warning('#send: $e'); } finally { + // unlock the request which results in a reattempt final db = await requestManager.getDb(); await cacheItem.unlock(db); } diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index 89f5d31e..28f3bbf1 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -189,6 +189,9 @@ abstract class OfflineFirstWithSupabaseRepository 504, ], bool? serialProcessing, + void Function(http.Request request, http.StreamedResponse response)? + onReattemptableResponse, + void Function(http.Request, Object)? onRequestError, }) { final client = RestOfflineQueueClient( innerClient ?? http.Client(), @@ -200,6 +203,8 @@ abstract class OfflineFirstWithSupabaseRepository ), ignorePaths: ignorePaths, reattemptForStatusCodes: reattemptForStatusCodes, + onReattemptableResponse: onReattemptableResponse, + onRequestError: onRequestError, ); return (client, RestOfflineRequestQueue(client: client)); } From be725a7a01a49f93b436cdec7b107c6900ebd58c Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Sat, 12 Oct 2024 17:52:04 +0200 Subject: [PATCH 02/19] refactor(OfflineFirstWithRestRepository): make callbacks abstract methods instead of params --- .../src/offline_first_with_rest_repository.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index 96c924b6..287bb1fb 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -30,6 +30,17 @@ abstract class OfflineFirstWithRestRepository @protected late RestOfflineRequestQueue offlineRequestQueue; + /// A callback triggered when the response of a request has a status code + /// which is present in the [reattemptForStatusCodes] list. + /// + /// Forwarded to [RestOfflineQueueClient]. + void Function(http.Request request, http.StreamedResponse response)? onReattemptableResponse; + + /// A callback triggered when a request throws an exception during execution. + /// + /// Forwarded to [RestOfflineQueueClient]. + void Function(http.Request, Object)? onRequestError; + OfflineFirstWithRestRepository({ super.autoHydrate, super.loggerName, @@ -49,11 +60,10 @@ abstract class OfflineFirstWithRestRepository List? reattemptForStatusCodes, /// This property is forwarded to `RestOfflineRequestQueue`. - void Function(http.Request request, http.StreamedResponse response)? - onReattemptableResponse, + this.onReattemptableResponse, /// This property is forwarded to `RestOfflineRequestQueue`. - void Function(http.Request, Object)? onRequestError, + this.onRequestError, required RestProvider restProvider, required super.sqliteProvider, }) : remoteProvider = restProvider, From c22504d08ecfc727f9ba40ef4db9a210551bde3f Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Sat, 12 Oct 2024 17:56:46 +0200 Subject: [PATCH 03/19] refactor: guard logging onReattemptableRespons --- .../src/offline_queue/rest_offline_queue_client.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart index 007835e9..dd5886c5 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart @@ -21,8 +21,7 @@ class RestOfflineQueueClient extends http.BaseClient { /// A callback triggered when the response of a request has a status code /// which is present in the [reattemptForStatusCodes] list. - void Function(http.Request request, http.StreamedResponse response)? - onReattemptableResponse; + void Function(http.Request request, http.StreamedResponse response)? onReattemptableResponse; /// A callback triggered when a request throws an exception during execution. void Function(http.Request request, Object error)? onRequestError; @@ -105,9 +104,10 @@ class RestOfflineQueueClient extends http.BaseClient { // request was successfully sent and can be removed _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); await cacheItem.delete(db); - } else { - _logger.finest( - 'request failed, will be reattempted: ${cacheItem.toSqlite()}'); + } else if (onReattemptableResponse != null) { + _logger.finer( + 'request failed, will be reattempted: ${cacheItem.toSqlite()}', + ); onReattemptableResponse?.call(request, resp); } } From d659ee65c025026bc46c1543e2fbe931f2357343 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Sat, 12 Oct 2024 17:58:09 +0200 Subject: [PATCH 04/19] docs: extend doc comment for onRequestError callback --- .../lib/src/offline_queue/rest_offline_queue_client.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart index dd5886c5..55f5b84b 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart @@ -24,6 +24,8 @@ class RestOfflineQueueClient extends http.BaseClient { void Function(http.Request request, http.StreamedResponse response)? onReattemptableResponse; /// A callback triggered when a request throws an exception during execution. + /// + /// `SocketException`s (errors thrown due to missing connectivity) will also be forwarded to this callback. void Function(http.Request request, Object error)? onRequestError; /// If the response returned from the client is one of these error codes, the request @@ -118,7 +120,7 @@ class RestOfflineQueueClient extends http.BaseClient { onRequestError?.call(request, e); _logger.warning('#send: $e'); } finally { - // unlock the request which results in a reattempt + // unlock the request for a later a reattempt final db = await requestManager.getDb(); await cacheItem.unlock(db); } From bd2992728312176faedb0451481521757947841d Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 14 Oct 2024 10:48:55 +0200 Subject: [PATCH 05/19] run dart format --- .../lib/src/offline_first_with_supabase_repository.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index 28f3bbf1..834a1c7c 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -189,8 +189,7 @@ abstract class OfflineFirstWithSupabaseRepository 504, ], bool? serialProcessing, - void Function(http.Request request, http.StreamedResponse response)? - onReattemptableResponse, + void Function(http.Request request, http.StreamedResponse response)? onReattemptableResponse, void Function(http.Request, Object)? onRequestError, }) { final client = RestOfflineQueueClient( From f78bb9d0c42dec90679bfca7440c5c27339737c5 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 14 Oct 2024 15:00:11 +0200 Subject: [PATCH 06/19] onReattemptableResponse: return response status code instead of StreamedResponse --- .../lib/src/offline_first_with_rest_repository.dart | 2 +- .../lib/src/offline_queue/rest_offline_queue_client.dart | 4 ++-- .../lib/src/offline_first_with_supabase_repository.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index 287bb1fb..2f9d6fe1 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -34,7 +34,7 @@ abstract class OfflineFirstWithRestRepository /// which is present in the [reattemptForStatusCodes] list. /// /// Forwarded to [RestOfflineQueueClient]. - void Function(http.Request request, http.StreamedResponse response)? onReattemptableResponse; + void Function(http.Request request, int statusCode)? onReattemptableResponse; /// A callback triggered when a request throws an exception during execution. /// diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart index 55f5b84b..202d4495 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart @@ -21,7 +21,7 @@ class RestOfflineQueueClient extends http.BaseClient { /// A callback triggered when the response of a request has a status code /// which is present in the [reattemptForStatusCodes] list. - void Function(http.Request request, http.StreamedResponse response)? onReattemptableResponse; + void Function(http.Request request, int statusCode)? onReattemptableResponse; /// A callback triggered when a request throws an exception during execution. /// @@ -110,7 +110,7 @@ class RestOfflineQueueClient extends http.BaseClient { _logger.finer( 'request failed, will be reattempted: ${cacheItem.toSqlite()}', ); - onReattemptableResponse?.call(request, resp); + onReattemptableResponse?.call(request, resp.statusCode); } } diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index 834a1c7c..d4549c6c 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -189,7 +189,7 @@ abstract class OfflineFirstWithSupabaseRepository 504, ], bool? serialProcessing, - void Function(http.Request request, http.StreamedResponse response)? onReattemptableResponse, + void Function(http.Request request, int statusCode)? onReattemptableResponse, void Function(http.Request, Object)? onRequestError, }) { final client = RestOfflineQueueClient( From 69b269287b6d875f0a5f1d09836b46a6c72b23d6 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Tue, 15 Oct 2024 07:46:54 +0200 Subject: [PATCH 07/19] fix doc comments --- .../lib/src/offline_first_with_rest_repository.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index 2f9d6fe1..ed477087 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -59,10 +59,10 @@ abstract class OfflineFirstWithRestRepository /// its defaults List? reattemptForStatusCodes, - /// This property is forwarded to `RestOfflineRequestQueue`. + /// This property is forwarded to `RestOfflineQueueClient`. this.onReattemptableResponse, - /// This property is forwarded to `RestOfflineRequestQueue`. + /// This property is forwarded to `RestOfflineQueueClient`. this.onRequestError, required RestProvider restProvider, required super.sqliteProvider, From 327caad0c972b58f64402678dbec5578f7e94aa3 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Tue, 15 Oct 2024 08:07:54 +0200 Subject: [PATCH 08/19] add callbacks to GraphqlOfflineQueueLink --- .../lib/src/graphql_offline_queue_link.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart index da995143..7247e7a7 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart @@ -15,8 +15,17 @@ class GraphqlOfflineQueueLink extends Link { final GraphqlRequestSqliteCacheManager requestManager; - GraphqlOfflineQueueLink(this.requestManager) - : _logger = Logger('GraphqlOfflineQueueLink#${requestManager.databaseName}'); + /// A callback triggered when a request failed, but will be reattempted. + final void Function(Request request, int statusCode)? onReattemptableResponse; + + /// A callback triggered when a request throws an exception during execution. + final void Function(Request request, Object error)? onRequestError; + + GraphqlOfflineQueueLink( + this.requestManager, { + this.onReattemptableResponse, + this.onRequestError, + }) : _logger = Logger('GraphqlOfflineQueueLink#${requestManager.databaseName}'); @override Stream request(Request request, [NextLink? forward]) async* { @@ -33,6 +42,7 @@ class GraphqlOfflineQueueLink extends Link { yield* forward!(request).handleError( (e) async { _logger.warning('GraphqlOfflineQueueLink#request: error $e'); + onRequestError?.call(request, e); final db = await requestManager.getDb(); await cacheItem.unlock(db); }, @@ -46,6 +56,8 @@ class GraphqlOfflineQueueLink extends Link { // request was successfully sent and can be removed _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); await cacheItem.delete(db); + } else { + onReattemptableResponse?.call(request, response.response['statusCode'] as int); } final db = await requestManager.getDb(); await cacheItem.unlock(db); From 3dce803e038030ae36ed86f5da5483c86b22d5dc Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Mon, 28 Oct 2024 09:15:14 -0700 Subject: [PATCH 09/19] testing editing From d0e1783a4970affa757fc1fc649b412f15d8681a Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Mon, 28 Oct 2024 10:14:25 -0700 Subject: [PATCH 10/19] organize --- .../offline_first_with_graphql_repository.md | 7 ++++++- packages/brick_offline_first_with_graphql/README.md | 13 +++++++++---- .../lib/src/graphql_offline_queue_link.dart | 5 +++-- .../lib/src/offline_first_with_rest_repository.dart | 4 ++-- .../offline_queue/rest_offline_queue_client.dart | 6 +++--- .../src/offline_first_with_supabase_repository.dart | 2 +- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/offline_first/offline_first_with_graphql_repository.md b/docs/offline_first/offline_first_with_graphql_repository.md index 1742d31a..d2183176 100644 --- a/docs/offline_first/offline_first_with_graphql_repository.md +++ b/docs/offline_first/offline_first_with_graphql_repository.md @@ -17,7 +17,12 @@ To cache outbound requests, apply `GraphqlOfflineQueueLink` in your GraphqlProvi ```dart GraphqlProvider( link: Link.from([ - GraphqlOfflineQueueLink(GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite')), + GraphqlOfflineQueueLink( + GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite'), + // Optionally specify callbacks for queue retries and errors + onReattemptableResponse: onReattemptableResponse, + onRequestError: onRequestError, + ), HttpLink(endpoint) ]), ); diff --git a/packages/brick_offline_first_with_graphql/README.md b/packages/brick_offline_first_with_graphql/README.md index 8b5cd831..e2e2edd8 100644 --- a/packages/brick_offline_first_with_graphql/README.md +++ b/packages/brick_offline_first_with_graphql/README.md @@ -11,7 +11,12 @@ To cache outbound requests, apply `GraphqlOfflineQueueLink` in your GraphqlProvi ```dart GraphqlProvider( link: Link.from([ - GraphqlOfflineQueueLink(GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite')), + GraphqlOfflineQueueLink( + GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite'), + // Optionally specify callbacks for queue retries and errors + onReattemptableResponse: onReattemptableResponse, + onRequestError: onRequestError, + ), HttpLink(endpoint) ]), ); @@ -43,9 +48,9 @@ Due to [an open analyzer bug](https://github.com/dart-lang/sdk/issues/38309), a ### Field Types -* Any unsupported field types from `GraphqlProvider` or `SqliteProvider` -* Future iterables of future models (i.e. `Future>>`. +- Any unsupported field types from `GraphqlProvider` or `SqliteProvider` +- Future iterables of future models (i.e. `Future>>`. ### Configuration -* `@OfflineFirst(where:` only supports extremely simple renames. Multiple `where` keys (`OfflineFirst(where: {'id': 'data["id"]', 'otherVar': 'data["otherVar"]'})`) or nested properties (`OfflineFirst(where: {'id': 'data["subfield"]["id"]})`) will be ignored. Be sure to use `@Graphql(name:)` to rename the generated document field. +- `@OfflineFirst(where:` only supports extremely simple renames. Multiple `where` keys (`OfflineFirst(where: {'id': 'data["id"]', 'otherVar': 'data["otherVar"]'})`) or nested properties (`OfflineFirst(where: {'id': 'data["subfield"]["id"]})`) will be ignored. Be sure to use `@Graphql(name:)` to rename the generated document field. diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart index 7247e7a7..e392ee23 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart @@ -56,8 +56,9 @@ class GraphqlOfflineQueueLink extends Link { // request was successfully sent and can be removed _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); await cacheItem.delete(db); - } else { - onReattemptableResponse?.call(request, response.response['statusCode'] as int); + } else if (response.response.containsKey('statusCode') && + response.response['statusCode'] is int) { + onReattemptableResponse?.call(request, response.response['statusCode']); } final db = await requestManager.getDb(); await cacheItem.unlock(db); diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index ed477087..11777ab5 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -73,9 +73,9 @@ abstract class OfflineFirstWithRestRepository remoteProvider.client = RestOfflineQueueClient( restProvider.client, offlineQueueManager, - reattemptForStatusCodes: reattemptForStatusCodes, - onRequestError: onRequestError, onReattemptableResponse: onReattemptableResponse, + onRequestError: onRequestError, + reattemptForStatusCodes: reattemptForStatusCodes, ); offlineRequestQueue = RestOfflineRequestQueue( client: remoteProvider.client as RestOfflineQueueClient, diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart index 202d4495..6c9ecee4 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart @@ -17,8 +17,6 @@ class RestOfflineQueueClient extends http.BaseClient { /// as detailed by [the Dart team](https://github.com/dart-lang/http/blob/378179845420caafbf7a34d47b9c22104753182a/README.md#using) final http.Client _inner; - final RequestSqliteCacheManager requestManager; - /// A callback triggered when the response of a request has a status code /// which is present in the [reattemptForStatusCodes] list. void Function(http.Request request, int statusCode)? onReattemptableResponse; @@ -28,6 +26,8 @@ class RestOfflineQueueClient extends http.BaseClient { /// `SocketException`s (errors thrown due to missing connectivity) will also be forwarded to this callback. void Function(http.Request request, Object error)? onRequestError; + final RequestSqliteCacheManager requestManager; + /// If the response returned from the client is one of these error codes, the request /// **will not** be removed from the queue. For example, if the result of a request produces a /// 404 status code response (such as in a Tunnel not found exception), the request will @@ -107,7 +107,7 @@ class RestOfflineQueueClient extends http.BaseClient { _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); await cacheItem.delete(db); } else if (onReattemptableResponse != null) { - _logger.finer( + _logger.finest( 'request failed, will be reattempted: ${cacheItem.toSqlite()}', ); onReattemptableResponse?.call(request, resp.statusCode); diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index d4549c6c..2d89a0fd 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -201,9 +201,9 @@ abstract class OfflineFirstWithSupabaseRepository serialProcessing: serialProcessing, ), ignorePaths: ignorePaths, - reattemptForStatusCodes: reattemptForStatusCodes, onReattemptableResponse: onReattemptableResponse, onRequestError: onRequestError, + reattemptForStatusCodes: reattemptForStatusCodes, ); return (client, RestOfflineRequestQueue(client: client)); } From c2a2453f76f1cce63161f861d50d8bac83a2c2cd Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Wed, 30 Oct 2024 18:35:57 +0100 Subject: [PATCH 11/19] add documentation --- docs/offline_first/offline_queue.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/offline_first/offline_queue.md b/docs/offline_first/offline_queue.md index b73d563b..1789a876 100644 --- a/docs/offline_first/offline_queue.md +++ b/docs/offline_first/offline_queue.md @@ -18,3 +18,12 @@ final link = GraphqlOfflineQueueLink( ![OfflineQueue logic flow](https://user-images.githubusercontent.com/865897/72175823-f44a3580-3391-11ea-8961-bbeccd74fe7b.jpg) !> The queue ignores requests that are not `DELETE`, `PATCH`, `POST`, and `PUT` for REST. In GraphQL, `query` and `subscription` operations are ignored. Fetching requests are not worth tracking as the caller may have been disposed by the time the app regains connectivity. + +## Queue Processing Callbacks + +For tracking the status of queued request processing, the constructor of `GraphqlOfflineQueueLink` and `RestOfflineQueueClient` accept callback functions, which are triggered for specific events when processing queued requests: + +- `onRequestError`: Invoked when a request encounters an exception during execution (e.g. a `SocketException`). The function receives both the request and an object containing the thrown exception. +- `onReattemptableResponse`: Invoked when a request results in a response with a status code listed in the `reattemptForStatusCodes` list. The function receives the request and the response HTTP status code. + +These callbacks provide practical ways to detect the state of the queue (e.g. if the client is offline, the queue is being processed or being blocked in case of serial processing). For more details, refer to the [discussion on this topic](https://github.com/GetDutchie/brick/issues/393). From 2ab4972b368be6f0b74e529464738585802ae51f Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Wed, 30 Oct 2024 19:13:21 +0100 Subject: [PATCH 12/19] add tests for RestOfflineQueueClient callbacks --- .../rest_offline_queue_client_test.dart | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart index d1dfec04..0abe803f 100644 --- a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart +++ b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:brick_offline_first_with_rest/src/offline_queue/rest_offline_queue_client.dart'; import 'package:brick_offline_first_with_rest/src/offline_queue/rest_request_sqlite_cache_manager.dart'; +import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:test/test.dart'; @@ -187,6 +190,94 @@ void main() { await client.patch(Uri.parse('http://0.0.0.0:3000'), body: 'new record'); expect(await requestManager.unprocessedRequests(), hasLength(4)); }); + + test('onReattemptableResponse callback is triggered for reattemptable status code', + () async { + http.Request? capturedRequest; + int? capturedStatusCode; + + final inner = stubResult(statusCode: 429); + final client = RestOfflineQueueClient( + inner, + requestManager, + reattemptForStatusCodes: [429], + onReattemptableResponse: (request, statusCode) { + capturedRequest = request; + capturedStatusCode = statusCode; + }, + ); + + final uri = Uri.parse('http://0.0.0.0:3000'); + + await client.post(uri, body: 'test'); + + expect(capturedRequest?.method, equals('POST')); + expect(capturedRequest?.url, equals(uri)); + expect(capturedStatusCode, equals(429)); + }); + + test('onReattemptableResponse is not triggered for non-reattemptable status code', + () async { + bool callbackTriggered = false; + + final inner = stubResult(statusCode: 404); + final client = RestOfflineQueueClient( + inner, + requestManager, + reattemptForStatusCodes: [429], + onReattemptableResponse: (request, statusCode) { + callbackTriggered = true; + }, + ); + + await client.post(Uri.parse('http://0.0.0.0:3000'), body: 'test'); + + expect(callbackTriggered, isFalse); + }); + + test('onRequestError callback is triggered for SocketException', () async { + http.Request? capturedRequest; + Object? capturedError; + + final inner = MockClient((req) async { + throw SocketException('test error'); + }); + + final client = RestOfflineQueueClient( + inner, + requestManager, + onRequestError: (request, error) { + capturedRequest = request; + capturedError = error; + }, + ); + + final uri = Uri.parse('http://0.0.0.0:3000'); + + await client.post(uri, body: 'test'); + + expect(capturedRequest?.method, equals('POST')); + expect(capturedRequest?.url, equals(uri)); + expect(capturedError, isA()); + expect((capturedError as SocketException).message, equals('test error')); + }); + + test('onRequestError is not triggered for successful request', () async { + bool callbackTriggered = false; + + final inner = stubResult(statusCode: 200); + final client = RestOfflineQueueClient( + inner, + requestManager, + onRequestError: (request, error) { + callbackTriggered = true; + }, + ); + + await client.post(Uri.parse('http://0.0.0.0:3000'), body: 'test'); + + expect(callbackTriggered, isFalse); + }); }); }); From cb6b40e696ce0130e805c72bf01d69bbeb3fef23 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Wed, 30 Oct 2024 18:00:23 -0700 Subject: [PATCH 13/19] add graphql tests --- .../lib/src/graphql_offline_queue_link.dart | 9 +- .../test/graphql_offline_queue_link_test.dart | 108 ++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart index e392ee23..b0f5f4d7 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart @@ -16,7 +16,7 @@ class GraphqlOfflineQueueLink extends Link { final GraphqlRequestSqliteCacheManager requestManager; /// A callback triggered when a request failed, but will be reattempted. - final void Function(Request request, int statusCode)? onReattemptableResponse; + final void Function(Request request)? onReattemptableResponse; /// A callback triggered when a request throws an exception during execution. final void Function(Request request, Object error)? onRequestError; @@ -56,10 +56,11 @@ class GraphqlOfflineQueueLink extends Link { // request was successfully sent and can be removed _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); await cacheItem.delete(db); - } else if (response.response.containsKey('statusCode') && - response.response['statusCode'] is int) { - onReattemptableResponse?.call(request, response.response['statusCode']); + } else if (response.errors != null) { + onRequestError?.call(request, response.errors!); } + + onReattemptableResponse?.call(request); final db = await requestManager.getDb(); await cacheItem.unlock(db); diff --git a/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart b/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart index 240e2bbc..7036a722 100644 --- a/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart +++ b/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart @@ -152,6 +152,114 @@ void main() { }); }); + group('#onReattemptableResponse', () { + test('callback is triggered when request retries', () async { + Request? capturedRequest; + + final mockLink = stubGraphqlLink({}, errors: ['Test failure']); + final client = GraphqlOfflineQueueLink( + requestManager, + onReattemptableResponse: (r) => capturedRequest = r, + ).concat(mockLink); + + final mutationRequest = Request( + operation: Operation( + document: parseString('''mutation {}'''), + operationName: 'fakeMutate', + ), + ); + await client.request(mutationRequest).first; + + expect(capturedRequest, isNotNull); + expect(capturedRequest, mutationRequest); + }); + + test('callback is not triggered when request succeeds', () async { + Request? capturedRequest; + + final mockLink = MockLink(); + final client = GraphqlOfflineQueueLink( + requestManager, + onReattemptableResponse: (r) => capturedRequest = r, + ).concat(mockLink); + + when( + mockLink.request(request), + ).thenAnswer( + (_) => Stream.fromIterable([response]), + ); + + client.request( + Request( + operation: Operation( + document: parseString('''mutation {}'''), + ), + ), + ); + + expect(capturedRequest, isNull); + }); + }); + + group('#onRequestError', () { + test('callback is triggered for a failed response', () async { + Request? capturedRequest; + Object? capturedError; + + final mockLink = stubGraphqlLink({}, errors: ['Test failure']); + final client = GraphqlOfflineQueueLink( + requestManager, + onRequestError: (r, err) { + capturedRequest = r; + capturedError = err; + }, + ).concat(mockLink); + + final mutationRequest = Request( + operation: Operation( + document: parseString('''mutation {}'''), + operationName: 'fakeMutate', + ), + ); + await client.request(mutationRequest).first; + + expect(capturedRequest, isNotNull); + expect(capturedError, isNotNull); + expect(capturedError.toString(), contains('Test failure')); + }); + + test('callback is not triggered on successful response', () async { + Request? capturedRequest; + Object? capturedError; + + final mockLink = MockLink(); + final client = GraphqlOfflineQueueLink( + requestManager, + onRequestError: (r, err) { + capturedRequest = r; + capturedError = err; + }, + ).concat(mockLink); + + when( + mockLink.request(request), + ).thenAnswer( + (_) => Stream.fromIterable([response]), + ); + + client.request( + Request( + operation: Operation( + document: parseString('''mutation {}'''), + ), + ), + ); + + expect(capturedRequest, isNull); + expect(capturedError, isNull); + }); + }); + test('request deletes after a successful response', () async { final mockLink = MockLink(); final client = GraphqlOfflineQueueLink(requestManager).concat(mockLink); From 52fb9c9378513750c1164b2304b23c8ff2858662 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Wed, 30 Oct 2024 18:05:05 -0700 Subject: [PATCH 14/19] minor doc grammar --- docs/offline_first/offline_queue.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/offline_first/offline_queue.md b/docs/offline_first/offline_queue.md index 1789a876..4dd0a85b 100644 --- a/docs/offline_first/offline_queue.md +++ b/docs/offline_first/offline_queue.md @@ -21,9 +21,9 @@ final link = GraphqlOfflineQueueLink( ## Queue Processing Callbacks -For tracking the status of queued request processing, the constructor of `GraphqlOfflineQueueLink` and `RestOfflineQueueClient` accept callback functions, which are triggered for specific events when processing queued requests: +For tracking the status of queued requests, the `GraphqlOfflineQueueLink` and `RestOfflineQueueClient` constructors accept callback functions which are triggered for specific events: - `onRequestError`: Invoked when a request encounters an exception during execution (e.g. a `SocketException`). The function receives both the request and an object containing the thrown exception. -- `onReattemptableResponse`: Invoked when a request results in a response with a status code listed in the `reattemptForStatusCodes` list. The function receives the request and the response HTTP status code. +- `onReattemptableResponse`: Invoked when a request will be retried. For REST, this is when the response has a status code listed in the `reattemptForStatusCodes` list. -These callbacks provide practical ways to detect the state of the queue (e.g. if the client is offline, the queue is being processed or being blocked in case of serial processing). For more details, refer to the [discussion on this topic](https://github.com/GetDutchie/brick/issues/393). +These callbacks provide practical ways to detect the state of the queue (e.g. if the client is offline, the queue is being processed, or the queue is blocked during serial processing). For more, refer to the [discussion on this topic](https://github.com/GetDutchie/brick/issues/393). From 333027048ff8916de22ae30157a25e810597542b Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Wed, 30 Oct 2024 18:09:49 -0700 Subject: [PATCH 15/19] prepare yaml version releases --- packages/brick_offline_first_with_graphql/CHANGELOG.md | 2 ++ packages/brick_offline_first_with_graphql/pubspec.yaml | 2 +- packages/brick_offline_first_with_rest/CHANGELOG.md | 2 ++ packages/brick_offline_first_with_rest/pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/brick_offline_first_with_graphql/CHANGELOG.md b/packages/brick_offline_first_with_graphql/CHANGELOG.md index 4c2dee92..ba4ef847 100644 --- a/packages/brick_offline_first_with_graphql/CHANGELOG.md +++ b/packages/brick_offline_first_with_graphql/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased +## 3.2.0 + ## 3.1.1 - Loosen constraints for `gql`, `gql_exec`, and `gql_link` diff --git a/packages/brick_offline_first_with_graphql/pubspec.yaml b/packages/brick_offline_first_with_graphql/pubspec.yaml index c979fe3d..d027fa3a 100644 --- a/packages/brick_offline_first_with_graphql/pubspec.yaml +++ b/packages/brick_offline_first_with_graphql/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.1.1 +version: 3.2.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/brick_offline_first_with_rest/CHANGELOG.md b/packages/brick_offline_first_with_rest/CHANGELOG.md index cda396f5..b7af756c 100644 --- a/packages/brick_offline_first_with_rest/CHANGELOG.md +++ b/packages/brick_offline_first_with_rest/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased +## 3.2.0 + ## 3.1.0 - Expose offline queue functionality in `offline_queue.dart` diff --git a/packages/brick_offline_first_with_rest/pubspec.yaml b/packages/brick_offline_first_with_rest/pubspec.yaml index 5a65d63a..ea3c9f47 100644 --- a/packages/brick_offline_first_with_rest/pubspec.yaml +++ b/packages/brick_offline_first_with_rest/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.1.0 +version: 3.1.1 environment: sdk: ">=2.18.0 <4.0.0" From 78aac5bbe69b08337be8ddd0a1e8a102cb3439ae Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Thu, 31 Oct 2024 07:10:35 +0100 Subject: [PATCH 16/19] rename onRequestError callback to onRequestException --- .../offline_first_with_graphql_repository.md | 2 +- docs/offline_first/offline_queue.md | 2 +- .../README.md | 2 +- .../lib/src/graphql_offline_queue_link.dart | 8 +++---- .../test/graphql_offline_queue_link_test.dart | 24 +++++++++---------- .../offline_first_with_rest_repository.dart | 6 ++--- .../rest_offline_queue_client.dart | 6 ++--- .../rest_offline_queue_client_test.dart | 16 ++++++------- ...ffline_first_with_supabase_repository.dart | 4 ++-- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/offline_first/offline_first_with_graphql_repository.md b/docs/offline_first/offline_first_with_graphql_repository.md index d2183176..bfbb53a8 100644 --- a/docs/offline_first/offline_first_with_graphql_repository.md +++ b/docs/offline_first/offline_first_with_graphql_repository.md @@ -21,7 +21,7 @@ GraphqlProvider( GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite'), // Optionally specify callbacks for queue retries and errors onReattemptableResponse: onReattemptableResponse, - onRequestError: onRequestError, + onRequestException: onRequestException, ), HttpLink(endpoint) ]), diff --git a/docs/offline_first/offline_queue.md b/docs/offline_first/offline_queue.md index 4dd0a85b..78f63ece 100644 --- a/docs/offline_first/offline_queue.md +++ b/docs/offline_first/offline_queue.md @@ -23,7 +23,7 @@ final link = GraphqlOfflineQueueLink( For tracking the status of queued requests, the `GraphqlOfflineQueueLink` and `RestOfflineQueueClient` constructors accept callback functions which are triggered for specific events: -- `onRequestError`: Invoked when a request encounters an exception during execution (e.g. a `SocketException`). The function receives both the request and an object containing the thrown exception. +- `onRequestException`: Invoked when a request encounters an exception during execution (e.g. a `SocketException`). The function receives both the request and an object containing the thrown exception. - `onReattemptableResponse`: Invoked when a request will be retried. For REST, this is when the response has a status code listed in the `reattemptForStatusCodes` list. These callbacks provide practical ways to detect the state of the queue (e.g. if the client is offline, the queue is being processed, or the queue is blocked during serial processing). For more, refer to the [discussion on this topic](https://github.com/GetDutchie/brick/issues/393). diff --git a/packages/brick_offline_first_with_graphql/README.md b/packages/brick_offline_first_with_graphql/README.md index e2e2edd8..29861e16 100644 --- a/packages/brick_offline_first_with_graphql/README.md +++ b/packages/brick_offline_first_with_graphql/README.md @@ -15,7 +15,7 @@ GraphqlProvider( GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite'), // Optionally specify callbacks for queue retries and errors onReattemptableResponse: onReattemptableResponse, - onRequestError: onRequestError, + onRequestException: onRequestException, ), HttpLink(endpoint) ]), diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart index b0f5f4d7..507d2e76 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart @@ -19,12 +19,12 @@ class GraphqlOfflineQueueLink extends Link { final void Function(Request request)? onReattemptableResponse; /// A callback triggered when a request throws an exception during execution. - final void Function(Request request, Object error)? onRequestError; + final void Function(Request request, Object error)? onRequestException; GraphqlOfflineQueueLink( this.requestManager, { this.onReattemptableResponse, - this.onRequestError, + this.onRequestException, }) : _logger = Logger('GraphqlOfflineQueueLink#${requestManager.databaseName}'); @override @@ -42,7 +42,7 @@ class GraphqlOfflineQueueLink extends Link { yield* forward!(request).handleError( (e) async { _logger.warning('GraphqlOfflineQueueLink#request: error $e'); - onRequestError?.call(request, e); + onRequestException?.call(request, e); final db = await requestManager.getDb(); await cacheItem.unlock(db); }, @@ -57,7 +57,7 @@ class GraphqlOfflineQueueLink extends Link { _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); await cacheItem.delete(db); } else if (response.errors != null) { - onRequestError?.call(request, response.errors!); + onRequestException?.call(request, response.errors!); } onReattemptableResponse?.call(request); diff --git a/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart b/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart index 7036a722..4a8f59e9 100644 --- a/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart +++ b/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart @@ -201,17 +201,17 @@ void main() { }); }); - group('#onRequestError', () { + group('#onRequestException', () { test('callback is triggered for a failed response', () async { Request? capturedRequest; - Object? capturedError; + Object? capturedonException; final mockLink = stubGraphqlLink({}, errors: ['Test failure']); final client = GraphqlOfflineQueueLink( requestManager, - onRequestError: (r, err) { - capturedRequest = r; - capturedError = err; + onRequestException: (request, exception) { + capturedRequest = request; + capturedonException = exception; }, ).concat(mockLink); @@ -224,20 +224,20 @@ void main() { await client.request(mutationRequest).first; expect(capturedRequest, isNotNull); - expect(capturedError, isNotNull); - expect(capturedError.toString(), contains('Test failure')); + expect(capturedonException, isNotNull); + expect(capturedonException.toString(), contains('Test failure')); }); test('callback is not triggered on successful response', () async { Request? capturedRequest; - Object? capturedError; + Object? capturedException; final mockLink = MockLink(); final client = GraphqlOfflineQueueLink( requestManager, - onRequestError: (r, err) { - capturedRequest = r; - capturedError = err; + onRequestException: (request, exception) { + capturedRequest = request; + capturedException = exception; }, ).concat(mockLink); @@ -256,7 +256,7 @@ void main() { ); expect(capturedRequest, isNull); - expect(capturedError, isNull); + expect(capturedException, isNull); }); }); diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index 11777ab5..c2bea96a 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -39,7 +39,7 @@ abstract class OfflineFirstWithRestRepository /// A callback triggered when a request throws an exception during execution. /// /// Forwarded to [RestOfflineQueueClient]. - void Function(http.Request, Object)? onRequestError; + void Function(http.Request, Object)? onRequestException; OfflineFirstWithRestRepository({ super.autoHydrate, @@ -63,7 +63,7 @@ abstract class OfflineFirstWithRestRepository this.onReattemptableResponse, /// This property is forwarded to `RestOfflineQueueClient`. - this.onRequestError, + this.onRequestException, required RestProvider restProvider, required super.sqliteProvider, }) : remoteProvider = restProvider, @@ -74,7 +74,7 @@ abstract class OfflineFirstWithRestRepository restProvider.client, offlineQueueManager, onReattemptableResponse: onReattemptableResponse, - onRequestError: onRequestError, + onRequestException: onRequestException, reattemptForStatusCodes: reattemptForStatusCodes, ); offlineRequestQueue = RestOfflineRequestQueue( diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart index 6c9ecee4..732e4d8b 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart @@ -24,7 +24,7 @@ class RestOfflineQueueClient extends http.BaseClient { /// A callback triggered when a request throws an exception during execution. /// /// `SocketException`s (errors thrown due to missing connectivity) will also be forwarded to this callback. - void Function(http.Request request, Object error)? onRequestError; + void Function(http.Request request, Object error)? onRequestException; final RequestSqliteCacheManager requestManager; @@ -47,7 +47,7 @@ class RestOfflineQueueClient extends http.BaseClient { this._inner, this.requestManager, { this.onReattemptableResponse, - this.onRequestError, + this.onRequestException, List? reattemptForStatusCodes, /// Any request URI that begins with one of these paths will not be @@ -117,7 +117,7 @@ class RestOfflineQueueClient extends http.BaseClient { return resp; } catch (e) { // e.g. SocketExceptions will be caught here - onRequestError?.call(request, e); + onRequestException?.call(request, e); _logger.warning('#send: $e'); } finally { // unlock the request for a later a reattempt diff --git a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart index 0abe803f..1fe664f4 100644 --- a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart +++ b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart @@ -235,9 +235,9 @@ void main() { expect(callbackTriggered, isFalse); }); - test('onRequestError callback is triggered for SocketException', () async { + test('onRequestException callback is triggered for SocketException', () async { http.Request? capturedRequest; - Object? capturedError; + Object? capturedException; final inner = MockClient((req) async { throw SocketException('test error'); @@ -246,9 +246,9 @@ void main() { final client = RestOfflineQueueClient( inner, requestManager, - onRequestError: (request, error) { + onRequestException: (request, exception) { capturedRequest = request; - capturedError = error; + capturedException = exception; }, ); @@ -258,18 +258,18 @@ void main() { expect(capturedRequest?.method, equals('POST')); expect(capturedRequest?.url, equals(uri)); - expect(capturedError, isA()); - expect((capturedError as SocketException).message, equals('test error')); + expect(capturedException, isA()); + expect((capturedException as SocketException).message, equals('test error')); }); - test('onRequestError is not triggered for successful request', () async { + test('onRequestException is not triggered for successful request', () async { bool callbackTriggered = false; final inner = stubResult(statusCode: 200); final client = RestOfflineQueueClient( inner, requestManager, - onRequestError: (request, error) { + onRequestException: (request, exception) { callbackTriggered = true; }, ); diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index 2d89a0fd..9213f54d 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -190,7 +190,7 @@ abstract class OfflineFirstWithSupabaseRepository ], bool? serialProcessing, void Function(http.Request request, int statusCode)? onReattemptableResponse, - void Function(http.Request, Object)? onRequestError, + void Function(http.Request, Object)? onRequestException, }) { final client = RestOfflineQueueClient( innerClient ?? http.Client(), @@ -202,7 +202,7 @@ abstract class OfflineFirstWithSupabaseRepository ), ignorePaths: ignorePaths, onReattemptableResponse: onReattemptableResponse, - onRequestError: onRequestError, + onRequestException: onRequestException, reattemptForStatusCodes: reattemptForStatusCodes, ); return (client, RestOfflineRequestQueue(client: client)); From 12b9ff8c6ba55d8d7bdb8d7a6f28f728feb5f067 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Thu, 31 Oct 2024 07:12:31 +0100 Subject: [PATCH 17/19] rename onReattemptableResponse callback to onReattempt --- .../offline_first_with_graphql_repository.md | 2 +- docs/offline_first/offline_queue.md | 2 +- packages/brick_offline_first_with_graphql/README.md | 2 +- .../lib/src/graphql_offline_queue_link.dart | 6 +++--- .../test/graphql_offline_queue_link_test.dart | 6 +++--- .../lib/src/offline_first_with_rest_repository.dart | 6 +++--- .../src/offline_queue/rest_offline_queue_client.dart | 8 ++++---- .../offline_queue/rest_offline_queue_client_test.dart | 10 ++++------ .../src/offline_first_with_supabase_repository.dart | 4 ++-- 9 files changed, 22 insertions(+), 24 deletions(-) diff --git a/docs/offline_first/offline_first_with_graphql_repository.md b/docs/offline_first/offline_first_with_graphql_repository.md index bfbb53a8..36b6e523 100644 --- a/docs/offline_first/offline_first_with_graphql_repository.md +++ b/docs/offline_first/offline_first_with_graphql_repository.md @@ -20,7 +20,7 @@ GraphqlProvider( GraphqlOfflineQueueLink( GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite'), // Optionally specify callbacks for queue retries and errors - onReattemptableResponse: onReattemptableResponse, + onReattempt: onReattempt, onRequestException: onRequestException, ), HttpLink(endpoint) diff --git a/docs/offline_first/offline_queue.md b/docs/offline_first/offline_queue.md index 78f63ece..15decaba 100644 --- a/docs/offline_first/offline_queue.md +++ b/docs/offline_first/offline_queue.md @@ -24,6 +24,6 @@ final link = GraphqlOfflineQueueLink( For tracking the status of queued requests, the `GraphqlOfflineQueueLink` and `RestOfflineQueueClient` constructors accept callback functions which are triggered for specific events: - `onRequestException`: Invoked when a request encounters an exception during execution (e.g. a `SocketException`). The function receives both the request and an object containing the thrown exception. -- `onReattemptableResponse`: Invoked when a request will be retried. For REST, this is when the response has a status code listed in the `reattemptForStatusCodes` list. +- `onReattempt`: Invoked when a request will be retried. For REST, this is when the response has a status code listed in the `reattemptForStatusCodes` list. These callbacks provide practical ways to detect the state of the queue (e.g. if the client is offline, the queue is being processed, or the queue is blocked during serial processing). For more, refer to the [discussion on this topic](https://github.com/GetDutchie/brick/issues/393). diff --git a/packages/brick_offline_first_with_graphql/README.md b/packages/brick_offline_first_with_graphql/README.md index 29861e16..91392cec 100644 --- a/packages/brick_offline_first_with_graphql/README.md +++ b/packages/brick_offline_first_with_graphql/README.md @@ -14,7 +14,7 @@ GraphqlProvider( GraphqlOfflineQueueLink( GraphqlRequestSqliteCacheManager('myAppRequestQueue.sqlite'), // Optionally specify callbacks for queue retries and errors - onReattemptableResponse: onReattemptableResponse, + onReattempt: onReattempt, onRequestException: onRequestException, ), HttpLink(endpoint) diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart index 507d2e76..f113e40e 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart @@ -16,14 +16,14 @@ class GraphqlOfflineQueueLink extends Link { final GraphqlRequestSqliteCacheManager requestManager; /// A callback triggered when a request failed, but will be reattempted. - final void Function(Request request)? onReattemptableResponse; + final void Function(Request request)? onReattempt; /// A callback triggered when a request throws an exception during execution. final void Function(Request request, Object error)? onRequestException; GraphqlOfflineQueueLink( this.requestManager, { - this.onReattemptableResponse, + this.onReattempt, this.onRequestException, }) : _logger = Logger('GraphqlOfflineQueueLink#${requestManager.databaseName}'); @@ -60,7 +60,7 @@ class GraphqlOfflineQueueLink extends Link { onRequestException?.call(request, response.errors!); } - onReattemptableResponse?.call(request); + onReattempt?.call(request); final db = await requestManager.getDb(); await cacheItem.unlock(db); diff --git a/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart b/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart index 4a8f59e9..0c88fde3 100644 --- a/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart +++ b/packages/brick_offline_first_with_graphql/test/graphql_offline_queue_link_test.dart @@ -152,14 +152,14 @@ void main() { }); }); - group('#onReattemptableResponse', () { + group('#onReattempt', () { test('callback is triggered when request retries', () async { Request? capturedRequest; final mockLink = stubGraphqlLink({}, errors: ['Test failure']); final client = GraphqlOfflineQueueLink( requestManager, - onReattemptableResponse: (r) => capturedRequest = r, + onReattempt: (r) => capturedRequest = r, ).concat(mockLink); final mutationRequest = Request( @@ -180,7 +180,7 @@ void main() { final mockLink = MockLink(); final client = GraphqlOfflineQueueLink( requestManager, - onReattemptableResponse: (r) => capturedRequest = r, + onReattempt: (r) => capturedRequest = r, ).concat(mockLink); when( diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index c2bea96a..ace2d462 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -34,7 +34,7 @@ abstract class OfflineFirstWithRestRepository /// which is present in the [reattemptForStatusCodes] list. /// /// Forwarded to [RestOfflineQueueClient]. - void Function(http.Request request, int statusCode)? onReattemptableResponse; + void Function(http.Request request, int statusCode)? onReattempt; /// A callback triggered when a request throws an exception during execution. /// @@ -60,7 +60,7 @@ abstract class OfflineFirstWithRestRepository List? reattemptForStatusCodes, /// This property is forwarded to `RestOfflineQueueClient`. - this.onReattemptableResponse, + this.onReattempt, /// This property is forwarded to `RestOfflineQueueClient`. this.onRequestException, @@ -73,7 +73,7 @@ abstract class OfflineFirstWithRestRepository remoteProvider.client = RestOfflineQueueClient( restProvider.client, offlineQueueManager, - onReattemptableResponse: onReattemptableResponse, + onReattempt: onReattempt, onRequestException: onRequestException, reattemptForStatusCodes: reattemptForStatusCodes, ); diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart index 732e4d8b..e1c61b12 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_queue_client.dart @@ -19,7 +19,7 @@ class RestOfflineQueueClient extends http.BaseClient { /// A callback triggered when the response of a request has a status code /// which is present in the [reattemptForStatusCodes] list. - void Function(http.Request request, int statusCode)? onReattemptableResponse; + void Function(http.Request request, int statusCode)? onReattempt; /// A callback triggered when a request throws an exception during execution. /// @@ -46,7 +46,7 @@ class RestOfflineQueueClient extends http.BaseClient { RestOfflineQueueClient( this._inner, this.requestManager, { - this.onReattemptableResponse, + this.onReattempt, this.onRequestException, List? reattemptForStatusCodes, @@ -106,11 +106,11 @@ class RestOfflineQueueClient extends http.BaseClient { // request was successfully sent and can be removed _logger.finest('removing from queue: ${cacheItem.toSqlite()}'); await cacheItem.delete(db); - } else if (onReattemptableResponse != null) { + } else if (onReattempt != null) { _logger.finest( 'request failed, will be reattempted: ${cacheItem.toSqlite()}', ); - onReattemptableResponse?.call(request, resp.statusCode); + onReattempt?.call(request, resp.statusCode); } } diff --git a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart index 1fe664f4..1c99791d 100644 --- a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart +++ b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart @@ -191,8 +191,7 @@ void main() { expect(await requestManager.unprocessedRequests(), hasLength(4)); }); - test('onReattemptableResponse callback is triggered for reattemptable status code', - () async { + test('onReattempt callback is triggered for reattemptable status code', () async { http.Request? capturedRequest; int? capturedStatusCode; @@ -201,7 +200,7 @@ void main() { inner, requestManager, reattemptForStatusCodes: [429], - onReattemptableResponse: (request, statusCode) { + onReattempt: (request, statusCode) { capturedRequest = request; capturedStatusCode = statusCode; }, @@ -216,8 +215,7 @@ void main() { expect(capturedStatusCode, equals(429)); }); - test('onReattemptableResponse is not triggered for non-reattemptable status code', - () async { + test('onReattempt is not triggered for non-reattemptable status code', () async { bool callbackTriggered = false; final inner = stubResult(statusCode: 404); @@ -225,7 +223,7 @@ void main() { inner, requestManager, reattemptForStatusCodes: [429], - onReattemptableResponse: (request, statusCode) { + onReattempt: (request, statusCode) { callbackTriggered = true; }, ); diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index 9213f54d..543181e6 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -189,7 +189,7 @@ abstract class OfflineFirstWithSupabaseRepository 504, ], bool? serialProcessing, - void Function(http.Request request, int statusCode)? onReattemptableResponse, + void Function(http.Request request, int statusCode)? onReattempt, void Function(http.Request, Object)? onRequestException, }) { final client = RestOfflineQueueClient( @@ -201,7 +201,7 @@ abstract class OfflineFirstWithSupabaseRepository serialProcessing: serialProcessing, ), ignorePaths: ignorePaths, - onReattemptableResponse: onReattemptableResponse, + onReattempt: onReattempt, onRequestException: onRequestException, reattemptForStatusCodes: reattemptForStatusCodes, ); From 46feebd58d396240cf2bbf3b04ebea18eca48c1b Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Thu, 31 Oct 2024 07:17:12 +0100 Subject: [PATCH 18/19] add changelog --- packages/brick_offline_first_with_graphql/CHANGELOG.md | 3 +++ packages/brick_offline_first_with_rest/CHANGELOG.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/brick_offline_first_with_graphql/CHANGELOG.md b/packages/brick_offline_first_with_graphql/CHANGELOG.md index ba4ef847..d4eab15e 100644 --- a/packages/brick_offline_first_with_graphql/CHANGELOG.md +++ b/packages/brick_offline_first_with_graphql/CHANGELOG.md @@ -2,6 +2,9 @@ ## 3.2.0 +- Add optional `onRequestException` callback function to `GraphqlOfflineQueueLink` +- Add optional `onReattempt` callback function to `RestOfflineQueueClient` + ## 3.1.1 - Loosen constraints for `gql`, `gql_exec`, and `gql_link` diff --git a/packages/brick_offline_first_with_rest/CHANGELOG.md b/packages/brick_offline_first_with_rest/CHANGELOG.md index b7af756c..b716c7d4 100644 --- a/packages/brick_offline_first_with_rest/CHANGELOG.md +++ b/packages/brick_offline_first_with_rest/CHANGELOG.md @@ -2,6 +2,9 @@ ## 3.2.0 +- Add optional `onRequestException` callback function to `RestOfflineQueueClient` +- Add optional `onReattempt` callback function to `RestOfflineQueueClient` + ## 3.1.0 - Expose offline queue functionality in `offline_queue.dart` From 69d6c25141b89a0d3ce55f0cb89b3b5a10fb4c35 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Wed, 30 Oct 2024 23:37:53 -0700 Subject: [PATCH 19/19] no need to store as instance fields --- .../offline_first_with_rest_repository.dart | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index ace2d462..f4e3c581 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -30,17 +30,6 @@ abstract class OfflineFirstWithRestRepository @protected late RestOfflineRequestQueue offlineRequestQueue; - /// A callback triggered when the response of a request has a status code - /// which is present in the [reattemptForStatusCodes] list. - /// - /// Forwarded to [RestOfflineQueueClient]. - void Function(http.Request request, int statusCode)? onReattempt; - - /// A callback triggered when a request throws an exception during execution. - /// - /// Forwarded to [RestOfflineQueueClient]. - void Function(http.Request, Object)? onRequestException; - OfflineFirstWithRestRepository({ super.autoHydrate, super.loggerName, @@ -59,11 +48,16 @@ abstract class OfflineFirstWithRestRepository /// its defaults List? reattemptForStatusCodes, - /// This property is forwarded to `RestOfflineQueueClient`. - this.onReattempt, + /// A callback triggered when the response of a request has a status code + /// which is present in the [reattemptForStatusCodes] list. + /// + /// Forwarded to [RestOfflineQueueClient]. + void Function(http.Request request, int statusCode)? onReattempt, - /// This property is forwarded to `RestOfflineQueueClient`. - this.onRequestException, + /// A callback triggered when a request throws an exception during execution. + /// + /// Forwarded to [RestOfflineQueueClient]. + void Function(http.Request, Object)? onRequestException, required RestProvider restProvider, required super.sqliteProvider, }) : remoteProvider = restProvider,