Protocol 14: a warm relay preloads relayed Up next items for everyone

The server keeps one small relay (32 MiB ahead, 8 behind) for Up next's
first relayed item, keyed by its queue id at a URL that stays the same
after the swap: fetched by the server for server-fetch links, fed by
the adder's app for through-me links and files, by a member with an
account for member-passed Jellyfin items. Every app preloads from it;
the swap promotes it with its buffer. Dropped when the item stops
being first or its adder leaves. Measured switch: ~1.6 s cold, 20-40 ms
preloaded. ROADMAP gains the research item on server-side fetching of
member-resolved URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-11 17:53:29 +03:00
1 parent e52a7633a0
commit d8585fb78e
17 files changed
+821 -151

No files matched your search

+155 -2
View File
@@ -92,9 +92,10 @@ void main() {
return m;
}
Future<RelaySource> startSource(_Member m, {RelayMedia? from}) async {
Future<RelaySource> startSource(_Member m, {RelayMedia? from, String? queueId}) async {
final src = RelaySource(
Uri.parse('ws://127.0.0.1:${server.boundPort}/r/default/${m.welcome.token}/source'),
Uri.parse('ws://127.0.0.1:${server.boundPort}/r/default/${m.welcome.token}/source'
'${queueId == null ? '' : '?q=$queueId'}'),
from ?? RelayMedia.single(FileRelayBytes(file)),
backoff: (_) => const Duration(milliseconds: 100));
await src.start();
@@ -444,6 +445,158 @@ void main() {
});
});
group('warm relay (Up next)', () {
late HttpServer origin;
var originBytes = 0;
setUp(() async {
originBytes = 0;
origin = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
origin.listen((req) async {
final r = resolveMediaRequest(rangeHeader: req.headers.value('range'), size: bytes.length, filename: 'x.mkv');
req.response.statusCode = r.status;
r.headers.forEach(req.response.headers.set);
if (r.status == 200 || r.status == 206) {
final end = r.end ?? bytes.length - 1;
originBytes += end - r.start + 1;
req.response.add(bytes.sublist(r.start, end + 1));
}
await req.response.close();
});
});
tearDown(() => origin.close(force: true));
List<Map> queueOf(_Member m) => [for (final q in (m.lastState?['queue'] as List?) ?? const []) q as Map];
Map? headRelay(_Member m) => (queueOf(m).firstOrNull?['media'] as Map?)?['relay'] as Map?;
Future<String> add(_Member m, Map<String, Object?> item, {bool next = false}) async {
final had = {for (final q in queueOf(m)) q['id']};
m.send({'t': 'queue', 'op': 'add', 'at': next ? 'next' : 'end', 'item': item});
await _until(() => queueOf(m).any((q) => !had.contains(q['id'])), 'queued');
return queueOf(m).firstWhere((q) => !had.contains(q['id']))['id'] as String;
}
Map<String, Object?> through(String key) => {
'media': {'name': 'ep 01.mkv', 'size': bytes.length, 'duration': 0},
'how': 'through',
'title': 'Ep 1',
'key': key,
};
String qUrl(_Member v, String queueId) =>
'http://127.0.0.1:${server.boundPort}/r/default/${v.welcome.token}/$queueId/${Uri.encodeComponent('ep 01.mkv')}';
test('a link the server fetches: preloaded from its queue id, promoted at the swap with its buffer', () async {
final alice = await join('alice');
final bob = await join('bob');
final id = await add(alice, {
'media': {'name': 'ep 01.mkv', 'size': 0, 'duration': 0, 'url': 'http://127.0.0.1:${origin.port}/x.mkv'},
'how': 'server',
'title': 'Ep 1',
});
await _until(() => headRelay(bob)?['live'] == true, 'warm relay live');
expect(headRelay(bob), {'fetch': true, 'live': true});
expect(jsonEncode(bob.lastState), isNot(contains('x.mkv')));
expect((await get(qUrl(bob, id))).$3, bytes);
// Small: the warm budget is far over this file, so all of it is in.
final fetched = originBytes;
alice.send({
't': 'swap',
'queueId': id,
'media': {'name': 'Ep 1', 'size': 0, 'duration': 0, 'relay': {'fetch': true}},
});
await _until(() => (bob.media?['relay'] as Map?)?['queueId'] == id, 'promoted');
expect(bob.media!['relay'], {'fetch': true, 'live': true, 'queueId': id});
expect(bob.media!['name'], 'ep 01.mkv', reason: 'the URL name the preload used');
expect(queueOf(bob), isEmpty);
expect(server.hub.warm, isNull);
// The same buffer: both URLs serve it, nothing is fetched again.
expect((await get(qUrl(bob, id))).$3, bytes);
expect((await get(viewerUrl(bob, mediaIdOf(bob)))).$3, bytes);
expect(originBytes, fetched);
final carol = await join('carol');
expect(carol.welcome.mediaUrl, qUrl(carol, id));
});
test('a link passed on: its adder feeds it before the swap, and the swap keeps that source', () async {
final alice = await join('alice');
final bob = await join('bob');
final id = await add(alice, through('k-abcdefgh'));
expect(headRelay(bob), {'source': alice.welcome.memberId});
final src = await startSource(alice, queueId: id);
await _until(() => headRelay(bob)?['live'] == true, 'warm relay live');
expect((await get(qUrl(bob, id), range: 'bytes=0-99')).$3, bytes.sublist(0, 100));
// Only its source may feed it.
final impostor = await startSource(bob, queueId: id);
await impostor.done.timeout(const Duration(seconds: 5));
alice.send({
't': 'swap',
'queueId': id,
'media': {'name': 'ep 01.mkv', 'size': bytes.length, 'duration': 0, 'relay': {}},
});
await _until(() => (bob.media?['relay'] as Map?)?['queueId'] == id, 'promoted');
expect(bob.media!['relay'], {'source': alice.welcome.memberId, 'live': true, 'queueId': id});
expect((await get(qUrl(bob, id))).$3, bytes);
expect(src.bytesSent, lessThan(bytes.length * 1.3));
// Still the source of the media it now relays.
await Future<void>.delayed(const Duration(milliseconds: 200));
expect(server.hub.relay!.live, isTrue);
});
test('dropped when the item stops being first: its URLs 404, its source stops', () async {
final alice = await join('alice');
final bob = await join('bob');
final id = await add(alice, through('k-abcdefgh'));
final src = await startSource(alice, queueId: id);
await _until(() => headRelay(bob)?['live'] == true, 'warm relay live');
await add(alice, through('k-ijklmnop'), next: true);
await src.done.timeout(const Duration(seconds: 5));
expect((await get(qUrl(bob, id))).$1, 404);
expect(server.hub.warm!.queueId, isNot(id));
});
test('not for an adder alone; dropped when its adder leaves', () async {
final alice = await join('alice');
await add(alice, through('k-abcdefgh'));
expect(headRelay(alice), isNull);
final bob = await join('bob');
await _until(() => headRelay(bob) != null, 'warm relay once someone could read it');
await alice.ws.close();
await _until(() => bob.lastState!['members'] is List && (bob.lastState!['members'] as List).length == 1, 'left');
expect(queueOf(bob), hasLength(1));
expect(headRelay(bob), isNull);
expect(server.hub.warm, isNull);
});
test('a Jellyfin item: fed by the first member with an account, for those without', () async {
final alice = await join('alice');
final bob = await join('bob');
void report(_Member m, bool access) => m.send({
't': 'report', 'position': 0, 'buffered': 0, 'eventId': 0, 'jf': {'srv1': access}, //
});
report(alice, true);
report(bob, false);
final id = await add(alice, {
'media': {
'name': 'Pilot',
'size': 0,
'duration': 0,
'jellyfin': {'serverId': 'srv1', 'itemId': '0123456789abcdef0123456789abcdef', 'title': 'Pilot'},
},
'how': 'jellyfin',
'title': 'Pilot',
});
await _until(() => headRelay(bob) != null, 'warm relay');
expect(headRelay(bob), isEmpty, reason: 'nobody feeds it yet');
final refused = await startSource(bob, queueId: id);
await refused.done.timeout(const Duration(seconds: 5));
await startSource(alice, queueId: id);
await _until(() => headRelay(bob)?['live'] == true, 'live');
expect(headRelay(bob)!['source'], alice.welcome.memberId);
report(bob, true);
await _until(() => headRelay(alice) == null, 'dropped once everyone has an account');
});
});
group('buffer', () {
// A feed that serves [bytes] in-process and counts what it sends.
late RelayBuffer buf;