pas2js as web worker calling WebAssembly SQLite

This page hosts two Pascal programs transpiled by pas2js into Javascript. The driver program, wasqliteapp, is loaded by this page. It then loads the worker program, wasqliteworker, and acts as the gateway between the worker and this page's DOM.

As per using web workers, wasqliteworker runs in its own thread. Running SQLite operations and/or other heavy computation in a web worker avoids bogging down the web browser's main UI thread, keep the browser responsive.

The worker has no direct access to the DOM, and relies on Javascript's postMessage() API to exchange data with the driver program. Below is the worker's output, written to the DOM by the driver. Reload this page if the output doesn't appear automatically; this may have to do with caching by your web browser, caching cleverness by HTMX, reverse proxy caching on my site, or other web weirdness.

This is placeholder text. Reload this web page to see pas2js and SQLite in action.

If you open your web browser's console, you will see SQL trace statements generated by SQLite. Tracing is controlled by an option flag when opening the database.

SQL TRACE #1 via sqlite3@651704: create table if not exists t (a, b)
SQL TRACE #2 via sqlite3@651704: delete from t
SQL TRACE #3 via sqlite3@651704: insert into t (a, b) values (?, ?)

wasqliteapp is implemented as a pas2js web browser application, and is loaded by this page thusly:

<html>
  <head>
    ...
    <script type="application/javascript" src="/demo/wasqliteapp.js"></script>
    ...
  </head>
  <body>
    ...
    <script type="application/javascript">rtl.run()</script>
    ...
  </body>
</html>

The main program is straightforward:

begin
  // Step 1: Create worker, loading the worker program. Note the `sqlite3.dir` query parameter.
  Worker := TJSWorker.New('/demo/wasqliteworker.js?sqlite3.dir=/dist');

  // Step 2: Create listener for messages from the worker.
  Worker.AddEventListener('message', @MessageHandler);

  // There is no step 3.
end.

See relative URI resolution when loading sqlite3.js from a worker for more information on the sqlite3.dir query parameter in the URL /worker-opfs/wasqliteworker.js?sqlite3.dir=/dist.

As for wasqliteworker, below are the notable differences between it and the main thread program:

  • It cannot use {$linklib ...} compiler directive to load sqlite3.mjs, SQLite's Javascript module file. Instead, it uses Javascript importScripts() via a pas2js asm block to load sqlite3.js, SQLite's Javascript API packaged as a plain old Javascript source file.

  • It cannot update the DOM directly. Instead it uses Javascript's postMessage() API, expressed as WebWorker.Self_.PostMessage() in Pascal, to send data to wasqliteapp. To receive data from the driver, wasqliteworker needs to implement its own message handler, not done in this demo.

  • It uses the Origin-Private FileSystem, OPFS for persistent storage in the web browser. OPFS is optimized for performance and its files are not visible to the web browser user, unlike files in the regular filesystem.

Below is the bulk of wasqliteworker:

var
  db: TWSQLiteOpfsDB;

procedure WorkerRun; async;
var
  args: JSValue;
  i: Integer;
  rows: TJSArray;
  obj: TJSObject;
begin
  asm
    importScripts('/dist/sqlite3.js');
  end;
  await(InitSQLite);
  WebWorker.Self_.PostMessage(Format('Loaded SQLite %s', [TWSQLiteCAPI.LibVersion]));

  args := new(['filename', 'demo.db', 'flags', 'ct']); // 't' flags enables SQLite tracing
  db := TWSQLiteOpfsDB.New(args);
  WebWorker.Self_.PostMessage('Create database file in OPFS');

  WebWorker.Self_.PostMessage('Create a table...');
  db.Exec('create table if not exists t (a, b)');

  WebWorker.Self_.PostMessage('Delete any data left from previous runs');
  db.Exec('delete from t');

  WebWorker.Self_.PostMessage('Insert some data using exec()');
  for i := 20 to 25 do
    begin
      args := new([
        'sql', 'insert into t (a, b) values (?, ?)',
        'bind', TJSArray._of(i, i*2)
      ]);
      db.Exec(args);
    end;

  WebWorker.Self_.PostMessage('Query data with exec() returning rows of objects');
  rows := TJSArray.New();
  args := new([
    'sql', 'select a, b from t order by b desc limit 4',
    'rowMode', 'object',
    'resultRows', rows
  ]);
  db.Exec(args);
  for i := 0 to 3 do // SQL returned limit 4
    begin
      obj := ToObject(rows[i]);
      WebWorker.Self_.PostMessage(Format('a = %s, b = %s', [String(obj['a']), String(obj['b'])]));
    end;

  db.Close;
end;

begin
  WorkerRun;
end.