工作线程和 Promise (又名 Worker1 和 Promiser)

sqlite3 JS API 可以加载到主线程或工作线程中,并在两者中以相同的方式使用。另一种方法是在其自己的专用工作线程中加载 sqlite3,使 sqlite3 API 完全无法访问客户端代码,除非通过本文档中描述的相对有限的 API。

通过 Worker 加载 sqlite3.js 时,请务必查看关于相对 URI 的注意事项

在开始之前,让我们简要了解一下与同步接口相比,此异步接口的一些限制和痛点。

Worker1

Worker1 API 为 sqlite3 提供了一个基于 Worker 的基本接口,用于客户端应用程序代码位于一个线程中,并希望 sqlite3 在其自己的线程中运行的情况。

它基于OO1 API,并支持在工作线程中建立多个数据库连接。在开始之前,似乎有必要指出此 API 的基于 Promise 的包装器使用起来要简单得多,因为它不需要处理 postMessage(),并且使用户能够更好地控制请求和响应的流程。

为了允许此 API 加载到工作线程中而不会自动注册 onmessage 处理程序,初始化工作线程 API 需要调用 sqlite3.initWorker1API()。如果从非工作线程调用此函数,则会抛出异常。每个工作线程只能调用一次。

启动工作线程的最简单方法是

const W = new Worker('sqlite3-worker1.js'); // or equivalent
W.onmessage = function(event){
  event = event.data;
  switch(event.type){
    case 'sqlite3-api':
      if('worker1-ready' === event.result){
        // The worker is now ready to accept messages
      }
    ...
  }
};

sqlite3-worker1.js 是一个轻量级包装器,它加载 sqlite3.js、其 WASM 模块,然后调用 sqlite3.initWorker1API(),这会触发客户端应监听的工作线程消息。

{type:'sqlite3-api', result:'worker1-ready'}

这使客户端知道它已初始化。

请注意,基于工作线程的接口由于其异步特性,可能有点古怪。特别是,在开始处理任何消息之前,可能会向工作线程发布任意数量的消息。例如,如果“打开”操作失败,则其后的所有消息也可能会失败。此 API 的基于 Promise 的包装器在这方面使用起来更方便,并且使客户端能够更好地控制发布消息的顺序和速率。

Worker1 消息格式

发布到工作线程的每条消息都包含一个与操作无关的信封和与操作相关的参数。

{
  type: string, // one of: 'open', 'close', 'exec', 'config-get'

  messageId: OPTIONAL arbitrary value. The worker will copy it as-is
  into response messages to assist in client-side dispatching.

  dbId: a db identifier string (returned by 'open') which tells the
  operation which database instance to work on. If not provided, the
  first-opened db is used. This is an "opaque" value, with no
  inherently useful syntax or information. Its value is subject to
  change with any given build of this API and cannot be used as a
  basis for anything useful beyond its one intended purpose.

  args: ...operation-dependent arguments...

  // the framework may add other properties for testing or debugging
  // purposes.
}

发布回调用工作线程的线程的响应消息如下所示。

{
  type: Same as the inbound message except for error responses,
  which have the type 'error',

  messageId: same value, if any, provided by the inbound message

  dbId: the id of the db which was operated on, if any, as returned
  by the corresponding 'open' operation.

  result: ...operation-dependent result...
}

错误响应以与操作无关的格式报告。

{
  type: "error",

  messageId: ...as above...,

  dbId: ...as above...

  result: {

    operation: type of the triggering operation: 'open', 'close', ...

    message: ...error message text...

    errorClass: string. The ErrorClass.name property from the thrown exception.

    input: the message object which triggered the error.

    stack: _if available_, a stack trace array.

  }
}

Worker1 方法

可用的消息类型按字母顺序列出如下。

关闭

close 消息关闭数据库。

消息格式

{
  type: "close",
  messageId: ...as above...
  dbId: ...as above...
  args: OPTIONAL {unlink: boolean}
}

如果 dbId 不引用打开的 ID,则此操作为无操作。如果 args 对象包含真值 unlink 值,则在关闭数据库后将取消链接(删除)该数据库。无法关闭数据库(因为它未打开)或删除其文件不会触发错误。

