@phroun/paged-buffer
Version:
High-performance buffer system for editing massive files with intelligent memory management and undo/redo capabilities
310 lines • 12.6 kB
JavaScript
"use strict";
/**
* @fileoverview Enhanced BufferOperation with position tracking and distance calculation - FIXED
* @author Jeffrey R. Day
* @version 1.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getOperationCounter = exports.resetOperationCounter = exports.OperationType = exports.BufferOperation = void 0;
const operation_distance_1 = require("./utils/operation-distance");
const common_1 = require("./types/common");
Object.defineProperty(exports, "OperationType", { enumerable: true, get: function () { return common_1.OperationType; } });
/**
* Global operation counter for determining chronological order
* Moved here from undo-system.js since BufferOperation needs it
*/
let globalOperationCounter = 0;
/**
* Reset the global operation counter (for testing)
*/
function resetOperationCounter() {
globalOperationCounter = 0;
}
exports.resetOperationCounter = resetOperationCounter;
/**
* Get current operation counter value (for testing)
*/
function getOperationCounter() {
return globalOperationCounter;
}
exports.getOperationCounter = getOperationCounter;
/**
* Enhanced BufferOperation with position tracking and distance calculation - FIXED
*/
class BufferOperation {
constructor(type, position, data, originalData = null, timestamp = null) {
this.postExecutionPosition = null;
this.type = type;
this.preExecutionPosition = position;
this.data = data;
this.originalData = originalData || undefined;
this.timestamp = timestamp || Date.now();
this.operationNumber = ++globalOperationCounter;
this.id = `op_${this.operationNumber}_${this.timestamp}`;
}
/**
* Legacy position property for backwards compatibility
*/
get position() {
return this.preExecutionPosition;
}
/**
* Set position for backwards compatibility
*/
set position(value) {
this.preExecutionPosition = value;
}
/**
* Set the position after this operation has executed
*/
setPostExecutionPosition(position) {
this.postExecutionPosition = position;
}
/**
* Calculate logical distance to another operation using the distance module
*/
getLogicalDistance(other, options = {}) {
// Convert both operations to descriptors
const thisDescriptor = operation_distance_1.OperationDescriptor.fromBufferOperation(this);
const otherDescriptor = operation_distance_1.OperationDescriptor.fromBufferOperation(other);
// Use the distance calculator
return operation_distance_1.OperationDistanceCalculator.calculateDistance(thisDescriptor, otherDescriptor, options);
}
/**
* Get the size impact of this operation
*/
getSizeImpact() {
switch (this.type) {
case common_1.OperationType.INSERT:
return this.data ? this.data.length : 0;
case common_1.OperationType.DELETE:
return this.originalData ? -this.originalData.length : 0;
case common_1.OperationType.OVERWRITE:
const oldSize = this.originalData ? this.originalData.length : 0;
const newSize = this.data ? this.data.length : 0;
return newSize - oldSize;
default:
return 0;
}
}
/**
* Get the end position of this operation (legacy method)
*/
getEndPosition() {
switch (this.type) {
case common_1.OperationType.INSERT:
return this.preExecutionPosition + (this.data ? this.data.length : 0);
case common_1.OperationType.DELETE:
return this.preExecutionPosition;
case common_1.OperationType.OVERWRITE:
return this.preExecutionPosition + (this.data ? this.data.length : 0);
default:
return this.preExecutionPosition;
}
}
/**
* Get the length of content that an operation inserts into the final buffer
*/
getInsertedLength() {
switch (this.type) {
case common_1.OperationType.INSERT:
return this.data ? this.data.length : 0;
case common_1.OperationType.DELETE:
return 0;
case common_1.OperationType.OVERWRITE:
return this.data ? this.data.length : 0;
default:
return 0;
}
}
/**
* Check if this operation can be merged with another - FIXED
*/
canMergeWith(other, timeWindow = 15000, positionWindow = -1) {
// Check time window first
const timeDiff = Math.abs(this.timestamp - other.timestamp);
const timeWithinWindow = timeDiff <= timeWindow;
if (!timeWithinWindow) {
return false;
}
// IMPROVED: If position window is 0 (default), skip position check
// This means operations only merge based on time, not position
if (positionWindow >= 0) {
// Check distance (for logical grouping)
let distance;
if (this.postExecutionPosition !== null) {
try {
distance = this.getLogicalDistance(other);
}
catch (error) {
distance = Math.abs(this.preExecutionPosition - other.preExecutionPosition);
}
}
else {
distance = Math.abs(this.preExecutionPosition - other.preExecutionPosition);
}
const distanceWithinWindow = distance <= positionWindow;
if (!distanceWithinWindow) {
return false;
}
}
// Check type compatibility for grouping
return this._areOperationsCompatible(this.type, other.type);
}
/**
* Check if two operation types are compatible for merging
*/
_areOperationsCompatible(type1, type2) {
// Same type operations are generally compatible
if (type1 === type2) {
return true;
}
// Cross-type compatibility rules
const compatibleCombinations = [
[common_1.OperationType.DELETE, common_1.OperationType.INSERT],
[common_1.OperationType.INSERT, common_1.OperationType.DELETE],
[common_1.OperationType.INSERT, common_1.OperationType.OVERWRITE],
[common_1.OperationType.DELETE, common_1.OperationType.OVERWRITE],
[common_1.OperationType.OVERWRITE, common_1.OperationType.INSERT],
[common_1.OperationType.OVERWRITE, common_1.OperationType.DELETE]
];
return compatibleCombinations.some(([first, second]) => (type1 === first && type2 === second) ||
(type1 === second && type2 === first));
}
/**
* Merge another operation into this one - FIXED VERSION
*/
mergeWith(other) {
// Determine chronological order
let firstOp, secondOp;
if (this.operationNumber <= other.operationNumber) {
firstOp = this;
secondOp = other;
}
else {
firstOp = other;
secondOp = this;
}
let mergedOp;
if (firstOp.type === 'insert' && secondOp.type === 'insert') {
mergedOp = this._mergeInsertOperations(firstOp, secondOp);
}
else if (firstOp.type === 'delete' && secondOp.type === 'delete') {
mergedOp = this._mergeDeleteOperations(firstOp, secondOp);
}
else {
// Mixed operations - merge as overwrite
mergedOp = this._mergeAsOverwrite(firstOp, secondOp);
}
// CRITICAL FIX: Update this operation with merged data properly
this.type = mergedOp.type;
this.preExecutionPosition = mergedOp.position;
this.data = mergedOp.data;
this.originalData = mergedOp.originalData;
// CRITICAL: Keep the earliest timestamp for proper chronological order
this.timestamp = Math.min(firstOp.timestamp, secondOp.timestamp);
// CRITICAL: Update post-execution position based on merged result
if (mergedOp.type === 'insert') {
this.postExecutionPosition = mergedOp.position;
}
else if (mergedOp.type === 'delete') {
this.postExecutionPosition = mergedOp.position;
}
else if (mergedOp.type === 'overwrite') {
this.postExecutionPosition = mergedOp.position;
}
}
/**
* FIXED: Merge two insert operations
*/
_mergeInsertOperations(firstOp, secondOp) {
// Determine which operation comes first in the final buffer
let finalFirstStart = firstOp.preExecutionPosition;
let finalSecondStart = secondOp.preExecutionPosition;
// Apply position adjustments based on chronological order
if (firstOp.operationNumber <= secondOp.operationNumber) {
// Second op gets pushed right by first op if first op comes before it
if (firstOp.preExecutionPosition <= secondOp.preExecutionPosition) {
finalSecondStart += firstOp.data ? firstOp.data.length : 0;
}
}
if (finalFirstStart <= finalSecondStart) {
// First operation comes before second in final buffer
return {
type: common_1.OperationType.INSERT,
position: firstOp.preExecutionPosition,
data: Buffer.concat([firstOp.data || Buffer.alloc(0), secondOp.data || Buffer.alloc(0)]),
originalData: Buffer.alloc(0)
};
}
else {
// Second operation comes before first in final buffer
return {
type: common_1.OperationType.INSERT,
position: secondOp.preExecutionPosition,
data: Buffer.concat([secondOp.data || Buffer.alloc(0), firstOp.data || Buffer.alloc(0)]),
originalData: Buffer.alloc(0)
};
}
}
/**
* FIXED: Merge two delete operations
*/
_mergeDeleteOperations(firstOp, secondOp) {
// Determine the final position (should be the lowest position)
const finalPosition = Math.min(firstOp.preExecutionPosition, secondOp.preExecutionPosition);
// Determine the correct order of data based on positions
let combinedData;
if (firstOp.preExecutionPosition <= secondOp.preExecutionPosition) {
// First operation is at lower/equal position
combinedData = Buffer.concat([
firstOp.originalData || Buffer.alloc(0),
secondOp.originalData || Buffer.alloc(0)
]);
}
else {
// Second operation is at lower position (backspace scenario)
combinedData = Buffer.concat([
secondOp.originalData || Buffer.alloc(0),
firstOp.originalData || Buffer.alloc(0)
]);
}
return {
type: common_1.OperationType.DELETE,
position: finalPosition,
data: Buffer.alloc(0),
originalData: combinedData
};
}
/**
* FIXED: Merge mixed operations as overwrite
*/
_mergeAsOverwrite(firstOp, secondOp) {
const startPos = Math.min(firstOp.preExecutionPosition, secondOp.preExecutionPosition);
// For mixed operations, we need to be careful about the final result
let finalData, originalData;
if (firstOp.type === 'delete' && secondOp.type === 'insert') {
// Delete then insert at same/nearby position
finalData = secondOp.data || Buffer.alloc(0);
originalData = firstOp.originalData || Buffer.alloc(0);
}
else if (firstOp.type === 'insert' && secondOp.type === 'delete') {
// Insert then delete - tricky case
originalData = firstOp.data || Buffer.alloc(0);
finalData = Buffer.alloc(0); // Net result is deletion
}
else {
// One of them is overwrite
finalData = secondOp.type === 'delete' ? Buffer.alloc(0) : (secondOp.data || Buffer.alloc(0));
originalData = firstOp.type === 'delete' ? (firstOp.originalData || Buffer.alloc(0)) : (firstOp.data || Buffer.alloc(0));
}
return {
type: common_1.OperationType.OVERWRITE,
position: startPos,
data: finalData,
originalData: originalData
};
}
}
exports.BufferOperation = BufferOperation;
//# sourceMappingURL=buffer-operation.js.map