Skip to content

Instantly share code, notes, and snippets.

@knaeckeKami
Last active January 8, 2024 14:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save knaeckeKami/b11ad83e4b69aa44638815d1471c2ba3 to your computer and use it in GitHub Desktop.
Save knaeckeKami/b11ad83e4b69aa44638815d1471c2ba3 to your computer and use it in GitHub Desktop.
import "dart:async";
import "dart:isolate";
import 'package:ferry/ferry.dart';
import 'package:ferry/ferry_isolate.dart';
import "package:gql_error_link/gql_error_link.dart";
import "package:gql_exec/gql_exec.dart";
import "package:gql_http_link/gql_http_link.dart";
import "package:gql_link/gql_link.dart";
import "package:gql_transform_link/gql_transform_link.dart";
import 'package:shared_preferences/shared_preferences.dart';
class HttpIsolateAuthLink extends Link {
late Link _link;
String? _token;
final SendPort _messageHandlerSendPort;
final Future<String> Function() updateToken;
Completer<String>? _inflightTokenRefreshRequest;
HttpIsolateAuthLink(String? initialToken, this._messageHandlerSendPort, this.updateToken)
: _token = initialToken {
_link = Link.concat(
ErrorLink(onException: handleException),
TransformLink(requestTransformer: transformRequest),
);
}
Future<String?> _handleTokenRefresh() async {
if (_inflightTokenRefreshRequest != null) {
return _inflightTokenRefreshRequest!.future;
}
_inflightTokenRefreshRequest = Completer();
try {
_token = await updateToken();
_messageHandlerSendPort.send(UpdatedTokenMessage(_token));
_inflightTokenRefreshRequest!.complete(_token);
return _token;
} catch (e, s) {
_inflightTokenRefreshRequest!.completeError(e, s);
rethrow;
} finally {
_inflightTokenRefreshRequest = null;
}
}
Stream<Response> handleException(
Request request,
NextLink forward,
LinkException exception,
) async* {
// or replace with different condition, like
// ```dart
// if (exception is HttpLinkServerException &&
// (exception.parsedResponse?.errors?.any((element) =>
// element.extensions?["code"] == "user_not_authorized") ??
// false)) {
if (exception is HttpLinkServerException && (exception.response.statusCode == 401)) {
await _handleTokenRefresh();
yield* forward(request);
return;
}
throw exception;
}
Request transformRequest(Request request) {
var updatedRequest = request.updateContextEntry<HttpLinkHeaders>(
(headers) => HttpLinkHeaders(
headers: <String, String>{
...headers?.headers ?? <String, String>{},
},
),
);
if (_token != null) {
updatedRequest = request.updateContextEntry<HttpLinkHeaders>(
(headers) => HttpLinkHeaders(
headers: <String, String>{
"Authorization": "Bearer ${_token!}",
},
),
);
}
return updatedRequest;
}
@override
Stream<Response> request(Request request, [forward]) async* {
if (_token == null) {
await _handleTokenRefresh();
}
yield* _link.request(request, forward);
}
}
class UpdatedTokenMessage {
final String? newToken;
UpdatedTokenMessage(this.newToken);
}
// called on main isolate
Future<IsolateClient> createIsolateClient() async {
final sharedPrefs = await SharedPreferences.getInstance();
final client = await IsolateClient.create(initClient,
params: {"initialToken": sharedPrefs.getString("token")}, messageHandler: (message) {
if (message is UpdatedTokenMessage) {
if (message.newToken != null) {
sharedPrefs.setString("token", message.newToken!);
} else {
sharedPrefs.remove("token");
}
}
});
return client;
}
// called on the ferry isolate
Future<Client> initClient(Map<String, dynamic>? params, SendPort? sendPort) async {
final httpLink = HttpLink('https://my.api');
final initialToken = params!["initialToken"] as String?;
final link = Link.concat(
HttpIsolateAuthLink(initialToken, sendPort!, () async {
//replace with real token refresh call
await Future.delayed(const Duration(milliseconds: 10));
return "new token";
}),
httpLink);
final client = Client(
link: link,
);
return client;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment