Skip to content

Commit b6cf135

Browse files
committed
Add remove multiple API
1 parent cefa453 commit b6cf135

File tree

15 files changed

+1696
-0
lines changed

15 files changed

+1696
-0
lines changed
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
# RemoveMultiple API Implementation with Cancellation Support
2+
3+
## Overview
4+
The `removeMultiple` API provides a comprehensive solution for batch deletion of S3 objects with advanced features like batching, retry logic, progress tracking, flexible error handling, and **cancellation support**.
5+
6+
## Key Features
7+
8+
### **Cancellation Support (NEW)**
9+
- **Operation Handle Pattern**: Returns an operation handle instead of a direct Promise
10+
- **Graceful Cancellation**: Stops processing new batches while allowing in-flight batches to complete
11+
- **Immediate Cancellation**: Cancels immediately even during delay periods
12+
- **Partial Results**: Always resolves (never rejects) when cancelled, returning processed results
13+
- **Idempotent Cancel**: Safe to call `cancel()` multiple times
14+
- **Cancellation Tracking**: Tracks which keys were not processed due to cancellation
15+
16+
### 🔧 **Core Features**
17+
- **Batch Processing**: Automatically handles S3's 1000-key limit with configurable batch sizes
18+
- **Dual Processing Strategies**: Sequential and parallel batch processing with concurrency control
19+
- **Advanced Error Handling**: Three strategies (throw, failEarly, continue) for different use cases
20+
- **Retry Logic**: Configurable retry mechanism with backoff for failed batches
21+
- **Progress Tracking**: Real-time progress callbacks with detailed batch information
22+
- **Safety Validations**: Prevents dangerous deletions (root folders, wildcards, empty keys)
23+
24+
## API Signature (Updated)
25+
26+
```typescript
27+
// OLD: Returns Promise directly
28+
function removeMultiple(input: RemoveMultipleInput): Promise<RemoveMultipleOutput>
29+
30+
// NEW: Returns Operation Handle
31+
function removeMultiple(input: RemoveMultipleInput): RemoveMultipleOperation
32+
```
33+
34+
### Operation Handle Interface
35+
```typescript
36+
interface RemoveMultipleOperation {
37+
/**
38+
* Promise that resolves with the operation result
39+
* Always resolves (never rejects) even when cancelled
40+
*/
41+
result: Promise<RemoveMultipleOutput>;
42+
43+
/**
44+
* Cancel the ongoing operation
45+
* - Stops processing new batches immediately
46+
* - Allows in-flight batches to complete
47+
* - Stops retry attempts for failed batches
48+
* - Cancels immediately even during delay periods
49+
*
50+
* Safe to call multiple times (idempotent)
51+
*/
52+
cancel(): void;
53+
54+
/**
55+
* Check if the operation has been cancelled
56+
*/
57+
isCancelled(): boolean;
58+
}
59+
```
60+
61+
### Updated Output Type
62+
```typescript
63+
interface RemoveMultipleOutput {
64+
summary: {
65+
totalRequested: number;
66+
successCount: number;
67+
failureCount: number;
68+
cancelledCount: number; // NEW: Number of cancelled keys
69+
batchesProcessed: number;
70+
batchesFailed: number;
71+
wasCancelled: boolean; // NEW: Whether operation was cancelled
72+
};
73+
deleted: Array<{
74+
key: string;
75+
versionId?: string;
76+
deletedAt?: Date;
77+
}>;
78+
failed: Array<{
79+
key: string;
80+
versionId?: string;
81+
error: {
82+
code: string;
83+
message: string;
84+
batchNumber: number;
85+
retryAttempts: number;
86+
};
87+
}>;
88+
cancelled?: Array<{ // NEW: Cancelled keys (optional)
89+
key: string;
90+
versionId?: string;
91+
batchNumber: number;
92+
}>;
93+
}
94+
```
95+
96+
## Usage Examples
97+
98+
### Basic Usage with Cancellation
99+
```typescript
100+
import { removeMultiple } from 'aws-amplify/storage/s3';
101+
102+
// Start the operation
103+
const operation = removeMultiple({
104+
keys: [
105+
{ key: 'file1.jpg' },
106+
{ key: 'file2.jpg' },
107+
// ... many more keys
108+
],
109+
options: {
110+
batchSize: 1000,
111+
batchStrategy: 'sequential'
112+
}
113+
});
114+
115+
// Cancel after 5 seconds
116+
setTimeout(() => {
117+
operation.cancel();
118+
console.log('Cancellation requested');
119+
}, 5000);
120+
121+
// Wait for result (always resolves, never rejects)
122+
const result = await operation.result;
123+
124+
console.log('Summary:', result.summary);
125+
// Output:
126+
// {
127+
// totalRequested: 5002,
128+
// successCount: 2000,
129+
// failureCount: 0,
130+
// cancelledCount: 3002,
131+
// batchesProcessed: 2,
132+
// batchesFailed: 0,
133+
// wasCancelled: true
134+
// }
135+
136+
console.log('Cancelled keys:', result.cancelled?.length);
137+
```
138+
139+
### Progress Tracking with Cancellation
140+
```typescript
141+
let currentOperation = null;
142+
143+
const operation = removeMultiple({
144+
keys: largeKeyArray,
145+
options: {
146+
batchStrategy: 'parallel',
147+
maxConcurrency: 10,
148+
onProgress: (progress) => {
149+
console.log(`Progress: ${progress.processedCount}/${progress.totalCount}`);
150+
console.log(`Success: ${progress.successCount}, Failed: ${progress.failureCount}`);
151+
}
152+
}
153+
});
154+
155+
currentOperation = operation;
156+
157+
// User clicks cancel button
158+
document.getElementById('cancelBtn').addEventListener('click', () => {
159+
if (currentOperation && !currentOperation.isCancelled()) {
160+
currentOperation.cancel();
161+
}
162+
});
163+
164+
const result = await operation.result;
165+
166+
if (result.summary.wasCancelled) {
167+
console.log('Operation was cancelled by user');
168+
console.log(`Processed ${result.summary.successCount} out of ${result.summary.totalRequested}`);
169+
} else {
170+
console.log('Operation completed successfully');
171+
}
172+
```
173+
174+
### Cancellation During Delays
175+
```typescript
176+
const operation = removeMultiple({
177+
keys: batchOfKeys,
178+
options: {
179+
batchStrategy: 'sequential',
180+
delayBetweenBatchesMs: 2000, // 2 second delay between batches
181+
batchSize: 500
182+
}
183+
});
184+
185+
// Cancel during delay period - stops immediately
186+
setTimeout(() => {
187+
operation.cancel();
188+
}, 3000); // Cancel after 3 seconds (likely during first delay)
189+
190+
const result = await operation.result;
191+
console.log('Cancelled during delay:', result.summary.wasCancelled);
192+
```
193+
194+
### Checking Cancellation Status
195+
```typescript
196+
const operation = removeMultiple({
197+
keys: keys,
198+
options: { batchStrategy: 'parallel' }
199+
});
200+
201+
// Check status
202+
console.log('Is cancelled?', operation.isCancelled()); // false
203+
204+
// Cancel
205+
operation.cancel();
206+
207+
console.log('Is cancelled?', operation.isCancelled()); // true
208+
209+
// Safe to call multiple times
210+
operation.cancel();
211+
operation.cancel();
212+
213+
const result = await operation.result;
214+
// Still resolves normally with partial results
215+
```
216+
217+
## Implementation Details
218+
219+
### Cancellation Token System
220+
```typescript
221+
class CancellationToken {
222+
private _isCancelled = false;
223+
224+
cancel(): void {
225+
this._isCancelled = true;
226+
}
227+
228+
isCancelled(): boolean {
229+
return this._isCancelled;
230+
}
231+
}
232+
```
233+
234+
### Cancellation Check Points
235+
1. **Before each batch**: Prevents starting new batches when cancelled
236+
2. **During delays**: Immediate cancellation during `delayBetweenBatchesMs`
237+
3. **Before retries**: Stops retry attempts when cancelled
238+
4. **In parallel processing**: Waits for in-flight batches to complete
239+
240+
### Cancellation Behavior
241+
- **Sequential Processing**: Stops immediately, marks remaining batches as cancelled
242+
- **Parallel Processing**: Stops queuing new batches, waits for in-flight batches
243+
- **Retry Logic**: Stops retrying failed batches, marks as cancelled
244+
- **Progress Callbacks**: Stops calling `onProgress` after cancellation
245+
- **Error Handling**: Always resolves (never rejects) when cancelled
246+
247+
### Sleep with Cancellation
248+
```typescript
249+
async function sleepWithCancellation(
250+
ms: number,
251+
cancellationToken: CancellationToken
252+
): Promise<boolean> {
253+
const startTime = Date.now();
254+
const checkInterval = 100; // Check every 100ms
255+
256+
while (Date.now() - startTime < ms) {
257+
if (cancellationToken.isCancelled()) {
258+
return true; // Cancelled
259+
}
260+
await sleep(Math.min(checkInterval, ms - (Date.now() - startTime)));
261+
}
262+
263+
return false; // Completed normally
264+
}
265+
```
266+
267+
## Files Created/Modified
268+
269+
### New Files Created:
270+
1. `src/internals/apis/removeMultiple.ts` - Internal API wrapper (updated)
271+
2. `src/providers/s3/apis/internal/removeMultiple.ts` - Core implementation (completely rewritten)
272+
3. `src/providers/s3/apis/removeMultiple.ts` - Public API (updated)
273+
4. `src/providers/s3/apis/removeMultiple.test.ts` - Updated tests
274+
5. `examples/removeMultiple-example.ts` - Updated examples with cancellation
275+
276+
### Files Modified:
277+
1. `src/internals/types/inputs.ts` - Added RemoveMultipleInput and ProgressInfo types
278+
2. `src/internals/types/outputs.ts` - Updated RemoveMultipleOutput, added RemoveMultipleOperation
279+
3. `src/internals/index.ts` - Added exports for new types and API
280+
4. `src/providers/s3/types/inputs.ts` - Added S3-specific types
281+
5. `src/providers/s3/types/outputs.ts` - Updated S3-specific output types, added operation type
282+
6. `src/providers/s3/types/index.ts` - Added type exports
283+
7. `src/providers/s3/apis/index.ts` - Added API export
284+
8. `src/providers/s3/index.ts` - Added main S3 exports
285+
9. `src/index.ts` - Added main package exports
286+
287+
## Migration Guide
288+
289+
### Before (Old API)
290+
```typescript
291+
try {
292+
const result = await removeMultiple({
293+
keys: [{ key: 'file1.txt' }, { key: 'file2.txt' }]
294+
});
295+
console.log('Success:', result.summary.successCount);
296+
} catch (error) {
297+
console.error('Failed:', error);
298+
}
299+
```
300+
301+
### After (New API with Cancellation)
302+
```typescript
303+
const operation = removeMultiple({
304+
keys: [{ key: 'file1.txt' }, { key: 'file2.txt' }]
305+
});
306+
307+
// Optional: Cancel if needed
308+
// setTimeout(() => operation.cancel(), 5000);
309+
310+
// Always resolves, never rejects
311+
const result = await operation.result;
312+
313+
if (result.summary.wasCancelled) {
314+
console.log('Cancelled:', result.summary.cancelledCount);
315+
} else {
316+
console.log('Success:', result.summary.successCount);
317+
}
318+
319+
if (result.summary.failureCount > 0) {
320+
console.log('Failures:', result.failed);
321+
}
322+
```
323+
324+
## Error Handling with Cancellation
325+
326+
### Key Principles
327+
1. **Always Resolve**: The `result` promise never rejects, even when cancelled
328+
2. **Partial Results**: Returns what was processed before cancellation
329+
3. **Clear Status**: `wasCancelled` flag indicates if operation was cancelled
330+
4. **Detailed Tracking**: Separate counts for success, failure, and cancellation
331+
332+
### Error Scenarios
333+
- **Validation Errors**: Thrown immediately before operation starts
334+
- **Network Errors**: Handled according to `errorHandling` strategy
335+
- **Cancellation**: Always treated as successful completion with partial results
336+
- **Mixed Scenarios**: Cancellation during error conditions returns partial results
337+
338+
## Performance Considerations
339+
340+
### Cancellation Overhead
341+
- **Minimal Impact**: Cancellation checks are lightweight boolean operations
342+
- **Sleep Optimization**: Cancellable sleep checks every 100ms during delays
343+
- **Memory Efficient**: No additional memory overhead for cancellation support
344+
- **Graceful Shutdown**: Allows in-flight operations to complete naturally
345+
346+
### Best Practices
347+
1. **Check Status**: Use `isCancelled()` to check status before expensive operations
348+
2. **UI Integration**: Bind cancel button to `operation.cancel()`
349+
3. **Progress Updates**: Use `onProgress` to show cancellation status in UI
350+
4. **Resource Cleanup**: No manual cleanup needed, handled automatically
351+
352+
## Testing
353+
354+
### Cancellation Test Cases
355+
-Basic cancellation functionality
356+
-Idempotent cancel calls
357+
-Cancellation status checking
358+
-Cancellation during delays
359+
-Cancellation during retries
360+
-Cancellation in parallel processing
361+
-Partial result handling
362+
-Progress callback behavior during cancellation
363+
364+
### Edge Cases Covered
365+
-Cancel before any processing starts
366+
-Cancel during first batch
367+
-Cancel during last batch
368+
-Cancel during retry attempts
369+
-Cancel during delay periods
370+
-Multiple cancel calls
371+
-Status checks before and after cancellation
372+
373+
## Platform Compatibility
374+
375+
The Operation Handle pattern is designed to work across:
376+
-**Web**: Full cancellation support with DOM integration
377+
-**React Native**: Compatible with mobile app lifecycle
378+
-**Node.js**: Server-side batch processing with cancellation
379+
-**iOS/Android**: Native app integration through bridge
380+
381+
## Summary
382+
383+
The updated `removeMultiple` API now provides comprehensive cancellation support while maintaining all existing functionality. The Operation Handle pattern ensures platform compatibility and provides a clean, intuitive API for managing long-running batch deletion operations.
384+
385+
Key benefits:
386+
- **User Control**: Users can cancel operations at any time
387+
- **Resource Efficiency**: Graceful cancellation prevents unnecessary work
388+
- **UI Integration**: Easy to integrate with cancel buttons and progress indicators
389+
- **Reliability**: Always resolves with clear status and partial results
390+
- **Backward Compatibility**: Easy migration path from Promise-based API

0 commit comments

Comments
 (0)