@uploadista/data-store-s3 0.0.3

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.
Files changed (65) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +5 -0
  3. package/LICENSE +21 -0
  4. package/README.md +588 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/observability.d.ts +45 -0
  9. package/dist/observability.d.ts.map +1 -0
  10. package/dist/observability.js +155 -0
  11. package/dist/s3-store-old.d.ts +51 -0
  12. package/dist/s3-store-old.d.ts.map +1 -0
  13. package/dist/s3-store-old.js +765 -0
  14. package/dist/s3-store.d.ts +9 -0
  15. package/dist/s3-store.d.ts.map +1 -0
  16. package/dist/s3-store.js +666 -0
  17. package/dist/services/__mocks__/s3-client-mock.service.d.ts +44 -0
  18. package/dist/services/__mocks__/s3-client-mock.service.d.ts.map +1 -0
  19. package/dist/services/__mocks__/s3-client-mock.service.js +379 -0
  20. package/dist/services/index.d.ts +2 -0
  21. package/dist/services/index.d.ts.map +1 -0
  22. package/dist/services/index.js +1 -0
  23. package/dist/services/s3-client.service.d.ts +68 -0
  24. package/dist/services/s3-client.service.d.ts.map +1 -0
  25. package/dist/services/s3-client.service.js +209 -0
  26. package/dist/test-observability.d.ts +6 -0
  27. package/dist/test-observability.d.ts.map +1 -0
  28. package/dist/test-observability.js +62 -0
  29. package/dist/types.d.ts +81 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/types.js +1 -0
  32. package/dist/utils/calculations.d.ts +7 -0
  33. package/dist/utils/calculations.d.ts.map +1 -0
  34. package/dist/utils/calculations.js +41 -0
  35. package/dist/utils/error-handling.d.ts +7 -0
  36. package/dist/utils/error-handling.d.ts.map +1 -0
  37. package/dist/utils/error-handling.js +29 -0
  38. package/dist/utils/index.d.ts +4 -0
  39. package/dist/utils/index.d.ts.map +1 -0
  40. package/dist/utils/index.js +3 -0
  41. package/dist/utils/stream-adapter.d.ts +14 -0
  42. package/dist/utils/stream-adapter.d.ts.map +1 -0
  43. package/dist/utils/stream-adapter.js +41 -0
  44. package/package.json +36 -0
  45. package/src/__tests__/integration/s3-store.integration.test.ts +548 -0
  46. package/src/__tests__/multipart-logic.test.ts +395 -0
  47. package/src/__tests__/s3-store.edge-cases.test.ts +681 -0
  48. package/src/__tests__/s3-store.performance.test.ts +622 -0
  49. package/src/__tests__/s3-store.test.ts +662 -0
  50. package/src/__tests__/utils/performance-helpers.ts +459 -0
  51. package/src/__tests__/utils/test-data-generator.ts +331 -0
  52. package/src/__tests__/utils/test-setup.ts +256 -0
  53. package/src/index.ts +1 -0
  54. package/src/s3-store.ts +1059 -0
  55. package/src/services/__mocks__/s3-client-mock.service.ts +604 -0
  56. package/src/services/index.ts +1 -0
  57. package/src/services/s3-client.service.ts +359 -0
  58. package/src/types.ts +96 -0
  59. package/src/utils/calculations.ts +61 -0
  60. package/src/utils/error-handling.ts +52 -0
  61. package/src/utils/index.ts +3 -0
  62. package/src/utils/stream-adapter.ts +50 -0
  63. package/tsconfig.json +19 -0
  64. package/tsconfig.tsbuildinfo +1 -0
  65. package/vitest.config.ts +15 -0
