@lossless.org/client 2.0.0 → 2.2.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.
@@ -60,6 +60,7 @@ const failureMessages: Record<TMultipartStreamFailureKind, string> = {
60
60
  'The multipart source emitted too many consecutive empty chunks.',
61
61
  'source-short': 'The multipart source ended before the declared content length.',
62
62
  'source-extra': 'The multipart source exceeded the declared content length.',
63
+ 'source-over-maximum': 'The multipart source exceeded the maximum content length.',
63
64
  'part-callback': 'A multipart part callback failed.',
64
65
  };
65
66
  const failureSettlementMaximumTimeoutMs = 30_000;
@@ -96,13 +97,6 @@ export class MultipartStreamConsumptionError extends Error {
96
97
  }
97
98
 
98
99
  const validatePlan = (planArg: TMultipartUploadPlan): void => {
99
- if (
100
- !Number.isSafeInteger(planArg.objectSize) ||
101
- planArg.objectSize < 0 ||
102
- planArg.objectSize > MULTIPART_MAX_OBJECT_SIZE
103
- ) {
104
- throw new TypeError('plan.objectSize is invalid.');
105
- }
106
100
  if (
107
101
  !Number.isSafeInteger(planArg.concurrency) ||
108
102
  planArg.concurrency < 1 ||
@@ -110,6 +104,32 @@ const validatePlan = (planArg: TMultipartUploadPlan): void => {
110
104
  ) {
111
105
  throw new TypeError('plan.concurrency is invalid.');
112
106
  }
107
+ if (planArg.classification === 'bounded') {
108
+ if (
109
+ !Number.isSafeInteger(planArg.maxObjectSize) ||
110
+ planArg.maxObjectSize < 1 ||
111
+ planArg.maxObjectSize > MULTIPART_MAX_OBJECT_SIZE ||
112
+ !Number.isSafeInteger(planArg.partSize) ||
113
+ planArg.partSize < MULTIPART_MIN_PART_SIZE ||
114
+ planArg.partSize > MULTIPART_MAX_PART_SIZE ||
115
+ planArg.maxPartCount !== Math.ceil(planArg.maxObjectSize / planArg.partSize) ||
116
+ planArg.maxPartCount > MULTIPART_MAX_PART_COUNT ||
117
+ planArg.concurrency > planArg.maxPartCount ||
118
+ planArg.maximumBufferedBytes !==
119
+ Math.min(planArg.maxObjectSize, planArg.partSize * planArg.concurrency) +
120
+ Math.min(planArg.maxObjectSize, planArg.partSize)
121
+ ) {
122
+ throw new TypeError('The bounded multipart plan is inconsistent.');
123
+ }
124
+ return;
125
+ }
126
+ if (
127
+ !Number.isSafeInteger(planArg.objectSize) ||
128
+ planArg.objectSize < 0 ||
129
+ planArg.objectSize > MULTIPART_MAX_OBJECT_SIZE
130
+ ) {
131
+ throw new TypeError('plan.objectSize is invalid.');
132
+ }
113
133
  if (planArg.classification === 'zero') {
114
134
  if (
115
135
  planArg.objectSize !== 0 ||
@@ -165,6 +185,14 @@ export const consumeExactMultipartStream = async (
165
185
  );
166
186
  }
167
187
 
188
+ const plan = optionsArg.plan;
189
+ // A bounded plan reads parts until the source ends and refuses more than
190
+ // its maximum; an exact plan reads exactly the declared length.
191
+ const bounded = plan.classification === 'bounded';
192
+ const sizeLimit = bounded ? plan.maxObjectSize : plan.objectSize;
193
+ const overLimitKind: TMultipartStreamFailureKind = bounded
194
+ ? 'source-over-maximum'
195
+ : 'source-extra';
168
196
  const reader = optionsArg.readableStream.getReader();
169
197
  const callbackAbortController = new AbortController();
170
198
  const inFlightParts = new Map<symbol, IInFlightPart>();
@@ -289,7 +317,11 @@ export const consumeExactMultipartStream = async (
289
317
  }
290
318
  };
291
319
 
292
- const dispatchPart = (dataArg: Uint8Array, partNumberArg: number): void => {
320
+ const dispatchPart = (
321
+ dataArg: Uint8Array,
322
+ partNumberArg: number,
323
+ isLastPartArg: boolean,
324
+ ): void => {
293
325
  const token = Symbol(`multipart-part-${partNumberArg}`);
294
326
  partCallbacksStarted++;
295
327
  currentPartBytes = 0;
@@ -299,8 +331,8 @@ export const consumeExactMultipartStream = async (
299
331
  .then(() =>
300
332
  optionsArg.onPart({
301
333
  partNumber: partNumberArg,
302
- byteOffset: (partNumberArg - 1) * optionsArg.plan.partSize,
303
- isLastPart: partNumberArg === optionsArg.plan.partCount,
334
+ byteOffset: (partNumberArg - 1) * plan.partSize,
335
+ isLastPart: isLastPartArg,
304
336
  data: dataArg,
305
337
  signal: callbackAbortController.signal,
306
338
  }),
@@ -347,15 +379,19 @@ export const consumeExactMultipartStream = async (
347
379
 
348
380
  for (
349
381
  let partNumber = 1;
350
- partNumber <= optionsArg.plan.partCount;
382
+ bounded ? !sourceClosed : partNumber <= plan.partCount;
351
383
  partNumber++
352
384
  ) {
353
385
  await waitForCapacity();
354
386
  const partByteLength = Math.min(
355
- optionsArg.plan.partSize,
356
- optionsArg.plan.objectSize -
357
- (partNumber - 1) * optionsArg.plan.partSize,
387
+ plan.partSize,
388
+ sizeLimit - (partNumber - 1) * plan.partSize,
358
389
  );
390
+ if (partByteLength <= 0) {
391
+ // A bounded source filled its maximum, which ends at or inside the
392
+ // previous part; the trailing read below refuses any further byte.
393
+ break;
394
+ }
359
395
  if (
360
396
  inFlightBytes + partByteLength >
361
397
  optionsArg.plan.maximumBufferedBytes
@@ -396,6 +432,9 @@ export const consumeExactMultipartStream = async (
396
432
  const readResult = await readWhileObservingCallbacks();
397
433
  if (readResult.done) {
398
434
  sourceClosed = true;
435
+ if (bounded) {
436
+ break;
437
+ }
399
438
  throw {
400
439
  kind: 'source-short',
401
440
  } satisfies IPrimaryFailure;
@@ -410,16 +449,16 @@ export const consumeExactMultipartStream = async (
410
449
  continue;
411
450
  }
412
451
  observedBytes += readResult.value.byteLength;
413
- if (observedBytes > optionsArg.plan.objectSize) {
452
+ if (observedBytes > sizeLimit) {
414
453
  throw {
415
- kind: 'source-extra',
454
+ kind: overLimitKind,
416
455
  } satisfies IPrimaryFailure;
417
456
  }
418
- if (readResult.value.byteLength > optionsArg.plan.partSize) {
457
+ if (readResult.value.byteLength > plan.partSize) {
419
458
  throw {
420
459
  kind: 'source-buffer-limit',
421
460
  cause: new RangeError(
422
- `Source chunks must not exceed ${optionsArg.plan.partSize} bytes.`,
461
+ `Source chunks must not exceed ${plan.partSize} bytes.`,
423
462
  ),
424
463
  } satisfies IPrimaryFailure;
425
464
  }
@@ -428,7 +467,24 @@ export const consumeExactMultipartStream = async (
428
467
  observeBufferedBytes();
429
468
  }
430
469
 
431
- dispatchPart(partData, partNumber);
470
+ if (partOffset === 0) {
471
+ // A bounded source ended at a part boundary.
472
+ currentPartBytes = 0;
473
+ break;
474
+ }
475
+ if (partOffset < partByteLength) {
476
+ // Only a bounded source ends inside a part; its last part keeps the
477
+ // bytes read, as an exact copy.
478
+ dispatchPart(partData.slice(0, partOffset), partNumber, true);
479
+ } else {
480
+ dispatchPart(
481
+ partData,
482
+ partNumber,
483
+ bounded
484
+ ? sourceClosed || observedBytes === sizeLimit
485
+ : partNumber === plan.partCount,
486
+ );
487
+ }
432
488
  }
433
489
 
434
490
  while (!sourceClosed) {
@@ -448,7 +504,7 @@ export const consumeExactMultipartStream = async (
448
504
  }
449
505
  observedBytes += readResult.value.byteLength;
450
506
  throw {
451
- kind: 'source-extra',
507
+ kind: overLimitKind,
452
508
  } satisfies IPrimaryFailure;
453
509
  }
454
510
 
@@ -571,7 +627,9 @@ export const consumeExactMultipartStream = async (
571
627
  completionAuthorized: false,
572
628
  kind: failure.kind,
573
629
  cause: failure.cause,
574
- expectedBytes: optionsArg.plan.objectSize,
630
+ ...(bounded
631
+ ? { maxBytes: plan.maxObjectSize }
632
+ : { expectedBytes: plan.objectSize }),
575
633
  observedBytes,
576
634
  partCallbacksStarted,
577
635
  completedPartNumbers: [...completedPartNumbers].sort(
@@ -4,6 +4,12 @@ export interface IMultipartPlanningOptions {
4
4
  requestedConcurrency?: number;
5
5
  }
6
6
 
7
+ export interface IMultipartBoundedPlanningOptions {
8
+ maxObjectSize: number;
9
+ requestedPartSize?: number;
10
+ requestedConcurrency?: number;
11
+ }
12
+
7
13
  interface IMultipartUploadPlanBase {
8
14
  objectSize: number;
9
15
  concurrency: number;
@@ -24,11 +30,34 @@ export interface IMultipartDataUploadPlan extends IMultipartUploadPlanBase {
24
30
  partCount: number;
25
31
  }
26
32
 
27
- export type TMultipartUploadPlan = IMultipartZeroUploadPlan | IMultipartDataUploadPlan;
33
+ /**
34
+ * A plan for a source of undeclared length: full parts are read until the
35
+ * source ends, and a source longer than `maxObjectSize` fails. The part size
36
+ * is chosen so that `maxObjectSize` fits into the multipart part limit.
37
+ */
38
+ export interface IMultipartBoundedUploadPlan {
39
+ classification: 'bounded';
40
+ maxObjectSize: number;
41
+ partSize: number;
42
+ /** Parts that `maxObjectSize` needs; the source may need fewer. */
43
+ maxPartCount: number;
44
+ concurrency: number;
45
+ maximumBufferedBytes: number;
46
+ }
47
+
48
+ /** A plan for a source of declared length. */
49
+ export type TMultipartDeclaredUploadPlan = IMultipartZeroUploadPlan | IMultipartDataUploadPlan;
50
+
51
+ export type TMultipartUploadPlan = TMultipartDeclaredUploadPlan | IMultipartBoundedUploadPlan;
28
52
 
29
53
  export interface IMultipartStreamPart {
30
54
  partNumber: number;
31
55
  byteOffset: number;
56
+ /**
57
+ * Under a bounded plan a part is only known to be the last when the source
58
+ * ended inside it or it reached the maximum; a source that ends exactly at
59
+ * a part boundary leaves its last part marked false.
60
+ */
32
61
  isLastPart: boolean;
33
62
  /**
34
63
  * An exact, exclusively owned copy. The consumer never mutates or reuses it.
@@ -61,6 +90,7 @@ export type TMultipartStreamFailureKind =
61
90
  | 'source-empty-chunk-limit'
62
91
  | 'source-short'
63
92
  | 'source-extra'
93
+ | 'source-over-maximum'
64
94
  | 'part-callback';
65
95
 
66
96
  export type TMultipartOwnedReaderCleanup =
@@ -97,7 +127,10 @@ export interface IMultipartStreamFailureState {
97
127
  completionAuthorized: false;
98
128
  kind: TMultipartStreamFailureKind;
99
129
  cause?: unknown;
100
- expectedBytes: number;
130
+ /** The declared length; absent under a bounded plan. */
131
+ expectedBytes?: number;
132
+ /** The maximum length of a bounded plan. */
133
+ maxBytes?: number;
101
134
  observedBytes: number;
102
135
  partCallbacksStarted: number;
103
136
  completedPartNumbers: number[];