Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add-browser-support #35

Open
wants to merge 10 commits into
base: pre-release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/continuous-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,24 @@ jobs:
with:
flutter_channel: stable
min_coverage: 0

test-clients:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Install Dependencies
run: flutter pub get

- name: Run Integration Tests VM
run: flutter test test/integration/eventflux_test_vm.dart

- name: Run Integration Tests Browser
run: flutter test test/browser --platform chrome
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@

# Changelog 📝

### v2.3.0-dev.1 🛠️
This release adds web support 🚀
- Added `webConfig` parameter to the `connect` method.
- This allows you to configure the web client.
- Refer README for more info.


### v2.2.2-dev.2 🛠️
- Potential fix for multiple connection issue when using single instance method
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ EventFlux is a Dart package designed for efficient handling of server-sent event
## Supported Platforms
| Android | iOS | Web | MacOS | Windows | Linux |
| ------ | ---- | ---- | ----- | ------- | ----- |
| ✅|✅|🏗️|✅|❓|❓|
| ✅|✅||✅|❓|❓|

*Pssst... see those question marks? That's your cue, tech adventurers! Dive in, test, and tell me all about it.* 🚀🛠️

Expand Down Expand Up @@ -211,6 +211,7 @@ Connects to a server-sent event stream.
| `tag` | `String` | Optional tag for debugging. | - |
| `logReceivedData` | `bool` | Whether to log received data. | `false` |
| `httpClient` | `HttpClientAdapter?` | Optional Http Client Adapter to allow usage of different http clients. | - |
| `webConfig` | `WebConfig?` | Allows configuring the web client. Ignored for non-web platforms. | - |

&nbsp;<br>
</details>
Expand Down
18 changes: 17 additions & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ packages:
path: ".."
relative: true
source: path
version: "2.2.1"
version: "2.2.2-dev.2"
fake_async:
dependency: transitive
description:
Expand All @@ -56,6 +56,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
fetch_api:
dependency: transitive
description:
name: fetch_api
sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
fetch_client:
dependency: transitive
description:
name: fetch_client
sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter:
dependency: "direct main"
description: flutter
Expand Down
58 changes: 38 additions & 20 deletions lib/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import 'dart:async';
import 'dart:convert';

import 'package:eventflux/enum.dart';
import 'package:eventflux/extensions/fetch_client_extension.dart';
import 'package:eventflux/http_client_adapter.dart';
import 'package:eventflux/models/base.dart';
import 'package:eventflux/models/data.dart';
import 'package:eventflux/models/exception.dart';
import 'package:eventflux/models/reconnect.dart';
import 'package:eventflux/models/response.dart';
import 'package:eventflux/models/web_config/web_config.dart';
import 'package:eventflux/utils.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';

/// A class for managing event-driven data streams using Server-Sent Events (SSE).
Expand All @@ -24,7 +27,8 @@ class EventFlux extends EventFluxBase {
static final EventFlux _instance = EventFlux._();

static EventFlux get instance => _instance;
Client? _client;
@visibleForTesting
Client? client;
StreamController<EventFluxData>? _streamController;
bool _isExplicitDisconnect = false;
StreamSubscription? _streamSubscription;
Expand Down Expand Up @@ -141,7 +145,13 @@ class EventFlux extends EventFluxBase {
bool logReceivedData = false,
List<MultipartFile>? files,
bool multipartRequest = false,

/// Optional web config to be used for the connection. Must be provided on web.
/// Will be ignored on non-web platforms.
WebConfig? webConfig,
}) {

assert(!(kIsWeb && webConfig == null), 'WebConfig must be provided on web');
// This check prevents redundant connection requests when a connection is already in progress.
// This does not prevent reconnection attempts if autoReconnect is enabled.

Expand Down Expand Up @@ -191,6 +201,7 @@ class EventFlux extends EventFluxBase {
logReceivedData: logReceivedData,
files: files,
multipartRequest: multipartRequest,
webConfig: webConfig,
);
}

