commit
This commit is contained in:
+29
-46
@@ -9,8 +9,6 @@ export class Client {
|
|||||||
public name: string = "";
|
public name: string = "";
|
||||||
// 当前文档
|
// 当前文档
|
||||||
public document: string = "";
|
public document: string = "";
|
||||||
// 当前服务端文档状态
|
|
||||||
public syncedDoc: string = "";
|
|
||||||
// 下一个期望的修订号
|
// 下一个期望的修订号
|
||||||
public revision: number;
|
public revision: number;
|
||||||
// 正在处理的操作,已经发送给服务端等待确认
|
// 正在处理的操作,已经发送给服务端等待确认
|
||||||
@@ -35,64 +33,48 @@ export class Client {
|
|||||||
this.sendOperationIfNeed();
|
this.sendOperationIfNeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收到来自服务器的新操作,可能是另一个客户端的操作,也可能是服务端确认本地操作的响应
|
// 接收到其它客户端的操作
|
||||||
applyServer(revision: number, operation: TextOperation): void {
|
applyServer(operation: TextOperation): void {
|
||||||
if (revision < 0 || this.revision > revision) {
|
if (!this.enableSync) {
|
||||||
throw new Error("operation revision not in history");
|
// 模拟离线,忽略其它客户端的操作
|
||||||
}
|
|
||||||
if (revision < this.revision) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelfOp = operation.id === this.outstanding?.id;
|
|
||||||
|
|
||||||
if (this.outstanding) {
|
if (this.outstanding) {
|
||||||
// 将outstanding操作转化为执行operation后的操作
|
// 将operation转换为基于outstanding操作后的操作
|
||||||
const newOp = TextOperation.transform(this.outstanding, operation);
|
operation = TextOperation.transform(this.outstanding, operation)[1];
|
||||||
newOp.id = this.outstanding.id;
|
|
||||||
this.outstanding = newOp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 现在,operation是基于outstanding操作后的操作,buffer[0]也是
|
||||||
|
// (如果outstanding为空,则都是基于server文档状态操作)
|
||||||
for (let i = 0; i < this.buffer.length; i++) {
|
for (let i = 0; i < this.buffer.length; i++) {
|
||||||
// 将buffer中的操作转换为基于服务端文档状态的操作
|
// 将operation转换基于buffer[i]操作后的操作
|
||||||
// FIXME: 对buffer中的操作应该合并后再transform
|
operation = TextOperation.transform(this.buffer[i], operation)[1];
|
||||||
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中的操作,合并其它客户端的操作到本地
|
console.log("client", this.name, "received operation", operation);
|
||||||
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;
|
|
||||||
|
|
||||||
// 更新本地同步状态
|
// 现在operation是基于本地最新document的操作了,直接在本地应用它!
|
||||||
this.syncedDoc = operation.apply(this.syncedDoc);
|
this.applyOperation(operation);
|
||||||
this.revision = revision;
|
this.revision++;
|
||||||
|
|
||||||
// 操作来自服务端确认本地操作的响应
|
|
||||||
if (isSelfOp) {
|
|
||||||
this.serverAck();
|
|
||||||
}
|
|
||||||
console.log("mattuy", "client", this.name, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务端已确认收到发送的操作,从buffer中移除该操作,尝试发送下一个
|
// 本地应用操作
|
||||||
|
applyOperation(operation: TextOperation): void {
|
||||||
|
this.document = operation.apply(this.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务端已确认收到发送的操作,尝试发送下一个操作
|
||||||
|
// 简单实现。即使是模拟离线状态我们也接收Ack,避免实现操作去重
|
||||||
serverAck(): void {
|
serverAck(): void {
|
||||||
|
console.log("client", this.name, "operation ack", this.outstanding);
|
||||||
this.outstanding = null;
|
this.outstanding = null;
|
||||||
|
this.revision++;
|
||||||
this.sendOperationIfNeed();
|
this.sendOperationIfNeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 逐个向服务端发送本地待提交的操作。发送后等待服务端确认,触发serverAck方法后继续发送下一个操作
|
// 逐个向服务端发送本地待提交的操作。发送后等待服务端确认,触发serverAck方法后继续发送下一个操作
|
||||||
async sendOperationIfNeed() {
|
async sendOperationIfNeed() {
|
||||||
// 如果禁用了同步,则不向服务端发送操作
|
// 模拟离线,不向服务端发送操作
|
||||||
if (!this.enableSync) {
|
if (!this.enableSync) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -102,16 +84,17 @@ export class Client {
|
|||||||
}
|
}
|
||||||
// 向服务端发送本地待提交的操作,这里没有实现操作压缩,我们一个一个发送
|
// 向服务端发送本地待提交的操作,这里没有实现操作压缩,我们一个一个发送
|
||||||
this.outstanding = this.buffer.shift()!;
|
this.outstanding = this.buffer.shift()!;
|
||||||
// 模拟向服务端发送操作,实际实现应该通过网络
|
// 模拟向服务端发送操作,实际实现应该通过网络发送
|
||||||
server.receiveOperation(this, this.revision, this.outstanding);
|
server.receiveOperation(this, this.revision, this.outstanding);
|
||||||
// 应该有超时重发机制,这里没有实现
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置是否启用同步到服务端
|
// 设置模拟离线状态
|
||||||
setEnableSync(enabled: boolean): void {
|
setEnableSync(enabled: boolean): void {
|
||||||
|
console.log("client", this.name, enabled ? "online" : "offline");
|
||||||
this.enableSync = enabled;
|
this.enableSync = enabled;
|
||||||
// 如果重新启用同步,尝试发送待发送的操作
|
// 重新启用同步,继续发送本地操作,发送一个空操作,确保服务端向我们推送最新操作
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
// this.buffer.push(new Noop());
|
||||||
this.sendOperationIfNeed();
|
this.sendOperationIfNeed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-47
@@ -1,93 +1,143 @@
|
|||||||
// 简单文本操作的实现
|
// 简单文本操作的实现
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
export abstract class SimpleTextOperation {
|
export abstract class SimpleTextOperation {
|
||||||
public id: string = uuidv4();
|
|
||||||
abstract toString(): string;
|
abstract toString(): string;
|
||||||
abstract equals(other: SimpleTextOperation): boolean;
|
abstract equals(other: SimpleTextOperation): boolean;
|
||||||
abstract apply(doc: string): string;
|
abstract apply(doc: string): string;
|
||||||
|
|
||||||
// 把操作a转换为基于应用操作b后的文档状态的操作,返回操作a'
|
/**
|
||||||
static transform(a: SimpleTextOperation, b: SimpleTextOperation): SimpleTextOperation {
|
* OT算法转换函数
|
||||||
|
* @param a 操作a
|
||||||
|
* @param b 操作b
|
||||||
|
* @returns
|
||||||
|
* [0] = a' 操作a基于操作b应用后的操作
|
||||||
|
* [1] = b' 操作b基于操作a应用后的操作
|
||||||
|
* apply(apply(S, a), b') = apply(apply(S, b), a')
|
||||||
|
*/
|
||||||
|
static transform(
|
||||||
|
a: SimpleTextOperation,
|
||||||
|
b: SimpleTextOperation
|
||||||
|
): [SimpleTextOperation, SimpleTextOperation] {
|
||||||
if (a instanceof Noop || b instanceof Noop) {
|
if (a instanceof Noop || b instanceof Noop) {
|
||||||
return a;
|
return [a, b];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 情况2: a 和 b 都是 Insert(插入操作)
|
||||||
if (a instanceof Insert && b instanceof Insert) {
|
if (a instanceof Insert && b instanceof Insert) {
|
||||||
if (a.position < b.position) {
|
// 情况2.1: a 的插入位置在 b 之前,或者位置相同但 a 的字符串字典序更小
|
||||||
// b在a插入的地方之后插入,不转换
|
// 处理: a 保持不变,b 的插入位置需要向后偏移 a 插入的字符串长度
|
||||||
return a;
|
if (a.position < b.position || (a.position === b.position && a.str < b.str)) {
|
||||||
|
return [a, new Insert(b.str, b.position + a.str.length)];
|
||||||
}
|
}
|
||||||
// b在a插入的地方之前插入,位置偏移b插入的str长度后插入
|
// 情况2.2: a 的插入位置在 b 之后,或者位置相同但 a 的字符串字典序更大
|
||||||
return new Insert(a.str, a.position + b.str.length);
|
// 处理: a 的插入位置需要向后偏移 b 插入的字符串长度,b 保持不变
|
||||||
|
if (a.position > b.position || (a.position === b.position && a.str > b.str)) {
|
||||||
|
return [new Insert(a.str, a.position + b.str.length), b];
|
||||||
|
}
|
||||||
|
// 情况2.3: a 和 b 插入位置相同且字符串完全相同
|
||||||
|
// 处理: 两个操作都变为 noop,因为插入的内容相同
|
||||||
|
return [noop, noop];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 情况3: a 是 Insert(插入),b 是 Delete(删除)
|
||||||
if (a instanceof Insert && b instanceof Delete) {
|
if (a instanceof Insert && b instanceof Delete) {
|
||||||
|
// 情况3.1: a 的插入位置在 b 的删除位置之前或相同
|
||||||
|
// 处理: a 保持不变,b 的删除位置需要向后偏移 a 插入的字符串长度
|
||||||
if (a.position <= b.position) {
|
if (a.position <= b.position) {
|
||||||
// b在a插入的地方之后删除,不转换
|
return [a, new Delete(b.count, b.position + a.str.length)];
|
||||||
return a;
|
|
||||||
}
|
}
|
||||||
|
// 情况3.2: a 的插入位置在 b 的删除范围之后
|
||||||
|
// 处理: a 的插入位置需要向前偏移 b 删除的字符数,b 保持不变
|
||||||
if (a.position >= b.position + b.count) {
|
if (a.position >= b.position + b.count) {
|
||||||
// b在a插入的位置之前删除,并且a插入位置的字符没有被删除
|
return [new Insert(a.str, a.position - b.count), b];
|
||||||
return new Insert(a.str, a.position - b.count);
|
|
||||||
}
|
}
|
||||||
// b在a插入的位置之前删除,并且a插入位置的字符被删除,在b删除的位置插入a的str
|
// 情况3.3: a 的插入位置在 b 的删除范围内部(有冲突)
|
||||||
return new Insert(a.str, b.position);
|
// 处理: a 变为 noop(插入的内容被删除),b 的删除范围需要包含 a 插入的字符串长度
|
||||||
|
// 注意: 这会丢失 a 的插入意图,但这是保持转换函数有效性的唯一方法
|
||||||
|
return [noop, new Delete(b.count + a.str.length, b.position)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 情况4: a 是 Delete(删除),b 是 Insert(插入)
|
||||||
if (a instanceof Delete && b instanceof Insert) {
|
if (a instanceof Delete && b instanceof Insert) {
|
||||||
|
// 情况4.1: a 的删除位置在 b 的插入位置之后或相同
|
||||||
|
// 处理: a 的删除位置需要向后偏移 b 插入的字符串长度,b 保持不变
|
||||||
if (a.position >= b.position) {
|
if (a.position >= b.position) {
|
||||||
// a在插入位置之后删除,增加删除操作的偏移
|
return [new Delete(a.count, a.position + b.str.length), b];
|
||||||
return new Delete(a.count, a.position + b.str.length);
|
|
||||||
}
|
}
|
||||||
|
// 情况4.2: a 的删除范围在 b 的插入位置之前(完全不相交)
|
||||||
|
// 处理: a 保持不变,b 的插入位置需要向前偏移 a 删除的字符数
|
||||||
if (a.position + a.count <= b.position) {
|
if (a.position + a.count <= b.position) {
|
||||||
// a在插入位置之前删除,并且b插入之前的字符没有被删除,不转换
|
return [a, new Insert(b.str, b.position - a.count)];
|
||||||
return a;
|
|
||||||
}
|
}
|
||||||
// b插入的内容在a删除的范围内,直接把b插入的内容一起删除
|
// 情况4.3: b 的插入位置在 a 的删除范围内部(有冲突)
|
||||||
// NOTICE: 这里b插入的内容丢失了,在生产环境应该裂变为前后两个删除操作,这里简化了实现
|
// 处理: a 的删除范围需要包含 b 插入的字符串长度,b 变为 noop(插入的内容被删除)
|
||||||
return new Delete(a.count + b.str.length, a.position);
|
// 注意: 这会丢失 b 的插入意图,但这是保持转换函数有效性的唯一方法
|
||||||
|
return [new Delete(a.count + b.str.length, a.position), noop];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 情况5: a 和 b 都是 Delete(删除操作)
|
||||||
if (a instanceof Delete && b instanceof Delete) {
|
if (a instanceof Delete && b instanceof Delete) {
|
||||||
// 删除位置相同
|
// 情况5.1: a 和 b 的删除位置相同
|
||||||
if (a.position === b.position) {
|
if (a.position === b.position) {
|
||||||
if (a.count <= b.count) {
|
// 情况5.1.1: 删除的字符数相同
|
||||||
// b删除得更多,已经把a要删除的删掉了,no-op
|
// 处理: 两个操作都变为 noop,因为删除的内容完全相同
|
||||||
return noop;
|
if (a.count === b.count) {
|
||||||
|
return [noop, noop];
|
||||||
}
|
}
|
||||||
// b删除得少,a删除得多,删除b没删掉的部分
|
// 情况5.1.2: a 删除的字符数少于 b
|
||||||
return new Delete(a.count - b.count, a.position);
|
// 处理: a 变为 noop(已经被 b 删除),b 只删除剩余的部分
|
||||||
|
else if (a.count < b.count) {
|
||||||
|
return [noop, new Delete(b.count - a.count, b.position)];
|
||||||
|
}
|
||||||
|
// 情况5.1.3: a 删除的字符数多于 b
|
||||||
|
// 处理: a 只删除剩余的部分,b 变为 noop(已经被 a 删除)
|
||||||
|
return [new Delete(a.count - b.count, a.position), noop];
|
||||||
}
|
}
|
||||||
|
// 情况5.2: a 的删除位置在 b 之前
|
||||||
if (a.position < b.position) {
|
if (a.position < b.position) {
|
||||||
|
// 情况5.2.1: a 的删除范围完全在 b 的删除位置之前(不相交)
|
||||||
|
// 处理: a 保持不变,b 的删除位置需要向前偏移 a 删除的字符数
|
||||||
if (a.position + a.count <= b.position) {
|
if (a.position + a.count <= b.position) {
|
||||||
// a在b删除的位置之前删除,并且a删除的结束位置在b删除的开始位置之前,不转换
|
return [a, new Delete(b.count, b.position - a.count)];
|
||||||
return a;
|
|
||||||
}
|
}
|
||||||
|
// 情况5.2.2: a 的删除范围完全包含 b 的删除范围
|
||||||
|
// 处理: a 只删除 b 未删除的部分,b 变为 noop(已经被 a 删除)
|
||||||
if (a.position + a.count >= b.position + b.count) {
|
if (a.position + a.count >= b.position + b.count) {
|
||||||
// a删除的范围包含b删除范围,减去b已经删除的长度
|
return [new Delete(a.count - b.count, a.position), noop];
|
||||||
return new Delete(a.count - b.count, a.position);
|
|
||||||
}
|
}
|
||||||
// a删除的范围在b删除的范围之前且有交集,减去交集部分长度
|
// 情况5.2.3: a 和 b 的删除范围有交集,a 在前,b 在后
|
||||||
// b: *****------****
|
// 处理: a 只删除到 b 开始位置之前的部分,b 删除从 a 结束位置之后到 b 结束位置的部分
|
||||||
// a: ***----********
|
// 示例: 文档 "0123456789"
|
||||||
// a': ***--**********
|
// a: 删除位置0-5 (删除 "01234")
|
||||||
return new Delete(b.position - a.position, a.position);
|
// b: 删除位置3-7 (删除 "3456")
|
||||||
|
// 结果: a' 删除位置0-3 (删除 "012"),b' 删除位置3-5 (删除 "67")
|
||||||
|
return [
|
||||||
|
new Delete(b.position - a.position, a.position),
|
||||||
|
new Delete(b.position + b.count - (a.position + a.count), a.position),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
// 情况5.3: a 的删除位置在 b 之后
|
||||||
if (a.position > b.position) {
|
if (a.position > b.position) {
|
||||||
|
// 情况5.3.1: a 的删除范围完全在 b 的删除范围之后(不相交)
|
||||||
|
// 处理: a 的删除位置需要向前偏移 b 删除的字符数,b 保持不变
|
||||||
if (a.position >= b.position + b.count) {
|
if (a.position >= b.position + b.count) {
|
||||||
// a删除的范围在b之后且没有交集,删除位置减去b删除的长度
|
return [new Delete(a.count, a.position - b.count), b];
|
||||||
return new Delete(a.count, a.position - b.count);
|
|
||||||
}
|
}
|
||||||
|
// 情况5.3.2: b 的删除范围完全包含 a 的删除范围
|
||||||
|
// 处理: a 变为 noop(已经被 b 删除),b 只删除剩余的部分
|
||||||
if (a.position + a.count <= b.position + b.count) {
|
if (a.position + a.count <= b.position + b.count) {
|
||||||
// b删除范围包含a删除范围,no-op
|
return [noop, new Delete(b.count - a.count, b.position)];
|
||||||
return noop;
|
|
||||||
}
|
}
|
||||||
// a删除的范围在b删除的范围之后且有交集,减去交集部分长度,并把开始位置放到交集结束位置
|
// 情况5.3.3: a 和 b 的删除范围有交集,b 在前,a 在后
|
||||||
// b: ***----*********
|
// 处理: a 删除从 b 结束位置之后到 a 结束位置的部分,b 删除从 b 开始位置到 a 开始位置的部分
|
||||||
// a: *****------*****
|
// 示例: 文档 "0123456789"
|
||||||
// a': *******----*****
|
// b: 删除位置3-7 (删除 "3456")
|
||||||
return new Delete(a.count - b.count, b.position + b.count);
|
// a: 删除位置5-9 (删除 "5678")
|
||||||
|
// 结果: a' 删除位置7-9 (删除 "89"),b' 删除位置3-5 (删除 "34")
|
||||||
|
return [
|
||||||
|
new Delete(a.position + a.count - (b.position + b.count), b.position),
|
||||||
|
new Delete(a.position - b.position, b.position),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-8
@@ -16,25 +16,24 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 每当从客户端接收到操作时调用此方法
|
// 每当从客户端接收到操作时调用此方法
|
||||||
async receiveOperation(client: Client, revision: number, operation: TextOperation) {
|
receiveOperation(client: Client, revision: number, operation: TextOperation) {
|
||||||
|
console.log("server", "received operation", revision, operation);
|
||||||
|
|
||||||
if (revision < 0 || this.operations.length < revision) {
|
if (revision < 0 || this.operations.length < revision) {
|
||||||
throw new Error("operation revision not in history");
|
throw new Error("operation revision not in history");
|
||||||
}
|
}
|
||||||
console.log("mattuy", "received operation", revision, operation);
|
|
||||||
// 查找客户端发送操作时不知道的所有操作...
|
// 查找客户端发送操作时不知道的所有操作...
|
||||||
const concurrentOperations = this.operations.slice(revision);
|
const concurrentOperations = this.operations.slice(revision);
|
||||||
|
|
||||||
// ...并将操作与所有这些操作进行转换...
|
// ...并将操作与所有这些操作进行转换...
|
||||||
const opId = operation.id;
|
|
||||||
const transform = TextOperation.transform;
|
const transform = TextOperation.transform;
|
||||||
for (let i = 0; i < concurrentOperations.length; i++) {
|
for (let i = 0; i < concurrentOperations.length; i++) {
|
||||||
// op' = transform(op, op1)
|
// op' = transform(op, op1)
|
||||||
// op基于 revision+i 版本的文档操作,op'基于 revision+i+1 版本的文档操作
|
// op基于 revision+i 版本的文档操作,op'基于 revision+i+1 版本的文档操作
|
||||||
operation = transform(operation, concurrentOperations[i]);
|
operation = transform(operation, concurrentOperations[i])[0];
|
||||||
// 向客户端发送其它客户端的操作
|
// 向客户端发送其它客户端的操作
|
||||||
client.applyServer(revision + i + 1, concurrentOperations[i]);
|
client.applyServer(concurrentOperations[i]);
|
||||||
}
|
}
|
||||||
operation.id = opId;
|
|
||||||
|
|
||||||
// ...并将其应用到文档上
|
// ...并将其应用到文档上
|
||||||
this.document = operation.apply(this.document);
|
this.document = operation.apply(this.document);
|
||||||
@@ -42,9 +41,16 @@ class Server {
|
|||||||
this.operations.push(operation);
|
this.operations.push(operation);
|
||||||
|
|
||||||
// 将操作发送给所有连接的客户端并向创建者发送确认
|
// 将操作发送给所有连接的客户端并向创建者发送确认
|
||||||
this.clients.forEach((client) => {
|
this.clients.forEach((otherClient) => {
|
||||||
client.applyServer(this.operations.length, operation);
|
if (otherClient === client) {
|
||||||
|
// 自己不需要接收自己的操作,会响应Ack
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
otherClient.applyServer(operation);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 客户端确认收到操作
|
||||||
|
client.serverAck();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟客户端连接
|
// 模拟客户端连接
|
||||||
|
|||||||
Reference in New Issue
Block a user