libfw-client 0.4.2 → 0.4.4
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/README.md +58 -12
- package/index.d.ts +34 -18
- package/index.js +145 -21
- package/package.json +1 -1
- package/pkg/libfw_client.d.ts +2 -2
- package/pkg/libfw_client.js +3 -3
- package/pkg/libfw_client_bg.wasm +0 -0
- package/pkg/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
The browser SDK for [libfw](../README.md): a zero-config wrapper around the
|
|
4
4
|
WASM engine, the File System Access API and IndexedDB.
|
|
5
5
|
|
|
6
|
+
> The same protocol engine is also available as a **native Rust client** for
|
|
7
|
+
> non-browser programs — `libfw_client::native::NativeClient` (async
|
|
8
|
+
> `tokio` + `reqwest`), with a runnable CLI in
|
|
9
|
+
> [`examples/rust-client`](../examples/rust-client/README.md).
|
|
10
|
+
|
|
6
11
|
## Usage
|
|
7
12
|
|
|
8
13
|
```js
|
|
@@ -16,8 +21,12 @@ const client = new LibfwClient({
|
|
|
16
21
|
downloadWindow: 4, // parallel byte-range GETs per single file download
|
|
17
22
|
// (raise to reduce download stutter on high-latency links)
|
|
18
23
|
compress: true, // zrip per-block compression
|
|
24
|
+
compressLevel: 'auto',// zrip level policy: 'auto' benchmarks uploads
|
|
25
|
+
// (needs autoTune) / 'fast' / 'balanced' / 'max' / N
|
|
19
26
|
autoTune: true, // adaptive tuning: probes /capabilities and ramps
|
|
20
|
-
|
|
27
|
+
tuneTtlMs: 3600000, // how long a settled result stays cached (browser,
|
|
28
|
+
// localStorage); 0 = never cache, re-ramp each time
|
|
29
|
+
// concurrency/windows/chunk size from real stats
|
|
21
30
|
onEvent: (e) => {
|
|
22
31
|
if (e.type === 'progress') updateProgressBar(e.done, e.total);
|
|
23
32
|
else if (e.type === 'tuning') renderTuning(e.phase, e.params, e.stats);
|
|
@@ -71,8 +80,14 @@ client.cancel();
|
|
|
71
80
|
into place. Only the chunks the server still misses are re-sent
|
|
72
81
|
(`x-libfw-session-status` probe seeds resume), so interrupted uploads
|
|
73
82
|
resume BitTorrent-style (only the broken/lost parts are re-transmitted).
|
|
83
|
+
A dropped connection (page refresh, crashed tab) keeps that partial on the
|
|
84
|
+
server, so a reloaded page resumes exactly where it stopped.
|
|
74
85
|
- Resume state (`etag`, `offset`, `size`) is persisted per path in
|
|
75
|
-
IndexedDB and re-validated on every retry.
|
|
86
|
+
IndexedDB and re-validated on every retry. `createWritable()` only
|
|
87
|
+
publishes a file on `close()`, so a download **checkpoints** its prefix to
|
|
88
|
+
disk every time the engine reports a durable offset (~4 MiB) — a hard page
|
|
89
|
+
refresh mid-download therefore resumes from the last checkpoint instead of
|
|
90
|
+
restarting from byte 0.
|
|
76
91
|
- Pause/resume/cancel drive the WASM state machine
|
|
77
92
|
(`idle → downloading/uploading → paused → resumed → completed/failed`).
|
|
78
93
|
|
|
@@ -102,25 +117,54 @@ dist/libfw-client.umd.js UMD bundle (after build:umd)
|
|
|
102
117
|
- `downloadWindow: number` (default `4`) — in-flight byte-range window per
|
|
103
118
|
single file download (how many concurrent `Range` GETs); raise it on
|
|
104
119
|
high-latency links. `1` disables parallelism (sequential downloads).
|
|
105
|
-
- `
|
|
106
|
-
|
|
107
|
-
(worst case ≈ `downloadWindow *
|
|
108
|
-
still receives data in order.
|
|
120
|
+
- `chunkSize: number` (default `2097152`, 2 MiB) — shared size used for
|
|
121
|
+
both upload chunks and parallel download ranges; the engine reorders
|
|
122
|
+
in-flight chunks in memory (worst case ≈ `downloadWindow * chunkSize`
|
|
123
|
+
bytes) so the SDK still receives data in order.
|
|
124
|
+
- `compress: boolean` (default `true`) — master switch for zrip
|
|
125
|
+
compression; `false` sends every body as identity.
|
|
126
|
+
- `compressLevel: number | 'auto' | 'fast' | 'balanced' | 'max'` (default
|
|
127
|
+
`'balanced'`) — zrip level policy when `compress` is on. `'fast'` is the
|
|
128
|
+
advertised minimum (least CPU, worst ratio), `'balanced'` the advertised
|
|
129
|
+
default, `'max'` the advertised maximum (best ratio); a number is clamped
|
|
130
|
+
into the advertised range. `'auto'` additionally micro-benchmarks the
|
|
131
|
+
advertised range against a real sample of the first uploaded file (while
|
|
132
|
+
`autoTune` is enabled) and picks the best bytes-saved-vs-CPU-time
|
|
133
|
+
trade-off for the measured link speed; downloads request the resolved
|
|
134
|
+
level from the server.
|
|
109
135
|
- `downloadMode: 'auto' | 'fs' | 'browser'` (default `'auto'`) — `'fs'`
|
|
110
136
|
streams downloads through the File System Access API; `'browser'` buffers
|
|
111
137
|
and triggers a traditional browser download (folders become `.zip`);
|
|
112
138
|
`'auto'` uses `'fs'` when the API exists and falls back to `'browser'`.
|
|
139
|
+
**Download resume requires `'fs'`** (or an injected `directoryHandle`):
|
|
140
|
+
an interrupted fs-mode download is continued from the bytes already on
|
|
141
|
+
disk, while the memory-backed `'browser'` fallback always restarts from
|
|
142
|
+
byte 0 — there is no partial file to continue from.
|
|
113
143
|
- `maxFallbackBytes: number` (default `536870912`, 512 MiB) — memory cap
|
|
114
144
|
for the in-memory `'browser'` fallback. File sizes are pre-checked
|
|
115
145
|
against it before buffering; a download that would exceed it rejects
|
|
116
146
|
with a `too-large` `LibfwError` instead of risking an OOM. `0` disables.
|
|
117
147
|
- `autoTune: boolean` (default `false`) — enable the adaptive tuning
|
|
118
148
|
engine. The engine probes the server's `/capabilities` limits and
|
|
119
|
-
TCP-style ramps
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
149
|
+
TCP-style ramps the per-file window and cross-file concurrency from the
|
|
150
|
+
advertised minimums using real transfer stats; the chunk size follows the
|
|
151
|
+
measured throughput (~100 ms of it, clamped into the advertised range) and
|
|
152
|
+
the zrip level is a client policy from `compressLevel`, never ramped. When
|
|
153
|
+
disabled, the configured static values are used as-is. Tuning state is
|
|
154
|
+
**in memory** for the lifetime of the client (a settle is reused by later
|
|
155
|
+
transfers and dropped on failure) **and** is cached in `localStorage` per
|
|
156
|
+
origin + direction, so a page refresh does not re-ramp — see `tuneTtlMs`.
|
|
157
|
+
- `tuneTtlMs: number` (default `3600000`, 1 hour) — how long a cached
|
|
158
|
+
tuning result stays usable. The cache is keyed by origin **and**
|
|
159
|
+
direction (an upload settle says nothing about a download), is tagged with
|
|
160
|
+
the `/capabilities` it was measured against, and expires `tuneTtlMs`
|
|
161
|
+
*after the ramp settled* — not after the last reuse — so a link measured
|
|
162
|
+
long ago is re-measured even if it is used constantly. An entry is also
|
|
163
|
+
discarded early when the capabilities change or a transfer fails. `0`
|
|
164
|
+
disables the cache entirely (every transfer re-ramps). Ignored unless
|
|
165
|
+
`autoTune` is enabled. Storage failures (private mode, quota, disabled
|
|
166
|
+
storage) are swallowed: caching is an optimisation and never fails a
|
|
167
|
+
transfer.
|
|
124
168
|
- `downloadFolder(token, dirPath?) → Promise<number>`
|
|
125
169
|
- `downloadFile(token, filePath) → Promise<number>`
|
|
126
170
|
- `upload(token, files?) → Promise<number>`
|
|
@@ -130,7 +174,9 @@ dist/libfw-client.umd.js UMD bundle (after build:umd)
|
|
|
130
174
|
- `tuneStatus() → { phase, params, stats, capsHash } | null` — live
|
|
131
175
|
adaptive-tuning status. `phase` is `uninitialized | ramping | settled |
|
|
132
176
|
degraded`; `params` is `{ concurrency, uploadWindow, downloadWindow,
|
|
133
|
-
chunkSize,
|
|
177
|
+
chunkSize, compressLevel }` (the zrip level is the resolved *policy*, i.e.
|
|
178
|
+
what downloads request — uploads may use an `'auto'`-benchmarked level for
|
|
179
|
+
the session); `stats` is
|
|
134
180
|
`{ rttMs, mbps }` (EWMA request RTT, last-window throughput). `null`
|
|
135
181
|
until the WASM engine is initialised.
|
|
136
182
|
- Events: with `autoTune` enabled, `onEvent` additionally receives
|
package/index.d.ts
CHANGED
|
@@ -55,10 +55,8 @@ export interface TuningParams {
|
|
|
55
55
|
uploadWindow: number;
|
|
56
56
|
/** In-flight byte-range GETs per single-file download. */
|
|
57
57
|
downloadWindow: number;
|
|
58
|
-
/**
|
|
58
|
+
/** Shared chunk size in bytes for uploads and parallel downloads. */
|
|
59
59
|
chunkSize: number;
|
|
60
|
-
/** Download byte-range size in bytes. */
|
|
61
|
-
downloadChunkSize: number;
|
|
62
60
|
/** zrip compression level (negative = faster, positive = smaller). */
|
|
63
61
|
compressLevel: number;
|
|
64
62
|
}
|
|
@@ -127,16 +125,21 @@ export interface LibfwClientOptions {
|
|
|
127
125
|
* parallelism (sequential downloads). Default `4`.
|
|
128
126
|
*/
|
|
129
127
|
downloadWindow?: number;
|
|
130
|
-
/**
|
|
131
|
-
* Byte range size for parallel downloads. The engine reorders in-flight
|
|
132
|
-
* chunks in memory (worst case ≈ `downloadWindow * downloadChunkSize`
|
|
133
|
-
* bytes) so the SDK still receives data strictly in order. Default
|
|
134
|
-
* `262144` (256 KiB).
|
|
135
|
-
*/
|
|
136
|
-
downloadChunkSize?: number;
|
|
137
128
|
/** Negotiate zrip compression. Default `true`. */
|
|
138
129
|
compress?: boolean;
|
|
139
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* zrip level policy when `compress` is on. `'fast'` = advertised minimum
|
|
132
|
+
* (least CPU), `'balanced'` (default) = advertised default, `'max'` =
|
|
133
|
+
* advertised maximum (best ratio); a number is that level clamped into the
|
|
134
|
+
* advertised range.
|
|
135
|
+
*
|
|
136
|
+
* `'auto'` additionally micro-benchmarks the advertised range against a
|
|
137
|
+
* real sample of the first uploaded file while `autoTune` is enabled,
|
|
138
|
+
* choosing the level with the best bytes-saved-vs-CPU-time trade-off.
|
|
139
|
+
* Downloads request the resolved level from the server.
|
|
140
|
+
*/
|
|
141
|
+
compressLevel?: number | 'auto' | 'fast' | 'balanced' | 'max';
|
|
142
|
+
/** Shared chunk size in bytes for uploads and parallel downloads. Default 2 MiB. */
|
|
140
143
|
chunkSize?: number;
|
|
141
144
|
/** Retries per chunk/file before failing. Default `3`. */
|
|
142
145
|
maxRetries?: number;
|
|
@@ -169,15 +172,28 @@ export interface LibfwClientOptions {
|
|
|
169
172
|
maxFallbackBytes?: number;
|
|
170
173
|
/**
|
|
171
174
|
* Enable the adaptive tuning engine: the engine probes the server's
|
|
172
|
-
* `/capabilities` limits and TCP-style ramps
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
175
|
+
* `/capabilities` limits and TCP-style ramps the per-file window and
|
|
176
|
+
* cross-file concurrency from the advertised minimums using real transfer
|
|
177
|
+
* stats. The chunk size follows the measured throughput (~100 ms of it,
|
|
178
|
+
* clamped into the advertised range) and the zrip level is a client policy
|
|
179
|
+
* from `compressLevel`, never ramped. When disabled the configured static
|
|
180
|
+
* values are used as-is. Default `false`.
|
|
181
|
+
*
|
|
182
|
+
* Tuning state lives in memory for this client instance (a settle is reused
|
|
183
|
+
* by later transfers and dropped on failure) **and** is cached in
|
|
184
|
+
* `localStorage` per origin + direction, so a page reload does not re-ramp.
|
|
185
|
+
* See `tuneTtlMs` for the lifetime of that cache. Default `false`.
|
|
176
186
|
*/
|
|
177
187
|
autoTune?: boolean;
|
|
178
188
|
/**
|
|
179
|
-
* How long
|
|
180
|
-
*
|
|
189
|
+
* How long a cached tuning result stays usable, in milliseconds. Default
|
|
190
|
+
* `3600000` (1 hour); `0` disables the cache so every transfer re-ramps.
|
|
191
|
+
*
|
|
192
|
+
* The cache is browser-only (the Rust native client keeps its settle in
|
|
193
|
+
* memory) and is keyed by origin + direction. The TTL counts from the moment
|
|
194
|
+
* the ramp settled, and an entry is discarded early when the server's
|
|
195
|
+
* `/capabilities` change or a transfer fails. Ignored unless `autoTune` is
|
|
196
|
+
* enabled.
|
|
181
197
|
*/
|
|
182
198
|
tuneTtlMs?: number;
|
|
183
199
|
/** Optional progress/state listener. Tuning updates arrive as `{ type: 'tuning', phase, params, stats }`. */
|
|
@@ -291,7 +307,7 @@ export declare class LibfwClient {
|
|
|
291
307
|
* @returns `{ phase, params, stats, capsHash }` — `phase` is
|
|
292
308
|
* `uninitialized | ramping | settled | degraded`; `params` holds the
|
|
293
309
|
* tuned `concurrency` / `uploadWindow` / `downloadWindow` / `chunkSize`
|
|
294
|
-
* / `
|
|
310
|
+
* / `compressLevel`; `stats` is
|
|
295
311
|
* `{ rttMs, mbps }` (EWMA request RTT, last-window throughput).
|
|
296
312
|
* `null` until the WASM engine is initialised (or when `autoTune` is
|
|
297
313
|
* disabled, `phase` stays `uninitialized`).
|
package/index.js
CHANGED
|
@@ -207,16 +207,24 @@ export class LibfwClient {
|
|
|
207
207
|
* transfer), so a single file's throughput is bounded by bandwidth
|
|
208
208
|
* instead of one connection's `chunkSize / RTT` on high-latency
|
|
209
209
|
* links. `1` disables parallelism (sequential downloads).
|
|
210
|
-
* @param {
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
210
|
+
* @param {boolean} [options.compress=true] negotiate zrip compression.
|
|
211
|
+
* Master switch: `false` sends every body as identity, regardless of
|
|
212
|
+
* `compressLevel`.
|
|
213
|
+
* @param {number|'auto'|'fast'|'balanced'|'max'} [options.compressLevel='balanced']
|
|
214
|
+
* zrip level policy when `compress` is on. `'fast'` = advertised
|
|
215
|
+
* minimum (least CPU, worst ratio), `'balanced'`/`'auto'`-without-a-
|
|
216
|
+
* sample = advertised default, `'max'` = advertised maximum (best
|
|
217
|
+
* ratio), a number = that level clamped into the advertised range.
|
|
218
|
+
* `'auto'` additionally micro-benchmarks the advertised range against
|
|
219
|
+
* a real sample of the first uploaded file (only while `autoTune` is
|
|
220
|
+
* enabled) and picks the level with the best
|
|
221
|
+
* bytes-saved-vs-CPU-time trade-off for the measured link speed.
|
|
222
|
+
* Downloads request the resolved level from the server (no sample is
|
|
223
|
+
* available before the transfer starts).
|
|
224
|
+
* @param {number} [options.chunkSize=2097152] shared chunk size in bytes for
|
|
225
|
+
* both upload chunks and parallel download ranges. The same value is
|
|
226
|
+
* used on both paths, and any value works as long as the server and
|
|
227
|
+
* memory budget permit it (larger = fewer, bigger requests).
|
|
220
228
|
* @param {number} [options.maxRetries=3] retries per chunk/file before failing
|
|
221
229
|
* @param {number} [options.baseRetryDelayMs=500] initial backoff (ms)
|
|
222
230
|
* @param {number} [options.maxRetryDelayMs=30000] backoff ceiling (ms)
|
|
@@ -237,11 +245,23 @@ export class LibfwClient {
|
|
|
237
245
|
* `0` disables the limit.
|
|
238
246
|
* @param {boolean} [options.autoTune=false] enable the adaptive tuning
|
|
239
247
|
* engine: the engine probes the server's `/capabilities` limits and
|
|
240
|
-
* TCP-style ramps
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
248
|
+
* TCP-style ramps the per-file window and cross-file concurrency from
|
|
249
|
+
* the advertised minimums using real transfer stats; the chunk size
|
|
250
|
+
* follows the measured throughput (~100 ms of it, clamped into the
|
|
251
|
+
* advertised range) and the zrip level is a client policy from
|
|
252
|
+
* `compressLevel`, never ramped. When disabled the configured static
|
|
253
|
+
* values are used as-is. A settled result is kept in memory for this
|
|
254
|
+
* client instance (reused by later transfers, dropped on failure)
|
|
255
|
+
* **and** cached in `localStorage` per origin + direction, so a page
|
|
256
|
+
* reload does not re-ramp; see `tuneTtlMs` for how long that cache
|
|
257
|
+
* lives.
|
|
258
|
+
* @param {number} [options.tuneTtlMs=3600000] how long a cached tuning
|
|
259
|
+
* result (browser only — the Rust native client keeps it in memory)
|
|
260
|
+
* stays usable, in milliseconds. The TTL counts from the moment the
|
|
261
|
+
* ramp settled, is scoped to one origin + direction, and is
|
|
262
|
+
* invalidated early if the server's `/capabilities` change or a
|
|
263
|
+
* transfer fails. `0` disables the cache entirely, so every transfer
|
|
264
|
+
* (and every page load) re-ramps. Ignored unless `autoTune` is on.
|
|
245
265
|
* @param {(event: {type: string, done: number, total: number, path?: string, error?: string}) => void} [options.onEvent]
|
|
246
266
|
* optional progress/state listener. Tuning updates arrive as
|
|
247
267
|
* `{ type: 'tuning', phase, params, stats }` events.
|
|
@@ -272,8 +292,8 @@ export class LibfwClient {
|
|
|
272
292
|
concurrency: 4,
|
|
273
293
|
uploadWindow: 8,
|
|
274
294
|
downloadWindow: 4,
|
|
275
|
-
downloadChunkSize: 256 * 1024,
|
|
276
295
|
compress: true,
|
|
296
|
+
compressLevel: null,
|
|
277
297
|
chunkSize: 2 * 1024 * 1024,
|
|
278
298
|
maxRetries: 3,
|
|
279
299
|
baseRetryDelayMs: 500,
|
|
@@ -283,7 +303,7 @@ export class LibfwClient {
|
|
|
283
303
|
downloadMode: 'auto',
|
|
284
304
|
maxFallbackBytes: 512 * 1024 * 1024,
|
|
285
305
|
autoTune: false,
|
|
286
|
-
tuneTtlMs:
|
|
306
|
+
tuneTtlMs: 60 * 60 * 1000,
|
|
287
307
|
onEvent: null,
|
|
288
308
|
directoryHandle: null,
|
|
289
309
|
resolveDisplayName: null,
|
|
@@ -338,8 +358,8 @@ export class LibfwClient {
|
|
|
338
358
|
concurrency: this._options.concurrency,
|
|
339
359
|
uploadWindow: this._options.uploadWindow,
|
|
340
360
|
downloadWindow: this._options.downloadWindow,
|
|
341
|
-
downloadChunkSize: this._options.downloadChunkSize,
|
|
342
361
|
compress: this._options.compress,
|
|
362
|
+
compressLevel: this._options.compressLevel,
|
|
343
363
|
chunkSize: this._options.chunkSize,
|
|
344
364
|
maxRetries: this._options.maxRetries,
|
|
345
365
|
baseRetryDelayMs: this._options.baseRetryDelayMs,
|
|
@@ -424,7 +444,15 @@ export class LibfwClient {
|
|
|
424
444
|
// poisons a later FS-API resume. Skip persisting download state
|
|
425
445
|
// while a fallback transfer is active.
|
|
426
446
|
if (direction === 'download' && this._fallback) return Promise.resolve();
|
|
427
|
-
return Idb.saveState(`${direction}:${path}`, state)
|
|
447
|
+
return Idb.saveState(`${direction}:${path}`, state).then(() => {
|
|
448
|
+
// The engine reports a durable offset every few MiB: use it to
|
|
449
|
+
// COMMIT the prefix to disk, so an interrupted download (a tab
|
|
450
|
+
// refresh or a crash — no `finally` block runs) still has a partial
|
|
451
|
+
// to resume from instead of starting over.
|
|
452
|
+
if (direction === 'download') {
|
|
453
|
+
return this._checkpointDownload(path, Number(state?.offset) || 0);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
428
456
|
},
|
|
429
457
|
getFileList: () => this._getFileList(),
|
|
430
458
|
readFile: (path, offset, length) => this._readFile(path, offset, length),
|
|
@@ -825,6 +853,35 @@ export class LibfwClient {
|
|
|
825
853
|
const writable = await handle.createWritable(
|
|
826
854
|
isResume ? { keepExistingData: true } : undefined
|
|
827
855
|
);
|
|
856
|
+
if (isResume) {
|
|
857
|
+
// `createWritable({ keepExistingData: true })` KEEPS the existing
|
|
858
|
+
// bytes but still positions the stream at 0 — it is not an append. A
|
|
859
|
+
// resumed tail written without seeking would overwrite the prefix and
|
|
860
|
+
// then close, leaving a file that is only the tail (measured: a
|
|
861
|
+
// 20 MiB download resumed at 6.8 MiB produced a 14.1 MiB, corrupt
|
|
862
|
+
// file). Seek past what is already on disk so the tail really
|
|
863
|
+
// appends; `_loadResumeState` clamped `offset` to the on-disk length.
|
|
864
|
+
const existing = (await handle.getFile()).size;
|
|
865
|
+
if (existing < offset) {
|
|
866
|
+
// The prefix we are asked to resume past is not on disk: appending
|
|
867
|
+
// would leave a hole. Fail loudly instead of committing a corrupt
|
|
868
|
+
// file (the caller can retry, which restarts cleanly).
|
|
869
|
+
await this._discardWritable(path, writable);
|
|
870
|
+
throw new LibfwError(
|
|
871
|
+
`cannot resume \`${path}\`: only ${existing} of ${offset} bytes are on disk`,
|
|
872
|
+
'storage'
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
await writable.seek(offset);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
await this._discardWritable(path, writable);
|
|
879
|
+
throw new LibfwError(
|
|
880
|
+
`cannot resume \`${path}\`: the destination cannot be seeked (${err?.message ?? err})`,
|
|
881
|
+
'storage'
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
828
885
|
entry = { writable, dir, name, lastOffset: offset };
|
|
829
886
|
this._writables.set(path, entry);
|
|
830
887
|
}
|
|
@@ -832,6 +889,74 @@ export class LibfwClient {
|
|
|
832
889
|
await entry.writable.write(data);
|
|
833
890
|
}
|
|
834
891
|
|
|
892
|
+
/**
|
|
893
|
+
* Commit the bytes received so far for a download to disk.
|
|
894
|
+
*
|
|
895
|
+
* `createWritable()` only publishes the file on `close()` — everything else
|
|
896
|
+
* lives in a Chromium swap file (`.name.crswap`) that is DISCARDED when the
|
|
897
|
+
* page dies. A hard refresh mid-download would therefore find a 0-byte (or
|
|
898
|
+
* stale) target and restart from byte 0. The engine reports a durable
|
|
899
|
+
* offset every few MiB (`RESUME_SAVE_EVERY`), so this closes the writable
|
|
900
|
+
* (publishing the prefix) and immediately reopens it with
|
|
901
|
+
* `keepExistingData` + `seek`, leaving a resumable partial on disk at the
|
|
902
|
+
* cost of one close/reopen per checkpoint.
|
|
903
|
+
*
|
|
904
|
+
* Called from the `saveState` callback, which the engine awaits between
|
|
905
|
+
* chunks — no write for this path is in flight here.
|
|
906
|
+
* @param {string} path
|
|
907
|
+
* @param {number} offset durable offset reported by the engine
|
|
908
|
+
* @returns {Promise<void>}
|
|
909
|
+
* @private
|
|
910
|
+
*/
|
|
911
|
+
async _checkpointDownload(path, offset) {
|
|
912
|
+
const entry = this._writables.get(path);
|
|
913
|
+
const handle = this._fileHandles.get(path);
|
|
914
|
+
if (!entry || !handle || offset <= 0) return;
|
|
915
|
+
let onDisk = 0;
|
|
916
|
+
try {
|
|
917
|
+
onDisk = (await handle.getFile()).size;
|
|
918
|
+
} catch {
|
|
919
|
+
return; // handle gone: nothing safe to commit
|
|
920
|
+
}
|
|
921
|
+
// Nothing new to publish (an earlier checkpoint already covers it), or
|
|
922
|
+
// the engine is behind the bytes we committed: leave the stream alone.
|
|
923
|
+
if (onDisk >= offset) return;
|
|
924
|
+
try {
|
|
925
|
+
await entry.writable.close();
|
|
926
|
+
} catch {
|
|
927
|
+
return; // keep the (still open) stream; the next chunk continues on it
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
const writable = await handle.createWritable({ keepExistingData: true });
|
|
931
|
+
// The committed file now holds everything written so far; `close()`
|
|
932
|
+
// published exactly `lastOffset` bytes, so appending resumes there.
|
|
933
|
+
await writable.seek((await handle.getFile()).size);
|
|
934
|
+
entry.writable = writable;
|
|
935
|
+
} catch {
|
|
936
|
+
// Reopening failed: forget the stream so the next chunk re-opens it
|
|
937
|
+
// through the normal resume path (which re-validates the prefix).
|
|
938
|
+
this._writables.delete(path);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Drop an open writable (and its uncommitted swap file) without failing the
|
|
944
|
+
* caller. Used when a resume cannot be honoured so no partial/corrupt file
|
|
945
|
+
* is committed.
|
|
946
|
+
* @param {string} path
|
|
947
|
+
* @param {FileSystemWritableFileStream} writable
|
|
948
|
+
* @returns {Promise<void>}
|
|
949
|
+
* @private
|
|
950
|
+
*/
|
|
951
|
+
async _discardWritable(path, writable) {
|
|
952
|
+
this._writables.delete(path);
|
|
953
|
+
try {
|
|
954
|
+
await writable.abort();
|
|
955
|
+
} catch {
|
|
956
|
+
/* best-effort discard */
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
835
960
|
/**
|
|
836
961
|
* Close the destination writable once a file's transfer completes.
|
|
837
962
|
* @param {string} path virtual path
|
|
@@ -1246,8 +1371,7 @@ export class LibfwClient {
|
|
|
1246
1371
|
*
|
|
1247
1372
|
* - `phase`: `uninitialized | ramping | settled | degraded`
|
|
1248
1373
|
* - `params`: `{ concurrency, uploadWindow, downloadWindow, chunkSize,
|
|
1249
|
-
*
|
|
1250
|
-
* currently tuned to
|
|
1374
|
+
* compressLevel }` — the parameters the engine is currently tuned to
|
|
1251
1375
|
* - `stats`: `{ rttMs, mbps }` — EWMA request RTT and last-window
|
|
1252
1376
|
* throughput of the most recent transfer
|
|
1253
1377
|
*
|
package/package.json
CHANGED
package/pkg/libfw_client.d.ts
CHANGED
|
@@ -29,8 +29,8 @@ export class LibfwClient {
|
|
|
29
29
|
has_callbacks(): boolean;
|
|
30
30
|
/**
|
|
31
31
|
* Create an engine. `options` may include:
|
|
32
|
-
* `{ concurrency, uploadWindow, downloadWindow,
|
|
33
|
-
*
|
|
32
|
+
* `{ concurrency, uploadWindow, downloadWindow, compress,
|
|
33
|
+
* compressLevel, chunkSize, maxRetries, baseRetryDelayMs,
|
|
34
34
|
* maxRetryDelayMs, timeoutMs, autoTune, tuneTtlMs }`.
|
|
35
35
|
*/
|
|
36
36
|
constructor(opts: any);
|
package/pkg/libfw_client.js
CHANGED
|
@@ -72,8 +72,8 @@ export class LibfwClient {
|
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Create an engine. `options` may include:
|
|
75
|
-
* `{ concurrency, uploadWindow, downloadWindow,
|
|
76
|
-
*
|
|
75
|
+
* `{ concurrency, uploadWindow, downloadWindow, compress,
|
|
76
|
+
* compressLevel, chunkSize, maxRetries, baseRetryDelayMs,
|
|
77
77
|
* maxRetryDelayMs, timeoutMs, autoTune, tuneTtlMs }`.
|
|
78
78
|
* @param {any} opts
|
|
79
79
|
*/
|
|
@@ -592,7 +592,7 @@ function __wbg_get_imports() {
|
|
|
592
592
|
return ret;
|
|
593
593
|
}, arguments); },
|
|
594
594
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
595
|
-
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx:
|
|
595
|
+
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 206, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
|
596
596
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___wasm_bindgen_b2c787b844a3ac79___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_b2c787b844a3ac79___JsError___true_);
|
|
597
597
|
return ret;
|
|
598
598
|
},
|
package/pkg/libfw_client_bg.wasm
CHANGED
|
Binary file
|
package/pkg/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libfw-client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"description": "WASM engine + JS SDK
|
|
5
|
-
"version": "0.4.
|
|
4
|
+
"description": "libfw client: native Rust client + WASM engine + JS SDK",
|
|
5
|
+
"version": "0.4.4",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|