mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(media): keep self-host media loading after restarts (#3283)
This commit is contained in:
1 parent
bd56174870
commit
46a356cc40
59 files changed
+1238
-499
No files matched your search
@@ -3,12 +3,12 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {Buffer} from 'node:buffer';
|
||||
import {Readable} from 'node:stream';
|
||||
import {describe, test} from 'node:test';
|
||||
import {describe, mock, test} from 'node:test';
|
||||
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
|
||||
|
||||
installElectronStub();
|
||||
|
||||
const {DesktopLocalAppProxyClient} = await import('./LocalAppProxyClient.ts');
|
||||
const {DesktopLocalAppProxyClient, LocalAppProxyTraffic} = await import('./LocalAppProxyClient.ts');
|
||||
|
||||
const PROXY_MAX_CONCURRENT_REQUESTS = 32;
|
||||
const PROXY_MAX_QUEUED_REQUESTS = 4096;
|
||||
@@ -93,7 +93,7 @@ function readAvatar(client, index, signal) {
|
||||
describe('the local app proxy bounds its own concurrency instead of overrunning outbound HTTP', () => {
|
||||
test('every avatar of a fully rendered chat resolves rather than tripping the outbound capacity cap', async () => {
|
||||
const {outboundHTTP, state} = createOutboundHTTP();
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP});
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.REMOTE_RESOURCE});
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetches = [];
|
||||
@@ -113,7 +113,7 @@ describe('the local app proxy bounds its own concurrency instead of overrunning
|
||||
|
||||
test('no more requests are in flight at any instant than the proxy concurrency limit', async () => {
|
||||
const {outboundHTTP, state} = createOutboundHTTP();
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP});
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.REMOTE_RESOURCE});
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetches = [];
|
||||
@@ -130,7 +130,7 @@ describe('the local app proxy bounds its own concurrency instead of overrunning
|
||||
|
||||
test('an aborted waiter rejects with the abort reason and leaves its queue slot to the next waiter', async () => {
|
||||
const {outboundHTTP, state} = createOutboundHTTP();
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP});
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.REMOTE_RESOURCE});
|
||||
const held = new AbortController();
|
||||
|
||||
const holding = [];
|
||||
@@ -161,7 +161,7 @@ describe('the local app proxy bounds its own concurrency instead of overrunning
|
||||
|
||||
test('the queue refuses a request past its bound rather than growing without limit', async () => {
|
||||
const {outboundHTTP, state} = createOutboundHTTP();
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP});
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.REMOTE_RESOURCE});
|
||||
const controller = new AbortController();
|
||||
|
||||
const accepted = [];
|
||||
@@ -178,3 +178,114 @@ describe('the local app proxy bounds its own concurrency instead of overrunning
|
||||
assert.equal(settled.length, PROXY_MAX_CONCURRENT_REQUESTS + PROXY_MAX_QUEUED_REQUESTS);
|
||||
});
|
||||
});
|
||||
|
||||
const RESOURCE_HEADERS_DEADLINE_MS = 30_000;
|
||||
|
||||
function createSilentUpstream() {
|
||||
const state = {started: [], aborted: 0};
|
||||
const outboundHTTP = {
|
||||
request: (request) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const entry = {
|
||||
url: request.url,
|
||||
serviceName: request.serviceName,
|
||||
answer: () =>
|
||||
resolve({
|
||||
headers: {'content-type': 'image/webp'},
|
||||
message: Readable.from([Buffer.from('avatar-bytes')]),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
url: new URL(request.url),
|
||||
}),
|
||||
};
|
||||
state.started.push(entry);
|
||||
request.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
state.aborted += 1;
|
||||
reject(request.signal.reason);
|
||||
},
|
||||
{once: true},
|
||||
);
|
||||
}),
|
||||
};
|
||||
return {outboundHTTP, state};
|
||||
}
|
||||
|
||||
function apiRequest(signal) {
|
||||
return {...avatarRequest(0, signal), targetURL: 'https://api.fluxer.app/v1/users/@me'};
|
||||
}
|
||||
|
||||
describe('media that a slow or silent self-host never answers cannot wedge the local app proxy', () => {
|
||||
test('avatars dropped while their upstream sends no headers free their permits at the headers deadline', async () => {
|
||||
mock.timers.enable({apis: ['setTimeout']});
|
||||
try {
|
||||
const {outboundHTTP, state} = createSilentUpstream();
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.REMOTE_RESOURCE});
|
||||
const renderer = new AbortController();
|
||||
const dropped = [];
|
||||
for (let index = 0; index < PROXY_MAX_CONCURRENT_REQUESTS; index += 1) {
|
||||
const fetch = client.fetch(avatarRequest(index, renderer.signal));
|
||||
fetch.catch(() => {});
|
||||
dropped.push(fetch);
|
||||
}
|
||||
await tick();
|
||||
const fresh = client.fetch(avatarRequest(1000, renderer.signal));
|
||||
await tick();
|
||||
assert.equal(state.started.length, PROXY_MAX_CONCURRENT_REQUESTS);
|
||||
|
||||
mock.timers.tick(RESOURCE_HEADERS_DEADLINE_MS);
|
||||
await Promise.allSettled(dropped);
|
||||
await tick();
|
||||
|
||||
assert.equal(state.aborted, PROXY_MAX_CONCURRENT_REQUESTS);
|
||||
assert.equal(state.started.length, PROXY_MAX_CONCURRENT_REQUESTS + 1);
|
||||
state.started.at(-1).answer();
|
||||
const response = await fresh;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(await response.text(), 'avatar-bytes');
|
||||
} finally {
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
test('API requests keep flowing while every media permit is held by a silent upstream', async () => {
|
||||
const {outboundHTTP, state} = createSilentUpstream();
|
||||
const media = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.REMOTE_RESOURCE});
|
||||
const api = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.API});
|
||||
const renderer = new AbortController();
|
||||
for (let index = 0; index < PROXY_MAX_CONCURRENT_REQUESTS * 2; index += 1) {
|
||||
media.fetch(avatarRequest(index, renderer.signal)).catch(() => {});
|
||||
}
|
||||
await tick();
|
||||
const apiCall = api.fetch(apiRequest(renderer.signal));
|
||||
await tick();
|
||||
const started = state.started.at(-1);
|
||||
assert.equal(started.url, 'https://api.fluxer.app/v1/users/@me');
|
||||
assert.equal(started.serviceName, LocalAppProxyTraffic.API);
|
||||
started.answer();
|
||||
assert.equal((await apiCall).status, 200);
|
||||
renderer.abort();
|
||||
});
|
||||
|
||||
test('the newest queued media request is admitted first so what is on screen loads before what scrolled away', async () => {
|
||||
const {outboundHTTP, state} = createSilentUpstream();
|
||||
const client = new DesktopLocalAppProxyClient({outboundHTTP, traffic: LocalAppProxyTraffic.REMOTE_RESOURCE});
|
||||
const renderer = new AbortController();
|
||||
const fetches = [];
|
||||
for (let index = 0; index < PROXY_MAX_CONCURRENT_REQUESTS + 3; index += 1) {
|
||||
const fetch = client.fetch(avatarRequest(index, renderer.signal));
|
||||
fetch.catch(() => {});
|
||||
fetches.push(fetch);
|
||||
}
|
||||
await tick();
|
||||
state.started[0].answer();
|
||||
await (await fetches[0]).text();
|
||||
await tick();
|
||||
assert.equal(
|
||||
state.started.at(-1).url,
|
||||
`https://media.fluxer.app/avatars/${PROXY_MAX_CONCURRENT_REQUESTS + 2}.webp`,
|
||||
);
|
||||
renderer.abort();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user