@percy/core 1.10.3 → 1.11.0

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.
package/dist/percy.js CHANGED
@@ -1,20 +1,19 @@
1
1
  import PercyClient from '@percy/client';
2
2
  import PercyConfig from '@percy/config';
3
3
  import logger from '@percy/logger';
4
- import Queue from './queue.js';
5
4
  import Browser from './browser.js';
6
5
  import { createPercyServer, createStaticServer } from './api.js';
7
- import { gatherSnapshots, validateSnapshotOptions, discoverSnapshotResources } from './snapshot.js';
8
- import { generatePromise, yieldAll } from './utils.js'; // A Percy instance will create a new build when started, handle snapshot
9
- // creation, asset discovery, and resource uploads, and will finalize the build
10
- // when stopped. Snapshots are processed concurrently and the build is not
11
- // finalized until all snapshots have been handled.
6
+ import { gatherSnapshots, createSnapshotsQueue, validateSnapshotOptions } from './snapshot.js';
7
+ import { discoverSnapshotResources, createDiscoveryQueue } from './discovery.js';
8
+ import { generatePromise, yieldAll, yieldTo } from './utils.js'; // A Percy instance will create a new build when started, handle snapshot creation, asset discovery,
9
+ // and resource uploads, and will finalize the build when stopped. Snapshots are processed
10
+ // concurrently and the build is not finalized until all snapshots have been handled.
12
11
 
