From e8a6397df99f45a3d0fa9c5927c74f52abd1734c Mon Sep 17 00:00:00 2001 From: Colton Date: Thu, 5 Nov 2026 23:40:49 -0500 Subject: [PATCH 1/5] Start HttpCacheManager without its local server (startServer: false) --- lib/src/cache_manager/http_cache_manager.dart | 27 ++++++++----- test/e2e/serverless_test.dart | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 test/e2e/serverless_test.dart diff --git a/lib/src/cache_manager/http_cache_manager.dart b/lib/src/cache_manager/http_cache_manager.dart index ece72f9..41b90a0 100644 --- a/lib/src/cache_manager/http_cache_manager.dart +++ b/lib/src/cache_manager/http_cache_manager.dart @@ -13,7 +13,9 @@ import '../etc/helpers.dart'; /// /// Use [init] to initialize the manager before creating streams. class HttpCacheManager { - final LocalCacheServer _server; + /// Null when [init] was told not to start it: streams are then read only + /// through [HttpCacheStream.request], with no cache URL to play. + final LocalCacheServer? _server; /// The global configuration used for all streams managed by this manager. final GlobalCacheConfig config; @@ -21,17 +23,21 @@ class HttpCacheManager { final Map _streams = {}; final Map _customCacheFiles = {}; HttpCacheManager._(this._server, this.config) { - _server.start(createStream); + _server?.start(createStream); } /// Gets the cache URL for the given source URL. /// If a custom cache file is provided, it will be saved and used for the cache stream. Uri getCacheUrl(final Uri sourceUrl, {final File? file}) { _checkDisposed(); + final server = _server; + if (server == null) { + throw StateError('HttpCacheManager was initialized without a server, so there is no cache URL. Use createStream instead.'); + } if (file != null) { _customCacheFiles[sourceUrl.requestKey] = file; } - return _server.encodeSourceUrl(sourceUrl); + return server.encodeSourceUrl(sourceUrl); } /// Ensures the local cache server is accepting connections, restarting it on the same port if it is not. @@ -41,7 +47,7 @@ class HttpCacheManager { /// that failed to connect. Future ensureActive() { _checkDisposed(); - return _server.ensureActive(); + return _server?.ensureActive() ?? Future.value(); } /// Create a [HttpCacheStream] instance for the given URL. If an instance already exists, the existing instance will be returned. @@ -72,7 +78,7 @@ class HttpCacheManager { final cacheStream = HttpCacheStream( sourceUrl: sourceUrl, - cacheUrl: _server.encodeSourceUrl(sourceUrl), + cacheUrl: _server?.encodeSourceUrl(sourceUrl) ?? sourceUrl, files: cacheFiles, config: config ?? createStreamConfig(), ); @@ -178,13 +184,13 @@ class HttpCacheManager { /// The input [url] can either be [sourceUrl] or [cacheUrl]. HttpCacheStream? getExistingStream(Uri url) { _checkDisposed(); - url = _server.decodeSourceUrl(url) ?? url; + url = _server?.decodeSourceUrl(url) ?? url; return _streams[url.requestKey]; } CacheFiles _resolveCacheFiles(Uri sourceUrl, [File? cacheFile]) { if (cacheFile == null) { - sourceUrl = _server.decodeSourceUrl(sourceUrl) ?? sourceUrl; + sourceUrl = _server?.decodeSourceUrl(sourceUrl) ?? sourceUrl; cacheFile = _customCacheFiles[sourceUrl.requestKey] ?? config.cacheFileResolver(config.cacheDirectory, sourceUrl); } @@ -202,7 +208,7 @@ class HttpCacheManager { _onStreamCreated = null; try { - await _server.close(force: true); + await _server?.close(force: true); } finally { _customCacheFiles.clear(); for (final stream in _streams.values.toList()) { @@ -240,11 +246,14 @@ class HttpCacheManager { /// [customHttpClient] is the custom http client to use. If null, a default http client will be used. /// You can also provide [GlobalCacheConfig] for the initial configuration. /// Use `port` to specify the port for the local server. If not provided, a random available port will be used. + /// With [startServer] false no local server is started (no socket is bound, which a sandbox may refuse): streams are + /// read through [HttpCacheStream.request] only, and [getCacheUrl] throws. static Future init({ final Directory? cacheDir, final http.Client? customHttpClient, final GlobalCacheConfig? config, final int? port, + final bool startServer = true, }) { assert(config == null || (cacheDir == null && customHttpClient == null), 'Cannot set cacheDir or httpClient when config is provided. Set them in the config instead.'); @@ -259,7 +268,7 @@ class HttpCacheManager { cacheDir ?? await GlobalCacheConfig.defaultCacheDirectory(), customHttpClient: customHttpClient, ); - final httpCacheServer = await LocalCacheServer.init(port: port); + final httpCacheServer = startServer ? await LocalCacheServer.init(port: port) : null; return _instance = HttpCacheManager._(httpCacheServer, cacheConfig); } finally { _initFuture = null; diff --git a/test/e2e/serverless_test.dart b/test/e2e/serverless_test.dart new file mode 100644 index 0000000..670cf9c --- /dev/null +++ b/test/e2e/serverless_test.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:http_cache_stream/http_cache_stream.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../support/payload.dart'; +import '../support/test_origin.dart'; + +/// `HttpCacheManager.init(startServer: false)` binds no socket (a sandbox +/// without the incoming-network entitlement refuses one): streams still +/// download and cache, read through `createStream`, and there is no cache URL. +void main() { + late Directory cacheDir; + late TestOrigin origin; + final payload = Payload.generate(256 * 1024); + + setUp(() async { + cacheDir = await Directory.systemTemp.createTemp('hcs_serverless_'); + origin = await TestOrigin.start(payload); + }); + + tearDown(() async { + if (HttpCacheManager.isInitialized) await HttpCacheManager.instance.dispose(); + await origin.close(); + if (cacheDir.existsSync()) await cacheDir.delete(recursive: true); + }); + + test('a stream downloads and caches without a server, and there is no cache URL', () async { + final manager = await HttpCacheManager.init(cacheDir: cacheDir, startServer: false); + final source = origin.url('/audio.mp3'); + final stream = manager.createStream(source); + expect(stream.cacheUrl, source, reason: 'no server to encode it'); + + final file = await stream.download(); + expect(await file.readAsBytes(), payload); + expect(() => manager.getCacheUrl(source), throwsStateError); + await manager.ensureActive(); + stream.dispose().ignore(); + }); +} From de12463d71a54b69bee8a04ba1dd18fbbe6c4309 Mon Sep 17 00:00:00 2001 From: Colton Date: Fri, 6 Nov 2026 00:43:33 -0500 Subject: [PATCH 2/5] Fail clearly on cache URLs without a server; add hasServer HttpCacheStream.cacheUrl fell back to the source URL when the manager had no server, so a player handed it would silently bypass the cache. It now throws CacheServerUnavailableException, as getCacheUrl does, and HttpCacheStream.hasCacheUrl / HttpCacheManager.hasServer let callers check first. Document that ensureActive() is a no-op and port is ignored without a server. --- lib/src/cache_manager/http_cache_manager.dart | 30 ++++++++++++++----- lib/src/cache_stream/http_cache_stream.dart | 17 ++++++++--- lib/src/models/exceptions/state_errors.dart | 13 ++++++++ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/lib/src/cache_manager/http_cache_manager.dart b/lib/src/cache_manager/http_cache_manager.dart index 41b90a0..c293dac 100644 --- a/lib/src/cache_manager/http_cache_manager.dart +++ b/lib/src/cache_manager/http_cache_manager.dart @@ -13,8 +13,8 @@ import '../etc/helpers.dart'; /// /// Use [init] to initialize the manager before creating streams. class HttpCacheManager { - /// Null when [init] was told not to start it: streams are then read only - /// through [HttpCacheStream.request], with no cache URL to play. + /// Null when [init] was called with `startServer: false`: streams are then read + /// through [HttpCacheStream.request] only, and there is no cache URL to play. final LocalCacheServer? _server; /// The global configuration used for all streams managed by this manager. @@ -26,13 +26,20 @@ class HttpCacheManager { _server?.start(createStream); } + /// Whether the local cache server was started, i.e. whether [getCacheUrl] and [HttpCacheStream.cacheUrl] are available. + /// + /// False when [init] was called with `startServer: false`. + bool get hasServer => _server != null; + /// Gets the cache URL for the given source URL. /// If a custom cache file is provided, it will be saved and used for the cache stream. + /// + /// Throws a [CacheServerUnavailableException] if the manager has no local cache server (see [hasServer]). Uri getCacheUrl(final Uri sourceUrl, {final File? file}) { _checkDisposed(); final server = _server; if (server == null) { - throw StateError('HttpCacheManager was initialized without a server, so there is no cache URL. Use createStream instead.'); + throw CacheServerUnavailableException(sourceUrl); } if (file != null) { _customCacheFiles[sourceUrl.requestKey] = file; @@ -45,6 +52,8 @@ class HttpCacheManager { /// On iOS, the server's socket can be reclaimed while the app is suspended in the background. The server is checked periodically /// and restarted automatically, but a player may connect before the next check. Call this before retrying a request to a cache URL /// that failed to connect. + /// + /// Does nothing if the manager has no local cache server (see [hasServer]): no server is started or bound. Future ensureActive() { _checkDisposed(); return _server?.ensureActive() ?? Future.value(); @@ -78,7 +87,7 @@ class HttpCacheManager { final cacheStream = HttpCacheStream( sourceUrl: sourceUrl, - cacheUrl: _server?.encodeSourceUrl(sourceUrl) ?? sourceUrl, + cacheUrl: _server?.encodeSourceUrl(sourceUrl), files: cacheFiles, config: config ?? createStreamConfig(), ); @@ -246,8 +255,14 @@ class HttpCacheManager { /// [customHttpClient] is the custom http client to use. If null, a default http client will be used. /// You can also provide [GlobalCacheConfig] for the initial configuration. /// Use `port` to specify the port for the local server. If not provided, a random available port will be used. - /// With [startServer] false no local server is started (no socket is bound, which a sandbox may refuse): streams are - /// read through [HttpCacheStream.request] only, and [getCacheUrl] throws. + /// + /// Set [startServer] to false to skip the local cache server, for consumers that read streams directly through + /// [createStream] and [HttpCacheStream.request] rather than over HTTP. No socket is bound, so this works where binding + /// is refused (e.g. a macOS App Sandbox without the `com.apple.security.network.server` entitlement). Downloading + /// and caching work as usual, but [getCacheUrl] and [HttpCacheStream.cacheUrl] throw a [CacheServerUnavailableException], + /// [ensureActive] does nothing, and [port] is ignored. See [hasServer]. + /// + /// If the manager is already initialized, the existing instance is returned and these arguments are ignored. static Future init({ final Directory? cacheDir, final http.Client? customHttpClient, @@ -268,7 +283,8 @@ class HttpCacheManager { cacheDir ?? await GlobalCacheConfig.defaultCacheDirectory(), customHttpClient: customHttpClient, ); - final httpCacheServer = startServer ? await LocalCacheServer.init(port: port) : null; + final httpCacheServer = + startServer ? await LocalCacheServer.init(port: port) : null; return _instance = HttpCacheManager._(httpCacheServer, cacheConfig); } finally { _initFuture = null; diff --git a/lib/src/cache_stream/http_cache_stream.dart b/lib/src/cache_stream/http_cache_stream.dart index 31f1c1a..66fb263 100644 --- a/lib/src/cache_stream/http_cache_stream.dart +++ b/lib/src/cache_stream/http_cache_stream.dart @@ -34,7 +34,16 @@ class HttpCacheStream { final Uri sourceUrl; /// The Url of the cached stream served by the local cache server (e.g., http://localhost:8080/http/example.com/file.mp3) - final Uri cacheUrl; + /// + /// Throws a [CacheServerUnavailableException] when the stream has no cache URL, which is the case when + /// `HttpCacheManager.init` was called with `startServer: false`. Check [hasCacheUrl] first, or read the content + /// through [request] instead. + Uri get cacheUrl => + _cacheUrl ?? (throw CacheServerUnavailableException(sourceUrl)); + final Uri? _cacheUrl; + + /// Whether this stream is served by the local cache server, i.e. whether [cacheUrl] is available. + bool get hasCacheUrl => _cacheUrl != null; /// The complete, partial, and metadata files used for the cache. final CacheFiles files; @@ -61,10 +70,10 @@ class HttpCacheStream { HttpCacheStream({ required this.sourceUrl, - required this.cacheUrl, + final Uri? cacheUrl, required this.files, required this.config, - }) { + }) : _cacheUrl = cacheUrl { _initFuture.run(() async { try { _cachedResponseHeaders = @@ -620,5 +629,5 @@ class HttpCacheStream { @override String toString() => - 'HttpCacheStream{sourceUrl: $sourceUrl, cacheUrl: $cacheUrl, cacheFile: $cacheFile}'; + 'HttpCacheStream{sourceUrl: $sourceUrl, cacheUrl: $_cacheUrl, cacheFile: $cacheFile}'; } diff --git a/lib/src/models/exceptions/state_errors.dart b/lib/src/models/exceptions/state_errors.dart index f6ac2b7..9dccf54 100644 --- a/lib/src/models/exceptions/state_errors.dart +++ b/lib/src/models/exceptions/state_errors.dart @@ -8,3 +8,16 @@ class CacheManagerDisposedException extends StateError { CacheManagerDisposedException() : super('Attempted to use a disposed HttpCacheManager'); } + +/// Thrown when a cache URL is requested from an `HttpCacheManager` that was +/// initialized with `startServer: false`. Without the local cache server there +/// is no URL to serve the cache from: read the content through +/// `HttpCacheStream.request` (or `download`) instead. +class CacheServerUnavailableException extends StateError { + final Uri sourceUrl; + CacheServerUnavailableException(this.sourceUrl) + : super( + 'HttpCacheManager was initialized without a local cache server (startServer: false), ' + 'so there is no cache URL for $sourceUrl. Read it through HttpCacheStream.request instead.', + ); +} From e5c13412ab3b20ad5db2a85747c9a460fb469c81 Mon Sep 17 00:00:00 2001 From: Colton Date: Fri, 6 Nov 2026 00:44:51 -0500 Subject: [PATCH 3/5] Clear HttpCacheManager's pending init future after it is stored Without a server and with a cacheDir or config given, init's closure has no await, so its finally block cleared _initFuture before the ??= stored it. The completed future stayed behind, and the next init after dispose handed back the disposed manager. --- lib/src/cache_manager/http_cache_manager.dart | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/src/cache_manager/http_cache_manager.dart b/lib/src/cache_manager/http_cache_manager.dart index c293dac..e4a23d0 100644 --- a/lib/src/cache_manager/http_cache_manager.dart +++ b/lib/src/cache_manager/http_cache_manager.dart @@ -275,21 +275,27 @@ class HttpCacheManager { if (_instance != null) { return Future.value(instance); } - return _initFuture ??= () async { - try { - final cacheConfig = config ?? - GlobalCacheConfig( - cacheDirectory: - cacheDir ?? await GlobalCacheConfig.defaultCacheDirectory(), - customHttpClient: customHttpClient, - ); - final httpCacheServer = - startServer ? await LocalCacheServer.init(port: port) : null; - return _instance = HttpCacheManager._(httpCacheServer, cacheConfig); - } finally { - _initFuture = null; - } + final pending = _initFuture; + if (pending != null) return pending; + final future = _initFuture = () async { + final cacheConfig = config ?? + GlobalCacheConfig( + cacheDirectory: + cacheDir ?? await GlobalCacheConfig.defaultCacheDirectory(), + customHttpClient: customHttpClient, + ); + final httpCacheServer = + startServer ? await LocalCacheServer.init(port: port) : null; + return _instance = HttpCacheManager._(httpCacheServer, cacheConfig); }(); + // Cleared once the future is stored, not from inside the closure: without a server + // and with cacheDir or config given, the closure has no await, so it would finish + // (and clear _initFuture) before the assignment above, leaving a stale future that + // hands out the old manager after it is disposed. + future.whenComplete(() { + if (identical(_initFuture, future)) _initFuture = null; + }).ignore(); + return future; } /// The singleton instance of [HttpCacheManager]. From 25a561f85e7a28d544bc8f297a15e789b8f5d9b3 Mon Sep 17 00:00:00 2001 From: Colton Date: Fri, 6 Nov 2026 00:44:51 -0500 Subject: [PATCH 4/5] Test serverless init, caching, ensureActive, dispose and cache URLs --- test/e2e/serverless_test.dart | 166 ++++++++++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 8 deletions(-) diff --git a/test/e2e/serverless_test.dart b/test/e2e/serverless_test.dart index 670cf9c..5281d68 100644 --- a/test/e2e/serverless_test.dart +++ b/test/e2e/serverless_test.dart @@ -1,14 +1,15 @@ import 'dart:io'; -import 'package:http_cache_stream/http_cache_stream.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:http_cache_stream/http_cache_stream.dart'; import '../support/payload.dart'; import '../support/test_origin.dart'; /// `HttpCacheManager.init(startServer: false)` binds no socket (a sandbox /// without the incoming-network entitlement refuses one): streams still -/// download and cache, read through `createStream`, and there is no cache URL. +/// download and cache, read through `createStream` and `request`, and there is +/// no cache URL. void main() { late Directory cacheDir; late TestOrigin origin; @@ -20,21 +21,170 @@ void main() { }); tearDown(() async { - if (HttpCacheManager.isInitialized) await HttpCacheManager.instance.dispose(); + if (HttpCacheManager.isInitialized) { + await HttpCacheManager.instance.dispose(); + } await origin.close(); if (cacheDir.existsSync()) await cacheDir.delete(recursive: true); }); - test('a stream downloads and caches without a server, and there is no cache URL', () async { - final manager = await HttpCacheManager.init(cacheDir: cacheDir, startServer: false); + Future> readAll(StreamResponse response) async { + final bytes = []; + await for (final chunk in response.stream) { + bytes.addAll(chunk); + } + return bytes; + } + + test('init without a server binds no socket', () async { + // Hold a port, then ask the manager for it: with a server, init would fail + // to bind; without one, the port is never touched. + final held = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + addTearDown(held.close); + + final manager = await HttpCacheManager.init( + cacheDir: cacheDir, + port: held.port, + startServer: false, + ); + expect(manager.hasServer, isFalse); + expect(HttpCacheManager.instanceOrNull, same(manager)); + }); + + test('a stream downloads, caches and serves from cache without a server', + () async { + final manager = + await HttpCacheManager.init(cacheDir: cacheDir, startServer: false); final source = origin.url('/audio.mp3'); final stream = manager.createStream(source); - expect(stream.cacheUrl, source, reason: 'no server to encode it'); + + // First read streams from the origin while caching. + final first = await stream.request(); + expect(await readAll(first), payload); final file = await stream.download(); expect(await file.readAsBytes(), payload); - expect(() => manager.getCacheUrl(source), throwsStateError); + final originRequests = origin.requestCount; + + // A ranged read is now served from the cache file, not the origin. + final ranged = await stream.request(start: 100, end: 200); + expect(ranged.source, ResponseSource.cacheFile); + expect(await readAll(ranged), payload.sublist(100, 200)); + expect(origin.requestCount, originRequests); + + expect(manager.getExistingStream(source), same(stream)); + await stream.dispose(force: true); + + // A fresh stream for the same URL picks up the completed cache. + final again = manager.createStream(source); + final cached = await again.request(); + expect(cached.source, ResponseSource.cacheFile); + expect(await readAll(cached), payload); + expect(origin.requestCount, originRequests); + await again.dispose(force: true); + }); + + test('preCacheUrl works without a server', () async { + final manager = + await HttpCacheManager.init(cacheDir: cacheDir, startServer: false); + final file = await manager.preCacheUrl(origin.url('/pre.mp3')); + expect(await file.readAsBytes(), payload); + }); + + test('cache URLs throw CacheServerUnavailableException without a server', + () async { + final manager = + await HttpCacheManager.init(cacheDir: cacheDir, startServer: false); + final source = origin.url('/audio.mp3'); + + expect( + () => manager.getCacheUrl(source), + throwsA(isA() + .having((e) => e.sourceUrl, 'sourceUrl', source)), + ); + + final stream = manager.createStream(source); + expect(stream.hasCacheUrl, isFalse); + expect( + () => stream.cacheUrl, + throwsA(isA()), + ); + // A StateError, so existing `on StateError` handlers still catch it. + expect(() => stream.cacheUrl, throwsStateError); + expect(stream.toString(), contains('cacheUrl: null')); + await stream.dispose(force: true); + }); + + test('ensureActive does nothing without a server', () async { + final held = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + addTearDown(held.close); + final manager = await HttpCacheManager.init( + cacheDir: cacheDir, + port: held.port, + startServer: false, + ); + + // Completes without trying to start or rebind a server on the held port. + await manager.ensureActive(); + await manager.ensureActive(); + expect(manager.hasServer, isFalse); + + // Streams still work afterwards. + final file = await manager.preCacheUrl(origin.url('/after.mp3')); + expect(await file.readAsBytes(), payload); + }); + + test('dispose works without a server', () async { + final manager = + await HttpCacheManager.init(cacheDir: cacheDir, startServer: false); + final stream = manager.createStream(origin.url('/audio.mp3')); + await stream.download(); + + await manager.dispose(); + expect(manager.isDisposed, isTrue); + expect(HttpCacheManager.isInitialized, isFalse); + await stream.future.timeout(const Duration(seconds: 5)); + expect(stream.isDisposed, isTrue); + expect(() => manager.ensureActive(), + throwsA(isA())); + + // A disposed serverless manager can be replaced by one with a server. + final next = await HttpCacheManager.init(cacheDir: cacheDir); + expect(next.hasServer, isTrue); + }); + + test('init after dispose returns a new manager', () async { + // Serverless init with a cacheDir completes without an await; the pending + // init future must still be cleared so the next init does not hand back + // the disposed manager. + final first = + await HttpCacheManager.init(cacheDir: cacheDir, startServer: false); + await first.dispose(); + final second = await HttpCacheManager.init( + config: GlobalCacheConfig(cacheDirectory: cacheDir), + startServer: false, + ); + expect(second, isNot(same(first))); + expect(second.isDisposed, isFalse); + expect(HttpCacheManager.instance, same(second)); + }); + + test('concurrent inits share one manager', () async { + final results = await Future.wait([ + HttpCacheManager.init(cacheDir: cacheDir, startServer: false), + HttpCacheManager.init(cacheDir: cacheDir, startServer: false), + ]); + expect(results[0], same(results[1])); + }); + + test('the default init still starts the server', () async { + final manager = await HttpCacheManager.init(cacheDir: cacheDir); + expect(manager.hasServer, isTrue); + final source = origin.url('/audio.mp3'); + final stream = manager.createStream(source); + expect(stream.hasCacheUrl, isTrue); + expect(stream.cacheUrl, manager.getCacheUrl(source)); + expect(stream.cacheUrl.host, InternetAddress.loopbackIPv4.address); await manager.ensureActive(); - stream.dispose().ignore(); }); } From 49f10ca8b9a8753f6cf2f15fe59f1a8be814290e Mon Sep 17 00:00:00 2001 From: Colton Date: Fri, 6 Nov 2026 00:45:12 -0500 Subject: [PATCH 5/5] Document serverless init; bump to 0.3.0 (unreleased) --- CHANGELOG.md | 12 ++++++++++++ README.md | 16 ++++++++++++++++ pubspec.yaml | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3994e77..f43a8e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.3.0 (unreleased) + +### New Features + +* Added `HttpCacheManager.init(startServer: false)`, which starts the manager without its local cache server, for apps that read streams directly through `createStream` and `HttpCacheStream.request` instead of over HTTP. No socket is bound, so it works where binding is refused, such as a macOS App Sandbox without the `com.apple.security.network.server` entitlement. Downloading and caching work as usual. The default is unchanged (`startServer: true`). +* Added `HttpCacheManager.hasServer` and `HttpCacheStream.hasCacheUrl`. +* Added `CacheServerUnavailableException` (a `StateError`), thrown by `HttpCacheManager.getCacheUrl` and `HttpCacheStream.cacheUrl` when the manager has no local cache server. Without a server, `ensureActive()` does nothing and `port` is ignored. + +### Fixes + +* Fixed `HttpCacheManager.init` returning the previous, disposed manager after `dispose()` when the init completed without an asynchronous step (no server, with `cacheDir` or `config` given). + ## 0.2.1 ### New Features diff --git a/README.md b/README.md index 66c5491..ab7a3a0 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,22 @@ Reference the file in `android/app/src/main/res/AndroidManifest.xml` by adding t See the example project for a full example. +### Without the local server + +If your app reads streams directly (through `createStream` and `HttpCacheStream.request`) rather than handing a URL to a player, you can skip the local server entirely. No socket is bound, so no cleartext or network-server configuration is needed, and it works in sandboxes that refuse to bind one (e.g. a macOS App Sandbox without the `com.apple.security.network.server` entitlement): + +```dart +await HttpCacheManager.init(startServer: false); + +final stream = HttpCacheManager.instance.createStream(sourceUrl); +final response = await stream.request(start: 0); // downloads and caches as it streams +await for (final chunk in response.stream) { + // ... +} +``` + +Without a server there is no cache URL: `getCacheUrl` and `HttpCacheStream.cacheUrl` throw a `CacheServerUnavailableException` (check `HttpCacheManager.hasServer` or `HttpCacheStream.hasCacheUrl`), and `ensureActive()` does nothing. + # Core Components diff --git a/pubspec.yaml b/pubspec.yaml index ceb4e97..44a713c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: http_cache_stream description: "Simultaneously download, cache, and stream remote content. Perfect for media players and any plugin that streams web content." -version: 0.2.1 +version: 0.3.0 homepage: https://github.com/Colton127/http_cache_stream repository: https://github.com/Colton127/http_cache_stream topics: