new demo mode & tenure
Some checks failed
Build and publish Docker image / build-and-push (push) Successful in 8m43s
CI / Check (push) Failing after 8m49s

This commit is contained in:
Andras Schmelczer 2026-06-17 07:54:30 +01:00
parent 7656f24544
commit 4a0f00f2a4
64 changed files with 2875 additions and 338 deletions

View file

@ -0,0 +1,141 @@
import { Component, Fragment, type ReactNode } from 'react';
import * as Sentry from '@sentry/react';
import { MapFallback } from './map-page/Fallbacks';
/**
* Scoped error boundary for the WebGL map.
*
* deck.gl runs in interleaved mode, sharing MapLibre's WebGL context. When that
* shared context gets into a bad state (lost context, a buffer/texture finalized
* while still referenced the "bufferSubData: no buffer" / "bindTexture: deleted
* object" storm), the next interleaved draw can throw. deck.gl's own animation
* loop swallows render errors via its `onError` prop, but interleaved draws fire
* from MapLibre's synchronous `render` event, which can run inside a React commit
* so the throw bubbles to the nearest React error boundary.
*
* Without this, that throw reaches the single top-level boundary and blanks the
* entire app ("Something went wrong / Refresh the page"). This boundary contains
* the blast radius to the map and auto-recovers by remounting it, which builds a
* fresh MapLibre + deck.gl context. Filters, selection and viewport live above
* this boundary (in MapPage / the URL), so a remount preserves them.
*/
const MAX_AUTO_RECOVERIES = 2;
// Remount after a short delay so a transient GL state can settle first.
const RECOVERY_DELAY_MS = 400;
// Crashes spaced further apart than this are treated as independent, so the
// auto-recovery budget resets — only a tight crash loop escalates to manual retry.
const STABILITY_WINDOW_MS = 30_000;
interface MapErrorBoundaryProps {
children: ReactNode;
}
interface MapErrorBoundaryState {
errored: boolean;
failedPermanently: boolean;
renderKey: number;
recoveries: number;
}
export class MapErrorBoundary extends Component<MapErrorBoundaryProps, MapErrorBoundaryState> {
state: MapErrorBoundaryState = {
errored: false,
failedPermanently: false,
renderKey: 0,
recoveries: 0,
};
private recoveryTimer: ReturnType<typeof setTimeout> | null = null;
private lastErrorAt = 0;
static getDerivedStateFromError(): Partial<MapErrorBoundaryState> {
return { errored: true };
}
componentDidCatch(error: Error, info: { componentStack?: string | null }) {
const now = Date.now();
const withinWindow = now - this.lastErrorAt < STABILITY_WINDOW_MS;
this.lastErrorAt = now;
const recoveries = withinWindow ? this.state.recoveries + 1 : 1;
Sentry.captureException(error, {
tags: { scope: 'map' },
extra: { componentStack: info.componentStack, recoveries },
});
if (recoveries <= MAX_AUTO_RECOVERIES) {
this.scheduleRecovery(recoveries);
} else {
this.setState({ failedPermanently: true, recoveries });
}
}
componentWillUnmount() {
this.clearRecoveryTimer();
}
private clearRecoveryTimer() {
if (this.recoveryTimer) {
clearTimeout(this.recoveryTimer);
this.recoveryTimer = null;
}
}
private scheduleRecovery(recoveries: number) {
this.clearRecoveryTimer();
this.recoveryTimer = setTimeout(() => {
this.recoveryTimer = null;
this.setState((prev) => ({
errored: false,
failedPermanently: false,
renderKey: prev.renderKey + 1,
recoveries,
}));
}, RECOVERY_DELAY_MS);
}
private handleManualRetry = () => {
this.clearRecoveryTimer();
this.lastErrorAt = 0;
this.setState((prev) => ({
errored: false,
failedPermanently: false,
renderKey: prev.renderKey + 1,
recoveries: 0,
}));
};
render() {
if (this.state.failedPermanently) {
return (
<div className="flex h-full w-full items-center justify-center bg-warm-100 px-6 text-center dark:bg-navy-950">
<div>
<h2 className="text-lg font-semibold text-warm-900 dark:text-warm-100">
The map ran into a problem
</h2>
<p className="mt-2 text-sm text-warm-600 dark:text-warm-300">
This can happen when your browser&apos;s graphics context is interrupted.
</p>
<button
type="button"
onClick={this.handleManualRetry}
className="mt-4 rounded-lg bg-teal-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-teal-700 dark:bg-teal-500 dark:hover:bg-teal-400"
>
Reload the map
</button>
</div>
</div>
);
}
// While auto-recovering, show the same spinner used for the lazy-load
// fallback so the remount reads as a brief reload rather than a crash.
if (this.state.errored) {
return <MapFallback />;
}
return <Fragment key={this.state.renderKey}>{this.props.children}</Fragment>;
}
}