@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.
Files changed (35) 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.test.ts +4 -4
  32. package/src/drivers/download_blob/download_blob.ts +68 -19
  33. package/src/drivers/helpers/helpers.ts +0 -9
  34. package/src/drivers/helpers/read_file.ts +29 -11
  35. 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 { 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) {