@@ -0,0 +1,459 @@
1
+ import { Effect } from "effect";
2
+
3
+ export interface PerformanceMetrics {
4
+ startTime: number;
5
+ endTime: number;
6
+ durationMs: number;
7
+ bytesProcessed: number;
8
+ throughputBps: number; // bytes per second
9
+ throughputMbps: number; // megabits per second
10
+ memoryDelta: number;
11
+ }
12
+
13
+ export interface MemoryMetrics {
14
+ heapUsedBefore: number;
15
+ heapUsedAfter: number;
16
+ heapUsedDelta: number;
17
+ heapTotalBefore: number;
18
+ heapTotalAfter: number;
19
+ external: number;
20
+ arrayBuffers: number;
21
+ }
22
+
23
+ export interface ConcurrentMetrics {
24
+ totalOperations: number;
25
+ totalDuration: number;
26
+ successfulOperations: number;
27
+ failedOperations: number;
28
+ averageDuration: number;
29
+ maxDuration: number;
30
+ minDuration: number;
31
+ concurrencyLevel: number;
32
+ }
33
+
34
+ /**
35
+ * Measure performance of an Effect operation
36
+ */
37
+ export const measurePerformance = <A, E>(
38
+ operation: Effect.Effect<A, E>,
39
+ bytesProcessed: number = 0,
40
+ ): Effect.Effect<{ result: A; metrics: PerformanceMetrics }, E> =>
41
+ Effect.gen(function* () {
42
+ const startTime = performance.now();
43
+ const startMemory = process.memoryUsage();
44
+
45
+ const result = yield* operation;
46
+
47
+ const endTime = performance.now();
48
+ const endMemory = process.memoryUsage();
49
+
50
+ const durationMs = endTime - startTime;
51
+ const throughputBps =
52
+ bytesProcessed > 0 ? (bytesProcessed * 1000) / durationMs : 0;
53
+ const throughputMbps = (throughputBps * 8) / (1024 * 1024);
54
+ const memoryDelta = endMemory.heapUsed - startMemory.heapUsed;
55
+
56
+ const metrics: PerformanceMetrics = {
57
+ startTime,
58
+ endTime,
59
+ durationMs,
60
+ bytesProcessed,
61
+ throughputBps,
62
+ throughputMbps,
63
+ memoryDelta,
64
+ };
65
+
66
+ return { result, metrics };
67
+ });
68
+
69
+ /**
70
+ * Measure memory usage of an Effect operation
71
+ */
72
+ export const measureMemory = <A, E>(
73
+ operation: Effect.Effect<A, E>,
74
+ ): Effect.Effect<{ result: A; memory: MemoryMetrics }, E> =>
75
+ Effect.gen(function* () {
76
+ // Force garbage collection if available (for more accurate measurements)
77
+ if (global.gc) {
78
+ global.gc();
79
+ }
80
+
81
+ const memoryBefore = process.memoryUsage();
82
+
83
+ const result = yield* operation;
84
+
85
+ // Force garbage collection again
86
+ if (global.gc) {
87
+ global.gc();
88
+ }
89
+
90
+ const memoryAfter = process.memoryUsage();
91
+
92
+ const memory: MemoryMetrics = {
93
+ heapUsedBefore: memoryBefore.heapUsed,
94
+ heapUsedAfter: memoryAfter.heapUsed,
95
+ heapUsedDelta: memoryAfter.heapUsed - memoryBefore.heapUsed,
96
+ heapTotalBefore: memoryBefore.heapTotal,
97
+ heapTotalAfter: memoryAfter.heapTotal,
98
+ external: memoryAfter.external,
99
+ arrayBuffers: memoryAfter.arrayBuffers,
100
+ };
101
+
102
+ return { result, memory };
103
+ });
104
+
105
+ /**
106
+ * Measure concurrent operations performance
107
+ */
108
+ export const measureConcurrentOps = <A, E>(
109
+ operations: Effect.Effect<A, E>[],
110
+ concurrency: number = operations.length,
111
+ ): Effect.Effect<{ results: A[]; metrics: ConcurrentMetrics }, E> =>
112
+ Effect.gen(function* () {
113
+ const startTime = performance.now();
114
+ const individualMetrics: PerformanceMetrics[] = [];
115
+
116
+ const results = yield* Effect.forEach(
117
+ operations,
118
+ (operation) =>
119
+ measurePerformance(operation).pipe(
120
+ Effect.tap(({ metrics }) =>
121
+ Effect.sync(() => individualMetrics.push(metrics)),
122
+ ),
123
+ Effect.map(({ result }) => result),
124
+ ),
125
+ { concurrency },
126
+ );
127
+
128
+ const endTime = performance.now();
129
+ const totalDuration = endTime - startTime;
130
+
131
+ const successfulOps = individualMetrics.length;
132
+ const failedOps = operations.length - successfulOps;
133
+
134
+ const durations = individualMetrics.map((m) => m.durationMs);
135
+ const averageDuration =
136
+ durations.reduce((sum, dur) => sum + dur, 0) / durations.length;
137
+ const maxDuration = Math.max(...durations);
138
+ const minDuration = Math.min(...durations);
139
+
140
+ const metrics: ConcurrentMetrics = {
141
+ totalOperations: operations.length,
142
+ successfulOperations: successfulOps,
143
+ failedOperations: failedOps,
144
+ totalDuration,
145
+ averageDuration,
146
+ maxDuration,
147
+ minDuration,
148
+ concurrencyLevel: concurrency,
149
+ };
150
+
151
+ return { results, metrics };
152
+ });
153
+
154
+ /**
155
+ * Performance benchmark for upload operations
156
+ */
157
+ export interface UploadBenchmark {
158
+ fileSize: number;
159
+ partSize?: number;
160
+ concurrency?: number;
161
+ expectedThroughputMbps?: number;
162
+ maxDurationMs?: number;
163
+ maxMemoryUsageMB?: number;
164
+ }
165
+
166
+ export const benchmarkUpload = (
167
+ uploadOperation: Effect.Effect<unknown, unknown>,
168
+ benchmark: UploadBenchmark,
169
+ ): Effect.Effect<
170
+ {
171
+ success: boolean;
172
+ metrics: PerformanceMetrics;
173
+ memory: MemoryMetrics;
174
+ issues: string[];
175
+ },
176
+ unknown
177
+ > =>
178
+ Effect.gen(function* () {
179
+ const issues: string[] = [];
180
+
181
+ const { metrics, memory } = yield* measureMemory(
182
+ measurePerformance(uploadOperation, benchmark.fileSize),
183
+ ).pipe(
184
+ Effect.map(({ result: { result, metrics }, memory }) => ({
185
+ result,
186
+ metrics,
187
+ memory,
188
+ })),
189
+ );
190
+
191
+ // Check performance benchmarks
192
+ if (
193
+ benchmark.expectedThroughputMbps &&
194
+ metrics.throughputMbps < benchmark.expectedThroughputMbps
195
+ ) {
196
+ issues.push(
197
+ `Throughput ${metrics.throughputMbps.toFixed(2)} Mbps below expected ` +
198
+ `${benchmark.expectedThroughputMbps} Mbps`,
199
+ );
200
+ }
201
+
202
+ if (
203
+ benchmark.maxDurationMs &&
204
+ metrics.durationMs > benchmark.maxDurationMs
205
+ ) {
206
+ issues.push(
207
+ `Duration ${metrics.durationMs.toFixed(2)}ms exceeds maximum ` +
208
+ `${benchmark.maxDurationMs}ms`,
209
+ );
210
+ }
211
+
212
+ const memoryUsageMB = memory.heapUsedDelta / (1024 * 1024);
213
+ if (
214
+ benchmark.maxMemoryUsageMB &&
215
+ memoryUsageMB > benchmark.maxMemoryUsageMB
216
+ ) {
217
+ issues.push(
218
+ `Memory usage ${memoryUsageMB.toFixed(2)}MB exceeds maximum ` +
219
+ `${benchmark.maxMemoryUsageMB}MB`,
220
+ );
221
+ }
222
+
223
+ return {
224
+ success: issues.length === 0,
225
+ metrics,
226
+ memory,
227
+ issues,
228
+ };
229
+ });
230
+
231
+ /**
232
+ * Utility to format performance metrics for test output
233
+ */
234
+ export const formatMetrics = (metrics: PerformanceMetrics): string => {
235
+ const sizeFormatted = (metrics.bytesProcessed / (1024 * 1024)).toFixed(2);
236
+ const throughputFormatted = metrics.throughputMbps.toFixed(2);
237
+ const durationFormatted = metrics.durationMs.toFixed(2);
238
+
239
+ return `${sizeFormatted}MB in ${durationFormatted}ms (${throughputFormatted} Mbps)`;
240
+ };
241
+
242
+ /**
243
+ * Utility to format memory metrics for test output
244
+ */
245
+ export const formatMemoryMetrics = (memory: MemoryMetrics): string => {
246
+ const heapDeltaMB = (memory.heapUsedDelta / (1024 * 1024)).toFixed(2);
247
+ const heapAfterMB = (memory.heapUsedAfter / (1024 * 1024)).toFixed(2);
248
+ const externalMB = (memory.external / (1024 * 1024)).toFixed(2);
249
+
250
+ return `Heap: ${heapAfterMB}MB (+${heapDeltaMB}MB), External: ${externalMB}MB`;
251
+ };
252
+
253
+ /**
254
+ * Utility to format concurrent metrics for test output
255
+ */
256
+ export const formatConcurrentMetrics = (metrics: ConcurrentMetrics): string => {
257
+ const successRate = (
258
+ (metrics.successfulOperations / metrics.totalOperations) *
259
+ 100
260
+ ).toFixed(1);
261
+ const avgDuration = metrics.averageDuration.toFixed(2);
262
+
263
+ return (
264
+ `${metrics.successfulOperations}/${metrics.totalOperations} ops (${successRate}%) ` +
265
+ `avg ${avgDuration}ms (${metrics.minDuration.toFixed(2)}-${metrics.maxDuration.toFixed(2)}ms)`
266
+ );
267
+ };
268
+
269
+ /**
270
+ * Create performance benchmarks for different file sizes
271
+ */
272
+ export const createPerformanceBenchmarks = (): Record<
273
+ string,
274
+ UploadBenchmark
275
+ > => ({
276
+ tiny: {
277
+ fileSize: 1024, // 1KB
278
+ expectedThroughputMbps: 0.1, // Much more relaxed for test environment
279
+ maxDurationMs: 1000,
280
+ maxMemoryUsageMB: 5, // More realistic for test environment
281
+ },
282
+ small: {
283
+ fileSize: 1024 * 1024, // 1MB
284
+ expectedThroughputMbps: 1, // More relaxed
285
+ maxDurationMs: 5000,
286
+ maxMemoryUsageMB: 10,
287
+ },
288
+ medium: {
289
+ fileSize: 10 * 1024 * 1024, // 10MB
290
+ expectedThroughputMbps: 5, // Much more relaxed
291
+ maxDurationMs: 10000,
292
+ maxMemoryUsageMB: 25,
293
+ },
294
+ large: {
295
+ fileSize: 50 * 1024 * 1024, // 50MB
296
+ expectedThroughputMbps: 10, // Much more relaxed
297
+ maxDurationMs: 30000,
298
+ maxMemoryUsageMB: 50,
299
+ },
300
+ });
301
+
302
+ /**
303
+ * Progress tracker for testing upload progress callbacks
304
+ */
305
+ export class ProgressTracker {
306
+ private updates: { offset: number; timestamp: number }[] = [];
307
+
308
+ readonly onProgress = (offset: number) => {
309
+ this.updates.push({ offset, timestamp: performance.now() });
310
+ };
311
+
312
+ getUpdates() {
313
+ return [...this.updates];
314
+ }
315
+
316
+ getProgressRate(): number {
317
+ if (this.updates.length < 2) return 0;
318
+
319
+ const first = this.updates[0];
320
+ const last = this.updates[this.updates.length - 1];
321
+
322
+ const bytesPerMs =
323
+ (last.offset - first.offset) / (last.timestamp - first.timestamp);
324
+ return bytesPerMs * 1000; // bytes per second
325
+ }
326
+
327
+ getTotalBytesTracked(): number {
328
+ return this.updates.length > 0
329
+ ? this.updates[this.updates.length - 1].offset
330
+ : 0;
331
+ }
332
+
333
+ getUpdateCount(): number {
334
+ return this.updates.length;
335
+ }
336
+
337
+ clear() {
338
+ this.updates = [];
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Stress test configuration
344
+ */
345
+ export interface StressTestConfig {
346
+ concurrentUploads: number;
347
+ fileSize: number;
348
+ totalFiles: number;
349
+ maxErrorRate: number; // 0-1, acceptable error rate
350
+ minThroughputMbps: number;
351
+ maxTestDurationMs: number;
352
+ }
353
+
354
+ /**
355
+ * Run a stress test with multiple concurrent uploads
356
+ */
357
+ export const runStressTest = <A, E>(
358
+ createUpload: () => Effect.Effect<A, E>,
359
+ config: StressTestConfig,
360
+ ): Effect.Effect<
361
+ {
362
+ success: boolean;
363
+ metrics: ConcurrentMetrics;
364
+ errorRate: number;
365
+ totalThroughputMbps: number;
366
+ issues: string[];
367
+ },
368
+ E
369
+ > =>
370
+ Effect.gen(function* () {
371
+ const issues: string[] = [];
372
+ const startTime = performance.now();
373
+
374
+ // Create batches of concurrent uploads
375
+ const batches = Math.ceil(config.totalFiles / config.concurrentUploads);
376
+ let totalResults: A[] = [];
377
+ const allMetrics: ConcurrentMetrics[] = [];
378
+
379
+ for (let batch = 0; batch < batches; batch++) {
380
+ const remainingFiles =
381
+ config.totalFiles - batch * config.concurrentUploads;
382
+ const batchSize = Math.min(config.concurrentUploads, remainingFiles);
383
+
384
+ const batchOperations = Array.from({ length: batchSize }, () =>
385
+ createUpload(),
386
+ );
387
+
388
+ const { results, metrics } = yield* measureConcurrentOps(
389
+ batchOperations,
390
+ config.concurrentUploads,
391
+ );
392
+
393
+ totalResults = [...totalResults, ...results];
394
+ allMetrics.push(metrics);
395
+
396
+ // Check if we're exceeding time limit
397
+ const currentTime = performance.now();
398
+ if (currentTime - startTime > config.maxTestDurationMs) {
399
+ issues.push(
400
+ `Test exceeded maximum duration ${config.maxTestDurationMs}ms`,
401
+ );
402
+ break;
403
+ }
404
+ }
405
+
406
+ // Aggregate metrics
407
+ const totalOps = allMetrics.reduce((sum, m) => sum + m.totalOperations, 0);
408
+
409
+ const totalSuccessful = allMetrics.reduce(
410
+ (sum, m) => sum + m.successfulOperations,
411
+ 0,
412
+ );
413
+ const totalFailed = allMetrics.reduce(
414
+ (sum, m) => sum + m.failedOperations,
415
+ 0,
416
+ );
417
+ const avgDuration =
418
+ allMetrics.reduce((sum, m) => sum + m.averageDuration, 0) /
419
+ allMetrics.length;
420
+
421
+ const errorRate = totalFailed / totalOps;
422
+ const endTime = performance.now();
423
+ const totalDurationMs = endTime - startTime;
424
+ const totalBytes = config.fileSize * totalSuccessful;
425
+ const totalThroughputMbps =
426
+ (totalBytes * 8) / (totalDurationMs * 1024 * 1024);
427
+
428
+ const aggregatedMetrics: ConcurrentMetrics = {
429
+ totalOperations: totalOps,
430
+ totalDuration: totalDurationMs,
431
+ successfulOperations: totalSuccessful,
432
+ failedOperations: totalFailed,
433
+ averageDuration: avgDuration,
434
+ maxDuration: Math.max(...allMetrics.map((m) => m.maxDuration)),
435
+ minDuration: Math.min(...allMetrics.map((m) => m.minDuration)),
436
+ concurrencyLevel: config.concurrentUploads,
437
+ };
438
+
439
+ // Check benchmarks
440
+ if (errorRate > config.maxErrorRate) {
441
+ issues.push(
442
+ `Error rate ${(errorRate * 100).toFixed(1)}% exceeds maximum ${(config.maxErrorRate * 100).toFixed(1)}%`,
443
+ );
444
+ }
445
+
446
+ if (totalThroughputMbps < config.minThroughputMbps) {
447
+ issues.push(
448
+ `Total throughput ${totalThroughputMbps.toFixed(2)} Mbps below minimum ${config.minThroughputMbps} Mbps`,
449
+ );
450
+ }
451
+
452
+ return {
453
+ success: issues.length === 0,
454
+ metrics: aggregatedMetrics,
455
+ errorRate,
456
+ totalThroughputMbps,
457
+ issues,
458
+ };
459
+ });