mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
fix(mobile): bound queued media authentication work
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -37,6 +37,9 @@ class MediaGetAuthService {
|
||||
|
||||
Map<String, String>? _cachedHeaders;
|
||||
DateTime? _refreshAt;
|
||||
// Includes the running job. Bound retained work during rapid history scrolls.
|
||||
static const _maxPendingSignatures = 64;
|
||||
int _pendingSignatures = 0;
|
||||
Future<void> _signingTail = Future<void>.value();
|
||||
|
||||
MediaGetAuthService({
|
||||
@@ -58,12 +61,18 @@ class MediaGetAuthService {
|
||||
///
|
||||
/// Image loading starts during layout. Serialize signing per auth scope so a
|
||||
/// viewport full of uncached images does not spawn an isolate per row at once.
|
||||
/// Timestamp each proof when its queued job starts, not when it was enqueued.
|
||||
/// At most 64 requests may be outstanding. Excess requests fail explicitly
|
||||
/// without spawning or retaining work; image failures are evicted so a later
|
||||
/// resolution can retry. Timestamp proofs when their queued jobs start.
|
||||
Future<Map<String, String>> headersForAsync(String url) {
|
||||
final nsec = _nsec;
|
||||
if (nsec == null || nsec.isEmpty || !isRelayMediaUrl(url)) {
|
||||
return Future.value(const {});
|
||||
}
|
||||
if (_pendingSignatures >= _maxPendingSignatures) {
|
||||
return Future.error(StateError('Media authentication queue is full'));
|
||||
}
|
||||
_pendingSignatures++;
|
||||
final released = Completer<void>();
|
||||
final result = _signingTail.then((_) async {
|
||||
try {
|
||||
@@ -76,6 +85,7 @@ class MediaGetAuthService {
|
||||
} finally {
|
||||
// A failed worker must propagate to its fetch without blocking later
|
||||
// requests in the queue.
|
||||
_pendingSignatures--;
|
||||
released.complete();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,6 +102,30 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('bounds queued signing and admits requests after it drains', () async {
|
||||
final auth = _auth(nsec: nostr.Keys.generate().nsec);
|
||||
// Enqueue in one turn, before a worker can finish. Record every failure
|
||||
// immediately so overflow futures cannot become unhandled errors.
|
||||
final requests = List.generate(
|
||||
80,
|
||||
(_) => auth
|
||||
.headersForAsync(_mediaUrl)
|
||||
.then<Object>(
|
||||
(headers) => headers,
|
||||
onError: (Object error) => error,
|
||||
),
|
||||
);
|
||||
final results = await Future.wait(requests);
|
||||
expect(results.whereType<Map<String, String>>(), hasLength(64));
|
||||
final rejected = results.whereType<StateError>().toList();
|
||||
expect(rejected, hasLength(16));
|
||||
expect(
|
||||
rejected.map((error) => error.message),
|
||||
everyElement('Media authentication queue is full'),
|
||||
);
|
||||
expect(await auth.headersForAsync(_mediaUrl), isNotEmpty);
|
||||
});
|
||||
|
||||
test('keeps the signing identity separate across auth scopes', () async {
|
||||
final first = nostr.Keys.generate();
|
||||
final second = nostr.Keys.generate();
|
||||
|
||||
Reference in New Issue
Block a user