跳到主要内容

rxdb-adapter-http

RxDB HTTP 适配器 — 让你自己的 REST API 充当 QueryCache 的远端事实源。

作为 RxDB 的 remote 适配器使用:本地 SQLite 只做行缓存,读写都以远端为准。 适合已有一套后端接口、不打算把数据库结构暴露给客户端的场景。

⚠️ v1 只支持 SyncType.QueryCache。 pullChanges / mergeChanges / getChangeCount 一律抛 HttpChangelogUnsupportedError,不会返回空数组或 0 假装「远端无变更」。 需要离线写、undo/redo 与冲突解决的实体请改用本地适配器 + Supabase。

特性​

  • ✅ transport 在包内 — fetch、auth 注入、状态码提取、错误分类都由适配器做;handler 只做协议 mapping,拿不到网络
  • ✅ 翻页 — offset(短页终止)与 token(nextPageToken 终止)两种形态,中途换形态 / token 不推进 / 连续空页 / 总页数触顶四种退化各自 fail-fast
  • ✅ 分块 findByIds — 按 idChunkSize 切块并合并;某块失败即整体失败,不当空块吞掉
  • ✅ 单次发射契约 — fetchMetadata / findByIds 都是「拼完再发一次」并 complete,逐页/逐块发射会让 core 的 forkJoin 只留最后一份
  • ✅ REST 模板工厂 — createRestHandlers() 一行替掉样板 handler,模板校验全在构造期
  • ✅ 错误分类锚定 core — 传输失败与超时抛 core 的 NetworkOfflineError(offlineFallback 可降级),HTTP 响应错误带数字 status(不降级)
  • ✅ 单请求超时 — 默认 30s,超时抛 NetworkOfflineError 而非裸 AbortError
  • ✅ 结构隔离 — 不 new 任何本地存储、不实现 upsertMany / deleteByIds / rawQuery

何时使用​

  • 已有 REST / JSON API,想让它接进 RxDB 的响应式查询
  • 远端权威、客户端只需要一层结构化行缓存
  • 不想把 SQL 或数据库结构暴露给客户端(请求体是 JSON RuleGroup,不是 SQL)

不适用:需要离线写、Full / Filter 双向同步、分支与工作树语义 —— 那些请用 Supabase 适配器。

安装​

pnpm add @aiao/rxdb @aiao/rxdb-adapter-http
# 行缓存用的本地适配器(按需二选一)
pnpm add @aiao/rxdb-adapter-wa-sqlite
# QueryCache 读写整条路径横跨三个插件包,缺一个 connect() 就抛
pnpm add @aiao/rxdb-plugin-history @aiao/rxdb-plugin-sync @aiao/rxdb-plugin-querycache

peerDependencies:@aiao/rxdb、rxjs ^7.8。无运行时依赖,请求走全局 fetch。

导出​

  • RxDBAdapterHttp — 远程适配器主类
  • ADAPTER_NAME — 常量 'http'
  • createRestHandlers — REST 资源 URL 模板工厂,产出的是普通 HttpHandlers
  • HttpAdapterOptions / HttpHandlers / HttpRequestSpec / HttpNumericConfig / HttpAuthHook 等配置与 handler 类型
  • RestHandlersOptions / RestOperation / RestOperationTemplate / RestPlaceholder — 模板工厂类型
  • 变更推送类型:HttpChangeFeedOptions、HttpChangeFeedUnavailableReport / HttpChangeFeedUnavailableHook、 HttpChangeFeedNotificationReport / HttpChangeFeedNotificationHook
  • 诊断类型:HttpEtagUnreadableReport / HttpEtagUnreadableHook、HttpResponseType
  • DEFAULT_HTTP_CONFIG — 六个数值配置的默认值
  • 错误类型:HttpAdapterError(基类)、HttpConfigError、HttpResponseError、HttpInvalidResponseError、 HttpRequestBuildError、HttpPaginationError、HttpInvalidMetadataError、HttpHandlerContractError、 HttpDisconnectedError、HttpChangelogUnsupportedError、HttpUnsupportedWireTypeError、 HttpUnsupportedOperationError
  • 错误成因的判别串:HttpRequestBuildFailure(HttpRequestBuildError.reason)、 HttpPaginationFailure(HttpPaginationError.reason)

⚠️ 不存在 createRxDatabase() / createHttpAdapter() 这类函数,注册方式见下方 rxdb.adapter()。

快速开始​

1. 两个槽位各归各位​

QueryCache 需要两个适配器,本包只占 remote 那一个:

