Skip to content

Commit

Permalink
feat(supabase): ignore supabase auth and storage #440 (#442)
Browse files Browse the repository at this point in the history
  • Loading branch information
tshedor authored Sep 18, 2024
1 parent aae10d9 commit d625f06
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 22 deletions.
28 changes: 28 additions & 0 deletions docs/offline_first/offline_first_with_supabase_repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,34 @@ await Supabase.initialize(httpClient: client)
final supabaseProvider = SupabaseProvider(Supabase.instance.client, modelDictionary: ...)
```

### Offline Queue Caveats

For Flutter users, `Supabase.instance.client` inherits this [offline client](https://github.com/supabase/supabase-flutter/blob/main/packages/supabase/lib/src/supabase_client.dart#L141-L142). Brick works around Supabase's default endpoints: the offline queue **will not** cache and retry requests to Supabase's Auth or Storage.

To ensure the queue handles all requests, pass an empty set:

```dart
final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(
databaseFactory: databaseFactory,
ignorePaths: {},
);
```

For implementations that [do not wish to retry functions](https://github.com/GetDutchie/brick/issues/440) and need to handle a response, add `'/functions/v1'` to this Set:

```dart
final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(
databaseFactory: databaseFactory,
ignorePaths: {
'/auth/v1',
'/storage/v1',
'/functions/v1'
},
);
```

!> This is an admittedly brittle solution for ignoring core Supabase paths. If you change the default values for `ignorePaths`, you are responsible for maintaining the right paths when Supabase changes or upgrades their endpoint paths.

### @ConnectOfflineFirstWithSupabase

`@ConnectOfflineFirstWithSupabase` decorates the model that can be serialized by one or more providers. Offline First does not have configuration at the class level and only extends configuration held by its providers:
Expand Down
1 change: 1 addition & 0 deletions packages/brick_offline_first_with_rest/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Expose offline queue functionality in `offline_queue.dart`
- Include `request` in `RestOfflineQueueClient`'s generic error response
- Add `ignorePaths` to `RestOfflineQueueClient`'s constructor. This parameter will not cache requests that begin with any of the supplied values.

## 3.0.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import 'package:logging/logging.dart';

/// Stores all requests in a SQLite database
class RestOfflineQueueClient extends http.BaseClient {
/// Any request URI that begins with one of these paths will not be
/// handled by the offline queue and will be forwarded to [_inner].
///
/// For example, if an ignore path is `/v1/ignored-path`, a request
/// to `http://0.0.0.0:3000/v1/ignored-path/endpoint` will not be
/// cached and retried on failure.
final RegExp? _ignorePattern;

/// A normal HTTP client, treated like a manual `super`
/// as detailed by [the Dart team](https://github.com/dart-lang/http/blob/378179845420caafbf7a34d47b9c22104753182a/README.md#using)
final http.Client _inner;
Expand All @@ -30,12 +38,31 @@ class RestOfflineQueueClient extends http.BaseClient {
this._inner,
this.requestManager, {
List<int>? reattemptForStatusCodes,

/// Any request URI that begins with one of these paths will not be
/// handled by the offline queue and will be forwarded to [_inner].
///
/// For example, if an ignore path is `/v1/ignored-path`, a request
/// to `http://0.0.0.0:3000/v1/ignored-path/endpoint` will not be
/// cached and retried on failure.
Set<String>? ignorePaths,
}) : _logger = Logger('OfflineQueueHttpClient#${requestManager.databaseName}'),
reattemptForStatusCodes = reattemptForStatusCodes ?? [404, 501, 502, 503, 504];
reattemptForStatusCodes = reattemptForStatusCodes ?? [404, 501, 502, 503, 504],
_ignorePattern = ignorePaths == null ? null : RegExp(ignorePaths.join('|'));

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final cachePolicy = (request as http.Request).headers.remove(policyHeader);
if (_ignorePattern != null && request.url.path.startsWith(_ignorePattern!)) {
return await _inner.send(request);
}

// Only handle http Requests
// https://github.com/GetDutchie/brick/issues/440#issuecomment-2357547961
if (request is! http.Request) {
return await _inner.send(request);
}

final cachePolicy = request.headers.remove(policyHeader);
final skipCache = cachePolicy == 'requireRemote';
final cacheItem = RestRequestSqliteCache(request);
_logger.finest('sending: ${cacheItem.toSqlite()}');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,6 @@ void main() {
expect(resp.body, 'hello from inner');
});

test('GET requests are not tracked', () async {
final inner = stubResult(statusCode: 404);
final client = RestOfflineQueueClient(inner, requestManager);
await client.get(Uri.parse('http://0.0.0.0:3000'));

expect(await requestManager.unprocessedRequests(), isEmpty);
});

