mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): defer timeline trim past StrictMode probe
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
b56b621195
commit
59daa7cc5b
@@ -33,6 +33,7 @@ import type { Channel, Identity, RelayEvent } from "@/shared/api/types";
|
||||
import { applyEditTagOverlay } from "@/features/messages/lib/applyEditTagOverlay.mjs";
|
||||
import { backfillAuxForMessages } from "@/features/messages/lib/auxBackfill";
|
||||
import { countTopLevelTimelineRows } from "@/features/messages/lib/formatTimelineMessages";
|
||||
import { deferredTimelineTrim } from "@/features/messages/lib/deferredTimelineTrim";
|
||||
import {
|
||||
mergeHistoryOverSnapshot,
|
||||
readMessageSnapshot,
|
||||
@@ -325,12 +326,7 @@ export function useChannelSubscription(channel: Channel | null) {
|
||||
}
|
||||
});
|
||||
|
||||
// Leaving the channel is the safe moment to enforce the timeline cap:
|
||||
// nothing is rendered from this cache anymore, so trimming to the newest
|
||||
// MAX_TIMELINE_MESSAGES window cannot evict rows out from under a
|
||||
// scrolled-back reader. Merges while the channel is open are deliberately
|
||||
// uncapped for the same reason.
|
||||
const trimTimelineOnLeave = useEffectEvent((leftChannelId: string) => {
|
||||
const trimTimelineCache = useEffectEvent((leftChannelId: string) => {
|
||||
queryClient.setQueryData<RelayEvent[]>(
|
||||
channelMessagesKey(leftChannelId),
|
||||
(current) =>
|
||||
@@ -345,6 +341,10 @@ export function useChannelSubscription(channel: Channel | null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// StrictMode immediately re-runs this setup after its synthetic cleanup.
|
||||
// Cancel the deferred trim while this channel remains active.
|
||||
deferredTimelineTrim.cancel(channelId);
|
||||
|
||||
let isDisposed = false;
|
||||
let cleanup: (() => Promise<void>) | undefined;
|
||||
const disposeReconnectListener = relayClient.subscribeToReconnects(() => {
|
||||
@@ -392,7 +392,9 @@ export function useChannelSubscription(channel: Channel | null) {
|
||||
if (cleanup) {
|
||||
void cleanup();
|
||||
}
|
||||
trimTimelineOnLeave(channelId);
|
||||
deferredTimelineTrim.schedule(channelId, () => {
|
||||
trimTimelineCache(channelId);
|
||||
});
|
||||
};
|
||||
}, [channelId, channelType]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createDeferredTimelineTrim } from "./deferredTimelineTrim.ts";
|
||||
|
||||
function fakeTimerHost() {
|
||||
let nextId = 1;
|
||||
const callbacks = new Map();
|
||||
return {
|
||||
host: {
|
||||
clearTimeout(id) {
|
||||
callbacks.delete(id);
|
||||
},
|
||||
setTimeout(callback) {
|
||||
const id = nextId++;
|
||||
callbacks.set(id, callback);
|
||||
return id;
|
||||
},
|
||||
},
|
||||
flush() {
|
||||
for (const [id, callback] of [...callbacks]) {
|
||||
callbacks.delete(id);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("StrictMode setup cancels the synthetic cleanup trim", () => {
|
||||
const timers = fakeTimerHost();
|
||||
const scheduler = createDeferredTimelineTrim(timers.host);
|
||||
let trims = 0;
|
||||
|
||||
scheduler.schedule("channel-a", () => trims++);
|
||||
scheduler.cancel("channel-a");
|
||||
timers.flush();
|
||||
|
||||
assert.equal(trims, 0);
|
||||
});
|
||||
|
||||
test("a genuine channel departure trims after the deferred task", () => {
|
||||
const timers = fakeTimerHost();
|
||||
const scheduler = createDeferredTimelineTrim(timers.host);
|
||||
let trims = 0;
|
||||
|
||||
scheduler.schedule("channel-a", () => trims++);
|
||||
timers.flush();
|
||||
|
||||
assert.equal(trims, 1);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
type TimerHost = {
|
||||
setTimeout: (
|
||||
handler: () => void,
|
||||
delay: number,
|
||||
) => ReturnType<typeof setTimeout>;
|
||||
clearTimeout: (timer: ReturnType<typeof setTimeout>) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defers leave-only cache trims by one task. React StrictMode immediately
|
||||
* re-runs an effect after its synthetic cleanup, so the matching setup can
|
||||
* cancel that trim while a real channel departure lets it run.
|
||||
*/
|
||||
export function createDeferredTimelineTrim(host: TimerHost = globalThis) {
|
||||
const pending = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
return {
|
||||
cancel(channelId: string) {
|
||||
const timer = pending.get(channelId);
|
||||
if (timer !== undefined) {
|
||||
host.clearTimeout(timer);
|
||||
pending.delete(channelId);
|
||||
}
|
||||
},
|
||||
schedule(channelId: string, trim: () => void) {
|
||||
this.cancel(channelId);
|
||||
const timer = host.setTimeout(() => {
|
||||
pending.delete(channelId);
|
||||
trim();
|
||||
}, 0);
|
||||
pending.set(channelId, timer);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const deferredTimelineTrim = createDeferredTimelineTrim();
|
||||
Reference in New Issue
Block a user