From 518750d6089f0ed6ab5b5bbd19ad735ca36832d4 Mon Sep 17 00:00:00 2001 From: CMEONE Date: Sat, 6 Mar 2021 12:35:02 -0800 Subject: [PATCH] Full offline functionality --- example/config.js | 10 ++-- example/views/about.html | 2 +- tApp-service-worker.js | 112 +++++++++++++++++++++++++++++++++++++++ tApp.js | 106 ++++++++++++++++++------------------ 4 files changed, 172 insertions(+), 58 deletions(-) create mode 100644 tApp-service-worker.js diff --git a/example/config.js b/example/config.js index ad89da7..0039540 100644 --- a/example/config.js +++ b/example/config.js @@ -7,9 +7,7 @@ tApp.configure({ forbidden: "#/403" }, caching: { - maxBytes: 5 * 1000 * 1000, // around 5MB in bytes (5 MB * 1000 KB/MB * 1000 bytes/KB) - updateCache: 5 * 60 * 1000, // updates the cache every 5 minutes in milliseconds (5 minutes * 60 seconds/minute * 1000 seconds/millisecond) - backgroundPages: ["./views/index.html", "./views/about.html"], + backgroundPages: ["./", "./config.js", "/tApp.js", "./views/index.html", "./views/about.html"], persistent: true } }); @@ -51,4 +49,8 @@ tApp.route("#/403", function(request) { `); }); -tApp.start(); \ No newline at end of file +tApp.start().then(() => { + tApp.install().then(() => { + tApp.update(); + }) +}); \ No newline at end of file diff --git a/example/views/about.html b/example/views/about.html index 95f84ea..225d60d 100644 --- a/example/views/about.html +++ b/example/views/about.html @@ -6,5 +6,5 @@

About

  • Caching allows your tApp to be blazingly fast. You can set custom cache size and time limits in your code, and tApp will save loaded pages in the cache so that they load instantly the next time the user navigates to that section of the tApp.
  • This tApp utilizes persistent caching, so after the very first page load, all requests are redirected to the cache until the cache time expires.
  • Routes can be specified to automatically load and cache asynchronously in the background when the user first loads your tApp, making the next page loads instantaneous.
  • -
  • tApps can work offline, just like normal apps. Since all server-like routing and computation can be handled in the client through tApp, users can browse the tApp offline. This offline functionality is not fully implemented but will be integrated in a later release of the library.
  • +
  • tApps can work offline, just like normal apps. Since all server-like routing and computation can be handled in the client through tApp, users can load browse the tApp offline at a later time after only loading the tApp once.
  • \ No newline at end of file diff --git a/tApp-service-worker.js b/tApp-service-worker.js new file mode 100644 index 0000000..3e2fc59 --- /dev/null +++ b/tApp-service-worker.js @@ -0,0 +1,112 @@ +var version = 'v16::'; + +self.addEventListener("install", function(event) { + event.waitUntil(() => { + self.skipWaiting(); + }); +}); + +self.addEventListener("fetch", function(event) { + let fetchResponse = (async () => { + return new Promise((resolve, reject) => { + if (event.request.method !== 'GET') { + // Handle other requests such as POST here + return; + } + let url = event.request.url.split("#")[0]; + let requestInit = indexedDB.open("tAppCache", 5); + let db; + function myFetch(page) { + return new Promise((resolve, reject) => { + fetch(page).then((response) => { + resolve(response); + }).catch(() => { + resolve(new Response("", { + status: 500, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/html' + }) + })); + }); + }); + } + requestInit.onupgradeneeded = function() { + db = requestInit.result; + if(!db.objectStoreNames.contains("cachedPages")) { + db.createObjectStore("cachedPages"); + } + if(!db.objectStoreNames.contains("offlineStorage")) { + db.createObjectStore("offlineStorage"); + } + } + requestInit.onsuccess = async function() { + db = requestInit.result; + function setCachedPage(fullPath, value) { + return new Promise((resolve, reject) => { + let request = db.transaction(["cachedPages"], "readwrite").objectStore("cachedPages").put(value, fullPath); + request.onerror = () => { + reject("tAppError: Persistent caching is not available in this browser."); + }; + request.onsuccess = () => { + resolve(true); + } + }); + } + function getCachedPage(fullPath) { + return new Promise((resolve, reject) => { + let request = db.transaction(["cachedPages"], "readwrite").objectStore("cachedPages").get(fullPath); + request.onerror = (err) => { + myFetch(url).then((response) => { + resolve(response); + }); + }; + request.onsuccess = () => { + myFetch(url).then((response) => { + if(response.status === 200) { + response.text().then((text) => { + + setCachedPage(url, { + data: text, + cachedAt: new Date().getTime() + }); + }).catch(() => { + + }); + } + }); + if(request.result != null) { + resolve(new Response(request.result.data, { + status: 200, + statusText: 'OK', + headers: new Headers({ + 'Content-Type': 'text/html' + }) + })); + } else { + myFetch(url).then((response) => { + resolve(response); + }); + } + }; + }); + } + getCachedPage(url).then((response) => { + resolve(response); + }); + }; + requestInit.onerror = (err) => { + myFetch(url).then((response) => { + resolve(response); + }); + } + }); + })(); + event.respondWith(fetchResponse); +}); + +self.addEventListener("activate", function(event) { + event.waitUntil(() => { + + }); +}); diff --git a/tApp.js b/tApp.js index 63b4a48..ec2cd13 100644 --- a/tApp.js +++ b/tApp.js @@ -7,7 +7,7 @@ class tApp { static database; static currentHash = "/"; static get version() { - return "v0.5.1"; + return "v0.6.0"; } static configure(params) { if(params == null) { @@ -61,12 +61,6 @@ class tApp { error: "tAppError: Invalid configure parameter, caching.backgroundPages is not of type Array." } } - if(params.caching != null && params.caching.maxBytes == null) { - params.caching.maxBytes = Infinity; - } - if(params.caching != null && params.caching.updateCache == null) { - params.caching.updateCache = Infinity; - } if(params.caching != null && params.caching.backgroundPages == null) { params.caching.backgroundPages = []; } @@ -199,27 +193,6 @@ class tApp { }); } - static updateCacheSize() { - return new Promise(async (resolve, reject) => { - let cachedData = await tApp.getCachedPages(); - let keys = Object.keys(cachedData); - let size = 0; - for(let i = 0; i < keys.length; i++) { - size += new Blob([keys[i]]).size; - size += new Blob([cachedData[keys[i]].data]).size; - } - while(size > tApp.config.caching.maxBytes) { - let num = Math.floor(Math.random() * keys.length); - if(num < keys.length) { - let removedPage = await tApp.removeCachedPage(keys[num]); - size -= new Blob([keys[num]]).size; - size -= new Blob([removedPage.data]).size; - } - } - tApp.cacheSize = size; - resolve(); - }); - } static getOfflineData(key) { return new Promise((resolve, reject) => { let request = tApp.database.transaction(["offlineStorage"], "readwrite").objectStore("offlineStorage").get(key); @@ -291,18 +264,19 @@ class tApp { return new Promise(async (resolve, reject) => { let fullPath = new URL(path, window.location.href).href; let cachedPage = await tApp.getCachedPage(fullPath); - if(cachedPage == null || cachedPage.cachedAt + tApp.config.caching.updateCache < new Date().getTime()) { - let xhr = new XMLHttpRequest(); - xhr.onreadystatechange = async () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - tApp.setCachedPage(fullPath, { - data: xhr.responseText, - cachedAt: new Date().getTime() - }); - tApp.updateCacheSize(); + let xhr = new XMLHttpRequest(); + xhr.onreadystatechange = async () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + tApp.setCachedPage(fullPath, { + data: xhr.responseText, + cachedAt: new Date().getTime() + }); + if(cachedPage == null) { resolve(xhr.responseText); - } else { + } + } else { + if(cachedPage == null) { reject({ error: "GET " + xhr.responseURL + " " + xhr.status + " (" + xhr.statusText + ")", errorCode: xhr.status @@ -310,9 +284,10 @@ class tApp { } } } - xhr.open("GET", path, true); - xhr.send(null); - } else { + } + xhr.open("GET", path, true); + xhr.send(null); + if(cachedPage != null) { resolve(cachedPage.data); } }); @@ -365,15 +340,6 @@ class tApp { for(let i = 0; i < tApp.config.caching.backgroundPages.length; i++) { tApp.get(tApp.config.caching.backgroundPages[i]); } - if(tApp.config.caching.updateCache < 60000) { - setTimeout(() => { - tApp.loadBackgroundPages(); - }, tApp.config.caching.updateCache); - } else { - setTimeout(() => { - tApp.loadBackgroundPages(); - }, 60000); - } } static start() { return new Promise((resolve, reject) => { @@ -398,15 +364,16 @@ class tApp { }, false); tApp.updatePage(window.location.hash); tApp.loadBackgroundPages(); + resolve(true); }; request.onsuccess = async (event) => { tApp.database = request.result; - await tApp.updateCacheSize(); window.addEventListener("hashchange", () => { tApp.updatePage(window.location.hash); }, false); tApp.updatePage(window.location.hash); tApp.loadBackgroundPages(); + resolve(true); }; request.onupgradeneeded = (event) => { @@ -424,11 +391,44 @@ class tApp { }, false); tApp.updatePage(window.location.hash); tApp.loadBackgroundPages(); + resolve(true); } - resolve(true); } else { reject("tAppError: tApp has already started."); } }); } + static install(pathToServiceWorker = '/tApp-service-worker.js') { + return new Promise((resolve, reject) => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register(pathToServiceWorker).then(function() { + resolve(true); + }, function() { + reject("tAppError: Unable to install full offline functionality, an error occurred."); + }); + } else { + reject("tAppError: Full offline functionality is not supported for this website. This issue can occur in unsupported browsers (such as IE) or in insecure contexts (HTTPS/SSL is required for full offline functionality)."); + } + }); + } + static uninstall() { + return new Promise((resolve, reject) => { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + for(let registration of registrations) { + registration.unregister() + } + resolve(true); + }); + }); + } + static update() { + return new Promise((resolve, reject) => { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + for(let registration of registrations) { + registration.update() + } + resolve(true); + }); + }); + } } \ No newline at end of file