In this series, we develop an offline-capable SvelteKit application utilizing SQLite as a client-side storage solution. In this part, we optimize the communication between the main thread and the Web Worker and create the client data tables.
In the previous instalment of our blog series, we created a Web Worker that manages the SQLite database. The main thread communicates with the Worker through messages. This is an efficient and straightforward method for communication between threads.
When we send a message to the worker we wait for a response that contains the result of the operation. This is implemented with an event listener:
This approach works effectively when operations run sequentially. But as we enhance the capabilities of the Worker we will introduce multiple concurrent reads, writes, and checks. Then we have the problem of not knowing which response belongs to which request.
Another thing we can do better is to offer an async API for the worker. By doing so, we can just await requests to our worker, resulting in cleaner and more comprehensible code:
I call this new system the MessageBus. It is a simple wrapper for both the postMessage and addEventListener functions, while also providing an asynchronous API for messaging the worker.
We create the function sendMsgToWorker that returns a promise. Now we have the ability to await the response from the worker. Each request is assigned a unique messageId, which we use alongside a compact callback function to resolve once the worker sends a response. To ensure easy access to this callback function, we store it in a Map using the messageId as the key.
Within the same file, we have the one and only event listener for the worker. As the worker sends a message, it must return the accompanying messageId from the request. We then retrieve the callback corresponding to that that messageId from the Map. By calling this callback and passing the worker’s data, we successfully resolve the promise initially created in the sendMsgToWorker function.
To enable offline data accessibility, we first require a data source. For this purpose, we’ll utilize the Northwind dataset, which encompasses structures such as customers, orders, and products. Our initial focus will be on making the customers table available for offline use.
As for the backend, I am also using SQLite. While this choice isn’t dictated by the client database being SQLite, it allows us to avoid managing a seperate database server and still fully meets our requirements. Feel free to use any other data storage system that suits your preferences.
To maintain loosely coupled code, we aim to avoid having the backend send a create table statement to the client directly. Instead, we’ll transmit a generic JSON containing column information, enabling the client to generate the create table statement.
To achieve this, we’ll establish an API endpoint by creating the file src/routes/api/data/customers_v1/structure/+server.ts. Notice the storage is labeled as customers_v1, which anticipates potential future structural changes. Should that occur, we can simply create a new storage called customers_v2 and retain the old one for compatibility or until all clients have synchronized their data modifications.
In case the table has not been created, we must establish it by obtaining the structure from the backend. This can be accomplished through a fetch call to our API, followed by requesting the Worker to create the table:
To convert the structure into table source SQL, we can employ the following utility function. It utilizes the storageId as the table name, the pkColumn as the primary key and the columns to generate the column definitions: