@aiao/rxdb-plugin-tree 0.0.0-stage → 0.0.28

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/dist/constants-DRludxr-.js +41 -0
  4. package/dist/constants.d.ts +49 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/entity/tree-entity-base.d.ts +44 -0
  7. package/dist/entity/tree-entity-base.d.ts.map +1 -0
  8. package/dist/entity/tree-entity.decorator.d.ts +7 -0
  9. package/dist/entity/tree-entity.decorator.d.ts.map +1 -0
  10. package/dist/entity/tree-entity.interface.d.ts +36 -0
  11. package/dist/entity/tree-entity.interface.d.ts.map +1 -0
  12. package/dist/entity/tree-metadata.interface.d.ts +34 -0
  13. package/dist/entity/tree-metadata.interface.d.ts.map +1 -0
  14. package/dist/generator/TreeRepositoryGenerator.d.ts +37 -0
  15. package/dist/generator/TreeRepositoryGenerator.d.ts.map +1 -0
  16. package/dist/generator/index.d.ts +17 -0
  17. package/dist/generator/index.d.ts.map +1 -0
  18. package/dist/generator.js +87 -0
  19. package/dist/index.d.ts +23 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +564 -0
  22. package/dist/plugin.d.ts +84 -0
  23. package/dist/plugin.d.ts.map +1 -0
  24. package/dist/query/merge-update-tree.d.ts +80 -0
  25. package/dist/query/merge-update-tree.d.ts.map +1 -0
  26. package/dist/query/merge_create.d.ts +9 -0
  27. package/dist/query/merge_create.d.ts.map +1 -0
  28. package/dist/query/merge_remove.d.ts +9 -0
  29. package/dist/query/merge_remove.d.ts.map +1 -0
  30. package/dist/query/merge_update.d.ts +9 -0
  31. package/dist/query/merge_update.d.ts.map +1 -0
  32. package/dist/query/query-tree.utils.d.ts +87 -0
  33. package/dist/query/query-tree.utils.d.ts.map +1 -0
  34. package/dist/query/tree-helper.d.ts +143 -0
  35. package/dist/query/tree-helper.d.ts.map +1 -0
  36. package/dist/query/tree-query.interface.d.ts +53 -0
  37. package/dist/query/tree-query.interface.d.ts.map +1 -0
  38. package/dist/repository/TreeRepository.d.ts +56 -0
  39. package/dist/repository/TreeRepository.d.ts.map +1 -0
  40. package/dist/repository/tree-level.utils.d.ts +9 -0
  41. package/dist/repository/tree-level.utils.d.ts.map +1 -0
  42. package/dist/repository/tree-repository.interface.d.ts +70 -0
  43. package/dist/repository/tree-repository.interface.d.ts.map +1 -0
  44. package/package.json +63 -3
  45. package/src/constants.ts +97 -0
  46. package/src/entity/tree-entity-base.ts +100 -0
  47. package/src/entity/tree-entity.decorator.ts +37 -0
  48. package/src/entity/tree-entity.interface.ts +43 -0
  49. package/src/entity/tree-metadata.interface.ts +35 -0
  50. package/src/generator/TreeRepositoryGenerator.ts +153 -0
  51. package/src/generator/index.ts +17 -0
  52. package/src/index.ts +23 -0
  53. package/src/plugin.ts +111 -0
  54. package/src/query/merge-update-tree.ts +558 -0
  55. package/src/query/merge_create.ts +125 -0
  56. package/src/query/merge_remove.ts +164 -0
  57. package/src/query/merge_update.ts +132 -0
  58. package/src/query/query-tree.utils.ts +204 -0
  59. package/src/query/tree-helper.ts +342 -0
  60. package/src/query/tree-query.interface.ts +66 -0
  61. package/src/repository/TreeRepository.ts +133 -0
  62. package/src/repository/tree-level.utils.ts +14 -0
  63. package/src/repository/tree-repository.interface.ts +76 -0
