// 客户端实现 import { SimpleTextOperation as TextOperation } from "./op"; import { getDemoServerInstance } from "./server"; // 直接获取server实例,这里省略了网络过程 const server = getDemoServerInstance(); export class Client { public name: string = ""; // 当前文档 public document: string = ""; // 当前服务端文档状态 public syncedDoc: string = ""; // 下一个期望的修订号 public revision: number; // 正在处理的操作,已经发送给服务端等待确认 public outstanding: TextOperation | null = null; // 本地缓存的待发送操作队列(包含outstanding) public buffer: TextOperation[] = []; // 是否启用同步到服务端 public enableSync: boolean = true; constructor() { // 模拟连接服务器,获取初始文档状态和修订版本号 const { document, revision } = server.connect(this); this.revision = revision; this.document = document; this.name = `client-${Math.random().toString(36).substring(2, 15)}`; } // 当用户更改文档时调用此方法 applyClient(operation: TextOperation): void { this.document = operation.apply(this.document); this.buffer.push(operation); this.sendOperationIfNeed(); } // 接收到来自服务器的新操作,可能是另一个客户端的操作,也可能是服务端确认本地操作的响应 applyServer(revision: number, operation: TextOperation): void { if (revision < 0 || this.revision > revision) { throw new Error("operation revision not in history"); } if (revision < this.revision) { return; } const isSelfOp = operation.id === this.outstanding?.id; if (this.outstanding) { // 将outstanding操作转化为执行operation后的操作 const newOp = TextOperation.transform(this.outstanding, operation); newOp.id = this.outstanding.id; this.outstanding = newOp; } for (let i = 0; i < this.buffer.length; i++) { // 将buffer中的操作转换为基于服务端文档状态的操作 // FIXME: 对buffer中的操作应该合并后再transform const newOp = TextOperation.transform( this.buffer[i], // 如果有outstanding,buffer[0]应该基于outstanding this.buffer[i - 1] || this.outstanding || operation ); newOp.id = this.buffer[i].id; this.buffer[i] = newOp; } // 本地重新应用buffer中的操作,合并其它客户端的操作到本地 let newDoc = this.syncedDoc; newDoc = operation.apply(newDoc); for (let i = 0; i < this.buffer.length; i++) { newDoc = this.buffer[i].apply(newDoc); } this.document = newDoc; // 更新本地同步状态 this.syncedDoc = operation.apply(this.syncedDoc); this.revision = revision; // 操作来自服务端确认本地操作的响应 if (isSelfOp) { this.serverAck(); } console.log("mattuy", "client", this.name, this); } // 服务端已确认收到发送的操作,从buffer中移除该操作,尝试发送下一个 serverAck(): void { this.outstanding = null; this.sendOperationIfNeed(); } // 逐个向服务端发送本地待提交的操作。发送后等待服务端确认,触发serverAck方法后继续发送下一个操作 async sendOperationIfNeed() { // 如果禁用了同步,则不向服务端发送操作 if (!this.enableSync) { return; } if (this.outstanding || this.buffer.length === 0) { // 有正在发送的操作,或者没有待提交的操作,no-op return; } // 向服务端发送本地待提交的操作,这里没有实现操作压缩,我们一个一个发送 this.outstanding = this.buffer.shift()!; // 模拟向服务端发送操作,实际实现应该通过网络 server.receiveOperation(this, this.revision, this.outstanding); // 应该有超时重发机制,这里没有实现 } // 设置是否启用同步到服务端 setEnableSync(enabled: boolean): void { this.enableSync = enabled; // 如果重新启用同步,尝试发送待发送的操作 if (enabled) { this.sendOperationIfNeed(); } } }