Skip to content

Commit

Permalink
Extract history from document
Browse files Browse the repository at this point in the history
  • Loading branch information
hackerwins committed Sep 22, 2023
1 parent 15d8e7a commit 7917aad
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 70 deletions.
83 changes: 13 additions & 70 deletions src/document/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
Presence,
PresenceChangeType,
} from '@yorkie-js-sdk/src/document/presence/presence';
import { History } from '@yorkie-js-sdk/src/document/history';

/**
* `DocumentOptions` are the options to create a new document.
Expand Down Expand Up @@ -399,8 +400,6 @@ type PathOf<TDocument, Depth extends number = 10> = PathOfInternal<
Depth
>;

export const MaxUndoRedoStackDepth = 50;

/**
* `Document` is a CRDT-based data type. We can represent the model
* of the application and edit it even while offline.
Expand Down Expand Up @@ -436,19 +435,14 @@ export class Document<T, P extends Indexable = Indexable> {
private presences: Map<ActorID, P>;

/**
* `history` manages undo and redo of document.
* `history` is exposed to the user to manage undo/redo operations.
*/
public history;

/**
* `undoStack` stores the history of undo operations.
*/
private undoStack: Array<Array<HistoryOperation<P>>>;

/**
* `redoStack` stores the history of redo operations.
* `internalHistory` is used to manage undo/redo operations internally.
*/
private redoStack: Array<Array<HistoryOperation<P>>>;
public internalHistory: History<P>;

/**
* `isUpdating` is whether the document is updating or not. It is used to
Expand All @@ -475,9 +469,7 @@ export class Document<T, P extends Indexable = Indexable> {
this.presences = new Map();

this.isUpdating = false;
this.undoStack = [];
this.redoStack = [];

this.internalHistory = new History();
this.history = {
canUndo: this.canUndo.bind(this),
canRedo: this.canRedo.bind(this),
Expand Down Expand Up @@ -550,9 +542,9 @@ export class Document<T, P extends Indexable = Indexable> {

this.localChanges.push(change);
if (reverseOps.length > 0) {
this.pushUndo(reverseOps);
this.internalHistory.pushUndo(reverseOps);
}
this.clearRedo();
this.internalHistory.clearRedo();
this.changeID = change.getID();

if (change.hasOperations()) {
Expand Down Expand Up @@ -1232,41 +1224,14 @@ export class Document<T, P extends Indexable = Indexable> {
* `canUndo` returns whether there are any operations to undo.
*/
private canUndo(): boolean {
return this.undoStack.length > 0 && !this.isUpdating;
return this.internalHistory.hasUndo() && !this.isUpdating;
}

/**
* `canRedo` returns whether there are any operations to redo.
*/
private canRedo(): boolean {
return this.redoStack.length > 0 && !this.isUpdating;
}

/**
* `pushUndo` pushes new undo operations of a change to undo stack.
*/
private pushUndo(undoOps: Array<HistoryOperation<P>>): void {
if (this.undoStack.length >= MaxUndoRedoStackDepth) {
this.undoStack.shift();
}
this.undoStack.push(undoOps);
}

/**
* `pushRedo` pushes new redo operations of a change to redo stack.
*/
private pushRedo(redoOps: Array<HistoryOperation<P>>): void {
if (this.redoStack.length >= MaxUndoRedoStackDepth) {
this.redoStack.shift();
}
this.redoStack.push(redoOps);
}

/**
* `clearRedo` flushes remaining redo operations.
*/
private clearRedo(): void {
this.redoStack = [];
return this.internalHistory.hasRedo() && !this.isUpdating;
}

