@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,44 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { S3ClientService } from "../s3-client.service";
3
+ export interface MockS3Config {
4
+ simulateLatency?: number;
5
+ errorRate?: number;
6
+ uploadFailureRate?: number;
7
+ maxObjectSize?: number;
8
+ enableErrorInjection?: boolean;
9
+ }
10
+ interface MockStorage {
11
+ objects: Map<string, Uint8Array>;
12
+ multipartUploads: Map<string, {
13
+ uploadId: string;
14
+ parts: Map<number, {
15
+ etag: string;
16
+ data: Uint8Array;
17
+ }>;
18
+ metadata: {
19
+ contentType?: string;
20
+ cacheControl?: string;
21
+ key: string;
22
+ bucket: string;
23
+ };
24
+ }>;
25
+ incompleteParts: Map<string, Uint8Array>;
26
+ }
27
+ interface MockMetrics {
28
+ operationCounts: Map<string, number>;
29
+ lastOperation?: string;
30
+ totalBytesUploaded: number;
31
+ totalBytesDownloaded: number;
32
+ }
33
+ export interface MockS3TestMethods {
34
+ readonly setConfig: (config: Partial<MockS3Config>) => Effect.Effect<void>;
35
+ readonly clearStorage: () => Effect.Effect<void>;
36
+ readonly injectError: (operation: string, error: Error) => Effect.Effect<void>;
37
+ readonly clearError: (operation: string) => Effect.Effect<void>;
38
+ readonly getMetrics: () => Effect.Effect<MockMetrics>;
39
+ readonly getStorage: () => Effect.Effect<MockStorage>;
40
+ }
41
+ export declare const makeMockS3ClientService: (bucket: string, initialConfig?: MockS3Config) => Effect.Effect<S3ClientService["Type"] & MockS3TestMethods, never>;
42
+ export declare const MockS3ClientLayer: (bucket: string, config?: MockS3Config) => Layer.Layer<S3ClientService, never, never>;
43
+ export {};
44
+ //# sourceMappingURL=s3-client-mock.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-client-mock.service.d.ts","sourceRoot":"","sources":["../../../src/services/__mocks__/s3-client-mock.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAO,MAAM,QAAQ,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,MAAM,WAAW,YAAY;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAGD,UAAU,WAAW;IACnB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjC,gBAAgB,EAAE,GAAG,CACnB,MAAM,EACN;QACE,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,UAAU,CAAA;SAAE,CAAC,CAAC;QACvD,QAAQ,EAAE;YACR,WAAW,CAAC,EAAE,MAAM,CAAC;YACrB,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB,GAAG,EAAE,MAAM,CAAC;YACZ,MAAM,EAAE,MAAM,CAAC;SAChB,CAAC;KACH,CACF,CAAC;IACF,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC1C;AAGD,UAAU,WAAW;IACnB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAGD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3E,QAAQ,CAAC,YAAY,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,CACpB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,KACT,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACtD,QAAQ,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;CACvD;AAED,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,EACd,gBAAe,YAAiB,KAC/B,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,GAAG,iBAAiB,EAAE,KAAK,CA8hBlE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,EAAE,SAAS,YAAY,+CACC,CAAC"}
@@ -0,0 +1,379 @@
1
+ import { UploadistaError } from "@uploadista/core/errors";
2
+ import { Effect, Layer, Ref } from "effect";
3
+ import { S3ClientService } from "../s3-client.service";
4
+ export const makeMockS3ClientService = (bucket, initialConfig = {}) => {
5
+ return Effect.gen(function* () {
6
+ const storageRef = yield* Ref.make({
7
+ objects: new Map(),
8
+ multipartUploads: new Map(),
9
+ incompleteParts: new Map(),
10
+ });
11
+ const metricsRef = yield* Ref.make({
12
+ operationCounts: new Map(),
13
+ totalBytesUploaded: 0,
14
+ totalBytesDownloaded: 0,
15
+ });
16
+ const configRef = yield* Ref.make(initialConfig);
17
+ const errorInjectionRef = yield* Ref.make(new Map());
18
+ const simulateLatency = () => Effect.gen(function* () {
19
+ const config = yield* Ref.get(configRef);
20
+ if (config.simulateLatency && config.simulateLatency > 0) {
21
+ yield* Effect.sleep(`${config.simulateLatency} millis`);
22
+ }
23
+ });
24
+ const recordOperation = (operation) => Effect.gen(function* () {
25
+ yield* Ref.update(metricsRef, (metrics) => ({
26
+ ...metrics,
27
+ lastOperation: operation,
28
+ operationCounts: new Map([
29
+ ...metrics.operationCounts,
30
+ [operation, (metrics.operationCounts.get(operation) || 0) + 1],
31
+ ]),
32
+ }));
33
+ });
34
+ const checkForInjectedError = (operation) => Effect.gen(function* () {
35
+ const errorMap = yield* Ref.get(errorInjectionRef);
36
+ const error = errorMap.get(operation);
37
+ if (error) {
38
+ // Don't remove the error - let it persist for retries
39
+ // Tests should clear errors explicitly when done
40
+ yield* Effect.fail(UploadistaError.fromCode("FILE_WRITE_ERROR", error));
41
+ }
42
+ });
43
+ const maybeInjectRandomError = (operation) => Effect.gen(function* () {
44
+ const config = yield* Ref.get(configRef);
45
+ if (config.errorRate && Math.random() < config.errorRate) {
46
+ yield* Effect.fail(UploadistaError.fromCode("FILE_WRITE_ERROR", new Error(`Random error in ${operation}`)));
47
+ }
48
+ });
49
+ const generateETag = (data) => {
50
+ // Simple hash for ETag simulation
51
+ let hash = 0;
52
+ for (let i = 0; i < data.length; i++) {
53
+ hash = ((hash << 5) - hash + data[i]) & 0xffffffff;
54
+ }
55
+ return `"${Math.abs(hash).toString(16)}"`;
56
+ };
57
+ const generateUploadId = () => {
58
+ return `upload-${Date.now()}-${Math.random().toString(36).substring(2)}`;
59
+ };
60
+ // Implementation of service methods
61
+ const setConfig = (config) => Ref.update(configRef, (current) => ({ ...current, ...config }));
62
+ const clearStorage = () => Effect.gen(function* () {
63
+ yield* Ref.set(storageRef, {
64
+ objects: new Map(),
65
+ multipartUploads: new Map(),
66
+ incompleteParts: new Map(),
67
+ });
68
+ // Also clear injected errors
69
+ yield* Ref.set(errorInjectionRef, new Map());
70
+ // Reset metrics
71
+ yield* Ref.set(metricsRef, {
72
+ operationCounts: new Map(),
73
+ totalBytesUploaded: 0,
74
+ totalBytesDownloaded: 0,
75
+ });
76
+ });
77
+ const injectError = (operation, error) => Ref.update(errorInjectionRef, (map) => new Map([...map, [operation, error]]));
78
+ const clearError = (operation) => Ref.update(errorInjectionRef, (map) => {
79
+ const newMap = new Map(map);
80
+ newMap.delete(operation);
81
+ return newMap;
82
+ });
83
+ const getMetrics = () => Ref.get(metricsRef);
84
+ const getObject = (key) => Effect.gen(function* () {
85
+ yield* simulateLatency();
86
+ yield* recordOperation("getObject");
87
+ yield* checkForInjectedError("getObject");
88
+ yield* maybeInjectRandomError("getObject");
89
+ const storage = yield* Ref.get(storageRef);
90
+ const data = storage.objects.get(key);
91
+ if (!data) {
92
+ yield* Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND", new Error(`Object not found: ${key}`)));
93
+ return new ReadableStream(); // Never reached but helps TypeScript
94
+ }
95
+ yield* Ref.update(metricsRef, (metrics) => ({
96
+ ...metrics,
97
+ totalBytesDownloaded: metrics.totalBytesDownloaded + data.length,
98
+ }));
99
+ // Convert Uint8Array to ReadableStream
100
+ return new ReadableStream({
101
+ start(controller) {
102
+ controller.enqueue(data);
103
+ controller.close();
104
+ },
105
+ });
106
+ });
107
+ const headObject = (key) => Effect.gen(function* () {
108
+ yield* simulateLatency();
109
+ yield* recordOperation("headObject");
110
+ yield* checkForInjectedError("headObject");
111
+ const storage = yield* Ref.get(storageRef);
112
+ const data = storage.objects.get(key);
113
+ return data?.length;
114
+ });
115
+ const putObject = (key, body) => Effect.gen(function* () {
116
+ yield* simulateLatency();
117
+ yield* recordOperation("putObject");
118
+ yield* checkForInjectedError("putObject");
119
+ yield* maybeInjectRandomError("putObject");
120
+ const config = yield* Ref.get(configRef);
121
+ if (config.maxObjectSize && body.length > config.maxObjectSize) {
122
+ yield* Effect.fail(UploadistaError.fromCode("FILE_WRITE_ERROR", new Error(`Object size ${body.length} exceeds maximum ${config.maxObjectSize}`)));
123
+ }
124
+ yield* Ref.update(storageRef, (storage) => ({
125
+ ...storage,
126
+ objects: new Map([...storage.objects, [key, body]]),
127
+ }));
128
+ yield* Ref.update(metricsRef, (metrics) => ({
129
+ ...metrics,
130
+ totalBytesUploaded: metrics.totalBytesUploaded + body.length,
131
+ }));
132
+ return generateETag(body);
133
+ });
134
+ const deleteObject = (key) => Effect.gen(function* () {
135
+ yield* simulateLatency();
136
+ yield* recordOperation("deleteObject");
137
+ yield* checkForInjectedError("deleteObject");
138
+ yield* Ref.update(storageRef, (storage) => {
139
+ const newObjects = new Map(storage.objects);
140
+ newObjects.delete(key);
141
+ return { ...storage, objects: newObjects };
142
+ });
143
+ });
144
+ const deleteObjects = (keys) => Effect.gen(function* () {
145
+ yield* simulateLatency();
146
+ yield* recordOperation("deleteObjects");
147
+ yield* checkForInjectedError("deleteObjects");
148
+ yield* Ref.update(storageRef, (storage) => {
149
+ const newObjects = new Map(storage.objects);
150
+ for (const key of keys) {
151
+ newObjects.delete(key);
152
+ }
153
+ return { ...storage, objects: newObjects };
154
+ });
155
+ return {
156
+ $metadata: {},
157
+ Deleted: keys.map((key) => ({ Key: key })),
158
+ Errors: [],
159
+ };
160
+ });
161
+ const createMultipartUpload = (context) => Effect.gen(function* () {
162
+ yield* simulateLatency();
163
+ yield* recordOperation("createMultipartUpload");
164
+ yield* checkForInjectedError("createMultipartUpload");
165
+ const uploadId = generateUploadId();
166
+ yield* Ref.update(storageRef, (storage) => ({
167
+ ...storage,
168
+ multipartUploads: new Map([
169
+ ...storage.multipartUploads,
170
+ [
171
+ uploadId,
172
+ {
173
+ uploadId,
174
+ parts: new Map(),
175
+ metadata: {
176
+ contentType: context.contentType,
177
+ cacheControl: context.cacheControl,
178
+ key: context.key,
179
+ bucket: context.bucket,
180
+ },
181
+ },
182
+ ],
183
+ ]),
184
+ }));
185
+ return {
186
+ uploadId,
187
+ bucket: context.bucket,
188
+ key: context.key,
189
+ };
190
+ });
191
+ const uploadPart = (context) => Effect.gen(function* () {
192
+ yield* simulateLatency();
193
+ yield* recordOperation("uploadPart");
194
+ yield* checkForInjectedError("uploadPart");
195
+ const config = yield* Ref.get(configRef);
196
+ if (config.uploadFailureRate &&
197
+ Math.random() < config.uploadFailureRate) {
198
+ yield* Effect.fail(UploadistaError.fromCode("FILE_WRITE_ERROR", new Error(`Upload failed for part ${context.partNumber}`)));
199
+ }
200
+ const storage = yield* Ref.get(storageRef);
201
+ const upload = storage.multipartUploads.get(context.uploadId);
202
+ if (!upload) {
203
+ return yield* Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND", new Error(`Upload not found: ${context.uploadId}`)));
204
+ }
205
+ const etag = generateETag(context.data);
206
+ upload.parts.set(context.partNumber, {
207
+ etag,
208
+ data: context.data,
209
+ });
210
+ yield* Ref.update(metricsRef, (metrics) => ({
211
+ ...metrics,
212
+ totalBytesUploaded: metrics.totalBytesUploaded + context.data.length,
213
+ }));
214
+ return etag;
215
+ });
216
+ const completeMultipartUpload = (context, parts) => Effect.gen(function* () {
217
+ yield* simulateLatency();
218
+ yield* recordOperation("completeMultipartUpload");
219
+ yield* checkForInjectedError("completeMultipartUpload");
220
+ const storage = yield* Ref.get(storageRef);
221
+ const upload = storage.multipartUploads.get(context.uploadId);
222
+ if (!upload) {
223
+ yield* Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND", new Error(`Upload not found: ${context.uploadId}`)));
224
+ return; // This will never execute but helps TypeScript
225
+ }
226
+ // Validate all parts are present
227
+ for (const part of parts) {
228
+ if (!part.PartNumber || !upload.parts.has(part.PartNumber)) {
229
+ yield* Effect.fail(UploadistaError.fromCode("FILE_WRITE_ERROR", new Error(`Part ${part.PartNumber} not found`)));
230
+ }
231
+ }
232
+ // Combine all parts into final object
233
+ const sortedParts = parts
234
+ .sort((a, b) => (a.PartNumber || 0) - (b.PartNumber || 0))
235
+ .map((part) => {
236
+ const partData = upload.parts.get(part.PartNumber || 0);
237
+ if (!partData)
238
+ throw new Error(`Part ${part.PartNumber} not found`);
239
+ return partData.data;
240
+ });
241
+ const totalLength = sortedParts.reduce((sum, part) => sum + part.length, 0);
242
+ const combinedData = new Uint8Array(totalLength);
243
+ let offset = 0;
244
+ for (const part of sortedParts) {
245
+ combinedData.set(part, offset);
246
+ offset += part.length;
247
+ }
248
+ // Store the final object
249
+ yield* Ref.update(storageRef, (storage) => ({
250
+ ...storage,
251
+ objects: new Map([...storage.objects, [context.key, combinedData]]),
252
+ multipartUploads: new Map([...storage.multipartUploads].filter(([id]) => id !== context.uploadId)),
253
+ }));
254
+ return `https://${context.bucket}.s3.amazonaws.com/${context.key}`;
255
+ });
256
+ const abortMultipartUpload = (context) => Effect.gen(function* () {
257
+ yield* simulateLatency();
258
+ yield* recordOperation("abortMultipartUpload");
259
+ yield* checkForInjectedError("abortMultipartUpload");
260
+ yield* Ref.update(storageRef, (storage) => ({
261
+ ...storage,
262
+ multipartUploads: new Map([...storage.multipartUploads].filter(([id]) => id !== context.uploadId)),
263
+ }));
264
+ });
265
+ const listParts = (context) => Effect.gen(function* () {
266
+ yield* simulateLatency();
267
+ yield* recordOperation("listParts");
268
+ yield* checkForInjectedError("listParts");
269
+ const storage = yield* Ref.get(storageRef);
270
+ const upload = storage.multipartUploads.get(context.uploadId);
271
+ if (!upload) {
272
+ return yield* Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND", new Error(`Upload not found: ${context.uploadId}`)));
273
+ }
274
+ const parts = Array.from(upload.parts.entries())
275
+ .map(([partNumber, part]) => ({
276
+ PartNumber: partNumber,
277
+ ETag: part.etag,
278
+ Size: part.data.length,
279
+ }))
280
+ .sort((a, b) => (a.PartNumber || 0) - (b.PartNumber || 0));
281
+ return {
282
+ parts,
283
+ isTruncated: false,
284
+ nextPartNumberMarker: undefined,
285
+ };
286
+ });
287
+ const listMultipartUploads = (_keyMarker, _uploadIdMarker) => Effect.gen(function* () {
288
+ yield* simulateLatency();
289
+ yield* recordOperation("listMultipartUploads");
290
+ yield* checkForInjectedError("listMultipartUploads");
291
+ const storage = yield* Ref.get(storageRef);
292
+ const uploads = Array.from(storage.multipartUploads.values()).map((upload) => ({
293
+ Key: upload.metadata.key,
294
+ UploadId: upload.uploadId,
295
+ Initiated: new Date(),
296
+ }));
297
+ return {
298
+ Uploads: uploads,
299
+ IsTruncated: false,
300
+ };
301
+ });
302
+ const getIncompletePart = (id) => Effect.gen(function* () {
303
+ yield* simulateLatency();
304
+ yield* recordOperation("getIncompletePart");
305
+ const storage = yield* Ref.get(storageRef);
306
+ const data = storage.incompleteParts.get(`${id}.part`);
307
+ if (!data) {
308
+ return undefined;
309
+ }
310
+ return new ReadableStream({
311
+ start(controller) {
312
+ controller.enqueue(data);
313
+ controller.close();
314
+ },
315
+ });
316
+ });
317
+ const getIncompletePartSize = (id) => Effect.gen(function* () {
318
+ yield* simulateLatency();
319
+ yield* recordOperation("getIncompletePartSize");
320
+ const storage = yield* Ref.get(storageRef);
321
+ const data = storage.incompleteParts.get(`${id}.part`);
322
+ return data?.length;
323
+ });
324
+ const putIncompletePart = (id, data) => Effect.gen(function* () {
325
+ yield* simulateLatency();
326
+ yield* recordOperation("putIncompletePart");
327
+ yield* checkForInjectedError("putIncompletePart");
328
+ yield* Ref.update(storageRef, (storage) => ({
329
+ ...storage,
330
+ incompleteParts: new Map([
331
+ ...storage.incompleteParts,
332
+ [`${id}.part`, data],
333
+ ]),
334
+ }));
335
+ yield* Ref.update(metricsRef, (metrics) => ({
336
+ ...metrics,
337
+ totalBytesUploaded: metrics.totalBytesUploaded + data.length,
338
+ }));
339
+ return generateETag(data);
340
+ });
341
+ const deleteIncompletePart = (id) => Effect.gen(function* () {
342
+ yield* simulateLatency();
343
+ yield* recordOperation("deleteIncompletePart");
344
+ yield* Ref.update(storageRef, (storage) => {
345
+ const newIncompleteParts = new Map(storage.incompleteParts);
346
+ newIncompleteParts.delete(`${id}.part`);
347
+ return { ...storage, incompleteParts: newIncompleteParts };
348
+ });
349
+ });
350
+ const getStorage = () => Ref.get(storageRef);
351
+ return {
352
+ bucket,
353
+ // S3ClientService methods
354
+ getObject,
355
+ headObject,
356
+ putObject,
357
+ deleteObject,
358
+ deleteObjects,
359
+ createMultipartUpload,
360
+ uploadPart,
361
+ completeMultipartUpload,
362
+ abortMultipartUpload,
363
+ listParts,
364
+ listMultipartUploads,
365
+ getIncompletePart,
366
+ getIncompletePartSize,
367
+ putIncompletePart,
368
+ deleteIncompletePart,
369
+ // Mock-specific test methods
370
+ setConfig,
371
+ clearStorage,
372
+ injectError,
373
+ clearError,
374
+ getMetrics,
375
+ getStorage,
376
+ };
377
+ });
378
+ };
379
+ export const MockS3ClientLayer = (bucket, config) => Layer.effect(S3ClientService, makeMockS3ClientService(bucket, config));
@@ -0,0 +1,2 @@
1
+ export * from "./s3-client.service";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1 @@
1
+ export * from "./s3-client.service";
@@ -0,0 +1,68 @@
1
+ import type AWS from "@aws-sdk/client-s3";
2
+ import type { S3ClientConfig } from "@aws-sdk/client-s3";
3
+ import type { UploadistaError } from "@uploadista/core/errors";
4
+ import { Context, Effect, Layer } from "effect";
5
+ import type { MultipartUploadInfo, S3OperationContext } from "../types";
6
+ declare const S3ClientService_base: Context.TagClass<S3ClientService, "S3ClientService", {
7
+ readonly bucket: string;
8
+ readonly getObject: (key: string) => Effect.Effect<ReadableStream, UploadistaError>;
9
+ readonly headObject: (key: string) => Effect.Effect<number | undefined, UploadistaError>;
10
+ readonly putObject: (key: string, body: Uint8Array) => Effect.Effect<string, UploadistaError>;
11
+ readonly deleteObject: (key: string) => Effect.Effect<void, UploadistaError>;
12
+ readonly deleteObjects: (keys: string[]) => Effect.Effect<AWS.DeleteObjectsCommandOutput, UploadistaError>;
13
+ readonly createMultipartUpload: (context: S3OperationContext) => Effect.Effect<MultipartUploadInfo, UploadistaError>;
14
+ readonly uploadPart: (context: S3OperationContext & {
15
+ partNumber: number;
16
+ data: Uint8Array;
17
+ }) => Effect.Effect<string, UploadistaError>;
18
+ readonly completeMultipartUpload: (context: S3OperationContext, parts: Array<AWS.Part>) => Effect.Effect<string | undefined, UploadistaError>;
19
+ readonly abortMultipartUpload: (context: S3OperationContext) => Effect.Effect<void, UploadistaError>;
20
+ readonly listParts: (context: S3OperationContext & {
21
+ partNumberMarker?: string;
22
+ }) => Effect.Effect<{
23
+ parts: AWS.Part[];
24
+ isTruncated: boolean;
25
+ nextPartNumberMarker?: string;
26
+ }, UploadistaError>;
27
+ readonly listMultipartUploads: (keyMarker?: string, uploadIdMarker?: string) => Effect.Effect<AWS.ListMultipartUploadsCommandOutput, UploadistaError>;
28
+ readonly getIncompletePart: (id: string) => Effect.Effect<ReadableStream | undefined, UploadistaError>;
29
+ readonly getIncompletePartSize: (id: string) => Effect.Effect<number | undefined, UploadistaError>;
30
+ readonly putIncompletePart: (id: string, data: Uint8Array) => Effect.Effect<string, UploadistaError>;
31
+ readonly deleteIncompletePart: (id: string) => Effect.Effect<void, UploadistaError>;
32
+ }>;
33
+ export declare class S3ClientService extends S3ClientService_base {
34
+ }
35
+ export declare const makeS3ClientService: (s3ClientConfig: S3ClientConfig, bucket: string) => {
36
+ bucket: string;
37
+ getObject: (key: string) => Effect.Effect<ReadableStream<any>, UploadistaError, never>;
38
+ headObject: (key: string) => Effect.Effect<number | undefined, UploadistaError, never>;
39
+ putObject: (key: string, body: Uint8Array) => Effect.Effect<string, UploadistaError, never>;
40
+ deleteObject: (key: string) => Effect.Effect<void, UploadistaError, never>;
41
+ deleteObjects: (keys: string[]) => Effect.Effect<AWS.DeleteObjectsCommandOutput, UploadistaError, never>;
42
+ createMultipartUpload: (context: S3OperationContext) => Effect.Effect<{
43
+ uploadId: string;
44
+ bucket: string;
45
+ key: string;
46
+ }, UploadistaError, never>;
47
+ uploadPart: (context: S3OperationContext & {
48
+ partNumber: number;
49
+ data: Uint8Array;
50
+ }) => Effect.Effect<string, UploadistaError, never>;
51
+ completeMultipartUpload: (context: S3OperationContext, parts: Array<AWS.Part>) => Effect.Effect<string | undefined, UploadistaError, never>;
52
+ abortMultipartUpload: (context: S3OperationContext) => Effect.Effect<void, UploadistaError, never>;
53
+ listParts: (context: S3OperationContext & {
54
+ partNumberMarker?: string;
55
+ }) => Effect.Effect<{
56
+ parts: AWS.Part[];
57
+ isTruncated: boolean;
58
+ nextPartNumberMarker: string | undefined;
59
+ }, UploadistaError, never>;
60
+ listMultipartUploads: (keyMarker?: string, uploadIdMarker?: string) => Effect.Effect<AWS.ListMultipartUploadsCommandOutput, UploadistaError, never>;
61
+ getIncompletePart: (id: string) => Effect.Effect<ReadableStream<any> | undefined, UploadistaError, never>;
62
+ getIncompletePartSize: (id: string) => Effect.Effect<number | undefined, UploadistaError, never>;
63
+ putIncompletePart: (id: string, data: Uint8Array) => Effect.Effect<string, UploadistaError, never>;
64
+ deleteIncompletePart: (id: string) => Effect.Effect<void, UploadistaError, never>;
65
+ };
66
+ export declare const S3ClientLayer: (s3ClientConfig: S3ClientConfig, bucket: string) => Layer.Layer<S3ClientService, never, never>;
67
+ export {};
68
+ //# sourceMappingURL=s3-client.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-client.service.d.ts","sourceRoot":"","sources":["../../src/services/s3-client.service.ts"],"names":[],"mappings":"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"}