槽位谁来做职责
remote@aiao/rxdb-adapter-httpfetchMetadata / findByIds + 可选的 create/update/delete
local任一 SQLite 适配器(你注册)行缓存:getMetadataByIds / upsertMany / deleteByIds

读引擎本身是第三样东西,也要你装,而且不止一个包:SyncType.QueryCache 的读路径住在 @aiao/rxdb-plugin-querycache,离线写出站住在 @aiao/rxdb-plugin-sync,后者又 inject: ['plugin:history']——三个包缺一个,connect() 就以 RxDBMissingPluginError 拒绝,不会静默退化成本地查询。对照表见 历史与同步拆包。

本包不持有也不创建任何本地存储——不 new SQLite、不打开 OPFS / IndexedDB。 inject: ['adapter:local'] 的插件(搜索、图查询等)因此绑到你注册的 SQLite,不会绑到本包。

import { RxDB, SyncType } from '@aiao/rxdb';
import { createRestHandlers, RxDBAdapterHttp } from '@aiao/rxdb-adapter-http';
import { RxDBAdapterWaSqlite } from '@aiao/rxdb-adapter-wa-sqlite';
import { rxDBPluginHistory } from '@aiao/rxdb-plugin-history';
import { rxDBPluginQueryCache } from '@aiao/rxdb-plugin-querycache';
import { rxDBPluginSync } from '@aiao/rxdb-plugin-sync';

const rxdb = new RxDB({
dbName: 'catalog',
entities: [Product],
sync: {
type: SyncType.QueryCache,
local: { adapter: 'wa-sqlite' },
remote: `{ adapter: 'http' }`
}
});

// 传的是插件工厂函数本身,不要调用它。注册顺序随意,先后由 `inject` 保证
rxdb.use(rxDBPluginHistory);
rxdb.use(rxDBPluginSync);
rxdb.use(rxDBPluginQueryCache);

rxdb.adapter('wa-sqlite', db => new RxDBAdapterWaSqlite(db));
rxdb.adapter(
'http',
db =>
new RxDBAdapterHttp(db, {
baseUrl: 'https://api.example.com/v1',
handlers: createRestHandlers({ resources: { Product: 'products' } }),
auth: () => ({ Authorization: `Bearer ${getToken()}` })
})
);

2A. REST 模板(推荐起步)​

createRestHandlers() 把「资源路径 + 固定 body 形状」这份样板收成一个工厂, 产出的仍是普通 HttpHandlers——适配器一行没改,翻页、分块、发射契约、错误分类全部照旧。

默认模板:

操作方法路径默认产出
fetchMetadataPOST:entity/metadata是(不可关)
findByIdsPOST:entity/by-ids是(不可关)
createPOST:entity是
updatePATCH:entity/:id是
deletePOST:entity/delete是
versionGET—否
isTableExistedHEAD:entity否
createRestHandlers({
resources: { Product: 'products' }, // 未列出的实体直接用实体名作路径片段
templates: {
version: { path: 'meta/version' }, // 覆盖:给对象
delete: null // 关闭:只读后端让 repo.delete() fail-fast
}
});

模板校验在构造期:路径为空 / 含空白、?、# / 方法非法 / 占位符集合不匹配 / 关掉必选 handler, 都当场抛 HttpConfigError。占位符必须恰好匹配——update 少 :id 会 PATCH 整个集合, 任意模板少 :entity 会让所有实体共用一个 URL,两种退化在网线上都可能是 2xx,等发出去再看响应码是看不出来的。

delete 默认走 POST :entity/delete 而不是 DELETE :entity + body:DELETE 的请求体会被不少代理、 网关和服务端框架直接丢弃,那样一条「删这 3 行」的请求会以「删整个集合」的面目到达服务端。 需要真 DELETE 的显式覆盖即可。

2B. 手写 handlers​

后端形状不规则时手写。handler 只做协议 mapping:给出请求描述、解析响应体,全程不碰网络。

import type { HttpHandlers } from '@aiao/rxdb-adapter-http';

const handlers: HttpHandlers = {
onFetchMetadata: {
request: ctx => ({
url: `${ctx.entityName}/metadata`,
method: 'POST',
body: `{ where: ctx.where, offset: ctx.offset, limit: ctx.limit }`
}),
parse: body => body as { id: string; updatedAt: string }[]
},
onFindByIds: {
request: ctx => ({ url: `${ctx.entityName}/rows`, method: 'POST', body: { ids: ctx.ids } }),
parse: body => body as unknown[]
}
};

