@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +269 -0
  3. package/dist/RxDBAdapterHttp.d.ts +251 -0
  4. package/dist/RxDBAdapterHttp.d.ts.map +1 -0
  5. package/dist/change-feed.d.ts +66 -0
  6. package/dist/change-feed.d.ts.map +1 -0
  7. package/dist/chunking.d.ts +29 -0
  8. package/dist/chunking.d.ts.map +1 -0
  9. package/dist/conditional-cache.d.ts +175 -0
  10. package/dist/conditional-cache.d.ts.map +1 -0
  11. package/dist/config.d.ts +30 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/errors.d.ts +215 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/handler-contract.d.ts +41 -0
  16. package/dist/handler-contract.d.ts.map +1 -0
  17. package/dist/http.interface.d.ts +519 -0
  18. package/dist/http.interface.d.ts.map +1 -0
  19. package/dist/index.d.ts +25 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +864 -0
  22. package/dist/metadata.d.ts +15 -0
  23. package/dist/metadata.d.ts.map +1 -0
  24. package/dist/pagination.d.ts +30 -0
  25. package/dist/pagination.d.ts.map +1 -0
  26. package/dist/rest.d.ts +94 -0
  27. package/dist/rest.d.ts.map +1 -0
  28. package/dist/transport.d.ts +174 -0
  29. package/dist/transport.d.ts.map +1 -0
  30. package/package.json +64 -0
  31. package/src/RxDBAdapterHttp.ts +786 -0
  32. package/src/change-feed.ts +470 -0
  33. package/src/chunking.ts +68 -0
  34. package/src/conditional-cache.ts +271 -0
  35. package/src/config.ts +83 -0
  36. package/src/errors.ts +295 -0
  37. package/src/handler-contract.ts +77 -0
  38. package/src/http.interface.ts +554 -0
  39. package/src/index.ts +25 -0
  40. package/src/metadata.ts +94 -0
  41. package/src/pagination.ts +184 -0
  42. package/src/rest.ts +476 -0
  43. package/src/transport.ts +699 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * ETag / If-None-Match 条件请求的**响应缓存**与 single-flight(US-212 AC#28)。
4
+ *
5
+ * @remarks
6
+ * **这里缓存的是响应,不是行。** 两者容易被读成一件事,而它们的归属正相反:
7
+ * 行缓存由 core 经 `localAdapter.upsertMany()` 落盘,本包按 AC#19 连碰都不能碰;
8
+ * 本模块只在内存里留住「上次那个 200 的 JSON body」,好在远端回 304 时把它还回去。
9
+ *
10
+ * **为什么不需要跨包失效协议。** 服务端回 304 的前提就是它认为该 URL 的内容与请求方
11
+ * 持有的 ETag 一致;一旦变了它会回 200 带新 body。缓存的正确性由 HTTP 协议本身担保,
12
+ * 不依赖任何人来通知失效——这正是 AC#28 能归本包、而 AC#29 / #30 拿不到 owner 的分界。
13
+ *
14
+ * **缓存按适配器实例存活,三组 header 只有一组进指纹。** 请求发出前有三处叠加 header:
15
+ * 适配器级静态配置、handler 在 `HttpRequestSpec.headers` 上给的、auth hook 产出的。
16
+ *
17
+ * - **auth hook 不进指纹。** token 每轮换一次就全量失效,等于没有缓存。代价必须写明:
18
+ * 同一实例上**换用户**必须走 `disconnect()` / `connect()`,那会清空并重建缓存。
19
+ * - **适配器级静态配置不进指纹。** 它对本实例的每个请求都一样,而缓存正是按实例存活的,
20
+ * 进去只是给每条键加同一段前缀。
21
+ * - **handler 给的进指纹。** 它是唯一逐请求变化的一组:同一条 URL 上按租户 / 语言分投影时,
22
+ * method + url + body 三者完全相同。少了这一维就是两条不同的请求共用一个条目——
23
+ * 而共用的后果不是未命中,是答错,见 {@link requestFingerprint}。
24
+ */
25
+ /** 写入缓存的一条已校验过的响应 */
26
+ export interface ConditionalCacheEntry {
27
+ /** 远端给的校验令牌,原样回填进 `if-none-match` */
28
+ readonly etag: string;
29
+ /** 上次 200 的**已 JSON 解码**响应体 */
30
+ readonly value: unknown;
31
+ }
32
+ /**
33
+ * 从缓存取出的一条响应。
34
+ *
35
+ * @remarks
36
+ * 响应体不作为字段直接暴露,只能经 {@link takeValue} 取——见类头「隔离」一节:
37
+ * 摊成 `value` 字段就等于把缓存自己那份对象的引用发出去,而调用方无从知道它是共享的。
38
+ */
39
+ export interface ConditionalCacheHit {
40
+ /** 远端给的校验令牌,原样回填进 `if-none-match` */
41
+ readonly etag: string;
42
+ /** 取一份**与缓存无关联**的响应体副本;每次调用都是新的一份 */
43
+ takeValue(): unknown;
44
+ }
45
+ /**
46
+ * 计算请求指纹。
47
+ *
48
+ * @remarks
49
+ * 前三段取 method + **已拼接的绝对 URL** + **序列化后的 body 字符串**,与真正发出去的
50
+ * 字节一一对应。不自己再规范化一次 body:transport 发出的就是这个字符串,两处各算一遍
51
+ * 迟早分叉,而分叉的表现是「换了个键」——那是缓存未命中,不是错误结果。
52
+ *
53
+ * 第四段是 handler 给的 header,三组 header 里只有它进指纹,分组理由见模块头。它治的是
54
+ * **答错**而不是未命中:同一条 URL 上按租户分投影时,两个租户的 method / url / body 全同,
55
+ * 共用条目会让 A 的 ETag 被拿去问 B。服务端若不按该 header 变更 ETag 就回 304,
56
+ * B 于是收到 A 的行;并发时更硬——`singleFlight` 直接把 B 合流进 A,一个字节都不上网。
57
+ *
58
+ * 反向的偏差都只是未命中,可以接受:`spec.headers` 的值未经 `Headers` 规范化
59
+ * (`' a '` 与 `'a'` 在这里是两个键,发到线上却是同一个),被 auth hook 覆盖掉的
60
+ * `spec.headers` 也照样计入。
61
+ *
62
+ * 用 `JSON.stringify` 拼数组而不是模板串:分隔符方案要额外论证「每段里都不含分隔符」,
63
+ * 而论证失败的代价是**指纹碰撞**——拿 A 请求的缓存回答 B 请求,比缓存未命中严重得多。
64
+ * 数组编码天然无歧义,顺带把 `undefined`(无 body)与 `''`(空 body)分成两个键。
65
+ *
66
+ * @param method - HTTP 方法
67
+ * @param url - 已拼接的绝对 URL
68
+ * @param body - 序列化后的请求体;无 body 时为 `undefined`
69
+ * @param variant - handler 给的 header,**名字须已折成小写**;无则不传。
70
+ * 不传与传 `{}` 同键——handler 有条件地拼 header 时那是同一个请求
71
+ */
72
+ export declare const requestFingerprint: (method: string, url: string, body?: string, variant?: Record<string, string>) => string;
73
+ /**
74
+ * 有界 LRU 响应缓存 + 按指纹的 single-flight 去重。
75
+ *
76
+ * @remarks
77
+ * **有界是硬要求**(AC#28):翻页会为每一页生成一个指纹,无界的 `Map` 会随查询范围
78
+ * 单调增长——那正是被移出本故事的 AC#30 要解决的问题,不能在这里复制一份。
79
+ *
80
+ * **逐出按访问顺序,不按写入顺序。** 翻页时第 1 页被反复重放,按写入顺序会正好把最热的
81
+ * 那条挤掉。`Map` 的迭代顺序即插入顺序,读命中时 delete + set 把条目移到队尾即可。
82
+ *
83
+ * **隔离:没有任何两个调用方拿到同一个对象。** 未启用条件请求时每次响应都是 `JSON.parse`
84
+ * 的新对象,调用方改自己手里那份是安全的;缓存若把同一个对象反复发出去,
85
+ * `conditionalRequests: true` 就凭空多出一条「上游就地改了行 → 之后每次 304 都返回被改过
86
+ * 的数据」的静默污染路径,与 `HttpAdapterOptions.conditionalRequests` 承诺的
87
+ * 「未启用时行为逐字相同」直接冲突。所以写入存私有副本、取出给独立副本、合流方各拿一份。
88
+ *
89
+ * 代价是每次命中多一次 `structuredClone`。它与刚发生的一次网络往返和 `JSON.parse`
90
+ * 同一量级,换掉的是一类只在生产环境、只在开了条件请求时才出现、且不报任何错的数据污染。
91
+ */
92
+ export declare class ConditionalRequestCache {
93
+ #private;
94
+ private readonly maxEntries;
95
+ /** 当前条目数 */
96
+ get size(): number;
97
+ /**
98
+ * @param maxEntries - 条目上限,已由 `resolveHttpConfig` 校验为 finite 正整数
99
+ */
100
+ constructor(maxEntries: number);
101
+ /**
102
+ * 取条目并刷新 recency。
103
+ *
104
+ * @remarks
105
+ * 拷贝**不在这里发生**,而是推迟到 {@link ConditionalCacheHit.takeValue}:调用方每次请求
106
+ * 前都要来取一次 ETag,但只有真收到 304 才需要那个 body。在这里拷就是给每个最终走 200
107
+ * 的请求白拷一份再扔掉。返回的闭包已经把值捕获住,之后条目被逐出也不影响它。
108
+ *
109
+ * @param key - {@link requestFingerprint} 的产出
110
+ */
111
+ get(key: string): ConditionalCacheHit | undefined;
112
+ /**
113
+ * 写入条目,超出上限时逐出最久未用的一条。
114
+ *
115
+ * @remarks
116
+ * 存的是**副本**:调用方手里那个对象同时被返回给了它自己,它事后改动不该反映进缓存。
117
+ *
118
+ * @param key - {@link requestFingerprint} 的产出
119
+ * @param entry - 本次 200 的 ETag 与已解码响应体
120
+ */
121
+ set(key: string, entry: ConditionalCacheEntry): void;
122
+ /**
123
+ * 丢弃单个条目:远端停发 ETag 时用,留着会拿一个再也换不到 304 的令牌去问。
124
+ *
125
+ * @remarks
126
+ * **不碰 {@link markEtagUnreadable} 的记录。** 读不到 ETag 的那一支每次都调本方法,
127
+ * 记录若跟着没了,「一个 key 只报一次」当场退化成「每次查询都报一次」。
128
+ */
129
+ delete(key: string): void;
130
+ /**
131
+ * 记下这个指纹已经报过一次「读不到 ETag」,并回答**本次是不是第一次**(US-215 AC#5)。
132
+ *
133
+ * @remarks
134
+ * 去重表和缓存同生命周期:翻页时每页一个指纹,一次全表刷新会连报几十条同义警告,
135
+ * 而它们指向的是同一件事——这个后端没把 ETag 交到客户端手上。
136
+ *
137
+ * 同样**有界**,上限与条目共用 `maxEntries`。无界的 `Set` 会随「读不到 ETag 的不同
138
+ * URL 数」单调增长,正是 `conditionalCacheSize` 存在的理由。逐出的代价是那个 key
139
+ * 之后会再报一次——有界的噪音好过无界的内存。
140
+ *
141
+ * @param key - {@link requestFingerprint} 的产出
142
+ * @returns 第一次记录返回 `true`,之后一律 `false`
143
+ */
144
+ markEtagUnreadable(key: string): boolean;
145
+ /**
146
+ * 清空所有条目。
147
+ *
148
+ * @remarks
149
+ * `disconnect()` 时调用(AC#28)。in-flight 记录**不清**:那些 Promise 已经有调用方在
150
+ * 等,抹掉映射只会让紧随其后的同指纹请求再发一次,去重白做——它们会各自被
151
+ * `disconnectSignal` 中止,那是 transport 的事。
152
+ *
153
+ * 诊断去重表**要清**(US-215):一次断开重连之间,后端的 CORS 配置完全可能被改过,
154
+ * 而记录留着就意味着「这次配对了没有」被永久封在上一段连接里。
155
+ */
156
+ clear(): void;
157
+ /**
158
+ * 同一指纹的并发调用合流到一个 Promise。
159
+ *
160
+ * @remarks
161
+ * 堵的是 AC#28 点名的那个空洞:第二个请求若独立发出,会带着同一个 `If-None-Match`
162
+ * 拿到 304,而此时第一个还没回填,缓存里没有 body——304 于是无处还原。
163
+ *
164
+ * 失败也合流,且 `finally` 里必须摘除映射:残留会让这个指纹永久返回那次失败。
165
+ *
166
+ * **合流方拿到的是副本**,不是发起方那个对象:否则去重本身会把两个本该互不相干的
167
+ * 调用方绑成共享一份数据,理由同类头「隔离」一节。失败路径不拷——错误对象共享无害,
168
+ * 而 `structuredClone` 反而会把它拷成一个丢了原型的普通对象。
169
+ *
170
+ * @param key - {@link requestFingerprint} 的产出
171
+ * @param factory - 真正发请求的工厂,仅在没有 in-flight 时调用
172
+ */
173
+ singleFlight<T>(key: string, factory: () => Promise<T>): Promise<T>;
174
+ }
175
+ //# sourceMappingURL=conditional-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditional-cache.d.ts","sourceRoot":"","sources":["../src/conditional-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,qBAAqB;AACrB,MAAM,WAAW,qBAAqB;IACpC,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,SAAS,IAAI,OAAO,CAAC;CACtB;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,MAAM,EACd,KAAK,MAAM,EACX,OAAO,MAAM,EACb,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/B,MAA6E,CAAC;AAEjF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,uBAAuB;;IActB,OAAO,CAAC,QAAQ,CAAC,UAAU;IARvC,YAAY;IACZ,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;gBAC0B,UAAU,EAAE,MAAM;IAE/C;;;;;;;;;OASG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAUjD;;;;;;;;OAQG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,IAAI;IAgBpD;;;;;;OAMG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB;;;;;;;;;;;;;OAaG;IACH,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAexC;;;;;;;;;;OAUG;IACH,KAAK,IAAI,IAAI;IAKb;;;;;;;;;;;;;;;OAeG;IACG,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAS1E"}
@@ -0,0 +1,30 @@
1
+ import { HttpNumericConfig } from './http.interface.js';
2
+ /**
3
+ * 六个数值配置的默认值。
4
+ *
5
+ * @remarks
6
+ * `pageSize` / `idChunkSize` 对标 supabase 适配器的 `SUPABASE_PAGE_SIZE`(1000)与
7
+ * `SUPABASE_IN_CHUNK_SIZE`(100)——同一份 metadata 契约下两家取值不同没有理由,
8
+ * 只会让「换个适配器就翻页行为变了」变成需要排查的现象。
9
+ *
10
+ * `conditionalCacheSize` 只在 `conditionalRequests: true` 时才有作用,但默认值仍然给足:
11
+ * 它决定的是缓存命中率,取小了只是白发请求,不会产生错误结果。
12
+ */
13
+ export declare const DEFAULT_HTTP_CONFIG: HttpNumericConfig;
14
+ /**
15
+ * 合并默认值并逐字段校验,返回可直接使用的完整配置。
16
+ *
17
+ * @remarks
18
+ * 在构造期调用、**同步抛错**:这些值决定翻页与分块的终止条件,等到首次查询才发现
19
+ * 配错了,错误会以「结果少了一半」的形态出现,而不是以配置错误的形态。
20
+ *
21
+ * 按字段名逐个取值而不是 `{ ...DEFAULT, ...options }`,是因为调用方传进来的是**整个**
22
+ * 适配器 options(还带着 `baseUrl` / `handlers`):整体展开会把它们一并塞进配置对象。
23
+ * 顺带把「显式传 `undefined`」正确地当成「没传」——展开写法会让它覆盖掉默认值。
24
+ *
25
+ * @param options - 用户覆盖项,缺省字段取 {@link DEFAULT_HTTP_CONFIG}
26
+ * @returns 六个字段齐全且均已校验的配置
27
+ * @throws HttpConfigError 任一字段不是 finite 正整数(`maxEmptyPages` 下界为 `0`)
28
+ */
29
+ export declare const resolveHttpConfig: (options: Partial<HttpNumericConfig>) => HttpNumericConfig;
30
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,EAAE,iBAOjC,CAAC;AAiCF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,OAAO,CAAC,iBAAiB,CAAC,KAAG,iBAQvE,CAAC"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * HTTP 适配器的错误类型。
4
+ *
5
+ * @remarks
6
+ * **判别位是类名,不是 `code`**(US-212「新错误的判别口径」)。仓库里并存三套口径:
7
+ * 主流按 `name` 判别(core 5 个错误类、supabase 全部错误类),supabase 附带 UPPER_SNAKE
8
+ * 的 `code` 作辅助,而 `RxDBError.ts` 的 `mixed_versioned_cache_transaction` 是**唯一**
9
+ * 用 snake_case `code` 作主判别位的特例——那句「唯一」保护着一条跨故事契约,本包照抄它
10
+ * 就会把「唯一」变成「三个」。因此本包一律:**类名主判别 + UPPER_SNAKE `code` 辅助**。
11
+ *
12
+ * **传输失败不在这里。** 连不上远端时抛的是 core 的 `NetworkOfflineError`,不是本文件的
13
+ * 任何一个类——`isNetworkError` 的第 1 条判据是 `instanceof NetworkOfflineError`,包进
14
+ * 自定义类会让它认不出,`offlineFallback` 静默失效(US-212 AC#13)。
15
+ */
16
+ /**
17
+ * HTTP 适配器错误基类。
18
+ *
19
+ * @remarks
20
+ * 只做归类,**不要直接抛它**——调用方按具体子类的类名判别。
21
+ */
22
+ export declare class HttpAdapterError extends Error {
23
+ readonly code: string;
24
+ constructor(message: string, code: string);
25
+ }
26
+ /**
27
+ * 构造期配置校验失败(US-212 AC#31)。
28
+ *
29
+ * @remarks
30
+ * 消息**必须**含字段名与实际值:构造期报错没有调用栈上下文,不带字段名等于让接入方猜。
31
+ */
32
+ export declare class HttpConfigError extends HttpAdapterError {
33
+ readonly field: string;
34
+ readonly value: unknown;
35
+ constructor(message: string, field: string, value: unknown);
36
+ }
37
+ /**
38
+ * v1 不支持 changelog / 分支同步(US-212 AC#10、#26)。
39
+ *
40
+ * @remarks
41
+ * 返回空数组或 `0` **算失败**:Full/Filter 会把它读成「远端无变更」并覆盖本地认知。
42
+ * unsupported throw 是唯一诚实行为。
43
+ */
44
+ export declare class HttpChangelogUnsupportedError extends HttpAdapterError {
45
+ readonly operation: string;
46
+ constructor(operation: string);
47
+ }
48
+ /**
49
+ * 实体声明了本故事未定义 wire codec 的字段类型(US-212 AC#15)。
50
+ *
51
+ * @remarks
52
+ * 在 `connect()` 扫描时抛,不是等首次查询。`JSON.stringify` 会把 `7n` 弄丢、
53
+ * 把 `Uint8Array` 塌成 `{"0":1,…}`——US-018 已经为同一类静默丢失付过学费。
54
+ */
55
+ export declare class HttpUnsupportedWireTypeError extends HttpAdapterError {
56
+ readonly entity: string;
57
+ readonly property: string;
58
+ readonly propertyType: string;
59
+ constructor(entity: string, property: string, propertyType: string);
60
+ }
61
+ /**
62
+ * 本适配器不支持该操作(US-212 AC#24、AC#32)。
63
+ *
64
+ * @remarks
65
+ * 两类调用点共用:`getRepository` / `saveMany` / `removeMany` / `mutations` 四个
66
+ * v1 无实现的必选成员(默认理由),以及 `version()` 这种「装了对应 handler 才有」的成员
67
+ * (由 `reason` 说明差别)。静默返回空数组 / `undefined` 会让调用方以为操作成功了。
68
+ *
69
+ * `reason` 可覆盖是必要的:`version` 的默认理由「v1 只支持 QueryCache」是错的——
70
+ * 配上 `onVersion` 它就能工作,把接入方指向错误的修复方向比不解释更费时间。
71
+ */
72
+ export declare class HttpUnsupportedOperationError extends HttpAdapterError {
73
+ readonly operation: string;
74
+ readonly reason: string;
75
+ constructor(operation: string, reason?: string);
76
+ }
77
+ /**
78
+ * handler 的 `parse` 返回了不合契约的形态。
79
+ *
80
+ * @remarks
81
+ * handler 是接入方写的代码,本包无法在类型层面担保它的运行时返回值。让一个字符串或
82
+ * `null` 顺着流下去,最终会以「这一页没有行」的面目出现——又是一次静默截断。
83
+ * 在解析出口就拦住,错误里带上是哪个 handler、拿到了什么。
84
+ */
85
+ export declare class HttpHandlerContractError extends HttpAdapterError {
86
+ readonly handler: string;
87
+ readonly entityName: string;
88
+ constructor(handler: string, entityName: string, detail: string);
89
+ }
90
+ /** 翻页 fail-fast 的四种成因(US-212 AC#7)。 */
91
+ export type HttpPaginationFailure =
92
+ /** 同一次查询中途换了返回形态(数组 ↔ `{ rows, nextPageToken }` 对象) */
93
+ 'shape_switch'
94
+ /** `nextPageToken` 与上一页相同,再翻就是死循环 */
95
+ | 'page_token_not_advancing'
96
+ /** 连续空页达到 `maxEmptyPages`,远端在空转 */
97
+ | 'empty_page_limit'
98
+ /** 总页数超过 `maxPages` */
99
+ | 'max_pages';
100
+ /**
101
+ * 翻页过程中检测到退化的 handler / 远端(US-212 AC#7)。
102
+ *
103
+ * @remarks
104
+ * 四种成因共用一个类、由 {@link HttpPaginationError.reason} 区分——它们是同一类故障
105
+ * (翻页无法安全终止)的不同触发点,拆成四个类只会让 `catch` 侧多写三个 `instanceof`。
106
+ * AC#7 要求的「各自抛可区分的错误」由 `reason` 满足,且四条各有独立用例。
107
+ *
108
+ * **任何一种都不得返回已拿到的部分结果**——那就是把静默截断搬进了客户端,
109
+ * 正是本包要防的假孤儿。
110
+ */
111
+ export declare class HttpPaginationError extends HttpAdapterError {
112
+ readonly reason: HttpPaginationFailure;
113
+ constructor(reason: HttpPaginationFailure, message: string);
114
+ }
115
+ /**
116
+ * 远端返回的 metadata 不合契约(US-212 AC#14)。
117
+ *
118
+ * @remarks
119
+ * 覆盖两类:缺 `id` / `updatedAt` 字段,以及 `updatedAt` 不是合法 ISO 8601 时间串。
120
+ * 后者**不能靠类型检查拦住**——不规范的字符串照样是 `string`,`diffMetadata` 照常运行,
121
+ * 错的只是结论(字典序比较得出反向答案,缓存卡死或无谓重拉)。
122
+ */
123
+ export declare class HttpInvalidMetadataError extends HttpAdapterError {
124
+ readonly entityName: string;
125
+ readonly detail: string;
126
+ constructor(entityName: string, detail: string);
127
+ }
128
+ /**
129
+ * 适配器已断开:请求被 `disconnect()` 取消,或断开后仍调用 duck(US-212 AC#24、#34)。
130
+ *
131
+ * @remarks
132
+ * **必须与超时可区分。** 超时抛 core 的 `NetworkOfflineError`(`isNetworkError` 判 `true`,
133
+ * 可降级到缓存);主动断开抛本类(判 `false`,**不得**降级——「取消」静默变成「返回缓存」
134
+ * 是 core 的 `NETWORK_ERROR_NAMES` 特意排除 `AbortError` 要防的事)。
135
+ *
136
+ * 本类既不叫 `NetworkError`/`TimeoutError`,也不带数字 `status`,因此 `isNetworkError`
137
+ * 五条判据一条不中,稳定判 `false`。
138
+ */
139
+ export declare class HttpDisconnectedError extends HttpAdapterError {
140
+ readonly operation: string;
141
+ constructor(operation: string);
142
+ }
143
+ /**
144
+ * 响应状态是 2xx,但响应体不是合法 JSON(US-212 AC#12)。
145
+ *
146
+ * @remarks
147
+ * 真实形态:代理或网关返回 `200` + HTML 错误页。裸 `SyntaxError` 冒到调用方那里
148
+ * 既没有 URL 也没有状态码,只能靠猜。
149
+ *
150
+ * 与 {@link HttpResponseError} 一样**带数字 `status`**:请求确实拿到了响应,
151
+ * 连接是通的,`isNetworkError` 判 `false`,不该降级到缓存。
152
+ */
153
+ export declare class HttpInvalidResponseError extends HttpAdapterError {
154
+ readonly status: number;
155
+ readonly url: string;
156
+ readonly detail: string;
157
+ constructor(status: number, url: string, detail: string);
158
+ }
159
+ /** 请求还没发出去就构造失败的四种成因 */
160
+ export type HttpRequestBuildFailure =
161
+ /** `JSON.stringify` 拒绝序列化请求体(bigint、循环引用等) */
162
+ 'body'
163
+ /** 要拼进路径的主键不是非空字符串 */
164
+ | 'id'
165
+ /** auth hook 或静态配置给出了非法的 header 名 / 值 */
166
+ | 'headers'
167
+ /** 方法与请求体不相容:`GET` / `HEAD` 按 fetch 规范不得携带 body */
168
+ | 'method'
169
+ /** `baseUrl` 与路径拼不出一个可解析的 URL */
170
+ | 'url';
171
+ /**
172
+ * 请求在**发出之前**就构造失败(US-212 AC#12 / #34)。
173
+ *
174
+ * @remarks
175
+ * 存在的唯一理由是把「本地数据/配置有问题」从「网络断了」里摘出来。
176
+ * `JSON.stringify` 遇 bigint 抛的是 `TypeError`,与 `fetch` 传输失败抛的**完全同型**;
177
+ * 若让它落进 `classify()`,就会被包成 `NetworkOfflineError`,
178
+ * 进而被 `offlineFallback` 静默换成陈旧缓存——脏数据与配置 bug 从此永远浮不出来。
179
+ *
180
+ * 五种成因共用一个类、由 {@link HttpRequestBuildError.reason} 区分,理由同
181
+ * {@link HttpPaginationError}:调用侧的处置一致(修本地,别重试),
182
+ * 拆成五个类只会让 catch 侧多写四个 `instanceof`。
183
+ *
184
+ * `method` 与 `url` 两条针对的是**同一个陷阱的另外两个入口**:`fetch` 对
185
+ * 「`GET` 带 body」与「URL 解析不了」抛的都是裸 `TypeError`,与传输失败同型,
186
+ * 落进 `classify()` 后同样会变成一次静默的缓存降级。它们不是理论边界——
187
+ * 把 `onFetchMetadata` 的方法覆盖成 `GET`(它恒带 body)就能走到第一条。
188
+ *
189
+ * `id` 那一条是**数据**问题而不是配置问题,这正是它不归 {@link HttpConfigError} 的理由:
190
+ * 主键为空时 `PUT /Recipe/` 会被不少路由框架读成集合端点,一次更新静默变成一次批量写;
191
+ * 而没有任何配置项改了能修好它,报成配置错误只会把排查方向指向一个不存在的选项。
192
+ *
193
+ * **不带 `status`**,但也**不是**网络错误:`isNetworkError` 的 5 条判据一条都不命中,
194
+ * 于是既不会被降级回退,也不会被误报成远端故障。
195
+ */
196
+ export declare class HttpRequestBuildError extends HttpAdapterError {
197
+ readonly reason: HttpRequestBuildFailure;
198
+ readonly operation: string;
199
+ constructor(reason: HttpRequestBuildFailure, operation: string, detail: string);
200
+ }
201
+ /**
202
+ * 远端返回了非 2xx 响应(US-212 AC#12)。
203
+ *
204
+ * @remarks
205
+ * **必须带数字 `status`**:`isNetworkError` 的第 2 条判据「拿到了状态码就说明连接是通的」
206
+ * 一命中即判非网络错误,401 / 409 / 422 于是自动不会被 `offlineFallback` 吞成缓存命中。
207
+ * 反过来说——传输失败抛出的错误**绝不能**带 `status`,那会把分类结果原地抵消。
208
+ */
209
+ export declare class HttpResponseError extends HttpAdapterError {
210
+ readonly status: number;
211
+ readonly url: string;
212
+ readonly body?: string | undefined;
213
+ constructor(status: number, url: string, body?: string | undefined);
214
+ }
215
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * handler `parse` 返回值的运行期出口校验(US-212 AC#26)。
4
+ *
5
+ * @remarks
6
+ * handler 的返回类型(`R`、`string`)是**接入方声明**的东西,本包一侧只有编译期的
7
+ * 一句断言,运行期什么担保都没有。`findByIds` 与 `fetchMetadata` 两条读路径各自
8
+ * 已有出口校验({@link findByIdsInChunks} 判数组、`metadata.ts` 判 `updatedAt`),
9
+ * 写路径与 `version` 此前是空的——本模块补的就是这两处。
10
+ *
11
+ * 校验放在**适配器**而不是各个 handler 里:`rest.ts` 那套工厂能自查,手写 handler
12
+ * 不能,而两者走的是同一个 duck。只在工厂里查等于「用了工厂的人才受保护」。
13
+ */
14
+ /**
15
+ * 写回执必须是一个对象。
16
+ *
17
+ * @remarks
18
+ * core 把这个返回值当「服务端最终形态」直接写进本地缓存(id 与时间戳都由远端决定)。
19
+ * 收下 `null`、字符串或数组,本地就会留下一条远端从不存在的行,而写操作本身报的是成功。
20
+ *
21
+ * @param handler - 出问题的 duck 名(`create` / `update`),进错误信息
22
+ * @param entityName - 实体名,进错误信息
23
+ * @param body - `parse` 的返回值
24
+ * @returns 原值,已确认是对象
25
+ * @throws HttpHandlerContractError 不是普通对象(含数组与 `null`)
26
+ */
27
+ export declare const assertHandlerRow: (handler: string, entityName: string, body: unknown) => unknown;
28
+ /**
29
+ * 远端版本必须是一个非空串。
30
+ *
31
+ * @remarks
32
+ * 空串(与只有空白的串)过得了 `typeof === 'string'`,但作为版本号它只能是
33
+ * 「没解析出来」。放行会让调用方的 `startsWith` / 区间比较在一个空值上得出安静的
34
+ * 错答案——比抛错更难查,因为 `version()` 报的是成功。
35
+ *
36
+ * @param value - `onVersion.parse` 的返回值
37
+ * @returns 原值,已确认是非空串
38
+ * @throws HttpHandlerContractError 不是字符串,或去掉空白后为空
39
+ */
40
+ export declare const assertHandlerVersion: (value: unknown) => string;
41
+ //# sourceMappingURL=handler-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-contract.d.ts","sourceRoot":"","sources":["../src/handler-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAgBH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,EAAE,YAAY,MAAM,EAAE,MAAM,OAAO,KAAG,OASrF,CAAC;AAKF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,OAAO,KAAG,MASrD,CAAC"}