|
|
|
@ -266,8 +266,10 @@ export default function FullTextSearchByRelay({ |
|
|
|
relayRows.length > 0 && relayRows.every((r) => r.phase === 'done' || r.phase === 'error') |
|
|
|
relayRows.length > 0 && relayRows.every((r) => r.phase === 'done' || r.phase === 'error') |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
const abort = new AbortController() |
|
|
|
/** Unmount / total wall only — must not abort in-flight NIP-50 when the “first hits + …ms” scheduling cutoff runs. */ |
|
|
|
|
|
|
|
const runAbort = new AbortController() |
|
|
|
let masterTimer: ReturnType<typeof setTimeout> | null = null |
|
|
|
let masterTimer: ReturnType<typeof setTimeout> | null = null |
|
|
|
|
|
|
|
let stopSchedulingTimer: ReturnType<typeof setTimeout> | null = null |
|
|
|
const myRun = ++runGeneration.current |
|
|
|
const myRun = ++runGeneration.current |
|
|
|
const cleanupInvalidatePreviousRun = () => { |
|
|
|
const cleanupInvalidatePreviousRun = () => { |
|
|
|
runGeneration.current += 1 |
|
|
|
runGeneration.current += 1 |
|
|
|
@ -277,7 +279,11 @@ export default function FullTextSearchByRelay({ |
|
|
|
clearTimeout(masterTimer) |
|
|
|
clearTimeout(masterTimer) |
|
|
|
masterTimer = null |
|
|
|
masterTimer = null |
|
|
|
} |
|
|
|
} |
|
|
|
abort.abort() |
|
|
|
if (stopSchedulingTimer != null) { |
|
|
|
|
|
|
|
clearTimeout(stopSchedulingTimer) |
|
|
|
|
|
|
|
stopSchedulingTimer = null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
runAbort.abort() |
|
|
|
cleanupInvalidatePreviousRun() |
|
|
|
cleanupInvalidatePreviousRun() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -307,38 +313,44 @@ export default function FullTextSearchByRelay({ |
|
|
|
|
|
|
|
|
|
|
|
/** Set when the first {@link runOneRelay} begins (first real NIP-50 query); master wall clock starts then. */ |
|
|
|
/** Set when the first {@link runOneRelay} begins (first real NIP-50 query); master wall clock starts then. */ |
|
|
|
let waveT0: number | null = null |
|
|
|
let waveT0: number | null = null |
|
|
|
let waveEndAt = 0 |
|
|
|
/** After first preview-visible relay hits: stop dequeuing new relays; in-flight REQs keep their per-relay budget. */ |
|
|
|
/** Only after ≥1 event from a relay: apply "first results + …ms" (empty EOSE must not shorten the wave). */ |
|
|
|
let stopSchedulingNewRelays = false |
|
|
|
let appliedRelativeWaveCutoff = false |
|
|
|
/** Only after ≥1 preview-visible event from a relay: stop starting new relays after …ms (empty EOSE must not shorten). */ |
|
|
|
|
|
|
|
let appliedRelativeSchedulingCutoff = false |
|
|
|
|
|
|
|
|
|
|
|
const scheduleMasterAbort = () => { |
|
|
|
const scheduleMasterWallAbort = () => { |
|
|
|
if (masterTimer != null) { |
|
|
|
if (masterTimer != null) { |
|
|
|
clearTimeout(masterTimer) |
|
|
|
clearTimeout(masterTimer) |
|
|
|
masterTimer = null |
|
|
|
masterTimer = null |
|
|
|
} |
|
|
|
} |
|
|
|
const ms = Math.max(0, waveEndAt - Date.now()) |
|
|
|
if (waveT0 === null) return |
|
|
|
|
|
|
|
const ms = Math.max(0, waveT0 + SEARCH_TOTAL_WALL_MS - Date.now()) |
|
|
|
masterTimer = setTimeout(() => { |
|
|
|
masterTimer = setTimeout(() => { |
|
|
|
masterTimer = null |
|
|
|
masterTimer = null |
|
|
|
abort.abort() |
|
|
|
stopSchedulingNewRelays = true |
|
|
|
|
|
|
|
runAbort.abort() |
|
|
|
}, ms) |
|
|
|
}, ms) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const beginWaveIfNeeded = () => { |
|
|
|
const beginWaveIfNeeded = () => { |
|
|
|
if (waveT0 !== null) return |
|
|
|
if (waveT0 !== null) return |
|
|
|
waveT0 = Date.now() |
|
|
|
waveT0 = Date.now() |
|
|
|
waveEndAt = waveT0 + SEARCH_TOTAL_WALL_MS |
|
|
|
scheduleMasterWallAbort() |
|
|
|
scheduleMasterAbort() |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const onFirstSearchHits = () => { |
|
|
|
const onFirstPreviewVisibleRelayHits = () => { |
|
|
|
if (appliedRelativeWaveCutoff || waveT0 === null) return |
|
|
|
if (appliedRelativeSchedulingCutoff || waveT0 === null) return |
|
|
|
appliedRelativeWaveCutoff = true |
|
|
|
appliedRelativeSchedulingCutoff = true |
|
|
|
const now = Date.now() |
|
|
|
if (stopSchedulingTimer != null) { |
|
|
|
waveEndAt = Math.min(waveT0 + SEARCH_TOTAL_WALL_MS, now + SEARCH_AFTER_FIRST_RELAY_MS) |
|
|
|
clearTimeout(stopSchedulingTimer) |
|
|
|
scheduleMasterAbort() |
|
|
|
} |
|
|
|
|
|
|
|
stopSchedulingTimer = setTimeout(() => { |
|
|
|
|
|
|
|
stopSchedulingTimer = null |
|
|
|
|
|
|
|
stopSchedulingNewRelays = true |
|
|
|
|
|
|
|
}, SEARCH_AFTER_FIRST_RELAY_MS) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
abort.signal.addEventListener( |
|
|
|
runAbort.signal.addEventListener( |
|
|
|
'abort', |
|
|
|
'abort', |
|
|
|
() => { |
|
|
|
() => { |
|
|
|
setRelayRows((prev) => |
|
|
|
setRelayRows((prev) => |
|
|
|
@ -415,7 +427,7 @@ export default function FullTextSearchByRelay({ |
|
|
|
includeOtherStoresFullText: true, |
|
|
|
includeOtherStoresFullText: true, |
|
|
|
fullTextStoreHitCap: 260 |
|
|
|
fullTextStoreHitCap: 260 |
|
|
|
}) |
|
|
|
}) |
|
|
|
if (myRun !== runGeneration.current || abort.signal.aborted) return |
|
|
|
if (myRun !== runGeneration.current || runAbort.signal.aborted) return |
|
|
|
const mergedLocalMatching = mergedLocal.filter((e) => mergedSearchNoteHasPreviewBody(e)) |
|
|
|
const mergedLocalMatching = mergedLocal.filter((e) => mergedSearchNoteHasPreviewBody(e)) |
|
|
|
if (mergedLocalMatching.length === 0) return |
|
|
|
if (mergedLocalMatching.length === 0) return |
|
|
|
applyMergedUpdate((map) => { |
|
|
|
applyMergedUpdate((map) => { |
|
|
|
@ -432,25 +444,24 @@ export default function FullTextSearchByRelay({ |
|
|
|
})() |
|
|
|
})() |
|
|
|
|
|
|
|
|
|
|
|
const runOneRelay = async (relayUrl: string) => { |
|
|
|
const runOneRelay = async (relayUrl: string) => { |
|
|
|
if (myRun !== runGeneration.current || abort.signal.aborted) return |
|
|
|
if (myRun !== runGeneration.current || runAbort.signal.aborted) return |
|
|
|
beginWaveIfNeeded() |
|
|
|
beginWaveIfNeeded() |
|
|
|
const t0 = performance.now() |
|
|
|
const t0 = performance.now() |
|
|
|
const remainingWaveMs = Math.max(500, waveEndAt - Date.now()) |
|
|
|
|
|
|
|
const perRelayBudget = Math.min(SEARCH_PER_RELAY_QUERY_MS, remainingWaveMs) |
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay( |
|
|
|
const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay( |
|
|
|
relayUrl, |
|
|
|
relayUrl, |
|
|
|
filter, |
|
|
|
filter, |
|
|
|
{ globalTimeout: perRelayBudget, signal: abort.signal } |
|
|
|
{ globalTimeout: SEARCH_PER_RELAY_QUERY_MS, signal: runAbort.signal } |
|
|
|
) |
|
|
|
) |
|
|
|
if (myRun !== runGeneration.current) return |
|
|
|
if (myRun !== runGeneration.current) return |
|
|
|
|
|
|
|
|
|
|
|
const sorted = [...raw] |
|
|
|
const sorted = [...raw] |
|
|
|
.sort((a, b) => compareEventsForDTagQuery(q, a, b)) |
|
|
|
.sort((a, b) => compareEventsForDTagQuery(q, a, b)) |
|
|
|
.slice(0, FULL_TEXT_SEARCH_MAX_NOTES_PER_RELAY) |
|
|
|
.slice(0, FULL_TEXT_SEARCH_MAX_NOTES_PER_RELAY) |
|
|
|
|
|
|
|
const previewVisible = sorted.filter((e) => mergedSearchNoteHasPreviewBody(e)) |
|
|
|
|
|
|
|
|
|
|
|
const ms = Math.round(performance.now() - t0) |
|
|
|
const ms = Math.round(performance.now() - t0) |
|
|
|
if (sorted.length === 0 && connectionError) { |
|
|
|
if (previewVisible.length === 0 && connectionError) { |
|
|
|
setRelayRows((prev) => |
|
|
|
setRelayRows((prev) => |
|
|
|
prev.map((r) => |
|
|
|
prev.map((r) => |
|
|
|
r.relayUrl === relayUrl |
|
|
|
r.relayUrl === relayUrl |
|
|
|
@ -462,10 +473,10 @@ export default function FullTextSearchByRelay({ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
mergeIntoHits(relayUrl, sorted) |
|
|
|
mergeIntoHits(relayUrl, sorted) |
|
|
|
void addSearchEventsToSessionCacheBatched(sorted, runGeneration, myRun) |
|
|
|
void addSearchEventsToSessionCacheBatched(previewVisible, runGeneration, myRun) |
|
|
|
|
|
|
|
|
|
|
|
if (sorted.length > 0) { |
|
|
|
if (previewVisible.length > 0) { |
|
|
|
onFirstSearchHits() |
|
|
|
onFirstPreviewVisibleRelayHits() |
|
|
|
} |
|
|
|
} |
|
|
|
setRelayRows((prev) => |
|
|
|
setRelayRows((prev) => |
|
|
|
prev.map((r) => |
|
|
|
prev.map((r) => |
|
|
|
@ -473,16 +484,16 @@ export default function FullTextSearchByRelay({ |
|
|
|
? { |
|
|
|
? { |
|
|
|
...r, |
|
|
|
...r, |
|
|
|
phase: 'done', |
|
|
|
phase: 'done', |
|
|
|
eventCount: sorted.length, |
|
|
|
eventCount: previewVisible.length, |
|
|
|
ms, |
|
|
|
ms, |
|
|
|
errorMessage: sorted.length > 0 ? undefined : connectionError |
|
|
|
errorMessage: previewVisible.length > 0 ? undefined : connectionError |
|
|
|
} |
|
|
|
} |
|
|
|
: r |
|
|
|
: r |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
if (myRun !== runGeneration.current) return |
|
|
|
if (myRun !== runGeneration.current) return |
|
|
|
if (abort.signal.aborted) return |
|
|
|
if (runAbort.signal.aborted) return |
|
|
|
const msg = err instanceof Error ? err.message : String(err) |
|
|
|
const msg = err instanceof Error ? err.message : String(err) |
|
|
|
const ms = Math.round(performance.now() - t0) |
|
|
|
const ms = Math.round(performance.now() - t0) |
|
|
|
setRelayRows((prev) => |
|
|
|
setRelayRows((prev) => |
|
|
|
@ -494,7 +505,7 @@ export default function FullTextSearchByRelay({ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const worker = async () => { |
|
|
|
const worker = async () => { |
|
|
|
while (myRun === runGeneration.current && !abort.signal.aborted) { |
|
|
|
while (myRun === runGeneration.current && !runAbort.signal.aborted && !stopSchedulingNewRelays) { |
|
|
|
const relayUrl = nextRelayUrl() |
|
|
|
const relayUrl = nextRelayUrl() |
|
|
|
if (!relayUrl) break |
|
|
|
if (!relayUrl) break |
|
|
|
await runOneRelay(relayUrl) |
|
|
|
await runOneRelay(relayUrl) |
|
|
|
|