@@ -0,0 +1,558 @@
1
+ import {
2
+ applyExternalEntityUpdate,
3
+ EntityType,
4
+ getEntityId,
5
+ QueryTask,
6
+ RxDBEntityId,
7
+ RxDBEntityLocalUpdatedEventData,
8
+ UpdateClassification,
9
+ UpdateDataCache
10
+ } from '@aiao/rxdb';
11
+ import { FindTreeOptions } from '../repository/tree-repository.interface.js';
12
+ import { get_tree_parent_id, hasTreeParentChanged } from './query-tree.utils.js';
13
+ import { resolveAncestorForCount, TreeHelper } from './tree-helper.js';
14
+
15
+ /**
16
+ * 一个**已在结果集中**的实体在本批 UPDATE 后的去留判定
17
+ *
18
+ * - `keep` 留在结果里
19
+ * - `drop` 因 where 命中翻为不匹配、或改父移出子树而移除。两种都会截断递归 CTE 的遍历,
20
+ * 其名下未被直接更新的子孙要做孤儿复查(RV-046:此前 where 翻转「不牵连子树」,
21
+ * 与 SQL 重查分叉)
22
+ * - `refresh` 本地判不了,交回 SQL 重算
23
+ */
24
+ type DescendantVerdict = 'keep' | 'drop' | 'refresh';
25
+
26
+ /**
27
+ * 处理 findDescendants 查询的增量更新
28
+ *
29
+ * UPDATE 场景下的复杂性:
30
+ * 1. 实体的 parentId 可能变化,导致树形结构重组
31
+ * 2. 实体可能从不是后代变成后代,或从后代变成非后代
32
+ * 3. 实体仍然是后代,但字段值变化(如名称、描述等)
33
+ * 4. 需要考虑 level 限制(只查询特定层级的后代)
34
+ *
35
+ * 更新策略:
36
+ * - 移除: 不再匹配 where 条件 或 不再是后代 或 超出 level 范围
37
+ * - 更新: 仍在结果集中的实体,更新其字段值
38
+ * - 添加: 新匹配 where 条件 且 成为后代 且 在 level 范围内
39
+ *
40
+ * @param task 查询任务
41
+ * @param data 更新的实体数据列表
42
+ * @param classification 更新分类结果
43
+ * @param cache 更新数据缓存
44
+ */
45
+ export const handleFindDescendantsUpdate = <T extends EntityType>(
46
+ task: QueryTask<T>,
47
+ data: RxDBEntityLocalUpdatedEventData<T>[],
48
+ classification: UpdateClassification,
49
+ cache: UpdateDataCache<T>
50
+ ) => {
51
+ const options = task.options as FindTreeOptions<T>;
52
+ const targetEntityId = options.entityId as RxDBEntityId | null | undefined; // 目标实体ID (查询谁的后代)
53
+ const level = options.level; // 层级限制 (undefined 表示无限制)
54
+
55
+ // 构建旧结果集的映射,用于快速查找
56
+ const oldResult = Array.from(task.resultEntitySet.values());
57
+ const oldResultMap = new Map<RxDBEntityId, InstanceType<T>>();
58
+ oldResult.forEach(existing => {
59
+ const existingId = getEntityId(existing);
60
+ if (existingId !== undefined) {
61
+ oldResultMap.set(existingId, existing);
62
+ }
63
+ });
64
+
65
+ // 旧结果集成员的冻结快照:`TreeHelper.resolveParentEntity` 走父链时会把本批序列化
66
+ // 实体回填进 `oldResultMap`,之后再拿它判「此前是否已在结果里」就会把刚走过的父节点
67
+ // 误判成旧成员。成员判定只读这份快照。
68
+ const oldResultIds = new Set(oldResultMap.keys());
69
+ const helper = new TreeHelper(cache, oldResultMap);
70
+ let hasChanges = false; // 标记是否有变化,避免不必要的更新
71
+ // 记录本批被摘出结果的非锚点(改父移出子树 / where 由匹配翻为不匹配),供孤儿复查使用:
72
+ // 递归 CTE 在递归成员上过 where,两种摘除都会把它名下整棵子树一起截断(RV-046)
73
+ const cutFromScope = new Set<RxDBEntityId>();
74
+
75
+ /**
76
+ * 该实体是否是递归 CTE 的**基准成员**行。
77
+ *
78
+ * 适配器生成的 SQL 里,基准成员是 `WHERE id = ?`(指定了 entityId)或
79
+ * `WHERE parentId IS NULL`(查全树),**既不带 where 规则、也不带
80
+ * `c.__level < N`** —— 两者只挂在递归成员上。所以锚点(或查全树时的各个根节点)
81
+ * 无论 where 命中怎么翻转、自身 parentId 怎么改,SQL 重跑都照样返回它。
82
+ * 增量合并必须同口径,否则本地会摘掉一行 SQL 一定会给的记录。
83
+ */
84
+ const isTreeAnchor = (entityId: RxDBEntityId, current: InstanceType<T>): boolean =>
85
+ targetEntityId === null || targetEntityId === undefined ?
86
+ get_tree_parent_id(current) === null
87
+ : entityId === targetEntityId;
88
+
89
+ /**
90
+ * 判定一个**已在结果集中**的实体在本批 UPDATE 后的去留。
91
+ *
92
+ * 抽成独立函数有两个理由:把原先的五层分支链压回三层以内(AGENTS.md 铁律),
93
+ * 以及让「序列化缺失」能表达成 {@link DescendantVerdict} 的 `refresh`
94
+ * —— `filter` 回调只能回布尔值,当初正是这个限制逼出了用裸 patch 合成假实体的兜底。
95
+ */
96
+ const verdictForExisting = (entityId: RxDBEntityId, entity: InstanceType<T>): DescendantVerdict => {
97
+ if (!classification.updatedIds.has(entityId)) return 'keep';
98
+
99
+ // 情况0: 基准成员恒在结果中 —— where 翻转与自身改父都不能把它摘掉。
100
+ // 锚点自己改父时,下面"是否仍是自己的后代"必然判否(节点不是自己的后代),
101
+ // 会把锚点连同整棵子树(孤儿复查)一起清空,而 SQL 其实一行没少。
102
+ if (isTreeAnchor(entityId, cache.getSerializedUpdate(entityId) ?? entity)) return 'keep';
103
+
104
+ // 情况1: 实体从匹配变为不匹配 where 条件 → 移除,子树随孤儿复查一并摘除
105
+ if (classification.newlyUnmatchedIds.has(entityId)) return 'drop';
106
+
107
+ // 情况2: 实体更新后仍然匹配,检查是否还是后代且在 level 范围内
108
+ if (!classification.matchNowIds.has(entityId)) return 'keep';
109
+
110
+ const eventData = cache.getData(entityId);
111
+ if (!eventData || !hasTreeParentChanged(eventData)) return 'keep';
112
+
113
+ // 序列化结果是"这个节点现在挂在哪"的唯一可信来源:`QueryManager.#serialize` 带
114
+ // P0-004 单调性守卫,迟到事件会原样返回缓存中的实体。取不到就说明本地根本不知道
115
+ // 它当前的位置 —— 用裸 patch 合成一个只有 `{ id, parentId }` 的假实体去走树,
116
+ // 走出来的层级没有依据(假实体没有真实父链,`isEntityDescendant` 只能靠
117
+ // patch 里那一跳)。交回 SQL 重算,不猜。
118
+ const updatedEntity = cache.getSerializedUpdate(entityId);
119
+ if (!updatedEntity) return 'refresh';
120
+
121
+ const { isDescendant, level: entityLevel } = helper.isEntityDescendant(updatedEntity, targetEntityId);
122
+ return isDescendant && (level === undefined || entityLevel <= level) ? 'keep' : 'drop';
123
+ };
124
+
125
+ let needsRefresh = false;
126
+ const afterRemoval = oldResult.filter(entity => {
127
+ const entityId = getEntityId(entity);
128
+ if (entityId === undefined) return true; // 无ID的实体保留(理论上不应该出现)
129
+
130
+ const verdict = verdictForExisting(entityId, entity);
131
+ if (verdict === 'refresh') {
132
+ needsRefresh = true;
133
+ return true;
134
+ }
135
+ if (verdict === 'keep') return true;
136
+ cutFromScope.add(entityId);
137
+ hasChanges = true;
138
+ return false;
139
+ });
140
+
141
+ if (needsRefresh) {
142
+ task.refresh();
143
+ return;
144
+ }
145
+
146
+ // 即使实体仍然是后代,其字段值(如 title、description)可能已变化,需要更新
147
+ const afterFieldUpdate = afterRemoval.map(entity => {
148
+ const entityId = getEntityId(entity);
149
+ if (entityId !== undefined && classification.updatedIds.has(entityId)) {
150
+ const eventData = cache.getData(entityId);
151
+ if (eventData) {
152
+ applyExternalEntityUpdate(entity, eventData.patch);
153
+ hasChanges = true;
154
+ }
155
+ }
156
+ return entity;
157
+ });
158
+
159
+ // 中间节点 P 改 parentId 移出子树、或 where 翻为不匹配后,第一步只看每个实体自己,
160
+ // 无法发现 P 名下的子节点 X —— 它们会作为孤儿残留在结果集中。
161
+ // 保守策略:仅当能"证明"某节点的父链穿过本批被移出的节点时才移除;
162
+ // 父链无法解析(缺失/循环)时一律保留,避免误删合法节点。
163
+ let updatedEntities = afterFieldUpdate;
164
+ if (cutFromScope.size > 0) {
165
+ const survivingMap = new Map<RxDBEntityId, InstanceType<T>>();
166
+ afterFieldUpdate.forEach(entity => {
167
+ const id = getEntityId(entity);
168
+ if (id !== undefined) survivingMap.set(id, entity);
169
+ });
170
+ const resolveParent = (id: RxDBEntityId): InstanceType<T> | undefined =>
171
+ cache.getSerializedUpdate(id) ?? survivingMap.get(id) ?? oldResultMap.get(id);
172
+
173
+ // 批级记忆:兄弟节点共享同一条父链,逐节点从头回溯是 O(n·depth)(拖拽流程每次都触发)。
174
+ // `orphanMemo` 记「这个 id 是否已因自身或某个祖先被移出子树而脱离结果集」。
175
+ // 走链时把"结论未知"的节点收进 `chain` —— 它们都是同一个断点/终点的后代,
176
+ // 共享同一个结论,循环结束后统一回填,整批摊平成 O(n)。
177
+ // 与 `merge_remove.ts` 的级联判定同构。
178
+ const orphanMemo = new Map<RxDBEntityId, boolean>();
179
+ const isOrphaned = (entity: InstanceType<T>): boolean => {
180
+ const entityId = getEntityId(entity);
181
+ const cached = entityId === undefined ? undefined : orphanMemo.get(entityId);
182
+ if (cached !== undefined) return cached;
183
+
184
+ const chain: RxDBEntityId[] = entityId === undefined ? [] : [entityId];
185
+ let orphaned = false;
186
+ let currentParentId = get_tree_parent_id<RxDBEntityId>(entity);
187
+ const visited = new Set<RxDBEntityId>();
188
+ while (currentParentId !== null && !visited.has(currentParentId)) {
189
+ const known = orphanMemo.get(currentParentId);
190
+ if (known !== undefined) {
191
+ orphaned = known; // 这条链的上半段之前算过
192
+ break;
193
+ }
194
+ if (cutFromScope.has(currentParentId)) {
195
+ orphaned = true; // 父链穿过被移出节点
196
+ break;
197
+ }
198
+ if (currentParentId === targetEntityId) break; // 仍连到目标
199
+ visited.add(currentParentId);
200
+ chain.push(currentParentId);
201
+ const parent = resolveParent(currentParentId);
202
+ if (!parent) break; // 链路缺失,保守保留
203
+ currentParentId = get_tree_parent_id<RxDBEntityId>(parent);
204
+ }
205
+
206
+ // `chain` 里只有"既没被移出、也不是 target"的节点:它们各自的父链就是本次走过的
207
+ // 剩余后缀,结论与起点完全一致。链路断裂或循环时结论是 false(保守保留)。
208
+ for (const id of chain) orphanMemo.set(id, orphaned);
209
+ return orphaned;
210
+ };
211
+ updatedEntities = afterFieldUpdate.filter(entity => {
212
+ const entityId = getEntityId(entity);
213
+ // 本批被直接更新、第一步判 keep 的实体也要复查:只改了字段的子节点,第一步看不到
214
+ // 它的父节点刚被摘掉。`entity` 已应用 patch,走的是更新后的父链。锚点恒在结果中。
215
+ if (entityId !== undefined && !isTreeAnchor(entityId, entity) && isOrphaned(entity)) {
216
+ hasChanges = true;
217
+ return false;
218
+ }
219
+ return true;
220
+ });
221
+ }
222
+
223
+ // 不在旧结果集、更新后匹配 where 的实体,有两条路进入 scope:
224
+ // - 父节点真的动了(判据走 `hasTreeParentChanged` 这一处实现,不在这里重抄 ——
225
+ // 抄一份就等于给 null/undefined 归一再开一个分叉口,F-11);
226
+ // - where 由不匹配翻为匹配:递归 CTE 在递归成员上过 where,它此前把整条遍历截断在
227
+ // 这里,名下匹配的子孙同样从未进过结果(RV-046)。
228
+ // 两种情况下本地增量缓存都看不到它的既有子孙,也无法证明它没有子孙;只把它自己
229
+ // 加进结果会漏掉整棵子树,跨 scope 边界只能整体交回 SQL 重算。
230
+ // 父链走不通(未知父节点)时 `isEntityDescendant` 在全树模式下仍判真 —— 这里只拿它
231
+ // 当「可能进入 scope」的保守信号去 refresh,而不是据此把实体直接拼进结果。
232
+ const entitiesToCheck = data.filter(d => {
233
+ const entityId = d.id as RxDBEntityId;
234
+ if (!classification.matchNowIds.has(entityId) || oldResultIds.has(entityId)) return false;
235
+ return classification.newlyMatchedIds.has(entityId) || hasTreeParentChanged(d);
236
+ });
237
+
238
+ for (const event of entitiesToCheck) {
239
+ const serialized = cache.getSerializedUpdate(event.id as RxDBEntityId);
240
+ if (!serialized) continue;
241
+
242
+ const { isDescendant, level: entityLevel } = helper.isEntityDescendant(serialized, targetEntityId);
243
+ if (isDescendant && (level === undefined || entityLevel <= level)) {
244
+ task.refresh();
245
+ return;
246
+ }
247
+ }
248
+
249
+ if (!hasChanges) {
250
+ return; // 没有变化,直接返回,避免不必要的通知
251
+ }
252
+ // 通知查询任务结果已更新
253
+ task.next(updatedEntities, true);
254
+ };
255
+
256
+ /**
257
+ * 处理 findAncestors 查询的增量更新
258
+ *
259
+ * UPDATE 场景下的复杂性:
260
+ * 1. 目标实体的祖先链路可能因为其他实体的 parentId 变化而改变
261
+ * 2. 实体可能从不是祖先变成祖先,或从祖先变成非祖先
262
+ * 3. 实体仍然是祖先,但字段值变化
263
+ * 4. 目标实体本身也是结果的一部分(level=0)
264
+ *
265
+ * 特殊情况:
266
+ * - 如果目标实体的 parentId 变化,整个祖先链都会改变,需要完全刷新
267
+ *
268
+ * @param task 查询任务
269
+ * @param data 更新的实体数据列表
270
+ * @param classification 更新分类结果
271
+ * @param cache 更新数据缓存
272
+ */
273
+ export const handleFindAncestorsUpdate = <T extends EntityType>(
274
+ task: QueryTask<T>,
275
+ data: RxDBEntityLocalUpdatedEventData<T>[],
276
+ classification: UpdateClassification,
277
+ cache: UpdateDataCache<T>
278
+ ) => {
279
+ const options = task.options as FindTreeOptions<T>;
280
+ const targetEntityId = options.entityId as RxDBEntityId | null | undefined; // 目标实体ID (查询谁的祖先)
281
+ const level = options.level; // 层级上限 (undefined 表示无限制)
282
+
283
+ // 构建旧结果集的映射
284
+ const oldResult = Array.from(task.resultEntitySet.values());
285
+ const oldResultMap = new Map<RxDBEntityId, InstanceType<T>>();
286
+ oldResult.forEach(existing => {
287
+ const existingId = getEntityId(existing);
288
+ if (existingId !== undefined) {
289
+ oldResultMap.set(existingId, existing);
290
+ }
291
+ });
292
+
293
+ // 走父链会把序列化实体回填进 `oldResultMap`,成员判定读冻结快照(同 findDescendants)
294
+ const oldResultIds = new Set(oldResultMap.keys());
295
+ const helper = new TreeHelper(cache, oldResultMap);
296
+
297
+ /**
298
+ * 获取目标实体的当前状态
299
+ * 优先从更新数据中获取,否则从旧结果集获取
300
+ */
301
+ const getTargetEntity = (): InstanceType<T> | undefined => {
302
+ if (targetEntityId === null || targetEntityId === undefined) {
303
+ return undefined;
304
+ }
305
+ const targetSerialized = cache.getSerializedUpdate(targetEntityId);
306
+ if (targetSerialized) {
307
+ return targetSerialized;
308
+ }
309
+ return oldResultMap.get(targetEntityId);
310
+ };
311
+
312
+ // 祖先链是一条自 target 向上的单链。本批内**任一**节点换父,都会让移动点之上的
313
+ // 整段链路发生变化——旧链路上方的祖先需要摘除,新链路上方的祖先需要补入。但新链路
314
+ // 上方那些节点从未出现在本批 UPDATE 事件里(只有被移动的节点自己变了,它的新父节点
315
+ // 及更上游对本地增量缓存完全陌生),局部只能摘除摘不了新增,无法拼出正确结果
316
+ // (这也是此前 needRecheckAll 分支的实际缺陷:它只会做旧链路的移除判断,
317
+ // 从不会发现新链路上方的祖先)。只能整体交回 SQL 重算。
318
+ //
319
+ // 判定范围必须是整批,而不能只看「target 自己」或「已在结果集里的祖先」:链上一个
320
+ // 不匹配 where、因而不在结果集里的中间节点改父,同样会让它上方的祖先整体进出结果。
321
+ // 这条规则与 `merge_update` 入口处的守卫是同一条(共用 `hasTreeParentChanged`),
322
+ // 入口那层先拦一道,是因为这类节点连 recalculate 的门都进不来。
323
+ if (data.some(hasTreeParentChanged)) {
324
+ task.refresh();
325
+ return;
326
+ }
327
+
328
+ // 走到这里说明本批没有任何祖先链位置发生变化(上面已提前 refresh),
329
+ // 剩下的只有纯 where 匹配翻转。递归成员上过 where:链上一个非目标节点翻为不匹配,
330
+ // 它上方的整段祖先随之被截断;翻为匹配,它上方此前被截断的祖先重新可达。上方那些
331
+ // 节点不在本批事件里,本地只能摘/补这一个节点,与 SQL 重查分叉(RV-046)。目标自己
332
+ // 是基准成员,不过 where,翻转不影响结果。
333
+ const targetNow = getTargetEntity();
334
+ const flipsOnChain = (id: RxDBEntityId): boolean => {
335
+ if (id === targetEntityId) return false;
336
+ if (oldResultIds.has(id)) return true;
337
+ const serialized = cache.getSerializedUpdate(id);
338
+ return !!targetNow && !!serialized && helper.isEntityAncestor(targetNow, serialized, level);
339
+ };
340
+ const flipped = [...classification.newlyMatchedIds, ...classification.newlyUnmatchedIds];
341
+ if (flipped.some(flipsOnChain)) {
342
+ task.refresh();
343
+ return;
344
+ }
345
+
346
+ let hasChanges = false;
347
+ const updatedEntities = oldResult.map(entity => {
348
+ const entityId = getEntityId(entity);
349
+ if (entityId !== undefined && classification.updatedIds.has(entityId)) {
350
+ const eventData = cache.getData(entityId);
351
+ if (eventData) {
352
+ applyExternalEntityUpdate(entity, eventData.patch);
353
+ hasChanges = true;
354
+ }
355
+ }
356
+ return entity;
357
+ });
358
+
359
+ // findAncestors 总是包含目标实体本身(level=0)
360
+ if (targetNow) {
361
+ const targetId = getEntityId(targetNow);
362
+ if (targetId !== undefined) {
363
+ const targetInResult = updatedEntities.some(e => getEntityId(e) === targetId);
364
+ if (!targetInResult) {
365
+ hasChanges = true;
366
+ updatedEntities.push(targetNow);
367
+ }
368
+ }
369
+ }
370
+
371
+ if (!hasChanges) {
372
+ return; // 没有变化,直接返回
373
+ }
374
+
375
+ // 通知查询任务结果已更新
376
+ task.next(updatedEntities, true);
377
+ };
378
+
379
+ /**
380
+ * 处理 countDescendants 查询的增量更新
381
+ *
382
+ * 计数查询的特点:
383
+ * - 只需要返回数量,不需要返回具体实体
384
+ * - 没有实体级追踪,看不到不在本批事件里的子孙
385
+ *
386
+ * UPDATE 场景下的计数变化:
387
+ * 1. scope 内(或无法判定)的非基准节点 parentId 变化 → 子树整体进出 / 平移深度,触发 SQL 刷新
388
+ * 2. scope 内(或无法判定)的非基准节点 where 命中翻转 → 递归遍历在它这里被截断或放开,
389
+ * 名下子孙随之进出,触发 SQL 刷新
390
+ * 3. 只改了其他字段、或变化发生在 scope 之外 → 计数不变,不通知
391
+ *
392
+ * @param task 查询任务
393
+ * @param data 更新的实体数据列表
394
+ * @param classification 更新分类结果
395
+ * @param cache 更新数据缓存
396
+ */
397
+ export const handleCountDescendantsUpdate = <T extends EntityType>(
398
+ task: QueryTask<T>,
399
+ data: RxDBEntityLocalUpdatedEventData<T>[],
400
+ classification: UpdateClassification,
401
+ cache: UpdateDataCache<T>
402
+ ) => {
403
+ const options = task.options as FindTreeOptions<T>;
404
+ const targetEntityId = options.entityId as RxDBEntityId | null | undefined; // 目标实体ID
405
+ const { level } = options; // 层级上限,口径同 FindTreeOptions.level(含当前节点)
406
+
407
+ const oldResultMap = new Map<RxDBEntityId, InstanceType<T>>();
408
+ const helper = new TreeHelper(cache, oldResultMap);
409
+
410
+ // 只在给了 level 时才按深度过滤:没给 level 就是「不限层级」,
411
+ // 沿用只判后代关系的旧路径,免得为了算深度而多要一次父链(要不到就得回 SQL)。
412
+ const isCountedDescendant = (entity: InstanceType<T> | null | undefined): boolean | undefined => {
413
+ if (level === undefined) return helper.isEntityDescendantForCount(entity, targetEntityId);
414
+ const resolved = helper.descendantDepthForCount(entity, targetEntityId);
415
+ if (resolved === undefined) return undefined;
416
+ return resolved.isDescendant && resolved.depth <= level;
417
+ };
418
+
419
+ /**
420
+ * 该实体是否是递归 CTE 的基准成员行(口径同 {@link handleFindDescendantsUpdate})。
421
+ *
422
+ * 基准成员不过 where 也不过 level。查全树(`entityId` 为空)时它是所有根节点,
423
+ * 而这条分支的 SQL 是裸 `count(*)` —— 根节点恒被计入,where 命中翻转不改变计数。
424
+ * 指定了 `entityId` 时基准成员只有目标自己,而 SQL 用 `max(count(*)-1, 0)` 把它减掉,
425
+ * 净贡献为 0,因此这里恒为 false。
426
+ */
427
+ const isTreeBaseMember = (entity: InstanceType<T> | null | undefined): boolean =>
428
+ (targetEntityId === null || targetEntityId === undefined) && !!entity && get_tree_parent_id(entity) === null;
429
+
430
+ // 递归成员上过 where:scope 内一个非基准节点 where 翻转或改父,都会把它名下整棵子树
431
+ // 一起截断/放开(改父还会整体平移子树深度,碰到 level 上限就有子孙进出)。count 只是个
432
+ // 数字,没有实体级追踪,看不到那些不在本批事件里的子孙,局部 ±1 只算得了这一个节点
433
+ // (RV-046,及 review 描述的「count 只加减 1」)。所以本处理器只做一件事:判断本批
434
+ // 有没有碰到 scope 的结构性变化,有就交回 SQL,没有就什么都不用发。
435
+ const needsRefresh = data.some(updateData => {
436
+ const entityId = updateData.id as RxDBEntityId;
437
+ // where 判定必须基于"完整实体",而 `classification` 正是拿
438
+ // `getSerializedBefore` / `getSerializedUpdate` 出来的完整实体跑同一个
439
+ // `isEntityMatchWhere` 算出来的(见 `classifyUpdates`)。直接读它的结论,
440
+ // 不在这里重算第二遍。
441
+ const whereFlipped = classification.matchBeforeIds.has(entityId) !== classification.matchNowIds.has(entityId);
442
+ const parentChanged = hasTreeParentChanged(updateData);
443
+ if (!whereFlipped && !parentChanged) return false;
444
+
445
+ const beforeEntity = cache.getSerializedBefore(entityId, updateData.inversePatch);
446
+ const afterEntity = cache.getSerializedUpdate(entityId);
447
+ // 前后都是基准成员(查全树时的根节点):不过 where、父也没动,计数不受影响
448
+ if (isTreeBaseMember(beforeEntity) && isTreeBaseMember(afterEntity)) return false;
449
+
450
+ const isDescendantBefore = isCountedDescendant(beforeEntity);
451
+ // 后代判定只读起点自身的 parentId,往上每一跳读的都是 `getSerializedUpdate`
452
+ // (更新后的祖先)。起点 parentId 没变,这趟走链与 before 那趟逐跳等价,
453
+ // 结论必然相同 —— 直接复用,不重走。
454
+ const isDescendantNow = parentChanged ? isCountedDescendant(afterEntity) : isDescendantBefore;
455
+ // 无法确定前/后的后代关系,或前后任一时刻落在 scope 内 → 交回 SQL
456
+ return isDescendantBefore !== false || isDescendantNow !== false;
457
+ });
458
+
459
+ if (needsRefresh) task.refresh();
460
+ };
461
+
462
+ /**
463
+ * 处理 countAncestors 查询的增量更新
464
+ *
465
+ * 计数查询的特点:
466
+ * - 只需要返回数量,不需要返回具体实体
467
+ * - 没有实体级追踪,看不到不在本批事件里的上方祖先
468
+ * - 如果无法确定关系(父实体链中断)或目标实体的 parentId 变化,则触发 SQL 刷新
469
+ *
470
+ * UPDATE 场景下的计数变化:
471
+ * 1. 目标实体的 parentId 变化 → 整个祖先链改变,必须触发刷新
472
+ * 2. 链上(或无法判定)的祖先 parentId 变化 → 它上方整段链被换掉,触发刷新
473
+ * 3. 链上(或无法判定)的祖先 where 命中翻转 → 递归遍历在它这里被截断或放开,
474
+ * 上方祖先随之进出,触发刷新
475
+ * 4. 只改了其他字段、或变化发生在链外 → 计数不变,不通知
476
+ *
477
+ * @param task 查询任务
478
+ * @param data 更新的实体数据列表
479
+ * @param classification 更新分类结果
480
+ * @param cache 更新数据缓存
481
+ */
482
+ export const handleCountAncestorsUpdate = <T extends EntityType>(
483
+ task: QueryTask<T>,
484
+ data: RxDBEntityLocalUpdatedEventData<T>[],
485
+ classification: UpdateClassification,
486
+ cache: UpdateDataCache<T>
487
+ ) => {
488
+ const options = task.options as FindTreeOptions<T>;
489
+ const targetEntityId = options.entityId as RxDBEntityId | null | undefined; // 目标实体ID
490
+ const { level } = options; // 层级上限,口径同 FindTreeOptions.level
491
+
492
+ if (targetEntityId === null || targetEntityId === undefined) {
493
+ // 目标实体ID无效,触发刷新
494
+ task.refresh();
495
+ return;
496
+ }
497
+
498
+ const targetUpdate = cache.getData(targetEntityId);
499
+ if (!targetUpdate) {
500
+ // 目标实体不在更新中,我们无法准确判断祖先关系
501
+ // 触发 SQL 刷新
502
+ task.refresh();
503
+ return;
504
+ }
505
+
506
+ const oldResultMap = new Map<RxDBEntityId, InstanceType<T>>();
507
+ const helper = new TreeHelper(cache, oldResultMap);
508
+
509
+ // 获取目标实体更新前后的状态
510
+ const targetBefore = cache.getSerializedBefore(targetUpdate.id as RxDBEntityId, targetUpdate.inversePatch);
511
+ const targetAfter = cache.getSerializedUpdate(targetUpdate.id as RxDBEntityId);
512
+
513
+ if (!targetBefore || !targetAfter) {
514
+ task.refresh();
515
+ return;
516
+ }
517
+
518
+ const targetParentIdBefore = get_tree_parent_id(targetBefore);
519
+ const targetParentIdAfter = get_tree_parent_id(targetAfter);
520
+
521
+ // 如果目标实体的 parentId 发生变化,祖先链会完全改变
522
+ // 必须触发 SQL 刷新以获取新的祖先列表
523
+ if (targetParentIdBefore !== targetParentIdAfter) {
524
+ task.refresh();
525
+ return;
526
+ }
527
+
528
+ // 目标的父链在本批内不变(上面 parentId 比对不等就已经 refresh 走了),链上每一跳
529
+ // 读的又都是 `getSerializedUpdate`,于是 before / after 两趟走链逐跳等价,整批候选
530
+ // 面对的是同一条链。走一次收成 Set,候选判定降为 O(1) 查表;此前是每个候选都从
531
+ // target 重走一遍整条链。
532
+ const targetAncestors = helper.collectAncestorIdsForCount(targetAfter, level);
533
+
534
+ // 链上一个祖先 where 翻转或改父,它上方的整段链都会随之截断/放开/换掉 —— 那些节点
535
+ // 不在本批事件里,per-entity 的 ±1 结构性地看不到(RV-046,及 review 描述的
536
+ // 「count 只加减 1」)。链外节点的变化碰不到这条链。所以只判断本批有没有碰到链,
537
+ // 有就交回 SQL,没有就什么都不用发。
538
+ const needsRefresh = data.some(updateData => {
539
+ const entityId = updateData.id as RxDBEntityId;
540
+ // 目标实体是基准成员(不是自己的祖先),它的 parentId 变化上面已处理
541
+ if (entityId === targetEntityId) return false;
542
+
543
+ // where 判定必须基于"完整实体",`classification` 已经用
544
+ // `getSerializedBefore` / `getSerializedUpdate` 跑过同一个 `isEntityMatchWhere`,
545
+ // 这里读结论即可(见 `classifyUpdates`)。
546
+ const whereFlipped = classification.matchBeforeIds.has(entityId) !== classification.matchNowIds.has(entityId);
547
+ if (!whereFlipped && !hasTreeParentChanged(updateData)) return false;
548
+
549
+ // 候选的 before / after 两份序列化必须分别查:`getEntityId` 可能只在其中一份上有值。
550
+ const entityBefore = cache.getSerializedBefore(entityId, updateData.inversePatch);
551
+ const wasAncestorBefore = resolveAncestorForCount(targetAncestors, entityBefore);
552
+ const isAncestorNow = resolveAncestorForCount(targetAncestors, cache.getSerializedUpdate(entityId));
553
+ // 无法确定前/后的祖先关系,或前后任一时刻在链上 → 交回 SQL
554
+ return wasAncestorBefore !== false || isAncestorNow !== false;
555
+ });
556
+
557
+ if (needsRefresh) task.refresh();
558
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @fileoverview 树查询的 CREATE 事件合并
3
+ *
4
+ * 与图查询「一律 refresh」不同,树的 `find*` 走真增量:新建节点是不是目标节点的
5
+ * 后代/祖先,本地靠 `parentId` 链就能判定,没必要回 SQL。`count*` 不维护
6
+ * `resultEntitySet`,仍只能刷新。
7
+ */
8
+ import {
9
+ EntityType,
10
+ isEntityMatchWhere,
11
+ isStaleEntityEvent,
12
+ queryNeedRefreshCreate,
13
+ QueryTask,
14
+ RefreshMatchRules,
15
+ RxDBEntityLocalCreatedEventData
16
+ } from '@aiao/rxdb';
17
+ import { TREE_QUERY_TYPES } from '../constants.js';
18
+ import { FindTreeOptions } from '../repository/tree-repository.interface.js';
19
+ import { buildEntityMap, concatIterables, isAncestorOf, isDescendantOf } from './query-tree.utils.js';
20
+
21
+ /**
22
+ * JS 增量更新查询结果
23
+ */
24
+ const _recalculate = <T extends EntityType>(task: QueryTask<T>, data: RxDBEntityLocalCreatedEventData<T>[]) => {
25
+ const where = (task.options as FindTreeOptions<T>).where;
26
+ const match_data = data.filter(e => !where || isEntityMatchWhere(e.patch, where));
27
+ if (match_data.length === 0) return;
28
+
29
+ const entities = match_data.map(d => task.serialize(d));
30
+
31
+ switch (task.type) {
32
+ case 'findDescendants': {
33
+ const options = task.options as FindTreeOptions<T>;
34
+ // 必须把本批新建实体一并纳入关系图,否则"父子同批新建"时,
35
+ // 子节点向上查父节点会查不到而被丢弃(与 findAncestors 分支保持一致)。
36
+ // 直接流式喂给 `buildEntityMap`:此前先 `Array.from` 再展开成第二个数组,
37
+ // 把整个结果集物化了两遍,而这两个数组除了建图之外没有别的用处。
38
+ const entities_map = buildEntityMap(concatIterables(task.resultEntitySet, entities), e => e.id);
39
+
40
+ const new_descendants = entities.filter(
41
+ entity =>
42
+ entity.id === options.entityId || isDescendantOf(entity, options.entityId, entities_map, options.level)
43
+ );
44
+ if (new_descendants.length === 0) return;
45
+
46
+ new_descendants.forEach(entity => task.resultEntitySet.add(entity));
47
+ task.next(Array.from(task.resultEntitySet.values()), true);
48
+ break;
49
+ }
50
+
51
+ case 'findAncestors': {
52
+ const options = task.options as FindTreeOptions<T>;
53
+ const entities_map = buildEntityMap(concatIterables(task.resultEntitySet, entities), e => e.id);
54
+
55
+ const new_ancestors = entities.filter(
56
+ entity => entity.id === options.entityId || isAncestorOf(entity, options.entityId, entities_map, options.level)
57
+ );
58
+ if (new_ancestors.length === 0) return;
59
+
60
+ new_ancestors.forEach(entity => task.resultEntitySet.add(entity));
61
+ task.next(Array.from(task.resultEntitySet.values()), true);
62
+ break;
63
+ }
64
+
65
+ case 'countDescendants':
66
+ case 'countAncestors':
67
+ // count 查询无法 JS 增量更新,由上层触发 SQL 刷新
68
+ break;
69
+ }
70
+ };
71
+
72
+ /**
73
+ * 处理树查询 CREATE 事件的缓存合并
74
+ *
75
+ * @param task 查询任务
76
+ * @param entities 创建的实体事件数据
77
+ */
78
+ export const merge_create = <T extends EntityType>(
79
+ task: QueryTask<T>,
80
+ entities: RxDBEntityLocalCreatedEventData<T>[]
81
+ ) => {
82
+ if (!TREE_QUERY_TYPES.has(task.type)) return;
83
+
84
+ // 首个权威结果尚未落地时不做增量:此刻没有可增量的基线,把事件负载直接当成第一个结果
85
+ // 等于凭空捏造一次查询答案。交给 `refresh()` 而不是直接 `return`:事件也可能**新于**
86
+ // runner 的快照,直接丢弃会让这次写入在活查询里永远缺席。
87
+ if (task.result === undefined) {
88
+ task.refresh();
89
+ return;
90
+ }
91
+
92
+ // 批内只要有一条比实体缓存更旧(创建后又被改过,CREATE 事件迟到),JS 增量的前提
93
+ // ——「事件负载 = 实体当前状态」——就不成立了:`serialize` 会返回缓存中的**新**实例,
94
+ // 而结果集里仍是**旧**的成员关系,二者拼出来的是旧祖先链与新祖先链的并集。
95
+ if (entities.some(event => isStaleEntityEvent(task.rxdb, event))) {
96
+ task.refresh();
97
+ return;
98
+ }
99
+
100
+ const refresh_rules: RefreshMatchRules = [];
101
+ const recalculate_rules: RefreshMatchRules = [];
102
+
103
+ switch (task.type) {
104
+ case 'findDescendants':
105
+ case 'findAncestors':
106
+ // JS 增量计算需要 match_where + not_match_relation_where;关系实体变更需要 SQL 刷新
107
+ recalculate_rules.push(['match_where', 'not_match_relation_where']);
108
+ refresh_rules.push(['match_relation_where']);
109
+ break;
110
+
111
+ case 'countDescendants':
112
+ case 'countAncestors':
113
+ // count 查询不维护 resultEntitySet,只能 SQL 刷新
114
+ refresh_rules.push(['match_where'], ['match_relation_where']);
115
+ break;
116
+ }
117
+
118
+ const result = queryNeedRefreshCreate(task, entities, refresh_rules, recalculate_rules);
119
+
120
+ if (result.refresh) {
121
+ task.refresh();
122
+ } else if (result.recalculate) {
123
+ _recalculate(task, result.current_entities);
124
+ }
125
+ };