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:
@@ -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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 Wi‑Fi 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 />
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WritableAtom, atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WritableAtom, atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WritableAtom, atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import { atom, useSetAtom } from 'jotai';
|
||||
import {
|
||||
ClientEvent,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import { atom, useSetAtom } from 'jotai';
|
||||
import {
|
||||
IRoomTimelineData,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user