libfw-client 0.2.1 → 0.2.3
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.js +93 -9
- package/package.json +1 -1
- package/pkg/libfw_client.js +8 -0
- package/pkg/libfw_client_bg.wasm +0 -0
- package/pkg/package.json +1 -1
package/index.js
CHANGED
|
@@ -371,7 +371,7 @@ export class LibfwClient {
|
|
|
371
371
|
onWriteChunk: (path, offset, data) => this._onWriteChunk(path, offset, data),
|
|
372
372
|
onFileCompleted: (path) => this._onFileCompleted(path),
|
|
373
373
|
onProgress: (done, total) => this._emit({ type: 'progress', done, total }),
|
|
374
|
-
loadState: (direction, path) =>
|
|
374
|
+
loadState: (direction, path) => this._loadResumeState(direction, path),
|
|
375
375
|
saveState: (direction, path, state) => {
|
|
376
376
|
// The in-memory browser-download fallback never commits bytes to
|
|
377
377
|
// disk, so a persisted download offset would be a phantom that
|
|
@@ -521,7 +521,7 @@ export class LibfwClient {
|
|
|
521
521
|
for (const p of order) {
|
|
522
522
|
entries.push({
|
|
523
523
|
name: this._safeEntryName(p),
|
|
524
|
-
data: this._concatBuffers(buffers.get(p) || []),
|
|
524
|
+
data: this._concatBuffers(buffers.get(p)?.chunks || []),
|
|
525
525
|
});
|
|
526
526
|
}
|
|
527
527
|
// Include files that were announced but produced no bytes (empty).
|
|
@@ -532,7 +532,7 @@ export class LibfwClient {
|
|
|
532
532
|
}
|
|
533
533
|
this._triggerBrowserDownload(createZip(entries), this._archiveName(path));
|
|
534
534
|
} else {
|
|
535
|
-
const data = this._concatBuffers(buffers.get(path) || []);
|
|
535
|
+
const data = this._concatBuffers(buffers.get(path)?.chunks || []);
|
|
536
536
|
this._triggerBrowserDownload(new Blob([data], { type: 'application/octet-stream' }), this._downloadName(path));
|
|
537
537
|
}
|
|
538
538
|
return total;
|
|
@@ -663,16 +663,39 @@ export class LibfwClient {
|
|
|
663
663
|
if (this._fallback) {
|
|
664
664
|
// Browser-download fallback: collect chunks in memory instead of
|
|
665
665
|
// streaming to disk. Chunks arrive in order, so a plain append works.
|
|
666
|
-
let
|
|
667
|
-
if (!
|
|
668
|
-
|
|
669
|
-
this._fallback.buffers.set(path,
|
|
666
|
+
let buf = this._fallback.buffers.get(path);
|
|
667
|
+
if (!buf) {
|
|
668
|
+
buf = { chunks: [], len: 0 };
|
|
669
|
+
this._fallback.buffers.set(path, buf);
|
|
670
670
|
this._fallback.order.push(path);
|
|
671
671
|
}
|
|
672
|
-
|
|
672
|
+
// A chunk whose absolute offset is at or before the last one delivered
|
|
673
|
+
// means the engine restarted this file (an internal retry re-delivers
|
|
674
|
+
// the same byte range) — drop the partial buffer so the prefix is not
|
|
675
|
+
// duplicated in the produced blob/zip.
|
|
676
|
+
if (offset <= buf.len) {
|
|
677
|
+
buf.chunks = [];
|
|
678
|
+
buf.len = 0;
|
|
679
|
+
}
|
|
680
|
+
buf.chunks.push(data);
|
|
681
|
+
buf.len += data.length;
|
|
673
682
|
return;
|
|
674
683
|
}
|
|
675
684
|
let entry = this._writables.get(path);
|
|
685
|
+
if (entry && offset <= entry.lastOffset) {
|
|
686
|
+
// The engine restarted this file (a network/protocol error triggered
|
|
687
|
+
// an internal retry that re-delivers the same byte range). Abort the
|
|
688
|
+
// still-open writable — discarding its uncommitted swap file — and
|
|
689
|
+
// reopen fresh, otherwise the re-delivered prefix would be appended a
|
|
690
|
+
// second time and the committed file silently corrupted.
|
|
691
|
+
this._writables.delete(path);
|
|
692
|
+
try {
|
|
693
|
+
await entry.writable.abort();
|
|
694
|
+
} catch {
|
|
695
|
+
/* best-effort discard */
|
|
696
|
+
}
|
|
697
|
+
entry = undefined;
|
|
698
|
+
}
|
|
676
699
|
if (!entry) {
|
|
677
700
|
const { dir, name, handle } = await this._ensureFileHandle(path);
|
|
678
701
|
this._fileHandles.set(path, handle);
|
|
@@ -687,9 +710,10 @@ export class LibfwClient {
|
|
|
687
710
|
const writable = await handle.createWritable(
|
|
688
711
|
isResume ? { keepExistingData: true } : undefined
|
|
689
712
|
);
|
|
690
|
-
entry = { writable, dir, name };
|
|
713
|
+
entry = { writable, dir, name, lastOffset: offset };
|
|
691
714
|
this._writables.set(path, entry);
|
|
692
715
|
}
|
|
716
|
+
entry.lastOffset = offset;
|
|
693
717
|
await entry.writable.write(data);
|
|
694
718
|
}
|
|
695
719
|
|
|
@@ -765,6 +789,66 @@ export class LibfwClient {
|
|
|
765
789
|
}
|
|
766
790
|
}
|
|
767
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Resolve a file handle for a virtual path WITHOUT creating it, so a
|
|
794
|
+
* missing file returns `null` instead of throwing.
|
|
795
|
+
* @param {string} path
|
|
796
|
+
* @returns {Promise<FileSystemFileHandle|null>}
|
|
797
|
+
* @private
|
|
798
|
+
*/
|
|
799
|
+
async _resolveFileHandle(path) {
|
|
800
|
+
if (!this._dirHandle) return null;
|
|
801
|
+
try {
|
|
802
|
+
const segments = splitPath(path);
|
|
803
|
+
if (segments.length === 0) return null;
|
|
804
|
+
let dir = this._dirHandle;
|
|
805
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
806
|
+
dir = await dir.getDirectoryHandle(segments[i], { create: false });
|
|
807
|
+
}
|
|
808
|
+
return await dir.getFileHandle(segments[segments.length - 1], { create: false });
|
|
809
|
+
} catch {
|
|
810
|
+
return null; // missing dir or file, or no FS API
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Load a persisted resume-state `{etag, offset}`, clamping the download
|
|
816
|
+
* `offset` down to the ACTUAL number of bytes committed on disk.
|
|
817
|
+
*
|
|
818
|
+
* `createWritable()` only commits to the real file on `close()`, so a
|
|
819
|
+
* download interrupted by a hard page kill leaves the persisted offset
|
|
820
|
+
* AHEAD of the bytes actually on disk. Resuming from that stale offset
|
|
821
|
+
* with `keepExistingData` would write past a prefix that isn't there,
|
|
822
|
+
* leaving a silent gap in the file. Clamping to the real on-disk length
|
|
823
|
+
* makes the resume pick up exactly where the file really ends.
|
|
824
|
+
* @param {string} direction
|
|
825
|
+
* @param {string} path
|
|
826
|
+
* @returns {Promise<object|null>}
|
|
827
|
+
* @private
|
|
828
|
+
*/
|
|
829
|
+
async _loadResumeState(direction, path) {
|
|
830
|
+
const state = await Idb.loadState(`${direction}:${path}`);
|
|
831
|
+
if (!state) return null;
|
|
832
|
+
// Only downloads that will append onto an on-disk file need the clamp;
|
|
833
|
+
// uploads drive resume from the server's own received ranges.
|
|
834
|
+
if (direction !== 'download') return state;
|
|
835
|
+
const offset = Number(state.offset) || 0;
|
|
836
|
+
if (offset <= 0) return state;
|
|
837
|
+
const handle = await this._resolveFileHandle(path);
|
|
838
|
+
if (!handle) return state; // can't know the on-disk length → trust state
|
|
839
|
+
let diskLen = 0;
|
|
840
|
+
try {
|
|
841
|
+
diskLen = (await handle.getFile()).size;
|
|
842
|
+
} catch {
|
|
843
|
+
return state;
|
|
844
|
+
}
|
|
845
|
+
// Clamp to the bytes actually on disk. If nothing was committed (hard
|
|
846
|
+
// kill before any close), resume from 0 so the whole file is fetched
|
|
847
|
+
// again rather than leaving a gap.
|
|
848
|
+
const clamped = Math.min(offset, diskLen);
|
|
849
|
+
return { ...state, offset: clamped, size: clamped };
|
|
850
|
+
}
|
|
851
|
+
|
|
768
852
|
/**
|
|
769
853
|
* Close all still-open writable streams (flush to disk). Called on
|
|
770
854
|
* success, failure or cancellation of a transfer.
|
package/package.json
CHANGED
package/pkg/libfw_client.js
CHANGED
|
@@ -231,6 +231,10 @@ function __wbg_get_imports() {
|
|
|
231
231
|
const ret = arg0.apply(arg1, arg2);
|
|
232
232
|
return ret;
|
|
233
233
|
}, arguments); },
|
|
234
|
+
__wbg_bufferedAmount_2f95480ad75d4988: function(arg0) {
|
|
235
|
+
const ret = arg0.bufferedAmount;
|
|
236
|
+
return ret;
|
|
237
|
+
},
|
|
234
238
|
__wbg_call_a6e5c5dce5018821: function() { return handleError(function (arg0, arg1, arg2) {
|
|
235
239
|
const ret = arg0.call(arg1, arg2);
|
|
236
240
|
return ret;
|
|
@@ -360,6 +364,10 @@ function __wbg_get_imports() {
|
|
|
360
364
|
state0.a = 0;
|
|
361
365
|
}
|
|
362
366
|
},
|
|
367
|
+
__wbg_now_86c0d4ba3fa605b8: function() {
|
|
368
|
+
const ret = Date.now();
|
|
369
|
+
return ret;
|
|
370
|
+
},
|
|
363
371
|
__wbg_of_5f1b88183ddb5d94: function(arg0, arg1) {
|
|
364
372
|
const ret = Array.of(arg0, arg1);
|
|
365
373
|
return ret;
|
package/pkg/libfw_client_bg.wasm
CHANGED
|
Binary file
|