aboutsummaryrefslogtreecommitdiff
path: root/contexts/data/lib/closure-library/closure/goog/module/loader.js
blob: a8e3183e183d7ef5eb7f5cc5c9ede8f7092c44ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 *
 * @fileoverview This class supports the dynamic loading of compiled
 * javascript modules at runtime, as descibed in the designdoc.
 *
 *   <http://go/js_modules_design>
 *
 */

goog.provide('goog.module.Loader');

goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.object');



/**
 * The dynamic loading functionality is defined as a class. The class
 * will be used as singleton. There is, however, a two step
 * initialization procedure because parameters need to be passed to
 * the goog.module.Loader instance.
 *
 * @constructor
 */
goog.module.Loader = function() {
  /**
   * Map of module name/array of {symbol name, callback} pairs that are pending
   * to be loaded.
   * @type {Object}
   * @private
   */
  this.pending_ = {};

  /**
   * Provides associative access to each module and the symbols of each module
   * that have aready been loaded (one lookup for the module, another lookup
   * on the module for the symbol).
   * @type {Object}
   * @private
   */
  this.modules_ = {};

  /**
   * Map of module name to module url. Used to avoid fetching the same URL
   * twice by keeping track of in-flight URLs.
   * Note: this allows two modules to be bundled into the same file.
   * @type {Object}
   * @private
   */
  this.pendingModuleUrls_ = {};

  /**
   * The base url to load modules from. This property will be set in init().
   * @type {?string}
   * @private
   */
  this.urlBase_ = null;

  /**
   * Array of modules that have been requested before init() was called.
   * If require() is called before init() was called, the required
   * modules can obviously not yet be loaded, because their URL is
   * unknown. The modules that are requested before init() are
   * therefore stored in this array, and they are loaded at init()
   * time.
   * @type {Array.<string>}
   * @private
   */
  this.pendingBeforeInit_ = [];
};
goog.addSingletonGetter(goog.module.Loader);


/**
 * Creates a full URL to the compiled module code given a base URL and a
 * module name. By default it's urlBase + '_' + module + '.js'.
 * @param {string} urlBase URL to the module files.
 * @param {string} module Module name.
 * @return {string} The full url to the module binary.
 * @private
 */
goog.module.Loader.prototype.getModuleUrl_ = function(urlBase, module) {
  return urlBase + '_' + module + '.js';
};


/**
 * The globally exported name of the load callback. Matches the
 * definition in the js_modular_binary() BUILD rule.
 * @type {string}
 */
goog.module.Loader.LOAD_CALLBACK = '__gjsload__';


/**
 * Loads the module by evaluating the javascript text in the current
 * scope. Uncompiled, base identifiers are visible in the global scope;
 * when compiled they are visible in the closure of the anonymous
 * namespace. Notice that this cannot be replaced by the global eval,
 * because the global eval isn't in the scope of the anonymous
 * namespace function that the jscompiled code lives in.
 *
 * @param {string} t_ The javascript text to evaluate. IMPORTANT: The
 *   name of the identifier is chosen so that it isn't compiled and
 *   hence cannot shadow compiled identifiers in the surrounding scope.
 * @private
 */
goog.module.Loader.loaderEval_ = function(t_) {
  eval(t_);
};


/**
 * Initializes the Loader to be fully functional. Also executes load
 * requests that were received before initialization. Must be called
 * exactly once, with the URL of the base library. Module URLs are
 * derived from the URL of the base library by inserting the module
 * name, preceded by a period, before the .js prefix of the base URL.
 *
 * @param {string} baseUrl The URL of the base library.
 * @param {Function=} opt_urlFunction Function that creates the URL for the
 *     module file. It will be passed the base URL for module files and the
 *     module name and should return the fully-formed URL to the module file to
 *     load.
 */
