Support runAsync with the cross-site linker.

Review at http://gwt-code-reviews.appspot.com/213801

Review by: robertvawter@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7806 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
index 7a805d0..9580913 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
@@ -186,7 +186,8 @@
     byte[][] bytes = new byte[js.length][];
     bytes[0] = generatePrimaryFragment(logger, context, result, js);
     for (int i = 1; i < js.length; i++) {
-      bytes[i] = Util.getBytes(js[i]);
+      bytes[i] = Util.getBytes(generateDeferredFragment(logger, context, i,
+          js[i]));
     }
 
     Collection<Artifact<?>> toReturn = new ArrayList<Artifact<?>>();
@@ -225,6 +226,11 @@
         + ".nocache.js", lastModified);
   }
 
+  protected String generateDeferredFragment(TreeLogger logger,
+      LinkerContext context, int fragment, String js) {
+    return js;
+  }
+
   /**
    * Generate the primary fragment. The default implementation is based on
    * {@link #getModulePrefix(TreeLogger, LinkerContext, String, int)} and
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
index e0c972b..ccc283c 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
@@ -57,6 +57,12 @@
   private static final String CHUNK_SIZE_PROPERTY = "iframe.linker.script.chunk.size";
 
   /**
+   * A configuration property that can be used to have the linker load from
+   * somewhere other than {@link #FRAGMENT_SUBDIR}
+   */
+  private static final String PROP_FRAGMENT_SUBDIR_OVERRIDE = "iframe.linker.deferredjs.subdir";
+
+  /**
    * Split a JavaScript string into multiple chunks, at statement boundaries.
    * Insert and end-script tag and a start-script tag in between each chunk.
    * This method is made default access for testing.
@@ -187,23 +193,38 @@
 
   /**
    * Returns the subdirectory name to be used by getModulPrefix when requesting
-   * a runAsync module. The default implementation returns the value of
-   * FRAGMENT_SUDBIR. This has been factored out for test cases.
+   * a runAsync module. It is specified by
+   * {@link #PROP_FRAGMENT_SUBDIR_OVERRIDE} and, aside from test cases, is
+   * always {@link #FRAGMENT_SUBDIR}.
    */
