@electric-sql/y-electric 演进全解:从 0.1.0 到 0.1.53,YJS 实时协作同步的实现脉络
2026/9/16 18:59:13 网站建设 项目流程

@electric-sql/y-electric 演进全解:从 0.1.0 到 0.1.53,YJS 实时协作同步的实现脉络

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

导读

@electric-sql/y-electric是 Electric 生态中负责Yjs 网络连接(Provider)的官方包,它把 Yjs 的 CRDT 文档、Electric 的 Shape 同步机制与 Postgres 的 BYTEA 二进制存储串联在一起,为开发者提供开箱即用的多人实时协作编辑能力。本篇文章以该包的 CHANGELOG.md 为时间轴骨架,完整梳理 0.1.0 首版发布到 0.1.53 的每个重要功能节点,并结合 y-electric 源码、类型定义、resume state 实现、测试用例 与 官方技能文档,讲透ElectricProvider的配置、双通道同步模型、写入 API、断线续传、防抖批量提交等核心机制。读完本文,你将能够独立完成"Yjs 文档 + Electric Shape + Postgres"的实时协作链路搭建。

一、版本演进全景:一份 CHANGELOG 揭示的发布节奏与功能轨迹

1.1 从 0.1.0 首版到 0.1.53 当前版

CHANGELOG.md 记录了该包自 0.1.0 起的完整版本历史。0.1.0 的发布说明只有一行,却定义了整个包的存在意义——"Yjs Electric network provider release"(Yjs Electric 网络 Provider 发布)。此后 53 个补丁版本整体上呈两个并行的演进线:

  • 跟随依赖线:几乎每个版本都包含- Updated dependencies条目,将@electric-sql/client从 1.0.5 一路升到 1.5.27。这符合 monorepo 下客户端与服务端 API 协议(Shape 协议、消息格式、offset/handle 语义)持续演进的现实。
  • 功能沉淀线:在依赖升级之间穿插了少量带有实质功能变更的条目,正是本文要重点展开的内容,包括防抖、offset=now、TanStack Intent 技能与 README 迭代。

1.2 三个关键功能里程碑

版本变更内容技术含义
0.1.0Yjs Electric network provider release包的首个版本,确立 Provider 基础架构
0.1.22Added debouncing to y-electric and use offset=now引入写防抖(debounceMs)与 awareness 通道offset=now语义
0.1.38Add TanStack Intent skills for AI agent guidance内置 9 个 AI Agent 技能,覆盖 shapes、proxy auth、schema design、debugging、deployment、Yjs collaboration 等
0.1.42chore: added keyword to support Tanstack Intent在 package.json keywords 中加入tanstack-intent以支持技能检索
0.1.53依赖升级至@electric-sql/client@1.5.27当前最新版本,同步最新客户端能力

这三个里程碑恰好对应本文后续要深挖的三块内容:双通道同步与写入模型、防抖与断线续传、以及面向 AI Agent 的技能分发。

二、核心架构:一个 Provider 如何打通 Yjs、Electric 与 Postgres

2.1 官方文档定义的四步流程

README.md 给出了使用y-electric的典型流程:

  1. 开发者暴露一个shape proxy,用于对 shape 请求做 授权;
  2. 客户端定义一个 shape,用于同步某个Y.Doc的变更;
  3. 开发者暴露一个write API(写接口),用于接收并落库 Yjs 更新;
  4. 之后,Y-Electric 自动在所有已连接的客户端之间共享更新。

这个流程的关键认知是:Electric 是"读路径"同步引擎,写路径由你自己提供。也就是说,下行(其他客户端产生的变更 → 当前客户端)走 Electric Shape 订阅,上行(当前客户端本地编辑 → 数据库)走你自定义的 HTTP 端点。

2.2 源码中的双通道模型

从 y-electric.ts 的构造器签名可以看到,ElectricProvider内部维护两条彼此独立的同步通道:

  • documentUpdates(必选):同步Y.Doc的文档更新,对应数据库中的ydoc_update/ydoc_updates表;
  • awarenessUpdates(可选):同步 Yjs Awareness(光标、在线状态等瞬态信息),对应ydoc_awareness表,需要传入y-protocols/awarenessAwareness实例。

