ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Teable v2 投影(Projection)架构解析:领域事件如何驱动实时快照引擎

Teable v2 投影(Projection)架构解析:领域事件如何驱动实时快照引擎 Teable v2 投影Projection架构解析领域事件如何驱动实时快照引擎【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable导读Teable v2 的核心架构在packages/v2/core中实现了事件驱动 六边形架构端口与适配器分离。其中application/projections目录承担着一个关键职责把领域事件Domain Event绑定为派生效果derived effects并将表、字段、视图、记录的状态变化投影为实时引擎Realtime Engine中的文档快照。读完本文你将理解IProjection/ProjectionHandler装饰器 /IRealtimeProjection三个核心抽象的定义与关系掌握TableCreated、FieldCreated、FieldDeleted、ViewColumnMetaUpdated等实时投影的具体实现原理以及快照缓存、事务后调度、并发控制、追踪埋点等配套基础设施的工作机制并能直接定位到对应源码继续深入。一、职责边界projections 目录在应用层中的位置根据 ARCHITECTURE.md 的说明本目录承担三项职责定义投影类型projection types把领域事件与派生效果绑定起来提供投影事件绑定的别名装饰器alias decorator保持投影自身就是 EventHandler在处理器内部不做事件类型分支no event type branching inside handlers。这是理解该目录的关键投影不是独立于事件体系的另一套机制而是加了 projection 角色标记的普通事件处理器。每个投影类只响应一个领域事件如TableCreated通过构造器注入所需的端口仓库、映射器、实时引擎在handle方法中完成读取最新状态 → 转换为 DTO → 写入实时引擎的完整链路。从目录结构看该目录包含三类文件基础抽象Projection.ts投影别名与装饰器、RealtimeProjection.ts实时投影标记类型具体投影实现以*RealtimeProjection.ts命名覆盖表、字段、视图、记录全生命周期事件如TableCreatedRealtimeProjection、FieldCreatedRealtimeProjection、FieldDeletedRealtimeProjection、ViewColumnMetaUpdatedRealtimeProjection、RecordCreatedRealtimeProjection、RecordUpdatedRealtimeProjection、RecordsBatchCreatedRealtimeProjection、RecordsBatchUpdatedRealtimeProjection、RecordsDeletedRealtimeProjection、RecordReorderedRealtimeProjection、FieldUpdatedRealtimeProjection、FieldOptionsAddedRealtimeProjection、ComputedActivityRealtimeProjection等支撑设施scheduleRealtimeProjection.ts事务后调度、RealtimeTableSnapshotCache.ts表快照缓存、runRealtimeTasks.ts并发执行器、TableRecordRealtimeDTO.ts记录 DTO、decorateRealtimeAttachmentValue.ts附件值装饰、traceRealtimeFanout.ts追踪以及测试 RealtimeProjections.spec.ts。二、核心抽象IProjection 与 ProjectionHandler 装饰器Projection.ts 是整个目录的基石全文仅两个导出import type { IDomainEvent } from ../../domain/shared/DomainEvent; import type { EventType, IEventHandler } from ../../ports/EventHandler; import { EventHandler } from ../../ports/EventHandler; export type IProjectionTEvent extends IDomainEvent IDomainEvent IEventHandlerTEvent; export const ProjectionHandler TEvent extends IDomainEvent(event: EventTypeTEvent) EventHandler(event, { role: projection });2.1 IProjection语义化的类型别名IProjection本质上是IEventHandler的类型别名它并没有引入新接口而是在领域语言层面标注这是一个投影处理器。这样做的收益是同一个事件处理器体系见 EventHandler.ts 中的IEventHandler接口其handle(context, event, dispatchScope?)返回PromiseResultvoid, DomainError可以被复用同时代码阅读者能从类型名上立刻区分业务处理与状态投影两类处理器。2.2 ProjectionHandler事件绑定的别名装饰器ProjectionHandler(event)是对底层EventHandler(event, { role: projection })的别名封装。从 EventHandler.ts 的源码可以看到role选项带来的三重效果自动追踪埋点EventHandler装饰器会用TraceSpan包装handle方法其中role projection时span 的component为projection并写入EVENT_NAME、EVENT_ROLE、EVENT_ASYNC三个追踪属性EVENT_ASYNC为true标识投影是异步派生的注册进事件处理器注册表eventHandlerRegistry维护EventType - EventHandlerClass[]的映射投影类会被追加到对应事件的处理器列表中供事件派发时统一获取getEventHandlerTokens记录角色元数据通过eventHandlerRoleSymbol将projection角色写到类上getEventHandlerRole可随时查询某个处理器是否为投影。2.3 IRealtimeProjection标记目标为实时引擎的投影RealtimeProjection.ts 同样是一个轻量标记类型export type IRealtimeProjectionTEvent extends IDomainEvent IDomainEvent IProjectionTEvent;它不添加任何新约束纯粹用来在类型层面标注该投影面向实时引擎输出。在 Teable v2 的六边形架构中RealtimeEnginePortports/RealtimeEngine是投影的唯一输出端口定义了ensure确保文档存在并写入初始快照、applyChange应用增量变更、delete删除文档三个核心操作由适配器层实现如adapter-realtime-sharedb将其桥接到 ShareDB。三、逐个拆解四类代表性实时投影的实现以下按文档架构说明中列出的文件 目录内其余关键投影组织逐一说明每个投影响应的事件、目标文档与核心逻辑。3.1 TableCreatedRealtimeProjection建表时发布表快照TableCreatedRealtimeProjection.ts 响应TableCreated事件职责是发布表快照。其处理流程见handle方法用safeTry包裹通过Table.specs(event.baseId).byId(event.tableId).build()构造规格Specification用注入的tableRepository.findOne加载表聚合用tableMapper.toDTO将聚合转换为持久化 DTO 快照在tbl_{baseId}集合下以表 ID 生成实时文档 IDRealtimeDocId.fromParts(collection, tableId)调用realtimeEngine.ensure写入表快照级联发布字段快照遍历snapshot.fields在fld_{tableId}集合下为每个字段调用realtimeEngine.ensure写入字段快照。源码中定义了两个集合前缀常量tableCollectionPrefix tbl、fieldCollectionPrefix fld后续所有表/字段投影共用同一约定。3.2 FieldCreatedRealtimeProjection建字段时增量发布字段快照FieldCreatedRealtimeProjection.ts 响应FieldCreated事件。与建表投影不同它不直接读库而是借助快照缓存loadRealtimeTableSnapshot加载表快照并传入一个可用性谓词isSnapshotUsable: (candidate) candidate.fields.some((field) field.id event.fieldId.toString())该谓词保证缓存中的快照已经包含刚创建的新字段从而避免把旧快照误当新状态发布。随后先ensure表文档源码注释特别说明为实时功能启用之前创建的表补建文档从快照中取出新字段的 DTO若缺失则返回domainError.validation错误Missing field snapshot在fld_{tableId}集合下为该字段ensure文档。整个逻辑通过scheduleRealtimeProjection调度详见第五节。3.3 FieldDeletedRealtimeProjection删字段时删除字段快照FieldDeletedRealtimeProjection.ts 是四类中逻辑最简的投影响应FieldDeleted事件根据fld_{tableId}集合与字段 ID 构造文档 ID直接调用realtimeEngine.delete(context, docId)删除快照全程无需查询仓储。3.4 ViewColumnMetaUpdatedRealtimeProjection视图列元数据变更的双文档同步ViewColumnMetaUpdatedRealtimeProjection.ts 是本目录中最复杂的投影响应ViewColumnMetaUpdated事件当字段被加入/移出视图的列元数据时触发。其难点在于要同时保持两类文档一致表级文档tbl_{baseId}通过applyChange以{ type: set, path: [views, viewIndex, columnMeta], value: viewDto.columnMeta }的方式做增量 patch供表级消费者订阅独立视图文档viw_{tableId}集合下的viewDocId先ensure视图 DTO再applyChange更新其columnMeta并携带version: event.oldVersion做版本校验对应 ShareDB/SDK 的视图订阅场景。该投影还实现了两个重要的并发与正确性机制可用性谓词canUseColumnMetaSnapshot检查快照中目标视图存在且columnMeta[fieldId]的存在与否与事件中event.fieldInColumnMeta语义一致新增字段应已出现在快照、移除字段应已从快照消失防止旧快照被误用去重保留reserveViewColumnMetaRealtimeProjection用viewColumnMetaRealtimePendingKeysbaseId:tableId:viewId三元组保证同一视图在派发周期内只调度一次投影其余重复事件直接返回ok避免同一视图的多个事件在同一个快照缓存上重复执行。四、记录级投影与 DTO 设计记录是实时更新的高频对象目录为此提供了一组记录投影和统一的 DTO。4.1 记录实时 DTO兼容 V1 的扁平结构TableRecordRealtimeDTO.ts 定义了记录实时文档的格式export interface ITableRecordRealtimeDTO { /** Record ID */ id: string; /** Field values as a flat map (fieldId - value) for easy patching */ fields: Recordstring, unknown; } export const recordCollectionPrefix rec; export const buildRecordCollection (tableId: string): string ${recordCollectionPrefix}_${tableId};注释中明确了两点设计意图每条记录是rec_{tableId}集合下的独立 ShareDB 文档fields采用fieldId - value的扁平映射便于做增量 patch。格式与 V1 记录快照保持一致以兼容既有客户端。4.2 记录的增删改投影以 RecordCreatedRealtimeProjection.ts 为例把事件中的fieldValues数组转换为扁平fields映射对每个字段值调用decorateRealtimeAttachmentValue附件值装饰注入预签名 URLpresignedUrl、smThumbnailUrl、lgThumbnailUrl这依赖AttachmentValueDecoratorService与AttachmentUrlSignerService端口测试中用FakeAttachmentUrlSignerService验证了装饰结果在rec_{tableId}集合下构造{ id, fields }快照调用realtimeEngine.ensure。目录中还有RecordUpdatedRealtimeProjection增量applyChange更新字段、RecordsBatchCreatedRealtimeProjection、RecordsBatchUpdatedRealtimeProjection、RecordsDeletedRealtimeProjection、RecordReorderedRealtimeProjection更新行序列viewId.toRowOrderColumnName()对应的字段值等。从 RealtimeProjections.spec.ts 的测试断言中可以看到几条值得注意的工程约束更新类投影绝不调用ensure只调用applyChange测试注释明确说明 ensure()broadcasts a create op with empty fields which would overwrite client data即ensure会广播空字段的 create op覆盖客户端已有数据大批次降级批量更新/删除超过阈值如 1001 条记录、或编排orchestration携带总数达到 1000时跳过逐条实时操作避免实时引擎被大事务压垮测试skips per-record realtime ops for large batch updates等直接验证该行为并发有界批量投影通过runRealtimeTasks并发执行maxInFlight严格等于REALTIME_TASK_CONCURRENCY_LIMIT。五、支撑设施调度、缓存与并发5.1 scheduleRealtimeProjection事务提交后的后台调度scheduleRealtimeProjection.ts 是投影异步化的核心。scheduleRealtimeProjection(context, projectionName, task, projectionScope)的工作方式剥离事务用withoutTransaction(context)生成后台执行上下文避免投影任务被业务事务阻塞优先挂到事务提交后registerAfterCommit尝试把任务注册到元数据事务getUnitOfWorkTransaction(context, meta)或数据事务data的afterCommit钩子上——只有业务事务成功提交后投影才执行保证先落库、再投影的最终一致性语义兜底立即调度若当前无事务钩子则直接交给调度器追踪埋点为后台任务创建teable.{projectionName}.backgroundspan写入HANDLER、EVENT_ASYNC属性任务成功/失败都会记录到 span失败时recordError调度器可替换默认调度器按setImmediate → setTimeout(0) → queueMicrotask → 直接执行的优先级降级选择并暴露setRealtimeProjectionSchedulerForTest供测试注入测试中用captureRealtimeTasks捕获任务并手动执行。5.2 RealtimeTableSnapshotCache派发作用域内的快照缓存RealtimeTableSnapshotCache.ts 实现以baseId:tableId为键的缓存缓存项持有snapshot或进行中的loadingPromise避免同一快照并发重复加载。loadRealtimeTableSnapshot的核心逻辑是命中缓存且isSnapshotUsable通过直接返回缓存快照未命中或快照不可用且已有 loading 在途则等待其完成并再次校验可用性否则通过tableRepository.findOnetableMapper.toDTO加载把 loading 写回缓存加载失败时保留旧快照或删除缓存项。每个派发作用域IEventDispatchScope通过getOrCreate惰性创建一份独立作用域tableSnapshotCacheviewColumnMetaRealtimePendingKeys让同一批次事件共享缓存与去重集合同时跨批次隔离。5.3 runRealtimeTasks有界并发的批量执行器runRealtimeTasks.ts 导出一个常量REALTIME_TASK_CONCURRENCY_LIMIT 32并实现基于 worker 池的并发执行器用Math.min(normalizedConcurrency, tasks.length)个 worker 循环取任务执行结果按原索引写入results数组。测试通过阻塞ensure/applyChange并统计maxInFlight验证了批量创建/更新投影的并发峰值始终被限制在 32。六、设计原则与延伸阅读综合以上源码分析可以把该目录的设计原则总结为四条一事件一投影、无分支每个投影类只处理一个领域事件handle内部不做事件类型判断靠ProjectionHandler(event)装饰器完成绑定与注册这与投影就是 EventHandler的职责描述完全一致只依赖端口、不依赖适配器所有投影只注入ITableRepository、ITableMapper、IRealtimeEngine、AttachmentValueDecoratorService等端口/服务抽象真正的实时引擎实现如 ShareDB 适配器由外层容器装配保证了核心与基础设施的解耦先提交后投影调度统一走afterCommit钩子配合withoutTransaction后台执行确保投影看到的是已提交的数据且不拖慢主事务快照复用 语义校验同一派发作用域内共享表快照缓存并用isSnapshotUsable谓词校验快照是否已包含事件所对应的变更从根上避免旧快照覆盖新状态视图列元数据投影进一步用 pending keys 去重同一视图的并发调度。感兴趣的读者可以继续深入以下文件事件处理器注册与角色机制ports/EventHandler.tsEventHandler装饰器、getEventHandlerTokens、getEventHandlerRole实时引擎端口契约ports/RealtimeEngine.tsensure/applyChange/delete与变更类型与 ports/RealtimeDocId.ts集合与文档 ID 的构造投影行为规格RealtimeProjections.spec.ts覆盖记录增删改、批量操作降级、并发上限、字段增删、视图列元数据等全部实时投影的 40 条用例事件本身domain/table/events 目录下的TableCreated、FieldCreated、FieldDeleted、ViewColumnMetaUpdated、RecordCreated等事件定义适配器层实现packages/v2/adapter-realtime-sharedb与packages/v2/adapter-repository-postgres分别回答实时文档最终落在哪里表快照从哪个仓库读出。通过本文你已经掌握了 Teable v2 事件驱动架构中投影这一环的完整图景从类型别名、装饰器、标记类型的三个抽象到表/字段/视图/记录四类投影的具体实现再到调度、缓存、并发、追踪的支撑设施。这套模式事件 → 投影 → 快照/增量变更 → 实时引擎可以作为实现领域事件驱动实时同步类系统的直接参考。【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表