/**
Expand All @@ -1277,7 +1242,7 @@ export class Document<T, P extends Indexable = Indexable> {
if (this.isUpdating) {
throw new Error('Undo is not allowed during an update');
}
const undoOps = this.undoStack.pop();
const undoOps = this.internalHistory.popUndo();
if (undoOps === undefined) {
throw new Error('There is no operation to be undone');
}
Expand Down Expand Up @@ -1322,7 +1287,7 @@ export class Document<T, P extends Indexable = Indexable> {
});
}
if (reverseOps.length > 0) {
this.pushRedo(reverseOps);
this.internalHistory.pushRedo(reverseOps);
}

this.localChanges.push(change);
Expand Down Expand Up @@ -1359,7 +1324,7 @@ export class Document<T, P extends Indexable = Indexable> {
throw new Error('Redo is not allowed during an update');
}

const redoOps = this.redoStack.pop();
const redoOps = this.internalHistory.popRedo();
if (redoOps === undefined) {
throw new Error('There is no operation to be redone');
}
Expand Down Expand Up @@ -1404,7 +1369,7 @@ export class Document<T, P extends Indexable = Indexable> {
});
}
if (reverseOps.length > 0) {
this.pushUndo(reverseOps);
this.internalHistory.pushUndo(reverseOps);
}

this.localChanges.push(change);
Expand All @@ -1431,26 +1396,4 @@ export class Document<T, P extends Indexable = Indexable> {
});
}
}

/**
* `getUndoStackForTest` returns the undo stack for test.
*/
public getUndoStackForTest(): Array<Array<string>> {
return this.undoStack.map((ops) =>
ops.map((op) => {
return op instanceof Operation ? op.toTestString() : JSON.stringify(op);
}),
);
}

/**
* `getRedoStackForTest` returns the redo stack for test.
*/
public getRedoStackForTest(): Array<Array<string>> {
return this.redoStack.map((ops) =>
ops.map((op) => {
return op instanceof Operation ? op.toTestString() : JSON.stringify(op);
}),
);
}
}
117 changes: 117 additions & 0 deletions src/document/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2023 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Operation } from './operation/operation';
import { Indexable } from './document';

/**
* `HistoryOperation` is a type of history operation.
*/
export type HistoryOperation<P extends Indexable> =
| Operation
| {
type: 'presence';
value: Partial<P>;
};

/**
* `MaxUndoRedoStackDepth` is the maximum depth of undo/redo stack.
*/
export const MaxUndoRedoStackDepth = 50;

/**
* `History` is a class that stores the history of the document.
*/
export class History<P extends Indexable> {
private undoStack: Array<Array<HistoryOperation<P>>> = [];
private redoStack: Array<Array<HistoryOperation<P>>> = [];

/**
* `hasUndo` returns true if there are undo operations.
*/
public hasUndo(): boolean {
return this.undoStack.length > 0;
}

/**
* `hasRedo` returns true if there are redo operations.
*/
public hasRedo(): boolean {
return this.redoStack.length > 0;
}

/**
* `pushUndo` pushes new undo operations of a change to undo stack.
*/
public pushUndo(undoOps: Array<HistoryOperation<P>>): void {
if (this.undoStack.length >= MaxUndoRedoStackDepth) {
this.undoStack.shift();
}
this.undoStack.push(undoOps);
}

/**
* `popUndo` pops the last undo operations of a change from undo stack.
*/
public popUndo(): Array<HistoryOperation<P>> | undefined {
return this.undoStack.pop();
}

/**
* `pushRedo` pushes new redo operations of a change to redo stack.
*/
public pushRedo(redoOps: Array<HistoryOperation<P>>): void {
if (this.redoStack.length >= MaxUndoRedoStackDepth) {
this.redoStack.shift();
}
this.redoStack.push(redoOps);
}

/**
* `popRedo` pops the last redo operations of a change from redo stack.
*/
public popRedo(): Array<HistoryOperation<P>> | undefined {
return this.redoStack.pop();
}

/**
* `clearRedo` flushes remaining redo operations.
*/
public clearRedo(): void {
this.redoStack = [];
}

/**
* `getUndoStackForTest` returns the undo stack for test.
*/
public getUndoStackForTest(): Array<Array<string>> {
return this.undoStack.map((ops) =>
ops.map((op) => {
return op instanceof Operation ? op.toTestString() : JSON.stringify(op);
}),
);
}

/**
* `getRedoStackForTest` returns the redo stack for test.
*/
public getRedoStackForTest(): Array<Array<string>> {
return this.redoStack.map((ops) =>
ops.map((op) => {
return op instanceof Operation ? op.toTestString() : JSON.stringify(op);
}),
);
}
}

0 comments on commit 7917aad

Please sign in to comment.