@eigenpal/sdk 0.16.3 → 0.16.4
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/client.ts +47 -9
- package/src/generated/index.ts +2 -2
- package/src/generated/sdk.gen.ts +41 -4
- package/src/generated/types.gen.ts +210 -1
- package/src/index.ts +7 -1
- package/src/lib/fetch-body.ts +92 -0
- package/src/lib/files.ts +154 -8
- package/src/lib/upload-presigned-multipart.ts +285 -0
- package/src/resources/files.ts +345 -26
- package/src/telemetry.ts +1 -1
package/src/resources/files.ts
CHANGED
|
@@ -7,7 +7,37 @@ import {
|
|
|
7
7
|
filesUploadsAbort,
|
|
8
8
|
filesUploadsComplete,
|
|
9
9
|
filesUploadsCreate,
|
|
10
|
+
filesUploadsGet,
|
|
11
|
+
filesUploadsPartsList,
|
|
12
|
+
filesUploadsPartsPresign,
|
|
10
13
|
} from '../generated/sdk.gen';
|
|
14
|
+
import {
|
|
15
|
+
byteViewToArrayBuffer,
|
|
16
|
+
destroyUnreadNodeReadable,
|
|
17
|
+
ensureNodeReadableFullySent,
|
|
18
|
+
isNodeReadableStream,
|
|
19
|
+
toPutBody,
|
|
20
|
+
type StreamableRequestInit,
|
|
21
|
+
} from '../lib/fetch-body';
|
|
22
|
+
import {
|
|
23
|
+
isFileDescriptor,
|
|
24
|
+
isFileInput,
|
|
25
|
+
isPathFileInput,
|
|
26
|
+
isStreamFactoryFileInput,
|
|
27
|
+
resolveFileBlob,
|
|
28
|
+
statUploadSize,
|
|
29
|
+
type FileInput,
|
|
30
|
+
} from '../lib/files';
|
|
31
|
+
import {
|
|
32
|
+
annotateMultipartCompleteFailure,
|
|
33
|
+
annotatePresignedPutCompleteFailure,
|
|
34
|
+
filterStorageHeaders,
|
|
35
|
+
PartUploadHttpError,
|
|
36
|
+
shouldAbortMultipartUploadSession,
|
|
37
|
+
shouldAbortPresignedPutUploadSession,
|
|
38
|
+
uploadPresignedMultipartParts,
|
|
39
|
+
type ListedUploadPart,
|
|
40
|
+
} from '../lib/upload-presigned-multipart';
|
|
11
41
|
|
|
12
42
|
type Dispatch = <T>(
|
|
13
43
|
call: () => Promise<OperationResult<T>>,
|
|
@@ -54,58 +84,136 @@ function newIdempotencyKey(): string {
|
|
|
54
84
|
return `upload_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
55
85
|
}
|
|
56
86
|
|
|
87
|
+
type UploadableFile = FileInput | Uint8Array | ArrayBuffer;
|
|
88
|
+
|
|
57
89
|
export class FilesResource {
|
|
58
90
|
constructor(
|
|
59
91
|
private readonly client: Client,
|
|
60
92
|
private readonly dispatch: Dispatch
|
|
61
93
|
) {}
|
|
62
94
|
|
|
63
|
-
async upload(file:
|
|
64
|
-
const
|
|
65
|
-
options.filename ??
|
|
66
|
-
(typeof File !== 'undefined' && file instanceof File ? file.name : undefined);
|
|
67
|
-
if (!filename) throw new Error('filename is required when uploading a Blob');
|
|
95
|
+
async upload(file: UploadableFile, options: UploadOptions = {}): Promise<AnyResponse> {
|
|
96
|
+
const source = await resolveUploadableSource(file, options.filename);
|
|
68
97
|
const idempotencyKey = options.idempotencyKey ?? newIdempotencyKey();
|
|
69
98
|
const negotiation = await this.createUpload(
|
|
70
99
|
{
|
|
71
|
-
filename,
|
|
72
|
-
contentType:
|
|
73
|
-
size:
|
|
100
|
+
filename: source.filename,
|
|
101
|
+
contentType: source.contentType,
|
|
102
|
+
size: source.size,
|
|
74
103
|
idempotencyKey,
|
|
75
104
|
...(options.purpose ? { purpose: options.purpose } : {}),
|
|
76
105
|
},
|
|
77
106
|
options
|
|
78
107
|
);
|
|
79
108
|
|
|
80
|
-
if (negotiation.transport === 'presigned-
|
|
109
|
+
if (negotiation.transport === 'presigned-multipart') {
|
|
110
|
+
if (!source.replayable) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'Cannot resume a multipart upload from a one-shot stream. Pass a file path, Blob, or a reusable stream factory.'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
let partsReady = false;
|
|
81
116
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
([name]) => name.toLowerCase() !== 'content-length'
|
|
87
|
-
)
|
|
88
|
-
),
|
|
89
|
-
body: file,
|
|
117
|
+
await uploadPresignedMultipartParts({
|
|
118
|
+
partCount: negotiation.partCount,
|
|
119
|
+
partSizeBytes: negotiation.partSizeBytes,
|
|
120
|
+
totalSize: source.size,
|
|
90
121
|
signal: options.signal,
|
|
122
|
+
onProgress: options.onProgress,
|
|
123
|
+
listParts: async () => {
|
|
124
|
+
const listed = await this.dispatch(
|
|
125
|
+
() =>
|
|
126
|
+
this.client.get({
|
|
127
|
+
url: negotiation.partsUrl,
|
|
128
|
+
signal: options.signal,
|
|
129
|
+
}) as Promise<OperationResult<{ parts?: ListedUploadPart[] }>>
|
|
130
|
+
);
|
|
131
|
+
return listed.parts ?? [];
|
|
132
|
+
},
|
|
133
|
+
presignPart: async (partNumber) => {
|
|
134
|
+
return this.dispatch(
|
|
135
|
+
() =>
|
|
136
|
+
this.client.post({
|
|
137
|
+
url: negotiation.partsUrl,
|
|
138
|
+
body: { partNumber } as never,
|
|
139
|
+
signal: options.signal,
|
|
140
|
+
}) as Promise<
|
|
141
|
+
OperationResult<{
|
|
142
|
+
url: string;
|
|
143
|
+
headers?: Record<string, string>;
|
|
144
|
+
partSizeBytes: number;
|
|
145
|
+
}>
|
|
146
|
+
>
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
putPart: async ({ url, headers, start, length }) => {
|
|
150
|
+
const body = await source.openPart(start, length);
|
|
151
|
+
const response = await putStorage(
|
|
152
|
+
url,
|
|
153
|
+
headers,
|
|
154
|
+
body,
|
|
155
|
+
options.signal,
|
|
156
|
+
source.stripContentLength,
|
|
157
|
+
length
|
|
158
|
+
);
|
|
159
|
+
if (!response.ok) throw new PartUploadHttpError(response.status);
|
|
160
|
+
},
|
|
91
161
|
});
|
|
162
|
+
partsReady = true;
|
|
163
|
+
return await this.dispatch(
|
|
164
|
+
() =>
|
|
165
|
+
this.client.post({
|
|
166
|
+
url: negotiation.completeUrl,
|
|
167
|
+
body: {} as never,
|
|
168
|
+
signal: options.signal,
|
|
169
|
+
}) as Promise<OperationResult<AnyResponse>>
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (shouldAbortMultipartUploadSession({ partsReady })) {
|
|
173
|
+
await this.abortUpload(negotiation.uploadId, {
|
|
174
|
+
signal: uploadAbortCleanupSignal(),
|
|
175
|
+
}).catch(() => undefined);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
throw annotateMultipartCompleteFailure(negotiation.uploadId, error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (negotiation.transport === 'presigned-put') {
|
|
183
|
+
let putReady = false;
|
|
184
|
+
try {
|
|
185
|
+
const body = await source.openPart(0, source.size);
|
|
186
|
+
const response = await putStorage(
|
|
187
|
+
negotiation.url,
|
|
188
|
+
(negotiation.headers ?? {}) as Record<string, string>,
|
|
189
|
+
body,
|
|
190
|
+
options.signal,
|
|
191
|
+
source.stripContentLength,
|
|
192
|
+
source.size
|
|
193
|
+
);
|
|
92
194
|
if (!response.ok) {
|
|
93
195
|
throw new Error(`Storage upload failed (${response.status}); retry the upload`);
|
|
94
196
|
}
|
|
95
|
-
|
|
96
|
-
|
|
197
|
+
putReady = true;
|
|
198
|
+
options.onProgress?.(source.size, source.size);
|
|
97
199
|
} catch (error) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
200
|
+
if (shouldAbortPresignedPutUploadSession({ putReady })) {
|
|
201
|
+
await this.abortUpload(negotiation.uploadId, {
|
|
202
|
+
signal: uploadAbortCleanupSignal(),
|
|
203
|
+
}).catch(() => undefined);
|
|
204
|
+
}
|
|
103
205
|
throw error;
|
|
104
206
|
}
|
|
207
|
+
try {
|
|
208
|
+
return await this.completeUpload(negotiation.uploadId, options);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw annotatePresignedPutCompleteFailure(negotiation.uploadId, error);
|
|
211
|
+
}
|
|
105
212
|
}
|
|
106
213
|
|
|
214
|
+
const blob = await source.asBlob();
|
|
107
215
|
const form = new FormData();
|
|
108
|
-
form.append('file',
|
|
216
|
+
form.append('file', blob, source.filename);
|
|
109
217
|
if (options.purpose) form.append('purpose', options.purpose);
|
|
110
218
|
const uploaded = await this.dispatch(
|
|
111
219
|
() =>
|
|
@@ -117,7 +225,7 @@ export class FilesResource {
|
|
|
117
225
|
signal: options.signal,
|
|
118
226
|
}) as Promise<OperationResult<AnyResponse>>
|
|
119
227
|
);
|
|
120
|
-
options.onProgress?.(
|
|
228
|
+
options.onProgress?.(source.size, source.size);
|
|
121
229
|
return uploaded;
|
|
122
230
|
}
|
|
123
231
|
|
|
@@ -159,6 +267,41 @@ export class FilesResource {
|
|
|
159
267
|
);
|
|
160
268
|
}
|
|
161
269
|
|
|
270
|
+
async getUpload(uploadId: string, options: SignalOptions = {}): Promise<AnyResponse> {
|
|
271
|
+
return this.dispatch(() =>
|
|
272
|
+
filesUploadsGet({
|
|
273
|
+
client: this.client,
|
|
274
|
+
path: { uploadId },
|
|
275
|
+
signal: options.signal,
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async listUploadParts(uploadId: string, options: SignalOptions = {}): Promise<AnyResponse> {
|
|
281
|
+
return this.dispatch(() =>
|
|
282
|
+
filesUploadsPartsList({
|
|
283
|
+
client: this.client,
|
|
284
|
+
path: { uploadId },
|
|
285
|
+
signal: options.signal,
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async presignUploadPart(
|
|
291
|
+
uploadId: string,
|
|
292
|
+
partNumber: number,
|
|
293
|
+
options: SignalOptions = {}
|
|
294
|
+
): Promise<AnyResponse> {
|
|
295
|
+
return this.dispatch(() =>
|
|
296
|
+
filesUploadsPartsPresign({
|
|
297
|
+
client: this.client,
|
|
298
|
+
path: { uploadId },
|
|
299
|
+
body: { partNumber },
|
|
300
|
+
signal: options.signal,
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
162
305
|
async get(fileId: string, options: SignalOptions = {}): Promise<AnyResponse> {
|
|
163
306
|
return this.dispatch(() =>
|
|
164
307
|
filesGet({ client: this.client, path: { id: fileId }, signal: options.signal })
|
|
@@ -186,3 +329,179 @@ export class FilesResource {
|
|
|
186
329
|
);
|
|
187
330
|
}
|
|
188
331
|
}
|
|
332
|
+
|
|
333
|
+
type ResolvedUploadSource = {
|
|
334
|
+
filename: string;
|
|
335
|
+
contentType: string;
|
|
336
|
+
size: number;
|
|
337
|
+
replayable: boolean;
|
|
338
|
+
stripContentLength: boolean;
|
|
339
|
+
openPart: (start: number, length: number) => Promise<BodyInit>;
|
|
340
|
+
asBlob: () => Promise<Blob>;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
async function resolveUploadableSource(
|
|
344
|
+
file: UploadableFile,
|
|
345
|
+
filenameOption?: string
|
|
346
|
+
): Promise<ResolvedUploadSource> {
|
|
347
|
+
if (file instanceof Uint8Array) {
|
|
348
|
+
const filename = filenameOption || 'file';
|
|
349
|
+
return {
|
|
350
|
+
filename,
|
|
351
|
+
contentType: 'application/octet-stream',
|
|
352
|
+
size: file.byteLength,
|
|
353
|
+
replayable: true,
|
|
354
|
+
stripContentLength: false,
|
|
355
|
+
openPart: async (start, length) =>
|
|
356
|
+
byteViewToArrayBuffer(file.subarray(start, start + length)),
|
|
357
|
+
asBlob: async () => new Blob([file as BlobPart]),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (file instanceof ArrayBuffer) {
|
|
361
|
+
const filename = filenameOption || 'file';
|
|
362
|
+
const bytes = new Uint8Array(file);
|
|
363
|
+
return {
|
|
364
|
+
filename,
|
|
365
|
+
contentType: 'application/octet-stream',
|
|
366
|
+
size: bytes.byteLength,
|
|
367
|
+
replayable: true,
|
|
368
|
+
stripContentLength: false,
|
|
369
|
+
openPart: async (start, length) =>
|
|
370
|
+
byteViewToArrayBuffer(bytes.subarray(start, start + length)),
|
|
371
|
+
asBlob: async () => new Blob([bytes as BlobPart]),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!isFileInput(file)) {
|
|
376
|
+
throw new Error('filename is required when uploading a Blob');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (typeof Blob !== 'undefined' && file instanceof Blob) {
|
|
380
|
+
const filename =
|
|
381
|
+
filenameOption ??
|
|
382
|
+
(typeof File !== 'undefined' && file instanceof File ? file.name : undefined);
|
|
383
|
+
if (!filename) throw new Error('filename is required when uploading a Blob');
|
|
384
|
+
return {
|
|
385
|
+
filename,
|
|
386
|
+
contentType: file.type || 'application/octet-stream',
|
|
387
|
+
size: file.size,
|
|
388
|
+
replayable: true,
|
|
389
|
+
stripContentLength: true,
|
|
390
|
+
openPart: async (start, length) => file.slice(start, start + length),
|
|
391
|
+
asBlob: async () => file,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (isPathFileInput(file) || hasNodePath(file)) {
|
|
396
|
+
const filePath = isPathFileInput(file) ? file.path : (file as { path: string }).path;
|
|
397
|
+
const meta = await statUploadSize(file);
|
|
398
|
+
const filename = filenameOption || meta.filename;
|
|
399
|
+
return {
|
|
400
|
+
filename,
|
|
401
|
+
contentType: meta.contentType,
|
|
402
|
+
size: meta.size,
|
|
403
|
+
replayable: true,
|
|
404
|
+
stripContentLength: false,
|
|
405
|
+
openPart: async (start, length) => {
|
|
406
|
+
if (length === 0) return byteViewToArrayBuffer(new Uint8Array());
|
|
407
|
+
const { createReadStream } = await import('node:fs');
|
|
408
|
+
return toPutBody(
|
|
409
|
+
createReadStream(filePath, {
|
|
410
|
+
start,
|
|
411
|
+
end: start + length - 1,
|
|
412
|
+
})
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
asBlob: async () => (await resolveFileBlob(file)).blob,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (isStreamFactoryFileInput(file)) {
|
|
420
|
+
return {
|
|
421
|
+
filename: filenameOption || file.filename,
|
|
422
|
+
contentType: file.mimeType || 'application/octet-stream',
|
|
423
|
+
size: file.size,
|
|
424
|
+
replayable: true,
|
|
425
|
+
stripContentLength: false,
|
|
426
|
+
openPart: async (start, length) => {
|
|
427
|
+
const part = file.open(start, length);
|
|
428
|
+
if (part instanceof Uint8Array) return byteViewToArrayBuffer(part);
|
|
429
|
+
if (isNodeReadableStream(part)) return toPutBody(part);
|
|
430
|
+
return part;
|
|
431
|
+
},
|
|
432
|
+
asBlob: async () => (await resolveFileBlob(file)).blob,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (isFileDescriptor(file)) {
|
|
437
|
+
const filename = filenameOption || file.filename;
|
|
438
|
+
const type = file.mimeType || 'application/octet-stream';
|
|
439
|
+
if (typeof Blob !== 'undefined' && file.content instanceof Blob) {
|
|
440
|
+
const blob = file.content;
|
|
441
|
+
return {
|
|
442
|
+
filename,
|
|
443
|
+
contentType: type,
|
|
444
|
+
size: blob.size,
|
|
445
|
+
replayable: true,
|
|
446
|
+
stripContentLength: true,
|
|
447
|
+
openPart: async (start, length) => blob.slice(start, start + length),
|
|
448
|
+
asBlob: async () => blob,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const bytes =
|
|
452
|
+
file.content instanceof ArrayBuffer
|
|
453
|
+
? new Uint8Array(file.content)
|
|
454
|
+
: new Uint8Array(
|
|
455
|
+
(file.content as ArrayBufferView).buffer,
|
|
456
|
+
(file.content as ArrayBufferView).byteOffset,
|
|
457
|
+
(file.content as ArrayBufferView).byteLength
|
|
458
|
+
);
|
|
459
|
+
return {
|
|
460
|
+
filename,
|
|
461
|
+
contentType: type,
|
|
462
|
+
size: bytes.byteLength,
|
|
463
|
+
replayable: true,
|
|
464
|
+
stripContentLength: false,
|
|
465
|
+
openPart: async (start, length) =>
|
|
466
|
+
byteViewToArrayBuffer(bytes.subarray(start, start + length)),
|
|
467
|
+
asBlob: async () => new Blob([bytes as BlobPart], { type }),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
throw new Error(
|
|
472
|
+
'Cannot upload a one-shot stream. Pass a file path, Blob, or a reusable stream factory.'
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function hasNodePath(file: FileInput): file is FileInput & { path: string } {
|
|
477
|
+
return typeof (file as { path?: unknown }).path === 'string';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function putStorage(
|
|
481
|
+
url: string,
|
|
482
|
+
headers: Record<string, string>,
|
|
483
|
+
body: BodyInit,
|
|
484
|
+
signal: AbortSignal | undefined,
|
|
485
|
+
stripContentLength: boolean,
|
|
486
|
+
expectedByteLength?: number
|
|
487
|
+
): Promise<Response> {
|
|
488
|
+
const stream = isNodeReadableStream(body);
|
|
489
|
+
const init: StreamableRequestInit = {
|
|
490
|
+
method: 'PUT',
|
|
491
|
+
headers: filterStorageHeaders(headers, stripContentLength && !stream),
|
|
492
|
+
body,
|
|
493
|
+
signal,
|
|
494
|
+
...(stream ? { duplex: 'half' as const } : {}),
|
|
495
|
+
};
|
|
496
|
+
let response: Response;
|
|
497
|
+
try {
|
|
498
|
+
response = await fetch(url, init);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (stream) destroyUnreadNodeReadable(body as NodeJS.ReadableStream);
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
if (stream) {
|
|
504
|
+
await ensureNodeReadableFullySent(body as NodeJS.ReadableStream, expectedByteLength);
|
|
505
|
+
}
|
|
506
|
+
return response;
|
|
507
|
+
}
|
package/src/telemetry.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
export const SDK_LANGUAGE = 'typescript';
|
|
20
20
|
// Rewritten at publish time by scripts/release/platform/render-sdk-versions.sh.
|
|
21
21
|
// Keep this string literal exactly stable — sed matches on it.
|
|
22
|
-
export const SDK_VERSION = '0.16.
|
|
22
|
+
export const SDK_VERSION = '0.16.4';
|
|
23
23
|
|
|
24
24
|
function detectRuntime(): string {
|
|
25
25
|
const g = globalThis as unknown as {
|