const URL = "https://8014-35-223-70-178.ngrok-free.app/"; // 1
const taskChannel = new BroadcastChannel('task-channel'); // 2
taskChannel.onmessage = event => { // 3
  persistTask(event.data.data); // 4
  registration.sync.register('task-sync'); // 5
};

let db = null; // 6
let request = indexedDB.open("TaskDB", 1); // 7
request.onupgradeneeded = function(event) { // 8
  db = event.target.result; // 9
  if (!db.objectStoreNames.contains("tasks")) { // 10
    let tasksObjectStore = db.createObjectStore("tasks", { autoIncrement: true }); // 11
  }
};
request.onsuccess = function(event) { db = event.target.result; }; // 12
request.onerror = function(event) { console.log("Error in db: " + event); }; // 13

persistTask = function(task){ // 14
  let transaction = db.transaction("tasks", "readwrite");
  let tasksObjectStore = transaction.objectStore("tasks");
  let addRequest = tasksObjectStore.add(task);
  addRequest.onsuccess = function(event){ console.log("Task added to DB"); };
  addRequest.onerror = function(event) { console.log(“Error: “ + event); };
}
self.addEventListener('sync', async function(event) { // 15
  if (event.tag == 'task-sync') {
    event.waitUntil(new Promise((res, rej) => { // 16
      let transaction = db.transaction("tasks", "readwrite");
      let tasksObjectStore = transaction.objectStore("tasks");
      let cursorRequest = tasksObjectStore.openCursor();
      cursorRequest.onsuccess = function(event) { // 17
        let cursor = event.target.result;
        if (cursor) {
          let task = cursor.value; // 18
          fetch(URL + 'todos/add', // a
            { method: 'POST', 
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ "task" : task }) 
            }).then((serverResponse) => {
              console.log("Task saved to backend.");
              deleteTasks(); // b
              res(); // b
            }).catch((err) => {
              console.log("ERROR: " + err);
              rej(); //c
            })
          }
        } 
    }))
  }
})
async function deleteTasks() { // 19
  const transaction = db.transaction("tasks", "readwrite");
  const tasksObjectStore = transaction.objectStore("tasks");
  tasksObjectStore.clear();
  await transaction.complete;
}

Agora vamos falar sobre o que está acontecendo neste código.

  1. Precisamos rotear nossas solicitações pelo mesmo túnel seguro que criamos com ngrokentão salvamos a URL aqui.
  2. Crie o canal de transmissão com o mesmo nome para que possamos ouvir as mensagens.
  3. Aqui, estamos observando task-channel eventos de mensagem. Ao responder a esses eventos, fazemos duas coisas:
  4. Chamar persistTask() para salvar a nova tarefa em IndexedDB.
  5. Registre um novo sync event. É isso que invoca a capacidade especial de tentar novamente solicitações de forma inteligente. O manipulador de sincronização nos permite especificar uma promessa de que ele tentará novamente quando a rede estiver disponível e implementa uma estratégia de recuo e condições de desistência.
  6. Feito isso, criamos uma referência para nosso objeto de banco de dados.
  7. Obtenha uma “solicitação” para o identificador em nosso banco de dados. Tudo em IndexedDB é tratado de forma assíncrona. (Para uma excelente visão geral de IndexedDBrecomendo esta série.)
  8. O onupgradeneeded O evento é disparado se estivermos acessando um banco de dados novo ou com versão atualizada.
  9. Dentro onupgradeneededobtemos um controle sobre o próprio banco de dados, com nosso global db objeto.
  10. Se a coleção de tarefas não estiver presente, criamos a coleção de tarefas.
  11. Se o banco de dados foi criado com sucesso, nós o salvamos em nosso db objeto.
  12. Registre o erro se a criação do banco de dados falhar.
  13. O persistTask() função chamada pelo evento de transmissão add-task (4). Isso simplesmente coloca o novo valor da tarefa na coleção de tarefas.
  14. Nosso evento de sincronização. Isso é chamado pelo evento de transmissão (5). Verificamos o event.tag campo sendo task-sync então sabemos que é nosso evento de sincronização de tarefas.
  15. event.waitUntil() nos permite contar o serviceWorker que não terminamos até que Promise dentro dele completa. Como estamos em um evento de sincronização, isso tem um significado especial. Em particular, se nosso Promise falhar, o algoritmo de sincronização continuará tentando. Além disso, lembre-se de que se a rede não estiver disponível, ele esperará até que fique disponível.
    1. Nós definimos um novo Promisee dentro dele começamos abrindo uma conexão com o banco de dados.
  16. Dentro do banco de dados onsuccess retorno de chamada, obtemos um cursor e o usamos para pegar a tarefa que salvamos. (Estamos aproveitando nosso wrapper Promise para lidar com chamadas assíncronas aninhadas.)
  17. Agora temos uma variável com o valor da nossa tarefa de transmissão nela. Com isso em mãos:
    1. Nós emitimos um novo fetch solicitação para nosso expressJS /todos/add ponto final.
    2. Observe que se a solicitação for bem-sucedida, excluímos a tarefa do banco de dados e chamamos res() para resolver nossa promessa externa.
    3. Se a solicitação falhar, ligamos rej(). Isso rejeitará a promessa contida, informando à API de sincronização que a solicitação deve ser repetida.
  18. O deleteTasks() O método auxiliar exclui todas as tarefas do banco de dados. (Este é um exemplo simplificado que assume uma tasks criação de cada vez.)

Claramente, há muito nisso, mas a recompensa é poder repetir solicitações sem esforço em segundo plano sempre que nossa rede estiver irregular. Lembre-se, estamos obtendo isso no navegador, em todos os tipos de dispositivos, móveis e outros.

Testando o exemplo PWA

Se você executar o PWA agora e criar uma tarefa, ela será enviada para o back-end e salva. O teste interessante é abrir o devtools (F12) e desabilitar a rede. Você pode encontrar a opção “Offline” no menu “throttling” da aba de rede assim: