@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,155 @@
1
+ import { Context, Effect, Layer, Metric, MetricBoundaries } from "effect";
2
+ // ============================================================================
3
+ // Metrics Definition
4
+ // ============================================================================
5
+ // Counter metrics
6
+ export const uploadRequestsTotal = Metric.counter("s3_upload_requests_total", {
7
+ description: "Total number of upload requests",
8
+ });
9
+ export const uploadPartsTotal = Metric.counter("s3_upload_parts_total", {
10
+ description: "Total number of individual parts uploaded",
11
+ });
12
+ export const uploadSuccessTotal = Metric.counter("s3_upload_success_total", {
13
+ description: "Total number of successful uploads",
14
+ });
15
+ export const uploadErrorsTotal = Metric.counter("s3_upload_errors_total", {
16
+ description: "Total number of upload errors",
17
+ });
18
+ export const s3ApiCallsTotal = Metric.counter("s3_api_calls_total", {
19
+ description: "Total number of S3 API calls",
20
+ });
21
+ // Histogram metrics for timing and sizes
22
+ export const uploadDurationHistogram = Metric.histogram("s3_upload_duration_seconds", MetricBoundaries.exponential({
23
+ start: 0.01, // 10ms
24
+ factor: 2,
25
+ count: 20, // Up to ~10 seconds
26
+ }), "Duration of upload operations in seconds");
27
+ export const partUploadDurationHistogram = Metric.histogram("s3_part_upload_duration_seconds", MetricBoundaries.exponential({
28
+ start: 0.001, // 1ms
29
+ factor: 2,
30
+ count: 15, // Up to ~32 seconds
31
+ }), "Duration of individual part uploads in seconds");
32
+ export const fileSizeHistogram = Metric.histogram("s3_file_size_bytes", MetricBoundaries.exponential({
33
+ start: 1024, // 1KB
34
+ factor: 2,
35
+ count: 25, // Up to ~33GB
36
+ }), "Size of uploaded files in bytes");
37
+ export const partSizeHistogram = Metric.histogram("s3_part_size_bytes", MetricBoundaries.linear({
38
+ start: 5_242_880, // 5MB (minimum part size)
39
+ width: 1_048_576, // 1MB increments
40
+ count: 20, // Up to ~25MB
41
+ }), "Size of upload parts in bytes");
42
+ // Gauge metrics for current state
43
+ export const activeUploadsGauge = Metric.gauge("s3_active_uploads", {
44
+ description: "Number of currently active uploads",
45
+ });
46
+ export const uploadThroughputGauge = Metric.gauge("s3_upload_throughput_bytes_per_second", {
47
+ description: "Current upload throughput in bytes per second",
48
+ });
49
+ // Summary metrics for percentiles
50
+ export const uploadLatencySummary = Metric.summary({
51
+ name: "s3_upload_latency_seconds",
52
+ maxAge: "10 minutes",
53
+ maxSize: 1000,
54
+ error: 0.01,
55
+ quantiles: [0.5, 0.9, 0.95, 0.99],
56
+ description: "Upload latency percentiles",
57
+ });
58
+ // ============================================================================
59
+ // Tracing Configuration
60
+ // ============================================================================
61
+ // Create a service tag for tracing context
62
+ export const TracingService = Context.GenericTag("TracingService");
63
+ // Create a simple tracing layer using Effect's native tracing (environment-agnostic)
64
+ export const createTracingLayer = (options) => {
65
+ const serviceName = options?.serviceName ?? "uploadista-s3-store";
66
+ // Return a layer that provides tracing service context
67
+ return Layer.succeed(TracingService, { serviceName });
68
+ };
69
+ // Default tracing layer for development
70
+ export const TracingLayerLive = createTracingLayer({
71
+ serviceName: "uploadista-s3-store",
72
+ });
73
+ // ============================================================================
74
+ // Observability Layer
75
+ // ============================================================================
76
+ export const ObservabilityLayer = Layer.mergeAll(TracingLayerLive
77
+ // Effect's native metrics are automatically available
78
+ );
79
+ // ============================================================================
80
+ // Utility Functions
81
+ // ============================================================================
82
+ export const withUploadMetrics = (uploadId, effect) => effect.pipe(Effect.tap(() => uploadRequestsTotal.pipe(Metric.tagged("upload_id", uploadId))(Effect.succeed(1))), Effect.tapError(() => uploadErrorsTotal.pipe(Metric.tagged("upload_id", uploadId))(Effect.succeed(1))), Effect.tap(() => uploadSuccessTotal.pipe(Metric.tagged("upload_id", uploadId))(Effect.succeed(1))));
83
+ export const withS3ApiMetrics = (operation, effect) => effect.pipe(Effect.tap(() => s3ApiCallsTotal.pipe(Metric.tagged("operation", operation))(Effect.succeed(1))));
84
+ export const withTimingMetrics = (metric, effect) => Effect.gen(function* () {
85
+ const startTime = yield* Effect.sync(() => Date.now());
86
+ const result = yield* effect;
87
+ const endTime = yield* Effect.sync(() => Date.now());
88
+ const duration = (endTime - startTime) / 1000; // Convert to seconds
89
+ yield* metric(Effect.succeed(duration));
90
+ return result;
91
+ });
92
+ export const classifyS3Error = (error) => {
93
+ if (!error || typeof error !== 'object')
94
+ return "unknown_error";
95
+ const errorCode = 'code' in error ? error.code : undefined;
96
+ if (!errorCode)
97
+ return "unknown_error";
98
+ switch (errorCode) {
99
+ case "NetworkError":
100
+ case "ECONNRESET":
101
+ case "ENOTFOUND":
102
+ case "ETIMEDOUT":
103
+ return "network_error";
104
+ case "InvalidAccessKeyId":
105
+ case "SignatureDoesNotMatch":
106
+ case "TokenRefreshRequired":
107
+ return "authentication_error";
108
+ case "AccessDenied":
109
+ case "AccountProblem":
110
+ return "authorization_error";
111
+ case "SlowDown":
112
+ case "RequestTimeTooSkewed":
113
+ return "throttling_error";
114
+ case "InternalError":
115
+ case "ServiceUnavailable":
116
+ return "server_error";
117
+ case "InvalidRequest":
118
+ case "MalformedXML":
119
+ case "RequestEntityTooLarge":
120
+ return "client_error";
121
+ default:
122
+ return "unknown_error";
123
+ }
124
+ };
125
+ export const trackS3Error = (operation, error, context = {}) => Effect.gen(function* () {
126
+ const errorCategory = classifyS3Error(error);
127
+ yield* uploadErrorsTotal.pipe(Metric.tagged("operation", operation), Metric.tagged("error_category", errorCategory))(Effect.succeed(1));
128
+ const errorDetails = {
129
+ operation,
130
+ error_category: errorCategory,
131
+ error_type: typeof error,
132
+ error_message: error instanceof Error ? error.message : String(error),
133
+ error_code: error && typeof error === 'object' && 'code' in error ? error.code : undefined,
134
+ ...context,
135
+ };
136
+ yield* Effect.logError(`S3 ${operation} failed`).pipe(Effect.annotateLogs(errorDetails));
137
+ });
138
+ // ============================================================================
139
+ // Enhanced Logging Helpers
140
+ // ============================================================================
141
+ export const logWithContext = (message, context) => Effect.log(message).pipe(Effect.annotateLogs(context));
142
+ export const logUploadProgress = (uploadId, progress) => logWithContext("Upload progress", {
143
+ upload_id: uploadId,
144
+ uploaded_bytes: progress.uploadedBytes,
145
+ total_bytes: progress.totalBytes,
146
+ progress_percentage: Math.round((progress.uploadedBytes / progress.totalBytes) * 100),
147
+ ...(progress.partNumber && { part_number: progress.partNumber }),
148
+ ...(progress.speed && { upload_speed_bps: progress.speed }),
149
+ });
150
+ export const logS3Operation = (operation, uploadId, metadata) => logWithContext(`S3 ${operation}`, {
151
+ operation,
152
+ upload_id: uploadId,
153
+ ...metadata,
154
+ });
155
+ export const logS3Error = (operation, error, context = {}) => trackS3Error(operation, error, context);
@@ -0,0 +1,51 @@
1
+ import type { S3ClientConfig } from "@aws-sdk/client-s3";
2
+ import { UploadistaError } from "@uploadista/core/errors";
3
+ import type { DataStore, KvStore, UploadFile } from "@uploadista/core/types";
4
+ import { UploadFileDataStore, UploadFileKVStore } from "@uploadista/core/types";
5
+ import { Effect, Layer } from "effect";
6
+ export type S3StoreOptions = {
7
+ deliveryUrl: string;
8
+ /**
9
+ * The preferred part size for parts send to S3. Can not be lower than 5MiB or more than 5GiB.
10
+ * The server calculates the optimal part size, which takes this size into account,
11
+ * but may increase it to not exceed the S3 10K parts limit.
12
+ */
13
+ partSize?: number;
14
+ /**
15
+ * The minimal part size for parts.
16
+ * Can be used to ensure that all non-trailing parts are exactly the same size.
17
+ * Can not be lower than 5MiB or more than 5GiB.
18
+ */
19
+ minPartSize?: number;
20
+ /**
21
+ * The maximum number of parts allowed in a multipart upload. Defaults to 10,000.
22
+ */
23
+ maxMultipartParts?: number;
24
+ useTags?: boolean;
25
+ maxConcurrentPartUploads?: number;
26
+ expirationPeriodInMilliseconds?: number;
27
+ s3ClientConfig: S3ClientConfig & {
28
+ bucket: string;
29
+ };
30
+ };
31
+ export type S3StoreFactoryOptions = S3StoreOptions;
32
+ export type S3Store = DataStore<UploadFile> & {
33
+ getUpload: (id: string) => Effect.Effect<UploadFile, UploadistaError>;
34
+ read: (id: string) => Effect.Effect<ReadableStream, UploadistaError>;
35
+ getChunkerConstraints: () => {
36
+ minChunkSize: number;
37
+ maxChunkSize: number;
38
+ optimalChunkSize: number;
39
+ requiresOrderedChunks: boolean;
40
+ };
41
+ };
42
+ export declare function createS3Storexx(options: Omit<S3StoreOptions, "kvStore">): Effect.Effect<S3Store, never, UploadFileKVStore>;
43
+ declare function createS3StoreImplementation({ deliveryUrl, partSize, minPartSize, useTags, maxMultipartParts, kvStore, maxConcurrentPartUploads, expirationPeriodInMilliseconds, // 1 week
44
+ s3ClientConfig: { bucket, ...restS3ClientConfig }, }: S3StoreOptions & {
45
+ kvStore: KvStore<UploadFile>;
46
+ }): S3Store;
47
+ export declare const createS3Store: (options: S3StoreFactoryOptions) => Effect.Effect<S3Store, never, UploadFileKVStore>;
48
+ export declare const S3StoreLayer: (options: Omit<S3StoreOptions, "kvStore">) => Layer.Layer<UploadFileDataStore, never, UploadFileKVStore>;
49
+ export declare const s3Store: typeof createS3StoreImplementation;
50
+ export {};
51
+ //# sourceMappingURL=s3-store-old.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-store-old.d.ts","sourceRoot":"","sources":["../src/s3-store-old.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACV,SAAS,EAGT,OAAO,EACP,UAAU,EAEX,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAgBhF,OAAO,EAAE,MAAM,EAAE,KAAK,EAAyB,MAAM,QAAQ,CAAC;AAiF9D,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,8BAA8B,CAAC,EAAE,MAAM,CAAC;IAExC,cAAc,EAAE,cAAc,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,cAAc,CAAC;AAQnD,MAAM,MAAM,OAAO,GAAG,SAAS,CAAC,UAAU,CAAC,GAAG;IAC5C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;IACtE,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IACrE,qBAAqB,EAAE,MAAM;QAC3B,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,CAAC;QACzB,qBAAqB,EAAE,OAAO,CAAC;KAChC,CAAC;CACH,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,oDAKvE;AAGD,iBAAS,2BAA2B,CAAC,EACnC,WAAW,EACX,QAAQ,EACR,WAAuB,EACvB,OAAc,EACd,iBAA0B,EAC1B,OAAO,EACP,wBAA6B,EAC7B,8BAAwD,EAAE,SAAS;AACnE,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,kBAAkB,EAAE,GAClD,EAAE,cAAc,GAAG;IAClB,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B,GAAG,OAAO,CAw9BV;AAGD,eAAO,MAAM,aAAa,GAAI,SAAS,qBAAqB,qDAQxD,CAAC;AAGL,eAAO,MAAM,YAAY,GAAI,SAAS,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,+DACV,CAAC;AAG5D,eAAO,MAAM,OAAO,oCAA8B,CAAC"}