响应

{
  type: "close",
  messageId: ...as above...,
  result: {
    filename: filename of closed db, or undefined if no db was closed
  }
}

获取配置

此操作获取 sqlite3 API 配置的可序列化部分。

消息格式

{
  type: "config-get",
  messageId: ...as above...,
  args: currently ignored and may be elided.
}

响应

{
  type: "config-get",
  messageId: ...as above...,
  result: {

    version: sqlite3.version object

    bigIntEnabled: bool. True if BigInt support is enabled.

    vfsList: result of sqlite3.capi.sqlite3_js_vfs_list()
 }
}

执行

exec 是运行任意 SQL 的接口。它是oo1.DB.exec() 方法的包装器,并支持其大部分功能。

所有 SQL 执行都通过 exec 操作处理。它提供了oo1.DB.exec() 方法的大部分功能,但由于状态必须跨线程边界,因此存在一些限制。

消息格式

{
  type: "exec",
  messageId: ...as above...
  dbId: ...as above...
  args: string (SQL) or {... see below ...}
}

响应

{
  type: "exec",
  messageId: ...as above...,
  dbId: ...as above...
  result: {
    input arguments, possibly modified. See below.
  }
}

参数采用 oo1.DB.exec() 接受的相同形式,但以下列出的例外情况除外。

函数类型 args.callback 属性无法跨窗口/工作线程边界,因此在此处无用。如果 args.callback 是字符串,则假定它是一个消息类型键,在这种情况下,将应用回调函数,该函数通过以下方式发布每行结果

postMessage({
  type: thatKeyType,
  rowNumber: 1-based-#,
  row: theRow,
  columnNames: anArray
})

row 属性包含由 rowMode 选项隐含的形式(默认为 'array')中的行结果。rowNumber 是一个基于 1 的整数,每次调用回调时递增 1。columnNames 数组包含结果行列的列名。

在结果集结束时(无论是否生成了任何结果行),它都会发布一条相同的消息,其中 (row=undefined, rowNumber=null),以提醒调用者结果集已完成。请注意,对于某些 arg.rowMode 值,null 行值是合法的行结果。

我们不使用 (row=undefined, rowNumber=undefined) 来指示结果集结束,因为获取它们将无法与从空对象中获取区分开来,除非客户端使用 hasOwnProperty()(或类似方法)来区分“缺少属性”和“具有未定义值的属性”。类似地,在某些情况下,nullrow 的合法值,而数据库层不会发出 undefined 的结果值。

回调代理不得递归进入此接口。exec() 调用将占用工作线程,导致任何递归尝试等待第一个 exec() 完成。

如果 countChanges 参数属性1 为真,则返回的对象包含的 result 属性将具有一个 changeCount 属性,该属性保存由提供的 SQL 进行的更改次数。由于 SQL 可能包含任意数量的语句,因此 changeCount 是通过在评估 SQL 前后调用 sqlite3_total_changes() 来计算的。如果 countChanges 的值为 64,则 changeCount 属性将以 BigInt 的形式返回为 64 位整数(请注意,这将在 BigInt 功能不足的构建中触发异常)。在后一种情况下,更改次数是通过在评估 SQL 前后调用 sqlite3_total_changes64() 来计算的。

响应是输入选项对象(如果仅传递字符串,则为合成对象),可能已修改。options.resultRowsoptions.columnNames 可能由对 DB.exec() 的调用填充,并且 options.changeCount 可能如上所述设置。

导出

exportsqlite3_js_db_export()的代理,它将数据库作为字节数组返回。

消息格式

{
  type: "export",
  messageId: ...as above...
  dbId: ...as above...
}

响应

{
  type: "export",
  messageId: ...as above...,
  dbId: ...as above...
  result: {
    byteArray: Uint8Array (as per sqlite3_js_db_export()),
    filename: the db filename,
    mimetype: "application/x-sqlite3"
  }
}

如果序列化由于内存不足条件而失败,则会生成错误响应。

打开

