feat: upgrade toolchain and soften sync recovery after network blips

Bump Vite 8, matrix-js-sdk, and related deps; fix immer/vanilla-extract imports for new majors. Replace blocking sync error dialog with background recovery on wake/offline, and disable Vite auto-open in dev. Add feature handoff documentation index.
This commit is contained in:
2026-07-09 01:55:09 +10:00
parent f67f08f1db
commit e058165830
61 changed files with 8456 additions and 5547 deletions

View File

@@ -1,20 +1,20 @@
import React, { useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { css } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
const transitionContainer = css({
const transitionContainer = style({
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden',
});
const transitionContent = css({
const transitionContent = style({
width: '100%',
height: '100%',
});
const animateIn = css({
const animateIn = style({
animation: 'routeFadeSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
'@keyframes': {
routeFadeSlideIn: {
@@ -30,7 +30,7 @@ const animateIn = css({
},
});
const animateOut = css({
const animateOut = style({
animation: 'routeFadeSlideOut 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards',
'@keyframes': {
routeFadeSlideOut: {

View File

@@ -2,7 +2,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Badge, Box, Button, Chip, config, Menu, Spinner, Text } from 'folds';
import { Icon, Icons } from '../../../components/icons';
import produce from 'immer';
import { produce } from 'immer';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';

View File

@@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import produce from 'immer';
import { produce } from 'immer';
import { useSpace } from '../../hooks/useSpace';
import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page';
import {

View File

@@ -1,6 +1,6 @@
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import produce from 'immer';
import { produce } from 'immer';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
import { useStateEventCallback } from './useStateEventCallback';

View File

@@ -143,13 +143,11 @@ const useBackgroundSync = (mx?: MatrixClient) => {
type ClientRootProps = {
children: ReactNode;
};
const MAX_SYNC_RETRIES = 3;
export function ClientRoot({ children }: ClientRootProps) {
const [loading, setLoading] = useState(true);
const [syncError, setSyncError] = useState<Error | null>(null);
const syncRetryCount = useRef(0);
const mxRef = useRef<MatrixClient | undefined>(undefined);
const recoverInFlight = useRef(false);
const { baseUrl } = getCurrentSession() ?? {};
// Enable view transitions for smooth navigation
@@ -202,31 +200,77 @@ export function ClientRoot({ children }: ClientRootProps) {
return () => clearTimeout(timeout);
}, [mx, loading]);
// Soft-recover from sync ERROR (e.g. sleep/wake with no network).
// Never block the UI — SyncStatus / title bar already show "Connection Lost".
useSyncState(
mx,
useCallback((state, prevState, data) => {
useCallback((state) => {
const client = mxRef.current;
if (state === 'PREPARED') {
if (state === SyncState.Prepared || state === SyncState.Syncing) {
setLoading(false);
setSyncError(null);
syncRetryCount.current = 0;
} else if (state === 'ERROR') {
if (syncRetryCount.current < MAX_SYNC_RETRIES) {
syncRetryCount.current += 1;
const attempt = syncRetryCount.current;
console.warn(`[ClientRoot] Sync error, auto-retrying (${attempt}/${MAX_SYNC_RETRIES})...`);
setTimeout(() => {
if (client) startClient(client).catch(() => {});
}, 2000 * attempt);
} else {
const error = data instanceof Error ? data : new Error('Sync failed');
console.error('[ClientRoot] Sync error after max retries:', error);
setSyncError(error);
}
recoverInFlight.current = false;
return;
}
if (state !== SyncState.Error || !client || recoverInFlight.current) {
return;
}
recoverInFlight.current = true;
console.warn('[ClientRoot] Sync error after sleep/network blip, soft-recovering...');
const recover = () => {
if (!mxRef.current) {
recoverInFlight.current = false;
return;
}
const c = mxRef.current;
if (!c.clientRunning) {
startClient(c)
.catch((err) => console.warn('[ClientRoot] Soft recover startClient failed:', err))
.finally(() => {
recoverInFlight.current = false;
});
return;
}
try {
c.retryImmediately();
} catch (err) {
console.warn('[ClientRoot] Soft recover retryImmediately failed:', err);
}
recoverInFlight.current = false;
};
// Give WiFi a moment to come back after lid open / resume
window.setTimeout(recover, 1500);
}, [])
);
// Also nudge sync when the laptop wakes / network returns
useEffect(() => {
if (!mx) return;
const nudge = () => {
if (document.visibilityState !== 'visible') return;
const syncState = mx.getSyncState();
if (syncState === SyncState.Error || syncState === SyncState.Reconnecting || !mx.clientRunning) {
console.log('[ClientRoot] Online/visible again, nudging sync', syncState);
if (!mx.clientRunning) {
startClient(mx).catch(() => {});
} else {
mx.retryImmediately();
}
}
};
window.addEventListener('online', nudge);
document.addEventListener('visibilitychange', nudge);
return () => {
window.removeEventListener('online', nudge);
document.removeEventListener('visibilitychange', nudge);
};
}, [mx]);
return (
<SpecVersions baseUrl={baseUrl!}>
<TitleBar mx={mx} />
@@ -251,23 +295,6 @@ export function ClientRoot({ children }: ClientRootProps) {
</Box>
</SplashScreen>
)}
{syncError && (
<SplashScreen>
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
<Dialog>
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
<Text>{`Sync failed: ${syncError.message}`}</Text>
<Text size="T300">Check your internet connection and homeserver status.</Text>
<Button variant="Critical" onClick={() => window.location.reload()}>
<Text as="span" size="B400">
Retry
</Text>
</Button>
</Box>
</Dialog>
</Box>
</SplashScreen>
)}
{loading || !mx ? (
<ClientRootLoading />
) : (

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai';
import produce from 'immer';
import { produce } from 'immer';
import {
atomWithLocalStorage,
getLocalStorageItem,

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai';
import produce from 'immer';
import { produce } from 'immer';
import {
atomWithLocalStorage,
getLocalStorageItem,

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai';
import produce from 'immer';
import { produce } from 'immer';
import { Path } from 'react-router-dom';
import {
atomWithLocalStorage,

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai';
import produce from 'immer';
import { produce } from 'immer';
import {
atomWithLocalStorage,
getLocalStorageItem,

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import { atom, useSetAtom } from 'jotai';
import {
ClientEvent,

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import { atom, useSetAtom } from 'jotai';
import {
IRoomTimelineData,

View File

@@ -1,5 +1,5 @@
import { atom } from 'jotai';
import produce from 'immer';
import { produce } from 'immer';
import {
atomWithLocalStorage,
getLocalStorageItem,

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import { atom, useSetAtom } from 'jotai';
import { MatrixClient, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk';
import { useEffect } from 'react';

View File

@@ -1,7 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import lottie from 'lottie-web/build/player/lottie_light';
// eslint-disable-next-line import/no-extraneous-dependencies
import pako from 'pako';
import { inflate } from 'pako';
// eslint-disable-next-line import/no-extraneous-dependencies
import GIF from 'gif.js';
@@ -26,7 +26,7 @@ const TRANSPARENT_COLOR = 0xFF00FF;
* @returns The Lottie animation JSON object
*/
function decompressTgs(tgsData: ArrayBuffer): object {
const decompressed = pako.inflate(new Uint8Array(tgsData), { to: 'string' });
const decompressed = inflate(new Uint8Array(tgsData), { to: 'string' });
return JSON.parse(decompressed);
}

View File

@@ -2,7 +2,7 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { enableMapSet } from 'immer';
import '@fontsource/inter/variable.css';
import '@fontsource-variable/inter';
import 'folds/dist/style.css';
import { configClass, varsClass } from 'folds';