fix(PROJ-11): address QA round-3 findings — BUG-18/19 fixed, BUG-11/12 improved
BUG-18 (Critical) — invitations.ts never joined the Matrix room for
members invited with a teamId (only teams.ts's add-member path did).
Standard onboarding flow left every invited team member stuck on
"Raum wird eingerichtet" forever, with no self-healing (reconciliation
only checks user provisioning, never room membership) and no repair
path (blocked by BUG-19). Now mirrors teams.ts: inline addUserToRoom
with an enqueueSyncJob("join_team_room") fallback.
BUG-19 (High) — no global Fastify error handler meant an uncaught
error (e.g. a duplicate-key insert) echoed back the raw Drizzle error,
including the full SQL statement and bound parameters, to the client.
Added app.setErrorHandler(): 5xx is logged server-side and replaced
with a generic message, 4xx keeps its already-safe original message.
BUG-12 (High) — use-team-unread-counts.ts had no SyncState.Error
handling, so an expired token meant matrix-js-sdk retried /sync
against Synapse forever with a dead token while badges silently
froze. Now stops the client and reconnects with a fresh token,
capped exponential backoff.
BUG-11 (High, partially mitigated) — deferred the app-level unread
client's start to requestIdleCallback instead of firing synchronously
on TeamsPage mount, taking it off the critical path. Does NOT fix the
underlying complaint (SDK still loads for anyone who visits the team
list, whether or not they open chat) — documented as an open
architecture question rather than declared fixed.
All four fixes verified end-to-end against the live talkhub/matrix
TLS deployment (invitation accept -> joined_rooms; duplicate insert ->
generic 500). Test data from QA round 3 and this verification pass
cleaned up (orgs, users, Matrix accounts erased, rooms purged).
Not addressed: BUG-20 (predictable Matrix IDs undermine the BUG-2
user_directory mitigation), BUG-5/6/7/9/10/13/14/15/16/17/21/22.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
This commit is contained in:
1 parent
c200fafd03
commit
121d80e707
4 files changed
+372
-13
No files matched your search
@@ -11,6 +11,9 @@ const ROOM_EVENT_RECEIPT = "Room.receipt";
|
||||
const CLIENT_EVENT_SYNC = "sync";
|
||||
const SYNC_STATE_PREPARED = "PREPARED";
|
||||
const SYNC_STATE_SYNCING = "SYNCING";
|
||||
const SYNC_STATE_ERROR = "ERROR";
|
||||
|
||||
const MAX_RECONNECT_DELAY_MS = 60_000;
|
||||
|
||||
interface TeamRoom {
|
||||
id: string;
|
||||
@@ -55,6 +58,8 @@ export function useTeamUnreadCounts(
|
||||
|
||||
let cancelled = false;
|
||||
let client: MatrixClient | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectAttempt = 0;
|
||||
|
||||
function refreshCounts() {
|
||||
if (!client) return;
|
||||
@@ -66,9 +71,30 @@ export function useTeamUnreadCounts(
|
||||
setCounts(next);
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
function teardownClient() {
|
||||
if (client) {
|
||||
client.removeAllListeners();
|
||||
client.stopClient();
|
||||
client = null;
|
||||
}
|
||||
clientRef.current = null;
|
||||
}
|
||||
|
||||
// BUG-12: previously there was no ERROR handling here at all — once the 1h
|
||||
// token expired, /sync started failing and matrix-js-sdk's own internal
|
||||
// retry kept hammering Synapse with the same dead token forever, while
|
||||
// badges silently froze. Stop the client and reconnect with a fresh
|
||||
// (refresh: true) token instead, with capped exponential backoff.
|
||||
function scheduleReconnect() {
|
||||
teardownClient();
|
||||
const delay = Math.min(1000 * 2 ** reconnectAttempt, MAX_RECONNECT_DELAY_MS);
|
||||
reconnectAttempt += 1;
|
||||
reconnectTimer = setTimeout(() => void connect(true), delay);
|
||||
}
|
||||
|
||||
async function connect(forceRefresh = false) {
|
||||
try {
|
||||
const creds = await fetchChatCredentials(authToken!);
|
||||
const creds = await fetchChatCredentials(authToken!, { refresh: forceRefresh });
|
||||
if (cancelled) return;
|
||||
client = await createMatrixClient(creds);
|
||||
if (cancelled) {
|
||||
@@ -83,11 +109,12 @@ export function useTeamUnreadCounts(
|
||||
on(ROOM_EVENT_TIMELINE, refreshCounts);
|
||||
on(ROOM_EVENT_RECEIPT, refreshCounts);
|
||||
on(CLIENT_EVENT_SYNC, (state) => {
|
||||
if (
|
||||
!cancelled &&
|
||||
(state === SYNC_STATE_PREPARED || state === SYNC_STATE_SYNCING)
|
||||
) {
|
||||
if (cancelled) return;
|
||||
if (state === SYNC_STATE_PREPARED || state === SYNC_STATE_SYNCING) {
|
||||
reconnectAttempt = 0;
|
||||
refreshCounts();
|
||||
} else if (state === SYNC_STATE_ERROR) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
});
|
||||
await client.startClient({ initialSyncLimit: 1 });
|
||||
@@ -95,19 +122,42 @@ export function useTeamUnreadCounts(
|
||||
// Not provisioned yet, or Synapse unreachable — tiles just show no badge.
|
||||
if (!(err instanceof ChatSetupPendingError) && !cancelled) {
|
||||
console.warn("[chat] unread-count client failed to start:", err);
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void connect();
|
||||
// BUG-11: this used to call connect() (and its dynamic matrix-js-sdk import)
|
||||
// synchronously on mount, so simply visiting TeamsPage with any provisioned
|
||||
// team room pulled the ~830 KB SDK chunk immediately — for every visitor,
|
||||
// whether or not they ever open a chat. Deferring to an idle callback keeps
|
||||
// it off the critical path for the team list itself. It does NOT eliminate
|
||||
// the download for users who never open chat; that needs a real decision
|
||||
// (e.g. move tile badges to a backend-summarized count instead of a
|
||||
// client-side Matrix client) — tracked as a known limitation, not silently
|
||||
// declared fixed.
|
||||
const idle = (
|
||||
window as typeof window & {
|
||||
requestIdleCallback?: (cb: () => void) => number;
|
||||
cancelIdleCallback?: (id: number) => void;
|
||||
}
|
||||
);
|
||||
let idleId: number | ReturnType<typeof setTimeout>;
|
||||
if (idle.requestIdleCallback) {
|
||||
idleId = idle.requestIdleCallback(() => void connect());
|
||||
} else {
|
||||
idleId = setTimeout(() => void connect(), 200);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (client) {
|
||||
client.removeAllListeners();
|
||||
client.stopClient();
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
if (idle.cancelIdleCallback && typeof idleId === "number") {
|
||||
idle.cancelIdleCallback(idleId);
|
||||
} else {
|
||||
clearTimeout(idleId as ReturnType<typeof setTimeout>);
|
||||
}
|
||||
clientRef.current = null;
|
||||
teardownClient();
|
||||
};
|
||||
}, [authToken, roomIds]);
|
||||
|
||||
|
||||
Reference in new issue
Block a user