@milaboratories/pl-drivers 1.5.57 → 1.5.59

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.
@@ -0,0 +1,6 @@
1
+ /** A storage where we keep all the assets.
2
+ * If you run tests locally, the storage should be named `library` and point
3
+ * to the `assets` directory in this repository,
4
+ * but in CI the storage is defined in env and it points to S3 bucket. */
5
+ export declare const libraryStorage: string;
6
+ //# sourceMappingURL=test_env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_env.d.ts","sourceRoot":"","sources":["../src/test_env.ts"],"names":[],"mappings":"AAAA;;;yEAGyE;AACzE,eAAO,MAAM,cAAc,QAEd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-drivers",
3
- "version": "1.5.57",
3
+ "version": "1.5.59",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -30,11 +30,12 @@
30
30
  "tar-fs": "^3.0.8",
31
31
  "undici": "~7.5.0",
32
32
  "zod": "~3.23.8",
33
- "@milaboratories/ts-helpers": "^1.2.0",
34
- "@milaboratories/computable": "^2.4.7",
35
- "@milaboratories/pl-tree": "^1.6.1",
36
- "@milaboratories/pl-model-common": "^1.15.0",
37
- "@milaboratories/pl-client": "^2.9.0"
33
+ "upath": "^2.0.1",
34
+ "@milaboratories/ts-helpers": "^1.3.0",
35
+ "@milaboratories/computable": "^2.4.8",
36
+ "@milaboratories/pl-client": "^2.9.1",
37
+ "@milaboratories/pl-model-common": "^1.15.1",
38
+ "@milaboratories/pl-tree": "^1.6.2"
38
39
  },
