libfw-client 0.1.0 → 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,10 +205,10 @@ 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
- loadState: (path) => Idb.loadState(path),
211
- saveState: (path, state) => Idb.saveState(path, state),
210
+ loadState: (direction, path) => Idb.loadState(`${direction}:${path}`),
211
+ saveState: (direction, path, state) => Idb.saveState(`${direction}:${path}`, state),
212
212
  getFileList: () => this._getFileList(),
213
213
  readFile: (path, offset, length) => this._readFile(path, offset, length),
214
214
  log: (msg) => {
@@ -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,16 +298,50 @@ 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
- // keepExistingData: true lets resumed downloads overwrite only the
274
- // tail without truncating the already-written prefix.
275
- writable = await handle.createWritable({ keepExistingData: true });
308
+ writable = await handle.createWritable(
309
+ isResume ? { keepExistingData: true } : undefined
310
+ );
276
311
  this._writables.set(path, writable);
277
312
  }
278
313
  await writable.write({ type: 'write', position: offset, data });
279
314
  }
280
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
+
281
345
  /**
282
346
  * Resolve (and create, if needed) the file handle for a virtual path,
283
347
  * creating any parent directories along the way.
@@ -298,13 +362,20 @@ export class LibfwClient {
298
362
  }
299
363
 
300
364
  /**
301
- * 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.
302
367
  * @returns {Promise<void>}
303
368
  * @private
304
369
  */
305
370
  async _flushWritables() {
306
- const pending = [...this._writables.values()].map((w) => w.close().catch(() => {}));
307
- 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
+ });
308
379
  await Promise.allSettled(pending);
309
380
  }
310
381
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "libfw-client",
3
- "version": "0.1.0",
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.0",
5
+ "version": "0.1.2",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",