teableio/teable

View on GitHub
apps/nestjs-backend/src/features/undo-redo/stack/undo-redo-stack.service.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import { CacheService } from '../../../cache/cache.service';
import type { IUndoRedoOperation } from '../../../cache/types';
import { IThresholdConfig, ThresholdConfig } from '../../../configs/threshold.config';
import { EventEmitterService } from '../../../event-emitter/event-emitter.service';
import { Events } from '../../../event-emitter/events';
import type { IClsStore } from '../../../types/cls';

@Injectable()
export class UndoRedoStackService {
  constructor(
    private readonly cls: ClsService<IClsStore>,
    private readonly eventEmitterService: EventEmitterService,
    private readonly cacheService: CacheService,
    @ThresholdConfig() private readonly thresholdConfig: IThresholdConfig
  ) {}

  private async getUndoStack(userId: string, tableId: string, windowId: string) {
    return (await this.cacheService.get(`operations:undo:${userId}:${tableId}:${windowId}`)) || [];
  }

  private async getRedoStack(userId: string, tableId: string, windowId: string) {
    return (await this.cacheService.get(`operations:redo:${userId}:${tableId}:${windowId}`)) || [];
  }

  private async setUndoStack(
    userId: string,
    tableId: string,
    windowId: string,
    undoStack: IUndoRedoOperation[]
  ) {
    await this.cacheService.set(
      `operations:undo:${userId}:${tableId}:${windowId}`,
      undoStack,
      this.thresholdConfig.undoExpirationTime
    );
  }

  private async setRedoStack(
    userId: string,
    tableId: string,
    windowId: string,
    redoStack: IUndoRedoOperation[]
  ) {
    await this.cacheService.set(
      `operations:redo:${userId}:${tableId}:${windowId}`,
      redoStack,
      this.thresholdConfig.undoExpirationTime
    );
  }

  async push(
    userId: string,
    tableId: string,
    windowId: string,
    operation: IUndoRedoOperation
  ): Promise<void> {
    const maxUndoStackSize = this.thresholdConfig.maxUndoStackSize;
    let undoStack = await this.getUndoStack(userId, tableId, windowId);

    undoStack.push(operation);
    if (undoStack.length > this.thresholdConfig.maxUndoStackSize) {
      undoStack = undoStack.slice(-maxUndoStackSize);
    }

    await this.setUndoStack(userId, tableId, windowId, undoStack);

    // Clear redo stack when a new operation is pushed
    await this.cacheService.del(`operations:redo:${userId}:${tableId}:${windowId}`);

    this.eventEmitterService.emit(Events.OPERATION_PUSH, operation);
  }

  async popUndo(tableId: string, windowId: string) {
    const userId = this.cls.get('user.id');
    const undoStack = await this.getUndoStack(userId, tableId, windowId);
    const redoStack = await this.getRedoStack(userId, tableId, windowId);

    const operation = undoStack.pop();

    return {
      operation,
      push: async (newOperation: IUndoRedoOperation) => {
        if (!newOperation) {
          throw new InternalServerErrorException('No operation to undo');
        }
        redoStack.push(newOperation);
        await this.setUndoStack(userId, tableId, windowId, undoStack);
        await this.setRedoStack(userId, tableId, windowId, redoStack);
      },
    };
  }

  async popRedo(tableId: string, windowId: string) {
    const userId = this.cls.get('user.id');
    const undoStack = await this.getUndoStack(userId, tableId, windowId);
    const redoStack = await this.getRedoStack(userId, tableId, windowId);

    const operation = redoStack.pop();

    return {
      operation,
      push: async (newOperation: IUndoRedoOperation) => {
        if (!newOperation) {
          throw new InternalServerErrorException('No operation to redo');
        }
        undoStack.push(newOperation);
        await this.setUndoStack(userId, tableId, windowId, undoStack);
        await this.setRedoStack(userId, tableId, windowId, redoStack);
      },
    };
  }
}