goog.module.Loader.prototype.init = function(baseUrl, opt_urlFunction) {
  // For the use by the module wrappers, loaderEval_ is exported to
  // the page. Note that, despite the name, this is not part of the
  // API, so it is here and not in api_app.js. Cf. BUILD. Note this is
  // done before the first load requests are sent.
  goog.exportSymbol(goog.module.Loader.LOAD_CALLBACK,
      goog.module.Loader.loaderEval_);

  this.urlBase_ = baseUrl.replace('.js', '');
  if (opt_urlFunction) {
    this.getModuleUrl_ = opt_urlFunction;
  }

  goog.array.forEach(this.pendingBeforeInit_, function(module) {
    this.load_(module);
  }, this);
  goog.array.clear(this.pendingBeforeInit_);
};


/**
 * Requests the loading of a symbol from a module. When the module is
 * loaded, the requested symbol will be passed as argument to the
 * function callback.
 *
 * @param {string} module The name of the module. Usually, the value
 *     is defined as a constant whose name starts with MOD_.
 * @param {number|string} symbol The ID of the symbol. Usually, the value is
 *     defined as a constant whose name starts with SYM_.
 * @param {Function} callback This function will be called with the
 *     resolved symbol as the argument once the module is loaded.
 */
goog.module.Loader.prototype.require = function(module, symbol, callback) {
  var pending = this.pending_;
  var modules = this.modules_;
  if (modules[module]) {
    // already loaded
    callback(modules[module][symbol]);
  } else if (pending[module]) {
    // loading is pending from another require of the same module
    pending[module].push([symbol, callback]);
  } else {
    // not loaded, and not requested
    pending[module] = [[symbol, callback]];  // Yes, really [[ ]].
    // Defer loading to initialization if Loader is not yet
    // initialized, otherwise load the module.
    if (this.urlBase_) {
      this.load_(module);
    } else {
      this.pendingBeforeInit_.push(module);
    }
  }
};


/**
 * Registers a symbol in a loaded module. When called without symbol,
 * registers the module to be fully loaded and executes all callbacks
 * from pending require() callbacks for this module.
 *
 * @param {string} module The name of the module. Cf. parameter module
 *     of method require().
 * @param {number|string=} opt_symbol The symbol being defined, or nothing when
 *     all symbols of the module are defined. Cf. parameter symbol of method
 *     require().
 * @param {Object=} opt_object The object bound to the symbol, or nothing when
 *     all symbols of the module are defined.
 */
goog.module.Loader.prototype.provide = function(
    module, opt_symbol, opt_object) {
  var modules = this.modules_;
  var pending = this.pending_;
  if (!modules[module]) {
    modules[module] = {};
  }
  if (opt_object) {
    // When an object is provided, just register it.
    modules[module][opt_symbol] = opt_object;
  } else if (pending[module]) {
    // When no object is provided, and there are pending require()
    // callbacks for this module, execute them.
    for (var i = 0; i < pending[module].length; ++i) {
      var symbol = pending[module][i][0];
      var callback = pending[module][i][1];
      callback(modules[module][symbol]);
    }
    delete pending[module];
    delete this.pendingModuleUrls_[module];
  }
};


/**
 * Starts to load a module. Assumes that init() was called.
 *
 * @param {string} module The name of the module.
 * @private
 */
goog.module.Loader.prototype.load_ = function(module) {
  // NOTE(user): If the module request happens inside a click handler
  // (presumably inside any user event handler, but the onload event
  // handler is fine), IE will load the script but not execute
  // it. Thus we break out of the current flow of control before we do
  // the load. For the record, for IE it would have been enough to
  // just defer the assignment to src. Safari doesn't execute the
  // script if the assignment to src happens *after* the script
  // element is inserted into the DOM.
  goog.Timer.callOnce(function() {
    // The module might have been registered in the interim (if fetched as part
    // of another module fetch because they share the same url)
    if (this.modules_[module]) {
      return;
    }

    var url = this.getModuleUrl_(this.urlBase_, module);

    // Check if specified URL is already in flight
    var urlInFlight = goog.object.containsValue(this.pendingModuleUrls_, url);
    this.pendingModuleUrls_[module] = url;
    if (urlInFlight) {
      return;
    }

    var s = goog.dom.createDom('script',
        {'type': 'text/javascript', 'src': url});
    document.body.appendChild(s);
  }, 0, this);
};