libfw-client 0.1.1 → 0.1.2

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/index.d.ts CHANGED
@@ -94,6 +94,16 @@ export declare class LibfwClient {
94
94
  */
95
95
  downloadFolder(token: string, dirPath?: string): Promise<number>;
96
96
 
97
+ /**
98
+ * Download a single file from the server at `filePath` into a user-selected
99
+ * local directory.
100
+ *
101
+ * @param token bearer token
102
+ * @param filePath virtual server path of the file to download
103
+ * @returns total bytes written
104
+ */
105
+ downloadFile(token: string, filePath: string): Promise<number>;
106
+
97
107
  /**
98
108
  * Upload files to the server.
99
109
  *
package/index.js CHANGED
@@ -158,7 +158,7 @@ export class LibfwClient {
158
158
  this._dirHandle = null;
159
159
  /** @type {Map<string, FileSystemFileHandle>} path → file handle */
160
160
  this._fileHandles = new Map();
161
- /** @type {Map<string, FileSystemWritableFileStream>} path → writable stream */
161
+ /** @type {Map<string, FileSystemWritableFileStream>} path → open writable stream */
162
162
  this._writables = new Map();
163
163
  /** @type {Map<string, File>} path → File (upload) */
164
164
  this._uploadFiles = new Map();
@@ -205,7 +205,7 @@ export class LibfwClient {
205
205
  return {
206
206
  onFileStart: (path, size) => this._emit({ type: 'fileStart', path, done: 0, total: size }),
207
207
  onWriteChunk: (path, offset, data) => this._onWriteChunk(path, offset, data),
208
- onFileCompleted: (path) => this._emit({ type: 'fileCompleted', path }),
208
+ onFileCompleted: (path) => this._onFileCompleted(path),
209
209
  onProgress: (done, total) => this._emit({ type: 'progress', done, total }),
210
210
  loadState: (direction, path) => Idb.loadState(`${direction}:${path}`),
211
211
  saveState: (direction, path, state) => Idb.saveState(`${direction}:${path}`, state),
@@ -259,6 +259,36 @@ export class LibfwClient {
259
259
  }
260
260
 
261
261
  /**
262
+ * Download a single file from the server at `filePath` into a local
263
+ * directory chosen via `showDirectoryPicker()`.
264
+ *
265
+ * @param {string} token bearer token
266
+ * @param {string} filePath virtual server path of the file to download
267
+ * @returns {Promise<number>} total bytes written
268
+ * @throws {LibfwError}
269
+ */
270
+ async downloadFile(token, filePath) {
271
+ const engine = await this._ready();
272
+ if (typeof window === 'undefined' || typeof window.showDirectoryPicker !== 'function') {
273
+ throw new LibfwError('File System Access API is not available in this browser', 'unsupported');
274
+ }
275
+ if (!filePath) throw new LibfwError('downloadFile requires a file path', 'path');
276
+ this._dirHandle = await window.showDirectoryPicker();
277
+ this._fileHandles.clear();
278
+ try {
279
+ return await engine.download_file(this._options.baseUrl, token, filePath);
280
+ } catch (err) {
281
+ throw toLibfwError(err);
282
+ } finally {
283
+ await this._flushWritables();
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Stream a decompressed chunk straight to disk at its absolute byte
289
+ * offset, keeping memory bounded regardless of file size (no whole-file
290
+ * buffering). The engine awaits this callback, so writes for a file are
291
+ * applied strictly in order.
262
292
  * @param {string} path virtual path
263
293
  * @param {number} offset byte offset
264
294
  * @param {Uint8Array} data decompressed chunk
@@ -268,15 +298,13 @@ export class LibfwClient {
268
298
  async _onWriteChunk(path, offset, data) {
269
299
  let writable = this._writables.get(path);
270
300
  if (!writable) {
301
+ // Open the destination once per file. A true resume (first chunk at
302
+ // offset > 0) keeps the existing prefix on disk; a fresh download opens
303
+ // a truncating writable. Always close the writable on completion/abort
304
+ // so no orphaned `.crswap` swap files are left behind.
305
+ const isResume = offset > 0;
271
306
  const handle = await this._ensureFileHandle(path);
272
307
  this._fileHandles.set(path, handle);
273
- // Only a true resume (first chunk arrives at offset > 0) needs to
274
- // keep the existing file's prefix. For a fresh download we open the
275
- // writable WITHOUT keepExistingData, which truncates and lets us
276
- // write sequentially. In real Chromium the keepExistingData +
277
- // position-write path can leave empty target files behind a trail of
278
- // orphaned `.crswap` swap files, so we avoid it unless required.
279
- const isResume = offset > 0;
280
308
  writable = await handle.createWritable(
281
309
  isResume ? { keepExistingData: true } : undefined
282
310
  );
@@ -285,6 +313,35 @@ export class LibfwClient {
285
313
  await writable.write({ type: 'write', position: offset, data });
286
314
  }
287
315
 
316
+ /**
317
+ * Close the destination writable once a file's transfer completes.
318
+ * @param {string} path virtual path
319
+ * @returns {Promise<void>}
320
+ * @private
321
+ */
322
+ async _onFileCompleted(path) {
323
+ await this._closeWritable(path);
324
+ this._emit({ type: 'fileCompleted', path });
325
+ }
326
+
327
+ /**
328
+ * Close (and forget) a file's writable, flushing buffered bytes to disk.
329
+ * @param {string} path virtual path
330
+ * @returns {Promise<void>}
331
+ * @private
332
+ */
333
+ async _closeWritable(path) {
334
+ const writable = this._writables.get(path);
335
+ if (writable) {
336
+ this._writables.delete(path);
337
+ try {
338
+ await writable.close();
339
+ } catch {
340
+ /* best-effort flush on failure/abort */
341
+ }
342
+ }
343
+ }
344
+
288
345
  /**
289
346
  * Resolve (and create, if needed) the file handle for a virtual path,
290
347
  * creating any parent directories along the way.
@@ -305,13 +362,20 @@ export class LibfwClient {
305
362
  }
306
363
 
307
364
  /**
308
- * Close all open writable streams (flush to disk).
365
+ * Close all still-open writable streams (flush to disk). Called on
366
+ * success, failure or cancellation of a transfer.
309
367
  * @returns {Promise<void>}
310
368
  * @private
311
369
  */
312
370
  async _flushWritables() {
313
- const pending = [...this._writables.values()].map((w) => w.close().catch(() => {}));
314
- this._writables.clear();
371
+ const pending = [...this._writables.entries()].map(async ([path, writable]) => {
372
+ this._writables.delete(path);
373
+ try {
374
+ await writable.close();
375
+ } catch {
376
+ /* best-effort flush */
377
+ }
378
+ });
315
379
  await Promise.allSettled(pending);
316
380
  }
317
381
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "libfw-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "High-performance streaming file & folder transfer SDK for the browser (libfw WASM engine + File System Access API + IndexedDB resume).",
5
- "license": "MIT OR Apache-2.0",
5
+ "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "index.js",
8
8
  "module": "index.js",
@@ -15,6 +15,12 @@ export class LibfwClient {
15
15
  * Bytes transferred so far.
16
16
  */
17
17
  done_bytes(): number;
18
+ /**
19
+ * Download a single file at `file_path` into the chosen local directory.
20
+ *
21
+ * Resolves with the number of bytes written.
22
+ */
23
+ download_file(base_url: string, token: string, file_path: string): Promise<any>;
18
24
  /**
19
25
  * Download every file under the virtual `dirPath` (empty = root).
20
26
  *
@@ -77,6 +83,7 @@ export interface InitOutput {
77
83
  readonly js_option_string: (a: any, b: number, c: number) => [number, number];
78
84
  readonly libfwclient_cancel: (a: number) => void;
79
85
  readonly libfwclient_done_bytes: (a: number) => number;
86
+ readonly libfwclient_download_file: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => any;
80
87
  readonly libfwclient_download_folder: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => any;
81
88
  readonly libfwclient_has_callbacks: (a: number) => number;
82
89
  readonly libfwclient_new: (a: any) => number;
@@ -28,6 +28,25 @@ export class LibfwClient {
28
28
  const ret = wasm.libfwclient_done_bytes(this.__wbg_ptr);
29
29
  return ret;
30
30
  }
31
+ /**
32
+ * Download a single file at `file_path` into the chosen local directory.
33
+ *
34
+ * Resolves with the number of bytes written.
35
+ * @param {string} base_url
36
+ * @param {string} token
37
+ * @param {string} file_path
38
+ * @returns {Promise<any>}
39
+ */
40
+ download_file(base_url, token, file_path) {
41
+ const ptr0 = passStringToWasm0(base_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
42
+ const len0 = WASM_VECTOR_LEN;
43
+ const ptr1 = passStringToWasm0(token, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
44
+ const len1 = WASM_VECTOR_LEN;
45
+ const ptr2 = passStringToWasm0(file_path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
46
+ const len2 = WASM_VECTOR_LEN;
47
+ const ret = wasm.libfwclient_download_file(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
48
+ return ret;
49
+ }
31
50
  /**
32
51
  * Download every file under the virtual `dirPath` (empty = root).
33
52
  *
@@ -431,7 +450,7 @@ function __wbg_get_imports() {
431
450
  return ret;
432
451
  },
433
452
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
434
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 128, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
453
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 133, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
435
454
  const ret = makeMutClosure(arg0, arg1, wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___wasm_bindgen_1e05ddb24c0b7df4___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_1e05ddb24c0b7df4___JsError___true_);
436
455
  return ret;
437
456
  },
Binary file
@@ -5,6 +5,7 @@ export const __wbg_libfwclient_free: (a: number, b: number) => void;
5
5
  export const js_option_string: (a: any, b: number, c: number) => [number, number];
6
6
  export const libfwclient_cancel: (a: number) => void;
7
7
  export const libfwclient_done_bytes: (a: number) => number;
8
+ export const libfwclient_download_file: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => any;
8
9
  export const libfwclient_download_folder: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => any;
9
10
  export const libfwclient_has_callbacks: (a: number) => number;
10
11
  export const libfwclient_new: (a: any) => number;
package/pkg/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "libfw-client",
3
3
  "type": "module",
4
4
  "description": "WASM engine + JS SDK for libfw browser clients",
5
- "version": "0.1.1",
5
+ "version": "0.1.2",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",