diff --git a/src/document/change/change.ts b/src/document/change/change.ts
index 5461ec37b..910604512 100644
--- a/src/document/change/change.ts
+++ b/src/document/change/change.ts
@@ -21,10 +21,8 @@ import {
} from '@yorkie-js-sdk/src/document/operation/operation';
import { CRDTRoot } from '@yorkie-js-sdk/src/document/crdt/root';
import { ChangeID } from '@yorkie-js-sdk/src/document/change/change_id';
-import {
- Indexable,
- HistoryOperation,
-} from '@yorkie-js-sdk/src/document/document';
+import { Indexable } from '@yorkie-js-sdk/src/document/document';
+import { HistoryOperation } from '@yorkie-js-sdk/src/document/history';
import {
PresenceChange,
PresenceChangeType,
diff --git a/src/document/document.ts b/src/document/document.ts
index 297dfa4ba..a683556a7 100644
--- a/src/document/document.ts
+++ b/src/document/document.ts
@@ -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.
@@ -253,13 +254,6 @@ export interface PresenceChangedEvent
*/
export type Indexable = Record;
-export type HistoryOperation =
- | Operation
- | {
- type: 'presence';
- value: Partial
;
- };
-
/**
* Document key type
* @public
@@ -399,8 +393,6 @@ type PathOf = 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.
@@ -436,19 +428,14 @@ export class Document {
private presences: Map;
/**
- * `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>>;
-
- /**
- * `redoStack` stores the history of redo operations.
+ * `internalHistory` is used to manage undo/redo operations internally.
*/
- private redoStack: Array>>;
+ public internalHistory: History;
/**
* `isUpdating` is whether the document is updating or not. It is used to
@@ -475,9 +462,7 @@ export class Document {
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),
@@ -550,9 +535,9 @@ export class Document {
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()) {
@@ -1232,41 +1217,14 @@ export class Document {
* `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>): 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>): 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;
}
/**
@@ -1277,7 +1235,7 @@ export class Document {
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');
}
@@ -1322,7 +1280,7 @@ export class Document {
});
}
if (reverseOps.length > 0) {
- this.pushRedo(reverseOps);
+ this.internalHistory.pushRedo(reverseOps);
}
this.localChanges.push(change);
@@ -1359,7 +1317,7 @@ export class Document {
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');
}
@@ -1404,7 +1362,7 @@ export class Document {
});
}
if (reverseOps.length > 0) {
- this.pushUndo(reverseOps);
+ this.internalHistory.pushUndo(reverseOps);
}
this.localChanges.push(change);
@@ -1436,21 +1394,13 @@ export class Document {
* `getUndoStackForTest` returns the undo stack for test.
*/
public getUndoStackForTest(): Array> {
- return this.undoStack.map((ops) =>
- ops.map((op) => {
- return op instanceof Operation ? op.toTestString() : JSON.stringify(op);
- }),
- );
+ return this.internalHistory.getUndoStackForTest();
}
/**
* `getRedoStackForTest` returns the redo stack for test.
*/
public getRedoStackForTest(): Array> {
- return this.redoStack.map((ops) =>
- ops.map((op) => {
- return op instanceof Operation ? op.toTestString() : JSON.stringify(op);
- }),
- );
+ return this.internalHistory.getRedoStackForTest();
}
}
diff --git a/src/document/history.ts b/src/document/history.ts
new file mode 100644
index 000000000..ad99b40bd
--- /dev/null
+++ b/src/document/history.ts
@@ -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 =
+ | Operation
+ | {
+ type: 'presence';
+ value: Partial
;
+ };
+
+/**
+ * `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
{
+ private undoStack: Array>> = [];
+ private redoStack: Array>> = [];
+
+ /**
+ * `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>): 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> | undefined {
+ return this.undoStack.pop();
+ }
+
+ /**
+ * `pushRedo` pushes new redo operations of a change to redo stack.
+ */
+ public pushRedo(redoOps: Array>): 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> | 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> {
+ 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> {
+ return this.redoStack.map((ops) =>
+ ops.map((op) => {
+ return op instanceof Operation ? op.toTestString() : JSON.stringify(op);
+ }),
+ );
+ }
+}