@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,164 @@
1
+ /**
2
+ * @fileoverview 树查询的 REMOVE 事件合并
3
+ *
4
+ * `findDescendants` 要顺着祖先链判定「父节点被删 ⇒ 子节点整棵脱离结果集」,
5
+ * 这条判定本地就能做,所以走真增量;`count*` 仍只能刷新。
6
+ */
7
+ import {
8
+ EntityType,
9
+ isStaleEntityRemoveEvent,
10
+ queryNeedRefreshRemove,
11
+ QueryTask,
12
+ RefreshMatchRules,
13
+ RxDBEntityId,
14
+ RxDBEntityLocalRemovedEventData
15
+ } from '@aiao/rxdb';
16
+ import { TREE_QUERY_TYPES } from '../constants.js';
17
+ import { FindTreeOptions } from '../repository/tree-repository.interface.js';
18
+ import { buildEntityMap, traverseAncestors } from './query-tree.utils.js';
19
+
20
+ /**
21
+ * JS 增量更新查询结果
22
+ */
23
+ const _recalculate = <T extends EntityType>(task: QueryTask<T>, data: RxDBEntityLocalRemovedEventData<T>[]) => {
24
+ const removed_ids = new Set(data.map(e => e.id));
25
+
26
+ switch (task.type) {
27
+ case 'findDescendants': {
28
+ const old_result = Array.from(task.resultEntitySet.values());
29
+ const entities_map = buildEntityMap(old_result, e => e.id);
30
+
31
+ // 批级记忆:同一条父链会被所有兄弟节点反复走一遍,逐节点回溯是 O(n·depth)。
32
+ // `detached_map` 记「这个 id 是否已因自身或祖先被删而脱离结果集」,
33
+ // 每条链只算一次,整批摊平成 O(n)。
34
+ const detached_map = new Map<RxDBEntityId, boolean>();
35
+
36
+ // 判定不能写成 `ancestor.id && removed_ids.has(...)`:`RxDBEntityId` 允许
37
+ // `0` / `0n` / `''`,真值判断会把这些合法主键当成"没有 id"跳过,被删节点
38
+ // 名下的子树就整棵留在结果里。`traverseAncestors` 本身只 yield 已解析到的
39
+ // 父实体(取不到就 break),所以 `ancestor` 恒非空,无需可选链。
40
+ const is_detached = (entity: InstanceType<T>): boolean => {
41
+ const cached = detached_map.get(entity.id);
42
+ if (cached !== undefined) return cached;
43
+
44
+ // `chain` 收集「结论未知」的节点:它们都是断点的后代,共享同一个结论,
45
+ // 循环结束后统一回填。
46
+ const chain: RxDBEntityId[] = [entity.id];
47
+ let detached = removed_ids.has(entity.id);
48
+
49
+ if (!detached) {
50
+ for (const { entity: ancestor } of traverseAncestors(entity, entities_map)) {
51
+ const known = detached_map.get(ancestor.id);
52
+ if (known !== undefined) {
53
+ detached = known;
54
+ break;
55
+ }
56
+ if (removed_ids.has(ancestor.id)) {
57
+ detached = true;
58
+ break;
59
+ }
60
+ chain.push(ancestor.id);
61
+ }
62
+ }
63
+
64
+ for (const id of chain) detached_map.set(id, detached);
65
+ return detached;
66
+ };
67
+
68
+ const filtered = old_result.filter(entity => !is_detached(entity));
69
+ if (filtered.length === old_result.length) return;
70
+
71
+ task.next(filtered, true);
72
+ break;
73
+ }
74
+
75
+ case 'findAncestors': {
76
+ const old_result = Array.from(task.resultEntitySet.values());
77
+ const filtered = old_result.filter(e => !removed_ids.has(e.id));
78
+ if (filtered.length === old_result.length) return;
79
+
80
+ const { entityId } = task.options as FindTreeOptions<T>;
81
+ if (entityId === null || entityId === undefined) {
82
+ // 查根节点的祖先:CTE 基准成员是 `parentId IS NULL` 的各个根,递归成员
83
+ // 往上找不到东西,结果就是这些根本身。彼此无链路关系,删谁摘谁。
84
+ task.next(filtered, true);
85
+ break;
86
+ }
87
+
88
+ // 祖先链是一条自 target 向上的单链。适配器的递归成员是
89
+ // `children.id = c.parentId`,走到被删节点就再也接不上,断点**上方**的祖先
90
+ // 不会出现在重跑结果里。只把被删的那一个过滤掉会把它们留下(结果比 SQL 多)。
91
+ // 这里改为从 target 出发、在幸存节点里重建可达链路:target 自己被删则结果为空,
92
+ // 与基准成员取不到行时 CTE 返回 0 行一致。
93
+ const survivors = buildEntityMap(filtered, e => e.id);
94
+ const reachable: InstanceType<T>[] = [];
95
+ const target = survivors.get(entityId);
96
+ if (target) {
97
+ reachable.push(target);
98
+ for (const { entity: ancestor } of traverseAncestors(target, survivors)) {
99
+ reachable.push(ancestor);
100
+ }
101
+ }
102
+ task.next(reachable, true);
103
+ break;
104
+ }
105
+
106
+ case 'countDescendants':
107
+ case 'countAncestors':
108
+ // count 查询无法 JS 增量更新,由上层触发 SQL 刷新
109
+ break;
110
+ }
111
+ };
112
+
113
+ /**
114
+ * 处理树查询 REMOVE 事件的缓存合并
115
+ *
116
+ * @param task 查询任务
117
+ * @param entities 删除的实体事件数据
118
+ */
119
+ export const merge_remove = <T extends EntityType>(
120
+ task: QueryTask<T>,
121
+ entities: RxDBEntityLocalRemovedEventData<T>[]
122
+ ) => {
123
+ if (!TREE_QUERY_TYPES.has(task.type)) return;
124
+
125
+ // 与 merge_create / merge_update 的同款守卫:首个权威结果落地前不做增量。
126
+ // runner 的快照可能取自删除提交之前,静默丢弃会让这行死数据永远留在活查询里。
127
+ if (task.result === undefined) {
128
+ task.refresh();
129
+ return;
130
+ }
131
+
132
+ // 旧删除事件不能撤掉比它新的缓存实体(例如 undo/redo 用同一 id 重建实体后又被更新,
133
+ // 姗姗来迟的过期 DELETE 才追上)。逐条过滤而非整批回退:DELETE 在 _recalculate 里
134
+ // 按 id 独立处理,不存在跨实体 patch 合并的正确性风险。
135
+ const freshEntities = entities.filter(event => !isStaleEntityRemoveEvent(task.rxdb, event));
136
+ if (freshEntities.length === 0) return;
137
+
138
+ const refresh_rules: RefreshMatchRules = [];
139
+ const recalculate_rules: RefreshMatchRules = [];
140
+
141
+ switch (task.type) {
142
+ case 'findAncestors':
143
+ recalculate_rules.push(['result_contains']);
144
+ refresh_rules.push(['match_relation_where']);
145
+ break;
146
+
147
+ case 'findDescendants':
148
+ recalculate_rules.push(['result_contains'], ['match_where']);
149
+ refresh_rules.push(['match_relation_where']);
150
+ break;
151
+
152
+ case 'countDescendants':
153
+ case 'countAncestors':
154
+ refresh_rules.push(['match_where'], ['match_relation_where']);
155
+ break;
156
+ }
157
+
158
+ const result = queryNeedRefreshRemove(task, freshEntities, refresh_rules, recalculate_rules);
159
+ if (result.refresh) {
160
+ task.refresh();
161
+ } else if (result.recalculate) {
162
+ _recalculate(task, result.current_entities);
163
+ }
164
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @fileoverview 树查询的 UPDATE 事件合并
3
+ *
4
+ * 四个任务类型全部走真增量:`parentId` 变更导致的树重组、where 命中翻转、字段值变化,
5
+ * 都由 {@link handleFindDescendantsUpdate} 等四个处理器在本地算清。
6
+ * 增量所需的判定原语(分类、外部更新落盘、过期事件)取自核心公开面,不复刻。
7
+ */
8
+ import {
9
+ EntityType,
10
+ prepareIncrementalUpdate,
11
+ queryNeedRefreshUpdate,
12
+ QueryTask,
13
+ RefreshMatchRules,
14
+ RxDBEntityLocalUpdatedEventData
15
+ } from '@aiao/rxdb';
16
+ import { TREE_QUERY_TYPES } from '../constants.js';
17
+ import {
18
+ handleCountAncestorsUpdate,
19
+ handleCountDescendantsUpdate,
20
+ handleFindAncestorsUpdate,
21
+ handleFindDescendantsUpdate
22
+ } from './merge-update-tree.js';
23
+ import { hasTreeParentChanged } from './query-tree.utils.js';
24
+
25
+ /**
26
+ * 重新计算查询结果(JS 增量更新)
27
+ */
28
+ const _recalculate = <T extends EntityType>(task: QueryTask<T>, data: RxDBEntityLocalUpdatedEventData<T>[]) => {
29
+ const { cache, classification } = prepareIncrementalUpdate(task, data);
30
+
31
+ switch (task.type) {
32
+ case 'findDescendants':
33
+ handleFindDescendantsUpdate(task, data, classification, cache);
34
+ break;
35
+
36
+ case 'findAncestors':
37
+ handleFindAncestorsUpdate(task, data, classification, cache);
38
+ break;
39
+
40
+ case 'countDescendants':
41
+ handleCountDescendantsUpdate(task, data, classification, cache);
42
+ break;
43
+
44
+ case 'countAncestors':
45
+ handleCountAncestorsUpdate(task, data, classification, cache);
46
+ break;
47
+ }
48
+ };
49
+
50
+ /**
51
+ * 处理树查询 UPDATE 事件的缓存合并
52
+ *
53
+ * @param task 查询任务
54
+ * @param entities 更新的实体事件数据
55
+ */
56
+ export const merge_update = <T extends EntityType>(
57
+ task: QueryTask<T>,
58
+ entities: RxDBEntityLocalUpdatedEventData<T>[]
59
+ ) => {
60
+ if (!TREE_QUERY_TYPES.has(task.type)) return;
61
+
62
+ // 与 merge_create 的同款守卫:首个权威结果尚未落地时不做增量——此刻 resultEntitySet
63
+ // 还是空的,增量合并会把事件负载当成完整结果发射。交给 `refresh()` 而不是直接 `return`:
64
+ // runner 的快照可能取自更新提交之前,直接丢弃会让这次更新在活查询里永远缺席。
65
+ if (task.result === undefined) {
66
+ task.refresh();
67
+ return;
68
+ }
69
+
70
+ // `findAncestors` 的结果就是一条祖先链。链上任一节点改父,整条链都要重算,
71
+ // 而新链上方那些节点从未进过结果集、也不在本批事件里,本地只能摘不能补。
72
+ //
73
+ // 这一层必须放在 `queryNeedRefreshUpdate` 之前:链上某个**不匹配 where**、
74
+ // 因而不在结果集里的中间节点改父,同样会让它上方匹配 where 的祖先整体进出结果,
75
+ // 但它既不满足 `result_contains` 也不满足 `match_where`,连 recalculate 的门都进不来。
76
+ // {@link handleFindAncestorsUpdate} 开头按同一条规则(同一个 `hasTreeParentChanged`)
77
+ // 再判一次,是为了让处理器脱离本调用方也成立,不是兜底:两处判的是同一条规则。
78
+ if (task.type === 'findAncestors' && entities.some(hasTreeParentChanged)) {
79
+ task.refresh();
80
+ return;
81
+ }
82
+
83
+ const refresh_rules: RefreshMatchRules = [];
84
+ const recalculate_rules: RefreshMatchRules = [];
85
+
86
+ switch (task.type) {
87
+ case 'findDescendants':
88
+ // 处理 parentId 变化导致的树形结构重组
89
+ // result_contains: 更新的实体在当前结果中
90
+ // match_where: 更新后匹配条件的实体
91
+ // match_where_before: 更新前匹配条件的实体
92
+ // not_match_relation_where: 关系实体没有变更时才能使用 JS 更新
93
+ recalculate_rules.push(
94
+ ['result_contains', 'not_match_relation_where'],
95
+ ['match_where', 'not_match_relation_where'],
96
+ ['match_where_before', 'not_match_relation_where']
97
+ );
98
+ refresh_rules.push(['match_relation_where']);
99
+ break;
100
+
101
+ case 'findAncestors':
102
+ // result_contains: 更新的实体在当前结果中
103
+ // match_where: 更新后匹配条件的实体
104
+ // not_match_relation_where: 关系实体没有变更时才能使用 JS 更新
105
+ recalculate_rules.push(
106
+ ['result_contains', 'not_match_relation_where'],
107
+ ['match_where', 'not_match_relation_where']
108
+ );
109
+ refresh_rules.push(['match_relation_where']);
110
+ break;
111
+
112
+ case 'countDescendants':
113
+ case 'countAncestors':
114
+ // match_where: 更新后匹配条件的实体
115
+ // match_where_before: 更新前匹配条件的实体
116
+ // not_match_relation_where: 关系实体没有变更时才能使用 JS 更新
117
+ recalculate_rules.push(
118
+ ['match_where', 'not_match_relation_where'],
119
+ ['match_where_before', 'not_match_relation_where']
120
+ );
121
+ refresh_rules.push(['match_relation_where']);
122
+ break;
123
+ }
124
+
125
+ const result = queryNeedRefreshUpdate(task, entities, refresh_rules, recalculate_rules);
126
+
127
+ if (result.refresh) {
128
+ task.refresh();
129
+ } else if (result.recalculate) {
130
+ _recalculate(task, entities);
131
+ }
132
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * 树形遍历配置
3
+ */
4
+ interface TreeTraversalOptions {
5
+ maxLevel?: number; // 最大层级限制
6
+ maxDepth?: number; // 最大跳数;不传则不限(环由 visited 兜住)
7
+ }
8
+
9
+ /**
10
+ * 获取实体的父级 ID
11
+ * 处理 parentId 可能是对象或字符串的情况
12
+ */
13
+ export const get_tree_parent_id = <ID = string>(entity: object | null | undefined): ID | null => {
14
+ if (!entity) return null;
15
+ return (Reflect.get(entity, 'parentId') as ID | null | undefined) ?? null;
16
+ };
17
+
18
+ /**
19
+ * 判断一条更新事件是否**真的**改动了 `parentId`
20
+ *
21
+ * @param event 带 `patch` / `inversePatch` 的更新事件
22
+ * @returns `true` 表示父节点发生了位移
23
+ *
24
+ * @remarks
25
+ * 两条都不能省:
26
+ *
27
+ * - **key 存在 ≠ 值变了**:只改 `isActive` 时 patch 仍可能带上未变的 `parentId`,
28
+ * 单看 `'parentId' in patch` 会把纯字段更新误判成"移动",触发不必要的 refresh。
29
+ * - **两侧都要归一**:在树语义里「缺失」「`null`」「`undefined`」都是「挂在根上」,
30
+ * 所以两侧统一走 {@link get_tree_parent_id}。裸 `Reflect.get` 比较会把
31
+ * `undefined → null` 这种无变化算成移动。
32
+ *
33
+ * 这是全插件判断"父节点变了没有"的**唯一**实现:此前 `merge_update.ts` 与
34
+ * `merge-update-tree.ts` 各有一套拼写,null/undefined 语义在两套之间分叉。
35
+ */
36
+ export const hasTreeParentChanged = (event: { patch: object; inversePatch: object }): boolean =>
37
+ 'parentId' in event.patch && get_tree_parent_id(event.patch) !== get_tree_parent_id(event.inversePatch);
38
+
39
+ /**
40
+ * 向上遍历父级链路
41
+ *
42
+ * @param entity 起始实体
43
+ * @param entitiesMap 实体 ID 映射表
44
+ * @param options 遍历配置
45
+ * @yields 每一层的父级实体和层级
46
+ */
47
+ export function* traverseAncestors<T extends object, ID>(
48
+ entity: T,
49
+ entitiesMap: Map<ID, T>,
50
+ options: TreeTraversalOptions = {}
51
+ ): Generator<{ entity: T; level: number }> {
52
+ const { maxLevel, maxDepth } = options;
53
+ const visited = new Set<ID>();
54
+ let currentParentId = get_tree_parent_id<ID>(entity);
55
+ let level = 1;
56
+
57
+ while (
58
+ currentParentId !== null &&
59
+ !visited.has(currentParentId) &&
60
+ (maxDepth === undefined || visited.size < maxDepth)
61
+ ) {
62
+ visited.add(currentParentId);
63
+
64
+ const parent = entitiesMap.get(currentParentId);
65
+ if (!parent) break;
66
+
67
+ // 检查层级限制
68
+ if (maxLevel !== undefined && level > maxLevel) break;
69
+
70
+ yield { entity: parent, level };
71
+
72
+ currentParentId = get_tree_parent_id<ID>(parent);
73
+ level++;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 检查实体是否是目标实体的后代
79
+ *
80
+ * @param entity 要检查的实体
81
+ * @param targetId 目标实体 ID (为 null 或 undefined 时表示查找根节点)
82
+ * @param entitiesMap 实体 ID 映射表
83
+ * @param maxLevel 最大层级 (undefined 表示无限制)
84
+ * @returns 如果是后代则返回 true
85
+ */
86
+ export function isDescendantOf<T extends object, ID>(
87
+ entity: T,
88
+ targetId: ID | null | undefined,
89
+ entitiesMap: Map<ID, T>,
90
+ maxLevel?: number
91
+ ): boolean {
92
+ // 特殊情况: targetId 为 null 或 undefined 时,计算节点相对根节点的层级
93
+ if (targetId === null || targetId === undefined) {
94
+ const visited = new Set<ID>();
95
+ let currentEntity = entity;
96
+ let level = 0;
97
+
98
+ while (maxLevel === undefined || level <= maxLevel) {
99
+ const currentParentId = get_tree_parent_id<ID>(currentEntity);
100
+ if (currentParentId === null) return true;
101
+ if (visited.has(currentParentId)) return false;
102
+
103
+ visited.add(currentParentId);
104
+ const parent = entitiesMap.get(currentParentId);
105
+ if (!parent) return false;
106
+
107
+ currentEntity = parent;
108
+ level++;
109
+ }
110
+
111
+ return false;
112
+ }
113
+
114
+ // 向上遍历父级链路
115
+ const visited = new Set<ID>();
116
+ let currentParentId = get_tree_parent_id<ID>(entity);
117
+ let level = 1;
118
+
119
+ while (currentParentId !== null && !visited.has(currentParentId)) {
120
+ // 找到目标实体
121
+ if (currentParentId === targetId) {
122
+ // 检查层级限制
123
+ if (maxLevel === undefined || level <= maxLevel) {
124
+ return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ visited.add(currentParentId);
130
+ level++;
131
+
132
+ // 从映射表中获取父实体以继续遍历
133
+ const parent = entitiesMap.get(currentParentId);
134
+ if (parent) {
135
+ currentParentId = get_tree_parent_id<ID>(parent);
136
+ } else {
137
+ break; // 找不到父实体,中断遍历
138
+ }
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * 检查实体是否是目标实体的祖先
146
+ *
147
+ * @param candidateEntity 候选祖先实体
148
+ * @param targetId 目标实体 ID (为 null 或 undefined 时直接返回 false,无 isDescendantOf 那种"查找根节点"语义)
149
+ * @param entitiesMap 实体 ID 映射表
150
+ * @param maxLevel 最大层级
151
+ * @returns 如果是祖先则返回 true
152
+ */
153
+ export function isAncestorOf<T extends object, ID>(
154
+ candidateEntity: T,
155
+ targetId: ID | null | undefined,
156
+ entitiesMap: Map<ID, T>,
157
+ maxLevel?: number
158
+ ): boolean {
159
+ if (targetId === null || targetId === undefined) return false;
160
+ const targetEntity = entitiesMap.get(targetId);
161
+ if (!targetEntity) return false;
162
+ for (const { entity: parent } of traverseAncestors(targetEntity, entitiesMap, { maxLevel })) {
163
+ if (Reflect.get(parent, 'id') === Reflect.get(candidateEntity, 'id')) {
164
+ return true;
165
+ }
166
+ }
167
+
168
+ return false;
169
+ }
170
+
171
+ /**
172
+ * 顺序串联多个可迭代源,不物化中间数组
173
+ *
174
+ * @param sources 按顺序遍历的可迭代源
175
+ * @yields 各源中的元素,顺序与传入顺序一致
176
+ *
177
+ * @remarks
178
+ * 专为 {@link buildEntityMap} 的「旧结果 `Set` + 本批事件数组」场景而设:
179
+ * `Array.from(set)` 再展开拼接会把旧结果集整体物化两遍,只为建一张查找表。
180
+ */
181
+ export function* concatIterables<T>(...sources: Iterable<T>[]): Generator<T> {
182
+ for (const source of sources) {
183
+ yield* source;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * 构建实体 ID 映射表(优化查找性能)
189
+ *
190
+ * @param entities 实体来源;接受任意可迭代对象(数组、`Set`、{@link concatIterables} 的产物)
191
+ * @param getIdFn 获取实体 ID 的函数
192
+ * @returns 实体 ID 到实体的映射表
193
+ */
194
+ export function buildEntityMap<T, ID>(
195
+ entities: Iterable<T>,
196
+ getIdFn: (entity: T) => ID | null | undefined
197
+ ): Map<ID, T> {
198
+ const map = new Map<ID, T>();
199
+ for (const entity of entities) {
200
+ const id = getIdFn(entity);
201
+ if (id !== null && id !== undefined) map.set(id, entity);
202
+ }
203
+ return map;
204
+ }