@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.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +588 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/observability.d.ts +45 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +155 -0
- package/dist/s3-store-old.d.ts +51 -0
- package/dist/s3-store-old.d.ts.map +1 -0
- package/dist/s3-store-old.js +765 -0
- package/dist/s3-store.d.ts +9 -0
- package/dist/s3-store.d.ts.map +1 -0
- package/dist/s3-store.js +666 -0
- package/dist/services/__mocks__/s3-client-mock.service.d.ts +44 -0
- package/dist/services/__mocks__/s3-client-mock.service.d.ts.map +1 -0
- package/dist/services/__mocks__/s3-client-mock.service.js +379 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/s3-client.service.d.ts +68 -0
- package/dist/services/s3-client.service.d.ts.map +1 -0
- package/dist/services/s3-client.service.js +209 -0
- package/dist/test-observability.d.ts +6 -0
- package/dist/test-observability.d.ts.map +1 -0
- package/dist/test-observability.js +62 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/calculations.d.ts +7 -0
- package/dist/utils/calculations.d.ts.map +1 -0
- package/dist/utils/calculations.js +41 -0
- package/dist/utils/error-handling.d.ts +7 -0
- package/dist/utils/error-handling.d.ts.map +1 -0
- package/dist/utils/error-handling.js +29 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/stream-adapter.d.ts +14 -0
- package/dist/utils/stream-adapter.d.ts.map +1 -0
- package/dist/utils/stream-adapter.js +41 -0
- package/package.json +36 -0
- package/src/__tests__/integration/s3-store.integration.test.ts +548 -0
- package/src/__tests__/multipart-logic.test.ts +395 -0
- package/src/__tests__/s3-store.edge-cases.test.ts +681 -0
- package/src/__tests__/s3-store.performance.test.ts +622 -0
- package/src/__tests__/s3-store.test.ts +662 -0
- package/src/__tests__/utils/performance-helpers.ts +459 -0
- package/src/__tests__/utils/test-data-generator.ts +331 -0
- package/src/__tests__/utils/test-setup.ts +256 -0
- package/src/index.ts +1 -0
- package/src/s3-store.ts +1059 -0
- package/src/services/__mocks__/s3-client-mock.service.ts +604 -0
- package/src/services/index.ts +1 -0
- package/src/services/s3-client.service.ts +359 -0
- package/src/types.ts +96 -0
- package/src/utils/calculations.ts +61 -0
- package/src/utils/error-handling.ts +52 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/stream-adapter.ts +50 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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"}
|