13
12
  export class Percy {
14
13
  log = logger('core');
15
14
  readyState = null;
16
- #uploads = new Queue();
17
- #snapshots = new Queue(); // Static shortcut to create and start an instance in one call
15
+ #discovery = null;
16
+ #snapshots = null; // Static shortcut to create and start an instance in one call
18
17
 
19
18
  static async start(options) {
20
19
  let instance = new this(options);
@@ -38,7 +37,7 @@ export class Percy {
38
37
  // implies `dryRun`, silent logs, and adds extra api endpoints
39
38
  testing,
40
39
  // configuration filepath
41
- config,
40
+ config: configFile,
42
41
  // provided to @percy/client
43
42
  token,
44
43
  clientInfo = '',
@@ -50,6 +49,15 @@ export class Percy {
50
49
  // options which will become accessible via the `.config` property
51
50
  ...options
52
51
  } = {}) {
52
+ let {
53
+ percy,
54
+ ...config
55
+ } = PercyConfig.load({
56
+ overrides: options,
57
+ path: configFile
58
+ });
59
+ deferUploads ?? (deferUploads = percy === null || percy === void 0 ? void 0 : percy.deferUploads);
60
+ this.config = config;
53
61
  if (testing) loglevel = 'silent';
54
62
  if (loglevel) this.loglevel(loglevel);
55
63
  this.testing = testing ? {} : null;
@@ -57,30 +65,18 @@ export class Percy {
57
65
  this.skipUploads = this.dryRun || !!skipUploads;
58
66
  this.skipDiscovery = this.dryRun || !!skipDiscovery;
59
67
  this.delayUploads = this.skipUploads || !!delayUploads;
60
- this.deferUploads = this.delayUploads || !!deferUploads;
61
- if (this.deferUploads) this.#uploads.stop();
62
- this.config = PercyConfig.load({
63
- overrides: options,
64
- path: config
65
- });
66
-
67
- if (this.config.discovery.concurrency) {
68
- let {
69
- concurrency
70
- } = this.config.discovery;
71
- this.#uploads.concurrency = concurrency;
72
- this.#snapshots.concurrency = concurrency;
73
- }
74
-
68
+ this.deferUploads = this.skipUploads || !!deferUploads;
75
69
  this.client = new PercyClient({
76
70
  token,
77
71
  clientInfo,
78
72
  environmentInfo
79
73
  });
80
74
  if (server) this.server = createPercyServer(this, port);
81
- this.browser = new Browser(this); // generator methods are wrapped to autorun and return promises
75
+ this.browser = new Browser(this);
76
+ this.#discovery = createDiscoveryQueue(this);
77
+ this.#snapshots = createSnapshotsQueue(this); // generator methods are wrapped to autorun and return promises
82
78
 
83
- for (let m of ['start', 'stop', 'flush', 'idle', 'snapshot']) {
79
+ for (let m of ['start', 'stop', 'flush', 'idle', 'snapshot', 'upload']) {
84
80
  // the original generator can be referenced with percy.yield.<method>
85
81
  let method = (this.yield || (this.yield = {}))[m] = this[m].bind(this);
86
82
 
@@ -101,7 +97,7 @@ export class Percy {
101
97
  } // Set client & environment info, and override loaded config options
102
98
 
103
99
 
104
- setConfig({
100
+ set({
105
101
  clientInfo,
106
102
  environmentInfo,
107
103
  ...config
@@ -126,96 +122,45 @@ export class Percy {
126
122
  this.config = PercyConfig.merge([this.config, config], (path, prev, next) => {
127
123
  // replace arrays instead of merging
128
124
  return Array.isArray(next) && [path, next];
129
- }); // adjust concurrency if necessary
130
-
131
- if (this.config.discovery.concurrency) {
132
- let {
133
- concurrency
134
- } = this.config.discovery;
135
- this.#uploads.concurrency = concurrency;
136
- this.#snapshots.concurrency = concurrency;
137
- }
125
+ }); // adjust queue concurrency
138
126
 
127
+ let {
128
+ concurrency
129
+ } = this.config.discovery;
130
+ this.#discovery.set({
131
+ concurrency
132
+ });
133
+ this.#snapshots.set({
134
+ concurrency
135
+ });
139
136
  return this.config;
140
- } // Resolves once snapshot and upload queues are idle
141
-
142
-
143
- async *idle() {
144
- yield* this.#snapshots.idle();
145
- yield* this.#uploads.idle();
146
- } // Immediately stops all queues, preventing any more tasks from running
147
-
148
-
149
- close() {
150
- this.#snapshots.close(true);
151
- this.#uploads.close(true);
152
- } // Starts a local API server, a browser process, and queues creating a new Percy build which will run
153
- // at a later time when uploads are deferred, or run immediately when not deferred.
137
+ } // Starts a local API server, a browser process, and internal queues.
154
138
 
155
139
 
156
140
  async *start() {
157
141
  // already starting or started
158
142
  if (this.readyState != null) return;
159
- this.readyState = 0; // create a percy build as the first immediately queued task
160
-
161
- let buildTask = this.#uploads.push('build/create', () => {
162
- // pause other queued tasks until after the build is created
163
- this.#uploads.stop();
164
- this.build = {};
165
- return this.client.createBuild().then(({
166
- data: {
167
- id,
168
- attributes
169
- }
170
- }) => {
171
- let url = attributes['web-url'];
172
- let number = attributes['build-number'];
173
- Object.assign(this.build, {
174
- id,
175
- url,
176
- number
177
- });
178
- if (!this.delayUploads) this.#uploads.run();
179
- });
180
- }, 0); // handle deferred build errors
181
-
182
- if (this.deferUploads) {
183
- buildTask.catch(err => {
184
- this.build = {
185
- error: 'Failed to create build'
186
- };
187
- this.log.error(this.build.error);
188
- this.log.error(err);
189
- this.close();
190
- });
191
- }
143
+ this.readyState = 0;
192
144
 
193
145
  try {
194
- var _this$server2;
195
-
196
- // when not deferred, wait until the build is created first
197
- if (!this.deferUploads) await buildTask; // maybe launch the discovery browser
146
+ // start the snapshots queue immediately when not delayed or deferred
147
+ if (!this.delayUploads && !this.deferUploads) yield this.#snapshots.start(); // do not start the discovery queue when not needed
198
148
 
199
- if (!this.skipDiscovery) yield this.browser.launch(); // start the server after everything else is ready
149
+ if (!this.skipDiscovery) yield this.#discovery.start(); // start a local API server for SDK communication
200
150
 
201
- yield (_this$server2 = this.server) === null || _this$server2 === void 0 ? void 0 : _this$server2.listen(); // mark instance as started
151
+ if (this.server) yield this.server.listen(); // log and mark this instance as started
202
152
 
203
153
  this.log.info('Percy has started!');
204
154
  this.readyState = 1;
205
155
  } catch (error) {
206
- var _this$server3;
207
-
208
- // on error, close any running server and browser
209
- await ((_this$server3 = this.server) === null || _this$server3 === void 0 ? void 0 : _this$server3.close());
210
- await this.browser.close(); // mark instance as closed
211
-
212
- this.readyState = 3; // when uploads are deferred, cancel build creation on abort
156
+ var _this$server2;
213
157
 
214
- if (this.deferUploads && error.name === 'AbortError') {
215
- this.#uploads.cancel('build/create');
216
- this.readyState = null;
217
- } // throw an easier-to-understand error when the port is taken
158
+ // on error, close any running server and end queues
159
+ await ((_this$server2 = this.server) === null || _this$server2 === void 0 ? void 0 : _this$server2.close());
160
+ await this.#discovery.end();
161
+ await this.#snapshots.end(); // mark this instance as closed unless aborting
218
162
 
163
+ this.readyState = error.name !== 'AbortError' ? 3 : null; // throw an easier-to-understand error when the port is in use
219
164
 
220
165
  if (error.code === 'EADDRINUSE') {
221
166
  throw new Error('Percy is already running or the port is in use');
@@ -223,53 +168,36 @@ export class Percy {
223
168
  throw error;
224
169
  }
225
170
  }
226
- } // Wait for currently queued snapshots then run and wait for resulting uploads
171
+ } // Resolves once snapshot and upload queues are idle
227
172
 
228
173
 
229
- async *flush(close) {
230
- try {
231
- // wait until the next event loop for synchronous snapshots
232
- yield new Promise(r => setImmediate(r)); // close the snapshot queue and wait for it to empty
233
-
234
- if (this.#snapshots.size) {
235
- if (close) this.#snapshots.close();
236
- yield* this.#snapshots.flush(s => {
237
- // do not log a count when not closing or if asset discovery is disabled
238
- if (!close || this.skipDiscovery) return;
239
- this.log.progress(`Processing ${s} snapshot${s !== 1 ? 's' : ''}...`, !!s);
240
- });
241
- } // run, close, and wait for the upload queue to empty
174
+ async *idle() {
175
+ yield* this.#discovery.idle();
176
+ yield* this.#snapshots.idle();
177
+ } // Wait for currently queued snapshots then run and wait for resulting uploads
242
178
 
243
179
 
244
- if (!this.skipUploads && this.#uploads.size) {
245
- if (close) this.#uploads.close(); // prevent creating an empty build when deferred
180
+ async *flush(options) {
181
+ if (!this.readyState || this.readyState > 2) return;
182
+ let callback = typeof options === 'function' ? options : null;
183
+ options && (options = !callback ? [].concat(options) : null); // wait until the next event loop for synchronous snapshots
246
184
 
247
- if (!this.deferUploads || !this.#uploads.has('build/create') || this.#uploads.size > 1) {
248
- if (this.build && !this.build.id) yield* this.#uploads.idle();
249
- yield* this.#uploads.flush(s => {
250
- // do not log a count when not closing or while creating a build
251
- if (!close || this.#uploads.has('build/create')) return;
252
- this.log.progress(`Uploading ${s} snapshot${s !== 1 ? 's' : ''}...`, !!s);
253
- });
254
- }
255
- }
256
- } catch (error) {
257
- // reopen closed queues when aborted
185
+ yield new Promise(r => setImmediate(r)); // flush and log progress for discovery before snapshots
258
186
 
259
- /* istanbul ignore else: all errors bubble */
260
- if (close && error.name === 'AbortError') {
261
- this.#snapshots.open();
262
- this.#uploads.open();
263
- }
187
+ if (!this.skipDiscovery && this.#discovery.size) {
188
+ if (options) yield* yieldAll(options.map(o => this.#discovery.process(o)));else yield* this.#discovery.flush(size => callback === null || callback === void 0 ? void 0 : callback('Processing', size));
189
+ } // flush and log progress for snapshot uploads
264
190
 
265
- throw error;
191
+
192
+ if (!this.skipUploads && this.#snapshots.size) {
193
+ if (options) yield* yieldAll(options.map(o => this.#snapshots.process(o)));else yield* this.#snapshots.flush(size => callback === null || callback === void 0 ? void 0 : callback('Uploading', size));
266
194
  }
267
- } // Stops the local API server and browser once snapshots have completed and finalizes the Percy
268
- // build. Does nothing if not running. When `force` is true, any queued tasks are cleared.
195
+ } // Stops the local API server and closes the browser and internal queues once snapshots have
196
+ // completed. Does nothing if not running. When `force` is true, any queued snapshots are cleared.
269
197
 
270
198
 
271
199
  async *stop(force) {
272
- var _this$server4, _this$build, _this$build2;
200
+ var _this$server3;
273
201
 
274
202
  // not started, but the browser was launched
275
203
  if (!this.readyState && this.browser.isConnected()) {
@@ -279,16 +207,24 @@ export class Percy {
279
207
 
280
208
  if (!this.readyState || this.readyState > 2) return; // close queues asap
281
209
 
282
- if (force) this.close(); // already stopping
210
+ if (force) {
211
+ this.#discovery.close(true);
212
+ this.#snapshots.close(true);
213
+ } // already stopping
214
+
283
215
 
284
216
  if (this.readyState === 2) return;
285
217
  this.readyState = 2; // log when force stopping
286
218
 
287
- if (force) this.log.info('Stopping percy...');
219
+ if (force) this.log.info('Stopping percy...'); // used to log snapshot count information
220
+
221
+ let info = (state, size) => `${state} ` + `${size} snapshot${size !== 1 ? 's' : ''}`;
288
222
 
289
223
  try {
290
- // process uploads and close queues
291
- yield* this.yield.flush(true);
224
+ // flush discovery and snapshot queues
225
+ yield* this.yield.flush((state, size) => {
226
+ this.log.progress(`${info(state, size)}...`, !!size);
227
+ });
292
228
  } catch (error) {
293
229
  // reset ready state when aborted
294
230
 
@@ -298,81 +234,32 @@ export class Percy {
298
234
  } // if dry-running, log the total number of snapshots
299
235
 
300
236
 
301
- if (this.dryRun && this.#uploads.size) {
302
- let total = this.#uploads.size - 1; // subtract the build task
303
-
304
- this.log.info(`Found ${total} snapshot${total !== 1 ? 's' : ''}`);
305
- } // close any running server and browser
306
-
307
-
308
- await ((_this$server4 = this.server) === null || _this$server4 === void 0 ? void 0 : _this$server4.close());
309
- await this.browser.close(); // finalize and log build info
237
+ if (this.dryRun && this.#snapshots.size) {
238
+ this.log.info(info('Found', this.#snapshots.size));
239
+ } // close server and end queues
310
240
 
311
- let meta = {
312
- build: this.build
313
- };
314
-
315
- if ((_this$build = this.build) !== null && _this$build !== void 0 && _this$build.failed) {
316
- // do not finalize failed builds
317
- this.log.warn(`Build #${this.build.number} failed: ${this.build.url}`, meta);
318
- } else if ((_this$build2 = this.build) !== null && _this$build2 !== void 0 && _this$build2.id) {
319
- // finalize the build
320
- await this.client.finalizeBuild(this.build.id);
321
- this.log.info(`Finalized build #${this.build.number}: ${this.build.url}`, meta);
322
- } else {
323
- // no build was ever created (likely failed while deferred)
324
- this.log.warn('Build not created', meta);
325
- } // mark instance as stopped
326
241
 
242
+ await ((_this$server3 = this.server) === null || _this$server3 === void 0 ? void 0 : _this$server3.close());
243
+ await this.#discovery.end();
244
+ await this.#snapshots.end(); // mark instance as stopped
327
245
 
328
246
  this.readyState = 3;
329
- } // Deprecated capture method
330
-
331
-
332
- capture(options) {
333
- this.log.deprecated('The #capture() method will be ' + 'removed in 1.0.0. Use #snapshot() instead.');
334
- return this.snapshot(options);
335
- } // Takes one or more snapshots of a page while discovering resources to upload with the
336
- // snapshot. Once asset discovery has completed, the queued snapshot will resolve and an upload
337
- // task will be queued separately. Accepts several different syntaxes for taking snapshots using
338
- // various methods.
339
- //
340
- // snapshot(url|{url}|[...url|{url}])
341
- // - requires fully qualified resolvable urls
342
- // - snapshot options may be provided with the object syntax
343
- //
344
- // snapshot({snapshots:[...url|{url}]})
345
- // - optional `baseUrl` prepended to snapshot urls
346
- // - optional `options` apply to all or specific snapshots
347
- //
348
- // snapshot(sitemap|{sitemap})
349
- // - required to be a fully qualified resolvable url ending in `.xml`
350
- // - optional `include`/`exclude` to filter snapshots
351
- // - optional `options` apply to all or specific snapshots
352
- //
353
- // snapshot({serve})
354
- // - server address is prepended to snapshot urls
355
- // - optional `baseUrl` used when serving pages
356
- // - optional `rewrites`/`cleanUrls` to control snapshot urls
357
- // - optional `include`/`exclude` to filter snapshots
358
- // - optional `snapshots`, with fallback to built-in sitemap.xml
359
- // - optional `options` apply to all or specific snapshots
360
- //
361
- // All available syntaxes will eventually push snapshots to the snapshot queue without the need to
362
- // await on this method directly. This method resolves after the snapshot upload is queued, but
363
- // does not await on the upload to complete.
247
+ } // Takes one or more snapshots of a page while discovering resources to upload with the resulting
248
+ // snapshots. Once asset discovery has completed for the provided snapshots, the queued task will
249
+ // resolve and an upload task will be queued separately.
364
250
 
365
251
 
366
252
  snapshot(options) {
367
- var _this$build3;
253
+ var _this$build;
368
254
 
369
255
  if (this.readyState !== 1) {
370
256
  throw new Error('Not running');
371
- } else if ((_this$build3 = this.build) !== null && _this$build3 !== void 0 && _this$build3.error) {
257
+ } else if ((_this$build = this.build) !== null && _this$build !== void 0 && _this$build.error) {
372
258
  throw new Error(this.build.error);
373
259
  } else if (Array.isArray(options)) {
374
260
  return yieldAll(options.map(o => this.yield.snapshot(o)));
375
- }
261
+ } // accept a url for a sitemap or snapshot
262
+
376
263
 
377
264
  if (typeof options === 'string') {
378
265
  options = options.endsWith('.xml') ? {
@@ -385,104 +272,94 @@ export class Percy {
385
272
 
386
273
  options = validateSnapshotOptions(options);
387
274
  this.client.addClientInfo(options.clientInfo);
388
- this.client.addEnvironmentInfo(options.environmentInfo); // return an async generator to allow cancelation
275
+ this.client.addEnvironmentInfo(options.environmentInfo); // without a discovery browser, capture is not possible
276
+
277
+ if (this.skipDiscovery && !this.dryRun && !options.domSnapshot) {
278
+ throw new Error('Cannot capture DOM snapshots when asset discovery is disabled');
279
+ } // return an async generator to allow cancelation
280
+
389
281
 
390
282
  return async function* () {
391
- let server = 'serve' in options ? await createStaticServer(options).listen() : null;
283
+ let server;
392
284
 
393
285
  try {
394
- if (server) {
395
- // automatically set specific static server options
396
- options.baseUrl = new URL(options.baseUrl || '', server.address()).href;
397
- if (!options.snapshots) options.sitemap = new URL('sitemap.xml', options.baseUrl).href;
398
- } // gather snapshots from options
399
-
400
-
401
- let snapshots = yield* gatherSnapshots(this, options);
402
-
403
- try {
404
- // use a try-catch to cancel snapshots that haven't started when the error occurred
405
- yield* yieldAll(snapshots.map(s => this._takeSnapshot(s)));
406
- } catch (error) {
407
- // cancel queued snapshots that may not have started
408
- snapshots.map(s => this._cancelSnapshot(s));
409
- throw error;
410
- }
286
+ if ('serve' in options) {
287
+ // create and start a static server
288
+ let {
289
+ baseUrl,
290
+ snapshots
291
+ } = options;
292
+ server = yield createStaticServer(options).listen();
293
+ baseUrl = options.baseUrl = new URL(baseUrl || '', server.address()).href;
294
+ if (!snapshots) options.sitemap = new URL('sitemap.xml', baseUrl).href;
295
+ } // gather snapshots and discover snapshot resources
296
+
297
+
298
+ yield* discoverSnapshotResources(this.#discovery, {
299
+ skipDiscovery: this.skipDiscovery,
300
+ dryRun: this.dryRun,
301
+ snapshots: yield* gatherSnapshots(options, {
302
+ meta: {
303
+ build: this.build
304
+ },
305
+ config: this.config
306
+ })
307
+ }, snapshot => {
308
+ // push each finished snapshot to the snapshots queue
309
+ this.#snapshots.push(snapshot);
310
+ });
411
311
  } finally {
412
- await (server === null || server === void 0 ? void 0 : server.close());
312
+ var _server;
313
+
314
+ // always close any created server
315
+ await ((_server = server) === null || _server === void 0 ? void 0 : _server.close());
413
316
  }
414
317
  }.call(this);
415
- } // Cancel any pending snapshot or snapshot uploads
416
-
318
+ } // Uploads one or more snapshots directly to the current Percy build
417
319
 
418
- _cancelSnapshot(snapshot) {
419
- this.#snapshots.cancel(`snapshot/${snapshot.name}`);
420
320
 
421
- for (let {
422
- name
423
- } of [snapshot, ...(snapshot.additionalSnapshots || [])]) {
424
- this.#uploads.cancel(`upload/${name}`);
425
- }
426
- } // Resolves after asset discovery has finished and uploads have been queued
321
+ upload(options) {
322
+ if (this.readyState !== 1) {
323
+ throw new Error('Not running');
324
+ } else if (Array.isArray(options)) {
325
+ return yieldAll(options.map(o => this.yield.upload(o)));
326
+ } // validate comparison uploads and warn about any errors
427
327
 
428
328
 
429
- _takeSnapshot(snapshot) {
430
- // cancel any existing snapshot with the same name
431
- this._cancelSnapshot(snapshot);
329
+ if ('tag' in options || 'tiles' in options) {
330
+ var _options$tag;
432
331
 
433
- return this.#snapshots.push(`snapshot/${snapshot.name}`, async function* () {
434
- try {
435
- yield* discoverSnapshotResources(this, snapshot, (snap, resources) => {
436
- if (!this.dryRun) this.log.info(`Snapshot taken: ${snap.name}`, snap.meta);
332
+ // throw when missing required snapshot or tag name
333
+ if (!options.name) throw new Error('Missing required snapshot name');
334
+ if (!((_options$tag = options.tag) !== null && _options$tag !== void 0 && _options$tag.name)) throw new Error('Missing required tag name for comparison'); // normalize, migrate, and remove certain properties from validating
437
335
 
438
- this._scheduleUpload(snap.name, { ...snap,
439
- resources
440
- });
441
- });
442
- } catch (error) {
443
- if (error.name === 'AbortError') {
444
- this.log.error('Received a duplicate snapshot name, ' + `the previous snapshot was aborted: ${snapshot.name}`, snapshot.meta);
445
- } else {
446
- this.log.error(`Encountered an error taking snapshot: ${snapshot.name}`, snapshot.meta);
447
- this.log.error(error, snapshot.meta);
448
- }
449
- }
450
- }.bind(this));
451
- } // Queues a snapshot upload with the provided options
336
+ options = PercyConfig.migrate(options, '/comparison');
337
+ let {
338
+ clientInfo,
339
+ environmentInfo,
340
+ ...comparison
341
+ } = options;
342
+ let errors = PercyConfig.validate(comparison, '/comparison');
452
343
 
344
+ if (errors) {
345
+ this.log.warn('Invalid upload options:');
453
346
 
454
- _scheduleUpload(name, options) {
455
- var _this$build4;
347
+ for (let e of errors) this.log.warn(`- ${e.path}: ${e.message}`);
348
+ }
349
+ } // add client & environment info
456
350
 
457
- if ((_this$build4 = this.build) !== null && _this$build4 !== void 0 && _this$build4.error) throw new Error(this.build.error); // maybe process any existing delayed uploads
458
351
 
459
- if (!this.skipUploads && this.delayUploads && (!this.build || this.build.id)) this.#uploads.run();
460
- return this.#uploads.push(`upload/${name}`, async () => {
461
- // when delayed, stop the queue before other uploads are processed
462
- if (this.readyState < 2 && this.delayUploads) this.#uploads.stop();
352
+ this.client.addClientInfo(options.clientInfo);
353
+ this.client.addEnvironmentInfo(options.environmentInfo); // return an async generator to allow cancelation
463
354
 
355
+ return async function* () {
464
356
  try {
465
- /* istanbul ignore if: useful for other internal packages */
466
- if (typeof options === 'function') options = await options();
467
- await this.client.sendSnapshot(this.build.id, options);
357
+ return yield* yieldTo(this.#snapshots.push(options));
468
358
  } catch (error) {
469
- var _error$response;
470
-
471
- let failed = ((_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.statusCode) === 422 && error.response.body.errors.find(e => {
472
- var _e$source;
473
-
474
- return ((_e$source = e.source) === null || _e$source === void 0 ? void 0 : _e$source.pointer) === '/data/attributes/build';
475
- });
476
- this.log.error(`Encountered an error uploading snapshot: ${name}`, options.meta);
477
- this.log.error((failed === null || failed === void 0 ? void 0 : failed.detail) ?? error, options.meta); // build failed at some point, stop accepting snapshots
478
-
479
- if (failed) {
480
- this.build.error = failed.detail;
481
- this.build.failed = true;
482
- this.close();
483
- }
359
+ this.#snapshots.cancel(options);
360
+ throw error;
484
361
  }
485
- });
362
+ }.call(this);
486
363
  }
487
364
 
488
365
  }