perf(search): decode large extension results off the UI isolate

This commit is contained in:
zarzet
2026-09-06 02:34:49 +07:00
parent 856139f908
commit 7b87a8ee15
2 changed files with 117 additions and 1 deletions
+4 -1
View File
@@ -1826,7 +1826,10 @@ class PlatformBridge {
'options': optionsJson,
'request_id': requestId,
});
final decoded = _decodeMapListResult(result, 'customSearchWithExtension');
final decoded = await _decodeMapListResultAsync(
result,
'customSearchWithExtension',
);
if (generation == _lookupCacheGeneration &&
_customSearchInFlight[cacheKey]?.requestId == requestId) {
_putMemoryCachedMapList(
@@ -0,0 +1,113 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('com.zarz.spotiflac/backend');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
tearDown(() => messenger.setMockMethodCallHandler(channel, null));
for (final format in ['small-json', 'large-json', 'native-list']) {
test('custom search decodes and caches $format results', () async {
final rows = List.generate(
format == 'large-json' ? 3000 : 2,
(index) => {
'id': '$index',
'name': 'Lagu $index — 日本語',
'artist_name': 'Artist',
'album_name': 'Album',
'duration': 123000,
},
);
final json = jsonEncode(rows);
if (format == 'large-json') {
expect(json.length, greaterThan(128 * 1024));
}
var calls = 0;
messenger.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'customSearchWithExtension');
calls++;
return format == 'native-list' ? rows : json;
});
final result = await PlatformBridge.customSearchWithExtension(
'decode-test-$format',
'query',
);
expect(result, rows);
result.first['name'] = 'Changed by caller';
final cached = await PlatformBridge.customSearchWithExtension(
'decode-test-$format',
'query',
);
expect(cached, rows);
expect(calls, 1);
});
}
test(
'large cancelled response cannot replace the latest cached search',
() async {
final pending = <Completer<String>>[];
messenger.setMockMethodCallHandler(channel, (call) async {
if (call.method == 'cancelExtensionRequest') return null;
expect(call.method, 'customSearchWithExtension');
final completer = Completer<String>();
pending.add(completer);
return completer.future;
});
Future<List<Map<String, dynamic>>> search(String query) =>
PlatformBridge.customSearchWithExtension(
'large-cancel-test',
query,
cancelPrevious: true,
);
final oldA = search('A');
await Future<void>.delayed(Duration.zero);
final b = search('B');
await Future<void>.delayed(Duration.zero);
final latestA = search('A');
await Future<void>.delayed(Duration.zero);
expect(pending, hasLength(3));
pending[2].complete('[{"id":"latest"}]');
expect((await latestA).single['id'], 'latest');
final oldRows = List.generate(
3000,
(index) => {
'id': 'old-$index',
'name': 'Previous search result that must not replace the new cache',
},
);
final oldJson = jsonEncode(oldRows);
expect(oldJson.length, greaterThan(128 * 1024));
pending[0].complete(oldJson);
pending[1].complete('[]');
await Future.wait([oldA, b]);
expect((await search('A')).single['id'], 'latest');
expect(pending, hasLength(3));
},
);
test('invalid results fail without poisoning the retry cache', () async {
var calls = 0;
messenger.setMockMethodCallHandler(channel, (_) async {
return ++calls == 1 ? '[42]' : '[{"id":"valid"}]';
});
Future<List<Map<String, dynamic>>> search() =>
PlatformBridge.customSearchWithExtension(
'invalid-decode-test',
'query',
);
await expectLater(search(), throwsFormatException);
expect((await search()).single['id'], 'valid');
expect(calls, 2);
});
}