119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
// 客户端实现
|
||
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();
|
||
}
|
||
}
|
||
}
|