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