Browse Source

bug-fixes

imwald
Silberengel 5 days ago
parent
commit
d65c5185d8
  1. 60
      assets/controllers/article_comments_controller.js
  2. 23
      src/Command/PrewarmCommand.php
  3. 2
      src/Entity/FeaturedAuthor.php
  4. 45
      src/Service/NostrClient.php

60
assets/controllers/article_comments_controller.js

@ -18,14 +18,9 @@ export default class extends Controller { @@ -18,14 +18,9 @@ export default class extends Controller {
return;
}
if (this.preloadedValue) {
const run = () => {
void this.load();
};
if (typeof requestIdleCallback !== 'undefined') {
requestIdleCallback(run, { timeout: 8_000 });
} else {
setTimeout(run, 800);
}
// Article SSR already included comments. Do not re-fetch: a slow or dropped
// request would replace working HTML with a generic error. Re-fetch on auth
// only (reply UI may need fresh permission state).
return;
}
void this.load();
@ -44,22 +39,41 @@ export default class extends Controller { @@ -44,22 +39,41 @@ export default class extends Controller {
async load() {
const t0 = performance.now();
try {
const res = await fetch(this.urlValue, {
headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
const perAttemptMs = 45_000;
const maxAttempts = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), perAttemptMs);
try {
const res = await fetch(this.urlValue, {
signal: controller.signal,
headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const html = await res.text();
this.containerTarget.innerHTML = html;
const ms = Math.round(performance.now() - t0);
if (attempt > 1) {
console.info(`[article-comments] fragment OK in ${ms}ms (after ${attempt} attempts)`, this.urlValue);
} else {
console.info(`[article-comments] fragment OK in ${ms}ms`, this.urlValue);
}
window.clearTimeout(timer);
return;
} catch (err) {
window.clearTimeout(timer);
if (attempt < maxAttempts) {
const delay = 1_200 * 2 ** (attempt - 1);
await new Promise((r) => setTimeout(r, delay));
continue;
}
const ms = Math.round(performance.now() - t0);
console.warn(`[article-comments] fragment failed after ${ms}ms`, this.urlValue, err);
this.containerTarget.innerHTML =
'<p class="text-subtle">Comments could not be loaded.</p>';
}
const html = await res.text();
this.containerTarget.innerHTML = html;
const ms = Math.round(performance.now() - t0);
console.info(`[article-comments] fragment OK in ${ms}ms`, this.urlValue);
} catch (err) {
const ms = Math.round(performance.now() - t0);
console.warn(`[article-comments] fragment failed after ${ms}ms`, this.urlValue, err);
this.containerTarget.innerHTML =
'<p class="text-subtle">Comments could not be loaded.</p>';
}
}
}

23
src/Command/PrewarmCommand.php

@ -17,11 +17,13 @@ use Psr\Log\LoggerInterface; @@ -17,11 +17,13 @@ use Psr\Log\LoggerInterface;
use swentel\nostr\Key\Key;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Terminal;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
@ -363,6 +365,27 @@ final class PrewarmCommand extends Command @@ -363,6 +365,27 @@ final class PrewarmCommand extends Command
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%%'."\n".' <comment>%message%</comment> <info>%elapsed:6s%</info> ');
$bar->setMessage($message);
// Long %message% lines (e.g. category slugs) wider than the terminal make Symfony’s ProgressBar
// shrink/expand the bar on every redraw; truncate so each line fits and the bar stays stable
// and can use the full width to the right.
$tw = (new Terminal())->getWidth();
if ($tw < 40) {
$tw = 80;
}
$messageMaxWidth = max(12, $tw - 18);
$bar->setPlaceholderFormatter('message', function (ProgressBar $b) use ($messageMaxWidth): string {
$m = (string) ($b->getMessage() ?? '');
if ($m === '') {
return '';
}
if (Helper::width($m) > $messageMaxWidth) {
return Helper::substr($m, 0, max(1, $messageMaxWidth - 1)).'…';
}
return $m;
});
$bar->setBarWidth(max(20, $tw - 32));
return $bar;
}

2
src/Entity/FeaturedAuthor.php

@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM; @@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
class FeaturedAuthor
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column]
private ?int $id = null;

45
src/Service/NostrClient.php

@ -33,6 +33,18 @@ class NostrClient @@ -33,6 +33,18 @@ class NostrClient
/** Extra wall time for {@see bin/nostr_relay_request_worker.php} process vs. WebSocket timeout. */
private const DISCUSSION_WORKER_GRACE_SEC = 5.0;
/**
* Hard cap on unique relay URLs for article discussion. More relays do not help much (indexers duplicate)
* but blow up wall time when we fall back to sequential in-process {@see Request::send()}.
*/
private const MAX_DISCUSSION_RELAY_URLS = 10;
/**
* {@see sendArticleDiscussionToRelaysSequential} visits relays one after another (~RELAY_REQUEST_TIMEOUT_SEC
* each). Keep this low so HTTP /fragment/comments and browsers do not hit 60–90s proxy cuts.
*/
private const MAX_SEQUENTIAL_RELAY_URLS = 3;
/** When a logged-in user lists this relay, also use {@see self::AGGR_NOSTR_LAND} for comment + profile reads. */
private const NOSTR_LAND = 'wss://nostr.land';
@ -972,6 +984,12 @@ class NostrClient @@ -972,6 +984,12 @@ class NostrClient
array_merge($baseForDiscussion, $authorRelays)
);
$plannedRelayUrls = array_values(array_unique($mergedForDiscussion, \SORT_REGULAR));
if (\count($plannedRelayUrls) > self::MAX_DISCUSSION_RELAY_URLS) {
$plannedRelayUrls = \array_slice($plannedRelayUrls, 0, self::MAX_DISCUSSION_RELAY_URLS);
$this->logger->notice('nostr.article_discussion.relay_list_capped', [
'max' => self::MAX_DISCUSSION_RELAY_URLS,
]);
}
$filters = $this->createArticleDiscussionFilters($coordinate, $rootEventHexId);
$subscription = new Subscription();
@ -990,7 +1008,8 @@ class NostrClient @@ -990,7 +1008,8 @@ class NostrClient
$tSend = microtime(true);
$workerPath = $this->projectDir.'/bin/nostr_relay_request_worker.php';
if (!\is_file($workerPath) || \count($plannedRelayUrls) <= 1) {
$response = $this->sendArticleDiscussionToRelaysSequential($plannedRelayUrls, $requestMessage);
$forSeq = $this->capRelayUrlsForSequentialPath($plannedRelayUrls);
$response = $this->sendArticleDiscussionToRelaysSequential($forSeq, $requestMessage);
} else {
try {
$response = $this->sendArticleDiscussionToRelaysParallel($plannedRelayUrls, $requestMessage);
@ -999,7 +1018,11 @@ class NostrClient @@ -999,7 +1018,11 @@ class NostrClient
'message' => $e->getMessage(),
'exception_class' => \get_class($e),
]);
$response = $this->sendArticleDiscussionToRelaysSequential($plannedRelayUrls, $requestMessage);
$forSeq = $this->capRelayUrlsForSequentialPath($plannedRelayUrls);
$this->logger->warning('nostr.article_discussion.sequential_fallback', [
'relays' => $forSeq,
]);
$response = $this->sendArticleDiscussionToRelaysSequential($forSeq, $requestMessage);
}
}
$sendMs = (int) round((microtime(true) - $tSend) * 1000);
@ -1084,6 +1107,24 @@ class NostrClient @@ -1084,6 +1107,24 @@ class NostrClient
return ['thread' => $thread, 'quotes' => $quotes];
}
/**
* @param list<string> $relayUrls
*
* @return list<string>
*/
private function capRelayUrlsForSequentialPath(array $relayUrls): array
{
if (\count($relayUrls) <= self::MAX_SEQUENTIAL_RELAY_URLS) {
return $relayUrls;
}
$this->logger->notice('nostr.article_discussion.sequential_relay_cap', [
'used' => self::MAX_SEQUENTIAL_RELAY_URLS,
'had' => \count($relayUrls),
]);
return \array_values(\array_slice($relayUrls, 0, self::MAX_SEQUENTIAL_RELAY_URLS));
}
/**
* One {@see Request} over all relays (library visits each wss:// in series).
*

Loading…
Cancel
Save