where 以 JSON RuleGroup 的形态原样交给你——本包不生成 SQL,服务端怎么翻译由你决定。 onCreate / onUpdate / onDelete 没配,适配器上就不存在对应方法,repo.create(...) 当场 fail-fast。

配置项​

new RxDBAdapterHttp(db, {
baseUrl: 'https://api.example.com',
handlers,
auth: () => ({ Authorization: `Bearer ${getToken()}` }),
headers: { 'X-App': 'catalog' },
pageSize: 1000,
idChunkSize: 100,
maxEmptyPages: 3,
maxPages: 1000,
requestTimeoutMs: 30000,
conditionalRequests: false,
conditionalCacheSize: 256
});
字段默认含义
pageSize1000单页条数,透传为 handler 的 ctx.limit
idChunkSize100findByIds 单块 id 数
maxEmptyPages3游标形态下连续空页容忍上限;0 = 不容忍
maxPages1000单次 fetchMetadata 总页数上限,触顶抛错
requestTimeoutMs30000单个请求的超时上限
conditionalCacheSize256条件请求响应缓存条目上限,仅在下方开关打开时生效

六个数值都必须是 finite 正整数(maxEmptyPages 可为 0),否则构造期抛 HttpConfigError 并带上字段名与实际值。

changeFeed(SSE 变更通知)缺省不存在、配了即开;它是实验性能力,不在 1.0 兼容承诺内,协议目前只有参考后端一个实现。 auth 在每次请求发出前调用,与 headers 冲突时以 auth 为准;auth 抛错则请求不发出。

条件请求(conditionalRequests,缺省关闭)​

打开后,fetchMetadata / findByIds 会带上 If-None-Match 复用上次 200 的解析结果,命中 304 时返回那份结果—— 不是空集(把 304 当空集就是本包全篇在防的假孤儿)。同一请求指纹的并发调用 single-flight 去重, 不会出现「后一个拿到 304 而前一个还没回填」的空洞。

new RxDBAdapterHttp(db, { baseUrl, handlers, conditionalRequests: true });

四条要点:

  • 必须显式开启。 它只在远端真的发 ETag 并认 If-None-Match 时才有收益,而这一点适配器无从探测。 关闭时行为与不带此特性的版本逐字相同:不发条件头、不去重、304 照旧当错误。
  • 缓存的是响应不是行。 行缓存归 core 经本地适配器落盘,本包不碰。响应缓存按适配器实例存活、 按 conditionalCacheSize 有界(LRU)、disconnect() 时清空。翻页 / 分块按单页 / 单块各自键控。
  • 三组 header 只有 handler 那组进请求指纹。 HttpRequestSpec.headers(handler 逐请求给的)进—— 在同一条 URL 上按租户 / 语言分投影是安全的,两个投影各占各的条目。适配器级的 headers(本实例恒定) 与 auth hook 产出的不进:前者进去只是给每条键加同一段前缀,后者进去会让每次 token 轮换都全量失效。
  • 换用户必须走 disconnect() / connect()。 这是 auth header 不进指纹的代价:在同一实例上 直接换 token 会读到上一个身份的响应。

错误与离线降级​

判别位是类名(instanceof),code 只作辅助。

情形抛出isNetworkErrorofflineFallback
非 2xx 响应(401/409/…)HttpResponseError(带数字 status)false不降级,原样上抛
2xx 但响应体不是 JSONHttpInvalidResponseErrorfalse不降级
请求发出前就构造失败HttpRequestBuildError(带 reason)false不降级
连不上远端(fetch reject)core 的 NetworkOfflineErrortrue有缓存则降级
单请求超时core 的 NetworkOfflineErrortrue有缓存则降级
disconnect() 主动取消HttpDisconnectedErrorfalse不降级

传输失败刻意不包进本包的错误类:isNetworkError 的第一条判据是 instanceof NetworkOfflineError, 包起来会让降级静默失效。

反过来,HttpRequestBuildError 存在的理由正是不被包成传输失败:body 带 bigint、header 非法、 GET 带 body、URL 拼不出来、主键不是非空字符串——这五种本地问题里,JSON.stringify 与 fetch 抛的 都是裸 TypeError,与真的连不上远端完全同型。不摘出来的话,一次脏数据会被 offlineFallback 静默换成一份陈旧缓存,从此再没有报错。reason 取 'body' / 'headers' / 'method' / 'url' / 'id'。

