1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
|
.. _fetch-api:
=========
Fetch API
=========
The Emscripten Fetch API allows native code to transfer files via XHR (HTTP GET,
PUT, POST) from remote servers, and to persist the downloaded files locally in
browser's IndexedDB storage, so that they can be reaccessed locally on
subsequent page visits. The Fetch API is callable from multiple threads, and the
network requests can be run either synchronously or asynchronously as desired.
.. note::
In order to use the Fetch API, you would need to compile your code with **-s
FETCH=1**.
Introduction
============
The use of the Fetch API is quick to illustrate via an example. The following
application downloads a file from a web server asynchronously to memory inside
the application heap.
.. code-block:: cpp
#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>
void downloadSucceeded(emscripten_fetch_t *fetch) {
printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
// The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
emscripten_fetch_close(fetch); // Free data associated with the fetch.
}
void downloadFailed(emscripten_fetch_t *fetch) {
printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
emscripten_fetch_close(fetch); // Also free data on failure.
}
int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
attr.onsuccess = downloadSucceeded;
attr.onerror = downloadFailed;
emscripten_fetch(&attr, "myfile.dat");
}
If a relative pathname is specified to a call to emscripten_fetch, like in the
above example, the XHR is performed relative to the href (URL) of the current
page. Passing a fully qualified absolute URL allows downloading files across
domains, however these are subject to `HTTP access control (CORS) rules
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS>`_.
By default the Fetch API runs asynchronously, which means that the
emscripten_fetch() function call returns immediately and the operation will
continue to occur on the background. When the operation finishes, either the
success or the failure callback will be invoked.
Persisting data
===============
The XHR requests issued by the Fetch API are subject to the usual browser
caching behavior. These caches are transient (temporary) so there is no
guarantee that the data will persist in the cache for a given period of time.
Additionally, if the files are somewhat large (multiple megabytes), browsers
typically don't cache the downloads at all.
To enable a more explicit control for persisting the downloaded files, the Fetch
API interacts with the browser's IndexedDB API, which can load and store large
data files that are available on subsequent visits to the page. To enable
IndexedDB storage, pass the EMSCRIPTEN_FETCH_PERSIST_FILE flag in the fetch
attributes:
.. code-block:: cpp
int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
...
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_PERSIST_FILE;
...
emscripten_fetch(&attr, "myfile.dat");
}
For a full example, see the file
tests/fetch/example_async_xhr_to_memory_via_indexeddb.cpp.
Persisting data bytes from memory
---------------------------------
Sometimes it is useful to persist a range of bytes from application memory to
IndexedDB (without having to perform any XHRs). This is possible with the
Emscripten Fetch API by passing the special HTTP action verb "EM_IDB_STORE" to
the Emscripten Fetch operation.
.. code-block:: cpp
void success(emscripten_fetch_t *fetch) {
printf("IDB store succeeded.\n");
emscripten_fetch_close(fetch);
}
void failure(emscripten_fetch_t *fetch) {
printf("IDB store failed.\n");
emscripten_fetch_close(fetch);
}
void persistFileToIndexedDB(const char *outputFilename, uint8_t *data, size_t numBytes) {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "EM_IDB_STORE");
attr.attributes = EMSCRIPTEN_FETCH_REPLACE | EMSCRIPTEN_FETCH_PERSIST_FILE;
attr.requestData = (char *)data;
attr.requestDataSize = numBytes;
attr.onsuccess = success;
attr.onerror = failure;
emscripten_fetch(&attr, outputFilename);
}
int main() {
// Create data
uint8_t *data = (uint8_t*)malloc(10240);
srand(time(NULL));
for(int i = 0; i < 10240; ++i) data[i] = (uint8_t)rand();
persistFileToIndexedDB("outputfile.dat", data, 10240);
}
Deleting a file from IndexedDB
------------------------------
Files can be cleaned up from IndexedDB by using the HTTP action verb "EM_IDB_DELETE":
.. code-block:: cpp
void success(emscripten_fetch_t *fetch) {
printf("Deleting file from IDB succeeded.\n");
emscripten_fetch_close(fetch);
}
void failure(emscripten_fetch_t *fetch) {
printf("Deleting file from IDB failed.\n");
emscripten_fetch_close(fetch);
}
int main() {
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "EM_IDB_DELETE");
emscripten_fetch(&attr, "filename_to_delete.dat");
}
Synchronous Fetches
===================
In some scenarios, it would be nice to be able to perform an XHR request or an
IndexedDB file operation synchronously in the calling thread. This can make
porting applications easier and simplify code flow by avoiding the need to pass
a callback.
All types of Emscripten Fetch API operations (XHRs, IndexedDB accesses) can be
performed synchronously by passing the EMSCRIPTEN_FETCH_SYNCHRONOUS flag. When
this flag is passed, the calling thread will block to sleep until the fetch
operation finishes. See the following example.
.. code-block:: cpp
int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS;
emscripten_fetch_t *fetch = emscripten_fetch(&attr, "file.dat"); // Blocks here until the operation is complete.
if (fetch->status == 200) {
printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
// The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
} else {
printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
}
emscripten_fetch_close(fetch);
}
In the above code sample, the success and failure callback functions are not
used. However, if specified, they will be synchronously called before
emscripten_fetch() returns.
.. note::
Synchronous Emscripten Fetch operations are subject to a number of
restrictions, depending on which Emscripten build mode (linker flags) is used:
- **No flags**: Only asynchronous Fetch operations are available.
- **--proxy-to-worker**: Synchronous Fetch operations are allowed for fetches
that only do an XHR but do not interact with IndexedDB.
- **-s USE_PTHREADS=1**: Synchronous Fetch operations are available on
pthreads, but not on the main thread.
- **--proxy-to-worker** + **-s USE_PTHREADS=1**: Synchronous Fetch operations
are available both on the main thread and pthreads.
Waitable Fetches
================
Emscripten Fetch operations can also run in a third mode, called a *waitable*
fetch. Waitable fetches start off as asynchronous, but at any point after the
fetch has started, the calling thread can issue a wait operation to either wait
for the completion of the fetch, or to just poll whether the fetch operation has
yet completed. The following code sample illustrates how this works.
.. code-block:: cpp
int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_WAITABLE;
emscripten_fetch_t *fetch = emscripten_fetch(&attr, "file.dat"); // Starts as asynchronous.
EMSCRIPTEN_RESULT ret = EMSCRIPTEN_RESULT_TIMED_OUT;
while(ret == EMSCRIPTEN_RESULT_TIMED_OUT) {
/* possibly do some other work; */
ret = emscripten_fetch_wait(fetch, 0/*milliseconds to wait, 0 to just poll, INFINITY=wait until completion*/);
}
// The operation has finished, safe to examine the fields of the 'fetch' pointer now.
if (fetch->status == 200) {
printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
// The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
} else {
printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
}
emscripten_fetch_close(fetch);
}
Waitable fetches allow interleaving multiple tasks in one thread so that the
issuing thread can perform some other work until the fetch completes.
.. note::
Waitable fetches are available only in certain build modes:
- **No flags** or **--proxy-to-worker**: Waitable fetches are not available.
- **-s USE_PTHREADS=1**: Waitable fetches are available on pthreads, but not
on the main thread.
- **--proxy-to-worker** + **-s USE_PTHREADS=1**: Waitable fetches are
available on all threads.
Tracking Progress
====================
For robust fetch management, there are several fields available to track the
status of an XHR.
The onprogress callback is called whenever new data has been received. This
allows one to measure the download speed and compute an ETA for completion.
Additionally, the emscripten_fetch_t structure passes the XHR object fields
readyState, status and statusText, which give information about the HTTP loading
state of the request.
The emscripten_fetch_attr_t object has a timeoutMSecs field which allows
specifying a timeout duration for the transfer. Additionally,
emscripten_fetch_close() can be called at any time for asynchronous and waitable
fetches to abort the download (this is currently broken, see `#8234
<https://github.com/emscripten-core/emscripten/issues/8234>`_).
The following example illustrates these fields
and the onprogress handler.
.. code-block:: cpp
void downloadProgress(emscripten_fetch_t *fetch) {
if (fetch->totalBytes) {
printf("Downloading %s.. %.2f%% complete.\n", fetch->url, fetch->dataOffset * 100.0 / fetch->totalBytes);
} else {
printf("Downloading %s.. %lld bytes complete.\n", fetch->url, fetch->dataOffset + fetch->numBytes);
}
}
int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
attr.onsuccess = downloadSucceeded;
attr.onprogress = downloadProgress;
attr.onerror = downloadFailed;
emscripten_fetch(&attr, "myfile.dat");
}
Managing Large Files
====================
Particular attention should be paid to the memory usage strategy of a fetch.
Previous examples have all passed the EMSCRIPTEN_FETCH_LOAD_TO_MEMORY flag,
which causes emscripten_fetch() to populate the downloaded file in full in
memory in the onsuccess() callback. This is convenient when the whole file is to
be immediately accessed afterwards, but for large files, this can be a wasteful
strategy in terms of memory usage. If the file is very large, it might not even
fit inside the application's heap area.
The following subsections provide ways to manage large fetches in a memory
efficient manner.
Downloading directly to IndexedDB
---------------------------------
If an application wants to download a file for local access, but does not
immediately need to use the file, e.g. when preloading data up front for later
access, it is a good idea to avoid the EMSCRIPTEN_FETCH_LOAD_TO_MEMORY flag
altogether, and only pass the EMSCRIPTEN_FETCH_PERSIST_FILE flag instead. This
causes the fetch to download the file directly to IndexedDB, which avoids
temporarily populating the file in memory after the download finishes. In this
scenario, the onsuccess() handler will only report the total downloaded file
size, but will not contain the data bytes to the file.
Streaming Downloads
-------------------
Note: This currently only works in Firefox as it uses 'moz-chunked-arraybuffer'.
If the application does not need random seek access to the file, but is able to
process the file in a streaming manner, it can use the
EMSCRIPTEN_FETCH_STREAM_DATA flag to stream through the bytes in the file as
they are downloaded. If this flag is passed, the downloaded data chunks are
passed into the onprogress() callback in coherent file sequential order. See the
following snippet for an example.
.. code-block:: cpp
void downloadProgress(emscripten_fetch_t *fetch) {
printf("Downloading %s.. %.2f%%s complete. HTTP readyState: %d. HTTP status: %d.\n"
"HTTP statusText: %s. Received chunk [%llu, %llu[\n",
fetch->url, fetch->totalBytes > 0 ? (fetch->dataOffset + fetch->numBytes) * 100.0 / fetch->totalBytes : (fetch->dataOffset + fetch->numBytes),
fetch->totalBytes > 0 ? "%" : " bytes",
fetch->readyState, fetch->status, fetch->statusText,
fetch->dataOffset, fetch->dataOffset + fetch->numBytes);
// Process the partial data stream fetch->data[0] thru fetch->data[fetch->numBytes-1]
// This buffer represents the file at offset fetch->dataOffset.
for(size_t i = 0; i < fetch->numBytes; ++i)
; // Process fetch->data[i];
}
int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_STREAM_DATA;
attr.onsuccess = downloadSucceeded;
attr.onprogress = downloadProgress;
attr.onerror = downloadFailed;
attr.timeoutMSecs = 2*60;
emscripten_fetch(&attr, "myfile.dat");
}
In this case, the onsuccess() handler will not receive the final file buffer at
all so memory usage will remain at a minimum.
Byte Range Downloads
--------------------
Large files can also be managed in smaller chunks by performing Byte Range
downloads on them. This initiates an XHR or IndexedDB transfer that only fetches
the desired subrange of the whole file. This is useful for example when a large
package file contains multiple smaller ones at certain seek offsets, which can
be dealt with separately.
.. code-block:: cpp
#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>
void downloadSucceeded(emscripten_fetch_t *fetch) {
printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
// The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
emscripten_fetch_close(fetch); // Free data associated with the fetch.
}
void downloadFailed(emscripten_fetch_t *fetch) {
printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
emscripten_fetch_close(fetch); // Also free data on failure.
}
int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
// Make a Range request to only fetch bytes 10 to 20
const char* headers[] = {"Range", "bytes=10-20"};
attr.requestHeaders = headers;
attr.onsuccess = downloadSucceeded;
attr.onerror = downloadFailed;
emscripten_fetch(&attr, "myfile.dat");
}
TODO To Document
================
Emscripten_fetch() supports the following operations as well, that need
documenting:
- Emscripten_fetch can be used to upload files to remote servers via HTTP PUT
- Emscripten_fetch_attr_t allows setting custom HTTP request headers (e.g. for
cache control)
- Document HTTP simple auth fields in Emscripten_fetch_attr_t.
- Document how to populate to a certain filesystem path location in IndexedB,
and e.g. fopen() it via ASMFS afterwards.
- Document overriddenMimeType attribute in Emscripten_fetch_attr_t.
- Reference documentation of the individual fields in Emscripten_fetch_attr_t,
Emscripten_fetch_t and #defines.
- Example about loading only from IndexedDB without XHRing.
- Example about overriding an existing file in IndexedDB with a new XHR.
- Example how to preload a whole filesystem to IndexedDB for easy replacement
of --preload-file.
- Example how to persist content as gzipped to IndexedDB and decompress on
load.
- Example how to abort and resume partial transfers to IndexedDB.
|