@aiao/rxdb-adapter-http
v0.0.26
Published
HTTP remote adapter for RxDB QueryCache: JSON RuleGroup over your own REST API.
Maintainers
Readme
@aiao/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-querycachepeerDependencies:
@aiao/rxdb、rxjs^7.8。无运行时依赖,请求走全局fetch。
导出
RxDBAdapterHttp— 远程适配器主类ADAPTER_NAME— 常量'http'createRestHandlers— REST 资源 URL 模板工厂,产出的是普通HttpHandlersHttpAdapterOptions/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-http | fetchMetadata / 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——适配器一行没改,翻页、分块、发射契约、错误分类全部照旧。
默认模板:
| 操作 | 方法 | 路径 | 默认产出 |
| :--------------- | :------ | :----------------- | :----------- |
| fetchMetadata | POST | :entity/metadata | 是(不可关) |
| findByIds | POST | :entity/by-ids | 是(不可关) |
| create | POST | :entity | 是 |
| update | PATCH | :entity/:id | 是 |
| delete | POST | :entity/delete | 是 |
| version | GET | — | 否 |
| isTableExisted | HEAD | :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
});| 字段 | 默认 | 含义 |
| :--------------------- | :------ | :----------------------------------------------- |
| pageSize | 1000 | 单页条数,透传为 handler 的 ctx.limit |
| idChunkSize | 100 | findByIds 单块 id 数 |
| maxEmptyPages | 3 | 游标形态下连续空页容忍上限;0 = 不容忍 |
| maxPages | 1000 | 单次 fetchMetadata 总页数上限,触顶抛错 |
| requestTimeoutMs | 30000 | 单个请求的超时上限 |
| conditionalCacheSize | 256 | 条件请求响应缓存条目上限,仅在下方开关打开时生效 |
六个数值都必须是 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 只作辅助。
| 情形 | 抛出 | isNetworkError | offlineFallback |
| :------------------------- | :------------------------------------- | :--------------- | :---------------- |
| 非 2xx 响应(401/409/…) | HttpResponseError(带数字 status) | false | 不降级,原样上抛 |
| 2xx 但响应体不是 JSON | HttpInvalidResponseError | false | 不降级 |
| 请求发出前就构造失败 | HttpRequestBuildError(带 reason) | false | 不降级 |
| 连不上远端(fetch reject) | core 的 NetworkOfflineError | true | 有缓存则降级 |
| 单请求超时 | core 的 NetworkOfflineError | true | 有缓存则降级 |
| disconnect() 主动取消 | HttpDisconnectedError | false | 不降级 |
传输失败刻意不包进本包的错误类: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
