@openmrs/esm-dynamic-loading 10.0.1-pre.5299 → 10.0.1-pre.5309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 5 files with swc (120.97ms)
1
+ [0] Successfully compiled: 5 files with swc (140.08ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -34,7 +34,8 @@ export declare function importDynamic<T = any>(jsPackage: string, share?: string
34
34
  * @internal
35
35
  *
36
36
  * This internal method is used to ensure that the script belonging
37
- * to the given package is loaded and added to the head.
37
+ * to the given package is loaded and added to the head. Concurrent and
38
+ * repeat calls for the same package share a single request for it.
38
39
  *
39
40
  * @param jsPackage The package to load
40
41
  * @param importMap The import map to use for loading this package.
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-loading.d.ts","sourceRoot":"","sources":["../src/dynamic-loading.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAI1E;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,UAEnC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,GAAG,EACzC,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,MAAkB,EACzB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,OAAO,CAAC,CAAC,CAAC,CAyCZ;AAuBD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,SAAS,iBAoD3E;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,uBAExC"}
1
+ {"version":3,"file":"dynamic-loading.d.ts","sourceRoot":"","sources":["../src/dynamic-loading.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAI1E;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,UAEnC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,GAAG,EACzC,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,MAAkB,EACzB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,OAAO,CAAC,CAAC,CAAC,CAyCZ;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,SAAS,iBAkD3E;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,uBAExC"}
@@ -84,7 +84,8 @@ import { getCurrentPageMap, getImportMapOverrideMap, resetImportMapOverrides } f
84
84
  * @internal
85
85
  *
86
86
  * This internal method is used to ensure that the script belonging
87
- * to the given package is loaded and added to the head.
87
+ * to the given package is loaded and added to the head. Concurrent and
88
+ * repeat calls for the same package share a single request for it.
88
89
  *
89
90
  * @param jsPackage The package to load
90
91
  * @param importMap The import map to use for loading this package.
@@ -110,9 +111,7 @@ import { getCurrentPageMap, getImportMapOverrideMap, resetImportMapOverrides } f
110
111
  }
111
112
  const isOverridden = jsPackage in getImportMapOverrideMap().imports;
112
113
  try {
113
- return await new Promise((resolve, reject)=>{
114
- loadScript(url, resolve, reject);
115
- });
114
+ return await loadScript(url);
116
115
  } catch (err) {
117
116
  if (isOverridden) {
118
117
  dispatchToastShown({
@@ -146,76 +145,65 @@ function isFederatedModule(a) {
146
145
  return typeof a === 'object' && a !== null && 'init' in a && typeof a['init'] === 'function' && 'get' in a && typeof a['get'] === 'function';
147
146
  }
148
147
  // internals to track script loading
149
- // basically, if we're already loading a script, we should wait until the script is loaded
150
- // we use a global to track this
151
- const OPENMRS_SCRIPT_LOADING = Symbol('__openmrs_script_loading');
148
+ // if we're already loading a script, we should wait until the script is loaded
149
+ // we use a global to track this; we use `Symbol.for()` in case this gets loaded
150
+ // twice
151
+ const OPENMRS_SCRIPT_LOADING = Symbol.for('__openmrs_script_loading');
152
152
  /**
153
- * Appends a `<script>` to the DOM with the given URL.
154
- */ function loadScript(url, resolve, reject) {
155
- const scriptElement = document.head.querySelector(`script[src="${url}"]`);
156
- let scriptLoading = window[OPENMRS_SCRIPT_LOADING];
157
- if (!scriptLoading) {
158
- scriptLoading = window[OPENMRS_SCRIPT_LOADING] = new Set([]);
153
+ * The scripts this loader has requested, keyed by URL. A pending entry is in flight and a settled
154
+ * one has loaded; failed requests are removed, so any entry can be awaited rather than re-requested.
155
+ */ function getScriptRegistry() {
156
+ var _window, _OPENMRS_SCRIPT_LOADING;
157
+ return (_window = window)[_OPENMRS_SCRIPT_LOADING = OPENMRS_SCRIPT_LOADING] ?? (_window[_OPENMRS_SCRIPT_LOADING] = new Map());
158
+ }
159
+ /**
160
+ * Appends a `<script>` to the DOM with the given URL, unless it has been requested already, and
161
+ * resolves once it has loaded. Callers arriving at any point in a script's lifecycle share one
162
+ * request for it.
163
+ *
164
+ * A script which fails loading is removed from the DOM and can be retried. A script which throws
165
+ * when loading, however, will remain in the DOM as the script loaded.
166
+ */ function loadScript(url) {
167
+ const registry = getScriptRegistry();
168
+ const requested = registry.get(url);
169
+ if (requested) {
170
+ return requested;
159
171
  }
160
- if (!scriptElement) {
161
- scriptLoading.add(url);
162
- const element = document.createElement('script');
163
- element.src = url;
164
- element.type = 'text/javascript';
165
- element.async = true;
166
- // loadTime() displays an error if a script takes more than 5 seconds to load
172
+ if (document.head.querySelector(`script[src="${url}"]`)) {
173
+ // something other than this loader put the script in the document, so assume it's usable
174
+ console.warn(`Script at ${url} already loaded. Not loading it again.`);
175
+ return Promise.resolve();
176
+ }
177
+ const element = document.createElement('script');
178
+ element.src = url;
179
+ element.type = 'text/javascript';
180
+ element.async = true;
181
+ const request = new Promise((resolve, reject)=>{
182
+ // displays an error if a script takes more than 5 seconds to load
167
183
  const loadTimeout = setTimeout(()=>{
168
184
  console.error(`The script at ${url} did not load within 5 seconds. This may indicate an issue with the imports in the script's entry-point file or with the script's bundler configuration.`);
169
185
  }, 5000); // 5 seconds; this is arbitrary
170
- let loadFn, errFn, finishScriptLoading;
171
- finishScriptLoading = ()=>{
186
+ function finishScriptLoading() {
172
187
  clearTimeout(loadTimeout);
173
- scriptLoading.delete(url);
174
- if (loadFn) {
175
- element.removeEventListener('load', loadFn);
176
- }
177
- if (errFn) {
178
- element.removeEventListener('error', errFn);
179
- }
180
- };
181
- loadFn = ()=>{
188
+ element.removeEventListener('load', loadFn);
189
+ element.removeEventListener('error', errFn);
190
+ }
191
+ function loadFn() {
182
192
  finishScriptLoading();
183
- resolve(null);
184
- };
185
- errFn = (ev)=>{
193
+ resolve();
194
+ }
195
+ function errFn(ev) {
186
196
  finishScriptLoading();
197
+ registry.delete(url);
198
+ element.remove();
187
199
  const msg = `Failed to load script from ${url}`;
188
200
  console.error(msg, ev);
189
201
  reject(ev.message ?? msg);
190
- };
202
+ }
191
203
  element.addEventListener('load', loadFn);
192
204
  element.addEventListener('error', errFn);
193
205
  document.head.appendChild(element);
194
- } else {
195
- if (scriptLoading.has(url)) {
196
- let loadFn, errFn, finishScriptLoading;
197
- finishScriptLoading = ()=>{
198
- if (loadFn) {
199
- scriptElement.removeEventListener('load', loadFn);
200
- }
201
- if (errFn) {
202
- scriptElement.removeEventListener('error', errFn);
203
- }
204
- };
205
- loadFn = ()=>{
206
- finishScriptLoading();
207
- resolve(null);
208
- };
209
- // this errFn does not log anything
210
- errFn = (ev)=>{
211
- finishScriptLoading();
212
- reject(ev.message);
213
- };
214
- scriptElement.addEventListener('load', loadFn);
215
- scriptElement.addEventListener('error', errFn);
216
- } else {
217
- console.warn(`Script at ${url} already loaded. Not loading it again.`);
218
- resolve(null);
219
- }
220
- }
206
+ });
207
+ registry.set(url, request);
208
+ return request;
221
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-dynamic-loading",
3
- "version": "10.0.1-pre.5299",
3
+ "version": "10.0.1-pre.5309",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Utilities for dynamically loading code in OpenMRS",
6
6
  "type": "module",
@@ -54,12 +54,12 @@
54
54
  "access": "public"
55
55
  },
56
56
  "peerDependencies": {
57
- "@openmrs/esm-globals": "^10.0.1-pre.5299",
58
- "@openmrs/esm-translations": "^10.0.1-pre.5299"
57
+ "@openmrs/esm-globals": "^10.0.1-pre.5309",
58
+ "@openmrs/esm-translations": "^10.0.1-pre.5309"
59
59
  },
60
60
  "devDependencies": {
61
- "@openmrs/esm-globals": "10.0.1-pre.5299",
62
- "@openmrs/esm-translations": "10.0.1-pre.5299",
61
+ "@openmrs/esm-globals": "10.0.1-pre.5309",
62
+ "@openmrs/esm-translations": "10.0.1-pre.5309",
63
63
  "@swc/cli": "0.8.1",
64
64
  "@swc/core": "1.15.21",
65
65
  "@vitest/coverage-v8": "^4.1.2",
@@ -58,6 +58,7 @@ describe('dynamic-loading', () => {
58
58
  beforeEach(() => {
59
59
  localStorage.clear();
60
60
  document.head.querySelectorAll('script').forEach((el) => el.remove());
61
+ delete (window as any)[Symbol.for('__openmrs_script_loading')];
61
62
  (globalThis as any).__webpack_share_scopes__ = { default: {} };
62
63
  (window as any).spaBase = '/openmrs/spa';
63
64
  mockGetImportMapOverrideMap.mockReturnValue({ imports: {} });
@@ -128,7 +129,7 @@ describe('dynamic-loading', () => {
128
129
 
129
130
  script.dispatchEvent(new Event('load'));
130
131
 
131
- await expect(promise).resolves.toBeNull();
132
+ await expect(promise).resolves.toBeUndefined();
132
133
  });
133
134
 
134
135
  it('rejects when the script fails to load', async () => {
@@ -201,7 +202,7 @@ describe('dynamic-loading', () => {
201
202
 
202
203
  script.dispatchEvent(new Event('load'));
203
204
 
204
- await expect(promise).resolves.toBeNull();
205
+ await expect(promise).resolves.toBeUndefined();
205
206
  expect(mockGetCurrentPageMap).not.toHaveBeenCalled();
206
207
  });
207
208
 
@@ -216,24 +217,89 @@ describe('dynamic-loading', () => {
216
217
  expect(script).not.toBeNull();
217
218
  script.dispatchEvent(new Event('load'));
218
219
 
219
- await expect(promise).resolves.toBeNull();
220
+ await expect(promise).resolves.toBeUndefined();
220
221
  });
221
222
 
222
- it('does not reload a script that is already in the DOM and finished loading', async () => {
223
+ it('does not re-request a script that has already loaded', async () => {
223
224
  const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
224
225
  mockGetCurrentPageMap.mockResolvedValue({
225
226
  imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' },
226
227
  });
227
228
 
228
- // First load
229
229
  const firstPromise = preloadImport('@openmrs/esm-foo');
230
230
  const script = await waitForScript('http://localhost/foo.js');
231
231
  script.dispatchEvent(new Event('load'));
232
232
  await firstPromise;
233
233
 
234
- // Second load script is in DOM but slug not on window, so it resolves with a warning
235
- await expect(preloadImport('@openmrs/esm-foo')).resolves.toBeNull();
234
+ // the package defines no container global, so this is decided by the script registry alone
235
+ await expect(preloadImport('@openmrs/esm-foo')).resolves.toBeUndefined();
236
+ expect(warnSpy).not.toHaveBeenCalled();
237
+ expect(document.head.querySelectorAll('script[src="http://localhost/foo.js"]')).toHaveLength(1);
238
+ });
239
+
240
+ it('shares the request with a caller that reaches it after the script loaded', async () => {
241
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
242
+ const importMap = { imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' } };
243
+
244
+ let releaseSecondMap!: () => void;
245
+ const secondMap = new Promise((resolve) => {
246
+ releaseSecondMap = () => resolve(importMap);
247
+ });
248
+ mockGetCurrentPageMap.mockResolvedValueOnce(importMap).mockReturnValueOnce(secondMap);
249
+
250
+ const first = preloadImport('@openmrs/esm-foo');
251
+ const second = preloadImport('@openmrs/esm-foo');
252
+
253
+ const script = await waitForScript('http://localhost/foo.js');
254
+ script.dispatchEvent(new Event('load'));
255
+ await first;
256
+
257
+ // the second caller only learns the URL now, long after the load event it needed to see
258
+ releaseSecondMap();
259
+
260
+ await expect(second).resolves.toBeUndefined();
261
+ expect(warnSpy).not.toHaveBeenCalled();
262
+ expect(document.head.querySelectorAll('script[src="http://localhost/foo.js"]')).toHaveLength(1);
263
+ });
264
+
265
+ it('warns about, and does not re-request, a script it did not load itself', async () => {
266
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
267
+ mockGetCurrentPageMap.mockResolvedValue({
268
+ imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' },
269
+ });
270
+
271
+ const foreign = document.createElement('script');
272
+ foreign.src = 'http://localhost/foo.js';
273
+ document.head.appendChild(foreign);
274
+
275
+ await expect(preloadImport('@openmrs/esm-foo')).resolves.toBeUndefined();
236
276
  expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('already loaded'));
277
+ expect(document.head.querySelectorAll('script[src="http://localhost/foo.js"]')).toHaveLength(1);
278
+ });
279
+
280
+ it('retries a script that failed to load', async () => {
281
+ vi.spyOn(console, 'error').mockImplementation(() => {});
282
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
283
+ mockGetCurrentPageMap.mockResolvedValue({
284
+ imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' },
285
+ });
286
+
287
+ const firstPromise = preloadImport('@openmrs/esm-foo');
288
+ const failed = await waitForScript('http://localhost/foo.js');
289
+ failed.dispatchEvent(new ErrorEvent('error', { message: 'net::ERR_FAILED' }));
290
+ await expect(firstPromise).rejects.toBe('net::ERR_FAILED');
291
+
292
+ // the dead element is gone, so the retry is a real request rather than a false success
293
+ expect(document.head.querySelector('script[src="http://localhost/foo.js"]')).toBeNull();
294
+
295
+ const secondPromise = preloadImport('@openmrs/esm-foo');
296
+ const retried = await waitForScript('http://localhost/foo.js');
297
+ expect(retried).not.toBe(failed);
298
+
299
+ retried.dispatchEvent(new Event('load'));
300
+
301
+ await expect(secondPromise).resolves.toBeUndefined();
302
+ expect(warnSpy).not.toHaveBeenCalled();
237
303
  });
238
304
 
239
305
  it('resolves both callers when the same script is preloaded concurrently', async () => {
@@ -248,8 +314,8 @@ describe('dynamic-loading', () => {
248
314
  const script = await waitForScript('http://localhost/foo.js');
249
315
  script.dispatchEvent(new Event('load'));
250
316
 
251
- await expect(first).resolves.toBeNull();
252
- await expect(second).resolves.toBeNull();
317
+ await expect(first).resolves.toBeUndefined();
318
+ await expect(second).resolves.toBeUndefined();
253
319
  });
254
320
 
255
321
  it('rejects both callers when a concurrently-loaded script fails', async () => {
@@ -109,7 +109,8 @@ function humanReadableMs(ms: number) {
109
109
  * @internal
110
110
  *
111
111
  * This internal method is used to ensure that the script belonging
112
- * to the given package is loaded and added to the head.
112
+ * to the given package is loaded and added to the head. Concurrent and
113
+ * repeat calls for the same package share a single request for it.
113
114
  *
114
115
  * @param jsPackage The package to load
115
116
  * @param importMap The import map to use for loading this package.
@@ -140,9 +141,7 @@ export async function preloadImport(jsPackage: string, importMap?: ImportMap) {
140
141
 
141
142
  const isOverridden = jsPackage in getImportMapOverrideMap().imports;
142
143
  try {
143
- return await new Promise<void>((resolve, reject) => {
144
- loadScript(url, resolve, reject);
145
- });
144
+ return await loadScript(url);
146
145
  } catch (err: any) {
147
146
  if (isOverridden) {
148
147
  dispatchToastShown({
@@ -198,99 +197,81 @@ function isFederatedModule(a: unknown): a is FederatedModule {
198
197
  }
199
198
 
200
199
  // internals to track script loading
201
- // basically, if we're already loading a script, we should wait until the script is loaded
202
- // we use a global to track this
203
- const OPENMRS_SCRIPT_LOADING = Symbol('__openmrs_script_loading');
200
+
201
+ // if we're already loading a script, we should wait until the script is loaded
202
+ // we use a global to track this; we use `Symbol.for()` in case this gets loaded
203
+ // twice
204
+ const OPENMRS_SCRIPT_LOADING = Symbol.for('__openmrs_script_loading');
205
+
206
+ /**
207
+ * The scripts this loader has requested, keyed by URL. A pending entry is in flight and a settled
208
+ * one has loaded; failed requests are removed, so any entry can be awaited rather than re-requested.
209
+ */
210
+ function getScriptRegistry(): Map<string, Promise<void>> {
211
+ return (window[OPENMRS_SCRIPT_LOADING] ??= new Map<string, Promise<void>>());
212
+ }
204
213
 
205
214
  /**
206
- * Appends a `<script>` to the DOM with the given URL.
215
+ * Appends a `<script>` to the DOM with the given URL, unless it has been requested already, and
216
+ * resolves once it has loaded. Callers arriving at any point in a script's lifecycle share one
217
+ * request for it.
218
+ *
219
+ * A script which fails loading is removed from the DOM and can be retried. A script which throws
220
+ * when loading, however, will remain in the DOM as the script loaded.
207
221
  */
208
- function loadScript(
209
- url: string,
210
- resolve: (value: unknown | PromiseLike<unknown>) => void,
211
- reject: (reason?: any) => void,
212
- ) {
213
- const scriptElement = document.head.querySelector(`script[src="${url}"]`);
214
- let scriptLoading: Set<string> = window[OPENMRS_SCRIPT_LOADING];
215
- if (!scriptLoading) {
216
- scriptLoading = window[OPENMRS_SCRIPT_LOADING] = new Set([]);
222
+ function loadScript(url: string): Promise<void> {
223
+ const registry = getScriptRegistry();
224
+
225
+ const requested = registry.get(url);
226
+ if (requested) {
227
+ return requested;
228
+ }
229
+
230
+ if (document.head.querySelector(`script[src="${url}"]`)) {
231
+ // something other than this loader put the script in the document, so assume it's usable
232
+ console.warn(`Script at ${url} already loaded. Not loading it again.`);
233
+ return Promise.resolve();
217
234
  }
218
235
 
219
- if (!scriptElement) {
220
- scriptLoading.add(url);
221
- const element = document.createElement('script');
222
- element.src = url;
223
- element.type = 'text/javascript';
224
- element.async = true;
236
+ const element = document.createElement('script');
237
+ element.src = url;
238
+ element.type = 'text/javascript';
239
+ element.async = true;
225
240
 
226
- // loadTime() displays an error if a script takes more than 5 seconds to load
241
+ const request = new Promise<void>((resolve, reject) => {
242
+ // displays an error if a script takes more than 5 seconds to load
227
243
  const loadTimeout = setTimeout(() => {
228
244
  console.error(
229
245
  `The script at ${url} did not load within 5 seconds. This may indicate an issue with the imports in the script's entry-point file or with the script's bundler configuration.`,
230
246
  );
231
247
  }, 5_000); // 5 seconds; this is arbitrary
232
248
 
233
- let loadFn: () => void, errFn: (ev: ErrorEvent) => void, finishScriptLoading: () => void;
234
-
235
- finishScriptLoading = () => {
249
+ function finishScriptLoading() {
236
250
  clearTimeout(loadTimeout);
237
- scriptLoading.delete(url);
238
-
239
- if (loadFn) {
240
- element.removeEventListener('load', loadFn);
241
- }
242
-
243
- if (errFn) {
244
- element.removeEventListener('error', errFn);
245
- }
246
- };
251
+ element.removeEventListener('load', loadFn);
252
+ element.removeEventListener('error', errFn);
253
+ }
247
254
 
248
- loadFn = () => {
255
+ function loadFn() {
249
256
  finishScriptLoading();
250
- resolve(null);
251
- };
257
+ resolve();
258
+ }
252
259
 
253
- errFn = (ev: ErrorEvent) => {
260
+ function errFn(ev: ErrorEvent) {
254
261
  finishScriptLoading();
262
+ registry.delete(url);
263
+ element.remove();
255
264
  const msg = `Failed to load script from ${url}`;
256
265
  console.error(msg, ev);
257
266
  reject(ev.message ?? msg);
258
- };
267
+ }
259
268
 
260
269
  element.addEventListener('load', loadFn);
261
270
  element.addEventListener('error', errFn);
262
271
 
263
272
  document.head.appendChild(element);
264
- } else {
265
- if (scriptLoading.has(url)) {
266
- let loadFn: () => void, errFn: (ev: ErrorEvent) => void, finishScriptLoading: () => void;
267
-
268
- finishScriptLoading = () => {
269
- if (loadFn) {
270
- scriptElement.removeEventListener('load', loadFn);
271
- }
272
-
273
- if (errFn) {
274
- scriptElement.removeEventListener('error', errFn);
275
- }
276
- };
277
-
278
- loadFn = () => {
279
- finishScriptLoading();
280
- resolve(null);
281
- };
282
-
283
- // this errFn does not log anything
284
- errFn = (ev: ErrorEvent) => {
285
- finishScriptLoading();
286
- reject(ev.message);
287
- };
288
-
289
- scriptElement.addEventListener('load', loadFn);
290
- scriptElement.addEventListener('error', errFn);
291
- } else {
292
- console.warn(`Script at ${url} already loaded. Not loading it again.`);
293
- resolve(null);
294
- }
295
- }
273
+ });
274
+
275
+ registry.set(url, request);
276
+ return request;
296
277
  }