open 消息指示工作线程打开数据库。

消息格式

{
  type: "open",
  messageId: ...as above...,
  args:{

    filename [=":memory:" or "" (unspecified)]: the db filename.
      See the sqlite3.oo1.DB constructor for peculiarities and
      transformations

    vfs: sqlite3_vfs name. Ignored if filename is ":memory:" or "".
      This may change how the given filename is resolved. The VFS may
      optionally be provided via a URL-style filename argument:
      filename: "file:foo.db?vfs=...". If both this argument and a
      URI-style argument are provided, which one has precedence is
      unspecified. By default it uses a transient database, created
      anew on each request.
  }
}

对于文件名,可以使用 URL 样式的名称,这些名称可能包含 VFS 名称,这使它们能够使用(例如)OPFS 支持file:foo.db?vfs=opfs。或者,可以在“vfs”选项中指定 VFS。

响应

{
    type: "open",
    messageId: ...as above...,
    result: {
      filename: db filename, possibly differing from the input.

      dbId: see below,

      persistent: true if the given filename resides in the
         known-persistent storage, else false.

      vfs: name of the underlying VFS
    }
}

dbId 是一个不透明的 ID 值,应将其传递到此 API 中其他调用的消息信封中,以告知它们使用哪个数据库。如果未将其提供给将来的调用,它们将默认操作于第一个打开的数据库。出于 API 一致性的考虑,此属性也是包含消息信封的一部分。只有 open 操作将其包含在 result 属性中。

注意:由于 postMessage() 事件会在应用程序既无法查看也无法操作的队列中排队执行,因此客户端可能会在实际处理 open 请求之前排队任意数量的消息。如果 open 失败,则其后的所有消息也可能会失败,但客户端代码或工作线程都无法取消它们。Promiser API 可以通过使客户端能够在继续之前“等待” open 响应来解决此问题。

基于 Promise 的包装器(又名 Worker1 Promiser)

围绕 Worker1 API 的基于 Promise 的包装器提供了比 postMessage() 更友好的用户界面。与 Worker1 API 一样,此接口在其自己的专用工作线程中加载主 sqlite3 API,与所有客户端代码分开。但是,它不是通过 postMessage() 访问它,而是通过基于 Promise 的接口访问它。在幕后,它使用 postMessage(),但 Promise 接口为客户端提供了对数据库操作时机的更多控制。

要加载它

<script src="path/to/sqlite3-worker1-promiser.js"></script>

请注意,sqlite3-worker1-promiser.js 是 sqlite3 JS/WASM 发行版的一部分,必须与 sqlite3 JS/WASM 的其余部分位于同一目录中。

Promiser 配置和实例化

它需要 sqlite3-worker1.jssqlite3.js,请注意,可以将其配置为使用不同的脚本加载 sqlite3 工作线程。

该脚本将安装一个名为 sqlite3Worker1Promiser() 的全局作用域函数,该函数充当创建 promiser 实例的工厂。它有三种调用形式

可以通过修改 sqlite3Worker1Promiser.defaultConfig 对象来配置在调用 sqlite3Worker1Promiser()之前的第二种和第三种调用形式的默认值。

config 对象从技术上讲是可选的,但其 onready 属性实际上是必需的,因为它是通知 sqlite3 模块的异步加载和初始化何时完成的唯一方法。

使用 onready 回调而不是返回 Promise 的讽刺意味开发人员并没有忘记。事实证明,对于 sqlite3Worker1Promiser() 返回 Promise 比使用 onready 回调更笨拙。

config 对象可以具有以下任何属性,除了 onready 之外,所有属性都具有可用的默认值。

配置对象到位后,promiser 会像这样实例化

const promiser = self.sqlite3Worker1Promiser(config);

Promiser 方法

Promiser 对象是一个具有两种调用签名的函数

其中 type 始终是字符串,args 值是特定于消息类型的。它始终返回一个 Promise 对象,该对象解析为一个对象

{
  type: messageType,
  result: type-specific result value,
  ... possibly other metadata ...
}

