ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ShareDB LocalPresence 实战指南:本地客户端 Presence 的提交、重发与销毁

ShareDB LocalPresence 实战指南:本地客户端 Presence 的提交、重发与销毁 后端数据库【免费下载链接】sharedbRealtime database backend based on Operational Transformation (OT)项目地址https://gitcode.com/gh_mirrors/sh/sharedb点击查看免费下载导读LocalPresence是 ShareDB 中代表**本地客户端自身 Presence存在/位置信息**的核心对象典型场景包括协作文本编辑器中本地光标的所在位置、复杂 JSON 对象中当前被高亮的字段、多人画布中本机鼠标指针的坐标等。本指南以 docs/api/local-presence.md 为主体完整讲解submit()、send()、destroy()三个核心 API 的语义与参数并结合 lib/client/presence/local-presence.js 及其周边实现深入剖析其消息协议、版本号与回调机制、断线重连时的缓存重发逻辑。读完本文你将能熟练地在自己的 ShareDB 应用中创建、更新、周期性心跳发送以及销毁本地 Presence并理解它在底层是如何与远端订阅者协同工作的。什么是 LocalPresence在 ShareDB 的 Presence 体系中一条 channel频道上的 Presence 数据由两类对象构成Presence实例代表某个频道上全部 Presence 数据的聚合容器维护remotePresences远端 Presence 值映射与localPresences本地LocalPresence实例映射详见 docs/api/presence.mdLocalPresence实例代表当前客户端在这一频道上的 Presence即我发出的那部分信息。LocalPresence由父级Presence通过presence.create()创建一个Presence上可以同时存在多个LocalPresence例如多列文本光标、多点触控输入。文档原文给出的典型例子是文本文档中光标的所在位置、复杂 JSON 对象中被高亮的字段。从源码看LocalPresence的构造函数位于 lib/client/presence/local-presence.jsfunction LocalPresence(presence, presenceId) { emitter.EventEmitter.call(this); if (!presenceId || typeof presenceId ! string) { throw new Error(LocalPresence presenceId must be a string); } this.presence presence; this.presenceId presenceId; this.connection presence.connection; this.presenceVersion 0; this.value null; this._pendingMessages []; this._callbacksByPresenceVersion Object.create(null); }其中presenceId必须是非空字符串否则构造时直接抛错。在 test/client/presence/presence.js 中也有对应测试throws an error when trying to create a presence with a non-string IDpresenceVersion是本地 Presence 的版本计数每条发出的消息都会携带自增的pv字段用于对端去重与乱序处理详见下文版本号与乱序防护value是当前本地 Presence 值初始为null表示尚未存在于文档中_pendingMessages与_callbacksByPresenceVersion用于连接未就绪时暂存待发消息并挂接回调。LocalPresence本身继承自EventEmitter通过emitter.mixin(LocalPresence)混入因此它也具备error等事件能力。创建 LocalPresenceLocalPresence不直接通过构造函数创建而是通过父级Presence的create()方法获得presence.create([presenceId])presenceIdstring可选本地 Presence 的唯一 ID。省略时会由hat()生成一个随机 ID见 lib/client/presence/presence.js 与Presence.prototype.create。返回值一个新的LocalPresence实例并已登记到presence.localPresences[presenceId]。注意由于同一客户端可能拥有多个Presence多光标、多触点文档与源码均提醒用户 ID 或客户端 ID 未必适合直接作为 presence ID——它需要能区分同一客户端的多个 Presence 实例。而Presence本身来自connection.getPresence(channel)未与文档绑定的普通 Presence或connection.getDocPresence(collection, id)与某个 Doc 绑定的DocPresence两个方法都定义在 lib/client/connection.js 的getPresence/getDocPresence中。DocPresence的 channel 由collection . id拼接而成见 lib/client/presence/doc-presence.js并将本地实例创建为LocalDocPresence详见下文DocPresence 下的本地 Presence。const presence connection.getPresence(my-channel) presence.subscribe() const localPresence presence.create() localPresence.submit({foo: bar})方法一submit()——更新并广播本地 PresencelocalPresence.submit(presence [, callback])presence-- Object要广播的 Presence 对象。其结构取决于所用的 type。callback-- Function可选function(error) { ... }一个回调会在 Presence 被发送后调用。submit()是日常使用最频繁的方法它同时完成两件事——更新本地的 Presence 表示并把这份新值广播给该频道上所有其他 Presence 订阅者。源码语义lib/client/presence/local-presence.js 中submit的实现极为简洁LocalPresence.prototype.submit function(value, callback) { this.value value; this.send(callback); };即先把value写入本地this.value然后委托给send()完成真正的发送。因此submit()等价于设置新值 send()。关于null值文档原文特别标注了一条关键约定值为null时将被解释为客户端不再存在no longer present。也就是说想宣告自己离线/离开时应提交null而不是其他标记值。远端收到null后会将该 Presence 从remotePresences中移除并触发presence.on(receive, (id, value))且value为nullUI 层据此清除该用户的游标/指针。这一行为在测试 test/client/presence/presence.js 的removes remote presence when it is set to null中得到了验证本地submit(null)后远端remotePresences变为{}。此外submit还有一个细节当连接尚未就绪connection.canSend false时消息会被暂存在_pendingMessages待连接恢复后统一补发见_sendPending。配合Presence事件的典型用法const presence connection.getPresence(my-channel) presence.subscribe() presence.on(receive, (presenceId, value) { if (value null) { // 远端客户端已不在场清理对应的游标/高亮 UI } else { // 用新值更新 UI } }) const localPresence presence.create(my-cursor) localPresence.submit({ index: 5 }, (error) { if (error) console.error(presence 发送失败, error) })方法二send()——仅重发当前值不修改内容localPresence.send([callback])callback-- Function可选function(error) { ... }一个回调会在 Presence 被发送后调用。send()与submit()的区别在于它只把当前value再次发送给其他订阅者而不更新值本身。源码中submit之所以等价于改值 send()正是因为send单独存在。典型应用周期性过期/心跳文档原文给出的典型场景是当本地 Presence 被设定为周期性过期时例如一段时间无操作后自动下线可以用send()持续广播我还活着。一个常见的实现是用户在操作时用submit()上报新位置同时启动一个定时器每次定时器触发时调用send()刷新远端对该客户端的存活判断当用户长时间无操作时submit(null)宣告离开。这样既不会因为频繁 submit 改变语义又能让远端感知到客户端仍在场。源码细节版本号与回调send()的实现lib/client/presence/local-presence.jsLocalPresence.prototype.send function(callback) { var message this._message(); this._pendingMessages.push(message); this._callbacksByPresenceVersion[message.pv] callback; this._sendPending(); };每次send()都会调用_message()构造一条消息其中pv使用当前presenceVersion并自增pv: this.presenceVersion把消息压入_pendingMessages以pv为键挂接本次回调触发_sendPending()尝试发送。消息结构由_message()定义lib/client/presence/local-presence.jsLocalPresence.prototype._message function() { return { a: ACTIONS.presence, // p见 lib/message-actions.js ch: this.presence.channel, id: this.presenceId, p: this.value, pv: this.presenceVersion }; };对应协议动作常量定义在 lib/message-actions.jspresence: p、presenceSubscribe: ps、presenceUnsubscribe: pu、presenceRequest: pr。连接未就绪时的队列重发_sendPending()lib/client/presence/local-presence.js只在connection.canSend为真时才真正通过connection.send(message)发出否则消息继续留在_pendingMessages中等待。当连接从断线恢复时Presence._onConnectionStateChangedlib/client/presence/presence.js会遍历所有localPresences并调用各自的_sendPending()补发积压消息同时自动执行_resubscribe()重新订阅。测试sends local presence once the connection can send与subscribes once the connection can send验证了这一行为。方法三destroy()——宣告离开并释放实例localPresence.destroy([callback])callback-- Function可选function(error) { ... }一个回调会在 Presence 被销毁后调用。destroy()的语义是通知所有远端客户端该 Presence 的值已变为null离线并删除自身以便垃圾回收。源码实现lib/client/presence/local-presence.jsLocalPresence.prototype.destroy function(callback) { var presence this; this.submit(null, function(error) { if (error) return presence._callbackOrEmit(error, callback); delete presence.presence.localPresences[presence.presenceId]; if (callback) callback(); }); };可以看到destroy()的本质就是submit(null)向所有远端订阅者广播该 ID 已离开等待发送成功或失败回调后再把自身从presence.localPresences映射中删除交给 GC 回收。由于submit的null约定远端会在receive事件中收到value null并清理对应 UI。与 Presence.destroy() 的关系父级Presence.destroy()lib/client/presence/presence.js会先标记_wantsDestroy true随后取消订阅并并行销毁该 Presence 上当前所有的LocalPresence调用各自的destroy()以及所有RemotePresence实例最后把自身从connection._presences中删除。因此销毁单个本地光标调用localPresence.destroy()销毁某频道上全部本地 Presence 并释放整个Presence调用presence.destroy()。值得注意的是Presence.create()在_wantsDestroy为真时会抛出Presence is being destroyed对应测试throws if trying to create local presence when wanting destroy。若在destroy进行期间需要重新获取 Presence可调用connection.getPresence()重新拿实例测试gets presence after destroy unsubscribe覆盖该场景。回调与 error 事件的统一处理三个方法都接受可选的function(error) {}回调其统一收口逻辑在_callbackOrEmitlib/client/presence/local-presence.jsLocalPresence.prototype._callbackOrEmit function(error, callback) { if (callback) return util.nextTick(callback, error); if (error) this.emit(error, error); };规则很明确若调用时传了回调错误通过回调返回callback(error)回调始终在nextTick中异步执行若调用时未传回调错误则转为在LocalPresence上触发error事件。因此调用方可以二选一要么始终挂回调处理错误要么监听localPresence.on(error, handler)。发送确认同样走_ack(error, presenceVersion)按消息的pv取回对应回调lib/client/presence/local-presence.js由Presence._receiveUpdatelib/client/presence/presence.js在收到服务器回执时按message.id路由到对应LocalPresence。版本号与乱序防护pv的作用Presence 消息与文档 op 一样都是独立异步传输的可能乱序到达。为此LocalPresence为每条消息生成自增的pvpresence version。接收侧RemotePresence.receiveUpdatelib/client/presence/remote-presence.js会做判断RemotePresence.prototype.receiveUpdate function(message) { if (message.pv this.presenceVersion) return; // 丢弃过期消息 this.value message.p; this.presenceVersion message.pv; this.presence._updateRemotePresence(this); };pv小于当前已接收版本的消息会被直接丢弃保证远端最终呈现的一定是最新值。测试ignores presence that arrives out of order验证了这一点即使发送方先发{index: 2}再发{index: 3}且中途被中间件暂停乱序放行接收方也只会触发一次receive且值为{index: 3}。DocPresence 下的本地 PresenceLocalDocPresence当通过connection.getDocPresence(collection, id)获得与文档绑定的 Presence 时presence.create()实际创建的是LocalDocPresencelib/client/presence/local-doc-presence.js它是LocalPresence的子类除继承submit()/send()/destroy()之外还具备两项增强同步防护由于 Presence 与文档 op 独立提交可能错位到达例如文本光标跳动。LocalDocPresence通过_docDataVersionByPresenceVersion记录提交时文档的数据状态版本并在文档发生op时用type.transformPresence(value, op, source)对待发送消息和当前值做变换_transformAgainstOp确保 Presence 始终对齐到正确的文档版本。仅当所用 type 支持 Presence 时才可变换否则抛出ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE文档状态守卫submit()在文档尚未创建无type时会拒绝发送若提交null则视为无害空操作直接成功若提交非空值则回调错误文档在硬回滚中返回ERR_DOC_IN_HARD_ROLLBACK否则返回ERR_DOC_DOES_NOT_EXIST文档create/del时待发消息与当前值会被重置为null。需要特别说明的是目前仅rich-text类型支持带文档的 Presence 信息见 docs/presence.md普通Presenceuntyped presence则对值没有任何结构约束。完整使用示例下面是一个完整的未绑定文档untypedPresence 使用流程综合了本文全部三个 API// 1. 获取 Presence 实例并订阅Presence 需要在 Backend 中启用new Backend({presence: true}) const presence connection.getPresence(my-channel) presence.subscribe((error) { if (error) console.error(订阅失败, error) }) // 2. 监听远端 Presence 更新 presence.on(receive, (presenceId, value) { if (value null) { // 远端客户端已离开 } else { // 更新 UI } }) // 3. 创建本地 Presence 并提交 const localPresence presence.create(my-cursor) localPresence.submit({ index: 5 }, (error) { if (error) console.error(提交失败, error) }) // 4. 周期性心跳只重发当前值不修改 setInterval(() { localPresence.send((error) { if (error) console.error(心跳发送失败, error) }) }, 30000) // 5. 离开时销毁向远端广播 null 并释放实例 localPresence.destroy((error) { if (error) console.error(销毁失败, error) })关键要点速查API作用是否修改本地值典型场景submit(value, callback?)更新本地值并广播是value可为null表示离开光标/指针/高亮字段位置变化send(callback?)仅重发当前值否周期性心跳、刷新存活状态destroy(callback?)提交null并释放实例是强制置null客户端离开、组件卸载清理其他要点presenceId必须为非空字符串缺省由hat()生成随机 ID值为null即表示客户端不再在场远端会移除该 Presence 并触发receive(id, null)消息携带自增pv版本号接收端丢弃乱序/过期消息保证最终一致性连接断线期间submit()/send()的消息会暂存恢复连接后自动补发并重新订阅回调与error事件二选一有回调则错误进回调nextTick异步无回调则触发error事件与文档绑定的DocPresence创建的LocalDocPresence会对 Presence 做 op 变换与文档状态守卫目前仅rich-text类型支持。延伸阅读Presence 总览Presence 的概念、未绑定文档与绑定文档两种用法、Backend({presence: true})开启要求Presence API 参考subscribe()/unsubscribe()/create()/destroy()与receive、error事件Connection APIgetPresence()与getDocPresence()的获取方式相关源码lib/client/presence/local-presence.js、lib/client/presence/presence.js、lib/client/presence/local-doc-presence.js、lib/client/presence/remote-presence.js测试用例test/client/presence/presence.js、test/client/presence/doc-presence.js协议动作常量lib/message-actions.js赞分享后端数据库【免费下载链接】sharedbRealtime database backend based on Operational Transformation (OT)项目地址https://gitcode.com/gh_mirrors/sh/sharedb点击查看免费下载相关推荐Aptos Keyless Pepper Service 开发指南本地运行、Firestore 集成与客户端交互实战Aptos Keyless Pepper Service 开发指南本地运行、Firestore 集成与客户端交互实战 导读 本文是 Aptos 仓库中 key区块链Web3Yakit 代码评审六维检查体系check-dimensions.md 检查细则与强制验证机制详解Yakit 代码评审六维检查体系check dimensions.md 检查细则与强制验证机制详解 本文以 Yakit 仓库内置的 六维检查细则 https:后端数据库ShareDB Presence API 详解实时在线状态与光标协同实战指南ShareDB Presence API 详解实时在线状态与光标协同实战指南 ShareDB 的 Presence 是实时协作中表达客户端此刻在文档里的位置后端数据库上一篇Dynamic-DatasourceSpring Boot多数据源管理的终极解决方案下一篇如何永久保存微信聊天记录WeChatMsg完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表