circular-history 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ pnpm prettier
@@ -0,0 +1 @@
1
+ pnpm prettier
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v22.20.0
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ pnpm-lock.yaml
3
+ dist
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "all",
4
+ "singleQuote": false,
5
+ "tabWidth": 2,
6
+ "printWidth": 100
7
+ }
package/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ MIT Licence
2
+
3
+ Copyright (c) Ihor ZAVIRIUKHA <ihor.zaviriukha@protonmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # Circular History
2
+
3
+ Data structure that holds a fixed amount of items of a specific data type. It's designed more for managing history, such as undo-redo functionality, therefore it allows you to overwrite the items after moving backward in order to discard the "redo" history.
4
+
5
+ ## Usage
6
+
7
+ ### 1. Create an instance of `CircularHistory` with a specified capacity and data type.
8
+
9
+ ```javascript
10
+ var history = new CircularHistory(5, "string");
11
+ ```
12
+
13
+ ### 2. Commit new items to the history.
14
+
15
+ ```javascript
16
+ history.commit("First");
17
+ history.commit("Second");
18
+ ```
19
+
20
+ Keep in mind that after moving backward, committing a new item will overwrite the items ahead of the current index.
21
+ It was designed this way to facilitate undo-redo functionality by discarding the "redo" history when a new action is taken.
22
+
23
+ When the capacity is reached, the oldest items will be overwritten in a circular manner.
24
+
25
+ ### 3. Get the current item.
26
+
27
+ ```javascript
28
+ var currentItem = history.current();
29
+ ```
30
+
31
+ Returns either the current item or `CircularHistory.FLAGS.empty` if current index is `-1` which means there are no committed items yet or you went backwards beyond the first committed item (basically an empty state).
32
+
33
+ ### 4. Move backward and forward in the history.
34
+
35
+ ```javascript
36
+ history.moveBackward();
37
+ history.moveForward();
38
+ ```
39
+
40
+ Moving backward and forward will adjust the current index accordingly and allow you to navigate within specific range which is determined by the number of committed items before navigation. If the number of committed items exceeds the capacity, the range will be limited to the capacity.
41
+
42
+ ### 5. Clear the history.
43
+
44
+ ```javascript
45
+ history.clear();
46
+ ```
47
+
48
+ ### 6. Get the history array.
49
+
50
+ ```javascript
51
+ var historyArray = history.dump();
52
+ ```
53
+
54
+ This will return an array of all committed items in the history. If capacity has not been reached, the array will contain items with `undefined` values for uncommitted slots.
55
+
56
+ If you want to get only the committed items, you can pass `true` as an argument.
57
+
58
+ ```javascript
59
+ var committedItems = history.dump(true);
60
+ ```
61
+
62
+ ### 7. Get the current index.
63
+
64
+ ```javascript
65
+ var currentIndex = history.getCurrentIndex();
66
+ ```
67
+
68
+ ### 8. Determine if start/end has been reached
69
+
70
+ ```javascript
71
+ var isAtStart = history.isStartReached();
72
+ var isAtEnd = history.isEndReached();
73
+ ```
74
+
75
+ ## Running tests
76
+
77
+ 1. `pnpm install`
78
+
79
+ 2. `pnpm test`
80
+
81
+ ## Usage example
82
+
83
+ Imagine that you have a drawing application. You have a layer where you can draw
84
+ shapes and you can create a snapshot of the layer's state to keep track of changes.
85
+
86
+ ```javascript
87
+ var history = new CircularHistory(50, "string");
88
+
89
+ function commitHistorySnapshot() {
90
+ const snapshot = drawLayer.toJSON();
91
+ if (!snapshot) return;
92
+ history.commit(snapshot);
93
+ }
94
+
95
+ function restoreHistorySnapshot(snapshot) {
96
+ if (!snapshot) return;
97
+
98
+ if (snapshot === CircularHistory.FLAGS.empty) {
99
+ drawLayer.clear();
100
+ drawLayer.redraw();
101
+ return;
102
+ }
103
+
104
+ drawLayer.unmount();
105
+
106
+ const restoredLayer = new Layer(snapshot);
107
+ drawLayer = restoredLayer;
108
+
109
+ restoredLayer.redraw();
110
+ }
111
+
112
+ function undo() {
113
+ history.moveBackward();
114
+ restoreHistorySnapshot(history.current());
115
+ }
116
+
117
+ function redo() {
118
+ history.moveForward();
119
+ restoreHistorySnapshot(history.current());
120
+ }
121
+ ```
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "circular-history",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "description": "Data structure with a fixed number of items of a specific type. Supports adding elements, moving backward and forward with wrapping. Suitable for undo/redo implementations such as history management.",
7
+ "scripts": {
8
+ "test": "vitest .",
9
+ "prettier": "prettier --check .",
10
+ "prettier:fix": "prettier --write .",
11
+ "build": "esbuild src/circular-history.js --bundle --packages=external --format=esm --minify --keep-names --outfile=./dist/index.js",
12
+ "prepare": "husky",
13
+ "prepare-release": "pnpm build && node ./scripts/prepare-release.js"
14
+ },
15
+ "devDependencies": {
16
+ "esbuild": "^0.27.2",
17
+ "husky": "^9.1.7",
18
+ "prettier": "^3.8.0",
19
+ "vite": "npm:rolldown-vite@7.2.5",
20
+ "vitest": "^4.0.17"
21
+ },
22
+ "pnpm": {
23
+ "overrides": {
24
+ "vite": "npm:rolldown-vite@7.2.5"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ import fs from "fs";
2
+
3
+ fs.copyFile("README.md", "./dist/README.md", (err) => {
4
+ if (err) throw err;
5
+ console.log("REAME.md was copied to dist folder");
6
+ });
7
+
8
+ fs.copyFile("./src/index.d.ts", "./dist/index.d.ts", (err) => {
9
+ if (err) throw err;
10
+ console.log("index.d.ts was copied to dist folder");
11
+ console.log("-----------------------------");
12
+ console.log("UPDATE THE VERSION IN package.json");
13
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * #Config
3
+ */
4
+
5
+ /**
6
+ * @description
7
+ * List of data types that are allowed to be stored in the items.
8
+ * All items should be of the same data type.
9
+ */
10
+ var ALLOWED_DATA_TYPES = ["number", "string", "bigint", "boolean", "symbol", "object"];
11
+
12
+ var NAVIGATION_LOWER_BOUND = 0;
13
+ var EMPTY_POINTER = -1;
14
+
15
+ /**
16
+ * @description
17
+ * List of values that can be returned by the CircularHistory methods
18
+ */
19
+ var FLAGS = {
20
+ empty: Symbol("empty"),
21
+ };
22
+
23
+ /**
24
+ * #Utils
25
+ */
26
+
27
+ var isTypeAllowed = (type) => ALLOWED_DATA_TYPES.includes(type);
28
+
29
+ var isValueTypeAllowed = (value) => {
30
+ var valueType = typeof value;
31
+ var isObject = valueType === "object" && value !== null;
32
+ return isObject || isTypeAllowed(valueType);
33
+ };
34
+
35
+ var canCommit = (value, dataType) => isValueTypeAllowed(value) && typeof value === dataType;
36
+ var isItemEmpty = (slot) => slot === undefined;
37
+ var makeIndex = (pointer, capacity) => pointer % capacity;
38
+
39
+ /**
40
+ * #State
41
+ *
42
+ * @description
43
+ * Using WeakMap to store private state in order to not expose private properties.
44
+ * This will ensure that the reference to the state will be dropped when instances
45
+ * are garbage collected.
46
+ */
47
+ var STATE = new WeakMap();
48
+
49
+ /**
50
+ * @description
51
+ *
52
+ * Circular History
53
+ *
54
+ * Data structure that holds a fixed amount of items of a specific data type.
55
+ * Supports committing new items, and moving backward and forward through the committed items.
56
+ * When the buffer is full the wrapping occurs and the oldest items are overwritten.
57
+ * After moving backward, new commits will overwrite the items ahead of the current pointer which
58
+ * makes this data structure suitable for undo-redo implementations like history management.
59
+ *
60
+ * @param {number} capacity - Maximum amount of items in the buffer
61
+ * @param {string} dataType - Data type of each slot
62
+ */
63
+ function CircularHistory(capacity, dataType) {
64
+ if (typeof capacity !== "number" || capacity <= 0 || !Number.isInteger(capacity)) {
65
+ throw new Error(`Capacity must be a positive integer. Got "${capacity}".`);
66
+ }
67
+
68
+ if (!isTypeAllowed(dataType)) {
69
+ throw new Error(`"${dataType}" is not allowed`);
70
+ }
71
+
72
+ STATE.set(this, {
73
+ /**
74
+ * Represents range [0, navigationUpperBound] of how many items can be used
75
+ * for navigation.
76
+ */
77
+ navigationUpperBound: NAVIGATION_LOWER_BOUND,
78
+
79
+ /**
80
+ * Represents current index within the navigation range [0, navigationUpperBound].
81
+ */
82
+ navigationIndex: NAVIGATION_LOWER_BOUND,
83
+
84
+ /**
85
+ * Current pointer. It does not point to an index in the buffer directly.
86
+ * It is used to calculate the index in the buffer using modulo operation.
87
+ */
88
+ pointer: EMPTY_POINTER,
89
+
90
+ /**
91
+ * Total maximum amount of items.
92
+ */
93
+ capacity: capacity,
94
+
95
+ /**
96
+ * Data type of each slot.
97
+ */
98
+ dataType: dataType,
99
+
100
+ /**
101
+ * Array of items.
102
+ * Should always be pre-allocated with capacity.
103
+ */
104
+ buffer: new Array(capacity),
105
+ });
106
+ }
107
+
108
+ CircularHistory.prototype.commit = function (value) {
109
+ var self = STATE.get(this);
110
+ var dataType = self.dataType;
111
+
112
+ if (!canCommit(value, dataType)) {
113
+ throw new Error(
114
+ `Type of ${value} is invalid. Expected "${dataType}", got "${value === null ? "null" : typeof value}".`,
115
+ );
116
+ }
117
+
118
+ var capacity = self.capacity;
119
+
120
+ if (self.pointer === self.capacity - 1) {
121
+ self.navigationUpperBound = capacity - 1;
122
+ self.navigationIndex = capacity - 1;
123
+ } else {
124
+ self.navigationIndex = ++self.navigationUpperBound;
125
+ }
126
+ self.buffer[makeIndex(++self.pointer, capacity)] = value;
127
+ };
128
+
129
+ CircularHistory.prototype.current = function () {
130
+ var self = STATE.get(this);
131
+ var buffer = self.buffer;
132
+
133
+ var pointer = self.pointer;
134
+ if (pointer === EMPTY_POINTER) return FLAGS.empty;
135
+ var nextItem = buffer[makeIndex(pointer, self.capacity)];
136
+
137
+ return isItemEmpty(nextItem) ? FLAGS.empty : nextItem;
138
+ };
139
+
140
+ CircularHistory.prototype.moveBackward = function () {
141
+ var self = STATE.get(this);
142
+ if (self.navigationIndex === NAVIGATION_LOWER_BOUND || self.pointer === EMPTY_POINTER) return;
143
+ self.pointer--;
144
+ self.navigationIndex--;
145
+ };
146
+
147
+ CircularHistory.prototype.moveForward = function () {
148
+ var self = STATE.get(this);
149
+ if (self.navigationIndex === self.navigationUpperBound) return;
150
+ self.pointer++;
151
+ self.navigationIndex++;
152
+ };
153
+
154
+ CircularHistory.prototype.clear = function () {
155
+ var self = STATE.get(this);
156
+ self.navigationIndex = NAVIGATION_LOWER_BOUND;
157
+ self.navigationUpperBound = NAVIGATION_LOWER_BOUND;
158
+ self.pointer = EMPTY_POINTER;
159
+ self.buffer = new Array(self.capacity);
160
+ };
161
+
162
+ CircularHistory.prototype.dump = function (discardHoles = false) {
163
+ var self = STATE.get(this);
164
+ var result = [...self.buffer];
165
+ return discardHoles ? result.filter((slot) => !isItemEmpty(slot)) : result;
166
+ };
167
+
168
+ CircularHistory.prototype.getCurrentIndex = function () {
169
+ var self = STATE.get(this);
170
+ return makeIndex(self.pointer, self.capacity);
171
+ };
172
+
173
+ CircularHistory.prototype.isStartReached = function () {
174
+ var self = STATE.get(this);
175
+ return self.navigationIndex === NAVIGATION_LOWER_BOUND;
176
+ };
177
+
178
+ CircularHistory.prototype.isEndReached = function () {
179
+ var self = STATE.get(this);
180
+ return self.navigationIndex === self.navigationUpperBound;
181
+ };
182
+
183
+ CircularHistory.FLAGS = FLAGS;
184
+
185
+ export { CircularHistory };
@@ -0,0 +1,432 @@
1
+ import { test, describe, expect } from "vitest";
2
+ import { CircularHistory } from "./circular-history.js";
3
+
4
+ describe("CircularHistory", () => {
5
+ test("should throw if capacity is not a positive integer", () => {
6
+ expect(() => new CircularHistory(0, "number")).toThrow();
7
+ expect(() => new CircularHistory(-5, "number")).toThrow();
8
+ expect(() => new CircularHistory(3.5, "number")).toThrow();
9
+ expect(() => new CircularHistory("10", "number")).toThrow();
10
+ });
11
+
12
+ test("should throw if data type is not allowed", () => {
13
+ expect(() => new CircularHistory(5, "invalidType")).toThrow();
14
+ expect(() => new CircularHistory(5, 123)).toThrow();
15
+ expect(() => new CircularHistory(5, null)).toThrow();
16
+ });
17
+
18
+ test("should successfully create a CircularHistory with valid parameters", () => {
19
+ var historyNumber = new CircularHistory(5, "number");
20
+ expect(historyNumber).toBeInstanceOf(CircularHistory);
21
+
22
+ var historyString = new CircularHistory(10, "string");
23
+ expect(historyString).toBeInstanceOf(CircularHistory);
24
+
25
+ var historyBigInt = new CircularHistory(3, "bigint");
26
+ expect(historyBigInt).toBeInstanceOf(CircularHistory);
27
+
28
+ var historyBoolean = new CircularHistory(7, "boolean");
29
+ expect(historyBoolean).toBeInstanceOf(CircularHistory);
30
+
31
+ var historySymbol = new CircularHistory(2, "symbol");
32
+ expect(historySymbol).toBeInstanceOf(CircularHistory);
33
+
34
+ var historyObject = new CircularHistory(4, "object");
35
+ expect(historyObject).toBeInstanceOf(CircularHistory);
36
+ });
37
+
38
+ test("should not commit invalid data types", () => {
39
+ var historyNumber = new CircularHistory(5, "number");
40
+ expect(() => historyNumber.commit("string")).toThrow();
41
+ expect(() => historyNumber.commit(true)).toThrow();
42
+ expect(() => historyNumber.commit({})).toThrow();
43
+ expect(() => historyNumber.commit([])).toThrow();
44
+ expect(() => historyNumber.commit(Symbol("sym"))).toThrow();
45
+ expect(() => historyNumber.commit(10n)).toThrow();
46
+
47
+ var historyString = new CircularHistory(5, "string");
48
+ expect(() => historyString.commit(100)).toThrow();
49
+ expect(() => historyString.commit(false)).toThrow();
50
+ expect(() => historyString.commit({})).toThrow();
51
+ expect(() => historyString.commit([])).toThrow();
52
+ expect(() => historyString.commit(Symbol("sym"))).toThrow();
53
+ expect(() => historyString.commit(10n)).toThrow();
54
+
55
+ var historyBigInt = new CircularHistory(5, "bigint");
56
+ expect(() => historyBigInt.commit(100)).toThrow();
57
+ expect(() => historyBigInt.commit("string")).toThrow();
58
+ expect(() => historyBigInt.commit(true)).toThrow();
59
+ expect(() => historyBigInt.commit({})).toThrow();
60
+ expect(() => historyBigInt.commit(Symbol("sym"))).toThrow();
61
+ expect(() => historyBigInt.commit(10.5)).toThrow();
62
+
63
+ var historyBoolean = new CircularHistory(5, "boolean");
64
+ expect(() => historyBoolean.commit(100)).toThrow();
65
+ expect(() => historyBoolean.commit("string")).toThrow();
66
+ expect(() => historyBoolean.commit({})).toThrow();
67
+ expect(() => historyBoolean.commit([])).toThrow();
68
+ expect(() => historyBoolean.commit(Symbol("sym"))).toThrow();
69
+ expect(() => historyBoolean.commit(10n)).toThrow();
70
+
71
+ var historySymbol = new CircularHistory(5, "symbol");
72
+ expect(() => historySymbol.commit(100)).toThrow();
73
+ expect(() => historySymbol.commit("string")).toThrow();
74
+ expect(() => historySymbol.commit(true)).toThrow();
75
+ expect(() => historySymbol.commit({})).toThrow();
76
+ expect(() => historySymbol.commit(10n)).toThrow();
77
+
78
+ var historyObject = new CircularHistory(5, "object");
79
+ expect(() => historyObject.commit(100)).toThrow();
80
+ expect(() => historyObject.commit("string")).toThrow();
81
+ expect(() => historyObject.commit(true)).toThrow();
82
+ expect(() => historyObject.commit(Symbol("sym"))).toThrow();
83
+ expect(() => historyObject.commit(10n)).toThrow();
84
+ });
85
+
86
+ test("should get an empty flag if history is empty", () => {
87
+ var history = new CircularHistory(5, "number");
88
+ expect(history.current()).toEqual(CircularHistory.FLAGS.empty);
89
+ });
90
+
91
+ test("should wrap around when capacity is exceeded", () => {
92
+ var history = new CircularHistory(3, "number");
93
+
94
+ history.commit(1);
95
+ history.commit(2);
96
+ history.commit(3);
97
+
98
+ expect(history.current()).toBe(3);
99
+ history.commit(4);
100
+
101
+ expect(history.current()).toBe(4);
102
+ expect(history.dump(true)).toEqual([4, 2, 3]);
103
+ });
104
+
105
+ test("should move backward and forward (no wrapping)", () => {
106
+ var history = new CircularHistory(3, "number");
107
+
108
+ history.commit(1);
109
+ history.commit(2);
110
+ history.commit(3);
111
+ expect(history.current()).toBe(3);
112
+
113
+ history.moveBackward();
114
+ expect(history.current()).toBe(2);
115
+
116
+ history.moveBackward();
117
+ expect(history.current()).toBe(1);
118
+
119
+ history.moveBackward();
120
+ expect(history.current()).toBe(CircularHistory.FLAGS.empty);
121
+
122
+ history.moveBackward();
123
+ expect(history.current()).toBe(CircularHistory.FLAGS.empty);
124
+ expect(history.getCurrentIndex()).toBe(-1);
125
+
126
+ history.moveForward();
127
+ expect(history.current()).toBe(1);
128
+
129
+ history.moveForward();
130
+ expect(history.current()).toBe(2);
131
+
132
+ history.moveForward();
133
+ expect(history.current()).toBe(3);
134
+
135
+ history.moveForward();
136
+ expect(history.current()).toBe(3);
137
+ });
138
+
139
+ test("should move backward and forward (with wrapping)", () => {
140
+ var history = new CircularHistory(3, "number");
141
+ history.commit(1);
142
+ history.commit(2);
143
+ history.commit(3);
144
+ expect(history.getCurrentIndex()).toBe(2);
145
+ history.commit(4);
146
+
147
+ expect(history.current()).toBe(4);
148
+ expect(history.getCurrentIndex()).toBe(0);
149
+
150
+ history.moveBackward();
151
+ expect(history.current()).toBe(3);
152
+
153
+ history.moveBackward();
154
+ expect(history.current()).toBe(2);
155
+
156
+ history.moveBackward();
157
+ expect(history.current()).toBe(2);
158
+
159
+ history.moveForward();
160
+ expect(history.current()).toBe(3);
161
+
162
+ history.moveForward();
163
+ expect(history.current()).toBe(4);
164
+
165
+ history.moveForward();
166
+ expect(history.current()).toBe(4);
167
+ });
168
+
169
+ test("should override items ahead after moving backward", () => {
170
+ var history = new CircularHistory(2, "number");
171
+
172
+ history.commit(1);
173
+ expect(history.current()).toBe(1);
174
+
175
+ history.commit(2);
176
+ expect(history.current()).toBe(2);
177
+
178
+ history.moveBackward();
179
+ expect(history.current()).toBe(1);
180
+
181
+ history.commit(3);
182
+ expect(history.current()).toBe(3);
183
+
184
+ history.moveBackward();
185
+ expect(history.current()).toBe(1);
186
+
187
+ history.moveForward();
188
+ expect(history.current()).toBe(3);
189
+
190
+ history.moveForward();
191
+ expect(history.current()).toBe(3);
192
+ });
193
+
194
+ test("should clear the history correctly", () => {
195
+ var history = new CircularHistory(3, "number");
196
+
197
+ history.commit(1);
198
+ history.commit(2);
199
+ history.commit(3);
200
+
201
+ expect(history.current()).toBe(3);
202
+
203
+ history.clear();
204
+ expect(history.current()).toEqual(CircularHistory.FLAGS.empty);
205
+ expect(history.getCurrentIndex()).toBe(-1);
206
+
207
+ history.commit(4);
208
+
209
+ expect(history.current()).toBe(4);
210
+ expect(history.getCurrentIndex()).toBe(0);
211
+ });
212
+
213
+ test("should return back the current history state with empty values", () => {
214
+ var history = new CircularHistory(3, "number");
215
+ history.commit(1);
216
+ history.commit(2);
217
+ expect(history.dump()).toEqual([1, 2, undefined]);
218
+ });
219
+
220
+ test("should return back the current history state without empty values", () => {
221
+ var history = new CircularHistory(3, "number");
222
+ history.commit(1);
223
+ history.commit(2);
224
+ expect(history.dump(true)).toEqual([1, 2]);
225
+ });
226
+
227
+ test("should return back the correct current pointer position", () => {
228
+ var history = new CircularHistory(3, "number");
229
+ expect(history.getCurrentIndex()).toBe(-1);
230
+
231
+ history.commit(1);
232
+ expect(history.getCurrentIndex()).toBe(0);
233
+
234
+ history.commit(2);
235
+ expect(history.getCurrentIndex()).toBe(1);
236
+
237
+ history.commit(3);
238
+ expect(history.getCurrentIndex()).toBe(2);
239
+
240
+ history.commit(4);
241
+ expect(history.getCurrentIndex()).toBe(0);
242
+
243
+ history.commit(5);
244
+ expect(history.getCurrentIndex()).toBe(1);
245
+
246
+ history.moveBackward();
247
+ expect(history.getCurrentIndex()).toBe(0);
248
+
249
+ history.moveBackward();
250
+ expect(history.getCurrentIndex()).toBe(2);
251
+
252
+ history.moveForward();
253
+ expect(history.getCurrentIndex()).toBe(0);
254
+
255
+ history.moveForward();
256
+ expect(history.getCurrentIndex()).toBe(1);
257
+
258
+ history.moveForward();
259
+ expect(history.getCurrentIndex()).toBe(1);
260
+
261
+ history.moveForward();
262
+ expect(history.getCurrentIndex()).toBe(1);
263
+ });
264
+
265
+ test("should correctly identify when start is reached", () => {
266
+ var history = new CircularHistory(3, "number");
267
+ expect(history.isStartReached()).toBe(true);
268
+
269
+ history.commit(1);
270
+ expect(history.isStartReached()).toBe(false);
271
+
272
+ history.commit(2);
273
+ expect(history.isStartReached()).toBe(false);
274
+
275
+ history.moveBackward();
276
+ expect(history.isStartReached()).toBe(false);
277
+
278
+ history.moveBackward();
279
+ expect(history.isStartReached()).toBe(true);
280
+
281
+ history.moveBackward();
282
+ expect(history.isStartReached()).toBe(true);
283
+ });
284
+
285
+ test("should correctly identify when end is reached", () => {
286
+ var history = new CircularHistory(3, "number");
287
+ expect(history.isEndReached()).toBe(true);
288
+
289
+ history.commit(1);
290
+ history.commit(2);
291
+ expect(history.current()).toBe(2);
292
+
293
+ history.moveForward();
294
+ expect(history.isEndReached()).toBe(true);
295
+
296
+ history.moveBackward();
297
+ expect(history.current()).toBe(1);
298
+ expect(history.isEndReached()).toBe(false);
299
+
300
+ history.moveBackward();
301
+ expect(history.current()).toBe(CircularHistory.FLAGS.empty);
302
+ expect(history.isEndReached()).toBe(false);
303
+
304
+ history.moveForward();
305
+ expect(history.current()).toBe(1);
306
+ expect(history.isEndReached()).toBe(false);
307
+
308
+ history.moveForward();
309
+ expect(history.current()).toBe(2);
310
+ expect(history.isEndReached()).toBe(true);
311
+
312
+ history.moveForward();
313
+ expect(history.current()).toBe(2);
314
+ expect(history.isEndReached()).toBe(true);
315
+ });
316
+
317
+ test("should return the last available item when capacity is not reached, the next item is empty and moving forward", () => {
318
+ var history = new CircularHistory(3, "number");
319
+
320
+ history.commit(1);
321
+ history.commit(2);
322
+ expect(history.current()).toBe(2);
323
+
324
+ history.moveForward();
325
+ expect(history.current()).toBe(2);
326
+
327
+ history.moveForward();
328
+ expect(history.current()).toBe(2);
329
+
330
+ history.moveBackward();
331
+ expect(history.current()).toBe(1);
332
+
333
+ history.moveBackward();
334
+ expect(history.current()).toBe(CircularHistory.FLAGS.empty);
335
+
336
+ history.moveBackward();
337
+ expect(history.current()).toBe(CircularHistory.FLAGS.empty);
338
+
339
+ history.moveForward();
340
+ expect(history.current()).toBe(1);
341
+
342
+ history.moveForward();
343
+ expect(history.current()).toBe(2);
344
+
345
+ history.moveForward();
346
+ expect(history.current()).toBe(2);
347
+ });
348
+
349
+ test("should behave correctly after multiple backward moves against empty history", () => {
350
+ var history = new CircularHistory(3, "number");
351
+
352
+ history.moveBackward();
353
+ history.moveBackward();
354
+ history.moveBackward();
355
+
356
+ history.commit(4);
357
+ expect(history.dump(true)).toEqual([4]);
358
+ expect(history.getCurrentIndex()).toBe(0);
359
+ expect(history.current()).toBe(4);
360
+ });
361
+
362
+ test("should behave correctly after multiple forward moves against empty history", () => {
363
+ var history = new CircularHistory(3, "number");
364
+
365
+ history.moveForward();
366
+ history.moveForward();
367
+ history.moveForward();
368
+
369
+ history.commit(3);
370
+ expect(history.dump(true)).toEqual([3]);
371
+ expect(history.getCurrentIndex()).toBe(0);
372
+
373
+ history.commit(5);
374
+ expect(history.dump(true)).toEqual([3, 5]);
375
+
376
+ history.moveForward();
377
+ history.moveForward();
378
+
379
+ expect(history.current()).toBe(5);
380
+ });
381
+
382
+ test("should not expose the next item after discarding previous after moving backwards", () => {
383
+ var history = new CircularHistory(10, "number");
384
+
385
+ history.commit(1);
386
+ history.commit(2);
387
+ history.commit(3);
388
+
389
+ history.moveBackward();
390
+ history.moveBackward();
391
+ expect(history.current()).toBe(1);
392
+
393
+ history.commit(4);
394
+ expect(history.dump(true)).toEqual([1, 4, 3]);
395
+
396
+ history.moveForward();
397
+ expect(history.getCurrentIndex()).toBe(1);
398
+ expect(history.isEndReached()).toBe(true);
399
+ expect(history.current()).toBe(4);
400
+
401
+ history.moveForward();
402
+ expect(history.current()).toBe(4);
403
+ });
404
+
405
+ test("should behave correctly after multiple moveBackward calls when some item was overriden", () => {
406
+ var history = new CircularHistory(3, "number");
407
+ history.commit(1);
408
+ history.commit(2);
409
+ history.commit(3);
410
+
411
+ history.moveBackward();
412
+ history.moveBackward();
413
+
414
+ history.commit(4);
415
+
416
+ history.moveBackward();
417
+ history.moveBackward();
418
+ history.moveBackward();
419
+ history.moveBackward();
420
+
421
+ expect(history.getCurrentIndex()).toBe(-1);
422
+
423
+ history.moveForward();
424
+ expect(history.current()).toBe(1);
425
+
426
+ history.moveForward();
427
+ expect(history.current()).toBe(4);
428
+
429
+ history.moveForward();
430
+ expect(history.current()).toBe(4);
431
+ });
432
+ });
package/src/index.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export declare const FLAGS: {
2
+ readonly empty: unique symbol;
3
+ };
4
+
5
+ export declare type CircularHistoryType = number | string | bigint | boolean | symbol | object;
6
+
7
+ type DataType = "number" | "string" | "bigint" | "boolean" | "symbol" | "object";
8
+
9
+ export declare class CircularHistory<T extends CircularHistoryType = CircularHistoryType> {
10
+ constructor(capacity: number, dataType: DataType);
11
+
12
+ commit(value: T): void;
13
+
14
+ current(): T | typeof FLAGS.empty;
15
+
16
+ moveBackward(): void;
17
+
18
+ moveForward(): void;
19
+
20
+ clear(): void;
21
+
22
+ dump(discardHoles?: boolean): T[];
23
+
24
+ getCurrentIndex(): number;
25
+
26
+ isStartReached(): boolean;
27
+
28
+ isEndReached(): boolean;
29
+
30
+ static readonly FLAGS: typeof FLAGS;
31
+ }