Expand All @@ -209,12 +220,14 @@ class EventFlux extends EventFluxBase {
bool logReceivedData = false,
List<MultipartFile>? files,
bool multipartRequest = false,
WebConfig? webConfig,
}) {
/// Initalise variables
/// Create a new HTTP client based on the platform
/// Uses and internal http client if no http client adapter is present
if (httpClient == null) {
_client = Client();
client =
kIsWeb ? FetchClientExtension.fromWebConfig(webConfig!) : Client();
}

/// Set `_isExplicitDisconnect` to `false` before connecting.
Expand Down Expand Up @@ -272,7 +285,7 @@ class EventFlux extends EventFluxBase {
response = httpClient.send(request);
} else {
// Use internal HTTP client
response = _client!.send(request);
response = client!.send(request);
}

response.then((data) async {
Expand Down Expand Up @@ -494,7 +507,7 @@ class EventFlux extends EventFluxBase {
try {
_streamSubscription?.cancel();
_streamController?.close();
_client?.close();
client?.close();
Future.delayed(const Duration(seconds: 1), () {});
eventFluxLog('Disconnected', LogEvent.info, _tag);
_status = EventFluxStatus.disconnected;
Expand Down Expand Up @@ -580,25 +593,30 @@ class EventFlux extends EventFluxBase {
break;

case ReconnectMode.exponential:
_interval = _interval * 2;
eventFluxLog("Trying again in ${_interval.toString()} seconds",
LogEvent.reconnect, _tag);

/// It waits for the specified interval before attempting to reconnect.
await Future.delayed(Duration(seconds: _interval), () {
_start(
type,
url,
onSuccessCallback: onSuccessCallback,
autoReconnect: autoReconnect,
onError: onError,
header: header,
onConnectionClose: onConnectionClose,
httpClient: httpClient,
body: body,
files: files,
multipartRequest: multipartRequest,
);
_interval = _interval * 2;
if (!isExplicitDisconnect) {
eventFluxLog("Trying again in ${_interval.toString()} seconds",
LogEvent.reconnect, _tag);

_status = EventFluxStatus.connectionInitiated;
_start(
type,
url,
onSuccessCallback: onSuccessCallback,
autoReconnect: autoReconnect,
onError: onError,
header: header,
onConnectionClose: onConnectionClose,
httpClient: httpClient,
body: body,
files: files,
multipartRequest: multipartRequest,
);
}

});
break;
}
Expand Down
16 changes: 16 additions & 0 deletions lib/extensions/fetch_client_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:eventflux/models/web_config/web_config.dart';
import 'package:fetch_client/fetch_client.dart';

extension FetchClientExtension on FetchClient {
static FetchClient fromWebConfig(WebConfig webConfig) {
return FetchClient(
mode: webConfig.mode.toRequestMode(),
credentials: webConfig.credentials.toRequestCredentials(),
cache: webConfig.cache.toRequestCache(),
referrer: webConfig.referrer,
referrerPolicy: webConfig.referrerPolicy.toRequestReferrerPolicy(),
redirectPolicy: webConfig.redirectPolicy.toRedirectPolicy(),
streamRequests: webConfig.streamRequests,
);
}
}
24 changes: 24 additions & 0 deletions lib/models/web_config/redirect_policy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This code is adapted from fetch_client package
// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
// Licensed under the MIT License

import 'package:fetch_client/fetch_client.dart';

/// How requests should handle redirects.
enum WebConfigRedirectPolicy {
/// Default policy - always follow redirects.
/// If redirect occurs the only way to know about it is via response properties.
alwaysFollow,

/// Probe via HTTP `GET` request.
probe,

/// Same as [probe] but using `HEAD` method.
probeHead;

RedirectPolicy toRedirectPolicy() => switch (this) {
WebConfigRedirectPolicy.alwaysFollow => RedirectPolicy.alwaysFollow,
WebConfigRedirectPolicy.probe => RedirectPolicy.probe,
WebConfigRedirectPolicy.probeHead => RedirectPolicy.probeHead,
};
}
68 changes: 68 additions & 0 deletions lib/models/web_config/request_cache.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This code is adapted from fetch_client package
// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
// Licensed under the MIT License

import 'package:eventflux/models/web_config/request_mode.dart';
import 'package:fetch_client/fetch_client.dart';

/// Controls how requests will interact with the browser's HTTP cache.
enum WebConfigRequestCache {
/// The browser looks for a matching request in its HTTP cache.
///
/// * If there is a match and it is fresh, it will be returned from the cache.
/// * If there is a match but it is stale, the browser will make
/// a conditional request to the remote server. If the server indicates
/// that the resource has not changed, it will be returned from the cache.
/// Otherwise the resource will be downloaded from the server and
/// the cache will be updated.
/// * If there is no match, the browser will make a normal request,
/// and will update the cache with the downloaded resource.
byDefault,

/// The browser fetches the resource from the remote server
/// without first looking in the cache, and will not update the cache
/// with the downloaded resource.
noStore,

/// The browser fetches the resource from the remote server
/// without first looking in the cache, but then will update the cache
/// with the downloaded resource.
reload,

/// The browser looks for a matching request in its HTTP cache.
///
/// * If there is a match, fresh or stale, the browser will make
/// a conditional request to the remote server. If the server indicates
/// that the resource has not changed, it will be returned from the cache.
/// Otherwise the resource will be downloaded from the server and
/// the cache will be updated.
/// * If there is no match, the browser will make a normal request,
/// and will update the cache with the downloaded resource.
noCache,

/// The browser looks for a matching request in its HTTP cache.
///
/// * If there is a match, fresh or stale, it will be returned from the cache.
/// * If there is no match, the browser will make a normal request,
/// and will update the cache with the downloaded resource.
forceCache,

/// The browser looks for a matching request in its HTTP cache.
///
/// * If there is a match, fresh or stale, it will be returned from the cache.
/// * If there is no match, the browser will respond
/// with a 504 Gateway timeout status.
///
/// The [onlyIfCached] mode can only be used if the request's mode
/// is [WebConfigRequestMode.sameOrigin].
onlyIfCached;

RequestCache toRequestCache() => switch (this) {
WebConfigRequestCache.byDefault => RequestCache.byDefault,
WebConfigRequestCache.noStore => RequestCache.noStore,
WebConfigRequestCache.reload => RequestCache.reload,
WebConfigRequestCache.noCache => RequestCache.noCache,
WebConfigRequestCache.forceCache => RequestCache.forceCache,
WebConfigRequestCache.onlyIfCached => RequestCache.onlyIfCached,
};
}
27 changes: 27 additions & 0 deletions lib/models/web_config/request_credentials.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// This code is adapted from fetch_client package
// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
// Licensed under the MIT License

import 'package:fetch_client/fetch_client.dart';

/// Controls what browsers do with credentials (cookies, HTTP authentication
/// entries, and TLS client certificates).
enum WebConfigRequestCredentials {
/// Tells browsers to include credentials with requests to same-origin URLs,
/// and use any credentials sent back in responses from same-origin URLs.
sameOrigin,

/// Tells browsers to exclude credentials from the request, and ignore
/// any credentials sent back in the response (e.g., any Set-Cookie header).
omit,

/// Tells browsers to include credentials in both same- and cross-origin
/// requests, and always use any credentials sent back in responses.
cors;

RequestCredentials toRequestCredentials() => switch (this) {
WebConfigRequestCredentials.sameOrigin => RequestCredentials.sameOrigin,
WebConfigRequestCredentials.omit => RequestCredentials.omit,
WebConfigRequestCredentials.cors => RequestCredentials.cors,
};
}
Loading