@tranquilload/core 0.1.0

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 (114) hide show
  1. package/.turbo/turbo-build.log +88 -0
  2. package/dist/compression-service-Bm1VBnhT.mjs +18 -0
  3. package/dist/compression-service-Bm1VBnhT.mjs.map +1 -0
  4. package/dist/compression-service-Bn86iTJe.cjs +35 -0
  5. package/dist/compression-service-Bn86iTJe.cjs.map +1 -0
  6. package/dist/compression-service-CiF7Px08.d.cts +15 -0
  7. package/dist/compression-service-CiF7Px08.d.cts.map +1 -0
  8. package/dist/compression-service-DI7ZXVxH.d.mts +15 -0
  9. package/dist/compression-service-DI7ZXVxH.d.mts.map +1 -0
  10. package/dist/errors.cjs +9 -0
  11. package/dist/errors.d.cts +2 -0
  12. package/dist/errors.d.mts +2 -0
  13. package/dist/errors.mjs +2 -0
  14. package/dist/index-Ch8xM6Xt.d.cts +60 -0
  15. package/dist/index-Ch8xM6Xt.d.cts.map +1 -0
  16. package/dist/index-DBGtgXEd.d.mts +60 -0
  17. package/dist/index-DBGtgXEd.d.mts.map +1 -0
  18. package/dist/logger-service-1J5r_akj.mjs +8 -0
  19. package/dist/logger-service-1J5r_akj.mjs.map +1 -0
  20. package/dist/logger-service-BF2pZOHN.d.mts +12 -0
  21. package/dist/logger-service-BF2pZOHN.d.mts.map +1 -0
  22. package/dist/logger-service-CbN12RhO.d.cts +12 -0
  23. package/dist/logger-service-CbN12RhO.d.cts.map +1 -0
  24. package/dist/logger-service-cx8vzkXs.cjs +19 -0
  25. package/dist/logger-service-cx8vzkXs.cjs.map +1 -0
  26. package/dist/middleware-CAI0cnW2.d.mts +10 -0
  27. package/dist/middleware-CAI0cnW2.d.mts.map +1 -0
  28. package/dist/middleware-CYcctmlY.d.cts +10 -0
  29. package/dist/middleware-CYcctmlY.d.cts.map +1 -0
  30. package/dist/multipart.cjs +244 -0
  31. package/dist/multipart.cjs.map +1 -0
  32. package/dist/multipart.d.cts +2 -0
  33. package/dist/multipart.d.mts +2 -0
  34. package/dist/multipart.mjs +243 -0
  35. package/dist/multipart.mjs.map +1 -0
  36. package/dist/normalize-callback-BNBZZ1jT.cjs +44 -0
  37. package/dist/normalize-callback-BNBZZ1jT.cjs.map +1 -0
  38. package/dist/normalize-callback-DQ6C4gaV.mjs +33 -0
  39. package/dist/normalize-callback-DQ6C4gaV.mjs.map +1 -0
  40. package/dist/oneshot.cjs +64 -0
  41. package/dist/oneshot.cjs.map +1 -0
  42. package/dist/oneshot.d.cts +28 -0
  43. package/dist/oneshot.d.cts.map +1 -0
  44. package/dist/oneshot.d.mts +28 -0
  45. package/dist/oneshot.d.mts.map +1 -0
  46. package/dist/oneshot.mjs +63 -0
  47. package/dist/oneshot.mjs.map +1 -0
  48. package/dist/pipeline.cjs +16 -0
  49. package/dist/pipeline.cjs.map +1 -0
  50. package/dist/pipeline.d.cts +9 -0
  51. package/dist/pipeline.d.cts.map +1 -0
  52. package/dist/pipeline.d.mts +9 -0
  53. package/dist/pipeline.d.mts.map +1 -0
  54. package/dist/pipeline.mjs +14 -0
  55. package/dist/pipeline.mjs.map +1 -0
  56. package/dist/progress.cjs +0 -0
  57. package/dist/progress.d.cts +3 -0
  58. package/dist/progress.d.mts +3 -0
  59. package/dist/progress.mjs +1 -0
  60. package/dist/services.cjs +8 -0
  61. package/dist/services.d.cts +3 -0
  62. package/dist/services.d.mts +3 -0
  63. package/dist/services.mjs +3 -0
  64. package/dist/upload-error-B2ISUc_k.d.cts +48 -0
  65. package/dist/upload-error-B2ISUc_k.d.cts.map +1 -0
  66. package/dist/upload-error-BUexBh08.cjs +119 -0
  67. package/dist/upload-error-BUexBh08.cjs.map +1 -0
  68. package/dist/upload-error-jol-eoDW.d.mts +48 -0
  69. package/dist/upload-error-jol-eoDW.d.mts.map +1 -0
  70. package/dist/upload-error-zDvpxT9X.mjs +72 -0
  71. package/dist/upload-error-zDvpxT9X.mjs.map +1 -0
  72. package/dist/upload-event-C9TOVp5l.d.mts +36 -0
  73. package/dist/upload-event-C9TOVp5l.d.mts.map +1 -0
  74. package/dist/upload-event-D77olieX.d.cts +36 -0
  75. package/dist/upload-event-D77olieX.d.cts.map +1 -0
  76. package/package.json +70 -0
  77. package/src/errors/index.ts +10 -0
  78. package/src/errors/upload-error.test.ts +218 -0
  79. package/src/errors/upload-error.ts +89 -0
  80. package/src/multipart/chunk-stream.test.ts +79 -0
  81. package/src/multipart/chunk-stream.ts +37 -0
  82. package/src/multipart/circuit-breaker.test.ts +95 -0
  83. package/src/multipart/circuit-breaker.ts +68 -0
  84. package/src/multipart/index.test.ts +283 -0
  85. package/src/multipart/index.ts +119 -0
  86. package/src/multipart/upload-stream.test.ts +336 -0
  87. package/src/multipart/upload-stream.ts +246 -0
  88. package/src/oneshot/index.test.ts +153 -0
  89. package/src/oneshot/index.ts +76 -0
  90. package/src/oneshot/upload.test.ts +130 -0
  91. package/src/oneshot/upload.ts +52 -0
  92. package/src/pipeline/compress.test.ts +69 -0
  93. package/src/pipeline/compress.ts +8 -0
  94. package/src/pipeline/index.ts +3 -0
  95. package/src/pipeline/middleware.test.ts +102 -0
  96. package/src/pipeline/middleware.ts +30 -0
  97. package/src/progress/getprogress.test.ts +102 -0
  98. package/src/progress/index.ts +10 -0
  99. package/src/progress/upload-event.test.ts +102 -0
  100. package/src/progress/upload-event.ts +37 -0
  101. package/src/scaffold.test.ts +5 -0
  102. package/src/services/compression-service.test.ts +68 -0
  103. package/src/services/compression-service.ts +31 -0
  104. package/src/services/index.ts +11 -0
  105. package/src/services/logger-service-integration.test.ts +98 -0
  106. package/src/services/logger-service.test.ts +40 -0
  107. package/src/services/logger-service.ts +17 -0
  108. package/src/utils/abort-interop.test.ts +65 -0
  109. package/src/utils/abort-interop.ts +14 -0
  110. package/src/utils/normalize-callback.test.ts +46 -0
  111. package/src/utils/normalize-callback.ts +18 -0
  112. package/tsconfig.json +8 -0
  113. package/tsdown.config.ts +16 -0
  114. package/vitest.config.ts +7 -0
