Skip to content

Commit efcbda7

Browse files
ENG-780 - Tldraw race condition (#369)
* WIP upload to test * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * Working * Update apps/roam/src/components/canvas/Tldraw.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 2f8f435 commit efcbda7

3 files changed

Lines changed: 121 additions & 4 deletions

File tree

apps/roam/src/components/canvas/Tldraw.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
defaultEditorAssetUrls,
4242
usePreloadAssets,
4343
StateNode,
44+
DefaultSpinner,
4445
} from "tldraw";
4546
import "tldraw/tldraw.css";
4647
import tldrawStyles from "./tldrawStyles";
@@ -86,6 +87,7 @@ import sendErrorEmail from "~/utils/sendErrorEmail";
8687
import { TLDRAW_DATA_ATTRIBUTE } from "./tldrawStyles";
8788
import { AUTO_CANVAS_RELATIONS_KEY } from "~/data/userSettings";
8889
import { getSetting } from "~/utils/extensionSettings";
90+
import { isPluginTimerReady, waitForPluginTimer } from "~/utils/pluginTimer";
8991

9092
declare global {
9193
interface Window {
@@ -123,6 +125,38 @@ const TldrawCanvas = ({ title }: { title: string }) => {
123125
const [maximized, setMaximized] = useState(false);
124126
const [isConvertToDialogOpen, setConvertToDialogOpen] = useState(false);
125127

128+
// Workaround to avoid a race condition when loading a canvas page directly
129+
// Start false to avoid noisy warnings on first render if timer isn't initialized yet
130+
const [isPluginReady, setIsPluginReady] = useState(false);
131+
132+
useEffect(() => {
133+
let cancelled = false;
134+
135+
if (!isPluginReady) {
136+
// If already ready, flip immediately
137+
if (isPluginTimerReady()) {
138+
setIsPluginReady(true);
139+
return;
140+
}
141+
142+
// Otherwise, wait up to the timeout and proceed either way
143+
void (async () => {
144+
const ready = await waitForPluginTimer();
145+
if (cancelled) return;
146+
147+
if (!ready) {
148+
console.warn("Plugin timer timeout — proceeding with canvas mount anyway.");
149+
// Optional: dispatchToastEvent({ id: 'tldraw-plugin-timer-timeout', title: 'Timed out waiting for plugin init', severity: 'warning' })
150+
}
151+
152+
setIsPluginReady(true);
153+
})();
154+
}
155+
156+
return () => {
157+
cancelled = true;
158+
};
159+
}, [isPluginReady]);
126160
const allRelations = useMemo(() => {
127161
const relations = getDiscourseRelations();
128162
discourseContext.relations = relations.reduce(
@@ -520,7 +554,7 @@ const TldrawCanvas = ({ title }: { title: string }) => {
520554
</button>
521555
</div>
522556
</div>
523-
) : !store || !assetLoading.done || !extensionAPI ? (
557+
) : !store || !assetLoading.done || !extensionAPI || !isPluginReady ? (
524558
<div className="flex h-full items-center justify-center">
525559
<div className="text-center">
526560
<h2 className="mb-2 text-2xl font-semibold">
@@ -529,9 +563,11 @@ const TldrawCanvas = ({ title }: { title: string }) => {
529563
: "Loading Canvas"}
530564
</h2>
531565
<p className="mb-4 text-gray-600">
532-
{error || assetLoading.error
533-
? "There was a problem loading the Tldraw canvas. Please try again later."
534-
: ""}
566+
{error || assetLoading.error ? (
567+
"There was a problem loading the Tldraw canvas. Please try again later."
568+
) : (
569+
<DefaultSpinner />
570+
)}
535571
</p>
536572
</div>
537573
</div>

apps/roam/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
removeDiscourseFloatingMenu,
2929
} from "./components/DiscourseFloatingMenu";
3030
import { createOrUpdateDiscourseEmbedding } from "./utils/syncDgNodesToSupabase";
31+
import { initPluginTimer } from "./utils/pluginTimer";
3132

3233
const initPostHog = () => {
3334
posthog.init("phc_SNMmBqwNfcEpNduQ41dBUjtGNEUEKAy6jTn63Fzsrax", {
@@ -92,6 +93,8 @@ export default runExtension(async (onloadArgs) => {
9293
});
9394
}
9495

96+
initPluginTimer();
97+
9598
await initializeDiscourseNodes();
9699
refreshConfigTree();
97100

apps/roam/src/utils/pluginTimer.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Global timer utility for the Discourse Graph plugin
3+
* Tracks when 3 seconds have passed since plugin initialization
4+
* This helps prevent race conditions when rendering tldraw components
5+
*/
6+
7+
let pluginStartTime: number | null = null;
8+
let isTimerReady = false;
9+
10+
/**
11+
* Initialize the plugin timer
12+
* Should be called when the plugin first loads
13+
*/
14+
export const initPluginTimer = (): void => {
15+
pluginStartTime = Date.now();
16+
17+
// Set a timeout to mark the timer as ready after 3 seconds
18+
setTimeout(() => {
19+
isTimerReady = true;
20+
}, 3000);
21+
};
22+
23+
/**
24+
* Check if the plugin timer is ready (3 seconds have passed)
25+
* @returns true if 3 seconds have passed since plugin initialization
26+
*/
27+
export const isPluginTimerReady = (): boolean => {
28+
if (pluginStartTime === null) {
29+
console.warn("Plugin timer not initialized");
30+
return false;
31+
}
32+
33+
return isTimerReady;
34+
};
35+
36+
/**
37+
* Get the elapsed time since plugin initialization
38+
* @returns elapsed time in milliseconds, or 0 if not initialized
39+
*/
40+
export const getPluginElapsedTime = (): number => {
41+
if (pluginStartTime === null) {
42+
return 0;
43+
}
44+
const time = Date.now() - pluginStartTime;
45+
return time;
46+
};
47+
48+
/**
49+
* Wait for the plugin timer to be ready
50+
* @param timeoutMs maximum time to wait (default: 5000ms)
51+
* @returns Promise that resolves when timer is ready or timeout is reached
52+
*/
53+
export const waitForPluginTimer = (
54+
timeoutMs: number = 5000,
55+
): Promise<boolean> => {
56+
return new Promise((resolve) => {
57+
if (isPluginTimerReady()) {
58+
resolve(true);
59+
return;
60+
}
61+
62+
const startTime = Date.now();
63+
const checkInterval = setInterval(() => {
64+
if (isPluginTimerReady()) {
65+
clearInterval(checkInterval);
66+
resolve(true);
67+
return;
68+
}
69+
70+
const elapsed = Date.now() - startTime;
71+
if (elapsed >= timeoutMs) {
72+
clearInterval(checkInterval);
73+
resolve(false);
74+
return;
75+
}
76+
}, 100);
77+
});
78+
};

0 commit comments

Comments
 (0)