@edgestore/sdk 1.0.0-next.1 → 1.0.0-next.2
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/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +15 -1
- package/dist/generated/api-v2.d.ts +71 -26
- package/dist/generated/api-v2.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/managementUploadOperations.d.ts +64 -0
- package/dist/internal/managementUploadOperations.d.ts.map +1 -0
- package/dist/internal/managementUploadOperations.js +72 -0
- package/dist/internal/multipartUpload.d.ts +6 -6
- package/dist/internal/multipartUpload.d.ts.map +1 -1
- package/dist/internal/runtimeOperations.d.ts.map +1 -1
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +20 -7
- package/dist/internal/uploadLifecycle.d.ts +57 -0
- package/dist/internal/uploadLifecycle.d.ts.map +1 -0
- package/dist/internal/uploadLifecycle.js +91 -0
- package/dist/internal/uploadProcessing.d.ts +20 -0
- package/dist/internal/uploadProcessing.d.ts.map +1 -0
- package/dist/internal/uploadProcessing.js +25 -0
- package/dist/internal/uploadSource.d.ts +18 -0
- package/dist/internal/uploadSource.d.ts.map +1 -0
- package/dist/internal/uploadSource.js +75 -0
- package/dist/internal/uploadTransfer.js +15 -1
- package/dist/managementClient.d.ts +2 -1
- package/dist/managementClient.d.ts.map +1 -1
- package/dist/managementClient.js +2 -2
- package/dist/managementResources.d.ts +5 -1
- package/dist/managementResources.d.ts.map +1 -1
- package/dist/managementResources.js +14 -49
- package/dist/managementUpload.d.ts +36 -0
- package/dist/managementUpload.d.ts.map +1 -0
- package/dist/managementUpload.js +74 -0
- package/dist/sdk.js +1 -1
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +42 -189
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/errors.ts +28 -0
- package/src/generated/api-v2.ts +71 -26
- package/src/generated/openapi-v2.json +237 -16
- package/src/generated/source.ts +2 -2
- package/src/index.ts +6 -0
- package/src/internal/managementUploadOperations.ts +99 -0
- package/src/internal/multipartUpload.ts +8 -8
- package/src/internal/transport.ts +21 -12
- package/src/internal/uploadLifecycle.ts +154 -0
- package/src/internal/uploadProcessing.ts +48 -0
- package/src/internal/uploadSource.ts +104 -0
- package/src/internal/uploadTransfer.ts +20 -1
- package/src/managementClient.ts +6 -2
- package/src/managementResources.ts +21 -51
- package/src/managementUpload.ts +139 -0
- package/src/sdk.ts +1 -1
- package/src/upload.ts +49 -274
- package/src/version.ts +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { OperationBody, OperationResult } from './operationTypes';
|
|
2
|
+
import type { Transport } from './transport';
|
|
3
|
+
|
|
4
|
+
type ProjectUploadInput = {
|
|
5
|
+
project: string;
|
|
6
|
+
uploadId: string;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ManagementUploadOperations = ReturnType<
|
|
11
|
+
typeof createManagementUploadOperations
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export function createManagementUploadOperations(transport: Transport) {
|
|
15
|
+
return {
|
|
16
|
+
request: (
|
|
17
|
+
input: OperationBody<'v2.management.uploads.request'> & {
|
|
18
|
+
project: string;
|
|
19
|
+
bucket: string;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
},
|
|
22
|
+
): Promise<OperationResult<'v2.management.uploads.request'>> => {
|
|
23
|
+
const { project, bucket, signal, ...body } = input;
|
|
24
|
+
return transport.execute((client) =>
|
|
25
|
+
client.POST(
|
|
26
|
+
'/management/projects/{projectRef}/buckets/{bucketName}/uploads',
|
|
27
|
+
{
|
|
28
|
+
params: { path: { projectRef: project, bucketName: bucket } },
|
|
29
|
+
body,
|
|
30
|
+
signal,
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
get: (
|
|
36
|
+
input: ProjectUploadInput,
|
|
37
|
+
): Promise<OperationResult<'v2.management.uploads.get'>> =>
|
|
38
|
+
transport.execute((client) =>
|
|
39
|
+
client.GET('/management/projects/{projectRef}/uploads/{uploadId}', {
|
|
40
|
+
params: {
|
|
41
|
+
path: { projectRef: input.project, uploadId: input.uploadId },
|
|
42
|
+
},
|
|
43
|
+
signal: input.signal,
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
getWithResponse: (input: ProjectUploadInput) =>
|
|
47
|
+
transport.executeWithResponse((client) =>
|
|
48
|
+
client.GET('/management/projects/{projectRef}/uploads/{uploadId}', {
|
|
49
|
+
params: {
|
|
50
|
+
path: { projectRef: input.project, uploadId: input.uploadId },
|
|
51
|
+
},
|
|
52
|
+
signal: input.signal,
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
cancel: (
|
|
56
|
+
input: ProjectUploadInput,
|
|
57
|
+
): Promise<OperationResult<'v2.management.uploads.cancel'>> =>
|
|
58
|
+
transport.execute((client) =>
|
|
59
|
+
client.DELETE('/management/projects/{projectRef}/uploads/{uploadId}', {
|
|
60
|
+
params: {
|
|
61
|
+
path: { projectRef: input.project, uploadId: input.uploadId },
|
|
62
|
+
},
|
|
63
|
+
signal: input.signal,
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
createParts: (
|
|
67
|
+
input: ProjectUploadInput &
|
|
68
|
+
OperationBody<'v2.management.uploads.parts.create'>,
|
|
69
|
+
): Promise<OperationResult<'v2.management.uploads.parts.create'>> => {
|
|
70
|
+
const { project, uploadId, signal, ...body } = input;
|
|
71
|
+
return transport.execute((client) =>
|
|
72
|
+
client.POST(
|
|
73
|
+
'/management/projects/{projectRef}/uploads/{uploadId}/parts',
|
|
74
|
+
{
|
|
75
|
+
params: { path: { projectRef: project, uploadId } },
|
|
76
|
+
body,
|
|
77
|
+
signal,
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
completeMultipart: (
|
|
83
|
+
input: ProjectUploadInput &
|
|
84
|
+
OperationBody<'v2.management.uploads.multipart.complete'>,
|
|
85
|
+
): Promise<OperationResult<'v2.management.uploads.multipart.complete'>> => {
|
|
86
|
+
const { project, uploadId, signal, ...body } = input;
|
|
87
|
+
return transport.execute((client) =>
|
|
88
|
+
client.POST(
|
|
89
|
+
'/management/projects/{projectRef}/uploads/{uploadId}/complete',
|
|
90
|
+
{
|
|
91
|
+
params: { path: { projectRef: project, uploadId } },
|
|
92
|
+
body,
|
|
93
|
+
signal,
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -5,8 +5,8 @@ import type { Transport } from './transport';
|
|
|
5
5
|
import { throwIfAborted } from './uploadRetry';
|
|
6
6
|
import { putWithRetry } from './uploadTransfer';
|
|
7
7
|
|
|
8
|
-
type
|
|
9
|
-
type
|
|
8
|
+
export type CompletedUploadPart = { partNumber: number; eTag: string };
|
|
9
|
+
export type SignedUploadPart = { partNumber: number; signedUrl: string };
|
|
10
10
|
type ProgressHandler = RuntimeUploadInput['onProgress'];
|
|
11
11
|
|
|
12
12
|
export async function uploadParts(
|
|
@@ -14,14 +14,14 @@ export async function uploadParts(
|
|
|
14
14
|
options: {
|
|
15
15
|
body: Blob;
|
|
16
16
|
uploadId: string;
|
|
17
|
-
parts:
|
|
17
|
+
parts: SignedUploadPart[];
|
|
18
18
|
partSizeBytes: number;
|
|
19
19
|
concurrency: number;
|
|
20
20
|
signal?: AbortSignal;
|
|
21
21
|
onProgress?: ProgressHandler;
|
|
22
22
|
},
|
|
23
|
-
): Promise<
|
|
24
|
-
const completed:
|
|
23
|
+
): Promise<CompletedUploadPart[]> {
|
|
24
|
+
const completed: CompletedUploadPart[] = Array(options.parts.length);
|
|
25
25
|
let nextIndex = 0;
|
|
26
26
|
let transferredBytes = 0;
|
|
27
27
|
const workerCount = Math.min(options.concurrency, options.parts.length);
|
|
@@ -70,14 +70,14 @@ export async function uploadStreamParts(
|
|
|
70
70
|
stream: ReadableStream<Uint8Array>;
|
|
71
71
|
totalBytes: number;
|
|
72
72
|
uploadId: string;
|
|
73
|
-
parts:
|
|
73
|
+
parts: SignedUploadPart[];
|
|
74
74
|
partSizeBytes: number;
|
|
75
75
|
signal?: AbortSignal;
|
|
76
76
|
onProgress?: ProgressHandler;
|
|
77
77
|
},
|
|
78
|
-
): Promise<
|
|
78
|
+
): Promise<CompletedUploadPart[]> {
|
|
79
79
|
const reader = createSizedStreamReader(options.stream);
|
|
80
|
-
const completed:
|
|
80
|
+
const completed: CompletedUploadPart[] = [];
|
|
81
81
|
let transferredBytes = 0;
|
|
82
82
|
|
|
83
83
|
try {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
EdgeStoreApiError,
|
|
9
9
|
EdgeStoreError,
|
|
10
10
|
EdgeStoreNetworkError,
|
|
11
|
+
EdgeStoreTimeoutError,
|
|
11
12
|
} from '../errors';
|
|
12
13
|
import type { paths } from '../generated/api-v2';
|
|
13
14
|
import {
|
|
@@ -55,18 +56,26 @@ export function createTransport(options: TransportOptions): Transport {
|
|
|
55
56
|
}
|
|
56
57
|
const client = createClient<paths>({
|
|
57
58
|
baseUrl: normalizeApiUrl(options.apiUrl ?? DEFAULT_API_URL),
|
|
58
|
-
fetch: (request) =>
|
|
59
|
-
fetch(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
fetch: async (request) => {
|
|
60
|
+
if (controlTimeoutMs === 0) return await fetch(request);
|
|
61
|
+
|
|
62
|
+
const timeoutSignal = AbortSignal.timeout(controlTimeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
return await fetch(
|
|
65
|
+
new Request(request, {
|
|
66
|
+
signal: AbortSignal.any([request.signal, timeoutSignal]),
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (timeoutSignal.aborted && !request.signal.aborted) {
|
|
71
|
+
throw new EdgeStoreTimeoutError(
|
|
72
|
+
`The EdgeStore API request timed out after ${controlTimeoutMs}ms.`,
|
|
73
|
+
{ cause: error },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
70
79
|
headers: {
|
|
71
80
|
authorization,
|
|
72
81
|
'user-agent': `${EDGE_STORE_PACKAGE_NAME}/${EDGE_STORE_PACKAGE_VERSION}`,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EdgeStoreUploadCleanupError,
|
|
3
|
+
EdgeStoreUploadProcessingTimeoutError,
|
|
4
|
+
} from '../errors';
|
|
5
|
+
import type { MultipartUploadPlan } from '../multipartPlan';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_MULTIPART_CONCURRENCY,
|
|
8
|
+
type UploadProgress,
|
|
9
|
+
} from '../uploadTypes';
|
|
10
|
+
import {
|
|
11
|
+
uploadParts,
|
|
12
|
+
uploadStreamParts,
|
|
13
|
+
type CompletedUploadPart,
|
|
14
|
+
type SignedUploadPart,
|
|
15
|
+
} from './multipartUpload';
|
|
16
|
+
import type { Transport } from './transport';
|
|
17
|
+
import { waitForUploadProcessing } from './uploadProcessing';
|
|
18
|
+
import {
|
|
19
|
+
reportUploadProgress,
|
|
20
|
+
type PreparedUploadSource,
|
|
21
|
+
} from './uploadSource';
|
|
22
|
+
import { putWithRetry } from './uploadTransfer';
|
|
23
|
+
import { getPositiveInteger } from './uploadValidation';
|
|
24
|
+
|
|
25
|
+
type RequestedUpload = {
|
|
26
|
+
upload:
|
|
27
|
+
| { id: string; kind: 'single'; signedUrl: string }
|
|
28
|
+
| { id: string; kind: 'multipart'; parts: SignedUploadPart[] };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type UploadState = { upload: { status: string } };
|
|
32
|
+
|
|
33
|
+
type UploadLifecycleOperations<TResult extends UploadState> = {
|
|
34
|
+
completeMultipart(input: {
|
|
35
|
+
uploadId: string;
|
|
36
|
+
parts: CompletedUploadPart[];
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
}): Promise<unknown>;
|
|
39
|
+
get(input: {
|
|
40
|
+
uploadId: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<{ data: TResult; response: Response }>;
|
|
43
|
+
cancel(input: { uploadId: string }): Promise<unknown>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export async function executeUploadLifecycle<
|
|
47
|
+
TResult extends UploadState,
|
|
48
|
+
>(options: {
|
|
49
|
+
transport: Transport;
|
|
50
|
+
requested: RequestedUpload;
|
|
51
|
+
prepared: PreparedUploadSource;
|
|
52
|
+
multipartPlan: MultipartUploadPlan | null;
|
|
53
|
+
multipartConcurrency?: number;
|
|
54
|
+
defaultMultipartConcurrency?: number;
|
|
55
|
+
processingTimeoutMs: number;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
58
|
+
operations: UploadLifecycleOperations<TResult>;
|
|
59
|
+
cleanupPolicy: 'preserve-original' | 'report-failure';
|
|
60
|
+
}): Promise<Extract<TResult, { upload: { status: 'completed' } }>> {
|
|
61
|
+
const {
|
|
62
|
+
transport,
|
|
63
|
+
requested,
|
|
64
|
+
prepared,
|
|
65
|
+
multipartPlan,
|
|
66
|
+
processingTimeoutMs,
|
|
67
|
+
signal,
|
|
68
|
+
onProgress,
|
|
69
|
+
operations,
|
|
70
|
+
} = options;
|
|
71
|
+
const uploadId = requested.upload.id;
|
|
72
|
+
const totalBytes = prepared.sizeBytes;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
reportUploadProgress(onProgress, {
|
|
76
|
+
transferredBytes: 0,
|
|
77
|
+
totalBytes,
|
|
78
|
+
phase: 'uploading',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (prepared.kind === 'body' && requested.upload.kind === 'single') {
|
|
82
|
+
await putWithRetry(transport, {
|
|
83
|
+
uploadId,
|
|
84
|
+
url: requested.upload.signedUrl,
|
|
85
|
+
body: prepared.body,
|
|
86
|
+
signal,
|
|
87
|
+
});
|
|
88
|
+
reportUploadProgress(onProgress, {
|
|
89
|
+
transferredBytes: totalBytes,
|
|
90
|
+
totalBytes,
|
|
91
|
+
phase: 'uploading',
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
if (requested.upload.kind !== 'multipart' || !multipartPlan) {
|
|
95
|
+
throw new TypeError(
|
|
96
|
+
'The API returned an upload plan that does not match the request.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const transfer = {
|
|
100
|
+
uploadId,
|
|
101
|
+
parts: requested.upload.parts,
|
|
102
|
+
partSizeBytes: multipartPlan.partSizeBytes,
|
|
103
|
+
signal,
|
|
104
|
+
onProgress,
|
|
105
|
+
};
|
|
106
|
+
const parts =
|
|
107
|
+
prepared.kind === 'stream'
|
|
108
|
+
? await uploadStreamParts(transport, {
|
|
109
|
+
...transfer,
|
|
110
|
+
stream: prepared.stream,
|
|
111
|
+
totalBytes,
|
|
112
|
+
})
|
|
113
|
+
: await uploadParts(transport, {
|
|
114
|
+
...transfer,
|
|
115
|
+
body: prepared.body,
|
|
116
|
+
concurrency: getPositiveInteger(
|
|
117
|
+
options.multipartConcurrency,
|
|
118
|
+
options.defaultMultipartConcurrency ??
|
|
119
|
+
DEFAULT_MULTIPART_CONCURRENCY,
|
|
120
|
+
'multipart.concurrency',
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
await operations.completeMultipart({ uploadId, parts, signal });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
reportUploadProgress(onProgress, {
|
|
127
|
+
transferredBytes: totalBytes,
|
|
128
|
+
totalBytes,
|
|
129
|
+
phase: 'processing',
|
|
130
|
+
});
|
|
131
|
+
return await waitForUploadProcessing<TResult>({
|
|
132
|
+
uploadId,
|
|
133
|
+
signal,
|
|
134
|
+
timeoutMs: processingTimeoutMs,
|
|
135
|
+
get: (requestSignal) =>
|
|
136
|
+
operations.get({ uploadId, signal: requestSignal }),
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof EdgeStoreUploadProcessingTimeoutError) throw error;
|
|
140
|
+
try {
|
|
141
|
+
await operations.cancel({ uploadId });
|
|
142
|
+
} catch (cleanupError) {
|
|
143
|
+
if (options.cleanupPolicy === 'report-failure') {
|
|
144
|
+
throw new EdgeStoreUploadCleanupError({
|
|
145
|
+
message: `Automatic cancellation of upload ${uploadId} failed.`,
|
|
146
|
+
uploadId,
|
|
147
|
+
uploadCause: error,
|
|
148
|
+
cleanupCause: cleanupError,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EdgeStoreUploadCanceledError,
|
|
3
|
+
EdgeStoreUploadProcessingTimeoutError,
|
|
4
|
+
} from '../errors';
|
|
5
|
+
import { getRetryAfterMs, retryOperation, sleep } from './uploadRetry';
|
|
6
|
+
|
|
7
|
+
type UploadState = { upload: { status: string } };
|
|
8
|
+
|
|
9
|
+
export async function waitForUploadProcessing<
|
|
10
|
+
TResult extends UploadState,
|
|
11
|
+
>(options: {
|
|
12
|
+
uploadId: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
get(signal?: AbortSignal): Promise<{ data: TResult; response: Response }>;
|
|
16
|
+
}): Promise<Extract<TResult, { upload: { status: 'completed' } }>> {
|
|
17
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
18
|
+
const timeoutError = () =>
|
|
19
|
+
new EdgeStoreUploadProcessingTimeoutError(
|
|
20
|
+
'Timed out while EdgeStore was processing the upload.',
|
|
21
|
+
options.uploadId,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
const { data, response } = await retryOperation(
|
|
26
|
+
(signal) => options.get(signal),
|
|
27
|
+
{
|
|
28
|
+
signal: options.signal,
|
|
29
|
+
deadline,
|
|
30
|
+
timeoutError,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (data.upload.status === 'completed') {
|
|
35
|
+
return data as Extract<TResult, { upload: { status: 'completed' } }>;
|
|
36
|
+
}
|
|
37
|
+
if (data.upload.status === 'canceled') {
|
|
38
|
+
throw new EdgeStoreUploadCanceledError(
|
|
39
|
+
'The EdgeStore upload was canceled.',
|
|
40
|
+
options.uploadId,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const delayMs = getRetryAfterMs(response) ?? 1000;
|
|
45
|
+
if (Date.now() + delayMs > deadline) throw timeoutError();
|
|
46
|
+
await sleep(delayMs, options.signal);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UploadMetadataValue,
|
|
3
|
+
UploadProgress,
|
|
4
|
+
UploadSource,
|
|
5
|
+
UploadStreamSource,
|
|
6
|
+
} from '../uploadTypes';
|
|
7
|
+
|
|
8
|
+
type PreparedUploadDetails = {
|
|
9
|
+
sizeBytes: number;
|
|
10
|
+
fileName?: string;
|
|
11
|
+
mimeType?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type PreparedUploadSource =
|
|
15
|
+
| (PreparedUploadDetails & { kind: 'body'; body: Blob })
|
|
16
|
+
| (PreparedUploadDetails & {
|
|
17
|
+
kind: 'stream';
|
|
18
|
+
stream: ReadableStream<Uint8Array>;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function prepareUploadSource(
|
|
22
|
+
source: UploadSource,
|
|
23
|
+
): PreparedUploadSource {
|
|
24
|
+
if (typeof source === 'string') {
|
|
25
|
+
const body = new Blob([source], { type: 'text/plain' });
|
|
26
|
+
return {
|
|
27
|
+
kind: 'body',
|
|
28
|
+
body,
|
|
29
|
+
sizeBytes: body.size,
|
|
30
|
+
mimeType: body.type,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (isStreamSource(source)) {
|
|
34
|
+
if (!Number.isSafeInteger(source.sizeBytes) || source.sizeBytes < 0) {
|
|
35
|
+
throw new RangeError('source.sizeBytes must be a non-negative integer.');
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
kind: 'stream',
|
|
39
|
+
stream: source.stream,
|
|
40
|
+
sizeBytes: source.sizeBytes,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (source instanceof Blob) {
|
|
44
|
+
return {
|
|
45
|
+
kind: 'body',
|
|
46
|
+
body: source,
|
|
47
|
+
sizeBytes: source.size,
|
|
48
|
+
fileName:
|
|
49
|
+
'name' in source && typeof source.name === 'string'
|
|
50
|
+
? source.name
|
|
51
|
+
: undefined,
|
|
52
|
+
mimeType: source.type || undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (source instanceof ArrayBuffer) {
|
|
56
|
+
const body = new Blob([source]);
|
|
57
|
+
return { kind: 'body', body, sizeBytes: body.size };
|
|
58
|
+
}
|
|
59
|
+
const body = new Blob([
|
|
60
|
+
Uint8Array.from(
|
|
61
|
+
new Uint8Array(source.buffer, source.byteOffset, source.byteLength),
|
|
62
|
+
),
|
|
63
|
+
]);
|
|
64
|
+
return { kind: 'body', body, sizeBytes: body.size };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeUploadMetadata(
|
|
68
|
+
metadata?: Record<string, UploadMetadataValue>,
|
|
69
|
+
): Record<string, string> | undefined {
|
|
70
|
+
if (!metadata) return undefined;
|
|
71
|
+
const entries = Object.entries(metadata).flatMap(([key, value]) =>
|
|
72
|
+
value === null || value === undefined ? [] : [[key, String(value)]],
|
|
73
|
+
);
|
|
74
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function reportUploadProgress(
|
|
78
|
+
onProgress: ((progress: UploadProgress) => void) | undefined,
|
|
79
|
+
progress: Omit<UploadProgress, 'percentage'>,
|
|
80
|
+
): void {
|
|
81
|
+
const { transferredBytes, totalBytes, phase } = progress;
|
|
82
|
+
onProgress?.({
|
|
83
|
+
...progress,
|
|
84
|
+
percentage:
|
|
85
|
+
totalBytes === 0
|
|
86
|
+
? phase === 'preparing'
|
|
87
|
+
? 0
|
|
88
|
+
: 100
|
|
89
|
+
: Math.round((transferredBytes / totalBytes) * 10_000) / 100,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isStreamSource(source: UploadSource): source is UploadStreamSource {
|
|
94
|
+
const stream =
|
|
95
|
+
typeof source === 'object' && source !== null && 'stream' in source
|
|
96
|
+
? Reflect.get(source, 'stream')
|
|
97
|
+
: undefined;
|
|
98
|
+
return (
|
|
99
|
+
typeof stream === 'object' &&
|
|
100
|
+
stream !== null &&
|
|
101
|
+
'getReader' in stream &&
|
|
102
|
+
typeof Reflect.get(stream, 'getReader') === 'function'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -79,7 +79,8 @@ export async function putWithRetry(
|
|
|
79
79
|
isRetryable: (error) =>
|
|
80
80
|
error instanceof SignedUploadResponseError
|
|
81
81
|
? isRetryableStatus(error.status)
|
|
82
|
-
: !(error
|
|
82
|
+
: !findBlobReadError(error) &&
|
|
83
|
+
!(error instanceof EdgeStoreUploadError),
|
|
83
84
|
getRetryDelayMs: (error) =>
|
|
84
85
|
error instanceof SignedUploadResponseError
|
|
85
86
|
? error.retryAfterMs
|
|
@@ -89,6 +90,8 @@ export async function putWithRetry(
|
|
|
89
90
|
} catch (error) {
|
|
90
91
|
if (error instanceof EdgeStoreAbortError) throw error;
|
|
91
92
|
if (error instanceof EdgeStoreUploadError) throw error;
|
|
93
|
+
const blobReadError = findBlobReadError(error);
|
|
94
|
+
if (blobReadError) throw blobReadError;
|
|
92
95
|
if (error instanceof SignedUploadResponseError) {
|
|
93
96
|
throw new EdgeStoreUploadError(error.message, options.uploadId);
|
|
94
97
|
}
|
|
@@ -99,6 +102,22 @@ export async function putWithRetry(
|
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
function findBlobReadError(error: unknown): DOMException | undefined {
|
|
106
|
+
const seen = new Set<unknown>();
|
|
107
|
+
let current = error;
|
|
108
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
109
|
+
if (
|
|
110
|
+
current instanceof DOMException &&
|
|
111
|
+
current.name === 'NotReadableError'
|
|
112
|
+
) {
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
seen.add(current);
|
|
116
|
+
current = current.cause;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
async function cancelResponseBody(
|
|
103
122
|
body: ReadableStream<Uint8Array> | null,
|
|
104
123
|
): Promise<void> {
|
package/src/managementClient.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createManagementResourceClient,
|
|
9
9
|
type ManagementResourceClient,
|
|
10
10
|
} from './managementResources';
|
|
11
|
+
import type { UploadDefaults } from './uploadTypes';
|
|
11
12
|
|
|
12
13
|
/** Complete administrative client available to Bearer credentials. */
|
|
13
14
|
export type ManagementClient = ManagementResourceClient &
|
|
@@ -19,9 +20,12 @@ export type ManagementClient = ManagementResourceClient &
|
|
|
19
20
|
}): Promise<OperationResult<'v2.whoami'>>;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
export function createManagementClient(
|
|
23
|
+
export function createManagementClient(
|
|
24
|
+
transport: Transport,
|
|
25
|
+
uploadDefaults?: UploadDefaults,
|
|
26
|
+
): ManagementClient {
|
|
23
27
|
return {
|
|
24
|
-
...createManagementResourceClient(transport),
|
|
28
|
+
...createManagementResourceClient(transport, uploadDefaults),
|
|
25
29
|
...createManagementAccessClient(transport),
|
|
26
30
|
whoami: (options) =>
|
|
27
31
|
transport.execute((client) =>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createManagementUploadOperations } from './internal/managementUploadOperations';
|
|
1
2
|
import type {
|
|
2
3
|
OperationBody,
|
|
3
4
|
OperationId,
|
|
@@ -5,6 +6,12 @@ import type {
|
|
|
5
6
|
OperationResult,
|
|
6
7
|
} from './internal/operationTypes';
|
|
7
8
|
import type { Transport } from './internal/transport';
|
|
9
|
+
import {
|
|
10
|
+
uploadManagementFile,
|
|
11
|
+
type ManagementUploadInput,
|
|
12
|
+
type ManagementUploadResult,
|
|
13
|
+
} from './managementUpload';
|
|
14
|
+
import type { UploadDefaults } from './uploadTypes';
|
|
8
15
|
|
|
9
16
|
type CallOptions = {
|
|
10
17
|
/** Cancels the request. */
|
|
@@ -126,6 +133,8 @@ export type ManagementResourceClient = {
|
|
|
126
133
|
): Promise<Result<'v2.management.files.delete'>>;
|
|
127
134
|
};
|
|
128
135
|
uploads: {
|
|
136
|
+
/** Transfers a file and waits for server-side processing to complete. */
|
|
137
|
+
upload(input: ManagementUploadInput): Promise<ManagementUploadResult>;
|
|
129
138
|
/** Requests signed upload destination(s) without transferring data. */
|
|
130
139
|
request(
|
|
131
140
|
input: BucketInput &
|
|
@@ -157,7 +166,9 @@ export type ManagementResourceClient = {
|
|
|
157
166
|
|
|
158
167
|
export function createManagementResourceClient(
|
|
159
168
|
transport: Transport,
|
|
169
|
+
uploadDefaults: UploadDefaults = {},
|
|
160
170
|
): ManagementResourceClient {
|
|
171
|
+
const uploadOperations = createManagementUploadOperations(transport);
|
|
161
172
|
return {
|
|
162
173
|
projects: {
|
|
163
174
|
list: ({ account, signal }) =>
|
|
@@ -323,58 +334,17 @@ export function createManagementResourceClient(
|
|
|
323
334
|
),
|
|
324
335
|
},
|
|
325
336
|
uploads: {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
params: {
|
|
332
|
-
path: { projectRef: project, bucketName: bucket },
|
|
333
|
-
},
|
|
334
|
-
body,
|
|
335
|
-
signal,
|
|
336
|
-
},
|
|
337
|
-
),
|
|
338
|
-
),
|
|
339
|
-
get: ({ project, uploadId, signal }) =>
|
|
340
|
-
transport.execute((client) =>
|
|
341
|
-
client.GET('/management/projects/{projectRef}/uploads/{uploadId}', {
|
|
342
|
-
params: { path: { projectRef: project, uploadId } },
|
|
343
|
-
signal,
|
|
344
|
-
}),
|
|
345
|
-
),
|
|
346
|
-
cancel: ({ project, uploadId, signal }) =>
|
|
347
|
-
transport.execute((client) =>
|
|
348
|
-
client.DELETE(
|
|
349
|
-
'/management/projects/{projectRef}/uploads/{uploadId}',
|
|
350
|
-
{
|
|
351
|
-
params: { path: { projectRef: project, uploadId } },
|
|
352
|
-
signal,
|
|
353
|
-
},
|
|
354
|
-
),
|
|
355
|
-
),
|
|
356
|
-
createParts: ({ project, uploadId, signal, ...body }) =>
|
|
357
|
-
transport.execute((client) =>
|
|
358
|
-
client.POST(
|
|
359
|
-
'/management/projects/{projectRef}/uploads/{uploadId}/parts',
|
|
360
|
-
{
|
|
361
|
-
params: { path: { projectRef: project, uploadId } },
|
|
362
|
-
body,
|
|
363
|
-
signal,
|
|
364
|
-
},
|
|
365
|
-
),
|
|
366
|
-
),
|
|
367
|
-
completeMultipart: ({ project, uploadId, signal, ...body }) =>
|
|
368
|
-
transport.execute((client) =>
|
|
369
|
-
client.POST(
|
|
370
|
-
'/management/projects/{projectRef}/uploads/{uploadId}/complete',
|
|
371
|
-
{
|
|
372
|
-
params: { path: { projectRef: project, uploadId } },
|
|
373
|
-
body,
|
|
374
|
-
signal,
|
|
375
|
-
},
|
|
376
|
-
),
|
|
337
|
+
upload: (input) =>
|
|
338
|
+
uploadManagementFile(
|
|
339
|
+
{ transport, operations: uploadOperations },
|
|
340
|
+
input,
|
|
341
|
+
uploadDefaults,
|
|
377
342
|
),
|
|
343
|
+
request: uploadOperations.request,
|
|
344
|
+
get: uploadOperations.get,
|
|
345
|
+
cancel: uploadOperations.cancel,
|
|
346
|
+
createParts: uploadOperations.createParts,
|
|
347
|
+
completeMultipart: uploadOperations.completeMultipart,
|
|
378
348
|
},
|
|
379
349
|
};
|
|
380
350
|
}
|