-  protected String getFragmentSubdir() {
-    return FRAGMENT_SUBDIR;
+  protected final String getFragmentSubdir(TreeLogger logger,
+      LinkerContext context) throws UnableToCompleteException {
+    String subdir = null;
+    for (ConfigurationProperty prop : context.getConfigurationProperties()) {
+      if (prop.getName().equals(PROP_FRAGMENT_SUBDIR_OVERRIDE)) {
+        subdir = prop.getValues().get(0);
+      }
+    }
+
+    if (subdir == null) {
+      logger.log(TreeLogger.ERROR, "Could not find property "
+          + PROP_FRAGMENT_SUBDIR_OVERRIDE);
+      throw new UnableToCompleteException();
+    }
+
+    return subdir;
   }
 
   @Override
   protected String getModulePrefix(TreeLogger logger, LinkerContext context,
-      String strongName) {
-    return getModulePrefix(context, strongName, true);
+      String strongName) throws UnableToCompleteException {
+    return getModulePrefix(logger, context, strongName, true);
   }
 
   @Override
   protected String getModulePrefix(TreeLogger logger, LinkerContext context,
-      String strongName, int numFragments) {
-    return getModulePrefix(context, strongName, numFragments > 1);
+      String strongName, int numFragments) throws UnableToCompleteException {
+    return getModulePrefix(logger, context, strongName, numFragments > 1);
   }
 
   @Override
@@ -257,8 +278,9 @@
    * This is the real implementation of <code>getModulePrefix</code> for this
    * linker. The other versions forward to this one.
    */
-  private String getModulePrefix(LinkerContext context, String strongName,
-      boolean supportRunAsync) {
+  private String getModulePrefix(TreeLogger logger, LinkerContext context,
+      String strongName, boolean supportRunAsync)
+      throws UnableToCompleteException {
     DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
     out.print("<html>");
     out.newlineOpt();
@@ -280,7 +302,7 @@
       out.print("function __gwtStartLoadingFragment(frag) {");
       out.indentIn();
       out.newlineOpt();
-      out.print("  return $moduleBase + '" + getFragmentSubdir()
+      out.print("  return $moduleBase + '" + getFragmentSubdir(logger, context)
           + "/'  + $strongName + '/' + frag + '" + FRAGMENT_EXTENSION + "';");
       out.indentOut();
       out.newlineOpt();
diff --git a/dev/core/src/com/google/gwt/core/linker/XSLinker.java b/dev/core/src/com/google/gwt/core/linker/XSLinker.java
index 6dbedba..1ffc1f6 100644
--- a/dev/core/src/com/google/gwt/core/linker/XSLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/XSLinker.java
@@ -17,18 +17,14 @@
 
 import com.google.gwt.core.ext.LinkerContext;
 import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.linker.Artifact;
-import com.google.gwt.core.ext.linker.CompilationResult;
 import com.google.gwt.core.ext.linker.LinkerOrder;
 import com.google.gwt.core.ext.linker.Shardable;
 import com.google.gwt.core.ext.linker.LinkerOrder.Order;
 import com.google.gwt.core.ext.linker.impl.SelectionScriptLinker;
 import com.google.gwt.dev.About;
+import com.google.gwt.dev.js.JsToStringGenerationVisitor;
 import com.google.gwt.dev.util.DefaultTextOutput;
 
-import java.util.Collection;
-
 /**
  * Generates a cross-site compatible bootstrap sequence.
  */
@@ -41,27 +37,65 @@
   }
 
   @Override
-  protected Collection<Artifact<?>> doEmitCompilation(TreeLogger logger,
-      LinkerContext context, CompilationResult result)
-      throws UnableToCompleteException {
-    if (result.getJavaScript().length != 1) {
-      logger.branch(TreeLogger.ERROR,
-          "The module must not have multiple fragments when using the "
-              + getDescription() + " Linker.", null);
-      throw new UnableToCompleteException();
-    }
-    return super.doEmitCompilation(logger, context, result);
+  protected String generateDeferredFragment(TreeLogger logger,
+      LinkerContext context, int fragment, String js) {
+    StringBuilder sb = new StringBuilder();
+    sb.append(context.getModuleFunctionName());
+    sb.append(".runAsyncCallback");
+    sb.append(fragment);
+    sb.append("(");
+    sb.append(JsToStringGenerationVisitor.javaScriptString(js));
+    sb.append(");\n");
+    return sb.toString();
   }
 
   @Override
   protected String getCompilationExtension(TreeLogger logger,
-      LinkerContext context) throws UnableToCompleteException {
+      LinkerContext context) {
     return ".cache.js";
   }
 
   @Override
   protected String getModulePrefix(TreeLogger logger, LinkerContext context,
-      String strongName) throws UnableToCompleteException {
+      String strongName) {
+    return getModulePrefix(context, strongName, true);
+  }
+
+  @Override
+  protected String getModulePrefix(TreeLogger logger, LinkerContext context,
+      String strongName, int numFragments) {
+    return getModulePrefix(context, strongName, numFragments > 1);
+  }
+
+  @Override
+  protected String getModuleSuffix(TreeLogger logger, LinkerContext context) {
+    DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
+
+    out.print("$stats && $stats({moduleName:'" + context.getModuleName()
+        + "',sessionId:$sessionId"
+        + ",subSystem:'startup',evtGroup:'moduleStartup'"
+        + ",millis:(new Date()).getTime(),type:'moduleEvalEnd'});");
+
+    // Generate the call to tell the bootstrap code that we're ready to go.
+    out.newlineOpt();
+    out.print("if (" + context.getModuleFunctionName() + " && "
+        + context.getModuleFunctionName() + ".onScriptLoad)"
+        + context.getModuleFunctionName() + ".onScriptLoad(gwtOnLoad);");
+    out.newlineOpt();
+    out.print("})();");
+    out.newlineOpt();
+
+    return out.toString();
+  }
+
+  @Override
+  protected String getSelectionScriptTemplate(TreeLogger logger,
+      LinkerContext context) {
+    return "com/google/gwt/core/linker/XSTemplate.js";
+  }
+
+  private String getModulePrefix(LinkerContext context, String strongName,
+      boolean supportRunAsync) {
     DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
 
     out.print("(function(){");
@@ -83,40 +117,23 @@
     out.newlineOpt();
     out.print("var $sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null;");
     out.newlineOpt();
+
     out.print("$stats && $stats({moduleName:'" + context.getModuleName()
         + "',sessionId:$sessionId"
         + ",subSystem:'startup',evtGroup:'moduleStartup'"
         + ",millis:(new Date()).getTime(),type:'moduleEvalStart'});");
     out.newlineOpt();
 
-    return out.toString();
-  }
-
-  @Override
-  protected String getModuleSuffix(TreeLogger logger, LinkerContext context)
-      throws UnableToCompleteException {
-    DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
-
-    out.print("$stats && $stats({moduleName:'" + context.getModuleName()
-        + "',sessionId:$sessionId"
-        + ",subSystem:'startup',evtGroup:'moduleStartup'"
-        + ",millis:(new Date()).getTime(),type:'moduleEvalEnd'});");
-
-    // Generate the call to tell the bootstrap code that we're ready to go.
-    out.newlineOpt();
-    out.print("if (" + context.getModuleFunctionName() + ") "
-        + context.getModuleFunctionName() + ".onScriptLoad(gwtOnLoad);");
-    out.newlineOpt();
-    out.print("})();");
-    out.newlineOpt();
+    if (supportRunAsync) {
+      out.print(context.getModuleFunctionName());
+      out.print(".installCode = function(code) { eval(code) };");
+      out.newlineOpt();
+      out.print("var __gwtModuleFunction = ");
+      out.print(context.getModuleFunctionName());
+      out.print(";");
+      out.newline();
+    }
 
     return out.toString();
   }
-
-  @Override
-  protected String getSelectionScriptTemplate(TreeLogger logger, LinkerContext context)
-      throws UnableToCompleteException {
-    return "com/google/gwt/core/linker/XSTemplate.js";
-  }
-
 }
diff --git a/dev/core/src/com/google/gwt/core/linker/XSTemplate.js b/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
index 5200a69..81498f1 100644
--- a/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
+++ b/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
@@ -21,6 +21,7 @@
   var $wnd = window
   ,$doc = document
   ,$stats = $wnd.__gwtStatsEvent ? function(a) {return $wnd.__gwtStatsEvent(a);} : null
+  ,$sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null
 
   // These variables gate calling gwtOnLoad; all must be true to start
   ,gwtOnLoad, bodyDone
@@ -163,8 +164,8 @@
   // Called when the compiled script identified by moduleName is done loading.
   //
   __MODULE_FUNC__.onScriptLoad = function(gwtOnLoadFunc) {
-    // remove this whole function from the global namespace to allow GC
-    __MODULE_FUNC__ = null;
+    // remove the callback to prevent it being called twice
+    __MODULE_FUNC__.onScriptLoad = null;
     gwtOnLoad = gwtOnLoadFunc;
     maybeStartModule();
   }
@@ -270,17 +271,17 @@
   // from *within* the stats script, guaranteeing order at the expense of near
   // total inscrutability :(
   var compiledScriptTag = '"<script src=\\"' + base + strongName + '.cache.js\\"></scr" + "ipt>"';
-  $doc.write('<script><!--\n'
+  $doc.write('<scr' + 'ipt><!-' + '-\n'
     + 'window.__gwtStatsEvent && window.__gwtStatsEvent({'
-    + 'moduleName:"__MODULE_NAME__", sessionId:$sessionId, subSystem:"startup",'
+    + 'moduleName:"__MODULE_NAME__", sessionId:window.__gwtStatsSessionId, subSystem:"startup",'
     + 'evtGroup: "loadExternalRefs", millis:(new Date()).getTime(),'
     + 'type: "end"});'
     + 'window.__gwtStatsEvent && window.__gwtStatsEvent({'
-    + 'moduleName:"__MODULE_NAME__", sessionId:$sessionId, subSystem:"startup",'
+    + 'moduleName:"__MODULE_NAME__", sessionId:window.__gwtStatsSessionId, subSystem:"startup",'
     + 'evtGroup: "moduleStartup", millis:(new Date()).getTime(),'
     + 'type: "moduleRequested"});'
     + 'document.write(' + compiledScriptTag + ');'
-    + '\n--></script>');
+    + '\n-' + '-></scr' + 'ipt>');
 }
 
 __MODULE_FUNC__();
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index bf07384..91b1579 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -69,6 +69,7 @@
 import com.google.gwt.dev.jjs.impl.FragmentLoaderCreator;
 import com.google.gwt.dev.jjs.impl.GenerateJavaAST;
 import com.google.gwt.dev.jjs.impl.GenerateJavaScriptAST;
+import com.google.gwt.dev.jjs.impl.HandleCrossFragmentReferences;
 import com.google.gwt.dev.jjs.impl.JavaScriptObjectNormalizer;
 import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
 import com.google.gwt.dev.jjs.impl.JsFunctionClusterer;
@@ -234,8 +235,8 @@
       JProgram jprogram = ast.getJProgram();
       JsProgram jsProgram = ast.getJsProgram();
       JJSOptions options = unifiedAst.getOptions();
-      Map<StandardSymbolData, JsName> symbolTable = new TreeMap<StandardSymbolData, JsName>(
-          new SymbolData.ClassIdentComparator());
+      Map<StandardSymbolData, JsName> symbolTable =
+          new TreeMap<StandardSymbolData, JsName>(new SymbolData.ClassIdentComparator());
 
       ResolveRebinds.exec(jprogram, permutation.getOrderedRebindAnswers());
 
@@ -345,6 +346,11 @@
         default:
           throw new InternalCompilerException("Unknown output mode");
       }
+      
+      // (10.8) Handle cross-island references.
+      // No new JsNames or references to JSNames can be introduced after this
+      // point.
+      HandleCrossFragmentReferences.exec(logger, jsProgram, propertyOracles);
 
       // (11) Perform any post-obfuscation normalizations.
 
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
index da27c6d..bfa8289 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
@@ -144,8 +144,8 @@
     srcWriter.println("if (!" + BROWSER_LOADER + ".isLoading(" + entryNumber
         + ")) {");
     srcWriter.println("  " + BROWSER_LOADER + ".inject(" + entryNumber + ",");
-    srcWriter.println("  new AsyncFragmentLoader.LoadErrorHandler() {");
-    srcWriter.println("    public void loadFailed(Throwable reason) {");
+    srcWriter.println("  new AsyncFragmentLoader.LoadTerminatedHandler() {");
+    srcWriter.println("    public void loadTerminated(Throwable reason) {");
     srcWriter.println("      runCallbackOnFailures(reason);");
     srcWriter.println("    }");
     srcWriter.println("  });");
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java b/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java
new file mode 100644
index 0000000..31a9b49
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.impl;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.dev.js.ast.JsBinaryOperation;
+import com.google.gwt.dev.js.ast.JsBinaryOperator;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsExpression;
+import com.google.gwt.dev.js.ast.JsFunction;
+import com.google.gwt.dev.js.ast.JsModVisitor;
+import com.google.gwt.dev.js.ast.JsName;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.dev.js.ast.JsObjectLiteral;
+import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsVars;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.js.ast.JsVars.JsVar;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * Rewrite JavaScript to better handle references from one code fragment to
+ * another. For any function defined off the initial download and accessed from
+ * a different island than the one it's defined on, predefine a variable in the
+ * initial download to hold its definition.
+ */
+public class HandleCrossFragmentReferences {
+  /**
+   * Find out which islands define and use each named function or variable.
+   * This visitor is not smart about which definitions and uses matter.  It
+   * blindly records all of them.
+   */
+  private class FindNameReferences extends JsVisitor {
+    Map<JsName, Set<Integer>> islandsDefining = new LinkedHashMap<JsName, Set<Integer>>();
+    Map<JsName, Set<Integer>> islandsUsing = new LinkedHashMap<JsName, Set<Integer>>();
+    private int currentIsland;
+
+    @Override
+    public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
+      JsName name = x.getName();
+      if (name != null) {
+        definitionSeen(name);
+      }
+    }
+
+    @Override
+    public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
+      if (x.getQualifier() == null) {
+        JsName name = x.getName();
+        if (name != null) {
+          referenceSeen(name);
+        }
+      }
+    }
+
+    @Override
+    public void endVisit(JsVars x, JsContext<JsStatement> ctx) {
+      for (JsVar var : x) {
+        JsName name = var.getName();
+        if (name != null) {
+          definitionSeen(name);
+        }
+      }
+    }
+
+    @Override
+    public boolean visit(JsProgram x, JsContext<JsProgram> ctx) {
+      for (int i = 0; i < x.getFragmentCount(); i++) {
+        currentIsland = i;
+        accept(x.getFragmentBlock(i));
+      }
+
+      return false;
+    }
+
+    private void definitionSeen(JsName name) {
+      /*
+       * Support multiple definitions, because local variables can reuse the
+       * same name.
+       */
+      Set<Integer> defs = islandsDefining.get(name);
+      if (defs == null) {
+        defs = new LinkedHashSet<Integer>();
+        islandsDefining.put(name, defs);
+      }
+      defs.add(currentIsland);
+    }
+
+    private void referenceSeen(JsName name) {
+      Set<Integer> refs = islandsUsing.get(name);
+      if (refs == null) {
+        refs = new HashSet<Integer>();
+        islandsUsing.put(name, refs);
+      }
+      refs.add(currentIsland);
+    }
+  }
+
+  /**
+   * Rewrite var and function declarations as assignments, if their name is
+   * accessed cross-island. Rewrite refs to such names correspondingly.
+   */
+  private class RewriteDeclsAndRefs extends JsModVisitor {
+    @Override
+    public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
+      if (namesToPredefine.contains(x.getName())) {
+        JsBinaryOperation asg = new JsBinaryOperation(x.getSourceInfo(),
+            JsBinaryOperator.ASG, makeRefViaJslink(x.getName(),
+                x.getSourceInfo()), x);
+        x.setName(null);
+        ctx.replaceMe(asg);
+      }
+    }
+
+    @Override
+    public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
+      if (namesToPredefine.contains(x.getName())) {
+        ctx.replaceMe(makeRefViaJslink(x.getName(), x.getSourceInfo()));
+      }
+    }
+
+    @Override
+    public void endVisit(JsVars x, JsContext<JsStatement> ctx) {
+      if (!ctx.canInsert()) {
+        return;
+      }
+
+      /*
+       * Loop through each var and see if it was predefined. If so, then remove
+       * the var. If the var has an initializer, then add back an assignment
+       * statement to initialize it. If there is no initializer, then don't add
+       * anything back; the var will still have undefined as its initial value,
+       * just like before.
+       * 
+       * A complication is that the variables that are predefined might be
+       * interspersed with variables that are not. That means the general result
+       * of this transformation has alternating var lists and assignment
+       * statements. The currentVar variable holds the most recently inserted
+       * statement, if that statement was a JsVars; otherwise it holds null.
+       */
+
+      JsVars currentVar = null;
+      Iterator<JsVar> varsIterator = x.iterator();
+      while (varsIterator.hasNext()) {
+        JsVar var = varsIterator.next();
+        if (namesToPredefine.contains(var.getName())) {
+          // The var was predefined
+          if (var.getInitExpr() != null) {
+            // If it has an initializer, add an assignment statement
+            JsBinaryOperation asg = new JsBinaryOperation(var.getSourceInfo(),
+                JsBinaryOperator.ASG, makeRefViaJslink(var.getName(),
+                    var.getSourceInfo()), var.getInitExpr());
+            ctx.insertBefore(asg.makeStmt());
+            currentVar = null;
+          }
+        } else {
+          // The var was not predefined; add it to a var list
+          if (currentVar == null) {
+            currentVar = new JsVars(x.getSourceInfo());
+            ctx.insertBefore(currentVar);
+          }
+          currentVar.add(var);
+        }
+      }
+
+      ctx.removeMe();
+    }
+
+    private JsNameRef makeRefViaJslink(JsName name, SourceInfo sourceInfo) {
+      JsNameRef ref = name.makeRef(sourceInfo);
+      ref.setQualifier(jslink.makeRef(sourceInfo));
+      return ref;
+    }
+  }
+
+  public static String PROP_PREDECLARE_VARS = "compiler.predeclare.cross.fragment.references";
+
+  public static void exec(TreeLogger logger, JsProgram jsProgram,
+      PropertyOracle[] propertyOracles) {
+    new HandleCrossFragmentReferences(logger, jsProgram, propertyOracles).execImpl();
+  }
+
+  private static boolean containsOtherThan(Set<Integer> set, int allowed) {
+    for (int elem : set) {
+      if (elem != allowed) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private JsName jslink;
+  private final JsProgram jsProgram;
+  private final Set<JsName> namesToPredefine = new LinkedHashSet<JsName>();
+  private final PropertyOracle[] propertyOracles;
+  private final TreeLogger logger;
+
+  private HandleCrossFragmentReferences(TreeLogger logger, JsProgram jsProgram,
+      PropertyOracle[] propertyOracles) {
+    this.logger = logger;
+    this.jsProgram = jsProgram;
+    this.propertyOracles = propertyOracles;
+  }
+
+  private void chooseNamesToPredefine(Map<JsName, Set<Integer>> map,
+      Map<JsName, Set<Integer>> islandsUsing) {
+    for (Entry<JsName, Set<Integer>> entry : map.entrySet()) {
+      JsName name = entry.getKey();
+      Set<Integer> defIslands = entry.getValue();
+      if (defIslands.size() != 1) {
+        // Only rewrite global variables, which should have exactly one
+        // definition
+        continue;
+      }
+      int defIsland = defIslands.iterator().next();
+      if (defIsland == 0) {
+        // Variables defined on the base island can be accessed directly from
+        // other islands
+        continue;
+      }
+      Set<Integer> useIslands = islandsUsing.get(name);
+      if (useIslands == null) {
+        // The variable is never used. Leave it alone.
+        continue;
+      }
+
+      if (containsOtherThan(islandsUsing.get(name), defIsland)) {
+        namesToPredefine.add(name);
+      }
+    }
+  }
+
+  /**
+   * Define the jslink object that will be used to fix up cross-island
+   * references.
+   */
+  private void defineJsLink() {
+    SourceInfo info = jsProgram.createSourceInfoSynthetic(
+        HandleCrossFragmentReferences.class, "defining jslink");
+    jslink = jsProgram.getScope().declareName("jslink");
+    JsVars vars = new JsVars(info);
+    JsVar var = new JsVar(info, jslink);
+    var.setInitExpr(new JsObjectLiteral(info));
+    vars.add(var);
+    jsProgram.getFragmentBlock(0).getStatements().add(0, vars);
+  }
+
+  private void execImpl() {
+    if (jsProgram.getFragmentCount() == 1) {
+      return;
+    }
+    if (!shouldPredeclareReferences()) {
+      return;
+    }
+    defineJsLink();
+    FindNameReferences findNameReferences = new FindNameReferences();
+    findNameReferences.accept(jsProgram);
+    chooseNamesToPredefine(findNameReferences.islandsDefining,
+        findNameReferences.islandsUsing);
+    new RewriteDeclsAndRefs().accept(jsProgram);
+  }
+
+  /**
+   * Check the property oracles for whether references should be predeclared or
+   * not. If any of them say yes, then do the rewrite.
+   */
+  private boolean shouldPredeclareReferences() {
+    for (PropertyOracle props : propertyOracles) {
+      try {
+        String propValue = props.getSelectionProperty(logger,
+            PROP_PREDECLARE_VARS).getCurrentValue();
+        if (Boolean.parseBoolean(propValue)) {
+          return true;
+        }
+      } catch (BadPropertyValueException e) {
+        // Property not defined; don't rewrite
+      }
+    }
+
+    return false;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
index 9e56801..4bdaff8 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2009 Google Inc.
- *
+ * 
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- *
+ * 
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * 
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -23,15 +23,19 @@
 import java.util.List;
 import java.util.LinkedList;
 import java.util.Iterator;
+import java.util.regex.Pattern;
 
 /**
  * Re-orders function declarations according to a given metric and clustering
  * algorithm in order to boost gzip/deflation compression efficiency. This
  * version uses the edit-distance algorithm as a metric, and a semi-greedy
- * strategy for grouping functions together. 
+ * strategy for grouping functions together.
  */
 public class JsFunctionClusterer extends JsAbstractTextTransformer {
 
+  private static Pattern functionPattern =
+      Pattern.compile("^(function |[A-Za-z0-9_$]+=function)");
+
   /**
    * Limit edit-distance search to MAX_DIST.
    */
@@ -39,25 +43,25 @@
 
   private static final int MAX_DISTANCE_LIMIT = 100;
 
-  private List<Integer> functionIndices;
-
   private int[] clusteredIndices;
 
-  public JsFunctionClusterer(String js, StatementRanges statementRanges) {
-    super(js, statementRanges);
-  }
+  private List<Integer> functionIndices;
 
   public JsFunctionClusterer(JsAbstractTextTransformer xformer) {
     super(xformer);
   }
 
+  public JsFunctionClusterer(String js, StatementRanges statementRanges) {
+    super(js, statementRanges);
+  }
+
   public void exec() {
     functionIndices = new LinkedList<Integer>();
 
     // gather up all of the indices of function decl statements
     for (int i = 0; i < statementRanges.numStatements(); i++) {
       String code = getJsForRange(i);
-      if (code.startsWith("function")) {
+      if (functionPattern.matcher(code).find()) {
         functionIndices.add(i);
       }
     }
@@ -88,8 +92,7 @@
       Iterator<Integer> it = functionIndices.iterator();
       int count = 0;
       // search up to MAX_DIST functions for the best match
-      while (it.hasNext() &&
-             count < Math.min(MAX_DIST, functionIndices.size())) {
+      while (it.hasNext() && count < Math.min(MAX_DIST, functionIndices.size())) {
         int functionIndex = it.next();
         String testCode = getJsForRange(functionIndex);
         int distanceLimit = Math.min(bestDistance, MAX_DISTANCE_LIMIT);
@@ -172,8 +175,8 @@
       while (j <= jmax) {
         char c2 = str2.charAt(j - 1);
         int costSwap = c1 == c2 ? 0 : 1;
-        nextRow[j] = Math.min(Math.min(lastRow[j] + 1, nextRow[j - 1] + 1),
-            lastRow[j - 1] + costSwap);
+        nextRow[j] =
+            Math.min(Math.min(lastRow[j] + 1, nextRow[j - 1] + 1), lastRow[j - 1] + costSwap);
         j = j + 1;
       }
       int tmpRow[] = nextRow;
diff --git a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
index 56b8200..a363f88 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
@@ -125,8 +125,136 @@
    */
   private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[a-zA-Z_$][\\w$]*");
 
-  protected boolean needSemi = true;
+  /**
+   * Generate JavaScript code that evaluates to the supplied string. Adapted
+   * from {@link com.google.gwt.dev.js.rhino.ScriptRuntime#escapeString(String)}
+   * . The difference is that we quote with either &quot; or &apos; depending on
+   * which one is used less inside the string.
+   */
+  public static String javaScriptString(String value) {
+    char[] chars = value.toCharArray();
+    final int n = chars.length;
+    int quoteCount = 0;
+    int aposCount = 0;
+    for (int i = 0; i < n; ++i) {
+      switch (chars[i]) {
+        case '"':
+          ++quoteCount;
+          break;
+        case '\'':
+          ++aposCount;
+          break;
+      }
+    }
 
+    StringBuffer result = new StringBuffer(value.length() + 16);
+
+    char quoteChar = (quoteCount < aposCount) ? '"' : '\'';
+    result.append(quoteChar);
+
+    for (int i = 0; i < n; ++i) {
+      char c = chars[i];
+
+      if (' ' <= c && c <= '~' && c != quoteChar && c != '\\') {
+        // an ordinary print character (like C isprint())
+        result.append(c);
+        continue;
+      }
+
+      int escape = -1;
+      switch (c) {
+        case '\b':
+          escape = 'b';
+          break;
+        case '\f':
+          escape = 'f';
+          break;
+        case '\n':
+          escape = 'n';
+          break;
+        case '\r':
+          escape = 'r';
+          break;
+        case '\t':
+          escape = 't';
+          break;
+        case '"':
+          escape = '"';
+          break; // only reach here if == quoteChar
+        case '\'':
+          escape = '\'';
+          break; // only reach here if == quoteChar
+        case '\\':
+          escape = '\\';
+          break;
+      }
+
+      if (escape >= 0) {
+        // an \escaped sort of character
+        result.append('\\');
+        result.append((char) escape);
+      } else {
+        /*
+         * Emit characters from 0 to 31 that don't have a single character
+         * escape sequence in octal where possible. This saves one or two
+         * characters compared to the hexadecimal format '\xXX'.
+         * 
+         * These short octal sequences may only be used at the end of the string
+         * or where the following character is a non-digit. Otherwise, the
+         * following character would be incorrectly interpreted as belonging to
+         * the sequence.
+         */
+        if (c < ' ' && (i == n - 1 || chars[i + 1] < '0' || chars[i + 1] > '9')) {
+          result.append('\\');
+          if (c > 0x7) {
+            result.append((char) ('0' + (0x7 & (c >> 3))));
+          }
+          result.append((char) ('0' + (0x7 & c)));
+        } else {
+          int hexSize;
+          if (c < 256) {
+            // 2-digit hex
+            result.append("\\x");
+            hexSize = 2;
+          } else {
+            // Unicode.
+            result.append("\\u");
+            hexSize = 4;
+          }
+          // append hexadecimal form of ch left-padded with 0
+          for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) {
+            int digit = 0xf & (c >> shift);
+            result.append(HEX_DIGITS[digit]);
+          }
+        }
+      }
+    }
+    result.append(quoteChar);
+    escapeClosingTags(result);
+    String resultString = result.toString();
+    return resultString;
+  }
+
+  /**
+   * Escapes any closing XML tags embedded in <code>str</code>, which could
+   * potentially cause a parse failure in a browser, for example, embedding a
+   * closing <code>&lt;script&gt;</code> tag.
+   * 
+   * @param str an unescaped literal; May be null
+   */
+  private static void escapeClosingTags(StringBuffer str) {
+    if (str == null) {
+      return;
+    }
+
+    int index = 0;
+
+    while ((index = str.indexOf("</", index)) != -1) {
+      str.insert(index + 1, '\\');
+    }
+  }
+
+  protected boolean needSemi = true;
   /**
    * "Global" blocks are either the global block of a fragment, or a block
    * nested directly within some other global block. This definition matters
@@ -135,7 +263,9 @@
    */
   private Set<JsBlock> globalBlocks = new HashSet<JsBlock>();
   private final TextOutput p;
+
   private ArrayList<Integer> statementEnds = new ArrayList<Integer>();
+
   private ArrayList<Integer> statementStarts = new ArrayList<Integer>();
 
   public JsToStringGenerationVisitor(TextOutput out) {
@@ -564,8 +694,7 @@
       accept(q);
       if (q instanceof JsNumberLiteral) {
         /**
-         * Fix for Issue #3796. "42.foo" is not allowed, but
-         * "42 .foo" is.
+         * Fix for Issue #3796. "42.foo" is not allowed, but "42 .foo" is.
          */
         _space();
       }
@@ -592,8 +721,8 @@
     }
 
     /*
-     * If a constructor call has no arguments, it may simply be
-     * replaced with "new Constructor" with no parentheses.
+     * If a constructor call has no arguments, it may simply be replaced with
+     * "new Constructor" with no parentheses.
      */
     List<JsExpression> args = x.getArguments();
     if (args.size() > 0) {
@@ -1232,25 +1361,6 @@
 
   // CHECKSTYLE_NAMING_ON
 
-  /**
-   * Escapes any closing XML tags embedded in <code>str</code>, which could
-   * potentially cause a parse failure in a browser, for example, embedding a
-   * closing <code>&lt;script&gt;</code> tag.
-   * 
-   * @param str an unescaped literal; May be null
-   */
-  private void escapeClosingTags(StringBuffer str) {
-    if (str == null) {
-      return;
-    }
-
-    int index = 0;
-
-    while ((index = str.indexOf("</", index)) != -1) {
-      str.insert(index + 1, '\\');
-    }
-  }
-
   private void indent() {
     p.indentIn();
   }
@@ -1259,113 +1369,8 @@
     p.indentOut();
   }
 
-  /**
-   * Adapted from
-   * {@link com.google.gwt.dev.js.rhino.ScriptRuntime#escapeString(String)}. The
-   * difference is that we quote with either &quot; or &apos; depending on which
-   * one is used less inside the string.
-   */
   private void printStringLiteral(String value) {
-    char[] chars = value.toCharArray();
-    final int n = chars.length;
-    int quoteCount = 0;
-    int aposCount = 0;
-    for (int i = 0; i < n; ++i) {
-      switch (chars[i]) {
-        case '"':
-          ++quoteCount;
-          break;
-        case '\'':
-          ++aposCount;
-          break;
-      }
-    }
-
-    StringBuffer result = new StringBuffer(value.length() + 16);
-
-    char quoteChar = (quoteCount < aposCount) ? '"' : '\'';
-    p.print(quoteChar);
-
-    for (int i = 0; i < n; ++i) {
-      char c = chars[i];
-
-      if (' ' <= c && c <= '~' && c != quoteChar && c != '\\') {
-        // an ordinary print character (like C isprint())
-        result.append(c);
-        continue;
-      }
-
-      int escape = -1;
-      switch (c) {
-        case '\b':
-          escape = 'b';
-          break;
-        case '\f':
-          escape = 'f';
-          break;
-        case '\n':
-          escape = 'n';
-          break;
-        case '\r':
-          escape = 'r';
-          break;
-        case '\t':
-          escape = 't';
-          break;
-        case '"':
-          escape = '"';
-          break; // only reach here if == quoteChar
-        case '\'':
-          escape = '\'';
-          break; // only reach here if == quoteChar
-        case '\\':
-          escape = '\\';
-          break;
-      }
-
-      if (escape >= 0) {
-        // an \escaped sort of character
-        result.append('\\');
-        result.append((char) escape);
-      } else {
-        /*
-         * Emit characters from 0 to 31 that don't have a single character
-         * escape sequence in octal where possible. This saves one or two
-         * characters compared to the hexadecimal format '\xXX'.
-         * 
-         * These short octal sequences may only be used at the end of the string
-         * or where the following character is a non-digit. Otherwise, the
-         * following character would be incorrectly interpreted as belonging to
-         * the sequence.
-         */
-        if (c < ' ' &&
-            (i == n - 1 || chars[i + 1] < '0' || chars[i + 1] > '9')) {
-          result.append('\\');
-          if (c > 0x7) {
-            result.append((char) ('0' + (0x7 & (c >> 3))));
-          }
-          result.append((char) ('0' + (0x7 & c)));
-        } else {
-          int hexSize;
-          if (c < 256) {
-            // 2-digit hex
-            result.append("\\x");
-            hexSize = 2;
-          } else {
-            // Unicode.
-            result.append("\\u");
-            hexSize = 4;
-          }
-          // append hexadecimal form of ch left-padded with 0
-          for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) {
-            int digit = 0xf & (c >> shift);
-            result.append(HEX_DIGITS[digit]);
-          }
-        }
-      }
-    }
-    result.append(quoteChar);
-    escapeClosingTags(result);
-    p.print(result.toString());
+    String resultString = javaScriptString(value);
+    p.print(resultString);
   }
 }
diff --git a/dev/core/test/com/google/gwt/dev/js/JavaScriptStringTest.java b/dev/core/test/com/google/gwt/dev/js/JavaScriptStringTest.java
new file mode 100644
index 0000000..f91996f
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/js/JavaScriptStringTest.java
@@ -0,0 +1,40 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.google.gwt.dev.js;
+
+import com.google.gwt.dev.js.JsToStringGenerationVisitor;
+import com.google.gwt.dev.js.rhino.TokenStream;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Tests {@link JsToStringGenerationVisitor#javaScriptString(String)}.
+ */
+public class JavaScriptStringTest extends TestCase {
+  private void test(String original) throws IOException {
+    String escaped = JsToStringGenerationVisitor.javaScriptString(original);
+
+    // Parse it back
+    TokenStream tokenStream = new TokenStream(new StringReader(escaped),
+        "virtual file", 1);
+    assertEquals(TokenStream.STRING, tokenStream.getToken());
+    assertEquals(original, tokenStream.getString());
+
+    // It should be the only token
+    assertEquals(TokenStream.EOF, tokenStream.getToken());
+  }
+
+  public void testBasic() throws IOException {
+    test("abc");
+    test("");
+    test("abc\0def");
+    test("abc\\def");
+    test("\u00CC\u1234\5678\uabcd");
+    test("'''");
+    test("\"\"\"");
+    test("\b\f\n\r\t");
+  }
+}
diff --git a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
index f8bbb27..0e1a9db 100644
--- a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
+++ b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
@@ -22,6 +22,17 @@
   <define-configuration-property name='compiler.splitpoint.initial.sequence'
     is-multi-valued='true' />
 
+  <!--
+    Whether or not the compiler should predeclare variables that are defined
+    outside the initial download and are referenced from a different code
+    fragment than the one defining them. This is usually determined by which
+    linker is used and is not directly meaningful to users.
+  -->
+  <define-property name="compiler.predeclare.cross.fragment.references"
+    values="true,false" />
+  <set-property name="compiler.predeclare.cross.fragment.references"
+    value="false" />
+
   <!-- From here down, the properties are unsupported and are only available for test cases -->
 
   <!--
@@ -42,7 +53,17 @@
     JsIEBlockSizeVisitor has its usual effect.
   -->
   <define-configuration-property name="iframe.linker.script.chunk.size"
-    is-multi-valued="false" />
+  is-multi-valued="false" />
   <set-configuration-property name="iframe.linker.script.chunk.size"
-    value="30000" /> 
+    value="30000" />
+
+  <!--
+    The compiler emits deferred code into a deferredjs subdirectory of the
+    output. This property allows test cases to have the iframe linker
+    load the deferred code from a different subdirectory.
+  -->
+  <define-configuration-property name="iframe.linker.deferredjs.subdir"
+    is-multi-valued="false" />
+  <set-configuration-property name="iframe.linker.deferredjs.subdir"
+    value="deferredjs" />
 </module>
diff --git a/user/src/com/google/gwt/core/Core.gwt.xml b/user/src/com/google/gwt/core/Core.gwt.xml
index cb351e9..443f3c4 100644
--- a/user/src/com/google/gwt/core/Core.gwt.xml
+++ b/user/src/com/google/gwt/core/Core.gwt.xml
@@ -29,7 +29,7 @@
 
   <define-linker name="sso" class="com.google.gwt.core.linker.SingleScriptLinker" />
   <define-linker name="std" class="com.google.gwt.core.linker.IFrameLinker" />
-  <define-linker name="xs" class="com.google.gwt.core.linker.XSLinker" />
+  <inherits name="com.google.gwt.core.XSLinker" />
 
   <define-linker name="soycReport" class="com.google.gwt.core.linker.SoycReportLinker" />
   <define-linker name="symbolMaps" class="com.google.gwt.core.linker.SymbolMapsLinker" />
diff --git a/user/src/com/google/gwt/core/XSLinker.gwt.xml b/user/src/com/google/gwt/core/XSLinker.gwt.xml
new file mode 100644
index 0000000..854a585
--- /dev/null
+++ b/user/src/com/google/gwt/core/XSLinker.gwt.xml
@@ -0,0 +1,28 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<!-- Defines the cross-site linker   -->
+<module>
+  <define-linker name="xs" class="com.google.gwt.core.linker.XSLinker" />
+
+  <replace-with class="com.google.gwt.core.client.impl.CrossSiteLoadingStrategy">
+    <when-type-is
+      class="com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy" />
+    <when-linker-added name="xs" />
+  </replace-with>
+
+  <set-property name="compiler.predeclare.cross.fragment.references" value="true">
+    <when-linker-added name="xs" />
+  </set-property>
+</module>
diff --git a/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java b/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
index 89119dc..a7c3bdc 100644
--- a/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
+++ b/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
@@ -35,39 +35,29 @@
  * </ul>
  * 
  * <p>
- * Since the precise way to load code depends on the linker, each linker should
- * provide functions for fragment loading for any compilation that includes more
- * than one fragment. Linkers should always provide a function
- * <code>__gwtStartLoadingFragment</code>. This function is called by
- * AsyncFragmentLoader with two arguments: an integer fragment number that needs
- * to be downloaded, and a one-argument loadFailed function that can be called
- * if the load fails. If the load fails, that function should be called with a
- * descriptive exception as the argument. If the mechanism for loading the
- * contents of fragments is provided by the linker, the
- * <code>__gwtStartLoadingFragment</code> function should return
- * <code>null</code> or <code>undefined</code>.
- * </p>
- * <p>
- * Alternatively, the function can return a URL designating from where the code
- * for the requested fragment can be downloaded. In that case, the linker should
- * also provide a function <code>__gwtInstallCode</code> for actually installing
- * the code once it is downloaded. That function will be passed the loaded code
- * once it has been downloaded.
- * </p>
+ * Since the precise way to load code depends on the linker, linkers should
+ * specify a rebind of {@link LoadingStrategy}.  The default rebind is
+ * {@link XhrLoadingStrategy}.
  */
 public class AsyncFragmentLoader {
   /**
-   * An interface for handlers of load errors.
+   * An interface for handlers of load completion.  On a failed download,
+   * this callback should be invoked or else the requested download will
+   * hang indefinitely.  On a successful download, it's optional to call
+   * this method.  If it is called at all, it must be called after
+   * the downloaded code has been installed, so that {@link AsyncFragmentLoader}
+   * can distinguish successful from unsuccessful downloads.
    */
-  public static interface LoadErrorHandler {
-    void loadFailed(Throwable reason);
+  public static interface LoadTerminatedHandler {
+    void loadTerminated(Throwable reason);
   }
 
   /**
    * A strategy for loading code fragments.
    */
   public interface LoadingStrategy {
-    void startLoadingFragment(int fragment, LoadErrorHandler loadErrorHandler);
+    void startLoadingFragment(int fragment,
+        LoadTerminatedHandler loadTerminatedHandler);
   }
 
   /**
@@ -99,6 +89,77 @@
   }
 
   /**
+   * The standard logger used in a web browser. It uses the lightweight metrics
+   * system.
+   */
+  public static class StandardLogger implements Logger {
+    /**
+     * Always use this as {@link isStatsAvailable} &amp;&amp;
+     * {@link #stats(JavaScriptObject)}.
+     */
+    private static native boolean stats(JavaScriptObject data) /*-{
+      return $stats(data);
+    }-*/;
+
+    public void logEventProgress(String eventGroup, String type,
+        int fragment, int size) {
+      @SuppressWarnings("unused")
+      boolean toss = isStatsAvailable()
+          && stats(createStatsEvent(eventGroup, type, fragment, size));
+    }
+
+    private native JavaScriptObject createStatsEvent(String eventGroup,
+        String type, int fragment, int size) /*-{
+      var evt = {
+       moduleName: @com.google.gwt.core.client.GWT::getModuleName()(), 
+        sessionId: $sessionId,
+        subSystem: 'runAsync',
+        evtGroup: eventGroup,
+        millis: (new Date()).getTime(),
+        type: type
+      };
+      if (fragment >= 0) {
+        evt.fragment = fragment;
+      }
+      if (size >= 0) {
+        evt.size = size;
+      }
+      return evt;
+    }-*/;
+
+    private native boolean isStatsAvailable() /*-{
+      return !!$stats;
+    }-*/;
+  }
+
+  /**
+   * An exception indicating than at HTTP download failed.
+   */
+  static class HttpDownloadFailure extends RuntimeException {
+    private final int statusCode;
+
+    public HttpDownloadFailure(String url, int statusCode, String statusText) {
+      super("Download of " + url + " failed with status " + statusCode + "("
+          + statusText + ")");
+      this.statusCode = statusCode;
+    }
+
+    public int getStatusCode() {
+      return statusCode;
+    }
+  }
+
+  /**
+   * An exception indicating than at HTTP download succeeded, but installing
+   * its body failed.
+   */
+  static class HttpInstallFailure extends RuntimeException {
+    public HttpInstallFailure(String url, String text, Throwable rootCause) {
+      super("Install of " + url + " failed with text " + text, rootCause);
+    }
+  }
+
+  /**
    * A trivial queue of int's that should compile much better than a
    * LinkedList&lt;Integer&gt;. It assumes that it has a bound on the number of
    * items added to the queue. Removing items does not free up more space, but
@@ -143,39 +204,21 @@
   }
 
   /**
-   * An exception indicating than at HTTP download failed.
-   */
-  static class HttpDownloadFailure extends RuntimeException {
-    private final int statusCode;
-
-    public HttpDownloadFailure(String url, int statusCode, String statusText) {
-      super("Download of " + url + " failed with status " + statusCode + "("
-          + statusText + ")");
-      this.statusCode = statusCode;
-    }
-
-    public int getStatusCode() {
-      return statusCode;
-    }
-  }
-
-  /**
-   * An exception indicating than at HTTP download succeeded, but installing
-   * its body failed.
-   */
-  static class HttpInstallFailure extends RuntimeException {
-    public HttpInstallFailure(String url, String text, Throwable rootCause) {
-      super("Install of " + url + " failed with text " + text, rootCause);
-    }
-  }
-
-  /**
    * Internal load error handler. This calls all user-provided error handlers
    * and cancels all pending downloads.
    */
-  private class ResetAfterDownloadFailure implements LoadErrorHandler {
-    public void loadFailed(Throwable reason) {
-      assert fragmentLoading >= 0;
+  private class ResetAfterDownloadFailure implements LoadTerminatedHandler {
+    private final int fragment;
+    
+    public ResetAfterDownloadFailure(int myFragment) {
+      this.fragment = myFragment;
+    }
+
+    public void loadTerminated(Throwable reason) {
+      if (fragmentLoading != fragment) {
+        // fragment already loaded successfully
+        return;
+      }
 
       // Cancel all pending downloads.
 
@@ -183,8 +226,8 @@
        * Make a local list of the handlers to run, in case one of them calls
        * another runAsync
        */
-      LoadErrorHandler[] handlersToRun = pendingDownloadErrorHandlers;
-      pendingDownloadErrorHandlers = new LoadErrorHandler[numEntries + 1];
+      LoadTerminatedHandler[] handlersToRun = pendingDownloadErrorHandlers;
+      pendingDownloadErrorHandlers = new LoadTerminatedHandler[numEntries + 1];
 
       /*
        * Call clear() here so that requestedExclusives makes all of its space
@@ -200,10 +243,10 @@
        */
       RuntimeException lastException = null;
 
-      for (LoadErrorHandler handler : handlersToRun) {
+      for (LoadTerminatedHandler handler : handlersToRun) {
         if (handler != null) {
           try {
-            handler.loadFailed(reason);
+            handler.loadTerminated(reason);
           } catch (RuntimeException e) {
             lastException = e;
           }
@@ -217,50 +260,6 @@
   }
 
   /**
-   * The standard logger used in a web browser. It uses the lightweight metrics
-   * system.
-   */
-  private static class StandardLogger implements Logger {
-    /**
-     * Always use this as {@link isStatsAvailable} &amp;&amp;
-     * {@link #stats(JavaScriptObject)}.
-     */
-    private static native boolean stats(JavaScriptObject data) /*-{
-      return $stats(data);
-    }-*/;
-
-    public void logEventProgress(String eventGroup, String type,
-        int fragment, int size) {
-      @SuppressWarnings("unused")
-      boolean toss = isStatsAvailable()
-          && stats(createStatsEvent(eventGroup, type, fragment, size));
-    }
-
-    private native JavaScriptObject createStatsEvent(String eventGroup,
-        String type, int fragment, int size) /*-{
-      var evt = {
-       moduleName: @com.google.gwt.core.client.GWT::getModuleName()(), 
-        sessionId: $sessionId,
-        subSystem: 'runAsync',
-        evtGroup: eventGroup,
-        millis: (new Date()).getTime(),
-        type: type
-      };
-      if (fragment >= 0) {
-        evt.fragment = fragment;
-      }
-      if (size >= 0) {
-        evt.size = size;
-      }
-      return evt;
-    }-*/;
-
-    private native boolean isStatsAvailable() /*-{
-      return !!$stats;
-    }-*/;
-  }
-
-  /**
    * The standard instance of AsyncFragmentLoader used in a web browser.  If
    * not in GWT (i.e our vanilla JUnit tests, or if referenced in a server
    * context), this filed is {@code null}.  In GWT, the parameters to this call
@@ -307,7 +306,7 @@
    * loaded. This array will hold the initial sequence of bases followed by the
    * leftovers fragment. It is filled in by
    * {@link com.google.gwt.dev.jjs.impl.CodeSplitter} modifying the initializer
-   * to {@link #INSTANCE}. The list does <em>not</em> include the leftovers
+   * to {@link #BROWSER_LOADER}. The list does <em>not</em> include the leftovers
    * fragment, which must be loaded once all of these are finished.
    */
   private final int[] initialLoadSequence;
@@ -331,7 +330,7 @@
    * Externally provided handlers for all outstanding and queued download
    * requests.
    */
-  private LoadErrorHandler[] pendingDownloadErrorHandlers;
+  private LoadTerminatedHandler[] pendingDownloadErrorHandlers;
 
   /**
    * Whether prefetching is currently enabled.
@@ -367,7 +366,7 @@
     int numEntriesPlusOne = numEntries + 1;
     requestedExclusives = new BoundedIntQueue(numEntriesPlusOne);
     isLoaded = new boolean[numEntriesPlusOne];
-    pendingDownloadErrorHandlers = new LoadErrorHandler[numEntriesPlusOne];
+    pendingDownloadErrorHandlers = new LoadTerminatedHandler[numEntriesPlusOne];
   }
 
   /**
@@ -402,7 +401,7 @@
    * 
    * @param splitPoint the split point whose code needs to be loaded
    */
-  public void inject(int splitPoint, LoadErrorHandler loadErrorHandler) {
+  public void inject(int splitPoint, LoadTerminatedHandler loadErrorHandler) {
     pendingDownloadErrorHandlers[splitPoint] = loadErrorHandler;
     if (!isInitial(splitPoint)) {
       requestedExclusives.add(splitPoint);
@@ -560,7 +559,7 @@
     fragmentLoading = fragment;
     logDownloadStart(fragment);
     loadingStrategy.startLoadingFragment(fragment,
-        new ResetAfterDownloadFailure());
+        new ResetAfterDownloadFailure(fragment));
   }
 
   /**
diff --git a/user/src/com/google/gwt/core/client/impl/CrossSiteLoadingStrategy.java b/user/src/com/google/gwt/core/client/impl/CrossSiteLoadingStrategy.java
new file mode 100644
index 0000000..7529055
--- /dev/null
+++ b/user/src/com/google/gwt/core/client/impl/CrossSiteLoadingStrategy.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.core.client.impl;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
+
+/**
+ * Load runAsync code using a script tag. The
+ * {@link com.google.gwt.core.linker.XSLinker} sets
+ * <code>__gwtModuleFunction</code> to point at the function that wraps the
+ * initially downloaded code. On that function is a property
+ * <code>installCode</code> that can be invoked to eval more code in a scope
+ * nested somewhere within that function. The loaded script for fragment 123 is
+ * expected to invoke __gwtModuleFunction.runAsyncCallback123 with the code to
+ * be installed.
+ */
+public class CrossSiteLoadingStrategy implements LoadingStrategy {
+  /**
+   * A trivial JavaScript map from ints to ints.
+   */
+  private static final class IntToIntMap extends JavaScriptObject {
+    public static IntToIntMap create() {
+      return (IntToIntMap) JavaScriptObject.createArray();
+    }
+
+    protected IntToIntMap() {
+    }
+
+    /**
+     * Get an entry. If there is no such entry, return 0.
+     */
+    public native int get(int x) /*-{
+      return this[x] ? this[x] : 0;
+    }-*/;
+    
+    public native void put(int x, int y) /*-{
+      this[x] = y;
+    }-*/;
+  }
+
+  @SuppressWarnings("unused")
+  private static RuntimeException LoadTerminated = new RuntimeException(
+      "Code download terminated");
+
+  /**
+   * Clear the success callback for fragment <code>fragment</code>.
+   */
+  @SuppressWarnings("unused")
+  private static native void clearOnSuccess(int fragment) /*-{
+    delete __gwtModuleFunction['runAsyncCallback'+fragment];
+  }-*/;
+
+  private static native JavaScriptObject createScriptTag(String url) /*-{
+    var head = document.getElementsByTagName('head').item(0);
+    var script = document.createElement('script');
+    script.src = url;
+    return script;
+  }-*/;
+
+  private static native void installScriptTag(JavaScriptObject script) /*-{
+    var head = document.getElementsByTagName('head').item(0);
+    head.appendChild(script);
+  }-*/;
+
+  private static native JavaScriptObject removeTagAndCallErrorHandler(
+      int fragment, JavaScriptObject tag,
+      LoadTerminatedHandler loadFinishedHandler) /*-{
+     return function(exception) {
+       var head = document.getElementsByTagName('head').item(0);
+       head.removeChild(tag);
+       @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearOnSuccess(*)(fragment);
+       loadFinishedHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler::loadTerminated(*)(
+         exception);
+     }
+   }-*/;
+
+  private static native JavaScriptObject removeTagAndEvalCode(int fragment,
+      JavaScriptObject tag) /*-{
+     return function(code) {
+       var head = document.getElementsByTagName('head').item(0);
+       head.removeChild(tag);
+       @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearOnSuccess(*)(fragment);
+       __gwtModuleFunction.installCode(code);
+     }
+   }-*/;
+
+  private static native void setOnFailure(JavaScriptObject script,
+      JavaScriptObject callback) /*-{
+    var exception = @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::LoadTerminated;
+    script.onerror = function() {
+      callback(exception);
+    }
+    script.onload = function() {
+      callback(exception);
+    }
+    script.onreadystatechange = function () {
+      if (script.readyState == 'loaded' || script.readyState == 'complete') {
+        script.onreadystatechange = function () { }
+        callback(exception);
+      }
+    }
+  }-*/;
+
+  /**
+   * Set the success callback for fragment <code>fragment</code>
+   * to the supplied JavaScript function.
+   */
+  private static native void setOnSuccess(int fragment, JavaScriptObject callback) /*-{
+    __gwtModuleFunction['runAsyncCallback'+fragment] = callback;
+  }-*/;
+
+  private final IntToIntMap serialNumbers = IntToIntMap.create();
+
+  @Override
+  public void startLoadingFragment(int fragment,
+      LoadTerminatedHandler loadFinishedHandler) {
+    JavaScriptObject tag = createScriptTag(getUrl(fragment));
+    setOnSuccess(fragment, removeTagAndEvalCode(fragment, tag));
+    setOnFailure(tag, removeTagAndCallErrorHandler(fragment, tag,
+        loadFinishedHandler));
+    installScriptTag(tag);
+  }
+
+  protected String getDeferredJavaScriptDirectory() {
+    return "deferredjs/";
+  }
+
+  private int getSerial(int fragment) {
+    int ser = serialNumbers.get(fragment);
+    serialNumbers.put(fragment, ser + 1);
+    return ser;
+  }
+
+  /**
+   * The URL to retrieve a fragment of code from. NOTE: this function is not
+   * stable. It tweaks the URL with each call so that browsers are not tempted
+   * to cache a download failure.
+   */
+  private String getUrl(int fragment) {
+    return GWT.getModuleBaseURL() + getDeferredJavaScriptDirectory()
+        + GWT.getPermutationStrongName() + "/" + fragment + ".cache.js?serial="
+        + getSerial(fragment);
+  }
+}
diff --git a/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java b/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java
index bdd7a38..e56de67 100644
--- a/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java
+++ b/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java
@@ -1,240 +1,262 @@
-/*

- * Copyright 2009 Google Inc.

- * 

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not

- * use this file except in compliance with the License. You may obtain a copy of

- * the License at

- * 

- * http://www.apache.org/licenses/LICENSE-2.0

- * 

- * Unless required by applicable law or agreed to in writing, software

- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

- * License for the specific language governing permissions and limitations under

- * the License.

- */

-package com.google.gwt.core.client.impl;

-

-import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure;

-import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpInstallFailure;

-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler;

-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;

-import com.google.gwt.xhr.client.ReadyStateChangeHandler;

-import com.google.gwt.xhr.client.XMLHttpRequest;

-

-/**

- * The standard loading strategy used in a web browser.

- */

-public class XhrLoadingStrategy implements LoadingStrategy {

-  

-  /**

-   * A {@link MockableXMLHttpRequest} that is really just a vanilla

-   * XMLHttpRequest.  This wrapper (and thus {@code MockableXMLHttpRequest) is

-   * needed because so much of {@link XMLHttpRequest} is final, which in turn

-   * is because it extends {@code JavaScriptObject} and is subject to its

-   * restrictions.

-   * 

-   * It is important that these methods be simple enough to be inlined away.

-   */

-  class DelegatingXMLHttpRequest implements MockableXMLHttpRequest {

-    private final XMLHttpRequest delegate;

-

-    public DelegatingXMLHttpRequest(XMLHttpRequest xmlHttpRequest) {

-      delegate = xmlHttpRequest;

-    }

-

-    public void clearOnReadyStateChange() {

-      delegate.clearOnReadyStateChange();

-    }

-

-    public int getReadyState() {

-      return delegate.getReadyState();

-    }

-

-    public String getResponseText() {

-      return delegate.getResponseText();

-    }

-

-    public int getStatus() {

-      return delegate.getStatus();

-    }

-

-    public String getStatusText() {

-      return delegate.getStatusText();

-    }

-

-    public void open(String method, String url) {

-      delegate.open(method, url);

-    }

-

-    public void send() {

-      delegate.send();

-    }

-

-    public void setOnReadyStateChange(ReadyStateChangeHandler handler) {

-      delegate.setOnReadyStateChange(handler);

-    }

-

-    public void setRequestHeader(String header, String value) {

-      delegate.setRequestHeader(header, value);

-    }

-  }

-

-  /**

-   * Delegates to the real XMLHttpRequest, except in test when we make a mock

-   * to jump through error/retry hoops.

-   */

-  interface MockableXMLHttpRequest {

-    void clearOnReadyStateChange();

-    int getReadyState();

-    String getResponseText();

-    int getStatus();

-    String getStatusText();

-    void open(String method, String url);

-    void send();

-    void setOnReadyStateChange(ReadyStateChangeHandler handler);

-    void setRequestHeader(String header, String value);

-  }

-

-  /**

-   * Since LoadingStrategy must support concurrent requests, including figuring

-   * which is which in the onLoadError handling, we need to keep track of this

-   * data for each outstanding request, which we index by xhr object.

-   */

-  protected class RequestData {

-    String url;

-    int retryCount;

-    LoadErrorHandler errorHandler = null;

-    

-    public RequestData(String url, LoadErrorHandler errorHandler) {

-      this.url = url;

-      this.errorHandler = errorHandler;

-      this.retryCount = 0;

-    }

-  }

-

-  static final String HTTP_GET = "GET";

-

-  /**

-   * Some UA's like Safari will have a "0" status code when loading from file:

-   * URLs. Additionally, the "0" status code is used sometimes if the server

-   * does not respond, e.g. if there is a connection refused.

-   */

-  static final int HTTP_STATUS_NON_HTTP = 0;

-

-  static final int HTTP_STATUS_OK = 200;

-

-  /**

-   * For error logging, max length of fragment response text to include in

-   * failed-to-install exception message.

-   */

-  private static final int MAX_LOG_LENGTH = 200;

-  

-  /**

-   * Number of retry attempts for a single fragment.  If a fragment download

-   * fails, we try again this many times before "really" failing out to user

-   * error-handling code.  If a fragment downloads but doesn't install, we

-   * don't retry at all.

-   */

-  private static final int MAX_RETRY_COUNT = 3;

-

-  public void startLoadingFragment(int fragment,

-      final LoadErrorHandler loadErrorHandler) {

-    String url = gwtStartLoadingFragment(fragment, loadErrorHandler);

-    if (url == null) {

-      // The download has already started; nothing more to do

-      return;

-    }

-

-    RequestData request = new RequestData(url, loadErrorHandler);

-    tryLoad(request);

-  }

-  

-  /**

-   * Overridable for tests.

-   */

-  protected MockableXMLHttpRequest createXhr() {

-    return new DelegatingXMLHttpRequest(XMLHttpRequest.create());

-  }

-

-  /**

-   * Call the linker-supplied <code>__gwtInstallCode</code> method. See the

-   * {@link AsyncFragmentLoader class comment} for more details.

-   */

-  protected native void gwtInstallCode(String text) /*-{

-    __gwtInstallCode(text);

-  }-*/;

-  

-  /**

-   * Call the linker-supplied __gwtStartLoadingFragment function. It should

-   * either start the download and return null or undefined, or it should

-   * return a URL that should be downloaded to get the code. If it starts the

-   * download itself, it can synchronously load it, e.g. from cache, if that

-   * makes sense.

-   */

-  protected native String gwtStartLoadingFragment(int fragment,

-      LoadErrorHandler loadErrorHandler) /*-{

-    function loadFailed(e) {

-      loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadErrorHandler::loadFailed(Ljava/lang/Throwable;)(e);

-    }

-    return __gwtStartLoadingFragment(fragment, loadFailed);

-  }-*/;

-

-  /**

-   * Error recovery from loading or installing code.

-   * @param request the requestData of this request

-   * @param e exception of the error

-   * @param mayRetry {@code true} if retrying might be helpful

-   */

-  protected void onLoadError(RequestData request, Throwable e, boolean mayRetry) {

-    if (mayRetry) {

-      request.retryCount++;

-      if (request.retryCount < MAX_RETRY_COUNT) {

-        tryLoad(request);

-        return;

-      }

-    }

-    request.errorHandler.loadFailed(e); 

-  }

-

-  /**

-   * Makes a single load-and-install attempt.

-   */

-  protected void tryLoad(final RequestData request) {

-    final MockableXMLHttpRequest xhr = createXhr();

-    

-    xhr.open(HTTP_GET, request.url);

-    if (request.retryCount > 0) {

-      // disable caching if we have to retry; one cause could be bad cache

-      xhr.setRequestHeader("Cache-Control", "no-cache");

-    }

-

-    xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {

-      public void onReadyStateChange(XMLHttpRequest ignored) {

-        if (xhr.getReadyState() == XMLHttpRequest.DONE) {

-          xhr.clearOnReadyStateChange();

-          if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP)

-              && xhr.getResponseText() != null

-              && xhr.getResponseText().length() != 0) {

-            try {

-              gwtInstallCode(xhr.getResponseText());

-            } catch (RuntimeException e) {

-              String textIntro = xhr.getResponseText();

-              if (textIntro != null && textIntro.length() > MAX_LOG_LENGTH) {

-                textIntro = textIntro.substring(0, MAX_LOG_LENGTH) + "...";

-              }

-              onLoadError(request, 

-                  new HttpInstallFailure(request.url, textIntro, e), false);

-            }

-          } else {

-            onLoadError(request,

-                new HttpDownloadFailure(request.url, xhr.getStatus(),

-                    xhr.getStatusText()), true);

-          }

-        }

-      }

-    });

-

-    xhr.send();

-  }

+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.core.client.impl;
+
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpInstallFailure;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
+import com.google.gwt.xhr.client.ReadyStateChangeHandler;
+import com.google.gwt.xhr.client.XMLHttpRequest;
+
+/**
+ * The standard loading strategy used in a web browser. The linker it is used
+ * with should provide JavaScript-level functions to indicate how to handle
+ * downloading and installing code. There is support to use XHR for the
+ * download.
+ * 
+ * Linkers should always provide a function
+ * <code>__gwtStartLoadingFragment</code>. This function is called by
+ * AsyncFragmentLoader with two arguments: an integer fragment number that needs
+ * to be downloaded, and a one-argument loadFinished function. If the load
+ * fails, that function should be called with a descriptive exception as the
+ * argument. If the load succeeds, that function may also be called, so long as
+ * it isn't called until the downloaded code has been installed.
+ * 
+ * 
+ * If the mechanism for loading the contents of fragments is provided by the
+ * linker, the <code>__gwtStartLoadingFragment</code> function should return
+ * <code>null</code> or <code>undefined</code>.
+ * 
+ * Alternatively, the function can return a URL designating from where the code
+ * for the requested fragment can be downloaded. In that case, the linker should
+ * also provide a function <code>__gwtInstallCode</code> for actually installing
+ * the code once it is downloaded. That function will be passed the loaded code
+ * once it has been downloaded.
+ */
+public class XhrLoadingStrategy implements LoadingStrategy {
+  
+  /**
+   * A {@link MockableXMLHttpRequest} that is really just a vanilla
+   * XMLHttpRequest.  This wrapper (and thus {@code MockableXMLHttpRequest} is
+   * needed because so much of {@link XMLHttpRequest} is final, which in turn
+   * is because it extends {@code JavaScriptObject} and is subject to its
+   * restrictions.
+   * 
+   * It is important that these methods be simple enough to be inlined away.
+   */
+  class DelegatingXMLHttpRequest implements MockableXMLHttpRequest {
+    private final XMLHttpRequest delegate;
+
+    public DelegatingXMLHttpRequest(XMLHttpRequest xmlHttpRequest) {
+      delegate = xmlHttpRequest;
+    }
+
+    public void clearOnReadyStateChange() {
+      delegate.clearOnReadyStateChange();
+    }
+
+    public int getReadyState() {
+      return delegate.getReadyState();
+    }
+
+    public String getResponseText() {
+      return delegate.getResponseText();
+    }
+
+    public int getStatus() {
+      return delegate.getStatus();
+    }
+
+    public String getStatusText() {
+      return delegate.getStatusText();
+    }
+
+    public void open(String method, String url) {
+      delegate.open(method, url);
+    }
+
+    public void send() {
+      delegate.send();
+    }
+
+    public void setOnReadyStateChange(ReadyStateChangeHandler handler) {
+      delegate.setOnReadyStateChange(handler);
+    }
+
+    public void setRequestHeader(String header, String value) {
+      delegate.setRequestHeader(header, value);
+    }
+  }
+
+  /**
+   * Delegates to the real XMLHttpRequest, except in test when we make a mock
+   * to jump through error/retry hoops.
+   */
+  interface MockableXMLHttpRequest {
+    void clearOnReadyStateChange();
+    int getReadyState();
+    String getResponseText();
+    int getStatus();
+    String getStatusText();
+    void open(String method, String url);
+    void send();
+    void setOnReadyStateChange(ReadyStateChangeHandler handler);
+    void setRequestHeader(String header, String value);
+  }
+
+  /**
+   * Since LoadingStrategy must support concurrent requests, including figuring
+   * which is which in the onLoadError handling, we need to keep track of this
+   * data for each outstanding request, which we index by xhr object.
+   */
+  protected class RequestData {
+    String url;
+    int retryCount;
+    LoadTerminatedHandler errorHandler = null;
+    
+    public RequestData(String url, LoadTerminatedHandler errorHandler) {
+      this.url = url;
+      this.errorHandler = errorHandler;
+      this.retryCount = 0;
+    }
+  }
+
+  static final String HTTP_GET = "GET";
+
+  /**
+   * Some UA's like Safari will have a "0" status code when loading from file:
+   * URLs. Additionally, the "0" status code is used sometimes if the server
+   * does not respond, e.g. if there is a connection refused.
+   */
+  static final int HTTP_STATUS_NON_HTTP = 0;
+
+  static final int HTTP_STATUS_OK = 200;
+
+  /**
+   * For error logging, max length of fragment response text to include in
+   * failed-to-install exception message.
+   */
+  private static final int MAX_LOG_LENGTH = 200;
+  
+  /**
+   * Number of retry attempts for a single fragment.  If a fragment download
+   * fails, we try again this many times before "really" failing out to user
+   * error-handling code.  If a fragment downloads but doesn't install, we
+   * don't retry at all.
+   */
+  private static final int MAX_RETRY_COUNT = 3;
+
+  public void startLoadingFragment(int fragment,
+      final LoadTerminatedHandler loadErrorHandler) {
+    String url = gwtStartLoadingFragment(fragment, loadErrorHandler);
+    if (url == null) {
+      // The download has already started; nothing more to do
+      return;
+    }
+
+    RequestData request = new RequestData(url, loadErrorHandler);
+    tryLoad(request);
+  }
+  
+  /**
+   * Overridable for tests.
+   */
+  protected MockableXMLHttpRequest createXhr() {
+    return new DelegatingXMLHttpRequest(XMLHttpRequest.create());
+  }
+
+  /**
+   * Call the linker-supplied <code>__gwtInstallCode</code> method. See the
+   * {@link AsyncFragmentLoader class comment} for more details.
+   */
+  protected native void gwtInstallCode(String text) /*-{
+    __gwtInstallCode(text);
+  }-*/;
+  
+  /**
+   * Call the linker-supplied __gwtStartLoadingFragment function. It should
+   * either start the download and return null or undefined, or it should
+   * return a URL that should be downloaded to get the code. If it starts the
+   * download itself, it can synchronously load it, e.g. from cache, if that
+   * makes sense.
+   */
+  protected native String gwtStartLoadingFragment(int fragment,
+      LoadTerminatedHandler loadErrorHandler) /*-{
+    function loadFailed(e) {
+      loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadTerminatedHandler::loadTerminated(*)(e);
+    }
+    return __gwtStartLoadingFragment(fragment, loadFailed);
+  }-*/;
+
+  /**
+   * Error recovery from loading or installing code.
+   * @param request the requestData of this request
+   * @param e exception of the error
+   * @param mayRetry {@code true} if retrying might be helpful
+   */
+  protected void onLoadError(RequestData request, Throwable e, boolean mayRetry) {
+    if (mayRetry) {
+      request.retryCount++;
+      if (request.retryCount < MAX_RETRY_COUNT) {
+        tryLoad(request);
+        return;
+      }
+    }
+    request.errorHandler.loadTerminated(e); 
+  }
+
+  /**
+   * Makes a single load-and-install attempt.
+   */
+  protected void tryLoad(final RequestData request) {
+    final MockableXMLHttpRequest xhr = createXhr();
+    
+    xhr.open(HTTP_GET, request.url);
+    if (request.retryCount > 0) {
+      // disable caching if we have to retry; one cause could be bad cache
+      xhr.setRequestHeader("Cache-Control", "no-cache");
+    }
+
+    xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
+      public void onReadyStateChange(XMLHttpRequest ignored) {
+        if (xhr.getReadyState() == XMLHttpRequest.DONE) {
+          xhr.clearOnReadyStateChange();
+          if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP)
+              && xhr.getResponseText() != null
+              && xhr.getResponseText().length() != 0) {
+            try {
+              gwtInstallCode(xhr.getResponseText());
+            } catch (RuntimeException e) {
+              String textIntro = xhr.getResponseText();
+              if (textIntro != null && textIntro.length() > MAX_LOG_LENGTH) {
+                textIntro = textIntro.substring(0, MAX_LOG_LENGTH) + "...";
+              }
+              onLoadError(request, 
+                  new HttpInstallFailure(request.url, textIntro, e), false);
+            }
+          } else {
+            onLoadError(request,
+                new HttpDownloadFailure(request.url, xhr.getStatus(),
+                    xhr.getStatusText()), true);
+          }
+        }
+      }
+    });
+
+    xhr.send();
+  }
 }
\ No newline at end of file
diff --git a/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java b/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
index b14c3c2..cac5944 100644
--- a/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
+++ b/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
@@ -15,7 +15,7 @@
  */
 package com.google.gwt.core.client.impl;
 
-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
 import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
 import com.google.gwt.core.client.impl.AsyncFragmentLoader.Logger;
 
@@ -33,20 +33,20 @@
  * correct lightweight metrics under a variety of request patterns.
  */
 public class AsyncFragmentLoaderTest extends TestCase {
-  private static class MockErrorHandler implements LoadErrorHandler {
+  private static class MockErrorHandler implements LoadTerminatedHandler {
     private boolean wasCalled = false;
 
     public boolean getWasCalled() {
       return wasCalled;
     }
 
-    public void loadFailed(Throwable reason) {
+    public void loadTerminated(Throwable reason) {
       wasCalled = true;
     }
   }
 
   private static class MockLoadStrategy implements LoadingStrategy {
-    public final Map<Integer, LoadErrorHandler> errorHandlers = new HashMap<Integer, LoadErrorHandler>();
+    public final Map<Integer, LoadTerminatedHandler> errorHandlers = new HashMap<Integer, LoadTerminatedHandler>();
     private List<Integer> loadRequests = new LinkedList<Integer>();
 
     public void assertFragmentsRequested(int... expectedAry) {
@@ -61,7 +61,7 @@
     }
 
     public void startLoadingFragment(int fragment,
-        LoadErrorHandler loadErrorHandler) {
+        LoadTerminatedHandler loadErrorHandler) {
       errorHandlers.put(fragment, loadErrorHandler);
       loadRequests.add(fragment);
     }
@@ -138,8 +138,8 @@
   private static final String END = "end";
   private static final String LEFTOVERS_DOWNLOAD = "leftoversDownload";
 
-  private static final LoadErrorHandler NULL_ERROR_HANDLER = new LoadErrorHandler() {
-    public void loadFailed(Throwable reason) {
+  private static final LoadTerminatedHandler NULL_ERROR_HANDLER = new LoadTerminatedHandler() {
+    public void loadTerminated(Throwable reason) {
     }
   };
 
@@ -285,7 +285,11 @@
     reqs.assertFragmentsRequested();
     assertFalse(error6try2.getWasCalled());
     progress.assertEvent("download6", END, 6);
-
+    
+    // a finish event should do nothing if the fragment has already succeeded
+    progress.assertNoEvents();
+    loadFailed(reqs, 6);
+    assertFalse(error6try2.getWasCalled());
     progress.assertNoEvents();
   }
 
@@ -726,6 +730,6 @@
   }
 
   private void loadFailed(MockLoadStrategy reqs, int fragment) {
-    reqs.errorHandlers.get(fragment).loadFailed(makeLoadFailedException());
+    reqs.errorHandlers.get(fragment).loadTerminated(makeLoadFailedException());
   }
 }
diff --git a/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java b/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java
index 4ca245b..de9d2a8 100644
--- a/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java
+++ b/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java
@@ -1,278 +1,270 @@
-/*

- * Copyright 2009 Google Inc.

- * 

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not

- * use this file except in compliance with the License. You may obtain a copy of

- * the License at

- * 

- * http://www.apache.org/licenses/LICENSE-2.0

- * 

- * Unless required by applicable law or agreed to in writing, software

- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

- * License for the specific language governing permissions and limitations under

- * the License.

- */

-package com.google.gwt.core.client.impl;

-

-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler;

-import com.google.gwt.core.client.impl.XhrLoadingStrategy.MockableXMLHttpRequest;

-import com.google.gwt.xhr.client.ReadyStateChangeHandler;

-

-import junit.framework.TestCase;

-

-import java.util.ArrayList;

-import java.util.Arrays;

-import java.util.HashMap;

-

-/**

- * Tests the default loading strategy and its retry behavior.

- */

-public class XhrLoadingStrategyTest extends TestCase {

-

-  static class MockXhr implements MockableXMLHttpRequest {

-    public static final String SUCCESSFUL_RESPONSE_TEXT =

-        "successful response text";

-    public static final String INSTALL_FAILED_RESPONSE_TEXT =

-        "install failed response text";

-

-    private ReadyStateChangeHandler handler;

-    private int httpStatus;

-    private int state;

-    private String statusText; 

-    private String text;

-    private HashMap<String,String> headers;

-    

-    public MockXhr(int status, String statusText, boolean loads,

-        boolean installs, String... headers) {

-      this.httpStatus = status;

-      this.statusText = statusText;

-      if (installs) {

-        text = SUCCESSFUL_RESPONSE_TEXT;

-      } else if (loads) {

-        text = INSTALL_FAILED_RESPONSE_TEXT;

-      } else {

-        text = null;

-      }

-      handler = null;

-      state = 0;

-      assert headers.length % 2 == 0;

-      this.headers = new HashMap<String,String>();

-      for (int i = 0; i < headers.length; i += 2) {

-        this.headers.put(headers[i], headers[i + 1]);

-      }

-    }

-

-    public void clearOnReadyStateChange() {

-      handler = null;

-    }

-

-    public int getReadyState() {

-      return state;

-    }

-

-    public String getResponseText() {

-      return state > 3 ? text : null;

-    }

-

-    public int getStatus() { 

-      return state > 1 ? httpStatus : 0;

-    }

-

-    public String getStatusText() {

-      return state > 1 ? statusText : null;

-    }

-

-    public void open(String method, String url) {

-      state = 1;

-    }

-

-    public void send() {

-      state = 4;

-      if (headers.size() != 0) {

-        throw new IllegalStateException("not all expected headers set");

-      }

-      if (handler != null) {

-        /* This is brittle, but I don't have a better idea.  The problem is

-         * that onReadyStateChange takes a REAL XMLHttpRequest, which I can't

-         * mock because it's all final.  I don't want to open

-         * ReadyStateChangeHandler's long-standing API to let it take a

-         * non-real XMLHttpRequest, just for my wee test here, so instead I

-         * admit that null works 'cause the handler won't *use* its argument.

-         */

-        handler.onReadyStateChange(null);

-      }

-    }

-

-    public void setOnReadyStateChange(ReadyStateChangeHandler handler) {

-      this.handler = handler;

-    }

-

-    public void setRequestHeader(String header, String value) {

-      String val = headers.get(header);

-      if (val == null) {

-        throw new IllegalArgumentException("set of unexpected header "

-            + header);

-      }

-      if (!val.equals(value)) {

-        throw new IllegalArgumentException("set of header "

-            + header + " to unexpected value " + value + ", not " + val);

-      }

-      headers.remove(header);

-    }

-  }

-

-  /**

-   * {@link XhrLoadingStrategy}, but without actual live XHRs.

-   */

-  static class MockXhrLoadingStrategy extends XhrLoadingStrategy {

-    private static final String FRAGMENT_URL = "http://nowhere.net/fragment";

-    private ArrayList<MockXhr> xhrs;

-    

-    public MockXhrLoadingStrategy(MockXhr... input) {

-      xhrs = new ArrayList<MockXhr>(Arrays.asList(input));

-    }

-

-    public void assertDone() {

-      if (xhrs.size() != 0) {

-        throw new IllegalStateException("leftover createXhr() data" +

-        " (too few load retries?)");

-      }

-    }

-

-    /**

-     * Test stub; install succeeds unless text says otherwise.

-     */

-    @Override

-    protected void gwtInstallCode(String text) {

-      if (MockXhr.INSTALL_FAILED_RESPONSE_TEXT.equals(text)) {

-        throw new RuntimeException(text);

-      }

-    }

-

-    /**

-     * Test stub; bypass the JSNI, but we're returning a (mock) URL.

-     */

-    @Override

-    protected String gwtStartLoadingFragment(int fragment,

-        LoadErrorHandler loadErrorHandler) {

-      return FRAGMENT_URL;

-    }

-

-    @Override

-    protected MockableXMLHttpRequest createXhr() {

-      if (xhrs.size() == 0) {

-        throw new IllegalStateException("createXhr() underflow" +

-            " (too many load retries?)");

-      }

-      return xhrs.remove(0);

-    }

-  }

-  

-  /**

-   * Basic succeeds-on-first-try case.

-   */

-  public void testNoRetrySucceeds() {

-    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(

-        new MockXhr(200, "200 Ok", true, true));

-    xls.startLoadingFragment(1, new LoadErrorHandler() {

-      public void loadFailed(Throwable reason) {

-        fail();

-      }

-    });

-    xls.assertDone();

-  }

-

-  /**

-   * Fails irrevocably on first try; doesn't retry.

-   */

-  public void testNoRetryFails() {

-    final boolean loadFailedCalled[] = new boolean[1];

-    loadFailedCalled[0] = false;

-    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(

-        new MockXhr(200, "Ok", true, false));

-    xls.startLoadingFragment(1, new LoadErrorHandler() {

-      public void loadFailed(Throwable reason) {

-        loadFailedCalled[0] = true;

-      }

-    });

-    xls.assertDone();

-    if (!loadFailedCalled[0]) {

-      fail("should have failed to install, but didn't");

-    }

-  }

-

-  /**

-   * Needs some retries, but succeeds.

-   */

-  public void testRetrySucceeds() {

-    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(

-        new MockXhr(0, "Could not connect", false, false),

-        new MockXhr(200, "Ok", true, true, "Cache-Control", "no-cache"));

-    xls.startLoadingFragment(1, new LoadErrorHandler() {

-      public void loadFailed(Throwable reason) {

-        fail();

-      }

-    });

-    xls.assertDone();    

-  }

-

-  /**

-   * Needs retries, and never succeeds.

-   */

-  public void testRetryFails() {

-    final boolean loadFailedCalled[] = new boolean[1];

-    loadFailedCalled[0] = false;

-    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(

-        new MockXhr(0, "Could not connect", false, false),

-        new MockXhr(0, "Could not connect", false, false,

-            "Cache-Control", "no-cache"),

-        new MockXhr(0, "Could not connect", false, false,

-            "Cache-Control", "no-cache"));

-    xls.startLoadingFragment(1, new LoadErrorHandler() {

-      public void loadFailed(Throwable reason) {

-        loadFailedCalled[0] = true;

-      }

-    });

-    xls.assertDone();

-    if (!loadFailedCalled[0]) {

-      fail("should have failed to install, but didn't");

-    }

-  }

-

-  /**

-   * A bizarre case we've seen in the wild...

-   */

-  public void testNull200Case() {

-    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(

-        new MockXhr(200, "Ok", false, false),

-        new MockXhr(200, "Ok", false, false,

-            "Cache-Control", "no-cache"),

-        new MockXhr(200, "Ok", true, true,

-            "Cache-Control", "no-cache"));

-    xls.startLoadingFragment(1, new LoadErrorHandler() {

-      public void loadFailed(Throwable reason) {

-        fail();

-      }

-    });

-    xls.assertDone();

-  }

-

-  /**

-   * Check some HTTP status codes....

-   */

-  public void testRetryCodes() {

-    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(

-        new MockXhr(500, "Server Error", false, false),

-        new MockXhr(404, "Not Found", false, false,

-            "Cache-Control", "no-cache"),

-        new MockXhr(200, "Ok", true, true,

-            "Cache-Control", "no-cache"));

-    xls.startLoadingFragment(1, new LoadErrorHandler() {

-      public void loadFailed(Throwable reason) {

-        fail();

-      }

-    });

-    xls.assertDone();

-  }

-}

+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.core.client.impl;
+
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
+import com.google.gwt.core.client.impl.XhrLoadingStrategy.MockableXMLHttpRequest;
+import com.google.gwt.xhr.client.ReadyStateChangeHandler;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Tests the default loading strategy and its retry behavior.
+ */
+public class XhrLoadingStrategyTest extends TestCase {
+
+  static class MockXhr implements MockableXMLHttpRequest {
+    public static final String SUCCESSFUL_RESPONSE_TEXT = "successful response text";
+    public static final String INSTALL_FAILED_RESPONSE_TEXT = "install failed response text";
+
+    private ReadyStateChangeHandler handler;
+    private int httpStatus;
+    private int state;
+    private String statusText;
+    private String text;
+    private HashMap<String, String> headers;
+
+    public MockXhr(int status, String statusText, boolean loads,
+        boolean installs, String... headers) {
+      this.httpStatus = status;
+      this.statusText = statusText;
+      if (installs) {
+        text = SUCCESSFUL_RESPONSE_TEXT;
+      } else if (loads) {
+        text = INSTALL_FAILED_RESPONSE_TEXT;
+      } else {
+        text = null;
+      }
+      handler = null;
+      state = 0;
+      assert headers.length % 2 == 0;
+      this.headers = new HashMap<String, String>();
+      for (int i = 0; i < headers.length; i += 2) {
+        this.headers.put(headers[i], headers[i + 1]);
+      }
+    }
+
+    public void clearOnReadyStateChange() {
+      handler = null;
+    }
+
+    public int getReadyState() {
+      return state;
+    }
+
+    public String getResponseText() {
+      return state > 3 ? text : null;
+    }
+
+    public int getStatus() {
+      return state > 1 ? httpStatus : 0;
+    }
+
+    public String getStatusText() {
+      return state > 1 ? statusText : null;
+    }
+
+    public void open(String method, String url) {
+      state = 1;
+    }
+
+    public void send() {
+      state = 4;
+      if (headers.size() != 0) {
+        throw new IllegalStateException("not all expected headers set");
+      }
+      if (handler != null) {
+        /*
+         * This is brittle, but I don't have a better idea. The problem is that
+         * onReadyStateChange takes a REAL XMLHttpRequest, which I can't mock
+         * because it's all final. I don't want to open
+         * ReadyStateChangeHandler's long-standing API to let it take a non-real
+         * XMLHttpRequest, just for my wee test here, so instead I admit that
+         * null works 'cause the handler won't *use* its argument.
+         */
+        handler.onReadyStateChange(null);
+      }
+    }
+
+    public void setOnReadyStateChange(ReadyStateChangeHandler handler) {
+      this.handler = handler;
+    }
+
+    public void setRequestHeader(String header, String value) {
+      String val = headers.get(header);
+      if (val == null) {
+        throw new IllegalArgumentException("set of unexpected header " + header);
+      }
+      if (!val.equals(value)) {
+        throw new IllegalArgumentException("set of header " + header
+            + " to unexpected value " + value + ", not " + val);
+      }
+      headers.remove(header);
+    }
+  }
+
+  /**
+   * {@link XhrLoadingStrategy}, but without actual live XHRs.
+   */
+  static class MockXhrLoadingStrategy extends XhrLoadingStrategy {
+    private static final String FRAGMENT_URL = "http://nowhere.net/fragment";
+    private ArrayList<MockXhr> xhrs;
+
+    public MockXhrLoadingStrategy(MockXhr... input) {
+      xhrs = new ArrayList<MockXhr>(Arrays.asList(input));
+    }
+
+    public void assertDone() {
+      if (xhrs.size() != 0) {
+        throw new IllegalStateException("leftover createXhr() data"
+            + " (too few load retries?)");
+      }
+    }
+
+    /**
+     * Test stub; install succeeds unless text says otherwise.
+     */
+    @Override
+    protected void gwtInstallCode(String text) {
+      if (MockXhr.INSTALL_FAILED_RESPONSE_TEXT.equals(text)) {
+        throw new RuntimeException(text);
+      }
+    }
+
+    /**
+     * Test stub; bypass the JSNI, but we're returning a (mock) URL.
+     */
+    @Override
+    protected String gwtStartLoadingFragment(int fragment,
+        LoadTerminatedHandler LoadFinishedHandler) {
+      return FRAGMENT_URL;
+    }
+
+    @Override
+    protected MockableXMLHttpRequest createXhr() {
+      if (xhrs.size() == 0) {
+        throw new IllegalStateException("createXhr() underflow"
+            + " (too many load retries?)");
+      }
+      return xhrs.remove(0);
+    }
+  }
+
+  /**
+   * Basic succeeds-on-first-try case.
+   */
+  public void testNoRetrySucceeds() {
+    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(200,
+        "200 Ok", true, true));
+    xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+      public void loadTerminated(Throwable reason) {
+        fail();
+      }
+    });
+    xls.assertDone();
+  }
+
+  /**
+   * Fails irrevocably on first try; doesn't retry.
+   */
+  public void testNoRetryFails() {
+    final boolean loadFailedCalled[] = new boolean[1];
+    loadFailedCalled[0] = false;
+    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(200,
+        "Ok", true, false));
+    xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+      public void loadTerminated(Throwable reason) {
+        loadFailedCalled[0] = true;
+      }
+    });
+    xls.assertDone();
+    if (!loadFailedCalled[0]) {
+      fail("should have failed to install, but didn't");
+    }
+  }
+
+  /**
+   * Needs some retries, but succeeds.
+   */
+  public void testRetrySucceeds() {
+    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(0,
+        "Could not connect", false, false), new MockXhr(200, "Ok", true, true,
+        "Cache-Control", "no-cache"));
+    xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+      public void loadTerminated(Throwable reason) {
+        fail();
+      }
+    });
+    xls.assertDone();
+  }
+
+  /**
+   * Needs retries, and never succeeds.
+   */
+  public void testRetryFails() {
+    final boolean loadFailedCalled[] = new boolean[1];
+    loadFailedCalled[0] = false;
+    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(0,
+        "Could not connect", false, false), new MockXhr(0, "Could not connect",
+        false, false, "Cache-Control", "no-cache"), new MockXhr(0,
+        "Could not connect", false, false, "Cache-Control", "no-cache"));
+    xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+      public void loadTerminated(Throwable reason) {
+        loadFailedCalled[0] = true;
+      }
+    });
+    xls.assertDone();
+    if (!loadFailedCalled[0]) {
+      fail("should have failed to install, but didn't");
+    }
+  }
+
+  /**
+   * A bizarre case we've seen in the wild...
+   */
+  public void testNull200Case() {
+    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(200,
+        "Ok", false, false), new MockXhr(200, "Ok", false, false,
+        "Cache-Control", "no-cache"), new MockXhr(200, "Ok", true, true,
+        "Cache-Control", "no-cache"));
+    xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+      public void loadTerminated(Throwable reason) {
+        fail();
+      }
+    });
+    xls.assertDone();
+  }
+
+  /**
+   * Check some HTTP status codes....
+   */
+  public void testRetryCodes() {
+    MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(500,
+        "Server Error", false, false), new MockXhr(404, "Not Found", false,
+        false, "Cache-Control", "no-cache"), new MockXhr(200, "Ok", true, true,
+        "Cache-Control", "no-cache"));
+    xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+      public void loadTerminated(Throwable reason) {
+        fail();
+      }
+    });
+    xls.assertDone();
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/dev/jjs/CompilerSuiteCrossSite.gwt.xml b/user/test/com/google/gwt/dev/jjs/CompilerSuiteCrossSite.gwt.xml
new file mode 100644
index 0000000..fa7ed94
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CompilerSuiteCrossSite.gwt.xml
@@ -0,0 +1,18 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.dev.jjs.CompilerSuite"/>
+  <add-linker name="xs"/>
+</module>
diff --git a/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncFailure.gwt.xml b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncFailure.gwt.xml
new file mode 100644
index 0000000..0a83b65
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncFailure.gwt.xml
@@ -0,0 +1,25 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.dev.jjs.RunAsyncFailure" />
+
+  <add-linker name="xs" />
+
+  <replace-with
+    class="com.google.gwt.user.client.runasync.CrossSiteLoadingStrategyForRunAsyncFailureTest">
+    <when-type-is
+      class="com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy" />
+  </replace-with>
+</module>
diff --git a/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncMetrics.gwt.xml b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncMetrics.gwt.xml
new file mode 100644
index 0000000..ad0e7d7
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncMetrics.gwt.xml
@@ -0,0 +1,20 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.dev.jjs.RunAsyncMetricsIntegrationTest" />
+  <add-linker name="xs" />
+
+  <public path="public" />
+</module>
diff --git a/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncSuite.java b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncSuite.java
new file mode 100644
index 0000000..ffd4eac
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncSuite.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.dev.jjs;
+
+import com.google.gwt.dev.jjs.test.CrossSiteRunAsyncFailureTest;
+import com.google.gwt.dev.jjs.test.CrossSiteRunAsyncMetricsTest;
+import com.google.gwt.dev.jjs.test.CrossSiteRunAsyncTest;
+import com.google.gwt.junit.tools.GWTTestSuite;
+
+import junit.framework.Test;
+
+/**
+ * Tests that GWT.runAsync works with the cross-site linker.
+ */
+public class CrossSiteRunAsyncSuite {
+
+  public static Test suite() {
+    GWTTestSuite suite = new GWTTestSuite("Test for com.google.gwt.dev.jjs");
+
+    suite.addTestSuite(CrossSiteRunAsyncMetricsTest.class);
+    suite.addTestSuite(CrossSiteRunAsyncFailureTest.class);
+    suite.addTestSuite(CrossSiteRunAsyncTest.class);
+    
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
index 29935b6..9e5ae07 100644
--- a/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
+++ b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
@@ -13,10 +13,10 @@
 <!-- limitations under the License.                                         -->
 
 <module>
-    <source path='test' />
-    <servlet path="/runAsyncFailure"
-        class="com.google.gwt.user.server.runasync.RunAsyncFailureServlet"/>
-    <define-linker name="runAsyncFailure"
-        class="com.google.gwt.user.server.runasync.RunAsyncFailureIFrameLinker"/>
-    <add-linker name="runAsyncFailure"/>
+  <inherits name="com.google.gwt.core.Core" />
+  <source path="test" />
+  <servlet path="/runAsyncFailure"
+    class="com.google.gwt.user.server.runasync.RunAsyncFailureServlet" />
+  <set-configuration-property name="iframe.linker.deferredjs.subdir"
+    value="runAsyncFailure/deferredjs" />
 </module>
diff --git a/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml b/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml
index 622cf88..d76e53c 100644
--- a/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml
+++ b/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml
@@ -18,4 +18,5 @@
 <!-- order of those events could change.                                -->
 <module>
     <inherits name="com.google.gwt.dev.jjs.CompilerSuite"/>
+    <script src="empty-gwt-stats.js" />
 </module>
diff --git a/user/test/com/google/gwt/dev/jjs/public/empty-gwt-stats.js b/user/test/com/google/gwt/dev/jjs/public/empty-gwt-stats.js
new file mode 100644
index 0000000..c0b1f28
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/public/empty-gwt-stats.js
@@ -0,0 +1,17 @@
+<!--                                                                        -->
+<!-- Copyright 2010 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<!-- Define an empty __gwtStatsEvent just to make sure that the      -->
+<!-- lightweight metrics machinery runs during bootstrapping  -->
+function __gwtStatsEvent(evt) { return true; }
diff --git a/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncFailureTest.java b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncFailureTest.java
new file mode 100644
index 0000000..9d690ba
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncFailureTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.dev.jjs.test;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests that the cross-site linker handles download failures.
+ */
+@DoNotRunWith(Platform.Devel)
+public class CrossSiteRunAsyncFailureTest extends RunAsyncFailureTest {
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.jjs.CrossSiteRunAsyncFailure";
+  }
+
+  @Override
+  public void testHttpFailureRetries() {
+    if (!GWT.isScript()) {
+      // Cross-site linker is not supported in dev mode
+      return;
+    }
+    super.testHttpFailureRetries();
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncMetricsTest.java b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncMetricsTest.java
new file mode 100644
index 0000000..07d9d2e
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncMetricsTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.test;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests runAsync lightweight metrics when the cross-site linker is used.
+ */
+@DoNotRunWith(Platform.Devel)
+public class CrossSiteRunAsyncMetricsTest extends
+    RunAsyncMetricsIntegrationTest {
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.jjs.CrossSiteRunAsyncMetrics";
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncTest.java b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncTest.java
new file mode 100644
index 0000000..b6c3ee2
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.test;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests GWT.runAsync when used with the cross-site linker.
+ */
+@DoNotRunWith(Platform.Devel)
+public class CrossSiteRunAsyncTest extends RunAsyncTest {
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.jjs.CompilerSuiteCrossSite";
+  }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java b/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
index 05e80d0..aaff5aa 100644
--- a/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
+++ b/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
@@ -17,12 +17,17 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.RunAsyncCallback;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.Timer;
 
 /**
  * Tests runAsync server/network failure handling.
+ *
+ * This is skipped in dev mode because runAsync never fails in dev mode.
  */
+@DoNotRunWith(Platform.Devel)
 public class RunAsyncFailureTest extends GWTTestCase {
 
   abstract static class MyRunAsyncCallback implements RunAsyncCallback {
diff --git a/user/test/com/google/gwt/user/client/runasync/CrossSiteLoadingStrategyForRunAsyncFailureTest.java b/user/test/com/google/gwt/user/client/runasync/CrossSiteLoadingStrategyForRunAsyncFailureTest.java
new file mode 100644
index 0000000..20fa0d5
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/runasync/CrossSiteLoadingStrategyForRunAsyncFailureTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.user.client.runasync;
+
+import com.google.gwt.core.client.impl.CrossSiteLoadingStrategy;
+
+/**
+ * A variant of {@link CrossSiteLoadingStrategy} used for the
+ * {@link com.google.gwt.dev.jjs.test.RunAsyncFailureTest}.
+ * It downloads code fragments via a faulty servlet
+ * instead of the normal "deferredjs" location.
+ */
+public class CrossSiteLoadingStrategyForRunAsyncFailureTest extends
+    CrossSiteLoadingStrategy {
+  @Override
+  protected String getDeferredJavaScriptDirectory() {
+    return "runAsyncFailure/deferredjs/";
+  }
+}
diff --git a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java
deleted file mode 100644
index 56b3222..0000000
--- a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2009 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.user.server.runasync;
-
-import com.google.gwt.core.ext.linker.Shardable;
-import com.google.gwt.core.linker.IFrameLinker;
-
-/**
- * Load modules from a custom servlet in order to test download failure
- * behavior.
- */
-@Shardable
-public class RunAsyncFailureIFrameLinker extends IFrameLinker {
-
-  @Override
-  protected String getFragmentSubdir() {
-    return "runAsyncFailure/" + FRAGMENT_SUBDIR;
-  }
-}
diff --git a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
index a4a28e5..ce19d82 100644
--- a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
+++ b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
@@ -31,48 +31,51 @@
  * Servlet for {@link com.google.gwt.dev.jjs.test.RunAsyncFailureTest}.
  */
 public class RunAsyncFailureServlet extends HttpServlet {
-  
+
   private static final boolean DEBUG = false;
   private static final HashSet<String> errorFragments = new HashSet<String>();
+
+  /**
+   * Sequence of response codes to send back. SC_OK must be last.
+   */
   private static final int[] responses = {
       HttpServletResponse.SC_SERVICE_UNAVAILABLE,
       HttpServletResponse.SC_GATEWAY_TIMEOUT,
       HttpServletResponse.SC_SERVICE_UNAVAILABLE,
-      HttpServletResponse.SC_GATEWAY_TIMEOUT,
-      HttpServletResponse.SC_OK // must be last
-  };
-  
+      HttpServletResponse.SC_GATEWAY_TIMEOUT, HttpServletResponse.SC_OK};
+
   static {
     errorFragments.add("2.cache.js");
   }
-  
+
   private static void debug(String message) {
     if (DEBUG) {
       System.out.println(message);
     }
   }
-  
-  HashMap<String,Integer> triesMap = new HashMap<String,Integer>();
-  
+
+  HashMap<String, Integer> triesMap = new HashMap<String, Integer>();
+
   private int sSerial = 0;
-  
+
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse resp)
       throws ServletException, IOException {
     String originalUri = req.getRequestURI();
     debug("doGet: " + originalUri);
     String uri = originalUri.replace("/runAsyncFailure", "");
-    
+
     int response = getDesiredResponse(uri);
     String fragment = uri.substring(uri.lastIndexOf('/') + 1);
     if (!errorFragments.contains(fragment)
         || response == HttpServletResponse.SC_OK) {
       // Delegate the actual data fetch to the main servlet
+
       String host = req.getLocalName();
       int port = req.getLocalPort();
       String realUrl = "http://" + host + ":" + port + uri;
       debug("Fetching: " + realUrl);
-      
+
       int bytes = 0;
       try {
         URL url = new URL(realUrl);
@@ -91,29 +94,29 @@
         debug("IOException fetching real data: " + e);
         throw e;
       }
-      
+
+      resp.setContentType("text/javascript");
       resp.setHeader("Cache-Control", "no-cache");
       resp.setContentLength(bytes);
-      resp.setContentType("text/html");
       resp.setStatus(HttpServletResponse.SC_OK);
-      
+
       debug("doGet: served " + uri + " (" + bytes + " bytes)");
     } else {
       resp.setHeader("Cache-Control", "no-cache");
       resp.sendError(response, "serial=" + getNextSerial());
-      
+
       debug("doGet: sent error " + response + " for " + uri);
     }
   }
-  
+
   private int getDesiredResponse(String resource) {
     Integer t = triesMap.get(resource);
     int tries = t == null ? 0 : t.intValue();
     triesMap.put(resource, tries + 1);
-    
+
     return responses[tries % responses.length];
   }
-  
+
   private synchronized int getNextSerial() {
     return sSerial++;
   }