@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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { Transform, PassThrough, Writable, Readable, Duplex, pipeline } from 'stream';
1
+ import { Transform, PassThrough, Writable, Readable, Duplex, pipeline as pipeline$1 } from 'stream';
2
+ import { pipeline } from 'stream/promises';
2
3
  import path, { extname, join, posix } from 'path';
3
4
  import { EOL } from 'os';
4
5
  import { chain } from 'stream-chain';
@@ -633,8 +634,7 @@ const TRANSFER_STAGES = Object.freeze([
633
634
  };
634
635
  const DEFAULT_VERSION_STRATEGY = 'ignore';
635
636
  const DEFAULT_SCHEMA_STRATEGY = 'strict';
636
- 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
637
- _currentStream = /*#__PURE__*/ _class_private_field_loose_key$6("_currentStream"), /**
637
+ 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"), /**
638
638
  * Create and return a transform stream based on the given stage and options.
639
639
  *
640
640
  * Allowed transformations includes 'filter' and 'map'.
@@ -749,11 +749,9 @@ class TransferEngine {
749
749
  }
750
750
  // Cause an ongoing transfer to abort gracefully
751
751
  async abortTransfer() {
752
- const err = new TransferEngineError('fatal', 'Transfer aborted.');
753
- if (!_class_private_field_loose_base$6(this, _currentStream)[_currentStream]) {
754
- throw err;
755
- }
756
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream].destroy(err);
752
+ _class_private_field_loose_base$6(this, _aborted)[_aborted] = true;
753
+ _class_private_field_loose_base$6(this, _currentStreamController)[_currentStreamController]?.abort();
754
+ throw new TransferEngineError('fatal', 'Transfer aborted.');
757
755
  }
758
756
  async init() {
759
757
  // Resolve providers' resource and store
@@ -1063,7 +1061,11 @@ class TransferEngine {
1063
1061
  writable: true,
1064
1062
  value: void 0
1065
1063
  });
1066
- Object.defineProperty(this, _currentStream, {
1064
+ Object.defineProperty(this, _currentStreamController, {
1065
+ writable: true,
1066
+ value: void 0
1067
+ });
1068
+ Object.defineProperty(this, _aborted, {
1067
1069
  writable: true,
1068
1070
  value: void 0
1069
1071
  });
@@ -1073,6 +1075,7 @@ class TransferEngine {
1073
1075
  schemaDiff: [],
1074
1076
  errors: {}
1075
1077
  };
1078
+ _class_private_field_loose_base$6(this, _aborted)[_aborted] = false;
1076
1079
  this.diagnostics = createDiagnosticReporter();
1077
1080
  validateProvider('source', sourceProvider);
1078
1081
  validateProvider('destination', destinationProvider);
@@ -1271,6 +1274,9 @@ function assertSchemasMatching(sourceSchemas, destinationSchemas) {
1271
1274
  }
1272
1275
  }
1273
1276
  async function transferStage(options) {
1277
+ if (_class_private_field_loose_base$6(this, _aborted)[_aborted]) {
1278
+ throw new TransferEngineError('fatal', 'Transfer aborted.');
1279
+ }
1274
1280
  const { stage, source, destination, transform, tracker } = options;
1275
1281
  const updateEndTime = ()=>{
1276
1282
  const stageData = this.progress.data[stage];
@@ -1302,27 +1308,42 @@ async function transferStage(options) {
1302
1308
  return;
1303
1309
  }
1304
1310
  _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('start', stage);
1305
- await new Promise((resolve, reject)=>{
1306
- let stream = source;
1311
+ try {
1312
+ const streams = [
1313
+ source
1314
+ ];
1307
1315
  if (transform) {
1308
- stream = stream.pipe(transform);
1316
+ streams.push(transform);
1309
1317
  }
1310
1318
  if (tracker) {
1311
- stream = stream.pipe(tracker);
1312
- }
1313
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream] = stream.pipe(destination).on('error', (e)=>{
1314
- updateEndTime();
1315
- _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('error', stage);
1316
- this.reportError(e, 'error');
1317
- destination.destroy(e);
1318
- reject(e);
1319
- }).on('close', ()=>{
1320
- _class_private_field_loose_base$6(this, _currentStream)[_currentStream] = undefined;
1321
- updateEndTime();
1322
- resolve();
1319
+ streams.push(tracker);
1320
+ }
1321
+ streams.push(destination);
1322
+ // NOTE: to debug/confirm backpressure issues from misbehaving stream, uncomment the following lines
1323
+ // source.on('pause', () => console.log(`[${stage}] Source paused due to backpressure`));
1324
+ // source.on('resume', () => console.log(`[${stage}] Source resumed`));
1325
+ // destination.on('drain', () =>
1326
+ // console.log(`[${stage}] Destination drained, resuming data flow`)
1327
+ // );
1328
+ // destination.on('error', (err) => console.error(`[${stage}] Destination error:`, err));
1329
+ const controller = new AbortController();
1330
+ const { signal } = controller;
1331
+ // Store the controller so you can cancel later
1332
+ _class_private_field_loose_base$6(this, _currentStreamController)[_currentStreamController] = controller;
1333
+ await pipeline(streams, {
1334
+ signal
1323
1335
  });
1324
- });
1325
- _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('finish', stage);
1336
+ _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('finish', stage);
1337
+ } catch (e) {
1338
+ updateEndTime();
1339
+ _class_private_field_loose_base$6(this, _emitStageUpdate)[_emitStageUpdate]('error', stage);
1340
+ this.reportError(e, 'error');
1341
+ if (!destination.destroyed) {
1342
+ destination.destroy(e);
1343
+ }
1344
+ } finally{
1345
+ updateEndTime();
1346
+ }
1326
1347
  }
1327
1348
  async function resolveProviderResource() {
1328
1349
  const sourceMetadata = await this.sourceProvider.getMetadata();
@@ -1894,9 +1915,7 @@ const createLinkQuery = (strapi, trx)=>{
1894
1915
  assignOrderColumns();
1895
1916
  const qb = connection.insert(payload).into(addSchema(joinTable.name));
1896
1917
  if (trx) {
1897
- await trx.transaction(async (nestedTrx)=>{
1898
- await qb.transacting(nestedTrx);
1899
- });
1918
+ await qb.transacting(trx);
1900
1919
  }
1901
1920
  }
1902
1921
  if ('morphColumn' in attribute && attribute.morphColumn) {
@@ -2349,7 +2368,7 @@ class LocalStrapiDestinationProvider {
2349
2368
  const provider = strapi.config.get('plugin::upload').provider;
2350
2369
  const fileId = fileEntitiesMapper?.[uploadData.id];
2351
2370
  if (!fileId) {
2352
- callback(new Error(`File ID not found for ID: ${uploadData.id}`));
2371
+ return callback(new Error(`File ID not found for ID: ${uploadData.id}`));
2353
2372
  }
2354
2373
  try {
2355
2374
  await strapi.plugin('upload').provider.uploadStream(uploadData);
@@ -2400,9 +2419,9 @@ class LocalStrapiDestinationProvider {
2400
2419
  provider
2401
2420
  }
2402
2421
  });
2403
- callback();
2422
+ return callback();
2404
2423
  } catch (error) {
2405
- callback(new Error(`Error while uploading asset ${chunk.filename} ${error}`));
2424
+ return callback(new Error(`Error while uploading asset ${chunk.filename} ${error}`));
2406
2425
  }
2407
2426
  });
2408
2427
  }
@@ -2824,7 +2843,11 @@ function _class_private_field_loose_key$4(name) {
2824
2843
  const createLocalStrapiSourceProvider = (options)=>{
2825
2844
  return new LocalStrapiSourceProvider(options);
2826
2845
  };
2827
- var _diagnostics$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_diagnostics"), _reportInfo$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_reportInfo");
2846
+ var _diagnostics$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_diagnostics"), _reportInfo$4 = /*#__PURE__*/ _class_private_field_loose_key$4("_reportInfo"), /**
2847
+ * Reports an error to the diagnostic reporter.
2848
+ */ _reportError = /*#__PURE__*/ _class_private_field_loose_key$4("_reportError"), /**
2849
+ * Handles errors that occur in read streams.
2850
+ */ _handleStreamError = /*#__PURE__*/ _class_private_field_loose_key$4("_handleStreamError");
2828
2851
  class LocalStrapiSourceProvider {
2829
2852
  async bootstrap(diagnostics) {
2830
2853
  _class_private_field_loose_base$4(this, _diagnostics$4)[_diagnostics$4] = diagnostics;
@@ -2886,12 +2909,22 @@ class LocalStrapiSourceProvider {
2886
2909
  createAssetsReadStream() {
2887
2910
  assertValidStrapi(this.strapi, 'Not able to stream assets');
2888
2911
  _class_private_field_loose_base$4(this, _reportInfo$4)[_reportInfo$4]('creating assets read stream');
2889
- return createAssetsStream(this.strapi);
2912
+ const stream = createAssetsStream(this.strapi);
2913
+ stream.on('error', (err)=>{
2914
+ _class_private_field_loose_base$4(this, _handleStreamError)[_handleStreamError]('assets', err);
2915
+ });
2916
+ return stream;
2890
2917
  }
2891
2918
  constructor(options){
2892
2919
  Object.defineProperty(this, _reportInfo$4, {
2893
2920
  value: reportInfo$4
2894
2921
  });
2922
+ Object.defineProperty(this, _reportError, {
2923
+ value: reportError
2924
+ });
2925
+ Object.defineProperty(this, _handleStreamError, {
2926
+ value: handleStreamError
2927
+ });
2895
2928
  Object.defineProperty(this, _diagnostics$4, {
2896
2929
  writable: true,
2897
2930
  value: void 0
@@ -2911,6 +2944,29 @@ function reportInfo$4(message) {
2911
2944
  kind: 'info'
2912
2945
  });
2913
2946
  }
2947
+ function reportError(message, error) {
2948
+ _class_private_field_loose_base$4(this, _diagnostics$4)[_diagnostics$4]?.report({
2949
+ details: {
2950
+ createdAt: new Date(),
2951
+ message,
2952
+ error,
2953
+ severity: 'fatal',
2954
+ name: error.name
2955
+ },
2956
+ kind: 'error'
2957
+ });
2958
+ }
2959
+ function handleStreamError(streamType, err) {
2960
+ const { message, stack } = err;
2961
+ const errorMessage = `[Data transfer] Error in ${streamType} read stream: ${message}`;
2962
+ const formattedError = {
2963
+ message: errorMessage,
2964
+ stack,
2965
+ timestamp: new Date().toISOString()
2966
+ };
2967
+ this.strapi?.log.error(formattedError);
2968
+ _class_private_field_loose_base$4(this, _reportError)[_reportError](formattedError.message, err);
2969
+ }
2914
2970
 
2915
2971
  const createDispatcher = (ws, retryMessageOptions = {
2916
2972
  retryMessageMaxRetries: 5,
@@ -3074,17 +3130,6 @@ const connectToWebsocket = (address, options, diagnostics)=>{
3074
3130
  const trimTrailingSlash = (input)=>{
3075
3131
  return input.replace(/\/$/, '');
3076
3132
  };
3077
- const wait = (ms)=>{
3078
- return new Promise((resolve)=>{
3079
- setTimeout(resolve, ms);
3080
- });
3081
- };
3082
- const waitUntil = async (test, interval)=>{
3083
- while(!test()){
3084
- await wait(interval);
3085
- }
3086
- return Promise.resolve();
3087
- };
3088
3133
 
3089
3134
  const TRANSFER_PATH = '/transfer/runner';
3090
3135
  const TRANSFER_METHODS = [
@@ -3505,6 +3550,17 @@ class RemoteStrapiSourceProvider {
3505
3550
  });
3506
3551
  // Init the asset map
3507
3552
  const assets = {};
3553
+ // Watch for stalled assets; if we don't receive a chunk within timeout, abort transfer
3554
+ const resetTimeout = (assetID)=>{
3555
+ if (assets[assetID].timeout) {
3556
+ clearTimeout(assets[assetID].timeout);
3557
+ }
3558
+ assets[assetID].timeout = setTimeout(()=>{
3559
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Asset ${assetID} transfer stalled, aborting.`);
3560
+ assets[assetID].status = 'errored';
3561
+ assets[assetID].stream.destroy(new Error(`Asset ${assetID} transfer timed out`));
3562
+ }, this.options.streamTimeout);
3563
+ };
3508
3564
  stream/**
3509
3565
  * Process a payload of many transfer assets and performs the following tasks:
3510
3566
  * - Start: creates a stream for new assets.
@@ -3515,56 +3571,46 @@ class RemoteStrapiSourceProvider {
3515
3571
  const { action, assetID } = item;
3516
3572
  // Creates the stream to send the incoming asset through
3517
3573
  if (action === 'start') {
3518
- // Ignore the item if a transfer has already been started for the same asset ID
3574
+ // if a transfer has already been started for the same asset ID, something is wrong
3519
3575
  if (assets[assetID]) {
3520
- continue;
3576
+ throw new Error(`Asset ${assetID} already started`);
3521
3577
  }
3578
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Asset ${assetID} starting`);
3522
3579
  // Register the asset
3523
3580
  assets[assetID] = {
3524
3581
  ...item.data,
3525
3582
  stream: new PassThrough(),
3526
- status: 'idle',
3583
+ status: 'ok',
3527
3584
  queue: []
3528
3585
  };
3586
+ resetTimeout(assetID);
3529
3587
  // Connect the individual asset stream to the main asset stage stream
3530
3588
  // Note: nothing is transferred until data chunks are fed to the asset stream
3531
3589
  await this.writeAsync(pass, assets[assetID]);
3532
- } else if (action === 'stream') {
3533
- // If the asset hasn't been registered, or if it's been closed already, then ignore the message
3590
+ } else if (action === 'stream' || action === 'end') {
3591
+ // If the asset hasn't been registered, or if it's been closed already, something is wrong
3534
3592
  if (!assets[assetID]) {
3535
- continue;
3593
+ throw new Error(`No id matching ${assetID} for stream action`);
3536
3594
  }
3537
- switch(assets[assetID].status){
3538
- // The asset is ready to accept a new chunk, write it now
3539
- case 'idle':
3540
- await writeAssetChunk(assetID, item.data);
3541
- break;
3542
- // The resource is busy, queue the current chunk so that it gets transferred as soon as possible
3543
- case 'busy':
3544
- assets[assetID].queue.push(item);
3545
- break;
3595
+ // On every action, reset the timeout timer
3596
+ if (action === 'stream') {
3597
+ resetTimeout(assetID);
3598
+ } else {
3599
+ clearTimeout(assets[assetID].timeout);
3546
3600
  }
3547
- } else if (action === 'end') {
3548
- // If the asset has already been closed, or if it was never registered, ignore the command
3549
- if (!assets[assetID]) {
3550
- continue;
3601
+ if (assets[assetID].status === 'closed') {
3602
+ throw new Error(`Asset ${assetID} is closed`);
3551
3603
  }
3552
- switch(assets[assetID].status){
3553
- // There's no ongoing activity, the asset is ready to be closed
3554
- case 'idle':
3555
- case 'errored':
3556
- await closeAssetStream(assetID);
3557
- break;
3558
- // The resource is busy, wait for a different state and close the stream.
3559
- case 'busy':
3560
- await Promise.race([
3561
- // Either: wait for the asset to be ready to be closed
3562
- waitUntil(()=>assets[assetID].status !== 'busy', 100),
3563
- // Or: if the last chunks are still not processed after ten seconds
3564
- wait(10000)
3565
- ]);
3566
- await closeAssetStream(assetID);
3567
- break;
3604
+ assets[assetID].queue.push(item);
3605
+ }
3606
+ }
3607
+ // each new payload will start new processQueue calls, which may cause some extra calls
3608
+ // it's essentially saying "start processing this asset again, I added more data to the queue"
3609
+ for(const assetID in assets){
3610
+ if (Object.prototype.hasOwnProperty.call(assets, assetID)) {
3611
+ const asset = assets[assetID];
3612
+ if (asset.queue?.length > 0) {
3613
+ await processQueue(assetID);
3568
3614
  }
3569
3615
  }
3570
3616
  }
@@ -3572,38 +3618,48 @@ class RemoteStrapiSourceProvider {
3572
3618
  pass.end();
3573
3619
  });
3574
3620
  /**
3575
- * Writes a chunk of data for the specified asset with the given id.
3576
- */ const writeAssetChunk = async (id, data)=>{
3621
+ * Start processing the queue for a given assetID
3622
+ *
3623
+ * Even though this is a loop that attempts to process the entire queue, it is safe to call this more than once
3624
+ * for the same asset id because the queue is shared globally, the items are shifted off, and immediately written
3625
+ */ const processQueue = async (id)=>{
3577
3626
  if (!assets[id]) {
3578
3627
  throw new Error(`Failed to write asset chunk for "${id}". Asset not found.`);
3579
3628
  }
3580
- const { status: currentStatus } = assets[id];
3581
- if (currentStatus !== 'idle') {
3629
+ const asset = assets[id];
3630
+ const { status: currentStatus } = asset;
3631
+ if ([
3632
+ 'closed',
3633
+ 'errored'
3634
+ ].includes(currentStatus)) {
3582
3635
  throw new Error(`Failed to write asset chunk for "${id}". The asset is currently "${currentStatus}"`);
3583
3636
  }
3584
- const nextItemInQueue = ()=>assets[id].queue.shift();
3585
- try {
3586
- // Lock the asset
3587
- assets[id].status = 'busy';
3588
- // Save the current chunk
3589
- await unsafe_writeAssetChunk(id, data);
3590
- // Empty the queue if needed
3591
- let item = nextItemInQueue();
3592
- while(item){
3593
- await unsafe_writeAssetChunk(id, item.data);
3594
- item = nextItemInQueue();
3637
+ while(asset.queue.length > 0){
3638
+ const data = asset.queue.shift();
3639
+ if (!data) {
3640
+ throw new Error(`Invalid chunk found for ${id}`);
3641
+ }
3642
+ try {
3643
+ // if this is an end chunk, close the asset stream
3644
+ if (data.action === 'end') {
3645
+ _class_private_field_loose_base$2(this, _reportInfo$2)[_reportInfo$2](`Ending asset stream for ${id}`);
3646
+ await closeAssetStream(id);
3647
+ break; // Exit the loop after closing the stream
3648
+ }
3649
+ // Save the current chunk
3650
+ await writeChunkToStream(id, data);
3651
+ } catch {
3652
+ if (!assets[id]) {
3653
+ throw new Error(`No id matching ${id} for writeAssetChunk`);
3654
+ }
3595
3655
  }
3596
- // Unlock the asset
3597
- assets[id].status = 'idle';
3598
- } catch {
3599
- assets[id].status = 'errored';
3600
3656
  }
3601
3657
  };
3602
3658
  /**
3603
3659
  * Writes a chunk of data to the asset's stream.
3604
3660
  *
3605
3661
  * Only check if the targeted asset exists, no other validation is done.
3606
- */ const unsafe_writeAssetChunk = async (id, data)=>{
3662
+ */ const writeChunkToStream = async (id, data)=>{
3607
3663
  const asset = assets[id];
3608
3664
  if (!asset) {
3609
3665
  throw new Error(`Failed to write asset chunk for "${id}". Asset not found.`);
@@ -3624,9 +3680,11 @@ class RemoteStrapiSourceProvider {
3624
3680
  await new Promise((resolve, reject)=>{
3625
3681
  const { stream } = assets[id];
3626
3682
  stream.on('close', ()=>{
3627
- delete assets[id];
3628
3683
  resolve();
3629
- }).on('error', reject).end();
3684
+ }).on('error', (e)=>{
3685
+ assets[id].status = 'errored';
3686
+ reject(new Error(`Failed to close asset "${id}". Asset stream error: ${e.toString()}`));
3687
+ }).end();
3630
3688
  });
3631
3689
  };
3632
3690
  return pass;
@@ -3741,6 +3799,9 @@ class RemoteStrapiSourceProvider {
3741
3799
  });
3742
3800
  this.name = 'source::remote-strapi';
3743
3801
  this.type = 'source';
3802
+ this.defaultOptions = {
3803
+ streamTimeout: 15000
3804
+ };
3744
3805
  this.writeAsync = (stream, data)=>{
3745
3806
  return new Promise((resolve, reject)=>{
3746
3807
  stream.write(data, (error)=>{
@@ -3751,7 +3812,10 @@ class RemoteStrapiSourceProvider {
3751
3812
  });
3752
3813
  });
3753
3814
  };
3754
- this.options = options;
3815
+ this.options = {
3816
+ ...this.defaultOptions,
3817
+ ...options
3818
+ };
3755
3819
  this.ws = null;
3756
3820
  this.dispatcher = null;
3757
3821
  }
@@ -4684,6 +4748,18 @@ const createPullController = handlerControllerFactory((proto)=>({
4684
4748
  kind: 'warning'
4685
4749
  });
4686
4750
  },
4751
+ onError (error) {
4752
+ this.diagnostics?.report({
4753
+ details: {
4754
+ message: error.message,
4755
+ error,
4756
+ createdAt: new Date(),
4757
+ name: error.name,
4758
+ severity: 'fatal'
4759
+ },
4760
+ kind: 'error'
4761
+ });
4762
+ },
4687
4763
  assertValidTransferAction (action) {
4688
4764
  // Abstract the constant to string[] to allow looser check on the given action
4689
4765
  const validActions = VALID_TRANSFER_ACTIONS;
@@ -4757,6 +4833,15 @@ const createPullController = handlerControllerFactory((proto)=>({
4757
4833
  let batch = [];
4758
4834
  const stream = this.streams?.[stage];
4759
4835
  const batchLength = ()=>Buffer.byteLength(JSON.stringify(batch));
4836
+ const maybeConfirm = async (data)=>{
4837
+ try {
4838
+ await this.confirm(data);
4839
+ } catch (error) {
4840
+ // Handle the error, log it, or take other appropriate actions
4841
+ strapi?.log.error(`[Data transfer] Message confirmation failed: ${error?.message}`);
4842
+ this.onError(error);
4843
+ }
4844
+ };
4760
4845
  const sendBatch = async ()=>{
4761
4846
  await this.confirm({
4762
4847
  type: 'transfer',
@@ -4765,6 +4850,7 @@ const createPullController = handlerControllerFactory((proto)=>({
4765
4850
  error: null,
4766
4851
  id
4767
4852
  });
4853
+ batch = [];
4768
4854
  };
4769
4855
  if (!stream) {
4770
4856
  throw new ProviderTransferError(`No available stream found for ${stage}`);
@@ -4775,7 +4861,6 @@ const createPullController = handlerControllerFactory((proto)=>({
4775
4861
  batch.push(chunk);
4776
4862
  if (batchLength() >= batchSize) {
4777
4863
  await sendBatch();
4778
- batch = [];
4779
4864
  }
4780
4865
  } else {
4781
4866
  await this.confirm({
@@ -4791,7 +4876,6 @@ const createPullController = handlerControllerFactory((proto)=>({
4791
4876
  }
4792
4877
  if (batch.length > 0 && stage !== 'assets') {
4793
4878
  await sendBatch();
4794
- batch = [];
4795
4879
  }
4796
4880
  await this.confirm({
4797
4881
  type: 'transfer',
@@ -4801,7 +4885,8 @@ const createPullController = handlerControllerFactory((proto)=>({
4801
4885
  id
4802
4886
  });
4803
4887
  } catch (e) {
4804
- await this.confirm({
4888
+ // TODO: if this confirm fails, can we abort the whole transfer?
4889
+ await maybeConfirm({
4805
4890
  type: 'transfer',
4806
4891
  data: null,
4807
4892
  ended: true,
@@ -4850,7 +4935,7 @@ const createPullController = handlerControllerFactory((proto)=>({
4850
4935
  };
4851
4936
  const BATCH_MAX_SIZE = 1024 * 1024; // 1MB
4852
4937
  if (!assets) {
4853
- throw new Error('bad');
4938
+ throw new Error('Assets read stream could not be created');
4854
4939
  }
4855
4940
  /**
4856
4941
  * Generates batches of 1MB of data from the assets stream to avoid
@@ -5101,7 +5186,7 @@ class LocalFileSourceProvider {
5101
5186
  });
5102
5187
  const loadAssetMetadata = _class_private_field_loose_base$1(this, _loadAssetMetadata)[_loadAssetMetadata].bind(this);
5103
5188
  _class_private_field_loose_base$1(this, _reportInfo$1)[_reportInfo$1]('creating assets read stream');
5104
- pipeline([
5189
+ pipeline$1([
5105
5190
  inStream,
5106
5191
  new tar.Parse({
5107
5192
  // find only files in the assets/uploads folder
@@ -5212,7 +5297,7 @@ function streamJsonlDirectory(directory) {
5212
5297
  const outStream = new PassThrough({
5213
5298
  objectMode: true
5214
5299
  });
5215
- pipeline([
5300
+ pipeline$1([
5216
5301
  inStream,
5217
5302
  new tar.Parse({
5218
5303
  filter (filePath, entry) {
@@ -5253,7 +5338,7 @@ function streamJsonlDirectory(directory) {
5253
5338
  }
5254
5339
  async function parseJSONFile(fileStream, filePath) {
5255
5340
  return new Promise((resolve, reject)=>{
5256
- pipeline([
5341
+ pipeline$1([
5257
5342
  fileStream,
5258
5343
  // Custom backup archive parsing
5259
5344
  new tar.Parse({