@milaboratories/pl-drivers 1.10.9 → 1.10.11
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/clients/download.cjs +1 -0
- package/dist/clients/download.cjs.map +1 -1
- package/dist/clients/download.d.ts +4 -3
- package/dist/clients/download.d.ts.map +1 -1
- package/dist/clients/download.js +1 -0
- package/dist/clients/download.js.map +1 -1
- package/dist/drivers/download_blob/download_blob.cjs +38 -12
- package/dist/drivers/download_blob/download_blob.cjs.map +1 -1
- package/dist/drivers/download_blob/download_blob.d.ts +8 -1
- package/dist/drivers/download_blob/download_blob.d.ts.map +1 -1
- package/dist/drivers/download_blob/download_blob.js +39 -13
- package/dist/drivers/download_blob/download_blob.js.map +1 -1
- package/dist/drivers/download_blob/sparse_cache/ranges.d.ts.map +1 -1
- package/dist/drivers/helpers/helpers.cjs.map +1 -1
- package/dist/drivers/helpers/helpers.d.ts.map +1 -1
- package/dist/drivers/helpers/helpers.js.map +1 -1
- package/dist/drivers/helpers/read_file.cjs +19 -11
- package/dist/drivers/helpers/read_file.cjs.map +1 -1
- package/dist/drivers/helpers/read_file.d.ts +6 -1
- package/dist/drivers/helpers/read_file.d.ts.map +1 -1
- package/dist/drivers/helpers/read_file.js +18 -11
- package/dist/drivers/helpers/read_file.js.map +1 -1
- package/dist/helpers/download.cjs +3 -0
- package/dist/helpers/download.cjs.map +1 -1
- package/dist/helpers/download.d.ts +2 -6
- package/dist/helpers/download.d.ts.map +1 -1
- package/dist/helpers/download.js +3 -0
- package/dist/helpers/download.js.map +1 -1
- package/package.json +8 -8
- package/src/clients/download.ts +5 -4
- package/src/drivers/download_blob/download_blob.test.ts +4 -4
- package/src/drivers/download_blob/download_blob.ts +68 -19
- package/src/drivers/helpers/helpers.ts +0 -9
- package/src/drivers/helpers/read_file.ts +29 -11
- package/src/helpers/download.ts +7 -7
|
@@ -140,21 +140,21 @@ test('should get on demand blob without downloading a blob range', async () => {
|
|
|
140
140
|
const blob = await c.getValue();
|
|
141
141
|
expect(blob).toBeDefined();
|
|
142
142
|
expect(blob.size).toEqual(3);
|
|
143
|
-
const content = await driver.getContent(blob!.handle, { from: 1, to: 2 });
|
|
143
|
+
const content = await driver.getContent(blob!.handle, { range: { from: 1, to: 2 } });
|
|
144
144
|
expect(content?.toString()).toStrictEqual('2');
|
|
145
145
|
|
|
146
146
|
const c2 = driver.getOnDemandBlob(downloadable, undefined);
|
|
147
147
|
const blob2 = await c2.getValue();
|
|
148
148
|
expect(blob2).toBeDefined();
|
|
149
149
|
expect(blob2.size).toEqual(3);
|
|
150
|
-
const content2 = await driver.getContent(blob2!.handle, { from: 0, to: 1 });
|
|
150
|
+
const content2 = await driver.getContent(blob2!.handle, { range: { from: 0, to: 1 } });
|
|
151
151
|
expect(content2?.toString()).toStrictEqual('4');
|
|
152
152
|
|
|
153
153
|
const c3 = driver.getOnDemandBlob(downloadable);
|
|
154
154
|
const blob3 = await c3.getValue();
|
|
155
155
|
expect(blob3).toBeDefined();
|
|
156
156
|
expect(blob3.size).toEqual(3);
|
|
157
|
-
const content3 = await driver.getContent(blob3!.handle, { from: 1, to: 3 });
|
|
157
|
+
const content3 = await driver.getContent(blob3!.handle, { range: { from: 1, to: 3 } });
|
|
158
158
|
expect(content3?.toString()).toStrictEqual('2\n');
|
|
159
159
|
});
|
|
160
160
|
});
|
|
@@ -216,7 +216,7 @@ test('should get undefined when releasing a blob from a small cache and the blob
|
|
|
216
216
|
const blob2 = await c.getValue();
|
|
217
217
|
expect(blob2).toBeDefined();
|
|
218
218
|
expect(blob2!.size).toBe(3);
|
|
219
|
-
expect((await driver.getContent(blob2!.handle, { from: 1, to: 3 }))?.toString()).toBe('2\n');
|
|
219
|
+
expect((await driver.getContent(blob2!.handle, { range: { from: 1, to: 3 } }))?.toString()).toBe('2\n');
|
|
220
220
|
|
|
221
221
|
// The blob is removed from a cache since the size is too big.
|
|
222
222
|
c.resetState();
|
|
@@ -12,6 +12,8 @@ import { resourceIdToString, stringifyWithResourceId } from '@milaboratories/pl-
|
|
|
12
12
|
import type {
|
|
13
13
|
AnyLogHandle,
|
|
14
14
|
BlobDriver,
|
|
15
|
+
ContentHandler,
|
|
16
|
+
GetContentOptions,
|
|
15
17
|
LocalBlobHandle,
|
|
16
18
|
LocalBlobHandleAndSize,
|
|
17
19
|
ReadyLogHandle,
|
|
@@ -39,11 +41,11 @@ import * as fsp from 'node:fs/promises';
|
|
|
39
41
|
import * as os from 'node:os';
|
|
40
42
|
import * as path from 'node:path';
|
|
41
43
|
import * as readline from 'node:readline/promises';
|
|
42
|
-
import { Writable } from 'node:stream';
|
|
43
44
|
import { buffer } from 'node:stream/consumers';
|
|
45
|
+
import { Readable } from 'node:stream';
|
|
44
46
|
import type { ClientDownload } from '../../clients/download';
|
|
45
47
|
import type { ClientLogs } from '../../clients/logs';
|
|
46
|
-
import {
|
|
48
|
+
import { withFileContent } from '../helpers/read_file';
|
|
47
49
|
import {
|
|
48
50
|
isLocalBlobHandle,
|
|
49
51
|
newLocalHandle,
|
|
@@ -293,13 +295,55 @@ export class DownloadDriver implements BlobDriver {
|
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
/** Gets a content of a blob by a handle. */
|
|
296
|
-
public async getContent(handle: LocalBlobHandle | RemoteBlobHandle
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
public async getContent(handle: LocalBlobHandle | RemoteBlobHandle): Promise<Uint8Array>;
|
|
299
|
+
public async getContent(
|
|
300
|
+
handle: LocalBlobHandle | RemoteBlobHandle,
|
|
301
|
+
options?: GetContentOptions,
|
|
302
|
+
): Promise<Uint8Array>;
|
|
303
|
+
/** @deprecated Use {@link getContent} with {@link GetContentOptions} instead */
|
|
304
|
+
public async getContent(
|
|
305
|
+
handle: LocalBlobHandle | RemoteBlobHandle,
|
|
306
|
+
range?: RangeBytes,
|
|
307
|
+
): Promise<Uint8Array>;
|
|
308
|
+
public async getContent(
|
|
309
|
+
handle: LocalBlobHandle | RemoteBlobHandle,
|
|
310
|
+
optionsOrRange?: GetContentOptions | RangeBytes,
|
|
311
|
+
): Promise<Uint8Array> {
|
|
312
|
+
let options: GetContentOptions = {};
|
|
313
|
+
if (typeof optionsOrRange === 'object' && optionsOrRange !== null) {
|
|
314
|
+
if ('range' in optionsOrRange) {
|
|
315
|
+
options = optionsOrRange;
|
|
316
|
+
} else {
|
|
317
|
+
const range = optionsOrRange as RangeBytes;
|
|
318
|
+
validateRangeBytes(range, `getContent`);
|
|
319
|
+
options = { range };
|
|
320
|
+
}
|
|
299
321
|
}
|
|
300
322
|
|
|
323
|
+
return await this.withContent(handle, {
|
|
324
|
+
...options,
|
|
325
|
+
handler: async (content) => {
|
|
326
|
+
const chunks: Uint8Array[] = [];
|
|
327
|
+
for await (const chunk of content) {
|
|
328
|
+
options.signal?.throwIfAborted();
|
|
329
|
+
chunks.push(chunk);
|
|
330
|
+
}
|
|
331
|
+
return Buffer.concat(chunks);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Gets a content stream of a blob by a handle and calls handler with it. */
|
|
337
|
+
public async withContent<T>(
|
|
338
|
+
handle: LocalBlobHandle | RemoteBlobHandle,
|
|
339
|
+
options: GetContentOptions & {
|
|
340
|
+
handler: ContentHandler<T>;
|
|
341
|
+
},
|
|
342
|
+
): Promise<T> {
|
|
343
|
+
const { range, signal, handler } = options;
|
|
344
|
+
|
|
301
345
|
if (isLocalBlobHandle(handle)) {
|
|
302
|
-
return await
|
|
346
|
+
return await withFileContent({ path: this.getLocalPath(handle), range, signal, handler });
|
|
303
347
|
}
|
|
304
348
|
|
|
305
349
|
if (isRemoteBlobHandle(handle)) {
|
|
@@ -307,19 +351,24 @@ export class DownloadDriver implements BlobDriver {
|
|
|
307
351
|
|
|
308
352
|
const key = blobKey(result.info.id);
|
|
309
353
|
const filePath = await this.rangesCache.get(key, range ?? { from: 0, to: result.size });
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
async (content) =>
|
|
354
|
+
signal?.throwIfAborted();
|
|
355
|
+
|
|
356
|
+
if (filePath) return await withFileContent({ path: filePath, range, signal, handler });
|
|
357
|
+
|
|
358
|
+
return await this.clientDownload.withBlobContent(
|
|
359
|
+
result.info,
|
|
360
|
+
{ signal },
|
|
361
|
+
options,
|
|
362
|
+
async (content, size) => {
|
|
363
|
+
const [handlerStream, cacheStream] = content.tee();
|
|
364
|
+
|
|
365
|
+
const handlerPromise = handler(handlerStream, size);
|
|
366
|
+
const _cachePromise = buffer(cacheStream)
|
|
367
|
+
.then((data) => this.rangesCache.set(key, range ?? { from: 0, to: result.size }, data));
|
|
368
|
+
|
|
369
|
+
return await handlerPromise;
|
|
370
|
+
}
|
|
319
371
|
);
|
|
320
|
-
await this.rangesCache.set(key, range ?? { from: 0, to: result.size }, data);
|
|
321
|
-
|
|
322
|
-
return data;
|
|
323
372
|
}
|
|
324
373
|
|
|
325
374
|
throw new Error('Malformed remote handle');
|
|
@@ -339,7 +388,7 @@ export class DownloadDriver implements BlobDriver {
|
|
|
339
388
|
|
|
340
389
|
return Computable.make((ctx) =>
|
|
341
390
|
this.getDownloadedBlob(res, ctx), {
|
|
342
|
-
postprocessValue: (v) => v ? this.getContent(v.handle, range) : undefined
|
|
391
|
+
postprocessValue: (v) => v ? this.getContent(v.handle, { range }) : undefined
|
|
343
392
|
}
|
|
344
393
|
).withStableType()
|
|
345
394
|
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BasicResourceData,
|
|
3
|
-
getField,
|
|
4
|
-
isNullResourceId,
|
|
5
|
-
PlClient,
|
|
6
|
-
ResourceId,
|
|
7
|
-
valErr,
|
|
8
|
-
} from '@milaboratories/pl-client';
|
|
9
|
-
|
|
10
1
|
/** Throws when a driver gets a resource with a wrong resource type. */
|
|
11
2
|
export class WrongResourceTypeError extends Error {
|
|
12
3
|
name = 'WrongResourceTypeError';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ConcurrencyLimitingExecutor } from '@milaboratories/ts-helpers';
|
|
2
2
|
import type { RangeBytes } from '@milaboratories/pl-model-common';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
-
import
|
|
4
|
+
import * as fsp from 'node:fs/promises';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
5
6
|
|
|
6
7
|
// Global concurrency limiter for file reads - limit to 32 parallel reads
|
|
7
8
|
const fileReadLimiter = new ConcurrencyLimitingExecutor(32);
|
|
@@ -10,20 +11,37 @@ const fileReadLimiter = new ConcurrencyLimitingExecutor(32);
|
|
|
10
11
|
* Reads file content with concurrency limiting and proper error handling.
|
|
11
12
|
* Ensures file descriptors are properly cleaned up even in error cases.
|
|
12
13
|
*/
|
|
13
|
-
export async function
|
|
14
|
+
export async function withFileContent<T>({
|
|
15
|
+
path,
|
|
16
|
+
range,
|
|
17
|
+
signal,
|
|
18
|
+
handler,
|
|
19
|
+
}: {
|
|
20
|
+
path: string;
|
|
21
|
+
range?: RangeBytes;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
handler: (content: ReadableStream, size: number) => Promise<T>;
|
|
24
|
+
}): Promise<T> {
|
|
14
25
|
return await fileReadLimiter.run(async () => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
26
|
+
const readOps = {
|
|
27
|
+
start: range?.from,
|
|
28
|
+
end: range?.to !== undefined ? range.to - 1 : undefined,
|
|
29
|
+
signal: signal,
|
|
30
|
+
};
|
|
21
31
|
let stream: fs.ReadStream | undefined;
|
|
32
|
+
let handlerSuccess = false;
|
|
33
|
+
|
|
22
34
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
const stat = await fsp.stat(path);
|
|
36
|
+
stream = fs.createReadStream(path, readOps);
|
|
37
|
+
const webStream = Readable.toWeb(stream);
|
|
38
|
+
|
|
39
|
+
const result = await handler(webStream, stat.size);
|
|
40
|
+
handlerSuccess = true;
|
|
41
|
+
return result;
|
|
25
42
|
} catch (error) {
|
|
26
|
-
|
|
43
|
+
// Cleanup on error (including handler errors)
|
|
44
|
+
if (!handlerSuccess && stream && !stream.destroyed) {
|
|
27
45
|
stream.destroy();
|
|
28
46
|
}
|
|
29
47
|
throw error;
|
package/src/helpers/download.ts
CHANGED
|
@@ -5,12 +5,7 @@ import { request } from 'undici';
|
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import type { ReadableStream } from 'node:stream/web';
|
|
7
7
|
import { text } from 'node:stream/consumers';
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
export interface DownloadOps {
|
|
11
|
-
signal?: AbortSignal;
|
|
12
|
-
range?: RangeBytes;
|
|
13
|
-
}
|
|
8
|
+
import type { GetContentOptions } from '@milaboratories/pl-model-common';
|
|
14
9
|
|
|
15
10
|
export type ContentHandler<T> = (content: ReadableStream, size: number) => Promise<T>;
|
|
16
11
|
|
|
@@ -25,7 +20,7 @@ export class RemoteFileDownloader {
|
|
|
25
20
|
async withContent<T>(
|
|
26
21
|
url: string,
|
|
27
22
|
reqHeaders: Record<string, string>,
|
|
28
|
-
ops:
|
|
23
|
+
ops: GetContentOptions,
|
|
29
24
|
handler: ContentHandler<T>,
|
|
30
25
|
): Promise<T> {
|
|
31
26
|
const headers = { ...reqHeaders };
|
|
@@ -40,14 +35,19 @@ export class RemoteFileDownloader {
|
|
|
40
35
|
headers,
|
|
41
36
|
signal: ops.signal,
|
|
42
37
|
});
|
|
38
|
+
ops.signal?.throwIfAborted();
|
|
43
39
|
|
|
44
40
|
const webBody = Readable.toWeb(body);
|
|
45
41
|
let handlerSuccess = false;
|
|
46
42
|
|
|
47
43
|
try {
|
|
48
44
|
await checkStatusCodeOk(statusCode, webBody, url);
|
|
45
|
+
ops.signal?.throwIfAborted();
|
|
46
|
+
|
|
49
47
|
const size = Number(responseHeaders['content-length']);
|
|
50
48
|
const result = await handler(webBody, size);
|
|
49
|
+
ops.signal?.throwIfAborted();
|
|
50
|
+
|
|
51
51
|
handlerSuccess = true;
|
|
52
52
|
return result;
|
|
53
53
|
} catch (error) {
|