@aiao/rxdb-adapter-http 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/dist/RxDBAdapterHttp.d.ts +251 -0
- package/dist/RxDBAdapterHttp.d.ts.map +1 -0
- package/dist/change-feed.d.ts +66 -0
- package/dist/change-feed.d.ts.map +1 -0
- package/dist/chunking.d.ts +29 -0
- package/dist/chunking.d.ts.map +1 -0
- package/dist/conditional-cache.d.ts +175 -0
- package/dist/conditional-cache.d.ts.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/errors.d.ts +215 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/handler-contract.d.ts +41 -0
- package/dist/handler-contract.d.ts.map +1 -0
- package/dist/http.interface.d.ts +519 -0
- package/dist/http.interface.d.ts.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +864 -0
- package/dist/metadata.d.ts +15 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/pagination.d.ts +30 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/rest.d.ts +94 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/transport.d.ts +174 -0
- package/dist/transport.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/RxDBAdapterHttp.ts +786 -0
- package/src/change-feed.ts +470 -0
- package/src/chunking.ts +68 -0
- package/src/conditional-cache.ts +271 -0
- package/src/config.ts +83 -0
- package/src/errors.ts +295 -0
- package/src/handler-contract.ts +77 -0
- package/src/http.interface.ts +554 -0
- package/src/index.ts +25 -0
- package/src/metadata.ts +94 -0
- package/src/pagination.ts +184 -0
- package/src/rest.ts +476 -0
- package/src/transport.ts +699 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* HTTP 远程适配器(US-212 阶段 A)。
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* v1 只支持 {@link SyncType.QueryCache}:远端权威 + **独立注册**的本地行缓存。
|
|
7
|
+
* 本类是 QueryCache 的**远端**一侧,只有 `fetchMetadata` / `findByIds` 两个必选 duck
|
|
8
|
+
* 加三个可选写入口;行缓存由 core 落到另一个槽位的本地适配器,本类不持有、不调用它。
|
|
9
|
+
* 这条结构隔离由 `RxDBAdapterHttp.spec.ts` 的契约用例冻结(AC#19、AC#25)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getEntityMetadata,
|
|
14
|
+
getSyncConfig,
|
|
15
|
+
getSyncType,
|
|
16
|
+
isSystemEntity,
|
|
17
|
+
PropertyType,
|
|
18
|
+
RxDBAdapterRemoteBase,
|
|
19
|
+
type EntityMetadata,
|
|
20
|
+
type EntityType,
|
|
21
|
+
type IRepository,
|
|
22
|
+
type IRxDBAdapter,
|
|
23
|
+
type QueryCacheEntityMetadata,
|
|
24
|
+
type RemoteChange,
|
|
25
|
+
type RemoteMergeResult,
|
|
26
|
+
type RuleGroup,
|
|
27
|
+
type RxDB
|
|
28
|
+
} from '@aiao/rxdb';
|
|
29
|
+
import { defer, from, map, of, type Observable } from 'rxjs';
|
|
30
|
+
import { assertChangeFeedUrl, HttpChangeFeed, type ChangeFeedEntity } from './change-feed.js';
|
|
31
|
+
import { findByIdsInChunks } from './chunking.js';
|
|
32
|
+
import { resolveHttpConfig } from './config.js';
|
|
33
|
+
import {
|
|
34
|
+
HttpChangelogUnsupportedError,
|
|
35
|
+
HttpConfigError,
|
|
36
|
+
HttpDisconnectedError,
|
|
37
|
+
HttpResponseError,
|
|
38
|
+
HttpUnsupportedOperationError,
|
|
39
|
+
HttpUnsupportedWireTypeError
|
|
40
|
+
} from './errors.js';
|
|
41
|
+
import { assertHandlerRow, assertHandlerVersion } from './handler-contract.js';
|
|
42
|
+
import type {
|
|
43
|
+
CreateContext,
|
|
44
|
+
HttpAdapterOptions,
|
|
45
|
+
HttpHandlers,
|
|
46
|
+
HttpNumericConfig,
|
|
47
|
+
UpdateContext
|
|
48
|
+
} from './http.interface.js';
|
|
49
|
+
import { fetchAllMetadataPages } from './pagination.js';
|
|
50
|
+
import { HttpTransport, readErrorBody } from './transport.js';
|
|
51
|
+
|
|
52
|
+
/** 适配器注册名 */
|
|
53
|
+
export const ADAPTER_NAME = 'http';
|
|
54
|
+
|
|
55
|
+
/** 未配 handler 时用来探测资源可达性的空条件 */
|
|
56
|
+
const MATCH_ALL: RuleGroup<unknown> = { combinator: 'and', rules: [] };
|
|
57
|
+
|
|
58
|
+
/** 本包没有 wire codec 的字段类型(AC#15) */
|
|
59
|
+
const UNSUPPORTED_WIRE_TYPES = new Set<string>([PropertyType.bigint, PropertyType.binary]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 基于用户自有 REST API 的 QueryCache 远程适配器。
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* **transport 归本类,不归 handler。** handler 只做协议映射:`request()` 产出请求描述,
|
|
66
|
+
* `parse()` 解码响应体。发请求、注入 auth、提取状态码、分类错误、翻页、分块全在这一侧。
|
|
67
|
+
* 让 handler 自己发请求的话,AC#12 / #13 / #16 三条契约的担保人就变成接入方,
|
|
68
|
+
* 本包只能退化成文档劝告。
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const rxdb = new RxDB({
|
|
73
|
+
* dbName: 'app',
|
|
74
|
+
* entities: [Recipe],
|
|
75
|
+
* sync: { type: SyncType.QueryCache, local: { adapter: 'sqlite' }, remote: { adapter: 'http' } }
|
|
76
|
+
* });
|
|
77
|
+
* // 读引擎随插件走(`@aiao/rxdb-plugin-querycache`),漏装时 `connect()` 直接拒绝
|
|
78
|
+
* rxdb.use(rxDBPluginQueryCache);
|
|
79
|
+
* rxdb.adapter('http', db => new RxDBAdapterHttp(db, {
|
|
80
|
+
* baseUrl: 'https://api.example.com',
|
|
81
|
+
* handlers: {
|
|
82
|
+
* onFetchMetadata: {
|
|
83
|
+
* request: ctx => ({ url: `${ctx.entityName}/metadata`, method: 'POST', body: ctx }),
|
|
84
|
+
* parse: body => body as QueryCacheEntityMetadata[]
|
|
85
|
+
* },
|
|
86
|
+
* onFindByIds: {
|
|
87
|
+
* request: ctx => ({ url: `${ctx.entityName}`, method: 'POST', body: { ids: ctx.ids } }),
|
|
88
|
+
* parse: body => body as unknown[]
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* }));
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export class RxDBAdapterHttp extends RxDBAdapterRemoteBase implements IRxDBAdapter {
|
|
95
|
+
#handlers: HttpHandlers;
|
|
96
|
+
#disconnected: AbortController;
|
|
97
|
+
#transport: HttpTransport;
|
|
98
|
+
/** 变更通知通道;未配 `changeFeed` 时**不存在**(US-023 AC#12:缺席即关闭) */
|
|
99
|
+
readonly #changeFeed: HttpChangeFeed | undefined;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 通道该不该跑的**意图**位,与「通道存不存在」是两件事。
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* 配了就默认为 `true`——`changeFeed` 这个配置项本身就是「我要收通知」的表达,
|
|
106
|
+
* 让它配完还得再调一次 `startChangeFeed()` 才生效,是给每个接入方加一道无意义的手续。
|
|
107
|
+
*/
|
|
108
|
+
#changeFeedEnabled: boolean;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `connect()` 是否成功走完过一遍,且此后没有 `disconnect()`。
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* 与 `#disconnected.signal.aborted` 是两个问题:那一位答的是「有没有被断开过」,
|
|
115
|
+
* 刚 `new` 出来的适配器上它是 `false`——于是「从未连接」和「连接正常」在它眼里一模一样。
|
|
116
|
+
* 开长连接必须分得清这两者,见 {@link RxDBAdapterHttp.startChangeFeed}。
|
|
117
|
+
*/
|
|
118
|
+
#connected = false;
|
|
119
|
+
|
|
120
|
+
readonly name = ADAPTER_NAME;
|
|
121
|
+
|
|
122
|
+
/** 已校验的数值配置,构造期定型 */
|
|
123
|
+
readonly config: HttpNumericConfig;
|
|
124
|
+
|
|
125
|
+
// ============================================
|
|
126
|
+
// QueryCache 写 duck(按 handler 存在与否挂载,AC#4)
|
|
127
|
+
// ============================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 创建实体。仅在配置了 `onCreate` 时**存在**。
|
|
131
|
+
*
|
|
132
|
+
* @remarks
|
|
133
|
+
* `QueryCacheEngine` 用 `if (!this.remoteAdapter.create)` 做特性探测,所以「不支持」
|
|
134
|
+
* 必须表现为**属性缺席**,不能是一个总在抛错的方法——后者会让探测判 `true`,
|
|
135
|
+
* AC#4 那句清晰的「Remote adapter does not support create」变成运行期意外。
|
|
136
|
+
*
|
|
137
|
+
* 因此这里只 `declare` 类型,实体由构造期的 `#installWriteDucks()` 按需挂上。
|
|
138
|
+
*/
|
|
139
|
+
declare create?: <R>(entityName: string, data: R) => Observable<R>;
|
|
140
|
+
|
|
141
|
+
/** 更新实体。仅在配置了 `onUpdate` 时存在,理由同 {@link RxDBAdapterHttp.create}。 */
|
|
142
|
+
declare update?: <R>(entityName: string, id: string, data: Partial<R>) => Observable<R>;
|
|
143
|
+
|
|
144
|
+
/** 删除实体。仅在配置了 `onDelete` 时存在,理由同 {@link RxDBAdapterHttp.create}。 */
|
|
145
|
+
declare delete?: (entityName: string, ids: string | string[]) => Observable<void>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 变更通知通道此刻**要不要**跑,由 {@link RxDBAdapterHttp.startChangeFeed} /
|
|
149
|
+
* {@link RxDBAdapterHttp.stopChangeFeed} 翻转。
|
|
150
|
+
*
|
|
151
|
+
* @returns 未配 `changeFeed` 时恒为 `false`
|
|
152
|
+
*
|
|
153
|
+
* @remarks
|
|
154
|
+
* 这是**意图**,不是连接状态:通道正在退避重连、或所在运行时压根没有 `EventSource` 时,
|
|
155
|
+
* 它照样是 `true`。想知道连接本身的死活,看 `changeFeed.onUnavailable` 的诊断上报——
|
|
156
|
+
* `EventSource` 不暴露状态码,一个从这里返回的布尔值答不了「为什么没连上」,
|
|
157
|
+
* 只会把两种问题混成一种。
|
|
158
|
+
*
|
|
159
|
+
* (夹在写 duck 与构造函数之间是 lint `member-ordering` 的要求:访问器排在字段之后、
|
|
160
|
+
* 构造函数之前。语义上它属于下面那组运行时开关。)
|
|
161
|
+
*/
|
|
162
|
+
get changeFeedEnabled(): boolean {
|
|
163
|
+
return this.#changeFeedEnabled;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param rxdb - 宿主 RxDB 实例
|
|
168
|
+
* @param options - baseUrl、handlers 与可选的数值/认证配置
|
|
169
|
+
* @throws HttpConfigError baseUrl 为空、缺必选 handler,或任一数值配置不是 finite 正整数
|
|
170
|
+
*/
|
|
171
|
+
constructor(
|
|
172
|
+
rxdb: RxDB,
|
|
173
|
+
readonly options: HttpAdapterOptions
|
|
174
|
+
) {
|
|
175
|
+
super(rxdb);
|
|
176
|
+
assertBaseUrl(options.baseUrl);
|
|
177
|
+
assertRequiredHandlers(options.handlers);
|
|
178
|
+
this.config = resolveHttpConfig(options);
|
|
179
|
+
this.#handlers = options.handlers;
|
|
180
|
+
this.#disconnected = new AbortController();
|
|
181
|
+
this.#transport = this.#createTransport();
|
|
182
|
+
this.#changeFeed = this.#createChangeFeed();
|
|
183
|
+
this.#changeFeedEnabled = this.#changeFeed !== undefined;
|
|
184
|
+
this.#installWriteDucks();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================
|
|
188
|
+
// 连接管理(AC#24)
|
|
189
|
+
// ============================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 不建立长连接,只做本地校验。
|
|
193
|
+
*
|
|
194
|
+
* @remarks
|
|
195
|
+
* **不发探测请求**:远端此刻不可达不代表配置错,那是查询期的网络错误,
|
|
196
|
+
* 交给 `isNetworkError` 分类。想在启动时确认可达性的应用自己调
|
|
197
|
+
* {@link RxDBAdapterHttp.isTableExisted},那是显式选择而非框架行为。
|
|
198
|
+
*
|
|
199
|
+
* **配了 `changeFeed` 才有长连接**,且它建在最后:校验没过的 `connect()` 不该留下一条
|
|
200
|
+
* 活着的通知连接。通道自身连不上不影响本方法的返回——它按 US-023 AC#17 只诊断不抛错。
|
|
201
|
+
*
|
|
202
|
+
* 通道走的是{@link RxDBAdapterHttp.changeFeedEnabled}这个**意图位**而不是「配了就开」:
|
|
203
|
+
* 调用方手动 {@link RxDBAdapterHttp.stopChangeFeed} 掉的通道,不该被下一次重连
|
|
204
|
+
* (切后端、纪元交替都会走到这里)悄悄复活。
|
|
205
|
+
*
|
|
206
|
+
* @returns 适配器自身
|
|
207
|
+
* @throws HttpUnsupportedWireTypeError 已注册实体声明了 bigint / binary 字段
|
|
208
|
+
*/
|
|
209
|
+
async connect(): Promise<IRxDBAdapter> {
|
|
210
|
+
this.#assertConfiguredEntitiesSupported();
|
|
211
|
+
// 换代之前先收口上一代:重复 connect() 若只是替换字段,旧请求仍绑在旧 signal 上,
|
|
212
|
+
// 而那个 controller 从此没有任何引用能 abort 它——之后的 disconnect() 只取消得了
|
|
213
|
+
// 新一代,旧请求只能等自己超时。等价于「重连隐含断开」,与 disconnect() 同一条口径
|
|
214
|
+
this.#disconnected.abort();
|
|
215
|
+
// 校验通过才重新武装:失败的 connect 不该把上一次的断开状态悄悄解除
|
|
216
|
+
this.#disconnected = new AbortController();
|
|
217
|
+
this.#transport = this.#createTransport();
|
|
218
|
+
this.#connected = true;
|
|
219
|
+
// start() 自带「先收口上一条」,重复 connect() 因此不会留下第二条连接
|
|
220
|
+
if (this.#changeFeedEnabled) {
|
|
221
|
+
this.#changeFeed?.start();
|
|
222
|
+
}
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 取消进行中的请求。
|
|
228
|
+
*
|
|
229
|
+
* @remarks
|
|
230
|
+
* 中止走 **error 通道**:complete 一个没发射过的 Observable 会让 core 的 `forkJoin`
|
|
231
|
+
* 静默产出「远端零条」,整表判成孤儿,比抛错危险得多。
|
|
232
|
+
*
|
|
233
|
+
* **已发出的写请求不回滚**——HTTP 没有事务,假装能回滚比不回滚更危险。
|
|
234
|
+
*
|
|
235
|
+
* 顺带清空条件请求缓存(AC#28):那些响应绑定的是断开前的认证身份与远端状态,
|
|
236
|
+
* 跨越一次断开继续复用,等于让重连后的第一批查询读到上一段连接的世界。
|
|
237
|
+
*
|
|
238
|
+
* 变更通知通道一并停掉,**含待执行的重连**:只关连接不取消定时器的话,退避窗口里的那次
|
|
239
|
+
* 重连会在断开之后把连接重新建起来。
|
|
240
|
+
*
|
|
241
|
+
* 但{@link RxDBAdapterHttp.changeFeedEnabled}**不动**:断开是生命周期事件,不是调用方
|
|
242
|
+
* 改了主意。把它一并清掉的话,一次重连就会把用户「我要收通知」的选择吃掉。
|
|
243
|
+
*/
|
|
244
|
+
async disconnect(): Promise<void> {
|
|
245
|
+
this.#connected = false;
|
|
246
|
+
this.#disconnected.abort();
|
|
247
|
+
this.#transport.clearConditionalCache();
|
|
248
|
+
this.#changeFeed?.stop();
|
|
249
|
+
return Promise.resolve();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================
|
|
253
|
+
// 变更通知的运行时开关
|
|
254
|
+
// ============================================
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 接通变更通知通道。
|
|
258
|
+
*
|
|
259
|
+
* @remarks
|
|
260
|
+
* 幂等:已经开着时只是重建连接(`start()` 自带「先收口上一条」),不会留下第二条。
|
|
261
|
+
* 连上之后照常触发一次全量失效(D7)——「从这一刻起我能收到变更了」对首次连接与
|
|
262
|
+
* 重新接通是同一句话。
|
|
263
|
+
*
|
|
264
|
+
* **连接状态先判**,与 {@link RxDBAdapterHttp.version} 同一条口径:在一个已经
|
|
265
|
+
* `disconnect()` 的适配器上开出一条活着的 SSE,是实打实的资源泄漏。
|
|
266
|
+
*
|
|
267
|
+
* 这里判的是 `#connected` 而不是 `#assertConnected` 那一位。**「从未 `connect()`」
|
|
268
|
+
* 与「已 `disconnect()`」在这个方法上是同一件事**:两种情况下都还没有一次成功的
|
|
269
|
+
* `connect()`,也就意味着 `#assertConfiguredEntitiesSupported()` 还没跑过——
|
|
270
|
+
* 用 `new` + `startChangeFeed()` 就能绕开线格式校验、在一个连不连得上都不知道的
|
|
271
|
+
* 适配器上开出真连接,还会对着一份尚未确认可用的实体表做 D7 全量失效。
|
|
272
|
+
*
|
|
273
|
+
* @throws HttpDisconnectedError 尚未 `connect()`,或已 `disconnect()`
|
|
274
|
+
* @throws HttpUnsupportedOperationError 未配置 `changeFeed`
|
|
275
|
+
*/
|
|
276
|
+
startChangeFeed(): void {
|
|
277
|
+
if (!this.#connected) {
|
|
278
|
+
throw new HttpDisconnectedError('startChangeFeed');
|
|
279
|
+
}
|
|
280
|
+
const feed = this.#requireChangeFeed('startChangeFeed');
|
|
281
|
+
this.#changeFeedEnabled = true;
|
|
282
|
+
feed.start();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 断开变更通知通道,**含待执行的重连**。
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* 断开期间远端的变更没有人会补发,重新
|
|
290
|
+
* {@link RxDBAdapterHttp.startChangeFeed} 时的那次全量失效才是补课的地方。
|
|
291
|
+
*
|
|
292
|
+
* 与 {@link RxDBAdapterHttp.startChangeFeed} 不同,本方法**不判断开状态**:已经
|
|
293
|
+
* `disconnect()` 的适配器上通道本来就停着,两条路通向同一个终点,为此抛错只会让
|
|
294
|
+
* 「关掉开关」这种收尾动作平白需要一个 try。幂等的停止不是兜底——它没有掩盖任何差异。
|
|
295
|
+
*
|
|
296
|
+
* @throws HttpUnsupportedOperationError 未配置 `changeFeed`
|
|
297
|
+
*/
|
|
298
|
+
stopChangeFeed(): void {
|
|
299
|
+
const feed = this.#requireChangeFeed('stopChangeFeed');
|
|
300
|
+
this.#changeFeedEnabled = false;
|
|
301
|
+
feed.stop();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 返回**远端服务端**版本。
|
|
306
|
+
*
|
|
307
|
+
* @remarks
|
|
308
|
+
* 与 sqlite / pglite / supabase 三家口径一致(都返回后端引擎版本)。HTTP 没有内建
|
|
309
|
+
* RPC,因此靠可选的 `onVersion` handler;未配置时抛错而**不**回落到本包
|
|
310
|
+
* `package.json` 的版本号——那是拿适配器版本冒充后端版本。
|
|
311
|
+
*
|
|
312
|
+
* **断开状态先判**:已 `disconnect()` 的适配器上,「未配 `onVersion`」不是调用方
|
|
313
|
+
* 此刻该看见的答案——配置问题下次连上仍在,而生命周期问题是当下这一次调用的实情。
|
|
314
|
+
* 反过来(先判 handler)还会让下面那句 `#assertConnected` 在缺 handler 的路径上
|
|
315
|
+
* 永远走不到,而那条路径正是它存在的两个理由之一(另一个是空 id 列表的 `findByIds`)。
|
|
316
|
+
*
|
|
317
|
+
* @throws HttpDisconnectedError 适配器已断开
|
|
318
|
+
* @throws HttpUnsupportedOperationError 未配置 `onVersion`
|
|
319
|
+
*/
|
|
320
|
+
async version(): Promise<string> {
|
|
321
|
+
this.#assertConnected('version');
|
|
322
|
+
const handler = this.#handlers.onVersion;
|
|
323
|
+
if (!handler) {
|
|
324
|
+
throw new HttpUnsupportedOperationError(
|
|
325
|
+
'version',
|
|
326
|
+
'no "onVersion" handler is configured; the adapter must not report its own package version as the backend version'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const body = await this.#transport.sendJson(handler.request(), 'version');
|
|
330
|
+
// `parse` 的返回类型 `string` 是接入方声明的,运行期什么都不担保
|
|
331
|
+
return assertHandlerVersion(handler.parse(body));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 按**远端资源可达性**回答,不恒 `true` 蒙混。
|
|
336
|
+
*
|
|
337
|
+
* @remarks
|
|
338
|
+
* `2xx` → `true`,`404` → `false`,其余状态码与传输失败 → 抛错。
|
|
339
|
+
* 抛错而不是返回 `false` 是刻意的:「不知道」和「不存在」必须区分,
|
|
340
|
+
* 把 500 读成「表不存在」会让调用方基于一个假答案做决定。
|
|
341
|
+
*
|
|
342
|
+
* 未配 `onIsTableExisted` 时复用 `onFetchMetadata` 的 `limit: 1` 探测。
|
|
343
|
+
*
|
|
344
|
+
* @param EntityType - 要检查的实体类
|
|
345
|
+
*/
|
|
346
|
+
async isTableExisted(EntityType: EntityType): Promise<boolean> {
|
|
347
|
+
this.#assertConnected('isTableExisted');
|
|
348
|
+
const entityName = getEntityMetadata(EntityType).name;
|
|
349
|
+
const spec =
|
|
350
|
+
this.#handlers.onIsTableExisted ?
|
|
351
|
+
this.#handlers.onIsTableExisted.request({ entityName })
|
|
352
|
+
: this.#handlers.onFetchMetadata.request({
|
|
353
|
+
entityName,
|
|
354
|
+
where: MATCH_ALL,
|
|
355
|
+
offset: 0,
|
|
356
|
+
limit: 1
|
|
357
|
+
});
|
|
358
|
+
// 判定写成 consume 回调交给 transport:读 body 这一段同样要罩在超时/断开窗口内,
|
|
359
|
+
// 否则「状态行秒回、body 挂死」的远端能让这次探测永不 settle
|
|
360
|
+
return this.#transport.execute(spec, 'isTableExisted', async response => {
|
|
361
|
+
if (response.ok || response.status === 404) {
|
|
362
|
+
// 两支都只看状态码,但 body 仍要读完:node/undici 下未消费的流会把 socket
|
|
363
|
+
// 挂到 GC 才归还,探测频繁时表现为连接池耗尽,且全程不报错
|
|
364
|
+
await response.body?.cancel().catch(() => undefined);
|
|
365
|
+
return response.ok;
|
|
366
|
+
}
|
|
367
|
+
// 读 body 用 transport 那份共享实现:`.catch(() => undefined)` 会把此刻的
|
|
368
|
+
// disconnect() 一起吞掉,最终报出一个 HttpResponseError,把「调用方叫停」
|
|
369
|
+
// 说成「服务端返回错误」
|
|
370
|
+
const body = await readErrorBody(response, this.#disconnected.signal);
|
|
371
|
+
throw new HttpResponseError(response.status, this.#transport.resolveUrl(spec), body);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ============================================
|
|
376
|
+
// QueryCache 读 duck
|
|
377
|
+
// ============================================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 拉取满足 `query` 的全部实体元数据。
|
|
381
|
+
*
|
|
382
|
+
* @remarks
|
|
383
|
+
* **恰好发射一次再 complete**(AC#23)。翻页循环整体落在一个 `Promise` 里,
|
|
384
|
+
* `from(promise)` 天然满足这条;逐页发射会让调用方的 `forkJoin` 只留最后一页,
|
|
385
|
+
* 前面各页当场丢失,那些 id 随即被判成「远端已删除」。
|
|
386
|
+
*
|
|
387
|
+
* @param entityName - 实体名
|
|
388
|
+
* @param query - 查询条件,原样透传给 handler
|
|
389
|
+
*/
|
|
390
|
+
fetchMetadata(entityName: string, query: RuleGroup<unknown>): Observable<QueryCacheEntityMetadata[]> {
|
|
391
|
+
return defer(() => {
|
|
392
|
+
this.#assertConnected('fetchMetadata');
|
|
393
|
+
return from(
|
|
394
|
+
fetchAllMetadataPages(
|
|
395
|
+
{ transport: this.#transport, handler: this.#handlers.onFetchMetadata, config: this.config },
|
|
396
|
+
{ entityName, where: query }
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 按 id 列表批量拉取完整行。
|
|
404
|
+
*
|
|
405
|
+
* @remarks
|
|
406
|
+
* 与 {@link RxDBAdapterHttp.fetchMetadata} 同款发射契约(AC#33):所有块合并后
|
|
407
|
+
* 恰好发射一次再 complete。逐块发射丢掉的是要写进本地缓存的完整行。
|
|
408
|
+
*
|
|
409
|
+
* @param entityName - 实体名
|
|
410
|
+
* @param ids - 需要拉取的 id 列表
|
|
411
|
+
*/
|
|
412
|
+
findByIds<R>(entityName: string, ids: string[]): Observable<R[]> {
|
|
413
|
+
return defer(() => {
|
|
414
|
+
this.#assertConnected('findByIds');
|
|
415
|
+
return from(
|
|
416
|
+
findByIdsInChunks(
|
|
417
|
+
{ transport: this.#transport, handler: this.#handlers.onFindByIds, config: this.config },
|
|
418
|
+
{ entityName, ids }
|
|
419
|
+
)
|
|
420
|
+
).pipe(map(rows => rows as R[]));
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================
|
|
425
|
+
// v1 无实现的必选成员(AC#32)
|
|
426
|
+
// ============================================
|
|
427
|
+
//
|
|
428
|
+
// 声明了 `Promise` 的一律加 `async`:同步 throw 与那个签名不符,调用侧的
|
|
429
|
+
// `adapter.saveMany(x).catch(h)` 里 `.catch` 根本没机会挂上,错误从调用处直接炸出去。
|
|
430
|
+
// `getRepository` 不加——它返回的就是 `RT`,同步抛才是对的。
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @throws HttpUnsupportedOperationError 总是
|
|
434
|
+
*
|
|
435
|
+
* @remarks
|
|
436
|
+
* QueryCache 的主仓储由 core 用**本地**适配器组装,本类这一侧没有可返回的东西。
|
|
437
|
+
*
|
|
438
|
+
* 这一组连形参都不声明(同 {@link RxDBAdapterHttp.pullChanges}):函数少写形参照样
|
|
439
|
+
* 满足 `IRxDBAdapter`,而留一个只为凑签名的 `_entities` 会让读者以为它被用上了。
|
|
440
|
+
*/
|
|
441
|
+
getRepository<E extends EntityType, RT extends IRepository<E> = IRepository<E>>(): RT {
|
|
442
|
+
throw new HttpUnsupportedOperationError('getRepository');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** @throws HttpUnsupportedOperationError 总是(Full/Filter 写路径,v1 无 Full-sync) */
|
|
446
|
+
async saveMany<E extends EntityType>(): Promise<InstanceType<E>[]> {
|
|
447
|
+
throw new HttpUnsupportedOperationError('saveMany');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** @throws HttpUnsupportedOperationError 总是(同 {@link RxDBAdapterHttp.saveMany}) */
|
|
451
|
+
async removeMany<E extends EntityType>(): Promise<InstanceType<E>[]> {
|
|
452
|
+
throw new HttpUnsupportedOperationError('removeMany');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @throws HttpUnsupportedOperationError 总是
|
|
457
|
+
*
|
|
458
|
+
* @remarks
|
|
459
|
+
* 不影响 QueryCache 的批量写:`EntityManager.mutations` 判定为 QueryCache 批后走
|
|
460
|
+
* `#mutations_query_cache` 的 remote-then-local,根本不经过这里。
|
|
461
|
+
*/
|
|
462
|
+
async mutations<E extends EntityType>(): Promise<InstanceType<E>[]> {
|
|
463
|
+
throw new HttpUnsupportedOperationError('mutations');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ============================================
|
|
467
|
+
// changelog 成员(AC#10)
|
|
468
|
+
// ============================================
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* @throws HttpChangelogUnsupportedError 总是
|
|
472
|
+
*
|
|
473
|
+
* @remarks
|
|
474
|
+
* 返回空数组**算失败**:Full-sync 会把它读成「远端无变更」并覆盖本地认知。
|
|
475
|
+
* v1 没有 changelog,unsupported throw 是唯一诚实行为。
|
|
476
|
+
*/
|
|
477
|
+
async pullChanges(): Promise<RemoteChange[]> {
|
|
478
|
+
throw new HttpChangelogUnsupportedError('pullChanges');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** @throws HttpChangelogUnsupportedError 总是(返回 `0` 同样会被读成「远端无变更」) */
|
|
482
|
+
async getChangeCount(): Promise<{ count: number; latestChangeId: number }> {
|
|
483
|
+
throw new HttpChangelogUnsupportedError('getChangeCount');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** @throws HttpChangelogUnsupportedError 总是 */
|
|
487
|
+
async mergeChanges(): Promise<RemoteMergeResult | number | void> {
|
|
488
|
+
throw new HttpChangelogUnsupportedError('mergeChanges');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// `pullChangesBatch` / `pushBranches` / `branchExists` / `pullBranches` 一律**不实现**:
|
|
492
|
+
// 它们的调用点都做特性探测,缺席即回落到同样 throw 的成员(AC#11、AC#26)。
|
|
493
|
+
// 写一个返回 `[]` / `false` 的版本会让 Full-sync 与分支同步以为远端确实空着。
|
|
494
|
+
|
|
495
|
+
// ============================================
|
|
496
|
+
// 内部
|
|
497
|
+
// ============================================
|
|
498
|
+
|
|
499
|
+
#createTransport(): HttpTransport {
|
|
500
|
+
return new HttpTransport({
|
|
501
|
+
baseUrl: this.options.baseUrl,
|
|
502
|
+
requestTimeoutMs: this.config.requestTimeoutMs,
|
|
503
|
+
disconnectSignal: this.#disconnected.signal,
|
|
504
|
+
auth: this.options.auth,
|
|
505
|
+
headers: this.options.headers,
|
|
506
|
+
// local-first 的可达性判定源:每次真发出去的请求都把结局交回去,
|
|
507
|
+
// 由 `isNetworkError` 一处定夺是不是离线。**现读 `this.rxdb`** 而不是构造期快照,
|
|
508
|
+
// 与下面 changeFeed 的三个回调同一条口径
|
|
509
|
+
reportResult: error => this.rxdb.reachability.report(error),
|
|
510
|
+
// 缺席即禁用:AC#28 要求未启用时行为与阶段 A 逐字相同,
|
|
511
|
+
// 传一个 `{ enabled: false }` 会让那句话依赖 transport 内部再判一次
|
|
512
|
+
// 诊断回调挂在 `conditional` 里面而不是与它并列:条件请求关掉时整个对象就不存在,
|
|
513
|
+
// 「关掉就不该触发」(US-215 AC#4)由结构保证,不靠触发点再判一次开关
|
|
514
|
+
conditional:
|
|
515
|
+
this.options.conditionalRequests === true ?
|
|
516
|
+
{
|
|
517
|
+
maxEntries: this.config.conditionalCacheSize,
|
|
518
|
+
onEtagUnreadable: this.options.onEtagUnreadable
|
|
519
|
+
}
|
|
520
|
+
: undefined
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 建通知通道,未配 `changeFeed` 时返回 `undefined`。
|
|
526
|
+
*
|
|
527
|
+
* @remarks
|
|
528
|
+
* 与 `conditional` 同一条口径:缺席即禁用。返回一个「停用状态的通道」会让 AC#12
|
|
529
|
+
* 的「零连接」依赖对象内部再判一次开关,而这里不存在这个对象。
|
|
530
|
+
*
|
|
531
|
+
* URL 借 transport 拼:`baseUrl` 的拼接规则只有一份,让通道自己再拼一遍,两处迟早分叉。
|
|
532
|
+
* 三个回调都是**现读**而不是构造期快照——`clientId` 在登录后会被换掉,实体清单也可能
|
|
533
|
+
* 在 `connect()` 之后才补齐。
|
|
534
|
+
*/
|
|
535
|
+
#createChangeFeed(): HttpChangeFeed | undefined {
|
|
536
|
+
const changeFeed = this.options.changeFeed;
|
|
537
|
+
if (changeFeed === undefined) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
assertChangeFeedUrl(changeFeed.url);
|
|
541
|
+
return new HttpChangeFeed({
|
|
542
|
+
url: this.#transport.resolveUrl({ url: changeFeed.url, method: 'GET' }),
|
|
543
|
+
options: changeFeed,
|
|
544
|
+
clientId: () => this.rxdb.context.clientId,
|
|
545
|
+
entities: () => this.#subscribedEntities(),
|
|
546
|
+
invalidate: (entity, namespace) => this.rxdb.invalidateRemoteEntity(entity, namespace)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* 走本适配器 remote 槽位的实体,即 D7 口中的「已订阅实体」。
|
|
552
|
+
*
|
|
553
|
+
* @remarks
|
|
554
|
+
* SSE 不按实体订阅——服务端广播它认识的全部实体。所以「已订阅」只能由本地配置定义:
|
|
555
|
+
* 连接建立时该失效的,正是那些以本适配器为远端权威的实体。别的实体的行由别的路径维护,
|
|
556
|
+
* 顺手失效它们是越界。
|
|
557
|
+
*/
|
|
558
|
+
#subscribedEntities(): readonly ChangeFeedEntity[] {
|
|
559
|
+
const entities: ChangeFeedEntity[] = [];
|
|
560
|
+
const seen = new Set<string>();
|
|
561
|
+
for (const EntityClass of this.rxdb.config.entities) {
|
|
562
|
+
const metadata = getEntityMetadata(EntityClass);
|
|
563
|
+
const key = entityKey(metadata.name, metadata.namespace);
|
|
564
|
+
if (seen.has(key) || !this.#isSubscribed(EntityClass, metadata)) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
seen.add(key);
|
|
568
|
+
entities.push({ name: metadata.name, namespace: metadata.namespace });
|
|
569
|
+
}
|
|
570
|
+
return entities;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* 单个实体是否由本适配器充当远端权威。
|
|
575
|
+
*
|
|
576
|
+
* @param EntityClass - 实体类,用来判系统表
|
|
577
|
+
* @param metadata - 该实体的元数据
|
|
578
|
+
*
|
|
579
|
+
* @remarks
|
|
580
|
+
* 三个条件缺一不可,前两个各自堵住一种「跟着库级配置被顺带算进来」的实体:
|
|
581
|
+
*
|
|
582
|
+
* - **不是系统表**:`SchemaManager.init()` 往 `config.entities` 里补的四张表不带自己的
|
|
583
|
+
* `sync`,于是一律跟随库级配置。库级写 `type: QueryCache` 时它们会被判成 querycache,
|
|
584
|
+
* 连后两个条件都拦不住——但它们是纯本地簿记,远端根本没有对应的资源。
|
|
585
|
+
* - **确实是 QueryCache**:v1 只服务这一种同步类型(其余入口一律抛
|
|
586
|
+
* {@link HttpUnsupportedOperationError})。库级 `type: None` + 两端俱全时,跟随全局的
|
|
587
|
+
* 实体会被判成 `full`,那种实体的行不经本适配器,失效它没有任何查询在等。
|
|
588
|
+
* - **remote 槽位指向本适配器**:同一个库里可以挂多个远端。
|
|
589
|
+
*
|
|
590
|
+
* 放任任何一类混进来的后果不是崩,是每次连接把 D7 的上报量放大若干倍,
|
|
591
|
+
* 而多出来的每一次都无人认领——面板上的「触发重跑」计数会跟着一起说谎。
|
|
592
|
+
*/
|
|
593
|
+
#isSubscribed(EntityClass: EntityType, metadata: EntityMetadata): boolean {
|
|
594
|
+
if (isSystemEntity(EntityClass)) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const globalSync = this.rxdb.config.sync;
|
|
598
|
+
return (
|
|
599
|
+
getSyncType(metadata, globalSync) === 'querycache' &&
|
|
600
|
+
getSyncConfig(metadata, globalSync)?.remote?.adapter === ADAPTER_NAME
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* 没有一次成功的 `connect()` 撑着时,任何 duck 调用都抛错。
|
|
606
|
+
*
|
|
607
|
+
* @remarks
|
|
608
|
+
* 判的是 `#connected` 而不是 `#disconnected.signal.aborted`:后者只答「有没有被断开过」,
|
|
609
|
+
* 刚 `new` 出来的适配器上它是 `false`,于是「从未连接」与「连接正常」在它眼里一模一样。
|
|
610
|
+
* 而 `connect()` 是 {@link RxDBAdapterHttp.#assertConfiguredEntitiesSupported} 唯一的
|
|
611
|
+
* 触发点——放行「从未 connect」等于给 bigint / binary 的线格式门禁开一个后门,
|
|
612
|
+
* `new` 完直接 `create()` 就能绕过去。{@link RxDBAdapterHttp.startChangeFeed} 早就是
|
|
613
|
+
* 这个口径,这里与它拉齐。
|
|
614
|
+
*
|
|
615
|
+
* transport 自己也查断开位,但有两条路径够不到它:`findByIds` 传空列表时一个请求都不发,
|
|
616
|
+
* `version` 未配 handler 时同理。那两条上「已断开」会悄悄退化成「远端没有这些行」。
|
|
617
|
+
*/
|
|
618
|
+
#assertConnected(operation: string): void {
|
|
619
|
+
if (!this.#connected) {
|
|
620
|
+
throw new HttpDisconnectedError(operation);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* 取通道实例,未配 `changeFeed` 时抛错。
|
|
626
|
+
*
|
|
627
|
+
* @remarks
|
|
628
|
+
* 与 `version()` 未配 `onVersion` 同一条判例:**配置缺失要吵**。静默 no-op 会让
|
|
629
|
+
* 「开关点了没反应」变成一个没有任何线索的现象,而这两个方法的调用点通常正是
|
|
630
|
+
* 一个用户看得见的开关。
|
|
631
|
+
*
|
|
632
|
+
* 这里也**不**走 `create` / `update` / `delete` 那种「缺席即不支持」的 `declare` 模式:
|
|
633
|
+
* 那套是为 core 的 `if (!this.remoteAdapter.create)` 特性探测服务的,通道没有探测方,
|
|
634
|
+
* 把方法做成可选只会让每个调用点多写一个 `?.`,而那个 `?.` 恰好就是静默 no-op。
|
|
635
|
+
*/
|
|
636
|
+
#requireChangeFeed(operation: string): HttpChangeFeed {
|
|
637
|
+
const feed = this.#changeFeed;
|
|
638
|
+
if (!feed) {
|
|
639
|
+
throw new HttpUnsupportedOperationError(
|
|
640
|
+
operation,
|
|
641
|
+
'no "changeFeed" is configured; there is no channel to switch on or off'
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
return feed;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** 按 handler 的有无挂载三个可选写 duck(AC#4 的特性探测语义) */
|
|
648
|
+
#installWriteDucks(): void {
|
|
649
|
+
const { onCreate, onUpdate, onDelete } = this.#handlers;
|
|
650
|
+
if (onCreate) {
|
|
651
|
+
this.create = <R>(entityName: string, data: R): Observable<R> => {
|
|
652
|
+
// 行类型 `R` 是 core duck 签名里**调用方声明**的东西,HTTP 这一侧没有任何手段验证它。
|
|
653
|
+
// 把 `as R` 收在这一行,好过让它以 `CreateContext<T>` 的形式伪装成静态保证
|
|
654
|
+
const ctx: CreateContext = { entityName, data };
|
|
655
|
+
return defer(() => {
|
|
656
|
+
this.#assertConnected('create');
|
|
657
|
+
return from(this.#transport.sendJson(onCreate.request(ctx), 'create'));
|
|
658
|
+
}).pipe(map(body => assertHandlerRow('create', entityName, onCreate.parse(body, ctx)) as R));
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (onUpdate) {
|
|
662
|
+
this.update = <R>(entityName: string, id: string, data: Partial<R>): Observable<R> => {
|
|
663
|
+
const ctx: UpdateContext = { entityName, id, data };
|
|
664
|
+
return defer(() => {
|
|
665
|
+
this.#assertConnected('update');
|
|
666
|
+
return from(this.#transport.sendJson(onUpdate.request(ctx), 'update'));
|
|
667
|
+
}).pipe(map(body => assertHandlerRow('update', entityName, onUpdate.parse(body, ctx)) as R));
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (onDelete) {
|
|
671
|
+
this.delete = (entityName: string, ids: string | string[]): Observable<void> =>
|
|
672
|
+
defer(() => {
|
|
673
|
+
// 断开检查排在空列表近路**之前**:反过来会让「已断开」在这一条路径上
|
|
674
|
+
// 退化成「删掉了零行,成功」。`findByIds` 的空列表分支同样是这个顺序
|
|
675
|
+
this.#assertConnected('delete');
|
|
676
|
+
// core 的 duck 签名是 `string | string[]`,归一由本包做:让每个 handler
|
|
677
|
+
// 各自判一次类型,迟早有一个把单个 id 当成字符数组
|
|
678
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
679
|
+
if (list.length === 0) {
|
|
680
|
+
// `{"ids":[]}` 在把「空过滤」读成「无过滤」的后端上是一次整表清空,而
|
|
681
|
+
// 那种后端的存在性本包无从判断。与 `findByIdsInChunks` 对齐:空列表
|
|
682
|
+
// 本地即答,一个请求都不发
|
|
683
|
+
return of(undefined);
|
|
684
|
+
}
|
|
685
|
+
return from(this.#transport.sendVoid(onDelete.request({ entityName, ids: list }), 'delete'));
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** 扫描所有走本适配器 remote 槽位的实体(AC#15) */
|
|
691
|
+
#assertConfiguredEntitiesSupported(): void {
|
|
692
|
+
// 目标实体的元数据从**同一份实体清单**里查,不走 schemaManager:后者要等
|
|
693
|
+
// `RxDB.connect()` 里的 `schemaManager.init()` 填好,而本方法在直接调用
|
|
694
|
+
// `adapter.connect()` 时也必须给出同一个答案,否则扫描会因为一张空表静默放行
|
|
695
|
+
const byName = new Map<string, EntityMetadata>();
|
|
696
|
+
for (const EntityType of this.rxdb.config.entities) {
|
|
697
|
+
const metadata = getEntityMetadata(EntityType);
|
|
698
|
+
byName.set(entityKey(metadata.name, metadata.namespace), metadata);
|
|
699
|
+
}
|
|
700
|
+
for (const metadata of byName.values()) {
|
|
701
|
+
this.#assertEntitySupported(metadata, byName);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#assertEntitySupported(metadata: EntityMetadata, byName: Map<string, EntityMetadata>): void {
|
|
706
|
+
const sync = getSyncConfig(metadata, this.rxdb.config.sync);
|
|
707
|
+
// bigint 只有在**要过 HTTP 线**时才是问题:本地实体带 bigint 与本包无关
|
|
708
|
+
if (sync?.remote?.adapter !== ADAPTER_NAME) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const property = findUnsupportedProperty(metadata, byName);
|
|
712
|
+
if (!property) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
throw new HttpUnsupportedWireTypeError(metadata.name, property.name, property.type);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** 实体在清单里的唯一键;namespace 参与是因为同名实体可以分属不同 namespace */
|
|
720
|
+
const entityKey = (name: string, namespace: string): string => `${namespace}\u0000${name}`;
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* 找出第一个没有 wire codec 的字段。
|
|
724
|
+
*
|
|
725
|
+
* @remarks
|
|
726
|
+
* `JSON.stringify` 会把 `7n` 抛成 `TypeError`、把 `Uint8Array` 塌成 `{"0":1,…}`,
|
|
727
|
+
* 后者尤其阴——它不报错,只是把二进制悄悄换成一个对象。US-018 已经为同一类静默丢失
|
|
728
|
+
* 付过一次学费,所以这里在连接期就拦。
|
|
729
|
+
*
|
|
730
|
+
* **外键列要单独扫一遍。** 它们不在 `propertyMap` 里——那张表来自 `@Entity` 的
|
|
731
|
+
* `properties`,而 `authorId` 是 `foreignKeyRelationMap` 从 `relations` 派生出来的。
|
|
732
|
+
* 只看 `propertyMap` 的话,一个自身字段全合法、却指向 bigint 主键实体的实体会被放行,
|
|
733
|
+
* 而它的 `authorId` 照样要过这条 HTTP 线。判定口径与 supabase 的
|
|
734
|
+
* `getUnsupportedProperty()` 逐字一致:取目标实体 `id` 的类型。
|
|
735
|
+
*/
|
|
736
|
+
const findUnsupportedProperty = (
|
|
737
|
+
metadata: EntityMetadata,
|
|
738
|
+
byName: Map<string, EntityMetadata>
|
|
739
|
+
): { name: string; type: string } | undefined => {
|
|
740
|
+
for (const property of metadata.propertyMap.values()) {
|
|
741
|
+
if (UNSUPPORTED_WIRE_TYPES.has(property.type)) {
|
|
742
|
+
return { name: property.name, type: property.type };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
for (const [foreignKeyName, relation] of metadata.foreignKeyRelationMap) {
|
|
746
|
+
const target = byName.get(entityKey(relation.mappedEntity, relation.mappedNamespace ?? metadata.namespace));
|
|
747
|
+
const type = target?.propertyMap.get('id')?.type;
|
|
748
|
+
if (type !== undefined && UNSUPPORTED_WIRE_TYPES.has(type)) {
|
|
749
|
+
return { name: foreignKeyName, type };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return undefined;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
/** baseUrl 必须是非空串:空 baseUrl 会把相对 URL 拼成 `/items` 打到当前源上 */
|
|
756
|
+
const assertBaseUrl = (baseUrl: string): void => {
|
|
757
|
+
if (typeof baseUrl !== 'string' || baseUrl.trim().length === 0) {
|
|
758
|
+
throw new HttpConfigError('HTTP adapter config "baseUrl" must be a non-empty string', 'baseUrl', baseUrl);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* 两个必选 handler 缺一不可。
|
|
764
|
+
*
|
|
765
|
+
* @remarks
|
|
766
|
+
* 它们是 `RxDBAdapterRemoteBase` 两个 abstract 成员的落地点,缺了整条 QueryCache 读路径
|
|
767
|
+
* 都不成立。拖到首次查询才报,等于让一个「连得上」的库带着注定失败的实体跑到运行期。
|
|
768
|
+
*/
|
|
769
|
+
const assertRequiredHandlers = (handlers: { onFetchMetadata?: unknown; onFindByIds?: unknown } | undefined): void => {
|
|
770
|
+
for (const required of ['onFetchMetadata', 'onFindByIds'] as const) {
|
|
771
|
+
if (handlers?.[required]) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
throw new HttpConfigError(
|
|
775
|
+
`HTTP adapter config "handlers.${required}" is required`,
|
|
776
|
+
`handlers.${required}`,
|
|
777
|
+
handlers?.[required]
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
declare module '@aiao/rxdb' {
|
|
783
|
+
interface RxDBAdapters {
|
|
784
|
+
[ADAPTER_NAME]: RxDBAdapterHttp;
|
|
785
|
+
}
|
|
786
|
+
}
|