@@ -0,0 +1,72 @@
1
+ //#region src/errors/upload-error.ts
2
+ var PartUploadError = class extends Error {
3
+ _tag = "PartUploadError";
4
+ constructor(partNumber, attempt, cause) {
5
+ super(`Part ${partNumber} failed on attempt ${attempt}`);
6
+ this.partNumber = partNumber;
7
+ this.attempt = attempt;
8
+ this.cause = cause;
9
+ this.name = "PartUploadError";
10
+ }
11
+ };
12
+ var MaxRetriesExceededError = class extends Error {
13
+ _tag = "MaxRetriesExceededError";
14
+ constructor(partNumber, totalAttempts, cause) {
15
+ super(`Part ${partNumber} failed after ${totalAttempts} attempts`);
16
+ this.partNumber = partNumber;
17
+ this.totalAttempts = totalAttempts;
18
+ this.cause = cause;
19
+ this.name = "MaxRetriesExceededError";
20
+ }
21
+ };
22
+ var PresignedUrlError = class extends Error {
23
+ _tag = "PresignedUrlError";
24
+ constructor(cause) {
25
+ super("Failed to obtain pre-signed URL");
26
+ this.cause = cause;
27
+ this.name = "PresignedUrlError";
28
+ }
29
+ };
30
+ var InitiateUploadError = class extends Error {
31
+ _tag = "InitiateUploadError";
32
+ constructor(cause) {
33
+ super("Failed to initiate multipart upload");
34
+ this.cause = cause;
35
+ this.name = "InitiateUploadError";
36
+ }
37
+ };
38
+ var ReconcileError = class extends Error {
39
+ _tag = "ReconcileError";
40
+ constructor(cause) {
41
+ super("Failed to reconcile completed parts");
42
+ this.cause = cause;
43
+ this.name = "ReconcileError";
44
+ }
45
+ };
46
+ var CompleteUploadError = class extends Error {
47
+ _tag = "CompleteUploadError";
48
+ constructor(cause) {
49
+ super("Failed to complete multipart upload");
50
+ this.cause = cause;
51
+ this.name = "CompleteUploadError";
52
+ }
53
+ };
54
+ var AbortError = class extends Error {
55
+ _tag = "AbortError";
56
+ constructor() {
57
+ super("Upload aborted");
58
+ this.name = "AbortError";
59
+ }
60
+ };
61
+ var CircuitOpenError = class extends Error {
62
+ _tag = "CircuitOpenError";
63
+ constructor(failedParts) {
64
+ super(`Circuit breaker opened after ${failedParts} consecutive part failures`);
65
+ this.failedParts = failedParts;
66
+ this.name = "CircuitOpenError";
67
+ }
68
+ };
69
+ //#endregion
70
+ export { MaxRetriesExceededError as a, ReconcileError as c, InitiateUploadError as i, CircuitOpenError as n, PartUploadError as o, CompleteUploadError as r, PresignedUrlError as s, AbortError as t };
71
+
72
+ //# sourceMappingURL=upload-error-zDvpxT9X.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-error-zDvpxT9X.mjs","names":[],"sources":["../src/errors/upload-error.ts"],"sourcesContent":["export class PartUploadError extends Error {\n readonly _tag = \"PartUploadError\" as const\n\n constructor(\n readonly partNumber: number,\n readonly attempt: number,\n override readonly cause: unknown\n ) {\n super(`Part ${partNumber} failed on attempt ${attempt}`)\n this.name = \"PartUploadError\"\n }\n}\n\nexport class MaxRetriesExceededError extends Error {\n readonly _tag = \"MaxRetriesExceededError\" as const\n\n constructor(\n readonly partNumber: number,\n readonly totalAttempts: number,\n override readonly cause: unknown\n ) {\n super(`Part ${partNumber} failed after ${totalAttempts} attempts`)\n this.name = \"MaxRetriesExceededError\"\n }\n}\n\nexport class PresignedUrlError extends Error {\n readonly _tag = \"PresignedUrlError\" as const\n\n constructor(override readonly cause: unknown) {\n super(\"Failed to obtain pre-signed URL\")\n this.name = \"PresignedUrlError\"\n }\n}\n\nexport class InitiateUploadError extends Error {\n readonly _tag = \"InitiateUploadError\" as const\n\n constructor(override readonly cause: unknown) {\n super(\"Failed to initiate multipart upload\")\n this.name = \"InitiateUploadError\"\n }\n}\n\nexport class ReconcileError extends Error {\n readonly _tag = \"ReconcileError\" as const\n\n constructor(override readonly cause: unknown) {\n super(\"Failed to reconcile completed parts\")\n this.name = \"ReconcileError\"\n }\n}\n\nexport class CompleteUploadError extends Error {\n readonly _tag = \"CompleteUploadError\" as const\n\n constructor(override readonly cause: unknown) {\n super(\"Failed to complete multipart upload\")\n this.name = \"CompleteUploadError\"\n }\n}\n\nexport class AbortError extends Error {\n readonly _tag = \"AbortError\" as const\n\n constructor() {\n super(\"Upload aborted\")\n this.name = \"AbortError\"\n }\n}\n\nexport class CircuitOpenError extends Error {\n readonly _tag = \"CircuitOpenError\" as const\n\n constructor(readonly failedParts: number) {\n super(`Circuit breaker opened after ${failedParts} consecutive part failures`)\n this.name = \"CircuitOpenError\"\n }\n}\n\nexport type UploadError =\n | PartUploadError\n | MaxRetriesExceededError\n | PresignedUrlError\n | InitiateUploadError\n | ReconcileError\n | CompleteUploadError\n | AbortError\n | CircuitOpenError\n"],"mappings":";AAAA,IAAa,kBAAb,cAAqC,MAAM;CACzC,OAAgB;CAEhB,YACE,YACA,SACA,OACA;AACA,QAAM,QAAQ,WAAW,qBAAqB,UAAU;AAJ/C,OAAA,aAAA;AACA,OAAA,UAAA;AACS,OAAA,QAAA;AAGlB,OAAK,OAAO;;;AAIhB,IAAa,0BAAb,cAA6C,MAAM;CACjD,OAAgB;CAEhB,YACE,YACA,eACA,OACA;AACA,QAAM,QAAQ,WAAW,gBAAgB,cAAc,WAAW;AAJzD,OAAA,aAAA;AACA,OAAA,gBAAA;AACS,OAAA,QAAA;AAGlB,OAAK,OAAO;;;AAIhB,IAAa,oBAAb,cAAuC,MAAM;CAC3C,OAAgB;CAEhB,YAAY,OAAkC;AAC5C,QAAM,kCAAkC;AADZ,OAAA,QAAA;AAE5B,OAAK,OAAO;;;AAIhB,IAAa,sBAAb,cAAyC,MAAM;CAC7C,OAAgB;CAEhB,YAAY,OAAkC;AAC5C,QAAM,sCAAsC;AADhB,OAAA,QAAA;AAE5B,OAAK,OAAO;;;AAIhB,IAAa,iBAAb,cAAoC,MAAM;CACxC,OAAgB;CAEhB,YAAY,OAAkC;AAC5C,QAAM,sCAAsC;AADhB,OAAA,QAAA;AAE5B,OAAK,OAAO;;;AAIhB,IAAa,sBAAb,cAAyC,MAAM;CAC7C,OAAgB;CAEhB,YAAY,OAAkC;AAC5C,QAAM,sCAAsC;AADhB,OAAA,QAAA;AAE5B,OAAK,OAAO;;;AAIhB,IAAa,aAAb,cAAgC,MAAM;CACpC,OAAgB;CAEhB,cAAc;AACZ,QAAM,iBAAiB;AACvB,OAAK,OAAO;;;AAIhB,IAAa,mBAAb,cAAsC,MAAM;CAC1C,OAAgB;CAEhB,YAAY,aAA8B;AACxC,QAAM,gCAAgC,YAAY,4BAA4B;AAD3D,OAAA,cAAA;AAEnB,OAAK,OAAO"}
@@ -0,0 +1,36 @@
1
+ import { Option } from "effect";
2
+
3
+ //#region src/progress/upload-event.d.ts
4
+ interface UploadInitiated {
5
+ readonly _tag: "UploadInitiated";
6
+ readonly uploadId: string;
7
+ readonly timestamp: number;
8
+ }
9
+ interface UploadCompleted {
10
+ readonly _tag: "UploadCompleted";
11
+ readonly uploadId: string;
12
+ readonly totalParts: number;
13
+ readonly timestamp: number;
14
+ }
15
+ interface PartCompleted {
16
+ readonly _tag: "PartCompleted";
17
+ readonly partNumber: number;
18
+ readonly etag: string;
19
+ readonly bytesUploaded: number;
20
+ readonly timestamp: number;
21
+ }
22
+ interface CircuitOpen {
23
+ readonly _tag: "CircuitOpen";
24
+ readonly failedParts: number;
25
+ readonly timestamp: number;
26
+ }
27
+ interface ProgressTick {
28
+ readonly _tag: "ProgressTick";
29
+ readonly bytesUploaded: number;
30
+ readonly totalBytes: Option.Option<number>;
31
+ readonly timestamp: number;
32
+ }
33
+ type UploadEvent = UploadInitiated | UploadCompleted | PartCompleted | ProgressTick | CircuitOpen;
34
+ //#endregion
35
+ export { UploadEvent as a, UploadCompleted as i, PartCompleted as n, UploadInitiated as o, ProgressTick as r, CircuitOpen as t };
36
+ //# sourceMappingURL=upload-event-C9TOVp5l.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-event-C9TOVp5l.d.mts","names":[],"sources":["../src/progress/upload-event.ts"],"mappings":";;;UAEiB,eAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,eAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA;EAAA,SACA,UAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,aAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAA;EAAA,SACA,IAAA;EAAA,SACA,aAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,WAAA;EAAA,SACN,IAAA;EAAA,SACA,WAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,YAAA;EAAA,SACN,IAAA;EAAA,SACA,aAAA;EAAA,SACA,UAAA,EAAY,MAAA,CAAO,MAAA;EAAA,SACnB,SAAA;AAAA;AAAA,KAGC,WAAA,GAAc,eAAA,GAAkB,eAAA,GAAkB,aAAA,GAAgB,YAAA,GAAe,WAAA"}
@@ -0,0 +1,36 @@
1
+ import { Option } from "effect";
2
+
3
+ //#region src/progress/upload-event.d.ts
4
+ interface UploadInitiated {
5
+ readonly _tag: "UploadInitiated";
6
+ readonly uploadId: string;
7
+ readonly timestamp: number;
8
+ }
9
+ interface UploadCompleted {
10
+ readonly _tag: "UploadCompleted";
11
+ readonly uploadId: string;
12
+ readonly totalParts: number;
13
+ readonly timestamp: number;
14
+ }
15
+ interface PartCompleted {
16
+ readonly _tag: "PartCompleted";
17
+ readonly partNumber: number;
18
+ readonly etag: string;
19
+ readonly bytesUploaded: number;
20
+ readonly timestamp: number;
21
+ }
22
+ interface CircuitOpen {
23
+ readonly _tag: "CircuitOpen";
24
+ readonly failedParts: number;
25
+ readonly timestamp: number;
26
+ }
27
+ interface ProgressTick {
28
+ readonly _tag: "ProgressTick";
29
+ readonly bytesUploaded: number;
30
+ readonly totalBytes: Option.Option<number>;
31
+ readonly timestamp: number;
32
+ }
33
+ type UploadEvent = UploadInitiated | UploadCompleted | PartCompleted | ProgressTick | CircuitOpen;
34
+ //#endregion
35
+ export { UploadEvent as a, UploadCompleted as i, PartCompleted as n, UploadInitiated as o, ProgressTick as r, CircuitOpen as t };
36
+ //# sourceMappingURL=upload-event-D77olieX.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-event-D77olieX.d.cts","names":[],"sources":["../src/progress/upload-event.ts"],"mappings":";;;UAEiB,eAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,eAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA;EAAA,SACA,UAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,aAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAA;EAAA,SACA,IAAA;EAAA,SACA,aAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,WAAA;EAAA,SACN,IAAA;EAAA,SACA,WAAA;EAAA,SACA,SAAA;AAAA;AAAA,UAGM,YAAA;EAAA,SACN,IAAA;EAAA,SACA,aAAA;EAAA,SACA,UAAA,EAAY,MAAA,CAAO,MAAA;EAAA,SACnB,SAAA;AAAA;AAAA,KAGC,WAAA,GAAc,eAAA,GAAkB,eAAA,GAAkB,aAAA,GAAgB,YAAA,GAAe,WAAA"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@tranquilload/core",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe upload library built on Effect",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Schrubitteflau/Tranquilload.git"
9
+ },
10
+ "author": {
11
+ "name": "Schrubitteflau",
12
+ "url": "https://github.com/Schrubitteflau"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Schrubitteflau/Tranquilload/issues"
16
+ },
17
+ "homepage": "https://github.com/Schrubitteflau/Tranquilload#readme",
18
+ "type": "module",
19
+ "exports": {
20
+ "./multipart": {
21
+ "types": "./dist/multipart.d.mts",
22
+ "import": "./dist/multipart.mjs",
23
+ "require": "./dist/multipart.cjs"
24
+ },
25
+ "./oneshot": {
26
+ "types": "./dist/oneshot.d.mts",
27
+ "import": "./dist/oneshot.mjs",
28
+ "require": "./dist/oneshot.cjs"
29
+ },
30
+ "./pipeline": {
31
+ "types": "./dist/pipeline.d.mts",
32
+ "import": "./dist/pipeline.mjs",
33
+ "require": "./dist/pipeline.cjs"
34
+ },
35
+ "./services": {
36
+ "types": "./dist/services.d.mts",
37
+ "import": "./dist/services.mjs",
38
+ "require": "./dist/services.cjs"
39
+ },
40
+ "./errors": {
41
+ "types": "./dist/errors.d.mts",
42
+ "import": "./dist/errors.mjs",
43
+ "require": "./dist/errors.cjs"
44
+ },
45
+ "./progress": {
46
+ "types": "./dist/progress.d.mts",
47
+ "import": "./dist/progress.mjs",
48
+ "require": "./dist/progress.cjs"
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "peerDependencies": {
55
+ "effect": ">=3.19.19"
56
+ },
57
+ "devDependencies": {
58
+ "@effect/vitest": "^0.27.0",
59
+ "effect": "3.19.19",
60
+ "tsdown": "^0.21.0",
61
+ "typescript": "^5.5.0",
62
+ "vitest": "^3.2.0"
63
+ },
64
+ "scripts": {
65
+ "build": "tsdown",
66
+ "dev": "tsdown --watch",
67
+ "test": "vitest run",
68
+ "typecheck": "tsc --noEmit"
69
+ }
70
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ PartUploadError,
3
+ MaxRetriesExceededError,
4
+ PresignedUrlError,
5
+ InitiateUploadError,
6
+ ReconcileError,
7
+ CompleteUploadError,
8
+ AbortError,
9
+ type UploadError,
10
+ } from "./upload-error.js"
@@ -0,0 +1,218 @@
1
+ import { it, describe, expect } from 'vitest'
2
+ import {
3
+ PartUploadError,
4
+ MaxRetriesExceededError,
5
+ PresignedUrlError,
6
+ InitiateUploadError,
7
+ ReconcileError,
8
+ CompleteUploadError,
9
+ AbortError,
10
+ CircuitOpenError,
11
+ type UploadError,
12
+ } from './upload-error.js'
13
+
14
+ describe("PartUploadError", () => {
15
+ it("is instanceof Error", () => {
16
+ const err = new PartUploadError(1, 1, new Error("network"))
17
+ expect(err instanceof Error).toBe(true)
18
+ })
19
+ it("has correct _tag", () => {
20
+ const err = new PartUploadError(1, 1, new Error("network"))
21
+ expect(err._tag).toBe("PartUploadError")
22
+ })
23
+ it("has human-readable message", () => {
24
+ const err = new PartUploadError(3, 2, new Error("timeout"))
25
+ expect(err.message).toBe("Part 3 failed on attempt 2")
26
+ })
27
+ it("name equals _tag for logger compat", () => {
28
+ const err = new PartUploadError(1, 1, new Error("x"))
29
+ expect(err.name).toBe("PartUploadError")
30
+ })
31
+ it("preserves cause", () => {
32
+ const cause = new Error("network failure")
33
+ const err = new PartUploadError(1, 1, cause)
34
+ expect(err.cause).toBe(cause)
35
+ })
36
+ it("preserves partNumber and attempt", () => {
37
+ const err = new PartUploadError(5, 3, null)
38
+ expect(err.partNumber).toBe(5)
39
+ expect(err.attempt).toBe(3)
40
+ })
41
+ })
42
+
43
+ describe("MaxRetriesExceededError", () => {
44
+ it("is instanceof Error", () => {
45
+ const err = new MaxRetriesExceededError(2, 4, new Error("exhausted"))
46
+ expect(err instanceof Error).toBe(true)
47
+ })
48
+ it("has correct _tag", () => {
49
+ const err = new MaxRetriesExceededError(2, 4, new Error("exhausted"))
50
+ expect(err._tag).toBe("MaxRetriesExceededError")
51
+ })
52
+ it("has human-readable message", () => {
53
+ const err = new MaxRetriesExceededError(2, 4, new Error("exhausted"))
54
+ expect(err.message).toBe("Part 2 failed after 4 attempts")
55
+ })
56
+ it("name equals _tag for logger compat", () => {
57
+ const err = new MaxRetriesExceededError(2, 4, null)
58
+ expect(err.name).toBe("MaxRetriesExceededError")
59
+ })
60
+ it("preserves partNumber and totalAttempts", () => {
61
+ const err = new MaxRetriesExceededError(7, 10, null)
62
+ expect(err.partNumber).toBe(7)
63
+ expect(err.totalAttempts).toBe(10)
64
+ })
65
+ it("preserves cause", () => {
66
+ const cause = new Error("last attempt failed")
67
+ const err = new MaxRetriesExceededError(2, 4, cause)
68
+ expect(err.cause).toBe(cause)
69
+ })
70
+ })
71
+
72
+ describe("PresignedUrlError", () => {
73
+ it("is instanceof Error", () => {
74
+ const err = new PresignedUrlError(new Error("403 Forbidden"))
75
+ expect(err instanceof Error).toBe(true)
76
+ })
77
+ it("has correct _tag", () => {
78
+ const err = new PresignedUrlError(new Error("403 Forbidden"))
79
+ expect(err._tag).toBe("PresignedUrlError")
80
+ })
81
+ it("has human-readable message", () => {
82
+ const err = new PresignedUrlError(null)
83
+ expect(err.message).toBe("Failed to obtain pre-signed URL")
84
+ })
85
+ it("name equals _tag for logger compat", () => {
86
+ const err = new PresignedUrlError(null)
87
+ expect(err.name).toBe("PresignedUrlError")
88
+ })
89
+ it("preserves cause", () => {
90
+ const cause = new Error("403 Forbidden")
91
+ const err = new PresignedUrlError(cause)
92
+ expect(err.cause).toBe(cause)
93
+ })
94
+ })
95
+
96
+ describe("InitiateUploadError", () => {
97
+ it("is instanceof Error", () => {
98
+ const err = new InitiateUploadError(new Error("S3 failure"))
99
+ expect(err instanceof Error).toBe(true)
100
+ })
101
+ it("has correct _tag", () => {
102
+ const err = new InitiateUploadError(new Error("S3 failure"))
103
+ expect(err._tag).toBe("InitiateUploadError")
104
+ })
105
+ it("has human-readable message", () => {
106
+ const err = new InitiateUploadError(null)
107
+ expect(err.message).toBe("Failed to initiate multipart upload")
108
+ })
109
+ it("name equals _tag for logger compat", () => {
110
+ const err = new InitiateUploadError(null)
111
+ expect(err.name).toBe("InitiateUploadError")
112
+ })
113
+ it("preserves cause", () => {
114
+ const cause = new Error("S3 failure")
115
+ const err = new InitiateUploadError(cause)
116
+ expect(err.cause).toBe(cause)
117
+ })
118
+ })
119
+
120
+ describe("ReconcileError", () => {
121
+ it("is instanceof Error", () => {
122
+ const err = new ReconcileError(new Error("storage unavailable"))
123
+ expect(err instanceof Error).toBe(true)
124
+ })
125
+ it("has correct _tag", () => {
126
+ const err = new ReconcileError(new Error("storage unavailable"))
127
+ expect(err._tag).toBe("ReconcileError")
128
+ })
129
+ it("has human-readable message", () => {
130
+ const err = new ReconcileError(null)
131
+ expect(err.message).toBe("Failed to reconcile completed parts")
132
+ })
133
+ it("name equals _tag for logger compat", () => {
134
+ const err = new ReconcileError(null)
135
+ expect(err.name).toBe("ReconcileError")
136
+ })
137
+ it("preserves cause", () => {
138
+ const cause = new Error("storage unavailable")
139
+ const err = new ReconcileError(cause)
140
+ expect(err.cause).toBe(cause)
141
+ })
142
+ })
143
+
144
+ describe("CompleteUploadError", () => {
145
+ it("is instanceof Error", () => {
146
+ const err = new CompleteUploadError(new Error("500 Internal"))
147
+ expect(err instanceof Error).toBe(true)
148
+ })
149
+ it("has correct _tag", () => {
150
+ const err = new CompleteUploadError(new Error("500 Internal"))
151
+ expect(err._tag).toBe("CompleteUploadError")
152
+ })
153
+ it("has human-readable message", () => {
154
+ const err = new CompleteUploadError(null)
155
+ expect(err.message).toBe("Failed to complete multipart upload")
156
+ })
157
+ it("name equals _tag for logger compat", () => {
158
+ const err = new CompleteUploadError(null)
159
+ expect(err.name).toBe("CompleteUploadError")
160
+ })
161
+ it("preserves cause", () => {
162
+ const cause = new Error("500 Internal")
163
+ const err = new CompleteUploadError(cause)
164
+ expect(err.cause).toBe(cause)
165
+ })
166
+ })
167
+
168
+ describe("AbortError", () => {
169
+ it("is instanceof Error", () => {
170
+ const err = new AbortError()
171
+ expect(err instanceof Error).toBe(true)
172
+ })
173
+ it("has correct _tag", () => {
174
+ const err = new AbortError()
175
+ expect(err._tag).toBe("AbortError")
176
+ })
177
+ it("has human-readable message", () => {
178
+ const err = new AbortError()
179
+ expect(err.message).toBe("Upload aborted")
180
+ })
181
+ it("name equals _tag for logger compat", () => {
182
+ const err = new AbortError()
183
+ expect(err.name).toBe("AbortError")
184
+ })
185
+ it("has no cause (abort is intentional, not an error chain)", () => {
186
+ const err = new AbortError()
187
+ expect(err.cause).toBeUndefined()
188
+ })
189
+ })
190
+
191
+ it("UploadError union is exhaustive", () => {
192
+ const check = (err: UploadError): string => {
193
+ switch (err._tag) {
194
+ case "PartUploadError": return "part"
195
+ case "MaxRetriesExceededError": return "maxRetries"
196
+ case "PresignedUrlError": return "presigned"
197
+ case "InitiateUploadError": return "initiate"
198
+ case "ReconcileError": return "reconcile"
199
+ case "CompleteUploadError": return "complete"
200
+ case "AbortError": return "abort"
201
+ case "CircuitOpenError": return "circuitOpen"
202
+ default: {
203
+ // If a new variant is added to UploadError without a matching case above,
204
+ // TypeScript will error here: "Type 'NewVariant' is not assignable to type 'never'"
205
+ const _exhaustive: never = err
206
+ return _exhaustive
207
+ }
208
+ }
209
+ }
210
+ expect(check(new AbortError())).toBe("abort")
211
+ expect(check(new PresignedUrlError(null))).toBe("presigned")
212
+ expect(check(new InitiateUploadError(null))).toBe("initiate")
213
+ expect(check(new ReconcileError(null))).toBe("reconcile")
214
+ expect(check(new CompleteUploadError(null))).toBe("complete")
215
+ expect(check(new PartUploadError(1, 1, null))).toBe("part")
216
+ expect(check(new MaxRetriesExceededError(1, 1, null))).toBe("maxRetries")
217
+ expect(check(new CircuitOpenError(3))).toBe("circuitOpen")
218
+ })
@@ -0,0 +1,89 @@
1
+ export class PartUploadError extends Error {
2
+ readonly _tag = "PartUploadError" as const
3
+
4
+ constructor(
5
+ readonly partNumber: number,
6
+ readonly attempt: number,
7
+ override readonly cause: unknown
8
+ ) {
9
+ super(`Part ${partNumber} failed on attempt ${attempt}`)
10
+ this.name = "PartUploadError"
11
+ }
12
+ }
13
+
14
+ export class MaxRetriesExceededError extends Error {
15
+ readonly _tag = "MaxRetriesExceededError" as const
16
+
17
+ constructor(
18
+ readonly partNumber: number,
19
+ readonly totalAttempts: number,
20
+ override readonly cause: unknown
21
+ ) {
22
+ super(`Part ${partNumber} failed after ${totalAttempts} attempts`)
23
+ this.name = "MaxRetriesExceededError"
24
+ }
25
+ }
26
+
27
+ export class PresignedUrlError extends Error {
28
+ readonly _tag = "PresignedUrlError" as const
29
+
30
+ constructor(override readonly cause: unknown) {
31
+ super("Failed to obtain pre-signed URL")
32
+ this.name = "PresignedUrlError"
33
+ }
34
+ }
35
+
36
+ export class InitiateUploadError extends Error {
37
+ readonly _tag = "InitiateUploadError" as const
38
+
39
+ constructor(override readonly cause: unknown) {
40
+ super("Failed to initiate multipart upload")
41
+ this.name = "InitiateUploadError"
42
+ }
43
+ }
44
+
45
+ export class ReconcileError extends Error {
46
+ readonly _tag = "ReconcileError" as const
47
+
48
+ constructor(override readonly cause: unknown) {
49
+ super("Failed to reconcile completed parts")
50
+ this.name = "ReconcileError"
51
+ }
52
+ }
53
+
54
+ export class CompleteUploadError extends Error {
55
+ readonly _tag = "CompleteUploadError" as const
56
+
57
+ constructor(override readonly cause: unknown) {
58
+ super("Failed to complete multipart upload")
59
+ this.name = "CompleteUploadError"
60
+ }
61
+ }
62
+
63
+ export class AbortError extends Error {
64
+ readonly _tag = "AbortError" as const
65
+
66
+ constructor() {
67
+ super("Upload aborted")
68
+ this.name = "AbortError"
69
+ }
70
+ }
71
+
72
+ export class CircuitOpenError extends Error {
73
+ readonly _tag = "CircuitOpenError" as const
74
+
75
+ constructor(readonly failedParts: number) {
76
+ super(`Circuit breaker opened after ${failedParts} consecutive part failures`)
77
+ this.name = "CircuitOpenError"
78
+ }
79
+ }
80
+
81
+ export type UploadError =
82
+ | PartUploadError
83
+ | MaxRetriesExceededError
84
+ | PresignedUrlError
85
+ | InitiateUploadError
86
+ | ReconcileError
87
+ | CompleteUploadError
88
+ | AbortError
89
+ | CircuitOpenError
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { Effect, Stream } from "effect"
3
+ import { chunkStream } from "./chunk-stream.js"
4
+
5
+ // Helper: create a ReadableStream from a Uint8Array (single chunk)
6
+ const fromBytes = (bytes: Uint8Array): ReadableStream<Uint8Array> =>
7
+ new ReadableStream({
8
+ start(controller) {
9
+ controller.enqueue(bytes)
10
+ controller.close()
11
+ },
12
+ })
13
+
14
+ // Helper: run chunkStream and collect chunks as a plain Array
15
+ const collectChunks = (
16
+ stream: ReadableStream<Uint8Array>,
17
+ chunkSize: number
18
+ ): Effect.Effect<Uint8Array[], unknown> =>
19
+ Stream.runCollect(chunkStream(stream, chunkSize)).pipe(
20
+ Effect.map((chunk) => Array.from(chunk))
21
+ )
22
+
23
+ describe("chunkStream", () => {
24
+ it.effect("splits into chunkSize chunks, last chunk smaller", () =>
25
+ Effect.gen(function* () {
26
+ // 10 bytes, chunkSize 3 → chunks: [3, 3, 3, 1]
27
+ const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
28
+ const chunks = yield* collectChunks(fromBytes(data), 3)
29
+
30
+ expect(chunks).toHaveLength(4)
31
+ expect(chunks[0]).toEqual(new Uint8Array([0, 1, 2]))
32
+ expect(chunks[1]).toEqual(new Uint8Array([3, 4, 5]))
33
+ expect(chunks[2]).toEqual(new Uint8Array([6, 7, 8]))
34
+ expect(chunks[3]).toEqual(new Uint8Array([9]))
35
+ })
36
+ )
37
+
38
+ it.effect("preserves all bytes across chunks", () =>
39
+ Effect.gen(function* () {
40
+ const data = new Uint8Array(100).map((_, i) => i % 256)
41
+ const chunks = yield* collectChunks(fromBytes(data), 7)
42
+
43
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0)
44
+ expect(totalLength).toBe(100)
45
+
46
+ // Verify byte values are intact
47
+ const reconstructed = new Uint8Array(100)
48
+ let offset = 0
49
+ for (const chunk of chunks) {
50
+ reconstructed.set(chunk, offset)
51
+ offset += chunk.length
52
+ }
53
+ expect(reconstructed).toEqual(data)
54
+ })
55
+ )
56
+
57
+ it.effect("emits single chunk when stream is smaller than chunkSize", () =>
58
+ Effect.gen(function* () {
59
+ const data = new Uint8Array([10, 20, 30])
60
+ const chunks = yield* collectChunks(fromBytes(data), 100)
61
+
62
+ expect(chunks).toHaveLength(1)
63
+ expect(chunks[0]).toEqual(new Uint8Array([10, 20, 30]))
64
+ })
65
+ )
66
+
67
+ it.effect("emits no trailing empty chunk on exact multiple", () =>
68
+ Effect.gen(function* () {
69
+ const data = new Uint8Array(9).fill(5) // 9 = 3 * 3
70
+ const chunks = yield* collectChunks(fromBytes(data), 3)
71
+
72
+ expect(chunks).toHaveLength(3)
73
+ for (const chunk of chunks) {
74
+ expect(chunk).toHaveLength(3)
75
+ expect(chunk).toEqual(new Uint8Array([5, 5, 5]))
76
+ }
77
+ })
78
+ )
79
+ })
@@ -0,0 +1,37 @@
1
+ import { Stream } from "effect"
2
+
3
+ export const chunkStream = (
4
+ stream: ReadableStream<Uint8Array>,
5
+ chunkSize: number
6
+ ): Stream.Stream<Uint8Array, unknown> => {
7
+ let buffer = new Uint8Array(0)
8
+
9
+ const transform = new TransformStream<Uint8Array, Uint8Array>({
10
+ transform(chunk, controller) {
11
+ // Concatenate incoming chunk into the buffer
12
+ const merged = new Uint8Array(buffer.length + chunk.length)
13
+ merged.set(buffer)
14
+ merged.set(chunk, buffer.length)
15
+ buffer = merged
16
+
17
+ // Emit every full-size chunk
18
+ while (buffer.length >= chunkSize) {
19
+ controller.enqueue(buffer.slice(0, chunkSize))
20
+ buffer = buffer.slice(chunkSize)
21
+ }
22
+ },
23
+ flush(controller) {
24
+ // Emit remaining bytes (last partial chunk)
25
+ if (buffer.length > 0) {
26
+ controller.enqueue(buffer)
27
+ }
28
+ },
29
+ })
30
+
31
+ const chunked = stream.pipeThrough(transform)
32
+
33
+ return Stream.fromReadableStream(
34
+ () => chunked,
35
+ (e) => e
36
+ )
37
+ }