每个消息类型对应一个 API 方法,所有这些方法都对应于 Worker1 API 中的方法,并且具有相同的参数和结果,除非在下面明确描述。

错误的报告方式与 Worker1 API 相同,但错误响应会导致 Promise 被拒绝。因此,客户端通过向其 Promise 添加 catch() 处理程序来监听这些错误。例如

promiser('open', {'filename':...}).then((msg)=>{
  ...
}).catch((e)=>{
  // Note that the error state is _not_ an Error object, but an
  // object in the same form the Worker1 API reports errors in.
  // That behavior is potentially subject to change in the future,
  // such that catch() always gets an Error object.
  console.error(e);
})

关闭

功能类似于 Worker1 的 close 方法,但如果它关闭了特定数据库,它还会清除内部默认的 dbId

获取配置

除了 open 之外,这是唯一不需要数据库连接的方法。

执行

此方法的工作原理几乎与其 Worker1 对应方法相同,但存在以下差异

exec{callback: STRING} 选项无法通过此接口使用(它会触发异常),但 {callback: function} 可以使用,并且工作方式与 Worker 中的 STRING 形式完全相同:回调会为结果集的每一行调用一次,并传递与 worker API 发出的相同的工作程序消息格式

{
  type: typeString,
  row: VALUE,
  rowNumber: 1-based-#,
  columnNames: anArray
}

其中 typeString 是一个内部合成的消息类型字符串,暂时用于 worker 消息分派。除了测试此 API 的客户端代码之外,所有其他客户端代码都可以忽略它。

在结果集的末尾,会使用 (row=undefined, rowNumber=null) 触发相同的事件,以指示已到达结果集的末尾。请注意,行是通过 worker 发布的消息到达的,并具有其所有含义。

打开

功能类似于 Worker1 的 open 方法,但如果这是打开的第一个数据库,它还会在内部记录来自响应的 dbId,以便它可以将该数据库 ID 用于不提供数据库 ID 的后续操作。

Promiser v2:另一个 Promise 和 ESM

Promiser v2 在 3.46 中添加,其工作原理与 v1 接口相同,只是初始化方式不同

以下是一个差异示例,首先以 v1 API 进行比较

// v1:
const factory = sqlite3Worker1Promiser({
  onready: function(f){
    /**
      The promiser factory (f) is now ready for use.
      f is the same function which is returned from
      sqlite3Worker1Promiser().
    */
  }
});
// Equivalent:
// const factory = sqlite3Worker1Promiser(function(f){...});

v2 返回一个 Promise,而不是一个函数,并且有两种加载它的选项。

首先,如果其代码以与 v1 相同的方式加载,则它可用作

const factory = await sqlite3Worker1Promiser.v2(/*optional config*/);

factory 是一个解析为函数的 Promise,而 v1 直接返回函数,但在模块完成初始化之前实际上无法使用它(客户端可以使用 v1 样式的 onready() 处理程序来检测)。

相同,但没有 await

sqlite3Worker1Promiser.v2(/*optional config*/)
  .then(f=>{
    // f is the function which the resulting Promise resolves to
    doSomeWork(f);
  });

其次,它可以作为 ESM 模块加载,如下所示。

import { default as promiserFactory } from "./jswasm/sqlite3-worker1-promiser.mjs";
const promiser = await promiserFactory(/* optional config */)
  .then(func){
    // func == the promiser factory function (same as `promiser` will resolve to).
    // Do any necessary client-side pre-work here, if needed, then...
    return func; // ensure that the promise resolves to the proper value
  });

以这种方式加载时,导出的函数是 v2 函数。

之后,promiser 的使用方法与 v1 API 完全相同。只有 v2 中的初始化发生了变化。

v2 接口接受但不强制要求 onready() 回调。如果提供了回调,或者 sqlite3Worker1Promiser.v2() 传递了一个函数,则会在结果 promise 解析之前立即调用它。如果它抛出异常,则 promise 会被拒绝。例如

promiserFactory( function(f){
  throw new Error("Testing onready throw.");
})
.catch(e=>{ console.error("caught:",e); });

  1. ^ countChanges 在 3.43 版中添加