@milaboratories/pl-drivers 1.10.9 → 1.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/clients/download.cjs +1 -0
  2. package/dist/clients/download.cjs.map +1 -1
  3. package/dist/clients/download.d.ts +4 -3
  4. package/dist/clients/download.d.ts.map +1 -1
  5. package/dist/clients/download.js +1 -0
  6. package/dist/clients/download.js.map +1 -1
  7. package/dist/drivers/download_blob/download_blob.cjs +38 -12
  8. package/dist/drivers/download_blob/download_blob.cjs.map +1 -1
  9. package/dist/drivers/download_blob/download_blob.d.ts +8 -1
  10. package/dist/drivers/download_blob/download_blob.d.ts.map +1 -1
  11. package/dist/drivers/download_blob/download_blob.js +39 -13
  12. package/dist/drivers/download_blob/download_blob.js.map +1 -1
  13. package/dist/drivers/download_blob/sparse_cache/ranges.d.ts.map +1 -1
  14. package/dist/drivers/helpers/helpers.cjs.map +1 -1
  15. package/dist/drivers/helpers/helpers.d.ts.map +1 -1
  16. package/dist/drivers/helpers/helpers.js.map +1 -1
  17. package/dist/drivers/helpers/read_file.cjs +19 -11
  18. package/dist/drivers/helpers/read_file.cjs.map +1 -1
  19. package/dist/drivers/helpers/read_file.d.ts +6 -1
  20. package/dist/drivers/helpers/read_file.d.ts.map +1 -1
  21. package/dist/drivers/helpers/read_file.js +18 -11
  22. package/dist/drivers/helpers/read_file.js.map +1 -1
  23. package/dist/helpers/download.cjs +3 -0
  24. package/dist/helpers/download.cjs.map +1 -1
  25. package/dist/helpers/download.d.ts +2 -6
  26. package/dist/helpers/download.d.ts.map +1 -1
  27. package/dist/helpers/download.js +3 -0
  28. package/dist/helpers/download.js.map +1 -1
  29. package/package.json +8 -8
  30. package/src/clients/download.ts +5 -4
  31. package/src/drivers/download_blob/download_blob.ts +68 -19
  32. package/src/drivers/helpers/helpers.ts +0 -9
  33. package/src/drivers/helpers/read_file.ts +29 -11
  34. package/src/helpers/download.ts +7 -7
@@ -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 { readFileContent } from '../helpers/read_file';
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, range?: RangeBytes): Promise<Uint8Array> {
297
- if (range) {
298
- validateRangeBytes(range, `getContent`);
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 readFileContent(this.getLocalPath(handle), range);
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
- if (filePath) {
311
- return await readFileContent(filePath, range);
312
- }
313
-
314
- const data = await this.clientDownload.withBlobContent(
315
- { id: result.info.id, type: result.info.type },
316
- undefined,
317
- { range },
318
- async (content) => await buffer(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 { buffer } from 'node:stream/consumers';
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 readFileContent(path: string, range?: RangeBytes): Promise<Uint8Array> {
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 ops: { start?: number; end?: number } = {};
16
- if (range) {
17
- ops.start = range.from;
18
- ops.end = range.to - 1;
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
- stream = fs.createReadStream(path, ops);
24
- return await buffer(stream);
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
- if (stream && !stream.destroyed) {
43
+ // Cleanup on error (including handler errors)
44
+ if (!handlerSuccess && stream && !stream.destroyed) {
27
45
  stream.destroy();
28
46
  }
29
47
  throw error;
@@ -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 { RangeBytes } from '@milaboratories/pl-model-common';
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: DownloadOps,
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) {