Files
ot-demo/src/ot/client.ts
T
2026-01-06 20:13:24 +08:00

119 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 客户端实现
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],
// 如果有outstandingbuffer[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();
}
}
}