1 | 'use strict';
|
2 |
|
3 | const url = require('url');
|
4 | const JSONStream = require('JSONStream');
|
5 | const request = require('request');
|
6 | const JsWriter = require('@asset-pipe/js-writer');
|
7 | const CssWriter = require('@asset-pipe/css-writer');
|
8 | const assert = require('assert');
|
9 | const { extname } = require('path');
|
10 | const isStream = require('is-stream');
|
11 | const Joi = require('joi');
|
12 | const Boom = require('boom');
|
13 | const { hashArray } = require('@asset-pipe/common');
|
14 | const schemas = require('./schemas');
|
15 | const { buildURL } = require('../lib/utils');
|
16 | const Metrics = require('@metrics/client');
|
17 | const abslog = require('abslog');
|
18 | const AssetDevMiddleware = require('@asset-pipe/dev-middleware');
|
19 | const ow = require('ow');
|
20 |
|
21 | function getStream(stream) {
|
22 | const data = [];
|
23 | return new Promise((resolve, reject) => {
|
24 | stream.once('error', reject);
|
25 | stream.on('data', chunk => data.push(chunk));
|
26 | stream.on('end', () => resolve(data));
|
27 | });
|
28 | }
|
29 |
|
30 | const extensions = Object.freeze({
|
31 | JS: '.js',
|
32 | CSS: '.css',
|
33 | });
|
34 |
|
35 | const endpoints = Object.freeze({
|
36 | UPLOAD: 'feed',
|
37 | BUNDLE: 'bundle',
|
38 | PUBLISH_ASSETS: 'publish-assets',
|
39 | PUBLISH_INSTRUCTIONS: 'publish-instructions',
|
40 | });
|
41 |
|
42 | const assetTypes = ['js', 'css'];
|
43 |
|
44 | function post(options) {
|
45 | const opts = {
|
46 | url: options.url,
|
47 | headers: {
|
48 | 'content-type': 'application/json',
|
49 | accept: 'application/json',
|
50 | },
|
51 | };
|
52 |
|
53 | if (options.serverId) {
|
54 | opts.headers['origin-server-id'] = options.serverId;
|
55 | }
|
56 |
|
57 | if (!isStream(options.body)) {
|
58 | opts.body = options.body;
|
59 | }
|
60 |
|
61 | return new Promise((resolve, reject) => {
|
62 | const req = request.post(opts, (error, response, body) => {
|
63 | if (error) return reject(error);
|
64 | try {
|
65 | body = JSON.parse(body);
|
66 | } catch (err) {}
|
67 | resolve({ response, body });
|
68 | });
|
69 |
|
70 | if (isStream(options.body)) {
|
71 | options.body.pipe(req);
|
72 | }
|
73 | });
|
74 | }
|
75 |
|
76 | function get(uri) {
|
77 | return new Promise((resolve, reject) => {
|
78 | request(uri, (error, response, body) => {
|
79 | if (error) return reject(error);
|
80 | resolve({ response, body });
|
81 | });
|
82 | });
|
83 | }
|
84 |
|
85 | module.exports = class AssetPipeClient {
|
86 | constructor({
|
87 | serverId,
|
88 | server,
|
89 | buildServerUri,
|
90 | minify,
|
91 | sourceMaps,
|
92 | logger,
|
93 | tag,
|
94 | development,
|
95 | rebundle = true,
|
96 | } = {}) {
|
97 | this.buildServerUri = server || buildServerUri;
|
98 | assert(
|
99 | this.buildServerUri,
|
100 | `Expected "server" to be a uri, got ${this.buildServerUri}`
|
101 | );
|
102 | this.publicFeedUrl = null;
|
103 | this.publicBundleUrl = null;
|
104 | this.serverId = serverId;
|
105 |
|
106 | this.transforms = [];
|
107 | this.plugins = [];
|
108 | this.minify = minify;
|
109 | this.sourceMaps = sourceMaps;
|
110 | this.metrics = new Metrics();
|
111 | this.log = abslog(logger);
|
112 | this.tag = tag;
|
113 | this.development = Boolean(development);
|
114 | this.instructions = { js: [], css: [] };
|
115 | this.assets = { js: null, css: null };
|
116 | this.hashes = { js: null, css: null };
|
117 | this.bundleURLs = { js: '', css: '' };
|
118 | this.publishPromises = [];
|
119 | this.bundlePromises = [];
|
120 | this.verifyingBundle = { js: false, css: false };
|
121 | this.rebundle = rebundle;
|
122 | }
|
123 |
|
124 | transform(transform, options) {
|
125 | this.transforms.push({
|
126 | transform,
|
127 | options,
|
128 | });
|
129 | }
|
130 |
|
131 | plugin(plugin, options) {
|
132 | this.plugins.push({
|
133 | plugin,
|
134 | options,
|
135 | });
|
136 | }
|
137 |
|
138 | uploadJsFeed(files, options) {
|
139 | const writer = new JsWriter(files, options);
|
140 |
|
141 | this.transforms.forEach(entry => {
|
142 | writer.transform(entry.transform, entry.options);
|
143 | });
|
144 |
|
145 | this.plugins.forEach(entry => {
|
146 | writer.plugin(entry.plugin, entry.options);
|
147 | });
|
148 |
|
149 | return post({
|
150 | url: url.resolve(this.buildServerUri, `${endpoints.UPLOAD}/js`),
|
151 | body: writer.bundle().pipe(JSONStream.stringify()),
|
152 | serverId: this.serverId,
|
153 | });
|
154 | }
|
155 |
|
156 | uploadCssFeed(files) {
|
157 | const writer = new CssWriter(files);
|
158 | return post({
|
159 | url: url.resolve(this.buildServerUri, `${endpoints.UPLOAD}/css`),
|
160 | body: writer.bundle().pipe(JSONStream.stringify()),
|
161 | serverId: this.serverId,
|
162 | });
|
163 | }
|
164 |
|
165 | |
166 |
|
167 |
|
168 |
|
169 | async uploadFeed(files, options = {}) {
|
170 | assert(
|
171 | Array.isArray(files),
|
172 | `Expected 'files' to be an array, instead got ${files}`
|
173 | );
|
174 | assert(
|
175 | files.length,
|
176 | `Expected 'files' array to contain at least 1 item`
|
177 | );
|
178 | assert(
|
179 | files.every(file => typeof file == 'string'),
|
180 | `Expected each item in array 'files' to be a string, got ${files}`
|
181 | );
|
182 | assert(
|
183 | files.every(file => file.includes('.js')) ||
|
184 | files.every(file => file.includes('.css')),
|
185 | `Expected ALL items in array 'files' to end with .js or ALL items in array 'files' to end with .css, got ${files.join(
|
186 | ', '
|
187 | )}`
|
188 | );
|
189 |
|
190 | let upload;
|
191 |
|
192 | switch (extname(files[0])) {
|
193 | case extensions.CSS:
|
194 | upload = await this.uploadCssFeed(files);
|
195 | break;
|
196 | case extensions.JS:
|
197 | upload = await this.uploadJsFeed(files, options);
|
198 | break;
|
199 | }
|
200 |
|
201 | const { response, body } = upload;
|
202 | if (response.statusCode === 200) {
|
203 | return Promise.resolve(body);
|
204 | }
|
205 | if (response.statusCode === 400) {
|
206 | throw new Error(body.message);
|
207 | }
|
208 | throw new Error(
|
209 | `Asset build server responded with unknown error. Http status ${
|
210 | response.statusCode
|
211 | }`
|
212 | );
|
213 | }
|
214 |
|
215 | |
216 |
|
217 |
|
218 |
|
219 | async createRemoteBundle(sources, type) {
|
220 | assert(
|
221 | Array.isArray(sources),
|
222 | `Expected argument 'sources' to be an array. Instead got ${typeof sources}`
|
223 | );
|
224 | assert(
|
225 | sources.every(source => typeof source === 'string'),
|
226 | `Expected all entries in array 'sources' to be strings. Instead got ${sources}`
|
227 | );
|
228 | assert(
|
229 | sources.every(source => source.includes('.json')),
|
230 | `Expected ALL items in array 'sources' to end with .json. Instead got ${sources}`
|
231 | );
|
232 | assert(
|
233 | assetTypes.includes(type),
|
234 | `Expected argument 'type' to be one of ${assetTypes.join(
|
235 | '|'
|
236 | )}. Instead got '${type}'`
|
237 | );
|
238 | const { response, body } = await post({
|
239 | url: url.resolve(
|
240 | this.buildServerUri,
|
241 | `${endpoints.BUNDLE}/${type}`
|
242 | ),
|
243 | body: JSON.stringify(sources),
|
244 | serverId: this.serverId,
|
245 | });
|
246 |
|
247 | const { statusCode } = response;
|
248 |
|
249 | if ([200, 202].includes(statusCode)) {
|
250 | return Promise.resolve(body);
|
251 | }
|
252 |
|
253 | const { message } = body;
|
254 |
|
255 | if (statusCode === 400) {
|
256 | throw new Error(message);
|
257 | }
|
258 |
|
259 | throw new Error(
|
260 | `Asset build server responded with unknown error. Http status ${statusCode}. Original message: "${message}".`
|
261 | );
|
262 | }
|
263 |
|
264 | writer(type, files, options) {
|
265 | Joi.assert(
|
266 | type,
|
267 | schemas.type,
|
268 | `Invalid 'type' argument given when attempting to determine writer.`
|
269 | );
|
270 |
|
271 | if (type === 'css') {
|
272 | return new CssWriter(files);
|
273 | } else {
|
274 | const writer = new JsWriter(files, options);
|
275 |
|
276 | this.transforms.forEach(entry => {
|
277 | writer.transform(entry.transform, entry.options);
|
278 | });
|
279 |
|
280 | this.plugins.forEach(entry => {
|
281 | writer.plugin(entry.plugin, entry.options);
|
282 | });
|
283 |
|
284 | return writer;
|
285 | }
|
286 | }
|
287 |
|
288 | determineType(values) {
|
289 | return extname((values && values[0]) || '').replace('.', '');
|
290 | }
|
291 |
|
292 | async publishAssets(tag, entrypoints, options = {}) {
|
293 | Joi.assert(
|
294 | tag,
|
295 | schemas.tag,
|
296 | `Invalid 'tag' argument given when attempting to publish assets.`
|
297 | );
|
298 |
|
299 | if (entrypoints && !Array.isArray(entrypoints)) {
|
300 | entrypoints = [entrypoints];
|
301 | }
|
302 |
|
303 | Joi.assert(
|
304 | entrypoints,
|
305 | schemas.files,
|
306 | `Invalid 'entrypoints' argument given when attempting to publish assets.`
|
307 | );
|
308 | Joi.assert(
|
309 | options,
|
310 | schemas.options,
|
311 | `Invalid 'options' argument given when attempting to publish assets.`
|
312 | );
|
313 |
|
314 | const opts = {
|
315 | minify: this.minify,
|
316 | sourceMaps: this.sourceMaps,
|
317 | rebundle: this.rebundle,
|
318 | ...options,
|
319 | };
|
320 |
|
321 | const metricEnd = this.metrics.timer({
|
322 | name: 'publish_assets_timer',
|
323 | description: 'Time spent publishing assets',
|
324 | meta: { assetType: null, statusCode: null },
|
325 | });
|
326 |
|
327 | const type = this.determineType(entrypoints);
|
328 |
|
329 | try {
|
330 | const writer = this.writer(type, entrypoints, options);
|
331 |
|
332 | const data = await getStream(writer.bundle());
|
333 |
|
334 | this.log.trace(
|
335 | `${type} asset feed produced for tag "${tag}", entrypoints "${JSON.stringify(
|
336 | entrypoints
|
337 | )}" and options "${JSON.stringify(options)}"`
|
338 | );
|
339 |
|
340 | const {
|
341 | response: { statusCode },
|
342 | body,
|
343 | } = await post({
|
344 | url: buildURL(
|
345 | url.resolve(
|
346 | this.buildServerUri,
|
347 | `${endpoints.PUBLISH_ASSETS}`
|
348 | ),
|
349 | {
|
350 | minify: opts.minify,
|
351 | sourceMaps: opts.sourceMaps,
|
352 | rebundle: opts.rebundle,
|
353 | }
|
354 | ),
|
355 | body: JSON.stringify({
|
356 | tag,
|
357 | type,
|
358 | data,
|
359 | }),
|
360 | serverId: this.serverId,
|
361 | });
|
362 |
|
363 | const { message } = body;
|
364 |
|
365 | if (statusCode === 200) {
|
366 | metricEnd({ meta: { assetType: type, statusCode } });
|
367 | this.log.debug(
|
368 | `${type} asset feed successfully published to asset server "${
|
369 | this.buildServerUri
|
370 | }" as files "${body.id}.json" and "${body.id}.${type}"`
|
371 | );
|
372 | return body;
|
373 | }
|
374 |
|
375 | if (statusCode === 400) {
|
376 | throw Boom.badRequest(message);
|
377 | }
|
378 |
|
379 | throw new Boom(
|
380 | `Asset build server responded with an error. Original message: ${message}.`,
|
381 | {
|
382 | statusCode,
|
383 | }
|
384 | );
|
385 | } catch (err) {
|
386 | metricEnd({
|
387 | meta: { assetType: type, statusCode: err.statusCode || 500 },
|
388 | });
|
389 |
|
390 | throw err;
|
391 | }
|
392 | }
|
393 |
|
394 | async publishInstructions(tag, type, data, options = {}) {
|
395 | Joi.assert(
|
396 | tag,
|
397 | schemas.tag,
|
398 | `Invalid 'tag' argument given when attempting to publish instructions.`
|
399 | );
|
400 | Joi.assert(
|
401 | type,
|
402 | schemas.type,
|
403 | `Invalid 'type' argument given when attempting to publish instructions.`
|
404 | );
|
405 | Joi.assert(
|
406 | data,
|
407 | schemas.bundleInstruction,
|
408 | `Invalid 'data' argument given when attempting to publish instructions.`
|
409 | );
|
410 |
|
411 | const opts = {
|
412 | minify: this.minify,
|
413 | sourceMaps: this.sourceMaps,
|
414 | ...options,
|
415 | };
|
416 |
|
417 | const metricEnd = this.metrics.timer({
|
418 | name: 'publish_instructions_timer',
|
419 | description: 'Time spent publishing instructions',
|
420 | meta: { assetType: null, statusCode: null },
|
421 | });
|
422 |
|
423 | try {
|
424 | const {
|
425 | response: { statusCode },
|
426 | body,
|
427 | } = await post({
|
428 | url: buildURL(
|
429 | url.resolve(
|
430 | this.buildServerUri,
|
431 | `${endpoints.PUBLISH_INSTRUCTIONS}`
|
432 | ),
|
433 | { minify: opts.minify, sourceMaps: opts.sourceMaps }
|
434 | ),
|
435 | body: JSON.stringify({ tag, type, data }),
|
436 | serverId: this.serverId,
|
437 | });
|
438 |
|
439 | const { message } = body;
|
440 |
|
441 | if ([200, 204].includes(statusCode)) {
|
442 | metricEnd({ meta: { assetType: type, statusCode } });
|
443 | this.log.debug(
|
444 | `${type} asset bundling instructions successfully published to asset server "${
|
445 | this.buildServerUri
|
446 | }" using tag "${tag}", data "${JSON.stringify(
|
447 | data
|
448 | )}" and options "${JSON.stringify(options)}"`
|
449 | );
|
450 | return Promise.resolve(body);
|
451 | }
|
452 |
|
453 | if (statusCode === 400) {
|
454 | throw Boom.badRequest(message);
|
455 | }
|
456 |
|
457 | throw new Boom(
|
458 | `Asset build server responded with an error. Original message: ${message}.`,
|
459 | {
|
460 | statusCode,
|
461 | }
|
462 | );
|
463 | } catch (err) {
|
464 | metricEnd({
|
465 | meta: { assetType: type, statusCode: err.statusCode || 500 },
|
466 | });
|
467 |
|
468 | throw err;
|
469 | }
|
470 | }
|
471 |
|
472 | async sync() {
|
473 | if (!this.publicFeedUrl || !this.publicBundleUrl) {
|
474 | const metricEnd = this.metrics.timer({
|
475 | name: 'asset_server_sync_timer',
|
476 | description: 'Time spent syncing with asset server',
|
477 | meta: { statusCode: null },
|
478 | });
|
479 |
|
480 | const { body } = await get(
|
481 | url.resolve(this.buildServerUri, '/sync/')
|
482 | );
|
483 | try {
|
484 | const parsedBody = JSON.parse(body);
|
485 | this.publicFeedUrl = parsedBody.publicFeedUrl;
|
486 | this.publicBundleUrl = parsedBody.publicBundleUrl;
|
487 |
|
488 | metricEnd({ meta: { statusCode: 200 } });
|
489 | this.log.debug(
|
490 | `asset server sync successfully performed against asset server "${
|
491 | this.buildServerUri
|
492 | }", publicFeedUrl set to "${
|
493 | this.publicFeedUrl
|
494 | }" and publicBundleUrl set to "${this.publicBundleUrl}"`
|
495 | );
|
496 | } catch (err) {
|
497 | metricEnd({ meta: { statusCode: err.statusCode || 500 } });
|
498 |
|
499 | throw Boom.boomify(err, {
|
500 | message:
|
501 | 'Unable to perform client/server sync as server returned an unparsable response',
|
502 | });
|
503 | }
|
504 | }
|
505 | }
|
506 |
|
507 | bundleHash(feedHashes) {
|
508 | return hashArray(feedHashes);
|
509 | }
|
510 |
|
511 | bundleFilename(hash, type) {
|
512 | return `${hash}.${type}`;
|
513 | }
|
514 |
|
515 | bundleURL(feedHashes, options = {}) {
|
516 | assert(
|
517 | Array.isArray(feedHashes),
|
518 | `Expected argument 'feedHashes' to be an array when calling 'bundleURL(feedHashes)'. Instead 'feedHashes' was ${typeof feedHashes}`
|
519 | );
|
520 | assert(
|
521 | feedHashes.every(source => typeof source === 'string'),
|
522 | `Expected all entries in array 'feedHashes' to be strings when calling 'bundleURL(feedHashes)'. Instead 'feedHashes' was ${feedHashes}`
|
523 | );
|
524 |
|
525 | if (feedHashes.length === 0) return null;
|
526 |
|
527 | const { type, prefix } = {
|
528 | prefix:
|
529 | this.publicBundleUrl ||
|
530 | url.resolve(this.buildServerUri, '/bundle/'),
|
531 | type: 'js',
|
532 | ...options,
|
533 | };
|
534 | const hash = this.bundleHash(feedHashes);
|
535 | const filename = this.bundleFilename(hash, type);
|
536 | return url.resolve(prefix, filename);
|
537 | }
|
538 |
|
539 | async bundlingComplete(feedHashes, options = {}) {
|
540 | assert(
|
541 | Array.isArray(feedHashes),
|
542 | `Expected argument 'feedHashes' to be an array when calling 'bundlingComplete(feedHashes)'. Instead 'feedHashes' was ${typeof feedHashes}`
|
543 | );
|
544 | assert(
|
545 | feedHashes.every(source => typeof source === 'string'),
|
546 | `Expected all entries in array 'feedHashes' to be strings when calling 'bundlingComplete(feedHashes)'. Instead 'feedHashes' was ${feedHashes}`
|
547 | );
|
548 |
|
549 | const uri = this.bundleURL(feedHashes, options);
|
550 | if (!uri) return true;
|
551 | const { response } = await get(uri);
|
552 | return response.statusCode >= 200 && response.statusCode < 300;
|
553 | }
|
554 |
|
555 | middleware() {
|
556 | const middlewares = [
|
557 | (req, res, next) =>
|
558 | this.ready()
|
559 | .then(() => next())
|
560 | .catch(next),
|
561 | ];
|
562 |
|
563 | if (this.development) {
|
564 | const devMiddleware = new AssetDevMiddleware(
|
565 | [this.assets.js].filter(Boolean),
|
566 | [this.assets.css].filter(Boolean)
|
567 | );
|
568 |
|
569 | this.transforms.forEach(entry => {
|
570 | devMiddleware.transform(entry.transform, entry.options);
|
571 | });
|
572 |
|
573 | this.plugins.forEach(entry => {
|
574 | devMiddleware.plugin(entry.plugin, entry.options);
|
575 | });
|
576 |
|
577 |
|
578 | middlewares.push(devMiddleware.router());
|
579 | }
|
580 |
|
581 |
|
582 | return middlewares;
|
583 | }
|
584 |
|
585 | js() {
|
586 | return this.hashes.js;
|
587 | }
|
588 |
|
589 | css() {
|
590 | return this.hashes.css;
|
591 | }
|
592 |
|
593 | publish(options = { js: null, css: null }) {
|
594 | ow(options, ow.object.hasAnyKeys('js', 'css'));
|
595 | const { js = null, css = null } = options;
|
596 | ow(js, ow.any(ow.null, ow.string));
|
597 | ow(css, ow.any(ow.null, ow.string));
|
598 |
|
599 | const metricEnd = this.metrics.timer({
|
600 | name: 'publish_all_assets_timer',
|
601 | description: 'Time spent on publishing all assets to asset server',
|
602 | });
|
603 |
|
604 | this.publishPromises = [];
|
605 | this.assets = { js, css };
|
606 |
|
607 | if (js) {
|
608 | const promise = this.publishAssets(this.tag, js, options).then(
|
609 | ({ id }) => (this.hashes.js = id)
|
610 | );
|
611 |
|
612 | this.publishPromises.push(promise);
|
613 | }
|
614 |
|
615 | if (css) {
|
616 | const promise = this.publishAssets(this.tag, css, options).then(
|
617 | ({ id }) => (this.hashes.css = id)
|
618 | );
|
619 |
|
620 | this.publishPromises.push(promise);
|
621 | }
|
622 |
|
623 | return Promise.all(this.publishPromises).then(() => {
|
624 | metricEnd();
|
625 | return this.hashes;
|
626 | });
|
627 | }
|
628 |
|
629 | bundle(options = { js: [], css: [] }) {
|
630 | ow(options, ow.object.hasAnyKeys('js', 'css'));
|
631 | const { js = [], css = [] } = options;
|
632 | ow(js, ow.array.ofType(ow.string));
|
633 | ow(css, ow.array.ofType(ow.string));
|
634 |
|
635 | this.bundlePromises = [];
|
636 | this.instructions = { js, css };
|
637 | if (this.development) return Promise.resolve();
|
638 |
|
639 | if (js.length) {
|
640 | const promise = this.publishInstructions(
|
641 | this.tag,
|
642 | 'js',
|
643 | js,
|
644 | options
|
645 | );
|
646 | this.bundlePromises.push(promise);
|
647 | }
|
648 |
|
649 | if (css.length) {
|
650 | const promise = this.publishInstructions(this.tag, 'css', css);
|
651 | this.bundlePromises.push(promise);
|
652 | }
|
653 |
|
654 | return Promise.all(this.bundlePromises);
|
655 | }
|
656 |
|
657 | assetUrlByType(hashes = [], type) {
|
658 | ow(hashes, ow.array.ofType(ow.string));
|
659 | ow(type, ow.string.oneOf(['js', 'css']));
|
660 |
|
661 | const tags = this.instructions[type];
|
662 | if (!hashes.length) {
|
663 | this.log.trace(
|
664 | `${type} "hashes" argument is an empty array, did you pass forget to pass this in when calling .scripts(hashes) or .styles(hashes)?`
|
665 | );
|
666 | return [];
|
667 | }
|
668 |
|
669 | const metricEnd = this.metrics.timer({
|
670 | name: 'calculate_asset_bundle_urls_timer',
|
671 | description: 'Time spent calculating asset bundle URLs',
|
672 | meta: { type },
|
673 | });
|
674 |
|
675 | if (tags.length === hashes.length) {
|
676 | const cachedBundleURL = this.bundleURLs[type];
|
677 | const calculatedBundleURL = this.bundleURL(hashes, { type });
|
678 |
|
679 |
|
680 | if (cachedBundleURL === calculatedBundleURL) {
|
681 | this.log.trace(
|
682 | `returning optimal bundle "[${calculatedBundleURL}]" as already verified and cached`
|
683 | );
|
684 | metricEnd({ meta: { status: 'in cache' } });
|
685 | return [cachedBundleURL];
|
686 | }
|
687 |
|
688 | if (!this.verifyingBundle[type]) {
|
689 | this.log.trace(
|
690 | `attempting to verify existence of optimal bundle "${calculatedBundleURL}"`
|
691 | );
|
692 | this.verifyingBundle[type] = true;
|
693 | this.bundleURLs[type] = null;
|
694 |
|
695 |
|
696 |
|
697 | this.bundlingComplete(hashes, { type })
|
698 | .then(success => {
|
699 | if (!success) {
|
700 | return Promise.reject(
|
701 | new Error('bundle verification incomplete')
|
702 | );
|
703 | }
|
704 | this.log.trace(
|
705 | `optimal bundle "${calculatedBundleURL}" successfully verified, inserting into cache`
|
706 | );
|
707 | this.bundleURLs[type] = calculatedBundleURL;
|
708 | this.verifyingBundle[type] = false;
|
709 | metricEnd({
|
710 | meta: { status: 'success' },
|
711 | });
|
712 | })
|
713 | .catch(err => {
|
714 | if (err.message !== 'bundle verification incomplete') {
|
715 | this.log.error(err.message);
|
716 | }
|
717 | this.verifyingBundle[type] = false;
|
718 | metricEnd({
|
719 | meta: { status: 'failure' },
|
720 | });
|
721 | });
|
722 | }
|
723 | } else {
|
724 | this.log.trace(
|
725 | `length of arrays "tags" and "hashes" do not match, cannot produce optimal bundle`
|
726 | );
|
727 |
|
728 | metricEnd({ meta: { status: 'not ready' } });
|
729 | }
|
730 |
|
731 | const fallbacks = hashes.map(
|
732 | hash => `${this.publicBundleUrl}${hash}.${type}`
|
733 | );
|
734 |
|
735 | this.log.trace(
|
736 | `optimal requested bundle not currently available, falling back to individual bundles "${JSON.stringify(
|
737 | fallbacks
|
738 | )}"`
|
739 | );
|
740 |
|
741 | return fallbacks;
|
742 | }
|
743 |
|
744 | scripts(hashes = []) {
|
745 | ow(hashes, ow.array.ofType(ow.string));
|
746 |
|
747 | return this.assetUrlByType(hashes, 'js');
|
748 | }
|
749 |
|
750 | styles(hashes = []) {
|
751 | ow(hashes, ow.array.ofType(ow.string));
|
752 |
|
753 | return this.assetUrlByType(hashes, 'css');
|
754 | }
|
755 |
|
756 | async ready() {
|
757 | const metricEnd = this.metrics.timer({
|
758 | name: 'publish_and_bundle_readiness_timer',
|
759 | description:
|
760 | 'Time spent waiting for publishing and bundling to complete after calling ready',
|
761 | });
|
762 |
|
763 | await Promise.all([...this.publishPromises, ...this.bundlePromises]);
|
764 |
|
765 | metricEnd();
|
766 |
|
767 | this.log.trace(
|
768 | `client.ready(): all publishing and bundling operations have now successfully completed`
|
769 | );
|
770 |
|
771 | return true;
|
772 | }
|
773 | };
|