connect()方法中,两条通道分别创建独立的ShapeStream

const operationsStream = new ShapeStream<RowWithDocumentUpdate>({ ...this.documentUpdates.shape, ...this.resumeState.document, // 用持久化的 offset/handle 续传 signal: abortController.signal, }) const awarenessStream = new ShapeStream<RowWithAwarenessUpdate>({ ...this.awarenessUpdates.shape, signal: abortController.signal, offset: `now`, // awareness 只订阅实时增量 })

注意 awareness 通道固定使用offset: 'now'——这正是 0.1.22 版本变更中"use offset=now"在源码里的落点:在线状态属于瞬态数据,无需回放历史,从当前时刻开始订阅即可。

2.3 下行同步:Shape 消息如何写回 Y.Doc

文档通道的消息处理器operationsShapeHandler(y-electric.ts)做了两件事:

  • isChangeMessage类型消息,调用getUpdateFromRow(message.value)取出行内的更新二进制,再循环decoding.readVarUint8Array读出每条 Yjs 操作,最后Y.applyUpdate(this.doc, operation, 'server')应用到本地文档;
  • 对携带control: 'up-to-date'控制头的消息,记录resumeState.document = { offset, handle },并在无待发更新时把stableStateVector推进到当前文档状态向量,随后触发syncedresumeState事件并置connected = true

三、基础接入:一个可运行的 ElectricProvider 配置

3.1 完整初始化代码

以下是 README.md 中的标准接入方式,结合 SKILL.md 的实践补充了debounceMs

import * as Y from 'yjs' import { ElectricProvider, LocalStorageResumeStateProvider, } from '@electric-sql/y-electric' import { Awareness } from 'y-protocols/awareness' import { parseToDecoder } from '@electric-sql/y-electric/utils' const ydoc = new Y.Doc() const awareness = new Awareness(ydoc) const room = 'my-document' // 1. resume state provider:把断线续传点持久化到 localStorage const resumeStateProvider = new LocalStorageResumeStateProvider(room) // 2. 创建 provider const provider = new ElectricProvider({ doc: ydoc, documentUpdates: { shape: { url: SHAPE_PROXY_URL, // shape proxy 地址 params: { table: `ydoc_update`, // 文档更新表 where: `room = '${room}'`, // 按房间过滤 }, parser: parseToDecoder, // BYTEA hex -> lib0 Decoder }, sendUrl: DOC_UPDATES_SEND_URL, // 文档更新 PUT 端点 getUpdateFromRow: (row) => row.op, // 从行中取更新列 }, awarenessUpdates: { shape: { url: SHAPE_PROXY_URL, params: { table: `ydoc_awareness`, // 在线状态表 where: `room = '${room}'`, }, parser: parseToDecoder, }, sendUrl: AWARENESS_UPDATES_SEND_URL, // awareness PUT 端点 protocol: awareness, // Awareness 协议实例 getUpdateFromRow: (row) => row.op, }, resumeState: resumeStateProvider.load(), // 3. 载入断线续传点 debounceMs: 100, // 4. 批量合并快速编辑(0.1.22 引入) }) // 5. 订阅 resume state 变化并持久化 resumeStateProvider.subscribeToResumeState(provider)

3.2 选项参数全表

根据 types.ts 中ElectricProviderOptions的类型定义,各参数含义如下:

参数类型必选说明
docY.Doc要同步的 Yjs 文档
documentUpdates.shapeShapeStreamOptions文档更新 Shape 流配置(url、params、parser 等)
documentUpdates.sendUrlstring \| URL文档更新的上传端点,Provider 用PUT+application/octet-stream提交
documentUpdates.getUpdateFromRow函数从数据行中取出更新二进制的列,兼容任意后端表结构
documentUpdates.sendErrorRetryHandler函数发送失败回调,返回true时重试
awarenessUpdates.shapeShapeStreamOptionsawareness 流配置(内部固定offset: 'now'
awarenessUpdates.sendUrlstring \| URLawareness 更新上传端点
awarenessUpdates.protocolAwarenessy-protocols/awareness实例
awarenessUpdates.getUpdateFromRow函数从行中取 awareness 更新列
resumeStateResumeState断线续传点;不传则每次连接拉取整个 Shape
connectboolean是否初始化即连接,默认true
fetchClienttypeof fetch自定义 fetch 实现(测试、Node 环境常用)
debounceMsnumber文档更新发送防抖窗口(毫秒),0或不传表示不防抖(0.1.22 起)

3.3 事件与生命周期

ElectricProvider继承自lib0/observableObservableV2,通过 types.ts 中YProvider类型暴露以下事件:

  • status:连接状态变更,值为'connecting' | 'connected' | 'disconnected'
  • sync/synced:收到服务端up-to-date控制消息、本地追赶完服务端最新变更时触发;
  • resumeState:Provider 发送或接收更新后触发,主要被ResumeStateProvider消费以持久化续传点;
  • connection-close:客户端从 Shape 取消订阅、断开连接时触发。
provider.on('status', ({ status }) => { console.log('Yjs sync status:', status) }) provider.on('sync', (synced: boolean) => { console.log('Document synced:', synced) }) provider.disconnect() // 手动断开 provider.connect() // 手动重连

四、写入路径:Bring Your Own API 的落库设计

4.1 文档更新的表结构与存储

Y-Electric 以二进制形式发送 Yjs 文档更新,你的写接口可以直接把请求体作为bytea列写入数据库。README 给出的推荐表结构:

-- Schema definition CREATE TABLE ydoc_updates( id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, room text NOT NULL, op bytea NOT NULL ) -- Save updates into individual rows INSERT INTO ydoc_updates (room, op) VALUES ($1, $2)

仓库中的 示例服务器 给出了端到端的参考实现:PUT /api/update端点读取请求体arrayBuffer转为Uint8Array,根据 URL 查询参数中是否有client_id区分文档更新与 awareness 更新,分别调用saveUpdateupsertAwarenessUpdate落库。同时它还实现了一个GET /shape-proxy/v1/shape反向代理,把请求转发给 Electric 服务并透传electric-offsetelectric-handleelectric-schemaelectric-cursor等响应头(通过 CORS 的exposeHeaders暴露给浏览器),这正是 README 第一步所要求的 shape proxy。

4.2 awareness 更新的 upsert 模型

Awareness 协议为每个客户端各自保存一份向量时钟,因此其表结构按(client_id, room)作为联合主键,更新采用ON CONFLICTupsert:

-- Schema definitions CREATE TABLE ydoc_awareness( client_id TEXT, room TEXT, op BYTEA NOT NULL, updated TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (client_id, room) ); -- Save INSERT INTO ydoc_awareness (room, client_id, op, updated) VALUES ($1, $2, $3, now()) ON CONFLICT (client_id, room) DO UPDATE SET op = $3, updated = now()

4.3 过期在线状态的垃圾回收

由于 Provider 无法可靠检测客户端何时离线,README 建议用数据库触发器回收过期的 awareness 行:

CREATE OR REPLACE FUNCTION gc_awareness_timeouts() RETURNS TRIGGER AS $$ BEGIN DELETE FROM ydoc_awareness WHERE updated < (CURRENT_TIMESTAMP - INTERVAL '30 seconds') AND room = NEW.room; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER gc_awareness_timeouts_trigger AFTER INSERT OR UPDATE ON ydoc_awareness FOR EACH ROW EXECUTE FUNCTION gc_awareness_timeouts();

4.4 发送失败与重试语义

在 y-electric.ts 的send函数中,上行提交使用PUT方法、Content-Type: application/octet-stream,若响应非 2xx 或网络异常,会调用可选的sendErrorRetryHandler决定是否重试;发送失败时 Provider 会把未发送的变更重新批回pendingChanges并主动disconnect(),等待下次重连再补发——这保证了本地编辑不丢失。

五、0.1.22 的两个关键机制:debounceMs 防抖与 offset=now

5.1 防抖批量提交的源码实现

版本 0.1.22 的 "Added debouncing" 对应源码中的三处协作:

  1. batch(update)(y-electric.ts)用Y.mergeUpdates把多次编辑合并成一条更新;
  2. scheduleSendOperations()(y-electric.ts)在debounceMs > 0时开启一个计时器,窗口内持续合并、窗口结束才真正发送;debounceMs = 0则立即发送;
  3. sendOperations()(y-electric.ts)通过sendingPendingChanges标志防止并发发送,并在发送完成后推进stableStateVector

换句话说,默认行为是每次击键都发一个 PUT,而设置debounceMs: 100可以把 100ms 窗口内的所有编辑合并为一次请求。这对协作编辑场景能显著降低服务器负载。

5.2 测试对防抖与断线合并的验证

测试用例 中有两条用例直接印证这套机制:

  • "should merge operations while disconnected and send them when reconnected":断开期间连续两次插入文本(helloworld),sendOperations被触发两次但合并为一条请求,重连后服务端收到的更新解码出来是完整的hello world
  • "should not send operations when disconnected":断开后编辑不产生任何发送请求。

这两条用例同时验证了测试基础设施 test-utils.ts 中createMockProvider+feedMessage模拟 Shape 流下发的可行性。

5.3 offset=now 的落地位置

0.1.22 同版本的另一半变更 "use offset=now" 落在connect()中 awareness ShapeStream 的配置上(见 2.2 节代码)。文档通道使用resumeState.document的 offset/handle 实现精确断点续传,而 awareness 通道始终从now开始,只关心当前在线的客户端状态。

六、断线续传与 LocalStorageResumeStateProvider

6.1 为什么必须持久化 resume state

ResumeState(types.ts)由两部分组成:

  • document.offset+document.handle:Shape 流的续传游标;
  • stableStateVector:最近一次与服务端同步完成时的文档状态向量。

如果不提供resumeState,Provider 每次(重)连接都会拉取整个文档 Shape;如果提供了stableStateVector,构造器会执行:

if (this.resumeState?.stableStateVector) { this.pendingChanges = Y.encodeStateAsUpdate( this.doc, this.resumeState.stableStateVector ) }

即只把"本地新产生的、尚未同步过的 diff"打包上行。SKILL.md 明确警告:没有stableStateVector时每次重连都会全量重传整个文档。这是协作应用中必须避免的高成本路径。

6.2 LocalStorage 参考实现的读写细节

local-storage-resume-state.ts 是一个开箱即用的参考实现,用两个 localStorage key 存储续传点:

  • ${key}:JSON 序列化的{ operations: { offset, handle } }
  • ${key}_vectorstableStateVector的 Base64 编码(不存在则删除该 key,避免脏数据)。

save()subscribeToResumeState(provider)注册的resumeState事件处理器驱动;load()在无缓存时返回{}。由于它依赖localStorage,本质是浏览器环境的参考实现,其他环境(如 React Native)需要按同一接口自定义ResumeStateProvider

七、parseToDecoder:BYTEA 与 lib0 Decoder 的桥接

由于 Yjs 更新以bytea二进制存储在 Postgres,而@electric-sql/client的 Shape 流默认把列值按文本返回,utils.ts 提供了parseToDecoder

export const parseToDecoder = { bytea: (hexString: string) => { const uint8Array = hexStringToUint8Array(hexString) // 去掉 "\x" 前缀并按十六进制还原字节 return decoding.createDecoder(uint8Array) }, }

它把 Postgresbytea的 hex 字符串转回Uint8Array,再包装成 lib0 的Decoder。SKILL.md 将"忘记传parser: parseToDecoder"列为高危错误:缺少 parser 时 Shape 返回的是原始 hex 字符串,Y.applyUpdate会静默失败或损坏文档。配合getUpdateFromRow的灵活取值,这套设计让 Y-Electric 可以对接任意后端表结构。

八、0.1.38 的 Agent 能力:内置 TanStack Intent 技能

0.1.38 是 CHANGELOG 中内容最丰富的补丁版本:"Add TanStack Intent skills for AI agent guidance. Ships 9 skills covering shapes, proxy auth, schema design, debugging, deployment, new feature setup, ORM integration, Postgres security, and Yjs collaboration." 同时 0.1.42 在 package.json 的keywords中加入tanstack-intent,并在files中把skills目录打进发布包,使技能可随 npm 分发。

仓库内与 y-electric 强相关的技能文档是 SKILL.md,它针对 AI Agent 编写,明确声明依赖electric-shapes技能,并给出协作编辑接入的完整指引:Postgres 建表、PUT端点实现、ElectricProvider配置、CORS 头暴露、resume state 续传、连接生命周期,以及三个常见误区:

误区级别原因与正确做法
不持久化 resume stateHIGH每次重连全量拉取文档;应配合LocalStorageResumeStateProvider+stableStateVector只传 diff
缺少parseToDecoderHIGHShape 返回 hex 字符串导致Y.applyUpdate失败;必须配置 parser
不设置debounceMsMEDIUM默认 0,每次击键一个 PUT;协作编辑应设为 100ms+ 批量合并

这标志着该包从"纯运行时库"向"AI Agent 可理解的开发指南载体"演进,是 0.1.x 阶段最重要的非功能性变化。

九、版本依赖对应全表

CHANGELOG 中 0.1.1~0.1.53 的依赖升级整体呈单调递增,核心是@electric-sql/client的同步演进。以下是主要的版本台阶:

y-electric 版本依赖的 @electric-sql/client
0.1.11.0.5
0.1.51.0.8
0.1.101.0.13
0.1.151.1.3
0.1.181.2.0
0.1.221.3.1(含防抖 + offset=now)
0.1.261.5.0
0.1.311.5.5
0.1.381.5.12(含 TanStack Intent 技能)
0.1.421.5.16(含 tanstack-intent 关键词)
0.1.481.5.22
0.1.531.5.27(当前最新)

从 package.json 可以看到,包的运行时依赖还包括yjs@^13.6.6y-protocols@^1.0.5lib0@^0.2.65,构建与测试工具链为tsup+vitest,提供 ESM/CJS 双格式产物。

十、验证与测试:协作同步行为如何被自动化守护

y-electric.test.ts 覆盖了 Provider 的核心行为契约:

  • 上行发送Y.Text被修改后,fetch被调用一次(模拟 PUT 提交);
  • 下行应用:通过feedMessage注入远端Y.encodeStateAsUpdate编码的更新,本地文档内容正确变更;
  • 断线语义:断开后编辑不发送、合并待发、重连后一次性补发(见 5.2);
  • 资源清理disconnect()+destroy()后,doc._observers上的update处理器被移除,避免内存泄漏。

测试通过 test-utils.ts 以vi.mock('@electric-sql/client')替换真实ShapeStream,用可编程的feedMessage模拟服务端消息,使协作场景可以在纯单元测试中确定性复现。

结语:从 CHANGELOG 读出的演进主线

@electric-sql/y-electric的 0.1.x 版本史是一条清晰的三段式演进:先立骨架(0.1.0 发布 Provider 与双通道同步模型)、再补体验(0.1.22 的防抖与offset=now解决高频编辑与瞬态状态问题)、后拓生态(0.1.38/0.1.42 的 TanStack Intent 技能让 AI Agent 可以按文档化技能接入)。对使用者而言,本文覆盖的四件事是实战刚需:用parseToDecoder正确解析 BYTEA、用LocalStorageResumeStateProvider避免重连全量拉取、用debounceMs抑制写放大、以及用 shape proxy + 自定义 PUT 端点补齐 Electric 只读同步之外的写路径。把这四件事做对,一个生产可用的 Yjs + Electric + Postgres 实时协作应用就具备了完整的闭环。

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询