jerry 3 年 前
コミット
47f4a6d2d5

+ 124 - 25
packages/ot-core/src/basics/Command.ts

@@ -1,59 +1,92 @@
 import {
-  Deserialize,
-  Describe,
   TransformUtils,
   Operation,
+  Describe,
+  Deserialize,
+  Consumer,
 } from "../internal";
 
 export class Command {
+  public static setOperationsCommand(operations: Array<Operation>, command: Command): Array<Operation> {
+    let length = operations.length;
+    for (let i = 0; i < length; i++) {
+      Command.setOperationCommand(operations[i], command);
+    }
+    return operations;
+  }
+
+  public static setOperationCommand(operation: Operation, command: Command): Operation {
+    operation.setCommand(command);
+    return operation;
+  }
+
   protected _describe: string = 'Command';
   protected _groupId: string;
-  protected _operation: Array<Operation>;
+  protected _operations: Array<Operation>;
 
   public constructor()
-  public constructor(serialize: Command.Serialize)
   public constructor(groupId: string)
+  public constructor(serialize: Command.Serialize)
+  public constructor(command: Command)
   public constructor(groupId: string, operation: Array<Operation>)
   public constructor(...parameter: any) {
     if (TransformUtils.hasLength(parameter, 0)) {
       this._groupId = 'default';
-      this._operation = [];
+      this._operations = [];
       return;
     }
     if (TransformUtils.hasLength(parameter, 1)) {
       if (TransformUtils.isString(parameter[0])) {
         const groupId = parameter[0];
-        this._operation = [];
+        this._operations = [];
         this._groupId = groupId;
         return;
       }
+      if (TransformUtils.isAssignableFrom(parameter[0], Command)) {
+        const command = parameter[0];
+        const clone = command.clone();
+        this._describe = clone._describe;
+        this._groupId = clone._groupId;
+        this._operations = [];
+        this.addOperations(clone._operations);
+        return;
+      }
       if (TransformUtils.isObject(parameter[0])) {
-        const serialize = <Command.Serialize>parameter[0];
+        const serialize = <Command.Serialize> parameter[0];
+        const operations = Deserialize.getInstance().formArray(serialize.operations);
         this._groupId = serialize.groupId;
-        this._operation = Deserialize.getInstance().formArray(serialize.operation);
+        this._operations = [];
+        this.addOperations(operations);
         return;
       }
     }
     if (TransformUtils.hasLength(parameter, 2)) {
       const groupId = parameter[0];
-      const operation = parameter[1];
+      const operations = parameter[1];
       this._groupId = groupId;
-      this._operation = operation;
+      this._operations = [];
+      this.addOperations(operations);
     }
   }
 
   public concat(target: Command): Command {
-    const result = new Command({
-      groupId: this._groupId,
-      describe: this._describe,
-      operation: [],
-    });
-    result._operation = this._operation.concat(target._operation);
+    const result = new Command();
+    result.setGroupId(this.getGroupId());
+    result.addOperations(this.getOperations());
+    result.addOperations(target.getOperations());
     return result;
   }
 
+  public forEach(consumer: Consumer<Operation>): void {
+    let operations = this._operations;
+    let loopLimit = operations.length;
+    for (let i = 0; i < loopLimit; i++) {
+      consumer(operations[i]);
+    }
+  }
+
   public apply(resource: unknown): void {
-    const operations = this._operation;
+    const operations = this._operations;
     const sizeof = operations.length;
     for (let i = 0; i < sizeof; i++) {
       const action = operations[i];
@@ -61,30 +94,96 @@ export class Command {
     }
   }
 
+  public addOperations(operations: Operation[]): void {
+    Command.setOperationsCommand(operations, this);
+    this._operations = this._operations.concat(operations);
+  }
+
+  public addOperation(operation: Operation): void {
+    Command.setOperationCommand(operation, this);
+    this._operations.push(operation);
+  }
+
+  public clearOperations(): void {
+    Command.setOperationsCommand(this._operations, null);
+    this._operations = [];
+  }
+
+  public removeOperation(index: number): void {
+    Command.setOperationsCommand(this._operations.splice(index, 1), null);
+  }
+
+  public replaceOperations(index: number, operations: Operation[])
+  public replaceOperations(operation: Operation, operations: Operation[])
+  public replaceOperations(...parameters: any) {
+    if (TransformUtils.hasLength(parameters, 2)) {
+      if (TransformUtils.isNumber(parameters[0])) {
+        const index = parameters[0];
+        const operations = parameters[1];
+        Command.setOperationsCommand(operations, this);
+        this.removeOperation(index);
+        this._operations.splice(index, 0, ...operations);
+        return;
+      }
+      if (TransformUtils.isAssignableFrom(parameters[0], Operation)) {
+        const operation = parameters[0];
+        const operations = parameters[1];
+        const index = this._operations.indexOf(operation);
+        if (index > -1) {
+          Command.setOperationsCommand(operations, this);
+          this.removeOperation(index);
+          this._operations.splice(index, 0, ...operations);
+        }
+      }
+    }
+  }
+
+  public setGroupId(groupId: string): void {
+    this._groupId = groupId;
+  }
+
   public getOperations(): Array<Operation> {
-    return this._operation;
+    return this._operations;
   }
 
   public getGroupId(): string {
     return this._groupId;
   }
 
-  public addOperation(operation: Operation): void {
-    this._operation.push(operation);
+  public getOperationSize(): number {
+    return this._operations.length;
+  }
+
+  public getOperation(index: number): Operation {
+    return this._operations[index];
+  }
+
+  public clone(): Command {
+    const clone = new Command();
+    const describe = this._describe;
+    const groupId = this._groupId;
+    const operations = this._operations.map((o) => o.clone());
+    clone._groupId = groupId;
+    clone._describe = describe;
+    clone._operations = Command.setOperationsCommand(operations, clone);
+    return clone;
   }
 
   public toJSON(): Command.Serialize {
+    const describe = this._describe;
+    const groupId = this._groupId;
+    const operations = this._operations.map((o) => o.toJSON());
     return {
-      describe: this._describe,
-      groupId: this._groupId,
-      operation: this._operation.map((o) => o.toJSON()),
+      describe,
+      groupId,
+      operations,
     };
   }
 }
 
 export namespace Command {
   export interface Serialize extends Describe {
-    operation: Array<Operation.Serialize>,
-    groupId: string
+    groupId: string,
+    operations: Array<Operation.Serialize>,
   }
 }

+ 14 - 18
packages/ot-core/src/basics/Conflict.ts

@@ -1,24 +1,22 @@
 import {
-  BiConsumer,
-  Operation,
-  Class,
   TransformUtils,
+  Operation,
   BiFunction,
-  Command,
+  Class,
 } from "../internal";
 
 export class Conflict {
-  protected _conflict: Array<Conflict.Wrapped>;
+  protected _conflict: Array<Conflict.Wrapped<Operation, Operation>>;
 
   public constructor() {
-    this._conflict = new Array<Conflict.Wrapped>();
+    this._conflict = new Array<Conflict.Wrapped<Operation, Operation>>();
   }
 
-  public get(targetOperation: Operation, savedOperation: Operation): Conflict.Wrapped {
-    const _conflict = this._conflict;
-    const sizeof = _conflict.length;
+  public get(targetOperation: Operation, savedOperation: Operation): Conflict.Wrapped<Operation, Operation> {
+    const conflict = this._conflict;
+    const sizeof = conflict.length;
     for (let i = 0; i < sizeof; i++) {
-      const wrapped = _conflict[i];
+      const wrapped = conflict[i];
       const targetFrom = TransformUtils.isAssignableFrom(targetOperation, wrapped.targetClass);
       const savedFrom = TransformUtils.isAssignableFrom(savedOperation, wrapped.savedClass);
       if (targetFrom && savedFrom) {
@@ -28,9 +26,9 @@ export class Conflict {
     return null;
   }
 
-  public add<T extends Operation, S extends Operation >(wrapped: Conflict.Wrapped<T, S>): void
-  public add<T extends Operation, S extends Operation >(targetClass: Class<T>, savedClass: Class<S>, conflict: Conflict.Process<Command, Command, T, S>): void
-  public add<T extends Operation, S extends Operation >(...parameter: any): void {
+  public add<T extends Operation, S extends Operation>(wrapped: Conflict.Wrapped<T, S>): void
+  public add<T extends Operation, S extends Operation>(targetClass: Class<T>, savedClass: Class<S>, conflict: Conflict.Process<T, S>): void
+  public add<T extends Operation, S extends Operation>(...parameter: any): void {
     if (TransformUtils.hasLength(parameter, 1)) {
       const wrapped = parameter[0];
       this._conflict.push(wrapped);
@@ -46,13 +44,11 @@ export class Conflict {
 }
 
 export namespace Conflict {
-  export type Transfer<TO extends Operation, SO extends Operation> = BiConsumer<TO, SO>;
-
-  export type Process<TC extends Command, SC extends Command, TO extends Operation, SO extends Operation > = BiFunction<TC, SC, Transfer<TO, SO>>;
+  export type Process<T extends Operation, S extends Operation > = BiFunction<T, S>;
 
-  export interface Wrapped<T extends Operation = Operation, S extends Operation = Operation> {
+  export interface Wrapped<T extends Operation, S extends Operation> {
     targetClass: Class<T>;
     savedClass: Class<S>;
-    process: Process<Command, Command, T, S>;
+    process: Process<T, S>;
   }
 }

+ 23 - 5
packages/ot-core/src/basics/Operation.ts

@@ -1,15 +1,33 @@
 import {
   Describe,
+  Command,
 } from "../internal";
 
-export interface Operation {
-  apply(resource: unknown): void
+export abstract class Operation<T = unknown> {
+  protected _command: Command;
 
-  clone(): Operation;
+  abstract apply(resource: T): void;
 
-  toJSON(): Operation.Serialize;
+  abstract clone(): Operation;
+
+  abstract toJSON(): Operation.Serialize;
+
+  public getCommand(): Command {
+    return this._command;
+  }
+
+  public setCommand(command: Command): void {
+    this._command = command;
+  }
+
+  public replaceOperations(...operations: Operation[]) {
+    const command = this._command;
+    if (command) {
+      command.replaceOperations(this, operations);
+    }
+  }
 }
 
 export namespace Operation {
-  export interface Serialize extends Describe{}
+  export interface Serialize extends Describe {}
 }

+ 8 - 1
packages/ot-core/src/basics/SwapData.ts

@@ -1,4 +1,7 @@
-import { Describe, TransformUtils } from "../internal";
+import {
+  TransformUtils,
+  Describe,
+} from "../internal";
 
 export class SwapData<T = unknown> {
   protected _describe: string = 'SwapData';
@@ -24,6 +27,10 @@ export class SwapData<T = unknown> {
     return this._data;
   }
 
+  public hasData(): boolean {
+    return this._data != null;
+  }
+
   public clone(): SwapData<T> {
     return this;
   }

+ 16 - 35
packages/ot-core/src/basics/Transform.ts

@@ -1,49 +1,30 @@
 import {
-  Command,
   Conflict,
+  Command,
+  Operation,
 } from "../internal";
 
 export class Transform {
   protected _conflict: Conflict;
 
-  public constructor(_conflict: Conflict) {
-    this._conflict = _conflict;
-  }
-
-  public transfer(targetCommand: Command, savedCommand: Command): Array<Command> {
+  protected _process(targetCommand: Command, savedCommand: Command): Array<Command> {
     const conflict = this._conflict;
-
-    const targetOperations = targetCommand.getOperations();
-    const savedOperations = savedCommand.getOperations();
-
-    const sizeof1 = targetOperations.length;
-    const sizeof2 = savedOperations.length;
-
-    const newTargetCommand = new Command({
-      groupId: targetCommand.getGroupId(),
-      operation: [],
-      describe: 'Command',
-    });
-    const newSavedCommand = new Command({
-      groupId: targetCommand.getGroupId(),
-      operation: [],
-      describe: 'Command',
-    });
-
-    for (let i = 0; i < sizeof1; i++) {
-      const targetOperation = targetOperations[i];
-      for (let j = 0; j < sizeof2; j++) {
-        const savedOperation = savedOperations[j];
+    targetCommand.forEach((targetOperation: Operation) => {
+      savedCommand.forEach((savedOperation: Operation) => {
         const wrapped = conflict.get(targetOperation, savedOperation);
         if (wrapped != null) {
-          const transfer = wrapped.process(newTargetCommand, newSavedCommand);
-          transfer(targetOperation.clone(), savedOperation.clone());
-          continue;
+          wrapped.process(targetOperation, savedOperation);
         }
-        throw new Error('not found _conflict process');
-      }
-    }
+      });
+    });
+    return [targetCommand, savedCommand];
+  }
+
+  public constructor(conflict: Conflict) {
+    this._conflict = conflict;
+  }
 
-    return [newTargetCommand, newSavedCommand];
+  public transfer(targetCommand: Command, savedCommand: Command): Array<Command> {
+    return this._process(targetCommand.clone(), savedCommand.clone());
   }
 }

+ 40 - 35
packages/ot-core/src/client/ClientSocket.ts

@@ -1,82 +1,87 @@
 import {
-  Socket,
-  io,
+  Socket, io,
 } from "socket.io-client";
 
 import {
+  ClientSocketObserver,
+  Client,
   UndoManager,
   Command,
-  User,
-  Client,
-  ClientSocketConfig,
   Response,
   Deserialize,
-  Execute,
+  ClientSocketConfig,
 } from "../internal";
 
 export class ClientSocket extends Client {
   protected _source: unknown;
   protected _socket: Socket;
   protected _roomId: string;
-  protected _users: Array<User>;
+  protected _observer = new ClientSocketObserver();
   protected _config: ClientSocketConfig;
-  protected _execute: Execute;
 
-  protected _initializeListener(): void {
-    const observer = this._undoManager.getObserver();
-    observer.addOnAddRedoListener({
-      onAddRedo: (wrapped: UndoManager.Wrapped): void => {
+  protected _initialize(): void {
+    this._undoManager.getObserver().addPerformCommandListener({
+      onPerformCommand: (wrapped: UndoManager.Wrapped): void => {
+        wrapped.redo.apply(this._source);
+        this.applyClient(wrapped.redo);
+      },
+    });
+    this._undoManager.getObserver().addPerformRedoListener({
+      onPerformRedo: (wrapped: UndoManager.Wrapped): void => {
         wrapped.redo.apply(this._source);
         this.applyClient(wrapped.redo);
       },
     });
-    observer.addOnAddUndoListener({
-      onAddUndo: (wrapped: UndoManager.Wrapped): void => {
+    this._undoManager.getObserver().addPerformUndoListener({
+      onPerformUndo: (wrapped: UndoManager.Wrapped): void => {
         wrapped.undo.apply(this._source);
         this.applyClient(wrapped.undo);
       },
     });
-  }
 
-  protected _initializeSocket(): void {
-    this._socket.on('open', (response: Response) => {
-      this._revision = response.revision;
-      this._users = response.users;
-      this._source = response.source;
-      this._roomId = response.roomId;
-      this._execute();
-    });
-    this._socket.on('ack', () => {
-      this.serverAck();
-    });
     this._socket.on('command', (clientId: number, command: Command.Serialize) => {
       const deserialize = Deserialize.getInstance().formObject<Command>(command);
       this.applyServer(deserialize);
+      this._observer.dispatchOnCommand();
+    });
+    this._socket.on('ack', () => {
+      this.serverAck();
+      this._observer.dispatchOnAck();
     });
     this._socket.on('reconnect', () => {
       this.serverReconnect();
+      this._observer.dispatchOnReconnect();
+    });
+    this._socket.on('connected', (response: Response) => {
+      this._revision = response.revision;
+      this._source = response.source;
+      this._roomId = response.roomId;
+      this._observer.dispatchOnConnected();
     });
   }
 
   public constructor(config: ClientSocketConfig) {
     super(config.conflict);
-    this._config = config;
     this._socket = io(config.url);
-    this._execute = () => {};
-    this._initializeListener();
-    this._initializeSocket();
+    this._config = config;
+    this._observer = new ClientSocketObserver();
+    this._initialize();
   }
 
-  public ready(execute: Execute): void {
-    this._execute = execute;
+  public applyCommand(command: Command): void {
+    command.apply(this.getSource());
+    this._undoManager.transform(command);
   }
 
   public sendCommand(revision: number, command: Command): void {
     this._socket.emit('command', revision, command.toJSON());
   }
 
-  public applyCommand(command: Command): void {
-    command.apply(this._source);
-    this._undoManager.transform(command);
+  public getSource<T>(): T {
+    return <T> this._source;
+  }
+
+  public getObserver(): ClientSocketObserver {
+    return this._observer;
   }
 }

+ 123 - 0
packages/ot-core/src/client/ClientSocketObserver.ts

@@ -0,0 +1,123 @@
+import { ArrayList } from "../library/ArrayList";
+
+export class ClientSocketObserver {
+  protected _onCommandListeners: ArrayList<ClientSocketObserver.OnCommandListener>;
+  protected _onAckListeners: ArrayList<ClientSocketObserver.OnAckListener>;
+  protected _onReconnectListeners: ArrayList<ClientSocketObserver.OnReconnectListener>;
+  protected _onConnectedListeners: ArrayList<ClientSocketObserver.OnConnectedListener>;
+
+  public addOnCommandListener(listener: ClientSocketObserver.OnCommandListener) {
+    if (this._onCommandListeners == null) {
+      this._onCommandListeners = new ArrayList<ClientSocketObserver.OnCommandListener>();
+    }
+    this._onCommandListeners.add(listener);
+  }
+
+  public addOnAckListener(listener: ClientSocketObserver.OnAckListener) {
+    if (this._onAckListeners == null) {
+      this._onAckListeners = new ArrayList<ClientSocketObserver.OnAckListener>();
+    }
+    this._onAckListeners.add(listener);
+  }
+
+  public addOnReconnectListener(listener: ClientSocketObserver.OnReconnectListener) {
+    if (this._onReconnectListeners == null) {
+      this._onReconnectListeners = new ArrayList<ClientSocketObserver.OnReconnectListener>();
+    }
+    this._onReconnectListeners.add(listener);
+  }
+
+  public addOnConnectedListener(listener: ClientSocketObserver.OnConnectedListener) {
+    if (this._onConnectedListeners == null) {
+      this._onConnectedListeners = new ArrayList<ClientSocketObserver.OnConnectedListener>();
+    }
+    this._onConnectedListeners.add(listener);
+  }
+
+  public removeOnCommandListener(listener: ClientSocketObserver.OnCommandListener) {
+    if (this._onCommandListeners == null) {
+      return;
+    }
+    this._onCommandListeners.remove(listener);
+  }
+
+  public removeOnAckListener(listener: ClientSocketObserver.OnAckListener) {
+    if (this._onAckListeners == null) {
+      return;
+    }
+    this._onAckListeners.remove(listener);
+  }
+
+  public removeOnReconnectListener(listener: ClientSocketObserver.OnReconnectListener) {
+    if (this._onReconnectListeners == null) {
+      return;
+    }
+    this._onReconnectListeners.remove(listener);
+  }
+
+  public removeOnConnectedListener(listener: ClientSocketObserver.OnConnectedListener) {
+    if (this._onConnectedListeners == null) {
+      return;
+    }
+    this._onConnectedListeners.remove(listener);
+  }
+
+  public dispatchOnCommand(): void {
+    let listeners = this._onCommandListeners;
+    if (listeners != null) {
+      const sizeof = listeners.sizeof();
+      for (let i = 0; i < sizeof; i++) {
+        const listener = listeners.get(i);
+        listener.onCommand();
+      }
+    }
+  }
+
+  public dispatchOnAck(): void {
+    let listeners = this._onAckListeners;
+    if (listeners != null) {
+      const sizeof = listeners.sizeof();
+      for (let i = 0; i < sizeof; i++) {
+        const listener = listeners.get(i);
+        listener.onAck();
+      }
+    }
+  }
+
+  public dispatchOnReconnect(): void {
+    let listeners = this._onReconnectListeners;
+    if (listeners != null) {
+      const sizeof = listeners.sizeof();
+      for (let i = 0; i < sizeof; i++) {
+        const listener = listeners.get(i);
+        listener.onReconnect();
+      }
+    }
+  }
+
+  public dispatchOnConnected(): void {
+    let listeners = this._onConnectedListeners;
+    if (listeners != null) {
+      const sizeof = listeners.sizeof();
+      for (let i = 0; i < sizeof; i++) {
+        const listener = listeners.get(i);
+        listener.onConnected();
+      }
+    }
+  }
+}
+
+export namespace ClientSocketObserver {
+  export interface OnCommandListener {
+    onCommand(): void
+  }
+  export interface OnAckListener {
+    onAck(): void
+  }
+  export interface OnReconnectListener {
+    onReconnect(): void
+  }
+  export interface OnConnectedListener {
+    onConnected(): void
+  }
+}

+ 23 - 24
packages/ot-core/src/client/UndoManager.ts

@@ -12,11 +12,12 @@ export class UndoManager {
   protected _observer: UndoManagerObserver;
 
   protected _transformStack(stack: Array<UndoManager.Wrapped>, command: Command): void {
+    debugger;
     const transform = this._transform;
-    const sizeof = stack.length - 1;
+    const sizeof = stack.length;
     let redoCommand = command;
     let undoCommand = command;
-    for (let i = sizeof; i >= 0; i--) {
+    for (let i = 0; i < sizeof; i++) {
       const pair1 = transform.transfer(stack[i].redo, redoCommand);
       const pair2 = transform.transfer(stack[i].undo, undoCommand);
       stack[i].redo = pair1[0];
@@ -39,11 +40,11 @@ export class UndoManager {
   }
 
   public canUndo(): boolean {
-    return this._undoStack.length !== 0;
+    return this._redoStack.length !== 0;
   }
 
   public canRedo(): boolean {
-    return this._redoStack.length !== 0;
+    return this._undoStack.length !== 0;
   }
 
   public addRedo(redo: Command, undo: Command): void
@@ -52,7 +53,6 @@ export class UndoManager {
     if (TransformUtils.hasLength(parameter, 1)) {
       const wrapped = parameter[0];
       this._redoStack.push(wrapped);
-      this._observer.dispatchOnAddRedo(wrapped);
       return;
     }
     if (TransformUtils.hasLength(parameter, 2)) {
@@ -60,7 +60,6 @@ export class UndoManager {
       const undo = parameter[1];
       const wrapped = { redo, undo };
       this._redoStack.push(wrapped);
-      this._observer.dispatchOnAddRedo(wrapped);
     }
   }
 
@@ -70,7 +69,6 @@ export class UndoManager {
     if (TransformUtils.hasLength(parameter, 1)) {
       const wrapped = parameter[0];
       this._undoStack.push(wrapped);
-      this._observer.dispatchOnAddUndo(wrapped);
       return;
     }
     if (TransformUtils.hasLength(parameter, 2)) {
@@ -78,7 +76,22 @@ export class UndoManager {
       const undo = parameter[1];
       const wrapped = { redo, undo };
       this._undoStack.push(wrapped);
-      this._observer.dispatchOnAddUndo(wrapped);
+    }
+  }
+
+  public performRedo(): void {
+    if (this.canRedo()) {
+      const wrapped = this._undoStack.pop();
+      this.addRedo(wrapped);
+      this._observer.dispatchPerformRedo(wrapped);
+    }
+  }
+
+  public performUndo(): void {
+    if (this.canUndo()) {
+      const wrapped = this._redoStack.pop();
+      this.addUndo(wrapped);
+      this._observer.dispatchPerformUndo(wrapped);
     }
   }
 
@@ -88,6 +101,7 @@ export class UndoManager {
     if (TransformUtils.hasLength(parameter, 1)) {
       const wrapped = parameter[0];
       this.addRedo(wrapped);
+      this._observer.dispatchPerformCommand(wrapped);
       return;
     }
     if (TransformUtils.hasLength(parameter, 2)) {
@@ -95,22 +109,7 @@ export class UndoManager {
       const undo = parameter[1];
       const wrapped = { redo, undo };
       this.addRedo(wrapped);
-    }
-  }
-
-  public performRedo(): void {
-    if (this.canRedo()) {
-      const wrapped = this._redoStack.pop();
-      this.addUndo(wrapped);
-      this._observer.dispatchOnRedo(wrapped);
-    }
-  }
-
-  public performUndo(): void {
-    if (this.canUndo()) {
-      const wrapped = this._undoStack.pop();
-      this.addRedo(wrapped);
-      this._observer.dispatchOnUndo(wrapped);
+      this._observer.dispatchPerformCommand(wrapped);
     }
   }
 

+ 56 - 86
packages/ot-core/src/client/UndoManagerObserver.ts

@@ -4,126 +4,96 @@ import {
 } from "../internal";
 
 export class UndoManagerObserver {
-  protected _onAddRedoListeners: ArrayList<UndoManagerObserver.OnAddRedoListener>;
-  protected _onAddUndoListeners: ArrayList<UndoManagerObserver.OnAddUndoListener>;
-  protected _onRedoListeners: ArrayList<UndoManagerObserver.OnRedoListener>;
-  protected _onUndoListeners: ArrayList<UndoManagerObserver.OnUndoListener>;
+  protected _onPerformRedoListeners: ArrayList<UndoManagerObserver.OnPerformRedoListener>;
+  protected _onPerformUndoListeners: ArrayList<UndoManagerObserver.OnPerformUndoListener>;
+  protected _onPerformCommandListeners: ArrayList<UndoManagerObserver.OnPerformCommandListener>;
 
-  public dispatchOnAddRedo(wrapped: UndoManager.Wrapped): void {
-    let listeners = this._onAddRedoListeners;
-    if (listeners != null) {
-      const sizeof = listeners.sizeof();
-      for (let i = 0; i < sizeof; i++) {
-        const listener = listeners.get(i);
-        listener.onAddRedo(wrapped);
-      }
-    }
-  }
-
-  public dispatchOnAddUndo(wrapped: UndoManager.Wrapped): void {
-    let listeners = this._onAddUndoListeners;
-    if (listeners != null) {
-      const sizeof = listeners.sizeof();
-      for (let i = 0; i < sizeof; i++) {
-        const listener = listeners.get(i);
-        listener.onAddUndo(wrapped);
-      }
+  public addPerformRedoListener(listener: UndoManagerObserver.OnPerformRedoListener): void {
+    if (this._onPerformRedoListeners == null) {
+      this._onPerformRedoListeners = new ArrayList<UndoManagerObserver.OnPerformRedoListener>();
     }
+    this._onPerformRedoListeners.add(listener);
   }
 
-  public dispatchOnRedo(wrapped: UndoManager.Wrapped): void {
-    let listeners = this._onRedoListeners;
-    if (listeners != null) {
-      const sizeof = listeners.sizeof();
-      for (let i = 0; i < sizeof; i++) {
-        const listener = listeners.get(i);
-        listener.onRedo(wrapped);
-      }
-    }
-  }
-
-  public dispatchOnUndo(wrapped: UndoManager.Wrapped): void {
-    let listeners = this._onUndoListeners;
-    if (listeners != null) {
-      const sizeof = listeners.sizeof();
-      for (let i = 0; i < sizeof; i++) {
-        const listener = listeners.get(i);
-        listener.onUndo(wrapped);
-      }
+  public addPerformUndoListener(listener: UndoManagerObserver.OnPerformUndoListener): void {
+    if (this._onPerformUndoListeners == null) {
+      this._onPerformUndoListeners = new ArrayList<UndoManagerObserver.OnPerformUndoListener>();
     }
+    this._onPerformUndoListeners.add(listener);
   }
 
-  public addOnAddRedoListener(listener: UndoManagerObserver.OnAddRedoListener): void {
-    if (this._onAddRedoListeners == null) {
-      this._onAddRedoListeners = new ArrayList<UndoManagerObserver.OnAddRedoListener>();
+  public addPerformCommandListener(listener: UndoManagerObserver.OnPerformCommandListener): void {
+    if (this._onPerformCommandListeners == null) {
+      this._onPerformCommandListeners = new ArrayList<UndoManagerObserver.OnPerformCommandListener>();
     }
-    this._onAddRedoListeners.add(listener);
+    this._onPerformCommandListeners.add(listener);
   }
 
-  public addOnAddUndoListener(listener: UndoManagerObserver.OnAddUndoListener): void {
-    if (this._onAddUndoListeners == null) {
-      this._onAddUndoListeners = new ArrayList<UndoManagerObserver.OnAddUndoListener>();
-    }
-    this._onAddUndoListeners.add(listener);
-  }
-
-  public addOnRedoListener(listener: UndoManagerObserver.OnRedoListener): void {
-    if (this._onRedoListeners == null) {
-      this._onRedoListeners = new ArrayList<UndoManagerObserver.OnRedoListener>();
+  public removePerformRedoListener(listener: UndoManagerObserver.OnPerformRedoListener): void {
+    if (this._onPerformRedoListeners == null) {
+      return;
     }
-    this._onRedoListeners.add(listener);
+    this._onPerformRedoListeners.remove(listener);
   }
 
-  public addOnUndoListener(listener: UndoManagerObserver.OnUndoListener): void {
-    if (this._onUndoListeners == null) {
-      this._onUndoListeners = new ArrayList<UndoManagerObserver.OnUndoListener>();
+  public removePerformUndoListener(listener: UndoManagerObserver.OnPerformUndoListener): void {
+    if (this._onPerformUndoListeners == null) {
+      return;
     }
-    this._onUndoListeners.add(listener);
+    this._onPerformUndoListeners.remove(listener);
   }
 
-  public removeOnAddRedoListener(listener: UndoManagerObserver.OnAddRedoListener): void {
-    if (this._onAddRedoListeners == null) {
+  public removePerformCommandListener(listener: UndoManagerObserver.OnPerformCommandListener): void {
+    if (this._onPerformCommandListeners == null) {
       return;
     }
-    this._onAddRedoListeners.remove(listener);
+    this._onPerformCommandListeners.remove(listener);
   }
 
-  public removeOnAddUndoListener(listener: UndoManagerObserver.OnAddUndoListener): void {
-    if (this._onAddUndoListeners == null) {
-      return;
+  public dispatchPerformRedo(wrapped: UndoManager.Wrapped): void {
+    let listeners = this._onPerformRedoListeners;
+    if (listeners != null) {
+      const sizeof = listeners.sizeof();
+      for (let i = 0; i < sizeof; i++) {
+        const listener = listeners.get(i);
+        listener.onPerformRedo(wrapped);
+      }
     }
-    this._onAddUndoListeners.remove(listener);
   }
 
-  public removeOnUndoListener(listener: UndoManagerObserver.OnUndoListener): void {
-    if (this._onUndoListeners == null) {
-      return;
+  public dispatchPerformUndo(wrapped: UndoManager.Wrapped): void {
+    let listeners = this._onPerformUndoListeners;
+    if (listeners != null) {
+      const sizeof = listeners.sizeof();
+      for (let i = 0; i < sizeof; i++) {
+        const listener = listeners.get(i);
+        listener.onPerformUndo(wrapped);
+      }
     }
-    this._onUndoListeners.remove(listener);
   }
 
-  public removeOnRedoListener(listener: UndoManagerObserver.OnRedoListener): void {
-    if (this._onRedoListeners == null) {
-      return;
+  public dispatchPerformCommand(wrapped: UndoManager.Wrapped): void {
+    let listeners = this._onPerformCommandListeners;
+    if (listeners != null) {
+      const sizeof = listeners.sizeof();
+      for (let i = 0; i < sizeof; i++) {
+        const listener = listeners.get(i);
+        listener.onPerformCommand(wrapped);
+      }
     }
-    this._onRedoListeners.remove(listener);
   }
 }
 
 export namespace UndoManagerObserver {
-  export interface OnAddRedoListener {
-    onAddRedo(wrapped: UndoManager.Wrapped): void
-  }
-
-  export interface OnAddUndoListener {
-    onAddUndo(wrapped: UndoManager.Wrapped): void
+  export interface OnPerformRedoListener {
+    onPerformRedo(wrapped: UndoManager.Wrapped): void
   }
 
-  export interface OnRedoListener {
-    onRedo(wrapped: UndoManager.Wrapped): void
+  export interface OnPerformUndoListener {
+    onPerformUndo(wrapped: UndoManager.Wrapped): void
   }
 
-  export interface OnUndoListener {
-    onUndo(wrapped: UndoManager.Wrapped): void
+  export interface OnPerformCommandListener {
+    onPerformCommand(wrapped: UndoManager.Wrapped): void
   }
 }

+ 3 - 1
packages/ot-core/src/config/ClientSocketConfig.ts

@@ -1,4 +1,6 @@
-import { Conflict } from "../internal";
+import {
+  Conflict,
+} from "../internal";
 
 export class ClientSocketConfig {
   url: string;

+ 2 - 0
packages/ot-core/src/internal.ts

@@ -14,6 +14,7 @@ export * from './config/ClientSocketConfig';
 // ================================================= library
 
 export * from './library/ArrayList';
+export * from './library/Random';
 
 // ================================================= basics
 
@@ -33,6 +34,7 @@ export * from './client/AwaitingWithBuffer';
 export * from './client/UndoManagerObserver';
 export * from './client/Client';
 export * from './client/ClientSocket';
+export * from './client/ClientSocketObserver';
 
 // ================================================= server
 

+ 26 - 0
packages/ot-core/src/library/Random.ts

@@ -0,0 +1,26 @@
+import { TransformUtils } from "../utils/TransformUtils";
+
+export class Random {
+  protected _max: number;
+  protected _min: number;
+
+  constructor()
+  constructor(max: number, min: number)
+  constructor(...parameter: any) {
+    if (TransformUtils.hasLength(parameter, 2)) {
+      this._max = parameter[0];
+      this._min = parameter[1];
+      return;
+    }
+    this._max = 1;
+    this._min = 0;
+  }
+
+  public next(): number {
+    return Math.random() * (this._max - this._min) + this._min;
+  }
+
+  public nextInt(): number {
+    return Math.trunc(this.next());
+  }
+}

+ 9 - 3
packages/ot-core/src/server/Server.ts

@@ -14,16 +14,22 @@ export class Server {
   }
 
   public receiveCommand(revision: number, command: Command): void {
-    if (revision < 0 || this._chronicle.length < revision) {
+    const chronicle = this._chronicle;
+    const transform = this._transform;
+
+    if (revision < 0 || chronicle.length < revision) {
       throw new Error("command revision not in history");
     }
-    let history = this._chronicle.slice(revision);
+
+    let history = chronicle.slice(revision);
+
     for (let i = 0; i < history.length; i++) {
       const saved = history[i];
       if (command.getGroupId() === saved.getGroupId()) {
-        command = this._transform.transfer(command, saved)[0];
+        command = transform.transfer(command, saved)[0];
       }
     }
+
     this._chronicle.push(command);
   }
 

+ 25 - 16
packages/ot-core/src/server/ServerSocket.ts

@@ -4,11 +4,11 @@ import {
 
 import {
   Command,
-  TransformUtils,
   User,
   Deserialize,
   Server,
   Conflict,
+  TransformUtils,
 } from "../internal";
 
 export class ServerSocket extends Server {
@@ -22,8 +22,8 @@ export class ServerSocket extends Server {
     if (TransformUtils.hasLength(parameter, 1)) {
       const conflict = parameter[0];
       super(conflict);
-      this._roomId = 'empty';
       this._users = [];
+      this._roomId = 'empty';
       this._source = null;
       return;
     }
@@ -31,18 +31,25 @@ export class ServerSocket extends Server {
       const conflict = parameter[0];
       const source = parameter[1];
       super(conflict);
-      this._roomId = 'empty';
       this._users = [];
+      this._roomId = 'empty';
       this._source = source;
     }
   }
 
-  public receiveCommand(revision: number, command: Command) {
-    super.receiveCommand(revision, command);
-    command.apply(this._source);
+  public onCommand(client: Socket, revision: number, command: Command.Serialize): void {
+    const deserialize = Deserialize.getInstance().formObject(command);
+    const userId = client.id;
+    this.receiveCommand(revision, deserialize);
+    client.emit('ack');
+    client.broadcast.in(this._roomId).emit('command', userId, deserialize.toJSON());
+  }
+
+  public onDisconnect(client: Socket): void {
+    console.log('Disconnect');
   }
 
-  public addSocketClient(client: Socket): void {
+  public addClient(client: Socket): void {
     client.join(this._roomId);
     client.on('command', (revision: number, command: Command.Serialize) => {
       this.onCommand(client, revision, command);
@@ -50,23 +57,25 @@ export class ServerSocket extends Server {
     client.on('disconnect', () => {
       this.onDisconnect(client);
     });
-    client.emit('open', {
+    client.emit('connected', {
       revision: this.getRevision(),
       clients: this._users,
       roomId: this._roomId,
       source: this._source,
     });
+    this.addUsers(client);
   }
 
-  public onCommand(client: Socket, revision: number, command: Command.Serialize): void {
-    const deserialize = Deserialize.getInstance().formObject(command);
-    let clientId = client.id;
-    this.receiveCommand(revision, deserialize);
-    client.emit('ack');
-    client.broadcast.in(this._roomId).emit('command', clientId, deserialize.toJSON());
+  public addUsers(client: Socket): void {
+    this._users.push({ id: client.id });
   }
 
-  public onDisconnect(client: Socket): void {
-    console.log('Disconnect');
+  public receiveCommand(revision: number, command: Command) {
+    super.receiveCommand(revision, command);
+    command.apply(this._source);
+  }
+
+  public getSource<T>(): T {
+    return <T> this._source;
   }
 }

+ 1 - 1
packages/ot-core/src/server/User.ts

@@ -1,3 +1,3 @@
 export class User {
-
+  id: string;
 }

+ 161 - 42
packages/ot-demo/src/basics/CreateConflict.ts

@@ -1,79 +1,198 @@
 import {
-  Conflict,
   SwapData,
+  Conflict,
 } from "ot-core";
 
 import {
+  RemoveRow,
   InsertRow,
 } from "../internal";
 
 export function CreateConflict(): Conflict {
   const conflict = new Conflict();
-  conflict.add(InsertRow, InsertRow, (targetCommand, savedCommand) => (targetOperation, savedOperation) => {
-    /**
-         * 服务端插入的行在当前行的下方
-         */
+  conflict.add(InsertRow, InsertRow, (targetOperation, savedOperation) => {
+    // 服务端插入的行在当前行的下方
     if (targetOperation.getHeight() <= savedOperation.getIndex()) {
+      // 将服务端插入的数据行向下转移
       const offset = savedOperation.getIndex() + targetOperation.getCount();
       savedOperation.setIndex(savedOperation.getIndex() + offset);
-      targetCommand.addOperation(targetOperation);
-      savedCommand.addOperation(savedOperation);
+      return;
     }
 
-    /**
-         * 服务端插入的行在当前行的上方
-         */
+    // 服务端插入的行在当前行的上方
     if (targetOperation.getIndex() >= savedOperation.getHeight()) {
+      // 将当前插入的数据行向下转移
       const offset = targetOperation.getIndex() + savedOperation.getCount();
       targetOperation.setIndex(targetOperation.getIndex() + offset);
-      targetCommand.addOperation(targetOperation);
-      savedCommand.addOperation(savedOperation);
+      return;
     }
 
-    /**
-         * 服务端插入的行被当前行包含
-         */
+    // 服务端插入的行被当前行包含
     if (targetOperation.getIndex() <= savedOperation.getIndex() && targetOperation.getHeight() >= savedOperation.getHeight()) {
-      const spliceCursor = savedOperation.getIndex() - targetOperation.getIndex();
-      const prevDataBuffer = targetOperation.getBuffer().getData().splice(0, spliceCursor);
-      const nextDataBuffer = targetOperation.getBuffer().getData().splice(spliceCursor);
+      const prevSwap = new SwapData();
+      const nextSwap = new SwapData();
+      const currSwap = targetOperation.getBuffer();
+      // 拆分点
+      const splicePoint = savedOperation.getIndex() - targetOperation.getIndex();
+      // 前操作
+      const prevIndex = targetOperation.getIndex();
+      const prevCount = savedOperation.getIndex() - targetOperation.getIndex();
+      // 后操作
+      const nextIndex = savedOperation.getHeight();
+      const nextCount = targetOperation.getHeight() - savedOperation.getIndex();
+      // 拆分数据
+      if (currSwap.hasData()) {
+        prevSwap.setData(currSwap.getData().slice(0, splicePoint));
+        nextSwap.setData(currSwap.getData().slice(splicePoint));
+      }
+      // 拆分操作
       const prevOperation = new InsertRow({
-        describe: 'InsertRowOperation',
-        buffer: new SwapData({ data: prevDataBuffer, describe: 'DataBuffer' }),
-        index: targetOperation.getIndex(),
-        count: savedOperation.getIndex() - targetOperation.getIndex(),
+        describe: 'InsertRow',
+        buffer: prevSwap,
+        index: prevIndex,
+        count: prevCount,
       });
       const nextOperation = new InsertRow({
-        describe: 'InsertRowOperation',
-        buffer: new SwapData({ data: nextDataBuffer, describe: 'DataBuffer' }),
-        index: savedOperation.getCount(),
-        count: targetOperation.getHeight() - savedOperation.getIndex(),
+        describe: 'InsertRow',
+        buffer: nextSwap,
+        index: nextIndex,
+        count: nextCount,
       });
-      targetCommand.addOperation(prevOperation);
-      targetCommand.addOperation(nextOperation);
+      // 保存命令
+      targetOperation.replaceOperations(prevOperation, nextOperation);
+      return;
     }
 
-    /**
-         * 服务端插入的行包含当前行
-         */
+    // 服务端插入的行包含当前行
     if (targetOperation.getIndex() >= savedOperation.getIndex() && targetOperation.getHeight() <= savedOperation.getHeight()) {
-      // TODO ....
+      // 将当前插入的数据行向下转移
+      const offset = targetOperation.getIndex() - savedOperation.getIndex() + savedOperation.getCount();
+      targetOperation.setIndex(targetOperation.getIndex() + offset);
+      return;
     }
 
-    /**
-         * 服务端插入的行和当前行上交叉
-         */
+    // 服务端插入的行和当前行上交叉
     if (targetOperation.getIndex() > savedOperation.getIndex() && targetOperation.getIndex() < savedOperation.getHeight() && targetOperation.getHeight() >= savedOperation.getHeight()) {
-      // TODO ..
-      // ...
+      // 将当前插入的数据行向下转移
+      const offset = targetOperation.getIndex() - savedOperation.getIndex() + savedOperation.getCount();
+      targetOperation.setIndex(targetOperation.getIndex() + offset);
+      return;
     }
 
-    /**
-         * 服务端插入的行和当前行下交叉
-         */
+    // 服务端插入的行和当前行下交叉
     if (targetOperation.getIndex() <= savedOperation.getIndex() && targetOperation.getHeight() > savedOperation.getIndex() && targetOperation.getHeight() < savedOperation.getHeight()) {
-      // TODO ...
-      //
+      const prevSwap = new SwapData();
+      const nextSwap = new SwapData();
+      const currSwap = targetOperation.getBuffer();
+      // 拆分点
+      const splicePoint = savedOperation.getIndex() - targetOperation.getIndex();
+      // 前操作
+      const prevIndex = targetOperation.getIndex();
+      const prevCount = savedOperation.getIndex() - targetOperation.getIndex();
+      // 后操作
+      const nextIndex = savedOperation.getHeight();
+      const nextCount = targetOperation.getHeight() - savedOperation.getIndex();
+      // 拆分数据
+      if (currSwap.hasData()) {
+        prevSwap.setData(currSwap.getData().slice(0, splicePoint));
+        nextSwap.setData(currSwap.getData().slice(splicePoint));
+      }
+      // 拆分操作
+      const prevOperation = new InsertRow({
+        describe: 'InsertRow',
+        buffer: prevSwap,
+        index: prevIndex,
+        count: prevCount,
+      });
+      const nextOperation = new InsertRow({
+        describe: 'InsertRow',
+        buffer: nextSwap,
+        index: nextIndex,
+        count: nextCount,
+      });
+      // 保存命令
+      targetOperation.replaceOperations(prevOperation, nextOperation);
+    }
+  });
+
+  conflict.add(RemoveRow, InsertRow, (targetOperation, savedOperation) => {
+    // 服务端插入的行在当前行的下方
+    if (targetOperation.getHeight() <= savedOperation.getIndex()) {
+      // 将服务端插入的数据行向下转移
+      const offset = savedOperation.getIndex() + targetOperation.getCount();
+      savedOperation.setIndex(savedOperation.getIndex() + offset);
+      return;
+    }
+
+    // 服务端插入的行在当前行的上方
+    if (targetOperation.getIndex() >= savedOperation.getHeight()) {
+      // 将当前插入的数据行向下转移
+      const offset = targetOperation.getIndex() + savedOperation.getCount();
+      targetOperation.setIndex(targetOperation.getIndex() + offset);
+      return;
+    }
+
+    // 服务端插入的行被当前行包含
+    if (targetOperation.getIndex() <= savedOperation.getIndex() && targetOperation.getHeight() >= savedOperation.getHeight()) {
+      // 前操作
+      const prevIndex = targetOperation.getIndex();
+      const prevCount = savedOperation.getIndex() - targetOperation.getIndex();
+      // 后操作
+      const nextIndex = savedOperation.getHeight() - prevCount;
+      const nextCount = targetOperation.getHeight() - savedOperation.getIndex();
+      // 拆分操作
+      const prevOperation = new RemoveRow({
+        describe: 'RemoveRow',
+        index: prevIndex,
+        count: prevCount,
+      });
+      const nextOperation = new RemoveRow({
+        describe: 'RemoveRow',
+        index: nextIndex,
+        count: nextCount,
+      });
+      // 保存命令
+      targetOperation.replaceOperations(prevOperation, nextOperation);
+      return;
+    }
+
+    // 服务端插入的行包含当前行
+    if (targetOperation.getIndex() >= savedOperation.getIndex() && targetOperation.getHeight() <= savedOperation.getHeight()) {
+      // 将当前插入的数据行向下转移
+      const offset = targetOperation.getIndex() - savedOperation.getIndex() + savedOperation.getCount();
+      targetOperation.setIndex(targetOperation.getIndex() + offset);
+      return;
+    }
+
+    // 服务端插入的行和当前行上交叉
+    if (targetOperation.getIndex() > savedOperation.getIndex() && targetOperation.getIndex() < savedOperation.getHeight() && targetOperation.getHeight() >= savedOperation.getHeight()) {
+      // 将当前插入的数据行向下转移
+      const offset = targetOperation.getIndex() - savedOperation.getIndex() + savedOperation.getCount();
+      targetOperation.setIndex(targetOperation.getIndex() + offset);
+      return;
+    }
+
+    // 服务端插入的行和当前行下交叉
+    if (targetOperation.getIndex() <= savedOperation.getIndex() && targetOperation.getHeight() > savedOperation.getIndex() && targetOperation.getHeight() < savedOperation.getHeight()) {
+      // 前操作
+      const prevIndex = targetOperation.getIndex();
+      const prevCount = savedOperation.getIndex() - targetOperation.getIndex();
+      // 后操作
+      const nextIndex = savedOperation.getHeight() - prevCount;
+      const nextCount = targetOperation.getHeight() - savedOperation.getIndex();
+      // 拆分操作
+      const prevOperation = new RemoveRow({
+        describe: 'RemoveRow',
+        index: prevIndex,
+        count: prevCount,
+      });
+      const nextOperation = new RemoveRow({
+        describe: 'RemoveRow',
+        index: nextIndex,
+        count: nextCount,
+      });
+      // 保存命令
+      targetOperation.replaceOperations(prevOperation, nextOperation);
     }
   });
   return conflict;

+ 79 - 6
packages/ot-demo/src/client.ts

@@ -1,8 +1,81 @@
-export { Command, SwapData } from 'ot-core';
+import {
+  Command,
+  SwapData,
+} from 'ot-core';
 
-export * from './client/Client';
+import {
+  RemoveRow,
+  InsertRow,
+  DemoClient,
+} from './internal';
 
-export * from './operated/InsertColumn';
-export * from './operated/InsertRow';
-export * from './operated/RemoveColumn';
-export * from './operated/RemoveRow';
+const webClient = new DemoClient('127.0.0.1:3000');
+const sheetId = 'sheet1';
+const table = document.getElementById('table');
+const insert = document.getElementById('insert');
+const redo = document.getElementById('redo');
+const undo = document.getElementById('undo');
+
+function updateTable(data: Array<Array<string>>) {
+  table.innerHTML = '';
+  data.forEach((row) => {
+    const tr = document.createElement('tr');
+    row.forEach((content) => {
+      const td = document.createElement('td');
+      td.innerText = `${content}`;
+      tr.append(td);
+    });
+    table.append(tr);
+  });
+}
+
+webClient.getObserver().addOnConnectedListener({
+  onConnected() {
+    updateTable(webClient.getSource());
+  },
+});
+webClient.getObserver().addOnAckListener({
+  onAck() {
+    updateTable(webClient.getSource());
+  },
+});
+webClient.getObserver().addOnCommandListener({
+  onCommand() {
+    updateTable(webClient.getSource());
+  },
+});
+
+insert.addEventListener('click', () => {
+  const undoManager = webClient.getUndoManager();
+  const location = <HTMLInputElement>document.getElementById('location');
+  const number = <HTMLInputElement>document.getElementById('number');
+  const content = <HTMLInputElement>document.getElementById('content');
+  const swapBufferData = new SwapData({
+    describe: "SwapData",
+    data: new Array<Array<string>>(parseInt(number.value, 10)).fill([content.value, content.value, content.value, content.value, content.value]),
+  });
+  const redoCommand = new Command(sheetId, [
+    new InsertRow({
+      describe: "InsertRow",
+      index: parseInt(location.value, 10),
+      count: parseInt(number.value, 10),
+      buffer: swapBufferData,
+    }),
+  ]);
+  const undoCommand = new Command(sheetId, [
+    new RemoveRow({
+      describe: "RemoveRow",
+      index: parseInt(location.value, 10),
+      count: parseInt(number.value, 10),
+    }),
+  ]);
+  undoManager.performCommand(redoCommand, undoCommand);
+});
+redo.addEventListener('click', () => {
+  const undoManager = webClient.getUndoManager();
+  undoManager.performRedo();
+});
+undo.addEventListener('click', () => {
+  const undoManager = webClient.getUndoManager();
+  undoManager.performUndo();
+});

+ 15 - 0
packages/ot-demo/src/client/DemoClient.ts

@@ -0,0 +1,15 @@
+import {
+  ClientSocket,
+} from "ot-core";
+
+import {
+  CreateConflict,
+} from "../internal";
+
+import "../basics/Bootstrap";
+
+export class DemoClient extends ClientSocket {
+  public constructor(post: string) {
+    super({ url: post, conflict: CreateConflict() });
+  }
+}

+ 12 - 0
packages/ot-demo/src/internal.ts

@@ -1,6 +1,18 @@
+// ================================================= operated
+
 export * from './operated/InsertColumn';
 export * from './operated/InsertRow';
 export * from './operated/RemoveColumn';
 export * from './operated/RemoveRow';
 
+// ================================================= basics
+
 export * from './basics/CreateConflict';
+
+// ================================================= client
+
+export * from './client/DemoClient';
+
+// ================================================= server
+
+export * from './server/DemoServer';

+ 3 - 1
packages/ot-demo/src/operated/InsertColumn.ts

@@ -5,7 +5,7 @@ import {
   TransformUtils,
 } from "ot-core";
 
-export class InsertColumn implements Operation {
+export class InsertColumn extends Operation {
   protected _describe: string = 'InsertColumn';
   protected _index: number;
   protected _count: number;
@@ -14,6 +14,7 @@ export class InsertColumn implements Operation {
   public constructor()
   public constructor(serialize: InsertColumn.Serialize)
   public constructor(...parameter: any) {
+    super();
     if (TransformUtils.hasLength(parameter, 0)) {
       this._index = 0;
       this._count = 0;
@@ -21,6 +22,7 @@ export class InsertColumn implements Operation {
       return;
     }
     if (TransformUtils.hasLength(parameter, 1)) {
+      super();
       const serialize = parameter[0];
       this._index = serialize.index;
       this._count = serialize.count;

+ 10 - 8
packages/ot-demo/src/operated/InsertRow.ts

@@ -1,19 +1,20 @@
 import {
+  TransformUtils,
   SwapData,
-  Deserialize,
   Operation,
-  TransformUtils,
+  Deserialize,
 } from "ot-core";
 
-export class InsertRow implements Operation {
+export class InsertRow extends Operation {
   protected _describe: string = 'InsertRow';
   protected _index: number;
   protected _count: number;
-  protected _buffer: SwapData<Array<Array<number>>>;
+  protected _buffer: SwapData<Array<Array<string>>>;
 
   public constructor()
   public constructor(serialize: InsertRow.Serialize)
   public constructor(...parameter: any) {
+    super();
     if (TransformUtils.hasLength(parameter, 0)) {
       this._index = 0;
       this._count = 0;
@@ -28,14 +29,15 @@ export class InsertRow implements Operation {
     }
   }
 
-  public apply(resource: Array<Array<number>>): void {
-    let data = this._buffer.getData();
+  public apply(resource: Array<Array<string>>): void {
     let index = this._index;
+    let buffer = this._buffer;
     let count = this._count;
-    if (data) {
+    if (buffer.hasData()) {
+      const data = buffer.getData();
       resource.splice(index, 0, ...data);
     } else {
-      const empty = new Array<Array<number>>(count).fill([]);
+      const empty = new Array<Array<string>>(count).fill([]);
       resource.splice(index, 0, ...empty);
     }
   }

+ 2 - 1
packages/ot-demo/src/operated/RemoveColumn.ts

@@ -4,7 +4,7 @@ import {
   SwapData,
 } from "ot-core";
 
-export class RemoveColumn implements Operation {
+export class RemoveColumn extends Operation {
   protected _describe: string = 'RemoveColumn';
   protected _index: number;
   protected _count: number;
@@ -14,6 +14,7 @@ export class RemoveColumn implements Operation {
   public constructor(serialize: RemoveColumn.Serialize)
   public constructor(serialize: RemoveColumn.Serialize, buffer: SwapData)
   public constructor(...parameter: any) {
+    super();
     if (TransformUtils.hasLength(parameter, 0)) {
       this._index = 0;
       this._count = 0;

+ 14 - 3
packages/ot-demo/src/operated/RemoveRow.ts

@@ -4,16 +4,17 @@ import {
   SwapData,
 } from "ot-core";
 
-export class RemoveRow implements Operation {
+export class RemoveRow extends Operation {
   protected _describe: string = 'RemoveRow';
   protected _index: number;
   protected _count: number;
-  protected _buffer: SwapData<unknown>;
+  protected _buffer: SwapData<Array<string>>;
 
   public constructor()
   public constructor(serialize: RemoveRow.Serialize)
   public constructor(serialize: RemoveRow.Serialize, buffer: SwapData)
   public constructor(...parameter: any) {
+    super();
     if (TransformUtils.hasLength(parameter, 0)) {
       this._index = 0;
       this._count = 0;
@@ -24,6 +25,7 @@ export class RemoveRow implements Operation {
       const serialize = parameter[0];
       this._index = serialize.index;
       this._count = serialize.count;
+      this._buffer = new SwapData();
       return;
     }
     if (TransformUtils.hasLength(parameter, 2)) {
@@ -35,7 +37,8 @@ export class RemoveRow implements Operation {
     }
   }
 
-  public apply(resource: unknown): void {
+  public apply(resource: Array<Array<string>>): void {
+    resource.splice(this._index, this._count);
   }
 
   public setIndex(index: number): void {
@@ -54,6 +57,14 @@ export class RemoveRow implements Operation {
     return this._count;
   }
 
+  public getHeight(): number {
+    return this._index + this._count;
+  }
+
+  public getBuffer(): SwapData<Array<string>> {
+    return this._buffer;
+  }
+
   public clone(): RemoveRow {
     const clone = new RemoveRow();
     clone._index = this._index;

+ 1 - 1
packages/ot-demo/src/server.ts

@@ -1 +1 @@
-export * from './server/Server';
+export * from './server/DemoServer';

+ 25 - 0
packages/ot-demo/src/server/DemoServer.ts

@@ -0,0 +1,25 @@
+import {
+  ServerSocket,
+  Command,
+} from "ot-core";
+
+import {
+  Socket,
+} from "socket.io";
+
+import {
+  CreateConflict,
+} from "../internal";
+
+import "../basics/Bootstrap";
+
+export class DemoServer extends ServerSocket {
+  public constructor() {
+    super(CreateConflict(), []);
+  }
+
+  public onCommand(client: Socket, revision: number, command: Command.Serialize) {
+    super.onCommand(client, revision, command);
+    console.log(this._source);
+  }
+}