@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.
- package/.turbo/turbo-build.log +88 -0
- package/dist/compression-service-Bm1VBnhT.mjs +18 -0
- package/dist/compression-service-Bm1VBnhT.mjs.map +1 -0
- package/dist/compression-service-Bn86iTJe.cjs +35 -0
- package/dist/compression-service-Bn86iTJe.cjs.map +1 -0
- package/dist/compression-service-CiF7Px08.d.cts +15 -0
- package/dist/compression-service-CiF7Px08.d.cts.map +1 -0
- package/dist/compression-service-DI7ZXVxH.d.mts +15 -0
- package/dist/compression-service-DI7ZXVxH.d.mts.map +1 -0
- package/dist/errors.cjs +9 -0
- package/dist/errors.d.cts +2 -0
- package/dist/errors.d.mts +2 -0
- package/dist/errors.mjs +2 -0
- package/dist/index-Ch8xM6Xt.d.cts +60 -0
- package/dist/index-Ch8xM6Xt.d.cts.map +1 -0
- package/dist/index-DBGtgXEd.d.mts +60 -0
- package/dist/index-DBGtgXEd.d.mts.map +1 -0
- package/dist/logger-service-1J5r_akj.mjs +8 -0
- package/dist/logger-service-1J5r_akj.mjs.map +1 -0
- package/dist/logger-service-BF2pZOHN.d.mts +12 -0
- package/dist/logger-service-BF2pZOHN.d.mts.map +1 -0
- package/dist/logger-service-CbN12RhO.d.cts +12 -0
- package/dist/logger-service-CbN12RhO.d.cts.map +1 -0
- package/dist/logger-service-cx8vzkXs.cjs +19 -0
- package/dist/logger-service-cx8vzkXs.cjs.map +1 -0
- package/dist/middleware-CAI0cnW2.d.mts +10 -0
- package/dist/middleware-CAI0cnW2.d.mts.map +1 -0
- package/dist/middleware-CYcctmlY.d.cts +10 -0
- package/dist/middleware-CYcctmlY.d.cts.map +1 -0
- package/dist/multipart.cjs +244 -0
- package/dist/multipart.cjs.map +1 -0
- package/dist/multipart.d.cts +2 -0
- package/dist/multipart.d.mts +2 -0
- package/dist/multipart.mjs +243 -0
- package/dist/multipart.mjs.map +1 -0
- package/dist/normalize-callback-BNBZZ1jT.cjs +44 -0
- package/dist/normalize-callback-BNBZZ1jT.cjs.map +1 -0
- package/dist/normalize-callback-DQ6C4gaV.mjs +33 -0
- package/dist/normalize-callback-DQ6C4gaV.mjs.map +1 -0
- package/dist/oneshot.cjs +64 -0
- package/dist/oneshot.cjs.map +1 -0
- package/dist/oneshot.d.cts +28 -0
- package/dist/oneshot.d.cts.map +1 -0
- package/dist/oneshot.d.mts +28 -0
- package/dist/oneshot.d.mts.map +1 -0
- package/dist/oneshot.mjs +63 -0
- package/dist/oneshot.mjs.map +1 -0
- package/dist/pipeline.cjs +16 -0
- package/dist/pipeline.cjs.map +1 -0
- package/dist/pipeline.d.cts +9 -0
- package/dist/pipeline.d.cts.map +1 -0
- package/dist/pipeline.d.mts +9 -0
- package/dist/pipeline.d.mts.map +1 -0
- package/dist/pipeline.mjs +14 -0
- package/dist/pipeline.mjs.map +1 -0
- package/dist/progress.cjs +0 -0
- package/dist/progress.d.cts +3 -0
- package/dist/progress.d.mts +3 -0
- package/dist/progress.mjs +1 -0
- package/dist/services.cjs +8 -0
- package/dist/services.d.cts +3 -0
- package/dist/services.d.mts +3 -0
- package/dist/services.mjs +3 -0
- package/dist/upload-error-B2ISUc_k.d.cts +48 -0
- package/dist/upload-error-B2ISUc_k.d.cts.map +1 -0
- package/dist/upload-error-BUexBh08.cjs +119 -0
- package/dist/upload-error-BUexBh08.cjs.map +1 -0
- package/dist/upload-error-jol-eoDW.d.mts +48 -0
- package/dist/upload-error-jol-eoDW.d.mts.map +1 -0
- package/dist/upload-error-zDvpxT9X.mjs +72 -0
- package/dist/upload-error-zDvpxT9X.mjs.map +1 -0
- package/dist/upload-event-C9TOVp5l.d.mts +36 -0
- package/dist/upload-event-C9TOVp5l.d.mts.map +1 -0
- package/dist/upload-event-D77olieX.d.cts +36 -0
- package/dist/upload-event-D77olieX.d.cts.map +1 -0
- package/package.json +70 -0
- package/src/errors/index.ts +10 -0
- package/src/errors/upload-error.test.ts +218 -0
- package/src/errors/upload-error.ts +89 -0
- package/src/multipart/chunk-stream.test.ts +79 -0
- package/src/multipart/chunk-stream.ts +37 -0
- package/src/multipart/circuit-breaker.test.ts +95 -0
- package/src/multipart/circuit-breaker.ts +68 -0
- package/src/multipart/index.test.ts +283 -0
- package/src/multipart/index.ts +119 -0
- package/src/multipart/upload-stream.test.ts +336 -0
- package/src/multipart/upload-stream.ts +246 -0
- package/src/oneshot/index.test.ts +153 -0
- package/src/oneshot/index.ts +76 -0
- package/src/oneshot/upload.test.ts +130 -0
- package/src/oneshot/upload.ts +52 -0
- package/src/pipeline/compress.test.ts +69 -0
- package/src/pipeline/compress.ts +8 -0
- package/src/pipeline/index.ts +3 -0
- package/src/pipeline/middleware.test.ts +102 -0
- package/src/pipeline/middleware.ts +30 -0
- package/src/progress/getprogress.test.ts +102 -0
- package/src/progress/index.ts +10 -0
- package/src/progress/upload-event.test.ts +102 -0
- package/src/progress/upload-event.ts +37 -0
- package/src/scaffold.test.ts +5 -0
- package/src/services/compression-service.test.ts +68 -0
- package/src/services/compression-service.ts +31 -0
- package/src/services/index.ts +11 -0
- package/src/services/logger-service-integration.test.ts +98 -0
- package/src/services/logger-service.test.ts +40 -0
- package/src/services/logger-service.ts +17 -0
- package/src/utils/abort-interop.test.ts +65 -0
- package/src/utils/abort-interop.ts +14 -0
- package/src/utils/normalize-callback.test.ts +46 -0
- package/src/utils/normalize-callback.ts +18 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +16 -0
- 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,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
|
+
}
|