39
40
  "devDependencies": {
40
41
  "eslint": "^9.25.1",
@@ -42,7 +43,8 @@
42
43
  "typescript": "~5.5.4",
43
44
  "vite": "^5.4.11",
44
45
  "@types/node": "~20.16.15",
45
- "vitest": "^2.1.8",
46
+ "vitest": "^2.1.9",
47
+ "@vitest/coverage-v8": "^2.1.9",
46
48
  "@types/tar-fs": "^2.0.4",
47
49
  "@milaboratories/eslint-config": "^1.0.4",
48
50
  "@milaboratories/platforma-build-configs": "1.0.3"
@@ -50,7 +52,7 @@
50
52
  "scripts": {
51
53
  "type-check": "tsc --noEmit --composite false",
52
54
  "build": "vite build",
53
- "test": "vitest",
55
+ "test": "vitest run --coverage",
54
56
  "lint": "eslint .",
55
57
  "do-pack": "rm -f *.tgz && pnpm pack && mv *.tgz package.tgz"
56
58
  }
@@ -41,7 +41,10 @@ export class ClientDownload {
41
41
  close() {}
42
42
 
43
43
  /** Gets a presign URL and downloads the file.
44
- * An optional range with 2 numbers from what byte and to what byte to download can be provided. */
44
+ * An optional range with 2 numbers from what byte and to what byte to download can be provided.
45
+ * @param fromBytes - from byte including this byte
46
+ * @param toBytes - to byte excluding this byte
47
+ */
45
48
  async downloadBlob(
46
49
  info: ResourceInfo,
47
50
  options?: RpcOptions,
@@ -61,14 +64,14 @@ export class ClientDownload {
61
64
 
62
65
  async readLocalFile(
63
66
  url: string,
64
- fromBytes?: number,
65
- toBytes?: number,
67
+ fromBytes?: number, // including this byte
68
+ toBytes?: number, // excluding this byte
66
69
  ): Promise<DownloadResponse> {
67
70
  const { storageId, relativePath } = parseLocalUrl(url);
68
71
  const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);
69
72
 
70
73
  return {
71
- content: Readable.toWeb(fs.createReadStream(fullPath, { start: fromBytes, end: toBytes })),
74
+ content: Readable.toWeb(fs.createReadStream(fullPath, { start: fromBytes, end: toBytes !== undefined ? toBytes - 1 : undefined })),
72
75
  size: (await fsp.stat(fullPath)).size,
73
76
  };
74
77
  }
@@ -1,11 +1,11 @@
1
1
  export function toHeadersMap(
2
2
  headers: { name: string; value: string }[],
3
- fromBytes?: number,
4
- toBytes?: number,
3
+ fromBytes?: number, // including this byte
4
+ toBytes?: number, // excluding this byte
5
5
  ): Record<string, string> {
6
6
  const result = Object.fromEntries(headers.map(({ name, value }) => [name, value]));
7
7
  if (fromBytes !== undefined && toBytes !== undefined) {
8
- result['Range'] = `bytes=${fromBytes}-${toBytes}`;
8
+ result['Range'] = `bytes=${fromBytes}-${toBytes - 1}`;
9
9
  }
10
10
 
11
11
  return result;
@@ -0,0 +1,15 @@
1
+ import { bigintToResourceId, ResourceId } from "@milaboratories/pl-client";
2
+ import * as path from 'node:path';
3
+
4
+ export function blobKey(rId: ResourceId): string {
5
+ return `${BigInt(rId)}`;
6
+ }
7
+
8
+ export function pathToKey(fPath: string): string {
9
+ return path.basename(fPath);
10
+ }
11
+
12
+ export function pathToBlobInfo(fPath: string): ResourceId | undefined {
13
+ const base = path.basename(fPath);
14
+ return bigintToResourceId(BigInt(base));
15
+ }
@@ -10,30 +10,22 @@ import {
10
10
  poll,
11
11
  TestHelpers,
12
12
  } from '@milaboratories/pl-client';
13
- import { ConsoleLoggerAdapter, HmacSha256Signer } from '@milaboratories/ts-helpers';
13
+ import { ConsoleLoggerAdapter, HmacSha256Signer, Signer } from '@milaboratories/ts-helpers';
14
14
  import * as fsp from 'node:fs/promises';
15
15
  import * as os from 'node:os';
16
16
  import * as path from 'node:path';
17
17
  import { scheduler } from 'node:timers/promises';
18
18
  import { createDownloadClient, createLogsClient } from '../../clients/constructors';
19
- import { DownloadDriver } from './download_blob';
19
+ import { DownloadDriver, DownloadDriverOps } from './download_blob';
20
20
  import type { OnDemandBlobResourceSnapshot } from '../types';
21
+ import * as env from '../../test_env';
21
22
 
22
23
  const fileName = 'answer_to_the_ultimate_question.txt';
23
24
 
24
- test('should download a blob and read its content', async () => {
25
+ test('should download a blob and read its content', { timeout: 10000 }, async () => {
25
26
  await TestHelpers.withTempRoot(async (client) => {
26
- const logger = new ConsoleLoggerAdapter();
27
- const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-1-'));
28
-
29
- const driver = new DownloadDriver(
30
- logger,
31
- createDownloadClient(logger, client, []),
32
- createLogsClient(client, logger),
33
- dir,
34
- new HmacSha256Signer(HmacSha256Signer.generateSecret()),
35
- { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 },
36
- );
27
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-driver'));
28
+ const driver = await genDriver(client, dir, genSigner(), { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 });
37
29
  const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
38
30
 
39
31
  const c = driver.getDownloadedBlob(downloadable);
@@ -53,23 +45,38 @@ test('should download a blob and read its content', async () => {
53
45
 
54
46
  console.log(`should download a blob: exiting`)
55
47
  });
56
- }, 10000);
48
+ });
57
49
 
58
- test('should not redownload a blob a file already exists', async () => {
50
+ test('should download a blob range and read its content with a range', async () => {
59
51
  await TestHelpers.withTempRoot(async (client) => {
60
- const logger = new ConsoleLoggerAdapter();
61
- const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-1-'));
52
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-driver'));
53
+ const driver = await genDriver(client, dir, genSigner(), { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 });
54
+ const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
55
+
56
+ const c = driver.getDownloadedBlob(downloadable);
57
+
58
+ console.log(`should download a blob range: getting computable first time`)
59
+ const blob = await c.getValue();
60
+ expect(blob).toBeUndefined();
61
+
62
+ console.log(`should download a blob range: awaiting change`)
63
+ await c.awaitChange();
64
+
65
+ console.log(`should download a blob range: getting the blob second time`)
66
+ const blob2 = await c.getValue();
67
+ expect(blob2).toBeDefined();
68
+ expect(blob2!.size).toBe(3);
69
+ expect((await driver.getContent(blob2!.handle, { from: 0, to: 2 }))?.toString()).toBe('42');
62
70
 
63
- const signer = new HmacSha256Signer(HmacSha256Signer.generateSecret());
71
+ console.log(`should download a blob range: exiting`)
72
+ });
73
+ });
64
74
 
65
- const driver = new DownloadDriver(
66
- logger,
67
- createDownloadClient(logger, client, []),
68
- createLogsClient(client, logger),
69
- dir,
70
- signer,
71
- { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 },
72
- );
75
+ test('should not redownload a blob when a file already exists', async () => {
76
+ await TestHelpers.withTempRoot(async (client) => {
77
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-driver'));
78
+ const signer = genSigner();
79
+ const driver = await genDriver(client, dir, signer, { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 });
73
80
 
74
81
  console.log('Download the first time');
75
82
  const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
@@ -77,20 +84,11 @@ test('should not redownload a blob a file already exists', async () => {
77
84
  await c.getValue();
78
85
  await c.awaitChange();
79
86
  const blob = await c.getValue();
80
- expect(blob).toBeDefined();
81
- expect(blob!.size).toBe(3);
82
87
  expect((await driver.getContent(blob!.handle))?.toString()).toBe('42\n');
83
88
 
84
89
  await driver.releaseAll();
85
90
 
86
- const driver2 = new DownloadDriver(
87
- logger,
88
- createDownloadClient(logger, client, []),
89
- createLogsClient(client, logger),
90
- dir,
91
- signer,
92
- { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 },
93
- );
91
+ const driver2 = await genDriver(client, dir, signer, { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 });
94
92
 
95
93
  console.log('Download the second time');
96
94
  const c2 = driver2.getDownloadedBlob(downloadable);
@@ -105,16 +103,8 @@ test('should not redownload a blob a file already exists', async () => {
105
103
 
106
104
  test('should get on demand blob without downloading a blob', async () => {
107
105
  await TestHelpers.withTempRoot(async (client) => {
108
- const logger = new ConsoleLoggerAdapter();
109
106
  const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-2-'));
110
- const driver = new DownloadDriver(
111
- logger,
112
- createDownloadClient(logger, client, []),
113
- createLogsClient(client, logger),
114
- dir,
115
- new HmacSha256Signer(HmacSha256Signer.generateSecret()),
116
- { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 },
117
- );
107
+ const driver = await genDriver(client, dir, genSigner(), { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 });
118
108
 
119
109
  const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
120
110
  const c = driver.getOnDemandBlob(downloadable);
@@ -128,18 +118,40 @@ test('should get on demand blob without downloading a blob', async () => {
128
118
  });
129
119
  });
130
120
 
121
+ test('should get on demand blob without downloading a blob range', async () => {
122
+ await TestHelpers.withTempRoot(async (client) => {
123
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-2-'));
124
+ const driver = await genDriver(client, dir, genSigner(), { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 });
125
+
126
+ const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
127
+
128
+ const c = driver.getOnDemandBlob(downloadable, undefined);
129
+ const blob = await c.getValue();
130
+ expect(blob).toBeDefined();
131
+ expect(blob.size).toEqual(3);
132
+ const content = await driver.getContent(blob!.handle, { from: 1, to: 2 });
133
+ expect(content?.toString()).toStrictEqual('2');
134
+
135
+ const c2 = driver.getOnDemandBlob(downloadable, undefined);
136
+ const blob2 = await c2.getValue();
137
+ expect(blob2).toBeDefined();
138
+ expect(blob2.size).toEqual(3);
139
+ const content2 = await driver.getContent(blob2!.handle, { from: 0, to: 1 });
140
+ expect(content2?.toString()).toStrictEqual('4');
141
+
142
+ const c3 = driver.getOnDemandBlob(downloadable);
143
+ const blob3 = await c3.getValue();
144
+ expect(blob3).toBeDefined();
145
+ expect(blob3.size).toEqual(3);
146
+ const content3 = await driver.getContent(blob3!.handle, { from: 1, to: 3 });
147
+ expect(content3?.toString()).toStrictEqual('2\n');
148
+ });
149
+ });
150
+
131
151
  test('should get undefined when releasing a blob from a small cache and the blob was deleted.', async () => {
132
152
  await TestHelpers.withTempRoot(async (client) => {
133
- const logger = new ConsoleLoggerAdapter();
134
153
  const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-3-'));
135
- const driver = new DownloadDriver(
136
- logger,
137
- createDownloadClient(logger, client, []),
138
- createLogsClient(client, logger),
139
- dir,
140
- new HmacSha256Signer(HmacSha256Signer.generateSecret()),
141
- { cacheSoftSizeBytes: 1, nConcurrentDownloads: 10 },
142
- );
154
+ const driver = await genDriver(client, dir, genSigner(), { cacheSoftSizeBytes: 1, nConcurrentDownloads: 10 });
143
155
  const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
144
156
 
145
157
  const c = driver.getDownloadedBlob(downloadable);
@@ -165,18 +177,10 @@ test('should get undefined when releasing a blob from a small cache and the blob
165
177
  });
166
178
  });
167
179
 
168
- test('should get the blob when releasing a blob, but a cache is big enough and it keeps a file on the local drive.', async () => {
180
+ test('should get undefined when releasing a blob from a small cache and the blob was deleted range.', async () => {
169
181
  await TestHelpers.withTempRoot(async (client) => {
170
- const logger = new ConsoleLoggerAdapter();
171
- const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-4-'));
172
- const driver = new DownloadDriver(
173
- logger,
174
- createDownloadClient(logger, client, []),
175
- createLogsClient(client, logger),
176
- dir,
177
- new HmacSha256Signer(HmacSha256Signer.generateSecret()),
178
- { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 },
179
- );
182
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-driver'));
183
+ const driver = await genDriver(client, dir, genSigner(), { cacheSoftSizeBytes: 1, nConcurrentDownloads: 10 });
180
184
  const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
181
185
 
182
186
  const c = driver.getDownloadedBlob(downloadable);
@@ -189,7 +193,7 @@ test('should get the blob when releasing a blob, but a cache is big enough and i
189
193
  const blob2 = await c.getValue();
190
194
  expect(blob2).toBeDefined();
191
195
  expect(blob2!.size).toBe(3);
192
- expect((await driver.getContent(blob2!.handle))?.toString()).toBe('42\n');
196
+ expect((await driver.getContent(blob2!.handle, { from: 1, to: 3 }))?.toString()).toBe('2\n');
193
197
 
194
198
  // The blob is removed from a cache since the size is too big.
195
199
  c.resetState();
@@ -197,18 +201,64 @@ test('should get the blob when releasing a blob, but a cache is big enough and i
197
201
 
198
202
  const c2 = driver.getDownloadedBlob(downloadable);
199
203
 
200
- const blob3 = await c2.getValue();
201
- expect(blob3).toBeDefined();
202
- expect(blob3!.size).toBe(3);
203
- expect((await driver.getContent(blob3!.handle))?.toString()).toBe('42\n');
204
+ const noBlob = await c2.getValue();
205
+ expect(noBlob).toBeUndefined();
204
206
  });
205
207
  });
206
208
 
209
+ // test('should get the blob range after init if it already existed.', async () => {
210
+ // await TestHelpers.withTempRoot(async (client) => {
211
+ // const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-driver'));
212
+ // const signer = genSigner();
213
+ // const driver = await genDriver(client, dir, signer, { cacheSoftSizeBytes: 720, nConcurrentDownloads: 10 });
214
+ // const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
215
+
216
+ // const c = driver.getDownloadedBlob(downloadable);
217
+ // const blob = await c.getValue();
218
+ // expect(blob).toBeUndefined();
219
+ // await c.awaitChange();
220
+
221
+ // const blob2 = await c.getValue();
222
+ // expect(blob2).toBeDefined();
223
+ // expect(blob2!.size).toBe(3);
224
+ // expect((await driver.getContent(blob2!.handle, { from: 1, to: 3 }))?.toString()).toBe('2\n');
225
+
226
+ // // Make the second driver, the cache was already initialized.
227
+ // // We should get the blob instantly.
228
+ // const driver2 = await genDriver(client, dir, signer, { cacheSoftSizeBytes: 720, nConcurrentDownloads: 10 });
229
+ // const c2 = driver2.getDownloadedBlob(downloadable);
230
+ // const blob3 = await c2.getValue();
231
+ // expect(blob3).toBeDefined();
232
+ // expect(blob3!.size).toBe(2);
233
+ // expect((await driver.getContent(blob3!.handle, { from: 1, to: 3 }))?.toString()).toBe('2\n');
234
+ // });
235
+ // })
236
+ ;
237
+
238
+ function genSigner() {
239
+ return new HmacSha256Signer(HmacSha256Signer.generateSecret())
240
+ }
241
+
242
+ async function genDriver(client: PlClient, dir: string, signer: Signer, ops: DownloadDriverOps) {
243
+ const logger = new ConsoleLoggerAdapter();
244
+
245
+ const driver = await DownloadDriver.init(
246
+ logger,
247
+ createDownloadClient(logger, client, []),
248
+ createLogsClient(client, logger),
249
+ dir,
250
+ signer,
251
+ ops,
252
+ );
253
+
254
+ return driver;
255
+ }
256
+
207
257
  async function makeDownloadableBlobFromAssets(client: PlClient, fileName: string) {
208
258
  await client.withWriteTx('MakeAssetDownloadable', async (tx: PlTransaction) => {
209
259
  const importSettings = jsonToData({
210
260
  path: fileName,
211
- storageId: 'library',
261
+ storageId: env.libraryStorage,
212
262
  });
213
263
  const importer = tx.createStruct({ name: 'BlobImportInternal', version: '1' }, importSettings);
214
264
  const importerBlob: FieldRef = {