限制​

  • bigint / binary 字段:connect() 当场抛 HttpUnsupportedWireTypeError——本包没有为这两种类型定义 wire codec
  • getRepository / saveMany / removeMany / mutations:一律抛 HttpUnsupportedOperationError; QueryCache 的批量写不经过它们(走 core 的 remote-then-local)
  • rawQuery、分支成员(pushBranches / branchExists / pullBranches):不实现
  • updatedAt 必须是合法 ISO 8601 串;带偏移或缺毫秒的会被 canonicalize 成 UTC + 3 位毫秒,非法串抛 HttpInvalidMetadataError

文档​

License​

MIT

Classes​

ClassDescription
HttpAdapterErrorHTTP 适配器错误基类。
HttpChangelogUnsupportedErrorv1 不支持 changelog / 分支同步(US-212 AC#10、#26)。
HttpConfigError构造期配置校验失败(US-212 AC#31)。
HttpDisconnectedError适配器已断开:请求被 disconnect() 取消,或断开后仍调用 duck(US-212 AC#24、#34)。
HttpHandlerContractErrorhandler 的 parse 返回了不合契约的形态。
HttpInvalidMetadataError远端返回的 metadata 不合契约(US-212 AC#14)。
HttpInvalidResponseError响应状态是 2xx,但响应体不是合法 JSON(US-212 AC#12)。
HttpPaginationError翻页过程中检测到退化的 handler / 远端(US-212 AC#7)。
HttpRequestBuildError请求在发出之前就构造失败(US-212 AC#12 / #34)。
HttpResponseError远端返回了非 2xx 响应(US-212 AC#12)。
HttpUnsupportedOperationError本适配器不支持该操作(US-212 AC#24、AC#32)。
HttpUnsupportedWireTypeError实体声明了本故事未定义 wire codec 的字段类型(US-212 AC#15)。
RxDBAdapterHttp基于用户自有 REST API 的 QueryCache 远程适配器。

Interfaces​

InterfaceDescription
CreateContextcreate 写入口的上下文。
CreateHandlercreate 的协议 mapping。
DeleteContextdelete 写入口的上下文。
DeleteHandlerdelete 的协议 mapping。
FetchMetadataContext单页 fetchMetadata 请求的上下文。
FetchMetadataHandlerfetchMetadata 的协议 mapping。
FindByIdsContext单块 findByIds 请求的上下文。
FindByIdsHandlerfindByIds 的协议 mapping。
HttpAdapterOptionsnew RxDBAdapterHttp(rxdb, options) 的配置。
HttpChangeFeedNotificationReport收到一条读得懂的变更通知时的事实(US-023 AC#24)。
HttpChangeFeedOptions变更通知通道(SSE)的配置(US-023 D5)。
HttpChangeFeedUnavailableReport变更通知通道不可用的那一刻的事实(US-023 AC#17)。
HttpEtagUnreadableReport「条件请求开着,却读不到 ETag」这一刻的事实(US-215)。
HttpHandlers阶段 A 的 handler 集合。
HttpNumericConfig六个数值配置,全部可覆盖、全部有默认。
HttpRequestSpechandler 产出、适配器执行的请求描述。
IsTableExistedHandlerisTableExisted() 的协议 mapping。
RestHandlersOptionscreateRestHandlers 的配置。
RestOperationTemplate单个操作的路径模板与方法覆盖。
UpdateContextupdate 写入口的上下文。data 是调用方给的部分字段,理由同 CreateContext。
UpdateHandlerupdate 的协议 mapping。
VersionHandlerversion() 的协议 mapping。

Type Aliases​

Type AliasDescription
FetchMetadataResult单页解析结果,两种形态判别式返回。
HttpAuthHookauth hook:注入 token / header。
HttpChangeFeedNotificationHook诊断回调:收到一条读得懂的变更通知时被调用(无论是否被抑制)。
HttpChangeFeedUnavailableHook诊断回调:变更通知通道不可用时被调用。
HttpEtagUnreadableHook诊断回调:条件请求开着却读不到 ETag 时被调用。
HttpMethodhandler 可以产出的 HTTP 方法。
HttpPaginationFailure翻页 fail-fast 的四种成因(US-212 AC#7)。
HttpRequestBuildFailure请求还没发出去就构造失败的四种成因
HttpResponseTypeResponse.type 的取值集合。
RestOperation可用模板表达的操作,与 HttpHandlers 的七个字段一一对应。
RestPlaceholder模板支持的占位符,只有这两个。

Variables​

VariableDescription
ADAPTER_NAME适配器注册名
DEFAULT_HTTP_CONFIG六个数值配置的默认值。

Functions​

FunctionDescription
createRestHandlers用 REST 资源 URL 模板产出一整套 HttpHandlers(US-212 AC#27)。