@strapi/data-transfer 5.10.3 → 5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var stream$1 = require('stream');
4
+ var promises = require('stream/promises');
4
5
  var path = require('path');
5
6
  var os = require('os');
6
7
  var streamChain = require('stream-chain');
@@ -654,8 +655,7 @@ const TRANSFER_STAGES = Object.freeze([
654
655
  };
655
656
  const DEFAULT_VERSION_STRATEGY = 'ignore';
656
657
  const DEFAULT_SCHEMA_STRATEGY = 'strict';
657
- var _metadata$1 = /*#__PURE__*/ _class_private_field_loose_key$6("_metadata"), _schema = /*#__PURE__*/ _class_private_field_loose_key$6("_schema"), _handlers = /*#__PURE__*/ _class_private_field_loose_key$6("_handlers"), // Save the currently open stream so that we can access it at any time
658
- _currentStream = /*#__PURE__*/ _class_private_field_loose_key$6("_currentStream"), /**
658
+ var _metadata$1 = /*#__PURE__*/ _class_private_field_loose_key$6("_metadata"), _schema = /*#__PURE__*/ _class_private_field_loose_key$6("_schema"), _handlers = /*#__PURE__*/ _class_private_field_loose_key$6("_handlers"), _currentStreamController = /*#__PURE__*/ _class_private_field_loose_key$6("_currentStreamController"), _aborted = /*#__PURE__*/ _class_private_field_loose_key$6("_aborted"), /**
659
659
  * Create and return a transform stream based on the given stage and options.
660
660
  *
661
661
  * Allowed transformations includes 'filter' and 'map'.
@@ -770,11 +770,9 @@ class TransferEngine {
770
770
  }
771
771
  // Cause an ongoing transfer to abort gracefully
772
772
  async abortTransfer() {
773
- const err = new TransferEngineError('fatal', 'Transfer aborted.');
774
- if (!_class_private_field_loose_base$6(this, _currentStream)[_currentStream]) {
775
- throw err;
776
- }
777
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream].destroy(err);
773
+ _class_private_field_loose_base$6(this, _aborted)[_aborted] = true;
774
+ _class_private_field_loose_base$6(this, _currentStreamController)[_currentStreamController]?.abort();
775
+ throw new TransferEngineError('fatal', 'Transfer aborted.');
778
776
  }
779
777
  async init() {
780
778
  // Resolve providers' resource and store
@@ -1084,7 +1082,11 @@ class TransferEngine {
1084
1082
  writable: true,
1085
1083
  value: void 0
1086
1084
  });
1087
- Object.defineProperty(this, _currentStream, {
1085
+ Object.defineProperty(this, _currentStreamController, {
1086
+ writable: true,
1087
+ value: void 0
1088
+ });
1089
+ Object.defineProperty(this, _aborted, {
1088
1090
  writable: true,
1089
1091
  value: void 0
1090
1092
  });
@@ -1094,6 +1096,7 @@ class TransferEngine {
1094
1096
  schemaDiff: [],
1095
1097
  errors: {}
1096
1098
  };
1099
+ _class_private_field_loose_base$6(this, _aborted)[_aborted] = false;
1097
1100
  this.diagnostics = createDiagnosticReporter();
1098
1101
  validateProvider('source', sourceProvider);
1099
1102
  validateProvider('destination', destinationProvider);
@@ -1292,6 +1295,9 @@ function assertSchemasMatching(sourceSchemas, destinationSchemas) {
1292
1295
  }
1293
1296
  }
1294
1297
  async function transferStage(options) {
1298
+ if (_class_private_field_loose_base$6(this, _aborted)[_aborted]) {
1299
+ throw new TransferEngineError('fatal', 'Transfer aborted.');
1300
+ }
1295
1301
  const { stage, source, destination, transform, tracker } = options;
1296
1302
  const updateEndTime = ()=>{
1297
1303
  const stageData = this.progress.data[stage];
@@ -1323,27 +1329,42 @@ async function transferStage(options) {
1323
1329
  return;
1324
1330
  }
1325
1331
  _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('start', stage);
1326
- await new Promise((resolve, reject)=>{
1327
- let stream = source;
1332
+ try {
1333
+ const streams = [
1334
+ source
1335
+ ];
1328
1336
  if (transform) {
1329
- stream = stream.pipe(transform);
1337
+ streams.push(transform);
1330
1338
  }
1331
1339
  if (tracker) {
1332
- stream = stream.pipe(tracker);
1333
- }
1334
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream] = stream.pipe(destination).on('error', (e)=>{
1335
- updateEndTime();
1336
- _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('error', stage);
1337
- this.reportError(e, 'error');
1338
- destination.destroy(e);
1339
- reject(e);
1340
- }).on('close', ()=>{
1341
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream] = undefined;
1342
- updateEndTime();
1343
- resolve();
1340
+ streams.push(tracker);
1341
+ }
1342
+ streams.push(destination);
1343
+ // NOTE: to debug/confirm backpressure issues from misbehaving stream, uncomment the following lines
1344
+ // source.on('pause', () => console.log(`[${stage}] Source paused due to backpressure`));
1345
+ // source.on('resume', () => console.log(`[${stage}] Source resumed`));
1346
+ // destination.on('drain', () =>
1347
+ // console.log(`[${stage}] Destination drained, resuming data flow`)
1348
+ // );
1349
+ // destination.on('error', (err) => console.error(`[${stage}] Destination error:`, err));
1350
+ const controller = new AbortController();
1351
+ const { signal } = controller;
1352
+ // Store the controller so you can cancel later
1353
+ _class_private_field_loose_base$6(this, _currentStreamController)[_currentStreamController] = controller;
1354
+ await promises.pipeline(streams, {
1355
+ signal
1344
1356
  });
1345
- });
1346
- _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('finish', stage);
1357
+ _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('finish', stage);
1358
+ } catch (e) {
1359
+ updateEndTime();
1360
+ _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('error', stage);
1361
+ this.reportError(e, 'error');
1362
+ if (!destination.destroyed) {
1363
+ destination.destroy(e);
1364
+ }
1365
+ } finally{
1366
+ updateEndTime();
1367
+ }
1347
1368
  }
1348
1369
  async function resolveProviderResource() {
1349
1370
  const sourceMetadata = await this.sourceProvider.getMetadata();
@@ -1915,9 +1936,7 @@ const createLinkQuery = (strapi, trx)=>{
1915
1936
  assignOrderColumns();
1916
1937
  const qb = connection.insert(payload).into(addSchema(joinTable.name));
1917
1938
  if (trx) {
1918
- await trx.transaction(async (nestedTrx)=>{
1919
- await qb.transacting(nestedTrx);
1920
- });
1939
+ await qb.transacting(trx);
1921
1940
  }
1922
1941
  }
1923
1942
  if ('morphColumn' in attribute && attribute.morphColumn) {
@@ -2370,7 +2389,7 @@ class LocalStrapiDestinationProvider {
2370
2389
  const provider = strapi.config.get('plugin::upload').provider;
2371
2390
  const fileId = fileEntitiesMapper?.[uploadData.id];
2372
2391
  if (!fileId) {
2373
- callback(new Error(`File ID not found for ID: ${uploadData.id}`));
2392
+ return callback(new Error(`File ID not found for ID: ${uploadData.id}`));
2374
2393
  }
2375
2394
  try {
2376
2395
  await strapi.plugin('upload').provider.uploadStream(uploadData);
@@ -2421,9 +2440,9 @@ class LocalStrapiDestinationProvider {
2421
2440
  provider
2422
2441
  }
2423
2442
  });
2424
- callback();
2443
+ return callback();
2425
2444
  } catch (error) {
2426
- callback(new Error(`Error while uploading asset ${chunk.filename} ${error}`));
2445
+ return callback(new Error(`Error while uploading asset ${chunk.filename} ${error}`));
2427
2446
  }
2428
2447
  });
2429
2448
  }
@@ -2845,7 +2864,11 @@ function _class_private_field_loose_key$4(name) {
2845
2864
  const createLocalStrapiSourceProvider = (options)=>{
2846
2865
  return new LocalStrapiSourceProvider(options);
2847
2866
  };
2848
- var _diagnostics$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_diagnostics"), _reportInfo$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_reportInfo");
2867
+ var _diagnostics$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_diagnostics"), _reportInfo$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_reportInfo"), /**
2868
+ * Reports an error to the diagnostic reporter.
2869
+ */ _reportError = /*#__PURE__*/ _class_private_field_loose_key$4("_reportError"), /**
2870
+ * Handles errors that occur in read streams.
2871
+ */ _handleStreamError = /*#__PURE__*/ _class_private_field_loose_key$4("_handleStreamError");
2849
2872
  class LocalStrapiSourceProvider {
2850
2873
  async bootstrap(diagnostics) {
2851
2874
  _class_private_field_loose_base$4(this, _diagnostics$4)[_diagnostics$4] = diagnostics;
@@ -2907,12 +2930,22 @@ class LocalStrapiSourceProvider {
2907
2930
  createAssetsReadStream() {
2908
2931
  assertValidStrapi(this.strapi, 'Not able to stream assets');
2909
2932
  _class_private_field_loose_base$4(this, _reportInfo$4)[_reportInfo$4]('creating assets read stream');
2910
- return createAssetsStream(this.strapi);
2933
+ const stream = createAssetsStream(this.strapi);
2934
+ stream.on('error', (err)=>{
2935
+ _class_private_field_loose_base$4(this, _handleStreamError)[_handleStreamError]('assets', err);
2936
+ });
2937
+ return stream;
2911
2938
  }
2912
2939
  constructor(options){
2913
2940
  Object.defineProperty(this, _reportInfo$4, {
2914
2941
  value: reportInfo$4
2915
2942
  });
2943
+ Object.defineProperty(this, _reportError, {
2944
+ value: reportError
2945
+ });
2946
+ Object.defineProperty(this, _handleStreamError, {
2947
+ value: handleStreamError
2948
+ });
2916
2949
  Object.defineProperty(this, _diagnostics$4, {
2917
2950
  writable: true,
2918
2951
  value: void 0
@@ -2932,6 +2965,29 @@ function reportInfo$4(message) {
2932
2965
  kind: 'info'
2933
2966
  });
2934
2967
  }
2968
+ function reportError(message, error) {
2969
+ _class_private_field_loose_base$4(this, _diagnostics$4)[_diagnostics$4]?.report({
2970
+ details: {
2971
+ createdAt: new Date(),
2972
+ message,
2973
+ error,
2974
+ severity: 'fatal',
2975
+ name: error.name
2976
+ },
2977
+ kind: 'error'
2978
+ });
2979
+ }
2980
+ function handleStreamError(streamType, err) {
2981
+ const { message, stack } = err;
2982
+ const errorMessage = `[Data transfer] Error in ${streamType} read stream: ${message}`;
2983
+ const formattedError = {
2984
+ message: errorMessage,
2985
+ stack,
2986
+ timestamp: new Date().toISOString()
2987
+ };
2988
+ this.strapi?.log.error(formattedError);
2989
+ _class_private_field_loose_base$4(this, _reportError)[_reportError](formattedError.message, err);
2990
+ }
2935
2991
 
2936
2992
  const createDispatcher = (ws, retryMessageOptions = {
2937
2993
  retryMessageMaxRetries: 5,
@@ -3095,17 +3151,6 @@ const connectToWebsocket = (address, options, diagnostics)=>{
3095
3151
  const trimTrailingSlash = (input)=>{
3096
3152
  return input.replace(/\/$/, '');
3097
3153
  };
3098
- const wait = (ms)=>{
3099
- return new Promise((resolve)=>{
3100
- setTimeout(resolve, ms);
3101
- });
3102
- };
3103
- const waitUntil = async (test, interval)=>{
3104
- while(!test()){
3105
- await wait(interval);
3106
- }
3107
- return Promise.resolve();
3108
- };
3109
3154
 
3110
3155
  const TRANSFER_PATH = '/transfer/runner';
3111
3156
  const TRANSFER_METHODS = [
@@ -3526,6 +3571,17 @@ class RemoteStrapiSourceProvider {
3526
3571
  });
3527
3572
  // Init the asset map
3528
3573
  const assets = {};
3574
+ // Watch for stalled assets; if we don't receive a chunk within timeout, abort transfer
3575
+ const resetTimeout = (assetID)=>{
3576
+ if (assets[assetID].timeout) {
3577
+ clearTimeout(assets[assetID].timeout);
3578
+ }
3579
+ assets[assetID].timeout = setTimeout(()=>{
3580
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Asset ${assetID} transfer stalled, aborting.`);
3581
+ assets[assetID].status = 'errored';
3582
+ assets[assetID].stream.destroy(new Error(`Asset ${assetID} transfer timed out`));
3583
+ }, this.options.streamTimeout);
3584
+ };
3529
3585
  stream/**
3530
3586
  * Process a payload of many transfer assets and performs the following tasks:
3531
3587
  * - Start: creates a stream for new assets.
@@ -3536,56 +3592,46 @@ class RemoteStrapiSourceProvider {
3536
3592
  const { action, assetID } = item;
3537
3593
  // Creates the stream to send the incoming asset through
3538
3594
  if (action === 'start') {
3539
- // Ignore the item if a transfer has already been started for the same asset ID
3595
+ // if a transfer has already been started for the same asset ID, something is wrong
3540
3596
  if (assets[assetID]) {
3541
- continue;
3597
+ throw new Error(`Asset ${assetID} already started`);
3542
3598
  }
3599
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Asset ${assetID} starting`);
3543
3600
  // Register the asset
3544
3601
  assets[assetID] = {
3545
3602
  ...item.data,
3546
3603
  stream: new stream$1.PassThrough(),
3547
- status: 'idle',
3604
+ status: 'ok',
3548
3605
  queue: []
3549
3606
  };
3607
+ resetTimeout(assetID);
3550
3608
  // Connect the individual asset stream to the main asset stage stream
3551
3609
  // Note: nothing is transferred until data chunks are fed to the asset stream
3552
3610
  await this.writeAsync(pass, assets[assetID]);
3553
- } else if (action === 'stream') {
3554
- // If the asset hasn't been registered, or if it's been closed already, then ignore the message
3611
+ } else if (action === 'stream' || action === 'end') {
3612
+ // If the asset hasn't been registered, or if it's been closed already, something is wrong
3555
3613
  if (!assets[assetID]) {
3556
- continue;
3614
+ throw new Error(`No id matching ${assetID} for stream action`);
3557
3615
  }
3558
- switch(assets[assetID].status){
3559
- // The asset is ready to accept a new chunk, write it now
3560
- case 'idle':
3561
- await writeAssetChunk(assetID, item.data);
3562
- break;
3563
- // The resource is busy, queue the current chunk so that it gets transferred as soon as possible
3564
- case 'busy':
3565
- assets[assetID].queue.push(item);
3566
- break;
3616
+ // On every action, reset the timeout timer
3617
+ if (action === 'stream') {
3618
+ resetTimeout(assetID);
3619
+ } else {
3620
+ clearTimeout(assets[assetID].timeout);
3567
3621
  }
3568
- } else if (action === 'end') {
3569
- // If the asset has already been closed, or if it was never registered, ignore the command
3570
- if (!assets[assetID]) {
3571
- continue;
3622
+ if (assets[assetID].status === 'closed') {
3623
+ throw new Error(`Asset ${assetID} is closed`);
3572
3624
  }
3573
- switch(assets[assetID].status){
3574
- // There's no ongoing activity, the asset is ready to be closed
3575
- case 'idle':
3576
- case 'errored':
3577
- await closeAssetStream(assetID);
3578
- break;
3579
- // The resource is busy, wait for a different state and close the stream.
3580
- case 'busy':
3581
- await Promise.race([
3582
- // Either: wait for the asset to be ready to be closed
3583
- waitUntil(()=>assets[assetID].status !== 'busy', 100),
3584
- // Or: if the last chunks are still not processed after ten seconds
3585
- wait(10000)
3586
- ]);
3587
- await closeAssetStream(assetID);
3588
- break;
3625
+ assets[assetID].queue.push(item);
3626
+ }
3627
+ }
3628
+ // each new payload will start new processQueue calls, which may cause some extra calls
3629
+ // it's essentially saying "start processing this asset again, I added more data to the queue"
3630
+ for(const assetID in assets){
3631
+ if (Object.prototype.hasOwnProperty.call(assets, assetID)) {
3632
+ const asset = assets[assetID];
3633
+ if (asset.queue?.length > 0) {
3634
+ await processQueue(assetID);
3589
3635
  }
3590
3636
  }
3591
3637
  }
@@ -3593,38 +3639,48 @@ class RemoteStrapiSourceProvider {
3593
3639
  pass.end();
3594
3640
  });
3595
3641
  /**
3596
- * Writes a chunk of data for the specified asset with the given id.
3597
- */ const writeAssetChunk = async (id, data)=>{
3642
+ * Start processing the queue for a given assetID
3643
+ *
3644
+ * Even though this is a loop that attempts to process the entire queue, it is safe to call this more than once
3645
+ * for the same asset id because the queue is shared globally, the items are shifted off, and immediately written
3646
+ */ const processQueue = async (id)=>{
3598
3647
  if (!assets[id]) {
3599
3648
  throw new Error(`Failed to write asset chunk for "${id}". Asset not found.`);
3600
3649
  }
3601
- const { status: currentStatus } = assets[id];
3602
- if (currentStatus !== 'idle') {
3650
+ const asset = assets[id];
3651
+ const { status: currentStatus } = asset;
3652
+ if ([
3653
+ 'closed',
3654
+ 'errored'
3655
+ ].includes(currentStatus)) {
3603
3656
  throw new Error(`Failed to write asset chunk for "${id}". The asset is currently "${currentStatus}"`);
3604
3657
  }
3605
- const nextItemInQueue = ()=>assets[id].queue.shift();
3606
- try {
3607
- // Lock the asset
3608
- assets[id].status = 'busy';
3609
- // Save the current chunk
3610
- await unsafe_writeAssetChunk(id, data);
3611
- // Empty the queue if needed
3612
- let item = nextItemInQueue();
3613
- while(item){
3614
- await unsafe_writeAssetChunk(id, item.data);
3615
- item = nextItemInQueue();
3658
+ while(asset.queue.length > 0){
3659
+ const data = asset.queue.shift();
3660
+ if (!data) {
3661
+ throw new Error(`Invalid chunk found for ${id}`);
3662
+ }
3663
+ try {
3664
+ // if this is an end chunk, close the asset stream
3665
+ if (data.action === 'end') {
3666
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Ending asset stream for ${id}`);
3667
+ await closeAssetStream(id);
3668
+ break; // Exit the loop after closing the stream
3669
+ }
3670
+ // Save the current chunk
3671
+ await writeChunkToStream(id, data);
3672
+ } catch {
3673
+ if (!assets[id]) {
3674
+ throw new Error(`No id matching ${id} for writeAssetChunk`);
3675
+ }
3616
3676
  }
3617
- // Unlock the asset
3618
- assets[id].status = 'idle';
3619
- } catch {
3620
- assets[id].status = 'errored';
3621
3677
  }
3622
3678
  };
3623
3679
  /**
3624
3680
  * Writes a chunk of data to the asset's stream.
3625
3681
  *
3626
3682
  * Only check if the targeted asset exists, no other validation is done.
3627
- */ const unsafe_writeAssetChunk = async (id, data)=>{
3683
+ */ const writeChunkToStream = async (id, data)=>{
3628
3684
  const asset = assets[id];
3629
3685
  if (!asset) {
3630
3686
  throw new Error(`Failed to write asset chunk for "${id}". Asset not found.`);
@@ -3645,9 +3701,11 @@ class RemoteStrapiSourceProvider {
3645
3701
  await new Promise((resolve, reject)=>{
3646
3702
  const { stream } = assets[id];
3647
3703
  stream.on('close', ()=>{
3648
- delete assets[id];
3649
3704
  resolve();
3650
- }).on('error', reject).end();
3705
+ }).on('error', (e)=>{
3706
+ assets[id].status = 'errored';
3707
+ reject(new Error(`Failed to close asset "${id}". Asset stream error: ${e.toString()}`));
3708
+ }).end();
3651
3709
  });
3652
3710
  };
3653
3711
  return pass;
@@ -3762,6 +3820,9 @@ class RemoteStrapiSourceProvider {
3762
3820
  });
3763
3821
  this.name = 'source::remote-strapi';
3764
3822
  this.type = 'source';
3823
+ this.defaultOptions = {
3824
+ streamTimeout: 15000
3825
+ };
3765
3826
  this.writeAsync = (stream, data)=>{
3766
3827
  return new Promise((resolve, reject)=>{
3767
3828
  stream.write(data, (error)=>{
@@ -3772,7 +3833,10 @@ class RemoteStrapiSourceProvider {
3772
3833
  });
3773
3834
  });
3774
3835
  };
3775
- this.options = options;
3836
+ this.options = {
3837
+ ...this.defaultOptions,
3838
+ ...options
3839
+ };
3776
3840
  this.ws = null;
3777
3841
  this.dispatcher = null;
3778
3842
  }
@@ -4705,6 +4769,18 @@ const createPullController = handlerControllerFactory((proto)=>({
4705
4769
  kind: 'warning'
4706
4770
  });
4707
4771
  },
4772
+ onError (error) {
4773
+ this.diagnostics?.report({
4774
+ details: {
4775
+ message: error.message,
4776
+ error,
4777
+ createdAt: new Date(),
4778
+ name: error.name,
4779
+ severity: 'fatal'
4780
+ },
4781
+ kind: 'error'
4782
+ });
4783
+ },
4708
4784
  assertValidTransferAction (action) {
4709
4785
  // Abstract the constant to string[] to allow looser check on the given action
4710
4786
  const validActions = VALID_TRANSFER_ACTIONS;
@@ -4778,6 +4854,15 @@ const createPullController = handlerControllerFactory((proto)=>({
4778
4854
  let batch = [];
4779
4855
  const stream = this.streams?.[stage];
4780
4856
  const batchLength = ()=>Buffer.byteLength(JSON.stringify(batch));
4857
+ const maybeConfirm = async (data)=>{
4858
+ try {
4859
+ await this.confirm(data);
4860
+ } catch (error) {
4861
+ // Handle the error, log it, or take other appropriate actions
4862
+ strapi?.log.error(`[Data transfer] Message confirmation failed: ${error?.message}`);
4863
+ this.onError(error);
4864
+ }
4865
+ };
4781
4866
  const sendBatch = async ()=>{
4782
4867
  await this.confirm({
4783
4868
  type: 'transfer',
@@ -4786,6 +4871,7 @@ const createPullController = handlerControllerFactory((proto)=>({
4786
4871
  error: null,
4787
4872
  id
4788
4873
  });
4874
+ batch = [];
4789
4875
  };
4790
4876
  if (!stream) {
4791
4877
  throw new ProviderTransferError(`No available stream found for ${stage}`);
@@ -4796,7 +4882,6 @@ const createPullController = handlerControllerFactory((proto)=>({
4796
4882
  batch.push(chunk);
4797
4883
  if (batchLength() >= batchSize) {
4798
4884
  await sendBatch();
4799
- batch = [];
4800
4885
  }
4801
4886
  } else {
4802
4887
  await this.confirm({
@@ -4812,7 +4897,6 @@ const createPullController = handlerControllerFactory((proto)=>({
4812
4897
  }
4813
4898
  if (batch.length > 0 && stage !== 'assets') {
4814
4899
  await sendBatch();
4815
- batch = [];
4816
4900
  }
4817
4901
  await this.confirm({
4818
4902
  type: 'transfer',
@@ -4822,7 +4906,8 @@ const createPullController = handlerControllerFactory((proto)=>({
4822
4906
  id
4823
4907
  });
4824
4908
  } catch (e) {
4825
- await this.confirm({
4909
+ // TODO: if this confirm fails, can we abort the whole transfer?
4910
+ await maybeConfirm({
4826
4911
  type: 'transfer',
4827
4912
  data: null,
4828
4913
  ended: true,
@@ -4871,7 +4956,7 @@ const createPullController = handlerControllerFactory((proto)=>({
4871
4956
  };
4872
4957
  const BATCH_MAX_SIZE = 1024 * 1024; // 1MB
4873
4958
  if (!assets) {
4874
- throw new Error('bad');
4959
+ throw new Error('Assets read stream could not be created');
4875
4960
  }
4876
4961
  /**
4877
4962
  * Generates batches of 1MB of data from the assets stream to avoid