Offline first with progressive web apps [ Part 3 / 3 ]: Background sync

IndexedDB: What is it?

Small promise based IndexedDB framework

const DB_VERSION = 1;
const DB_NAME = "vid-voter";

const openDB = () => {
return new Promise((resolve, reject) => {
if (!window.indexedDB) {
reject("IndexedDB not supported");
}

const request = window.indexedDB.open(DB_NAME, DB_VERSION);

request.onerror = (event) => {
reject("DB error: " + event.target.error);
};

request.onupgradeneeded = (event) => {
const db = event.target.result;

if (!db.objectStoreNames.contains("votes")) {
db.createObjectStore("votes", {keyPath: "id"});
}
};

request.onsuccess = (event) => {
resolve(event.target.result);
};
});
};
const openObjectStore = (db, name, transactionMode) => {
return db.transaction(name, transactionMode).objectStore(name);
};
const addObject = (storeName, object) => {
return new Promise((resolve, reject) => {
openDB().then(db => {
openObjectStore(db, storeName, "readwrite")
.add(object)
.onsuccess = resolve;
}).catch(reason => reject(reason));
});
};
const updateObject = (storeName, id, object) => {
return new Promise((resolve, reject) => {
openDB().then(db => {
openObjectStore(db, storeName, "readwrite")
.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
reject(`No object store found for '${storeName}'`)
}

if (cursor.value.id === id) {
cursor.update(object).onsuccess = resolve;
}

cursor.continue();
}
}).catch(reason => reject(reason));
});
};
const deleteObject = (storeName, id) => {
return new Promise((resolve, reject) => {
openDB().then(db => {
openObjectStore(db, storeName, "readwrite")
.delete(id)
.onsuccess = resolve;
}).catch(reason => reject(reason));
});
};
const getVideos = () => {
return new Promise(resolve => {
openDB().then(db => {
const store = openObjectStore(db, "videos", "readwrite");
const videos = [];
store.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
videos.push(cursor.value);
cursor.continue();
} else {
if (videos.length > 0) {
resolve(videos);
} else {
getVideosFromServer().then((videos) => {
for (const video of videos) {
addObject("videos", video);
}
resolve(videos);
});
}
}
}
}).catch(function() {
getVideosFromServer().then((videos) => {
resolve(videos);
});
});
});
};

const getVideosFromServer = () => {
return new Promise((resolve) => $.getJSON("http://localhost:3000/videos", resolve));
};

It is important that when you create new objects, you create them in IndexedDB and on the server side

Render videos from IndexedDB

<script src="js/promise-based-indexedDB.js"></script>
const loadVideoList = () => {
getVideos().then(renderVideos);
};

Add a video

const addVideo = () => {
const titleInput = $("#title");
const urlInput = $("#url");
const postData = {
id: Date.now().toString().substring(3, 11),
title: titleInput.val(),
link: urlInput.val(),
points: 0
};

titleInput.val('');
urlInput.val('');

// Add video to the object store
addObject("videos", postData)
.catch(e => console.error(e));

$.ajax({
type: 'POST',
url: 'http://localhost:3000/videos',
data: JSON.stringify(postData),
success: renderVideos,
contentType: 'application/json',
dataType: 'json'
});
};
IndexedDB Store in dev tools
IndexedDB Store in dev tools
const getVideosFromServer = () => {
return new Promise((resolve) => {
if (self.$) {
$.getJSON("http://localhost:3000/videos", resolve)
} else if (self.fetch) {
fetch("http://localhost:3000/videos").then((response) => {
return response.json();
}).then(function (videos) {
resolve(videos);
});
}
}
);
};

Background Sync

Can I use: Background Sync

Adding Background Sync to our app

const addVideo = () => {
const titleInput = $("#title");
const urlInput = $("#url");
const postData = {
id: +Date.now().toString().substring(3, 11),
title: titleInput.val(),
link: urlInput.val(),
points: 0,
status: "sending"
};

titleInput.val('');
urlInput.val('');

// Add video to the object store
addObject("videos", postData)
.catch(e => console.error(e));

if ("serviceWorker" in navigator && "SyncManager" in window) {
navigator.serviceWorker.ready.then(function(registration) {
registration.sync.register("sync-videos");
});
} else {
$.ajax({
type: 'POST',
url: 'http://localhost:3000/videos',
data: JSON.stringify(postData),
success: renderVideos,
contentType: 'application/json',
dataType: 'json'
});
}
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
const upgradeTransaction = event.target.transaction;
let videoStore;

if (!db.objectStoreNames.contains("videos")) {
videoStore = db.createObjectStore("videos", {keyPath: "id"});
} else {
videoStore = upgradeTransaction.objectStore("videos");
}

if (!videoStore.indexNames.contains("idx_status")) {
videoStore.createIndex("idx_status", "status", { unique: false });
}

};

Do not forget to increase your DB_VERSION variable!

const getVideos = (indexName, indexValue) => {
return new Promise(resolve => {
openDB().then(db => {
const store = openObjectStore(db, "videos", "readwrite");
const videos = [];

const openCursor = indexName && indexValue ?
store.index(indexName).openCursor(indexValue) :
store.openCursor();

openCursor.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
videos.push(cursor.value);
cursor.continue();
} else {
if (videos.length > 0) {
resolve(videos);
} else {
getVideosFromServer().then((videos) => {
for (const video of videos) {
addObject("videos", video);
}
resolve(videos);
});
}
}
}
}).catch(function (e) {
console.error(e);
getVideosFromServer().then((videos) => {
resolve(videos);
});
});
});
};
self.addEventListener("sync", function(event) {
if (event.tag === "sync-videos") {
event.waitUntil(syncVideos());
}
});
const syncVideos = () => {
return getVideos("idx_status", "sending").then((videos) => {
return Promise.all(videos.map((video) => {
return syncVideo(video)
.then((newVideo) => updateObject("videos", newVideo.id, newVideo));
})
);
});
};

const syncVideo = (video) => {
return fetch('http://localhost:3000/videos', {
method: 'post',
body: JSON.stringify(video)
}).then(function (response) {
return response.json();
})
};

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store