test('${RestOfflineQueueClient.policyHeader} requests are not tracked', () async {
final inner = stubResult(statusCode: 404);
final client = RestOfflineQueueClient(inner, requestManager);
await client.post(
Uri.parse('http://0.0.0.0:3000'),
body: 'new record',
headers: {RestOfflineQueueClient.policyHeader: 'requireRemote'},
);

expect(await requestManager.unprocessedRequests(), isEmpty);
});

test('request is stored in SQLite', () async {
final inner = stubResult(statusCode: 501);
final client = RestOfflineQueueClient(inner, requestManager);
Expand Down Expand Up @@ -90,6 +70,60 @@ void main() {
expect(resp.statusCode, 501);
});

test('GET requests are not tracked', () async {
final inner = stubResult(statusCode: 404);
final client = RestOfflineQueueClient(inner, requestManager);
await client.get(Uri.parse('http://0.0.0.0:3000'));

expect(await requestManager.unprocessedRequests(), isEmpty);
});

test('${RestOfflineQueueClient.policyHeader} requireRemote requests are not tracked',
() async {
final inner = stubResult(statusCode: 404);
final client = RestOfflineQueueClient(inner, requestManager);
await client.post(
Uri.parse('http://0.0.0.0:3000'),
body: 'new record',
headers: {RestOfflineQueueClient.policyHeader: 'requireRemote'},
);

expect(await requestManager.unprocessedRequests(), isEmpty);
});

test('ignored path is not not tracked', () async {
final inner = stubResult(statusCode: 404);
final client =
RestOfflineQueueClient(inner, requestManager, ignorePaths: {'/ignored-path'});
await client.post(
Uri.parse('http://0.0.0.0:3000/ignored-path'),
body: 'new record',
);

expect(await requestManager.unprocessedRequests(), isEmpty);

final multiplePaths = RestOfflineQueueClient(
inner,
requestManager,
ignorePaths: {'/ignored-path', '/other-path'},
);
await multiplePaths.post(
Uri.parse('http://0.0.0.0:3000/other-path'),
body: 'new record',
);

expect(await requestManager.unprocessedRequests(), isEmpty);

final nestedPath =
RestOfflineQueueClient(inner, requestManager, ignorePaths: {'/v1/ignored-path'});
await nestedPath.post(
Uri.parse('http://0.0.0.0:3000/v1/ignored-path'),
body: 'new record',
);

expect(await requestManager.unprocessedRequests(), isEmpty);
});

group('request is not deleted', () {
test('after an unsuccessful response and is created', () async {
final inner = MockClient((req) async {
Expand Down
28 changes: 28 additions & 0 deletions packages/brick_offline_first_with_supabase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ await Supabase.initialize(httpClient: client)
final supabaseProvider = SupabaseProvider(Supabase.instance.client, modelDictionary: ...)
```

### Offline Queue Caveats

For Flutter users, `Supabase.instance.client` inherits this [offline client](https://github.com/supabase/supabase-flutter/blob/main/packages/supabase/lib/src/supabase_client.dart#L141-L142). Brick works around Supabase's default endpoints: the offline queue **will not** cache and retry requests to Supabase's Auth or Storage.

To ensure the queue handles all requests, pass an empty set:

```dart
final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(
databaseFactory: databaseFactory,
ignorePaths: {},
);
```

For implementations that [do not wish to retry functions](https://github.com/GetDutchie/brick/issues/440) and need to handle a response, add `'/functions/v1'` to this Set:

```dart
final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(
databaseFactory: databaseFactory,
ignorePaths: {
'/auth/v1',
'/storage/v1',
'/functions/v1'
},
);
```

:warning: This is an admittedly brittle solution for ignoring core Supabase paths. If you change the default values for `ignorePaths`, you are responsible for maintaining the right paths when Supabase changes or upgrades their endpoint paths.

## Models

### @ConnectOfflineFirstWithSupabase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,17 @@ abstract class OfflineFirstWithSupabaseRepository
/// the queue is used to add offline to the repository.
static (RestOfflineQueueClient, RestOfflineRequestQueue) clientQueue({
required DatabaseFactory databaseFactory,

/// These paths will not be stored in the offline queue.
/// By default, Supabase Auth and Storage paths are ignored.
///
/// For implementations that wish to retry functions and do not
/// need to handle a response, add `'/functions/v1'` to this Set.
/// https://github.com/GetDutchie/brick/issues/440
Set<String>? ignorePaths = const {
'/auth/v1',
'/storage/v1',
},
http.Client? innerClient,
Duration? processingInterval,
List<int> reattemptForStatusCodes = const [
Expand All @@ -187,6 +198,7 @@ abstract class OfflineFirstWithSupabaseRepository
processingInterval: processingInterval,
serialProcessing: serialProcessing,
),
ignorePaths: ignorePaths,
reattemptForStatusCodes: reattemptForStatusCodes,
);
return (client, RestOfflineRequestQueue(client: client));
Expand Down

0 comments on commit d625f06

Please sign in to comment.