\n
if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('\/service-worker.js')\n .then(reg => console.log('Service Worker registered', reg))\n .catch(err => console.error('Service Worker registration failed', err));\n}\n<\/code><\/pre>\n<\/div>\nChecking For Network Connectivity<\/h3>\n
Remember, the problem we are trying to solve is caused by unreliable network connectivity<\/strong>. If this problem does not exist, there is no point in trying to solve anything. Therefore, once the image is selected, we need to check if the user has a reliable internet connection before registering a sync event and storing the image in IndexedDB<\/code>.<\/p>\nfunction uploadImage() {\n if (navigator.onLine) {\n \/\/ Upload Image\n } else {\n \/\/ register Sync Event\n \/\/ Store Images in IndexedDB\n }\n}\n<\/code><\/pre>\nNote<\/strong>: I\u2019m only using the navigator.onLine<\/code> property here to demonstrate how the system would work. The navigator.onLine<\/code> property is unreliable<\/strong>, and I would suggest you come up with a custom solution to check whether the user is connected to the internet or not. One way you can do this is by sending a ping request to a server endpoint you\u2019ve created.<\/p>\nRegistering The Sync Event<\/h3>\n
Once the network test fails, the next step is to register a sync event. The sync event needs to be registered at the point where the system fails to upload the image due to a poor internet connection.<\/p>\n
async function registerSyncEvent() {\n if ('SyncManager' in window) {\n const registration = await navigator.serviceWorker.ready;\n await registration.sync.register('uploadImages');\n console.log('Background Sync registered');\n }\n}\n<\/code><\/pre>\nAfter registering the sync event, you need to listen for it in the service worker.<\/p>\n
self.addEventListener('sync', (event) => {\n if (event.tag === 'uploadImages') {\n event.waitUntil(sendImages());\n }\n});\n<\/code><\/pre>\nThe sendImages<\/code> function is going to be an asynchronous process that will retrieve the image from IndexedDB<\/code> and upload it to the server. This is what it\u2019s going to look like:<\/p>\nasync function sendImages() {\n try {\n \/\/ await image retrieval and upload\n } catch (error) {\n \/\/ throw error\n }\n}\n<\/code><\/pre>\nOpening The Database<\/h3>\n
The first thing we need to do in order to store our image locally is to open an IndexedDB<\/code> store. As you can see from the code below, we are creating a global variable to store the database instance<\/strong>. The reason for doing this is that, subsequently, when we want to retrieve our image from IndexedDB<\/code>, we wouldn\u2019t need to write the code to open the database again.<\/p>\n\n
let database; \/\/ Global variable to store the database instance\n\nfunction openDatabase() {\n return new Promise((resolve, reject) => {\n if (database) return resolve(database); \/\/ Return existing database instance \n\n const request = indexedDB.open(\"myDatabase\", 1);\n\n request.onerror = (event) => {\n console.error(\"Database error:\", event.target.error);\n reject(event.target.error); \/\/ Reject the promise on error\n };\n\n request.onupgradeneeded = (event) => {\n const db = event.target.result;\n \/\/ Create the \"images\" object store if it doesn't exist.\n if (!db.objectStoreNames.contains(\"images\")) {\n db.createObjectStore(\"images\", { keyPath: \"id\" });\n }\n console.log(\"Database setup complete.\");\n };\n\n request.onsuccess = (event) => {\n database = event.target.result; \/\/ Store the database instance globally\n resolve(database); \/\/ Resolve the promise with the database instance\n };\n });\n}\n<\/code><\/pre>\n<\/div>\n<\/div>\n
Storing The Image In IndexedDB<\/h3>\n
With the IndexedDB<\/code> store open, we can now store our images.<\/p>\nNow, you may be wondering why an easier solution like localStorage<\/code> wasn\u2019t used for this purpose.<\/p>\nThe reason for that is that IndexedDB<\/code> operates asynchronously and doesn\u2019t block the main JavaScript thread, whereas localStorage<\/code> runs synchronously and can block the JavaScript main thread if it is being used.<\/p><\/blockquote>\nHere\u2019s how you can store the image in IndexedDB<\/code>:<\/p>\n\n
async function storeImages(file) {\n \/\/ Open the IndexedDB database.\n const db = await openDatabase();\n \/\/ Create a transaction with read and write access.\n const transaction = db.transaction(\"images\", \"readwrite\");\n \/\/ Access the \"images\" object store.\n const store = transaction.objectStore(\"images\");\n \/\/ Define the image record to be stored.\n const imageRecord = {\n id: IMAGE_ID, \/\/ a unique ID\n image: file \/\/ Store the image file (Blob)\n };\n \/\/ Add the image record to the store.\n const addRequest = store.add(imageRecord);\n \/\/ Handle successful addition.\n addRequest.onsuccess = () => console.log(\"Image added successfully!\");\n \/\/ Handle errors during insertion.\n addRequest.onerror = (e) => console.error(\"Error storing image:\", e.target.error);\n}\n<\/code><\/pre>\n<\/div>\nWith the images stored and the background sync set, the system is ready to upload the image whenever the network connection is restored.<\/p>\n
Retrieving And Uploading The Images<\/h3>\n
Once the network connection is restored, the sync event will fire, and the service worker will retrieve the image from IndexedDB<\/code> and upload it.<\/p>\n\n
async function retrieveAndUploadImage(IMAGE_ID) {\n try {\n const db = await openDatabase(); \/\/ Ensure the database is open\n const transaction = db.transaction(\"images\", \"readonly\");\n const store = transaction.objectStore(\"images\");\n const request = store.get(IMAGE_ID);\n request.onsuccess = function (event) {\n const image = event.target.result;\n if (image) {\n \/\/ upload Image to server here\n } else {\n console.log(\"No image found with ID:\", IMAGE_ID);\n }\n };\n request.onerror = () => {\n console.error(\"Error retrieving image.\");\n };\n } catch (error) {\n console.error(\"Failed to open database:\", error);\n }\n}\n<\/code><\/pre>\n<\/div>\nDeleting The IndexedDB Database<\/h3>\n
Once the image has been uploaded, the IndexedDB<\/code> store is no longer needed. Therefore, it should be deleted along with its content to free up storage.<\/p>\n\n
function deleteDatabase() {\n \/\/ Check if there's an open connection to the database.\n if (database) {\n database.close(); \/\/ Close the database connection\n console.log(\"Database connection closed.\");\n }\n\n \/\/ Request to delete the database named \"myDatabase\".\n const deleteRequest = indexedDB.deleteDatabase(\"myDatabase\");\n\n \/\/ Handle successful deletion of the database.\n deleteRequest.onsuccess = function () {\n console.log(\"Database deleted successfully!\");\n };\n\n \/\/ Handle errors that occur during the deletion process.\n deleteRequest.onerror = function (event) {\n console.error(\"Error deleting database:\", event.target.error);\n };\n\n \/\/ Handle cases where the deletion is blocked (e.g., if there are still open connections).\n deleteRequest.onblocked = function () {\n console.warn(\"Database deletion blocked. Close open connections and try again.\");\n };\n}\n<\/code><\/pre>\n<\/div>\nWith that, the entire process is complete!<\/p>\n
<\/div>\n
Considerations And Limitations<\/h2>\n
While we\u2019ve done a lot to help improve the experience by supporting offline uploads, the system is not without its limitations. I figured I would specifically call those out because it\u2019s worth knowing where this solution might fall short of your needs.<\/p>\n
\n- No Reliable Internet Connectivity Detection<\/strong>
\nJavaScript does not provide a foolproof way to detect online status. For this reason, you need to come up with a custom solution for detecting online status.<\/li>\n- Chromium-Only Solution<\/strong>
\nThe Background Sync API is currently limited to Chromium-based browsers<\/a>. As such, this solution is only supported by Chromium browsers. That means you will need a more robust solution if you have the majority of your users on non-Chromium browsers.<\/li>\nIndexedDB<\/code> Storage Policies<\/strong>
\nBrowsers impose storage limitations and eviction policies for IndexedDB<\/code>. For instance, in Safari, data stored in IndexedDB<\/code> has a lifespan of seven days if the user doesn\u2019t interact with the website. This is something you should bear in mind if you do come up with an alternative for the background sync API that supports Safari.<\/li>\n<\/ul>\nEnhancing The User Experience<\/h2>\n
Since the entire process happens in the background, we need a way to inform the users when images are stored, waiting to be uploaded, or have been successfully uploaded. Implementing certain UI elements<\/strong> for this purpose will indeed enhance the experience for the users. These UI elements may include toast notifications, upload status indicators like spinners (to show active processes), progress bars (to show state progress), network status indicators, or buttons to provide retry and cancel options.<\/p>\nWrapping Up<\/h2>\n
Poor internet connectivity can disrupt the user experience of a web application. However, by leveraging PWA technologies such as IndexedDB<\/code>, service workers, and the Background Sync API, developers can help improve the reliability of web applications for their users, especially those in areas with unreliable internet connectivity.<\/p>\n\n

\n
(gg, yk)<\/span>\n<\/div>\n<\/article>\n","protected":false},"excerpt":{"rendered":"Building An Offline-Friendly Image Upload System Building An Offline-Friendly Image Upload System Amejimaobari Ollornwi 2025-04-23T10:00:00+00:00 2025-05-01T09:03:46+00:00 So, you\u2019re filling out an online form, and it asks you to upload a file. You click the input, select a file from your desktop, and are good to go. But something happens. The network drops, the file disappears, […]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[],"_links":{"self":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts\/1206"}],"collection":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/comments?post=1206"}],"version-history":[{"count":1,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts\/1206\/revisions"}],"predecessor-version":[{"id":1207,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/posts\/1206\/revisions\/1207"}],"wp:attachment":[{"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/media?parent=1206"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/categories?post=1206"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.azxyfun.com\/index.php\/wp-json\/wp\/v2\/tags?post=1206"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}